Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • RTL Foundations36
  • Combinational Design25
  • Sequential Design27
    • Flip-Flops and Registers
    • Counters and Timers
      • Counting as Controlled State Evolution
      • Up/Down Counter with Reload
      • A Prescaler Feeding a Timer Tick
      • Watchdog Timer Example
      • A Gray-Code Counter
      • Mini Project: a Programmable Timer
    • Shift Registers and LFSRs
    • Pipelining
    • Stalls, Bubbles and Hazards
    • Retiming
  • FSM & Control27
  • Timing & Clocking39
  • Reset & CDC34
  • Memory & Buses57
  • Advanced RTL28
  • Senior & Interview76
  • Glossary and Notation
Loading lesson

‹Counters and Timers
  1. Topics
  2. RTL Design
  3. Sequential Design
  4. Counters and Timers
  5. Mini Project: a Programmable Timer

Part 3 · Sequential · Intermediate

Mini Project: a Programmable Timer

A hands-on mini project: a timer peripheral core with an 8-bit prescaler, a 16-bit counter, one-shot and auto-reload modes, and a sticky compare interrupt, checked cycle by cycle against a reference model.

On this page

    Why this matters

    Building the timer proves you can turn a spec full of boundaries (prescale, period, one-shot end, set against clear) into registers that match a cycle-accurate model on every edge, including random register changes mid-period.

    After this lesson you can: write a priority-ordered timer with prescaler, load, one-shot and auto-reload modes and a write-1-to-clear flag, check it against a cycle model, and count its flops in Yosys.

    Build the timer every microcontroller has

    A 50 MHz chip needs a 1 ms interrupt. Set the prescaler to 49 and the period to 999, and the timer steps once every 50 cycles and fires once every 1000 steps: 50,000 cycles, exactly 1 ms. If the prescaler divides by 49 instead of 50, the interrupt comes every 0.98 ms and a clock built on it gains about 29 minutes a day. Every boundary in a timer is an off-by-one waiting to happen, so this project writes them all down first.

    You will build the core of a timer peripheral: a prescaler, a 16-bit counter with load, one-shot and auto-reload modes, and a compare match that raises a sticky interrupt flag. It reuses this topic's counter, prescaler and watchdog lessons.

    The timer as built: the prescaler makes tick, the counter steps on tick and wraps or stops at top, the compare finds the step that lands on cmp, and the irq flag holds it until it is cleared

    Read it left to right along the top, then down.

    • Prescaler: a tick enable, not a divided clock. The whole timer runs on clk.

    • Counter: steps only on a tick. At top it wraps to 0 (auto-reload) or stops and raises done (one-shot).

    • Compare and flag: a match is a step that lands on cmp. The flag remembers it until software writes 1 to clear it.

    The spec

    Port

    Direction

    Width

    Meaning

    clk, rst_n

    input

    1, 1

    Clock; asynchronous active-low reset

    en

    input

    1

    1 runs the timer, 0 pauses it

    reload_mode

    input

    1

    1 auto-reload, 0 one-shot

    psc

    input

    8

    One step every psc + 1 enabled cycles

    top

    input

    16

    Last count of a period

    cmp

    input

    16

    Compare value

    load, load_val

    input

    1, 16

    Pulse: load the count

    irq_clr

    input

    1

    Write-1-to-clear for irq

    count

    output

    16

    The count

    tick

    output

    1

    High in the cycle a step is due

    irq

    output

    1

    Sticky compare-match flag

    done

    output

    1

    One-shot has finished

    1. Reset clears the count, the prescaler, irq and done.

    2. Prescaler: while en is 1 it counts 0 to psc, and tick is high in the last cycle, so a step is due every psc + 1 enabled cycles. en = 0 freezes the prescaler and the count; resuming keeps the phase.

    3. Step: on a tick, unless done or load is high, the count adds 1. At top, auto-reload goes to 0; one-shot holds the count and sets done. A period is (psc + 1) x (top + 1) cycles.

    4. Load beats counting: count gets load_val, the prescaler and done clear. A load never sets irq.

    5. Match: irq sets on the edge where a step moves the count to cmp, so it rises in the cycle the count first shows cmp.

    6. Flag: irq stays 1 until irq_clr. If a match and irq_clr arrive in the same cycle, set wins.

    7. Robust limits: pcnt >= psc and count >= top end the prescale or period, so a smaller value written mid-count takes effect at once instead of running round all 2¹⁶ values.

    Before you start

    • Counting as controlled state evolution: terminal count and why >= recovers.

    • Up/down counter with reload: the priority chain between reset, load and enable.

    • A prescaler feeding a timer tick: the tick enable this design uses.

    • Watchdog timer example: a sticky flag that only one thing clears.

    Done when

    Check

    How

    Done when

    Clean compile

    iverilog -g2012 -Wall -o sim.out timer_core.sv tb_timer_core.sv

    No output at all

    Clean lint

    verilator --lint-only -Wall timer_core.sv

    No warnings

    Bench passes

    vvp sim.out

    The PASS line, after the coverage line

    Bench can fail

    Swap the flag's set and clear (see Common mistakes)

    MISMATCH lines and a FAIL

    No latch, flops counted

    Yosys synth, then stat

    26 flops (8 prescaler, 16 count, done, irq), no $_DLATCH_

    The synthesis check, trimmed:

    Diagram
    $ yosys -p "read_verilog -sv timer_core.sv; synth -flatten -top timer_core; stat"
    ...
          270 cells
            9   $_ANDNOT_
           85   $_AND_
           26   $_DFFE_PN0P_
           16   $_MUX_
           39   $_NAND_
            1   $_NOR_
            6   $_NOT_
           44   $_ORNOT_
           11   $_OR_
           13   $_XNOR_
           20   $_XOR_

    Count the state in the spec first: 8 prescaler bits, 16 count bits, done and irq make 26. Yosys reports 26 $_DFFE_PN0P_ cells, flops with an enable and an asynchronous reset to 0, and no latch. A 27th flop would be a question about your code.

    Reference solution

    The full listing is under The test. Read it in three parts.

    design.sv
    1
    2
    3
    4
    5
    6
    // snippet: part 1, the decisions, all combinational
    assign tick       = en && (pcnt >= psc);
    assign step       = tick && !done && !load;
    assign at_top     = (count >= top);
    assign count_next = at_top ? 16'd0 : count + 16'd1;
    assign match      = step && !(at_top && !reload_mode) && (count_next == cmp);

    Every rule in the spec is one line here. step is the only event that moves the count. count_next is where a step goes. match compares that destination with cmp, which is why irq rises in the same cycle the count shows cmp. The one-shot stop is excluded: it moves nowhere, so it cannot match. Comparing count == cmp alone would stay true for every cycle the count waits on cmp, so a clear would be undone at once.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // snippet: part 2, the counter and prescaler registers
    end else if (load) begin                  // load beats counting
      pcnt  <= '0;
      count <= load_val;
      done  <= 1'b0;
    end else if (en) begin
      pcnt <= tick ? 8'd0 : pcnt + 8'd1;
      if (step) begin
        if (at_top && !reload_mode) done  <= 1'b1;  // one-shot: stop at top
        else                        count <= count_next;
      end
    end

    The if order is the priority from the spec: reset, then load, then enabled counting. With en low nothing is assigned, so every register holds; that is the pause.

    design.sv
    1
    2
    3
    4
    5
    // snippet: part 3, the sticky flag in its own block
    always_ff @(posedge clk or negedge rst_n)
      if (!rst_n)       irq <= 1'b0;
      else if (match)   irq <= 1'b1;
      else if (irq_clr) irq <= 1'b0;

    The set is tested before the clear, so a match in the clearing cycle wins. The flag has its own block so that load does not touch it.

    One period, slot by slot

    Each slot is one clock cycle; a step happens at the edge that closes a slot where tick is high.

    • Slots 1, 3, 5, 7: tick is high every second cycle (psc = 1), so the count reads 0, 1, 2 and 3 from slots 0, 2, 4 and 6.

    • Slot 4: the step lands on cmp = 2 and irq rises in the same slot the count shows 2.

    • Slot 7: the count is at top = 3 with tick high, so the next edge reloads it: slot 8 reads 0.

    • Slot 8: irq_clr is high with no match on that edge, so irq falls in slot 9.

    Project files

    The complete listing below contains every file of this project, each starting at a // ---- file: <name> ---- line. Save each part under its name in one folder, then run the script.

    File

    What it holds

    timer_core.sv

    The design: prescaler, counter, compare, reload and sticky interrupt

    tb_timer_core.sv

    The bench: directed cases and seeded random writes against a cycle model

    run.sh

    Diagram
    #!/bin/sh
    set -e
    iverilog -g2012 -Wall -o sim.out timer_core.sv tb_timer_core.sv
    vvp sim.out
    yosys -q -p "read_verilog -sv timer_core.sv; synth -flatten -top timer_core; stat"   # optional

    Expected output: the line a correct design makes the simulation print at the end of its run.

    Diagram
    PASS: timer_core matches the cycle model (directed load, pause, reload, one-shot, set beats clear; seeded random writes)

    The test

    A reference model in plain integers runs beside the design and predicts count, irq, done and tick after every rising edge; any difference is a MISMATCH. On top of that, directed checks name the behaviour they expect:

    • Load and count: load 1, then with psc = 2 two steps in six cycles reach cmp = 3 and set irq.

    • Stop and start: five paused cycles leave the count alone; the next step comes on time after resuming.

    • Reload: nine more steps run to top = 9, wrap to 0 and match cmp again.

    • Set beats clear: with irq already 1, irq_clr is driven in exactly the cycle that steps 6 to 7 = cmp. irq must stay 1, then a clear alone must drop it.

    • One-shot: from 0 to top = 4, done rises on the fifth step, the count holds for 20 more cycles, and a load restarts it.

    • Seeded random register writes: 20000 cycles with random en, load, load_val, irq_clr, and occasional new psc, top, cmp and mode, so values change mid-period.

    Diagram
    $ iverilog -g2012 -Wall -o sim.out timer_core.sv tb_timer_core.sv
    $ vvp sim.out
    checked 20076 cycles: 234 matches, 34 set-and-clear collisions, 759 reloads, 188 one-shot ends
    PASS: timer_core matches the cycle model (directed load, pause, reload, one-shot, set beats clear; seeded random writes)
    tb_timer_core.sv:110: $finish called at 200770 (1s)

    The coverage line matters as much as the PASS. Random writes produced 34 set-and-clear collisions, so the rule was tested far beyond the one directed case.

    timer_core.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    // ---- file: timer_core.sv ----
    module timer_core (
      input  logic        clk, rst_n,
      input  logic        en,          // 1: run, 0: pause (prescaler and count hold)
      input  logic        reload_mode, // 1: auto-reload at top, 0: one-shot
      input  logic [7:0]  psc,         // one count step every psc + 1 enabled cycles
      input  logic [15:0] top,         // last count of a period
      input  logic [15:0] cmp,         // compare value
      input  logic        load,        // pulse: count <= load_val, prescaler and done cleared
      input  logic [15:0] load_val,
      input  logic        irq_clr,     // write-1-to-clear for irq
      output logic [15:0] count,
      output logic        tick,        // the cycle in which a step is due
      output logic        irq,         // sticky compare-match flag
      output logic        done         // one-shot finished; cleared by load
    );
      // Prescaler: count enabled cycles 0..psc; tick in the last one.
      logic [7:0]  pcnt;
      assign tick = en && (pcnt >= psc);          // >=: a smaller psc written mid-count still ticks
    
      // One step of the counter, and the value it moves to.
      logic        step, at_top, match;
      logic [15:0] count_next;
      assign step       = tick && !done && !load;
      assign at_top     = (count >= top);         // >=: a smaller top written mid-period still ends it
      assign count_next = at_top ? 16'd0 : count + 16'd1;
      assign match      = step && !(at_top && !reload_mode) && (count_next == cmp);
    
      always_ff @(posedge clk or negedge rst_n)
        if (!rst_n) begin
          pcnt  <= '0;
          count <= '0;
          done  <= 1'b0;
        end else if (load) begin                  // load beats counting
          pcnt  <= '0;
          count <= load_val;
          done  <= 1'b0;
        end else if (en) begin
          pcnt <= tick ? 8'd0 : pcnt + 8'd1;
          if (step) begin
            if (at_top && !reload_mode) done  <= 1'b1;  // one-shot: stop at top
            else                        count <= count_next;
          end
        end
    
      // Sticky flag: set beats clear, so a match in the clearing cycle is not lost.
      always_ff @(posedge clk or negedge rst_n)
        if (!rst_n)       irq <= 1'b0;
        else if (match)   irq <= 1'b1;
        else if (irq_clr) irq <= 1'b0;
    endmodule
    

    Common mistakes

    Clear-first is the natural way to write the flag, because clearing feels like the command and setting like the background:

    design.sv
    // bad-example: clear tested first
    else if (irq_clr) irq <= 1'b0;
    else if (match)   irq <= 1'b1;
    Diagram
    MISMATCH cycle 47: count=7 want 7, irq=0 want 1, done=0 want 0, tick=1
    FAIL: set beats clear in the same cycle (count=7 irq=0 done=0)
    ...
    FATAL: tb_timer_core.sv:106: FAIL: 228 errors in 20076 cycles

    Cycle 47 is the directed collision: the count steps to 7, the match fires, and the clear erases it. The model says irq is 1; the design says 0.

    Common pitfall

    A clear that beats a set loses events. Software reads irq, handles the match, and writes 1 to clear it. If the next match lands in that same cycle, a clear-first flag drops it: in one-shot mode the interrupt never comes, and in auto-reload it comes a full period late. Make set win, and test the collision on purpose.

    Check yourself A 50 MHz clock, a 1 ms interrupt. Give one psc and top pair, and say why psc = 50 is wrong.

    A period is (psc + 1) x (top + 1) cycles, and 1 ms is 50,000 cycles. psc = 49 and top = 999 gives 50 x 1000. psc = 50 divides by 51, so the period is 51,000 cycles: 1.02 ms.

    Check yourself One-shot mode, psc = 0, top = 4, load_val = 0. How many rising edges after the load edge does done rise, and what does count read then?

    Five. With psc = 0 every enabled cycle is a step: four steps take the count from 0 to 4, and the fifth finds it at top, sets done and leaves the count at 4. That is top + 1 steps, the same period length as auto-reload.

    Stretch goals

    1. Add a capture input. On a rising edge of an external input, copy count into a capture register and set a second sticky flag. Synchronise the input first; see The two-flop synchroniser.

    2. Put it behind the CSR block. Map psc, top, cmp and a control word as read-write fields, count as read-only and irq as write-1-to-clear, then drive the bench through register writes. See Field access types and Interrupt register pattern.

    3. Add a watchdog mode. In this mode a kick input reloads the count, and reaching top sets a sticky reset request that only reset clears. See Watchdog timer example.

    Remember

    • A period is (psc + 1) x (top + 1) cycles; compare with >= so a smaller limit written mid-count ends the period at once.

    • Match on the step that lands on cmp, not on count == cmp, or the flag sets again while the count waits.

    • A sticky flag's set must beat its clear, or a match in the clearing cycle is lost.

    Rule of thumb: for every flag, write down what happens when set and clear arrive in the same cycle, then test exactly that cycle.

    How this was checked

    Tools: Icarus Verilog 13.0, Verilator 5.048, Yosys 0.69+post (git sha1 143eb14f9); run on 2026-10-17.

    Commands: iverilog -g2012 -Wall -o sim.out timer_core.sv tb_timer_core.sv && vvp sim.out; verilator --lint-only -Wall timer_core.sv; yosys -p "read_verilog -sv timer_core.sv; synth -flatten -top timer_core; stat"; node scripts/tools/gen-waves.mjs sequential:counters-timers for the waveform (wave-src/counters-timers/timer-reload.sv, the same core with psc = 1, top = 3, cmp = 2).

    Result: the compile prints nothing, lint is clean, the bench prints the coverage and PASS lines shown (20076 cycles, 34 set-and-clear collisions), and Yosys reports 270 cells with 26 $_DFFE_PN0P_ flops and no latch cell.

    Variant: timer_core_bad.sv (irq_clr tested before match) run on the same bench prints the MISMATCH at cycle 47 shown and fails with 228 errors.

    Scope and limits: functional, zero-delay, single clock, seed 11 for the random phase; the reference model is a second implementation, so a misunderstanding shared by spec and model would not be caught. No bus interface or CDC is modelled.

    Keep going

    • Practice: Up/down counter with sync load, enable, and wrap vs saturate modes

    • Code lab: Timer with prescaler

    • Code lab: Counter with load

    • Interview: run a mock interview on this topic

    Related topics

    • A prescaler feeding a timer tick

    • Interrupt register pattern

    Previous

    72 / 350

    Next: Shift Registers and LFSRs

    Up next

    Up nextShift Registers and LFSRs

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Counting as Controlled State Evolution
    • Up/Down Counter with Reload
    • A Prescaler Feeding a Timer Tick
    clktickcount012301irqirq_clr
    The timer with psc = 1, top = 3 and cmp = 2 in auto-reload mode: ticks every second cycle, the count reaching cmp and setting irq, the reload to 0, and a write-1-to-clear