嵌入式软件开发
概述
嵌入式软件运行在资源受限的硬件上,与硬件紧密耦合。本文涵盖裸机编程、RTOS、硬件抽象层、Bootloader、固件更新和调试技术。
相关内容:嵌入式系统
1. 裸机编程(Bare-Metal Programming)
1.1 寄存器级操作
嵌入式编程常常直接操作硬件寄存器:
// STM32 GPIO 寄存器级操作
#define GPIOA_BASE 0x40020000
#define GPIOA_MODER (*(volatile uint32_t *)(GPIOA_BASE + 0x00))
#define GPIOA_ODR (*(volatile uint32_t *)(GPIOA_BASE + 0x14))
// 设置 PA5 为输出模式
GPIOA_MODER &= ~(3 << (5 * 2)); // 清除位
GPIOA_MODER |= (1 << (5 * 2)); // 设置为输出 (01)
// 点亮 LED (PA5)
GPIOA_ODR |= (1 << 5); // 置高
GPIOA_ODR &= ~(1 << 5); // 置低
volatile 关键字:告诉编译器该变量可能被外部(硬件、中断)修改,禁止优化。
1.2 启动代码(Startup Code)
MCU 上电后执行的第一段代码:
1. 中断向量表(Vector Table)
- 初始栈指针 (SP)
- Reset Handler 地址
- 各中断处理函数地址
2. Reset Handler:
a. 初始化 .data 段(从 Flash 复制到 RAM)
b. 清零 .bss 段
c. 初始化系统时钟
d. 调用 main()
// 简化的启动代码
extern uint32_t _sdata, _edata, _sidata; // 链接器脚本定义
extern uint32_t _sbss, _ebss;
void Reset_Handler(void) {
// 复制 .data 段
uint32_t *src = &_sidata;
uint32_t *dst = &_sdata;
while (dst < &_edata) *dst++ = *src++;
// 清零 .bss 段
dst = &_sbss;
while (dst < &_ebss) *dst++ = 0;
// 初始化系统
SystemInit();
// 进入 main
main();
// main 不应返回
while (1);
}
1.3 中断处理
// 中断服务例程 (ISR)
void EXTI0_IRQHandler(void) {
if (EXTI->PR & (1 << 0)) { // 检查中断标志
// 处理中断
button_pressed = 1;
EXTI->PR = (1 << 0); // 清除中断标志
}
}
// 中断优先级配置 (NVIC)
NVIC_SetPriority(EXTI0_IRQn, 2); // 优先级 2
NVIC_EnableIRQ(EXTI0_IRQn); // 使能中断
ISR 编写原则:
- 尽可能短:只做标志设置,复杂处理放到主循环
- 不要使用阻塞操作:不调用
printf、malloc、sleep - 注意重入:共享变量用
volatile修饰
2. 实时操作系统(RTOS)
2.1 FreeRTOS
FreeRTOS 是最流行的嵌入式 RTOS,提供任务调度、同步和通信原语。
任务创建:
void vLEDTask(void *pvParameters) {
while (1) {
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
vTaskDelay(pdMS_TO_TICKS(500)); // 延迟 500ms
}
}
void vSensorTask(void *pvParameters) {
while (1) {
float temp = read_temperature();
xQueueSend(xTempQueue, &temp, portMAX_DELAY);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
int main(void) {
xTaskCreate(vLEDTask, "LED", 128, NULL, 1, NULL);
xTaskCreate(vSensorTask, "Sensor", 256, NULL, 2, NULL);
vTaskStartScheduler(); // 启动调度器,不返回
}
队列(Queue):
QueueHandle_t xTempQueue = xQueueCreate(10, sizeof(float));
// 发送
float temp = 25.5;
xQueueSend(xTempQueue, &temp, pdMS_TO_TICKS(100));
// 接收
float received;
if (xQueueReceive(xTempQueue, &received, pdMS_TO_TICKS(1000)) == pdTRUE) {
process_temperature(received);
}
信号量(Semaphore):
// 二值信号量:用于中断-任务同步
SemaphoreHandle_t xBinarySem = xSemaphoreCreateBinary();
// ISR 中释放
void UART_IRQHandler(void) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xSemaphoreGiveFromISR(xBinarySem, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
// 任务中等待
void vUARTTask(void *pvParameters) {
while (1) {
xSemaphoreTake(xBinarySem, portMAX_DELAY);
process_uart_data();
}
}
互斥量(Mutex):
SemaphoreHandle_t xMutex = xSemaphoreCreateMutex();
void safe_write(const char *msg) {
xSemaphoreTake(xMutex, portMAX_DELAY);
uart_send(msg); // 临界区
xSemaphoreGive(xMutex);
}
2.2 Zephyr RTOS
Zephyr 是 Linux 基金会支持的现代嵌入式 RTOS:
- 支持 200+ 开发板
- 完整的网络栈(BLE、WiFi、Thread、LTE)
- 设备树(Devicetree)描述硬件
- Kconfig 配置系统
- 内置 POSIX 兼容层
3. 硬件抽象层(HAL)
HAL 将硬件操作封装为统一接口,提高代码可移植性:
// HAL 接口定义
typedef struct {
void (*init)(void);
void (*write)(uint8_t *data, uint16_t len);
uint16_t (*read)(uint8_t *buf, uint16_t max_len);
} uart_driver_t;
// STM32 实现
void stm32_uart_init(void) { /* ... */ }
void stm32_uart_write(uint8_t *data, uint16_t len) { /* ... */ }
uart_driver_t stm32_uart = {
.init = stm32_uart_init,
.write = stm32_uart_write,
.read = stm32_uart_read,
};
// 应用层使用统一接口
void app_send_message(uart_driver_t *uart, const char *msg) {
uart->write((uint8_t *)msg, strlen(msg));
}
4. Bootloader
Bootloader 是系统上电后运行的第一个程序,负责初始化硬件和加载应用固件。
Flash 布局:
┌─────────────────┐ 0x08000000
│ Bootloader │ 16-64KB
├─────────────────┤
│ App Header │ 元数据(版本、CRC、大小)
├─────────────────┤
│ Application │ 主应用固件
├─────────────────┤
│ OTA Staging │ 新固件暂存区(可选)
└─────────────────┘
Bootloader 启动流程:
1. 硬件初始化(时钟、GPIO)
2. 检查是否有固件更新请求
├── 有 → 从暂存区复制新固件到应用区,验证 CRC
└── 无 → 继续
3. 验证应用固件完整性(CRC/签名)
├── 通过 → 跳转到应用
└── 失败 → 等待重新刷写
4. 设置栈指针,跳转到应用的 Reset Handler
5. 固件更新(OTA)
OTA(Over-The-Air)允许远程更新设备固件。
5.1 更新策略
| 策略 | 说明 | 安全性 |
|---|---|---|
| A/B 分区 | 两个固件分区交替使用 | 高(可回滚) |
| 差分更新 | 只传输变更部分 | 中(节省带宽) |
| 压缩更新 | 压缩完整固件 | 中 |
5.2 A/B 分区更新流程
1. 设备从 分区A 运行当前固件
2. 下载新固件到 分区B
3. 验证 分区B 的完整性(CRC + 签名)
4. 标记 分区B 为待启动
5. 重启 → Bootloader 加载 分区B
6. 新固件自检
├── 成功 → 确认,分区B 成为活动分区
└── 失败 → 回滚到 分区A
5.3 安全考虑
- 签名验证:使用 RSA/ECDSA 验证固件来源
- 加密传输:TLS 加密下载通道
- 回滚保护:版本号单调递增,防止降级攻击
- 安全启动链:从 Bootloader 到应用逐级验证
6. 交叉编译
嵌入式开发通常在 PC 上编译,在目标硬件上运行。
# ARM 交叉编译工具链
arm-none-eabi-gcc -mcpu=cortex-m4 -mthumb -mfloat-abi=hard \
-Os -Wall -ffunction-sections -fdata-sections \
-c main.c -o main.o
# 链接
arm-none-eabi-gcc -mcpu=cortex-m4 -mthumb \
-T linker_script.ld -Wl,--gc-sections \
main.o -o firmware.elf
# 生成 bin/hex 文件
arm-none-eabi-objcopy -O binary firmware.elf firmware.bin
arm-none-eabi-objcopy -O ihex firmware.elf firmware.hex
# 查看大小
arm-none-eabi-size firmware.elf
# text data bss dec hex filename
# 12456 128 2048 14632 3928 firmware.elf
CMake 交叉编译配置:
# toolchain.cmake
set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR arm)
set(CMAKE_C_COMPILER arm-none-eabi-gcc)
set(CMAKE_CXX_COMPILER arm-none-eabi-g++)
set(CMAKE_ASM_COMPILER arm-none-eabi-gcc)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
7. 调试
7.1 JTAG/SWD
| 接口 | 引脚数 | 速度 | 说明 |
|---|---|---|---|
| JTAG | 4-5 | 快 | 传统接口,功能完整 |
| SWD | 2 | 快 | ARM 专用,引脚少 |
| UART | 2 | 慢 | 调试打印,简单 |
7.2 GDB 远程调试
# 启动 OpenOCD(连接调试器)
openocd -f interface/stlink.cfg -f target/stm32f4x.cfg
# 另一个终端启动 GDB
arm-none-eabi-gdb firmware.elf
(gdb) target remote :3333 # 连接 OpenOCD
(gdb) monitor reset halt # 复位并暂停
(gdb) load # 下载固件
(gdb) break main # 设置断点
(gdb) continue # 运行
(gdb) print sensor_value # 查看变量
(gdb) x/16xw 0x40020000 # 查看内存/寄存器
7.3 常用调试技术
| 技术 | 说明 | 适用场景 |
|---|---|---|
| 断点调试 | JTAG/SWD + GDB | 逻辑问题 |
| 串口打印 | UART printf | 快速调试 |
| 逻辑分析仪 | 捕获数字信号波形 | 时序问题 |
| 示波器 | 观察模拟/数字信号 | 电气问题 |
| GPIO 翻转 | 用示波器测量代码执行时间 | 性能分析 |
| 硬件断点 | 片上调试单元(DWT) | 数据访问监控 |
8. 嵌入式开发最佳实践
| 实践 | 说明 |
|---|---|
| 防御性编程 | 检查所有输入、返回值、指针有效性 |
| 看门狗 | 使用 WDT 检测死机并自动复位 |
| 内存管理 | 避免动态分配,使用静态分配或内存池 |
| 低功耗 | 使用睡眠模式,按需唤醒外设 |
| 代码审查 | 嵌入式 bug 修复成本高,预防为主 |
| 单元测试 | 在 PC 上测试与硬件无关的逻辑 |
与其他主题的关系
- 参见 物联网系统,理解固件、设备接入与云端管理如何拼成完整链条
- 参见 操作系统,理解实时任务、调度、中断与资源管理
- 参见 计算机体系结构综述,理解 MCU、存储层次与外设接口的硬件背景
- 参见 测试与质量保障,理解嵌入式场景下的验证、调试与回归控制
参考文献
- "Making Embedded Systems" - Elecia White
- "Real-Time Operating Systems for ARM Cortex-M" - Jonathan Valvano
- FreeRTOS 官方文档:https://freertos.org
- Zephyr 官方文档:https://docs.zephyrproject.org