TL;DR本文面向嵌入式开发者总结了 10 条代码维护技巧——避免汇编、防止注释蠕变、不过早优化、简化 ISR、保留调试代码、编写硬件包装器、合理分解功能、重视文档、避免过度聪明、集中管理定义。核心目标是让代码在数十年后依然可读、可维护、可移植。1. 引言代码维护是应用程序开发的重要方面而为了缩短上市时间通常会忽略代码维护。对于某些应用程序这可能不会造成重大问题因为这些应用程序的寿命很短或者已部署该应用程序并且再也不会碰它。但是嵌入式系统应用程序的使用寿命可能长达数十年这意味着一些早期的错误可能会在以后导致可观的成本。在开发可能具有长寿命的嵌入式应用程序时在设计和实现上都必须考虑维护。以下技巧绝不会构成一个完整列表但是它们解决了一些常见问题这些问题可能会使您的应用程序维护者有理由诅咒您的名字并且不要忘记您可能是其中之一2. 提示 1避免使用汇编代码当然在低端 PIC 上您别无选择而在高端 ARM 上您可能不需要它但是在这两种极端之间有很多平台使用汇编代码来实现以下目的提高性能并减少代码大小。但是问题在于简单地选择使用汇编代码可能会使您的项目脱轨并使您陷入困境。尽管汇编代码允许您直接访问机器的功能但由于难以理解程序中正在发生的事情因此可以轻易地忽略性能优势。正是出于这个原因构思了高级语言例如 C 和 Java。由于调试高级语言的安全功能非常容易因此请在调试时将每条汇编代码都视为可疑的。如果必须使用汇编请在发表评论时尽量保持谨慎。在 C 或 Java 中注释可以使代码混乱但是将注释组合在一起可以节省大量时间和挫败感。您可以选择注释汇编块但请确保每个块中的指令不超过 5 或 6 条。理想情况下应该在注释中直接用伪代码拼写所使用的算法请参阅提示 8。下面通过一个对比表格直观地展示使用汇编代码与使用高级语言如 C在关键维度上的差异对比维度汇编代码高级语言如 C代码可读性指令级操作晦涩难懂逻辑意图不直观接近自然语言的抽象逻辑清晰、易于理解调试难度缺少类型检查与安全机制定位问题困难编译器提供类型检查与安全功能调试更轻松性能收益可精细控制每条指令极致优化空间大编译器优化成熟常规场景性能已足够可移植性与特定 CPU 架构强绑定换平台需重写源码级可移植换平台只需重新编译3. 提示 2避免注释蠕变这是一个通用的编程提示但是在长寿命应用程序中变得尤为重要的提示“管理您的注释与它们记录的代码的关联。随着代码的更新注释的迁移非常容易并且结果很难理解。以下示例说明了随着时间的推移注释蠕变的发生有多么容易//此函数将两个数字相加并返回结果#if__DEBUGvoidprintNumber(intnum){printf(Output: %d\n,num);}#endif//此函数将两个数字相乘并返回结果intmultiply(inta,intb){returna*b;}intadd(inta,intb){#if__DEBUG//调试输出printNumber(ab);#endifreturnab;}请注意功能“添加”的注释位于列表的顶部而实际功能则位于下方。如果注释和函数之间存在空格则可能会超时发生。可能的原因是在“add”及其注释描述之间添加了 printNumber 函数。后来有人看到有一个加法函数并且在其上加上 multiply 似乎是合乎逻辑的注释的蠕变导致代码内的文档脱节。要解决此问题请尝试将代码保留在其文档的功能内或通过在注释上方和下方插入行来使注释块非常明显。4. 提示 3不要过早优化编程的主要缺点之一是过早的优化。但是由于时间限制草率的编码或过分热心的工程师该规则在实践中经常被打破。您编写的任何程序都应尽可能简单地开始并且仍然提供所需的功能。如果需要性能请尝试简单地实现该程序即使它与性能不匹配。一旦测试并调试了完整的单元它是大型系统的编程器或组件然后回去进行优化。危险地优化代码会导致维护噩梦因为优化后的代码通常较难理解并且您可能无法理解您需要的性能结果。理想情况下使用探查器例如与 GCC 一起使用的 gprof 或 Intel 的 VTune来查看瓶颈所在并专注于这些领域——真正缓慢的事情可能会让您感到惊讶。下面通过一个对比表格直观地展示「过早优化」与「先实现后优化」两种做法在关键维度上的差异对比维度过早优化先实现后优化代码可读性优化技巧常让逻辑晦涩难懂可读性明显下降保持简单直白的实现代码更易被他人理解调试难度复杂优化叠加后难以定位问题调试成本高逻辑清晰、单元完整问题更容易被隔离和排查维护成本后续维护者难以理解意图长期维护负担沉重结构简单、改动风险低长期维护更省心性能收益常优化在非瓶颈处收益有限甚至得不偿失基于剖析数据精准优化性能提升更有效5. 提示 4ISR 应该很简单出于性能和维护方面的考虑中断服务例程ISR应该尽可能简单。作为异步性质的 ISR 本质上比“常规”程序代码更难调试因此将其责任降到最低对于您的应用程序的总体可维护性很重要。尝试将所有数据处理移出 ISR 并移至主程序中然后 ISR 仅负责获取数据例如从硬件中获取并将其放置在缓冲区中以备后用。可以使用一个简单的标志来向主程序发出信号通知有要处理的数据。6. 提示 5将调试代码保留在源文件中在开发过程中您可能会添加大量旨在调试“详细输出、声明、LED 闪烁等”的代码。当项目结束时可能很想删除其中的这些部分。代码以清理整个应用程序尤其是在随意添加调试代码的情况下。尽管清理应用程序是一个崇高的追求但是删除调试代码会在以后产生问题。任何试图维护该代码的人都可能会复制原始开发中创建的许多步骤如果代码已经存在则维护变得非常容易。如果需要在生产版本中删除代码请使用条件编译或将调试代码放在中央模块或库中不要将其链接到生产版本中。应用程序的初始开发应包括编写文档和清理调试代码的时间花费的额外时间将是值得的。7. 提示 6为系统调用编写包装器尝试通过接口将低级 I/O 例程与高级程序逻辑分开因为通过单片开发可以使程序难以管理。将应用程序的所有功能放到几个大功能中会使代码难以理解并且更难更新和调试。对于硬件接口尤其如此。您可能可以直接访问硬件寄存器或 I/O甚至可以访问平台供应商提供的 API但是有很多动机来创建自己的“包装程序”接口。您通常无法控制硬件的功能并且如果将来必须更改平台则在应用程序中使用特定于硬件的代码API 或直接操作这无关紧要将使移植更加困难。如果您创建自己的包装器接口就像创建为硬件 API 定义的宏一样简单则代码可以是一致的并且移植所需的所有更新都将位于集中位置。下面是一个基于 C 语言的硬件寄存器访问包装器示例通过宏封装底层寄存器操作将平台相关的细节集中到一处/* * hw_reg.h —— 硬件寄存器访问包装层 * 应用代码只调用 HW_REG_READ / HW_REG_WRITE * 移植到新平台时只需修改本文件无需改动上层逻辑。 * *//* 平台 A直接操作寄存器地址 */#defineHW_REG_BASE_ADDR0x40000000UL#defineHW_REG(offset)(*(volatileuint32_t*)(HW_REG_BASE_ADDR(offset)))/* 平台 B示例改用供应商提供的 API 时只需替换宏实现 *//* #define HW_REG(offset) platform_read_reg(offset) *//* 统一读接口读取指定偏移处的寄存器值 */#defineHW_REG_READ(offset)HW_REG(offset)/* 统一写接口向指定偏移处的寄存器写入值 */#defineHW_REG_WRITE(offset,val)(HW_REG(offset)(val))/* 示例控制 LED 的 GPIO 寄存器 */#defineGPIO_LED_OFFSET0x18#defineLED_ON0x01#defineLED_OFF0x00voidled_set(inton){if(on){HW_REG_WRITE(GPIO_LED_OFFSET,LED_ON);}else{HW_REG_WRITE(GPIO_LED_OFFSET,LED_OFF);}}上层业务代码只依赖HW_REG_READ/HW_REG_WRITE这两个宏完全不接触具体寄存器地址或供应商 API。将来更换硬件平台时只需修改hw_reg.h中的宏实现所有移植相关的更新都集中在这一处这正是包装器接口带来的可维护性收益。8. 提示 7仅分解功能嵌入式应用程序将与 PC 应用程序不同因为许多功能将专用于您正在使用的硬件。不建议将功能单元尽可能地拆分为最小——将单个作用域功能中的功能调用数保持在 5 或 6 以下并使硬件的功能单元与软件中的功能单元相对应。进一步分解程序将创建调用图的蜘蛛网从而使调试和理解变得困难。9. 提示 8文档保留所有文档以及代码理想情况下还应保留硬件副本。在记录应用程序时请尝试将尽可能多的设计和应用程序模型直接放入源代码中。如果必须将其分开则将其作为巨大的注释放入源文件中并将其链接到程序中。至少如果您使用版本控制系统例如 CVS 或 Microsoft Source Safe则将文档与源代码检查到同一目录中——如果不与源代码一起放置则丢失文档确实很容易。理想情况下将所有文档和源文件放在 CD或您选择的便携式存储设备上将其与使用的硬件和开发工具一起密封在袋子中然后将其放在安全的地方——您的后继者将感谢您。10. 提示 9不要机灵类似于过早的优化聪明的编码会导致麻烦。由于 C 和 C 仍然是嵌入式世界中的主导语言因此有很多方法可以解决一个问题。模板、继承、goto、三元运算符“”、列表会不断出现。真正聪明的程序员可以提出使用这些工具解决问题的极其紧凑和优雅的方法。问题是通常只有程序员才能理解聪明的解决方案以后可能会忘记它是如何工作的。唯一的解决方案是例如避免聪明并尽量减少使用深奥的语言功能例如不要依赖 C 语句中的短路评估也不要使用三元运算符进行程序控制使用 if 语句代替。11. 提示 10将所有定义放在一个地方如果您有很多常量定义或条件定义请将它们放在中央位置。这可能是单个文件或源代码目录但是如果将定义深埋在实现中它会再次咬住您。12. 总结以上 10 个技巧涵盖了嵌入式系统代码维护的常见问题从避免汇编代码、防止注释蠕变、避免过早优化到简化 ISR、保留调试代码、编写硬件包装器、合理分解功能、重视文档、避免过度聪明以及集中管理定义。这些实践的核心目标是一致的——让代码在数十年后依然可读、可维护、可移植。希望这些建议能帮助您和您的后继者减少维护噩梦。
