100 Languages Speedrun: Episode 29: Verilog

taw

Tomasz Wegrzanowski

Posted on December 19, 2021

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
Enter fullscreen mode Exit fullscreen mode

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 module
  • input [3:0] A; - input wire A, 4 bits wide
  • input S; - input wire S, 1 bits wide, so we don't need to specify it
  • output [3:0] O; - output wire O, 4 bits wide
  • wire notS - internal wire, 1 bit wide
  • wire [3:0] AX - internal wire, 4 bits wide
  • not(notS, S); - NOT gate, it takes S as input, and outputs notS
  • and(AX[0], A[0], notS) etc. - four AND gates, as a result, AX[i] = A[i] & ~S, or AX = S ? 0 : A
  • and(BX[0], B[0], S) etc. - four AND gates, as a result, BX[i] = B[i] & S, or AX = S ? B : 0
  • or(O[0], AX[0], BX[0]) etc. - four OR gates, as a result O[i] = AX[i] | BX[i], or O = AX | BX, or O = 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 wide
  • reg S - register, 1 bit wide
  • mux4bit M(A, B, S, O); - our component mux4bit_tb uses another component mux4bit - 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 at 0 etc. As this is test bench and not real component, we can put a lot of code there
  • (!$value$plusargs("A=%d", A)), get A from command line, and return false if not passed. This starts from $ to mark it as not part of regular hardware stuff.
  • $display - basically printf(), marked with $ to make it clear it's testing command not part of the hardware.
  • $finish - end simulation
  • S = 0 - assign 0 to register S
  • #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 empty
  • S = 1 - assign 0 to register S - 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 - otherwise O 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

$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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 M*M size table to calculate (A * 2^N) % M
  • we know (A * 2^N) % M (from previous step) and B mod M (from small scale algorithm), we can then use M*M 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
Enter fullscreen mode Exit fullscreen mode

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 print
  • mod3_32bit, mod3_16bit, mod3_8bit, and mod3_4bit all just setup 2 smaller modules, then add their results together
  • mod3_2bit could do that with mod3_1bit, but it just calculates the outputs with some logic gates
  • mod3_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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Code for the Verilog episode is available here.

💖 💪 🙅 🚩
taw
Tomasz Wegrzanowski

Posted on December 19, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related