【嵌入式架构】项目越来越难维护?从全局变量到分层架构的避坑指南
做过几个嵌入式项目后你大概率会遇到这种情况——项目刚开始开发得飞快后面越来越难维护。新增一个功能要改十几个文件。修一个Bug冒出来三个新Bug。新人看代码看得头皮发麻。老员工走了以后没人敢动代码。问题到底出在哪今天我想把这件事掰开了聊。一、项目是怎么一步步失控的每个项目刚开始的时候代码不到100行逻辑简单结构清晰开发效率极高。然后需求像潮水一样涌来——按键处理、OLED显示、蓝牙通信、温湿度采集、电池管理、OTA升级、手机APP对接、云平台数据同步、日志系统……工程规模从几百行膨胀到几万行。然后各种诡异的问题开始冒头改了蓝牙代码显示功能莫名其妙挂了。修了显示逻辑功耗突然飙了。加了一个传感器OTA升级开始失败。这不是个别现象而是几乎所有嵌入式项目都会经历的失控曲线代码量在涨开发周期在涨Bug数量也在涨而且涨得越来越快。二、问题真的出在代码质量吗很多人第一反应是代码写得差。但代码质量差只是表象。真正的问题是系统复杂度失控了。随着需求增加模块越来越多、数据越来越多、依赖关系越来越复杂。如果没有架构设计来约束系统会像滚雪球一样不断膨胀最后变成一团拆不开的毛线球。打个比方你在一个房间里堆东西刚开始随便放找什么都容易。但东西越来越多之后没有分类、没有标签、没有固定位置最后你自己都不知道什么东西在哪。嵌入式软件也一样。模块之间的依赖关系如果不受控复杂度就会指数级增长。我见过一个项目光梳理模块间的调用关系就花了两个人一周。一周什么正事都没干就画了张依赖图。三、全局变量——最危险的代码说到依赖失控全局变量是头号元凶。刚开始用extern全局变量的时候你会觉得非常方便——想用数据直接访问想修改状态直接赋值开发速度飞快。但随着项目变大全局变量越来越多每个模块都可以访问和修改最后没人知道一个变量在哪里被修改、哪些模块依赖它。看似只修改了一个变量实际上影响了整个系统。看看这张图。5个全局变量6个模块15条依赖线。这还只是一个简化后的例子。真实项目中几十个全局变量、上百条依赖线是常态。全局变量最大的危害不是不安全是不可追踪。你改了一个变量的值根本不知道这个改动会影响到哪些模块影响有多深。我之前review过一个项目一个sys_mode变量被7个文件读写改了一个赋值语句三个不相关的功能同时出了问题。查了两天才定位到原因。四、模块之间为什么越来越耦合来看一个典型的场景显示模块里直接判断ble_connected这个变量。// display.cvoiddisplay_update(void){if(ble_connected){lcd_show_icon(ICON_BT_ON);}else{lcd_show_icon(ICON_BT_OFF);}}看起来没什么问题对吧但这里已经埋下了隐患——显示模块依赖了蓝牙模块的内部状态。未来蓝牙逻辑变化比如从直连变成网关转发显示模块也必须跟着修改。这就是耦合。耦合最大的特点是修改一个模块影响多个模块。项目越大问题越明显。左边是典型的耦合设计——每个模块互相直接访问牵一发而动全身。右边是解耦设计——模块之间通过事件总线通信改一个模块不影响其他模块。说实话左边那种全连接的拓扑我见过太多团队这么干了。刚开始就两三个模块互相调用很自然。等模块多了才发现依赖关系已经是一团乱麻想拆都拆不开。五、为什么后期开发越来越慢很多团队都有这样的经历第一版开发用了3个月。第二版用了6个月。第三版用了1年。为什么因为后期开发的时间大部分不是在写代码而是在理解代码。阅读旧代码、查找依赖关系、回归测试——这些时间占了开发周期的大头。说明系统复杂度已经超过了团队的掌控能力。没有架构设计的项目开发效率会随着版本迭代急剧下降。有架构设计的项目效率曲线平稳得多。两条线之间的差距就是架构设计的价值。我带过一个项目V3.0的时候新人入职光熟悉代码就花了三周。后来花了两个月重构V4.0的时候新人一周就能上手。这两个月的重构投入后面省下来的时间远不止两个月。六、那好的项目是怎么做的我参与过一个20万行代码、5年开发周期的智能穿戴产品。期间经历多次功能升级系统依然稳定。回头看做对的事情其实就几件每模块职责单一不越界。比如传感器模块只管采集不管显示也不管上报。想获取数据调接口。// sensor_module.h — 只暴露接口不暴露内部intsensor_init(void);intsensor_read(float*temp,float*humi);voidsensor_deinit(void);// sensor_module.c — 内部状态用static保护staticfloats_last_temp0;staticfloats_last_humi0;staticbool s_initializedfalse;模块之间不直接访问对方内部通过事件通信。typedefenum{EVT_TEMP_CHANGED,EVT_BT_CONNECTED,EVT_BT_DISCONNECTED,EVT_BATTERY_LOW,}event_type_t;typedefstruct{event_type_ttype;int32_tvalue;}event_t;voidevent_subscribe(event_type_ttype,void(*handler)(event_t*));voidevent_publish(event_t*evt);数据有唯一拥有者。温度数据归传感器模块管蓝牙状态归蓝牙模块管。其他模块想用这些数据订阅事件不要直接去读全局变量。这三件事说起来简单做起来需要坚持。尤其是项目赶进度的时候最容易妥协的就是这些原则。但每次妥协都是在给未来埋债。七、分层架构把原则落地上面说的这些原则落地最实用的框架就是分层架构。四层结构从下往上HAL层封装寄存器操作向上提供GPIO/SPI/UART/I2C等抽象接口。换芯片只改这一层。模块层是各类硬件驱动传感器驱动、通信驱动、存储驱动。每个模块独立编译、独立测试。服务层放协议栈、数据管理、OTA等跨模块功能。封装模块层能力向上提供业务级接口。应用层是业务逻辑、状态机、GUI页面。只调服务层接口不直接碰底层。有一条纪律必须遵守上层只能调下层的接口不能跨层访问更不能反向依赖。违反这条分层就白做了。我见过不少号称分层的项目应用层直接调HAL层函数操作寄存器服务层反过来调应用层的回调。这种分层比不分还糟因为给人造成了有架构的错觉实际上依赖关系比扁平结构还乱。八、架构设计到底在干什么说了这么多架构设计就一个目的控制复杂度。项目长大以后决定系统寿命的不是编码能力而是你能不能把复杂度控制在团队能驾驭的范围内。模块边界控制模块内部的复杂度让每个模块能被单独理解。统一接口控制模块之间的复杂度让依赖关系可追踪。数据管理控制数据流动的复杂度让数据变更可追溯。三者配合才能把系统复杂度控制在团队能驾驭的范围内。好的架构是什么样的新功能容易扩展Bug容易定位新人能在一周内上手模块可以跨项目复用。差的架构是什么样的改一处崩三处Bug越修越多新人三周还看不懂代码换个项目全部推倒重来。写在最后做了这么多年嵌入式我越来越觉得限制工程师成长的已经不是语法也不是驱动开发能力而是系统设计能力。从写功能到设计系统这是嵌入式工程师最重要的一次跨越。很多人写了十年代码还是停留在实现需求的层面从来没想过怎么让代码在三年后还能被维护。项目越来越难维护通常不是因为代码写得差而是模块边界不清晰、全局变量泛滥、模块耦合严重、缺少统一接口、系统复杂度失控。软件架构的本质就是控制复杂度。听起来朴素做到需要坚持。