Data Buses: From Individual Bits to Bytes

In the previous article we built a FullAdder — a module that adds three bits (a, b, and a carry) and outputs one sum bit and one carry bit.

The 8-bit Verilog adder in the RTL viewer: a chain of eight full adders
The 8-bit adder in the RTL viewer

But let's imagine: we have two 8-bit numbers to add. Say, 113 (01110001) and 29 (00011101). How do we do it if we only have a single-bit FullAdder?

The obvious solution comes to mind: take 8 Full Adders, connect them in series, feed each one the corresponding bits of the numbers, and feed the carry from the previous one into the next one's input. And that is the right solution.

But how do we write this in Verilog? Surely we don't have to declare 16 separate inputs (a0, a1, ..., a7, b0, b1, ..., b7) and 16 intermediate wires? That's 32 names, 32 lines of declarations, and every wire has to be connected by hand. For 8-bit addition that's already tedious, and for 64-bit — practically impossible.

Fortunately, Verilog has the concept of a bus, or a vector.

What Is a Bus and Why Do You Need It

In a real digital device, data almost never travels over a single wire. When a processor reads an 8-bit value from memory, it needs 8 wires, one per bit. These 8 wires, laid out in parallel, form a data bus.

In Verilog, a bus is declared with a range in square brackets:

input  [7:0] a;   // 8-bit input a (bits 7 down to 0)
input  [7:0] b;   // 8-bit input b
output [7:0] sum; // 8-bit output sum
output       cout; // 1-bit carry output

The notation [7:0] reads as "from 7 to 0 inclusive". The first number (7) is the most significant bit (MSB, Most Significant Bit). The last (0) is the least significant bit (LSB).

How to Access Individual Bus Bits

A vector is essentially an array of wires. You can access any bit by index:

wire [7:0] data;
wire lsb = data[0];  // least significant bit (bit 0)
wire msb = data[7];  // most significant bit (bit 7)

You can select a range of bits (part-select):

wire [3:0] low_nibble  = data[3:0];  // the low 4 bits
wire [3:0] high_nibble = data[7:4];  // the high 4 bits

Building the Ripple Carry Adder

Now we have the tool to build an 8-bit adder. The idea is simple: take 8 FullAdder instances, connect them in a chain, where the carry from each previous one feeds into the next one's input. This design is called a Ripple Carry Adder — "an adder with a rippling carry".

Why "rippling"? Because the carry runs along the chain of adders like a "wave" (ripple): first the least significant bit computes, emits a carry, it runs to the next one, the next one computes with the carry in mind, emits its own carry, and so on all the way to the most significant bit.

Notice: the least significant adder (FA 0) has its cin input connected to 0, because there's nowhere to carry from "bit -1". The most significant one's cout output is the global carry, which signals overflow (the result didn't fit into 8 bits).

Writing the Code: 8x FullAdder by Hand

module RippleCarryAdder8(
    input  [7:0] a,
    input  [7:0] b,
    output [7:0] sum,
    output       cout
);
    wire [7:0] c;

    FullAdder fa0 (.a(a[0]), .b(b[0]), .cin(1'b0), .sum(sum[0]), .cout(c[0]));
    FullAdder fa1 (.a(a[1]), .b(b[1]), .cin(c[0]), .sum(sum[1]), .cout(c[1]));
    FullAdder fa2 (.a(a[2]), .b(b[2]), .cin(c[1]), .sum(sum[2]), .cout(c[2]));
    FullAdder fa3 (.a(a[3]), .b(b[3]), .cin(c[2]), .sum(sum[3]), .cout(c[3]));
    FullAdder fa4 (.a(a[4]), .b(b[4]), .cin(c[3]), .sum(sum[4]), .cout(c[4]));
    FullAdder fa5 (.a(a[5]), .b(b[5]), .cin(c[4]), .sum(sum[5]), .cout(c[5]));
    FullAdder fa6 (.a(a[6]), .b(b[6]), .cin(c[5]), .sum(sum[6]), .cout(c[6]));
    FullAdder fa7 (.a(a[7]), .b(b[7]), .cin(c[6]), .sum(sum[7]), .cout(cout));
endmodule

The code is a bit long, but it's crystal clear: each of the 8 bits is added separately, and the carries are passed along the chain.

Taming Repetition with generate

module RippleCarryAdder8(
    input  [7:0] a,
    input  [7:0] b,
    output [7:0] sum,
    output       cout
);
    wire [8:0] c;
    assign c[0] = 1'b0;

    genvar i;
    generate
        for (i = 0; i < 8; i = i + 1) begin : fa_chain
            FullAdder fa (
                .a(a[i]), .b(b[i]), .cin(c[i]),
                .sum(sum[i]), .cout(c[i+1])
            );
        end
    endgenerate

    assign cout = c[8];
endmodule

generate is a synthesis-time directive that lets you create repeating structures. Imagine a for loop, but not during program execution — during circuit assembly.

How the Carry "Ripples"

Every logic gate has a signal propagation delay. When a[0] and b[0] change, FA0 computes sum[0] and c[1]. For FA1 to start computing, it has to wait for c[1]. When FA1 finishes, c[2] runs to FA2... and so on, like a wave.

The delay grows linearly: for an 8-bit adder it's ~48 gate delays, for a 64-bit one — ~384. This is called the critical path. The longer the critical path, the slower the processor can run.

That's exactly why real processors don't use 64-bit Ripple Carry Adders — the delay is too great. Instead, trickier circuits are used: the Carry Lookahead Adder (an adder with accelerated carry).

Another Example: Bus Concatenation

wire [7:0] a = 8'b10101010;
wire [15:0] word = {a, b}; // 16-bit word

// Concatenation: assembling a bus from individual bits
wire [7:0] d = {a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7]};

Concatenation {...} is a powerful tool. It works like "assembling" a bus from pieces.

Summary

1. Buses (vectors) are declared with a range [7:0] and let you work with a group of wires as a single whole.

2. Individual bus bits can be accessed by index: a[3], a[0].

3. Ripple Carry Adder is a chain of FullAdders, where the carry of each previous adder feeds into the next one's input.

4. For repeating structures, generate is used — a "loop during synthesis".

5. Signal propagation delay (ripple delay) is the main problem of sequential carry.

In level 3.35 you will build an 8-bit Ripple Carry Adder, using 8 FullAdder instances (which you built in level 3.34) and bus routing from two 8-bit inputs to an 8-bit output. Try both ways: manual and with generate.

Try it in the simulator →