第1章:嵌入式软件架构设计原则
各位同学,大家好。我是你们这门课的主讲。今天咱们聊点实在的——嵌入式软件架构设计原则。说实话,我见过太多血糖仪项目,代码写得跟一团乱麻似的。功能倒是能跑,可一旦要加个新特性,或者换个传感器,那改起来简直要命。
为什么会这样?说白了,就是架构没设计好。今天这一章,我把自己这些年踩过的坑、总结的经验,统统倒给你们。
1.1 分层架构:把系统拆成“千层饼”
分层架构,你想想看,就像做千层饼。每一层只管自己的事,层与层之间通过固定的接口打交道。这样做的好处是什么?我举个例子——
有一次,客户突然说要换血糖试纸的型号。新试纸的通信协议完全不同。如果代码是揉成一团的,那完了,整个项目得重写。但如果是分层架构呢?我只需要改最底层的驱动层,上面的业务逻辑完全不用动。
在血糖仪里,我习惯把软件分成这几层:
- HAL(硬件抽象层):把MCU的寄存器操作封装成标准接口。比如“读GPIO”、“写SPI”。
- BSP(板级支持包):针对具体电路板,初始化时钟、配置引脚。说白了,就是让板子“活过来”。
- Driver(驱动层):操作具体外设,比如血糖传感器驱动、LCD屏幕驱动、按键驱动。
- Middleware(中间件):提供通用服务,比如FATFS文件系统、FreeRTOS实时内核、加密算法库。
- Application(应用层):业务逻辑,比如测量流程、数据存储、报警判断。
核心原则:上层只能调用下层,下层绝不能反过来调用上层。这叫“单向依赖”。
嗯,这里要注意。很多新手会犯一个错误:在驱动层里直接写业务逻辑。比如在按键驱动里判断“长按3秒进入设置模式”。千万别这么干!驱动层只负责“检测到按键按下”,至于按下后干什么,那是应用层的事。
1.2 模块化设计:每个模块都是“乐高积木”
模块化设计,说白了就是把功能拆成一个个独立的“积木”。每个积木有明确的输入、输出、职责。我当年做第一个血糖仪项目时,把整个测量流程写在一个2000行的文件里。结果呢?每次调试都要从头看到尾,改一个bug能引出三个新bug。
后来我学乖了。一个典型的血糖仪测量模块,我会拆成这样:
| 模块名称 | 职责 | 接口 |
|---|---|---|
| SensorManager | 管理传感器初始化、测量、校准 | Init(), Measure(), Calibrate() |
| DataProcessor | 处理原始数据,计算血糖值 | Process(rawData) → glucoseValue |
| DisplayManager | 控制屏幕显示内容 | ShowResult(value), ShowError(code) |
| StorageManager | 读写历史记录到Flash | SaveRecord(record), LoadRecords() |
| AlarmManager | 判断是否触发高/低血糖报警 | CheckAlarm(value) → alarmType |
每个模块的代码放在单独的文件里。比如 sensor_manager.c、data_processor.c。头文件只暴露必要的接口函数,内部实现细节全部隐藏。
我的小技巧:每个模块的.c文件,函数数量控制在10-15个以内。超过这个数,说明这个模块太“胖”了,该拆了。
1.3 接口隔离原则:别让上层知道太多
接口隔离原则,用大白话说就是:只给调用者它需要的东西,别把一堆用不上的接口塞给它。
我曾经接手过一个项目,底层驱动暴露了50多个函数给上层。应用层开发者每次调用都要翻半天文档,还经常用错。后来我重构时,把接口精简到10个以内。
举个例子。血糖仪的LCD驱动,我只会暴露三个接口:
// 屏幕驱动接口(头文件 lcd_driver.h)
void LCD_Init(void); // 初始化屏幕
void LCD_Clear(void); // 清屏
void LCD_DisplayString(uint8_t x, uint8_t y, const char* str); // 显示字符串
至于底层是怎么操作SPI的、用什么寄存器、DMA怎么配置的——统统不暴露。上层开发者根本不需要知道这些。
警告:千万别为了“方便”把内部函数也声明成全局的。我见过有人把中断服务函数都写在头文件里,结果别人不小心调用了,系统直接崩溃。血的教训。
1.4 低耦合高内聚:让模块“各扫门前雪”
低耦合,就是模块之间依赖少。高内聚,就是模块内部功能紧密相关。这两个是孪生兄弟。
怎么判断耦合度高不高?我有个土办法:看改一个模块时,需要改多少个其他模块。如果需要改3个以上,那耦合度就太高了。
举个例子。血糖仪里有个“测量完成”事件。低耦合的做法是这样:
// 测量模块完成测量后,只发一个事件
void Measurement_Complete(void) {
Event_Post(EVENT_MEASUREMENT_DONE, glucoseValue);
}
// 其他模块各自注册事件处理
void Display_OnMeasurementDone(Event_t* event) {
LCD_DisplayResult(event->data.value);
}
void Storage_OnMeasurementDone(Event_t* event) {
Storage_SaveRecord(event->data.value);
}
高耦合的做法呢?测量模块直接调用显示模块和存储模块的函数。一旦显示模块要改接口,测量模块也得跟着改。这就是耦合。
高内聚的例子:DataProcessor 模块只做数据处理,不做显示、不做存储、不做报警。它的所有函数都是围绕“把原始数据变成血糖值”这一件事。
记住:一个函数只做一件事。一个模块只做一类事。这是低耦合高内聚的落地方法。
1.5 避坑指南:我踩过的那些坑
最后,分享几个我亲身经历的教训:
- 坑一:我曾经把所有驱动都写在一个文件里,结果编译一次要3分钟。后来拆成模块,编译时间降到30秒。
- 坑二:有个项目,应用层直接操作了GPIO寄存器。后来换了MCU,所有代码都得重写。如果当时用了HAL层,只需要改底层实现。
- 坑三:接口设计得太“胖”。一个初始化函数传了8个参数,调用者根本记不住顺序。后来改成结构体传参,清晰多了。
嗯,这一章的内容就到这里。记住:架构设计不是写代码,而是思考代码之间的关系。花时间把架构想清楚,后面写代码就是水到渠成的事。
下一章,咱们聊聊具体的HAL层怎么设计。到时候我会拿一个真实的血糖仪MCU——STM32L4系列来举例。
课后思考:你现在手头的项目,如果让你按分层架构重新组织,你会怎么拆?试着画一张架构图出来。