Capstone: a programmable timer

Turn a timer brief into a sketch and then into RTL: registers, a prescaler that makes an enable, a down counter with reload, two modes and a restart, with the pulse spacing counted on the bench.

Lesson
R2.7
Time
35 min
Runs on
Your browser
Check
5 questions

A timer was programmed with a period of 100 and pulsed every 101 cycles. The terminal count was wrong, the oldest bug in this module. It hid inside a block that also had a prescaler, a one-shot mode and a restart.

The timer with PERIOD 4 pulses five edges apartEdges 4 to 12 of the lab bench, after CTRL starts the timer at edge 4 with PERIOD 4 and PRESCALE 1.
012345678clkcount432104321pulse
  1. Cycle 0

    Edge 4: the write to CTRL loads count with PERIOD, 4, and starts the timer.

  2. Cycle 3

    Edge 7: three steps after the start, count is 1. The terminal count is 0, so there is no pulse yet.

  3. Cycle 5

    Edge 9: the terminal count was 0, so count reloads only now and pulse is 1. Left running, the timer would pulse next at edge 14, five edges later; the bench restarts it there instead.

Measured on the lab bench. The count holds five values, 4 to 0, for a period of 4.

From the brief to a sketch

Start with the brief, then sketch registers and decisions, and only then write RTL. The brief fixes every choice the code must make, including the priorities.

The timer brief.
PortDirectionWidthMeaning
clkinput1clock
rst_ninput1reset, asynchronous, active-low
weinput1register write
addrinput[1:0]addr 0 CTRL: bit 0 enable, bit 1 one-shot; addr 1 PERIOD, 1 to 255; addr 2 PRESCALE, 1 to 15, 0 acts as 1
wdatainput[7:0]register write data
countoutput[7:0]the main counter, counting down: one step every PRESCALE cycles
pulseoutput11 for one cycle every PERIOD steps; comes from a flop: one cycle wide, no edge detector
runningoutput1cleared when a one-shot stops
Parameters
Modes: periodic reloads PERIOD; one-shot pulses once, stops with count at 0, and clears running
Corner cases
PERIOD 1; PRESCALE 0; a write to PERIOD while running; one-shot after periodic. Priority: reset, then a write to CTRL or PERIOD (restart), then the terminal count, then a step, then hold
The sketch: registers, two decisions, one outputArrows carry data. The prescaler produces step, an enable for the main counter; it never makes a clock.
The sketch: registers, two decisions, one outputArrows carry data. The prescaler produces `step`, an enable for the main counter; it never makes a clock. register CTRL, PERIOD,PRESCALE 13 flops, written bywe register prescaler pre_q 4 flops: counts toPRESCALE logic next count restart, reload, stop,step or hold register count, running 9 flops output pulse 1 flop: the terminalcount, registered PRESCALEstepPERIOD, modenext valuetc
  1. Prescaler

    The prescaler counts cycles and raises step in one cycle out of PRESCALE. Everything after it runs on the same clock.

  2. Next count

    One chain decides the next value: a write restarts from PERIOD, the terminal count reloads or stops, a step subtracts one, otherwise hold.

  3. Pulse

    The terminal count at a step is registered into pulse. It shows in the cycle the counter reloads, one cycle wide.

Read the diagram as text
  • CTRL, PERIOD, PRESCALE: 13 flops, written by we
  • prescaler pre_q: 4 flops: counts to PRESCALE
  • next count: restart, reload, stop, step or hold
  • count, running: 9 flops
  • pulse: 1 flop: the terminal count, registered
  • CTRL, PERIOD, PRESCALE → prescaler pre_q: PRESCALE
  • prescaler pre_q → next count: step
  • CTRL, PERIOD, PRESCALE → next count: PERIOD, mode
  • next count → count, running: next value
  • count, running → pulse: tc

Measured on this flow: 27 flops in all, the same in the lab design and its fix.

The reload is where the off-by-one hides

A down counter that loads PERIOD and pulses when it reaches 0 holds PERIOD + 1 values: 4, 3, 2, 1, 0. The reload value and the terminal count decide the period together, so change one and you must check the other.

