| name | embedded-c-guidelines |
| description | C99 embedded firmware — HAL Ops Table pattern, FreeRTOS static tasks/queues/semaphores, ISR-safe ring buffer, no-malloc policy, Unity test with mock drivers, CMake HOST/STM32 dual-build. For writing drivers (GPIO/UART/SPI/I2C), HAL abstraction, RTOS tasks, or MCU firmware (STM32/ESP32). |
| triggers | ["embedded","firmware","HAL","FreeRTOS","MCU","microcontroller","STM32","ESP32","RTOS","ring buffer","Unity test","mock driver","embedded C","peripheral driver","hal_gpio","hal_uart"] |
Embedded C Guidelines
Reference
Base code: base/c-embedded/ — C99 + CMake + HAL Ops Table + FreeRTOS 패턴
Core Architecture
src/
├── hal/ ← HAL 인터페이스 + 디스패처 (플랫폼 독립)
│ ├── hal_types.h ← 공통 타입 (hal_err_t, GPIO/UART 열거형)
│ ├── hal_gpio.* ← GPIO HAL
│ ├── hal_uart.* ← UART HAL
│ └── hal_timer.* ← Timer HAL
├── drivers/
│ ├── mock/ ← Host 테스트용 Mock (gpio_mock, uart_mock)
│ └── stm32/ ← MCU 드라이버 stub (실 환경 시 교체)
├── tasks/
│ ├── freertos_types.h ← FreeRTOS 타입 (real/mock 호환)
│ ├── task_led.* ← 기본 태스크 패턴 (vTaskDelay 루프)
│ └── task_uart_monitor.* ← Queue 패턴 (ISR → Task)
└── utils/
├── ring_buffer.* ← 정적 Ring Buffer (ISR-safe, no malloc)
└── logger.* ← UART 디버그 로거
Pattern 1: HAL Ops Table (Function Pointer Table)
핵심 원칙: 앱 코드는 HAL API만 호출. 플랫폼 드라이버는 런타임에 등록.
typedef struct {
hal_err_t (*init) (uint8_t pin, hal_gpio_dir_t dir);
hal_err_t (*write) (uint8_t pin, hal_gpio_level_t level);
hal_err_t (*read) (uint8_t pin, hal_gpio_level_t *out_level);
hal_err_t (*toggle)(uint8_t pin);
void (*deinit)(uint8_t pin);
} hal_gpio_ops_t;
hal_err_t hal_gpio_register(const hal_gpio_ops_t *ops);
hal_err_t hal_gpio_init (uint8_t pin, hal_gpio_dir_t dir);
hal_err_t hal_gpio_write (uint8_t pin, hal_gpio_level_t level);
hal_err_t hal_gpio_read (uint8_t pin, hal_gpio_level_t *out_level);
hal_err_t hal_gpio_toggle(uint8_t pin);
void hal_gpio_deinit(uint8_t pin);
static const hal_gpio_ops_t *s_ops = NULL;
hal_err_t hal_gpio_register(const hal_gpio_ops_t *ops) {
if (ops == NULL) return HAL_ERR_PARAM;
s_ops = ops;
return HAL_OK;
}
hal_err_t hal_gpio_write(uint8_t pin, hal_gpio_level_t level) {
if (s_ops == NULL || s_ops->write == NULL) return HAL_ERR_NOT_INIT;
return s_ops->write(pin, level);
}
#ifdef PLATFORM_HOST
#define GPIO_OPS gpio_mock_ops
#elif defined(PLATFORM_STM32)
#define GPIO_OPS gpio_stm32_ops
#endif
hal_gpio_register(&GPIO_OPS);
새 MCU 포팅: src/drivers/[platform]/gpio_[platform].c에 ops 구현 후 main.c에서 등록.
Pattern 2: FreeRTOS Task — Static Allocation
핵심 원칙: 태스크 TCB와 스택 정적 할당. xTaskCreate 대신 xTaskCreateStatic 사용.
FreeRTOSConfig.h 필수 설정 — 없으면 xTaskCreateStatic 링크 에러 + idle task 구현 필요:
#define configSUPPORT_STATIC_ALLOCATION 1
void vApplicationGetIdleTaskMemory(StaticTask_t **ppTCB,
StackType_t **ppStack, uint32_t *pSize);
#define TASK_LED_STACK_SIZE (256U)
typedef struct {
uint8_t pin;
uint32_t period_ms;
} task_led_params_t;
void task_led_run(void *pv_params);
extern StaticTask_t g_task_led_tcb;
extern StackType_t g_task_led_stack[TASK_LED_STACK_SIZE];
StaticTask_t g_task_led_tcb;
StackType_t g_task_led_stack[TASK_LED_STACK_SIZE];
void task_led_run(void *pv_params)
{
const task_led_params_t *p = (const task_led_params_t *)pv_params;
uint8_t pin = p ? p->pin : GPIO_LED_STATUS_PIN;
uint32_t period_ms = p ? p->period_ms : 1000U;
hal_err_t err = hal_gpio_init(pin, HAL_GPIO_DIR_OUTPUT);
if (err != HAL_OK) {
vTaskSuspend(NULL);
}
for (;;) {
hal_gpio_write(pin, HAL_GPIO_LEVEL_HIGH);
vTaskDelay(pdMS_TO_TICKS(period_ms / 2U));
hal_gpio_write(pin, HAL_GPIO_LEVEL_LOW);
vTaskDelay(pdMS_TO_TICKS(period_ms / 2U));
}
}
static task_led_params_t s_led_params = {
.pin = GPIO_LED_STATUS_PIN,
.period_ms = 1000U,
};
TaskHandle_t h = xTaskCreateStatic(
task_led_run,
"led",
TASK_LED_STACK_SIZE,
&s_led_params,
2U,
g_task_led_stack,
&g_task_led_tcb
);
configASSERT(h != NULL);
Pattern 3: FreeRTOS Queue — ISR → Task
핵심 원칙: ISR은 xQueueSendFromISR 사용. Task는 xQueueReceive로 블로킹 대기.
StaticQueue_t g_uart_monitor_queue_storage;
uart_monitor_msg_t g_uart_monitor_queue_buf[UART_MONITOR_QUEUE_SIZE];
QueueHandle_t g_uart_monitor_queue;
BaseType_t task_uart_monitor_push_byte_from_isr(uint8_t port, uint8_t byte);
QueueHandle_t task_uart_monitor_queue_init(void) {
g_uart_monitor_queue = xQueueCreateStatic(
UART_MONITOR_QUEUE_SIZE,
sizeof(uart_monitor_msg_t),
(uint8_t *)s_queue_buf,
&s_queue_storage
);
return g_uart_monitor_queue;
}
void UART_RX_IRQHandler(void) {
uint8_t byte = platform_uart_read_byte();
uart_monitor_msg_t msg = {
.type = UART_MSG_BYTE_RECEIVED,
.data = byte,
.port = UART_DEBUG_PORT,
};
BaseType_t woken = pdFALSE;
xQueueSendFromISR(g_uart_monitor_queue, &msg, &woken);
portYIELD_FROM_ISR(woken);
}
void task_uart_monitor_run(void *pv_params) {
(void)pv_params;
uart_monitor_msg_t msg;
for (;;) {
if (xQueueReceive(g_uart_monitor_queue, &msg, portMAX_DELAY) == pdPASS) {
switch (msg.type) {
case UART_MSG_BYTE_RECEIVED:
handle_byte(msg.data);
break;
default:
break;
}
}
}
}
Pattern 4: Static Ring Buffer (ISR-Safe)
핵심 원칙: 정적 배열만 사용. 크기는 반드시 2의 거듭제곱. head/tail은 volatile.
typedef struct {
uint8_t *buf;
size_t capacity;
size_t mask;
volatile size_t head;
volatile size_t tail;
} ring_buf_t;
hal_err_t ring_buf_init (ring_buf_t *rb, uint8_t *storage, size_t size);
hal_err_t ring_buf_write_byte(ring_buf_t *rb, uint8_t byte);
hal_err_t ring_buf_read_byte (ring_buf_t *rb, uint8_t *out);
size_t ring_buf_available (const ring_buf_t *rb);
static uint8_t s_uart_buf[256];
static ring_buf_t s_uart_rb;
void app_init(void) {
ring_buf_init(&s_uart_rb, s_uart_buf, sizeof(s_uart_buf));
}
void UART_RX_IRQHandler(void) {
uint8_t b = uart_read_byte();
ring_buf_write_byte(&s_uart_rb, b);
}
void process_uart(void) {
uint8_t b;
while (ring_buf_read_byte(&s_uart_rb, &b) == HAL_OK) {
handle_byte(b);
}
}
Pattern 5: Unity Test with Mock Driver
구조: 각 테스트 파일이 독립 실행파일. setUp/tearDown 중복 정의 충돌 없음.
#include "unity.h"
#include "hal_gpio.h"
#include "gpio_mock.h"
void setUp(void) { hal_gpio_register(&gpio_mock_ops); }
void tearDown(void) { hal_gpio_deinit(0U); }
void test_gpio_write_high_sets_level(void) {
hal_gpio_init(0U, HAL_GPIO_DIR_OUTPUT);
hal_gpio_write(0U, HAL_GPIO_LEVEL_HIGH);
hal_gpio_level_t level;
hal_gpio_read(0U, &level);
TEST_ASSERT_EQUAL_INT(HAL_GPIO_LEVEL_HIGH, (int)level);
}
void test_gpio_read_null_returns_error(void) {
hal_gpio_init(0U, HAL_GPIO_DIR_OUTPUT);
hal_err_t err = hal_gpio_read(0U, NULL);
TEST_ASSERT_EQUAL_INT(HAL_ERR_PARAM, (int)err);
}
int main(void) {
UnityBegin("test_hal_gpio.c");
RUN_TEST(test_gpio_write_high_sets_level);
RUN_TEST(test_gpio_read_null_returns_error);
return UnityEnd();
}
extern const hal_gpio_ops_t gpio_mock_ops;
void gpio_mock_set_input_level(uint8_t pin, hal_gpio_level_t level);
hal_gpio_level_t gpio_mock_get_level(uint8_t pin);
# tests/CMakeLists.txt — 각 테스트 파일을 독립 실행파일로 빌드
add_executable(test_hal_gpio test_hal_gpio.c)
add_executable(test_ring_buffer test_ring_buffer.c)
target_link_libraries(test_hal_gpio PRIVATE hal drivers unity)
target_link_libraries(test_ring_buffer PRIVATE utils unity)
add_test(NAME gpio_hal COMMAND test_hal_gpio)
add_test(NAME ring_buffer COMMAND test_ring_buffer)
CMake Dual Build
# CMakeLists.txt
option(BUILD_FOR_HOST "Build for host PC (mock drivers + tests)" ON)
if(BUILD_FOR_HOST)
add_compile_definitions(PLATFORM_HOST FREERTOS_MOCK)
set(DRIVER_SRC
src/drivers/mock/gpio_mock.c
src/drivers/mock/uart_mock.c
)
else()
add_compile_definitions(PLATFORM_STM32)
set(DRIVER_SRC
src/drivers/stm32/gpio_stm32.c
src/drivers/stm32/uart_stm32.c
)
# -DCMAKE_TOOLCHAIN_FILE=arm-none-eabi-toolchain.cmake 로 크로스 컴파일
endif()
cmake -B build -DBUILD_FOR_HOST=ON -DBUILD_TESTS=ON
cmake --build build
ctest --test-dir build -V
cmake -B build_stm32 \
-DBUILD_FOR_HOST=OFF \
-DCMAKE_TOOLCHAIN_FILE=arm-none-eabi-toolchain.cmake
cmake --build build_stm32
FreeRTOS Mock Types (Host Build)
#ifdef FREERTOS_MOCK
typedef uint32_t TickType_t;
typedef uint32_t StackType_t;
typedef struct { uint8_t dummy[128]; } StaticTask_t;
typedef struct { uint8_t dummy[64]; } StaticQueue_t;
#define pdMS_TO_TICKS(ms) ((TickType_t)(ms))
#define portMAX_DELAY ((TickType_t)0xFFFFFFFFUL)
static inline void vTaskDelay(TickType_t t) { (void)t; }
static inline void vTaskSuspend(TaskHandle_t h){ (void)h; }
#else
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "semphr.h"
#endif
hal_types.h — 공통 에러 코드
typedef enum {
HAL_OK = 0,
HAL_ERR_GENERIC = -1,
HAL_ERR_PARAM = -2,
HAL_ERR_BUSY = -3,
HAL_ERR_TIMEOUT = -4,
HAL_ERR_NOT_INIT = -5,
HAL_ERR_OVERFLOW = -6,
} hal_err_t;
Porting to a New Platform
include/app_config.h — 핀 번호, 스택 크기, 포트 번호 등 모든 커스터마이징 상수 정의
src/drivers/[platform]/gpio_[platform].c 작성 — hal_gpio_ops_t 구현
src/drivers/[platform]/uart_[platform].c 작성 — hal_uart_ops_t 구현
CMakeLists.txt에 PLATFORM_[PLATFORM] 조건 분기 + #define GPIO_OPS 추가
- FreeRTOS 실 소스 연결:
FreeRTOS/Source/ + MCU 포트 + FreeRTOSConfig.h
Anti-Patterns
uint8_t *buf = malloc(256);
static uint8_t buf[256];
uint8_t buf[100];
ring_buf_init(&rb, buf, sizeof(buf));
uint8_t buf[128];
void IRQHandler(void) {
xQueueSend(q, &msg, 0);
}
void IRQHandler(void) {
BaseType_t woken = pdFALSE;
xQueueSendFromISR(q, &msg, &woken);
portYIELD_FROM_ISR(woken);
}
add_executable(test_foo test_foo.c)
add_executable(test_bar test_bar.c)
#include "FreeRTOS.h"
#include "freertos_types.h"
NVIC_SetPriority(USART1_IRQn, 0U);
NVIC_SetPriority(USART1_IRQn, 6U);
Line count: < 500 ✅ | Reference: base/c-embedded/ ✅ | Anti-patterns: 7 ✅