【嵌入式架构】项目越来越难维护?从全局变量到分层架构的避坑指南
做过几个嵌入式项目后,你大概率会遇到这种情况——
项目刚开始开发得飞快,后面越来越难维护。新增一个功能要改十几个文件。修一个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_temp=0;staticfloats_last_humi=0;staticbool s_initialized=false;模块之间不直接访问对方内部,通过事件通信。
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越修越多,新人三周还看不懂代码,换个项目全部推倒重来。
写在最后
做了这么多年嵌入式,我越来越觉得,限制工程师成长的已经不是语法,也不是驱动开发能力,而是系统设计能力。
从"写功能"到"设计系统",这是嵌入式工程师最重要的一次跨越。很多人写了十年代码,还是停留在"实现需求"的层面,从来没想过怎么让代码在三年后还能被维护。
项目越来越难维护,通常不是因为代码写得差,而是模块边界不清晰、全局变量泛滥、模块耦合严重、缺少统一接口、系统复杂度失控。
软件架构的本质就是控制复杂度。听起来朴素,做到需要坚持。
