Embedded development has constraints that software development doesn’t: fixed memory, timing requirements, hardware dependencies, and limited debugging capability. Claude Code writes firmware that respects these constraints — properly sized RTOS stacks, interrupt-safe data structures, HAL-abstracted hardware drivers, and the patterns that prevent silent failures in production hardware.
CLAUDE.md for Embedded Projects
## Embedded Stack
- MCU: STM32H743 (Cortex-M7, 2MB Flash, 1MB RAM) or equivalent
- RTOS: FreeRTOS 11.x
- Hardware Abstraction: STM32 HAL (generated by CubeMX)
- Build: CMake + ARM GCC toolchain
- Debugging: OpenOCD + GDB, ITM tracing for printf-style debug
- Memory: heap_4 allocator; no dynamic allocation after init
- Stacks: always add 20% margin to empirically measured stack depth
- No blocking operations in ISRs — use queues/notifications to defer to tasks
- Critical sections: minimum duration — only protect shared data access
FreeRTOS Task Design
// firmware/src/tasks/sensor_task.c
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
// Statically allocated task and stack — no heap allocation at runtime
static StaticTask_t sensor_task_buffer;
static StackType_t sensor_task_stack[512]; // 512 * 4 = 2KB
// Queue handle — shared between ISR and task
static QueueHandle_t sensor_data_queue;
static StaticQueue_t sensor_queue_buffer;
static SensorReading_t sensor_queue_storage[8];
// Task function: processes sensor readings from the ISR-populated queue
void sensor_task(void *pvParameters) {
SensorReading_t reading;
// Initialize peripheral (safe to do in task context)
sensor_init();
for (;;) {
// Block on queue — yields CPU to other tasks while waiting
if (xQueueReceive(sensor_data_queue, &reading, pdMS_TO_TICKS(1000)) == pdTRUE) {
// Process reading — this can take as long as needed
process_sensor_reading(&reading);
// Notify data task that fresh data is available
xTaskNotify(data_task_handle, SENSOR_DATA_READY_BIT, eSetBits);
} else {
// Timeout: sensor not responding
handle_sensor_timeout();
}
}
}
// Call once during init — creates the task statically
void sensor_task_init(void) {
sensor_data_queue = xQueueCreateStatic(
8, // Queue depth
sizeof(SensorReading_t), // Item size
(uint8_t *)sensor_queue_storage,
&sensor_queue_buffer
);
configASSERT(sensor_data_queue != NULL);
xTaskCreateStatic(
sensor_task,
"SensorTask",
512, // Stack depth in words
NULL,
tskIDLE_PRIORITY + 2, // Priority — higher = runs first
sensor_task_stack,
&sensor_task_buffer
);
}
Interrupt Service Routine (ISR)
// firmware/src/drivers/uart_driver.c
// Principle: ISRs do minimal work — data goes into a queue, task does processing
#define UART_RX_BUFFER_SIZE 256
typedef struct {
uint8_t data[UART_RX_BUFFER_SIZE];
uint16_t length;
TickType_t timestamp;
} UartFrame;
static QueueHandle_t uart_rx_queue;
static uint8_t rx_buffer[UART_RX_BUFFER_SIZE];
static uint16_t rx_index = 0;
// DMA complete or IDLE line interrupt — called from ISR context
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
if (huart->Instance != USART1) return;
BaseType_t higher_prio_task_woken = pdFALSE;
UartFrame frame;
memcpy(frame.data, rx_buffer, rx_index);
frame.length = rx_index;
frame.timestamp = xTaskGetTickCountFromISR();
// Non-blocking send — if queue is full, drop the frame (don't block ISR)
xQueueSendFromISR(uart_rx_queue, &frame, &higher_prio_task_woken);
// Reset buffer for next frame
rx_index = 0;
HAL_UART_Receive_IT(huart, rx_buffer + rx_index, 1);
// Yield to higher-priority task if it was woken (critical for RTOS latency)
portYIELD_FROM_ISR(higher_prio_task_woken);
}
// ISR for IDLE line detection (end of UART frame)
void USART1_IRQHandler(void) {
if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE)) {
__HAL_UART_CLEAR_IDLEFLAG(&huart1);
uint16_t bytes_received = UART_RX_BUFFER_SIZE - huart1.hdmarx->Instance->NDTR;
if (bytes_received > 0) {
rx_index = bytes_received;
HAL_UART_RxCpltCallback(&huart1);
}
}
HAL_UART_IRQHandler(&huart1);
}
SPI Driver with Hardware Abstraction
// firmware/src/drivers/display_driver.c — SPI display driver
#include "spi_hal.h"
#define DISPLAY_SPI_TIMEOUT_MS 10
#define DISPLAY_CS_PIN GPIO_PIN_4
#define DISPLAY_CS_PORT GPIOA
#define DISPLAY_DC_PIN GPIO_PIN_5
#define DISPLAY_DC_PORT GPIOA
typedef struct {
SPI_HandleTypeDef *hspi;
GPIO_TypeDef *cs_port;
uint16_t cs_pin;
GPIO_TypeDef *dc_port;
uint16_t dc_pin;
} DisplayHandle;
static DisplayHandle display;
HAL_StatusTypeDef display_write_command(uint8_t cmd) {
HAL_GPIO_WritePin(display.dc_port, display.dc_pin, GPIO_PIN_RESET); // DC=0 -> command
HAL_GPIO_WritePin(display.cs_port, display.cs_pin, GPIO_PIN_RESET); // CS active low
HAL_StatusTypeDef status = HAL_SPI_Transmit(
display.hspi, &cmd, 1, DISPLAY_SPI_TIMEOUT_MS
);
HAL_GPIO_WritePin(display.cs_port, display.cs_pin, GPIO_PIN_SET);
return status;
}
HAL_StatusTypeDef display_write_data(const uint8_t *data, uint16_t length) {
HAL_GPIO_WritePin(display.dc_port, display.dc_pin, GPIO_PIN_SET); // DC=1 -> data
HAL_GPIO_WritePin(display.cs_port, display.cs_pin, GPIO_PIN_RESET);
// DMA transfer for large data — doesn't block CPU
HAL_StatusTypeDef status = HAL_SPI_Transmit_DMA(display.hspi, (uint8_t *)data, length);
// Wait for DMA complete notification (not busy-wait)
if (xSemaphoreTake(spi_dma_complete_semaphore, pdMS_TO_TICKS(100)) != pdTRUE) {
HAL_GPIO_WritePin(display.cs_port, display.cs_pin, GPIO_PIN_SET);
return HAL_TIMEOUT;
}
HAL_GPIO_WritePin(display.cs_port, display.cs_pin, GPIO_PIN_SET);
return HAL_OK;
}
Ring Buffer (Lock-Free for Single Producer/Consumer)
// firmware/src/lib/ring_buffer.h — lock-free for ISR producer, task consumer
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#include <stdatomic.h>
#define RING_BUFFER_SIZE 256 // Must be power of 2
typedef struct {
uint8_t buffer[RING_BUFFER_SIZE];
atomic_uint_fast16_t read_index;
atomic_uint_fast16_t write_index;
} RingBuffer;
// Called from ISR: producer
bool ring_buffer_write(RingBuffer *rb, uint8_t byte) {
uint_fast16_t write = atomic_load_explicit(&rb->write_index, memory_order_relaxed);
uint_fast16_t read = atomic_load_explicit(&rb->read_index, memory_order_acquire);
uint_fast16_t next_write = (write + 1) & (RING_BUFFER_SIZE - 1);
if (next_write == read) return false; // Full
rb->buffer[write] = byte;
atomic_store_explicit(&rb->write_index, next_write, memory_order_release);
return true;
}
// Called from task: consumer
bool ring_buffer_read(RingBuffer *rb, uint8_t *byte) {
uint_fast16_t write = atomic_load_explicit(&rb->write_index, memory_order_acquire);
uint_fast16_t read = atomic_load_explicit(&rb->read_index, memory_order_relaxed);
if (read == write) return false; // Empty
*byte = rb->buffer[read];
atomic_store_explicit(&rb->read_index, (read + 1) & (RING_BUFFER_SIZE - 1), memory_order_release);
return true;
}
Stack Usage Analysis
// firmware/src/debug/stack_monitor.c — detect stack overflow before it happens
void check_task_stacks(void) {
TaskStatus_t task_status_array[16];
volatile UBaseType_t task_count;
task_count = uxTaskGetNumberOfTasks();
if (uxTaskGetSystemState(task_status_array, task_count, NULL) == task_count) {
for (UBaseType_t i = 0; i < task_count; i++) {
uint32_t stack_high_water_mark = task_status_array[i].usStackHighWaterMark;
// Alert if less than 64 words (256 bytes) remaining
if (stack_high_water_mark < 64) {
ITM_SendString("[WARN] Task '");
ITM_SendString(task_status_array[i].pcTaskName);
ITM_SendChar('\'');
ITM_SendString(" stack nearly full! HWM=");
ITM_SendUint32(stack_high_water_mark);
ITM_SendString(" words\n");
}
}
}
}
// FreeRTOS hook: called on stack overflow (enable via configCHECK_FOR_STACK_OVERFLOW=2)
void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) {
// Can't use printf/UART here (undefined state) — trigger debugger or reset
(void)xTask;
(void)pcTaskName;
__disable_irq();
while (1) {
// Halt for debugger — will show task name in xTask
__BKPT(0);
}
}
For the Rust embedded development as an alternative to C firmware, the Rust async guide covers the async runtime patterns that apply to Embassy (Rust embedded framework). For the supply chain security considerations for firmware dependencies, the supply chain security guide covers SBOM generation applicable to embedded projects. The Claude Skills 360 bundle includes embedded systems skill sets covering FreeRTOS patterns, HAL drivers, and interrupt-safe data structures. Start with the free tier to try firmware pattern generation.