Digital Hardware Design : Understanding Combinational Circuits.

Digital Hardware Design : Understanding Combinational Circuits.

A fast ramp up on digital system design. Blog 2 of many.

The first time I looked at some RTL, I saw a fair amount of programming constructs—quite similar to the if and switch statements familiar to most programmers. As a software engineer, you can pretty much use the two statements interchangeably. However, there is some difference when it comes to hardware.

In this blog, I try to break down these programming constructs into basic combinational circuits using Yosys, an open-source tool for synthesis.

Always block for Combinational Circuits

In my last blog I mentioned that HDLs usually follow a concurrent hardware model and that is true but you can achieve procedural execution in Verilog using something called as the "always" block.

always @([senstivity_list])
begin
// procedural statements
end

The procedural statements inside the always block get executed when one of the signals in the senstivity list changes value. Here's an example of a block that generates a pulse width modulated signal.

module pwm_gen(
    input [7:0]Dutycycle,
    input clk, 
    output reg [7:0] counter, 
    output reg pwmout
);

always @(posedge clk) begin
    if(counter < 99)
        counter <= counter + 1;
    else 
        counter <= 0;
end

always @(posedge clk) begin
    if(counter < Dutycycle) 
        pwmout <= 1'b1;
    else if (counter == 99) 
        pwmout <= 1'b0;
    else
        pwmout <= 1'b0;
end         
endmodule

You can see that the senstivity list has the positive edge of the clock signal so the always block executes everytime there is a positive edge on the clock and we see the counter signal increase.

Logic Synthesis with Yosys

Once you have a verilog file, you can use Yosys to visualize the design. Yosys is an open source logic synthesis tool starting from RTL netlist and finally to gates in the target technology.

While constructs such as if, switch in a language like C are executed sequentially, these constructs in verilog are realised using combinational circuits which have no sequential control and are therefore realised using routing networks. All the expressions in a routing network are evaluated concurrently and the desired result is routed to the output. We'll use yosys to understand the routing structure of common programming constructs.

# installing yosys
sudo apt-get install yosys
# yosys interactive mode
yosys
# read input file
readverilog filename.v
# convert to DFFs and muxes
proc
# perform some optimization
opt
# visualize the design
show

You can also create a script for Yosys by pasting all the commands in a .ys file.

yosys synth.ys

Result of yosys script for the pwm_gen module gives something like this.

More information about Yosys can be found here.

If Statement

The previous example of a PWM generator uses an if statement in the always block. What does that translate to on the hardware? Let's find out using Yosys. To that end let us take the simple example of a n-to-2^n decoder with n = 2.

Truth table of 2-to-4 decoder with enable is as follows :

ena[1]a[0]y[3:0]
0--0000
1000001
1010010
1100100
1111000
module decoder (
    input wire [1:0] a,
    output reg [3:0] y,
    input wire en
);

always @ * begin
    if (en == 1'b0)
        y = 4'b0000;
    else if (a == 2'b00)
        y = 4'b0001;
    else if (a == 2'b01)
        y = 4'b0010;
    else if (a == 2'b10)
        y = 4'b0100;
    else
        y = 4'b1000;
end

endmodule

Routing Structure

The if-else statement implies a priority routing network in verilog. It is implemented by a sequence of 2-to-1 multiplexers as showing in the figure below.

The four 2-to-1 multiplexors form the priority routing network and other components represent the boolean/arithmetic expressions apart from the if-else ladder. The number of muxes increases propotionally to the number of if statements and a large number of if-else statements can cause a significant propogation delay.

Case Statement

module decode_case (
    input wire [1:0] a,
    output reg [3:0] y,
    input wire en
);

always @ * begin
    case ({en, a})
    3'b000, 3'b001, 3'b010, 3'b011: y = 4'b0000;
    3'b100: y = 4'b0001;
    3'b101: y = 4'b0010;
    3'b110: y = 4'b0100;
    3'b111: y = 4'b1000;
    endcase

end

endmodule

Routing Structure

The case statement implies a multiplexing network and you can see how implementation of the same decoder results in two different circuits based on the programming construct used.

The pmux in the figure below represents a priority mux which can be thought of as a series of cascaded multiplexors used in the if statement or as a single n-to-1 multiplexer. Each value of the case expression can be mapped to one input of an n-to-1 multiplexer. Here, the number of inputs in the multiplexer would increase with the number of case expressions.

As usual the code and other files are in this github repo.

Parting thoughts

We checked out two routing structures in this blog and both implementations have tradeoffs. Priority routing would be benficial when there is some kind of weightage to the cases. Multiplexing networks would shine where the implementation is more truth-table oriented.

Let me know what you guys thought of this one. Feedback is always appreciated.