Verilog#

Verilog Specifications#

Specification

IEEE 1364

Lifespan

1984-2005, 2005-present (stable release)

Open-Source Implementations

Verilator, Icarus, Yosys

History#

Before the 1980s, circuit design was largely a manual process. Designers would draw a schematic with pen and paper, then use truth tables and elbow grease to minimize the number of gates. To build an integrated circuit with their design, they’d hand-draw gates onto huge Mylar sheets, with limited or non-existent assistance from computer simulators.

In the 1980s, electronic design automation (or EDA) tools became available. This era introduced the concept of the hardware description language (HDL): programming languages which could both describe and simulate a digital circuit.

Such languages can perform logic synthesis – automatically compiling the program into a network of logic gates, also known as a netlist.

The resulting netlist can be used for analysis, or translated into wires and gate masks to be printed on silicon. Ideally, this would all happen with minimal intervention from the designer.

The two dominant HDLs are Verilog and VHDL. They are functionally similar, though VHDL is a little more verbose and strict, and Verilog is a little more ambiguous and succinct.

Running a Verilog program is called simulation, and it occurs in discrete time steps. Often, each time step corresponds with the main clock – so a single clock cycle takes two simulator steps, one for the rising edge and one for the falling edge.

Simulator#

The IDE includes a Verilog simulator which compiles and executes your Verilog code clock cycle-by-cycle. It is translated into JavaScript or WebAssembly, then executed in the browser.

In the default Scope View, the IDE displays the waveforms of input and output signals. You can scroll forward/backward in time, and drag the cursor left and right to see numeric values at specific times.

In the CRT View, the IDE simulates a CRT monitor in real-time, allowing us to develop video games. This view is displayed when certain output signals are present in the main module. Note that there’s a draggable gutter control at the bottom of the CRT view. You can pull this control upwards to expose the Scope View.

If there are multiple modules, the IDE will look for a top module named top or ending with _top.

Special Signals#

clk (input)

Main clock signal, always present.

reset (input)

Reset signal.

hsync, vsync, rgb (output)

Simulated CRT outputs. If rgb is a 3-bit value, it outputs 8 colors. A 4-bit value outputs 16 colors. It can also be a 32-bit RGB value with the upper 8 bits set.

hpaddle, vpaddle (input)

Paddle inputs. These are synchronized with the CRT beam, so that they go high (1) at the top of the screen when the paddle is turned to the left, and at the bottom when the paddle is turned right.

spkr (output)

1-bit speaker output.

keycode (input), keystrobe (output)

keycode is an 8-bit value with the high bit set when a key is pressed. When keystrobe is asserted, the high bit is cleared by the simulator.

Verilog Reference#

For more information, see Books.

Modules#

    // module definition
    module binary_counter(clk, reset, outbits, flag, bus);
        input clk, reset;   // inputs
        output [3:0] outbits;   // outputs
        output reg flag;    // output register
        inout [7:0] bus;    // bi-directional
    endmodule

    // module instance
    binary_counter bincount(.clk(clk), .reset(reset), ...);

Literals#

 Binary byte       8'b10010110
 Decimal byte      8'123
 Hexadecimal byte  2'h1f
 6-bit Octal       2'O71
 Unsized decimal   123

Registers, Nets, and Buses#

    reg bit;    // 1-bit register
    reg x[7:0]; // 8-bit register
    wire signal;    // 1-bit wire
    wire y[15:0];   // 16-bit wire

Bit Slices#

    x[0]        // first bit (rightmost, or least significant)
    x[1]        // second bit
    x[3:1]      // fourth, third, and second bits
    {x[3], x[2], x[1]}  // concatenation, same as above

Binary Operators#

+   binary addition
-   binary subtraction
<<  shift left
>>  shift right
>   greater than
>=  greater than or equal to
<   less than
<=  less than or equal to
==  equals
!=  not equals
&   bitwise AND
^   bitwise XOR
|   bitwise OR
&&  logical AND
||  logical OR

*   multiply    // NOTE: these operators
/   divide      // don't always synthesize
\%  modulus     // very efficiently.

Unary Operators#

!   logical negation
~   bitwise negation
-   arithmetic negation

Reduction Operators#

These operators collapse several bits into one bit.

&   AND reduction (true if all bits are set)
|   OR reduction (true if any bits are set)
^   XOR reduction (true if odd number of bits set)

Conditional Operator#

        condition ? if_true_expr : if_false_expr

If Statements#

        if (condition) <expression> else <expression>;

Always Blocks#

Rules for usage:

Keyword

Logic

Assign To

Operator

always @(posedge clk)

sequential

reg

<=

always @(*)

combinational

wire

=

assign

combinational

wire

=

On rising clock edge:

        always @(posedge clk) begin ... end

On rising clock edge or asynchronous reset:

        always @(posedge clk or posedge reset) begin ... end

