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:

  • 3V3 drives 3.3 V at R_SUPPLY; 5V drives 5 V at R_SUPPLY; GND and GNDb drive 0 V. The board is always powered (USB-C). It is a supply for the rest of the circuit. VBAT is an input the board ignores: it is always powered from the 3.3 V rail here.
  • NRST is 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, PBn and PCn is an inout GPIO. Output (MODER = 01 or 10): push-pull at 3.3 V or 0 V through R_STRONG, or open-drain (OTYPER bit 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 for PUPDR = 01 or a 40 k pull-down for PUPDR = 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.
  • PC13 carries the on-board blue LED, anode to 3.3 V through 470 R, cathode to the pin: the LED lights when PC13 is driven low. It is a load on the pin, and the visual shows it from a probe bit (section 5).
  • PA0 also carries the KEY button, which shorts PA0 to ground while held. There is no external pull-up: firmware uses the internal one. The button is reached through poke (section 5), not through a pin.
  • PA9/PA10 are 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/PA12 are USB D-/D+ on the real board and plain GPIO here.
  • PA13/PA14 come out of reset in alternate-function mode for SWD, exactly as the part does, which means they are high-Z until firmware reprograms MODER.

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 (set MSP, which the core has already done from vector 0; copy .data from its load address in flash, clear .bss, run the C++ static constructors, call main), and a default handler that stops.
  • link.ld: flash at 0x0800_0000 for 512 KB, SRAM at 0x2000_0000 for 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, PC13 are constants, and LED_BUILTIN is PC13.

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: PC13 at 1 Hz, active low, delay(500) parked on WFI.
  • button: the KEY button on PA0 with INPUT_PULLUP lights PC13.
  • serial: prints millis() once a second and echoes what it receives.
  • fade: analogWrite on PA0 breathing an LED, with PC13 following.

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. VBAT is 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.