Describing digital hardware with HDL
Summary
To design the digital hardware, HDL (Hardware Description Language) is used. By using HDL, we can describe digital hardware like a program. Also, by using reconfigurable hardware like FPGA (Field Programmable Gate Array), we can implement the logic described in HDL, without making actual IC chip.
In this article, I’ll show HDL description of Half-adder, Full-adder, and 4-bit adder, and basic HDL syntax to describe combinational logic.
Logic circuit
In previous article, I explained how the binary adder works.
How the computer calculates “a + b”
To make those digital hardware, we need logical operations. In digital circuit, we use the symbols below to represent logical operations.
We call those logic gates. We can make logic circuits by using combinations of logic gates above. For example, half-adder and full-adder were described with equations like below.
- Half-adder
Y = A xor B
Cout = A and B
- Full-adder
Y = A xor B xor Cin
Cout = (A and B) or ((A or B) and Cin)
This logic will be drawn like below.
HDL description of Half-adder and Full-adder
Let’s describe those hardware using HDL. HDL description of Half-adder looks like below.
module half_adder(
input a,
input b,
output y,
output cout
);
assign cout = a & b; // Carry over
assign y = a ^ b;
endmodule
First, we need to define “modules” using keyword “module”. In parentheses, the interface of this module will be defined. In this case, there are two inputs a and b, two outputs y and cout are declared. Then, the logic is described by “assign” keyword. We can make combinational logic by using this, assign keyword. Logical operators are equivalent to C language. Comments can be written after double slashes.
Next example is HDL description of Full-adder
module full_adder(
input a,
input b,
input cin,
output y,
output cout
);
wire xor_ab; assign xor_ab = a ^ b;
assign y = xor_ab ^ cin;
assign cout = (xor_ab & cin) | (a & b);
endmodule
This is a bit more complicated, but basically, the description is almost the same. All the inputs (a, b, and cin) and outputs (y, and cout) signals are defined in parentheses after module name, and logic is defined after “assign” keyword. Only one difference is, there’s a “wire” keyword. By using this keyword, we can define internal signal connection. Also, in this example, I’m using “xor” instead of “or” to reduce the number of logic gates to be used.
HDL description of 4-bit adder
We are going to make 4-bit adder, by instantiating, and connecting Half-adder, and Full-adders. The block diagram of 4-bit adder is shown below.
Instantiation of the module is pretty similar to variable declaration. It’s shown below.
<module name> <instanse name>(<connections>);
It looks quite similar to instantiation of the class/structure variables. One important difference is, in parentheses, we define port connections instead of initialize-list. Here’s the example of 4-bit adder HDL description.
module adder (
input [3:0] a,
input [3:0] b,
output [3:0] y
);
wire carry[3:0]; half_adder U_0(
.a (a[0]),
.b (b[0]),
.y (y[0]),
.cout (carry[0])); full_adder U_1(
.a (a[1]),
.b (b[1]),
.cin (carry[0]),
.cout (carry[1]),
.y (y[1])); full_adder U_2(
.a (a[2]),
.b (b[2]),
.cin (carry[1]),
.cout (carry[2]),
.y (y[2])); full_adder U_3(
.a (a[3]),
.b (b[3]),
.cin (carry[2]),
.cout (carry[3]),
.y (y[3]));
assign y[4] = carry[3];
endmodule
Because this is 4-bit adder, we need 4-bit inputs of a and b. To describe multi-bit signals, we use syntax like below.
wire carry[3:0];
It looks similar to array in programming, but the meaning is different. This is defining 4-bit wide signal line. Also, a[2] means 3rd bit of the 4-bit signal a (the index starts from 0). So basically, what the above HDL does is, instantiating 4 modules (1 half-adder, and 3 full-adders), and connecting each other.
Better description
You might have realized the description above is not ideal. Because, imagine, if you want to make 8-bit adder, you have to re-write all the indices, and you have to instantiate and connect 4 more full-adders… If it’s 8-bit, it might be okay, but you don’t want to make 64-bit adder in this way.
To solve this problem, we can use parameters and generate block. Here’s an example.
module adder #(parameter integer C_WIDTH = 32)
(
input [C_WIDTH-1:0]a,
input [C_WIDTH-1:0]b,
output [C_WIDTH:0]y
);
wire carry[C_WIDTH-1:0]; half_adder U_0(
.a (a[0]),
.b (b[0]),
.y (y[0]),
.cout (carry[0])); generate
genvar i;
for(i = 1; i < C_WIDTH; i = i + 1) begin
full_adder U_i(
.a (a[i]),
.b (b[i]),
.cin (carry[i - 1]),
.cout (carry[i]),
.y (y[i]));
end
endgenerate
assign y[C_WIDTH] = carry[C_WIDTH-1];
endmodule
We can add parameters using parameter keyword in another parentheses after #, followed by port connection list. In this case, 32 for C_WIDTH is default parameter. It can be changed when this module is instantiated in other modules. Also note that, there’s a for-loop in a code, but this doesn’t mean the loop. You can think this for-loop as pre-processor command. It repeatedly instantiates full_adder modules, and therefore, it’ll be expanded to the example in previous paragraph before the actual synthesis.
That’s it for this chapter. I’ll explain how to verify the HDL using test bench.