Non-blocking assignment (all right sides evaluated first):

        variable <= ...expression...;

Combinational logic (blocking assignment):

        always @(*) ...

        assign variable = ...expression...;

Miscellaneous#

Include file:

        `include "file.v"

Define macros:

        // macro definitions
        `define OP_INC 4'h2
        `define I_COMPUTE(dest,op) { 2'b00, (dest), (op) }

        // macro expansion (w/ parameters)
        `I_COMPUTE(`DEST_A, `OP_ADD)

Define static array (e.g. ROM):

        initial begin
            rom[0] = 8'7f;
            rom[1] = 8'1e;
            ...
        end

Parameters:

        localparam unchangable = 123;
        parameter change_me = 234;

Case statement:

        case (value)
            0: begin .. end;
            1: begin .. end;
        endcase

Casez statement:

        casez (opcode)
                8'b00??????: begin
                  state <= S_COMPUTE;
                end
        endcase

Stop the program:

        $stop;

Driving a tri-state bus using a high-impedance (Z) value:

        assign data = we ? 8'bz : mem[addr];

Functions:

         function signed [3:0] sin_16x4;
            ...
         endfunction

for loops (for replicating logic or filling static arrays):

        for (int i=0; i<16; i++) begin
            ...
        end

NANOASM Assembler#

NANOASM can translate custom assembly language for Verilog CPUs.

Configuration#

The CPU’s language is defined in a a JSON configuration file.

Our assembler’s configuration format has a number of rules. Each rule has a format that matches a line of assembly code, and a bit pattern that is emitted when matched. For example, this is the rule for the swapab instruction:

    {"fmt":"swapab", "bits":["10000001"]}

The “fmt” attribute defines the pattern to be matched, which in this case is just a simple instruction without any operands.

If the rule is matched, the “bits” attribute defines the machine code to be emitted. This can be a combination of binary constants and variables. Here we just emit the bits 10000001 – i.e., the byte $81.

Let’s say we want to match the following format, sta <n> where <n> is a variable 4-bit operand:

	sta [0-15]	; 4-bit constant

We can specify different types of variables in the \textbf{vars} section of the configuration file. For example, this defines a 4-bit variable named const4:

    "const4":{"bits":4}

The assembler rules are big-endian by default (most significant bits first) so if you need constants larger than a single machine word, set the “endian” property:

    "abs16":{"bits":16,"endian":"little"}

To include a variable in a rule, prefix the variable’s name with a tilde (\textasciitilde). For example, our sta rule takes one ~const4 variable:

    {"fmt":"sta ~const4", "bits":["1001",0]}

We also have to include the value of the variable in the instruction encoding. To do this, we put an integer into the “bits” array – 0 for the first variable, 1 for the second, etc.

An example: The assembler is given the instruction sta 15. It matches the rule sta ~const4, and assigns 15 to the first variable slot. It then outputs the the bits 1001 and then the 4-bit value 15, or 1111. The final opcode is 10011111 or $9f.

For instruction sets where immediate values are split up into multiple bitslices, like RISC-V, you can pull them apart by specifying the variable index (a) start bit index (b) and number of bits (n):

    {"fmt":"sb ~reg,~imm12(~reg)", "bits":[
        {"a":1,"b":5,"n":7},     // var #1, bits 5-11
        2,                       // var #2
        0,                       // var #0
        "000",                   // bits "000"
        {"a":1,"b":0,"n":5},     // var #1, bits 0-4
        "0100011"                // bits "0100011"
    ]}

Tokens#

Variables can also be defined by tokens. For example, the following rule defines a variable reg with four possible values – a, b, ip, or none, encoded as two bits:

    "reg":{"bits":2, "toks":["a", "b", "ip", "none"]},

Here’s an example of a rule that uses it:

    {"fmt":"mov ~reg,[b]", "bits":["11",0,"1011"]},

When decoding mov a,[b], the assembler sees that a is the first token in the variable, and substitutes the bit pattern 00. The final bit pattern is 11 00 1011 which makes a full byte.

More complex instructions are possible, by using multiple variables in a single rule:

    {"fmt":"~binop ~reg,#~imm8", "bits":["01",1,"1",0,2]},

In this rule, binop, reg, and imm8 (2) are variables, identified by the integers 0, 1, and 2. add b,#123 is an example of a matching instruction. This rule emits an opcode 16 bits (two bytes) long.

Directives#

NANOASM supports these directives:

.arch <arch> – Required. Loads the file <arch>.json and configures the assembler.

.org <address> – The start address of the ROM, as seen by the CPU.

.len <length> – The length of the ROM file output by the assembler.

.width <value> – Specify the size in bits of an machine word. Default = 8.

.define <label> <value> – Define a label with a given numeric value.

.data $aa $bb ... – Includes raw data in the output.

.string ..... – Converts a string to machine words, then includes it in the output.

.align <value> – Align the current IP to a multiple of <value>.

Comments