前置知识: C

嵌入式C编程

00:00
2 min Advanced 2026/6/14

嵌入式系统C编程要点

概述

嵌入式C编程是在资源受限的微控制器(MCU)上编写软件的技术。与桌面开发不同,嵌入式系统通常内存有限(几KB到几MB)、没有操作系统或只有实时操作系统RTOS)、需要直接操作硬件寄存器。嵌入式C编程强调确定性、低内存占用和对硬件的精确控制。

基础概念

嵌入式系统的特点

  • 资源受限:内存小、CPU频低、功耗敏感
  • 实时要求:必须在确定时间响应事件
  • 可靠性要求系统可能运不重启
  • 直接操作硬件通过寄存器和中断与外设交互
  • 通常没有标准库完整支持

嵌入式开发工具链

工具说明
交叉编译如 arm-none-eabi-gcc
烧录器如 OpenOCD、J-Link
调试器如 GDB(通过 JTAG/SWD)
仿真器如 QEMU

关键字和修饰符

// volatile: 告诉编译器变量可能被外部修改,不要优化
volatile uint32_t *reg = (volatile uint32_t *)0x40020000;

// const: 常量,通常放在Flash中节省RAM
const char message[] = "Hello";

// static: 限制作用域,或持久化局部变量
static int counter = 0;

// register: 建议编译器将变量放在寄存器中
register int fast_var;

快速上手

寄存器操作

#include <stdint.h>

// 定义寄存器访问宏
#define REG(addr) (*(volatile uint32_t *)(addr))

// STM32 GPIOA 基地址
#define GPIOA_BASE   0x40020000
#define GPIOA_MODER  REG(GPIOA_BASE + 0x00)  // 模式寄存器
#define GPIOA_OTYPER REG(GPIOA_BASE + 0x04)  // 输出类型寄存器
#define GPIOA_ODR    REG(GPIOA_BASE + 0x14)  // 输出数据寄存器
#define GPIOA_IDR    REG(GPIOA_BASE + 0x10)  // 输入数据寄存器

// 设置PA5为输出模式(LED引脚)
void led_init(void) {
    // 清除第10-11位(PA5的模式位),然后设为01(通用输出)
    GPIOA_MODER &= ~(3U << 10);  // 清除
    GPIOA_MODER |= (1U << 10);   // 设为输出
}

// 点亮LED
void led_on(void) {
    GPIOA_ODR |= (1U << 5);  // 设置第5位
}

// 熄灭LED
void led_off(void) {
    GPIOA_ODR &= ~(1U << 5); // 清除第5位
}

// 翻转LED
void led_toggle(void) {
    GPIOA_ODR ^= (1U << 5);  // 翻转第5位
}

简单的延时函数

#include <stdint.h>

// 简单的忙等待延时(不精确,依赖时钟频率)
void delay(volatile uint32_t count) {
    while (count--) {
        // 空循环,编译器不会优化掉 volatile 变量
    }
}

// 使用 SysTick 定时器的精确延时
#define SYSTICK_LOAD  (*(volatile uint32_t *)0xE000E014)
#define SYSTICK_VAL   (*(volatile uint32_t *)0xE000E018)
#define SYSTICK_CTRL  (*(volatile uint32_t *)0xE000E010)

void systick_init(uint32_t reload_value) {
    SYSTICK_LOAD = reload_value - 1;
    SYSTICK_VAL = 0;
    SYSTICK_CTRL = 0x07; // 使能,中断,系统时钟
}

void delay_ms(uint32_t ms) {
    for (uint32_t i = 0; i < ms; i++) {
        SYSTICK_VAL = 0;
        while (!(SYSTICK_CTRL & (1U << 16))); // 等待计数标志
    }
}

详细用法

中断服务程序

#include <stdint.h>

// 外部中断0的中断服务程序
void EXTI0_IRQHandler(void) {
    // 检查中断标志
    if (EXTI->PR & (1U << 0)) {
        EXTI->PR |= (1U << 0); // 清除中断标志(写1清除)

        // 处理中断事件
        led_toggle();
    }
}

// 配置外部中断
void exti_init(void) {
    // 使能SYSCFG时钟
    RCC->APB2ENR |= RCC_APB2ENR_SYSCFGEN;

    // 将PA0映射到EXTI0
    SYSCFG->EXTICR[0] &= ~0x000F;

    // 使能EXTI0中断
    EXTI->IMR |= (1U << 0);

    // 设置下降沿触发
    EXTI->FTSR |= (1U << 0);

    // 使能NVIC中断
    NVIC_EnableIRQ(EXTI0_IRQn);
}

串口通信(UART)

#include <stdint.h>

#define USART2_BASE  0x40004400
#define USART2_SR    (*(volatile uint32_t *)(USART2_BASE + 0x00))
#define USART2_DR    (*(volatile uint32_t *)(USART2_BASE + 0x04))
#define USART2_BRR   (*(volatile uint32_t *)(USART2_BASE + 0x08))
#define USART2_CR1   (*(volatile uint32_t *)(USART2_BASE + 0x0C))

