Circuit simulation with hardware description languages
Test Run
Designing field-programmable gate arrays is only half the job: The hardest part is the simulation, but Linux is the best place to tackle certain challenges.
In the previous issue of Linux Magazine, I introduced the digital integrated circuits called field-programmable gate arrays (FPGAs) [1], explained why they are important, and described the bare minimum needed to set up and try a state-of-the-art FPGA design environment on Linux. This article, which is divided into three parts, concludes that introduction.
First I'll describe the main types of elementary digital circuits that can be built, combining the logic gates covered in the first tutorial and the basic features of hardware description languages (HDLs) that allow their implementation and simulation.
The central part combines some HDL code that's freely available online [2] [3] into one simple HDL module and the code necessary to simulate it. The final part shows a run of that simulation and concludes by explaining why Linux is the perfect platform for working with FPGAs.
Representing Digital Circuits with HDL
FPGAs (and every other digital integrated circuit) comprise millions of logic gates, with boolean input and output signals – that is, having one of two states, conventionally represented as 1 or 0.
Sequential components are combinations of logic gates that sample all their input ports, and (with constant, predefined delays) update their outputs on the positive or negative fronts of one or more signals called clocks (i.e., square waves of a fixed frequency that constitute common timing references for the chip).
The simplest sequential block is a memory element, also called a flip-flop [4], that can load and preserve binary values: Its output changes only on the fronts of a control signal, almost always a clock, copying and then preserving whatever value the input pin has in those instants.
You can think of combinational logic circuits as the exact opposite of sequential circuits, because they use no clock signal and have no memory. Their output never depends on a previous value but changes immediately every time one of the inputs changes. In practice, there will always be some delay, which will depend on the complexity of the wiring.
Synchronous circuits are what you get by attaching the outputs of a combinational circuit to the inputs of a memory element. The inputs of the combinational circuit are generic signals combined in a feedback loop with the outputs of that same memory element. The simplest synchronous circuit possible is probably the toggle [4] (Figure 1). The triangle represents a combinational inverter, whose output is always the opposite of its input: When Q is 1, D is 0, and vice versa. Q, however, is only updated when the clock (clk) signal has a positive transition and thus has the behavior shown in Figure 2.
Of course, synchronous circuits can be much more complicated than a toggle. Theoretically, a synchronous circuit can have any number of memory elements and input signals in addition to the clock. Consequently, the output of a synchronous circuit will be multibit binary buses, not single wires, and the combinational part could be a very complex boolean function of all the inputs and memory elements together.
The presence of a clock signal that makes the outputs change only at predefined instances makes synchronous circuits very reliable, but with a significant constraint: They behave as designed only if the inputs of all their memory elements are always stable when a clock transition comes to sample them. Because those signals come out of the combinational part of the circuit, that part cannot have so many levels of logic gates that signal propagation through it would take more than one clock cycle.
Aside from memory banks, the most common type of synchronous circuit is the finite-state machine (FSM), which is an "abstract machine that can be in exactly one of a finite number of states at any given time" [5]. In practice, the state of an FSM at any clock cycle can depend on the whole sequence of transitions it experienced in the previous N clock cycles, where N depends on how many states and input signals the FSM has and how complex its combinational part is. Because that "state" could represent pretty much anything, from a plain counter to the phases of a real-time cryptographic algorithm, FSMs are the most common component of many HDL designs.
Making It in HDL
To facilitate design and reuse, digital circuits are partitioned in hardware modules that can contain submodules; however, as far as the rest of the circuit is concerned, it is basically a black box.
Digital designers create models of their module with HDL languages such as VHDL or Verilog. I focus on Verilog in this article, but almost everything you learn here applies to VHDL as well.
HDLs add two classes of features to those present in software programming languages. One class comprises all the operators and constructs needed to describe the physical characteristics of the logic gates, such as the capability to change only on clock transitions. The other class includes everything needed to simulate and analyze the real-time behavior of a circuit model.
HDLs let designers describe blocks that will always operate and be simulated in parallel with all the other blocks of the design. If you follow the rules, the behavior of every block will always be the same regardless of the order in which all the other blocks are instantiated. The other main features are the data types and syntax constructs that support modeling the circuit types.
Reg vs. Wires
The most important of HDL language data types are wires and regs (registers). Both wires and regs can store multiple bits.
Wires correspond to logical "nets" (i.e., connections between circuit elements). Although wires can represent actual direct connections between two or more modules, in those cases, you cannot assign values to a wire (except by forcing it, for debug purposes), because it's only driven by modules. You may, however, define a local wire whose value is an instantaneous Boolean function of some other signal. In all cases, when the moment comes to create the circuit, wires always translate to combinational logic.
Unlike wires, regs are a Verilog data type that can store values. The values must be explicitly calculated in the HDL code. Depending on how you write that code, regs can become either combinational or sequential logic.
HDL code that explicitly declares every reg variable that must exist in the target silicon and explicitly calculates its values is called Register Transfer Level (RTL) code. Well-written RTL code might take more time to write than higher level code, but you will have a much higher probability of generating a physical circuit that behaves exactly like its HDL model without having to declare and connect every single logic gate manually to all the others.
The deliberately dumb code of Listing 1 shows both kinds of data types and the Verilog syntax to handle them at the RTL level.
Listing 1
Sample Verilog Code
01 module sample_counter; 02 input CLK; // Clock signal 03 input RST; // Reset signal, active low 04 output reg[23:0] COUNTER; // 24-bit counter; 05 output wire SOME_OTHER_SIGNAL; // single bit output 06 wire EIGHT_PULSE; 07 08 always @(posedge CLK) 09 begin 10 if (RST == 1;b0) 11 COUNTER <= 24'd0; // set all the bits of COUNTER to 0 12 else 13 COUNTER <= COUNTER + 24'd1; // 24 bit adder 14 end 15 16 assign EIGHT_PULSE = (COUNTER[2:0] == 3'd0) ? 1'b1: 1'b0; 17 18 controller ctl (EIGHT_PULSE, SOME_OTHER_SIGNAL); 19 20 endmodule
In Verilog, modules are enclosed by the module
and endmodule
keywords. Verilog modules always have a unique name and can instantiate other modules. Listing 1 describes a simple module called sample_counter
that contains an instance called ctl
of another module called controller
.
The first five lines after the module
declaration (lines 2-6) list all the input and output ports of the module and their nature: two single-bit inputs, CLK
and RST
; two outputs, COUNTER
and SOME_OTHER_SIGNAL
; and a local single-bit wire, EIGHT_PULSE
. COUNTER
is a 24-bit-wide reg, and SOME_OTHER_SIGNAL
is a single-bit wire. Comments have the same syntax as in C. I recommend you use comments extensively to document what each signal is and how the module works.
Between the declaration of the variables and the instantiation of the ctl
submodule, sample_counter
contains two blocks, or processes, that work concurrently. The always
block starting on line 8 declares that the value of COUNTER
must be updated at every positive edge of the CLK
signal with a non-blocking assignment. As long as RST
is low, COUNTER
will be zero; otherwise, its value will be incremented by 1. It is important to note the syntax of the non-blocking assignment operator (<=
) and that you should always declare the width of numeric constants. If line 11 said 5'd0
, for example, the five least significant bits of COUNTER
would be set to zeroes, but all the other bits might end up with undefined values.
See the "Blocking" box for more on what blocking and non-blocking means in HDL. For now, just take note that because it uses the non-blocking assignment and works on clock edges, the always
block will be synthesized as a synchronous 24-bit counter.
Blocking
Blocking assignments are just like "normal" software assignments and always translate to combinational logic. The name describes the fact that they "block" the simulator, which must execute them serially, one at a time, in the order they appear. The obvious consequence is that, all inputs being equal, the complete output of a series of blocking assignment will depend on their order. In other words, blocking assignments are one of the most common ways for inattentive novice HDL designers to hurt themselves, producing code that mysteriously does not work as they wanted.
Non-blocking assignments are almost the opposite. For example, assume that a simulation has a time resolution of 1ns; that is, it must calculate all the values of all the wires and registers it contains every nanosecond. Non-blocking means that, at the beginning of each nanosecond, the simulator:
- samples the values in that instant of all the variables that appear on the right side of all the non-blocking statements it contains, at every level of the hierarchy, and
- then uses those sampled values (some of which might be feedback loops!) to calculate the next values of all the variables on the left side of all the non-blocking statements.
The ability of HDL simulators not to block themselves when calculating any next value before they have collected all the inputs for all the assignments made with the <=
operator is what emulates the real behavior of real hardware registers. Working in that way, the result of each statement does not depend on the order in which all the non-blocking statements are written.
Line 13 shows the beauty and power of HDL languages: You don't need to know, or write down in detail, how 24-bit binary addition actually works. Just use +
, and the Verilog compiler, synthesizer, and simulator take care of all the implementation and simulation details.
The same considerations apply to the last piece of the sample_counter
: The assign
keyword that controls EIGHT_PULSE
is a continuous blocking assignment, because it uses =
instead of <=
, which runs every time any of the three least significant bits of COUNTER
change (that is what COUNTER[2:0]
means). When all those bits are zero, that is when COUNTER
is
or 8
, 16
, …; then, EIGHT_PULSE
becomes immediately high; otherwise, it is low. Because it uses the assign
keyword and the blocking assignment, this part of the module will be synthesized as combinational logic.
Buy this article as PDF
(incl. VAT)