Sequential Circuit Design & Edge-Triggered Realities
Combinational logic has no memory and no opinion about time. The moment you add a flip-flop, time becomes a constraint you can violate - and most silicon bugs that survive to the lab live in exactly this territory: reset release, clock division, and the few hundred picoseconds either side of a clock edge.
3.1 The D flip-flop: master-slave silicon physics
A latch is level-sensitive: while its enable is high, the output follows the input. A flip-flop is edge-sensitive: the output changes only at one instant. But there is no magic "edge detector" inside a flop. An edge-triggered D flip-flop is just two latches in series, clocked in opposite phase.
D was, and the slave opens to
release it. There is no path from D straight through to Q at any
instant - which is precisely why a flip-flop, unlike a latch, cannot be transparent.
q1 <= d; q2 <= q1; makes a two-stage pipeline rather than
a wire? Because q1's slave and q2's master are never open
simultaneously. The non-blocking assignment is not a simulator convention - it is a
faithful model of this topology.
The three timing numbers
Because the master latch needs time to physically settle before it closes, the data cannot be changing right at the edge. That gives every flip-flop a forbidden window:
t_su before the edge, t_h after
it, and t_cq from the edge until Q is valid. Data arriving
inside the crimson window leaves the flop in an undefined state - the subject of Volume 05.
| Parameter | What it means | If you violate it | How it is fixed |
|---|---|---|---|
| tsu | Data stable before the edge | Flop may capture the wrong value or go metastable | Slow the clock, or shorten the combinational path |
| th | Data stable after the edge | New data races through and corrupts the capture | Add delay buffers - slowing the clock does nothing |
| tcq | Edge to valid Q |
Not a violation - it is a cost you budget for | Eats into the next stage's timing budget |
t_cq, combinational delay and t_h, with the clock period
appearing nowhere in the equation. You cannot fix a hold violation by slowing the
clock. A chip with hold violations is broken at every frequency, including DC.
We derive both equations formally in Volume 07.
Writing flops in Verilog
// ---- Plain positive-edge D flip-flop ------------------------------------
module dff (
input wire clk,
input wire d,
output reg q
);
always @(posedge clk)
q <= d;
endmodule
// ---- Negative-edge triggered -------------------------------------------
module dff_neg (
input wire clk,
input wire d,
output reg q
);
// Legal, but mixing edge polarities in one design complicates timing
// closure. Production RTL is almost always single-edge.
always @(negedge clk)
q <= d;
endmodule
// ---- Flop with a clock ENABLE (not a gated clock!) ---------------------
module dff_en (
input wire clk,
input wire en,
input wire d,
output reg q
);
// The clock runs FREELY. The enable feeds a MUX in front of the D pin,
// or maps to the flop's dedicated CE pin. This is the correct way to
// "stop" a register - never by gating the clock in RTL.
always @(posedge clk)
if (en) q <= d;
// No else: holding the old value IS the intended behaviour, and in a
// clocked block that is a flop, not an inferred latch.
endmodule
// ---- SYNCHRONOUS active-low reset --------------------------------------
module dff_sync_rst (
input wire clk,
input wire rst_n,
input wire d,
output reg q
);
// rst_n is NOT in the sensitivity list - it is just data that happens
// to win. Nothing happens without a clock edge.
always @(posedge clk)
if (!rst_n) q <= 1'b0;
else q <= d;
endmodule
// ---- ASYNCHRONOUS active-low reset -------------------------------------
module dff_async_rst (
input wire clk,
input wire rst_n,
input wire d,
output reg q
);
// The reset edge is in the sensitivity list, so the flop clears the
// instant rst_n falls - no clock required.
always @(posedge clk or negedge rst_n)
if (!rst_n) q <= 1'b0;
else q <= d;
endmodule
always @(posedge clk or posedge rst_n) and then
testing if (!rst_n) is a real, common bug. The polarity of the edge in the
list must match the polarity you test: negedge rst_n pairs with
if (!rst_n), posedge rst pairs with if (rst). Get
it wrong and simulation and synthesis will disagree - the tool infers the reset from the
sensitivity list, while the simulator obeys the if.
Interview grilling - "Why can a latch not be used where a flip-flop is required?"
"A latch is transparent for half the clock period. During that window the input feeds straight through to the output, so a signal can propagate through multiple stages of logic in a single cycle - a race. A flip-flop is never transparent, so exactly one stage of logic happens per clock, which is what makes synchronous timing analysis possible at all."
The follow-up: "Then why do latches exist in real chips?"
Time borrowing (a slow path can steal slack from the next stage), roughly half the transistor count of a master-slave flop, and the clock-gating cell itself is built from a latch. See the Volume 01 discussion - the distinction is always deliberate versus accidental.
3.2 Synchronous vs asynchronous reset trade-offs
Every design needs to start from a known state. The argument about how is one of the oldest in the field, and the correct answer is not "pick one" - it is assert asynchronously, release synchronously.
| Synchronous reset | Asynchronous reset | |
|---|---|---|
| Sensitivity list | @(posedge clk) |
@(posedge clk or negedge rst_n) |
| Needs a running clock? | Yes - dead clock means no reset | No - works before the PLL locks |
| Glitch immunity | High - the clock filters short glitches | None - a 200 ps glitch resets the chip |
| Timing checks | Ordinary setup/hold on the data path | Recovery and removal checks on the reset pin |
| Area | Adds a MUX in front of every D pin | Uses the flop's dedicated reset pin - free |
| Typical home | FPGA fabric (LUT inputs are already there) | ASIC (dedicated reset pin, works at power-up) |
The dangerous half is the release
Asserting an asynchronous reset is genuinely safe: it does not care about the clock, which is the entire point. Releasing it is the problem. Two new timing checks exist on the reset pin, and they are the exact mirror of setup and hold:
| Check | Definition | Analogous to |
|---|---|---|
| Recovery | Minimum time reset must be de-asserted before the next active clock edge | Setup time |
| Removal | Minimum time reset must stay asserted after a clock edge | Hold time |
Now consider what happens in a real chip. Reset is a high-fanout net reaching tens of thousands of flops, each through a different amount of wire delay. If the release happens near a clock edge, some flops see reset gone before the edge and some after. Half your design starts on cycle n and half on cycle n+1 - and any flop caught exactly in the window can go metastable.
The reset synchronizer (AASD)
CLR pins tied to the raw
asynchronous reset, so assertion is instant. De-assertion has to shift a
1 through two flops, so it lands cleanly on a clock edge - with the second
flop absorbing any metastability the first one picks up.
module reset_sync #(
parameter STAGES = 2 // 2 is standard; 3 for very high frequency
) (
input wire clk, // destination clock domain
input wire arst_n, // raw asynchronous reset, active low
output wire rst_n // clean reset for this clock domain
);
reg [STAGES-1:0] sync;
always @(posedge clk or negedge arst_n) begin
if (!arst_n)
sync <= {STAGES{1'b0}}; // ASSERT: asynchronous, immediate
else
sync <= {sync[STAGES-2:0], 1'b1}; // DE-ASSERT: shift a 1 through
end
assign rst_n = sync[STAGES-1];
endmodule
reset_sync instance driven by its own clock. This
is one of the most common review findings in real designs, and it connects directly to
reset domain crossing in Volume 05.
3.3 Counters & modulo-N design
A counter is an adder feeding a register feeding the adder. Everything interesting is in the control: when to count, when to wrap, and how to tell the next stage you wrapped.
module counter_mod_n #(
parameter N = 10, // counts 0 .. N-1
parameter W = $clog2(N)
) (
input wire clk,
input wire rst_n,
input wire en, // count enable
input wire clr, // synchronous clear
input wire load, // synchronous parallel load
input wire [W-1:0] load_val,
output reg [W-1:0] count,
output wire tc // terminal count
);
// TC asserts on the LAST value, while still enabled - so it can drive
// the enable of the next counter in a cascade without losing a count.
assign tc = en && (count == N-1);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) count <= {W{1'b0}};
else if (clr) count <= {W{1'b0}}; // clear beats load
else if (load) count <= load_val; // load beats count
else if (en) begin
// Explicit wrap. Do NOT rely on natural rollover unless N is a
// power of two - for N = 10 the counter would run to 15.
if (count == N-1) count <= {W{1'b0}};
else count <= count + 1'b1;
end
end
endmodule
if / else if chain above encodes reset > clear > load > count.
That ordering is real hardware - a priority MUX in front of the register - and it is
exactly what an interviewer means when they ask "what happens if clr and
load assert together?" There is no universally correct answer, but there is
a wrong one: not knowing which your code implements.
Gray code counters
A binary counter can change several bits at once - 0111 → 1000 flips four. If
another clock domain samples that value mid-transition it can read a value that was never
actually there. A Gray code counter changes exactly one bit per step, so a
mistimed sample can only ever be off by one count.
module gray_counter #(
parameter W = 4
) (
input wire clk,
input wire rst_n,
input wire en,
output reg [W-1:0] gray,
output reg [W-1:0] bin
);
wire [W-1:0] bin_next = bin + 1'b1;
wire [W-1:0] gray_next = bin_next ^ (bin_next >> 1);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
bin <= {W{1'b0}};
gray <= {W{1'b0}};
end else if (en) begin
bin <= bin_next;
gray <= gray_next; // registered, so it is glitch-free at the output
end
end
endmodule
3.4 Shift registers & linear feedback shift registers
A shift register is a chain of flops. Trivial as hardware, and enormously useful: serial interfaces, delay lines, synchronizers, pattern detectors, and - with one XOR gate added - pseudo-random number generation.
module shift_reg_universal #(
parameter W = 8
) (
input wire clk,
input wire rst_n,
input wire [1:0] mode, // 00 hold, 01 shift right, 10 shift left, 11 load
input wire sin_r, // serial input for a right shift (enters MSB)
input wire sin_l, // serial input for a left shift (enters LSB)
input wire [W-1:0] pin, // parallel input
output reg [W-1:0] q
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) q <= {W{1'b0}};
else begin
case (mode)
2'b00: q <= q; // hold
2'b01: q <= {sin_r, q[W-1:1]}; // shift right
2'b10: q <= {q[W-2:0], sin_l}; // shift left
2'b11: q <= pin; // parallel load
endcase
end
end
endmodule
The LFSR
Feed a few bits of a shift register through an XOR and back into the input, and the register stops shifting in data and starts walking a long, deterministic, statistically random-looking cycle. With the right tap positions an n-bit LFSR visits every state except one before repeating.
// NOTE: tap positions are specific to the register WIDTH - they come from
// a table of primitive polynomials (see below). So these modules are fixed
// at 8 bits on purpose. Parameterising the width without also swapping the
// taps would silently destroy the maximal-length property.
module lfsr8_fibonacci (
input wire clk,
input wire rst_n,
input wire en,
output reg [7:0] lfsr
);
wire feedback;
// Polynomial x^8 + x^6 + x^5 + x^4 + 1.
// Bit 8 of the polynomial is lfsr[7], bit 6 is lfsr[5], and so on.
assign feedback = lfsr[7] ^ lfsr[5] ^ lfsr[4] ^ lfsr[3];
always @(posedge clk or negedge rst_n) begin
// The seed MUST be non-zero. Any non-zero value works and gives the
// same cycle, just entered at a different point.
if (!rst_n) lfsr <= 8'hFF;
else if (en) lfsr <= {lfsr[6:0], feedback};
end
endmodule
// ---- Self-correcting variant: 256 states, including all-zeros ----------
module lfsr8_de_bruijn (
input wire clk,
input wire rst_n,
input wire en,
output reg [7:0] lfsr
);
// Inverting the feedback when the low 7 bits are all zero splices the
// all-zero state into the cycle, giving a full 2^8-length sequence.
// Now a zero seed is harmless - useful on FPGAs, where registers
// power up to zero.
wire feedback = lfsr[7] ^ lfsr[5] ^ lfsr[4] ^ lfsr[3] ^ (lfsr[6:0] == 7'b0);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) lfsr <= 8'h00;
else if (en) lfsr <= {lfsr[6:0], feedback};
end
endmodule
| Width | Polynomial | Tap bits | Sequence length |
|---|---|---|---|
| 4 | x⁴ + x³ + 1 | 4, 3 | 15 |
| 8 | x⁸ + x⁶ + x⁵ + x⁴ + 1 | 8, 6, 5, 4 | 255 |
| 16 | x¹⁶ + x¹⁵ + x¹³ + x⁴ + 1 | 16, 15, 13, 4 | 65 535 |
| 32 | x³² + x²² + x² + x + 1 | 32, 22, 2, 1 | 4 294 967 295 |
Interview grilling - "Why does an LFSR lock up, and how many states does it really have?"
The lock-up. With XOR feedback, the all-zeros state is a fixed point:
the XOR of any number of zeros is zero, so the register shifts in a zero forever. That is
why the sequence is 2ⁿ - 1 and not 2ⁿ - the all-zero state is
excluded, forming its own one-state cycle.
The variant they will probe: "What if I use XNOR feedback instead?" Then the dead state is all ones, for exactly the same reason. XNOR is often preferred in FPGAs because registers power up to zero, so an XNOR LFSR self-starts without needing an explicit non-zero seed.
The other classic follow-up: "Fibonacci or Galois?"
| Fibonacci | Galois | |
|---|---|---|
| Structure | Many taps → one XOR chain → input | One bit fans out to XORs spread through the chain |
| Critical path | Chain of XORs - grows with tap count | One XOR - constant |
| Readability | Matches the polynomial directly | Harder to eyeball |
| Use when | Clarity matters | Speed matters |
Both produce maximal-length sequences of the same period from the same polynomial - just in a different order. Galois is what you want at high frequency.
Where LFSRs actually get used: built-in self-test pattern generation (BIST), CRC computation, scramblers on serial links, and cheap counters where you only need "N distinct states" rather than counting in order - an LFSR counter is smaller and faster than a binary counter because it has no carry chain.
3.5 Clock dividers: even, odd & the 50% duty problem
The clock enable alternative
// What you should almost always do instead: ONE clock domain, and a
// periodic enable pulse that makes downstream logic advance every Nth cycle.
module tick_gen #(
parameter N = 3
) (
input wire clk,
input wire rst_n,
output wire tick // one-cycle pulse, once every N clocks
);
localparam W = $clog2(N);
reg [W-1:0] cnt;
assign tick = (cnt == N-1);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) cnt <= {W{1'b0}};
else if (tick) cnt <= {W{1'b0}};
else cnt <= cnt + 1'b1;
end
endmodule
// Downstream logic then reads:
// always @(posedge clk) if (tick) ... ;
// Same clock everywhere. No new clock domain. No skew. Timing analysis
// stays trivial.
Even division is easy
// Divide by 2: a single toggle flop. Duty cycle is exactly 50% by
// construction, because the output flips once per input rising edge.
module clk_div2 (
input wire clk,
input wire rst_n,
output reg clk_out
);
always @(posedge clk or negedge rst_n)
if (!rst_n) clk_out <= 1'b0;
else clk_out <= ~clk_out;
endmodule
// Divide by any EVEN N: count to N/2 - 1, then toggle.
module clk_div_even #(
parameter N = 6 // must be even
) (
input wire clk,
input wire rst_n,
output reg clk_out
);
localparam HALF = N/2;
localparam W = $clog2(HALF);
reg [W-1:0] cnt;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
cnt <= {W{1'b0}};
clk_out <= 1'b0;
end else if (cnt == HALF-1) begin
cnt <= {W{1'b0}};
clk_out <= ~clk_out; // toggle every N/2 input clocks
end else begin
cnt <= cnt + 1'b1;
end
end
endmodule
Odd division at 50% duty: the classic question
Divide by 3 and you need the output high for 1.5 input clock periods. You cannot get half a period from rising edges alone - so use the falling edge too. Build the same divide-by-3 waveform twice, once on each edge, and AND them. The negative-edge copy is offset by exactly half a period, and the AND trims exactly that half period off the high time.
neg_wave is shifted by half a period, the AND is high for
2T - 0.5T = 1.5T out of 3T. The first pulse is a start-up artefact; the
waveform is periodic from the second cycle on.
module clk_div_odd #(
parameter N = 3 // must be ODD and >= 3
) (
input wire clk,
input wire rst_n,
output wire clk_out
);
localparam W = $clog2(N);
reg [W-1:0] pos_cnt, neg_cnt;
// ---- counter clocked on the RISING edge -------------------------------
always @(posedge clk or negedge rst_n) begin
if (!rst_n) pos_cnt <= {W{1'b0}};
else if (pos_cnt == N-1) pos_cnt <= {W{1'b0}};
else pos_cnt <= pos_cnt + 1'b1;
end
// ---- identical counter clocked on the FALLING edge --------------------
// This is the only place in the whole course where a negedge block is
// the right answer - it is what buys us the half-period offset.
always @(negedge clk or negedge rst_n) begin
if (!rst_n) neg_cnt <= {W{1'b0}};
else if (neg_cnt == N-1) neg_cnt <= {W{1'b0}};
else neg_cnt <= neg_cnt + 1'b1;
end
// Each wave is high for (N-1) of N states - for N = 3 that is 2 of 3.
wire pos_wave = (pos_cnt != N-1);
wire neg_wave = (neg_cnt != N-1);
// The AND trims exactly half a period off the high time:
// (N-1)T - 0.5T = N/2 * T -> exactly 50% duty.
assign clk_out = pos_wave & neg_wave;
endmodule
AND2. Say this unprompted in
an interview and you have demonstrated you have actually built one.
| Requirement | Correct approach |
|---|---|
| Slower logic, same domain | Clock enable - no new clock at all |
| Divide by 2, 4, 8… | Toggle flop chain, or a counter bit |
| Divide by any even N | Counter to N/2, toggle |
| Divide by odd N, 50% duty | Dual-edge counters ANDed, as above |
| Non-integer ratio (e.g. ×2.5) | PLL / MMCM. Logic cannot do this cleanly |
| A real clock output pin | Clock output buffer, constrained in the SDC |
Volume 03 recap
| Concept | The one thing to remember |
|---|---|
| Master-slave DFF | Two latches, opposite phase, never open together. That is the "edge". |
| Setup vs hold | Setup is fixed by slowing the clock. Hold is not - period is absent from its equation. |
| Sensitivity list | negedge rst_n must pair with if (!rst_n). |
| Reset | Assert asynchronously, de-assert synchronously. One synchronizer per clock domain. |
| Recovery / removal | Setup and hold, but for the reset pin on its release. |
| Modulo-N counter | Wrap explicitly unless N is a power of two. State your priority order. |
| Gray code | One bit changes per step, so a mistimed sample is only ever off by one. |
| LFSR | 2ⁿ-1 states; all-zeros is dead with XOR, all-ones with XNOR. Galois for speed. |
| Clock division | Prefer a clock enable. For odd N at 50%, AND a posedge and negedge waveform. |