项目实战四:嵌入式传感器驱动框架(抽象层、硬件层、应用层)

好,咱们今天聊点实在的。前面几章我们把模块化编程的理论、多文件编译、接口设计都过了一遍。但说实话,这些东西光看代码是记不住的,你得亲手搭一个框架,踩几个坑,才能真正理解。

这一章,我带你做一个嵌入式传感器驱动框架。说白了,就是一套通用的代码结构,让你以后换传感器、换MCU,都不用从头写驱动。我自己的经验是,框架搭得好,后面能省80%的调试时间。

为什么需要分层?

你想想看,一个传感器驱动,最核心的需求是什么?

  • 能读到数据
  • 数据是准确的
  • 换一个传感器,改动尽量小
  • 换一个MCU,改动也尽量小

嗯,这就是分层的意义。我们把驱动拆成三层:

  • 硬件层:管I2C、SPI、GPIO这些底层操作
  • 抽象层:管传感器的寄存器读写、初始化、数据转换
  • 应用层:管业务逻辑,比如温度高了报警、数据上报频率

我在一个智能家居项目里,一开始没分层,所有代码揉在一个文件里。后来客户要换传感器,我改了三天,还改出了bug。从那以后,我再也不敢不分层了。

核心原则:硬件层只做"怎么读写",抽象层只做"传感器是什么",应用层只做"数据怎么用"。三层之间通过接口函数通信,不要跨层调用。

先画个框架图

下面这张图,是我做驱动框架时必画的。你照着这个结构去写代码,基本不会乱。

应用层 app_sensor.c / app_sensor.h 业务逻辑:数据采集周期、报警阈值、上报策略 调用抽象层接口 抽象层 sensor_drv.c / sensor_drv.h 传感器初始化、数据读取、寄存器映射、校准 调用硬件层接口 硬件层 hal_i2c.c / hal_spi.c / hal_gpio.c I2C读写、SPI收发、GPIO高低电平控制

你看,这个结构很清晰。每一层只跟相邻层打交道,绝不越级。我见过有人直接在应用层里写I2C读写函数,那叫一个酸爽——换MCU的时候,所有代码都得重写。

硬件层:把底层操作封装好

硬件层是最底层的。它负责跟MCU的寄存器打交道。我一般会把它做成一个独立的模块,叫 hal_i2c.c 或者 hal_spi.c

举个例子,一个I2C读写的接口,我会这样设计:

// hal_i2c.h
#ifndef HAL_I2C_H
#define HAL_I2C_H

#include <stdint.h>

// 初始化I2C外设
void hal_i2c_init(uint32_t freq);

// 写一个字节到从设备
int hal_i2c_write_byte(uint8_t dev_addr, uint8_t reg_addr, uint8_t data);

// 从从设备读一个字节
int hal_i2c_read_byte(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data);

// 从从设备读多个字节
int hal_i2c_read_multi(uint8_t dev_addr, uint8_t reg_addr, 
                       uint8_t *buf, uint16_t len);

#endif

这里有个细节:返回值用 int,0表示成功,负数表示错误。为什么?因为I2C通信可能失败(比如设备没应答),你不能假设每次读写都成功。我在一个项目里吃过这个亏——没检查返回值,结果读到的全是0xFF,还以为是传感器坏了。

我的习惯:硬件层的函数名统一加 hal_ 前缀。这样在代码里一眼就能看出哪些是硬件相关操作,方便后期移植。

抽象层:把传感器逻辑独立出来

抽象层是核心。它不关心I2C怎么实现的,它只关心传感器的寄存器怎么配置、数据怎么转换。

比如一个温湿度传感器SHT30,我会这样设计:

// sensor_drv.h
#ifndef SENSOR_DRV_H
#define SENSOR_DRV_H

#include <stdint.h>

// 传感器数据结构
typedef struct {
    float temperature;
    float humidity;
    uint8_t crc_ok;   // 校验是否通过
} sensor_data_t;

// 初始化传感器
int sensor_init(void);

// 读取温湿度
int sensor_read(sensor_data_t *data);

// 获取上次读取的时间戳(毫秒)
uint32_t sensor_get_last_read_time(void);

#endif

你看,抽象层的接口跟硬件完全无关。它只暴露"初始化"、"读取数据"、"获取时间"这些概念。至于底层是用I2C还是SPI,抽象层不关心。

