Skip to content

Hardware and operating systems · part 1 of 3

ALU and registers: adding, comparing and holding operands

How the ALU adds, subtracts, compares and does logic, what the Z, N, C and V flags mean, and what registers are for inside the CPU.

intermediatePublished on · 34 min read

Font size:
Column width:
Tone:
Line spacing:
Typeface:

ALU and registers: adding, comparing and holding operands

Think of a cashier at an old-fashioned shop. There is a calculator in front of them that does the sums, and a drawer beside it with the slips of paper holding the numbers they are using right now. The cashier does not walk to the storeroom for the next number, because the drawer is one arm movement away. In the CPU, the calculator is the ALU and the drawer is the registers. The analogy has one clear limit: the cashier’s calculator does not decide what to do with the result. The instruction does that, and it only shows up in the next lessons of this phase.

This lesson answers one question: how does the ALU add, compare and do logic operations, and what are registers for? It starts from two things already settled. The first is the full adder and the 1-bit adder-subtractor, covered in Combinational circuits. The second is flip-flops and load registers, covered in Sequential circuits. We will not rebuild those circuits here. We will use them to assemble an n-bit ALU with flags, a register file and one instruction running end to end. How an instruction is encoded, and how the CPU fetches and decodes it, are left to ISA, CISC and RISC and Instruction cycle and control unit.

Lesson map

SectionQuestion it answers
1. What the ALU is and what a register isWho calculates and who holds?
2. What it is forWhy does the CPU need both, and not just one?
3. Examples in different contextsWhere do these parts show up outside a computer architecture book?
4. How it worksHow does the ALU add, compare, do logic and shift, and how is the register file read and written?
5. Details and edgesWhere do architectures differ and where does the overflow rule stop applying?
6. Common mistakes and good practiceWhich mistakes appear when someone starts reading flags?
7. The program that simulatesHow do the ALU and the register file behave when they run?
8. ExercisesHow do you practice, with answers?
9. GlossaryWhich terms does the lesson use?

1. What the ALU is and what a register is

The ALU, short for arithmetic logic unit, is a combinational circuit. It receives two operands, the numbers being operated on, and a code that says which operation to perform. It returns a result and a set of status bits called flags, which describe the result: whether it came out zero, whether the sign bit is set, whether there was a carry or a borrow, and whether the operation overflowed the signed range. The ALU stores nothing. If you change the operands, the output changes at once, regardless of what was computed before.

A register is the opposite. It is a set of flip-flops sharing one clock, as seen in Sequential circuits, and it holds a whole word, usually the size the CPU processes at once. It only changes on the clock edge, when writing is enabled. The register is the memory closest to the ALU, and the difference in role between the two parts sums up the lesson: the ALU calculates, and the register holds what is about to be calculated and what was just calculated.

The flags sit in between. The ALU computes them, but they are stored in a register of their own, so the next instruction can read the result of the previous one. Section 4.2 explains each flag.

2. What it is for

Most of the time, a CPU does three things with numbers: it adds and subtracts, it compares two values, and it combines bits with masks. The ALU exists so those three tasks are a single block of hardware, with an operation code choosing the path. Without it, each operation would be a separate circuit, and a processor with dozens of arithmetic instructions would have dozens of circuits wired to the same bus. With the ALU, the cost is one adder, a set of logic gates and a selector.

Registers exist for a reason of timing. For the ALU to operate, the operands must be at its input. If every operand came from main memory, the ALU would wait on memory for every calculation. Phase 4 of the track measures that wait with numbers, but the idea is enough here: working on values already within the ALU’s reach is what makes the CPU useful. That is why the architecture separates the two. The ALU sees only registers, and memory enters the path only through read and write instructions, as section 4.8 shows.

3. Examples in different contexts

The first example is a shop’s stock control. Each sale subtracts one unit from the balance, and each restock adds one. The calculation is an addition or a subtraction on the same circuit, and only the operation code changes. Inside the CPU, a loop counter and an array address are added and subtracted the same way, often many times in every program.

The second example is the rule “if the balance is below the limit, block the purchase”. The decision depends on a comparison, and a comparison inside the CPU is a subtraction that does not keep the result, only the flags. Section 4.3 shows how that reading works, and why the same pair of bits can give a different answer depending on whether the numbers are treated as signed or unsigned.

The third example is a permissions byte, where each bit switches a right on or off. To ask whether a right is active, the operation is an AND with a mask that has only that right’s bit set. This use of logic gates, with no addition at all, shows up in any code that needs to look at one specific bit, such as reading a configuration register. Section 4.4 does this calculation with 4 bits.

