本节摘要:性能优化的专业姿态不是"哪里慢优化哪里",而是先编预算表、再按偏差优化:时间预算论证响应能力,内存预算防住资源危机,功耗预算兑现电池寿命。本节给出三本预算表的编制方法、每本账的典型优化手段与优先级排序,并用一个真实项目演示从预算超支到收敛的完整过程。
嵌入式系统的资源是硬约束——Flash 就那么大,主频就那么高,电池就那颗。预算思维的三原则由此而来。原则一:最坏情况记账。演示看平均,产品看最坏:响应时间按"最长 ISR 加最深嵌套"算,内存按"最深调用加最大并发"算,2.4 节的功耗账已经是这个算法。原则二:先测量后优化。直觉式优化十次有九次砍错树——先量出每段的真实耗时与每个区域的真实占用,再决定砍哪里。原则三:留 20% 余量。预算打满的设计在第一次需求变更时就会崩,余量是给未来留的活路。
时间预算回答"系统跟得上吗"。编制方法分四步:列出所有周期性任务与其周期(按键扫描 20ms、控制环 1ms、上报 1s);列出所有中断及其最坏执行时间(打点法实测,5.4 节的方法);算 CPU 占用率——各任务的"最坏执行时间除以周期"求和,经验红线是 70%(留出中断、抖动与未来的余量);对有硬性截止期的关键链路(中断响应到输出动作)单独画链路账,累加每一环的最坏耗时。
/* 时间预算的测量底座:节拍计数与打点 */ volatile uint32_t g_ticks; /* 1ms 系统节拍 */ uint32_t profile_start, profile_cost; /* 打点法测耗时 */ profile_start = g_ticks; /* 粗粒度:毫秒级 */ do_control_loop(); profile_cost = g_ticks - profile_start; /* 细粒度用调试引脚:进函数拉高 出函数拉低,示波器量 */ #define PIN_HIGH() (GPIOB->BSRR = (1 << 5)) #define PIN_LOW() (GPIOB->BSRR = (1 << (5 + 16)))
优化的优先级排序有讲究。第一优先:算法与频次——把 1ms 才需要做的事放到 10ms 做,收益是十倍级的且无风险;第二优先:搬家——重活从 ISR 搬进任务,从 CPU 搬给外设(DMA、硬件滤波);第三优先:编译器优化等级与查表代替计算(4.5 的 gamma 表、7.1 的校准曲线都是查表思维的实例);最后才轮到手工微调代码(寄存器变量、循环展开)——收益小、可读性代价大,穷尽前三层之前别碰。
内存预算回答"装得下吗、撑得住吗"。三张子表:Flash 表(代码段、常量、各中间件的实际占用,链接器报告直接给出,优化手段是功能裁剪与优化等级);RAM 静态表(全局与静态变量、各任务栈、各缓冲区,累加后与总容量对照);栈水位实测表——这张最要紧,2.2 节的"栈踩内存"案例已经演示过栈溢出的杀伤力,实测方法是把栈空间预先填充固定字节(如 0xA5),跑完最重负载路径后检查填充残留,得出栈的真实最深用量,再乘 1.5 倍做定值。
| RAM 预算示例(32KB RAM 的项目) | 预算 | 实测 | 占比 |
|---|---|---|---|
| 全局与静态变量 | 6KB | 4.2KB | 13% |
| 主循环栈 | 2KB | 实测 0.9KB,定值 1.5KB | 5% |
| 通信任务栈 | 1.5KB | 实测 0.6KB,定值 1KB | 3% |
| 串口收发缓冲 | 2KB | 2KB | 6% |
| 协议与日志缓冲 | 3KB | 2.5KB | 8% |
| RTOS 内核与空闲 | 2KB | 2KB | 6% |
| 余量 | 15.5KB | —— | 48% |
这张表的读法:占比合计没到 70% 前,内存不是瓶颈,别为省几百字节牺牲可读性;一旦逼近,按"缓冲区减配、任务栈按实测重定、最后才是算法重构"的顺序收敛。堆的问题在第 2.2 节已经裁决过——嵌入式尽量不用,预算表里自然也没有堆的位置。
功耗预算回答"电池撑得到承诺吗"。方法在 2.4 节已经完整示范(电流账:各状态电流乘时长累加,除以电池容量再打折),本节补充预算视角的两个要点。其一,预算要分摊到模块:整机平均电流目标 50µA,就拆成 MCU 均摊 20µA、传感器均摊 15µA、无线均摊 15µA——超支时立刻知道拧哪个旋钮。其二,验证要上功耗实测:预算表算得再好,也要用采样电阻加示波器或功耗分析仪量一次真实电流曲线,对照预算找偏差——常见偏差源是"以为睡了其实没睡"(某外设时钟没关、引脚配置漏了),实测曲线会直接把"没睡的时间段"亮出来。
背景:某穿戴设备样机,电池寿命实测只有承诺值的一半——平均电流 240µA,预算 120µA,超支一倍。操作:按三本账逐层排查。功耗实测曲线显示两个异常:每 10 秒有一个 80mA 持续 200ms 的大电流尖峰(无线模块的搜网动作),以及 MCU 几乎没有回到停机态的时间段。解读与收敛:搜网尖峰是大头——把上报间隔从 10 秒放宽到 60 秒(需求侧谈判),并把搜网策略从"全频段扫描"改为"记住上次频点优先尝试"(每次省六成时间),尖峰摊薄后均摊电流降 90µA;"没睡"问题根因是调试串口时钟未关加一个引脚悬空,修复后停机电流从 400µA 降到 6µA,均摊再降 40µA。两轮合计降到 105µA,达标并留 12% 余量。整个过程没有一行"性能优化代码"——预算思维优化的常常是需求参数与配置疏漏,而不是算法。变式:若三本账都收敛后仍不达标,那就是选型错了——预算先于选型,反过来验证了第 1.4 节"选型前先算账"的方法论。
预算思维还有个常被忽视的推论——知道什么时候不优化。立三条纪律。一,没量过不优化:直觉指认的热点十有八九不是真热点(人总是怀疑慢语句,而真凶常是意外的函数调用与中断抖动),先测后改是硬纪律。二,没超支不优化:预算表显示 CPU 占用 35%、内存余量过半、功耗达标——那就不动。提前优化买来的是难懂的代码与引入新缺陷的机会,还堵死了后续需求的空间(精简过的代码最难再塞功能)。三,影响可读性须过门槛:任何用可读性换性能的手法(手工展开循环、查表替代计算、汇编热点),必须拿实测数字证明收益达到量级差(十倍起),并在注释里留好"优化前版本去哪找"的路标。性能是预算表上的一个数字,不是工程师的荣誉勋章——预算达标的项目里,朴素可读的代码就是最优代码。
建两次。选型前建"纸面版":用数据手册参数与需求推算三本账的初步数字,它的作用是排除明显不行的芯片——这比"买回来试"便宜两个数量级(呼应 1.4 选型)。集成完成后建"实测版":逐项替换成实测值,与纸面版对照,差异超过两成的条目必须解释(不是参数错了就是有漏算的消耗源)。此后预算表转为回归工具:每次大改动后重测关键项,账目劣化在合并前拦截。两份表的差距本身就是学习材料——它量出的是你"从参数表到真实电路"的推算误差,做三个项目后误差会收敛到一成以内,那时你的纸面预算就有了直接指导选型的资格。