100 Languages Speedrun: Episode 29: Verilog
Hardware design is about evenly split between two languages - Verilog and VHDL. They both have serious issues, and people keep trying to write a better language, and getting little traction.
Verilog is a lot less verbose than VHDL, so let's do some Verilog.
We'll specifically be using Icarus Verilog.
Hardware design
Verilog programs describe hardware. Generally there are no variables, functions, loops, if/elses, or such fancy features, because that's not how hardware works.
If you've never done any hardware, here's extremely simplified view:
- hardware we're designing is a bunch of logic gates (ANDs, ORs, NOTs, XORs etc.) and wires connecting stuff together
- every wire can be connected to any number of places, but only one can output (write) to it, everything else only inputs (reads) from it
- each wire can only have 0 or 1 on it at any given time, so if you need 16-bit number, you need 16 wires, and Verilog conveniently handles such bundles of wires
- some of those wires are connected to the outside of the device (some as inputs, some as outputs)
- there's often one special external wire called a "clock" which keeps going from 0 to 1 and back, and these transitions can be used to synchronize things in our design (that's what "CPU clock" refers to)
- a bunch of gates and wires can be looped together to provide some number of bits of storage - we generally treat that as a "register" and not bother with individual components at Verilog level
- Verilog also has some non-hardware commands to help the simulator, which we'll use for testing
Once you create your hardware in Verilog, you can run it on a simulator - that's what we'll be doing. Or you can export it to run on real hardware, either a FPGA or an ASIC.
While hardware is mostly designed by huge corporations, this isn't a completely crazy thing to do for a regular person - if some new cryptocoin becomes popular, people will try to design specialized hardware that can mine it faster than CPUs and GPUs.
Hello, World!
As hardware deals with numbers and not text, most of the usual tasks like Hello, World and FizzBuzz don't make much sense in Verilog. So let's start by creating very simple circuit known as "multiplexer", making it 4 bit wide:
- there's 4 input wires A
- there's 4 input wires B
- there's 1 input wire S
- there's 4 output wires O
- if S=0, then O=A
- if S=1, then O=B
So basically, it's O = S ? A : B
, except with wires instead of code.
Multiplexers are everywhere in computer hardware - hardware doesn't really have "if"s, so to do some conditional calculations, it calculates both things, and then selects which one it really wanted with a multiplexer. It might seem like a waste at first, but modern CPUs have tens of billions of transistors.
Let's write it down:
module mux4bit(A, B, S, O);
input [3:0] A;
input [3:0] B;
input S;
output [3:0] O;
wire notS;
wire [3:0] AX;
wire [3:0] BX;
not(notS, S);
and(AX[0], A[0], notS);
and(AX[1], A[1], notS);
and(AX[2], A[2], notS);
and(AX[3], A[3], notS);
and(BX[0], B[0], S);
and(BX[1], B[1], S);
and(BX[2], B[2], S);
and(BX[3], B[3], S);
or(O[0], AX[0], BX[0]);
or(O[1], AX[1], BX[1]);
or(O[2], AX[2], BX[2]);
or(O[3], AX[3], BX[3]);
endmodule
We specify every single logic gate (1x NOT, 8x AND, 4x OR), and every single wire separately, both external, and internal to the component.
Of course real Verilog doesn't actually use this extremely verbose style, and we'll get to some more concise syntax later.
Step by step explanation:
module mux4bit(A, B, S, O);
- start of the module, and list of external wires we'd need to connect if we actually use this moduleinput [3:0] A;
- input wire A, 4 bits wideinput S;
- input wire S, 1 bits wide, so we don't need to specify itoutput [3:0] O;
- output wire O, 4 bits widewire notS
- internal wire, 1 bit widewire [3:0] AX
- internal wire, 4 bits widenot(notS, S);
- NOT gate, it takesS
as input, and outputsnotS
and(AX[0], A[0], notS)
etc. - four AND gates, as a result,AX[i] = A[i] & ~S
, orAX = S ? 0 : A
and(BX[0], B[0], S)
etc. - four AND gates, as a result,BX[i] = B[i] & S
, orAX = S ? B : 0
or(O[0], AX[0], BX[0])
etc. - four OR gates, as a resultO[i] = AX[i] | BX[i]
, orO = AX | BX
, orO = S ? B : A
Now we need to write a "test bench". Writing test benches for complex devices is really complicated subject, but we'll do something really trivial here.
module mux4bit_tb;
reg [3:0] A;
reg [3:0] B;
reg S;
wire [3:0] O;
mux4bit M(A, B, S, O);
initial begin
if (!$value$plusargs("A=%d", A)) begin
$display("ERROR: please specify +A=<value>");
$finish;
end
if (!$value$plusargs("B=%d", B)) begin
$display("ERROR: please specify +B=<value>");
$finish;
end
S = 0;
#1;
$display("A = %d, B = %d, S = %d, O = %d", A, B, S, O);
S = 1;
#1;
$display("A = %d, B = %d, S = %d, O = %d", A, B, S, O);
$finish;
end
endmodule
Let's try to use it:
$ iverilog -o mux4bit_tb.vvp mux4bit.v mux4bit_tb.v
% ./mux4bit_tb.vvp +A=7 +B=3
A = 7, B = 3, S = 0, O = 7
A = 7, B = 3, S = 1, O = 3
% ./mux4bit_tb.vvp +A=4 +B=2
A = 4, B = 2, S = 0, O = 4
A = 4, B = 2, S = 1, O = 2
% ./mux4bit_tb.vvp +A=1 +B=6
A = 1, B = 6, S = 0, O = 1
A = 1, B = 6, S = 1, O = 6
% ./mux4bit_tb.vvp +A=2 +B=2
A = 2, B = 2, S = 0, O = 2
A = 2, B = 2, S = 1, O = 2
As you can see, we compile all the module files together into a vvp
file, then we optionally pass some arguments to the simulation, then simulation does some things.
Let's go through our test bench code:
reg [3:0] A
- we declare a register, which is like a variable, this one is 4 bit widereg S
- register, 1 bit widemux4bit M(A, B, S, O);
- our componentmux4bit_tb
uses another componentmux4bit
- to instantiate a component we need to specify what all its inputs and outputs connect to. In this case A, B, and S connect to test bench's register, and O connect to some internal wires.initial begin ... end
- normally we'd use it to specify start state of a component, for example that counter starts at0
etc. As this is test bench and not real component, we can put a lot of code there(!$value$plusargs("A=%d", A))
, getA
from command line, and returnfalse
if not passed. This starts from$
to mark it as not part of regular hardware stuff.$display
- basicallyprintf()
, marked with$
to make it clear it's testing command not part of the hardware.$finish
- end simulationS = 0
- assign0
to registerS
#1
wait 1 clock tick - Verilog allows every component to specify how long it takes to recalculate values, so you can simulate complex timing, but we won't be using any such functionality.$display("A = %d, B = %d, S = %d, O = %d", A, B, S, O);
- print all the inputs and outputs -O
is only calculated because we waited a tick, otherwise it would be emptyS = 1
- assign0
to registerS
- we waited a tick, so we're not trying to write two different values at the same time (generally a bad idea)#1
wait another tick - otherwiseO
would still have old value$display("A = %d, B = %d, S = %d, O = %d", A, B, S, O);
- because we waited,O
is updated now
Hopefully that doesn't sound too crazy.
More concise multiplexer
I showed the first multiplexer in extremely most explicit style, but of course nobody writes this way. Here's 8 bit multiplexer:
module mux8bit(A, B, S, O);
input [7:0] A;
input [7:0] B;
input S;
output [7:0] O;
assign O = S ? B : A;
endmodule
It is pretty much equivalent to the long code. assign O = S ? B : A;
sets up all the wires and logic gates to make this expression work.
module mux8bit_tb;
reg [7:0] A;
reg [7:0] B;
reg S;
wire [7:0] O;
reg [31:0] i;
mux8bit M(A, B, S, O);
initial begin
$monitor("A=%d B=%d S=%d O=%D", A, B, S, O);
for (i=0; i<256*256*2; i=i+1) begin
{A,B,S} = i;
#1;
end
$finish;
end
endmodule
Instead of passing specific values, we can just tell the test bench to generate every possible value. We could have triple nested for
, but i
contains 8+8+1 bits already (and some extras), so {A,B,S} = i
will assign the right bits of i
to A
(bits 16 to 9), B
(bits 8 to 1) and S
(bit 0).
We can either check every value, or just get a random sample (randsample
from unix-utilities
collection):
$ iverilog -o mux8bit_tb.vvp mux8bit.v mux8bit_tb.v
$ ./mux8bit_tb.vvp | randsample 20
I= 15911, A= 31 B= 19 S=1 O= 19
I= 113470, A=221 B=159 S=0 O=221
I= 28346, A= 55 B= 93 S=0 O= 55
I= 18224, A= 35 B=152 S=0 O= 35
I= 84536, A=165 B= 28 S=0 O=165
I= 28159, A= 54 B=255 S=1 O=255
I= 123640, A=241 B=124 S=0 O=241
I= 15950, A= 31 B= 39 S=0 O= 31
I= 50381, A= 98 B=102 S=1 O=102
I= 33082, A= 64 B=157 S=0 O= 64
I= 77032, A=150 B=116 S=0 O=150
I= 52870, A=103 B= 67 S=0 O=103
I= 63252, A=123 B=138 S=0 O=123
I= 59551, A=116 B= 79 S=1 O= 79
I= 104170, A=203 B=117 S=0 O=203
I= 126954, A=247 B=245 S=0 O=247
I= 130665, A=255 B= 52 S=1 O= 52
I= 14603, A= 28 B=133 S=1 O=133
I= 118586, A=231 B=157 S=0 O=231
I= 82511, A=161 B= 39 S=1 O= 39
$monitor
displays the value whenever any of its arguments change, so we don't need to do $display
in the loop, we can just set it up once.
Odd Even
Here's a very simple module that checks if 16-bit number passed is odd or even. It can just check the lowest bit:
module oddeven(A, O);
input [15:0] A;
output O;
assign O = A[0];
endmodule
For more complex component, testing every possible value is not really practical. One common approach is random testing, so let's give it a try:
module oddeven_tb;
reg [15:0] A;
wire O;
reg [31:0] i;
oddeven OE(A, O);
initial begin
$monitor("A=%d O=%d", A, O);
for (i=0; i<20; i=i+1) begin
A = $random;
#1;
end
$finish;
end
endmodule
Which we can run with:
$ iverilog -o oddeven.vvp oddeven.v oddeven_tb.v
$ ./oddeven.vvp
A=13604 O=0
A=24193 O=1
A=54793 O=1
A=22115 O=1
A=31501 O=1
A=39309 O=1
A=33893 O=1
A=21010 O=0
A=58113 O=1
A=52493 O=1
A=61814 O=0
A=52541 O=1
A=22509 O=1
A=63372 O=0
A=59897 O=1
A= 9414 O=0
A=33989 O=1
A=53930 O=0
A=63461 O=1
A=29303 O=1
Modulo math
Before we get to the FizzBuzz, we need to figure out how we can handle divisibility by 3 and 5, without adding hardware division module to the design, as they take huge number of transistors.
Here are basic mathematical facts about modulo operations:
- (A + B) % M = ((A % M) + (B % M)) % M
- (A B) % M = ((A % M) (B % M)) % M
Well, what does it have to do with anything?
It can be used for a clever algorithm:
- for 1-bit number A, we know what it is modulo 3 or 5 - it's just always A (1 or 0)
- so by induction, let's say we have an algorithm for calculating what N-bit numbers modulo M are, and we want to use it to implement 2N-bit numbers
- let's say 2N-bit number is
(A * 2^N) + B
- we can precompute (2^N mod M)` as it's a constant
- we know
(A % M)
(from small scale algorithm) and(2^N mod M)
(as it's a constant), we can then use MM size table to calculate `(A 2^N) % M` - we know
(A * 2^N) % M
(from previous step) andB mod M
(from small scale algorithm), we can then use MM size table to calculate `(A 2^N) + B`
Modulo 3 Component
We're going to have a lot of components, each with 3 wires out, indicating if division by 3 gives 0, 1, or 2.
Each component is in file corresponding to its name, I'll just list them all together:
module mod3_32bit(A, Mod);
input [31:0] A;
output [2:0] Mod;
wire [2:0] ModHi;
wire [2:0] ModLow;
mod3_16bit Hi(A[31:16], ModHi);
mod3_16bit Low(A[15:0], ModLow);
// No need to do shift as (2**16) % 3 == 1
mod3_add Add(ModHi, ModLow, Mod);
endmodule
module mod3_16bit(A, Mod);
input [15:0] A;
output [2:0] Mod;
wire [2:0] ModHi;
wire [2:0] ModLow;
mod3_8bit Hi(A[15:8], ModHi);
mod3_8bit Low(A[7:0], ModLow);
// No need to do shift as (2**8) % 3 == 1
mod3_add Add(ModHi, ModLow, Mod);
endmodule
module mod3_8bit(A, Mod);
input [7:0] A;
output [2:0] Mod;
wire [2:0] ModHi;
wire [2:0] ModLow;
mod3_4bit Hi(A[7:4], ModHi);
mod3_4bit Low(A[3:0], ModLow);
// No need to do shift as (2**4) % 3 == 1
mod3_add Add(ModHi, ModLow, Mod);
endmodule
module mod3_4bit(A, Mod);
input [3:0] A;
output [2:0] Mod;
wire [2:0] ModHi;
wire [2:0] ModLow;
mod3_2bit Hi(A[3:2], ModHi);
mod3_2bit Low(A[1:0], ModLow);
// No need to do shift as (2**2) % 3 == 1
mod3_add Add(ModHi, ModLow, Mod);
endmodule
module mod3_2bit(A, Mod);
input [1:0] A;
output [2:0] Mod;
// 2 => 2 mod 3
assign Mod[2] = A[1] & ~A[0];
// 1 => 1 mod 3
assign Mod[1] = ~A[1] & A[0];
// 0 or 3 => 0 mod 3
assign Mod[0] = (~A[0] & ~A[1]) | (A[0] & A[1]);
endmodule
module mod3_add(ModA, ModB, ModOut);
input [2:0] ModA;
input [2:0] ModB;
output [2:0] ModOut;
// 0 + 0 = 0 => 0
// 1 + 2 = 3 => 0
// 2 + 1 = 3 => 0
assign ModOut[0] = (ModA[0] & ModB[0]) | (ModA[1] & ModB[2]) | (ModA[2] & ModB[1]);
// 0 + 1 = 1 => 1
// 1 + 0 = 1 => 1
// 2 + 2 = 4 => 1
assign ModOut[1] = (ModA[0] & ModB[1]) | (ModA[1] & ModB[0]) | (ModA[2] & ModB[2]);
// 0 + 2 = 2 => 2
// 2 + 0 = 2 => 2
// 1 + 1 = 2 => 2
assign ModOut[2] = (ModA[0] & ModB[2]) | (ModA[2] & ModB[0]) | (ModA[1] & ModB[1]);
endmodule
module mod3_tb;
reg [31:0] A;
wire [2:0] Mod;
reg [31:0] i;
mod3_32bit M(A, Mod);
initial begin
$monitor("A=%d O[0]=%d O[1]=%d O[2]=%d", A, Mod[0], Mod[1], Mod[2]);
for (i=0; i<20; i=i+1) begin
A = $random;
#1;
end
$finish;
end
endmodule
That's a lot of code, but it's really not much new:
mod3_tb
is same test bench we used for Odd Even, just with different printmod3_32bit
,mod3_16bit
,mod3_8bit
, andmod3_4bit
all just setup 2 smaller modules, then add their results togethermod3_2bit
could do that withmod3_1bit
, but it just calculates the outputs with some logic gatesmod3_add
takes two remainders, and returns their sum (with each wire being potential remainder)
In general, this recursion might need to do some bit reshuffling, but as it so happens, for division by 3 we don't have to do anything (mod3_2bit
would need to do reshuffling step, if we implemented it the same as others).
We can now run it:
$ ./mod3_tp.vvp
A= 303379748 O[0]=0 O[1]=0 O[2]=1
A=3230228097 O[0]=1 O[1]=0 O[2]=0
A=2223298057 O[0]=0 O[1]=1 O[2]=0
A=2985317987 O[0]=0 O[1]=0 O[2]=1
A= 112818957 O[0]=1 O[1]=0 O[2]=0
A=1189058957 O[0]=0 O[1]=0 O[2]=1
A=2999092325 O[0]=0 O[1]=0 O[2]=1
A=2302104082 O[0]=0 O[1]=1 O[2]=0
A= 15983361 O[0]=1 O[1]=0 O[2]=0
A= 114806029 O[0]=0 O[1]=1 O[2]=0
A= 992211318 O[0]=1 O[1]=0 O[2]=0
A= 512609597 O[0]=0 O[1]=0 O[2]=1
A=1993627629 O[0]=1 O[1]=0 O[2]=0
A=1177417612 O[0]=0 O[1]=1 O[2]=0
A=2097015289 O[0]=0 O[1]=1 O[2]=0
A=3812041926 O[0]=1 O[1]=0 O[2]=0
A=3807872197 O[0]=0 O[1]=1 O[2]=0
A=3574846122 O[0]=1 O[1]=0 O[2]=0
A=1924134885 O[0]=1 O[1]=0 O[2]=0
A=3151131255 O[0]=1 O[1]=0 O[2]=0
Modulo 5 Component
This is basically identical to modulo 3, except mod5_4bit
does need to do some reshuffling.
module mod5_32bit(A, Mod);
input [31:0] A;
output [4:0] Mod;
wire [4:0] ModHi;
wire [4:0] ModLow;
mod5_16bit Hi(A[31:16], ModHi);
mod5_16bit Low(A[15:0], ModLow);
// No need to do shift as (2**16) % 5 == 1
mod5_add Add(ModHi, ModLow, Mod);
endmodule
module mod5_16bit(A, Mod);
input [15:0] A;
output [4:0] Mod;
wire [4:0] ModHi;
wire [4:0] ModLow;
mod5_8bit Hi(A[15:8], ModHi);
mod5_8bit Low(A[7:0], ModLow);
// No need to do shift as (2**8) % 5 == 1
mod5_add Add(ModHi, ModLow, Mod);
endmodule
module mod5_8bit(A, Mod);
input [7:0] A;
output [4:0] Mod;
wire [4:0] ModHi;
wire [4:0] ModLow;
mod5_4bit Hi(A[7:4], ModHi);
mod5_4bit Low(A[3:0], ModLow);
// No need to do shift as (2**4) % 5 == 1
mod5_add Add(ModHi, ModLow, Mod);
endmodule
module mod5_4bit(A, Mod);
input [3:0] A;
output [4:0] Mod;
wire [4:0] ModHi;
wire [4:0] ModHi4;
wire [4:0] ModLow;
mod5_2bit Hi(A[3:2], ModHi);
mod5_2bit Low(A[1:0], ModLow);
// We need to do shift as (2**2) % 5 == 4
// Notice that if we do this reshuffle twice,
// everything will be back where it started
// That's why 8bit and bigger components don't
// need to do any reshuffle.
// mod5_8bit should do this 2x, but that's same as not doing it.
// mod5_16bit should do this 4x, but that's same as not doing it etc.
assign ModHi4[0] = ModHi[0]; // 0 * 4 = 0 => 0
assign ModHi4[4] = ModHi[1]; // 1 * 4 = 4 => 4
assign ModHi4[3] = ModHi[2]; // 2 * 4 = 8 => 3
assign ModHi4[2] = ModHi[3]; // 3 * 4 = 12 => 2
assign ModHi4[1] = ModHi[4]; // 4 * 4 = 16 => 1
mod5_add Add(ModHi4, ModLow, Mod);
endmodule
module mod5_2bit(A, Mod);
input [1:0] A;
output [4:0] Mod;
// no 2 bit number is 4 mod 5
assign Mod[4] = 0;
// numbers 0-3 are themselves mod 5
assign Mod[3] = A[0] & A[1];
assign Mod[2] = A[1] & ~A[0];
assign Mod[1] = ~A[1] & A[0];
assign Mod[0] = ~A[0] & ~A[1];
endmodule
module mod5_add(ModA, ModB, ModOut);
input [4:0] ModA;
input [4:0] ModB;
output [4:0] ModOut;
// 0 + 0 = 0 => 0
// 1 + 4 = 5 => 0
// 2 + 3 = 5 => 0
// 3 + 2 = 5 => 0
// 4 + 1 = 5 => 0
assign ModOut[0] = (ModA[0] & ModB[0]) | (ModA[1] & ModB[4]) | (ModA[2] & ModB[3]) | (ModA[3] & ModB[2]) | (ModA[4] & ModB[1]);
// 0 + 1 = 1 => 1
// 1 + 0 = 1 => 1
// 2 + 4 = 6 => 1
// 3 + 3 = 6 => 1
// 4 + 2 = 6 => 1
assign ModOut[1] = (ModA[0] & ModB[1]) | (ModA[1] & ModB[0]) | (ModA[2] & ModB[4]) | (ModA[3] & ModB[3]) | (ModA[4] & ModB[2]);
// 0 + 2 = 2 => 2
// 1 + 1 = 2 => 2
// 2 + 0 = 2 => 2
// 3 + 4 = 7 => 2
// 4 + 3 = 7 => 2
assign ModOut[2] = (ModA[0] & ModB[2]) | (ModA[1] & ModB[1]) | (ModA[2] & ModB[0]) | (ModA[3] & ModB[4]) | (ModA[4] & ModB[3]);
// 0 + 3 = 3 => 3
// 1 + 2 = 3 => 3
// 2 + 1 = 3 => 3
// 3 + 0 = 3 => 3
// 4 + 4 = 8 => 3
assign ModOut[3] = (ModA[0] & ModB[3]) | (ModA[1] & ModB[2]) | (ModA[2] & ModB[1]) | (ModA[3] & ModB[0]) | (ModA[4] & ModB[4]);
// 0 + 4 = 4 => 4
// 1 + 3 = 4 => 4
// 2 + 2 = 4 => 4
// 3 + 1 = 4 => 4
// 4 + 0 = 4 => 4
assign ModOut[4] = (ModA[0] & ModB[4]) | (ModA[1] & ModB[3]) | (ModA[2] & ModB[2]) | (ModA[3] & ModB[1]) | (ModA[4] & ModB[0]);
endmodule
module mod5_tb;
reg [31:0] A;
wire [4:0] Mod;
reg [31:0] i;
mod5_32bit M(A, Mod);
initial begin
$monitor("A=%d O[0]=%d O[1]=%d O[2]=%d O[3]=%d O[4]=%d", A, Mod[0], Mod[1], Mod[2], Mod[3], Mod[4]);
for (i=0; i<20; i=i+1) begin
A = $random;
#1;
end
$finish;
end
endmodule
All the modules are just wider versions of the same thing, except mod5_4bit
where I put some comments.
We can see it in action:
$ ./mod5_tp.vvp
A= 303379748 O[0]=0 O[1]=0 O[2]=0 O[3]=1 O[4]=0
A=3230228097 O[0]=0 O[1]=0 O[2]=1 O[3]=0 O[4]=0
A=2223298057 O[0]=0 O[1]=0 O[2]=1 O[3]=0 O[4]=0
A=2985317987 O[0]=0 O[1]=0 O[2]=1 O[3]=0 O[4]=0
A= 112818957 O[0]=0 O[1]=0 O[2]=1 O[3]=0 O[4]=0
A=1189058957 O[0]=0 O[1]=0 O[2]=1 O[3]=0 O[4]=0
A=2999092325 O[0]=1 O[1]=0 O[2]=0 O[3]=0 O[4]=0
A=2302104082 O[0]=0 O[1]=0 O[2]=1 O[3]=0 O[4]=0
A= 15983361 O[0]=0 O[1]=1 O[2]=0 O[3]=0 O[4]=0
A= 114806029 O[0]=0 O[1]=0 O[2]=0 O[3]=0 O[4]=1
A= 992211318 O[0]=0 O[1]=0 O[2]=0 O[3]=1 O[4]=0
A= 512609597 O[0]=0 O[1]=0 O[2]=1 O[3]=0 O[4]=0
A=1993627629 O[0]=0 O[1]=0 O[2]=0 O[3]=0 O[4]=1
A=1177417612 O[0]=0 O[1]=0 O[2]=1 O[3]=0 O[4]=0
A=2097015289 O[0]=0 O[1]=0 O[2]=0 O[3]=0 O[4]=1
A=3812041926 O[0]=0 O[1]=1 O[2]=0 O[3]=0 O[4]=0
A=3807872197 O[0]=0 O[1]=0 O[2]=1 O[3]=0 O[4]=0
A=3574846122 O[0]=0 O[1]=0 O[2]=1 O[3]=0 O[4]=0
A=1924134885 O[0]=1 O[1]=0 O[2]=0 O[3]=0 O[4]=0
A=3151131255 O[0]=1 O[1]=0 O[2]=0 O[3]=0 O[4]=0
FizzBuzz
Now that we have modules telling us if something is divisible by 5 or 3, we can do the FizzBuzz.
Our FizzBuzz won't do any printing, it will have one input for 32bit number, and 4 output wires - PrintFizz
, PrintBuzz
, PrintFizzBuzz
and PrintNumber
. We can connect it to some printing component to do the actual printing.
module fizzbuzz(A, PrintNumber, PrintFizz, PrintBuzz, PrintFizzBuzz);
input [31:0] A;
output PrintNumber, PrintFizz, PrintBuzz, PrintFizzBuzz;
wire [2:0] Mod3;
wire [4:0] Mod5;
mod3_32bit M3(A, Mod3);
mod5_32bit M5(A, Mod5);
assign PrintNumber = ~Mod3[0] & ~Mod5[0];
assign PrintFizz = Mod3[0] & ~Mod5[0];
assign PrintBuzz = ~Mod3[0] & Mod5[0];
assign PrintFizzBuzz = Mod3[0] & Mod5[0];
endmodule
module fizzbuzz_tb;
reg [31:0] A;
wire PrintNumber, PrintFizz, PrintBuzz, PrintFizzBuzz;
reg [31:0] i;
fizzbuzz M(A, PrintNumber, PrintFizz, PrintBuzz, PrintFizzBuzz);
initial begin
$monitor("A=%d Number=%d Fizz=%d Buzz=%d FizzBuzz=%d", A, PrintNumber, PrintFizz, PrintBuzz, PrintFizzBuzz);
for (i=1; i<=100; i=i+1) begin
A = i;
#1;
end
$finish;
end
endmodule
And it totally works:
$ ./fizzbuzz_tp.vvp
A= 1 Number=1 Fizz=0 Buzz=0 FizzBuzz=0
A= 2 Number=1 Fizz=0 Buzz=0 FizzBuzz=0
A= 3 Number=0 Fizz=1 Buzz=0 FizzBuzz=0
A= 4 Number=1 Fizz=0 Buzz=0 FizzBuzz=0
A= 5 Number=0 Fizz=0 Buzz=1 FizzBuzz=0
A= 6 Number=0 Fizz=1 Buzz=0 FizzBuzz=0
A= 7 Number=1 Fizz=0 Buzz=0 FizzBuzz=0
A= 8 Number=1 Fizz=0 Buzz=0 FizzBuzz=0
A= 9 Number=0 Fizz=1 Buzz=0 FizzBuzz=0
A= 10 Number=0 Fizz=0 Buzz=1 FizzBuzz=0
A= 11 Number=1 Fizz=0 Buzz=0 FizzBuzz=0
A= 12 Number=0 Fizz=1 Buzz=0 FizzBuzz=0
A= 13 Number=1 Fizz=0 Buzz=0 FizzBuzz=0
A= 14 Number=1 Fizz=0 Buzz=0 FizzBuzz=0
A= 15 Number=0 Fizz=0 Buzz=0 FizzBuzz=1
A= 16 Number=1 Fizz=0 Buzz=0 FizzBuzz=0
A= 17 Number=1 Fizz=0 Buzz=0 FizzBuzz=0
A= 18 Number=0 Fizz=1 Buzz=0 FizzBuzz=0
A= 19 Number=1 Fizz=0 Buzz=0 FizzBuzz=0
A= 20 Number=0 Fizz=0 Buzz=1 FizzBuzz=0
Should you use Verilog?
In this episode I used very low level and verbose Verilog, and only used prints for testing. In real life you'd mostly write at much higher level, and there's a lot of sophisticated visualization tools for testing and simulating the circuit. So don't take this episode to be representative of typical Verilog.
From what I've seen, actual hardware designers seem to be very unhappy with both Verilog and VHDL, and new hardware design languages are being created all the time. But for now, it's pretty much the overwhelming standard (with VHDL being a more verbose but otherwise very similar language).
The other key skill in hardware design is automated verification, and for that you'd need to know a logic language like Z3.
For people who want to play with electronics, I'd recommend one of the video games first. I personally liked Silicon Zeroes best. Shenzhen I/O is more about programming microcontrollers than making circuits, but it's also very popular. Once you play a bunch of such games, and want to try something more real, Verilog is a fun language to try. And then who knows, maybe you'll design an FPGA or an ASIC for the next big cryptocurrency and get rich.
Code
All code examples for the series will be in this repository.