STM32 HAL and CubeMX skill for Codex
Regenerate the .ioc without losing a line. Codex treats the CubeMX .ioc as the source of truth, keeps application code in USER CODE blocks and separate files, and checks every HAL return value with finite timeouts. It knows the I2C address shift, idle-line DMA for UART, Cortex-M7 cache maintenance and DTCM limits, and asks before erasing chips or touching read-out protection.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- stm32-hal Installs to ~/.agents/skills/stm32-hal/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install stm32-hal The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project stm32-hal Installs to .agents/skills/stm32-hal/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/stm32-hal.zipunzip -o stm32-hal.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) stm32-hal PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $stm32-hal, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Develops STM32 firmware with STM32CubeMX and the HAL/LL drivers - .ioc regeneration without losing code in USER CODE blocks, CMake or STM32CubeIDE builds, UART receive-to-idle DMA, I2C/SPI with timeouts, timers and EXTI callbacks, clock tree, printf retargeting, flashing with STM32_Programmer_CLI or OpenOCD, and HardFault decoding. Use when an STM32 peripheral returns HAL_ERROR or HAL_BUSY, UART drops bytes, regeneration wiped code, an H7 DMA reads stale data, or setting up a command-line build and flash for a Cube project.
Use it when
- An STM32 HAL call returns HAL_ERROR, HAL_BUSY or times out
- Regenerating from CubeMX keeps wiping application code
- UART, SPI or ADC data is dropped or stale, especially with DMA on F7/H7
- A HardFault needs tracing back to a line of code
Use something else when
- The STM32 project runs Zephyr rather than Cube HAL: use zephyr-rtos
- You are writing async Rust for STM32: use embedded-rust
How to check the result
Regenerate from the .ioc and diff to prove no user code was lost, then run a peak-rate transfer test and confirm zero dropped bytes in the device log.
The mistake it prevents
Passing a 7-bit I2C address to HAL without shifting it left by one, so every transfer NACKs.
Done well means: the .ioc file is the single source for pins and clocks and can be
regenerated at any time without losing code, every HAL call’s return value is handled,
high-rate I/O runs on DMA or interrupts rather than polling, and the project builds and
flashes from the command line.
Inspect first
- Read
AGENTS.md, then open the.iocas text: MCU part (Mcu.UserName), CubeMX version, firmware package version (ProjectManager.FirmwarePackage), toolchain (ProjectManager.TargetToolchain). - Note the build:
CMakeLists.txtpluscmake/stm32cubemx/(CubeMX 6.11+ CMake export),.cproject(STM32CubeIDE) or aMakefile. - Find where application code lives. Anything outside
/* USER CODE BEGIN ... */and/* USER CODE END ... */in generated files is overwritten on regeneration.
cmake --preset Debug && cmake --build --preset Debug # CubeMX CMake projects
arm-none-eabi-size build/Debug/app.elf
The Arm GNU toolchain and STM32CubeCLT must be installed on the host; installing them needs network and admin rights, so ask rather than attempting it in the sandbox.
Project structure that survives regeneration
- Enable “Generate peripheral initialization as a pair of .c/.h files per peripheral” in
CubeMX so
main.cstays small. - Put application code in
App/(orCore/Src/app_*.c) and call it from one USER CODE block inmain.c:app_init();before the loop,app_run();inside it. - Never fix a generated file outside a USER CODE block. If a generated setting is wrong,
change the
.iocand regenerate. Commit the.ioctogether with generated output. - Changing pins or clocks: edit in CubeMX, regenerate, then
git diffthe generated files to confirm only the expected peripherals changed.
HAL call discipline
HAL_StatusTypeDef st = HAL_I2C_Mem_Read(&hi2c1, BME280_ADDR << 1, 0xD0,
I2C_MEMADD_SIZE_8BIT, &id, 1, 50);
if (st != HAL_OK) {
log_err("i2c read %d err 0x%lx", st, HAL_I2C_GetError(&hi2c1));
i2c_recover(&hi2c1); /* deinit/init, toggle SCL if bus stuck */
return st;
}
- HAL I2C takes the 7-bit address shifted left by one. Passing the raw 7-bit address is the most common “device not responding” bug.
- Always pass a finite timeout in milliseconds;
HAL_MAX_DELAYhangs forever on a stuck bus. HAL_BUSYusually means a previous IT/DMA transfer on the same handle has not finished, or an error left the handle state stuck. Checkhuart->gState/RxStateand the error callback.- HAL callbacks (
HAL_UART_RxCpltCallback,HAL_GPIO_EXTI_Callback, …) are weak functions called from interrupt context. Keep them short and defer work.
UART receive: idle-line DMA
Use ReceiveToIdle with DMA for variable-length input; polling and byte-by-byte interrupts
drop data at higher baud rates.
#define RX_SZ 256
static uint8_t rx_buf[RX_SZ];
void uart_start(void) {
HAL_UARTEx_ReceiveToIdle_DMA(&huart2, rx_buf, RX_SZ);
__HAL_DMA_DISABLE_IT(huart2.hdmarx, DMA_IT_HT); /* no half-transfer events */
}
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t size) {
if (huart->Instance == USART2) {
rx_push(rx_buf, size); /* copy out or hand to a task */
HAL_UARTEx_ReceiveToIdle_DMA(&huart2, rx_buf, RX_SZ);
__HAL_DMA_DISABLE_IT(huart2.hdmarx, DMA_IT_HT);
}
}
void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart) {
uart_start(); /* overrun/framing: restart */
}
For continuous high-rate streams use circular DMA and track the write position from
size across callbacks instead of restarting.
Cache and DMA on Cortex-M7 (F7, H7)
- DMA does not see the D-cache. Before a DMA transmit, clean the buffer
(
SCB_CleanDCache_by_Addr); after a DMA receive, invalidate it (SCB_InvalidateDCache_by_Addr). Buffers must be 32-byte aligned and sized in multiples of 32 or the invalidate corrupts neighbours. - Or place DMA buffers in a non-cacheable region configured by the MPU.
- On H7, DMA1/DMA2 cannot reach DTCM RAM (0x20000000). Put DMA buffers in AXI SRAM or SRAM1-3 with a linker section, or transfers silently do nothing.
Timebase, clocks and RTOS
- With FreeRTOS, CubeMX asks to move the HAL timebase off SysTick; pick a basic timer (TIM6/TIM7). SysTick belongs to the kernel.
- Clock tree changes affect UART baud, timer periods and ADC sampling. After changing
SystemClock_Config, recheck every timer prescaler and the actual baud with a scope or logic analyser. - Set
NVIC_PRIORITYGROUP_4and keep ISRs that call RTOS APIs at or below the RTOS syscall priority (freertoscovers the rule).
printf over UART or SWO
int _write(int fd, char *ptr, int len) {
(void)fd;
HAL_UART_Transmit(&huart2, (uint8_t *)ptr, (uint16_t)len, 100);
return len;
}
Float formatting with newlib-nano needs -u _printf_float at link time and costs several
KB of flash. For high-rate logs prefer SWO/ITM or RTT over a blocking UART.
Flashing and debugging
STM32_Programmer_CLI -c port=SWD -w build/Debug/app.elf -v -rst
openocd -f interface/stlink.cfg -f target/stm32f4x.cfg \
-c "program build/Debug/app.elf verify reset exit"
Hardware is usually attached to the user’s machine, not the Codex sandbox: give the exact command and ask for its output.
Ask for explicit confirmation before any of these, and state the consequence:
- Full chip erase (
-e all): wipes calibration or data stored in flash. - Option bytes changes, especially read-out protection. RDP level 1 back to 0 mass-erases the chip; RDP level 2 is permanent and disables debug forever.
- Writing to the system memory or OTP areas.
HardFault triage
- In the debugger read
SCB->CFSR(0xE000ED28),HFSR(0xE000ED2C),MMFAR,BFAR. - Recover the stacked PC from the exception frame (MSP or PSP depending on bit 2 of LR)
and map it with
arm-none-eabi-addr2line -e app.elf 0x0800xxxx.
| CFSR bit | Typical cause |
|---|---|
PRECISERR with valid BFAR | Access to a disabled peripheral (clock not enabled) or bad pointer |
IMPRECISERR | Buffered write fault; set DISDEFWBUF temporarily to make it precise |
UNALIGNED / INVSTATE | Unaligned access with trapping on, or call through a bad function pointer |
STKERR / MSTKERR | Stack overflow during exception entry |
Verification
- Regenerate from
.ioc;git diffshows no lost user code - Command-line build clean at the release optimisation level
- Every HAL call result checked; error callbacks restart the peripheral
- Peak-rate test (max baud, back-to-back frames) without dropped bytes, from user logs
- On M7 parts, DMA buffers aligned and cache-maintained or in non-cacheable RAM
Report
**MCU:** STM32H743ZI, CubeMX 6.13, STM32CubeH7 1.12, CMake + Ninja
**Root cause:** UART RX buffer in DTCM, unreachable by DMA1; data stayed zero
**Change:** RX buffer moved to .dma_buffer in RAM_D2 (32-byte aligned); ReceiveToIdle DMA
**Verified:** 921600 baud, 10 min burst test, 0 dropped frames (user-provided log) What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "STM32 HAL and CubeMX"
short_description: "Regenerate the .ioc without losing a line"
default_prompt: "$stm32-hal HAL_I2C_Mem_Read keeps returning HAL_ERROR on my STM32, debug it" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.