夜雨聆风学习资料网

ARTICLE · 1224832

嵌入式软件开发:在只有64KB的世界里,写出能活十年的代码

嵌入式软件开发:在只有64KB的世界里,写出能活十年的代码

一句话概括:你写的不是一段程序,是一个要在没有屏幕、没有日志、没有重启机会的环境里,独自运行十年的东西。

刚入行那年,我做的第一个项目是给一块电表加个远程抄表功能。

需求文档只有三页,我花了两周写完代码,烧进板子,跑通了。当时觉得自己挺行。半年后客户加了个需求:要在原有协议上兼容一款老设备。我打开自己的代码,盯着那个 1200 行的 main.c,沉默了十分钟——里面有 17 个全局变量,4 层 switch 嵌套,还有一个我至今没搞懂为什么会进入的 if 分支。

那次改造花了我三个星期,并且引入了一个只在特定温度下才会出现的死机问题。

一、第一性原理:资源是有限的,而且你逃不掉

做互联网开发的朋友常说"性能不够就加机器"。这句话放在嵌入式世界里,约等于"饿了就多吃点",前提是你得先有钱。

典型嵌入式项目的硬约束

MCU 主频 72MHz · RAM 64KB · Flash 256KB · 功耗预算 30mA · 成本卡在 8 块钱以内,BOM 已定稿。

第一,你得知道每一字节去哪了。

栈有多深?最大的中断嵌套会吃掉多少?一个 printf("%f") 会链接进多少 KB 的库代码?这些不是"优化阶段再考虑"的事,是设计阶段就要算清楚的账。

我见过太多项目在功能做完 90% 的时候突然发现 RAM 不够,然后开始疯狂地把 int 改成 int16_t,把全局数组改成动态分配——最后 RAM 是够了,稳定性没了。

第二,性能优化的顺序和 PC 端是反的。

PC 上先写清楚,再优化热点。嵌入式上,架构选错,后面无论怎么优化都是徒劳。用轮询还是中断?用阻塞还是状态机?数据放栈上还是静态区?这些决策在第一周就做完了,而它们决定了这个项目能不能活到第三年。

第三,成本是硬约束,不是软指标。

多一个 32.768kHz 晶振,单板成本涨三毛;量产十万台,就是三万块。所以在嵌入式里,很多时候"技术最优解"要让位于"成本最优解"。这不庸俗,这是工程。

二、硬件不是黑盒,是你的合作方

一个真实的幽灵 bug

设备在实验室跑了一周没问题,装机到现场,每天凌晨三点准时重启。查了两周,发现是供电模块在低温下纹波超标,导致 MCU 的 BOR(欠压复位)误触发。"凌晨三点"只是因为那是厂房温度最低的时候。

这个 bug 教会我一件事:软件工程师必须理解硬件,至少理解到"它会怎么坑你"的程度。

1时序。I2C 的上升沿时间、SPI 的建立保持时间、ADC 的采样窗口。datasheet 里那些你没看的表格,就是 bug 的来源。

1电气特性。上拉电阻多大?引脚能不能直接驱动 LED?ESD 防护做了没有?

1器件的脾气。有些 Flash 在写入时会拉低总线电压,有些传感器上电后需要 100ms 才能响应,有些 RTC 芯片在掉电切换时会丢一个字节。

最实用的建议:从第一天起就养成读 datasheet 的习惯,而不是读别人的例程。例程能跑通,不代表它是对的。很多厂商的 demo 为了"好看",故意省略了错误处理和延时的边界条件,你直接抄过来,坑就埋下了。

三、并发不是"开个线程",是"抢同一个东西"

嵌入式的并发模型和服务器完全不同。没有操作系统帮你调度,没有锁可以用,你手上只有:中断、主循环,以及一个可能存在的 RTOS。

铁律一:ISR 要短,极短

理想情况下只做三件事:读寄存器、置标志位、退出。所有计算、判断、字符串处理,全部丢给主循环。

因为 ISR 执行期间,同级和低级中断都被阻塞。你在 ISR 里多做 10 微秒,就可能让一个串口丢一个字节,让一个电机控制环超时。

铁律二:共享变量的读写必须有明确保护

一个 32 位变量在 8 位机上不是原子的。主循环正在读它,中断进来改了一半,读到的是什么?没人知道。

解决办法很朴素:临界区短到极致,加个开关中断;或者用"双缓冲 + 标志位"模式,让中断只写备用区,主循环从主区读。

铁律三:用 RTOS 就先想清楚优先级

最常见的错误是给所有任务都很高的优先级,结果等于没有优先级,还多了调度开销和优先级反转的风险。

四、内存:能用静态分配,就别用 malloc

