|
| 1 | +# 嵌入式的资源与实时约束 |
| 2 | + |
| 3 | +## 一、前言:为什么嵌入式不能“随便写” |
| 4 | + |
| 5 | +在 PC 或服务器开发中,我们习惯于一种“默认”前提:内存不够可以加、算力不足可以扩、系统调度由操作系统兜底。程序的目标往往只需满足“功能正确 + 平均性能尚可”。 |
| 6 | + |
| 7 | +但嵌入式系统并不生活在这样的世界里。在嵌入式环境中,资源是被严格量化的:Flash 可能只有几十 KB,RAM 只有几 KB,CPU 主频只有几十 MHz,而系统却承担着实时控制、设备安全、工业或消费级可靠性等责任。在这里,程序不只是“能跑”就够了,它还必须: |
| 8 | + |
| 9 | +- 在规定时间内完成任务 |
| 10 | +- 在最坏情况下依然行为正确 |
| 11 | +- 在有限资源下保持长期稳定运行 |
| 12 | + |
| 13 | +嵌入式工程的本质,是在一个资源受限的世界里,追求系统行为的确定性。当然,这一部分跟C++喜欢把一些事情藏起来存在冲突,但是用好C++中的大部分特性,的确可以推动不小的性能改进。 |
| 14 | + |
| 15 | +## 二、Flash / ROM 约束:代码不是“免费”的 |
| 16 | + |
| 17 | +### 2.1 Flash 的现实规模 |
| 18 | + |
| 19 | +嵌入式系统中,程序存储空间首先受到 Flash / ROM 容量的严格限制: |
| 20 | + |
| 21 | +- STM32F103:64KB ~ 128KB Flash |
| 22 | +- STM32F4 系列:512KB ~ 2MB Flash |
| 23 | +- 低端 MCU:甚至只有 16KB |
| 24 | + |
| 25 | +与动辄几十 MB 可执行文件的 PC 程序相比,这种容量差异是数量级的鸿沟。 |
| 26 | + |
| 27 | +### 2.2 Flash 约束如何影响软件设计 |
| 28 | + |
| 29 | +在这样的环境下,“写什么代码”本身就是一种工程决策。代码体积直接决定系统是否可部署,功能冗余意味着真实的存储浪费。引入库不再是“好不好用”,而是“**能不能放下**”(对的,笔者真见过printf一拉进来二进制文件爆炸性拉大)。因此,嵌入式工程师必须掌握一些常见的编译器优化选项: |
| 30 | + |
| 31 | +- 编译器优化选项(如 `-Os` 优化代码大小) |
| 32 | +- 函数与段级别的垃圾回收(`-ffunction-sections`, `-fdata-sections` 配合 `--gc-sections`) |
| 33 | +- 精确控制链接行为 |
| 34 | + |
| 35 | +这个别着急,后面我们有一个专门的章节好好了解。 |
| 36 | + |
| 37 | +## 三、RAM 约束:内存不是“用完就算” |
| 38 | + |
| 39 | +如果说 Flash 约束限制了“能写多少功能”,那么 RAM 约束则直接影响系统是否能稳定运行。 |
| 40 | + |
| 41 | +### 3.1 RAM 的数量级现实 |
| 42 | + |
| 43 | +嵌入式系统中,RAM 往往只有:2KB / 8KB / 20KB / 64KB。在这样的环境下,我们真有可能搞出来栈溢出,把SP指针跑飞了,而且,如果内存管理算法搞的不好,我们的实时系统可能在数小时或数天后,其堆碎片就会把系统搞崩溃了(allocate行为找不到合适的buffer了) |
| 44 | + |
| 45 | +### 3.2 栈的风险 |
| 46 | + |
| 47 | +栈空间主要消耗于:函数调用深度、中断嵌套、局部变量。在嵌入式中,以下行为往往被严格限制甚至禁止。 |
| 48 | + |
| 49 | +- 你肯定是不允许搞递归的——我们都知道递归的本质是自己调自己,一不小心把栈叠太大了,会直接把系统搞崩了(毕竟我们没办法预知到底要迭代多少,你算的过来多少,用户和其他任务堆栈可不管你) |
| 50 | +- 大型的局部数组也不要开,仍然一样——把栈叠太大了,会直接把系统搞崩了 |
| 51 | + |
| 52 | +一次不可预期的栈增长就可能直接破坏系统。 |
| 53 | + |
| 54 | +如果你真的需要大数组,这样玩: |
| 55 | +```c |
| 56 | +// 避免大型局部数组 |
| 57 | +void process_data(void) { |
| 58 | + // 不建议:uint8_t buffer[4096]; // 可能溢出 |
| 59 | + // 建议:使用静态或全局内存,或分段处理 |
| 60 | + static uint8_t buffer[256]; // 或从内存池分配 |
| 61 | +} |
| 62 | +``` |
| 63 | +
|
| 64 | +### 3.3 堆的风险 |
| 65 | +
|
| 66 | +运行期动态内存分配在嵌入式系统中一直是高风险操作: |
| 67 | +
|
| 68 | +- `malloc`/`free` 的时间复杂度不可预测 |
| 69 | +- 长期运行会产生内存碎片 |
| 70 | +- 错误难以复现与调试 |
| 71 | +
|
| 72 | +成熟的嵌入式系统通常会采用: |
| 73 | +
|
| 74 | +- 启动阶段一次性分配 |
| 75 | +- 内存池 / 对象池 |
| 76 | +- 完全静态内存模型 |
| 77 | +
|
| 78 | +```c |
| 79 | +#define POOL_SIZE 1024 |
| 80 | +#define BLOCK_SIZE 32 |
| 81 | +#define NUM_BLOCKS (POOL_SIZE / BLOCK_SIZE) |
| 82 | +
|
| 83 | +static uint8_t memory_pool[POOL_SIZE]; |
| 84 | +static bool block_used[NUM_BLOCKS] = {0}; |
| 85 | +
|
| 86 | +void* mempool_alloc(void) { |
| 87 | + for (int i = 0; i < NUM_BLOCKS; i++) { |
| 88 | + if (!block_used[i]) { |
| 89 | + block_used[i] = true; |
| 90 | + return &memory_pool[i * BLOCK_SIZE]; |
| 91 | + } |
| 92 | + } |
| 93 | + return NULL; // 无可用内存 |
| 94 | +} |
| 95 | +``` |
| 96 | + |
| 97 | +在嵌入式系统中,内存管理首先服务于确定性,而不是便利性。 |
| 98 | + |
| 99 | +## 四、CPU 约束:算力是被精确计数的 |
| 100 | + |
| 101 | +在 PC/服务器的世界里,我们习惯把 CPU 当成一个“几乎用不完”的资源: |
| 102 | +算法慢一点?加个缓存;分支多一点?交给乱序执行;浮点算不动?硬件来兜底。CPU 在那儿更像个背景板——只要不太慢就行。但在嵌入式里,事情不是“快不快”的问题,**CPU 是个需要被精确计量与精确预算的资源**。当然,现代的芯片,如果资源不是很紧张,犯不着这样做,但是成本在这,你的老板一定会狠狠要求你压榨的,不是吗? |
| 103 | + |
| 104 | +------ |
| 105 | + |
| 106 | +### 4.1 MCU 的算力特征 |
| 107 | + |
| 108 | +典型 MCU 的算力特征,和桌面 CPU 几乎处在两个世界: |
| 109 | + |
| 110 | +- 主频有限(几十到几百 MHz) |
| 111 | +- 无乱序执行,基本严格顺序流水 |
| 112 | +- 分支预测能力弱,甚至没有 |
| 113 | +- Cache 极小,甚至没有 Cache |
| 114 | + |
| 115 | +结论很直接:在 MCU 上,代码的行为**几乎可以直接映射到指令流**。你写下的每一个 `if`、每一个循环、每一次函数调用,最终都会变成实打实的一条条指令,按顺序执行。 |
| 116 | + |
| 117 | +------ |
| 118 | + |
| 119 | +### 4.2 时间复杂度的“工程化” |
| 120 | + |
| 121 | +在嵌入式世界,时间复杂度往往不是 `O(n)` 那样的数学讨论,真正的问题是: |
| 122 | + |
| 123 | +> **这段代码,能不能在一个控制周期内跑完?** |
| 124 | +
|
| 125 | +比如: |
| 126 | + |
| 127 | +- 在没有 FPU 的 MCU 上,一个浮点运算可能要几十个周期。 |
| 128 | +- 一次整数除法,往往比几十次加减更昂贵。 |
| 129 | +- 中断响应时间取决于 CPU 当时正在执行的指令路径。 |
| 130 | + |
| 131 | +所以嵌入式工程师会做一些对桌面程序员看着“反直觉”的事: |
| 132 | + |
| 133 | +- 分析 **最坏执行时间(WCET)** |
| 134 | +- 避免不可预测的循环次数 |
| 135 | +- 控制分支数量,减少执行路径的不确定性 |
| 136 | +- 必要时看反汇编、手动估算 cycle 数 |
| 137 | + |
| 138 | +下面的例子看起来只是微小重构,但在 MCU 上意义重大: |
| 139 | + |
| 140 | +```c |
| 141 | +// 优化前:条件判断在循环内 |
| 142 | +for (int i = 0; i < n; i++) { |
| 143 | + if (condition) { |
| 144 | + process_a(data[i]); |
| 145 | + } else { |
| 146 | + process_b(data[i]); |
| 147 | + } |
| 148 | +} |
| 149 | +``` |
| 150 | + |
| 151 | +问题不在逻辑错误,而在于:**每一次循环都要经历一次分支判断**。在没有分支预测的 CPU 上,这就是稳定且可观的性能损耗。改法也很朴素: |
| 152 | + |
| 153 | +```c |
| 154 | +// 优化后:减少分支预测失败 |
| 155 | +if (condition) { |
| 156 | + for (int i = 0; i < n; i++) { |
| 157 | + process_a(data[i]); |
| 158 | + } |
| 159 | +} else { |
| 160 | + for (int i = 0; i < n; i++) { |
| 161 | + process_b(data[i]); |
| 162 | + } |
| 163 | +} |
| 164 | +``` |
| 165 | + |
| 166 | +优化点不是“更聪明”,而是:**把一次不确定的分支,换成一次确定的执行路径**。在嵌入式里,这种“看起来啰嗦”的写法,常常才是工程上真正安全且可分析的代码。 |
| 167 | + |
| 168 | +------ |
| 169 | + |
| 170 | +## 五、功耗约束:程序在“消耗能量” |
| 171 | + |
| 172 | +很多新手以为功耗完全是硬件的事:芯片型号、供电电压、工艺制程。可事实是,**软件行为在功耗上起着直接且可观的作用**。 |
| 173 | + |
| 174 | +一句话概括: |
| 175 | + |
| 176 | +> **你的程序在运行的每一秒,都在真实地消耗能量。** |
| 177 | +
|
| 178 | +------ |
| 179 | + |
| 180 | +### 5.1 软件行为决定功耗 |
| 181 | + |
| 182 | +下列看似“无害”的软件行为,都会直接转成电流消耗: |
| 183 | + |
| 184 | +- 忙等待(busy loop) |
| 185 | +- 高频轮询外设状态 |
| 186 | +- 外设常年保持开启 |
| 187 | +- 系统被频繁、无意义地唤醒 |
| 188 | + |
| 189 | +即便 CPU “什么都不做”,只要还在执行指令、还在跑时钟,功耗就持续在发生。换言之:**“CPU 在忙”本身就是一种能量消耗状态。** |
| 190 | + |
| 191 | +------ |
| 192 | + |
| 193 | +### 5.2 面向低功耗的软件设计 |
| 194 | + |
| 195 | +嵌入式低功耗设计的核心,不是“算得更快”,而是: |
| 196 | + |
| 197 | +> **该醒的时候醒,该睡的时候睡。** |
| 198 | +
|
| 199 | +常见策略有: |
| 200 | + |
| 201 | +- 用事件驱动替代轮询 |
| 202 | +- 使用中断而不是 while-loop |
| 203 | +- 合理进入 Sleep / Stop / Standby 模式 |
| 204 | +- 把零散工作合并成批量处理 |
| 205 | + |
| 206 | +典型的低功耗主循环长这样: |
| 207 | + |
| 208 | +```c |
| 209 | +void main_loop(void) { |
| 210 | + while (1) { |
| 211 | + // 检查是否有事件待处理 |
| 212 | + if (!event_pending()) { |
| 213 | + // 无事件时进入低功耗模式 |
| 214 | + enter_sleep_mode(); |
| 215 | + wait_for_interrupt(); // 硬件特定指令 |
| 216 | + } |
| 217 | + // 处理所有待处理事件 |
| 218 | + process_all_events(); |
| 219 | + } |
| 220 | +} |
| 221 | +``` |
| 222 | +
|
| 223 | +高级之处不在复杂逻辑,而在明确告诉系统:**没事别硬撑,让硬件帮你省电**。在嵌入式里,写得“更聪明”的代码——往往比写得“更快”的代码更省电。 |
| 224 | +
|
| 225 | +------ |
| 226 | +
|
| 227 | +## 六、启动时间约束:从上电到可用 |
| 228 | +
|
| 229 | +在很多嵌入式场景里,“启动完成”不是模糊概念,而是**写进需求的硬指标**:必须在限定时间内进入可用状态。 |
| 230 | +
|
| 231 | +------ |
| 232 | +
|
| 233 | +### 6.1 启动时间为何重要 |
| 234 | +
|
| 235 | +这些场景尤其敏感: |
| 236 | +
|
| 237 | +- 工业控制(上电即需进入控制状态) |
| 238 | +- 汽车电子(不能“慢慢想”) |
| 239 | +- 消费电子(用户体验) |
| 240 | +
|
| 241 | +你不能像 PC 那样“转圈加载”,系统必须在规定时间内、以可预测的方式变为可用。 |
| 242 | +
|
| 243 | +------ |
| 244 | +
|
| 245 | +### 6.2 启动链路的成本 |
| 246 | +
|
| 247 | +典型启动链路: |
| 248 | +
|
| 249 | +1. 上电复位 |
| 250 | +2. BootROM 执行 |
| 251 | +3. Bootloader 初始化 |
| 252 | +4. 外设与内存初始化 |
| 253 | +5. 进入主控制逻辑 |
| 254 | +
|
| 255 | +链路上的每一步,都会消耗启动时间。原则就是:**只做必须做的事,复杂或非关键的初始化尽量延后**。 |
| 256 | +
|
| 257 | +```c |
| 258 | +// 只初始化必要的外设,延迟初始化其他 |
| 259 | +void system_init(void) { |
| 260 | + init_clock(); // 必须首先初始化 |
| 261 | + init_watchdog(); // 尽早启用看门狗 |
| 262 | + init_critical_io(); // 关键 IO 初始化 |
| 263 | + |
| 264 | + // 非关键外设延迟初始化 |
| 265 | + // init_uart(); // 移到需要时初始化 |
| 266 | + // init_spi(); // 同上 |
| 267 | +} |
| 268 | +``` |
| 269 | + |
| 270 | +这种“克制”的初始化方式,常常是达成启动时间指标的关键。 |
| 271 | + |
| 272 | +------ |
| 273 | + |
| 274 | +## 七、实时性与确定性:嵌入式系统的灵魂 |
| 275 | + |
| 276 | +### 7.1 实时性并不等于“快” |
| 277 | + |
| 278 | +新手常把“实时”等同于“更快”,其实实时系统更关心的是: |
| 279 | + |
| 280 | +> **时间约束是否可被满足。** |
| 281 | +
|
| 282 | +- 硬实时(Hard Real-Time):一旦超时,系统判失败。 |
| 283 | +- 软实时(Soft Real-Time):允许偶尔超时,但必须可控。 |
| 284 | + |
| 285 | +是否实时,取决于能否在**最坏情况下**仍按时完成任务。 |
| 286 | + |
| 287 | +------ |
| 288 | + |
| 289 | +### 7.2 确定性(Determinism) |
| 290 | + |
| 291 | +确定性意味着:在相同输入与状态下,程序的执行路径、耗时与结果都是**可预测的**。回头看前面的约束,你会发现它们都指向同一个目标: |
| 292 | + |
| 293 | +- Flash 约束,限制功能规模 |
| 294 | +- RAM 策略,避免运行期不确定性 |
| 295 | +- CPU 约束,强迫可分析的执行路径 |
| 296 | +- 功耗与启动约束,限制系统行为模型 |
| 297 | + |
| 298 | +嵌入式系统真正的价值,不在于“跑得多快”,而在于: |
| 299 | + |
| 300 | +> **在最坏情况下,依然可控。** |
| 301 | +
|
| 302 | +下面是一个极简但确定性的调度器示例: |
| 303 | + |
| 304 | +```c |
| 305 | +// 简单的周期任务调度器 |
| 306 | +typedef struct { |
| 307 | + void (*task)(void); |
| 308 | + uint32_t period_ticks; |
| 309 | + uint32_t last_run; |
| 310 | +} scheduled_task_t; |
| 311 | + |
| 312 | +void scheduler_run(void) { |
| 313 | + uint32_t now = get_system_tick(); |
| 314 | + |
| 315 | + for (int i = 0; i < NUM_TASKS; i++) { |
| 316 | + if ((now - tasks[i].last_run) >= tasks[i].period_ticks) { |
| 317 | + tasks[i].task(); // 执行任务 |
| 318 | + tasks[i].last_run = now; // 更新执行时间 |
| 319 | + } |
| 320 | + } |
| 321 | +} |
| 322 | +``` |
| 323 | +
|
| 324 | +它不复杂、不华丽,但行为是**可分析、可推导、可验证的**——这正是嵌入式最看重的特质。 |
| 325 | +
|
0 commit comments