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.
- Cycle 0
Edge 4: the write to CTRL loads
countwith PERIOD, 4, and starts the timer. - Cycle 3
Edge 7: three steps after the start,
countis 1. The terminal count is 0, so there is no pulse yet. - Cycle 5
Edge 9: the terminal count was 0, so
countreloads only now andpulseis 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.
| Port | Direction | Width | Meaning |
|---|---|---|---|
| clk | input | 1 | clock |
| rst_n | input | 1 | reset, asynchronous, active-low |
| we | input | 1 | register write |
| addr | input | [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 |
| wdata | input | [7:0] | register write data |
| count | output | [7:0] | the main counter, counting down: one step every PRESCALE cycles |
| pulse | output | 1 | 1 for one cycle every PERIOD steps; comes from a flop: one cycle wide, no edge detector |
| running | output | 1 | cleared when a one-shot stops |
- Parameters
- Modes: periodic reloads PERIOD; one-shot pulses once, stops with
countat 0, and clearsrunning - 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
step, an enable for the main counter; it never makes a clock.- Prescaler
The prescaler counts cycles and raises
stepin one cycle out of PRESCALE. Everything after it runs on the same clock. - 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.
- 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 value | Terminal count | Edges between pulses, PERIOD 4 |
|---|---|---|
| PERIOD | 0 | 5 (the lab design) |
| PERIOD | 1 | 4 |
| PERIOD - 1 | 0 | 4 |
| PERIOD - 1 | 1 | 3 |
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
The prescaler makes an enable
It counts cycles and raises
steponce every PRESCALE cycles. The main counter still runs onclkand only moves whenstepis 1. A divided clock would be a new clock domain, with the problems of R2.3.One-shot changes one branch
At the terminal count, periodic mode reloads PERIOD. One-shot mode loads 0 and clears
runninginstead. Everything else is shared.A write restarts the count
Writing CTRL or PERIOD loads
countand clears the prescaler in the same edge. This is a reset carried as data, so it sits in the chain just belowrst_n.The pulse is registered
pulse <= tcgives a clean one-cycle output from a flop. No edge detector is needed, because the terminal count is already one cycle long.
- Cycle 3
Edge 7:
countis 1 and a step is due: this is the terminal count. - Cycle 4
Edge 8: the counter reloads 4 and the registered
pulseis 1, four edges after the start at edge 4. - 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
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.
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.
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.
Synthesize the design to measure its cells and inspect the mapped gates.
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 e59Original 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
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.