嵌入式的资源与实时约束¶
一、前言:为什么嵌入式不能“随便写”¶
在 PC 或服务器开发中,我们习惯于一种“默认”前提:内存不够可以加、算力不足可以扩、系统调度由操作系统兜底。程序的目标往往只需满足“功能正确 + 平均性能尚可”。
但嵌入式系统并不生活在这样的世界里。在嵌入式环境中,资源是被严格量化的:Flash 可能只有几十 KB,RAM 只有几 KB,CPU 主频只有几十 MHz,而系统却承担着实时控制、设备安全、工业或消费级可靠性等责任。在这里,程序不只是“能跑”就够了,它还必须:
- 在规定时间内完成任务
- 在最坏情况下依然行为正确
- 在有限资源下保持长期稳定运行
嵌入式工程的本质,是在一个资源受限的世界里,追求系统行为的确定性。当然,这一部分跟C++喜欢把一些事情藏起来存在冲突,但是用好C++中的大部分特性,的确可以推动不小的性能改进。
二、Flash / ROM 约束:代码不是“免费”的¶
2.1 Flash 的现实规模¶
嵌入式系统中,程序存储空间首先受到 Flash / ROM 容量的严格限制:
- STM32F103:64KB ~ 128KB Flash
- STM32F4 系列:512KB ~ 2MB Flash
- 低端 MCU:甚至只有 16KB
与动辄几十 MB 可执行文件的 PC 程序相比,这种容量差异是数量级的鸿沟。
2.2 Flash 约束如何影响软件设计¶
在这样的环境下,“写什么代码”本身就是一种工程决策。代码体积直接决定系统是否可部署,功能冗余意味着真实的存储浪费。引入库不再是“好不好用”,而是“能不能放下”(对的,笔者真见过printf一拉进来二进制文件爆炸性拉大)。因此,嵌入式工程师必须掌握一些常见的编译器优化选项:
- 编译器优化选项(如
-Os优化代码大小) - 函数与段级别的垃圾回收(
-ffunction-sections,-fdata-sections配合--gc-sections) - 精确控制链接行为
这个别着急,后面我们有一个专门的章节好好了解。
三、RAM 约束:内存不是“用完就算”¶
如果说 Flash 约束限制了“能写多少功能”,那么 RAM 约束则直接影响系统是否能稳定运行。
3.1 RAM 的数量级现实¶
嵌入式系统中,RAM 往往只有:2KB / 8KB / 20KB / 64KB。在这样的环境下,我们真有可能搞出来栈溢出,把SP指针跑飞了,而且,如果内存管理算法搞的不好,我们的实时系统可能在数小时或数天后,其堆碎片就会把系统搞崩溃了(allocate行为找不到合适的buffer了)
3.2 栈的风险¶
栈空间主要消耗于:函数调用深度、中断嵌套、局部变量。在嵌入式中,以下行为往往被严格限制甚至禁止。
- 你肯定是不允许搞递归的——我们都知道递归的本质是自己调自己,一不小心把栈叠太大了,会直接把系统搞崩了(毕竟我们没办法预知到底要迭代多少,你算的过来多少,用户和其他任务堆栈可不管你)
- 大型的局部数组也不要开,仍然一样——把栈叠太大了,会直接把系统搞崩了
一次不可预期的栈增长就可能直接破坏系统。
如果你真的需要大数组,这样玩:
// 避免大型局部数组
void process_data(void) {
// 不建议:uint8_t buffer[4096]; // 可能溢出
// 建议:使用静态或全局内存,或分段处理
static uint8_t buffer[256]; // 或从内存池分配
}
3.3 堆的风险¶
运行期动态内存分配在嵌入式系统中一直是高风险操作:
malloc/free的时间复杂度不可预测- 长期运行会产生内存碎片
- 错误难以复现与调试
成熟的嵌入式系统通常会采用:
- 启动阶段一次性分配
- 内存池 / 对象池
- 完全静态内存模型
#define POOL_SIZE 1024
#define BLOCK_SIZE 32
#define NUM_BLOCKS (POOL_SIZE / BLOCK_SIZE)
static uint8_t memory_pool[POOL_SIZE];
static bool block_used[NUM_BLOCKS] = {0};
void* mempool_alloc(void) {
for (int i = 0; i < NUM_BLOCKS; i++) {
if (!block_used[i]) {
block_used[i] = true;
return &memory_pool[i * BLOCK_SIZE];
}
}
return NULL; // 无可用内存
}
在嵌入式系统中,内存管理首先服务于确定性,而不是便利性。
四、CPU 约束:算力是被精确计数的¶
在 PC/服务器的世界里,我们习惯把 CPU 当成一个“几乎用不完”的资源: 算法慢一点?加个缓存;分支多一点?交给乱序执行;浮点算不动?硬件来兜底。CPU 在那儿更像个背景板——只要不太慢就行。但在嵌入式里,事情不是“快不快”的问题,CPU 是个需要被精确计量与精确预算的资源。当然,现代的芯片,如果资源不是很紧张,犯不着这样做,但是成本在这,你的老板一定会狠狠要求你压榨的,不是吗?
4.1 MCU 的算力特征¶
典型 MCU 的算力特征,和桌面 CPU 几乎处在两个世界:
- 主频有限(几十到几百 MHz)
- 无乱序执行,基本严格顺序流水
- 分支预测能力弱,甚至没有
- Cache 极小,甚至没有 Cache
结论很直接:在 MCU 上,代码的行为几乎可以直接映射到指令流。你写下的每一个 if、每一个循环、每一次函数调用,最终都会变成实打实的一条条指令,按顺序执行。
4.2 时间复杂度的“工程化”¶
在嵌入式世界,时间复杂度往往不是 O(n) 那样的数学讨论,真正的问题是:
这段代码,能不能在一个控制周期内跑完?
比如:
- 在没有 FPU 的 MCU 上,一个浮点运算可能要几十个周期。
- 一次整数除法,往往比几十次加减更昂贵。
- 中断响应时间取决于 CPU 当时正在执行的指令路径。
所以嵌入式工程师会做一些对桌面程序员看着“反直觉”的事:
- 分析 最坏执行时间(WCET)
- 避免不可预测的循环次数
- 控制分支数量,减少执行路径的不确定性
- 必要时看反汇编、手动估算 cycle 数
下面的例子看起来只是微小重构,但在 MCU 上意义重大:
// 优化前:条件判断在循环内
for (int i = 0; i < n; i++) {
if (condition) {
process_a(data[i]);
} else {
process_b(data[i]);
}
}
问题不在逻辑错误,而在于:每一次循环都要经历一次分支判断。在没有分支预测的 CPU 上,这就是稳定且可观的性能损耗。改法也很朴素:
// 优化后:减少分支预测失败
if (condition) {
for (int i = 0; i < n; i++) {
process_a(data[i]);
}
} else {
for (int i = 0; i < n; i++) {
process_b(data[i]);
}
}
优化点不是“更聪明”,而是:把一次不确定的分支,换成一次确定的执行路径。在嵌入式里,这种“看起来啰嗦”的写法,常常才是工程上真正安全且可分析的代码。
五、功耗约束:程序在“消耗能量”¶
很多新手以为功耗完全是硬件的事:芯片型号、供电电压、工艺制程。可事实是,软件行为在功耗上起着直接且可观的作用。
一句话概括:
你的程序在运行的每一秒,都在真实地消耗能量。
5.1 软件行为决定功耗¶
下列看似“无害”的软件行为,都会直接转成电流消耗:
- 忙等待(busy loop)
- 高频轮询外设状态
- 外设常年保持开启
- 系统被频繁、无意义地唤醒
即便 CPU “什么都不做”,只要还在执行指令、还在跑时钟,功耗就持续在发生。换言之:“CPU 在忙”本身就是一种能量消耗状态。
5.2 面向低功耗的软件设计¶
嵌入式低功耗设计的核心,不是“算得更快”,而是:
该醒的时候醒,该睡的时候睡。
常见策略有:
- 用事件驱动替代轮询
- 使用中断而不是 while-loop
- 合理进入 Sleep / Stop / Standby 模式
- 把零散工作合并成批量处理
典型的低功耗主循环长这样:
void main_loop(void) {
while (1) {
// 检查是否有事件待处理
if (!event_pending()) {
// 无事件时进入低功耗模式
enter_sleep_mode();
wait_for_interrupt(); // 硬件特定指令
}
// 处理所有待处理事件
process_all_events();
}
}
高级之处不在复杂逻辑,而在明确告诉系统:没事别硬撑,让硬件帮你省电。在嵌入式里,写得“更聪明”的代码——往往比写得“更快”的代码更省电。
六、启动时间约束:从上电到可用¶
在很多嵌入式场景里,“启动完成”不是模糊概念,而是写进需求的硬指标:必须在限定时间内进入可用状态。
6.1 启动时间为何重要¶
这些场景尤其敏感:
- 工业控制(上电即需进入控制状态)
- 汽车电子(不能“慢慢想”)
- 消费电子(用户体验)
你不能像 PC 那样“转圈加载”,系统必须在规定时间内、以可预测的方式变为可用。
6.2 启动链路的成本¶
典型启动链路:
- 上电复位
- BootROM 执行
- Bootloader 初始化
- 外设与内存初始化
- 进入主控制逻辑
链路上的每一步,都会消耗启动时间。原则就是:只做必须做的事,复杂或非关键的初始化尽量延后。
// 只初始化必要的外设,延迟初始化其他
void system_init(void) {
init_clock(); // 必须首先初始化
init_watchdog(); // 尽早启用看门狗
init_critical_io(); // 关键 IO 初始化
// 非关键外设延迟初始化
// init_uart(); // 移到需要时初始化
// init_spi(); // 同上
}
这种“克制”的初始化方式,常常是达成启动时间指标的关键。
七、实时性与确定性:嵌入式系统的灵魂¶
7.1 实时性并不等于“快”¶
新手常把“实时”等同于“更快”,其实实时系统更关心的是:
时间约束是否可被满足。
- 硬实时(Hard Real-Time):一旦超时,系统判失败。
- 软实时(Soft Real-Time):允许偶尔超时,但必须可控。
是否实时,取决于能否在最坏情况下仍按时完成任务。
7.2 确定性(Determinism)¶
确定性意味着:在相同输入与状态下,程序的执行路径、耗时与结果都是可预测的。回头看前面的约束,你会发现它们都指向同一个目标:
- Flash 约束,限制功能规模
- RAM 策略,避免运行期不确定性
- CPU 约束,强迫可分析的执行路径
- 功耗与启动约束,限制系统行为模型
嵌入式系统真正的价值,不在于“跑得多快”,而在于:
在最坏情况下,依然可控。
下面是一个极简但确定性的调度器示例:
// 简单的周期任务调度器
typedef struct {
void (*task)(void);
uint32_t period_ticks;
uint32_t last_run;
} scheduled_task_t;
void scheduler_run(void) {
uint32_t now = get_system_tick();
for (int i = 0; i < NUM_TASKS; i++) {
if ((now - tasks[i].last_run) >= tasks[i].period_ticks) {
tasks[i].task(); // 执行任务
tasks[i].last_run = now; // 更新执行时间
}
}
}
它不复杂、不华丽,但行为是可分析、可推导、可验证的——这正是嵌入式最看重的特质。