通用系统的 malloc 经过几十年优化,有复杂的内存池、碎片整理策略。而嵌入式上的 malloc 往往是精简版,甚至是某个开源库的简化实现——它不保证实时性,不保证碎片可控,更不保证在长时间运行后还能分配出连续块。

一个跑三个月的设备,每天分配释放几千次,内存碎片会缓慢累积。到某一天,一个原本 200 字节的分配突然失败,设备就"莫名其妙"死机了。

策略

做法

收益

静态优先

容量上限在设计阶段确定,用静态数组/静态区

零碎片,行为可预期

内存池

固定大小块,开几个池对应几种尺寸,永不归还系统

分配时间恒定,无碎片

算栈

工具分析最坏情况栈深,留 30% 余量,计入中断嵌套

避免栈溢出踩内存

配额制

每个任务/模块划定内存上限

责任清晰,防止互相吃光

五、调试:printf 不是唯一手段,但常常是最好的

很多新人觉得 printf 调试"不专业"。我的看法正相反:在没有 JTAG、没有串口、设备已经封装在现场的情况下,能留下有效日志的能力,决定了你能不能解决问题。

1分级。ERROR / WARN / INFO / DEBUG,发布版本只留前两级。别在量产固件里开着 DEBUG,既拖性能又泄露信息。

1有上下文。不要只打印 "error",要打印"模块 + 错误码 + 关键参数 + 时间戳"。

1环形缓冲。把最近 N 条日志存在 RAM 的环形缓冲区里,死机重启后还能读出来。这一招救过我三次。

1HardFault 要能自证。在异常处理里保存 PC、LR、栈指针和关键寄存器,重启后打印出来,配合 map 文件就能定位到出错的代码行。

除此之外,值得掌握的还有:逻辑分析仪(看时序问题几乎是唯一手段)、SWD 断点与变量监视、内存使用分析(map 文件、stack usage 报告)。

工具不贵,时间很贵。一个两三百块的分析仪,能省你两周的排查。

六、架构:让代码活过三年

回到开头那个 1200 行的 main.c。问题不在于写得丑,而在于它没有边界。

一个好的嵌入式架构,至少要回答三个问题:

1. 业务逻辑和硬件操作分开了吗?

驱动层只管寄存器,中间层做协议和状态机,应用层做业务。这样换一颗 MCU,你只需要重写驱动层;改一个业务规则,你不用碰寄存器。

2. 状态机是显式的吗?

嵌入式里 80% 的复杂逻辑,本质都是状态机。用 switch(state) 显式写出来,比一堆 if-else 加标志位好维护一百倍。而且状态机能画图、能评审、能穷举测试。

3. 模块之间的通信是收敛的吗?

事件/消息机制、统一的回调注册、明确的数据流向。而不是"A 模块直接改 B 模块的全局变量"。

我给团队定的最低标准是:任何一个新同事,应该能在半小时内说清楚这个项目的模块划分和数据流。 说不清,就是架构有问题,不是他能力不行。

七、测试:在你看不见的地方,代码必须是对的

嵌入式测试难,但不是不能做。

1单元测试可以做。 把纯逻辑(协议解析、算法、状态机)抽出来,在 PC 上编译测试,用 Ceedling / Unity 这类框架。硬件相关的部分打桩。

1HIL(硬件在环)值得投入。 一台自动化测试台,能跑回归、能模拟异常输入、能连续跑一周。它抓到的 bug,比人工测试多一个数量级。

1异常路径要主动测。 断电、通信中断、数据越界、传感器返回非法值。这些场景在实验室永远不会自然发生,但它们在生产现场每周都会发生。

1看门狗不是万能的,但没有它是万万不能的。 关键是喂狗的位置要正确——不能在中断里喂,那等于关掉了它。

八、给刚入行的朋友

第一年

死磕基础。

C 语言的指针和内存模型、MCU 启动流程、中断向量表、链接脚本、时钟树。这些是地基,急不得。

第二年

完整做一个产品。

从原理图评审到量产测试全流程走一遍。你会明白软件只是系统的一部分。

第三年

关心可维护性。

读优秀开源项目:FreeRTOS、lwIP、LittleFS。看他们怎么做抽象、处理错误、写注释。

然后你会发现,嵌入式这行最迷人的地方在于:它是软件世界里少数几个"你必须同时对抽象和物理世界负责"的地方。

结尾

写这篇文章的时候,我翻出了那个 1200 行的 main.c。

它还在,作为反面教材存在我的硬盘里。有时候我会想,如果当年有人告诉我"先画模块图再写代码",那三个星期是不是能省下来。

但大概也不会。工程这门手艺,终究是要自己踩过坑,才知道坑有多深。

只希望你踩的时候,能少踩几个。

如果这篇对你有用,欢迎点赞、在看、转发给正在被 HardFault 折磨的同事。

相关学习资料