Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified benches/gust/drivers/adc-thin/adc-thin-cm3.o
Binary file not shown.
33 changes: 23 additions & 10 deletions benches/gust/drivers/adc-thin/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
19 changes: 12 additions & 7 deletions benches/gust/silicon/RESULTS-f100.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
13 changes: 9 additions & 4 deletions benches/gust/src/bin/gust_adc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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"
Expand Down
17 changes: 11 additions & 6 deletions benches/gust/src/bin/gust_adc_probe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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).
Expand Down Expand Up @@ -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);
Expand All @@ -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
Expand Down
26 changes: 14 additions & 12 deletions benches/gust/src/bin/gust_adc_silicon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down
Loading