Skip to content

ECE 2300 Coding Conventions

Any significant hardware design project will usually require developers to use a standardized set of coding conventions. These conventions may be set by company leaders, owners of an open-source project, or simply through historical precedent. Standardized coding conventions allows the code we write to be consistent with others using the same convention and improves readability, maintainability, and extensibility. We have developed a simple set of coding conventions for ECE 2300 which students are required to use in all lab assignments. Keep in mind that these are just guidelines, and there may be situations where it is appropriate to defy a convention if this ultimately improves the overall code quality. These guidelines cover Verilog hardware designs implemented using gate-level (GL) modeling and register-transfer-level (RTL) modeling as well as Verilog test benches and Verilog interactive simulators.

1. Directories and Files

This section discusses the physical structure of how files should be organized in a project. In ECE 2300, we will provide you all of the files you need and they will already be organized into the appropriate directories. However, it can still be useful to understand the overall organization.

1.1. Directories

A project is comprised of multiple subprojects (e.g., each lab will be a separate subproject). Each subproject is a subdirectory with two additional subdirectories for test benches and interactive simulators. If we have a subproject named fb then it would be organized as follows.

 /fb
   FooBar_GL.v
   /test
     FooBar_GL-test.v
   /sim
     foo-bar-sim.v

Verilog files with hardware modules are in the fb subdirectory. Verilog files with test benches are in the fb/test subdirectory. Verilog files with interactive simulators are in the fb/sim subdirectory.

1.2. File Names

All Verilog files should use the .v filename extension.

In general, each Verilog hardware module should be in a separate file, and the name of the file should match the name of the Verilog hardware module. If a Verilog file contains a Verilog hardware module named FooBar_GL, then the name of the Verilog file should be FooBar_GL.v.

In general, each Verilog hardware module should have its own Verilog test bench file with the same name as the Verilog hardware module and a -test suffix. So the Verilog test bench file for FooBar_GL would be named FooBar_GL-test.v. Sometimes we will factor out test cases so we can share them across multiple test benches in which case the corresponding Verilog file will have a -test-cases suffix.

Verilog files for interactive simulators usually use all lowercase, a dash (-) as a separator, and a -sim suffix. So the interactive simulator for the FooBar hardware module might be named foo-bar-sim.v.

1.3. Includes

To instantiate a Verilog module in a different Verilog module you must explicitly include the appropriate Verilog hardware module file using the Verilog include preprocessor directive. You should use the complete path starting at the root of the project. So for example, if we wanted to instantiate the FooBar_GL module in a different module we will use the following:

`include "fb/FooBar_GL.v"

1.4. Include Guards

All Verilog hardware module files should have include guards. These make sure that the contents of a Verilog file are only included once in the overall project, even if they are included multiple times from different files. An include guard uses the Verilog ifndef/endif and define preprocessor directives as follows.

`ifndef FOO_BAR_GL_V
`define FOO_BAR_GL_V

// ... FooBar_GL module definition ...

`endif /* FOO_BAR_GL_V */

We name the guard with the same name as the Verilog file but using all caps, underscore (_) as a separator, and a _V suffix.

2. Formatting

This section discusses general formatting of files across all kinds of files.

2.1. Line Length

In general, you should attempt to keep the length of a line in your file to less than 74 characters. This amount of characters is ideal as it makes code easier to read, enables printing on standard sized paper, and allows the viewing of two files side-by-side on a modern laptop or four files on 24" to 27" inch monitors. Lines longer than 80 characters should be avoided unless there is a compelling reason to use longer lines to increase code quality.

2.2. Indentation

Absolutely no tabs are allowed. Only spaces are allowed for the purposes of indentation. The standard number of spaces per level of indentation is two. Note that in VS Code usually when you press the tab key it will actually insert spaces which is fine. We just want to avoid real tab characters inserted into Verilog files since this means the code formatting requires every reader to use the same tabstop settings.

2.3. Vertical Whitespace

Vertical whitespace can and should be used to separate conceptually distinct portions of your code. A blank line within a block of code serves like a paragraph break in prose: visually separating two thoughts. Vertical whitespace should be limited to a single blank line. Do not use two or more blank lines in a row.