The fourth example is multiplying by powers of two. Shifting a number one place to the left is the same as multiplying it by 2, as long as the bit that falls off is not lost. Shifting is an ALU operation in many instruction sets, and the C flag keeps the bit that fell off, so the program can tell whether the shift lost information.

4. How it works

The figure below splits the ALU into blocks. The operands enter on the left and reach the adder, the logic gates and the shifter at the same time, and each computes its own part. The operation code, in orange, reaches the selector, which chooses which output becomes the result. The flags come out of the adder and from the result itself, and section 4.2 explains each one.

Inside a 4-bit ALU: A and B feed the adder, the logic gates and the shifter, and the operation code picks which output becomes the result
figure 1 · Inside a 4-bit ALU: the operands go through the adder, the bitwise logic and the

Read the figure from left to right. The green path carries the operands, and the orange path carries the code. One reading note: the adder and the shifter appear as separate blocks, but a real ALU may share circuitry between them. The figure shows the function of each part, not the layout.

4.1 Adding and subtracting with the same circuit

Addition with carry and the adder-subtractor were already built in Combinational circuits, and we will not repeat that build. The idea that makes subtraction fit the same circuit is worth recalling: A minus B is A plus the complement of B plus 1, and the complement of B is every bit of B inverted. An XOR with a SUB control line inverts B when SUB is 1, and the same line feeds the adder’s initial carry-in. The ALU in this lesson uses exactly that arrangement.

The figure below shows the calculation 3 minus 5 in 4 bits. The result is 1110, which read as unsigned is 14, and read as signed two’s complement is −2. Notice that the adder’s final carry-out is 0, because nothing went beyond the 4-bit range, yet this lesson’s C flag is 1. The difference comes from a convention, which section 4.2 explains carefully: in subtraction, the C flag is the opposite of the carry-out of the adder.

Subtraction 3 minus 5 on one adder: B is inverted by XOR, SUB feeds the carry-in, and the result 1110 comes out with C equal to 1 for a borrow
figure 2 · Subtraction 3 minus 5 on the same adder: B is inverted, SUB feeds the carry-in a

Follow the figure from top to bottom and left to right. Operands A and B enter on the left, and the SUB control, in orange, reaches the block that inverts B and the carry-in. B comes out inverted as 1010, and the adder receives A, the inverted B and the carry-in 1. The result 1110 leaves on the right, and the flags appear in the blue block, with C equal to 1, N equal to 1 and V equal to 0. This figure shows one case. The calculation holds for any pair of 4-bit numbers, and section 7 checks every pair.

4.2 The four flags

The ALU in this lesson produces four flags. Each answers a different question about the result, and mixing them up is the most common mistake when you start reading architecture.

The Z flag, for zero, is 1 when every bit of the result is 0. It is the simplest and the most used, because an equality comparison becomes a subtraction whose result is zero.

The N flag, for negative, copies the most significant bit of the result. In a signed two’s complement number that bit is the sign, so N = 1 means the result, read as signed, is negative. It does not say whether the operation overflowed, and that is V’s job.

The C flag has two meanings, depending on the operation. In addition, it is the carry out of the most significant bit, and it says the result did not fit in the unsigned range. In subtraction, this lesson’s convention follows x86: C is 1 when there was a borrow, that is, when A is less than B unsigned. That value is the opposite of the adder’s final carry-out, because the complement adder sums the inverted B and a carry only remains when A is greater than or equal to B. Section 5 shows that ARM uses the opposite convention.

The V flag, for overflow, says the signed result left the range that fits in the register. For an n-bit addition, the signed range goes from −2 raised to n−1 up to 2 raised to n−1 minus 1. The hardware rule is short: signed overflow is the XOR of the carry into the sign bit and the carry out of it. The Wikipedia page on the overflow flag describes the same rule as an XOR between the carry into and out of the sign bit. The rule was checked against the arithmetic definition in section 7, for every pair of 4-bit and 8-bit numbers.

An example shows why C and V are different things. Add 0111, which is 7, and 0001, which is 1, in 4 bits. The result is 1000. Unsigned, 7 plus 1 is 8, which fits in 4 bits, so C is 0. Signed, 7 plus 1 is 8, and the largest positive 4-bit number is 7, so there was an overflow and V is 1. The sign bit of the result also ended up set, so N is 1. The figure shows the calculation bit by bit.

Signed overflow in 0111 plus 0001: the carries into and out of bit 3 go through an XOR that gives V equal to 1 even though the final carry is zero
figure 3 · Signed overflow in 0111 + 0001: the carry into and out of the sign bit, combined

