3. I2C驱动开发:I2C总线协议、RK I2C控制器驱动、外设驱动适配与调试
I2C,说白了就是两根线搞定一堆外设。SCL(时钟)和SDA(数据),就这么简单。但真要在RK平台上把它跑稳,坑还真不少。我最早接触I2C是在一个温湿度传感器项目上,当时觉得协议简单,结果被ACK信号折腾了一整天。嗯,咱们今天就把这事彻底聊透。
3.1 I2C总线协议速览
先过一遍基础,不然后面代码你看着会懵。
- 主从结构:RK芯片永远是Master,外设是Slave。一个总线上可以挂多个Slave,靠7位地址区分。
- 起始条件:SCL高电平时,SDA从高拉低。停止条件:SCL高电平时,SDA从低拉高。
- 数据传输:每字节8位,后面跟一个ACK位。Slave拉低SDA表示应答,不拉就是NACK。
- 速率模式:标准模式100kHz,快速模式400kHz,高速模式3.4MHz。RK平台一般用100k或400k。
关键点:I2C的ACK机制是硬件自动完成的,但驱动层必须正确处理NACK。我曾经遇到一个EEPROM,写入后需要等待内部擦除,如果驱动不处理NACK,数据就丢了。
3.2 RK I2C控制器驱动架构
RK的I2C控制器,在芯片内部是一个独立的硬件模块。驱动代码在Linux内核里,路径是drivers/i2c/busses/i2c-rk3x.c。这个文件,我建议你直接打开看一眼,结构很清晰。
3.2.1 硬件框图
先看一张图,理解RK I2C控制器在系统里的位置。
3.2.2 驱动核心数据结构
驱动里最重要的结构体是struct rk3x_i2c。它管理着硬件寄存器、时钟、DMA、完成量等。我挑几个关键字段说说:
| 字段 | 类型 | 说明 |
|---|---|---|
regs |
void __iomem * | 寄存器基地址,ioremap后的虚拟地址 |
clk |
struct clk * | I2C模块时钟,频率决定SCL速率 |
msg |
struct i2c_msg * | 当前正在处理的消息 |
state |
enum rk3x_i2c_state | 状态机:IDLE, START, READ, WRITE, STOP |
error |
int | 错误码,用于中断处理中传递错误 |
个人习惯:调试I2C时,我第一件事就是检查clk的频率。很多奇怪问题,比如SCL波形不对,都是时钟配置错了。用clk_get_rate打印一下,心里就有底了。
3.3 外设驱动适配实战
这部分是重头戏。你拿到一个新外设,比如一个温度传感器,怎么在RK平台上把驱动写出来?
3.3.1 设备树配置
RK平台用设备树描述硬件。I2C外设的节点要挂在对应的I2C总线节点下。举个例子:
&i2c1 {
status = "okay";
clock-frequency = <400000>; /* 400kHz */
temperature_sensor: tmp117@48 {
compatible = "ti,tmp117";
reg = <0x48>;
status = "okay";
};
};
这里要注意几点:
clock-frequency:我建议先从100kHz开始,稳定后再提频。直接上400kHz,如果PCB走线不好,容易丢数据。reg:7位地址,左移1位填入。比如0x48,实际发送时是0x90(写)和0x91(读)。compatible:驱动匹配的关键字符串,必须和驱动里of_match_table一致。
3.3.2 驱动编写模板
一个标准的I2C外设驱动,骨架是这样的:
#include <linux/i2c.h>
#include <linux/module.h>
static int tmp117_probe(struct i2c_client *client)
{
struct device *dev = &client->dev;
dev_info(dev, "TMP117 detected at 0x%02x\n", client->addr);
/* 初始化外设,注册硬件监控接口等 */
return 0;
}
static void tmp117_remove(struct i2c_client *client)
{
/* 清理资源 */
}
static const struct of_device_id tmp117_of_match[] = {
{ .compatible = "ti,tmp117" },
{ }
};
MODULE_DEVICE_TABLE(of, tmp117_of_match);
static struct i2c_driver tmp117_driver = {
.driver = {
.name = "tmp117",
.of_match_table = tmp117_of_match,
},
.probe = tmp117_probe,
.remove = tmp117_remove,
};
module_i2c_driver(tmp117_driver);
写驱动时,我习惯先实现probe函数,只打印一条日志,确认设备树匹配成功。然后再逐步添加读写功能。这样能快速定位问题。
3.3.3 读写操作
I2C的读写,内核提供了i2c_transfer接口。但更常用的是封装好的i2c_smbus_read_byte_data和i2c_smbus_write_byte_data。举个例子:
/* 读取TMP117温度寄存器(0x00) */
int tmp117_read_temp(struct i2c_client *client)
{
int ret;
s16 raw;
ret = i2c_smbus_read_word_data(client, 0x00);
if (ret < 0) {
dev_err(&client->dev, "read temp failed: %d\n", ret);
return ret;
}
raw = (s16)ret;
/* TMP117分辨率0.0078125°C */
return (raw * 78125) / 10000;
}
我曾经踩过的坑:i2c_smbus_read_word_data返回的数据是CPU字节序,但外设可能是大端。TMP117就是大端,直接赋值会得到错误值。一定要根据数据手册确认字节序,必要时用be16_to_cpu转换。
3.4 调试方法与工具
驱动写好了,怎么验证?我有一套固定的调试流程。
3.4.1 硬件层面
示波器是王道。抓SCL和SDA波形,看起始条件、地址、数据、ACK。我常用的检查点:
- SCL频率对不对?用示波器测周期,算频率。
- 地址对不对?Slave地址是否和设备树一致。
- ACK有没有?Slave没应答,说明地址错或外设没上电。
3.4.2 软件层面
内核提供了i2cdetect、i2cget、i2cset等工具。在RK板子上,直接敲命令:
# 扫描I2C1总线上的设备
i2cdetect -y 1
# 读取TMP117 0x00寄存器
i2cget -y 1 0x48 0x00 w
如果i2cdetect能扫到设备,但驱动probe不执行,那问题大概率在设备树compatible字符串不匹配。我遇到过好几次,都是因为少打了个逗号。
3.4.3 内核调试开关
打开I2C的调试日志,能看到每次传输的细节:
# echo 'file i2c-core.c +p' > /sys/kernel/debug/dynamic_debug/control
# echo 'file i2c-rk3x.c +p' > /sys/kernel/debug/dynamic_debug/control
然后看dmesg输出。你会看到类似这样的信息:
[ 12.345] i2c-core: i2c_transfer: msg[0] addr=0x48, flags=0x0, len=1
[ 12.346] i2c-core: i2c_transfer: msg[1] addr=0x48, flags=0x1, len=2
这能帮你确认数据是否真的发到了总线上。
3.5 常见问题与避坑
| 问题 | 现象 | 解决方案 |
|---|---|---|
| SCL无波形 | 示波器看SCL一直高 | 检查I2C时钟是否enable,clk_prepare_enable调了没 |
| Slave无ACK | 第九个SCL脉冲后SDA仍高 | 确认地址、外设供电、IO电压是否匹配 |
| 数据错位 | 读回来的值不对 | 检查字节序、寄存器地址是否要拼接 |
| 总线锁死 | SDA一直被拉低 | 复位外设,或给SCL发9个脉冲恢复 |
避坑指南:我曾经在一个项目里,I2C总线莫名其妙锁死。查了半天,发现是Slave在启动时拉低了SDA。解决办法是在驱动probe之前,先通过GPIO模拟9个SCL脉冲,强制释放总线。这个技巧,你记一下,迟早用得上。
好了,I2C这块的内容就这些。从协议到驱动,再到调试,每一步都有坑,但也都有解法。你按这个流程走,大部分问题都能搞定。