2.4. Horizontal Whitespace

In general, whitespace should be used to separate distinct conceptual "tokens". Do not cram all of the characters in an expression together without any horizontal whitespace. Additional horizontal whitespace can be used to align visual columns when appropriate.

3. Naming

Proper naming is critical to enable the reader to quickly understand your code.

3.1. Port and Wire Names

All port and wire names should use snake case which means we use lower-case letters and an underscore (_) to separate words. Do not use camel case for port or wire names. Single letter port or wire names should be used sparingly. Use port and wire names that clearly indicate the purpose of the signal.

3.2. Module Names

All module names should use camel case which means we use upper-case letters and no other separator to separate words. Module names might also include a suffix separated by an underscore (_) to specify the bitwidth and level of modeling. For example, a 32-bit ripple-carry adder implemented using GL modeling would be named AdderRippleCarry_32b_GL and a 32-bit adder implemented using register-transfer-level modeling would be named Adder_32b_RTL. A bitwidth suffix does not make sense for all kinds of hardware modules. For example, a module which is parameterized by the bitwidth does not have a bitwidth suffix. So an adder parameterized by the bitwidth implemented using RTL modeling would be named Adder_RTL. While a bitwidth suffix does not make sense for all kinds of hardware modules, all hardware modules must have a suffix indicating the level of modeling.

3.3. Instance Names

Module instance names should use snake case which means we use lower-case letters and an underscore (_) to separate words. Do not use camel case for module instance names. Use module instance names that clearly indicate the purpose of the instantiated module.

Unlike modern programming languages like Python or C++, Verilog does not have a clean way to manage namespaces for module names. This means if you define modules with the same name in two different files, then it could cause a namespace collision which can be difficult to debug. Thus module names must be unique across the entire project.

4. Gate-Level (GL) Modeling

This section focuses on coding conventions suitable for gate-level (GL) modeling.

4.1. GL Allowable Constructs

In GL modeling, students must explicitly declare gates and then use wires to connect these gates to create a gate-level network using the following constructs:

  • wire (single bit and multiple bit)
  • not, and, or, xor, nand, nor, xnor

