第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.cdata_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系列来举例。

课后思考:你现在手头的项目,如果让你按分层架构重新组织,你会怎么拆?试着画一张架构图出来。