摘要 :在嵌入式世界里,摩尔定律告诉我们芯片会变快,但墨菲定律 告诉我们:凡是可能出错的事,必定会出错。I2C 总线会死锁,Flash 数据会翻转,晶振会停振,甚至宇宙射线会导致 RAM 位反转。本文将抛弃具体的代码实现,从系统设计哲学 的角度,探讨如何通过契约式设计、逻辑看门狗、数据完整性校验 以及故障安全(Fail-Safe)机制,构建一个具备"反脆弱"能力的工业级嵌入式系统。
一、 核心世界观:不要相信硬件,更不要相信自己
做上位机软件的工程师,默认 CPU 是完美的,内存是可靠的。 但做嵌入式系统的架构师,必须建立一个**"被迫害妄想症"**的世界观:
-
传感器在撒谎:ADC 读回来的 0V 可能是因为断线,而不是因为没电压。
-
存储器会失忆:EEPROM 里读出来的数据可能是乱码。
-
总线会罢工:I2C 从机可能随时把 SDA 拉低锁死总线。
-
程序会跑飞:PC 指针可能因为干扰跳到 Flash 的空白区域。
防御性编程的本质,就是不仅要处理"正常的业务逻辑",更要花费 50% 以上的代码量去处理"不可能发生的情况"。
二、 契约式设计 (Design by Contract):给函数戴上镣铐
很多工程师写函数,默认输入参数是合法的。这是取祸之道。 在防御性架构中,每个核心模块的入口和出口,都必须签署"契约"。
1. 前置条件 (Pre-condition)
在执行任何逻辑之前,先通过 ASSERT(断言)检查所有的输入。
-
指针是空的吗?
-
索引越界了吗?
-
状态机的当前状态是否允许执行这个操作?
-
硬件外设是否处于 Ready 状态?
注意:在嵌入式中,Release 版本通常不应该由编译器移除 ASSERT 。相反,ASSERT 触发后不应简单打印,而应记录日志并执行安全复位。
2. 后置条件 (Post-condition)
函数执行完了,不代表就成功了。
-
写入 Flash 后,有没有回读检查?
-
发送 DMA 后,传输完成标志位真的置位了吗?
-
计算完 PID 输出,结果是否在物理极限范围内(比如 PWM 占空比 0-100%)?
如果后置条件不满足,说明系统已经处于"不可预测"的状态,此时唯一的选择是报错或复位,而不是继续带病运行。
三、 驯服看门狗:不仅仅是喂狗
90% 的嵌入式项目,看门狗(WDT)形同虚设。 通常的做法是:在主循环 while(1) 的末尾放一个 FeedDog()。
这种做法的漏洞在于: 如果主循环还在跑,但定时器中断挂了 ?或者串口接收任务死锁了 ?或者传感器采集线程卡在 I2C 忙状态? 此时主循环依然在愉快地喂狗,但系统实际上已经瘫痪了(部分功能失效)。
高阶玩法:逻辑窗口看门狗
真正的工业级设计,采用**"签到机制"**。
-
系统有 A、B、C 三个关键任务。
-
每个任务必须在自己的循环内,设置专属的"存活标志位"。
-
只有当**"监工任务"**检查到 A、B、C 三个标志位在一个周期内都更新了,才去喂一次硬件看门狗。
-
如果有任何一个任务卡死,标志位不更新,监工不喂狗,系统复位。
看门狗不是用来防止死机的,是用来在死机后重启系统的最后一道防线。
四、 数据的反腐败:CRC 无处不在
你以为 RAM 里的变量写进去是 1,读出来就一定是 1 吗? 在强干扰环境下(如大功率电机旁),RAM 可能会发生位翻转。
1. 关键配置的保护
对于系统的核心参数(如过流阈值、校准系数),不能只存一个 float。 应该设计一个结构体,包含:数据本身 + 数据的反码 + CRC校验码。 每次使用这些参数前,先做完整性检查。如果不通过,说明内存已损坏,必须立即停机并从 Flash 恢复默认值。
2. 通信数据的保护
不要相信 UART/CAN/SPI 收到的任何数据,除非它通过了 CRC 校验。 不要试图去解析一个校验失败的数据包,直接丢弃。
3. 运行时堆栈保护 (Stack Canary)
编译器通常提供"栈金丝雀"功能。在函数进入时在栈底放一个魔术数,退出时检查该数。如果变了,说明发生了栈溢出,此时应立即触发 HardFault,而不是让程序带着被踩坏的局部变量继续跑。
五、 故障安全 (Fail-Safe) vs 故障运行 (Fail-Operational)
当系统检测到不可恢复的错误(如传感器断线、内存损坏)时,该怎么办? 这取决于你的系统设计目标。
1. 故障安全 (Fail-Safe)
适用于安全性优先 的设备(如电暖器、甚至电梯)。 原则:一旦出错,立即进入一个物理上最安全的状态。
-
电暖器控制器死机 -> 必须关闭加热管继电器。
-
无人机失控 -> 电机停转(或降落模式)。
-
医疗输液泵出错 -> 夹紧输液管并报警。
架构要求:GPIO 的默认上下拉电阻、继电器的常开/常闭触点选择,必须保证在 MCU 复位或断电时,处于"无害"状态。
2. 故障运行 (Fail-Operational)
适用于可用性优先 的设备(如汽车刹车、飞机飞控)。 原则:主系统挂了,备用系统必须顶上,功能可以降级,但不能消失。
-
主传感器坏了 -> 使用估算模型(观测器)维持运行。
-
主 MCU 死锁 -> 协处理器接管控制权。
六、 总结:从"代码工"到"架构师"
写出功能代码,只是嵌入式开发的起点。 架构师的价值,在于他能预判系统将会如何崩溃,并提前在废墟上设计好了逃生通道。
-
防御性:假设所有输入都是恶意的,所有外设都是坏的。
-
可观测性:死机时能留下遗言(保存日志到 Flash),而不是默默重启。
-
确定性 :在实时系统中,错误的答案比迟到的答案更好,但最坏的是不确定的答案。
当你的代码中充满了对错误的敬畏,你的系统才能在混乱的物理世界中长久生存。