--- name: embedded-stm32 description: "Best practices for embedded C/C++ development on STM32 microcontrollers using the HAL, covering peripherals, DMA, interrupts, memory constraints, and hardware-focused testing. Use when writing STM32 HAL code, configuring peripherals generated by STM32CubeMX, working with interrupts or DMA, debugging with SWD/JTAG, managing memory-constrained firmware, or writing hardware-in-the-loop or host-build tests for embedded C." --- # Embedded STM32 / HAL Development This skill covers firmware development for STM32 microcontrollers using the STM32 HAL, including project structure, peripheral and interrupt handling, memory and timing constraints, and testing strategies for hardware-focused code. ## Workflow for STM32 HAL Firmware Development 1. **Configure the hardware in CubeMX** — Set up clocks, pins, and peripherals in the `.ioc` file; generate the HAL initialization code. 2. **Separate generated and hand-written code** — Keep CubeMX-generated files untouched except in their designated `USER CODE BEGIN/END` blocks; put application logic in separate files. 3. **Initialize peripherals once** — Centralize `HAL_*_Init()` calls in `main()`/`MX_*_Init()` and avoid ad hoc reconfiguration elsewhere in the code. 4. **Write interrupt handlers** — Keep ISRs (`HAL_*_Callback` functions, `EXTI`/`DMA`/timer IRQ handlers) short; set flags or push to a queue and defer real work to the main loop or an RTOS task. 5. **Use DMA for high-throughput I/O** — Configure DMA for UART/SPI/I2C/ADC transfers that would otherwise block or burn CPU cycles on polling. 6. **Add timeouts everywhere** — Every blocking HAL call and every hardware wait loop needs a timeout and an explicit error path. 7. **Test in layers** — Unit-test pure logic on a host build (no hardware dependency), then validate peripheral behavior with hardware-in-the-loop tests. 8. **Flash and debug** — Use SWD/JTAG (ST-Link, OpenOCD, or J-Link) with a debugger, plus rate-limited serial logs, to verify behavior on real hardware. ## Project Structure - Keep board support (pin/clock configuration), drivers, middleware (e.g., FreeRTOS, USB stack), application logic, and tests in clearly separated directories. - Isolate CubeMX-generated or vendor code (`Core/Src/main.c`, `Drivers/`) from hand-written application code so regenerating with CubeMX doesn't clobber custom logic — only edit inside `/* USER CODE BEGIN */ ... /* USER CODE END */` markers in generated files. - Put hardware access behind narrow interfaces (e.g., a `motor_driver.h` with `motor_set_speed()`) so application logic can be unit-tested on a host build without real peripherals. - Document the clock tree (`SYSCLK`, `HCLK`, `PCLK1`/`PCLK2` and their max rates), pin mappings, peripheral ownership, and interrupt priority assignments in a single reference (README or header comments) — this is the first thing a debugging session needs. ## STM32 HAL and Peripherals - Initialize each peripheral in exactly one place; avoid scattering `HAL_*_Init()`/`HAL_*_MspInit()` calls or ad hoc register writes across multiple files. - Always check the return value of HAL calls (`HAL_OK`, `HAL_ERROR`, `HAL_BUSY`, `HAL_TIMEOUT`) and handle timeout/error cases explicitly — a silently ignored `HAL_TIMEOUT` from `HAL_UART_Transmit` is a classic source of "it works on my desk" bugs. - Keep blocking HAL calls (`HAL_UART_Transmit`, `HAL_I2C_Master_Receive` without `_IT`/`_DMA` suffix) out of time-critical paths like control loops or ISRs. - Use DMA for high-throughput UART, SPI, I2C, ADC, or timer-capture paths when the CPU shouldn't spend cycles byte-shuffling. - Document buffer ownership and lifetime for every DMA operation — a buffer being read by DMA must not be modified or freed by the CPU until the transfer-complete callback fires. - Use `volatile` only for memory shared with an ISR or memory-mapped hardware registers; `volatile` is not a substitute for a proper memory barrier or critical section when data is shared between contexts. ### Example: Non-Blocking UART Receive with DMA and Idle-Line Detection ```c /* USER CODE BEGIN Includes */ #include "main.h" #include #define RX_BUF_SIZE 128 static uint8_t rx_buf[RX_BUF_SIZE]; static volatile uint8_t rx_ready = 0; static volatile uint16_t rx_len = 0; extern UART_HandleTypeDef huart2; extern DMA_HandleTypeDef hdma_usart2_rx; /* USER CODE END Includes */ /* USER CODE BEGIN 2 */ void app_uart_start_receive(void) { /* Enable idle-line interrupt so a packet of unknown length completes * the transfer without waiting for the buffer to fill. */ __HAL_UART_ENABLE_IT(&huart2, UART_IT_IDLE); if (HAL_UART_Receive_DMA(&huart2, rx_buf, RX_BUF_SIZE) != HAL_OK) { Error_Handler(); } } /* USER CODE END 2 */ /* USER CODE BEGIN 4 */ void USART2_IRQHandler(void) { if (__HAL_UART_GET_FLAG(&huart2, UART_FLAG_IDLE)) { __HAL_UART_CLEAR_IDLEFLAG(&huart2); HAL_UART_DMAStop(&huart2); rx_len = RX_BUF_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart2_rx); rx_ready = 1; /* Deferred: main loop processes the packet. */ /* Re-arm for the next packet. */ HAL_UART_Receive_DMA(&huart2, rx_buf, RX_BUF_SIZE); return; } HAL_UART_IRQHandler(&huart2); } /* USER CODE END 4 */ /* Main loop excerpt: heavy work deferred out of the ISR. */ void app_main_loop(void) { if (rx_ready) { rx_ready = 0; uint16_t len = rx_len; /* Copy out or parse rx_buf[0..len) here. Do NOT touch rx_buf * again until this point, since DMA may already be refilling it. */ (void)len; } } ``` ## Interrupts and Concurrency - Keep ISRs short and deterministic — set a flag, copy a small fixed-size value, or push to a lock-free queue, then return. - Defer heavy work (parsing, computation, logging) from interrupts to the main loop, an RTOS task, or an event queue processed outside interrupt context. - Protect data shared between an ISR and the main context with critical sections (`__disable_irq()`/`__enable_irq()`, or `taskENTER_CRITICAL()` under an RTOS), atomics, or lock-free queues — never assume a multi-byte read/write is atomic. - Avoid dynamic allocation (`malloc`/`new`) inside interrupt handlers; allocation is neither deterministic nor guaranteed reentrant-safe. - Make interrupt priority decisions explicit and documented (`NVIC_SetPriority`) — a mis-prioritized interrupt can starve time-critical peripherals or violate FreeRTOS's `configMAX_SYSCALL_INTERRUPT_PRIORITY` constraint. ## Memory and Timing - Avoid heap allocation in firmware unless the project explicitly allows and budgets for it — prefer static allocation and fixed-size buffers/pools. - Check stack usage for both ISRs and RTOS tasks (link-time stack usage reports, or `uxTaskGetStackHighWaterMark()` under FreeRTOS) — stack overflow on embedded targets typically corrupts silently. - Keep lookup tables `const` so the linker places them in flash instead of consuming scarce RAM. - Use fixed-width integer types (`uint8_t`, `int32_t`, `uint32_t`) for anything hardware-facing (register values, protocol fields, buffer sizes) instead of `int`/`long`, whose width isn't guaranteed. - Add a timeout to every hardware wait — polling a status flag with no bound will hang forever if the hardware never sets it (a common outcome of a misconfigured clock or a disconnected peripheral). - Treat the independent/window watchdog as part of application design from day one, not a late add-on — decide the refresh strategy before writing the main loop, not after a field failure. ## Testing and Debugging - Unit test pure logic (protocol parsing, state machines, math) on a host build (native gcc/clang) with the hardware layer mocked or stubbed out behind the narrow interfaces from the project structure. - Use hardware-in-the-loop tests for actual peripheral behavior (timing, electrical signaling, real sensor data) that a host build can't exercise. - Add assertions (`assert()` or a custom `configASSERT`-style macro) for impossible hardware states in debug builds, compiled out in release builds if code size is tight. - Use SWD/JTAG (ST-Link/V2, OpenOCD, J-Link) for live debugging, a logic analyzer for signal-level issues, and serial logs with rate limiting (never flood a UART inside a tight loop or ISR). - Keep fault handlers (`HardFault_Handler`, etc.) useful: capture the reset reason (`RCC->CSR`), relevant fault status registers (`SCB->CFSR`, `SCB->HFSR`), and firmware build version/hash so a field crash is diagnosable after the fact. ## Common Mistakes - Modifying CubeMX-generated files outside `USER CODE` blocks, so the next regeneration silently deletes the changes. - Busy-waiting forever on a hardware status flag with no timeout, hanging the firmware on any hardware anomaly. - Sharing a buffer between DMA and the CPU without synchronization (cache invalidation on cores with a data cache, or simply reading before the transfer-complete flag/callback fires). - Assuming a peripheral's register state is unchanged after waking from a low-power mode (Stop/Standby) — many peripherals require re-initialization after these modes.