Register Files and Random-Access Memory
Storage organized for access by address. We build the register file (a small bank of registers with addressed read ports and clocked write ports, the exact structure Y86-64's decode and write-back stages use), then descend to the SRAM and DRAM cells of main memory, why one is fast and dear and the other dense and slow, and how a row decoder picks a word out of a memory array.
╌╌╌╌
The last lesson built a register: flip-flops on a shared clock holding one word. A processor has many such registers and a vast main memory, and the interesting engineering is how you reach one word out of many by its address. This final Digital Logic lesson builds the two storage structures a machine actually uses, the register file and random-access memory, and the addressing logic, built from the decoders and registers of earlier lessons, that selects a word from each.
The register file
A register file is a small bank of registers (Y86-64 has fifteen 64-bit program registers) addressed by a register identifier (a small number naming which register). What makes it more than an array of registers is its port structure: it serves multiple reads and accepts a write at once, the way an instruction needs.
Y86-64's register file has two read ports and two write ports. Each read
port takes a register id (srcA, srcB) and combinationally drives out that
register's current contents (valA, valB): no clock needed, the data is just
there. Each write port takes a register id and a data word and is clocked: on
the rising edge, the addressed register loads the data. The write ports are named
for what they carry — port E takes a computed result (dstE, valE), port M a
value read from memory (dstM, valM). Why two? One instruction can produce
both kinds of result at once: popq %rax must update %rsp with the incremented
pointer (through port E) and load %rax from memory (through port M) in the
same cycle.
This is the register file Y86-64 uses. In the decode stage the
processor reads the two source operands by their ids; in the write-back stage
it writes results on the clock edge — an ALU result through port E, a loaded
value through port M, and for popq both at once. Two subtleties make it work
cleanly. First, the read is combinational so an instruction can fetch its
operands within the same cycle, while the writes are clocked so state
advances exactly once per cycle. Second, when the same cycle reads and writes the
same register, the read returns the old value (the write takes effect at the
edge, after the read has been used) — a timing detail the pipeline must respect.
A third rule covers the corner where both write ports name the same register:
port M wins, and the instruction that needs the rule is popq %rsp, whose port-E
write (the incremented pointer) and port-M write (the old stack top) collide on
%rsp — the ISA says the memory value prevails, as the
SEQ stage tables will
rely on.
Open the box and there is nothing new inside: registers, decoders, and muxes
from the last three lessons, assembled into an addressed memory. Each read
port is a word-wide multiplexer: the register id srcA is its select code,
steering the chosen register's output onto valA. Each write port is the
decoder
put to work: dstE drives it, exactly one of its one-hot outputs goes high, and
that line, ANDed with the clock, becomes the load-enable of the one register
allowed to capture valE at the edge. Every other register sees its enable low
and holds. Port M repeats the structure — a second decoder on dstM whose
one-hot line ORs into the same enables and steers a small mux in front of each
register, so that when both decoders select one register, valM is the value
captured.
The two halves obey different clocks because reads and writes have different
semantics. The contents of are defined at every instant, so the read mux is
pure combinational logic and valA follows srcA within a propagation delay.
Writing changes state, and state changes only at clock edges — so the
enable is gated with the clock, and between edges the decoder's output can
wiggle freely without corrupting anything. Y86-64's register file scales this
picture to fifteen 64-bit registers: 4-bit ids, two 4-to-16 write decoders (one
code, 0xF, means no register
), and two read muxes, each fifteen words wide.
Trace one cycle of addq %rax, %rbx (compute %rbx = %rbx + %rax, storing
through port E). Say %rax is id holding
and %rbx is id holding , and the ALU result is .
- Decode (combinational read).
srcAandsrcBdrive the two read muxes. Mux A selects register 's output ontovalA; mux B selects ontovalB. No clock — the values are on the wires as soon as the ids settle. - Execute. The ALU adds
valAandvalBto , which becomesvalE. - Write-back (clocked).
dstEdrives the write decoder; its one-hot output raises only line . That line ANDed with the clock forms 's load enable; every other register sees enable . On the rising edge capturesvalEand the other fourteen hold. Port M is idle this cycle (dstM0xF, no register), so its decoder raises no line.
Notice the read used the old %rbx , correctly: the write to lands
at the edge, after the mux already delivered the operand. Had this been
popq %rax instead, both ports would fire — port E writing the incremented
%rsp, port M writing the loaded %rax — and the port M wins on a collision
rule would decide any tie. One decoder, one mux per port, and the timing
discipline of the previous lesson: nothing more.
SRAM and DRAM: two ways to store a bit
The register file is small and fast because each bit is a full flip-flop. Main memory needs billions of bits, and a flip-flop per bit would be impossibly large and power-hungry. Memory technology offers two cheaper cells, trading speed for density.
- A static RAM (SRAM) cell stores a bit in a small bistable latch, typically six transistors (6T): a cross-coupled inverter pair holding the bit, plus two access transistors connecting it to the bit lines. It holds its value as long as power is applied, with no refresh, and is fast. But six transistors per bit is expensive in area, so SRAM is used where speed matters most: registers and caches.
- A dynamic RAM (DRAM) cell stores a bit as charge on a tiny capacitor, read and written through a single access transistor: one transistor plus one capacitor (1T1C). One transistor per bit makes DRAM enormously dense and cheap, which is why main memory is DRAM. The cost is that the capacitor leaks: the charge decays in milliseconds, so every cell must be refreshed (read and rewritten) periodically, and a read is destructive (it drains the capacitor and must be restored). DRAM is therefore slower than SRAM.
Reading each cell explains its speed. Assert the SRAM cell's word line and its two access transistors connect the inverter loop, which is actively driving both storage nodes, straight onto the bit-line pair; the lines split apart quickly and cleanly, and the cell's state is untouched. Assert the DRAM cell's word line and all that reaches the bit line is the charge of one tiny capacitor, tens of femtofarads dumped onto a long wire with many times its capacitance. The resulting voltage nudge is millivolts, so a sense amplifier must recover the value and then — because dumping the charge destroyed it — write it back. Every DRAM read is really a read-plus-restore, and that restore is part of why DRAM access takes tens of nanoseconds while on-chip SRAM answers in a fraction of one.
Put numbers on the DRAM read to see why the sense amplifier is unavoidable. A storage capacitor holds around fF; the bit line it must drive is a long wire with fF, ten times larger. When the access transistor opens, the cell's charge redistributes by charge sharing across the combined capacitance. If the bit line is pre-charged to the midpoint and the cell holds a full (a stored ), the new bit-line voltage settles to roughly
a nudge of only above the midpoint — tens of millivolts on a V supply. That tiny swing is why a differential sense amplifier, comparing the bit line against an untouched reference at exactly , is what actually resolves the bit; the cell alone cannot swing the line to a logic level. And because the charge sharing left the cell sitting near too, its stored is now half-gone: the amplifier must write the row back as part of the read. The SRAM cell, by contrast, actively drives both bit lines from its inverter loop, splitting them apart in tens of picoseconds and leaving its own state untouched — the whole speed-versus- density trade in two numbers.
The leak sets the other clock. A DRAM capacitor loses its charge in tens of
milliseconds, so every cell must be refreshed before its bit fades. The standard contract is that every row survives 64 ms, so a chip
with rows per bank refreshes one row about every , an
obligation the memory controller schedules forever in the background. Each
refresh occupies the bank for a few tens of nanoseconds, so the tax is small,
around a percent of the memory's time, but it is one more reason dynamic
memory is cheap per bit and never quite as responsive as static.
The two form a hierarchy: SRAM's speed serves the processor's hottest storage (registers, on-chip caches), and DRAM's density serves the bulk of main memory. The whole memory hierarchy is a later topic, but its physical root is right here — the 6T cell is fast and dear, the 1T1C cell is slow and cheap.
A memory array and address decoding
Memory cells are laid out in a two-dimensional array of rows and columns; each row is one addressable word. To read or write a word you must select its row, and that is the job of a row decoder — the same -to- decoder from the adder lesson. The address feeds the decoder, which asserts exactly one of its output lines, the word line for that row, turning on the access transistors of every cell in that row and connecting them to the column bit lines that carry the data in or out.
Reads are combinational in spirit (assert an address, and after the decoder and bit lines settle the word appears) while writes are gated by a write-enable and the clock so a word changes only when intended, exactly mirroring the register file's read/write asymmetry but at the scale of a whole memory.
At real sizes the geometry has to bend. A one-gigabit chip holds cells, and a single decoder with outputs is not buildable — nor is routing a billion word lines. So the array is kept roughly square: rows of cells, a 15-bit row decoder down one side, and a 15-bit column selection (a wide mux) along the bottom that picks the wanted bits out of the selected row. The address splits in half, and DRAM chips push the split onto the pins: the same address wires carry the row half and the column half in two beats, which is why DRAM protocol speaks of a row address strobe and a column address strobe. Better still, the sense amplifiers that read the selected row form a row buffer holding all bits of it; further accesses that hit the same row skip the slow array access entirely and pay only the column mux.
Locality in the address stream becomes speed for free — a theme the memory-hierarchy module builds an entire architecture on. Every refinement keeps the core idea: a decoder turns an address into a one-hot selection, and everything else is engineering around wire length and charge.
Ports, error correction, and real DRAM
The 6T cell, the 1T1C cell, and the row decoder are the enduring core; the public memory literature wraps three layers of engineering around them.
Ports cost area, quadratically. The register file above shows one read mux and one write decoder per port, which understates the price. Each additional port adds a word line and bit line pair to every cell, so an -read, -write SRAM cell grows roughly with in area — the wires dominate. This is why a wide superscalar core, whose several ALUs may each need two operands per cycle, cannot simply keep adding ports: real designs bank the register file, replicate it (two identical copies, each serving half the read ports), or clock it at double rate so one physical port serves two logical reads. The clean two-read/two-write picture is Y86-64's teaching simplification.
Error-correcting memory. A stored bit can flip from a cosmic ray or a leaky
cell, so server memory adds error-correcting codes: a 64-bit word carries 8
extra bits computed as a SEC-DED Hamming code (Hamming, 1950), enough to
correct any single-bit error and detect any double-bit one. The extra bits ride
alongside the data through the same array and decoder; the correction logic is a
parity tree — the XOR-and-AND net of
lesson 2 — on
the read path. Caches use the same trick, and the beat
structure of DRAM makes
room for the check bits almost for free.
DRAM is also a protocol. The row-buffer split above is the seed of
modern DRAM. A chip is divided into banks, each with its own row buffer, so one
bank can be servicing a column read while another is opening a fresh row —
bank-level parallelism that hides the slow array access. DDR (double data rate
) transfers on both clock edges, and the memory controller schedules the
row-activate / column-read / precharge commands to keep the banks busy and honor the
refresh contract — a small scheduler whose job is turning the address stream's
locality into open-row hits. Jacob, Ng, and Wang's Memory Systems (2007) is the
public reference. Every layer still bottoms out in this lesson: a decoder turns an
address into a one-hot word-line, and a cell either drives its bit or dribbles a
capacitor's charge onto a sense amplifier.
This closes the Digital Logic module: from a single transistor switch, through gates, combinational function units, and clocked memory, to the addressed storage a processor reads and writes. The next module puts these pieces together into a datapath that fetches and executes instructions.
╌╌ END ╌╌