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.
STM32F411 "Black Pill" board contract (Phase 4)
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).
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, USB-C at the top. Catalog order is the left header top to bottom, then the right header top to bottom.
Left header: VBAT, PC13, PC14, PC15, NRST, PA0, PA1, PA2, PA3,
PA4, PA5, PA6, PA7, PB0, PB1, PB2, PB10, PA13, PA14, GNDb
Right header: 3V3, GND, 5V, PB9, PB8, PB7, PB6, PB5, PB4,
PB3, PA15, PA12, PA11, PA10, PA9, PA8, PB15, PB14, PB13,
PB12
40 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 last
three positions of the left header are the board's separate 4-pin SWD header
folded into the same row: PA13 (SWDIO), PA14 (SWCLK) and a second ground.
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,PBnandPCnis 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 and Floating both read 0 with no error, as on the Uno. 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 | USART1, SYSCFG, EXTI, section 3 |
| AHB1 | 0x4002_0000 |
64 KB | GPIOA..GPIOC 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 (0x4002_0000, 0x4002_0400, 0x4002_0800)
| 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 |
AF2 TIM3 |
PA6 CH1, PA7 CH2, PB0 CH3, PB1 CH4, PB4 CH1, PB5 CH2 |
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.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 84 MHz for USART1 (APB2) and 42 MHz for USART2 (APB1), section 5.
USART1 is the host channel. 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.
USART2 is a second, identical USART 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. 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.
3.7 What is not modeled
The clock tree and flash latency, DMA, USB, ADC, I2C, SPI, SDIO, the RTC, the
watchdogs, TIM1 and TIM9..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". readHost returns USART1 TX bytes as they
finish going out on the wire; writeHost pushes bytes into the USART1 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, one cycle per 1/84 us. 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 | frequency |
|---|---|
| SYSCLK, HCLK, CPU | 84 MHz |
| APB1 peripheral clock (USART2) | 42 MHz |
| APB1 timer clock (TIM2..TIM5) | 84 MHz |
| APB2 (USART1) | 84 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 488.28 Hz (PSC = 671, a 125 kHz
tick, and ARR = 255, which is 84 MHz / 672 / 256) 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. There is no analogRead: the ADC
is not modeled (section 3.7).
Serial is USART1 at PA9/PA10, 8N1, receive interrupt-driven into a 64-byte
ring, transmit polled on TXE so a long println costs simulated time at the
baud rate the way it does on the desk. 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, and
"elf32 arm stm32f411" reads /firmware/stm32/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. crates/stm32f411/tests/firmware.rs loads real ELFs and drives
GPIO, the timers, USART1, 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 84 MHz 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 84 MHz. 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), the ADC, I2C, SPI, I2S, SDIO,
the RTC, the watchdogs, TIM1 and TIM9 to TIM11, CRC, the FPU's registers, and flash
programming. The ADC's absence is the one worth saying out loud: every PA and PB0/
PB1 pin is an ADC input on a real F411, and analogRead has nothing behind it here.