Describing digital hardware with HDL

Yuhei Horibe
5 min readMay 25, 2020

--

Simulation result of multiplier

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.

Logic gates

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.

Half-adder and Full-adder

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.

4-bit binary adder

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.

--

--