I2C/SPI总线驱动开发:从QNX到Android的实战之路
说实话,I2C和SPI这两条总线,是我在座舱开发中打交道最多的外设接口。温度传感器、光照传感器、触控屏、甚至某些音频编解码器,背后跑的都是这两条总线。今天我就把QNX和Android两边怎么玩转I2C/SPI,掰开揉碎了讲给你听。
QNX I2C/SPI驱动框架:底层怎么搭
QNX对I2C和SPI的支持,核心是它的资源管理器(Resource Manager)框架。说白了,就是把一个外设抽象成一个文件,你open、read、write、ioctl就完事了。
我个人习惯把I2C驱动分成三层:
- 硬件抽象层(HAL):直接操作I2C控制器的寄存器,处理时钟、中断、DMA。
- 协议层:封装标准的I2C读写时序,比如start、stop、ack/nack检测。
- 设备层:针对具体传感器(比如TMP102温度传感器),实现它的寄存器读写命令。
来看一个QNX下I2C设备驱动的骨架代码:
#include <hw/i2c.h>
#include <sys/resmgr.h>
// 设备结构体
typedef struct {
i2c_handle_t handle; // I2C控制器句柄
uint8_t slave_addr; // 从设备地址
uint32_t freq; // 总线频率
} tmp102_dev_t;
// 打开设备
static int tmp102_open(resmgr_context_t *ctp, io_open_t *msg,
RESMGR_OCB_T *ocb) {
tmp102_dev_t *dev = ocb->extra;
// 初始化I2C控制器
dev->handle = i2c_attach(dev->slave_addr, dev->freq);
return EOK;
}
// 读取温度寄存器
static int tmp102_read_temp(tmp102_dev_t *dev, float *temp) {
uint8_t reg = 0x00; // 温度寄存器地址
uint8_t buf[2];
// 写寄存器地址
i2c_write(dev->handle, ®, 1);
// 读温度数据
i2c_read(dev->handle, buf, 2);
// 转换:12位精度,每LSB 0.0625°C
int16_t raw = (buf[0] << 4) | (buf[1] >> 4);
if (raw & 0x0800) raw |= 0xF000; // 符号扩展
*temp = raw * 0.0625f;
return EOK;
}
这里有个坑,我踩过好几次——I2C的从设备地址是7位还是10位?QNX的i2c_attach函数默认是7位地址,如果你遇到一个10位地址的传感器,记得在flags里加上I2C_ADDR_10BIT。嗯,这个细节在数据手册里通常藏在角落,不看就会翻车。
Android I2C/SPI HAL封装:从内核到Framework
Android这边,I2C/SPI的驱动路径是:内核驱动 → HAL → Hardware Service → Framework。内核驱动我们通常用Linux标准的i2c-dev或spidev,HAL层负责把内核的ioctl封装成Android的HIDL或AIDL接口。
我建议你这样做HAL封装:
- 在HAL里封装一个
ISensor接口,定义open、close、read、ioctl方法。 - 每个传感器实现这个接口,比如
LightSensor、TempSensor。 - 通过
hw_get_module加载HAL模块,Framework层直接调用。
来看一个Android I2C HAL的典型实现片段:
// sensors_hal.cpp
#include <hardware/sensors.h>
#include <fcntl.h>
#include <linux/i2c-dev.h>
#include <sys/ioctl.h>
static int light_sensor_open(const struct hw_module_t* module,
const char* id,
struct hw_device_t** device) {
struct sensors_poll_device_t *dev = ...;
// 打开I2C设备节点
int fd = open("/dev/i2c-2", O_RDWR);
if (fd < 0) return -errno;
// 设置从设备地址
ioctl(fd, I2C_SLAVE, 0x23); // 光照传感器地址
dev->fd = fd;
*device = (struct hw_device_t*)dev;
return 0;
}
static int light_sensor_poll(sensors_poll_device_t* dev,
sensors_event_t* data) {
uint8_t reg = 0x00;
uint8_t buf[2];
// I2C读取
struct i2c_msg msgs[2] = {
{ .addr = 0x23, .flags = 0, .len = 1, .buf = ® },
{ .addr = 0x23, .flags = I2C_M_RD, .len = 2, .buf = buf }
};
struct i2c_rdwr_ioctl_data msgset = {
.msgs = msgs, .nmsgs = 2
};
ioctl(dev->fd, I2C_RDWR, &msgset);
// 解析光照值
uint16_t lux = (buf[0] << 8) | buf[1];
data->light = lux;
return 0;
}
你想想看,为什么这里要用I2C_RDWR而不是分开的write和read?因为很多传感器需要先写寄存器地址再读数据,如果分成两次ioctl,中间可能被其他设备打断,导致数据错乱。用I2C_RDWR可以保证原子操作。
/dev/i2c-*,SPI设备节点放在/dev/spidev*.*。这样上层通过文件路径就能区分总线类型,调试时也方便用i2c-tools直接操作。
传感器驱动实战:温度与光照
我们拿两个最常见的传感器来练手:TMP102(温度)和TSL2561(光照)。这两个芯片在座舱里很常见,前者监控电池温度,后者检测环境光来自动调节屏幕亮度。
TMP102温度传感器驱动要点
- I2C地址:0x48(默认),A0引脚接地就是0x48,接VCC就是0x49。
- 寄存器:0x00是温度寄存器,0x01是配置寄存器。
- 精度:12位模式,0.0625°C/LSB。13位模式是0.03125°C/LSB。
- 转换时间:默认模式下每250ms更新一次。如果你需要更快,可以设置成连续转换模式。
我在项目中遇到过一个问题:TMP102在低温下(-10°C以下)读数偶尔跳变。后来发现是I2C时钟线太长,信号反射导致时序错误。解决办法是在SCL和SDA上各加一个4.7kΩ上拉电阻,并且把时钟频率从400kHz降到100kHz。
TSL2561光照传感器驱动要点
- I2C地址:0x29(ADDR接地)、0x39(ADDR浮空)、0x49(ADDR接VCC)。
- 工作模式:有积分模式(Integration Mode)和脉冲模式(Pulse Mode)。座舱里一般用积分模式,精度更高。
- 通道:CH0(全光谱)和CH1(红外),通过差值计算可见光。
- 增益:1x、16x可选。室内弱光用16x,户外强光用1x。
我曾经踩过一个坑:TSL2561的积分时间设置不当,导致在快速变化的灯光下(比如隧道进出),读数滞后严重。后来我把积分时间从402ms缩短到101ms,并且开启了自动增益控制,效果好了很多。
总线时序调试技巧:示波器与逻辑分析仪
调试I2C/SPI总线,说白了就是看波形。我常用的工具是逻辑分析仪,便宜又好用。下面是我总结的几个调试要点:
| 问题现象 | 可能原因 | 排查方法 |
|---|---|---|
| I2C总线卡死(SCL低电平) | 从设备未释放时钟线 | 检查从设备电源、复位引脚;用示波器看SCL波形是否被拉低 |
| SPI数据错位 | 时钟极性/相位不匹配 | 确认CPOL和CPHA设置;用逻辑分析仪抓取CS、SCLK、MOSI、MISO |
| I2C NACK | 从设备地址错误或忙 | 用i2cdetect扫描地址;检查从设备是否处于睡眠模式 |
| SPI速率上不去 | 走线过长或负载电容过大 | 降低时钟频率;缩短PCB走线;增加驱动能力 |
我个人习惯的调试流程是这样的:
- 先看波形:用逻辑分析仪抓取总线信号,确认时钟、数据、片选是否正常。
- 再查时序:对照数据手册,检查建立时间、保持时间、上升/下降沿是否满足要求。
- 最后看数据:用i2cget/spi_read等工具读取寄存器值,和预期值对比。
举个例子,有一次SPI通信总是丢字节,我抓了波形发现MISO线上有毛刺。后来在MISO和GND之间加了一个100pF的电容,问题就解决了。嗯,这种小技巧,书上不会写,只有实战才能积累。
SVG:I2C/SPI驱动开发知识体系
下面这张图,是我对本章知识体系的总结。你可以把它当作一个思维导图,快速回顾核心内容。
这张图把I2C/SPI驱动开发分成了四个模块:QNX框架、Android HAL、传感器实战、调试技巧。每个模块之间是递进关系——先理解底层框架,再封装上层接口,然后实战传感器驱动,最后掌握调试方法。嗯,这就是我平时带团队时的培训路径。
好了,关于I2C/SPI总线驱动开发,今天就聊到这里。记住,驱动开发不是写代码,而是理解硬件、读懂时序、善用工具。下次你遇到总线通信问题,别急着改代码,先拿逻辑分析仪看看波形——很多时候,问题就出在你以为没问题的地方。