3.2 Sequential Logic and Synchronization
Combinational Logic only describes
output = F(input). Once state storage is introduced, outputs depend on past inputs as well; clock signals, setup time, hold time, and cross-clock-domain issues emerge.
Feedback Gives Circuits History
On the central control panel, the adder can now compute the next address, but once the input is removed, the result vanishes. To build a program counter, the circuit must retain the current address and accept a new value at the right moment.
Feeding the output of combinational logic back into its input creates two stable states. This intuition leads directly to latches and flip-flops, but feedback also introduces propagation delay, race conditions, and analog behavior that can no longer be ignored.
A sequential circuit can be abstracted as:
next_state = F(current_state, input)
output = G(current_state, input) # Mealy-type
or
output = G(current_state) # Moore-typeCombinational logic computes next_state, while storage elements capture and hold that value, transforming it into a new current_state at a specified event.
SR Latch: The Smallest Holding Structure
Two cross-coupled NOR gates can form a high-assertive SR latch. The functional table, ignoring propagation delays, is:
| S | R | Next state of Q | Meaning |
|---|---|---|---|
| 0 | 0 | Hold | Retains previous value |
| 1 | 0 | 1 | Set |
| 0 | 1 | 0 | Reset |
| 1 | 1 | Forbidden | Q and its complement are both driven to 0 simultaneously |
"Forbidden" is not merely a cosmetic flaw in the table. When S and R are both driven high and then released simultaneously, the two feedback paths may compete, and the final state depends on tiny timing differences, potentially resulting in metastability. If NAND gates are used to build a low-assertive version, the input polarity and forbidden combination are inverted; before examining the circuit, one must confirm the convention in use.
The SR latch illustrates how a state can emerge, but it is not a suitable model for synchronization in software readers' minds. In practice, designs more commonly rely on D inputs with enable or clock constraints to prevent callers from directly creating S-R conflicts.
D Latches and D Flip-Flops Are Not the Same Component
A D latch is level-sensitive: while the enable signal is active, the output Q follows the input D; once the enable is removed, Q retains its last value. This active window is commonly referred to as the transparent phase.
An edge-triggered D flip-flop samples the D input only near a specified clock edge, holding the current value of Q at all other times:
Clock rising edge: Q(next) = D
All other times: Q(next) = Q"Reading the value at only a mathematical instant" remains a digital abstraction. Physical flip-flops require that D be stable for at least the setup time before the clock edge and remain stable for at least the hold time after the edge.
Latches are not inherently inferior or "prone to interference." Well-designed latch-based pipelines can leverage timing from adjacent stages, though timing analysis becomes more complex. Beginners in synchronous systems typically start with the edge-triggered model because the stage boundaries are clearer and easier to reason about.
Registers Are a Group of Bits Sampled in Parallel
A group of flip-flops driven by the same clock edge can store a multi-bit value. With the addition of an enable signal, the register updates only on the edge of the clock when the enable is active:
if rising_edge(clock):
if enable: Q(next) = D
else: Q(next) = QAll bits must evaluate the edge condition based on the same previous state, then update together. An older example in a loop causes the first bit to update a shared previous_clock before the others see the rising edge, this clearly demonstrates that software sequential assignment cannot casually mimic concurrent hardware behavior.
The following C17 code serves only as a discrete-event reference model, yet it correctly places edge detection outside the register group:
#include <assert.h>
#include <stdbool.h>
#include <stdint.h>
struct Register8 {
uint8_t value;
bool previous_clock;
};
static void drive_register8(struct Register8 *reg, bool clock,
bool enable, uint8_t data) {
bool rising_edge = clock && !reg->previous_clock;
if (rising_edge && enable) {
reg->value = data;
}
reg->previous_clock = clock;
}
int main(void) {
struct Register8 reg = {0, false};
drive_register8(®, false, true, UINT8_C(0x12));
assert(reg.value == UINT8_C(0x00));
drive_register8(®, true, true, UINT8_C(0x12));
assert(reg.value == UINT8_C(0x12));
drive_register8(®, true, true, UINT8_C(0x34));
assert(reg.value == UINT8_C(0x12));
drive_register8(®, false, true, UINT8_C(0x34));
drive_register8(®, true, false, UINT8_C(0x34));
assert(reg.value == UINT8_C(0x12));
return 0;
}C local variables are not equivalent to "a row of fixed flip-flops." Optimizers may move them into physical registers, stack memory, eliminate them entirely, or reuse storage locations at different times. The register semantics in hardware description languages are fundamentally different from how registers are treated in ordinary software compilation.
A Clock Cycle Must Accommodate the Full Data Path
A typical register-to-register path within a single clock domain is:
Source register --clock-to-Q--> Combinational logic and routing --setup--> Destination registerIgnoring directional details of the symbols, the setup constraint can be summarized as:
Tclock >= t_clk_to_q(max) + t_comb(max) + t_setup + clock uncertaintyTo ensure new data does not arrive too early relative to the same clock edge, the hold constraint must also be maintained:
t_clk_to_q(min) + t_comb(min) >= t_hold + worst-case clock skew termSetup violations are typically addressed by reducing clock frequency, shortening combinational paths, pipelining, or improving physical implementation. Hold violations cannot be resolved solely by lowering clock frequency, since they pertain to the shortest path near the same clock edge, often requiring additional data path delay or adjustments to the clock tree.
Actual static timing analysis also accounts for clock skew, jitter, process/ voltage/ temperature corners, clock gating, and exception paths. In short, the simplistic statement "cycle time exceeds gate delay" overlooks the timing budget required by flip-flops and the clock network itself.
Synchronous Update Requirements: Compute Next Before Unified Commit
Consider two state registers exchanging values on the same rising edge:
A(next) = B
B(next) = AHardware reads the old values before the edge, so the exchange succeeds. In software, if the model executes sequentially A = B; B = A;, the second line reads the new value of A, leading to incorrect results.
A reliable simulator typically performs this in two phases:
- Compute all next-state values using the prior state;
- Commit all changes uniformly at the clock event.
SystemVerilog's clock-triggered processes commonly use nonblocking assignment (<=) to express such synchronous updates. Blocking assignment (=) is appropriate for local computations in combinational logic; mixing these constructs must adhere to the project's HDL guidelines and tool validation rules.
Metastability is when reality leaks into digital abstraction
If D violates setup or hold timing near a sampling edge, internal nodes within a flip-flop may temporarily linger between logical thresholds, resolving to 0 or 1 after an unpredictable delay. The downstream risk isn't a stable, usable "third value", instead; it's either prolonged resolution times or inconsistent results observed across different paths, rather than a reliable logic state.
Single-bit control signals from buttons, peripherals, or other asynchronous clock domains are commonly synchronized using two or more stages of synchronizers to reduce the probability of metastability propagating:
async input -> flip-flop 1 -> flip-flop 2 -> synchronous logicSynchronizers can only reduce metastability probability to an engineering target, they cannot mathematically eliminate it. The output of the first stage should never be fed directly into general-purpose logic.
For multi-bit buses, simply applying two-stage synchronizers to each bit is insufficient, because bits may be received in different clock cycles, potentially forming combinations that were never intended to be sent. Pulses, counters, and data streams typically require semantic-aware CDC solutions such as handshaking, toggle protocols, Gray code, or async FIFOs.
Reset is also part of timing design
Synchronous reset only takes effect on clock edges and is easily captured in standard timing analysis. Asynchronous reset, however, can immediately drive the state to a known value when the clock stops, but reset release must satisfy recovery and removal timing requirements, commonly implemented as "asynchronous assertion, synchronous release."
Not every data-path register needs a reset. Excessive reset networks increase area, routing congestion, and timing pressure. As long as the control state ensures that uninitialized data is overwritten before use, reset may be unnecessary. The decision depends on safety requirements, power-up protocols, and process technology.
Avoid arbitrarily combining clock signals with standard AND/OR gates. Changes in enable gate delays can generate narrow pulses or unintended clock edges. When clock gating is required, use integrated clock-gating units provided by the library and constraints approved by the tool. Many logic functions can also directly leverage register enable signals.
State Machines Turn Console Flow Into Circuitry
The Earth Core console can't just compute "what input was received", it must also remember which state it last exited from. A state machine captures this history in registers and uses combinational logic to compute the next state and output.
A typical synchronous finite state machine consists of:
- A state register that holds the current state;
- Combinational logic that computes the next state based on the current state and input;
- Output logic that generates outputs according to either Moore or Mealy models.
For example, a two-step access controller might have three states: LOCKED, BADGE_OK, and OPEN. Swiping a card transitions the state from LOCKED to BADGE_OK, and only after a correct PIN is entered before timeout does it advance to OPEN. This approach is far clearer than packing all such conditions into a single massive Boolean expression.
State encoding can use binary, one-hot, or Gray code schemes. One-hot uses more flip-flops but may reduce decoding logic complexity. The optimal choice depends on the target hardware (FPGA or ASIC), fault tolerance requirements, and timing constraints. Don't judge implementation cost solely by the number of bits.
From Registers to Processor State
The processor arranges combinational data paths and timing boundaries in sequence:
Program Counter -> Fetch Logic -> Pipeline Register
-> Decode/Read Registers -> Pipeline Register
-> Execution Unit -> Pipeline RegisterThis is a conceptual diagram. Modern CPUs may execute instructions out of order, rename registers, speculatively branch, and allow a single instruction to span multiple cycles. Source assignments in code do not mechanically correspond to specific clock edges. Digital logic provides the building blocks, but it's the instruction set architecture (ISA) and microarchitecture that determine how these building blocks are assembled.
Draw Out Timing Relationships
- Sketch waveforms for S, R, and Q of a high-effective NOR SR latch, and clearly mark the input combinations that are forbidden.
- For the same D/enable waveform, draw the Q outputs of a level-sensitive high-assertion D latch and a rising-edge triggered D flip-flop separately.
- Given
t_clk_to_q=80 ps, a combinational path delay of 620 ps, a setup time of 70 ps, and an uncertainty of 30 ps, calculate the minimum allowable clock period. - Explain why reducing the operating frequency typically does not resolve hold time violations.
- Design a handshake mechanism to transfer a 32-bit configuration word across a clock domain, and specify when the data is permitted to change.
- Express a three-state gate-controlled finite state machine (FSM) as a two-phase model (current state and next state) and include logic for recovering from invalid states.
Welding Gates and State into an Executable Engine
We now have two types of building blocks: composite operations and synchronized persistence. The next chapter moves into CPU and ISA, distinguishing the instruction set as a software contract from the concrete microarchitecture implementation, and then traces the execution of a single instruction through fetch, decode, execute, and commit.