Other allowable constructs include literals, wire slicing, and assign statements used only for connecting wires together.

  • literals (e.g., 1'b0, 1'b1)
  • wire slicing (e.g., x[0], x[1:0])
  • assign for connecting wires (e.g., assign x = y;);
  • assign for setting a wire to a constant value (e.g., assign x = 1'b0;)

Module instantiation is allowed as long as the instantiated module is itself adheres to the GL allowable constructs.

4.2. GL Signal Declaration

You may only use wire for GL modeling. Do not use any other signal types such as logic or reg. Wires should be created close to where they will be first used.

Declaring multi-bit wires is allowed using the following syntax.

wire [7:0] x; // 8-bit wire
wire [3:0] y; // 4-bit wire
wire [5:0] z; // 6-bit wire

An unpacked arrays of one-bit wires should not be used in place of a multi-bit wire. So the following is incorrect.

wire x [8]; // unpacked array of 8 1-bit wires
wire y [4]; // unpacked array of 4 1-bit wires
wire z [6]; // unpacked array of 6 1-bit wires

Never use any other kind of indexing. So the following are all incorrect:

wire [0:7] x; // incorrect indexing
wire [4:1] y; // incorrect indexing
wire [7:2] y; // incorrect indexing

Vertically right align the closing square brackets and vertically left align the wire names. The following is correct:

wire        w;
wire  [7:0] x;
wire  [3:0] y;
wire [15:0] z;

The following is incorrect:

wire w;        // incorrect vertical alignment
wire [7:0] x;  // incorrect vertical alignment
wire [3:0] y;  // incorrect vertical alignment
wire [15:0] z; // incorrect vertical alignment

4.3. Primitive Gate Instantiation

There should be spaces between wires when instantiating a logic gate. You can optionally also include a space after the opening parenthesis and before the closing parenthesis, but it is not required.

wire x;
and(x,in1,in2);     // incorrect, no horizontal whitespace
and(x, in1,in2 );   // incorrect, inconsistent horizontal whitespace
and(x, in1, in2);   // correct, no space after/before parenthesis
and( x, in1, in2 ); // correct, space after/before parenthesis

Additional horizontal whitespace can be used to align visual columns when appropriate. The following is correct if we want to align the visual columns to improve readability.

wire x, y;
and( x, in1_n, in2   );
or ( y, in1,   in2_n );

4.4. Literals

In hardware modeling, avoid using literals without specifying the bitwidth. So prefer 1'b0 instead of 0 and 1'b1 instead of 1. In test benches, using 0 and 1 is acceptable. Use underscores (_) to help make long literals more readable as follows

16'b0000_1010_0110_1111

4.5. Assign

Signal assignment using assign is allowed to implement physical connections. Concatenation is not allowed, because it obscures bit-level behavior. Instead, write explicit bit-level assignments.

Do not declare and assign to a wire in a single statement. So this is not allowed:

wire foobar = in[3:0];

Instead declare a wire and the assign to that wire in two separate statements.

wire foobar;
assign foobar = in[3:0];

Consider aligning the = sign if it improves readable as below.

assign foobar = in[3:0];
assign baz    = in[7:4];

4.6. GL Module Definition

A GL module definition should specify each port on a separate line (indented by two spaces) and place the opening and closing parenthesis on their own lines. The definition should vertically align the wire keywords, bitwidth declarations, and port names as follows.

module FooBar_GL
(
  input  wire        val,
  input  wire  [3:0] addr,
  input  wire [15:0] data,
  output wire        wait
);

  // ... module implementation here ...

endmodule

4.7. GL Module Instantation

A GL module may only instantiate other GL modules. Module instantation should specify each port connection on a separate line (indented by two spaces) and place the opening and closing parenthesis on their own lines. The definition should vertically align the port connections as follows.

FooBar_GL foo_bar
(
  .val  (foo_val),
  .addr (foo_addr),
  .data (foo_data),
  .wait (foo_wait)
);

It is fine for ports and wires to have the same name as long as the intent is clear. We recommend using a direct transformation of the module name to create the module instance name (i.e., foo_bar is a direct transformation from FooBar_GL), but this is not required. Using the module instance name as a prefix for the wires used to connect to the module can sometimes be useful.

The following is incorrect formatting since: (1) the opening parenthesis is not on its own line; (2) the port connections are not indented by two spaces; and (3) the port connections are not vertically aligned.

FooBar_GL foo_bar(
.val (foo_val),
.addr (foo_addr),
.data (foo_data),
.wait (foo_wait)
);

5. Register Transfer Level (RTL) Modeling

This section focuses on coding conventions suitable for register-transfer-level (RTL) modeling. This section inherits the coding conventions from the previous section where appropriate.

5.1. RTL Operators

In RTL, students describe hardware behavior using operators and always blocks instead of instantiating primitive gates. Some verilog operators you may be allowed to use, depending on lab instructions, are:

Operation Type Verilog Operator Example Usage
Bitwise AND & assign y = a & b;
Bitwise OR | assign y = a | b;
Bitwise XOR ^ assign y = a ^ b;
Bitwise NOT ~ assign y = ~a;
Logical NOT ! assign y = !a;
Logical AND && assign y = a && b;
Logical OR || assign y = a || b;
Shift Left << assign y = a << b;
Shift Right >> assign y = a >> b;
Addition + assign sum = a + b;
Subtraction - assign diff = a - b;
Multiplication * assign prod = a * b;

5.2. RTL Signal Declaration

You may only use logic for GL modeling. Do not use any other types such as wire or reg.

5.3. Combinational Blocks

Combinational always blocks use always_comb to describe logic where outputs depend only on current inputs, with no memory or state. You are not allowed to use always @(*) as it adds ambiguity and prevents linters from warning about common mistakes. Combinational blocks should always use begin/end and increase the level identation.

always_comb begin
  x = y + 1;
  z = w << 2;
end

5.2. Inferred Latches

Inferred latches occur when a signal is not fully assigned in all control paths so the signal "remembers" its previous value. Latches are usually unintentional and cause unpredictable issues.

They occur in always_comb blocks when doing if else statements. The example below causes a inferred latch; when en is zero, y is not assigned and thus will "remeber" its previous value.

always_comb begin
  if ( en )
    y = a;
end

Hence, for if statements, students must set a default signal before the if statement. For the example before, we can prevent inferred latches with the following.

always_comb begin

  y = 'x;

  if (en)
    y = a;

end

The default value need not be x. Inferred latches can also occur with case statements. The example below causes an inferred latch; when sel is 11, y is not updated and thus will "remember" its previous value.

always_comb begin
  case ( sel )
    2'b00: y = a;
    2'b01: y = b;
    2'b10: y = c;
  endcase
end

For case statements, students must include a default case. In addition, the default case must assign all signals to x to avoid X-optimism as described in the next section.

always_comb begin
  case ( sel )
    2'b00: y = a;
    2'b01: y = b;
    2'b10: y = c;
    default: y = 'x;
  endcase
end

5.3. X-Optimism

X-optimism is when simulation converts unknown X values into known values (i.e., it treats X values "optimistically"). Conditional statements like if and case can cause of X-optimism, because these statements treat X values as if they were false. This can potentially cause the design to pass tests in simulation but fail in real hardware.

For if statements, we require students to explicitly propgate X values using the ECE2300_XPROP macro. The macro forces the output to be X whenever a condition is satisfied. Here is an example of using this macro to avoid X-optimism.

always_comb begin

  // must use default value

  diff = 'x;

  // absdiff logic

  if ( in0 > in1 )
    diff = in0 - in1;
  else
    diff = in1 - in0;

  // explicit x-propagation

  `ECE2300_XPROP( diff, $isunknown(in0) || $isunknown(in1) );

end

The macro makes sure that if in0 or in1 are X, which is what $isunknown does, then diff should also be X.

For case statement, we simply require the default case to assign all outputs to be X.

5.3. Sequential Blocks

Sequential always blocks describe hardware that has state and updates only on a clock edge. Unlike combinational logic, outputs here depend on both current inputs and the previous state. You must use always_ff. You cannot use always. In this course, only positive edge-triggered flip-flops are allowed. Asynchronous reset is not allowed, only synchronous reset is allowed. Combinational blocks should always use begin/end and increase the level identation.

always_ff @( posedge clk ) begin

  if ( !rst )
    q <= 1'b0;
  else
    q <= d;

  `ECE2300_SEQ_XPROP( q, $isunknown(rst) );

end

Sequential always blocks should be very simple and model just a flip-flop. Very little combinational logic should be placed in sequential always block. Do not include arithmetic, nested if statements, or other complex logic. This additional combinational logic should be refactored into a separate combinational always block.

5.5 RTL Module Definition

We create modules for RTL in a similar way as how we do for GL except that we must use logic instead of wire. An example is as follows:

module FooBar_RTL
(
  input  logic        val,
  input  logic  [3:0] addr,
  input  logic [15:0] data,
  output logic        wait
);

  // ... module implementation here ...

endmodule

Parameterization allows an RTL module to be written once but customized at instantiation time. Instead of hard-coding values like bus widths, etc., you define them as parameters in the module header. When instantiating the module, the designer can override these defaults, making the same RTL usable in many contexts (e.g., a 4-bit, 8-bit, or 32-bit counter without rewriting the code). Parameters should use a p_ prefix.

Defining a parameterized module must use the following format.

module FooBar_RTL
#(
  parameter p_addr_nbits = 1,
  parameter p_data_nbits = 1
)(
  input  logic                    val,
  input  logic [p_addr-nbits-1:0] addr,
  input  logic [p_data_nbits-1:0] data,
  output logic                    wait
);

  // ... module implementation here ...

