Skip to content

A timer peripheral is a down-counter wrapped in a CSR block, and almost everything subtle about it lives in when the counter reloads. You have learned the APB slave interface and the register-design vocabulary — RW config, RO status, W1C clear, write-only triggers. A timer is where all of it lands in one real, shippable block: a value you write (TimerLoad), a count you read (TimerValue), a control word that picks the mode (TimerControl), an interrupt you clear by writing (TimerIntClr), and a background load (TimerBGLoad) that changes the period without kicking the counter mid-flight. We ground this in the Arm SP804 dual-timer register map — a part you will meet in real SoCs — and the single non-obvious idea to carry is the BGLoad vs Load distinction: writing Load restarts the current period from a new value; writing BGLoad changes the next period's reload value while letting the in-flight count run undisturbed to zero. Confuse the two and you ship a timer that glitches its period every time firmware retunes it.

1. Problem statement

The problem is building a programmable down-counter that firmware drives entirely through an APB register interface — start it, pick its mode, time an interval, take an interrupt, and re-time it on the fly — without ever disturbing a count that is mid-flight.

A timer looks trivial: load a number, count down, interrupt at zero. The engineering is in the corners that a one-line description hides:

  • It must be fully programmable over the bus. Everything — the load value, the enable, the mode, the prescale divider, the counter width, the interrupt enable and mask — is a field firmware writes through APB. So the timer is, first, a CSR block: a register map with mixed access types decoded off PADDR, committed off PSEL && PENABLE && PWRITE, read back through a data mux.
  • It has three distinct counting personalities. One-shot counts down once and stops. Periodic counts down, reloads from Load, and repeats. Free-running ignores Load at zero and wraps through the full counter width. These are not three timers — they are one down-counter whose reload-and-stop behaviour is selected by TimerControl bits.
  • Re-timing must not glitch the in-flight period. This is the real problem. Firmware that wants to change a periodic timer's interval has two registers it could write: TimerLoad, which immediately reloads the counter and restarts the current period from the new value (a visible glitch — the current interval is cut short or stretched), or TimerBGLoad, which writes the reload register only, so the in-flight count finishes its current period normally and the new value takes effect at the next zero crossing. Picking the wrong one is the classic timer bug.
  • The interrupt must be a clean, software-clearable event. At zero the timer raises a raw interrupt; masked by the interrupt-enable it becomes the line the CPU sees. Software clears it by writing a clear register (write-only, W1C) — not by reading, not by writing the count — and that clear must deassert the masked interrupt deterministically.

So the job is not "count down." It is to wrap a prescaled down-counter in an APB CSR interface that exposes load, value, control, and interrupt-clear registers, supports three modes, and — the load-bearing part — separates the reload value from the live count so the period can be retuned without a glitch.

2. Why previous knowledge is insufficient

This module taught the register vocabulary; the timer is the first chapter where you must assemble that vocabulary into a real peripheral with live, self-changing state — and the assembly raises a problem none of the building-block chapters faced.

  • CSR fundamentals typed registers in isolation. It taught RW, RO, WO, W1C as independent access contracts. A timer forces them to interact: TimerValue (RO) is a live image of a counter that TimerLoad (RW) seeds, TimerControl (RW) gates, and TimerIntClr (WO/W1C) reacts to. The hard part is the coupling between registers, not any one register's access type.
  • Status registers taught W1C for static event flags. The timer's RIS/MIS/IntClr are exactly that mechanism — but driven by a counter reaching zero, with a mask (INTENABLE) between raw and masked. The W1C chapter gave you the clear mechanic; it did not give you the event source (a zero-crossing) or the mask path (raw → masked) that a timer wires up.
  • Configuration registers treated config as set-once-then-frozen. TimerControl is configuration — but a timer's config is changed while it runs (retune the period, flip one-shot to periodic), and changing it interacts with a live counter. The "lock it and freeze it" model from that chapter is the wrong frame for a register you reprogram mid-run.
  • None of them has a register that changes by itself. Every prior register held what software wrote until software wrote again. TimerValue decrements every (prescaled) clock with no bus activity at all, and reloads itself at zero. That self-modifying behaviour — and the question of which register feeds the reload — is the genuinely new thing, and it is exactly where BGLoad vs Load lives.