实现的时候,抽象层会调用硬件层的接口:

// sensor_drv.c
#include "sensor_drv.h"
#include "hal_i2c.h"

#define SHT30_ADDR  0x44
#define CMD_MEASURE 0x2C06

int sensor_init(void) {
    // 初始化I2C
    hal_i2c_init(100000);  // 100kHz
    
    // 发送初始化命令
    int ret = hal_i2c_write_byte(SHT30_ADDR, 0x00, 0x00);
    if (ret != 0) {
        return -1;  // 初始化失败
    }
    
    return 0;
}

int sensor_read(sensor_data_t *data) {
    uint8_t buf[6];
    
    // 触发测量
    hal_i2c_write_byte(SHT30_ADDR, 0x2C, 0x06);
    
    // 等待测量完成(实际项目中用延时或轮询)
    delay_ms(20);
    
    // 读取6字节数据
    hal_i2c_read_multi(SHT30_ADDR, 0x00, buf, 6);
    
    // 数据转换(SHT30的原始数据转温湿度)
    data->temperature = -45.0 + 175.0 * (buf[0] << 8 | buf[1]) / 65535.0;
    data->humidity = 100.0 * (buf[3] << 8 | buf[4]) / 65535.0;
    
    // CRC校验(略)
    data->crc_ok = 1;
    
    return 0;
}

注意:抽象层里不要出现任何跟MCU相关的代码。比如 delay_ms() 函数,它应该由应用层提供,或者通过回调函数注入。我曾经在一个项目里直接在抽象层调了 HAL_Delay(),结果换到另一家MCU时,所有延时都得改,烦死了。

应用层:只管业务逻辑

应用层是最上层的。它只调用抽象层的接口,不关心传感器怎么工作的。

// app_sensor.c
#include "sensor_drv.h"
#include "app_sensor.h"

#define TEMP_ALARM_THRESHOLD  50.0f  // 温度报警阈值

void app_sensor_task(void) {
    sensor_data_t data;
    
    // 初始化传感器
    if (sensor_init() != 0) {
        // 处理初始化失败
        return;
    }
    
    while (1) {
        // 读取传感器数据
        if (sensor_read(&data) == 0) {
            // 检查温度是否超限
            if (data.temperature > TEMP_ALARM_THRESHOLD) {
                app_alarm_trigger();  // 触发报警
            }
            
            // 上报数据
            app_data_report(data.temperature, data.humidity);
        }
        
        // 每5秒采集一次
        delay_ms(5000);
    }
}

你看,应用层里没有I2C读写,没有寄存器地址,没有数据转换公式。它只关心"温度高了报警"、"每5秒采集一次"这些业务逻辑。

这样做的好处是什么?我举个例子。假设客户说"把报警阈值改成60度",你只需要改应用层的一个宏定义。如果客户说"换一个传感器",你只需要重写抽象层,应用层几乎不用动。

避坑指南:我踩过的几个坑

嗯,这里分享几个我实际项目中遇到的坑,你写代码的时候注意避开。

  • 坑一:硬件层不检查返回值。我曾经在I2C读写后直接使用数据,结果设备没应答,读到的全是0xFF,导致温度显示-45度。从那以后,我每个I2C操作都检查返回值。
  • 坑二:抽象层里写死延时。不同MCU的延时函数不一样,抽象层里写死 HAL_Delay() 会导致移植困难。我现在的做法是:抽象层定义一个 delay_ms 函数指针,由应用层在初始化时注入。
  • 坑三:应用层直接操作寄存器。我见过有人为了"性能优化",在应用层直接调用I2C读写。结果换传感器时,应用层代码改得面目全非。记住:应用层只调用抽象层的接口,别越级。

总结一下

这个传感器驱动框架,说白了就是三个字:分层解耦。硬件层管通信,抽象层管传感器,应用层管业务。每一层只做自己的事,通过接口跟相邻层通信。

我做了这么多年嵌入式,最大的体会就是:代码结构比代码本身更重要。结构好了,后期维护、移植、扩展都轻松。结构乱了,改一个功能可能引发三个bug。

好,这一章就到这里。你把这个框架理解透了,后面做多传感器系统、做驱动库,都会顺手很多。

一句话记住:硬件层管"怎么读写",抽象层管"传感器是什么",应用层管"数据怎么用"。别越级,别混层。


公众号:蓝海资料掘金营,微信deep3321