In this exercise, I will be creating a very simple design which is a NOT gate or an inverter. If you have zero knowledge on Verilog, do not worry, as I will discuss the code in a detailed manner.
A NOT gate is a circuit that accepts a 1 bit input and produces an inverted value of the input. To summarize it:
- Input logic 1 (HIGH) will result to Output logic 0 (LOW)
- Input logic 0 (LOW) will result to Output logic 1 (HIGH)
The onboard push button should be used to get the user input and use one of the onboard LEDs to show the output.
Step2: Develop HDL
In Verilog, the basic building blocks is called a module. If you are familiar with LEGO, you can think of it as a single brick. It has a unique shape and has interface so that other bricks can connect to it. If you are an experienced software engineer you can think Verilog modules are similar to classes. It has certain functionality and has interface to connect to other classes. It has major differences compared to classes though, as it is static in nature and does not support that much of inheritance and polymorphism. I don't intend to discuss classes vs modules in details in this tutorial maybe on my succeeding blog post but I think it is helpful to have that kind of analogy.
The syntax for a Verilog module is as follows:
The terms module and endmodule are reserved keywords to create a module. The term my_not_gate, is a user defined name for that module, you can name it anything you want like your_not_gate , as long as it complies with the Verilog naming rules. One of those rules is you cannot use an operator, like + or used a reserved keyword like module. If you do that, an error will be flagged during compilation. I will show you later how it looks like.
If we imagine what the code above looks like, it will be something like this. An empty module named my_not_gate.
The next step is to add input and output ports to the following module. The code for that is as follows:
The syntax is straight forward, input and output are Verilog keywords for declaring port direction whether it is an input or an output of the module. Port names a and b are user defined, you can name it anything you want as long as it complies to Verilog naming rules.
The equivalent diagram of that code is something like this:
Since the example module has now ports, it can now receive an input and provide an output. The next thing we need to do is to add the functionality. To do that, I will be using Verilog bitwise invert operator ~ . I will also use Verilog assign keyword to assign value to the output port. The corresponding code is as follows:
The equivalent diagram of the code above is shown below:
Step3: Simulate
The next step is to simulate the design. Simulation means we will create a test bench that will provide inputs to the design and also will check the correctness of its output. I have created another module named my_not_gate_tb which is shown below.
Inside this module I will create two internal nets, reg tb_a, and wire tb_b.
The names tb_a and tb_b are user defined, you can give it any name just like the input and output ports. The terms reg and wire are Verilog keywords that specifies the type of the net. The net named tb_a is declared as a reg which means register and tb_b is wire which means it is a wire. You might be wondering, what is the difference between the two, why not declare wire for both of them. The reason for that is wire can only be assigned or connected once. Just like when you are designing a schematic, if you connect wires together, the connection will persist unless you deleted it. In Verilog, it is the same thing, wire connection should only be made once and it will stay throughout the simulation lifetime. You can terminate the simulation, then re-wire things and run again. You cannot re-wire at the middle or when the simulation is running. You will also be flagged with compilation issue when you used the wire keyword incorrectly. Registers on the other hand, are like cash registers, you can put and retrieve values anytime in the simulation lifetime.
I will now instantiate my_not_gate (design) inside the my_not_gate_tb (testbench). The corresponding code is shown below:
The term dut is called instance name, it is user defined so you can name it based on your preference but I chose dut to indicate design under test. The equivalent diagram of the code above is as follows:
Now we, will connect tb_a to a and tb_b to b. The code is as follows:
The equivalent diagram is as follows:
Now, that we successfully instantiated and connected our design to the test bench we can now create our test sequence. The test sequence will contain the code for driving the inputs to exercise the design. We will be using Verilog initial block, the initial block works very similar to typical programming like C, in which all the code inside it will be executed line by line. The example code is shown below:
The example code above initially sets tb_a to be zero then waits for 1 time unit and checks if tb_b is 1. It also checks the condition with tb_a is 1. The time unit is define on line 1, this means #1 means 1 ns.
Another thing that we can add in out test bench is the waveform dumping. Our example code is very simple and does not involved a lot of logic so it is easy to debug. However, if we have complicated design or test bench, dumping waves is valuable to analyze overall design and root cause any encountered issue. To to this, we can add the another initial block with waveform dumping commands as follows:
The $dumpfile specifies the filename of the output waveform file. The file extension is .vcd (value change dump). The $dumpvars specifies what variables to dump. In this example, it is all variables of my_not_gate_tb including the variables of the modules instantiated inside it.
Now, that we have a complete test bench, we can now run it. I'm using a Windows PC, so the instruction to run will be based on that. If you have other OS, it may have some difference.
Open the start.bat file inside the oss-cad-suite folder
You should see something like this:
Go to the folder where you created your .v files by using the cd <location> command.
Then do the
iverilog command like this (don't forget to hit Enter). The iverilog command compiles the verilog files and outputs an executable file named
a.out. The a.out file can be run using the
vvp command.
You can use vvp command like this. The vvp command will run the simulation.
As you can see, we did not see any "Test failed" printed in the terminal, which means our design is behaving as expected. One thing you can try out to ensure that your test bench is not giving you false pass, you can try deliberately making a mistake in your design, you can remove the ~ in the my_not_gate module and rerun the iverilog and vvp command. This time you should get the test failed print. Do not forget to revert the design to the correct version if you made changes for testing for false pass.
Apart from the message prints in the terminal we can also use the waveform viewer gtkwave, which is also a part of the oss-cad-suite, to open it, you can do the following command:
It will open a GUI like this:
On the left side, click on my_not_gate_tb and you will see the signals inside that module on the side bar of the GUI like the one below. It shows the signal name and the corresponding type (reg or wire).
Double click on the name to plot the waveform of the signal. It will show the value of signal across simulation time. In our example, we set tb_a to 0 at time 0ns then after 1 ns we changed it to 1, then finally after another 1 ns we called the $finish to end the simulation. The tb_b which is a test bench wire that is directly connected to the output port b of dut can also be plotted in the waveform and you should see that it is just the inverse of the input tb_a. The waveform produced by the simulation should be like this:
In the waveform above, we just plotted the test bench signals, in some cases it is also helpful when debugging to view the signals of the design itself . We can do that by, clicking and expanding the my_not_gate_tb on the left side bar. You will see that there will be an item named dut under it. You will also see in the sidebar the signals of the dut named a and b. You can plot it on the waveform just like you did for the test bench signals. You should get something like this:
Step4: Synthesize
In this step we will use
yosys to synthesize our design. First close the GTK Wave GUI. You should see something like this:
Enter the following command in the terminal:
yosys -p "read_verilog my_not_gate.v; synth_gowin -json my_not_gate.json"
It should be completed very quickly and you should have logs displayed in terminal like this:
The yosys command is easy to understand. The command read_verilog reads the Verilog file supplied as argument, in our case my_not_gate.v. It is worth noting that we only need to synthesize design files and not test bench files. Test bench files is only for simulation purpose to verify your design's correctness with respect to its specification. The synth_gowin , is the specific command to use to synthesize for Gowin-based FPGAs. The -json , indicates the output file will be in json format and will take the name we supplied after it, in our case my_not_gate.json.
You can open the my_not_gate.json if you see what it looks like but I will not go into the details of the file itself because it is not very easy to understand for beginners. What is important is you are able to synthesize the design and generated the .json file without any errors.
Step5: Place and Route
In this step we will be mapping the results of the synthesis into the physical device and therefore we will set configurations specific to Tang nano 9k FPGA board.
First let us create a constraint file. We can create my_not_gate.cst. Type the following lines on the file and save it. It should look like this:
The
IO_LOC maps the specified port to the specific pin number in the FPGA board. In our example, the port
b of our design is mapped into pin 10 of FPGA which is connected to an onboard LED. The port
a is connected to pin 3 which is connected to the on-board push button. FPGA constraint files are device specific and it should be provided by the manufacturer of the FPGA board. For Tang Nano 9k, you can get it on the Sipeed website.
We will use the nextpnr-himbaechel tool to do the place and route. To do this, in your terminal, you can type the following command: nextpnr-himbaechel --json my_not_gate.json --write pnr.json --device GW1NR-LV9QN88PC6/I5 --vopt family=GW1N-9C --vopt cst=my_not_gate.cst
You should see similar output like this:
The breakdown of the command is as follows:
- nextpnr-himbaechel is the tool you are calling to do PnR
- --json my_not_gate.json specifies the input .json file (output from synthesis)
- --write pnr.json specifies the output json file of the nextpnr-himbaechel
- --device GW1NR-LV9QN88PC6/I5 specifies the target FPGA chip
- --vopt family=GW1N-9C specifies the family of the target FPGA chip
- --vopt cst=my_not_gate.cst specifies the constraint file
Step6: Bit file Generation
In this step, we will get the output of the PnR and build a bit file. To do this, you should enter the following command in your terminal: gowin_pack -d GW1N-9C -o pack.fs pnr.json
The breakdown of the command is as follows:
- gowin_pack is the tool for building bit file
- -d GW1N-9C is the FPGA family
- -o pack.fs specifies the output .fs file (the bit file)
- pnr.json is the input .json file (output of PnR stage)
Once you entered the command you should not see any errors, and should have a file named pack.fs.
If you open that file, you will see only 1s and 0s, that is why it is called bit file. It should be something like this:
Step7: Download Bit file to FPGA
Now that you have generated the bit file the remaining step is to download the bit file to your board. To do this, plug in you FPGA board to you computer, then type the following command: openFPGALoader -b tangnano9k pack.fs
The breakdown of the command is as follows:
- openFPGALoader is the tool for downloading the bit file
- -b tangnano9k specifies the type of FPGA board
- pack.fs is the bit file to be downloaded
Once you executed the command, you should see something like this:
As you can see in the terminal, the bit file was loaded on SRAM which is a volatile memory. This means that whenever you remove power such such unplugging the board from the USB port, the configuration data will be lost and you need to reconfigure again. If you want your configuration to be persistent, you have to download it into the flash memory which is a non-volatile memory. You can do that by simply adding -f in the command similar to the snapshot below.
Step8: Validate your Design
Check that your design is properly implemented in the physical board. Sometimes tools do not show errors but it does not guarantee design will work perfectly in physical word. This is because even simulation is tying to model the physical world, it cannot be modelled 100%, so you should do your due diligence and validate the design.
Shown below is a simplified schematic diagram for the pushbutton and LED found in Tang nano 9k board. The diagram below is not 100% the same with official schematic but functionally equivalent (you can download the official schematic on Sipeed Tang Nano 9k official website). If we take a look on this, in order of the LED to turn on, PIN 10 should be driven by logic 0. Meanwhile, for the pushbutton, the normal state (not pressed) is logic 1 because PIN 3 is pulled up.
Going back to our NOT gate design, this means that if we pressed the button, we are setting PIN 3 which is mapped to port a to logic 0. This will drive PIN 10 to logic 1 (because b = ~a), this means that LED will be OFF. If we do NOT press the button, we are setting port a to logic 1 and therefore should expect LED will be ON.
Shown in the photos below are the the actual results.
Input is 1 (pushbutton is NOT pressed) Output is 0 (LED on)
Input is 0 (pushbutton is pressed) Output is 1 (LED off)
Things you can try
For me, I learn more if I try things out so if you reached up to this point of the tutorial, you probably read and understand the basic concepts or maybe you just scrolled down 😂. Kidding aside, you can try implementing the other logic gates.
This are some useful information if you want to try that exercise.
Verilog Bit-wise Operators
- & is AND
- | is OR
- ^ is XOR
- ~^ is XNOR
Constraint File
Pushbuttons are connected at pins 3 and 4. LEDs are connected at pins 10, 11, 13, 14, 15, 16. I specified all of the 6 LED locations you can play around with it. You can turn everything on at the same time, use them to show the state on the input (whether push button is pressed or not) and many more.
Thank you to all the contributors of the open source tools in the OSS-CAD-Suite. Your contributions towards democratizing fpga or chip design is very valuable.