第二章 固件架构设计:分层架构思想、驱动层/中间件层/应用层划分、模块解耦原则
好,咱们进入正题。这一章聊的是固件架构,说白了就是代码该怎么组织。很多新手工程师上来就写,写到后面发现改一个功能要动半座山。我早期也干过这种事,后来被现实教育了——一个万用表项目,客户要加个自动量程切换,我改了三天,还引入了两个新bug。从那以后,我老老实实研究架构。
2.1 分层架构思想:为什么非分不可?
你想想看,一个万用表固件里有什么?ADC采样、量程切换、按键扫描、LCD显示、数据校准、通信协议……如果全揉在一起,那代码就是一团乱麻。分层架构的核心思想就一句话:各司其职,互不干扰。
我个人习惯把固件分成三层:
- 驱动层:直接跟硬件寄存器打交道。比如操作ADC、GPIO、SPI、I2C。
- 中间件层:封装通用功能。比如数据滤波、校准算法、协议解析。
- 应用层:业务逻辑。比如用户按了哪个键、该显示什么、量程要不要切换。
为什么要这么分?因为每一层只关心自己的事。驱动层不知道什么叫“电压超量程”,它只管把ADC值读回来。应用层不知道SPI时序怎么配,它只管说“我要读电压”。这样改驱动不影响应用,改应用不影响驱动。嗯,这就是解耦。
核心原则:上层依赖下层,下层绝不依赖上层。驱动层不能调用应用层的函数,应用层只能通过接口调用下层服务。
2.2 驱动层:把硬件“藏”起来
驱动层是离硬件最近的一层。它的任务就是把硬件细节封装成简单的API。比如ADC驱动,你不需要让上层知道是哪个通道、什么触发方式、怎么配置寄存器。你只需要提供:
// 驱动层接口示例
int32_t drv_adc_read_channel(uint8_t ch);
void drv_adc_init(void);
void drv_adc_set_range(uint16_t range);
我在项目中遇到过一个问题:某款MCU的ADC在连续采样时会有通道串扰。如果应用层直接操作寄存器,那每个用到ADC的地方都得加补偿代码。后来我把补偿逻辑封装在驱动层,上层调用者完全不知道这回事。这就是驱动的价值——把脏活累活自己扛了。
我的习惯:驱动层函数命名统一加 drv_ 前缀,一眼就能看出是哪个层的东西。另外,驱动层尽量不包含业务判断,比如“如果电压超过3.3V就切换量程”——这种逻辑绝对不要放在驱动里。
2.3 中间件层:复用才是王道
中间件层是承上启下的关键。它不直接操作硬件,也不处理用户交互,而是提供通用算法和数据处理能力。比如:
- 滑动平均滤波
- 校准曲线拟合
- 单位换算(mV转V、Ω转kΩ)
- 协议帧解析(比如Modbus)
举个例子,万用表测量电阻时,原始数据是ADC码值,但用户想看的是欧姆数。中间件层就负责做这个转换:
// 中间件层:电阻测量处理
float mid_resistance_calc(uint16_t adc_raw, uint8_t range) {
// 查表 + 线性插值
float voltage = drv_adc_raw_to_voltage(adc_raw, range);
float current = drv_reference_current(range);
return voltage / current; // R = V / I
}
你看,中间件层调用了驱动层的接口,但它自己不知道硬件细节。它只做数学运算和逻辑判断。这样如果以后换了ADC芯片,我只需要改驱动层,中间件层的代码一行都不用动。
避坑指南:我曾经把校准参数直接写在中间件层的代码里,结果每次换硬件都要重新编译。后来我改成从EEPROM读取校准参数,中间件层只负责“用参数”,不负责“存参数”。这样灵活多了。
2.4 应用层:业务逻辑的“总指挥”
应用层是用户看得见的部分。它负责:
- 按键事件处理
- 测量模式切换(电压/电流/电阻)
- 量程自动/手动切换
- LCD显示刷新
- 数据保存与通信
应用层不关心ADC怎么配置,也不关心滤波算法具体怎么算。它只调用中间件层和驱动层的接口,然后组合成完整的业务流程。比如自动量程切换的逻辑:
// 应用层:自动量程切换
void app_auto_range(void) {
float value = mid_measure_voltage(); // 调用中间件
if (value > 10.0f) {
drv_adc_set_range(RANGE_100V); // 调用驱动
} else if (value > 1.0f) {
drv_adc_set_range(RANGE_10V);
} else {
drv_adc_set_range(RANGE_1V);
}
}
你看,应用层只关心“值太大就换大量程”,至于怎么换、换的时候要不要延时、要不要重新校准——那是驱动层和中间件层的事。这就是分层的好处:各层各干各的,出了问题也好定位。
2.5 模块解耦原则:别让代码“牵一发动全身”
模块解耦,说白了就是让每个模块尽量独立。一个模块改了,其他模块最好不受影响。我总结了几个实用原则:
| 原则 | 说明 | 反面例子 |
|---|---|---|
| 接口最小化 | 只暴露必要的函数,内部实现全部隐藏 | 驱动层暴露了寄存器地址给应用层 |
| 单向依赖 | 上层依赖下层,下层不依赖上层 | 驱动层回调应用层的函数 |
| 数据驱动 | 模块间通过数据结构传递信息,不直接调用 | 一个模块直接修改另一个模块的全局变量 |
| 回调机制 | 用回调函数实现反向通知,避免硬编码 | 驱动层直接调用LCD刷新函数 |
举个例子,按键模块检测到长按事件,它不应该直接调用“切换模式”的函数。而是应该通过回调或者消息队列,把事件发出去。应用层注册一个回调函数,收到事件后再决定做什么。这样按键模块可以复用,不管你是用在万用表还是示波器上。
我的经验:解耦不是一蹴而就的。我通常先写出能跑的代码,然后重构时逐步拆分。一开始就追求完美解耦,反而容易过度设计。记住一个原则:当你在一个文件里看到两个不相关的功能时,就该拆了。
2.6 实战中的分层示例
最后,我画一个简单的分层调用关系,帮你理解实际项目中怎么用:
应用层(app_)
├── app_main.c // 主循环、状态机调度
├── app_key_handler.c // 按键事件处理
├── app_display.c // 显示刷新逻辑
└── app_measure.c // 测量流程控制
中间件层(mid_)
├── mid_filter.c // 数据滤波
├── mid_calibration.c // 校准算法
├── mid_conversion.c // 单位换算
└── mid_protocol.c // 通信协议解析
驱动层(drv_)
├── drv_adc.c // ADC驱动
├── drv_lcd.c // LCD驱动
├── drv_key.c // 按键驱动
├── drv_eeprom.c // EEPROM驱动
└── drv_uart.c // 串口驱动
每一层内部还可以继续细分模块。比如驱动层的 drv_adc.c 里,可以再分初始化、采样、校准三个子模块。但对外只暴露三个函数:drv_adc_init()、drv_adc_read()、drv_adc_calibrate()。这就是封装。
小建议:刚开始做分层时,别怕多写几个文件。一个文件只放一个模块,命名规范统一。我见过有人把所有驱动写在一个2000行的文件里……嗯,那维护起来真是噩梦。宁可文件多,不要文件乱。
好了,这一章就到这里。分层架构不是银弹,但它能让你的代码更清晰、更可维护。下一章咱们聊聊状态机设计,那才是万用表固件的灵魂。到时候我会结合一个完整的量程切换状态机,带你一步步实现。