How does a pin become a configuration bit without firmware? BOOT_MODE would tell the ROM where the image lives, TEST_MODE would tell the chip whether to enter scan, and both must be decided while the reset controller still holds everything else, from pins that may float, bounce or arrive late. Get the sampling wrong and the chip boots from the wrong device or wakes in test mode on a customer's board. A sampler that looks safe because its default is safe can also be a sampler that never captures anything, and the script below shows how.
Three rules make straps safe. First, defined pulls: BOOT_MODE[1:0] and TEST_MODE carry pull-downs (the same pulls the GPIO pads have at reset), so an open pin selects the default and mission mode, never test mode and never an exotic boot. Second, sample timing: the straps are sampled once, inside the oscillator-ready wait of the cold-boot sequence, which is 1024 cycles (42.667 µs) after the power-on reset releases. By then the supplies are valid, the pull has settled and the synchroniser has been running for a thousand cycles; sampling earlier reads a rising rail, and sampling later risks reading a pin that firmware or the board has already driven. Third, synchronise the sample: the strap crosses from the pad into the 24 MHz always-on clock through two flops, because a strap that changes at the sample edge is exactly the metastability the synchroniser lessons teach against.
The sampler's own reset matters as much as its timing. If the flops were held in reset until the peripheral reset releases, which comes after the oscillator-ready point, a sample pulse during the oscillator-ready wait would be ignored and the module would return its reset value whatever the board does. So the sampler resets with the power-on reset only, free-runs its synchroniser, captures at the pulse and then holds the value until the next power-on reset. A pad reset therefore does not resample the straps. The table below is the extension's contract with the board.
BOOT_MODE[1:0] | Meaning | Board | Notes |
|---|---|---|---|
00 | SPI image source | Open (pull-downs) | Default; this is NanoSoC's boot flow. |
01 | Reserved | n/a | No other image source exists; the ROM treats every reserved value as 00. |
10 | Reserved | n/a | As above. |
11 | Reserved | n/a | As above. |
The module below is the teaching sampler: two-flop synchroniser into the always-on clock, a sample enable pulsed once at the oscillator-ready point, and a done flag so that a second pulse is ignored. It resets with the power-on reset only.
// Extension, not NanoSoC v1: strap sampler with a two-flop synchroniser.
// Samples once when sample_en pulses (oscillator-ready point); holds until por_n.
module nano_strap_sample (
input logic clk_per, // always-on 24 MHz clock
input logic por_n, // power-on reset only: a pad reset must not resample
input logic strap_in, // from the pad receiver
input logic sample_en, // one-cycle pulse at the oscillator-ready point
output logic strap_q // sampled strap value
);
logic sync_a, sync_b, done;
always_ff @(posedge clk_per or negedge por_n) begin
if (!por_n) begin
sync_a <= 1'b0;
sync_b <= 1'b0;
strap_q <= 1'b0; // safe default (the pull-down value)
done <= 1'b0;
end else begin
sync_a <= strap_in;
sync_b <= sync_a;
if (sample_en && !done) begin
strap_q <= sync_b;
done <= 1'b1;
end
end
end
endmoduleThe script below models the same logic cycle by cycle in two versions. In the first the flops reset with the power-on reset only, as above. In the second they are held in reset until after the sample pulse, the mistake described earlier. The board drives the strap high from cycle 100. The first version must capture a 1; the second returns 0 whatever the board does, which the script requires, so a sampler that looks safe but cannot capture is caught. Then a strap table is checked by one function: every strap needs a pull, the default must select SPI, and no boot mode may select test; bad tables with a test-mode default and a missing pull must be rejected.
def run(strap, rst_release, sample_cycle, cycles=1100):
a = b = q = 0
done = False
for t in range(cycles):
if t < rst_release: # flops held in reset
a = b = q = 0
done = False
continue
sample = (t == sample_cycle)
take = sample and not done
q, b, a = (b if take else q), a, strap(t) # nonblocking update order
done = done or take
return q
board = lambda t: 1 if t >= 100 else 0 # a board that drives the strap high from cycle 100
assert run(board, rst_release=0, sample_cycle=1024) == 1 # reset with por_n only: captures
assert run(board, rst_release=1026, sample_cycle=1024) == 0 # reset held past the pulse: never captures
assert run(lambda t: 0, rst_release=0, sample_cycle=1024) == 0 # an open pin (pull-down) reads 0
late = lambda t: 1 if t >= 1030 else 0
assert run(late, rst_release=0, sample_cycle=1024) == 0 # a strap driven after the window is ignored
def check(modes, default, pulls):
for pin, pull in pulls.items():
assert pull in ("down", "up"), pin + " has no defined pull"
assert modes[default] == "SPI", "the default does not select SPI"
assert all("TEST" not in m for m in modes.values()), "a boot mode selects test"
return True
MODES = {"00": "SPI", "01": "SPI", "10": "SPI", "11": "SPI"} # reserved values fall back to SPI
PULLS = {"BOOT0": "down", "BOOT1": "down", "TEST_MODE": "down"}
assert check(MODES, "00", PULLS)
for bad in (({**MODES, "00": "TEST"}, "00", PULLS), (MODES, "00", {**PULLS, "TEST_MODE": "none"})):
try:
check(*bad)
raise SystemExit("a bad strap table was accepted")
except AssertionError:
pass
print("PASS: sampler captures a driven strap, an open pin reads 0, a late strap is ignored; the held-in-reset sampler never captures; bad strap tables rejected")The strap table with pulls, default and reserved handling, signed with the board team.
The sample point named on the cold-boot sequence, inside the oscillator-ready wait.
The sampler's reset source named (power-on reset only) and the synchroniser shown between pad and consumer.
A simulation in which a strap driven high is actually captured, not only one where the default is read.
Verify sampling with a slow-ramp simulation: sweep the supply rise and the strap drive across corners and confirm the sampled value equals the pulled value whenever the board leaves the pin open, and equals the driven value whenever it drives. On the bench, a strap suspect is confirmed by forcing the pin both ways and watching the boot source change; a pin that changes nothing is unsampled or overridden downstream.
Because the sampling window closes at reset release: a later driver fights the pull and rewrites what the sampler would see. The strap is valid only inside its window; after it, the pin belongs to its mission function.