Each addition block shows the bit’s sum and the carry it produces, and the wires labeled c1 to c4 show the carries between blocks. The carry into bit 3 is c3, and the one out of it is c4. The XOR in the corner receives c3 and c4, which are 1 and 0, and gives V equal to 1. Note that the final carry c4 is 0, so C is also 0, and only the V flag reveals the problem. Anyone who reads only the final carry thinks the calculation went fine.

4.3 Comparing is subtracting without keeping the result

A compare instruction performs the subtraction A minus B, throws the result away and keeps only the flags. The next program step, a conditional branch, reads those flags to decide the path. The figure below shows that chain for 3 minus 5, where the ALU stores nothing and the branch reads N and V.

For signed numbers, the rule is that A is less than B when N and V are different. The reason is that, without overflow, the result’s sign bit is the true sign of the difference, and with overflow the sign bit is flipped. V is exactly the bit that records that flip, so N different from V rebuilds the true sign. For 3 minus 5 in 4 bits, the result is 1110, with N = 1 and V = 0. The two differ, so 3 is less than 5.

Signed branch after 3 minus 5: the ALU does not store the result, the branch reads N and V, and N different from V makes the program skip the block
figure 4 · Signed branch: the ALU computes 3 minus 5 without storing the result, and the br

The flow has four steps, read from top to bottom. The ALU computes the subtraction and updates the flags without keeping the number. The flags, in the blue block, reach the diamond decision, which asks whether N differs from V. If it does, the branch is taken and the program skips the block. If they are equal, the program runs the block. The dashed arrow between the ALU and the flags shows that the numeric result does not move on.

For unsigned numbers, the rule is simpler. In the x86 convention, which is this lesson’s, A is less than B when C is 1, because C indicates a borrow. With 7 and 15 in 4 bits, C is 1 and 7 really is less than 15 unsigned. Signed, the same pair gives another answer, which section 6 uses as a common mistake.

4.4 Logic operations and shifting

The AND, OR and XOR gates work bit by bit. Bit 3 of A combines only with bit 3 of B, and there is never a carry between positions. That is the central difference from addition, and it is why logic operations are the simplest in the ALU: each position is independent of the others.

The most useful of these is the mask. A mask is a value in which the bits of interest are 1 and the rest are 0. An AND between the value and the mask zeroes everything that does not matter and keeps what does. To test whether bit 3 of 1101 is set, use the mask 1000: the result 1000 is nonzero, which says the bit is set. To test bit 2, use 0100, and the result 0000 says it is clear. The figure shows the calculation for bit 3, with the four AND gates receiving the bits of A and of the mask.

Mask 1000 with bitwise AND gates on 1101: only bit 3 passes, and the output 1000 shows the most significant bit was set
figure 5 · Mask 1000 with a bitwise AND gate: only bit 3 of 1101 passes, and the result 100

Each AND gate receives one bit of 1101 and one bit of 1000, and delivers the result for its position. The bit 3 gate receives 1 and 1, and outputs 1. The other three receive a bit of A that may be 1, but the mask bit is 0, and they output 0. The box on the right gathers the four results and shows 1000. The gates are not wired to the box because the figure is for reading each position’s calculation, not for showing the electrical path.

Shifting moves every bit one place. In a left shift, 0 enters from the right and the most significant bit leaves through the C flag. So 1101 shifted left gives 1010, with C = 1. In a logical right shift, 0 enters from the left and the least significant bit leaves through C. So 1101 shifted right gives 0110, with C = 1. The V flag has no single rule for shifts, and section 5 explains why this lesson leaves it at 0.

4.5 The register file

The ALU needs two operands in the same step, so the register file has two read ports. Each port is a 4-to-1 selector of words. The rs1 and rs2 fields of the instruction choose which register enters each port. Writing a result needs another path, and the file has one write port, a 2-to-4 decoder tied to an enable signal called we. The decoder activates only the line of the register chosen by rd, and only if we is 1. The other registers keep the value they already had, because the flip-flop with enable, seen in Sequential circuits, holds its data when there is no write.

The figure shows the file with four 4-bit registers. The state shown is the one after the instruction ADD R2, R0, R1, with R0 equal to 0101, R1 equal to 0011 and R2 receiving 1000. The rd field is 10, which chooses R2, and we is 1, so only R2 is updated.

Register file with four registers, two reads through 4-to-1 selectors and one write through a decoder, showing R2 receiving 1000 after the ADD
figure 6 · Register file with 4 registers: two reads through 4-to-1 selectors and one write

Read the figure from left to right. The rs1 and rs2 fields, in orange, choose the reads, and the two selector outputs enter the ALU as A and B. The ALU result, 1000, returns to R2 through the thick arrow, and the decoder, tied to rd and we, enables only that line. R0 and R1 appear as sources, because they are read and do not change. R3 appears with 0000 and no arrow, because no instruction in this sequence uses it.

