diff --git a/benches/gust/drivers/adc-thin/adc-thin-cm3.o b/benches/gust/drivers/adc-thin/adc-thin-cm3.o index 15e1fa3..913be2f 100644 Binary files a/benches/gust/drivers/adc-thin/adc-thin-cm3.o and b/benches/gust/drivers/adc-thin/adc-thin-cm3.o differ diff --git a/benches/gust/drivers/adc-thin/src/lib.rs b/benches/gust/drivers/adc-thin/src/lib.rs index 24519da..d659b1c 100644 --- a/benches/gust/drivers/adc-thin/src/lib.rs +++ b/benches/gust/drivers/adc-thin/src/lib.rs @@ -49,6 +49,13 @@ const SR_EOC: u32 = 1 << 1; // end of conversion const CR2_ADON: u32 = 1 << 0; // A/D on const CR2_CONT: u32 = 1 << 1; // continuous conversion (MUST be 0 for single-shot) const CR2_SWSTART: u32 = 1 << 22; // start conversion of regular channels +// On STM32F1 the SWSTART bit is IGNORED unless the regular-group external trigger is +// enabled AND its source is the software trigger: EXTTRIG=1 (bit 20) + EXTSEL=111 +// (bits 19:17). Without these, writing SWSTART does nothing (silicon-confirmed on the +// F100 Vrefint anchor: SR.STRT stayed 0). The driver sets them in `adc_start` so the +// software trigger actually fires — the true single-shot path (gale#216). +const CR2_EXTTRIG: u32 = 1 << 20; // enable external/software trigger for regular group +const CR2_EXTSEL_SWSTART: u32 = 0x7 << 17; // EXTSEL = 111 → SWSTART as the trigger source const DR_MASK: u32 = 0x0FFF; // 12-bit right-aligned result @@ -259,19 +266,19 @@ fn wr(a: u32, v: u32) { // ---- exported protocol primitives (the driver's gust:hal-facing surface) ---- /// Configure the ADC at `base` for a single-channel single conversion: sample time -/// (SMPRx picked by channel), regular sequence (SQR3 channel, SQR1 length 1), and -/// power on with CONT=0 (single-shot — never free-run). Table-free: every value is -/// bit arithmetic. `cr2_extra` = caller-supplied CR2 bits OR'd into the managed -/// CR2 write (e.g. `TSVREFE` (1<<23) to read F1 internal channels Vrefint/temp; also -/// ALIGN, EXTTRIG). The managed bits (ADON, CONT=0) stay authoritative — gale#216. +/// (SMPRx picked by channel) and regular sequence (SQR3 channel, SQR1 length 1). +/// Table-free: every value is bit arithmetic. #[no_mangle] -pub extern "C" fn adc_configure(base: u32, channel: u32, sample_code: u32, cr2_extra: u32) { +pub extern "C" fn adc_configure(base: u32, channel: u32, sample_code: u32) { wr(base + smpr_reg(channel), smpr_bits(channel, sample_code)); wr(base + SQR3, sqr3_channel(channel)); wr(base + SQR1, sqr1_length(1)); - // ADON on, CONT off: exactly one conversion per SWSTART. cr2_extra ORs in - // caller-supplied CR2 bits (e.g. TSVREFE) without touching the managed bits. - wr(base + CR2, CR2_ADON | cr2_extra); + // Register config ONLY — does NOT touch CR2. ADON was set by `adc_enable` + // (and stays set), so `adc_configure` writing ADON again would, on real F1, + // trigger a spurious conversion (a repeated ADON-write-while-on starts a + // conversion). Leaving CR2 alone here makes the conversion a strict + // single-shot: exactly one SWSTART in `adc_start`. CONT stays 0 throughout + // (never set by enable/configure/start), so single-shot is preserved. gale#216. } /// Power on + select `channel`: Off → Ready. Returns the packed Ready state or @@ -299,7 +306,13 @@ pub extern "C" fn adc_enable(base: u32, state: u32, channel: u32, cr2_extra: u32 pub extern "C" fn adc_start(base: u32, state: u32, cr2_extra: u32) -> u32 { match begin(unpack(state)) { Ok(next) => { - wr(base + CR2, CR2_ADON | CR2_SWSTART | cr2_extra); + // ADON (kept on) + the software-trigger setup (EXTTRIG + EXTSEL=SWSTART) so + // the SWSTART bit actually starts the conversion on F1; CONT stays 0 + // (single-shot). cr2_extra carries e.g. TSVREFE for internal channels. + wr( + base + CR2, + CR2_ADON | CR2_EXTTRIG | CR2_EXTSEL_SWSTART | CR2_SWSTART | cr2_extra, + ); pack(next) } Err(_) => ADC_FAULT, diff --git a/benches/gust/silicon/RESULTS-f100.md b/benches/gust/silicon/RESULTS-f100.md index c0bb909..74716e4 100644 --- a/benches/gust/silicon/RESULTS-f100.md +++ b/benches/gust/silicon/RESULTS-f100.md @@ -82,10 +82,15 @@ gust-adc-silicon OK: Vrefint = 1646 raw on real STM32F100 silicon — the dissol above its 1.24 V spec max — impossible — which independently confirms the 3.0 V rail and that the code really is Vrefint. -**Silicon-vs-model note (tracked, gale#216):** the driver writes `CR2=ADON` in both -`adc_enable` and `adc_configure`; on F1 a `1`-written-while-ADON=1 can kick a -conversion, so on hardware `adc_configure` may trigger an extra ch17 conversion before -`adc_start`. Both convert ch17/Vrefint, so the DR holds a valid sample either way (the -1646 read confirms it) — but a strict single-shot on F1 would want the FSM to separate -power-on from register config. Harmless for this single-channel read; noted for a -follow-on refinement. +**Strict single-shot fix (gale#216, silicon-driven — 2026-07-23).** The original read +(1646) worked by accident: `adc_configure` re-wrote `CR2=ADON`, and on F1 an +ADON-write-while-ADON=1 restarts a conversion — so *that* write was the de-facto +trigger, while `adc_start`'s `SWSTART` bit was inert. A diagnostic build confirmed it on +the board: right after `adc_start`, `SR = 0x0` (STRT=0, EOC=0) — the conversion never +started — because on F1 `SWSTART` is ignored unless `EXTTRIG=1` + `EXTSEL=111` (SWSTART +source) are also set, which the driver didn't do. Fixed in two parts: `adc_configure` +no longer touches CR2 (no stray restart), and `adc_start` now writes +`ADON|EXTTRIG|EXTSEL(SWSTART)|SWSTART` so the software trigger actually fires. Re-flashed +→ **Vrefint = 1645 raw, VDDA ≈ 2987 mV, EOC cleared** — a true single conversion via one +SWSTART. adc-thin Kani 7/7 preserved; qemu probe + Renode gate updated to the new start +register value (`CR2 = 0x5E0001`). diff --git a/benches/gust/src/bin/gust_adc.rs b/benches/gust/src/bin/gust_adc.rs index c2f058b..e9c96fa 100644 --- a/benches/gust/src/bin/gust_adc.rs +++ b/benches/gust/src/bin/gust_adc.rs @@ -28,7 +28,7 @@ pub extern "C" fn mmio_write32(addr: u32, val: u32) { } extern "C" { - fn adc_configure(base: u32, channel: u32, sample_code: u32, cr2_extra: u32); + fn adc_configure(base: u32, channel: u32, sample_code: u32); fn adc_enable(base: u32, state: u32, channel: u32, cr2_extra: u32) -> u32; fn adc_start(base: u32, state: u32, cr2_extra: u32) -> u32; fn adc_poll(base: u32, state: u32) -> u32; @@ -49,6 +49,9 @@ const DR: u32 = 0x4C; const SR_EOC: u32 = 1 << 1; const CR2_ADON: u32 = 1 << 0; const CR2_SWSTART: u32 = 1 << 22; +// F1 software-trigger enable bits adc_start now sets so SWSTART actually fires (gale#216). +const CR2_EXTTRIG: u32 = 1 << 20; +const CR2_EXTSEL_SWSTART: u32 = 0x7 << 17; const ADC_FAULT: u32 = 0xFFFF_FFFF; const PHASE_SHIFT: u32 = 30; @@ -118,7 +121,7 @@ fn main() -> ! { // 2) configure: table-free SMPR2/SQR3/SQR1 land exactly. // smpr_bits(3,4)=4<<9=0x800; SQR3 SQ1=3; SQR1 length(1)=0; CR2=ADON. - adc_configure(ADC1, CH, SAMPLE_CODE, 0); + adc_configure(ADC1, CH, SAMPLE_CODE); let smpr2 = read_volatile((ADC1 + SMPR2) as *const u32); let sqr3 = read_volatile((ADC1 + SQR3) as *const u32); let sqr1 = read_volatile((ADC1 + SQR1) as *const u32); @@ -129,10 +132,12 @@ fn main() -> ! { b"adc-config-bad\n" }); - // 3) start: Ready → Converting, the DRIVER writes CR2=ADON|SWSTART. + // 3) start: Ready → Converting, the DRIVER writes CR2=ADON|EXTTRIG|EXTSEL(SWSTART)|SWSTART + // (the F1 software-trigger setup so SWSTART actually fires — gale#216). let s3 = adc_start(ADC1, s1, 0); let cr2_3 = read_volatile((ADC1 + CR2) as *const u32); - tx(if s3 != ADC_FAULT && phase_of(s3) == PH_CONVERTING && cr2_3 == (CR2_ADON | CR2_SWSTART) { + tx(if s3 != ADC_FAULT && phase_of(s3) == PH_CONVERTING + && cr2_3 == (CR2_ADON | CR2_EXTTRIG | CR2_EXTSEL_SWSTART | CR2_SWSTART) { b"adc-start-ok\n" } else { b"adc-start-bad\n" diff --git a/benches/gust/src/bin/gust_adc_probe.rs b/benches/gust/src/bin/gust_adc_probe.rs index 50aeb21..e2373c6 100644 --- a/benches/gust/src/bin/gust_adc_probe.rs +++ b/benches/gust/src/bin/gust_adc_probe.rs @@ -30,7 +30,7 @@ pub extern "C" fn mmio_write32(addr: u32, val: u32) { } extern "C" { - fn adc_configure(base: u32, channel: u32, sample_code: u32, cr2_extra: u32); + fn adc_configure(base: u32, channel: u32, sample_code: u32); fn adc_enable(base: u32, state: u32, channel: u32, cr2_extra: u32) -> u32; fn adc_start(base: u32, state: u32, cr2_extra: u32) -> u32; fn adc_poll(base: u32, state: u32) -> u32; @@ -54,6 +54,9 @@ const DR: u32 = 0x4C; const SR_EOC: u32 = 1 << 1; const CR2_ADON: u32 = 1 << 0; const CR2_SWSTART: u32 = 1 << 22; +// F1 software-trigger enable bits adc_start now sets so SWSTART actually fires (gale#216). +const CR2_EXTTRIG: u32 = 1 << 20; +const CR2_EXTSEL_SWSTART: u32 = 0x7 << 17; const ADC_FAULT: u32 = 0xFFFF_FFFF; // Packed-state field layout (must match the driver's dissolve ABI). @@ -109,8 +112,9 @@ fn main() -> ! { // 2) configure: table-free bit arithmetic lands in SMPR2/SQR3/SQR1 exactly. // smpr_bits(3,4) = 4 << ((3%10)*3) = 4 << 9 = 0x800 in SMPR2; SQR3 SQ1 = 3; - // SQR1 length(1) = 0; CR2 = ADON (single-shot, CONT=0). - unsafe { adc_configure(base, CH, SAMPLE_CODE, 0) }; + // SQR1 length(1) = 0. configure no longer touches CR2 (gale#216) — CR2 stays + // ADON from adc_enable above (single-shot, CONT=0). + unsafe { adc_configure(base, CH, SAMPLE_CODE) }; let smpr2 = rw(SMPR2); let sqr3 = rw(SQR3); let sqr1 = rw(SQR1); @@ -128,11 +132,12 @@ fn main() -> ! { // 3) start: Ready → Converting, driver WRITES CR2=ADON|SWSTART (keeps CONT=0). let s3 = unsafe { adc_start(base, s1, 0) }; let cr2_3 = rw(CR2); - if s3 == ADC_FAULT || phase_of(s3) != PH_CONVERTING || cr2_3 != (CR2_ADON | CR2_SWSTART) { - hprintln!("adc-start FAIL: s3={:#x} phase={} CR2={:#x}", s3, phase_of(s3), cr2_3); + let cr2_start_want = CR2_ADON | CR2_EXTTRIG | CR2_EXTSEL_SWSTART | CR2_SWSTART; + if s3 == ADC_FAULT || phase_of(s3) != PH_CONVERTING || cr2_3 != cr2_start_want { + hprintln!("adc-start FAIL: s3={:#x} phase={} CR2={:#x} want {:#x}", s3, phase_of(s3), cr2_3, cr2_start_want); ok = false; } else { - hprintln!("adc-start ok: Converting, CR2=ADON|SWSTART={:#x}", cr2_3); + hprintln!("adc-start ok: Converting, CR2=ADON|EXTTRIG|EXTSEL(SWSTART)|SWSTART={:#x}", cr2_3); } // 4) read-after-EOC (distinctive): reading DR WHILE Converting — before EOC — is diff --git a/benches/gust/src/bin/gust_adc_silicon.rs b/benches/gust/src/bin/gust_adc_silicon.rs index b9179af..31c0f96 100644 --- a/benches/gust/src/bin/gust_adc_silicon.rs +++ b/benches/gust/src/bin/gust_adc_silicon.rs @@ -15,16 +15,17 @@ //! //! Reading an F1 internal channel needs `CR2.TSVREFE` (bit 23) set *during* the //! conversion — which the driver's absolute CR2 writes used to drop (gale#216). Fixed -//! by threading `cr2_extra = TSVREFE` through adc_enable/adc_configure/adc_start so the -//! dissolved driver keeps TSVREFE on every managed CR2 write. +//! by threading `cr2_extra = TSVREFE` through adc_enable/adc_start so the dissolved +//! driver keeps TSVREFE on every managed CR2 write. //! -//! Silicon-vs-model note: the driver writes CR2=ADON in BOTH adc_enable (wake) and -//! adc_configure (config). On the RAM-window probe a repeated ADON write is inert; on -//! real F1 a `1`-written-while-ADON=1 can kick a conversion. Both such conversions -//! select ch17 (SQR3 is set before the configure CR2 write), so the DR holds a Vrefint -//! sample regardless — this anchor's job is to confirm that empirically. A future -//! refinement (separate power-on from register config in the FSM) would make it a -//! strict single-shot on F1; tracked with gale#216. +//! Strict single-shot on F1 (RESOLVED, gale#216, silicon-driven): the driver used to +//! rely on `adc_configure` re-writing CR2=ADON — on F1 an ADON-write-while-ADON=1 +//! restarts a conversion, so that write was the *de facto* trigger, and `adc_start`'s +//! SWSTART bit was actually inert (the F100 diagnostic showed SR.STRT=0 after start: +//! on F1 SWSTART is ignored unless EXTTRIG=1 + EXTSEL=111 are also set). Fixed in two +//! parts: (1) `adc_configure` no longer touches CR2 (no stray ADON-restart), and +//! (2) `adc_start` now sets EXTTRIG|EXTSEL(SWSTART)|SWSTART so the software trigger +//! actually fires — exactly one conversion. Confirmed on real F100: Vrefint = 1645. //! //! F1 ADC bring-up the FSM does NOT model (done here in firmware): enable the ADC1 //! clock (RCC APB2ENR), wake + tSTAB, and RSTCAL/CAL self-calibration — all one-time, @@ -50,7 +51,7 @@ pub extern "C" fn mmio_write32(addr: u32, val: u32) { // The dissolved adc-thin driver (state-threaded FSM; cr2_extra carries TSVREFE). extern "C" { fn adc_enable(base: u32, state: u32, channel: u32, cr2_extra: u32) -> u32; - fn adc_configure(base: u32, channel: u32, sample_code: u32, cr2_extra: u32); + fn adc_configure(base: u32, channel: u32, sample_code: u32); fn adc_start(base: u32, state: u32, cr2_extra: u32) -> u32; fn adc_poll(base: u32, state: u32) -> u32; fn adc_read(base: u32, state: u32) -> u32; @@ -138,8 +139,9 @@ fn main() -> ! { while rd(ADC_BASE + CR2) & CR2_CAL != 0 {} // --- dissolved conversion lifecycle: configure -> start -> poll -> read --- - // configure: SMPR1 ch17 = 239.5 cyc, SQR3 = 17, SQR1 len = 1, CR2 = ADON|TSVREFE. - unsafe { adc_configure(ADC_BASE, VREFINT_CH, SAMPLE_CODE, CR2_TSVREFE) }; + // configure: SMPR1 ch17 = 239.5 cyc, SQR3 = 17, SQR1 len = 1. Does NOT touch CR2 + // (gale#216) — CR2 stays ADON|TSVREFE from adc_enable above. + unsafe { adc_configure(ADC_BASE, VREFINT_CH, SAMPLE_CODE) }; // start: CR2 = ADON | SWSTART | TSVREFE -> conversion of ch17. let s2 = unsafe { adc_start(ADC_BASE, s1, CR2_TSVREFE) }; if s2 == ADC_FAULT {