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, &reg, 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。嗯,这个细节在数据手册里通常藏在角落,不看就会翻车。

注意:QNX的I2C驱动在中断上下文里不能调用i2c_read/write,因为这些函数可能会睡眠。如果你在中断里需要读写I2C,得用工作队列(workqueue)推迟执行。

Android I2C/SPI HAL封装:从内核到Framework

Android这边,I2C/SPI的驱动路径是:内核驱动 → HAL → Hardware Service → Framework。内核驱动我们通常用Linux标准的i2c-dev或spidev,HAL层负责把内核的ioctl封装成Android的HIDL或AIDL接口。

我建议你这样做HAL封装:

  1. 在HAL里封装一个ISensor接口,定义open、close、read、ioctl方法。
  2. 每个传感器实现这个接口,比如LightSensorTempSensor
  3. 通过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 = &reg },
        { .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而不是分开的writeread?因为很多传感器需要先写寄存器地址再读数据,如果分成两次ioctl,中间可能被其他设备打断,导致数据错乱。用I2C_RDWR可以保证原子操作。

经验之谈:Android HAL层里,我习惯把I2C设备节点放在/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走线;增加驱动能力

我个人习惯的调试流程是这样的:

  1. 先看波形:用逻辑分析仪抓取总线信号,确认时钟、数据、片选是否正常。
  2. 再查时序:对照数据手册,检查建立时间、保持时间、上升/下降沿是否满足要求。
  3. 最后看数据:用i2cget/spi_read等工具读取寄存器值,和预期值对比。

举个例子,有一次SPI通信总是丢字节,我抓了波形发现MISO线上有毛刺。后来在MISO和GND之间加了一个100pF的电容,问题就解决了。嗯,这种小技巧,书上不会写,只有实战才能积累。

核心要点:I2C/SPI调试,90%的问题出在时序和电气特性上。不要一上来就怀疑代码,先看波形。逻辑分析仪是你最好的朋友。

SVG:I2C/SPI驱动开发知识体系

下面这张图,是我对本章知识体系的总结。你可以把它当作一个思维导图,快速回顾核心内容。

I2C/SPI驱动开发 QNX驱动框架 • 资源管理器模型 • 硬件抽象层(HAL) • 协议层封装 • 设备层实现 Android HAL封装 • HIDL/AIDL接口 • i2c-dev/spidev • 传感器HAL实现 • Framework集成 传感器驱动实战 • TMP102温度传感器 • TSL2561光照传感器 • 寄存器配置 • 数据转换 总线时序调试 • 逻辑分析仪使用 • 时序参数检查 • 常见问题排查 • 电气特性优化 核心:先看波形,再查时序,最后看数据

这张图把I2C/SPI驱动开发分成了四个模块:QNX框架、Android HAL、传感器实战、调试技巧。每个模块之间是递进关系——先理解底层框架,再封装上层接口,然后实战传感器驱动,最后掌握调试方法。嗯,这就是我平时带团队时的培训路径。


好了,关于I2C/SPI总线驱动开发,今天就聊到这里。记住,驱动开发不是写代码,而是理解硬件、读懂时序、善用工具。下次你遇到总线通信问题,别急着改代码,先拿逻辑分析仪看看波形——很多时候,问题就出在你以为没问题的地方。

一句话总结:I2C/SPI驱动,七分硬件三分软件。把时序吃透了,驱动就成功了一大半。
公众号:蓝海资料掘金营,微信deep3321