The number of registers is an architecture choice, and the figure uses four only to fit on screen. The source consulted gives the numbers for three families. RISC-V has 32 integer registers in the base set, with the first, x0, fixed at zero, so reading it always returns 0 and writing to it has no effect. x86-64 has 16 general-purpose 64-bit registers, against 8 on 32-bit x86. AArch64 has 31 general-purpose 64-bit registers, plus a register that serves as zero or as the stack pointer depending on the instruction. The hardware rule is the same in all three: two reads and one write per instruction, with selectors and decoders sized to the number of registers.

4.6 One instruction step by step

Putting the pieces together, an ADD R2, R0, R1 instruction goes through the decoder, the register file and the ALU, and back to the register file. With R0 equal to 0101 and R1 equal to 0011, the result is 1000, with V = 1 and N = 1. The figure shows the messages in the order they happen.

Execution of ADD R2, R0, R1 as a sequence: the decoder passes the fields, the register file hands R0 and R1 to the ALU, and the sum 1000 goes back to R2
figure 7 · Execution of ADD R2, R0, R1: the CPU reads two registers, adds them in the ALU a

Each arrow is a step, read from top to bottom. First, the decoder hands rs1, rs2 and rd to the register file. Then the register file sends the value of R0 and the value of R1 to the ALU, which adds them and produces 1000. The note beside the ALU shows the result and the V flag. Finally, the ALU returns the result to the register file, which writes it to R2. The figure does not show the state of R2 before the write, because the previous value does not matter to the instruction.

The example has a detail worth keeping. Unsigned, 5 plus 3 is 8, which fits in 4 bits, and C would be 0. Signed, 5 plus 3 is 8, which is greater than the largest signed 4-bit number, so V is 1. A program that uses signed numbers and does not read V accepts 1000 as if it were a correct answer, when the value has already overflowed. That is why flags exist: the result alone does not tell you whether the calculation made sense.

4.7 Registers the programmer does not choose

General-purpose registers are the ones an instruction names, like R0 to R3 above. Inside the CPU there are also special registers, which the programmer does not pick by name but which execution uses all the time. The figure shows four of them, next to the general-purpose ones and memory.

The program counter, called PC in this lesson, holds the address of the next instruction to fetch. After each instruction it advances, unless a branch sends another value. The instruction register, IR, holds the instruction being executed, so the control unit can decode it. The stack pointer, SP, holds the top of the function call stack, and the status register, called status or CCR, holds the flags of the last operation.

General-purpose and special CPU registers: PC and IR link the core to memory, and the flags register receives Z, N, C and V from the ALU
figure 8 · General-purpose and special registers inside the CPU: the program counter and th

The figure separates the two groups in dashed boxes. The general-purpose registers, on the left, supply operands to the ALU. The special ones, on the right, have fixed roles: the PC sends an address to memory through the orange arrow, and the IR receives the instruction back through the gray arrow. The dotted arrow from the ALU to the flags register shows that the flags are the only ALU output that goes to a special register, while the numeric value goes to a general-purpose register. The drawing does not show the control unit, which belongs to the instruction cycle lesson.

4.8 Load-store: why the ALU only sees registers

A load-store architecture splits instructions into two classes. One class accesses memory, with load instructions, which bring a value from memory into a register, and store instructions, which take a register back to memory. The other class performs ALU operations, and those work only between registers. Wikipedia gives this definition and cites PowerPC, SPARC, RISC-V, ARM and MIPS as examples.

The pattern has a practical consequence that the figure shows. To add two values that sit in memory, the program loads each one into a register, then the ALU adds the two registers and the result goes to another register, and finally a store takes the result back to memory. Three transfer instructions and one ALU instruction for one addition. In exchange, each instruction does one thing only, and the ALU can be a simple block, with no special paths to memory.

Load-store pattern in sequence: RAM hands a value to a register, the ALU adds two registers, and the result returns to memory with a store
figure 9 · Load, operate and store: the ALU only adds registers, and memory is reached only

Read the figure from top to bottom. First, a load brings 5 from RAM into R1. Then the ALU receives R1 equal to 5 and R2 equal to 3, and returns 8 in R3. Finally, a store writes R3 to address 17. The solid arrows are calls, and the dashed ones are data returns. The ALU never touches RAM, and that is the point of the architecture.

Families that do not follow this pattern, such as x86 in many of its instructions, let the ALU take an operand straight from memory. This lesson does not check that rule against a primary source, and it is recorded in the limits section.

5. Details and edges

The first difference is the C flag convention in subtraction. The Wikipedia page on the carry flag states that x86 processors set the carry flag on a borrow, while ARM clears it on a borrow. The same calculation, 3 minus 5, gives the result 1110 on both architectures, but the similarly named flag has the opposite value. The figure compares the two cases.

