Sequential Logic and always_ff

Combinational logic has a superpower and a matching disability: it answers instantly, and it remembers nothing. It cannot count to three, because counting means remembering where you got to. Every interesting machine — a processor, a traffic light, your GPU-to-be — needs state, and in synchronous digital design all state lives in one kind of place: the flip-flop, marching to the beat of a clock. This lesson gives hardware its heartbeat — and introduces a little simulator we will keep using for the rest of the course, all the way up to a working CPU and GPU.

The flip-flop and the clock

A D flip-flop is a 1-bit camera: at the instant its clock ticks from 0 to 1 (the rising edge), it photographs its input D and holds that photo on its output Q until the next tick — ignoring everything D does in between. A register is a row of flip-flops sharing one clock. In SystemVerilog, clocked state has its own block:

always_ff @(posedge clk) begin count <= count + 1; // a counter: at each tick, capture current count + 1 end

Read @(posedge clk) as "at every rising clock edge". Between edges, nothing in this block happens: the combinational cloud computing count + 1 settles at its leisure, and only the edge commits the result into the register. That rhythm — settle, then snap — is the entire discipline of synchronous design. It is why a chip with a billion gates can be reasoned about at all: time is chopped into cycles, and each cycle asks one tractable question, "what are the register values at the next edge, given the values at this one?"

Reading the waveform

Designers live in diagrams like this one — time flowing right, one row per signal:

Notice what the bottom row does not do: Q never reacts to D's mid-cycle changes. The flip-flop turns messy, continuously-varying signals into a clean sequence of per-cycle values — analogue chaos in, clockwork out.

Non-blocking assignment: everyone snaps together

The arrow <= is the non-blocking assignment, and it encodes the deepest fact about registers: at a clock edge, every register in the chip samples its input simultaneously, using the old values of everything else. The classic proof is the swap:

always_ff @(posedge clk) begin a <= b; // both right-hand sides read the OLD values… b <= a; // …then both registers update together: a genuine swap! end

In software those two lines destroy a. In hardware they exchange two registers, because <= means "sample now, commit at the edge": both right-hand sides are read from the pre-edge world, and both photos develop at once. Two flip-flops with crossed wires — of course they swap. The statement order is irrelevant, just as it was for assign.

Registers also need a way to start from a known value — a reset:

always_ff @(posedge clk) begin if (!rst_n) count <= '0; // synchronous reset, checked at the edge else count <= count + 1; end

This is a synchronous reset (obeyed at a clock edge); the alternative, an asynchronous reset (@(posedge clk or negedge rst_n)), yanks the register to zero the moment reset asserts, clock or no clock. Chips use both; what matters now is that every register gets some reset story, or it wakes up holding x — the unknown.

The two-phase simulator

"Settle, then snap" is not just a slogan — it is an algorithm, and it is how we will simulate every design in this course. Represent the registers as a state object; write one function computing what the combinational logic feeds each register's input; stepping the clock means calling it and committing the result all at once. Here it is, running a 3-bit counter and a 4-bit shift register side by side:

// ── The reusable pattern: state + next(state) = the combinational logic. ── interface State { count: number; shift: number[]; a: number; b: number } function next(s: State, serialIn: number): State { return { count: (s.count + 1) % 8, // count <= count + 1 (3 bits wrap) shift: [serialIn, ...s.shift.slice(0, 3)], // shift <= {serialIn, shift[3:1]} a: s.b, // a <= b } the swap, b: s.a, // b <= a } non-blocking style }; } // ── The clock: settle (compute next) then snap (replace state atomically). ── let state: State = { count: 0, shift: [0, 0, 0, 0], a: 1, b: 0 }; const serial = [1, 0, 1, 1, 0, 0, 1, 0]; // bits arriving one per cycle console.log("cycle | count | shift | a b"); console.log("------+-------+---------+----"); for (let cycle = 0; cycle < 8; cycle++) { console.log(` ${cycle} | ${state.count} | ${state.shift.join(" ")} | ${state.a} ${state.b}`); state = next(state, serial[cycle]); // one rising edge }

Three behaviours, one mechanism. The counter climbs and wraps at 8; the shift register slides each arriving bit along one place per tick; and a/b swap forever — the non-blocking semantics falls out for free, because next reads only the old state and the whole new state replaces it in one go. Keep this pattern in mind: the CPU in Module 6 and the GPU capstone are this exact loop with a richer State.

Somewhere on every board sits a sliver of quartz, ringing like a microscopic tuning fork a few million times a second; on-chip circuits multiply that ring up to gigahertz. That one signal then fans out to every register on the die — hundreds of millions of flip-flops — through a distribution network so demanding it gets an entire lesson later in this course. A 3 GHz clock gives each cycle just 333 picoseconds; in that time light itself travels about ten centimetres, roughly the width of the laptop the chip sits in. Every combinational cloud on the chip must settle within that sliver of time, every cycle, billions of times a second, for years — which is why the question "how fast can the clock go?" (the critical path) governs so much of chip design.

Write the swap with ordinary = (the blocking assignment) and it breaks exactly the way software does: a = b; b = a; executes in order during the edge, so both end up holding b's value — and worse, the behaviour now depends on statement order, which is poison in a language describing simultaneous hardware. The community's iron rule: non-blocking <= in always_ff, blocking = in always_comb, and never mix them in one block. Follow it blindly now; you will meet the rare principled exceptions (and the simulation race conditions that punish violators) in the verification module.