---
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 <string.h>

#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.