The same subtraction 3 minus 5 under the x86 and ARM conventions: x86 sets the carry on a borrow and ARM clears it, so the same operation flips the flag
figure 10 · The same subtraction 3 minus 5 on x86 and ARM: x86 sets the carry on borrow, ARM

The two columns of the figure hold the same calculation. The left one shows the x86 convention, where a borrow sets the CF flag. The right one shows ARM, where the inverted carry clears the C flag in the same case. When reading code for another architecture, the first question should be which convention the flag follows, because a branch written for one convention does the opposite on the other.

The second edge is overflow in shifts. The same Wikipedia page on the overflow flag records that, on x86, the instruction set defines the overflow flag only for multiplications and for 1-bit shifts, and that shifts of more than one bit leave the flag undefined. That is why this lesson’s ALU zeroes V on shifts. This choice is a teaching simplification, not an architecture rule.

The third edge is width. This lesson’s program uses 4 and 8 bits to fit on screen and to allow exhaustive checking. Real registers are 64 bits wide in 64-bit families, and the position of the V rule moves with the width, but the logic is the same. The fourth edge is what the lesson leaves out: multiplication, division and floating-point operations. They use other circuits, or the same ALU in stages, and are not covered here.

6. Common mistakes and good practice

The most frequent mistake is mixing up C and V. Someone who reads only the final carry of an addition believes a calculation with no carry cannot be wrong. That is true for unsigned addition and false for signed addition. In the example 0111 plus 0001, the final carry is 0, and the signed calculation overflowed. Reading only C lets this error through, and the fix is to look at V whenever the number represents something signed.

The second mistake is comparing with the wrong flag. For 0111 minus 1111, the unsigned reading says 7 is less than 15, because C is 1. The signed reading says 7 is not less than −1, because N and V are 1 and 1, and are equal. Both are right, each for its own kind of number. The mistake is using the unsigned reading for a value the program treats as negative. Before choosing the branch, confirm whether the operands are signed or unsigned.

The third mistake is assuming a flag always means the same thing. The C flag is carry in addition and borrow in subtraction under the x86 convention, and ARM inverts it. The V flag is signed overflow, and does not apply to logic operations. Good practice is to read the architecture specification before writing a branch, and not to guess the convention from the flag’s name.

The fourth mistake is writing to a register that is fixed. In RISC-V, writing to x0 has no effect, and reading x0 returns zero, according to the documentation cited in section 4.5. Anyone who generates code without knowing this loses the result of a calculation with no warning.

The general good practice follows what this text did: write the table of edge cases before trusting the circuit. Test the largest positive, the smallest negative, zero, and a pair that overflows signed but not unsigned. These cases expose the convention errors that reading the figure lets slip.

7. The program that simulates

The program below implements the n-bit ALU with logic gates, and the register file with a decoder and selectors. It checks the ALU against Python arithmetic for every operation and every pair of 4-bit and 8-bit numbers, which gives 1792 and 458752 cases. Then it checks the signed and unsigned comparisons, and finally it runs the instruction sequence from section 4.6 to show the state of the file. The program was run with Python 3.12.8, in isolated mode, with the command python3 -I ula_registradores.py. The output below is the real one, copied unedited. Identifiers, comments and messages stay in Portuguese, as they were when it ran.

# ULA de n bits e banco de registradores, construídos só com portas e um laço de leitura.
# Cada resultado é conferido contra a aritmética direta de Python.

import itertools

def NOT(a): return 1 - a
def AND(a, b): return a & b
def OR(a, b): return a | b
def XOR(a, b): return a ^ b

def to_bits(x, n):
    return [(x >> i) & 1 for i in range(n)]

def from_bits(bits):
    return sum(b << i for i, b in enumerate(bits))

def somador_completo(a, b, cin):
    s1 = XOR(a, b); c1 = AND(a, b)
    s2 = XOR(s1, cin); c2 = AND(s1, cin)
    return s2, OR(c1, c2)

def somador(a_bits, b_bits, cin):
    # devolve soma, vai-um de saída e o vai-um que entra no bit de sinal
    s, c = [], cin
    c_antes_do_sinal = cin
    for i, (a, b) in enumerate(zip(a_bits, b_bits)):
        if i == len(a_bits) - 1:
            c_antes_do_sinal = c
        si, c = somador_completo(a, b, c)
        s.append(si)
    return s, c, c_antes_do_sinal

