UART · verified TMP117 · 24.8 C MAX30102 · IR streaming IWDG · 4s watchdog BPM · 111 verified FreeRTOS V10.5.1

STM32
Vitals
Monitor

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.

I2C sensor drivers moving average filter BPM from PPG IWDG watchdog FreeRTOS V10.5.1 3-layer architecture MISRA-C analysed hardware verified
MCU
STM32L476RG
Core
Cortex-M4
Clock
4 MHz MSI
Sensors
TMP117 + MAX30102
HAL
None
RTOS
FreeRTOS V10.5.1
View source → Build log

Verified output

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.

CoolTerm - COM7 · 9600 8-N-1 · Phase 1-3 3-layer refactor · filter decay visible
STM32 Vitals Monitor
TMP117 OK
MAX30102 OK
Temp(C) | IR raw | IR filt
--------+---------+--------
23.4 | 719 | 719
23.4 | 712 | 715
// finger on sensor - raw jumps immediately, filter ramps over 8 rows
23.5 | 88991 | 11752
23.5 | 90863 | 34064
23.5 | 92885 | 55903
23.5 | 92344 | 89113
// finger removed - filter decays smoothly over 8 rows
23.5 | 730 | 78074
23.5 | 714 | 23649
23.5 | 715 | 719
+ BPM column + FreeRTOS V10.5.1 + IWDG kick every 500ms + vTaskDelay replaces delay_ms
CoolTerm - COM7 · 9600 8-N-1 · Phase 4 FreeRTOS V10.5.1 · BPM verified · April 2026
STM32 Vitals Monitor
TMP117 OK
MAX30102 OK
Temp(C) | IR raw | IR filt | BPM
--------+---------+---------+----
24.8 | 860 | 860 | ---
24.8 | 857 | 858 | --- // BPM shows --- until 2 crossings confirmed
24.8 | 95865 | 27233 | --- // finger on sensor
24.8 | 96977 | 51250 | 111
24.8 | 97259 | 75353 | 111
24.8 | 97398 | 93215 | 111
Bug #1 - wrong register address

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.

Bug #2 - IWDG hung before main() printed anything

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.

Register map

Every address cross-referenced to RM0351. No guessing.

Register Address Bits used Note
RCC_AHB2ENR0x4002104CBit 0 = GPIOA, Bit 1 = GPIOBClock enable before any peripheral access
RCC_APB1ENR10x40021058Bit 17 = USART2, Bit 21 = I2C1
GPIOA_MODER0x48000000Bits 5:4 = PA2, Bits 11:10 = PA510 = AF mode, 01 = output
GPIOA_AFRL0x48000020Bits 11:8 = PA2 → AF7AF7 = USART2_TX
USART2_CR10x40004400Bit 0 = UE, Bit 3 = TESet TE before UE
USART2_BRR ⚑ 0x4000440C BRR = 417 for 9600 @ 4MHz Offset 0x0C, not 0x08 (that is CR3)
USART2_ISR0x4000441CBit 7 = TXEWait for 1 before each TDR write
USART2_TDR0x40004428Write byte to transmit
I2C1_TIMINGR0x400054100x00100D14100kHz standard mode at 4MHz
RCC_CSR0x40021094Bit 0 = LSION, Bit 1 = LSIRDYLSI must be running before IWDG SR flags can clear
IWDG_KR0x400030000xCCCC = start, 0x5555 = unlock, 0xAAAA = kick0xCCCC must come before 0x5555 - order matters
IWDG_PR0x400030040x03 = prescaler /321 ms per tick at 32 kHz LSI
IWDG_RLR0x400030080xFA0 = 40004000 ms timeout - 8x safety margin at 500 ms kick rate

Architecture

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.

Application
FreeRTOS task_vitals wraps the sensor loop. vTaskDelay owns timing. IWDG kicked every 500 ms. No register access.
main.c
↓ calls only layer below
Processing
Moving average filter - circular buffer, O(1) per sample, no malloc. BPM detection from threshold crossings on filtered PPG.
filter.c · bpm.c
↓ calls only layer below
Driver
Register writes only. UART, I2C, TMP117, MAX30102, IWDG. Nothing leaks upward.
uart · i2c · tmp117 · max30102 · iwdg

Build log

PHASE 1

Sensors, drivers, signal processing, 3-layer refactor. Register-level, no HAL.

✓ DONE
LED blink - first register write

RCC_AHB2ENR - GPIOA_MODER - GPIOA_ODR. LD2 at 1Hz. No HAL involved anywhere.

✓ DONE
UART - COM7 · 9600 baud

PA2 - AF7. USART2 configured bare-metal. BRR address bug found and documented. Clean output in CoolTerm.

✓ DONE
I2C scanner - both devices found

TMP117 at 0x49, MAX30102 at 0x57. Both responding on I2C1 bus on PB8 and PB9.

✓ DONE
TMP117 temperature driver

Device ID 0x0117 verified. Temperature register read. 23.5 C over UART. 0.0078125 C per LSB resolution.

✓ DONE
MAX30102 driver + raw IR data

Part ID 0x15 verified. HR mode configured. FIFO streaming. Values climb from ~700 ambient to 92000+ with finger contact.

✓ DONE
Both sensors combined + moving average filter

Temperature, raw IR, and filtered IR in one loop. Circular buffer window=8. Filter smoothing and decay visible in terminal output.

✓ DONE
Refactor to 3-layer architecture

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.

PHASE 2

Hardware quality - calibrated timing, watchdog reliability.

✓ DONE
SysTick calibrated delay + IWDG watchdog

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.

PHASE 3

Signal processing - BPM from raw PPG data.

✓ DONE
BPM detection from PPG signal

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.

PHASE 4

RTOS integration - FreeRTOS task scheduling verified on hardware.

✓ DONE
FreeRTOS V10.5.1 task scheduling

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.