This is the engineering reference for the model: what it implements, register by register, for people writing firmware against it. For using the board in the editor, start with The STM32F411 Black Pill and the board page.
The third microcontroller, and the first Cortex-M board. One kernel component
(stm32f411 in crates/parts) wraps an STM32F411CEU6 SoC model
(crates/stm32f411) built on the ARMv7-M core (crates/cortexm-core, proven in
proofs/cortexm/REPORT.md). Firmware is a plain ARM Thumb ELF that talks to the
peripherals below at their real STM32F411 addresses, taken from RM0383 (the
STM32F411xC/E reference manual) and DS10314 (the datasheet), so a program built
for Mokxi also runs on a real WeAct Black Pill.
The catalog type is stm32f411 (WeAct MiniF4 "Black Pill" v3.x, STM32F411CEU6).
A second board, nucleof411re (ST's Nucleo-F411RE, the same die as the
STM32F411RET6), is the same SoC model with a board configuration of its own:
its clock, its host USART, its pins and its runtime build. Section 8 is that
board; everything else here holds for both unless it says otherwise.
Three agents build against this file: the SoC and part (Rust), the firmware
runtime and examples (C/C++, clang), the board visual (TypeScript). Change it
only by editing this file first. Everything in docs/esp32c3.md about how a
board part behaves (slices, host channel, reset, crash line) and everything in
docs/uno.md about how a board sits in a circuit applies here unless this file
says otherwise.
Running vendor binaries (STM32Cube HAL, libopencm3, the STM32duino core) is not a Phase 4 goal. Those need the clock tree brought up from HSI, the flash latency and prefetch, DMA and USB, none of which are modeled. What is a goal is that every register a bare-metal program touches to blink a pin, print on a UART, read a button and drive a PWM output is at its real address with its real bits, so the same source cross-compiled against CMSIS headers would work on silicon.
1. Board pins (catalog order)
The board is 2x20 pins on a 0.1 inch pitch plus a 4-pin SWD header across the bottom edge, USB-C at the top, in the order of WeAct's own pinout diagram for the STM32F4x1 v2.0+ boards. Catalog order is the left header top to bottom, then the right header top to bottom, then the SWD header left to right.
Left header: PB12, PB13, PB14, PB15, PA8, PA9, PA10, PA11,
PA12, PA15, PB3, PB4, PB5, PB6, PB7, PB8, PB9, 5V, GND,
3V3
Right header: 5Vb, GNDc, 3V3b, PB10, PB2, PB1, PB0, PA7, PA6,
PA5, PA4, PA3, PA2, PA1, PA0, NRST, PC15, PC14, PC13,
VBAT
SWD header: 3V3c, PA13 (SWDIO), PA14 (SWCLK), GNDb
44 pins. PB11 is absent: the STM32F411CEU6 is a 48-pin part and does not bring
it out, so PB0..PB15 is "as populated": fifteen pins, not sixteen. The rails
appear more than once (three 3.3 V, two 5 V, three grounds) and carry a letter
because catalog pin names are unique. The SWD pins do not plug into a
breadboard the board sits in (plug: false in web/src/parts/stm32f411.ts).
Until late October 2026 the drawing had the board end for end (VBAT at the
USB end) with the SWD header folded into the VBAT row as PA13, PA14 and
GNDb, and no second 5 V or 3.3 V pins. A diagram saved then is read with the
part's header setting at early and keeps that drawing
(web/src/parts/header-layout.ts); the model does not read the setting.
The silkscreen prints the short forms (VB, C13, R, A0, B9, G); the
catalog uses the full port names because a netlist pin reference has to be
unambiguous.
Electrical:
3V3drives 3.3 V atR_SUPPLY;5Vdrives 5 V atR_SUPPLY;GNDandGNDbdrive 0 V. The board is always powered (USB-C). It is a supply for the rest of the circuit.VBATis an input the board ignores: it is always powered from the 3.3 V rail here.NRSTis an input with a 40 k internal pull-up to 3.3 V (RM0383 says 30 k to 50 k); driven below 1.65 V it holds the CPU in reset; releasing it restarts the program from the reset vector.- Every
PAn,PBnandPCn(and on the NucleoPD2) is an inout GPIO. Output (MODER= 01 or 10): push-pull at 3.3 V or 0 V throughR_STRONG, or open-drain (OTYPERbit set) where a high is high-Z. Input or analog (MODER= 00 or 11): high-Z, plus a 40 k pull-up to 3.3 V forPUPDR= 01 or a 40 k pull-down forPUPDR= 10. Input reads the net voltage against 1.65 V (half of 3.3 V); Unknown reads 0, and a floating input reads noise, as on the Uno (docs/uno.mdsection 1). PC13carries the on-board blue LED, anode to 3.3 V through 470 R, cathode to the pin: the LED lights whenPC13is driven low. It is a load on the pin, and the visual shows it from a probe bit (section 5).PA0also carries theKEYbutton, which shortsPA0to ground while held. There is no external pull-up: firmware uses the internal one. The button is reached throughpoke(section 5), not through a pin.PA9/PA10are USART1 TX/RX on the real board and here; the USART's bytes go to the host channel (section 4), and the two pins stay GPIO on the net so a scope on them shows whatever the GPIO block is doing.PA11/PA12are USB D-/D+ on the real board and plain GPIO here.PA13/PA14come out of reset in alternate-function mode for SWD, exactly as the part does, which means they are high-Z until firmware reprogramsMODER.
2. Memory map (real STM32F411CE addresses)
| region | address | size | notes |
|---|---|---|---|
| flash | 0x0800_0000 |
512 KB | the STM32F411CE's flash; writes are ignored (no flash programming interface) |
| flash alias | 0x0000_0000 |
512 KB | the same bytes, because BOOT0 is low and the boot area maps main flash to 0 |
| SRAM | 0x2000_0000 |
128 KB | |
| APB1 | 0x4000_0000 |
32 KB | TIM2..TIM5 and USART2, section 3 |
| APB2 | 0x4001_0000 |
32 KB | TIM1, USART1, ADC1, SYSCFG, EXTI, TIM10, section 3 |
| AHB1 | 0x4002_0000 |
64 KB | GPIOA..GPIOD and RCC, section 3 |
| SCS | 0xE000_E000 |
4 KB | SysTick, NVIC and the SCB, answered inside cortexm-core and never seen on the bus |
Anything else is an access fault: the core takes a BusFault, which with the
reset SHCSR escalates to HardFault. Unmapped registers inside a mapped
peripheral block read 0 and ignore writes, with a counter for diagnostics.
Firmware ELF: loadProgram takes an ELF32 little-endian ARM executable
(e_machine 40). Every PT_LOAD segment must fall inside the flash (through
either view) or the SRAM. Reset is the architectural one, not an entry-point
jump: MSP comes from the word at the vector table base and PC from the
second word, with the low bit cleared, so a firmware image is a real vector
table and not a bare entry point. VTOR resets to 0, which is the flash alias.
A load at run time resets the board.
The core is Profile::V7M with Cpu::set_priority_bits(4); the STM32F4 family
implements the top four bits of each 8-bit priority field, so NVIC_IPR
behaves the way a CMSIS NVIC_SetPriority expects. There is no FPU: CPACR
is stored and never acted on, and every VFP and DSP instruction raises
UNDEFINSTR. Compile firmware with -mfloat-abi=soft.
3. Peripheral registers (subset, real offsets, real bit positions)
Registers are 32 bits and word aligned. Byte and halfword access works and hits the containing word, which is what the AHB/APB bridges do for the registers modeled here.
3.1 RCC (0x4002_3800)
The clock tree is not modeled: the part behaves as if the PLL is already
locked and SYSCLK = HCLK = 84 MHz from power-on. What RCC is here is the set
of registers firmware writes before it can touch anything else, so that a
program written the normal way runs unchanged.
| offset | name | behavior |
|---|---|---|
0x00 |
RCC_CR |
stored; HSIRDY (1), HSERDY (17) and PLLRDY (25) read back set whenever their ON bit is set, so a spin on "ready" terminates immediately |
0x04 |
RCC_PLLCFGR |
stored, read back; reset 0x2400_3010 |
0x08 |
RCC_CFGR |
stored; SWS[3:2] always reads back SW[1:0], so a spin on "the switch happened" terminates immediately. HPRE, PPRE1, PPRE2 are stored and ignored: the model runs everything at the frequencies in section 5 |
0x0C |
RCC_CIR |
stored, read back |
0x10 0x20 0x24 |
RCC_AHB1RSTR RCC_APB1RSTR RCC_APB2RSTR |
a 1 written to a peripheral's bit resets that peripheral's registers; the register itself reads 0 |
0x30 |
RCC_AHB1ENR |
bit 0 GPIOAEN, 1 GPIOBEN, 2 GPIOCEN, 3 GPIODEN, 4 GPIOEEN, 7 GPIOHEN, 12 CRCEN, 21 DMA1EN, 22 DMA2EN |
0x34 |
RCC_AHB2ENR |
stored, read back |
0x40 |
RCC_APB1ENR |
bit 0 TIM2EN, 1 TIM3EN, 2 TIM4EN, 3 TIM5EN, 11 WWDGEN, 14 SPI2EN, 15 SPI3EN, 17 USART2EN, 21 I2C1EN, 22 I2C2EN, 23 I2C3EN, 28 PWREN |
0x44 |
RCC_APB2ENR |
bit 0 TIM1EN, 4 USART1EN, 5 USART6EN, 8 ADC1EN, 11 SDIOEN, 12 SPI1EN, 13 SPI4EN, 14 SYSCFGEN, 16 TIM9EN, 17 TIM10EN, 18 TIM11EN |
0x50 0x54 0x60 0x64 |
the four LPENR registers |
stored, read back |
0x70 |
RCC_BDCR |
stored; LSERDY (1) follows LSEON (0) |
0x74 |
RCC_CSR |
stored; LSIRDY (1) follows LSION (0); PINRSTF (26) set after an NRST reset, PORRSTF (27) and BORRSTF (25) after power-on; a write with RMVF (24) clears the flags |
A clock enable bit is a real gate: a peripheral whose enable bit is clear
reads 0 and ignores writes, and a stray counter records the access. That is
the single commonest bare-metal STM32 mistake and hiding it would make it
invisible here.
3.2 GPIOA, GPIOB, GPIOC, GPIOD (0x4002_0000, 0x4002_0400, 0x4002_0800, 0x4002_0C00)
Port D is on the die in both packages; the 48-pin F411CE bonds out none of it
and the 64-pin F411RE only PD2. Its reset values are all zero. The clock
enable is RCC_AHB1ENR bit 3, GPIODEN.
| offset | name | bits |
|---|---|---|
0x00 |
MODER |
2 bits per pin: 00 input, 01 output, 10 alternate function, 11 analog |
0x04 |
OTYPER |
1 bit per pin: 0 push-pull, 1 open-drain |
0x08 |
OSPEEDR |
2 bits per pin; stored, read back, no effect on edge timing |
0x0C |
PUPDR |
2 bits per pin: 00 none, 01 pull-up, 10 pull-down |
0x10 |
IDR |
read-only, bits 0..15: the pin levels |
0x14 |
ODR |
bits 0..15: the output latch |
0x18 |
BSRR |
write-only: bits 0..15 set the matching ODR bit, bits 16..31 clear it; a reset wins over a set in the same write. Reads 0 |
0x1C |
LCKR |
stored, read back; the lock sequence is not enforced |
0x20 0x24 |
AFRL AFRH |
4 bits per pin, pins 0..7 and 8..15 |
Reset values are the part's own, which matters because they are what leaves the
SWD pins alone: GPIOA_MODER = 0xA800_0000, GPIOA_OSPEEDR = 0x0C00_0000,
GPIOA_PUPDR = 0x6400_0000; GPIOB_MODER = 0x0000_0280, GPIOB_OSPEEDR =
0x0000_00C0, GPIOB_PUPDR = 0x0000_0100; everything else 0.
IDR reads back what this chip is driving on a pin it drives, and what the
board sampled off the net on a pin it does not, exactly as PINx does on the
Uno.
Alternate function: with MODER = 10 the pad is driven by whichever
peripheral AFRL/AFRH selects for that pin, from the subset in section 3.3
and 3.4. An alternate function selection this model does not implement leaves
the pad high-Z and counts a stray. The mappings modeled (DS10314 table 9):
| AF | pins |
|---|---|
AF1 TIM2 |
PA0 CH1, PA1 CH2, PA2 CH3, PA3 CH4, PA5 CH1, PA15 CH1, PB3 CH2, PB10 CH3 |
AF1 TIM1 |
PA8 CH1, PA9 CH2, PA10 CH3, PA11 CH4; complementary PA7, PB13 CH1N, PB0, PB14 CH2N, PB1, PB15 CH3N |
AF2 TIM3 |
PA6 CH1, PA7 CH2, PB0 CH3, PB1 CH4, PB4 CH1, PB5 CH2, PC6 CH1, PC7 CH2, PC8 CH3, PC9 CH4 |
AF3 TIM10 |
PB8 CH1 |
AF2 TIM4 |
PB6 CH1, PB7 CH2, PB8 CH3, PB9 CH4 |
AF2 TIM5 |
PA0 CH1, PA1 CH2, PA2 CH3, PA3 CH4 |
AF7 USART1 |
PA9 TX, PA10 RX, PB6 TX, PB7 RX |
AF7 USART2 |
PA2 TX, PA3 RX |
3.3 TIM2, TIM3, TIM4, TIM5 (0x4000_0000, 0x4000_0400, 0x4000_0800, 0x4000_0C00)
TIM2 and TIM5 have 32-bit CNT, ARR and CCRx; TIM3 and TIM4 are 16-bit.
All four are clocked at 84 MHz (section 5).
| offset | name | bits |
|---|---|---|
0x00 |
CR1 |
CEN 0, UDIS 1, URS 2, OPM 3, DIR 4, CMS 5..6, ARPE 7, CKD 8..9 |
0x04 |
CR2 |
stored, read back |
0x08 |
SMCR |
stored, read back; slave and trigger modes are not modeled |
0x0C |
DIER |
UIE 0, CC1IE 1, CC2IE 2, CC3IE 3, CC4IE 4, TIE 6; the DMA enables are stored |
0x10 |
SR |
UIF 0, CC1IF 1..CC4IF 4, TIF 6, CC1OF 9..CC4OF 12. Write 0 to a bit to clear it, 1 leaves it (rc_w0, as the manual says) |
0x14 |
EGR |
write-only: UG 0 reloads the prescaler and counter and sets UIF, CC1G 1..CC4G 4 set the capture/compare flags, TG 6 sets TIF |
0x18 |
CCMR1 |
output mode: CC1S 0..1, OC1FE 2, OC1PE 3, OC1M 4..6, OC1CE 7, CC2S 8..9, OC2FE 10, OC2PE 11, OC2M 12..14, OC2CE 15 |
0x1C |
CCMR2 |
the same for channels 3 and 4 |
0x20 |
CCER |
CC1E 0, CC1P 1, CC2E 4, CC2P 5, CC3E 8, CC3P 9, CC4E 12, CC4P 13 |
0x24 |
CNT |
|
0x28 |
PSC |
16 bits; the counter ticks at 84 MHz / (PSC + 1) |
0x2C |
ARR |
reload value; the period is ARR + 1 ticks up-counting |
0x34 0x38 0x3C 0x40 |
CCR1 CCR2 CCR3 CCR4 |
|
0x48 0x4C |
DCR DMAR |
stored, read back |
0x50 |
TIM2_OR / TIM5_OR |
stored, read back |
Counting: up (DIR = 0), down (DIR = 1) and center-aligned (CMS != 0) are
all modeled, each with the update event where the manual puts it. ARPE
buffers ARR and OCxPE buffers CCRx until the update event, which is what
makes a duty-cycle change glitch-free.
Output compare modes (OCxM): 0 frozen, 1 active on match, 2 inactive on match,
3 toggle, 4 force inactive, 5 force active, 6 PWM mode 1 (active while
CNT < CCRx up-counting), 7 PWM mode 2 (the inverse). CCxP inverts the output
polarity. The pin only moves while CCxE is set and the pad is in alternate
function mode with that timer selected (section 3.2).
Input capture is not modeled: CCxS != 0 stores and counts a stray.
NVIC lines: TIM2 = 28, TIM3 = 29, TIM4 = 30, TIM5 = 50. Each asserts while any
of its SR flags is set with the matching DIER enable, held as a level. The
handler clears the flag, as on silicon.
3.3a TIM1 (0x4001_0000) and TIM10 (0x4001_4400)
TIM1 is the advanced-control timer: the registers of section 3.3, 16 bits wide,
plus RCR (0x30, stored; the repetition counter is not acted on) and BDTR
(0x44). BDTR.MOE (bit 15) gates every TIM1 output: with it clear none of
them drives a pin. CCER adds CCxNE (bit 4n+2) and CCxNP (bit 4n+3)
for channels 1 to 3: the complementary output alone is the channel's reference
level with CCxNP as its polarity, and with CCxE on as well it is the
reference's complement (RM0383 table 56 with no dead time; dead time and the
break input are stored and not modeled). Clock enable RCC_APB2ENR bit 0.
TIM10 is a 16-bit timer with one channel, modeled with the section 3.3
registers; the ones it lacks on silicon read as stored. Clock enable
RCC_APB2ENR bit 17.
Both count at the APB2 timer clock, which is HCLK on both boards. NVIC lines: TIM1_UP_TIM10 = 25 (TIM1's update, or any TIM10 interrupt), TIM1_TRG_COM_TIM11 = 26 (TIM1's trigger), TIM1_CC = 27 (TIM1's compare flags).
3.4 USART1 (0x4001_1000) and USART2 (0x4000_4400)
| offset | name | bits |
|---|---|---|
0x00 |
SR |
PE 0, FE 1, NE 2, ORE 3, IDLE 4, RXNE 5, TC 6, TXE 7, LBD 8, CTS 9. RXNE, TC and LBD/CTS are cleared by writing 0; TC and ORE also clear on the read-SR-then-read-DR sequence, as the manual says |
0x04 |
DR |
write: transmit; read: the received byte and clear RXNE |
0x08 |
BRR |
DIV_Fraction 0..3, DIV_Mantissa 4..15; baud = f_PCLK / (8 x (2 - OVER8) x USARTDIV) |
0x0C |
CR1 |
SBK 0, RWU 1, RE 2, TE 3, IDLEIE 4, RXNEIE 5, TCIE 6, TXEIE 7, PEIE 8, PS 9, PCE 10, WAKE 11, M 12, UE 13, OVER8 15 |
0x10 |
CR2 |
stored, read back; STOP 12..13 is stored and 8N1 is assumed for pacing |
0x14 |
CR3 |
stored, read back |
0x18 |
GTPR |
stored, read back |
f_PCLK is HCLK for USART1 (APB2) and HCLK / 2 for USART2 (APB1), section 5:
84 and 42 MHz on the Black Pill, 100 and 50 MHz on the Nucleo.
USART1 is the host channel on the Black Pill; on the Nucleo it is USART2,
which that board wires to the ST-LINK's virtual COM port, and USART1 is the
port whose bytes go nowhere. Transmit is the part's own single buffer plus
shift register: a write to DR with TXE set clears TXE, the shift register
takes the byte and TXE comes straight back, and the byte reaches the host one
frame time (10 bit times at the configured baud) later, when TC sets. Receive:
host bytes queue in a FIFO the size of the real one-byte receive register plus a
host-side backlog of 256 bytes so typed text is not lost; RXNE is set while a
byte waits; reading DR pops it, and a byte arriving with RXNE already set
sets ORE.
The other USART is a second, identical one whose transmitted bytes go nowhere (they are counted, so a test can see them) and whose receiver never has anything to read. It exists so firmware written for two ports links and runs.
NVIC lines: USART1 = 37, USART2 = 38. Each asserts while RXNE & RXNEIE,
TXE & TXEIE, TC & TCIE or IDLE & IDLEIE holds. Those are conditions, not
edge flags, so entering the handler clears nothing. A TXE handler with
nothing left to send has to switch itself off, as on the chip.
3.5 SYSCFG (0x4001_3800) and EXTI (0x4001_3C00)
| offset | name | bits |
|---|---|---|
SYSCFG 0x00 |
MEMRMP |
stored, read back; the boot remap is fixed at main flash |
SYSCFG 0x04 |
PMC |
stored, read back |
SYSCFG 0x08..0x14 |
EXTICR1..EXTICR4 |
4 bits per line: 0 = port A, 1 = port B, 2 = port C, 3 = port D. Line n watches pin n of the selected port |
SYSCFG 0x20 |
CMPCR |
reads 0 |
EXTI 0x00 |
IMR |
interrupt mask, one bit per line 0..15 (lines 16..22 are stored and never fire) |
EXTI 0x04 |
EMR |
event mask; stored, and a masked line wakes the core from WFE |
EXTI 0x08 |
RTSR |
rising trigger select |
EXTI 0x0C |
FTSR |
falling trigger select |
EXTI 0x10 |
SWIER |
writing 1 sets the matching PR bit |
EXTI 0x14 |
PR |
pending; write 1 to clear |
NVIC lines: EXTI0 = 6, EXTI1 = 7, EXTI2 = 8, EXTI3 = 9, EXTI4 = 10, EXTI9_5 =
23, EXTI15_10 = 40. A line asserts its NVIC input while its PR bit is set and
its IMR bit is set; the handler clears PR, as on silicon.
3.6 SysTick and the NVIC
Both are inside cortexm-core at the architectural addresses in the System
Control Space and never reach the bus (proofs/cortexm/REPORT.md section 5).
SYST_CALIB is programmed to 10500 with SKEW and NOREF clear: the board's
external reference is HCLK/8 = 10.5 MHz, so a 10 ms tick is 105,000 of them,
which does not fit 24 bits: the part's own answer, and the reason CMSIS
SysTick_Config uses the processor clock. With CLKSOURCE clear the model
still counts processor cycles; the /8 divider is not modeled and is called out
here rather than hidden.
stm32f411.h also has CMSIS's spellings, for code written against ST's
headers: the SysTick_CTRL_*_Msk and _Pos names, SysTick_LOAD_RELOAD_Msk
and SysTick_VAL_CURRENT_Msk, SystemCoreClock (84,000,000, a variable as in
CMSIS) and an empty SystemCoreClockUpdate(). The runtime's millis() runs on
SysTick, so a sketch that reprograms it changes millis() and delay().
3.6a ADC1 (0x4001_2000, common registers at 0x4001_2300)
One regular group, started by SWSTART (bit 30 of CR2) while ADON is set,
in single or continuous mode, with SCAN walking the sequence and leaving the
last result in DR, which is what a part with no DMA does. There is no "write
ADON twice" start on this family. SR: EOC (1) sets when a conversion
finishes and clears when DR is read; STRT (4) sets when one starts; both
are rc_w0. CR1.RES (bits 24..25) picks 12, 10, 8 or 6 bits; CR2.ALIGN
left-aligns; CR1.EOCIE raises NVIC line 18 (ADC). ADC_CCR.ADCPRE (bits
16..17) divides PCLK2 by 2, 4, 6 or 8 for the ADC clock, and a conversion takes
the channel's SMPRx sample time (3, 15, 28, 56, 84, 112, 144 or 480 ADC
clocks) plus one clock per bit (RM0383 11.5): EOC is neither early nor late.
Channels 0..7 are PA0..PA7, 8 and 9 PB0 and PB1, 10..15 PC0..PC5
(DS10314 table 8). The board hands each one the voltage on its net as a
twelve-bit count against 3.3 V; a floating pin reads noise. Channel 17
(VREFINT, 1.21 V, count 1502) and 18 (the temperature sensor at 25 C, 0.76 V,
count 943) read their nominal values while ADC_CCR.TSVREFE (bit 23) is set,
and 0 otherwise. Not modeled: the injected group, the analog watchdog,
external triggers, DMA and the overrun flag. Clock enable RCC_APB2ENR bit 8.
3.7 What is not modeled
The clock tree and flash latency, DMA, USB, I2C, SPI, SDIO, the RTC, the
watchdogs, TIM9 and TIM11, the CRC unit, the power controller's low-power
modes, the option bytes, and flash programming. Their registers are unmapped
(access fault) except where a block is listed above, where an unimplemented
offset reads 0, ignores writes and counts a stray. WFI and WFE park the
core; SLEEPDEEP is stored and behaves as ordinary sleep.
4. Host channel
host = "utf-8 bytes on USART1" ("utf-8 bytes on USART2" on the Nucleo).
readHost returns the host USART's TX bytes as they finish going out on the
wire; writeHost pushes bytes into its receive queue (overflow drops and sets ORE, as on the chip). The serial monitor shows
bytes as text, decoding UTF-8 and treating \n as a line end (\r ignored).
5. Time model and the part
84 MHz on the Black Pill, one cycle per 1/84 us (100 MHz on the Nucleo, one
cycle per 10 ns exactly, and a slice of 1000 cycles; the rest of this section
gives the Black Pill's numbers). Cycle counts come from the core (Cpu::run
returns cycles; the core's cycle counter is the clock). A cycle is not a whole
number of picoseconds, so time is derived as ps = cycles * 1_000_000 / 84,
exactly, never accumulated, always recomputed from the cycle count, so it can
never drift. A 10 us slice is exactly 840 cycles, and 840 cycles is exactly
10,000,000 ps, so slice boundaries are exact.
Clocks, all fixed:
| domain | Black Pill | Nucleo |
|---|---|---|
| SYSCLK, HCLK, CPU | 84 MHz | 100 MHz |
| APB1 peripheral clock (USART2) | 42 MHz | 50 MHz |
| APB1 timer clock (TIM2..TIM5) | 84 MHz | 100 MHz |
| APB2 (USART1, ADC1) and its timers (TIM1, TIM10) | 84 MHz | 100 MHz |
Inside a slice the core is stepped so that every peripheral store carries the
CPU cycle it happened at: a pad change becomes a drive_after at that offset
from the start of the slice, so two BSRR writes four cycles apart really are
48 ns apart on the net. Timers are advanced analytically. Each timer's next
event (a compare match, an update, an overflow) is computed from its count,
prescaler, ARR and mode, the core is run up to that cycle, the event is
applied, and the run continues, so a PWM edge lands on the exact cycle rather
than at the end of a slice. Reads of IDR see the pin levels as of the slice
start plus any change the program itself made in the slice.
WFI (and WFE with the event register clear) parks the core: the part stops
slicing and schedules one wake at the earliest of the SysTick reload, the next
timer event that can raise an enabled interrupt, and the next USART event; a pin
change or a host byte wakes it immediately. This is the whole idle story.
proofs/cortexm/REPORT.md measures the core at 1.02x of a real STM32F411 in
Chromium and says plainly that there is no headroom, so a sketch that busy-waits
through a delay() will not hold real time. delay() in the runtime therefore
parks on WFI and is woken by SysTick, and an idle board costs the kernel a
handful of events a simulated second instead of 84 million cycles.
Probe: bit 0 = running (1 while executing, 0 in reset or halted), bit 1 =
sleeping (parked in WFI/WFE), bit 2 = the on-board LED (PC13 driven low),
bit 3 = TX activity in the last 20 ms, bit 4 = RX activity in the last 20 ms. So
probe is a small bitmask, 0 with no program.
Poke: 0 = release KEY, 1 = press KEY (shorts PA0 to ground while held),
2 = hold NRST, 3 = release NRST.
Crash handling as in docs/esp32c3.md section 5: the core reports a Trap, and
the part halts and prints one line on the host channel, for example
firmware crashed at pc=0x08000412, HardFault (CFSR 0x00000100). A lockup, an
undefined instruction, a BKPT and an unmapped vector table all arrive as
traps. The part then stops asking for wakeups.
6. Firmware runtime (firmware/stm32/)
A freestanding C and C++ runtime for the STM32F411 built with clang 18
(--target=thumbv7em-none-eabi -mfloat-abi=soft -mcpu=cortex-m4 -Os) and lld,
no newlib, no CMSIS, no HAL, nothing GPL:
crt0.S: the 16 system vectors plus the 85 IRQ vectors at the head of flash,_start(setMSP, which the core has already done from vector 0; copy.datafrom its load address in flash, clear.bss, run the C++ static constructors, callmain), and a default handler that stops.link.ld: flash at0x0800_0000for 512 KB, SRAM at0x2000_0000for 128 KB, stack from the top of SRAM, with an assert that the static data leaves room for it.stm32f411.h: every register in section 3 as a volatile macro with the real ST name (GPIOC->ODR,RCC->AHB1ENR,TIM3->CCR1,USART1->DR), plus the bit constants and the IRQ numbers.arduino.h: the same sketch surface as the Uno runtime, on this part's pins:pinMode,digitalWrite,digitalRead,analogWrite,delay,delayMicroseconds,millis,micros,Serial,attachInterrupt,shiftOut,map,random. Pin numbers are the port names:PA0,PB9,PC13are constants, andLED_BUILTINisPC13.
millis() comes from SysTick at 1 kHz (reload 83,999). micros() comes from
TIM2, a 32-bit free-running counter at 1 MHz (PSC = 83, ARR = 0xFFFFFFFF),
so it has 1 us resolution and wraps every 71.6 minutes. delay() parks the core
on WFI until the SysTick count reaches the deadline; delayMicroseconds()
busy-waits on TIM2.
analogWrite(pin, 0..255) drives PWM at 1 kHz, as STM32duino does
(PWM_FREQUENCY 1000: PSC = 1, a 42 MHz tick, and ARR = 41,999, which is
84 MHz / 2 / 42,000), with CCRx = 42,000 x value / 255, the core's
setCaptureCompare scaling, on twelve pins:
| timer | channel 1 | channel 2 | channel 3 | channel 4 |
|---|---|---|---|---|
| TIM5 | PA0 |
PA1 |
PA2 |
PA3 |
| TIM3 | PA6 |
PA7 |
PB0 |
PB1 |
| TIM4 | PB6 |
PB7 |
PB8 |
PB9 |
0 and 255 are not PWM: they disconnect the compare output and drive the pin low
or high, so analogWrite(pin, 0) really is off. A digitalWrite on a PWM pin
takes the pin back, as the Arduino core does.
analogRead(pin) converts on ADC1 (section 3.6a) as STM32duino does: the pin
to analog mode, ADCPRE PCLK2 / 4, 15 cycles of sampling, SWSTART and a wait
on EOC, and the result in ten bits by default (ADC_RESOLUTION 10).
analogReadResolution(bits) picks the converter's nearest resolution (6, 8, 10
or 12) and maps to the bits asked for with the core's mapResolution. A number
below the board's analog input count is an analog input (analogRead(0) is
A0), as in STM32duino; on the Black Pill A0 to A9 are PA0 to PA7,
PB0 and PB1.
Serial is USART1 at PA9/PA10, 8N1, receive interrupt-driven into a 64-byte
ring, transmit through a 64-byte ring emptied by the TXE interrupt, as
STM32duino's HardwareSerial does (SERIAL_TX_BUFFER_SIZE 64): a print that
fits returns at once and the bytes leave at the baud rate, a longer one waits
for room, and availableForWrite() is the room left (63 when empty). attachInterrupt(pin, fn, mode) uses
SYSCFG and EXTI: one handler per line, CHANGE, RISING and FALLING.
Examples under firmware/stm32/examples/<name>/sketch.ino, built to
web/public/firmware/stm32/<name>.elf with web/public/firmware/stm32/index.json
in the same shape as the Uno index, each entry carrying
"program": "elf32 arm stm32f411":
blink:PC13at 1 Hz, active low,delay(500)parked onWFI.button: theKEYbutton onPA0withINPUT_PULLUPlightsPC13.serial: printsmillis()once a second and echoes what it receives.fade:analogWriteonPA0breathing an LED, withPC13following.
The UI picks the firmware list by the part's catalog program string
(docs/sim-api.md section 3): "elf32 riscv32imc" reads /firmware/index.json,
"elf32 avr atmega328p" reads /firmware/uno/index.json,
"elf32 arm stm32f411" reads /firmware/stm32/index.json, and
"elf32 arm stm32f411 nucleo" reads /firmware/nucleof411re/index.json.
7. What this model is sure of, and what it is not
Three groups, the same sorting docs/esp32c6.md section 7 uses. It exists because a
board model that quietly guesses an address is worse than one that says it did not know.
Taken from the reference manual, and run through by real firmware. Every base
address, register offset and bit position in sections 2 and 3 is from the STM32F4
reference manual (RM0383) and the STM32F411CE data sheet: RCC at 0x4002_3800, GPIOA/B/C
at 0x4002_0000/0400/0800, TIM2 to TIM5 at 0x4000_0000/0400/0800/0C00, USART1 at
0x4001_1000 and USART2 at 0x4000_4400, SYSCFG at 0x4001_3800 and EXTI at 0x4001_3C00,
SysTick and the NVIC at their ARM addresses, 128 KB of SRAM at 0x2000_0000 and 512 KB of
flash at 0x0800_0000; GPIOD at 0x4002_0C00, TIM1 at 0x4001_0000, ADC1 at 0x4001_2000 and
TIM10 at 0x4001_4400 with the ADC's bit fields and timings from RM0383 chapter 11.
crates/stm32f411/tests/firmware.rs and tests/nucleo.rs load real ELFs and drive
GPIO, the timers, both USARTs, the ADC, SysTick and EXTI through them. The WeAct Black Pill's header
order and its PC13 LED are the board's own.
Modeled, but not held to the reference manual's numbers. Behaviors rather than addresses:
- The clock is fixed (84 MHz, or 100 on the Nucleo) and the clock tree is not there. RCC's enable bits are modeled as gates, but the PLL, the HSE crystal, the prescalers and the flash latency are not: whatever a sketch programs, the core runs at its board's clock. A real board that forgets to configure the PLL runs at 16 MHz off the HSI and everything timed is five times slow; here it is right by accident.
- The slice has two gears, and the first store after a quiet spell is early. The core runs in coarse batches until a store lands on a GPIO, timer or USART register and then one instruction at a time for a while. Everything inside a burst is exact; the first store of a burst is timed at the head of the batch it happened in, which is up to 6.1 us early.
- The pads are three states and a threshold. Push-pull at 3.3 V, open drain, high-Z,
or a 40 kΩ internal pull (the data sheet gives 30 kΩ to 50 kΩ), read back against half
the supply, with no hysteresis, no speed setting, no rise time and no current limit.
The on-board LED is an LED behind 470 Ω on
PC13, anode to the rail, so a low lights it. - The board is an ideal supply: 3V3 and 5V at rail impedance for ever, so nothing
browns out and the regulator is not there.
VBATis a pin and no more. Startup is a microsecond, with no option bytes and no boot pins.
Deliberately absent, as section 3.7 lists: the clock tree and flash latency, DMA,
USB (so the board's USB socket carries nothing; Serial reaches the monitor through
the host channel and PA11/PA12 stay plain GPIO), I2C, SPI, I2S, SDIO, the RTC, the
watchdogs, TIM9 and TIM11, CRC, the FPU and its registers, and flash programming. The
FPU's absence costs time, not results: the runtime is built for the soft-float ABI,
so a float is computed in software and comes out the same, in more cycles than the
chip's FPU takes.
8. The Nucleo-F411RE (nucleof411re)
ST's NUCLEO-F411RE (MB1136, UM1724): the STM32F411RET6, the same die in the
64-pin package, on a board with an ST-LINK. The part is the same adapter as the
Black Pill (crates/parts/src/parts/stm32f411.rs) with the board
configuration stm32f411::NUCLEO_F411RE; the runtime is firmware/stm32/
built with -DMOKXI_BOARD_NUCLEO_F411RE (firmware/nucleof411re/build.sh).
Catalog program = "elf32 arm stm32f411 nucleo", which reads
/firmware/nucleof411re/index.json; host = "utf-8 bytes on USART2".
Clock. 100 MHz, the clock STM32duino's NUCLEO_F411RE variant programs (HSE
bypass from the ST-LINK's 8 MHz, PLLM 4, PLLN 100, PLLP 2; APB1 / 2, APB2 / 1),
so a cycle is exactly 10 ns and a 10 us slice 1000 cycles. The core costs the
host the same per cycle as on the Black Pill, and there are 19 % more cycles a
second, so a sketch that busy-waits needs 19 % more of the machine to hold real
time; one that sleeps in delay() costs nothing either way.
Pins (catalog order): the Arduino headers CN6 IOREF, NRST, 3V3,
5V, GND, GNDb, VIN; CN8 A0..A5; CN5 D15, D14, AVDD,
GNDc, D13..D8; CN9 D7..D2; then the pins only the morpho headers
bring out, CN7 PC10, PC11, PC12, PD2, VDD, E5V, BOOT0, GNDd,
PA13, PA14, PA15, PB7, PC13, VBAT, PC2, PC3 and CN10 PC9,
PC8, PC6, PC5, U5V, GNDe, PA12, PA11, PB12, PB2, PB1,
PB15, PB14, PB13, AGND, PC4, PA2, PA3 (63 pins). A pad on both an
Arduino header and a morpho header is one pin, under its Arduino name:
| Arduino | D2 |
D3 |
D4 |
D5 |
D6 |
D7 |
D8 |
D9 |
D10 |
D11 |
D12 |
D13 |
D14 |
D15 |
A0 |
A1 |
A2 |
A3 |
A4 |
A5 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| pad | PA10 |
PB3 |
PB5 |
PB4 |
PB10 |
PA8 |
PA9 |
PC7 |
PB6 |
PA7 |
PA6 |
PA5 |
PB9 |
PB8 |
PA0 |
PA1 |
PA4 |
PB0 |
PC1 |
PC0 |
Left out because the board as shipped does not connect them (UM1724 6.8,
6.12): D0/D1 (SB62/SB63 open; PA3/PA2 go to the ST-LINK and are on
the morpho header), PC14/PC15 (the LSE crystal) and PH0/PH1 (the
ST-LINK's MCO). VIN, E5V and BOOT0 are inputs the board ignores; IOREF,
VDD, AVDD and VBAT (tied to VDD on this board) are 3.3 V; U5V is
5 V.
On the board. LD2, green, from PA5 through 510 ohms to ground: it is
lit while the pin drives high, and only while it is an output (probe bit 2).
B1, the blue USER button, shorts PC13 to ground (poke 1 holds it, 0
releases), and R30, 4.7 k from PC13 to 3.3 V, is always on the pin, so an
input reads high released. B2 is NRST (poke 2, 3). The probe's bits are
the Black Pill's; bits 3 and 4 are the ST-LINK's LD1 COM.
Runtime. STM32duino's NUCLEO_F411RE numbering: 0..15 are D0..D15,
16..51 the variant's morpho order, PIN_A0..PIN_A15 = 192..207 its
analog inputs, and every port name is defined as one of those numbers (PA5 is
PIN_A15, which is digital pin 13), so 13, D13, PA5 and LED_BUILTIN
are one pin. USER_BTN is PC13. Serial is USART2 on PA2/PA3
(SERIAL_UART_INSTANCE 2), also named Serial2; there is no Serial1.
analogWrite uses the first timer the variant's PinMap_TIM lists for each
pin, at 1 kHz (PSC 1, ARR 49,999): TIM2 on PA0..PA3, PA5, PA15,
PB3, PB10; TIM3 on PA6, PB4, PB5, PC6..PC9; TIM4 on PB6..PB9;
TIM1 on PA8..PA11 and, as CH1N..CH3N, PA7, PB0, PB1, PB13..
PB15. So micros() runs on TIM5 here, and tone() on TIM10, the variant's
TIMER_TONE. Wire defaults to D14/D15, SPI to D13, D12, D11,
D10. Examples in firmware/nucleof411re/examples/: blink, button,
serial, fade (D9) and analog (A0).