// 初始化串口
void uart_init(uint32_t baudrate) {
    // 使能GPIOA和USART2时钟
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
    RCC->APB1ENR |= RCC_APB1ENR_USART2EN;

    // 配置PA2(TX)和PA3(RX)为复用功能
    GPIOA->MODER &= ~(3U << 4) & ~(3U << 6);
    GPIOA->MODER |= (2U << 4) | (2U << 6);
    GPIOA->AFR[0] |= (7U << 8) | (7U << 12); // AF7

    // 设置波特率
    USART2_BRR = SystemCoreClock / baudrate;

    // 使能发送和接收
    USART2_CR1 |= (1U << 13) | (1U << 3) | (1U << 2);
}

// 发送一个字节
void uart_putc(char c) {
    while (!(USART2_SR & (1U << 7))); // 等待发送缓冲区空
    USART2_DR = c;
}

// 发送字符串
void uart_puts(const char *str) {
    while (*str) {
        uart_putc(*str++);
    }
}

// 接收一个字节
char uart_getc(void) {
    while (!(USART2_SR & (1U << 5))); // 等待接收缓冲区非空
    return (char)(USART2_DR & 0xFF);
}

内存池替代 malloc

#include <stdint.h>
#include <string.h>

#define POOL_BLOCK_SIZE  32
#define POOL_BLOCK_COUNT 64

// 内存池
static uint8_t pool_buffer[POOL_BLOCK_SIZE * POOL_BLOCK_COUNT];
static uint8_t pool_used[POOL_BLOCK_COUNT]; // 使用标志

// 从内存池分配
void *pool_alloc(void) {
    for (int i = 0; i < POOL_BLOCK_COUNT; i++) {
        if (!pool_used[i]) {
            pool_used[i] = 1;
            return &pool_buffer[i * POOL_BLOCK_SIZE];
        }
    }
    return NULL; // 内存不足
}

// 释放到内存池
void pool_free(void *ptr) {
    if (!ptr) return;

    // 计算块索引
    uintptr_t offset = (uintptr_t)ptr - (uintptr_t)pool_buffer;
    if (offset % POOL_BLOCK_SIZE != 0) return; // 非法指针

    int index = offset / POOL_BLOCK_SIZE;
    if (index >= 0 && index < POOL_BLOCK_COUNT) {
        pool_used[index] = 0;
    }
}

常见场景

场景一:状态机实现

#include <stdint.h>

typedef enum {
    STATE_IDLE,
    STATE_RUNNING,
    STATE_ERROR
} SystemState;

typedef struct {
    SystemState state;
    uint32_t timer;
    uint32_t error_code;
} System;

void system_init(System *sys) {
    sys->state = STATE_IDLE;
    sys->timer = 0;
    sys->error_code = 0;
}

void system_update(System *sys) {
    switch (sys->state) {
        case STATE_IDLE:
            if (start_button_pressed()) {
                sys->state = STATE_RUNNING;
                sys->timer = 1000;
            }
            break;

        case STATE_RUNNING:
            sys->timer--;
            if (sys->timer == 0) {
                sys->state = STATE_IDLE;
            }
            if (sensor_error()) {
                sys->state = STATE_ERROR;
                sys->error_code = 0x01;
            }
            break;

        case STATE_ERROR:
            if (reset_button_pressed()) {
                sys->state = STATE_IDLE;
                sys->error_code = 0;
            }
            break;
    }
}

场景二:环形缓冲区

#include <stdint.h>
#include <stdbool.h>

#define BUF_SIZE 64

typedef struct {
    uint8_t data[BUF_SIZE];
    volatile uint16_t head;
    volatile uint16_t tail;
} RingBuffer;

// 初始化
void rb_init(RingBuffer *rb) {
    rb->head = 0;
    rb->tail = 0;
}

// 写入一个字节(中断中调用)
bool rb_put(RingBuffer *rb, uint8_t byte) {
    uint16_t next = (rb->head + 1) % BUF_SIZE;
    if (next == rb->tail) return false; // 缓冲区满
    rb->data[rb->head] = byte;
    rb->head = next;
    return true;
}

// 读取一个字节(主循环中调用)
bool rb_get(RingBuffer *rb, uint8_t *byte) {
    if (rb->head == rb->tail) return false; // 缓冲区空
    *byte = rb->data[rb->tail];
    rb->tail = (rb->tail + 1) % BUF_SIZE;
    return true;
}

// 检查是否为空
bool rb_empty(RingBuffer *rb) {
    return rb->head == rb->tail;
}

场景三:看门狗

#include <stdint.h>

