| name | embedded |
| description | Develop embedded systems firmware and hardware interfaces with Claude Code |
Embedded Systems
When to activate
Writing firmware for microcontrollers (STM32, ESP32, nRF52, RP2040), designing FreeRTOS task architectures, implementing HAL peripheral drivers, writing interrupt service routines, configuring DMA transfers, optimizing code for memory-constrained devices, or debugging timing-sensitive issues in bare-metal or RTOS environments.
When NOT to use
Linux-based embedded systems (Raspberry Pi, Yocto) where standard Linux programming applies. High-level IoT connectivity without firmware concerns (use iot.md). FPGA HDL design. General C/C++ application development on desktop hardware. Scripting for embedded Linux devices where Python/shell is appropriate.
Instructions
Memory Layout
Understanding the linker sections is essential for debugging hard faults and sizing firmware:
Flash (read-only, persistent):
.text — compiled machine code
.rodata — read-only constants (const char*, lookup tables)
.data — initial values for initialized globals (copied to RAM at startup)
RAM (read-write, volatile):
.data — initialized globals (copied from flash at startup)
.bss — uninitialized globals (zero-filled at startup)
.heap — grows upward (malloc region)
[gap]
.stack — grows downward from top of RAM
Hard fault on cortex-M → check PSP/MSP for stack overflow into heap
Calculate stack usage with FreeRTOS uxTaskGetStackHighWaterMark() to find the minimum stack headroom observed since task creation. Set task stacks 20-30% larger than measured peak.
Check firmware size before flashing:
arm-none-eabi-size build/firmware.elf
volatile for Hardware Registers
The compiler is allowed to cache register reads in CPU registers and eliminate repeated loads it deems "redundant." volatile prevents this:
while (!(USART1->SR & USART_SR_TXE));
while (!(USART1->SR & USART_SR_TXE));
volatile bool g_data_ready = false;
void USART1_IRQHandler(void) {
g_data_ready = true;
}
void main_loop(void) {
if (g_data_ready) {
process_data();
g_data_ready = false;
}
}
For atomic access to multi-byte variables shared with ISRs, disable interrupts around the critical section:
uint32_t read_counter_atomic(void) {
__disable_irq();
uint32_t val = g_counter;
__enable_irq();
return val;
}
ISR Constraints
ISRs run with preemption disabled (or at elevated priority on Cortex-M). Violations cause priority inversion, missed deadlines, and hard faults:
- No blocking calls (
vTaskDelay, HAL_Delay, osDelay)
- No dynamic memory allocation (
malloc, pvPortMalloc — can deadlock if heap mutex is held)
- No UART
printf (internally uses HAL blocking delays)
- No FreeRTOS
xQueueSend — use xQueueSendFromISR with BaseType_t pxHigherPriorityTaskWoken
- Target ISR execution time < 1 µs for high-frequency interrupts; < 10 µs as a general rule
static QueueHandle_t g_sensor_queue;
void SPI1_IRQHandler(void) {
BaseType_t higher_prio_task_woken = pdFALSE;
SensorReading reading = read_dma_buffer();
xQueueSendFromISR(g_sensor_queue, &reading, &higher_prio_task_woken);
portYIELD_FROM_ISR(higher_prio_task_woken);
}
FreeRTOS Patterns
Task priorities: higher number = higher priority on most ports. Assign carefully:
Priority 5 (highest) — critical real-time tasks (motor control, safety)
Priority 4 — sensor acquisition
Priority 3 — data processing
Priority 2 — communication (UART, BLE, WiFi)
Priority 1 — logging, housekeeping
Priority 0 — idle task (never block, never starve)
Queue for ISR-to-task data transfer:
void sensor_task(void *params) {
SensorReading reading;
for (;;) {
if (xQueueReceive(g_sensor_queue, &reading, pdMS_TO_TICKS(100)) == pdTRUE) {
process_reading(&reading);
} else {
log_warn("Sensor timeout");
}
}
}
Mutex for shared resource protection:
static SemaphoreHandle_t g_spi_mutex;
bool spi_transceive(uint8_t *tx, uint8_t *rx, size_t len) {
if (xSemaphoreTake(g_spi_mutex, pdMS_TO_TICKS(50)) != pdTRUE) {
return false;
}
HAL_SPI_TransmitReceive(&hspi1, tx, rx, len, HAL_MAX_DELAY);
xSemaphoreGive(g_spi_mutex);
return true;
}
Never call vTaskDelay(0) in a loop to yield — use taskYIELD() for an explicit yield without a time delay.
DMA for High-Throughput Peripherals
DMA transfers data between peripheral and memory without CPU involvement. Use for SPI, UART, ADC when transfer size > 4 bytes:
void adc_dma_init(void) {
hadc1.Init.ContinuousConvMode = ENABLE;
hadc1.Init.DMAContinuousRequests = ENABLE;
HAL_ADC_Init(&hadc1);
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)g_adc_buffer, ADC_BUFFER_SIZE);
}
void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc) {
process_adc_samples(g_adc_buffer, ADC_BUFFER_SIZE / 2);
}
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) {
process_adc_samples(&g_adc_buffer[ADC_BUFFER_SIZE / 2], ADC_BUFFER_SIZE / 2);
}
SPI DMA for display or sensor burst reads — start transfer, yield task, resume in callback:
static SemaphoreHandle_t g_spi_dma_done;
void spi_dma_send(uint8_t *data, size_t len) {
HAL_SPI_Transmit_DMA(&hspi1, data, len);
xSemaphoreTake(g_spi_dma_done, portMAX_DELAY);
}
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) {
BaseType_t woken = pdFALSE;
xSemaphoreGiveFromISR(g_spi_dma_done, &woken);
portYIELD_FROM_ISR(woken);
}
Fixed-Point Arithmetic
Avoid floating-point on MCUs without FPU (Cortex-M0, M0+). Q16.16 format: upper 16 bits = integer part, lower 16 bits = fractional part.
typedef int32_t fixed16_t;
#define FLOAT_TO_FIXED(x) ((fixed16_t)((x) * 65536.0f))
#define FIXED_TO_FLOAT(x) ((float)(x) / 65536.0f)
#define FIXED_MUL(a, b) ((fixed16_t)(((int64_t)(a) * (b)) >> 16))
#define FIXED_DIV(a, b) ((fixed16_t)(((int64_t)(a) << 16) / (b)))
fixed16_t pid_update(PIDController *pid, fixed16_t setpoint, fixed16_t measured) {
fixed16_t error = setpoint - measured;
pid->integral += error;
fixed16_t derivative = error - pid->prev_error;
pid->prev_error = error;
return FIXED_MUL(pid->kp, error)
+ FIXED_MUL(pid->ki, pid->integral)
+ FIXED_MUL(pid->kd, derivative);
}
Watchdog Timer
The watchdog resets the MCU if the main loop or critical task stops feeding it — essential for unattended deployments:
void watchdog_init(void) {
hiwdg.Instance = IWDG;
hiwdg.Init.Prescaler = IWDG_PRESCALER_64;
hiwdg.Init.Reload = 2000;
HAL_IWDG_Init(&hiwdg);
}
void watchdog_feed(void) {
HAL_IWDG_Refresh(&hiwdg);
}
void watchdog_task(void *params) {
for (;;) {
xEventGroupWaitBits(g_alive_bits, ALL_TASKS_ALIVE_MASK,
pdTRUE, pdTRUE, pdMS_TO_TICKS(2000));
watchdog_feed();
}
}
Example
Design FreeRTOS firmware for STM32F4 reading temperature/pressure from BMP388 via SPI DMA, processing in a task, outputting formatted readings over UART:
- SPI DMA:
spi_dma_send and spi_dma_receive use DMA with a semaphore signaled in HAL_SPI_TxRxCpltCallback. Sensor driver calls these — zero CPU cycles during transfer.
- ISR → task:
EXTI interrupt fires on BMP388 data-ready pin. ISR calls xQueueSendFromISR with a trigger token into g_sensor_queue. No processing in ISR.
- Sensor task (priority 4): blocks on
xQueueReceive(g_sensor_queue). On trigger, initiates SPI DMA read, blocks on DMA semaphore, converts raw ADC values using fixed-point Q16.16 math (no FPU needed on M4 in this config for portability), posts result to g_result_queue.
- UART task (priority 2): blocks on
g_result_queue, formats reading as binary frame, calls HAL_UART_Transmit_DMA.
- Watchdog task (priority 5): waits on event group bits set by sensor and UART tasks each cycle. If both bits are not set within 2s, the IWDG resets the device. Stack high water mark checked in debug builds and logged over UART at boot.