The Machine Underneath

Computer architecture, from the switch up

The machine underneath your program

Eight chapters that start with a transistor acting as a switch and end with a program you assemble and trace yourself. Every micro-operation is shown at register transfer level, with real hex, the way it is taught on the board.

13 chaptersEvery instruction tracedRegister transfer level, the way it is taught

Chapter 01

How machines hold a number

A computer never stores a number, a letter, or a color, only patterns of 1s and 0s, and it is the code reading those bits back that decides what they mean.

Counting in binary

A bit is a single 1 or 0. Put several bits in a row and you get unsigned binary, plain place-value counting in base 2 instead of base 10. Each position is worth double the one to its right: 1, 2, 4, 8, 16, and so on. To read a bit pattern as a number, add up the weight of every position that holds a 1.

1011 (binary) = 1x8 + 0x4 + 1x2 + 1x1 = 11 (decimal)

With 8 bits you can make 256 different patterns, so an 8-bit unsigned value covers 0 through 255. Add a bit and the count of patterns doubles, that is the only rule you need: n bits give 2^n distinct values.

Hex, bytes, and words

Eight bits grouped together is a byte, the smallest chunk most hardware and languages address directly. A word is whatever chunk size the CPU is built to move and compute on in one go, 32 bits on an older machine, 64 bits on almost anything you will program today.

Writing out 8 or 32 bits by hand is slow and error-prone, so people write hex (base 16) instead. Hex is just a shorthand: each hex digit stands for exactly 4 bits, so a byte is always two hex digits, and a 32-bit word is always eight. 0xFF is one byte, all bits set.

binaryhexdecimal
000000
000111
001022
001133
010044
010155
011066
011177
100088
100199
1010A10
1011B11
1100C12
1101D13
1110E14
1111F15
unitbitsunsigned range
nibble40 to 15
byte80 to 255
word (64-bit machine)640 to 18,446,744,073,709,551,615

Two's complement: how negative numbers work

Unsigned binary has no minus sign, so negative numbers need a convention. The one every mainstream CPU uses is two's complement: to negate a number, flip every bit, then add 1.

 5  = 0000 0101
-5: flip bits -> 1111 1010
     add 1     -> 1111 1011

The top bit still works like a sign: 0 means non-negative, 1 means negative, but the value is not just "flip the sign bit," it is the whole flip-and-add-one dance. For 8 bits this gives a range of -128 to 127, one more negative value than positive, because 0 uses up one of the 128 patterns on the non-negative side.

Two engineers before two's complement tried sign-magnitude (a plain sign bit plus a magnitude) and ones' complement (just flip the bits, no add-1). Two's complement beat both for two concrete reasons. First, both of the older schemes have two different bit patterns for zero, +0 and -0, which wastes a value and forces every comparison to special-case it. Two's complement has exactly one zero. Second, and more important for the hardware: with two's complement, the same binary adder circuit produces the correct answer whether you treat the bits as unsigned or as signed. Subtraction becomes addition of a negated number, so the chip never needs to know, or care, which interpretation the programmer intended. That is one fewer circuit to design, test, and pay for in silicon.

bits unsigned signed 0000 0 0 0001 1 1 0010 2 2 0011 3 3 0100 4 4 0101 5 5 0110 6 6 0111 7 7 1000 8 -8 1001 9 -7 1010 10 -6 1011 11 -5 1100 12 -4 1101 13 -3 1110 14 -2 1111 15 -1 same count order, flips to negative
The bit pattern never changes as you count up, only the label on top of it does. 0111 to 1000 is a plain increment in hardware, but read as signed it jumps from 7 to -8.

Overflow

Overflow happens when the true result of an operation does not fit in the number of bits you have. An 8-bit unsigned adder computing 255 + 1 produces 0, because the 9th bit that would have made it 256 has nowhere to go and is simply dropped. An 8-bit signed adder computing 127 + 1 produces -128, for the same reason, the bit pattern wraps, but the wraparound point lands somewhere different because the top bit is now read as a sign.

Floating point in a hurry

IEEE-754 is the standard nearly every language uses to store numbers with a fractional part. A 32-bit float splits its bits into three fields: one sign bit, 8 exponent bits, and 23 mantissa bits (the significant digits). The value they encode is roughly sign times mantissa times 2 to the exponent, the same idea as scientific notation, just in binary.

sign 1 bit exponent 8 bits mantissa 23 bits
value = (-1)^sign x 1.mantissa x 2^(exponent - 127)

Binary fractions can only represent exactly the numbers you get from adding up powers of two, like 1/2, 1/4, 1/8. Plain decimal fractions like 0.1 and 0.2 are not exact in binary any more than 1/3 is exact in decimal, so they get stored as the closest 23-bit approximation. Add two approximations together and the tiny rounding errors do not cancel out.

0.1 + 0.2 === 0.3   // false
0.1 + 0.2            // 0.30000000000000004

The bit board

Click a bit to flip it. The same 8 bits get read four different ways below. Try increment from 0111 1111 to watch signed overflow, or shift left from 1000 0000 to watch a bit fall off the end.

