Tutorial: build an ALU
Write a small arithmetic unit and its test from a specification, starting from the Blank starter.
On this page
ObjectiveBefore you startThe specification1. Create the project2. Write the design3. Write the test4. Simulate5. Break it on purpose6. Synthesize7. Build the chip8. Update the specificationHow to tell it workedCommon problemsWhere to go from hereObjective#
Design an 8-bit arithmetic and logic unit, write a test that checks it against its specification, and synthesize it. The code on this page was run with the lab's tool versions; the expected results below are what it produced.
Before you start#
- You have done at least the counter tutorial.
- You know how to edit a file in the Workspace. See Editor and terminal.
The specification#
| Port | Direction | Width | Meaning |
|---|---|---|---|
clk | input | 1 | Clock, rising edge |
rst_n | input | 1 | Reset, active low, asynchronous |
op | input | 2 | 00 add, 01 subtract, 10 AND, 11 XOR |
a, b | input | 8 | Operands, unsigned |
result | output | 8 | The result |
carry | output | 1 | Carry out of an add, borrow out of a subtract, otherwise 0 |
zero | output | 1 | High when result is 0 |
- Latency: one clock.
resultandcarryappear one clock aftera,bandopare sampled. - Reset:
resultandcarryare 0. - Overflow:
resultwraps to 8 bits. The ninth bit is reported incarry.
1. Create the project#
nelix new alu --template blank
cd ~/project/alu2. Write the design#
Replace the contents of rtl/top.sv with this.
`default_nettype none
// 8-bit ALU with a registered result.
// op: 00 add, 01 subtract, 10 AND, 11 XOR.
// result, carry and zero appear one clock after a, b and op are sampled.
module top (
input logic clk,
input logic rst_n, // asynchronous, active low
input logic [1:0] op,
input logic [7:0] a,
input logic [7:0] b,
output logic [7:0] result,
output logic carry, // carry out of add, borrow out of subtract
output logic zero // result is 0
);
logic [8:0] next; // one extra bit holds the carry or borrow
always_comb begin
next = 9'd0; // a default first, so no latch is inferred
case (op)
2'b00: next = {1'b0, a} + {1'b0, b};
2'b01: next = {1'b0, a} - {1'b0, b};
2'b10: next = {1'b0, a & b};
2'b11: next = {1'b0, a ^ b};
default: next = 9'd0;
endcase
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
result <= 8'd0;
carry <= 1'b0;
end else begin
result <= next[7:0];
carry <= next[8];
end
end
assign zero = (result == 8'd0);
endmodule
`default_nettype wireThree things to notice:
nextis 9 bits, one more than the operands, so the carry is not lost.- The
always_combblock givesnexta default before thecase, so no latch can be inferred. zerois worked out from the registeredresult, so it always describes what is on the output.
3. Write the test#
Replace the contents of tb/test_top.py with this.
import random
import cocotb
from cocotb.clock import Clock
from cocotb.triggers import FallingEdge
ADD, SUB, AND, XOR = 0, 1, 2, 3
NAMES = {ADD: "add", SUB: "subtract", AND: "and", XOR: "xor"}
def expected(op, a, b):
"""The specification, in Python: (result, carry)."""
if op == ADD:
total = a + b
return total & 0xFF, total >> 8
if op == SUB:
return (a - b) & 0xFF, 1 if a < b else 0
if op == AND:
return a & b, 0
return a ^ b, 0
@cocotb.test()
async def test_alu(dut):
"""Every operation, at the edges and with random values."""
cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start())
dut.op.value, dut.a.value, dut.b.value = 0, 0, 0
dut.rst_n.value = 0
await FallingEdge(dut.clk)
await FallingEdge(dut.clk)
assert int(dut.result.value) == 0, "result should be 0 in reset"
assert int(dut.zero.value) == 1, "zero should be 1 when result is 0"
dut.rst_n.value = 1
random.seed(7)
cases = [(op, a, b) for op in (ADD, SUB, AND, XOR)
for a, b in ((0, 0), (255, 1), (0, 1), (255, 255), (128, 128))]
cases += [(random.randrange(4), random.randrange(256), random.randrange(256)) for _ in range(40)]
for op, a, b in cases:
dut.op.value, dut.a.value, dut.b.value = op, a, b
await FallingEdge(dut.clk)
want, want_carry = expected(op, a, b)
got, got_carry = int(dut.result.value), int(dut.carry.value)
assert got == want, f"{NAMES[op]} {a}, {b}: expected {want}, saw {got}"
assert got_carry == want_carry, f"{NAMES[op]} {a}, {b}: expected carry {want_carry}, saw {got_carry}"
assert int(dut.zero.value) == (1 if want == 0 else 0), f"{NAMES[op]} {a}, {b}: zero flag is wrong"
dut._log.info("All tests passed")The function expected is the specification written in Python. It never looks at the design's code. The test tries every operation at the edges, where mistakes hide: zero, the largest value, a subtraction that borrows. Then it tries 40 random cases, with a fixed seed so a failure repeats.
4. Simulate#
nelix testExpected:
test_top.test_alu passed
** TESTS=1 PASS=1 FAIL=0 SKIP=0 **5. Break it on purpose#
Prove to yourself that the test can catch a bug. In rtl/top.sv, change the subtract line to add:
2'b01: next = {1'b0, a} + {1'b0, b};Run nelix test.
Expected: it fails, and the message says exactly where.
AssertionError: subtract 255, 1: expected 254, saw 0
** TESTS=1 PASS=0 FAIL=1 SKIP=0 **Put the - back and run the test again.
6. Synthesize#
nelix synthExpected: the log ends with
Number of cells: 207
$_AND_ 80
$_DFF_PN0_ 9
$_MUX_ 9
$_NOT_ 17
$_OR_ 62
$_XOR_ 30Nine flip-flops: eight for result and one for carry. The multiplexers choose between the four operations. The log also contains the line No latch inferred, which is what the default in always_comb bought you.
7. Build the chip#
nelix buildThe Blank starter's settings give a 100 by 100 micrometre die and a 10 ns clock. Read the result on the build card. This step was not run for this page, so there is no expected output to compare with; your build's own report is the answer.
8. Update the specification#
Replace docs/DESIGN.md with the specification above, under the headings the starters use. See Project specifications.
How to tell it worked#
- The test passes, and fails when you break the design.
- Synthesis reports nine flip-flops and no latch.
Common problems#
| Problem | Cause |
|---|---|
syntax error on the first line of the module | The file was pasted with something missing. Check the first and last lines |
| The test fails on every case | The design or the test was only partly replaced |
Latch inferred in the synthesis log | The default next = 9'd0; at the top of always_comb was removed |
Where to go from here#
- Add a fifth operation, such as OR. You will need a wider
op. Change the specification first, then the test, then the design. - Make the operands signed and add an overflow flag. See Bit widths, signed arithmetic and overflow.
Verified. Simulation and synthesis run with Icarus Verilog 12.0, cocotb 2.1.0 and Yosys 0.33 on 5 October 2026. The build step was not run.
Something unclear or missing? Send feedbackSign in and use Feedback to tell us.