Design Challenge: Digital Stopwatch — Clock, Counters, Registers, and 7-Segment Display
Build a complete digital stopwatch pipeline — frequency divider, BCD counters, registers, and 7-segment decoder driving a multi-digit display — tying together timing, sequential counting, and combinational display decoding into one working real-time instrument.
The spec gives the exact block pipeline for this project: Clock → Frequency Divider → Counters → Registers → 7-Segment Decoder → Display. This project builds each block in that pipeline, in order, and shows how they connect: a fast crystal-oscillator-style clock gets divided down to a slow, human-usable 1-tick-per-10ms (centisecond) rate; that tick drives a chain of BCD counters (centiseconds, seconds, minutes) that roll over into each other exactly like a mechanical odometer; each counter's current digit is captured (directly or through a register stage) and fed to a 7-segment decoder per digit; and the decoders drive a multi-digit 7-segment display showing MM:SS.CC. Along the way this project also covers the START/STOP/RESET control logic that makes it a genuine stopwatch rather than a free-running clock.
1. Requirements and Specification
Inputs: a fast reference clock (this design assumes a 10 kHz source clock, a common crystal-derived rate, though any known frequency works — the frequency divider is what adapts it), START/STOP (a toggle button that enables or freezes the counting), and RESET (clears the display to 00:00.00). Output: a 6-digit 7-segment display showing MM:SS.CC (minutes, seconds, centiseconds), each digit driven by its own 7-segment decoder.
The display must count centiseconds from 00-99, seconds from 00-59, and minutes from 00-59 (this design caps at 59 minutes for simplicity, matching how most physical stopwatches roll over or stop at a max value rather than counting indefinitely), with each field correctly rolling over into the next (centiseconds 99→00 causes seconds to increment; seconds 59→00 causes minutes to increment) — this cascading-rollover behavior, not any single counter in isolation, is the core logic-design content of this project.
2. High-Level Architecture (Following the Spec's Pipeline Exactly)
Clock: a 10 kHz reference (in a real build, from a crystal oscillator and a small PLL/divider IC; in simulation, just declare a 10 kHz clock signal).
Frequency Divider: divides 10 kHz down to a 100 Hz (10 ms period = 1 centisecond) tick using a divide-by-100 counter (a mod-100 counter that pulses once every 100 input cycles) — this single 100 Hz tick is the master timebase for the entire rest of the circuit; every counter below only advances on this tick, gated by START/STOP.
Counters: three cascaded mod-N BCD (or mod-N binary, decoded to BCD-equivalent digits) counters — a mod-100 centisecond counter (0-99, actually built as two mod-10 BCD digit counters: ones and tens of centiseconds) that increments on every 100 Hz tick while running; when it rolls 99→00, it emits a carry pulse that increments a mod-60 seconds counter (two mod-6/mod-10 BCD digits); when seconds rolls 59→00, it emits a carry pulse that increments a mod-60 minutes counter — a direct hardware implementation of 'ripple carry' at the digit-field level, exactly analogous to how a ripple-carry adder's carry chain works, just clocked far more slowly.
Registers: in the simplest version, the counters' outputs feed the display directly (no separate register stage needed) since 7-segment displays update continuously; a register stage becomes necessary only if you add a LAP/HOLD feature (freezing the DISPLAYED time while the counters keep running underneath) — this design includes that register stage since the spec explicitly lists Registers as a required block, using it to implement LAP.
7-Segment Decoder: one BCD-to-7-segment decoder per displayed digit (6 decoders total for MM:SS.CC), each taking that digit's 4-bit BCD value and producing the 7 segment-driving outputs (a-g).
Display: 6 physical (or simulated) 7-segment display digits, each driven by its own decoder's 7 outputs, plus 2 fixed colon/decimal-point segments between MM:SS and SS.CC that are not decoder-driven (just tied to always-on).
3. Detailed Logic Design
Frequency divider as a synchronous mod-100 counter: a 7-bit counter (0-99 needs 7 bits) that increments every 10 kHz clock edge while RUN=1, and emits TICK=1 for one 10 kHz cycle whenever it reaches 99, simultaneously resetting itself to 0. This TICK signal — not the raw 10 kHz clock — is what every counter downstream actually uses as its clock enable.
Each BCD digit counter (ones-of-centiseconds, tens-of-centiseconds, ones-of-seconds, etc.) is a mod-10 up-counter (4-bit, counts 0000-1001 i.e. 0-9, then wraps to 0000) that increments on its enable input and emits a carry-out pulse exactly when it wraps from 9 back to 0 — standard BCD counter behavior covered in this course's Counters module, reused six times here (once per digit) with each digit's carry-out becoming the next digit's enable input, forming the full cascading MM:SS.CC chain.
START/STOP control: a single toggle flip-flop (RUN) that flips state on each START/STOP button press (debounced); the frequency divider's counter only advances while RUN=1, so stopping the stopwatch is as simple as gating RUN into the divider's clock-enable — every counter downstream automatically freezes too, since they only ever see ticks derived from the divider.
7-segment decoder logic (per digit, BCD input D3 D2 D1 D0 to segments a-g): this is a standard combinational truth table with 10 valid input rows (0-9) and 6 don't-care rows (10-15, since BCD never produces those); this design reuses the exact decoder logic from this course's Combinational Circuits module rather than re-deriving it, since re-deriving a 7-segment decoder is not new content for a capstone project — the new content here is wiring six decoder instances to six cascaded counters and getting the carry chain timing right.
4. Testing and Verification
Verify the frequency divider in isolation first: with a 10 kHz input, confirm TICK pulses at exactly 100 Hz (one pulse per 100 input clock edges) before connecting anything downstream — an off-by-one in the mod-100 counter (wrapping at 100 instead of 99, or vice versa) will make the entire stopwatch run at a slightly wrong rate, a subtle bug that's very hard to catch once six counters are cascaded on top of it.
Test the full rollover chain explicitly at all three boundaries: run the stopwatch (in simulation, fast-forward) until centiseconds goes 99→00 and confirm seconds increments by exactly 1 on that same tick, not one tick early or late; similarly verify seconds 59→00 correctly increments minutes; and verify the 'triple rollover' edge case — centiseconds AND seconds AND minutes all wrapping simultaneously (e.g., from 59:59.99 to 00:00.00, if you choose to let it wrap rather than stop at max) — since a carry chain implemented with combinational (not registered) carry pulses can glitch or miss a carry if the chain isn't given a full clock cycle to ripple through all three stages.
Test START/STOP/RESET interaction: confirm RESET clears all six digits to 0 regardless of RUN state; confirm toggling STOP mid-count freezes the display exactly at its current value (no drift, no missed or extra ticks when resumed); and confirm pressing START again resumes counting from exactly where it stopped, not from 0.
Pipeline and Cascade Equations
A mod-60 field (seconds, minutes) is built from a tens-digit that counts 0-5 (not 0-9) — so its carry condition checks tens==5, ones==9, distinct from the mod-100 centisecond field where tens counts the full 0-9. Every counter's enable ultimately traces back to RUN AND TICK (or a cascaded carry from the stage below), which is what makes STOP freeze the entire display atomically.
10 kHz clock
| divide by 100 (mod-100 counter, wraps 99->0, emits TICK)
v
100 Hz TICK (1 centisecond per tick)
| gated by RUN (START/STOP)
v
CENTISEC (mod-100, two BCD digits) --carry(99->00)--> SEC (mod-60)
SEC --carry(59->00)--> MIN (mod-60)
Per-digit BCD counter carry-out: carry = (digit == 9) AND enable
Per-field carry-out (e.g. seconds): carry = (ones==9) AND (tens==5) AND enableCode Examples
This is the key verification case: a naive design that lets the carry 'ripple' through combinational logic across three cascaded counters can, depending on how each counter's clocking is wired, either correctly resolve all three digit changes within the same clock edge (correct — shown here) or introduce a one-tick lag between centisec's rollover and seconds noticing it (a bug where the display briefly shows 10:00.99->10:00.00 one tick late, or similar). Explicitly simulate this exact boundary before considering the stopwatch verified. Expected result: All three digit-fields must update within the same clock edge for a correct synchronous cascade design.
n+2 10:00.00 centisec 99->00 carries to seconds;
Because every digit shares the same TICK clock and each digit's enable is gated by the carry from the stage before it, all cascaded digit updates that need to happen on a given tick DO happen on that same tick (synchronous design, not a slow asynchronous ripple) — this is what correctly handles the triple-rollover case from the worked example above.
assign carry_out = enable && (q == 4'd9);
Common mistake: A common bug is computing carry_out from q AFTER the clocked update (i.e., checking the NEW value instead of gating on the OLD value before it increments), which either delays every carry by one extra tick or double-counts it — always compute carry_out combinationally from the CURRENT registered value, as shown, not from a value you're still in the process of updating.
tick display event
... 09:59.97 running normally
n 09:59.98 centisec = 98
n+1 09:59.99 centisec = 99
n+2 10:00.00 centisec 99->00 carries to seconds;
seconds 59->00 carries to minutes;
minutes 09->10 (no further carry)
ALL THREE happen on the SAME tick n+2module bcd_digit (
input clk, reset, enable,
output reg [3:0] q,
output carry_out
);
assign carry_out = enable && (q == 4'd9);
always @(posedge clk or posedge reset) begin
if (reset)
q <= 4'd0;
else if (enable)
q <= (q == 4'd9) ? 4'd0 : q + 4'd1;
end
endmodule
// Top-level cascade sketch (centisecond ones -> tens -> seconds ones -> ...):
// bcd_digit cs_ones (.clk(TICK), .enable(RUN), .carry_out(c1), ...);
// bcd_digit cs_tens (.clk(TICK), .enable(RUN && c1), .carry_out(c2), ...);
// bcd_digit s_ones (.clk(TICK), .enable(RUN && c2), .carry_out(c3), ...);
// ... and so on through s_tens, m_ones, m_tensDigital Stopwatch Pipeline (as specified)
The exact 6-block pipeline the spec calls for: Clock -> Frequency Divider -> Counters -> Registers -> 7-Segment Decoder -> Display.
10kHz CLK RESET RUN(START/STOP)
| | |
v | |
+-----------+ | |
| FREQUENCY |<------+--------+
| DIVIDER | (mod-100, emits 100Hz TICK)
+-----+-----+
| TICK
v
+-----------------------------------------------------------+
| COUNTERS: CS(ones,tens) -carry-> SEC(ones,tens) -carry-> MIN(ones,tens) |
+-----+-------------------------------------------------------+
| 6 x 4-bit BCD digit values
v
+-----------+ (LAP button latches these into a
| REGISTERS |<--- hold-register that freezes what's shown,
+-----+-----+ while counters keep running underneath)
|
v
+-------------------+
| 7-SEGMENT DECODER | (x6, one per digit)
+---------+----------+
v
DISPLAY: MM : SS . CCImportant Rules
Every counter downstream of the frequency divider must derive its clock/enable from the divider's TICK output, never directly from the raw 10 kHz clock, so STOP can freeze the entire chain atomically.
A digit's carry-out must be computed combinationally from its current registered value (before the clocked update), not from the value it is transitioning to, so multi-digit rollovers resolve within a single clock edge.
A mod-60 field's tens digit must wrap at 5 (0-5), not 9, distinguishing seconds/minutes fields from the mod-100 centisecond field's tens digit (0-9).
RESET must clear all six digits and the frequency divider's internal counter together, so a reset mid-tick cannot leave a stale partial count.
Every Digital Clock and Timer Uses This Exact Pipeline
This Clock → Frequency Divider → Counters → Registers → 7-Segment Decoder → Display pipeline is not a simplified teaching example — it is structurally identical to real digital clocks, kitchen timers, sports scoreboards, and lap-timer wristwatches, all of which take a crystal oscillator's fast, precise frequency, divide it down to a human-scale tick rate, drive it through cascaded BCD counters, and decode the result to a visible display. The 'divide fast reference clock down to a slow tick, then cascade counters with carry, then decode for display' pattern is one of the most common structures in all of digital design, appearing again in real-time clocks (RTCs) inside every computer motherboard and phone.
Driving each of the six BCD digit counters directly off the raw 10 kHz clock, with each digit deciding on its own whether to increment based on a locally-computed 'is it time yet' condition.
Route every digit's counting through the single shared 100 Hz TICK signal produced by one frequency divider, cascading enables via carry-outs.
If each digit independently derives timing from the raw clock instead of sharing one TICK source, small differences in each digit's local divide logic can cause the digits to drift out of sync with each other over time (e.g., centiseconds ticking at a very slightly different effective rate than seconds), producing a stopwatch that slowly becomes internally inconsistent — a classic bug avoided entirely by having exactly one shared timebase.
Computing a digit's carry-out based on its NEXT (about-to-be-written) value rather than its current registered value.
Compute carry_out combinationally as a function of the digit's CURRENT value (before the clock edge), e.g. carry_out = enable AND (q == 9), as shown in the Verilog sketch.
If carry-out is derived from the next value instead, the carry pulse either arrives one clock cycle late (making a downstream digit's rollover lag behind where it should be, visible as a display glitch exactly at each rollover boundary) or is computed from a value that hasn't been written yet (a race condition), producing intermittent, hard-to-reproduce counting errors.
Why must every BCD counter in the stopwatch derive its enable/clock from the frequency divider's shared TICK signal, rather than each digit independently deriving its own timing from the raw 10 kHz clock? A. It uses fewer logic gates overall B. Sharing one TICK source guarantees all digits stay synchronized to the exact same timebase; independent per-digit timing derivation risks small drift between digits, causing the displayed time to become internally inconsistent over long runs C. The raw 10 kHz clock is too fast for any counter to use directly under any circumstance D. 7-segment decoders require a single shared clock input by definition
B. Sharing one TICK source guarantees all digits stay synchronized to the exact same timebase; independent per-digit timing derivation risks small drift between digits, causing the displayed time to become internally inconsistent over long runs — A single shared TICK, generated once by the frequency divider and fanned out to every counter's enable/carry chain, guarantees every digit advances in lockstep with exactly the same timebase. If each digit instead computed its own local divide-down logic, tiny implementation differences between them could cause gradual drift, where (for example) the centisecond digits run at an imperceptibly different effective rate than the seconds digits.
In the mod-10 BCD digit module, why is carry_out computed as enable && (q == 4'd9) using the CURRENT value of q, rather than checking the digit's value after it has already incremented? A. Both approaches are electrically identical and equally correct B. Checking the current (pre-increment) value lets the carry-out signal be valid combinationally within the same clock edge that the increment happens, so a downstream digit's cascaded enable is correctly asserted on that same edge instead of arriving one tick late C. Verilog does not allow reading a register's next value under any circumstances D. It reduces the total gate count of the design
B. Checking the current (pre-increment) value lets the carry-out signal be valid combinationally within the same clock edge that the increment happens, so a downstream digit's cascaded enable is correctly asserted on that same edge instead of arriving one tick late — carry_out needs to be ready and correct at the same clock edge the digit wraps from 9 to 0, so that the NEXT digit in the cascade (which uses carry_out as its own enable) increments on that very same edge. Computing carry_out from the current registered value (before the clocked always-block updates q) makes this combinational signal available in time; deriving it from the post-increment value would either be a race condition or delay the cascade by a full clock cycle.
The stopwatch is running and shows 09:59.99. On the next 100 Hz tick, what should the display correctly show, and what would indicate a rollover-cascade bug if it showed something else? A. 09:59.00 — only centiseconds should roll over; seconds and minutes update on a later tick B. 10:00.00 — centiseconds roll from 99 to 00, which carries into seconds (59 to 00), which itself carries into minutes (09 to 10), all resolving on this single tick; anything showing a one-tick lag in either carry indicates a bug C. 10:00.99 — minutes and seconds update immediately but centiseconds lag behind by one tick D. 00:00.00 — a full reset should occur automatically at this boundary
B. 10:00.00 — centiseconds roll from 99 to 00, which carries into seconds (59 to 00), which itself carries into minutes (09 to 10), all resolving on this single tick; anything showing a one-tick lag in either carry indicates a bug — This is exactly the triple-rollover worked example: a correctly designed synchronous cascade resolves all three digit-field carries (centiseconds, seconds, and minutes) within the same clock tick, producing 10:00.00 immediately. If the display instead showed an intermediate state like 10:00.99 or lagged a tick before seconds/minutes caught up, that indicates the carry chain isn't fully resolving within one clock edge — precisely the bug class this lesson's testing section calls out.
Add a LAP button that freezes the DISPLAYED time (via the Registers block) while the underlying counters keep running invisibly, and a second press of LAP updates the display to show the current (now-caught-up) time.
Add a lap_hold toggle flip-flop set/cleared by the LAP button; the 6 registers between Counters and the 7-Segment Decoders normally copy their corresponding counter value every TICK (transparent pass-through), but while lap_hold=1, they stop copying and simply retain their last-latched value, so the display freezes even though CS/SEC/MIN counters underneath continue incrementing; when lap_hold returns to 0, the registers resume copying the (now further-advanced) live counter values on the next TICK, so the display visibly jumps forward to the correct current time.
You already have the Registers block in the architecture for exactly this — add a LAP toggle flip-flop; when LAP is 'held', the register stage stops copying the live counter values and displays whatever was last latched; when LAP is released, the register resumes copying live values, which will jump to show all the time that passed while held.