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.
Scanned 9/7/2026
Install to Claude Code
npx -y skills add Mindrally/skills --skill embedded-stm32 --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Embedded Stm32?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/mindrally-embedded-stm32)More formats (shields.io, HTML) on the badges page.
---
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.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!