Using HDL the Right Way
For digital design, the fastest way to design a circuit is using a hardware description language (HDL). All HDLs have one common flaw, they have constructs and syntax that do NOT describe hardware. This causes fundamental issues for engineers designing ASICs, FPGAs, CPLDs, etc.
To overcome the fundamental problem with HDLs, I propose a few simple steps to allow designers to write code that translates to optimally synthesized logic. The steps are:
1. Use an HDL to describe the hardware.
The key word in “hardware description language” is description. HDLs should be used to describe a digital circuit. Unfortunately engineers often use an HDL to create a circuit that were it not for the synthesizer they would have no idea how to design it. This almost always results in a sub-optimal design. If you don’t know how to make a circuit, why should the synthesizer?
Before writing any HDL code, you should sit down and either make a diagram or have a good mental view of the circuit you are trying to design. Once you have this, you can use the HDL to syntactically describe the circuit.
2. Use only HDL syntax that can directly synthesize to logic.
As mentioned earlier, HDLs contain syntax that doesn’t describe digital hardware. Do not use these constructs. Only use blatant hardware-based assignments and operators. This will allow your synthesized design to follow closely to what is found in the HDL code.
Books like “The Designer’s Guide to VHDL” actually do the designer a disservice. 99% of this book talks about unsynthesizable code while the last 1% is useful synthesizable code. Digital logic is very easy. It doesn’t require many types of syntax. Our digital design world would flow much better if HDLs were only designed to describe hardware. The unfortunate fact that many HDLs have testbench-like syntax causes less-knowledgable designers to use these constructs out of ignorance. This will undoubtedly burn them at some point in their career.
3. Use a netlist viewer.
After designing a digital circuit using an HDL, synthesize it and use an RTL netlist viewer to verify that the synthesized design contains the proper logic. This is critical! Often times the synthesizer will not properly infer logic blocks. Using a netlist viewer will allow you to double-check the synthesized results. This will also help you find bugs in your code that may not have been syntax bugs. Usually when a bug makes it through the synthesis stage, the result will be quite different from what you expected. Using an RTL netlist viewer creates one more process step, but it will reduce your development time because you’ll find and correct problems in earlier stages.
For you FPGA and CPLD designers, using a technology netlist viewer will give you yet another verification step. This is very useful when you are using an HDL to describe some device primitive such as block RAMs, clock muxes, tristate drivers, dual-data registers, digital signal processing (DSP) blocks, and many more.
4. Do not use your hardware HDL for your testbench HDL.
This is a commonly debated topic. I don’t think you absolutely have to follow my advice to produce good hardware, but I definitely think it makes it easier. I believe that there is a fundamental problem with HDLs in that they attempt to satisfy the syntactical needs of hardware and testbenches. This would be better off split into two languages. Using the same language causes issues because if you made a logic mistake in your hardware, what makes you think you wouldn’t make the same or inverse mistake in your testbench?
For myself I have adopted a pretty simple strategy. I write all my hardware code in VHDL or Verilog. I only use basic hardware-like constructs and avoid any use of complex functions that have no simple hardware explanation. For testing, I use SystemVerilog. SystemVerilog provides a very cool interface between hardware and computer-language-like programmability. The typical problem with creating testbenches is that you feel like you are creating another hardware suite. Using SystemVerilog I create drivers which send and receive object-oriented data structures to and from my top-level hardware design. These drivers have a hardware side that is attached to my hardware design. They also have a programmable side which is attached to my testing logic.
Here is an example. If my hardware design was an IP packet router, I would create a SystemVerilog class that represents an IP packet. I can use computer-language-like programming to create and monitor the status of these packets. From this programmable side, I send all the created packets to the driver. The driver takes the data and communicates with my hardware unit over the physical protocol defined by the hardware. I would also have a driver for receiving packets. After all is said and done, I can use typical C++ like programming to verify proper IP routing of my hardware device. Simple, right?
Examples of what NOT to do:
It is common in communication systems to send a known pattern of bits at the beginning of each frame so that the receiving side can synchronize itself to the bit stream. For communication systems, you often need to be tolerant of a few bits errors. To search for the sync bits, you just need to XOR the last received bits with the known sync pattern then count the number of ones, which is the number of errors. For counting the post-XOR ones, I often see a VHDL function declared like this:
function count_ones (a : std_logic_vector) return unsigned is variable b : unsigned(log(a'length) downto 0) := (others => '0'); begin for i in a'range loop if (a(i) = '1') then b := b + 1; end if; end loop; return b; end;
This may look harmless, but try to think of what kind of hardware it will make. All the synthesizers I’ve tried this on make an a’length series sequence of b’length adders. Obviously this produces absolutely horrible timing results. There are better ways to count ones. Don’t get stuck with a sequences of adders.
Back when I was a digital design rookie, I was trying to figure out how to take a binary number and produce a sequence of BCD values. For example 10100010(162) would convert to 0001(1), 0110(6), 0010(2). I found a commonly known algorithm for this. The 8-bit algorithm is:
1. If any column (100’s, 10’s, 1’s, etc.) is 5 or greater, add 3 to that column.
2. Shift all #’s to the left 1 position.
3. If 8 shifts have been performed, it’s done! Evaluate each column for the BCD values.
4. Go to step 1.
I then attempted to translate this into hardware. I wanted a completely combinational implementation for single clock latency. This is what I naively produced:
module bcd ( input [7:0] binary, output reg [3:0] hundreds, output reg [3:0] tens, output reg [3:0] ones); integer i; always @(binary) begin // set 100's, 10's, and 1's to zero hundreds = 4'd0; tens = 4'd0; ones = 4'd0; // loop 8 times for (i=7; i>=0; i=i-1) begin // add 3 to columns >= 5 if (hundreds >= 5) hundreds = hundreds + 3; if (tens >= 5) tens = tens + 3; if (ones >= 5) ones = ones + 3; // shift left one hundreds = hundreds << 1; hundreds = tens; tens = tens << 1; tens = ones; ones = ones << 1; ones = binary[i]; end end endmodule
Yes, I know, there numerous issues in this code. Can anyone look at this code and figure out what it will make? I can’t! Even though this code properly produces the BCD sequence, it produces a very large combinational path. Don’t use it!