The gap is this: prior chapters gave you static registers with clean read/write semantics. A timer is a register block married to an autonomous state machine, where one register (Load/BGLoad) feeds a reload value and another (Value) is the live count, and the whole design turns on keeping those two separable so a retune doesn't disturb a running period. Building that is this chapter.

3. Mental model

The model: a timer is one down-counter with two value registers behind it — a reload register (what the counter restarts from) and the live count (what it is right now) — and every subtle behaviour is a question of which one a write touches and when the reload fires.

Picture three pieces feeding a single counter:

  • The reload register holds the period. TimerLoad writes it and force-loads the counter immediately; TimerBGLoad writes only this register, leaving the counter alone. So both writes change "the next period," but only Load also restarts "the current period." This is the whole BGLoad-vs-Load distinction in one sentence: same destination for the period, different effect on the live count.
  • The prescaler sits between PCLK and the counter's decrement. PRESCALE in TimerControl selects divide-by-1, -16, or -256, so one decrement happens every 1, 16, or 256 clocks — stretching the timable range without a wider counter. The counter only ticks on a prescaler tick-enable, not every clock.
  • The mode logic decides what happens at the zero crossing. Free-running (MODE=0, the SP804 power-up default): ignore the reload, wrap to all-ones, keep going. Periodic (MODE=1): reload the counter from the reload register and continue. One-shot (ONESHOT=1, overrides MODE): reload nothing, halt the counter, leave ENABLE effectively spent until reprogrammed.

Three refinements make it precise:

  • The interrupt is a separate, latched event hung off the zero crossing. Reaching zero sets the raw interrupt status (RIS). The mask INTENABLE ANDs raw → masked (MIS), which is the actual CPU line. Software clears it by writing TimerIntClr (W1C/WO) — the clear resets the latched RIS, not the counter. The count and the interrupt are decoupled: the counter keeps running (in periodic/free-running) while the interrupt waits to be serviced.
  • Value is a read-only window onto the live counter, never a write target. Firmware reads TimerValue to sample the count; it cannot poke it. To change the count you write Load. This is the RO contract — a hardware-owned live value exposed for observation only.
  • One-shot is a trap door, not a mode bit you can casually leave set. ONESHOT=1 makes the timer fire once and stop, overriding periodic. A periodic interrupt that "only fires once" is almost always a stale ONESHOT bit — the single most common timer-config bug, and the reason mode bits must be read back and verified.
A block diagram of an APB timer. On the left the APB signals enter a register-decode block exposing TimerLoad, TimerBGLoad, TimerValue, TimerControl, TimerIntClr, TimerRIS and TimerMIS. TimerLoad and TimerBGLoad feed a reload register; a reload mux selects between the reload value and a force-load on a Load write, driving a down-counter. PCLK feeds a prescaler that gates the counter decrement. TimerControl supplies ENABLE, MODE, ONESHOT, SIZE and INTENABLE to mode logic that at zero either reloads, wraps, or halts and sets an interrupt latch. The latch sets RIS, INTENABLE masks it to MIS and the IRQ output, and TimerIntClr clears it. TimerValue reads the live counter.
Figure 1 — the SP804-style timer peripheral as an APB CSR block wrapped around one prescaled down-counter. The APB interface (PSEL/PENABLE/PWRITE/PADDR/PWDATA/PRDATA) decodes into the register file: TimerLoad and TimerBGLoad both write the reload register, but Load also force-loads the live counter through the reload mux while BGLoad updates the reload value only and leaves the counter running. PCLK feeds a prescaler (÷1/÷16/÷256 from TimerControl.PRESCALE) whose tick-enable gates the down-counter's decrement. TimerControl supplies ENABLE, MODE (free-running/periodic), ONESHOT, SIZE (16/32-bit) and INTENABLE to the mode-and-reload logic. At the zero crossing the mode logic reloads (periodic) or wraps (free-running) or halts (one-shot) and pulses the interrupt latch, which sets RIS; INTENABLE masks RIS into MIS; the W1C TimerIntClr write clears the latch and deasserts the IRQ. TimerValue is a read-only window onto the live counter. The figure frames the timer as register-fed control logic over a single down-counter, with the reload mux as the heart of the BGLoad-vs-Load distinction.

4. Real SoC implementation

