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。
通信流程:
- 主设备发送起始条件(SCL高电平时,SDA从高变低)。
- 主设备发送7位从设备地址 + 1位读写位(0表示写,1表示读)。
- 从设备应答(ACK):拉低SDA表示收到。
- 数据传输:每8位数据后跟一个ACK位。
- 主设备发送停止条件(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, ®, 1);
read(fd, buf, 2);
注意:在Android中,用户空间访问/dev/节点需要SELinux权限。我曾经在项目上线前才发现,app读不到传感器数据,就是因为SELinux策略没加。记得在file_contexts和te文件中添加对应规则。
好了,这一章的内容就到这里。I2C和SPI是车载传感器的命脉,搞懂了它们,后面写驱动就顺风顺水了。下一章我们聊聊中断和GPIO,这可是传感器数据上报的关键环节。