3. 传感器驱动开发:I2C/SPI通信协议详解、寄存器读写封装、驱动分层架构(HAL->Driver->Sensor)
好,咱们进入第三章。这一章,我打算聊聊传感器驱动开发的核心。
你想想看,一个传感器,不管它多高级,最终都得跟主控芯片“说话”。怎么说话?无非就是 I2C 或者 SPI。这两种协议,是嵌入式工程师的必修课。我个人习惯,拿到一个新传感器,第一件事就是看它的通信接口——是 I2C 还是 SPI?这决定了后续所有代码的写法。
3.1 I2C 与 SPI:两种“对话”方式
先说 I2C。它用两根线:SCL(时钟)和 SDA(数据)。所有设备都挂在这两根线上,每个设备有个唯一的地址。主控发起通信,从设备应答。好处是省引脚,坏处是速度相对慢,而且总线上的设备多了,容易出问题。
我在项目中遇到过一件事:一个板子上挂了三个 I2C 传感器,结果其中一个老是丢数据。查了半天,发现是上拉电阻阻值选小了,导致信号边沿不够陡。嗯,这里要注意:I2C 总线的上拉电阻,不是随便选的,得根据总线电容和通信速率来算。
再说 SPI。它用四根线:SCLK(时钟)、MOSI(主出从入)、MISO(主入从出)、CS(片选)。每个从设备独占一根 CS 线。好处是速度快,全双工,而且协议简单。坏处是占引脚多。
我建议,如果传感器数据量大(比如摄像头、高精度 ADC),优先选 SPI。如果只是读个温度、湿度,I2C 就够了。
核心区别速览:
| 特性 | I2C | SPI |
|---|---|---|
| 线数 | 2(SCL, SDA) | 4(SCLK, MOSI, MISO, CS) |
| 通信方式 | 半双工 | 全双工 |
| 速度 | 标准 100k/400k,快的有 1M | 轻松上 10M,甚至更高 |
| 设备寻址 | 软件地址(7位或10位) | 硬件片选(CS) |
| 典型场景 | 温湿度、加速度计、EEPROM | 显示屏、SD卡、高速ADC |
3.2 寄存器读写封装:别裸奔
说白了,操作传感器就是读写它的寄存器。但你不能直接对着硬件寄存器操作,那样代码会乱成一锅粥。我见过太多新手,直接在应用层里写 HAL_I2C_Mem_Write(),然后满屏都是魔数地址。这不行。
我的做法是:封装一层寄存器读写函数。比如:
// 底层读写接口(基于 HAL 库)
static int32_t i2c_write_reg(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data, uint16_t len);
static int32_t i2c_read_reg(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data, uint16_t len);
// 传感器专用封装
int32_t bme280_write_reg(uint8_t reg, uint8_t value) {
return i2c_write_reg(BME280_ADDR, reg, &value, 1);
}
int32_t bme280_read_reg(uint8_t reg, uint8_t *value) {
return i2c_read_reg(BME280_ADDR, reg, value, 1);
}
你看,这样上层代码就只需要关心 bme280_write_reg(),不用管底层是 I2C 还是 SPI。万一以后要换通信接口,只需要改最底层的两个函数就行。
我的小技巧: 寄存器地址最好用枚举定义,别用宏。枚举在调试器里能直接看到名字,宏只能看到数字。我曾经因为一个宏定义写错了地址,排查了整整一个下午。
3.3 驱动分层架构:HAL -> Driver -> Sensor
这是本章的重点。一个好的驱动架构,应该像洋葱一样,一层包一层。我习惯分三层:
- HAL 层(硬件抽象层): 直接跟 MCU 的硬件打交道。比如 STM32 的 HAL 库,或者你自己写的寄存器操作。这一层只负责收发字节,不关心数据含义。
- Driver 层(设备驱动层): 封装传感器的寄存器读写。这一层知道传感器的寄存器地址、位定义、时序要求。它提供的是“写寄存器”、“读寄存器”、“设置模式”这样的接口。
- Sensor 层(传感器应用层): 调用 Driver 层接口,完成具体的功能。比如“读取温度值”、“启动连续测量”、“校准传感器”。这一层是给应用工程师用的,他们不需要知道寄存器细节。
为什么要这么分?我举个例子。有一次,项目需要把 BME280 从 I2C 换成 SPI。如果代码是分层写的,我只需要改 HAL 层,把 I2C 的收发函数换成 SPI 的,Driver 层和 Sensor 层完全不用动。如果代码是揉在一起的,那就要重写整个驱动。
避坑指南: 我曾经在一个项目里,把传感器配置代码直接写在了主循环里。结果后来要加一个新传感器,发现配置逻辑和读取逻辑混在一起,根本拆不开。从那以后,我强制要求所有传感器驱动必须分层,Driver 层只做寄存器操作,Sensor 层才做业务逻辑。
3.4 知识体系结构图
下面这张图,是我画的一个驱动分层架构的示意图。你可以看到数据是怎么从物理层一路传到应用层的。
3.5 实战:一个完整的读写流程
咱们拿一个真实的传感器来举例。假设你用的是 BME280,要读取温度值。按照分层架构,流程是这样的:
- Sensor 层 调用
bme280_read_temperature()。 - 这个函数内部调用 Driver 层 的
bme280_read_reg(0xFA, raw_data, 3),读取三个字节的原始温度数据。 - Driver 层调用 HAL 层 的
HAL_I2C_Mem_Read(),通过 I2C 总线从传感器物理地址 0x76 的寄存器 0xFA 处读取 3 个字节。 - 数据原路返回:HAL 层拿到字节,Driver 层拼成 20 位整数,Sensor 层用公式算出实际温度值。
你看,每一层各司其职。出了问题也好排查:如果读回来的数据全是 0xFF,那可能是 HAL 层通信失败;如果数据有值但算出来不对,那可能是 Driver 层的寄存器地址错了;如果数据对但应用显示不对,那可能是 Sensor 层的计算公式写错了。
我的经验: 调试传感器驱动时,我习惯先在 Driver 层加一个“寄存器回读”函数。写进去一个值,再读出来看看是不是一样的。如果这一步对了,那 HAL 层和 Driver 层基本就没问题了。剩下的就是 Sensor 层的算法问题。
好了,这一章的内容就这些。记住:I2C 和 SPI 是工具,寄存器封装是习惯,分层架构是修养。这三样东西,缺一不可。