4、I2C/SPI总线驱动开发:I2C协议详解、SPI协议详解、在Android内核中注册I2C/SPI设备、用户空间访问

各位同学,今天我们来聊聊车载传感器开发中最核心的两个总线——I2C和SPI。说实话,我在做车载BSP的这些年,90%的传感器都是挂在这两条总线上的。你想想看,一个智能座舱里,温度传感器、气压传感器、触摸屏、陀螺仪……哪个离得开它们?

我刚开始接触这块时,总觉得协议层的东西太理论,直到有一次在实车上调试一个胎压监测模块,因为I2C时序没调好,数据老是丢包,折腾了整整三天。从那以后,我再也不敢小看这些基础协议了。

4.1 I2C协议详解

I2C,全称Inter-Integrated Circuit,是飞利浦公司搞出来的。它只用两根线——SCL(时钟线)和SDA(数据线),就能挂载多个设备。说白了,就是一条总线上可以同时接多个传感器,每个传感器有自己的地址。

核心特点:

  • 主从架构:Android SoC通常是主设备(Master),传感器是从设备(Slave)。
  • 7位/10位地址:车载传感器大多用7位地址,比如0x48、0x76这种。
  • 速率:标准模式100kHz,快速模式400kHz,高速模式3.4MHz。车载上常用400kHz。

通信流程:

  1. 主设备发送起始条件(SCL高电平时,SDA从高变低)。
  2. 主设备发送7位从设备地址 + 1位读写位(0表示写,1表示读)。
  3. 从设备应答(ACK):拉低SDA表示收到。
  4. 数据传输:每8位数据后跟一个ACK位。
  5. 主设备发送停止条件(SCL高电平时,SDA从低变高)。

避坑指南:我曾经在一个项目中,发现传感器偶尔不响应。查了半天,原来是上拉电阻选小了。I2C的SDA和SCL必须接上拉电阻,典型值4.7kΩ。如果总线电容大,电阻要选小一点,比如2.2kΩ。不然信号上升沿太慢,从设备根本认不出来。

4.2 SPI协议详解

SPI,全称Serial Peripheral Interface,摩托罗拉发明的。它比I2C快得多,但线也多。四条线:SCLK(时钟)、MOSI(主出从入)、MISO(主入从出)、CS(片选)。

为什么车载上很多高速传感器用SPI? 因为SPI是全双工的,可以同时收发数据。而且没有地址概念,每个设备一根片选线,想跟谁说话就拉低谁的CS。速率轻松上10MHz甚至更高。

四种模式:

模式 CPOL(时钟极性) CPHA(时钟相位) 说明
模式0 0 0 空闲时SCLK为低,第一个边沿采样
模式1 0 1 空闲时SCLK为低,第二个边沿采样
模式2 1 0 空闲时SCLK为高,第一个边沿采样
模式3 1 1 空闲时SCLK为高,第二个边沿采样

嗯,这里要注意:传感器数据手册里一定会写它支持哪种模式。我见过有人把模式0的传感器配成模式3,结果读出来的数据全是0xFF。所以拿到一个新传感器,第一件事就是看数据手册的时序图。

4.3 在Android内核中注册I2C/SPI设备

这部分是BSP开发的核心工作。说白了,就是告诉内核:这个I2C总线上挂了一个传感器,地址是多少,用哪个驱动来处理它。

4.3.1 I2C设备注册

在Android内核中,注册I2C设备有两种方式:

方式一:设备树(推荐)

现在主流做法。在dts文件中添加节点:

&i2c3 {
    status = "okay";
    clock-frequency = <400000>;  // 400kHz

    temperature_sensor: tmp117@48 {
        compatible = "ti,tmp117";
        reg = <0x48>;  // I2C地址
        status = "okay";
    };
};

方式二:板级文件(老项目还在用)

在board-xxx.c中调用i2c_register_board_info()。我个人建议新项目别这么干了,维护起来太痛苦。

4.3.2 SPI设备注册

SPI设备注册也类似,设备树中这样写:

&spi1 {
    status = "okay";
    cs-gpios = <&gpio3 20 GPIO_ACTIVE_LOW>;

    accelerometer: adxl345@0 {
        compatible = "adi,adxl345";
        reg = <0>;  // 片选索引
        spi-max-frequency = <5000000>;  // 5MHz
        status = "okay";
    };
};

个人经验:我习惯在设备树中把reg和spi-max-frequency写清楚。曾经有个同事没写spi-max-frequency,内核用了默认的1MHz,结果传感器数据更新率跟不上,定位轨迹全是锯齿。后来改成5MHz,问题解决。

4.4 用户空间访问

驱动写好了,怎么在应用层读写传感器数据?Android下主要有两种方式。

4.4.1 通过sysfs

内核驱动注册后,会在/sys/bus/i2c/devices/或/sys/bus/spi/devices/下生成设备节点。驱动里实现show/store函数,用户空间就可以用cat/echo来读写。

// 读取温度
cat /sys/bus/i2c/devices/3-0048/temp_input

// 写入配置
echo 0x0C > /sys/bus/i2c/devices/3-0048/config

4.4.2 通过/dev/节点

更灵活的方式。驱动中注册字符设备,生成/dev/i2c-x或/dev/spidevX.Y节点。用户空间可以用ioctl来操作。

#include <linux/i2c-dev.h>
#include <fcntl.h>
#include <sys/ioctl.h>

int fd = open("/dev/i2c-3", O_RDWR);
ioctl(fd, I2C_SLAVE, 0x48);  // 设置从设备地址

// 写寄存器
unsigned char buf[2] = {0x00, 0x1C};  // 寄存器地址+数据
write(fd, buf, 2);

// 读数据
unsigned char reg = 0x00;
write(fd, &reg, 1);
read(fd, buf, 2);

注意:在Android中,用户空间访问/dev/节点需要SELinux权限。我曾经在项目上线前才发现,app读不到传感器数据,就是因为SELinux策略没加。记得在file_contexts和te文件中添加对应规则。

好了,这一章的内容就到这里。I2C和SPI是车载传感器的命脉,搞懂了它们,后面写驱动就顺风顺水了。下一章我们聊聊中断和GPIO,这可是传感器数据上报的关键环节。