#define IWDG_BASE   0x40003000
#define IWDG_KR     (*(volatile uint32_t *)(IWDG_BASE + 0x00))
#define IWDG_PR     (*(volatile uint32_t *)(IWDG_BASE + 0x04))
#define IWDG_RLR    (*(volatile uint32_t *)(IWDG_BASE + 0x08))
#define IWDG_SR     (*(volatile uint32_t *)(IWDG_BASE + 0x0C))

// 初始化独立看门狗
void watchdog_init(uint32_t timeout_ms) {
    // 启用寄存器访问
    IWDG_KR = 0x5555;

    // 设置预分频器
    IWDG_PR = 4; // 64分频

    // 设置重载值
    uint32_t reload = (timeout_ms * 40) / 64; // 40kHz LSI时钟
    IWDG_RLR = reload > 0xFFF ? 0xFFF : reload;

    // 启动看门狗
    IWDG_KR = 0xCCCC;
}

// 喂狗(必须在超时前调用)
void watchdog_feed(void) {
    IWDG_KR = 0xAAAA;
}

// 在主循环中定期喂狗
int main(void) {
    watchdog_init(1000); // 1秒超时

    while (1) {
        // 执行任务
        do_work();

        // 喂狗
        watchdog_feed();
    }
}

注意事项

volatile 的重要性

嵌入式编程中,硬件寄存器和中断变量必须使用 volatile

// 错误:编译器可能优化掉对寄存器的读取
uint32_t *reg = (uint32_t *)0x40020000;
uint32_t val = *reg; // 可能被优化掉

// 正确:使用 volatile 防止优化
volatile uint32_t *reg = (volatile uint32_t *)0x40020000;
uint32_t val = *reg; // 每次都会实际读取

避免使用标准库函数

嵌入式环境中可能没有完整标准库支持,应避免使用:

  • printf/scanf(太重,使用自定义串口输出
  • malloc/free(不确定的执行时间,使用静态分配或内存
  • float运算(某些MCU没有FPU,使用定数)

中断中的注意事项

中断服务程序(ISR)应尽量短小:

// 错误:ISR中做太多工作
void UART_IRQHandler(void) {
    char buf[256];
    process_data(buf); // 太耗时
}

// 正确:ISR中只做最少的工作
volatile uint8_t rx_byte;
volatile bool rx_ready = false;

void UART_IRQHandler(void) {
    rx_byte = UART->DR;   // 只读取数据
    rx_ready = true;       // 设置标志
}

// 主循环中处理
while (1) {
    if (rx_ready) {
        process_byte(rx_byte);
        rx_ready = false;
    }
}

进阶用法

使用链接脚本控制内存布局

/* linker.ld - 链接脚本 */
MEMORY {
    FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 512K
    RAM   (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}

SECTIONS {
    .text : {
        *(.vectors)    /* 中断向量表 */
        *(.text)       /* 代码 */
        *(.rodata)     /* 只读数据 */
    } > FLASH

    .data : {
        *(.data)       /* 已初始化数据 */
    } > RAM AT > FLASH

    .bss : {
        *(.bss)        /* 未初始化数据 */
    } > RAM
}

DMA 传输

#include <stdint.h>

#define DMA1_BASE    0x40026000
#define DMA1_STREAM5 (*(volatile uint32_t *)(DMA1_BASE + 0x50))

// 配置DMA传输(内存到串口)
void dma_uart_send(const uint8_t *data, uint16_t length) {
    // 禁用DMA流
    DMA1_STREAM5 &= ~(1U << 0);

    // 等待DMA流完全禁用
    while (DMA1_STREAM5 & (1U << 0));

    // 配置DMA
    DMA1->S5CR = (4U << 25)   // 通道4(USART2_TX)
               | (1U << 10)   // 内存地址递增
               | (0U << 13)   // 8位内存宽度
               | (0U << 16);  // 8位外设宽度

    DMA1->S5NDTR = length;    // 传输长度
    DMA1->S5M0AR = (uint32_t)data;    // 内存地址
    DMA1->S5PAR  = (uint32_t)&USART2_DR; // 外设地址

    // 使能DMA流
    DMA1->S5CR |= (1U << 0);
}

RTOS 任务

#include "FreeRTOS.h"
#include "task.h"

// LED闪烁任务
void led_task(void *param) {
    while (1) {
        led_toggle();
        vTaskDelay(pdMS_TO_TICKS(500)); // 延时500ms
    }
}

// 传感器读取任务
void sensor_task(void *param) {
    while (1) {
        uint16_t value = adc_read();
        process_sensor(value);
        vTaskDelay(pdMS_TO_TICKS(100)); // 延时100ms
    }
}

int main(void) {
    // 硬件初始化
    led_init();
    adc_init();

    // 创建任务
    xTaskCreate(led_task, "LED", 128, NULL, 1, NULL);
    xTaskCreate(sensor_task, "Sensor", 256, NULL, 2, NULL);

    // 启动调度器
    vTaskStartScheduler();

    while (1); // 不应到达此处
}

知识检测

学习进度

-- 已学文档
--% 知识覆盖率

学习推荐

专注模式