The SP804 dual-timer exposes, per timer, the register map below (offsets are within one timer's 0x20 block; the second timer sits at +0x20). This is the real map — load, value, control, three interrupt registers, and the background load.

OffsetRegisterAccessResetPurpose
0x00TimerXLoadRW0x0Load value; writing it force-loads the counter immediately and restarts the current period
0x04TimerXValueRO0xFFFFFFFFLive current count — read-only window onto the down-counter
0x08TimerXControlRW0x20ENABLE[7], MODE[6] (0=free-run,1=periodic), INTENABLE[5], PRESCALE[3:2] (÷1/÷16/÷256), SIZE[1] (16/32-bit), ONESHOT[0]
0x0CTimerXIntClrWOWrite any value to clear the latched interrupt (W1C semantics — the act of writing clears)
0x10TimerXRISRO0x0Raw interrupt status — set at the zero crossing, before masking
0x14TimerXMISRO0x0Masked interrupt status — RIS && INTENABLE; this is the line the CPU sees
0x18TimerXBGLoadRW0x0Background load — updates the reload value only, does not force-load the running counter

The RTL below is the heart of it: APB decode, the prescaled down-counter with the reload mux that distinguishes Load from BGLoad, the TimerControl field decode, and the W1C TimerIntClr against the raw/masked interrupt. Real APB signal names throughout.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ============================================================
// SP804-style APB timer — one timer instance (0x20 register block)
// ============================================================
//  Offset  Register        Access  Reset        Notes
//  0x00    TimerXLoad      RW      0x0          write force-loads the counter NOW (restarts period)
//  0x04    TimerXValue     RO      0xFFFFFFFF   live down-counter
//  0x08    TimerXControl   RW      0x20         ENABLE/MODE/INTENABLE/PRESCALE/SIZE/ONESHOT
//  0x0C    TimerXIntClr    WO      -            any write clears the latched interrupt (W1C)
//  0x10    TimerXRIS       RO      0x0          raw interrupt (set at zero crossing)
//  0x14    TimerXMIS       RO      0x0          masked interrupt = RIS & INTENABLE
//  0x18    TimerXBGLoad    RW      0x0          updates reload value only (NO force-load)
 
logic [31:0] load_reg;     // the reload value (period) — written by BOTH Load and BGLoad
logic [31:0] counter;      // the live down-counter — TimerValue reads this
logic [7:0]  ctrl;         // TimerControl
logic        ris;          // raw interrupt latch
 
// --- TimerControl field decode (real SP804 bit positions) ---
wire        en        = ctrl[7];            // ENABLE
wire        mode      = ctrl[6];            // 0 = free-running, 1 = periodic
wire        int_en    = ctrl[5];            // INTENABLE (mask)
wire [1:0]  prescale  = ctrl[3:2];          // 00:÷1  01:÷16  10:÷256
wire        size32    = ctrl[1];            // 0 = 16-bit, 1 = 32-bit
wire        oneshot   = ctrl[0];            // ONESHOT overrides MODE
 
// --- APB access decode (write commits in ACCESS phase) ---
wire wr_en   = psel && penable && pwrite;
wire wr_load   = wr_en && (paddr[4:0] == 5'h00);  // TimerLoad
wire wr_ctrl   = wr_en && (paddr[4:0] == 5'h08);  // TimerControl
wire wr_intclr = wr_en && (paddr[4:0] == 5'h0C);  // TimerIntClr (W1C — any write clears)
wire wr_bgload = wr_en && (paddr[4:0] == 5'h18);  // TimerBGLoad
 
// --- Prescaler: one tick-enable every 1 / 16 / 256 clocks ---
logic [7:0] pre_cnt;
wire [7:0]  pre_max = (prescale == 2'b00) ? 8'd0    // ÷1   -> tick every clock
                    : (prescale == 2'b01) ? 8'd15   // ÷16
                    :                       8'd255;  // ÷256
wire tick = en && (pre_cnt == pre_max);             // counter decrements on tick
 
always_ff @(posedge pclk or negedge presetn)
  if (!presetn)         pre_cnt <= 8'd0;
  else if (!en)         pre_cnt <= 8'd0;
  else if (pre_cnt == pre_max) pre_cnt <= 8'd0;
  else                  pre_cnt <= pre_cnt + 8'd1;
 
// --- The reload register: BOTH Load and BGLoad write it, masked by SIZE ---
wire [31:0] wmask = size32 ? 32'hFFFF_FFFF : 32'h0000_FFFF;
always_ff @(posedge pclk or negedge presetn)
  if (!presetn)            load_reg <= 32'h0;
  else if (wr_load)        load_reg <= pwdata & wmask;   // Load updates reload value...
  else if (wr_bgload)      load_reg <= pwdata & wmask;   // ...and so does BGLoad (same dest)
 
// --- The down-counter: THIS is where Load and BGLoad DIFFER ---
wire at_zero = (counter == 32'h0);
always_ff @(posedge pclk or negedge presetn) begin
  if (!presetn)            counter <= 32'hFFFF_FFFF;     // free-running default at reset
  else if (wr_load)        counter <= pwdata & wmask;    // Load FORCE-LOADS the live count NOW
  // NOTE: wr_bgload is deliberately ABSENT here — BGLoad must NOT disturb the running count.
  else if (tick) begin
    if (at_zero) begin
      if (oneshot)         counter <= counter;           // one-shot: halt at zero, no reload
      else if (mode)       counter <= load_reg;          // periodic: reload from (possibly BG-updated) value
      else                 counter <= wmask;             // free-running: wrap to all-ones
    end else
                           counter <= counter - 32'h1;   // normal decrement
  end
end
 
// --- TimerControl write ---
always_ff @(posedge pclk or negedge presetn)
  if (!presetn)     ctrl <= 8'h20;                 // reset: free-running, int disabled
  else if (wr_ctrl) ctrl <= pwdata[7:0];
 
// --- Interrupt: raw latch set at the zero crossing; W1C clear via TimerIntClr ---
wire zero_cross = en && tick && at_zero;          // pulse when counter hits zero on a tick
always_ff @(posedge pclk or negedge presetn)
  if (!presetn)        ris <= 1'b0;
  else if (wr_intclr)  ris <= 1'b0;                // ANY write to IntClr clears the latch (W1C)
  else if (zero_cross) ris <= 1'b1;                // zero crossing latches raw interrupt
wire mis = ris & int_en;                           // masked interrupt = the CPU-visible IRQ
assign timer_irq = mis;
 
// --- APB read mux: TimerValue / RIS / MIS are read-only live images ---
always_comb unique case (paddr[4:0])
  5'h00:   prdata = load_reg;
  5'h04:   prdata = counter;                       // TimerValue: live count, RO
  5'h08:   prdata = {24'h0, ctrl};
  5'h10:   prdata = {31'h0, ris};                  // RIS, RO
  5'h14:   prdata = {31'h0, mis};                  // MIS, RO
  5'h18:   prdata = load_reg;                       // BGLoad reads back the reload value
  default: prdata = 32'h0;                          // IntClr (WO) reads as 0
endcase

Three facts make this a timer and not a plain counter. First, the reload register and the live counter are two separate flops, and the only structural difference between Load and BGLoad is that wr_load appears in the counter's force-load branch while wr_bgload does not — that single absent line is the entire BGLoad guarantee. Second, the prescaler gates the decrement, not the clock: tick is a one-cycle enable, so the counter sees PCLK but only counts down once per prescale period, which is how a 32-bit counter at ÷256 times very long intervals. Third, the interrupt is latched and decoupled from the count: ris is set by the zero crossing and cleared only by a TimerIntClr write (W1C), so the counter keeps reloading and running in periodic mode while the interrupt patiently waits to be serviced — and mis = ris & int_en means masking happens at the very last gate, exactly as the SP804 RIS/MIS split prescribes. The commit machinery underneath is the ordinary APB write logic and read-data mux; the timer only adds the counter and the reload semantics on top.

5. Engineering tradeoffs

The design decisions in a timer are mostly about which knob to expose and what it costs — and the central one is the Load/BGLoad pair, which exists precisely because force-load and background-load are two genuinely different operations with different uses.

DecisionOption AOption BWhen to pick which
Retune the periodTimerLoad — force-loads now, restarts the current periodTimerBGLoad — updates reload value only, current period finishesUse Load to immediately change timing (start fresh, abort current interval); use BGLoad to seamlessly change the next period with no glitch to the in-flight count
Counting personalityPeriodic — auto-reload at zero, repeatsOne-shot — fire once, haltPeriodic for a steady tick (RTOS scheduler, PWM); one-shot for a single timeout (watchdog kick, debounce)
Range vs resolutionPRESCALE=÷1 — full clock resolution, short max intervalPRESCALE=÷256 — coarse resolution, 256× longer max intervalFine resolution for short precise delays; high prescale to time long intervals without a wider counter
Counter widthSIZE=16-bit — smaller, shorter max countSIZE=32-bit — larger range16-bit for legacy/compact timers; 32-bit when the interval (even prescaled) needs the range
Interrupt visibilityRead RIS (raw)Read MIS (masked)RIS to see the event regardless of mask (debug, polling a disabled-interrupt timer); MIS to see exactly what the CPU's interrupt controller sees

The throughline: Load and BGLoad are not redundant — they are the force-load and the seamless-update of the same period, and choosing wrongly is a functional bug, not a style choice. A scheduler that retunes its tick period with Load introduces a one-time glitch (the current tick is cut short or stretched) every time it changes frequency; the same scheduler using BGLoad changes the period with the current tick finishing cleanly and the new period starting at the next reload — invisible to the timing it controls. The prescale and size knobs trade range against resolution and area; the mode and interrupt knobs trade autonomy (periodic, masked) against control (one-shot, raw-visible). None of these is free, and the spec exposes all of them because real systems need each.

6. Common RTL mistakes

7. Debugging scenario

The signature timer bug is the period glitch from using Load instead of BGLoad to retune a running periodic timer — the timer keeps working, but every frequency change produces a one-shot wrong interval that ripples into whatever the timer paces.

  • Observed symptom: an audio or PWM subsystem driven by a periodic timer produces an audible click or a visible flicker exactly when firmware changes the rate — never otherwise. The timer is clearly running and interrupting at the right new rate afterward; only the transition is wrong. Single-stepping the rate-change code never reproduces it because the glitch is one period long and the debugger's pauses mask it.

  • Waveform clue: the APB trace shows a write to TimerLoad (PSEL, PENABLE, PWRITE high, PADDR = 0x00) carrying the new period — and on that exact cycle TimerValue jumps from its mid-count value straight to the new load value (Figure 2 shows the configuration-then-count sequence this is a corruption of). The current period, which had counted down to, say, 0x340 of an original 0x1000, is abandoned and a fresh 0x800 period starts immediately. The interval between the interrupt before the change and the one after is neither the old period nor the new one — it is a truncated stub.

  • Root cause: firmware retuned the period by writing TimerLoad, which force-loads the live counter, restarting the current period mid-flight. The current interval is cut short (or, if the new value is larger, stretched), producing a single glitched period at every rate change. The reload value and the live count were both changed when only the reload value should have been.

  • Correct fix: retune with TimerBGLoad. It writes the reload register only, so the in-flight count finishes its current period undisturbed and the new period takes effect at the next zero crossing — no glitch, no truncated stub. The RTL guarantee is structural: wr_bgload updates load_reg but is deliberately absent from the counter's force-load branch, so a BGLoad write physically cannot perturb the running count.

  • Verification assertion: prove BGLoad never disturbs the live counter, and prove Load does:

    Azvya Education Pvt. Ltd.VLSI Mentor
    Snippet
    // BGLoad updates the reload value but must NOT change the live count this cycle.
    property bgload_no_count_disturb;
      @(posedge pclk) disable iff (!presetn)
        wr_bgload |=> $stable(counter) || (tick && !$past(at_zero));
    endproperty
    assert property (bgload_no_count_disturb);
     
    // Load force-loads: the cycle after a Load write, counter equals the loaded value.
    assert property (@(posedge pclk) disable iff (!presetn)
      wr_load |=> (counter == $past(pwdata & wmask)));
  • Debug habit: when a timer-paced subsystem glitches only at a rate change and runs perfectly otherwise, do not chase the counter logic — chase which register firmware writes to retune. Open the rate-change routine and check the offset: 0x00 (Load) force-loads and glitches; 0x18 (BGLoad) is seamless. The "glitches only on transition" signature almost always means Load was used where BGLoad was required.

A waveform showing APB configuration writes then a counter starting. PCLK runs across the top. Over several cycles PSEL, PENABLE and PWRITE pulse high for three APB write accesses: a TimerControl write selecting periodic mode, a TimerLoad write of 0x1000 that seeds the reload register and loads the counter, and a TimerControl write setting ENABLE. Each access's completing ACCESS-phase edge is marked. After ENABLE is set, the down-counter TimerValue begins at 0x1000 and decrements on each prescaler tick toward zero, with the first decrement edge marked.
Figure 2 — APB configuration of an SP804 timer followed by the counter starting. Across the setup phase firmware drives a sequence of APB writes (each completing when PSEL, PENABLE and PWRITE are high in the ACCESS phase): first TimerControl is written to select periodic mode with the interrupt enabled but ENABLE still low, then TimerLoad is written with the period (0x1000), which seeds the reload register and force-loads the counter. A final TimerControl write sets ENABLE high, and from the next prescaler tick the down-counter begins decrementing from 0x1000 toward zero. The completing edge of each APB access is marked, as is the first decrement, showing the clean handoff from bus-configuration to autonomous counting. The figure is the correct counterpart to the debugging scenario: configure first, then enable, and let the counter run — a mid-run TimerLoad here would force-reload and glitch the period.

8. Verification perspective

A timer is verified by proving the autonomous behaviour — the counter and interrupt that move with no bus activity — matches the register-programmed intent, with the highest-value properties sitting on the reload semantics, the mode transitions, and the W1C interrupt clear.

  • Load resets Value; BGLoad does not. The two anchor properties. A TimerLoad write must force the counter to the written value next cycle; a TimerBGLoad write must leave the live counter undisturbed (it only updates the reload register). These are the properties that catch the period-glitch bug and the inverse — a Load that fails to force-load:

    Azvya Education Pvt. Ltd.VLSI Mentor
    Snippet
    // Writing TimerLoad force-loads the counter (Value reflects the new load).
    assert property (@(posedge pclk) disable iff (!presetn)
      wr_load |=> (counter == $past(pwdata & wmask)));
     
    // Writing TimerBGLoad updates the reload value but does NOT reload the live count.
    assert property (@(posedge pclk) disable iff (!presetn)
      (wr_bgload && !(tick && at_zero)) |=> $stable(counter));
  • Periodic reload at zero; one-shot halt at zero. Prove that in periodic mode (MODE=1, ONESHOT=0) the counter reloads from load_reg on the tick after it hits zero, and in one-shot (ONESHOT=1) it stays at zero. This is the mode-transition coverage that catches a stale ONESHOT masquerading as periodic:

    Azvya Education Pvt. Ltd.VLSI Mentor
    Snippet
    // Periodic: at a zero-crossing tick, the counter reloads from the reload register.
    assert property (@(posedge pclk) disable iff (!presetn)
      (en && tick && at_zero && mode && !oneshot) |=> (counter == $past(load_reg)));
  • TimerIntClr deasserts MIS; the zero crossing asserts RIS. Prove the W1C clear path: a write to TimerIntClr must clear RIS (and therefore MIS) the next cycle, and a zero crossing with the timer enabled must set RIS. Pair with a never-X check on the IRQ:

    Azvya Education Pvt. Ltd.VLSI Mentor
    Snippet
    // A TimerIntClr write clears the raw interrupt -> masked interrupt deasserts.
    assert property (@(posedge pclk) disable iff (!presetn)
      wr_intclr |=> !ris);
     
    // The zero crossing latches the raw interrupt.
    assert property (@(posedge pclk) disable iff (!presetn)
      zero_cross |=> ris);
  • Prescale and coverage. Cover all three PRESCALE settings (÷1/÷16/÷256) crossed with both SIZE widths and all three modes, and assert that the counter decrements exactly once per prescale period (not every clock). The high-value cross is {periodic, one-shot, free-run} × {÷1, ÷16, ÷256} × {Load-retune, BGLoad-retune} — the corner where a Load/BGLoad mistake hides is a mid-run retune in periodic mode, so it must be an explicit bin, not left to random.

The point: the timer's hard properties are about self-changing state. Verify that the counter reloads when and only when it should, that Load and BGLoad differ exactly at the live count, and that the interrupt sets at zero and clears on the W1C write — not just that you can read the registers back. The APB assertions and monitors of the verification module are the substrate these timer-specific properties bind onto.

9. Interview discussion

"Walk me through an APB timer peripheral, and tell me the difference between Load and BGLoad" is a favourite because the first half checks whether you can assemble a CSR block, and the second half — the Load/BGLoad distinction — separates people who have integrated a real timer from those who have only read "it counts down."

Lead with the structure: a timer is a prescaled down-counter wrapped in an APB register fileTimerLoad (the reload value, RW, force-loads on write), TimerValue (the live count, RO), TimerControl (ENABLE/MODE/INTENABLE/PRESCALE/SIZE/ONESHOT), TimerIntClr (W1C clear), RIS/MIS (raw and masked interrupt), and TimerBGLoad (background reload). Name the three personalities crisply — free-running wraps, periodic auto-reloads, one-shot fires once and halts (and ONESHOT overrides MODE). Then deliver the depth point that signals real silicon work: Load and BGLoad write the same reload register, but Load also force-loads the live counter and restarts the current period, while BGLoad updates only the reload value so the in-flight period finishes cleanly and the new period starts at the next zero crossing. Give the consequence: retuning a running periodic timer with Load glitches one period (the current interval is truncated); BGLoad retunes seamlessly. Land two more experience markers — a periodic interrupt that fires once is a stale ONESHOT bit, and the interrupt is cleared by writing TimerIntClr (W1C), never by reading the value — and note the masking split (MIS = RIS && INTENABLE, so poll RIS to see events on a masked timer). Closing with "the BGLoad-vs-Load split is the whole reason the SP804 has two load registers — one to restart, one to retune without disturbing the count" shows you understand why the map is shaped the way it is, not just what the bits are.

10. Practice

  1. Trace the reload mux. Given load_reg = 0x1000 and counter = 0x340 mid-count, state the resulting counter next cycle for (a) a TimerLoad write of 0x800, and (b) a TimerBGLoad write of 0x800. Explain which one glitches the period and why.
  2. Decode the control word. Given TimerControl = 0x67, decode every field (ENABLE, MODE, INTENABLE, PRESCALE, SIZE, ONESHOT) and state how the timer behaves at the next zero crossing.
  3. Find the stale ONESHOT. A periodic timer interrupts exactly once and then never again, though it stays enabled. Name the bug, the exact TimerControl bit, and how you would confirm it from a register read.
  4. Write the W1C clear. Write the SVA that proves a TimerIntClr write deasserts MIS next cycle, and a separate property that the zero crossing sets RIS. State why reading TimerValue must not clear the interrupt.
  5. Choose the prescale. You must time a 1-second interval from a 50 MHz PCLK with a 32-bit counter. State whether ÷1, ÷16, or ÷256 is needed and roughly the load value, and explain the resolution you give up at the higher prescale.

11. Q&A

12. Key takeaways

  • A timer peripheral is a prescaled down-counter wrapped in an APB CSR blockTimerLoad/Value/Control/IntClr/RIS/MIS/BGLoad — where the registers configure and observe an autonomous counter that changes itself every (prescaled) clock and reloads at zero.
  • Load vs BGLoad is the load-bearing distinction: both write the same reload register, but TimerLoad also force-loads the live counter and restarts the current period, while TimerBGLoad updates only the reload value so the in-flight period finishes cleanly and the new period starts at the next zero crossing — use BGLoad to retune a running timer without a glitch.
  • Three modes, one counter: free-running wraps to all-ones, periodic auto-reloads from Load, one-shot fires once and halts — and ONESHOT overrides MODE, so a periodic interrupt that fires only once is almost always a stale ONESHOT bit.
  • The interrupt is a latched event, cleared by a write, not a read: the zero crossing sets RIS, INTENABLE masks it into MIS (the CPU line), and a W1C write to TimerIntClr clears it — reading TimerValue or RIS never clears it.
  • The prescaler gates the decrement, not the clock: PRESCALE selects ÷1/÷16/÷256 so the counter ticks once per prescale period, trading resolution for range without a wider counter; verify it decrements once per period, not every PCLK.
  • Verify the self-changing state: prove Load resets Value and BGLoad does not, that periodic reloads at zero and one-shot halts, and that TimerIntClr deasserts MIS — built on the APB assertions and monitors substrate. (Sources: Arm SP804 Dual-Timer Technical Reference Manual; AMBA APB Protocol Specification, IHI 0024C, §2.1.)