Bare-metal firmware on STM32L476RG (Cortex-M4). No HAL. No CubeMX.
Every register address looked up in RM0351 and written directly.
Git history shows the full prototype-to-RTOS evolution across four phases.
Both sensors on the same I2C bus. TMP117 at 0x49, MAX30102 at 0x57. Output below shows the progression across phases - filter behaviour from Phase 1-3, then BPM and FreeRTOS added in Phase 4.
USART2_BRR sits at offset 0x0C from the USART2 base - absolute address 0x4000440C.
Offset 0x08 is CR3. Writing a baud rate divisor to CR3 produces no output and no error.
Four hours of debugging, one wrong hex digit. RM0351 page 1388.
The IWDG SR update flags (PVU, RVU) only clear once the watchdog is running.
Writing 0x5555 (unlock) before 0xCCCC (start) means they never clear -
while(IWDG_SR != 0) loops forever. Also: LSI oscillator must be explicitly enabled via
RCC_CSR and LSIRDY confirmed before any IWDG config.
Found with GDB - firmware appeared dead, no UART output at all. RM0351 section 34.3.
Every address cross-referenced to RM0351. No guessing.
| Register | Address | Bits used | Note |
|---|---|---|---|
| RCC_AHB2ENR | 0x4002104C | Bit 0 = GPIOA, Bit 1 = GPIOB | Clock enable before any peripheral access |
| RCC_APB1ENR1 | 0x40021058 | Bit 17 = USART2, Bit 21 = I2C1 | |
| GPIOA_MODER | 0x48000000 | Bits 5:4 = PA2, Bits 11:10 = PA5 | 10 = AF mode, 01 = output |
| GPIOA_AFRL | 0x48000020 | Bits 11:8 = PA2 → AF7 | AF7 = USART2_TX |
| USART2_CR1 | 0x40004400 | Bit 0 = UE, Bit 3 = TE | Set TE before UE |
| USART2_BRR ⚑ | 0x4000440C | BRR = 417 for 9600 @ 4MHz | Offset 0x0C, not 0x08 (that is CR3) |
| USART2_ISR | 0x4000441C | Bit 7 = TXE | Wait for 1 before each TDR write |
| USART2_TDR | 0x40004428 | Write byte to transmit | |
| I2C1_TIMINGR | 0x40005410 | 0x00100D14 | 100kHz standard mode at 4MHz |
| RCC_CSR | 0x40021094 | Bit 0 = LSION, Bit 1 = LSIRDY | LSI must be running before IWDG SR flags can clear |
| IWDG_KR | 0x40003000 | 0xCCCC = start, 0x5555 = unlock, 0xAAAA = kick | 0xCCCC must come before 0x5555 - order matters |
| IWDG_PR | 0x40003004 | 0x03 = prescaler /32 | 1 ms per tick at 32 kHz LSI |
| IWDG_RLR | 0x40003008 | 0xFA0 = 4000 | 4000 ms timeout - 8x safety margin at 500 ms kick rate |
Prototype phase: one file. Refactor phase: three layers. Phase 4: FreeRTOS wraps the loop in a single task. The Git history shows the full evolution - prototype, refactor, signal processing, RTOS. An interviewer can read every decision.
Sensors, drivers, signal processing, 3-layer refactor. Register-level, no HAL.
RCC_AHB2ENR - GPIOA_MODER - GPIOA_ODR. LD2 at 1Hz. No HAL involved anywhere.
PA2 - AF7. USART2 configured bare-metal. BRR address bug found and documented. Clean output in CoolTerm.
TMP117 at 0x49, MAX30102 at 0x57. Both responding on I2C1 bus on PB8 and PB9.
Device ID 0x0117 verified. Temperature register read. 23.5 C over UART. 0.0078125 C per LSB resolution.
Part ID 0x15 verified. HR mode configured. FIFO streaming. Values climb from ~700 ambient to 92000+ with finger contact.
Temperature, raw IR, and filtered IR in one loop. Circular buffer window=8. Filter smoothing and decay visible in terminal output.
Monolithic main.c split into driver, processing, and application layers. Zero errors on first compile after refactor. Git history preserves the full prototype-to-architecture evolution.
Hardware quality - calibrated timing, watchdog reliability.
SysTick configured for 1 ms tick. Calibrated delay_ms() replaces busy-wait loop. IWDG with 4-second timeout using LSI oscillator. LSI must be enabled and LSIRDY confirmed before IWDG config - got this wrong first, debugged with GDB.
Signal processing - BPM from raw PPG data.
Dynamic threshold peak detector on filtered IR signal. Threshold = 60% of rolling max. Beat interval from timestamp delta. BPM = 60000 / interval. Shows --- until two crossings confirmed, then live BPM. Verified 68-74 BPM at rest.
RTOS integration - FreeRTOS task scheduling verified on hardware.
ARM_CM4F port. FPU enabled before vTaskStartScheduler (CPACR write required). Single task wraps the sensor loop. vTaskDelay replaces delay_ms. IWDG kicked inside task before delay. Verified on hardware: TMP117 24.8 C, BPM 111, no resets in 8-minute run.