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.
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:
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 may not make sense for every hardware module, but 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]) - wire concatenation (e.g.,
{ a, b }) assignfor connecting wires (e.g.,assign x = y;);assignfor 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.
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 (not allowed)
wire y [4]; // unpacked array of 4 1-bit wires (not allowed)
wire z [6]; // unpacked array of 6 1-bit wires (not allowed)
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:
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 is always exactly one space between the name of the primitive gate and the opening parenthesis. The wires are separated by a comma and a single space, and there is no space after the opening parenthesis or before the closing parenthesis. This is the only allowed formatting for a primitive gate instantiation.
wire x;
and(x, in1, in2); // incorrect, no space before the parenthesis
and (x,in1,in2); // incorrect, no horizontal whitespace
and (x, in1,in2 ); // incorrect, inconsistent horizontal whitespace
and ( x, in1, in2 ); // incorrect, space after/before parenthesis
and (x, in1, in2); // correct
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
4.5. Assign
Signal assignment using assign is allowed to implement physical
connections.
Do not declare and assign to a wire in a single statement. So this is not allowed:
Instead declare a wire and the assign to that wire in two separate statements.
Consider aligning the = sign if it improves readable as below.
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.
Every port connection is written as the port name, a space, and then the
signal in parenthesis (i.e., .val (foo_val)). There is no space after
the opening parenthesis and no space before the closing parenthesis. This
is the only allowed formatting for a port connection. The only extra
horizontal whitespace allowed is additional spaces before the opening
parenthesis so that the port connections vertically align as in the
example above. In the following, only the last port connection is
correct.
FooBar_GL foo_bar
(
.val(foo_val), // incorrect, no space before the parenthesis
.addr ( foo_addr ), // incorrect, space after/before parenthesis
.data( foo_data ), // incorrect, both of the above
.wait (foo_wait) // correct
);
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.
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; |
| Tenrary | ?: |
assign out = (c) ? a : b; |
5.2. RTL Signal Declaration
You may only use logic for RTL modeling. Do not use any other types such
as wire or reg.
5.3. Combinational Always Blocks
Combinational alwaysblocks use always_comb and are only allowed to be
used with a single case or casez statement. No other constructs are
allowed in combinational always blocks. You are not allowed to use
always @(*) as it adds ambiguity and prevents linters from warning
about common mistakes. Combinational always blocks should always use
begin/end and increase the level identation.
There are two very important concerns with combinational always blocks:
-
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.
-
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.
To avoid inferred latches and X-optimism, students must include a
default case, and the default case must assign all signals to x. The
following shows a correct combinational always block with a case
statement.
always_comb begin
case ( sel )
2'b00: y = a;
2'b01: y = b;
2'b10: y = c;
default: y = 'x;
endcase
end
Note that blocking assignments or if statements are not allowed in combinational always blocks.
5.4. Sequential Always 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.
To avoid X-optimism, students must include a final else clause which
must assign all signals to x. The following shows a correct
sequential block implementing a reset flip-flop.
Sequential always blocks should be very simple and model just a flip-flop. No real combinational logic should be placed in sequential always block. Do not include arithmetic, nested if statements, or other complex logic.
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
5.6. RTL Module Instantation
RTL Module Instantation is done in the same way as GL, including the
.val (foo_val) formatting of every port connection described in Section
4.7. As a refresher, it must be formated as follows:
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.
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_FLOATING 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_FLOATING( tens );
`ECE2300_FLOATING( ones );
Do not just blindly remove all instructor comments! Most of the instructor comments are useful and will improve your code quality score!
7. Autoformatter
The sections above describe the formatting we expect to see in every file you submit. Some of that formatting is applied for you by an autoformatter, and the rest is left to you. This section goes into a little more detail on exactly what the autoformatter handles and what it deliberately leaves alone.
The autoformatter is a small wrapper named ece2300-format around a
customized version of
Verible, an open-source
suite of SystemVerilog developer tools. Both are already installed on
ecelinux, so there is nothing for you to set up.
Running ece2300-format on a file does two things in sequence. The first
pass checks your file for some of the problems the formatter cannot fix
for you and stops if it finds any (Section 7.3); the second pass then
rewrites the code of the file that made it through (Section 7.1). The
file is overwritten in place, so a file that fails the first pass is left
essentially as you wrote it.
Please make sure that for the parts the autoformatter cannot handle, you keep to the conventions discussed above so that you don't lose points on your submissions.
7.1. What the Autoformatter Fixes
You do not need to fix any of the following by hand. Write the code, run the autoformatter, and it will do all of this for you.
- Indentation
- Runs of two or more blank lines, collapsed down to one
- Horizontal whitespace between tokens
- Vertical alignment of
wireandlogicdeclarations - Vertical alignment of
assignstatements - The shape of a module definition, with each port on its own line and the opening and closing parenthesis on their own lines
- The shape of a module instantiation, with each port connection on its
own line, the parentheses on their own lines, and a single space in
.val (foo_val) - The single space between a primitive gate and its opening parenthesis,
as in
and (x, in1, in2) - Vertical alignment of port declarations, of port connections, and of the trailing comments on port declarations
- Indentation of
begin/endblocks
Wrapping long lines to 74 characters is mostly handled, but the autoformatter will not break a comment or a single long expression. Lines like the one below are left exactly as they are, and it is up to you to shorten them if you do not like the specific look.
It is advised to shorten code lines to 74 characters even before any formatting calls are made as we would hope the code is always relatively readable!!!
7.2. What the Autoformatter Does Not Fix
The autoformatter only rewrites whitespace. It never renames anything, never rewrites the text of a comment, and never changes what your hardware does. You are still responsible for all of the following.
-
Everything in Section 1: which directory a file lives in, what the file is named, how it is included, and its include guards
-
All naming (Section 3): snake case for port, wire, and instance names; camel case for module names; and the bitwidth and modeling-level suffixes on a module name. We will lint and tell you that something doesn't look right, but it is up to you to actually fix the naming.
-
The GL allowable constructs (Section 4.1), and the restriction to
wirefor GL modeling andlogicfor RTL modeling -
Sized literals, and underscores in long literals (Section 4.4)
-
Declaring a wire and assigning to it in two separate statements (Section 4.5)
-
The default case in a
casestatement, which prevent inferred latches -
Almost everything in Section 6: what you comment and why, ASCII-only comments,
//instead of/* */, the space after//, and the 74-character horizontal rules in title blocks -
Everything written inside a
task ... endtaskor acasez ... endcase, which the autoformatter skips on purpose (Section 7.4) -
Test benches
-
Tab characters and trailing spaces
7.3. What the First Pass Reports
The first pass of the formatter can be important as it reports some formatting issues that the autoformatter currently does not try to handle and stops rather than trying to format the rest of your file. These are things that will need to be manually fixed before the formatter will let you try to format the file. Among the things it will report are:
- leftover lab assignment instructor comments (Section 6.6)
always @(*)used instead ofalways_comb<=used in analways_combblock, or=used in analways_ffblock- a
casestatement with no default case - a port or wire name that is not snake case
- a parameter name that is not snake case, or a localparam name that is neither snake case nor all caps
- a tab character, or trailing whitespace at the end of a line
- a packed range that is not declared in decreasing order, such as
wire [0:7] x - a file whose name does not match the name of the module inside it
- a positional port connection where a named port connection is required
- a numeric literal wider than its declared bitwidth
A tab character and a trailing space are both reported rather than fixed. These are yours to clean up before the autoformatter will touch the file at all. We have these in place, partially to also check the non-autoformatted parts, which we hope will help you out.
7.4. What the Autoformatter Leaves Alone
Formatting is disabled inside task ... endtask and inside casez ...
endcase. Everything between those keywords is passed through exactly as
you wrote it, while the code around them is still formatted normally.
Only those two constructs are exempt: a plain case statement and a
function ... endfunction are both formatted like any other code.
Test benches are written almost entirely as tasks, and the column labels
in a directed test case (Section 6.4) would not survive the
autoformatter's alignment. The same is true of the aligned state tables
that a casez is usually used for. These regions are yours to format
by hand, and the conventions in earlier sections apply to them.
Because these regions are yours, they are also where the horizontal whitespace described in Section 2.4 does the most work. A control signal table is only readable when its columns line up, and the autoformatter will neither create that alignment for you nor repair it if you do not fix it yourself.
The example below, for a vending machine controller, is the shape we expect. Each port of the task is on its own line, the assignments in the task body are aligned, there are comments to tell what each row is for, and every entry in the table is padded so that it sits underneath its label.
localparam IDLE = 2'd0;
localparam PAID = 2'd1;
localparam VEND = 2'd2;
// Next state combinational logic
always_comb begin
casez ( { state, coin_in, item_sel } )
// coin item
// state in sel
{ IDLE, 1'b0, 1'b? } : state_next = IDLE;
{ IDLE, 1'b1, 1'b? } : state_next = PAID;
{ PAID, 1'b?, 1'b0 } : state_next = PAID;
{ PAID, 1'b?, 1'b1 } : state_next = VEND;
{ VEND, 1'b?, 1'b? } : state_next = IDLE;
default : state_next = 'x;
endcase
end
// Task for setting the output control signals
task automatic cs
(
input logic coin_rdy_,
input logic motor_en_,
input logic disp_en_
);
coin_rdy = coin_rdy_;
motor_en = motor_en_;
disp_en = disp_en_;
endtask
// Output control signal table
always_comb begin
casez ( state )
// coin motor disp
// rdy en en
IDLE: cs( 1'b1, 1'b0, 1'b0 );
PAID: cs( 1'b0, 1'b0, 1'b1 );
VEND: cs( 1'b0, 1'b1, 1'b0 );
default: cs( 'x, 'x, 'x );
endcase
end
A directed test case works the same way. The task is skipped, so the
column labels and the padding between the arguments of each check call
are entirely up to you; the example in Section 6.4 is the shape we
expect.
7.5. Turning the Autoformatter Off
The autoformatter is a tool. As stated at the very beginning of this document, these conventions are guidelines, and there are situations where defying these guidelines can actually improve the overall code quality. The same is true of the autoformatter. If it rewrites a region into something you find harder to read than what you wrote, you are allowed to keep your version. Wrap the region in a pair of formatting directives and the formatter will pass it through exactly as you wrote it.
Always give a short reason on the off directive, as in the example
above, so that a reader can tell this is a deliberate choice and not
something you forgot to turn back on. Use // for the directives rather
than the /* */ form, which the formatter also accepts but Section 6.2
does not.
Two things are worth keeping in mind. First, turning the formatter off does not turn the conventions off. Everything in Sections 1 through 6 still applies inside a disabled region, and you are now the one responsible for all of it. Second, the directives only disable the second pass. The first pass still reads the whole file, so a tab, a trailing space, or a missing default case inside a disabled region is still reported, and runs of two or more blank lines are still collapsed down to one.
Reach for this when you have a real reason: a hand-aligned table, a
column of related assignments, an expression whose line breaks carry
meaning. The point of every rule in this document is that your code is
readable to you and to everyone else who has to work on it, and where
your own layout serves that better than the tool's, keep your layout.
What this is not is a way to opt out of the conventions wholesale. A
file wrapped end to end in verilog_format: off is not a formatted
file, and we will read it as such.