4、传感器接口抽象:ADC接口设计、I2C/SPI总线抽象、传感器驱动分层模型
各位工程师朋友,咱们接着聊。上一章我们把GPIO和定时器捋清楚了,这一章要啃的,是医疗贴片里最核心的传感器接口抽象。
你想想看,一个医疗贴片,不管是测心电、血氧还是体温,最终都要把物理世界的模拟信号,变成数字世界的0和1。这个过程中,ADC、I2C、SPI这些接口,就是桥梁。桥没搭好,数据就是歪的。
4.1 ADC接口设计:从模拟到数字的第一道关
ADC(模数转换器)在医疗贴片里有多重要?我这么说吧,心电信号的幅度也就0.5mV到4mV之间,稍微有点噪声,波形就糊了。我早期做过一个贴片项目,ADC采样率设得挺高,但分辨率只有10位,结果心电的P波根本看不清。后来换成12位,效果立竿见影。
所以,ADC接口抽象,我个人的习惯是分三层:
- 硬件层:直接操作寄存器,配置采样率、分辨率、参考电压。
- 驱动层:封装成统一的API,比如
ADC_Init()、ADC_Read()。 - 应用层:拿到原始数据后,做滤波、缩放、单位转换。
这里给一个我常用的ADC驱动接口示例:
/* adc_interface.h */
typedef struct {
uint8_t channel; /* ADC通道号 */
uint16_t resolution; /* 分辨率,如12位 */
uint32_t sampling_rate; /* 采样率,单位Hz */
float ref_voltage; /* 参考电压,单位V */
} adc_config_t;
typedef struct {
adc_config_t config;
uint16_t (*read_raw)(void); /* 读取原始值 */
float (*read_voltage)(void); /* 读取电压值 */
void (*start_dma)(uint16_t *buf, uint32_t len); /* DMA方式读取 */
} adc_device_t;
为什么要用结构体指针?说白了,就是为了方便切换不同的ADC硬件。同一个接口,换一颗MCU,只要重新实现这几个函数指针就行。上层代码完全不用动。
4.2 I2C/SPI总线抽象:让传感器“即插即用”
医疗贴片里,I2C和SPI是传感器通信的主力。温度传感器、加速度计、血氧模块,几乎都跑在这两条总线上。
我个人习惯,把总线抽象成两个层次:
- 总线驱动层:负责最底层的时序,比如I2C的起始条件、停止条件、ACK检测。SPI的片选、时钟极性、相位配置。
- 设备驱动层:基于总线驱动,封装具体传感器的读写命令。比如读MAX30102的血氧值,就是先写寄存器地址,再读数据。
来看一个I2C总线抽象的典型实现:
/* i2c_interface.h */
typedef enum {
I2C_OK,
I2C_BUSY,
I2C_NACK,
I2C_TIMEOUT
} i2c_status_t;
typedef struct {
void (*init)(uint32_t freq); /* 初始化总线频率 */
i2c_status_t (*write)(uint8_t addr, uint8_t *data, uint16_t len);
i2c_status_t (*read)(uint8_t addr, uint8_t *data, uint16_t len);
i2c_status_t (*write_read)(uint8_t addr, uint8_t *wdata, uint16_t wlen,
uint8_t *rdata, uint16_t rlen);
} i2c_bus_t;
嗯,这里要注意一点。I2C的地址是7位还是10位?大部分医疗传感器都是7位地址,但有些复杂的模块(比如多通道ADC)会用10位。我建议在i2c_bus_t里加一个地址长度字段,或者干脆统一用7位,遇到10位地址时做转换。我在一个项目中就吃过这个亏,传感器死活不响应,查了半天才发现是地址格式不对。
SPI的抽象类似,但多了一个片选管理:
/* spi_interface.h */
typedef struct {
void (*init)(uint32_t freq, uint8_t mode); /* mode: 0-3, 对应CPOL/CPHA */
void (*cs_enable)(void);
void (*cs_disable)(void);
uint8_t (*transfer)(uint8_t data); /* 全双工收发 */
void (*transfer_dma)(uint8_t *tx, uint8_t *rx, uint16_t len);
} spi_bus_t;
4.3 传感器驱动分层模型:让代码“活”起来
好了,ADC和总线都抽象好了,接下来就是传感器驱动本身。我推荐一个三层模型,这个模型我在三个医疗贴片项目里验证过,效果很好。
| 层次 | 职责 | 示例 |
|---|---|---|
| 硬件抽象层(HAL) | 封装MCU外设操作,如ADC、I2C、SPI、GPIO | adc_device_t, i2c_bus_t |
| 传感器驱动层 | 实现具体传感器的初始化、配置、数据读取 | max30102_init(), max30102_read_hr() |
| 传感器管理层 | 管理多个传感器,调度采样时序,处理数据融合 | sensor_manager_run() |
举个例子,假设我们要驱动一个血氧传感器MAX30102和一个加速度计LIS3DH:
/* sensor_driver_max30102.c */
#include "i2c_interface.h"
#include "max30102_regs.h"
static i2c_bus_t *bus;
void max30102_init(i2c_bus_t *i2c_bus) {
bus = i2c_bus;
bus->init(400000); /* 400kHz I2C */
/* 配置传感器:采样率、LED电流等 */
uint8_t cfg[] = {0x03, 0x2F}; /* 配置寄存器地址和值 */
bus->write(MAX30102_ADDR, cfg, 2);
}
uint32_t max30102_read_hr(void) {
uint8_t reg = 0x07; /* FIFO数据寄存器 */
uint8_t data[4];
bus->write_read(MAX30102_ADDR, ®, 1, data, 4);
return (data[0] << 16) | (data[1] << 8) | data[2];
}
你看,传感器驱动层完全不关心底层是哪个MCU,它只调用i2c_bus_t里的函数指针。换MCU?只要重新实现i2c_bus_t就行,驱动代码一个字都不用改。
核心思想:分层的目的,就是让每一层只关心自己的事。硬件抽象层管寄存器,传感器驱动层管协议,管理层管调度。这样,任何一个层出问题,都不会波及到其他层。我曾经在一个项目中,把MAX30102的驱动从STM32F4移植到NXP的LPC55系列,只花了半天时间,就是因为分层做得好。
4.4 避坑指南:我踩过的那些坑
做传感器接口抽象,有几个坑是绕不开的。我一个个说:
- 坑一:I2C的时钟延展。有些传感器(比如TI的TMP117)在转换数据时会拉低SCL,告诉主机“我还没准备好”。如果你的I2C驱动不支持时钟延展,通信就会卡死。我建议在I2C驱动里加一个超时机制,比如等待SCL释放超过10ms就报错。
- 坑二:SPI的片选毛刺。GPIO模拟片选时,如果初始化顺序不对,片选引脚可能会产生一个短暂的脉冲,导致传感器误触发。解决办法:先把片选引脚拉高(或拉低,取决于有效电平),再配置GPIO模式。
- 坑三:ADC的参考电压噪声。医疗贴片里,ADC的参考电压如果直接从电源取,噪声会直接耦合到采样结果里。我建议用独立的参考电压芯片,或者在PCB布局时把参考电压走线远离开关电源。
嗯,这些坑说起来都是小细节,但任何一个都能让你调试好几天。我当年在第一个医疗贴片项目里,就因为I2C时钟延展的问题,整整折腾了一周。从那以后,我写驱动时都会把异常处理写在前头。
4.5 小结
这一章的内容,说白了就是三件事:ADC接口怎么抽象、I2C/SPI总线怎么封装、传感器驱动怎么分层。这三件事做好了,你的代码就能在不同的MCU和传感器之间自由切换。医疗贴片产品迭代快,今天用这个传感器,明天可能就换另一个。抽象层做得好,换传感器就像换灯泡一样简单。
下一章,我们会讲电源管理与低功耗设计。医疗贴片对功耗的要求,比消费电子苛刻得多。到时候我会分享一些我在电池续航优化上的实战经验。