linux i2c总线分析使用设备树
i2c总线简要说明
I2C总线是由Philips公司开发的一种简单、双向二线制同步串行总线。它只需要两根线即可在连接于总线上的器件之间传送信息。分别为时钟线SCL和数据线SDA,这里不重点分析i2c的物理特性,我们主要是分析linux下的i2c的软件框架,在MCU用过i2c的读者应该知道,i2c在MCU里面属于一个外设控制器,用户设置好时钟,从机地址,和数据搬运到读写寄存器外设控制器就可以把数据在i2c总线上发送出去。
linux下i2c的总线模型
linux是一个支持多平台的操作系统,可以支持很多的芯片NXP,ST,Rockchip等,每个厂商使用的i2c外设控制器实现的方法都不一样,配置的寄存器都不一样,为了让使用者无需关心各个平台的i2c控制器的实现方法,就是不必深入了解如何配置寄存器,linux系统引入了i2c总线的软件框架,框架下面分为控制器驱动和设备驱动(比如触摸屏设备),下面我们主要解释一下重要的一些概念。
- i2c控制器,抽象为struct i2c_adapter,提供i2c控制器收发数据等。
- 用来描述从机i2c地址、工作的速率,抽象为struct i2c_client
- 设备驱动部分,TP(触摸屏),陀螺仪sensor,光感sensor如何使用i2c进行收发数据, 抽象为struct i2c_driver
- 每个struct i2c_client都有一个struct i2c_driver进行匹配上,然后驱动才能工作
我们知道在主控SOC芯片这边有i2c控制器所以linux系统把这部分抽象为struct i2c_adapter。在i2c控制器上可以挂载多个从机设备可能有TP(触摸屏),陀螺仪sensor,光感sensor等,每个从机设备都有一个从机地址和别人是不同的,工作的速率可能也是不同的这些属性就由struct i2c_client来描述。TP(触摸屏),陀螺仪sensor,每个产品的寄存器代表的意思都不一样,需要实现不同的驱动,所以linx i2c软件框架会对这种不同的东西单独抽象出来一个结构体叫struct i2c_driver。
查看Rockchip芯片平台的i2c adapter控制器驱动 dts定义
i2c0: i2c@ff180000 {
compatible = "rockchip,rk3399-i2c"; //每个厂商都会实现 adapter 部分的代码,搜索这个字符串可以搜索到源代码的位置
reg = <0x0 0xff180000 0x0 0x1000>; //i2c adapter 寄存器的地址范围
clocks = <&cru SCLK_I2C0>, <&cru PCLK_I2C0>; //i2c adapter 的时钟来源
clock-names = "i2c", "pclk";
interrupts = <GIC_SPI 7 IRQ_TYPE_LEVEL_HIGH>;
pinctrl-names = "default";
pinctrl-0 = <&i2c0_xfer>; //GPIO设置为i2c模式
#address-cells = <1>;
#size-cells = <0>;
status = "disabled"; //默认关闭,使用的时候再打开
};
在linux i2c软件架构上,芯片厂商要完成i2c adapter部分的代码,然后用户就可以很简单的使用芯片平台的i2c外设而不必关心怎么配置芯片厂商的寄存器和时钟等,我们可以通过ompatible = “rockchip,rk3399-i2c”;这个字符串找到代码所以的.c文件下面我们逐步分析Rockchip是如何将i2c如何添加到i2c软件框架中的。
Soc芯片控制器驱动部分
\kernel\drivers\i2c\busses\i2c-rk3x.c //通过命令 grep "rockchip,rk3399-i2c" -nR 搜索到源码的位置
static struct platform_driver rk3x_i2c_driver = {
.probe = rk3x_i2c_probe, //step 1 probe函数会被调用
.remove = rk3x_i2c_remove,
.driver = {
.name = "rk3x-i2c",
.of_match_table = rk3x_i2c_match,
.pm = &rk3x_i2c_pm_ops,
},
};
static int rk3x_i2c_probe(struct platform_device *pdev)
{
i2c = devm_kzalloc(&pdev->dev, sizeof(struct rk3x_i2c), GFP_KERNEL);
if (!i2c)
return -ENOMEM;
i2c->adap.algo = &rk3x_i2c_algorithm; //控制器端提供i2c读写函数,和硬件平台相关,不重点分析
//原始的代码非常多有设置时钟,有芯片平台独有的设置,有分配中断等,这些都是和Rockchip芯片平台相关,为了不迷路我都删除了,因为这些都不是我们需要关心的平台做好了我们拿来使用就好了。
clk_rate = clk_get_rate(i2c->clk);
rk3x_i2c_adapt_div(i2c, clk_rate);
//下面这个函数是最重要的,上面已经分配了struct i2c_adapter adap结构体,然后注册到系统中,下面我们就跟踪后面做了什么
ret = i2c_add_adapter(&i2c->adap); //step 2
if (ret < 0) {
dev_err(&pdev->dev, "Could not register adapter\n");
goto err_clk_notifier;
}
return 0;
}
//芯片平台添加adapter和client代码到linux i2c总线框架跟踪
static int rk3x_i2c_probe(struct platform_device *pdev)
i2c_add_adapter(&i2c->adap);
__i2c_add_numbered_adapter(adapter);
i2c_register_adapter(adap);
res = device_register(&adap->dev);//i2c总线底层使用的也是linux下的bus总线
of_i2c_register_devices(adap);//通过设备树的方式添加devices节点,这里的devices会转化为我们上面说的struct i2c_client这个概念,大家也可以理解为devices就是i2c_client
for_each_available_child_of_node(adap->dev.of_node, node) {
of_i2c_register_device(adap, node); //添加每个(devices)i2c_client,和i2c adapter关联,也可以理解为添加到adapter上,每个devices代表下个挂有多少个i2c从机设备。
i2c_new_device(adap, &info);//添加每个i2c_client 到bus总线
{
struct i2c_client * client = kzalloc(sizeof *client, GFP_KERNEL);
status = device_register(&client->dev);
}
}
上面分析的是芯片控制器端提供的驱动部分i2c_adapter和i2c_client是如何注册到软件架构中的,for_each_available_child_of_node函数就是根据下面的设备树节点把两个i2c_client添加到链表中的,下面是分析i2c_client的节点在dts中写法。
&i2c0 { //引用外设驱动dts节点
status = "okay"; //上面默认关闭,现在使用的打开
clock-frequency = <400000>;
i2c-scl-rising-time-ns = <280>;
i2c-scl-falling-time-ns = <16>;
//从下面两个子节点可以看到这个i2c上挂载有两个从机设备,分别为rk809,和触摸屏gt9xx。
rk809: pmic@20 {
compatible = "rockchip,rk809";
reg = <0x20>; //从机地址
}
gt9xx:gt9xx@5d {
compatible = "goodix,gt9xx";
reg = <0x5d>; //从机地址
}
上面有可i2c_client就应该有i2c_driver,因为只有i2c_client和i2c_driver成对存在驱动才能正常工作,i2c_driver在那里定义呢?i2c_driver一般定义在具体的设备驱动中,比如触摸屏的驱动,会定义一个struct i2c_driver结构体,然后注册到i2c_add_driver,下面是设备驱动的代码的分析。
static struct of_device_id goodix_ts_dt_ids[] = { //step 1
{ .compatible = "goodix,gt9xx" },
};
static struct i2c_driver goodix_ts_driver = { //step 2
.probe = goodix_ts_probe,
.remove = goodix_ts_remove,
.id_table = goodix_ts_id,
.driver = {
.name = GTP_I2C_NAME,
.of_match_table = of_match_ptr(goodix_ts_dt_ids),
},
};
i2c_add_driver(&goodix_ts_driver); //step3
//step 4
static int goodix_ts_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
//代码已经精简,只挑重点
ts->client = client; //保存struct i2c_client *client结构体,用于数据收发。
ret = gtp_request_input_dev(client, ts);//注册一个input设备,这里我们不关心
return ret;
}
//step 5
s32 gtp_i2c_write(struct i2c_client *client,u8 *buf,s32 len)
{
//设备驱动使用标准i2c发送函数进行数据发送
struct i2c_msg msg; //定义一个msg
msg.flags = (!I2C_M_RD); //设置为写
msg.len = len;
msg.buf = buf;
ret = i2c_transfer(client->adapter, &msg, 1); //调用这个标准的函数将数据发送出去,需要注意的是这里要传入goodix_ts_probe保存的struct i2c_client指针。
return ret;
}
//step 6
s32 gtp_i2c_read(struct i2c_client *client, u8 *buf, s32 len)
{
struct i2c_msg msgs[2];
msgs[0].flags = !I2C_M_RD; //设置为写,先写触摸屏的寄存器地址
msgs[0].addr = client->addr;
msgs[0].len = GTP_ADDR_LENGTH;
msgs[0].buf = &buf[0];
msgs[1].flags = I2C_M_RD; //设置为读,将触摸屏寄存器读回来
msgs[1].addr = client->addr;
msgs[1].len = len - GTP_ADDR_LENGTH;
msgs[1].buf = &buf[GTP_ADDR_LENGTH];
ret = i2c_transfer(client->adapter, msgs, 2); //数据传输
return ret;
}
上面是精简版触摸屏设备驱动程序,我们为了说明linux下i2c的驱动框架,我们删除了很多和触摸屏相关的代码,step1 step2 step3是为了在系统初始化的时候把goodix_ts_driver注册到系统中,这个i2c_driver和上面控制器端的i2c_client匹配会跑到step4 goodix_ts_probe这个函数被调用,重点是保存struct i2c_client *client这结构体指针,然后通过这个结构体指针gtp_i2c_write和gtp_i2c_read就可以调用系统提供的i2c_transfer函数进行数据收发。