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 STM32 Blue Pill and the board page.

STM32 "Blue Pill" F103 board contract (Phase 9)

The fifth microcontroller and the second Cortex-M board. One kernel component (bluepill in crates/parts) wraps an STM32F103C8T6 SoC model (crates/stm32f103) built on the same ARMv7-M core as the Black Pill (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 STM32F103 addresses, taken from RM0008 (the STM32F101/102/103/105/107 reference manual) and the STM32F103x8/xB datasheet, so a program built for Mokxi also runs on a real Blue Pill.

The catalog type is bluepill (STM32F103C8T6, 48-pin LQFP, the board everybody calls the Blue Pill). 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.

Why this is not the Black Pill with different numbers

docs/stm32f411.md is the sibling of this document, and the temptation is to read one as the other. Do not. The F1 is a different generation of the family and the differences are exactly the ones a program can see:

F411 (Black Pill) F103 (Blue Pill)
core Cortex-M4, no FPU Cortex-M3
clock 84 MHz 72 MHz
flash / SRAM 512 KB / 128 KB 64 KB / 20 KB
pin configuration MODER, OTYPER, PUPDR, AFRL/AFRH one four-bit field in CRL/CRH
input pull direction PUPDR that pin's ODR bit
clear a pin BSRR high half BSRR high half, or BRR
GPIO clock gate RCC_AHB1ENR RCC_APB2ENR
alternate function a number per pin in AFR fixed per peripheral, remapped in AFIO_MAPR
EXTI port select SYSCFG_EXTICR AFIO_EXTICR
RCC base 0x4002_3800 0x4002_1000
timers TIM2 and TIM5 are 32-bit every counter is 16-bit
ADC not modeled ADC1, single conversion
debug pins at reset PA13, PA14, PA15, PB4 pulled the same five, released by SWJ_CFG

Not one address is shared. The two SoC crates are siblings, not a template and a copy.

Running vendor binaries (STM32Cube HAL, libopencm3, the STM32duino core) is not a 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, read a voltage 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, 53 x 23 mm, with the micro-USB socket 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, PA0, PA1, PA2, PA3, PA4, PA5, PA6, PA7, PB0, PB1, PB10, PB11, NRST, 3V3, GND, 5V

Right header: GNDb, GNDc, 3V3b, PB12, PB13, PB14, PB15, PA8, PA9, PA10, PA11, PA12, PA15, PB3, PB4, PB5, PB6, PB7, PB8, PB9

40 pins: 32 I/O, three grounds, two 3.3 V, one 5 V, VBAT and NRST. The silkscreen prints the short forms (VB, C13, R, A0, B9, G, 3.3); the catalog uses the full port names because a netlist pin reference has to be unambiguous, and suffixes the repeats (GNDb, GNDc, 3V3b) because two pins of one part may not share a name.

Three chip pins are not on a header: PA13 and PA14 are the separate four-pin SWD header along the bottom edge, and PB2 is the BOOT1 jumper at the top. They exist in the chip model (they have to, because the debug port holds them), but no wire can reach them here, exactly as on the board.

Electrical:

  • 3V3 and 3V3b drive 3.3 V at R_SUPPLY; 5V drives 5 V at R_SUPPLY; GND, GNDb and GNDc drive 0 V. The board is always powered (USB). 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 (the datasheet gives 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 (MODE != 00): push-pull at 3.3 V or 0 V through R_STRONG, or open-drain (CNF = 01 or 11) where a high is high-Z with no pull at all, because an F1 output has none. Input (MODE = 00): high-Z for floating (CNF = 01) and analog (CNF = 00), or a 40 k pull whose direction is that pin's ODR bit for CNF = 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 green LED, anode to 3.3 V through 1 k, 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). 1 k rather than the Black Pill's 470 R, which is why a Blue Pill's LED is the dimmer of the two. ST's own datasheet note applies here too: PC13, PC14 and PC15 are on the backup power domain and can sink only 3 mA, so they drive an indicator and not much else.
  • There is no user button. The Blue Pill's one button is RESET, which is reached through poke (section 5). A sketch that wants a button needs one on the breadboard, which is why so many Blue Pill tutorials start by wiring one.
  • PA9/PA10 are USART1 TX/RX on the real board and here; the USART's bytes go to the host channel (section 4). PA11/PA12 are USB D-/D+ on the real board and plain GPIO here.
  • PA15, PB3 and PB4 are on the header, but the SWJ debug port holds them out of reset: PA15 and PB4 pulled up, PB3 floating. Firmware gets them back by writing SWJ_CFG in AFIO_MAPR (section 3.5). This is the single most common surprise on this board and the model reproduces it rather than hiding it.

2. Memory map (real STM32F103C8 addresses)

region address size notes
flash 0x0800_0000 64 KB the STM32F103C8's flash; writes are ignored (no flash programming interface)
flash alias 0x0000_0000 64 KB the same bytes, because BOOT0 is low and the boot area maps main flash to 0
SRAM 0x2000_0000 20 KB
APB1 0x4000_0000 32 KB TIM2..TIM4, USART2, USART3, section 3
APB2 0x4001_0000 32 KB AFIO, EXTI, GPIOA..GPIOC, ADC1, TIM1, USART1, section 3
AHB 0x4002_0000 RCC at 0x4002_1000, 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.

Many C8 boards really carry 128 KB of flash, because the die is a CB with half of it untested. 64 KB is what the part number promises, so 64 KB is what this model gives and what firmware may rely on; the link script asserts it.

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 STM32F1 family implements the top four bits of each 8-bit priority field, like the F4, so NVIC_IPR behaves the way a CMSIS NVIC_SetPriority expects. A Cortex-M3 is ARMv7-M with neither the M4's DSP extension nor an FPU, which is exactly what cortexm-core implements and refuses: every VFP and saturating-SIMD encoding raises UNDEFINSTR. No feature had to be switched off for this part; the M4's extras were never there. Compile firmware -mcpu=cortex-m3 -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 APB bridge does for the registers modeled here.

3.1 RCC (0x4002_1000)

The clock tree is not modeled: the part behaves as if the PLL is already locked and SYSCLK = HCLK = 72 MHz from power-on, which is the HSE crystal times nine. 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. Reset 0x0000_0083
0x04 RCC_CFGR stored; SWS[3:2] always reads back SW[1:0], so a spin on "the switch happened" terminates immediately. HPRE, PPRE1, PPRE2, PLLMUL, PLLSRC and ADCPRE are stored and ignored: the model runs everything at the frequencies in section 5
0x08 RCC_CIR stored, read back
0x0C RCC_APB2RSTR a 1 written to a peripheral's bit resets that peripheral's registers; the register itself reads 0
0x10 RCC_APB1RSTR the same for the APB1 peripherals
0x14 RCC_AHBENR stored, read back; reset 0x0000_0014 (SRAMEN, FLITFEN)
0x18 RCC_APB2ENR bit 0 AFIOEN, 2 IOPAEN, 3 IOPBEN, 4 IOPCEN, 5 IOPDEN, 6 IOPEEN, 9 ADC1EN, 10 ADC2EN, 11 TIM1EN, 12 SPI1EN, 14 USART1EN
0x1C RCC_APB1ENR bit 0 TIM2EN, 1 TIM3EN, 2 TIM4EN, 11 WWDGEN, 14 SPI2EN, 17 USART2EN, 18 USART3EN, 21 I2C1EN, 22 I2C2EN, 23 USBEN, 25 CANEN, 27 BKPEN, 28 PWREN
0x20 RCC_BDCR stored; LSERDY (1) follows LSEON (0)
0x24 RCC_CSR stored; LSIRDY (1) follows LSION (0); PINRSTF (26) set after an NRST reset, PORRSTF (27) after power-on, SFTRSTF (28) after AIRCR.SYSRESETREQ; a write with RMVF (24) clears the flags. There is no BORRSTF: this family has no brown-out reset flag, and bit 25 is reserved

GPIO is gated from APB2ENR, not from an AHB register. That one line is the difference between a Blue Pill sketch that works and one where every pin write vanishes, and it is where an F4 habit fails first.

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 (0x4001_0800, 0x4001_0C00, 0x4001_1000)

offset name bits
0x00 CRL 4 bits per pin, pins 0..7: MODE in 0..1, CNF in 2..3
0x04 CRH the same for pins 8..15
0x08 IDR read-only, bits 0..15: the pin levels
0x0C ODR bits 0..15: the output latch, and the pull direction of a pulled input
0x10 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
0x14 BRR write-only: bits 0..15 clear the matching ODR bit. The F4 has no such register
0x18 LCKR stored, read back; the lock sequence is not enforced

MODE: 00 input, 01 output at 10 MHz, 10 output at 2 MHz, 11 output at 50 MHz. The three output rates are stored and have no effect on edge timing here, which is called out rather than hidden.

CNF while MODE is 00: 00 analog, 01 floating, 10 pulled (and the pull direction is that pin's ODR bit, 1 up and 0 down), 11 reserved.

CNF while MODE is not 00: 00 general-purpose push-pull, 01 general-purpose open drain, 10 alternate-function push-pull, 11 alternate-function open drain.

Reset value of both CRL and CRH is 0x4444_4444: every pin a floating input. ODR resets to 0.

IDR reads back what this chip is driving on a pin it drives push-pull, and what the board sampled off the net on any other pin, exactly as PINx does on the Uno. An open-drain output therefore reads the pin rather than the latch, which is how a one-wire bus works at all.

The SWJ debug port holds five pins out of reset, whatever CRL/CRH say (RM0008 9.3.5): PA13 (JTMS/SWDIO) pulled up, PA14 (JTCK/SWCLK) pulled down, PA15 (JTDI) pulled up, PB3 (JTDO) floating, PB4 (NJTRST) pulled up. AFIO_MAPR.SWJ_CFG decides which it keeps: 000 all five, 001 all but PB4, 010 only PA13/PA14 (SWD, and the usual choice), 100 none. A pin the debug port holds ignores the GPIO block entirely.

3.3 TIM1, TIM2, TIM3, TIM4 (0x4001_2C00, 0x4000_0000, 0x4000_0400, 0x4000_0800)

Every counter on this part is 16-bit. All four are clocked at 72 MHz (section 5). TIM1 is the advanced timer; the other three are general purpose.

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. 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, counter and repetition 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, and the same eight bits up for channel 2
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 16 bits
0x28 PSC 16 bits; the counter ticks at 72 MHz / (PSC + 1)
0x2C ARR reload value; the period is ARR + 1 ticks up-counting
0x30 RCR TIM1 only, 8 bits: the update event happens every RCR + 1 overflows. Reserved on the other three, and a write there counts a stray
0x34 0x38 0x3C 0x40 CCR1 CCR2 CCR3 CCR4 16 bits
0x44 BDTR TIM1 only: only MOE (15) is acted on, and without it the channels reach no pin. Dead time, the break input and the complementary outputs are stored and not modeled
0x48 0x4C DCR DMAR 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, on TIM1, BDTR.MOE as well) and the pad's CNF says alternate function (section 3.2), and AFIO_MAPR has that channel on that pin (section 3.5).

Input capture is not modeled: CCxS != 0 stores and counts a stray.

NVIC lines: TIM1 splits its flags across TIM1_UP = 25 (the update flag) and TIM1_CC = 27 (the four compare flags and the trigger); TIM2 = 28, TIM3 = 29, TIM4 = 30 each carry all of theirs. 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_3800), USART2 (0x4000_4400), USART3 (0x4000_4800)

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
0x04 DR write: transmit; read: the received byte and clear RXNE
0x08 BRR DIV_Fraction 0..3, DIV_Mantissa 4..15; baud = f_PCLK / (16 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. Bit 15 is reserved: there is no OVER8 on this part, and oversampling is always by sixteen
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 72 MHz for USART1 (APB2) and 36 MHz for USART2 and USART3 (APB1), section 5. At 72 MHz and 115200 baud USARTDIV is exactly 39.0625, so BRR = 0x271 and the rate is exact; that is one reason 72 MHz is the number everyone picks for this part.

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 on a full queue sets ORE.

USART2 and USART3 are identical USARTs whose transmitted bytes go nowhere (they are counted, so a test can see them) and whose receivers never have anything to read. They exist so firmware written for more than one port links and runs.

NVIC lines: USART1 = 37, USART2 = 38, USART3 = 39. 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 AFIO (0x4001_0000) and EXTI (0x4001_0400)

offset name bits
AFIO 0x00 EVCR stored, read back
AFIO 0x04 MAPR the remaps below, plus SWJ_CFG in 24..26
AFIO 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
AFIO 0x1C MAPR2 stored, read back
EXTI 0x00 IMR interrupt mask, one bit per line 0..15 (lines 16..18 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

There is no AFRL/AFRH on this part: a peripheral's pins are fixed, and AFIO_MAPR moves a few of them wholesale. The mappings modeled:

MAPR field value channel 1 / TX 2 3 4
TIM1_REMAP 6..7 00, 01 PA8 PA9 PA10 PA11
11 port E, not modeled
TIM2_REMAP 8..9 00 PA0 PA1 PA2 PA3
01 PA15 PB3 PA2 PA3
10 PA0 PA1 PB10 PB11
11 PA15 PB3 PB10 PB11
TIM3_REMAP 10..11 00 PA6 PA7 PB0 PB1
10 PB4 PB5 PB0 PB1
11 PC6 PC7 PC8 PC9
TIM4_REMAP 12 0 PB6 PB7 PB8 PB9
1 port D, not modeled
USART1_REMAP 2 0 / 1 PA9 / PB6 TX
USART2_REMAP 3 0 PA2 TX (1 is port D, not modeled)
USART3_REMAP 4..5 00 / 01 PB10 / PC10 TX

A remap onto a port this model does not have (D or E) leaves the pins alone rather than inventing them. A pin can be two peripherals' alternate function (PA9 is USART1 TX and TIM1 channel 2), which on silicon is a configuration mistake; the model settles it in one place, USART before timer, so a pad has exactly one owner and no peripheral can push another one off a pin behind its back.

SWJ_CFG (24..26) is what releases the debug pins; see section 3.2.

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 ADC1 (0x4001_2400)

A twelve-bit successive-approximation converter, clocked at PCLK2 / 6 = 12 MHz (section 5), which is the only divider that keeps a 72 MHz APB2 inside the part's 14 MHz limit. One ADC clock is six CPU cycles.

offset name bits
0x00 SR AWD 0, EOC 1, JEOC 2, JSTRT 3, STRT 4; write 0 to clear
0x04 CR1 EOCIE 5, SCAN 8; the rest is stored
0x08 CR2 ADON 0, CONT 1, CAL 2, RSTCAL 3, ALIGN 11, EXTSEL 17..19, EXTTRIG 20, SWSTART 22
0x0C 0x10 SMPR1 SMPR2 3 bits per channel: 1.5, 7.5, 13.5, 28.5, 41.5, 55.5, 71.5, 239.5 ADC clocks. SMPR2 holds channels 0..9
0x14..0x20 JOFR1..JOFR4 stored, read back
0x24 0x28 HTR LTR stored, read back; the analog watchdog is not modeled
0x2C SQR1 L 20..23, the group length minus one, and conversions 13..16
0x30 0x34 SQR2 SQR3 five bits per conversion; SQR3[4:0] is the first
0x38 JSQR stored, read back; the injected group is not modeled
0x3C..0x48 JDR1..JDR4 read 0
0x4C DR the last completed regular conversion; reading it clears EOC

A conversion takes sample time + 12.5 ADC clocks, and the result is not delivered early: firmware really does wait on EOC. At the default 1.5-cycle sample that is 14 ADC clocks (84 CPU cycles, 1.17 us); at the 55.5 the runtime uses, 68 clocks (408 cycles, 5.7 us).

Starting a conversion: writing ADON while it is already 1 starts one, which is the part's own idiom, and so does SWSTART with EXTTRIG set and EXTSEL = 111. CONT restarts at the end of each. CAL and RSTCAL are accepted and read back clear at once (there is nothing to calibrate), and neither starts a conversion, because a calibrating converter does not convert. ALIGN puts the twelve bits in DR[15:4].

SCAN walks the regular group and leaves the last result in DR, which is exactly what a part with no DMA does, and the reason SCAN without DMA is a trap.

Channels: 0..7 are PA0..PA7 and 8..9 are PB0..PB1. Channels 10..15 are on port C pins a 48-pin part does not have. Channel 16 is the temperature sensor, fixed at its 25 C reading of 1.43 V (1775 counts), and channel 17 is VREFINT, fixed at 1.20 V (1489 counts). The board part converts the net voltage on each analog pin to a count against the 3.3 V rail; a floating or contended net reads 0, as it does digitally.

NVIC line: ADC1_2 = 18, asserted while EOC & EOCIE holds. ADC2 is not modeled.

3.7 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 90,000 with SKEW and NOREF clear: the external reference is HCLK/8 = 9 MHz, so a 10 ms tick is exactly 90,000 of them and the field is exact, unlike the Black Pill's, where it does not fit. With CLKSOURCE clear the model still counts processor cycles; the /8 divider is not modeled and is called out here rather than hidden.

3.8 What is not modeled

The clock tree and flash latency, DMA, USB, CAN, I2C, SPI, the RTC and backup registers, the watchdogs, ADC2, the ADC's injected group and analog watchdog, TIM1's break input and complementary outputs, input capture, 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

72 MHz, one cycle per 1/72 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 / 72, exactly, never accumulated, always recomputed from the cycle count, so it can never drift. A 10 us slice is exactly 720 cycles, and 720 cycles is exactly 10,000,000 ps, so slice boundaries are exact.

Clocks, all fixed:

domain frequency
SYSCLK, HCLK, CPU 72 MHz
APB1 peripheral clock (USART2, USART3) 36 MHz
APB1 timer clock (TIM2..TIM4) 72 MHz
APB2 (USART1, TIM1, ADC prescaler input) 72 MHz
ADC 12 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 55 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, the next USART event and the end of an ADC conversion; a pin change or a host byte wakes it immediately. This is the whole idle story. proofs/cortexm/REPORT.md measures the core at about real time 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 72 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 RESET, 1 = hold RESET. There is no second button on this board to poke.

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, BKPT #0 (CFSR 0x00000000, HFSR 0x40000000). 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/bluepill/)

A freestanding C and C++ runtime for the STM32F103 built with clang 18 (--target=thumbv7m-none-eabi -mcpu=cortex-m3 -mfloat-abi=soft -Os) and lld, no newlib, no CMSIS, no HAL, nothing GPL:

  • crt0.S: the 16 system vectors plus the 60 IRQ vectors at the head of flash, reset entry (copy .data from its load address in flash, clear .bss, run the C++ static constructors, call main; the core has already loaded MSP from vector 0), and a default handler that executes BKPT so a stray interrupt becomes one line on the serial monitor rather than a silent spin.
  • link.ld: flash at 0x0800_0000 for 64 KB, SRAM at 0x2000_0000 for 20 KB, stack from the top of SRAM, with an assert that the static data leaves 2 KB for it.
  • stm32f103.h: every register in section 3 as a volatile macro with the real ST name (GPIOC->ODR, RCC->APB2ENR, TIM3->CCR1, USART1->DR, ADC1->SQR3), plus the bit constants and the IRQ numbers.
  • arduino.h: the same sketch surface as the Uno and Black Pill runtimes, on this part's pins: pinMode, digitalWrite, digitalRead, analogRead, 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. There is one extra pinMode, INPUT_ANALOG, because the F1's converter may only sample a pin whose CNF says so.

millis() comes from SysTick at 1 kHz (reload 71,999). micros() comes from TIM2 at 1 MHz (PSC = 71, ARR = 0xFFFF) extended to 32 bits in software: every counter on this part is sixteen bits, so TIM2 overflows every 65.536 ms and its update interrupt carries the top half. micros() reads the two parts with a guard against an overflow landing between them, and folds in an overflow whose handler has not run, which is what noInterrupts() around a micros() call means. Fifteen interrupts a second is nothing next to SysTick's thousand. delay() parks the core on WFI until the SysTick count reaches the deadline; delayMicroseconds() busy-waits on micros().

analogRead(pin) returns the converter's own twelve bits, 0 at 0 V and 4095 at 3.3 V, not the ten an Arduino returns: this part's ADC really is twelve-bit and rounding it down to look like an AVR would throw away two bits for nothing. The first call brings ADC1 up, sets ADCPRE to /6 and runs the reset-calibrate sequence. The ten pins are PA0..PA7, PB0 and PB1; anything else returns 0.

analogWrite(pin, 0..255) drives PWM at 488.28 Hz (PSC = 575, a 125 kHz tick, and ARR = 255, which is 72 MHz / 576 / 256) on ten pins:

timer channel 1 channel 2 channel 3 channel 4
TIM3 PA6 PA7 PB0 PB1
TIM4 PB6 PB7 PB8 PB9
TIM1 PA8 none none PA11

PA0..PA3 are not PWM pins here, unlike on the Black Pill: TIM2 is the microsecond counter and it needs a whole timer to itself. TIM1's channels 2 and 3 are PA9 and PA10, which are Serial, so they are left out too. TIM1 needs BDTR.MOE, which the runtime sets when it starts that timer.

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.

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 AFIO and EXTI: one handler per line, CHANGE, RISING and FALLING.

Examples under firmware/bluepill/examples/<name>/sketch.ino, built to web/public/firmware/bluepill/<name>.elf with web/public/firmware/bluepill/index.json in the same shape as the Uno index, each entry carrying "program": "elf32 arm stm32f103":

  • blink: PC13 at 1 Hz, active low, delay(500) parked on WFI.
  • button: a pushbutton on PB0 with INPUT_PULLUP lights PC13.
  • serial: prints millis() and an analogRead(PA0) once a second, and echoes what it receives.
  • fade: analogWrite on PA6 breathing an LED from TIM3, with PC13 following.

The UI picks the firmware list by the part's catalog program string (docs/sim-api.md section 3): "elf32 arm stm32f411" reads /firmware/stm32/index.json and "elf32 arm stm32f103" reads /firmware/bluepill/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 STM32F1 reference manual (RM0008) and the STM32F103C8 data sheet: RCC at 0x4002_1000, GPIOA/B/C at 0x4001_0800/0C00/1000 with the F1's own CRL/CRH four-bit configuration nibbles, TIM1 to TIM4, USART1 to USART3, AFIO at 0x4001_0000, EXTI at 0x4001_0400 and ADC1 at 0x4001_2400, with 20 KB of SRAM and 64 KB of flash. crates/stm32f103/tests/firmware.rs loads real ELFs and drives GPIO, the timers, USART1, ADC1, SysTick and EXTI through them. The Blue Pill's forty-pin header order, its PC13 LED wired anode-to-rail, and the fact that PA15, PB3 and PB4 come up as JTAG pins are the board's own.

Modeled, but not held to the reference manual's numbers. Behaviors rather than addresses:

  • The clock is 72 MHz and the clock tree is not there. RCC's enable bits are gates; the PLL, the 8 MHz HSE crystal, the prescalers and the flash latency are not modeled, so whatever a sketch programs, the core runs at 72 MHz. A real Blue Pill that never configures the PLL runs at 8 MHz off the HSI and everything timed is nine times slow.
  • The slice has two gears, and the first store after a quiet spell is early, up to 7.1 us. Everything inside a burst is exact. The reasoning is the same as docs/stm32f411.md section 7.
  • ADC1 has no error in it. A conversion is the straight line against VREF+ = 3.3 V, sampled at the instant it started, with no sample-and-hold, no input impedance, no nonlinearity, no noise and none of the offset a real F103's ADC needs calibrating for (ADC_CR2's CAL bit is accepted and does nothing here).
  • 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 board is an ideal supply: 3V3 and 5V at rail impedance forever, so nothing browns out and the regulator (which on a clone Blue Pill is frequently the wrong part and the reason the board dies) is not there. Startup is a microsecond, with no boot pins and no option bytes.
  • There is no user button, because the board has none; the one button holds NRST low and arrives through poke.

Deliberately absent, as section 3.8 lists: the clock tree and flash latency, DMA, USB (so Serial reaches the monitor through the host channel and PA11/PA12 stay plain GPIO), CAN, I2C, SPI, the RTC and the backup registers, the watchdogs, TIM1's break input and complementary outputs, ADC2, the ADC's injected group and analog watchdog, and flash programming.