128
64
32
16
8
4
2
1
Unsigned0
Signed (two's complement)0
Hex0x00
Meaningnull byte
Statusready

Chapter 02

From transistors to an ALU

Every add, compare, and bit-shift your CPU runs is a few million transistors wired to only ever answer yes or no.

The transistor as a switch

A transistor has three terminals. One of them, the gate, controls whether current can flow between the other two. Feed the gate a high voltage and the path conducts, that is a 1. Feed it a low voltage and the path is broken, that is a 0. That is the entire trick: a transistor is a switch with no moving parts, flipped by a voltage instead of a finger.

in out gate = 0, no current in out gate = 1, current flows
Same switch, two states. Wire enough of these together and their on/off patterns can represent logic, then numbers.

NAND: the only gate you need

Chain transistors into small groups and you get logic gates: AND, OR, NOT, and one that turns out to be special, NAND (not-and). NAND outputs 0 only when both inputs are 1, otherwise it outputs 1.

ABA NAND B
001
011
101
110

NAND is a universal gate: every other gate can be built from NAND alone. Tie both of its inputs together and you get NOT. Feed a NOT into the output of a NAND and you get AND. Chip designers do not build a NAND out of an AND and a NOT, they build everything else out of NAND, because it takes the fewest transistors to make directly.

Half adder, then full adder

A half adder adds two single bits. XOR gives the sum bit (1 when exactly one input is 1), AND gives the carry bit (1 only when both inputs are 1).

half adder
ABSumCarry
0000
0110
1010
1101
full adder
ABCinSumCout
00000
00110
01010
01101
10010
10101
11001
11111

A full adder adds three bits: two operand bits plus a carry-in from the position to the right. Build it from two half adders in series (the second one adds the carry-in into the first one's sum) plus an OR gate to combine the two carries. A full adder is what lets you chain positions together, one per bit, to add whole numbers instead of single digits.

half adder A B XOR AND Sum Carry full adder A B Cin XOR AND XOR AND Sum OR Cout
A half adder handles two bits. A full adder chains a second half adder to fold in a carry-in, then ORs the two carry outputs together, so full adders can be wired end to end to add numbers of any width.

Ripple carry vs carry lookahead

Chain 4 full adders together, bit 0's carry-out feeding bit 1's carry-in, and so on, and you get a 4-bit adder. That is called ripple carry: the carry has to physically propagate through every stage before the top bit's sum is correct. Bit 3 cannot know its answer until bit 2 has settled, which cannot know until bit 1 has settled, and so on down to bit 0.

Every gate takes a small but nonzero amount of time to produce a stable output after its inputs change, that is gate delay. The critical path of a circuit is the longest chain of gate delays that any output depends on, in a ripple-carry adder that is the full carry chain from bit 0 to the top bit. The clock cannot tick faster than the time it takes the critical path to settle, or the circuit will report an answer before the real one has arrived. That is the direct link between "how the carry moves" and "how fast the chip can run."

Carry lookahead is a way to shortcut the wait: extra logic computes, for every bit position, whether that position will generate a carry on its own or merely propagate one coming in, and combines those signals in parallel instead of waiting for each stage in turn. It trades more gates (more silicon, more power) for a shorter critical path, which is why fast adders in real CPUs use it instead of plain ripple carry.

The ALU: a bank of units and a mux

An ALU (arithmetic logic unit) does not pick one circuit per instruction, it runs every operation it supports, adder, AND, OR, XOR, shifter, on the same two inputs at the same time, all in parallel. A multiplexer (mux), a gate structure that selects one of several inputs based on a set of select bits, then picks which of those already-computed results to send to the output based on the opcode. The unused results are simply thrown away that cycle.

A B ADD AND OR XOR MUX Result select (opcode bits)
Every unit computes its result every cycle. The opcode only decides which wire the mux passes through, the other results are just discarded.

4-bit ripple-carry adder

Toggle the bits of A and B, then run the addition to watch the carry move from bit 0 to bit 3, one full adder at a time.

A

B

bit 0

cin-
cout-
sum-

bit 1

cin-
cout-
sum-

bit 2

cin-
cout-
sum-

bit 3

cin-
cout-
sum-
Result-
Overflow-

Chapter 03

Registers, shift registers and counters

A register is where a machine keeps a value between clock pulses, and everything past this point, the bus, the adder sharing, the whole control unit, is just registers wired together with the right terminal doing the right job on the right edge. This chapter is the parts list: what a register is, how a shift register moves bits, how a counter counts, and the multiplexer that shows up inside almost all of it.

What a register is

A register is a group of flip flops that together hold one binary word. Each flip flop holds one bit, so a 4-bit register is 4 flip flops storing 4 bits, and in general an n-bit register is n flip flops storing an n-bit word. The register's value is just whatever its flip flops currently hold, and that value only changes on a clock edge, never in between.

Register control terminals

Draw a register as a box and every one of these lines has to go somewhere: parallel inputs bring in a full word to load, parallel outputs read the current word back out, a Load control tells the register to capture the parallel inputs on the next edge, a Clear control resets every flip flop to 0, and CP is the clock pulse that makes any of it actually happen. Without an active edge on CP, none of the other controls do anything, they are just sitting there waiting.

R n-bit register parallel input, n bits parallel output, n bits Load Clear CP
A register's terminals: parallel input and output on the data side, Load and Clear as controls, CP as the clock edge everything else waits on.

Shift registers

A shift register adds two more terminals to a plain register: SI, the serial input, and SO, the serial output. On a shift right, every flip flop takes the value of its higher-order neighbor, the high-order bit position is left vacant and gets filled from SI, and the low-order bit that gets pushed out the other end appears at SO. Shift left runs the same idea in the other direction: SI fills the vacated low-order position, and the high-order bit that falls off appears at SO. A bidirectional shift register keeps a plain register's Load and Clear terminals and adds a Shift control plus a direction line, so one clock edge can load, clear, or shift either way, whichever control is armed for that pulse.

SI FF3 (MSB) FF2 FF1 FF0 (LSB) SO shift right: every flip flop takes its left neighbor's value on CP common CP line to all four flip flops
Shift right on a 4-bit register: FF3 down to FF0 each take their left neighbor's value, SI fills the vacated MSB, and the bit pushed out of the LSB is SO.

Counters

A counter is a register that can add 1 to its own value without going through the parallel inputs at all. Its terminals are outputs, Load, Clear, and a Count control that increments the register's own current value on the next edge, no parallel input involved. An n-bit counter can hold 2^n different values, 0 through 2^n minus 1, and asserting Count when the counter already holds the maximum value wraps it straight back to 0. There is no overflow terminal on the counter itself, the wraparound is just what the bits do.

The control terminal table

One terminal, one job. This is the table that pins down, for any micro-operation written on a register, which terminal actually does the work:

Micro-operationTerminal used
R <- 0Clear
R <- R + 1 or R <- R - 1Count
R <- shl R or R <- shr RShift
R <- another register or an adder outputLoad

Multiplexers

Everything from here on shares one part underneath it: the multiplexer. A shared adder, a shared bus, a register that can load from two different sources, all of it is a mux. A multiplexer with n select lines chooses among 2^n inputs, only one of them ever reaching the output at a time. The smallest useful case is a 2x1 mux: one select line S, two data inputs I0 and I1, and the output follows whichever input S currently points at.

SOutput
0I0
1I1

Try it: clocked register bench

Arm exactly one control below, then press Pulse CP. Leave nothing armed and press CP anyway, watch the value not move, that is the whole lesson: no armed terminal, no change, no matter how many times the clock ticks. After a shift, the bit that just arrived from SI is highlighted.

Load value (8 bits, editable)
SI, serial input bit (editable)
Before, binary00000000
Before, hex00
After, binary00000000
After, hex00
Terminal that firednone
SO, bit shifted out-

no control armed

Micro-operation log

(nothing yet, arm a control and pulse CP)

Chapter 04

Register transfer and building the hardware

Everything a hardware question asks for comes down to the same three moves: name the transfer, decide which terminal does it, and wire the control function to that terminal. This chapter is register transfer notation, why a swap is not the same as making two registers equal, and a full worked example that turns a set of register transfer statements into an actual circuit, one adder, two multiplexers, and a handful of control gates.

Register transfer notation

R1 <- R2 means the content of R2 is copied into R1. R2 itself is unchanged, only R1 gets a new value. Most transfers are conditional, and the condition is written as a control function in front of the arrow: xT2: R1 <- R2 means this transfer happens only when x is 1 during timing signal T2, and nowhere else does this line do anything. Two micro-operations written on the same control function, separated by a comma, both happen on that same clock edge, in parallel, not one after the other.

The 3-step method

Every hardware question from here follows the same three steps, in this order:

  1. Decide which devices the problem needs: plain registers, a counter, a shift register, an adder, whatever the micro-operations actually call for.
  2. Decide which terminal on each device does each micro-operation: Load, Clear, Count, or Shift.
  3. Connect the devices, then for every terminal, OR together the control function of every micro-operation that uses it. That sum is the terminal's control gate expression.

Worked example, one adder, three control functions

Using one adder, draw the hardware for
x1'x3: R2 <- R1, R3 <- R3 + 1, R4 <- 0
x1x2:  R2 <- R1 + R3, R4 <- shr R4
x1x2': R4 <- R1 + R2, R3 <- R2

Step 1, devices: 2 registers, R1 and R2, a counter R3, a shift register R4, one adder, and a control unit. The adder is shared by two of the three lines, so a 2x1 mux picks its second input, R3 for the x1x2 line or R2 for the x1x2' line, and a second 2x1 mux picks what actually loads into R2, R1 straight through for the x1'x3 line or the adder's output for the x1x2 line.

Step 2 and 3: terminal per register, then OR the control functions

R1 never appears on the left of an arrow anywhere in these three lines, it is only ever read. No terminal, no control gate, R1 just sits there as one of the adder's steady inputs.

TerminalConditionMicro-operation
Loadx1'x3R2 <- R1
Loadx1x2R2 <- R1 + R3

Two lines load R2, none clear or shift it, so OR the two conditions:

Load(R2) = x1'x3 + x1x2
TerminalConditionMicro-operation
Countx1'x3R3 <- R3 + 1
Loadx1x2'R3 <- R2

R3 gets one line on Count and one line on Load, each already a single term, nothing to OR together:

Count(R3) = x1'x3
Load(R3)  = x1x2'
TerminalConditionMicro-operation
Clearx1'x3R4 <- 0
Shiftx1x2R4 <- shr R4
Loadx1x2'R4 <- R1 + R2

R4 uses all three of its non-output terminals, one micro-operation each, so each control gate is just that one condition, confirmed on the board:

Clear(R4) = x1'x3
Shift(R4) = x1x2
Load(R4)  = x1x2'

The two muxes need a control gate too, one select line each instead of a register terminal. MUX A picks R3 when the x1x2 line is active and R2 when the x1x2' line is active. x1x2 has x2=1 and x1x2' has x2=0, so the whole condition collapses to one signal, select R2 whenever x2 is 0: S(MUX A) = x2'. MUX B picks the adder's output when x1x2 is active and R1 directly when x1'x3 is active. x1'x3 has x1=0 and x1x2 has x1=1, so S(MUX B) = x1', select R1 directly whenever x1 is 0.

R1 read only here, no terminal R2 Load R3 counter Count, Load R4 shift register Clear, Shift, Load MUX A S = x2' ADDER MUX B S = x1' R1 to adder in1 I0 I1 MUX A out, adder in2 I0 adder output also loads directly into R4 I1, R1 direct, bypasses the adder MUX B output is the data that loads into R2 Load(R2) = x1'x3 + x1x2 Count(R3) = x1'x3 Load(R3) = x1x2' Clear(R4) = x1'x3 Shift(R4) = x1x2 Load(R4) = x1x2' S(MUX A) = x2' S(MUX B) = x1'
R1 feeds the adder and MUX B directly. R3 and R2 feed MUX A, whose output is the adder's second input, so one adder serves both the x1x2 and the x1x2' lines. The adder output loads straight into R4 and also feeds MUX B, which chooses between that adder output and R1 direct for whatever loads into R2.

Try it: control gate derivation trainer

Toggle x1, x2 and x3 the way an exam question sets them, then press Clock. Every register transfer below reads old values and lands together on one edge, same rule as the swap above.

control function: none
Micro-operations firing this clocknone
R1 terminal-
R2 terminal-
R3 terminal-
R4 terminal-
R15
R23
R310
R4160
Start R1
Start R2
Start R3
Start R4

Derived control gates, live

Chapter 05

The memory unit

Every memory unit boils down to the same three pieces: an address register that says which word, a data register that carries the word in or out, and two control lines, Read and Write, that say which direction. Get the sizing rules for AR and DR right and the rest of this chapter is bookkeeping.

Structure: AR, DR, Read, Write

A memory unit is a collection of words. To use one word out of the collection, hardware needs to know two things: which word, and what to do with it. The Address Register (AR) holds which word. The Data Register (DR) holds the word itself, whatever is going in on a write or coming out on a read. Two control lines, Read and Write, say which of those two directions this access is.

Nothing in the memory unit can be addressed without going through AR first, and nothing can move in or out without passing through DR. That is the whole structure.

The two sizing rules

These two rules answer almost every sizing question this chapter asks. Given a word count, take log base 2 to get AR's width. Given a word size, that number is DR's width directly, no conversion needed. Multiply the two (words times bits per word) for total capacity.

Worked sizing examples

MemoryWordsARWord size mDRTotal size
128 x 52^7 = 1287 bits5 bits5 bits640 bits
4096 x 162^12 = 409612 bits16 bits16 bits65536 bits = 8 KB
256K x 322^18 = 26214418 bits32 bits32 bits8388608 bits = exactly 1 MB

A 4-word memory is the smallest case worth checking by hand: 4 = 2^2, so AR is only 2 bits, enough for addresses 0, 1, 2, 3 and nothing else.

Read Write 5 bits per word 0 1 2 . . . 127 AR 7 bits address DR 5 bits data
The 128 x 5 memory as the lecturer draws it: 128 word slots stacked in a column, each 5 bits wide, addressed by a 7-bit AR and read or written through a 5-bit DR. Read and Write are separate control lines, never both asserted at once.

Read and write as micro-operations

Exam questions in this chapter give a register-transfer line and ask what the memory does with it. The method is always the same: work out whether AR is already loaded or needs loading first, then decide whether the transfer moves data into memory, a Write, or out of it, a Read.

TransferKindMicro-operation sequence
R5 <- M[AR]ReadDR <- M[AR], R5 <- DR
M[AR] <- R2WriteDR <- R2, M[AR] <- DR
R3 <- M[R1]ReadAR <- R1, DR <- M[AR], R3 <- DR
M[R2] <- R5WriteAR <- R2, DR <- R5, M[AR] <- DR
M[R5] <- M[R3]Read, then writeAR <- R3, DR <- M[AR], AR <- R5, M[AR] <- DR

The last one is the one people get wrong under time pressure. M[R5] <- M[R3] looks like a single move, but memory cannot talk to memory directly, everything routes through DR. Read the source first, AR <- R3 then DR <- M[AR], only then repoint AR at the destination and write DR back out, AR <- R5 then M[AR] <- DR. DR just sits there holding the value while AR is being reloaded in between, nothing else touches it.

Memory sizing calculator

Enter a word count (128, 4096, 256K, 1M, and so on) and a word size in bits, and see AR, DR, and total capacity worked out the way the exam wants it shown.



AR-
DR-
Address range (decimal)-
Address range (hex)-
Total size-
Bytes-
KB-
MB-

Read and write animator

Pick one of the five micro-operation sequences above and step through it. Watch AR load, Read or Write go high, DR fill, and the destination update, one micro-operation at a time. The 8-word memory and the registers below are editable.

Registers, this small model uses an 8-word memory, addresses 0-7:

AR
R1
R2
R3
R5

Memory, 8 words, the highlighted column is the address AR currently points at:

- -
Current micro-operation-
AR-
DR-
Read line0
Write line0

    Chapter 06

    The bus, built from multiplexers

    A bus is not a special piece of hardware, it is a set of shared lines plus a pile of multiplexers, one per bit, all steered by the same selection lines. Once you can count muxes and selection lines from a register count, the rest of this chapter is just reading the diagram.

    What a bus is

    An m-bit bus is m separate lines connecting 2^n registers, so that any one register can put its contents on the shared lines and any other register can take them off. Only one register drives the bus at a time.

    Building an m-bit bus for 2^n registers

    That is the exam answer, in full: count the bits in the bus, that is how many muxes to draw. Count the registers, that decides each mux's size and how many selection lines feed all of them at once. The same n selection lines drive every mux together, so all m muxes always pick the same register at the same time, and exactly one register's worth of bits appears on the bus.

    Worked example: a 3-bit bus connecting 4 registers

    Registers A, B, C, D, each 3 bits wide. m = 3, so 3 multiplexers. 2^n = 4, so n = 2 selection lines. Each mux is a 4-to-1 mux.

    A 2 1 0 B 2 1 0 C 2 1 0 D 2 1 0 mux 2 0 1 2 3 mux 1 0 1 2 3 mux 0 0 1 2 3 bus bit 2 bus bit 1 bus bit 0 S1 S0, shared select lines
    Mux 2 takes bit 2 of every register, mux 1 takes bit 1, mux 0 takes bit 0. The dashed line is S1 S0, the same two selection lines feeding all three muxes at once, so all three always agree on which register is on the bus.
    S1 S0Bus gets
    00A
    01B
    10C
    11D

    Worked example: a 2-bit bus connecting 8 registers

    Same rule, different numbers. m = 2, so 2 multiplexers. 2^n = 8, so n = 3 selection lines. Each mux is an 8-to-1 mux.

    Multiplexers2
    Mux size8-to-1
    Selection lines3 (S2 S1 S0)
    Registers reachable8

    A transfer over the bus

    C <- A on the 4-register bus above: set S1 S0 = 00 to put A on the bus, and assert Load on C. Nothing else changes, every other register's Load stays off, so only C actually captures the value while it passes by.

    Why buses matter

    Without a shared bus, connecting every register directly to every other register needs a separate path for every pair, and that count explodes, on the order of r^2 wires for r registers, not r. A bus trades that away: only one transfer can happen per pulse, in exchange for one shared set of m lines that scales with r instead of r^2. That saved wiring is the entire reason the mux-based bus exists.

    Bus builder

    Set the word size and the number of registers, and see how many muxes it takes, of what size, and how many selection lines. Then transfer a value across the bus and watch the selection bits, the bus contents, and the Load line.



    Multiplexers needed-
    Mux size-
    Selection lines-

    Live transfer:

    ->
    Selection bits-
    Bus contents-
    Load asserted-

    Chapter 07

    Instruction formats and how wide each field must be

    Mano's second chapter opens with three ideas the lecturer turns straight into exam questions: a program sitting in memory looks no different from the data next to it, an instruction word is built from an opcode field, a memory address field and a register field, each one sized by how many things it has to name, and every address field can either point straight at an operand or at a pointer to one. This chapter works through the sizing arithmetic and the direct versus indirect comparison exactly the way the board asks for them.

    A program in memory

    Memory holds 2^m words, so naming every one of them takes an m-bit address. A program is nothing more than a run of instruction words sitting in memory, usually starting at the low addresses, with its data operands placed wherever the programmer put them. No bit anywhere in a word says it is an instruction or says it is data, memory just holds patterns. The only thing that decides what gets executed next is the path the program counter walks through memory. Fetch the address of a data word by mistake and the machine will happily try to execute it.

    program 0 I opcode address 1 next instruction ... ... data 350 10 351 -7
    Address 0 holds an instruction, split into its fields. Address 350 holds the plain data word 10. Nothing in memory marks either row as instruction or data, only whether the program counter's path lands on it decides what the machine fetches next as an instruction.

    The general instruction format

    The format itself is simple: an instruction word is an opcode field, then a memory address field, then a register address field, laid out one after another. What the exam actually tests is how wide each field has to be, and that comes down to three rules.

    For 2^n distinct instructions, the opcode field needs n bits, one pattern for every instruction.

    For a memory of 2^m words, the memory address field needs m bits, one pattern for every address.

    For 2^k registers, the register address field needs k bits, one pattern for every register.

    opcode address register n bits m bits k bits 2^n instructions 2^m words 2^k registers
    Every field is sized by the count it has to select from, not by habit. Change any of n, m or k and the field width changes with it.

    Worked sizing examples

    The same rule, applied to the numbers the exam actually hands out. All verified by working the power of two out in full.

    SetupAs a power of twoField width
    4096-word memory, 16-bit word4096 = 2^1212-bit address field, this is the Mano basic computer's own address field and word size
    32 instructions32 = 2^55-bit opcode field
    16 registers16 = 2^44-bit register field
    16-word memory16 = 2^44-bit address field
    32-word memory32 = 2^55-bit address field

    4096 = 2^12 256K = 2^18 1M = 2^20 worth recognising on sight, they come up again and again.

    The exam question from the sheet: a computer has memory 256K word x 32. 256K is 2^18, so the memory address field needs 18 bits, and each word is 32 bits wide. Spend 18 of those 32 bits on the address and 14 are left over for the opcode and any register field together.

    Word size32 bits
    Address field, 256K = 2^1818 bits
    Left for opcode + register32 - 18 = 14 bits

    Instruction format calculator

    Enter counts, see the bit width each field needs and whether they fit the word. Memory words accepts a plain number or a K/M suffix, 4096, 256K, 1M and so on.

    Instructions
    Memory words
    Registers
    Word size (bits)

    -

    Direct versus indirect addressing

    The address field of a memory-reference instruction can be read two ways, controlled by a single bit, I. With I=0, addressing is direct: the address field holds the address of the operand itself, one memory read gets it. With I=1, addressing is indirect: the address field holds the address of a word that itself holds the operand's address, so it takes an extra memory read to get there.

    The board's example: ADD 350, I=1. M[350] = 720, and M[720] holds the operand to add. Because I=1, the address field is not the operand's address, it is the address of a pointer. The machine reads M[350] first, gets 720, then reads M[720] to get the actual operand. The effective address, the address the operand actually sits at, is 720, not 350.

    M[350] = 720 M[720] = operand effective address = 720
    The address field never touches the operand directly, it only gets there by way of the pointer stored at 350.
    Direct, I = 0Indirect, I = 1
    Memory accesses to reach the operand12
    What the address field holdsthe operand's own addressthe address of a word that itself holds the operand's address
    How far it can reachonly an address that fits inside the address fieldany address in memory, the field only has to reach the pointer
    Worth the extra read whennot needed, the direct address already fitsthe real target address does not fit in the address field, or the code is following a pointer variable

    Direct versus indirect, step by step

    Flip I, press Step to reveal one memory access at a time, and watch the count grow.

    InstructionADD 350
    I bit
    M[350]
    M[720]

    Accesses so far0 accesses
    Statusready, press Step
    #address readvalue found
    Direct (I=0): not yet run Indirect (I=1): not yet run

    Mano's own numbers

    Chapter 08

    Micro-operations, step by step

    Chapter 4 drew fetch, decode, execute, memory, writeback as five soft-edged boxes. Here they get pinned to a clock: a real machine, the basic computer from Mano's Computer System Architecture, ticking through named timing signals T0, T1, T2, one register transfer per pulse.

    The registers on the board

    Everything the basic computer does is a transfer between a small set of registers, all wired to one common data path. PC (12 bits) holds the address of the next instruction. AR (12 bits) is the address register, whatever memory cell is about to be read or written. IR (16 bits) holds the instruction currently being decoded. DR (16 bits) is the data register, a staging area for an operand fetched from memory. AC (16 bits) is the accumulator, where arithmetic and logic results land. E is a single carry bit sitting just outside AC(15), catching whatever overflows out of an add or a circular shift. SC, the sequence counter, is the register that makes this whole chapter possible: it counts 0, 1, 2, 3... and each value is a timing signal, T0, T1, T2, that pulses exactly one register transfer into existence. Three more one-bit flags, FGI, FGO and IEN, plus two 8-bit registers INPR and OUTR, handle input and output the same way.

    Memory is 4096 words of 16 bits, so an address is 12 bits, written as three hex digits, 000 to FFF. A register transfer is the smallest thing that can happen on one clock pulse, one register's value copied, added, or shifted into another. A micro-operation is just the name for that one transfer, written the way it goes on the board: destination on the left, an arrow, source on the right.

    The instruction word

    Every instruction is one 16-bit word. Bit 15 is I, the indirect flag. Bits 14 to 12 are the opcode, three bits, eight possible patterns, decoded straight into eight signals D0 through D7, one active at a time. Bits 11 to 0 are the address field. For D0 through D6 that field is an address in memory. For D7 it is not an address at all, each of its 12 bits picks out one register-reference or input-output operation, only one bit is ever set.

    I opcode address bit 15 bits 14-12 bits 11-0 indirect flag D0..D7 operand, or opcode bits when D7
    The same 16 bits mean two different things depending on the opcode: an address for D0-D6, twelve individual switches for D7.

    Fetch and decode: the first three pulses, always

    Every instruction, no matter which one, starts the same way. T0 copies PC into AR, aiming the next read at the current instruction. T1 reads that word into IR and, in the same pulse, advances PC to the following instruction, so the return address is already correct before the current instruction has even been decoded. T2 splits IR apart: the opcode bits pick D0-D7, the low 12 bits move into AR as a candidate address, and I is latched from bit 15. Nothing here depends on which instruction it is, D isn't even known until T2 finishes.

    What happens at T3 is the first fork. If the opcode is D0-D6, a memory-reference instruction, T3 checks I. When I = 1 the address field is not the final address, it points at a memory cell that holds the real one, so T3 does one extra read, AR <- M[AR], to chase that pointer down to the effective address. When I = 0, AR already holds the effective address, and T3 does nothing at all. If the opcode is D7, there is no address to resolve, T3 is where the instruction actually executes, register-reference if I = 0, input-output if I = 1, and SC is reset to 0 in that same pulse.

    Three families, one branch at T2

    D0 through D6 are the seven memory-reference instructions, AND, ADD, LDA, STA, BUN, BSA, ISZ, each finishing its own execute states after T3, DR <- M[AR] for the ones that need an operand, then the actual add, load, store, or branch. D7 with I = 0 is the twelve register-reference instructions, CLA, CLE, CMA, CME, CIR, CIL, INC, SPA, SNA, SZA, SZE, HLT, none of them touch memory, each is a single bit in IR(0-11) decoded into one register transfer at T3. D7 with I = 1 is the six input-output instructions, INP, OUT, SKI, SKO, ION, IOF, same shape, also done in one shot at T3.

    Trace it yourself

    The tracer below runs the exact engine described above, one T state per press of Step. Pick any of the 25 instructions from the preset list, direct or indirect, or hand-edit PC, AC, E and memory yourself. It starts loaded with the worked indirect ADD from the board: PC = 020, AC = 7EC3, M[020] = 932E, M[32E] = 09AC, M[9AC] = 8B9F.

    Register-transfer tracer

    Step advances one T state. Run finishes the instruction in progress. Reset restarts the setup currently loaded.

    -

    IR decode

    I (bit 15) = - opcode (14-12) = - address (11-0) = - instruction: -

    Registers

    PC-
    AR-
    IR-
    DR-
    AC-
    E-
    SC-

    I/O registers

    INPR-
    OUTR-
    FGI-
    FGO-
    IEN-

    Memory in play

    addressvaluejust

    Trace log

    Tmicro-operationwith valueswhat just happened

    Mano basic computer, every instruction traced

    The basic computer has 25 instructions in three families: 7 memory-reference instructions (opcode in D0-D6), 12 register-reference instructions (opcode D7 with I=0), and 6 input/output instructions (opcode D7 with I=1). Every instruction starts with the same fetch and decode, shown once below, then branches into its own execute states. Each trace below has two columns for every T state: the symbolic micro-operation as the book writes it, and the same line with the actual hex values from that example substituted in, so you can see the register transfer happen, not just describe it.

    Common phases, every instruction runs these first

    D0..D7 come from decoding IR(12-14). T3 only does something when the instruction is memory-reference and indirect.

    T statemicro-operation
    T0AR <- PC
    T1IR <- M[AR], PC <- PC + 1
    T2D0..D7 <- decode IR(12-14), AR <- IR(0-11), I <- IR(15)
    T3 (memory-reference only)if I = 1 then AR <- M[AR], else nothing

    SC is the sequence counter that drives T0, T1, T2... forward. Every instruction below ends by writing SC <- 0, which is what sends control back to T0 for the next fetch. HLT is the one exception: it clears the start-stop flip-flop instead, so SC never gets the chance to restart.

    mnemonicdirect hexindirect hexD signalwhat it doesT states
    AND0xxx8xxxD0AC <- AC and M[addr]6 (7 indirect)
    ADD1xxx9xxxD1AC <- AC + M[addr], carry into E6 (7 indirect)
    LDA2xxxAxxxD2AC <- M[addr]6 (7 indirect)
    STA3xxxBxxxD3M[addr] <- AC5 (6 indirect)
    BUN4xxxCxxxD4PC <- addr, unconditional branch5 (6 indirect)
    BSA5xxxDxxxD5store return address at addr, PC <- addr + 16 (7 indirect)
    ISZ6xxxExxxD6M[addr] <- M[addr] + 1, skip next if result is 07 (8 indirect)
    CLA7800-D7AC <- 04
    CLE7400-D7E <- 04
    CMA7200-D7AC <- complement of AC4
    CME7100-D7E <- complement of E4
    CIR7080-D7circular shift right through E4
    CIL7040-D7circular shift left through E4
    INC7020-D7AC <- AC + 14
    SPA7010-D7skip next if AC is positive (AC(15) = 0)4
    SNA7008-D7skip next if AC is negative (AC(15) = 1)4
    SZA7004-D7skip next if AC = 04
    SZE7002-D7skip next if E = 04
    HLT7001-D7clear the start-stop flip-flop, halt4
    INP-F800D7, I=1AC(0-7) <- INPR, FGI <- 04
    OUT-F400D7, I=1OUTR <- AC(0-7), FGO <- 04
    SKI-F200D7, I=1skip next if FGI = 14
    SKO-F100D7, I=1skip next if FGO = 14
    ION-F080D7, I=1IEN <- 1, enable interrupts4
    IOF-F040D7, I=1IEN <- 0, disable interrupts4

    The 7 memory-reference instructions, full traces

    Each of these needs an operand or a target address in memory, which is why they go through T3 and T4 (and sometimes T5, T6). The address in the instruction word is only the final address when I = 0. When I = 1, that address instead points at a memory cell holding the real address, so T3 does one extra fetch to chase the pointer, that is the indirect trace below under ADD.

    AND, direct

    Starting state: PC = 100, AC = F0F0, M[100] = 0020 (the AND instruction itself), M[020] = 0FF0.

    Tsymbolicwith values
    T0AR <- PCAR <- 100
    T1IR <- M[AR], PC <- PC + 1IR <- M[100] = 0020, PC <- 101
    T2D0, AR <- IR(0-11), I <- IR(15)D0, AR <- 020, I <- 0
    T3I = 0, nothingI = 0, nothing
    T4DR <- M[AR]DR <- M[020] = 0FF0
    T5AC <- AC and DR, SC <- 0AC <- F0F0 and 0FF0 = 00F0, SC <- 0

    Finished: PC = 101, AC = 00F0, E unchanged, M[020] unchanged at 0FF0.

    ADD, indirect

    Starting state: PC = 200, AC = FFF8, E = 0, M[200] = 9050 (ADD, I=1, address field 050), M[050] = 300 (the pointer), M[300] = 0009 (the real operand).

    Tsymbolicwith values
    T0AR <- PCAR <- 200
    T1IR <- M[AR], PC <- PC + 1IR <- M[200] = 9050, PC <- 201
    T2D1, AR <- IR(0-11), I <- IR(15)D1, AR <- 050, I <- 1
    T3I = 1, AR <- M[AR]AR <- M[050] = 300
    T4DR <- M[AR]DR <- M[300] = 0009
    T5AC <- AC + DR, E <- Cout, SC <- 0AC <- FFF8 + 0009 = 0001, E <- 1, SC <- 0

    Finished: PC = 201, AC = 0001, E = 1. FFF8 + 0009 is 10001 in 17 bits, the low 16 bits go into AC and the carry out of bit 15 goes into E, that is the indirect fetch at T3 chasing M[050] to find the real operand address 300.

    LDA, direct

    Starting state: PC = 010, M[010] = 2033, M[033] = 7E2D, AC = 0000.

    Tsymbolicwith values
    T0AR <- PCAR <- 010
    T1IR <- M[AR], PC <- PC + 1IR <- M[010] = 2033, PC <- 011
    T2D2, AR <- IR(0-11), I <- IR(15)D2, AR <- 033, I <- 0
    T3I = 0, nothingI = 0, nothing
    T4DR <- M[AR]DR <- M[033] = 7E2D
    T5AC <- DR, SC <- 0AC <- 7E2D, SC <- 0

    Finished: PC = 011, AC = 7E2D. M[033] is untouched, a load only reads.

    STA, direct

    Starting state: PC = 050, M[050] = 3080, AC = 1234, M[080] = 0000.

    Tsymbolicwith values
    T0AR <- PCAR <- 050
    T1IR <- M[AR], PC <- PC + 1IR <- M[050] = 3080, PC <- 051
    T2D3, AR <- IR(0-11), I <- IR(15)D3, AR <- 080, I <- 0
    T3I = 0, nothingI = 0, nothing
    T4M[AR] <- AC, SC <- 0M[080] <- 1234, SC <- 0

    Finished: PC = 051, M[080] = 1234 (was 0000), AC unchanged at 1234. STA has no T5, there is nothing left to compute once the write happens.

    BUN, direct

    Starting state: PC = 005, M[005] = 4200.

    Tsymbolicwith values
    T0AR <- PCAR <- 005
    T1IR <- M[AR], PC <- PC + 1IR <- M[005] = 4200, PC <- 006
    T2D4, AR <- IR(0-11), I <- IR(15)D4, AR <- 200, I <- 0
    T3I = 0, nothingI = 0, nothing
    T4PC <- AR, SC <- 0PC <- 200, SC <- 0

    Finished: PC = 200. The 006 that T1 wrote during fetch is simply thrown away, PC is overwritten again at T4.

    BSA, direct

    Starting state: PC = 010, M[010] = 5100, M[100] = 0000 (a reserved cell for the return address).

    Tsymbolicwith values
    T0AR <- PCAR <- 010
    T1IR <- M[AR], PC <- PC + 1IR <- M[010] = 5100, PC <- 011
    T2D5, AR <- IR(0-11), I <- IR(15)D5, AR <- 100, I <- 0
    T3I = 0, nothingI = 0, nothing
    T4M[AR] <- PC, AR <- AR + 1M[100] <- 011, AR <- 101
    T5PC <- AR, SC <- 0PC <- 101, SC <- 0

    Finished: PC = 101, M[100] = 011. This is a subroutine call, the return address (011, the instruction right after the BSA) is parked at the subroutine's own entry cell, and the subroutine body starts one word later at 101. Returning later is just BUN indirect through address 100.

    ISZ, direct

    Starting state: PC = 002, M[002] = 6050, M[050] = FFFF.

    Tsymbolicwith values
    T0AR <- PCAR <- 002
    T1IR <- M[AR], PC <- PC + 1IR <- M[002] = 6050, PC <- 003
    T2D6, AR <- IR(0-11), I <- IR(15)D6, AR <- 050, I <- 0
    T3I = 0, nothingI = 0, nothing
    T4DR <- M[AR]DR <- M[050] = FFFF
    T5DR <- DR + 1DR <- FFFF + 1 = 0000
    T6M[AR] <- DR, if DR = 0 then PC <- PC + 1, SC <- 0M[050] <- 0000, DR = 0 so PC <- 004, SC <- 0

    Finished: PC = 004, M[050] = 0000. FFFF + 1 wraps to 0000 in 16 bits, which is exactly the case ISZ is built to catch, so the skip fires and the instruction right after ISZ is jumped over. Had M[050] held anything else, PC would have stopped at 003.

    The 12 register-reference instructions

    None of these touch memory. Each one is a single control bit in IR(0-11) decoded straight into a register transfer, so they all execute in one shot at T3 and then set SC <- 0.

    CLA 7800

    AC <- 0

    beforeAC = 3C7F
    afterAC = 0000

    CLE 7400

    E <- 0

    beforeE = 1
    afterE = 0

    CMA 7200

    AC <- complement of AC (every bit flipped)

    beforeAC = 00FF
    afterAC = FF00

    CME 7100

    E <- complement of E

    beforeE = 0
    afterE = 1

    CIR 7080

    AC <- shr AC, AC(15) <- E, E <- AC(0). AC and E form one ring, everything moves one step right.

    beforeAC = 0001, E = 1
    afterAC = 8000, E = 1

    The old bit 0 of AC (a 1) becomes the new E, and the old E (also a 1) slides into the vacated bit 15 of AC.

    CIL 7040

    AC <- shl AC, AC(0) <- E, E <- AC(15). Same ring, one step left.

    beforeAC = 8001, E = 0
    afterAC = 0002, E = 1

    The old bit 15 of AC (a 1) becomes the new E, and the old E (a 0) slides into the vacated bit 0 of AC.

    INC 7020

    AC <- AC + 1

    beforeAC = 00FF
    afterAC = 0100

    HLT 7001

    Clears the run flip-flop instead of SC, so the machine stops instead of fetching the next instruction.

    beforerunning = 1
    afterrunning = 0, halted

    The four skip instructions all follow the same shape, test a condition on AC or E, and if it holds, add one more increment to PC on top of the one T1 already did during fetch, so the instruction right after the skip is jumped over.

    instrconditiontest case, skip takentest case, no skip
    SPA 7010AC(15) = 0AC = 0F00, PC 021 -> 022AC = 8500, PC 021 -> 021
    SNA 7008AC(15) = 1AC = 8500, PC 030 -> 031AC = 0F00, PC 030 -> 030
    SZA 7004AC = 0AC = 0000, PC 040 -> 041AC = 0001, PC 040 -> 040
    SZE 7002E = 0E = 0, PC 050 -> 051E = 1, PC 050 -> 050

    The 6 input/output instructions

    These use opcode D7 like the register-reference group, but with I = 1, and they also execute in one shot at T3. They talk to two 1-bit flags: FGI goes to 1 when a character has arrived in INPR and is waiting to be read, FGO goes to 1 when OUTR is free and ready to accept the next character. A program never assumes the device is ready, it polls SKI (or SKO) first and only issues INP (or OUT) once the flag confirms it, otherwise it would read a stale character or overwrite one the device hasn't taken yet.

    instrhexactionexample
    INPF800AC(0-7) <- INPR, FGI <- 0INPR = 41, AC 0000 -> 0041, FGI 1 -> 0
    OUTF400OUTR <- AC(0-7), FGO <- 0AC = 1259, OUTR 00 -> 59, FGO 1 -> 0
    SKIF200if FGI = 1 then PC <- PC + 1FGI = 1: PC 060 -> 061  |  FGI = 0: PC 060 -> 060
    SKOF100if FGO = 1 then PC <- PC + 1FGO = 1: PC 070 -> 071  |  FGO = 0: PC 070 -> 070
    IONF080IEN <- 1IEN 0 -> 1
    IOFF040IEN <- 0IEN 1 -> 0

    Chapter 09

    The control unit and the common bus

    Every register transfer in the basic computer, AR <- PC, DR <- M[AR], AC <- DR, and every other one, is driven by exactly one thing: a control signal that is high for exactly one clock pulse, built by ANDing a decoded opcode with a decoded timing state. This chapter is how that AND-OR machinery gets built, and how one 16-bit bus carries every one of those transfers.

    The registers this chapter moves data between

    Nothing new here, just the full list in one place, since every example below names one of these.

    registerwidthrole
    AR12address register, holds a memory address
    PC12program counter
    DR16data register, holds an operand fetched from memory
    AC16accumulator
    IR16instruction register, holds the instruction currently executing
    TR16temporary register
    OUTR8output register
    INPR8input register
    E1carry out of AC
    SC4sequence counter, counts the T states

    From instruction word to control signal

    Once IR holds the current instruction, three things happen to it at once. IR bit 15 loads straight into the I flip-flop, the indirect bit. IR bits 12 through 14, the opcode, feed a 3x8 decoder that produces eight lines, D0 through D7, one per opcode value. IR bits 0 through 11 are the address field, and they go straight to the control logic as is, no decoding needed, since the address is used directly rather than tested against fixed values.

    A decoder is a circuit with a fixed rule: for any binary number on its input, exactly one output line goes high, all the rest stay low. That is the whole reason this design works. Feed the 3x8 decoder a 3-bit opcode and exactly one of D0..D7 is high. Feed a 4x16 decoder a 4-bit number and exactly one of T0..T15 is high. Since D0..D7 and T0..T15 are each guaranteed one-hot, ANDing one specific D with one specific T, a term like D5T4, is true at exactly one instant across the entire run of the machine: the one clock pulse where the current instruction is D5 and the current timing state is T4. That is why a control condition is written as a product of signals like this, and why building a control line is just OR-ing a handful of these products together, a sum of products, one product per micro-operation that needs to fire.

    The sequence counter, SC, is a 4-bit counter feeding its own 4x16 decoder to make T0 through T15. SC increments by 1 on every clock pulse by default, which is what walks the timing signal forward, T0, T1, T2, and so on. The micro-operation SC <- 0 is a clear, not an increment, and it is itself one of the control signals the logic below emits. Almost every instruction ends by asserting it, which snaps SC back to 0 so the very next clock pulse lands on T0 again, the start of fetching the next instruction. HLT is the one exception: it clears the run flip-flop instead, so the clock stops advancing SC at all.

    IR bit 15 (I) IR bits 14-12 IR bits 11-0 IR (16 bits) I flip-flop 3x8 decoder D0 D1 D2 D3 D4 D5 D6 D7 exactly one high at a time +1 each clock pulse SC (4-bit) 4x16 decoder T0 T1 T2 T3 T4 T5 T6 T7 T8 T9 T10 T11 T12 T13 T14 T15 exactly one high at a time Control logic gates D0..D7, T0..T15, I, address bits a product like D5T4 is true for one instant only Control signals LD, INR, CLR, R/W IR bit 15 loads the I flip-flop IR bits 14 to 12 feed the opcode decoder The 3x8 decoder produces D0 through D7 IR bits 11 to 0 reach the control logic gates as the address field SC feeds the 4x16 timing decoder The 4x16 decoder produces T0 through T15 D0 through D7 reach the control logic gates T0 through T15 reach the control logic gates I reaches the control logic gates The control logic gates emit the control signals
    D0..D7 and T0..T15 are each guaranteed one-hot by their decoders, so a term like D5T4 names exactly one clock pulse. The control logic gates block combines these with I and the raw address bits to drive LD, INR, CLR on every register and read or write on memory. SC <- 0 is one of those control signals too, it clears SC so the next pulse restarts at T0.

    The common bus: one line, seven drivers

    The whole machine shares one 16-bit path, the common bus. At any instant at most one unit is allowed to put a value on it, chosen by a 3-bit code, S2 S1 S0. Every register also has three control inputs of its own: LD loads whatever is currently on the bus, INR increments the register's own current value by 1, and CLR resets it to 0. INR and CLR never touch the bus at all, they just tell the register to update itself from its own held value, that is why they only need the register's own enable line, not a bus selection.

    S2 S1 S0unit on the bus
    000nothing
    001AR
    010PC
    011DR
    100AC
    101IR
    110TR
    111Memory

    Reading a register transfer as bus traffic is just naming these three facts every time: who drives the bus, what S2S1S0 that means, and which control input on the destination fires. AR <- PC means set S2S1S0 = 010 so PC drives the bus, and assert LD(AR) so AR captures it on the clock edge. Nothing more happens on that pulse, PC's own value is untouched, only AR changes.

    The S bits themselves come from a small encoder, not a hand-picked table. Give it seven inputs, x1 through x7, one per source, with exactly one allowed high at a time, and it produces: S0 = x1 + x3 + x5 + x7, S1 = x2 + x3 + x6 + x7, S2 = x4 + x5 + x6 + x7. Set x2 (PC) high and everything else low: S0 = 0, S1 = 1, S2 = 0, which is 010. That is the whole trick, S2S1S0 is just the binary number of whichever xN is currently 1, computed by three OR gates instead of a lookup table.

    S2 S1 S0 selector 000 16-bit common bus AR bus 1 LDINRCLR PC bus 2 LDINRCLR DR bus 3 LDINRCLR AC bus 4 LDINRCLR IR bus 5 LDINRCLR TR bus 6 LDINRCLR Memory bus 7 R / W address to memory always comes from AR, this line does not use the bus or the selector
    Seven units, seven bus numbers, one 16-bit line. The S2 S1 S0 selector decides who drives it. Memory has no LD, INR, or CLR, only a read or a write, and its address input is wired straight to AR at all times, whether or not AR is the one on the bus that pulse.

    Bus traffic explorer

    Pick a register transfer and see exactly what the hardware does with it: the S bits, who drives the bus, which control input fires, and the encoder equations that produce those S bits.

    S2 S1 S0-
    drives the bus-
    control input asserted-
    typical timing-

    Encoder equations, this transfer's x values substituted in:

    Reading a transfer as bus traffic, worked examples

    IR <- M[AR]: Memory is the source, so S2S1S0 = 111. That asserts LD(IR), and AR is already holding the address (PC was copied into it one T state earlier), so this is the fetch, reading the instruction word out of memory and into IR.

    M[AR] <- AC: AC is the source now, S2S1S0 = 100. But the destination is memory, which has no LD, only a write strobe, and memory's address input is hardwired to AR, not selected by S2S1S0. So this pulse is: AC's value goes out onto the bus, memory's write line fires, and the address it writes to is whatever AR already holds, all in the same clock pulse. This is STA.

    AR <- AR + 1: no unit is selected, S2S1S0 = 000, "nothing" is on the bus, because this is an increment, not a load. AR just adds 1 to its own current value and INR(AR) is the only signal that fires. The bus plays no part at all. This is the AR increment inside BSA.

    transferS2S1S0drives buscontrol inputtypical timing
    AR <- PC010PCLD(AR)R'T0
    IR <- M[AR]111MemoryLD(IR)R'T1
    DR <- M[AR]111MemoryLD(DR)D0T4, D1T4, D2T4 or D6T4
    AC <- DR011DRLD(AC)D2T5
    M[AR] <- AC100ACmemory writeD3T4
    PC <- AR001ARLD(PC)D4T4
    AR <- M[AR]111MemoryLD(AR)D7'IT3
    TR <- PC010PCLD(TR)RT0
    M[AR] <- TR110TRmemory writeRT1
    AR <- AR + 1000nothingINR(AR)D5T4
    AC <- 0000nothingCLR(AC)D7I'T3, CLA bit

    Deriving control gates: the method

    Every register on the bus needs three control expressions built for it, LD, CLR, and INR. The method is the same every time: go through the entire instruction set, write down every single micro-operation that loads that register, and OR their control conditions together, that sum is LD for that register. Do the same separately for every micro-operation that clears it, that sum is CLR, and again for every one that increments it, that sum is INR. There is no cleverness beyond collecting the list correctly and OR-ing it.

    AR, worked in full

    Every micro-operation anywhere in the basic computer that touches AR.

    kindconditionmicro-operation
    LDR'T0AR <- PC
    LDR'T2AR <- IR(0-11)
    LDD7'IT3AR <- M[AR]
    CLRRT0AR <- 0
    INRD5T4AR <- AR + 1

    Three lines load AR, one clears it, one increments it. OR each group together:

    LD(AR)  = R'T0 + R'T2 + D7'IT3
    CLR(AR) = RT0
    INR(AR) = D5T4

    R is the interrupt flip-flop, R' means not in an interrupt cycle, the normal instruction cycle. Notice LD and CLR can never both be true at once here, R'T0 needs R = 0 and RT0 needs R = 1, they are mutually exclusive by construction, which is exactly what you want: AR should never be told to load and clear on the same pulse.

    PC, a further example

    Same method, different register.

    kindconditionmicro-operation
    LDD4T4PC <- AR (BUN branches to the resolved address)
    LDD5T5PC <- AR (BSA jumps into the subroutine body)
    CLRRT1PC <- 0 (interrupt cycle parks PC while the return address is saved)
    INRR'T1PC <- PC + 1 (every fetch moves PC to the next instruction)
    INRRT2PC <- PC + 1 (interrupt cycle advances PC once it is done)
    LD(PC)  = D4T4 + D5T5
    CLR(PC) = RT1
    INR(PC) = R'T1 + RT2

    AC, a further example

    AC has more going on than AR or PC, worth seeing why.

    kindconditionmicro-operation
    LDD2T5AC <- DR (LDA, the only time AC loads straight off the bus)
    CLRD7I'T3B11AC <- 0 (CLA)
    INRD7I'T3B5AC <- AC + 1 (INC)
    LD(AC)  = D2T5
    CLR(AC) = D7I'T3B11
    INR(AC) = D7I'T3B5

    D7I'T3 is the condition every register-reference instruction shares, D7 and not indirect, at T3, and B11, B5 pick out CLA's and INC's own bit inside IR(0-11), the same bit numbers behind the 7800 and 7020 hex codes from the instruction set chapter.

    Control signal builder

    Set the current D, the current T, and the I and R flip-flops, and see which control lines go high right now. The term that actually fired is underlined in each expression.

    Current D (exactly one, matching the one-hot decoder):

    Current T (exactly one):

    D0, T0, I=0, R=0

    The interrupt cycle, for completeness

    When R = 1, the machine is not fetching and executing the next instruction, it is running the interrupt cycle instead, using the same T0, T1, T2 timing states for a completely different purpose.

    T statemicro-operations
    RT0AR <- 0, TR <- PC
    RT1M[AR] <- TR, PC <- 0
    RT2PC <- PC + 1, IEN <- 0, R <- 0, SC <- 0

    This parks the interrupted program's return address (PC, saved into TR then written to memory location 0) and sends PC to address 1, where the interrupt service routine starts. IEN <- 0 stops the machine from interrupting itself again mid-service, and R <- 0 at RT2 is what hands control back to the normal R'T0 fetch on the very next pulse.

    Chapter 10

    Programming the basic computer

    The instruction set only matters once you can write a program with it. This chapter turns the opcode table into working code: an assembly source file, an assembler that turns it into hex, and a method for tracing that hex by hand, register by register, the way it gets checked on the board.

    Word size16 bits
    Memory4096 words
    Address3 hex digits
    RegistersPC AR IR DR AC E SC

    The instruction word

    Every word the machine fetches as an instruction has the same shape: bit 15 is the indirect bit I, bits 14 to 12 are the opcode, and bits 11 to 0 are a 12-bit address, which is why every address in this machine is written as 3 hex digits.

    I opcode address 15 14-12 11-0 1 bit 3 bits, D0..D7 12 bits, 3 hex digits
    When the opcode is 111 (D7), bits 11-0 stop being an address and instead pick a register-reference operation (I=0) or an input-output operation (I=1).

    A worked decode, the kind an exam gives you cold: M[21] = A222. Split the top hex digit into bits, A = 1010, so I=1 and opcode=010=D2. D2 is LDA. So A222 is an indirect LDA through address 222, not LDA of 222 itself. The effective address is whatever is stored at location 222, not 222.

    7 memory-reference 12 register-reference 6 input-output for 25 instructions total. Keep this table open while reading hex.

    MnemonicDirectIndirectMeaning
    AND0xxx8xxxAC <- AC and M[EA]
    ADD1xxx9xxxAC <- AC + M[EA], carry into E
    LDA2xxxAxxxAC <- M[EA]
    STA3xxxBxxxM[EA] <- AC
    BUN4xxxCxxxPC <- EA
    BSA5xxxDxxxM[EA] <- PC, PC <- EA+1 (call)
    ISZ6xxxExxxM[EA] <- M[EA]+1, skip next if 0
    HexMnemonicMeaning
    7800CLAAC <- 0
    7400CLEE <- 0
    7200CMAAC <- complement of AC
    7100CMEE <- complement of E
    7080CIRrotate AC right through E
    7040CILrotate AC left through E
    7020INCAC <- AC + 1
    7010SPAskip next if AC(15)=0
    7008SNAskip next if AC(15)=1
    7004SZAskip next if AC=0
    7002SZEskip next if E=0
    7001HLTstop
    HexMnemonicMeaning
    F800INPAC(7-0) <- input register
    F400OUToutput register <- AC(7-0)
    F200SKIskip next if input flag set
    F100SKOskip next if output flag set
    F080IONinterrupt on
    F040IOFinterrupt off

    From assembly source to machine code

    The path is source, assembler, binary. You write mnemonics and labels, the assembler turns each line into one 16-bit word, and the machine only ever sees the words. The assembler does this in two passes over the same source.

    Pass one walks the source top to bottom, keeping a running address called the location counter. It does not emit any code yet, it just watches for labels, a name written with a trailing comma at the start of a line, and records each one in a symbol table as label to address. Pass two walks the source again, and this time it emits a word for every instruction and every DEC or HEX, looking up any label it meets in the symbol table built in pass one. That is why a label can be used before it is defined, LDA can reference a data word three lines further down, because by the time pass two runs, pass one already knows where everything landed.

    Program structure

    The convention the source always follows: ORG N to set the starting address, then the instructions, then HLT, then the data words with their DEC or HEX labels, then END.

    The 4 pseudo-instructions

    Pseudo-opDoesProduces a word?
    ORG Nsets the location counter to N, so the next line is placed at address Nno
    ENDmarks the end of the source, assembly stops hereno
    DEC none word holding the signed decimal value n, stored in 16-bit two's complementyes
    HEX hone word holding the hex value h exactlyyes

    Worked example: F = A + B

    Load A, add B, store the sum in F, stop. The line-by-line source next to the hex it assembles to:

    AddressHexSource line
    1002104LDA A
    1011105ADD B
    1023106STA F
    1037001HLT
    104000AA, DEC 10
    1050014B, DEC 20
    1060000F, DEC 0

    LDA A and STA F assemble with direct addresses 104 and 106 because A and B are labels the symbol table already resolved by the time pass two ran. Result: F = 30.

    Worked example: F = A - B

    The machine has no subtract instruction, so subtraction is done as addition: A minus B is the same value as A plus the two's complement of B. Complementing every bit of B and adding 1 turns it into -B, so CMA then INC then ADD A computes A + (-B).

    AddressHexSource line
    1007800CLA
    1012108LDA B
    1027200CMA
    1037020INC
    1041107ADD A
    1053109STA F
    1067001HLT
    1070032A, DEC 50
    1080012B, DEC 18
    1090000F, DEC 0

    With A=50 and B=18: result F = 32.

    Writing a loop

    ISZ is what makes a loop possible without an extra compare instruction: it increments a memory word and skips the next instruction only if that word becomes 0. Set a counter to -(count), ISZ it once per pass, and the skip fires exactly when the counter reaches 0, right after the loop has run count times. BUN at the bottom sends control back to the top for every pass except the last.

    This program sums a small array using that technique, and also uses ISZ a second time to walk a pointer through the array, since this machine has no indexed addressing.

    AddressHexSource line
    100210ALDA N
    101310BSTA CTR
    1027800CLA
    1039109LOP, ADD PTR I
    1046109ISZ PTR
    105610BISZ CTR
    1064103BUN LOP
    107310FSTA SUM
    1087001HLT
    109010CPTR, HEX 10C
    10AFFFDN, DEC -3
    10B0000CTR, HEX 0
    10C0005NUM1, DEC 5
    10D000FNUM2, DEC 15
    10E0014NUM3, DEC 20
    10F0000SUM, HEX 0

    PTR starts pointing at NUM1 and is incremented by ISZ PTR every pass, so ADD PTR I reads a different array element each time through. CTR starts at -3 and reaches 0 after three passes, which is the pass where the skip finally fires and BUN LOP gets skipped. Result: SUM = 5 + 15 + 20 = 40.

    Hand-tracing a program

    The exam version of tracing is a table: one row per instruction executed, PC before the fetch, the instruction in hex and mnemonic, AC and E after it runs, and a note if it wrote memory. Tracing F = A + B from AC=0000, E=0:

    PCInstructionACEMemory changed
    1002104 LDA A000A0
    1011105 ADD B001E0
    1023106 STA F001E0F <- 001E
    1037001 HLT001E0

    Two isolated instruction traces, the kind used to check a single register-reference or memory-reference instruction in isolation:

    BeforeInstructionAfter
    AC=A334, E=07040 CILAC=4668, E=1
    AC=1234, M[333]=A5760333 AND 333AC=0034
    AC=00347020 INCAC=0035

    Try it: two-pass assembler

    Assemble Mano source

    Edit the source and press Assemble. Pass one builds the symbol table below, pass two produces the hex listing.

    Symbol table (pass one)

    LabelAddress

    Assembled output (pass two)

    LineAddressHexSource

    Try it: program tracer

    Trace the assembled program

    Assembling loads the program here. Set a starting AC and E to reproduce an exam question, then Step or Run.

    Start AC (hex)
    Start E (0 or 1)
    no program loaded
    PCInstruction (hex)MnemonicACEMemory changed

    Chapter 11

    The instruction set is the contract

    Software never touches a transistor directly, it only ever touches a fixed list of instructions the hardware promises to run, and that list is the entire deal between the two sides.

    The line between hardware and software

    The instruction set architecture, or ISA, is the list of instructions a processor understands, plus the rules for how they are encoded, what registers exist, and how memory is addressed. It is a contract, the chip promises every instruction on the list will do exactly what it says, and in exchange the software never has to know whether it is running on a cheap embedded core or a fast server core, as long as both implement the same ISA. A program compiled for RISC-V runs unmodified on either one, because both honor the same contract underneath very different circuitry.

    Two philosophies: RISC and CISC

    RISC (reduced instruction set computer) keeps every instruction simple and the same fixed size, and only load and store instructions touch memory, everything else works on registers. CISC (complex instruction set computer) allows instructions of varying length that can read and write memory directly as part of an arithmetic operation. RISC-V and ARM are RISC, x86 is CISC.

    RISC-V, register add

    add x1, x2, x3

    x1, x2, x3 are all registers. Nothing here can be a memory address, an add instruction only ever adds registers.

    x86-64, the same register add

    add eax, ebx

    x86 also allows add [rax], ebx, which reads a value from the memory address held in rax, adds ebx to it, and writes the result straight back to memory, all in one instruction. RISC-V has no such instruction, that would take a separate load, add, then store.

    Neither approach is free. CISC packs more work into one instruction, so a compiled program can be smaller, but the chip needs extra circuitry to handle instructions of different lengths and memory-touching arithmetic. RISC keeps every instruction the same size and shape, which makes the hardware that fetches and decodes them simpler and faster to build, at the cost of needing more instructions to do the same job.

    Anatomy of an encoded instruction

    An instruction is not text, it is a 32-bit number. The bits are split into fields, and which field means what depends on the instruction's format. This is the R-type format, used by register-to-register instructions like add, shown with the real bits for add x1, x2, x3:

    funct7 31:25 0000000 rs2 24:20 00011 x3 rs1 19:15 00010 x2 funct3 14:12 000 rd 11:7 00001 x1 opcode 6:0 0110011 R-type op bit 31 bit 0
    0000000 00011 00010 000 00001 0110011, or 0x3100b3. funct3 and funct7 together pick add over sub and the other R-type operations, opcode marks the instruction as R-type, rd, rs1, rs2 name registers.

    Addressing modes

    An addressing mode is a rule for where an instruction's data comes from. RISC-V keeps the list short:

    ModeExampleWhere the value comes from
    Registeradd x1, x2, x3directly out of a register, no calculation
    Immediateaddi x1, x2, 5a constant baked into the instruction itself
    Base plus offsetlw x1, 8(x2)memory at the value in x2, plus 8
    PC-relativebeq x1, x2, loopmemory at the current instruction's address, plus an offset, used for branches

    x86 adds several more, including register-indirect and scaled index, base plus index times 1, 2, 4, or 8, for stepping through arrays. Every addressing mode adds address-calculation logic that every instruction pays for in chip area and delay, whether it uses that mode or not, which is why RISC-V deliberately stops at these four.

    Registers versus memory

    A register is a storage slot built directly into the processor. RISC-V has 32 of them, and reading or writing one takes a fraction of a clock cycle. Memory sits off to the side, is enormously larger, and takes many cycles to reach. That is why every arithmetic instruction works on registers only, and why loading a value from memory, doing the math, then storing the result back, is a distinct, deliberate step rather than something arithmetic ever does for free.

    Registers stay few on purpose. Every register number an instruction names has to fit inside the instruction's fixed 32 bits, RISC-V spends 5 bits per register field, and 5 bits counts exactly 32 values, doubling to 64 registers would cost another bit on every field of every instruction that names a register. More registers also means more wiring, the register file needs a physical read or write connection for every register it can touch in one cycle, and that circuitry still has to finish inside a single clock tick. A handful of fast registers, backed by a large, slower main memory, is the right trade, because most values a program is actively working with fit in a handful of variables at any one time anyway.

    What the assembler and the compiler each do

    A compiler turns source code in a language like C into assembly, or directly into machine code. Along the way it decides which instructions to use and which values live in which registers, and it can reorder or eliminate work while preserving the program's behavior. An assembler takes assembly, the human-readable mnemonics like addi and beq, and translates each line into its exact 32-bit encoding, turning labels like loop into real addresses. Compiling is a many-to-many translation full of judgment calls, assembling is close to a one-to-one lookup.

    A small C function, and roughly what an unoptimized compiler produces for it:

    int sum_to(int n) {
        int s = 0;
        for (int i = 1; i <= n; i++) {
            s += i;
        }
        return s;
    }
    sum_to:
        addi t0, x0, 0     # s = 0
        addi t1, x0, 1     # i = 1
    loop:
        blt  a0, t1, done  # if n < i, exit loop
        add  t0, t0, t1    # s += i
        addi t1, t1, 1     # i++
        beq  x0, x0, loop  # repeat
    done:
        add  a0, t0, x0    # return s
        ret

    a0 holds the argument n, and later the return value, by RISC-V's calling convention. This is close to what an unoptimized compiler emits, a real optimizing compiler might keep s and i in different registers, unroll part of the loop, or, for a simple case like this one, work out the closed-form answer and skip the loop entirely.

    Chapter 12

    One instruction, one trip through the machine

    Every instruction a processor runs takes the same five-step trip through the same physical hardware, once per clock tick.

    Five stages, one instruction

    A single-cycle processor runs every instruction through the same five steps, in the same clock cycle:

    StageWhat happens
    Fetchread the instruction word at address PC out of instruction memory
    Decodesplit that word into opcode, register numbers, and immediate value, and figure out what kind of instruction it is
    Executerun the ALU, an add, a subtract, or a comparison for a branch
    Memoryif it is a load or a store, read or write data memory, every other instruction skips this step
    Writebackif the instruction produces a value, write it into the destination register

    In a single-cycle design, all five steps for one instruction finish inside one clock tick, then the next instruction starts its own five steps from scratch. Nothing overlaps.

    The datapath: the wiring that makes it happen

    The datapath is the physical hardware the five stages run on, the PC (a register holding the address of the current instruction), instruction memory, the register file, the ALU, and data memory, all connected by wires. Because the same wires carry every instruction, whether it is an add or a load, some of them need to carry one signal or another depending on what is running. That is what a mux, short for multiplexer, is for, a switch that picks one of several input wires to pass through, based on a control signal.

    Tiny CPU simulator

    Step through a small RISC-V-like program one stage at a time, or press Run to watch it play out. It sums 1 through 5.

    Fetch
    PC0

    Program

    Registers

    Data memory

    Press Step to begin.

    Watching one instruction cross the datapath

    The diagram below draws the same five stages as hardware blocks and wires. Step through it and watch which parts light up and why, then check the six control signals underneath, they are what tell the muxes which input to pass through for this particular instruction.

    Datapath walkthrough

    Shares its Step and Reset with the simulator above, both move the same instruction forward.

    Fetch
    Fetch: PC feeds instruction memory, next PC logic reads the current PC Decode: instruction fields reach the register file, the control unit, and the branch target adder Execute: the ALU combines register values, or a register and an immediate Memory: loads and stores reach data memory, every other instruction leaves it idle Writeback: the result reaches the register file, and the next PC is decided PC Instr Memory Register File Control opcode in, signals out M ALU Data Memory M Next PC
    RegWriteALUSrcMemReadMemWriteMemToRegBranch
    0 0 0 0 0 0

    The control unit is a lookup table

    The control unit does not compute anything, it looks up the opcode bits from the fetched instruction and outputs a fixed set of control signals, wires holding a 0 or a 1, connected straight into the muxes and into the enable pins of data memory and the register file. Six signals decide almost everything in this datapath:

    Signal1 means
    RegWritewrite the result into the destination register
    ALUSrcthe ALU's second input is the immediate, not a register
    MemReadread data memory
    MemWritewrite data memory
    MemToRegthe value written back comes from memory, not the ALU
    Branchthis instruction can change the PC instead of just moving to the next one

    For six opcodes, the whole control unit is a six-row table, opcode in, six bits out, no arithmetic involved. add and sub are 1,0,0,0,0,0. addi is 1,1,0,0,0,0. lw is 1,1,1,0,1,0. sw is 0,1,0,1,0,0. beq is 0,0,0,0,0,1. The table above the diagram shows the live row for whatever instruction the walkthrough is on.

    The clock, and why the slowest instruction sets the pace

    Every block in the datapath takes real time to produce an answer, a register file read takes some fraction of a nanosecond, an ALU add takes a bit more, a data memory access takes more still. In a single-cycle design, the clock period, the length of one tick, has to be long enough for the slowest instruction to finish its entire trip through fetch, register read, ALU, memory access, and writeback, all in series. That is normally lw, since it is the only instruction that uses every stage for real work. Every other instruction, including one that never touches memory, like add, still has to wait out the same fixed clock period, because the clock ticks at one fixed rate for the whole chip. A single-cycle processor is simple to build and simple to reason about, and it wastes most of a clock cycle on every instruction that is not the slowest one.

    Chapter 13

    The memory hierarchy

    No single kind of memory is both fast and big enough, so every real computer fakes it with a hierarchy, and how well your code respects that hierarchy can change its speed by an order of magnitude.

    The gap

    Registers are tiny (a few dozen to a few hundred) and about as fast as the CPU itself. Below that sit caches, usually three levels, each bigger and slower than the last: L1 (smallest, fastest, often per core), L2 (bigger, a bit slower), and L3 (bigger still, shared across cores). Below the caches is DRAM, the main memory, gigabytes in size but far slower than any cache. Below that is an SSD, terabytes in size but far slower again. Every step down trades speed for capacity, by a lot.

    The numbers below are approximate, real hardware varies by chip and generation, but the order of magnitude is stable and worth knowing. The last column rescales everything to human time by pretending a register access takes one second.

    LevelTypical latency (approx)If a register access took one second
    Registerabout 0.3 ns (roughly one clock cycle at 3 GHz)1 second
    L1 cacheabout 1 nsabout 3 seconds
    L2 cacheabout 4 nsabout 13 seconds
    L3 cacheabout 15 to 20 nsroughly a minute
    DRAM (main memory)about 100 nsroughly 5 to 6 minutes
    SSD (random read)roughly 50 to 150 microsecondsroughly 2 to 6 days
    Registers L1 L2 L3 DRAM SSD
    Narrower at the top means smaller and faster. Every step down is bigger and slower than the one above it.

    Why locality saves you

    Caches only help because real programs are not random. Temporal locality is reusing the same data again soon, like a loop counter touched every iteration. Spatial locality is using data that sits near data you just used, like walking through an array. Caches exploit spatial locality by never fetching a single word alone, they fetch a whole cache line, a contiguous chunk of memory, usually 64 bytes on a modern chip. Touch one word and its neighbors come along for free, ready for when you need them a few instructions later.

    Where a line can live: direct-mapped, set-associative, fully associative

    Given an address, the cache needs to know where to look for it, and how much freedom it has to place it there. In a direct-mapped cache, every address maps to exactly one line, decided by a few bits of the address, simple and fast to check but two addresses that land on the same line will keep evicting each other even if the rest of the cache sits empty. A set-associative cache (say, 2-way) groups lines into sets, and an address can live in any line within its set, so the cache checks a couple of tags in parallel instead of just one, trading a bit of complexity for fewer of those needless evictions. A fully associative cache drops the grouping entirely, an address can live anywhere, which removes that kind of eviction completely but means checking every single line's tag on every access, so it is only practical for very small caches, like a TLB with a few dozen entries.

    To find or place a line, the address splits into three fields. The low bits are the offset, which word within the line. The next bits are the index, which line (or set) it maps to. Everything above that is the tag, stored alongside the line so the cache can confirm it actually holds the address you asked for, not just some other address that happens to share the same index. In the simulator below, with 4 words per line, the offset is 2 bits. With 8 lines direct-mapped, the index is 3 bits. Switch to 2-way and there are only 4 sets, so the index shrinks to 2 bits and the tag grows by one bit to make up for it.

    Direct-mapped versus 2-way cache

    Pick a preset access pattern or type an address (a word number, not a byte address) and click Access. Switch associativity to see conflict misses disappear.

    Direct-mapped (8 lines, 1 way)

    Last access

    Address (word)-
    Tag-
    Index-
    Offset-
    Result-
    Hit rate0 / 0

    Cache contents (by set)

    Access log

    Hits, misses, and the three Cs

    A hit means the line is already in the cache, a miss means it is not and has to be fetched from further down the hierarchy. Misses come in three flavors. A compulsory miss is the first time you ever touch a piece of data, no cache policy can prevent that one, you saw exactly this in the sequential scan above, one miss to open each new line, three hits to use the rest of it, 75 percent hits. A capacity miss happens when your working set is bigger than the whole cache, it would miss even with full associativity and infinite cleverness in placement. A conflict miss happens when several addresses fight over the same line even though the cache overall has room to spare, that is exactly what the strided-by-64 pattern does on a direct-mapped cache, two addresses colliding on the same line, evicting each other on every single access. Switch to 2-way and both addresses fit in the same set at once, the conflict misses disappear and the pattern turns almost entirely into hits.

    Write-through versus write-back

    Reads are not the only traffic, writes need a policy too. Write-through sends every write to the cache and immediately down to the next level as well, simple and always consistent, but it turns every single write into a slow trip to memory. Write-back writes only touch the cache line, which gets marked with a dirty bit, and the update only travels down to memory when that line is eventually evicted. Write-back does far less memory traffic for code that writes the same location repeatedly, at the cost of needing that extra bit of bookkeeping and a slightly trickier recovery story if something goes wrong before the write-back happens.

    Why row-major beats column-major

    How you loop over a 2D array matters more than it looks like it should, because most languages store a 2D array in row-major order, one full row after another in memory. Walking it in that same order is cache-friendly, walking it the other way, column-major, jumping a full row on every step, is cache-hostile.

    Row-major traversal (cache-friendly)

    for i in 0..N-1:
        for j in 0..N-1:
            sum += A[i][j]

    Consecutive iterations touch consecutive words. With a 64-byte line and 8-byte values, one line holds 8 elements, so roughly 1 access in 8 misses, about a 12.5 percent miss rate.

    Column-major traversal (cache-hostile)

    for j in 0..N-1:
        for i in 0..N-1:
            sum += A[i][j]

    Consecutive iterations jump a full row apart. Once a row no longer fits in cache, almost every access lands on a fresh line, close to a 100 percent miss rate.

    Same additions, same data, same array, just a different loop order. In practice, summing a large matrix this way, big enough that it does not fit in any cache, is commonly measured at roughly 10 to 20 times slower in column-major order than row-major, purely from cache misses, the arithmetic itself is identical.