嵌入式系统的可预测性

作者 Leehyon HNG | 1275 字, 3 分钟 | 0 评论 | 2026-05-25 | 栏目 notes

coding, cpp, design

最近在修 QAC 报的一些错误,也因此重新认识了很多规则。比如:

  • POD
  • constexpr
  • 禁止全局构造
  • 控制 static
  • Init 顺序

开始的时候,我不以为然,只是当成要做的任务一样,枯燥乏味,但总是要做的。唯一的成就感是看着自己负责模块的 issues 数字越来越少,当然里面有很大一部分是通过 // PRQA S 给屏蔽掉了。

后来,不能说是“良心发现”,大概率是不想浪费了时间却还是一知半解,这也归因于我对代码还是有点洁癖的,便开始思考规则背后的缘由,整理如下。

能跑的代码,为什么仍然是“错误”的

在一般的软件开发中,能跑就行,我们平时也看到过很多梗图。确实如此,比如 PC 上,大不了杀了重启。但在嵌入式,能跑可能还不太够。就比如哪天你开着导航还唱着歌,车机突然黑屏重启了,这忍忍也能接受,但如果涉及到驾驶安全,我们需要系统不仅“能运行”,而且要非常“可靠”。

所以,嵌入式系统更关心的是另一件事:

系统行为是否可预测。

为什么“可预测性”是嵌入式(汽车电子)的核心

嵌入式系统不是运行在资源无限、调度宽松的环境里。它通常面对的是:

  • 固定周期任务
  • 有限 RAM / Flash
  • 中断打断
  • 严格的启动顺序
  • 明确的实时性要求

在这样的系统里,代码不只是“实现功能”,它还定义了系统在时间、顺序和状态上的行为。如果这些行为不可预测,系统就很难被验证,也很难被信任。

这些规则在解决什么“本质问题”

表面上看,QAC 在限制很多东西:

  • 不要随便用全局变量
  • 不要全局构造复杂对象
  • 尽量使用 constexpr
  • 控制 static
  • 明确初始化顺序
  • 避免隐式依赖

但本质上,它们是在减少三类不确定性。

1. 时间的不确定性

std::vector<int> data;
data.push_back(1);

这段代码看起来很普通,但在嵌入式里,问题是:

  • 是否会触发动态分配?
  • 分配需要多久?
  • 是否可能失败?
  • 是否会造成堆碎片?
  • 最坏执行时间是多少?

如果这些问题无法回答,那它就不适合出现在实时路径里,因为没法估算可执行时间。

2. 顺序的不确定性

// A.cpp
Foo foo;

// B.cpp
Bar bar(foo);

这类代码最大的问题不是语法,而是初始化顺序不一定由你控制。所以规范会要求:

  • 禁止复杂全局构造
  • 全局对象只允许 POD / constexpr
  • 复杂初始化放到显式 Init() 阶段
  • 谨慎使用静态局部变量
  • 避免跨文件初始化依赖

本质上,是把“隐式顺序”变成“显式顺序”。

3. 状态的不确定性

static int state;

单就这行代码没啥问题,但如果它被多个任务、中断或模块间接访问,问题就变复杂了。全局变量和静态变量的问题,不只是“作用域大”。更大的问题是:

谁能改它?什么时候改?是否并发访问?是否有生命周期约束?

所以好的设计会尽量让状态:

  • 有明确 owner
  • 有明确生命周期
  • 有明确访问边界
  • 有明确同步策略

尽量让系统在运行前就已经确定

所以,不难理解:

  • constexpr 的价值,是把计算提前到编译期
  • POD 的价值,是避免运行期构造逻辑
  • 禁止全局复杂对象的价值,是避免启动阶段出现不可控行为
  • 控制 static 的价值,是避免隐藏状态和隐式初始化
  • 显式初始化顺序的价值,是让依赖关系写在代码里,而不是交给编译器、链接器或运行时

思考模型

以后写嵌入式 C++ 时,我觉得可以反复问自己四个问题:

  • 它什么时候初始化?编译期?启动?第一次调用?
  • 它依赖什么?
  • 它耗时是否固定?
  • 它的状态谁能改?

如果以上问题都能全部回答,那你的设计就是嵌入式友好的。

嵌入式 C++ 的所有“限制”,本质不是限制功能,而是把 C++ 从“自由系统”变成“可预测”,进而“可证明”。

相关文章