Verilog#
Open 8bitworkshop IDE
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. Whenkeystrobe
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 |
---|---|---|---|
|
sequential |
reg |
<= |
|
combinational |
wire |
= |
|
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>
.