def ula(n, a, b, op):
    """op: 'ADD', 'SUB', 'AND', 'OR', 'XOR', 'SHL', 'SHR'. Devolve (resultado, Z, N, C, V)."""
    A = to_bits(a, n)
    B = to_bits(b, n)
    if op == 'ADD':
        s, cout, cin_sinal = somador(A, B, 0)
        C = cout
        V = XOR(cin_sinal, cout)
    elif op == 'SUB':
        # A - B = A + NOT(B) + 1. O vai-um de saída vale 1 quando não há empréstimo.
        # A flag C desta aula segue o x86: C = 1 quando há empréstimo, ou seja, NOT(vai-um).
        s, cout, cin_sinal = somador(A, [NOT(b_) for b_ in B], 1)
        C = NOT(cout)
        V = XOR(cin_sinal, cout)
    elif op == 'AND':
        s = [AND(x, y) for x, y in zip(A, B)]; C = 0; V = 0
    elif op == 'OR':
        s = [OR(x, y) for x, y in zip(A, B)]; C = 0; V = 0
    elif op == 'XOR':
        s = [XOR(x, y) for x, y in zip(A, B)]; C = 0; V = 0
    elif op == 'SHL':
        # o bit de maior peso sai pela C; entra 0 pela direita
        s = [0] + A[:-1]; C = A[-1]; V = 0
    elif op == 'SHR':
        # deslocamento lógico: o bit de menor peso sai pela C; entra 0 pela esquerda
        s = A[1:] + [0]; C = A[0]; V = 0
    res = from_bits(s)
    Z = 1 if res == 0 else 0
    N = s[-1]
    return res, Z, N, C, V

def com_sinal(x, n):
    return x - (1 << n) if x >= (1 << (n - 1)) else x

# 1. conferência exaustiva contra a aritmética, para n = 4 e n = 8
def referencia(n, a, b, op):
    m = (1 << n) - 1
    if op == 'ADD':
        r = (a + b) & m
        C = (a + b) >> n
        V = int(not (-(1 << (n - 1)) <= com_sinal(a, n) + com_sinal(b, n) < (1 << (n - 1))))
    elif op == 'SUB':
        r = (a - b) & m
        C = int(a < b)                       # empréstimo, convenção do x86
        V = int(not (-(1 << (n - 1)) <= com_sinal(a, n) - com_sinal(b, n) < (1 << (n - 1))))
    elif op == 'AND': r, C, V = a & b, 0, 0
    elif op == 'OR':  r, C, V = a | b, 0, 0
    elif op == 'XOR': r, C, V = a ^ b, 0, 0
    elif op == 'SHL': r, C, V = (a << 1) & m, a >> (n - 1), 0
    elif op == 'SHR': r, C, V = a >> 1, a & 1, 0
    Z = int(r == 0)
    N = (r >> (n - 1)) & 1
    return r, Z, N, C, V

for n in (4, 8):
    ok = True
    casos = 0
    for a, b in itertools.product(range(1 << n), repeat=2):
        for op in ('ADD', 'SUB', 'AND', 'OR', 'XOR', 'SHL', 'SHR'):
            casos += 1
            if ula(n, a, b, op) != referencia(n, a, b, op):
                ok = False
    print("ULA %d bits, %d casos, todas as operações e flags conferem: %s" % (n, casos, ok))

# 2. comparações: com sinal, a < b é N diferente de V; sem sinal, a < b é C = 1
for n in (4, 8):
    ok = True
    for a, b in itertools.product(range(1 << n), repeat=2):
        _, Z, N, C, V = ula(n, a, b, 'SUB')
        if (N ^ V) != int(com_sinal(a, n) < com_sinal(b, n)):
            ok = False
        if C != int(a < b):
            ok = False
    print("comparacao com e sem sinal, %d bits, todos os pares: %s" % (n, ok))

print()
# 3. os exemplos da aula, com as flags
exemplos = [
    ("0111 + 0001", 4, 0b0111, 0b0001, 'ADD'),
    ("0011 - 0101 (3 - 5)", 4, 0b0011, 0b0101, 'SUB'),
    ("0101 - 0101 (5 - 5)", 4, 0b0101, 0b0101, 'SUB'),
    ("1101 AND 1000", 4, 0b1101, 0b1000, 'AND'),
    ("1101 << 1 (SHL)", 4, 0b1101, 0, 'SHL'),
    ("1101 >> 1 (SHR)", 4, 0b1101, 0, 'SHR'),
]
for nome, n, a, b, op in exemplos:
    r, Z, N, C, V = ula(n, a, b, op)
    print("%-22s -> resultado %s  Z=%d N=%d C=%d V=%d" % (nome, format(r, '0%db' % n), Z, N, C, V))

