Skip to content

Commit ca4fce2

Browse files
feat: new C++98 and Embedded Limitations
1 parent 46d5fb7 commit ca4fce2

4 files changed

Lines changed: 1718 additions & 3 deletions

File tree

drafts/Content-Table-Draft.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
### 第0章 一切的前提
66

77
- [x] 前言
8-
- [ ] 嵌入式的资源与实时约束(Flash/ROM、RAM、CPU、功耗、启动时间、确定性)
8+
- [x] 嵌入式的资源与实时约束(Flash/ROM、RAM、CPU、功耗、启动时间、确定性)
99
- [x] 急速C语言速通复习
10-
- [ ] 传统C++98比C多了什么呢?
10+
- [x] 传统C++98比C多了什么呢?
1111
- [ ] 语言选择原则:何时用 C++、用哪些 C++ 特性(折中与禁用项)
1212
- [ ] 性能 vs 可维护性的真实取舍
1313
- [ ] 编码规范建议(小型嵌入式友好的 C++ 子集/风格指南)
Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
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

Comments
 (0)