endmodule

5.6 RTL Module Instantation

RTL Module Instantation is done in the same way as RTL. As a refresher, it must be formated as follows:

FooBar_RTL foo_bar
(
  .val  (foo_val),
  .addr (foo_addr),
  .data (foo_data),
  .wait (foo_wait)
);

The format for instantiating a parameterize module is as follows:

FooBar_RTL
#(
  .p_addr_nbits (8),
  .p_data_nbits (32)
)
foo_bar
(
  .val  (val),
  .addr (addr),
  .data (data),
  .wait (wait)
);

If there is a single parameter and that parameter specifies a bitwidth, then the following more compact format is allowed.

Register_RTL#(32) a_reg
(
  .clk (clk),
  .rst (rst),
  .en  (1'b1),
  .d   (a_reg_in),
  .q   (a_reg_out)
);

6. Comments

Though challenging to write, comments are absolutely vital to keeping our code readable. The following rules describe what you should comment and where. But remember: while comments are very important, the best code is self-documenting. Giving sensible names to wires and module instances is much better than using obscure names that you must then explain through comments. When writing your comments, write in a manner such that in a few years time, you can still look back and be able to understand your code and/or logic.

Do not state the obvious. In particular, don't literally describe what code does, unless the behavior is not obvious to a reader who generally understands Verilog. Instead, provide higher level comments that describe why the code does what it does, or make the code self describing. Over commenting is just as bad as under commenting.

6.1 ASCII Characters

Comments must only use ASCII characters. Do not use any kind of unicode characters in your comments. Including emoji's or foreign characters using unicode is not allowed.

6.2 Comment Style

Use // comments. Do not use /* */ comments. Include a space after // before you start commenting.

//without space, incorrect formatting
// with space, correct formatting

6.3 Trailing Comments

Trailing comments are acceptable as long as they are short. For example, trailing comments to describe ports works well.

module FooBar_GL
(
  input  wire        val,   // valid
  input  wire  [3:0] addr,  // write address
  input  wire [15:0] data,  // write data
  output wire        wait   // module is busy, must wait
);

6.4 Test Case Comments

Every test case should start with a test case title block which at a minimum gives the name of the test case. Often, it can also be helpful to write a small description of what the test case is trying to achieve. Within a directed test case using check tasks always use a comment to label the columns so it is clear what you are checking. An example is shown below.

  //----------------------------------------------------------------------
  // test_case_1_basic
  //----------------------------------------------------------------------
  // This is a basic test case just for smoke testing. Passing this test
  // case in no way guarantees any kind of functionality!

  task test_case_1_basic();
    t.test_case_begin( "test_case_1_basic" );

    //     in        tens     ones
    check( 5'b00000, 4'b0000, 4'b0000 );
    check( 5'b00001, 4'b0000, 4'b0001 );
    check( 5'b01111, 4'b0001, 4'b0101 );
    check( 5'b11111, 4'b0011, 4'b0001 );

    t.test_case_end();
  endtask

The horizontal lines used in the test case title block should extend exactly 74 characters (i.e., two spaces, two '/' characters, and 70 - characters).

6.5 File Comments

All files should include a "title block". This is a comment at the beginning of the file which at minimum must give the name of the file. Often, it can also be helpful to write a small description of the interface in a comment right below the title block. So the title block for Foo_GL.v should look something like:

//=======================================================================
// FooBar_GL
//=======================================================================
// Memory module which enables writing data. If the memory module is busy
// then the wait signal will be one.

The horizontal lines used in the title block should extend exactly 74 characters (i.e., two '/' characters and 72 = characters).

6.6 Instructor Comments

You must remove the lab assignment comments provided in the released code which are purely there to tell you what to do. These comments are no longer applicable once you have followed the instructions and implemented your hardware design. You should also remove any unnecessary ECE2300_UNUSED and/or ECE2300_UNDRIVEN macros that were provided in the released code. So example, these should be removed:

  //''' LAB ASSIGNMENT '''''''''''''''''''''''''''''''''''''''''''''''''''
  // Implement the binary to binary coded decimal converter
  //>'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

  // remove these lines before starting your implementation
  `ECE2300_UNUSED( in );
  `ECE2300_UNDRIVEN( tens );
  `ECE2300_UNDRIVEN( ones );

Do not just blindly remove all instructor comments! Most of the instructor comments are useful and will improve your code quality score!