print()
# 4. banco de 4 registradores de 4 bits: duas leituras, uma escrita
def decodificador2x4(sel, we):
    # uma linha de escrita por registrador; só a linha escolhida fica ativa, e só se we = 1
    s1, s0 = (sel >> 1) & 1, sel & 1
    linhas = [AND(NOT(s1), NOT(s0)), AND(NOT(s1), s0), AND(s1, NOT(s0)), AND(s1, s0)]
    return [AND(we, l) for l in linhas]

def mux4_palavra(palavras, sel, n):
    # seleciona uma palavra de n bits: cada bit da saída é um mux 4:1 sobre o mesmo bit das quatro palavras
    s1, s0 = (sel >> 1) & 1, sel & 1
    linhas = [AND(NOT(s1), NOT(s0)), AND(NOT(s1), s0), AND(s1, NOT(s0)), AND(s1, s0)]
    bits = []
    for i in range(n):
        r = 0
        for palavra, l in zip(palavras, linhas):
            r = OR(r, AND(to_bits(palavra, n)[i], l))
        bits.append(r)
    return from_bits(bits)

banco = [0, 0, 0, 0]

def escreve(rd, valor, we):
    global banco
    habilita = decodificador2x4(rd, we)
    banco = [valor if habilita[i] else banco[i] for i in range(4)]

def le(rs):
    return mux4_palavra(banco, rs, 4)

def instrucao(nome, rd, rs1, rs2, op, we=1):
    a, b = le(rs1), le(rs2)
    r, Z, N, C, V = ula(4, a, b, op)
    escreve(rd, r, we)
    print("%-26s rs1=R%d (%s) rs2=R%d (%s) ULA=%s  ->  R%d = %s  Z=%d N=%d C=%d V=%d" % (
        nome, rs1, format(a, '04b'), rs2, format(b, '04b'), op,
        rd, format(r, '04b'), Z, N, C, V))

escreve(0, 5, 1)
escreve(1, 3, 1)
print("estado inicial:", " ".join("R%d=%s" % (i, format(v, '04b')) for i, v in enumerate(banco)))
instrucao("ADD R2, R0, R1", 2, 0, 1, 'ADD')
instrucao("SUB R3, R0, R1", 3, 0, 1, 'SUB')
instrucao("ADD R2, R0, R0 (we=0)", 2, 0, 0, 'ADD', we=0)
print("estado final:", " ".join("R%d=%s" % (i, format(v, '04b')) for i, v in enumerate(banco)))
print("banco bate com a conta direta (5, 3, 8, 2):", banco == [5, 3, 8, 2])

Real output of the program, with Python 3.12.8:

ULA 4 bits, 1792 casos, todas as operações e flags conferem: True
ULA 8 bits, 458752 casos, todas as operações e flags conferem: True
comparacao com e sem sinal, 4 bits, todos os pares: True
comparacao com e sem sinal, 8 bits, todos os pares: True

0111 + 0001            -> resultado 1000  Z=0 N=1 C=0 V=1
0011 - 0101 (3 - 5)    -> resultado 1110  Z=0 N=1 C=1 V=0
0101 - 0101 (5 - 5)    -> resultado 0000  Z=1 N=0 C=0 V=0
1101 AND 1000          -> resultado 1000  Z=0 N=1 C=0 V=0
1101 << 1 (SHL)        -> resultado 1010  Z=0 N=1 C=1 V=0
1101 >> 1 (SHR)        -> resultado 0110  Z=0 N=0 C=1 V=0

estado inicial: R0=0101 R1=0011 R2=0000 R3=0000
ADD R2, R0, R1             rs1=R0 (0101) rs2=R1 (0011) ULA=ADD  ->  R2 = 1000  Z=0 N=1 C=0 V=1
SUB R3, R0, R1             rs1=R0 (0101) rs2=R1 (0011) ULA=SUB  ->  R3 = 0010  Z=0 N=0 C=0 V=0
ADD R2, R0, R0 (we=0)      rs1=R0 (0101) rs2=R0 (0101) ULA=ADD  ->  R2 = 1010  Z=0 N=1 C=0 V=1
estado final: R0=0101 R1=0011 R2=1000 R3=0010
banco bate com a conta direta (5, 3, 8, 2): True

Read the output in two parts. The first lines confirm the ALU against direct arithmetic, and the comparison lines confirm the two rules from section 4.3. The next block shows the six examples with their flags, and the calculation matches the figures and the text: 0111 plus 0001 gives 1000 with V = 1, and 0011 minus 0101 gives 1110 with C = 1. The last part shows the register file after three instructions. The instruction with we = 0 computes 1010 and does not write it, and R2 stays at 1000. The final state, 5, 3, 8 and 2, matches the hand calculation.

