This lesson is an extension. NanoSoC has no watchdog: no block, no RST_CAUSE bit and no interrupt input. A real chip built from it usually wants one, and adding it is a good test of whether you understand the shared tables. The block is the easy part; the three tables it touches, the reset tree, the cause register and the interrupt map, are the integrator's work. Every number below is an illustrative teaching value, not a NanoSoC fact. Its one job is the changes a watchdog forces; the wake policy for the existing chip is next.
Suppose a block nano_wdt is added to PD_AON on clk_per: a 24-bit down-counter with a refresh register, in an APB slot of its own (illustrative: a slot after the existing seven). It must be on clk_per because it has to fire when software has stopped the PLL or gated the fast clocks. It changes three shared tables.
Table | Today in NanoSoC | What the extension changes |
|---|---|---|
Interrupt map | Inputs 6 and 7 reserved, tied to 0; the connectivity test flags any use | Do not use input 6 or 7 (that breaks the check). Add the warning as bit [2] of PWR_IRQ so it shares irq_pwr, priority 0, with no map change; or make a deliberate ECO to the map and the test. |
RST_CAUSE | Four bits: POR, PAD, SW, WAKE | Add bit [4] WDT. A watchdog reset must not set SW: the boot code treats SW as a deliberate restart and WDT as a fault to log. The boot decoder gains a fifth row. |
Reset tree | Four sources into three nets; sw_rst holds 16 clk_sys cycles | A new source wdt_rst in nano_rstpwr, held 16 clk_per cycles (666.7 ns), asserting rst_sys_n and rst_cpu_n asynchronously. It cannot reuse sw_rst: that hold counts clk_sys cycles, and the fast clocks may be dead when the watchdog fires. |
The release is the subtle part. If the watchdog fired because the PLL stopped, clk_sys is not running, so the 18-cycle release R5 cannot count. The release must restart at the PLL: lock (4 + 256 clk_per), open the gates (2 clk_per), then R5 and R6 as usual. That is the wake sequence's shape, not the software reset's. Using the NanoSoC specification's cycle counts, (260 + 2) x 41.667 ns + 187.5 ns + 93.75 ns is about 11.2 us from expiry to the CPU leaving reset, an illustrative figure for this extension.
// snippet: watchdog counter, EXTENSION (not part of NanoSoC)
// (partial: bus writes, the window compare, the synchronised reset request
// and cause capture live outside this fragment, in nano_rstpwr and the APB
// register block; refresh here is already the output of a legal write).
module nano_wdt_core (
input logic clk_per,
input logic rst_per_n,
input logic refresh, // pulsed by a legal bus write
input logic [23:0] reload, // illustrative teaching value width
output logic warn_irq, // staged interrupt when one quarter is left
output logic reset_req // asserted at zero, held until reset
);
logic [23:0] count;
always_ff @(posedge clk_per or negedge rst_per_n) begin
if (!rst_per_n) begin
count <= reload;
reset_req <= 1'b0;
end else if (refresh) begin
count <= reload;
end else if (count != 24'h0) begin
count <= count - 24'h1;
end else begin
reset_req <= 1'b1;
end
end
assign warn_irq = (count < (reload >> 2)) && !reset_req;
endmoduleCLK_PER = 24e6
WIDTH = 24
MAX_S = (2 ** WIDTH - 1) / CLK_PER
assert abs(MAX_S - 0.699) < 0.001, MAX_S # about 0.7 s full range
# Illustrative run-time default: reload for 0.5 s, warn when one quarter is
# left, that is after three quarters of the timeout have elapsed.
RELOAD = 12_000_000
timeout_s = RELOAD / CLK_PER
warn_left_s = (RELOAD // 4) / CLK_PER
assert abs(timeout_s - 0.5) < 1e-9 and abs(warn_left_s - 0.125) < 1e-9
# Boot: the SPI image load takes 10.928 ms (NanoSoC specification). A short BOOT
# timeout of 5 ms (illustrative) would trip 2.2 times during the load, so
# the ROM must refresh at least every quarter timeout: 1.25 ms -> 9 refreshes.
SPI_LOAD_S, BOOT_TIMEOUT_S = 10.928e-3, 5e-3
assert abs(SPI_LOAD_S / BOOT_TIMEOUT_S - 2.19) < 0.01
import math
refreshes = math.ceil(SPI_LOAD_S / (BOOT_TIMEOUT_S / 4))
assert refreshes == 9, refreshes
# The check can fail: with the 0.5 s default no refresh is needed in the load.
assert SPI_LOAD_S < timeout_s / 4
assert not (SPI_LOAD_S < BOOT_TIMEOUT_S / 4)
# Release after expiry (PLL possibly dead): lock, gates, R5, R6.
rel_ns = (260 + 2) * (1e9 / 24e6) + 18 * (1e9 / 96e6) + (8 * (1e9 / 96e6) + 2 * (1e9 / 192e6))
assert abs(rel_ns - 11197.9) < 0.5, rel_ns
print("PASS: 0.5 s default; 5 ms boot timeout needs 9 refreshes in the load; release 11.2 us")The three table changes made together: interrupt map, RST_CAUSE and reset tree, with the connectivity test and the boot decoder updated in the same change.
The counter clock and supply traced to clk_per and PD_AON, independent of every failure the watchdog guards.
A release path that does not depend on a running fast clock, with its length computed.
Stop refreshing in simulation and confirm the staged order: warning first, reset request at zero, the WDT cause bit set and SW clear, system and CPU reset, nothing else. Then repeat with the PLL disabled before expiry and confirm the release still completes. Refresh early, before a window opens, and confirm it does not count if a window is implemented: a watchdog that accepts any refresh is a loop counter, not a supervisor.
The boot code branches on cause: SW is a deliberate restart that expects clean state, while a watchdog reset is a fault whose evidence should be logged. Sharing the bit makes every hang look like a planned restart.