4、I2C驱动开发:I2C总线协议、MTK I2C控制器、DTS配置、I2C client驱动编写、读写时序
好,咱们进入第四章节。I2C这个总线,说实话,在嵌入式开发里太常见了。传感器、触摸屏、音频Codec,十有八九都挂在I2C上。MTK平台也不例外。这一章,我会把I2C驱动开发的几个关键点串起来讲,从协议到代码,咱们一步步来。
4.1 I2C总线协议速览
I2C协议本身并不复杂。两条线:SCL(时钟)和SDA(数据)。主设备发起通信,从设备响应地址。我个人习惯把I2C通信比作打电话:主设备先拨号(发送从机地址+读写位),从机应答后,就开始传数据。
这里有几个关键点,我提一下:
- 起始条件:SCL高电平时,SDA从高变低。
- 停止条件:SCL高电平时,SDA从低变高。
- 应答机制:每发送一个字节,接收方必须拉低SDA作为ACK。如果没收到ACK,主设备可以发停止信号终止传输。
避坑指南:我曾经在一个项目里,I2C通信偶尔失败,查了半天发现是从机地址写错了。MTK的I2C地址是7位,但很多数据手册给的是8位地址(左移了一位)。你写驱动时,记得把8位地址右移一位,或者直接用7位地址。这个坑,我踩过。
4.2 MTK I2C控制器架构
MTK的I2C控制器,说白了就是一个硬件状态机。它负责处理SCL时钟生成、数据移位、起始/停止条件等底层细节。我们驱动开发者,主要操作的是它的寄存器。
MTK的I2C控制器有几个关键寄存器:
| 寄存器 | 作用 |
|---|---|
| CON | 控制寄存器,配置主从模式、中断使能等 |
| TAR | 目标地址寄存器,设置从机地址 |
| DATA_CMD | 数据命令寄存器,写数据或读数据 |
| SS_SCL_HCNT / SS_SCL_LCNT | 标准模式下SCL高/低电平计数,决定时钟频率 |
| FS_SCL_HCNT / FS_SCL_LCNT | 快速模式下SCL高/低电平计数 |
嗯,这里要注意。MTK的I2C控制器支持标准模式(100kHz)和快速模式(400kHz)。时钟频率是通过配置SCL高/低电平计数来实现的。我建议你根据外设的数据手册来选模式,别一味追求高速。有些老旧的传感器,跑400kHz可能不稳定。
4.3 DTS配置:设备树里怎么描述I2C设备
在MTK平台上,I2C控制器和挂载的设备,都是在设备树(DTS)里描述的。我个人觉得,DTS配置是I2C驱动开发里最容易出错的地方之一。
一个典型的I2C控制器节点长这样:
i2c0: i2c@11009000 {
compatible = "mediatek,mt6765-i2c";
reg = <0x11009000 0x1000>;
interrupts = <GIC_SPI 76 IRQ_TYPE_LEVEL_LOW>;
clock-frequency = <400000>;
#address-cells = <1>;
#size-cells = <0>;
status = "okay";
/* 挂载的从设备 */
touchscreen@38 {
compatible = "goodix,gt911";
reg = <0x38>;
interrupt-parent = <&pio>;
interrupts = <10 IRQ_TYPE_EDGE_FALLING>;
reset-gpios = <&pio 11 GPIO_ACTIVE_LOW>;
};
};
这里有几个关键点:
- clock-frequency:设置I2C总线频率。我习惯先设400000,如果通信不稳定再降到100000。
- reg:从设备的I2C地址。注意是7位地址,比如0x38。
- interrupts:如果设备支持中断,这里要配好。我曾经遇到一个触摸屏,中断引脚没配对,导致中断一直触发不了,触摸没反应。
小技巧:调试I2C设备时,可以先在DTS里把status设为"okay",然后通过/sys/bus/i2c/devices/目录查看设备是否被正确枚举。如果设备没出现,大概率是地址或DTS语法有问题。
4.4 I2C Client驱动编写
I2C client驱动,说白了就是写一个内核模块,注册到I2C子系统里。核心结构体是i2c_driver和i2c_client。
一个最简单的I2C client驱动框架:
#include <linux/i2c.h>
#include <linux/module.h>
static int my_i2c_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
dev_info(&client->dev, "I2C device found!\n");
/* 初始化硬件、注册输入设备等 */
return 0;
}
static int my_i2c_remove(struct i2c_client *client)
{
dev_info(&client->dev, "I2C device removed!\n");
return 0;
}
static const struct i2c_device_id my_i2c_id[] = {
{ "my_i2c_device", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, my_i2c_id);
static struct i2c_driver my_i2c_driver = {
.driver = {
.name = "my_i2c_driver",
.of_match_table = of_match_ptr(my_i2c_of_match),
},
.probe = my_i2c_probe,
.remove = my_i2c_remove,
.id_table = my_i2c_id,
};
module_i2c_driver(my_i2c_driver);
MODULE_LICENSE("GPL");
嗯,这里要注意。probe函数里,你可以通过client->addr获取设备地址,通过client->irq获取中断号。我建议你在probe里先读一下设备的ID寄存器,确认通信正常,再继续初始化。
4.5 读写时序:SMBus vs 原生I2C
I2C的读写操作,Linux内核提供了两套API:SMBus和原生I2C。SMBus是I2C的一个子集,操作更简单,但功能有限。原生I2C更灵活,可以自定义消息序列。
我个人习惯:如果设备支持SMBus协议,优先用SMBus API,代码更简洁。如果设备需要复杂的读写时序(比如先写寄存器地址,再读多个字节),那就用原生I2C。
SMBus读写示例:
// 读一个字节
s32 val = i2c_smbus_read_byte_data(client, reg_addr);
// 写一个字节
i2c_smbus_write_byte_data(client, reg_addr, val);
原生I2C读写示例:
// 构造消息
struct i2c_msg msg[2];
u8 write_buf[] = { reg_addr };
u8 read_buf[10];
msg[0].addr = client->addr;
msg[0].flags = 0; // 写
msg[0].len = 1;
msg[0].buf = write_buf;
msg[1].addr = client->addr;
msg[1].flags = I2C_M_RD; // 读
msg[1].len = 10;
msg[1].buf = read_buf;
// 发送消息
i2c_transfer(client->adapter, msg, 2);
警告:使用原生I2C时,消息数组的顺序很重要。上面的例子是先写寄存器地址,再读数据。如果你把顺序搞反了,读到的数据就是错的。我曾经在一个气压传感器驱动里犯过这个错,读出来的气压值忽高忽低,排查了半天才发现是消息顺序反了。
4.6 调试技巧:用i2c-tools快速验证
在写驱动之前,我建议先用i2c-tools工具验证一下硬件通信是否正常。MTK平台上,i2c-tools通常已经编译进系统了。
常用命令:
- i2cdetect -l:列出所有I2C总线。
- i2cdetect -y 0:扫描总线0上的设备,显示地址。
- i2cget -y 0 0x38 0x00:从地址0x38的设备,读寄存器0x00的值。
- i2cset -y 0 0x38 0x00 0xFF:向地址0x38的设备,写寄存器0x00为0xFF。
嗯,这里要注意。i2cdetect扫描时,如果某个地址显示"UU",表示该地址已被内核驱动占用。如果显示"--",表示无应答。如果显示数字(如0x38),表示设备应答了。我习惯先用i2cdetect确认设备地址,再写驱动,这样能省不少调试时间。
总结一下:I2C驱动开发,说白了就是三步:配好DTS、写好probe/remove、用好读写API。MTK的I2C控制器很成熟,只要地址和时序对了,基本不会出大问题。但细节决定成败,地址移位、消息顺序、时钟频率,这些地方多留个心眼。
下一章,咱们聊聊SPI驱动开发。SPI比I2C快,但线也多,到时候再细说。