8. Exercises

  1. Add 1001 and 0111 in 4 bits and give the result and the Z, N, C and V flags. Answer: the result is 0000, with a carry-out of 1. Z is 1, N is 0, C is 1 and V is 0. Read as unsigned, 9 plus 7 is 16, which overflows the unsigned range. Read as signed, −7 plus 7 is 0, which fits, so V is 0.

  2. Compute 0110 minus 1001 in 4 bits, with all four flags. Answer: the result is 1101, with N = 1, Z = 0, C = 1 and V = 1. Unsigned, 6 is less than 9, so there is a borrow and C is 1. Signed, 6 minus −7 is 13, outside the range −8 to 7, so V is 1.

  3. For 0010 minus 1111 in 4 bits, say whether 2 is less than 15 unsigned and whether 2 is less than −1 signed, using the flags. Answer: C is 1, so 2 is less than 15 unsigned. N and V are 0 and 0, which are equal, so 2 is not less than −1 signed.

  4. Use a mask to test whether bit 2 of 1011 is set. What is the mask and what is the result? Answer: the mask is 0100, and the AND of 1011 with 0100 gives 0000. Z is 1, so bit 2 is clear, which checks out, because bit 2 of 1011 is 0.

  5. In the four-register file, an instruction has we equal to 0. What happens to the register that rd points to, and to the ALU result? Answer: the register keeps its previous value, because no write line is enabled. The ALU keeps calculating and the flags keep being updated, but the result is not written to the file.

  6. Why does the sequence 0111 minus 1111 call for attention to the number type? Answer: C is 1 and the unsigned reading says 7 is less than 15. N and V are 1 and 1, so the signed reading says 7 is not less than −1. The same pair of bits gives different answers depending on how the program treats the numbers, and the code has to pick the right reading.

9. Glossary

ALU: arithmetic logic unit, a combinational circuit that operates on two operands according to an operation code and returns a result and flags.

Register: a set of flip-flops with a common clock that holds a word, and only changes when writing is enabled.

Flag: a status bit computed by the ALU and kept in a status register, describing the result of the last operation.

Z flag: is 1 when the result is zero.

N flag: copies the most significant bit of the result, which is the sign in two’s complement.

C flag: carry in unsigned addition and, in the x86 convention used in this lesson, borrow in subtraction.

V flag: signals signed overflow, computed as the XOR of the carry into the sign bit and the carry out of it.

Mask: a value used with AND to isolate or clear specific bits of another value.

Shift: an operation that moves the bits one place left or right, with the bit that falls off going to the C flag.

Register file: the set of general-purpose registers, with read and write ports selected by the instruction fields.

Load-store architecture: an architecture in which the ALU works only on registers, and only load and store instructions access memory.

Program counter (PC): the register that holds the address of the next instruction to fetch.

Instruction register (IR): the register that holds the instruction being executed.

What was not confirmed or measured

The program in this lesson was run and its outputs are real. It checks the logic of the ALU and the register file against Python arithmetic, but it does not simulate voltage, current or propagation time. No delay, frequency or latency figure appears in this lesson, and phase 4 deals with those values.

The circuit figures, 03, 05 and 09, are gate-level and block-level representations, not the real electrical path. Figure 05 shows four 4-bit registers for clarity, and figure 09 does not wire the gates to the output box, because it shows the calculation bit by bit. Figures 01, 02, 04, 06, 07, 08 and 10 are block and sequence diagrams, with no measured values.

The register counts for three families come from English Wikipedia, on the pages cited below, and not from architecture manuals. The numbers are the ones Wikipedia reports on the date of consultation, and checking the official specifications may show details the page does not.

The C convention in subtraction follows the page on the carry flag, which describes x86 and ARM. The lesson did not read the official Intel or ARM manual to confirm the convention line by line. The V rule for shifts follows the same page on overflow, and the lesson did not check that rule in the manual.

The rule that x86 accepts a memory operand in arithmetic instructions, cited in section 4.8, was not confirmed in a primary source in this lesson. The lesson also does not describe the flags of logic operations on each real architecture, and zeroing C and V on logic operations is a choice of the model.

Section 4.8 cites a definition of load-store architecture from Wikipedia, without consulting the original academic literature.

Sources

Consulted on November 1, 2026.

The sources confirm the concepts and conventions cited. The case tables, the examples and the exercise answers were calculated and checked by the program in section 7, not copied from the sources.

  • Hardware track
  • CPU
  • How the CPU executes instructions
  • Combinational circuits
  • Sequential circuits
  • Pipeline, cache and parallelism
  • ISA, CISC and RISC
  • Instruction cycle and control unit

↑ Back to top

Keyboard shortcuts

Open search
/
Toggle light/dark theme
t
Previous lesson in series
←
Next lesson in series
→
Shortcuts list
?