Reload valueTerminal countEdges between pulses, PERIOD 4
PERIOD05 (the lab design)
PERIOD14
PERIOD - 104
PERIOD - 113

Measured on the lab bench with PRESCALE 1. Two pairs are right; the brief's wording picks one.

Terminal count 0

wire tc = step && (count == 8'd0);

Five values per turn: pulses five edges apart for PERIOD 4.

Terminal count 1

wire tc = step && (count == 8'd1);

Four values, 4 down to 1: pulses at edges 8 and 12.

This timer keeps the reload at PERIOD, so count shows the steps left, 4 down to 1. The fix is one constant. Then the bench counts the spacing for you: every pulse line prints the edges since the last restart or pulse.

Prescaler, modes and restart

  1. The prescaler makes an enable

    It counts cycles and raises step once every PRESCALE cycles. The main counter still runs on clk and only moves when step is 1. A divided clock would be a new clock domain, with the problems of R2.3.

  2. One-shot changes one branch

    At the terminal count, periodic mode reloads PERIOD. One-shot mode loads 0 and clears running instead. Everything else is shared.

  3. A write restarts the count

    Writing CTRL or PERIOD loads count and clears the prescaler in the same edge. This is a reset carried as data, so it sits in the chain just below rst_n.

  4. The pulse is registered

    pulse <= tc gives a clean one-cycle output from a flop. No edge detector is needed, because the terminal count is already one cycle long.

The fixed timer, PERIOD 4Edges 4 to 12 of the lab bench with the terminal count at 1.
012345678clkcount432143214pulse
  1. Cycle 3

    Edge 7: count is 1 and a step is due: this is the terminal count.

  2. Cycle 4

    Edge 8: the counter reloads 4 and the registered pulse is 1, four edges after the start at edge 4.

  3. Cycle 8

    Edge 12: the next pulse, four edges later.

Measured on the lab bench. With PRESCALE 2 the same timer pulses every 8 edges.

The rest of the bench, measured on the fixed timer: PRESCALE 2 gives pulses 8 edges apart. One-shot with PERIOD 3 pulses once, 3 edges after the start, then reads running=0 count=0. PERIOD 5 gives 5 edges, and rewriting PERIOD to 2 while running restarts it: 2 edges.

The lab

The lab ships the timer with the terminal count at 0. Run it: the pulse lines read 5, 10, 4, 6 and 3 edges where the brief asks for 4, 8, 3, 5 and 2. Change the 0 in tc to 1 and run again. Synthesize both: 131 cells and 15 levels of logic before, 143 cells and 16 levels after, 27 flops each.

Asked in interviews
  1. How does a prescaler work?

    A counter on the main clock that raises a one-cycle enable every N cycles. The logic it feeds stays on the same clock and moves only when the enable is 1. It never drives a clock pin.

  2. Design a timer with a programmable period: what registers?

    Control (enable, mode), period, and prescale, written by the bus; then the prescaler count, the main count and a running flag; and a flop for the output pulse. Write the brief and the priorities first.

  3. Where does the off-by-one hide?

    Between the reload value and the terminal count. Loading PERIOD and stopping at 0 counts PERIOD + 1 steps. Pick a pair that gives PERIOD, then count the pulse spacing on a bench.

Try it

The first nine lines watch every edge: count runs 4, 3, 2, 1, 0 and reloads at edge 9 with the pulse. After that, each pulse line gives the distance from the last restart or pulse: 10 with PRESCALE 2, 4 for a one-shot of 3, 6 and 3 for periods of 5 and 2. Each is one step too long.

With count == 8'd1 the watched pulses fall at edges 8 and 12, and the pulse lines read 4, then 8, 8 and 8, then 3, then 5, 2 and 2.

RTLIcarus Verilog · in your browser · Lint: Verilator service · Synthesize: Yosys service
Edit the design, then Run, Lint or Synthesize
Expected: e4 count=4 pulse=0 e5 count=3 pulse=0 e6 count=2 pulse=0 e7 count=1 pulse=0 e8 count=0 pulse=0 e9 count=4 pulse=1 e10 count=3 pulse=0 e11 count=2 pulse=0 e12 count=1 pulse=0 -- PRESCALE 2 e24 pulse, 10 edges after e14 e34 pulse, 10 edges after e24 -- one-shot, PERIOD 3, PRESCALE 1 e45 pulse, 4 edges after e41 running=0 count=0 -- periodic, PERIOD 5, then PERIOD 2 while running e57 pulse, 6 edges after e51 e62 pulse, 3 edges after e59

Original design · 131 cells · 27 flip-flops · 0 latches · 15 logic levels. Synthesis uses Yosys; counts depend on the toolchain.

Read included file: tb.sv
`timescale 1ns/1ps
module tb;
  logic clk = 0;
  always #5 clk = ~clk;                   // rising edges at 5, 15, 25, ...
  logic rst_n = 1;                        // falls at 1 ns: a real edge, so an asynchronous reset acts before any clock
  int edge_n = -1;
  always @(posedge clk) edge_n = edge_n + 1;
  logic       we = 0;
  logic [1:0] addr = 0;
  logic [7:0] wdata = 0;
  logic [7:0] count;
  logic       pulse, running;
  bit watch = 0;                 // 1: one line per edge; 0: one line per pulse
  int last = 0;                  // edge of the last restart or pulse
  // One register write: we is 1 for one edge. CTRL (0) and PERIOD (1) restart the count.
  task automatic write(input logic [1:0] a, input logic [7:0] v);
    we = 1; addr = a; wdata = v; tick(); we = 0;
    if (a != 2) last = edge_n;
  endtask
  timer dut (.clk(clk), .rst_n(rst_n), .we(we), .addr(addr), .wdata(wdata),
             .count(count), .pulse(pulse), .running(running));
  // Advance to the next rising edge, wait for the design to settle, then print one line.
  task automatic tick();
    @(posedge clk); #1;
    if (watch) $display("e%0d count=%0d pulse=%b", edge_n, count, pulse);
    else if (pulse) $display("e%0d pulse, %0d edges after e%0d", edge_n, edge_n - last, last);
    if (pulse) last = edge_n;
  endtask
  initial begin
    #1 rst_n = 0; tick(); tick(); rst_n = 1;   // asserted at 1 ns, edges 0 and 1 in reset, released 1 ns after edge 1
    write(2, 1);                 // PRESCALE 1
    write(1, 4);                 // PERIOD 4
    watch = 1;
    write(0, 8'b01);             // CTRL: enable, periodic
    repeat (8) tick();
    watch = 0;
    $display("-- PRESCALE 2");
    write(2, 2);
    write(0, 8'b01);             // restart
    repeat (24) tick();
    $display("-- one-shot, PERIOD 3, PRESCALE 1");
    write(2, 1);
    write(1, 3);
    write(0, 8'b11);             // CTRL: enable, one-shot
    repeat (8) tick();
    $display("running=%b count=%0d", running, count);
    $display("-- periodic, PERIOD 5, then PERIOD 2 while running");
    write(1, 5);
    write(0, 8'b01);
    repeat (7) tick();
    write(1, 2);
    repeat (4) tick();
    $finish(0);                             // quiet: no "$finish called" line
  end
endmodule

Check yourself

Predict the outputThe fixed timer (terminal count 1) is programmed with PRESCALE 1 and PERIOD 5, then started. The program counts pulses over the next 20 edges. What does it print?
// timer: a programmable timer with a prescaler, periodic and one-shot modes.
// Registers, written through we/addr/wdata (one write per cycle):
//   addr 0 CTRL     bit 0 enable, bit 1 one-shot (0: periodic)
//   addr 1 PERIOD   1..255: pulse every PERIOD steps
//   addr 2 PRESCALE 1..15: one step every PRESCALE cycles (0 acts as 1)
// A write to CTRL or PERIOD restarts the count from PERIOD.
module timer (
  input  logic       clk,
  input  logic       rst_n,
  input  logic       we,
  input  logic [1:0] addr,
  input  logic [7:0] wdata,
  output logic [7:0] count,      // the main counter, counting down
  output logic       pulse,      // one cycle wide, every PERIOD steps
  output logic       running
);
  logic       oneshot_q;
  logic [7:0] period_q;
  logic [3:0] prescale_q, pre_q;

  wire wr_ctrl   = we && addr == 2'd0;
  wire wr_period = we && addr == 2'd1;
  wire wr_pre    = we && addr == 2'd2;

  // Register writes.
  always_ff @(posedge clk or negedge rst_n)
    if (!rst_n) begin
      oneshot_q <= 1'b0; period_q <= 8'd1; prescale_q <= 4'd1;
    end else begin
      if (wr_ctrl)   oneshot_q  <= wdata[1];
      if (wr_period) period_q   <= wdata;
      if (wr_pre)    prescale_q <= wdata[3:0];
    end

  // The prescaler: step is 1 in one cycle out of PRESCALE while running.
  wire pre_tc = (pre_q + 4'd1 >= prescale_q);
  wire step   = running && pre_tc;
  always_ff @(posedge clk or negedge rst_n)
    if (!rst_n)                      pre_q <= '0;
    else if (wr_ctrl || wr_period)   pre_q <= '0;            // restart
    else if (running)                pre_q <= pre_tc ? 4'd0 : pre_q + 4'd1;

  // The main counter: loads PERIOD, counts down one per step, reloads at the terminal count.
  wire tc = step && (count == 8'd1);
  always_ff @(posedge clk or negedge rst_n)
    if (!rst_n) begin
      count <= 8'd0; running <= 1'b0; pulse <= 1'b0;
    end else begin
      pulse <= tc;
      if (wr_ctrl || wr_period) begin                       // restart
        count   <= wr_period ? wdata : period_q;
        if (wr_ctrl) running <= wdata[0];
      end else if (tc) begin
        if (oneshot_q) begin count <= 8'd0; running <= 1'b0; end   // stop
        else           count <= period_q;                         // reload
      end else if (step)
        count <= count - 8'd1;
    end
endmodule

module tb;
  logic clk = 0, rst_n = 1, we = 0;
  logic [1:0] addr = 0;
  logic [7:0] wdata = 0, count;
  logic       pulse, running;
  int pulses = 0, n = 0, prev = -1, bad = 0;
  timer dut (.clk(clk), .rst_n(rst_n), .we(we), .addr(addr), .wdata(wdata),
             .count(count), .pulse(pulse), .running(running));
  task automatic edge1(); #1 clk = 1; #1 clk = 0; n++; endtask
  task automatic write(input logic [1:0] a, input logic [7:0] v);
    we = 1; addr = a; wdata = v; edge1(); we = 0;
  endtask
  initial begin
    #1 rst_n = 0;
    #1 rst_n = 1;
    write(2, 1);                         // PRESCALE 1
    write(1, 5);                 // PERIOD 5
    write(0, 8'b01);                     // CTRL: enable, periodic
    repeat (20) begin
      edge1();
      if (pulse) begin
        pulses++;
        if (prev >= 0 && n - prev != 5) bad++;
        prev = n;
      end
    end
    $display("pulses=%0d", pulses);
    $finish(0);
  end
endmodule
Spot the bugEach option is the terminal-count condition tc of the timer, which reloads count with PERIOD. The bench sets PERIOD 3 and expects four pulses, 3 edges apart, in 12 edges. Which condition fails?
ConceptWhy does the prescaler drive an enable, step, into the main counter instead of a divided clock?
ConceptCompared with periodic mode, what does one-shot mode change in the timer's next-count decision?
Read the waveformThis is the fixed timer with PERIOD 4, edges 4 to 12. At which edge is the terminal count seen, and when does the reload show?
The fixed timer
012345678count432143214pulse

One value per edge, printed after the edge, from edge 4 on the left to edge 12 on the right.

Remember

  • Write the brief first: registers, modes, priorities, outputs and corner cases; then sketch registers and decisions; then write RTL.
  • A down counter's reload value and terminal count set its period together; loading PERIOD and stopping at 0 gives PERIOD + 1.
  • A prescaler produces a one-cycle enable on the main clock, never a divided clock.
  • Count the pulse spacing on the bench for every mode; a restart, a one-shot and a rewrite while running each need a line.