第二章 固件架构设计:分层架构思想、驱动层/中间件层/应用层划分、模块解耦原则

好,咱们进入正题。这一章聊的是固件架构,说白了就是代码该怎么组织。很多新手工程师上来就写,写到后面发现改一个功能要动半座山。我早期也干过这种事,后来被现实教育了——一个万用表项目,客户要加个自动量程切换,我改了三天,还引入了两个新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行的文件里……嗯,那维护起来真是噩梦。宁可文件多,不要文件乱。

好了,这一章就到这里。分层架构不是银弹,但它能让你的代码更清晰、更可维护。下一章咱们聊聊状态机设计,那才是万用表固件的灵魂。到时候我会结合一个完整的量程切换状态机,带你一步步实现。