0、IIC总线汇总概览
(1)三根通线线:GND、SCL、SDA
(2)同步(同步通信,通信双方有一根时钟线,主从在同一个时钟下工作)、电平(通信线上传输的是电平信号)、低速、近距离
(3)总线式结构,支持多个设备挂接在同一条总线上
(4)主从式结构,通信双方必须一个是主设备、一个是从设备。主设备掌握每次通信的主动权,从设备按照主设备的节奏被动响应,每个从设备在总线中有唯一的地址,主设备通过从地址找到自己要通信的从设备(本质是广播)。
(5)IIC主要用途就是主SOC和外围设备之间的通信,最大的优势是可以在总线上扩展多个外围设备的支持,常见的各种物联网传感器芯片(如sensor、温度、湿度、光强度、酸碱度、烟雾浓度、压力等)均使用IIC总线和SoC进行通信。
(6)电容触摸屏芯片的多个引脚构成2个接口,一个接口是IIC的,负责和主SoC连接(本身作为从设备),主SoC通过该接口初始化及控制电容触摸屏芯片,芯片通过该接口向SoC汇报触摸事件的信息(触摸坐标等),我们使用电容触摸屏主要关注就是这个接口,另一个接口是电容触摸板的管理接口,电容触摸芯片通过该接口来控制触摸屏硬件。该接口是电容触摸屏公司关心的,他们的触摸屏芯片内部固件编程处理这部分,我们使用电容触摸屏的人并不关心这里。
1、Linux内核的IIC驱动概览
(1)IIC驱动框架的主要目标:让驱动开发者可以在内核中可以方便的添加自己的iic设备的驱动程序,从而可以更容易的在Linux下驱动自己的IIC接口硬件。
(2)linux系统提供了2种iic驱动实现方法:第一种叫IIC-DEV,对应driver/i2c/i2c-dev.c,这种方法只是封装了主机的IIC基本操作,并且向应用层提供相应的操作接口,应用层代码需要自己去实现对slave的控制和操作,所以这种iic驱动相当于只是提供给应用层可以访问slave硬件设备的接口,本身并未对硬件做任何操作,应用需要实现对硬件的操作,因此写应用的人需要对硬件非常了解,其实相当于传统的驱动中干的活都丢给了应用去做了,所以这种IIC驱动又叫做“应用层驱动”,这种方式并不是主流,它的优势在于把差异化都放在了应用层,这样在设备比较难缠(尤其是slave是非标准的iic时)时不用动驱动,而只需要修改应用就可以实现对各种设备的驱动。第2种iic驱动是所有代码都放在驱动层实现,直接向应用层提供最终结果。应用层甚至不需要知道这里面有iic存在,譬如电容式触摸屏驱动,直接向应用层提供/dev/input/event1的操作接口,应用层编程的人根本不知道event1种涉及到了iic。(不同的IIC设备的主要不同点在于其寄存器列表不同,初始化不同,但是对不同的IIC设备的操作方法基本相同)
2、IIC子系统的4个关键结构体
(1)struct i2c_adapter
描述主机所对应的iic控制器,每个控制器对应一个i2c_adapter结构体(主机驱动)
(2)struct i2c_algorithm
描述主机IIC跟从机iic设备通信的算法,算法由iic控制器执行,i2c_algorithm包含在i2c_adapter结构体中
(3)struct i2c_client
其实就是i2c_device,跟i2c_driver配对,是iic从机的设备信息
(4)struct i2c_driver
iic的设备驱动(从机设备)
3、关键文件
(1)i2c-core.c
IIC总线实现
(2)busses目录
i2c adapter 集合
(3)algos目录
算法集合
4、i2c-core.c分析
postcore_initcall(i2c_init) //函数入口 动态加载
static int __init i2c_init(void) //iic总线初始化
retval = bus_register(&i2c_bus_type); //注册了一个iic 类型的总线,完成这步后在/sys/bus目录下可以看到一个i2c文件
retval = i2c_add_driver(&dummy_driver); //向我们这个总线中添加了一个driver(dummy_driver是一个空驱动)
总结:i2c_init 主要就完成了一个工作:注册IIC 总线:i2c_bus_type
struct bus_type i2c_bus_type = {
.name = "i2c", //在/sys/bus目录下文件的名字
.match = i2c_device_match, //iic设备和驱动的匹配函数
.probe = i2c_device_probe, //设备和驱动匹配上后执行探针函数
.remove = i2c_device_remove,
.shutdown = i2c_device_shutdown,
};
i2c_device_match
driver = to_i2c_driver(drv); //找到iic 的driver
if (driver->id_table) //调用driver中的id_table
return i2c_match_id(driver->id_table, client) //正真的匹配函数
if (strcmp(client->name, id->name) == 0) //name比较(所以驱动和设备代码中的name要匹配的上)
i2c_device_probe
status = driver->probe(client, i2c_match_id(driver->id_table, client)); //总线的probe函数中调用了驱动的probe函数,所以最终需要分析的是驱动的probe函数。
总结:IIC总线上有2个分支:i2c_client链和i2c_driver链,当我们任何一个driver或者client注册时,iic总线都会调用match函数(i2c_device_match)去对client中的name和driver中的name进行循环匹配;如果匹配成功,则表明client 和 driver是适用的,那么iic总线就会调用自身的probe函数(i2c_device_probe),而这个函数又会去调用driver中提供的probe函数,driver中的probe函数会通过匹配上的client获取设备的信息 ,借此完成对设备硬件的初始化和后续工作。
核心层开放给其他部分的注册接口
i2c_add_adapter / i2c_add_numbered_adapter 注册adapter,前者由系统动态分配一个id,后者是由自己指定一个id进行注册
i2c_add_driver 注册driver
i2c_new_device 注册client
i2c_add_adapter
id = idr_alloc(&i2c_adapter_idr, adapter, //这里涉及到了一个idr结构,它是一种高效的搜索树,且这个树预先存放了一些内存,避免了内存不够时出现问题,通过调用idr_alloc将我们的结构放入里面,它会返回一个id,以后我们凭借这个id就能在idr中找到相应的结构了。
i2c_register_adapter(adapter) //注册adapter 备注,需要添加打印信息
INIT_LIST_HEAD(&adap->userspace_clients); //初始化链表 userspace_clients
dev_set_name(&adap->dev, "i2c-%d", adap->nr); //adapter 的名字
adap->dev.bus = &i2c_bus_type; //adapter和总线绑定
adap->dev.type = &i2c_adapter_type; //初始化adapter结构体中内嵌的struct device
res = device_register(&adap->dev); //设备注册
5、adapter 驱动分析(i2c-rk3x.c)
(1)通过platform总线实现adapter驱动
module_platform_driver(rk3x_i2c_driver); //调用宏定义,解析后为下面这样:
static int __init rk3x_i2c_driver_init(void){
return platform_driver_register(&rk3x_i2c_driver);
}
module_init(rk3x_i2c_driver_init);
static int __exit rk3x_i2c_driver_exit(void){
return platform_driver_unregister(&rk3x_i2c_driver);
}
module_exit(rk3x_i2c_driver_init);
所以调用module_platform_driver宏定义,最终其实完成了module_init以及module_exit的入口定义,以及平台驱动的注册。
(2)adapter 的device和driver可以match上,从而调用adapter driver中的probe函数rk3x_i2c_probe
i2c = devm_kzalloc(&pdev->dev, sizeof(struct rk3x_i2c), GFP_KERNEL); //给i2c结构体分配内存
match = of_match_node(rk3x_i2c_match, np);
i2c->soc_data = (struct rk3x_i2c_soc_data *)match->data; //确定计算时序的函数。后面会调用
i2c_parse_fw_timings(&pdev->dev, &i2c->t, true); //获取iic的时序参数
ret = device_property_read_u32(dev, "clock-frequency", &t->bus_freq_hz);获取IIC的时钟频率
ret = device_property_read_u32(dev, "i2c-scl-rising-time-ns", &t->scl_rise_ns);//获取IIC的scl 上升沿时间
ret = device_property_read_u32(dev, "i2c-scl-falling-time-ns", &t->scl_fall_ns);//获取IIC的scl 下降沿时间
device_property_read_u32(dev, "i2c-scl-internal-delay-ns", >scl_int_delay_ns);//获取IIC 的scl internal delay time
ret = device_property_read_u32(dev, "i2c-sda-falling-time-ns", &>sda_fall_ns);//获取IIC的sda 的下降沿时间(默认值为scl_fall_ns)
i2c->adap.owner = THIS_MODULE;
i2c->adap.algo = &rk3x_i2c_algorithm; //为adapter绑定一个iic通信算法
i2c->adap.retries = 3; //主机发送起始信号无应答会重试三次
i2c->adap.dev.of_node = np;
i2c->adap.algo_data = i2c;
i2c->adap.dev.parent = &pdev->dev /*adapter 结构体的填充*/
i2c->i2c_restart_nb.notifier_call = rk3x_i2c_restart_notify; //内核通知链
ret = register_i2c_restart_handler(&i2c->i2c_restart_nb);//内核通知链注册
mem = platform_get_resource(pdev, IORESOURCE_MEM, 0);//获取mem信息
i2c->regs = devm_ioremap_resource(&pdev->dev, mem); //remap
bus_nr = of_alias_get_id(np, "i2c"); //获取adapter number
irq = platform_get_irq(pdev, 0); //中断号获取
ret = devm_request_irq(&pdev->dev, irq, rk3x_i2c_irq, //申请中断,添加中断处理函数,主机给从机发送请求后,从机回复触发中断。
static irqreturn_t rk3x_i2c_irq(int irqno, void *dev_id) //iic中断处理函数
ipd = i2c_readl(i2c, REG_IPD); //读取中断信息
ipd &= ~(REG_INT_BRF | REG_INT_BTF);//清掉不关心的中断信息位
if (ipd & REG_INT_NAKRCV) //判断是否存在非应答情况
rk3x_i2c_stop(i2c, -ENXIO); //停止传输报错
if ((ipd & REG_INT_ALL) == 0) //判断是否有其他任何不正常情况
goto out; //报错
switch (i2c->state) //根据iic的状态完成不同的操作
rk3x_i2c_handle_start(i2c, ipd);
rk3x_i2c_handle_write(i2c, ipd);
rk3x_i2c_handle_read(i2c, ipd);
rk3x_i2c_handle_stop(i2c, ipd);
if (i2c->soc_data->calc_timings == rk3x_i2c_v0_calc_timings) //计算iic 时序
i2c->clk = devm_clk_get(&pdev->dev, NULL);
i2c->pclk = i2c->clk;
ret = clk_prepare(i2c->clk);
ret = clk_prepare(i2c->pclk);
clk_rate = clk_get_rate(i2c->clk);
rk3x_i2c_adapt_div(i2c, clk_rate);
ret = i2c->soc_data->calc_timings(clk_rate, t, &calc); 调用前面确定的时序计算函数
static int rk3x_i2c_v0_calc_timings //计算scl所需的分频器值,并将获得的相关参数保存在&calc
val |= calc.tuning;// 获取计算的值
i2c_writel(i2c, val, REG_CON); //写进IIC控制寄存器
i2c_writel(i2c, (calc.div_high << 16) | (calc.div_low & 0xffff),REG_CLKDIV);
ret = i2c_add_adapter(&i2c->adap); //调用i2c-core.c中提供的接口,注册一个adapter
总结: rk3x_i2c_probe实现了IIC控制器的设备信息获取,完成了adapter结构体的填充和注册,同时完成了中断的获取、注册以及中断处理函数的添加。并完成了时序参数的获取,并就该参数完成了控制器寄存器的初始化(计算出目标时序所需的寄存器值)
6、i2c_algorithm 分析(i2c-rk3x.c)
由前面的adapter驱动的分析可以知道,i2c_algorithm 是和一个adapter绑定在一起的,也就是说每个adapter都有一个自己的i2c_algorithm这样的通信算法实现自己的iic通信。rk1808中的iic绑定的i2c_algorithm 算法是下面这个结构体:
static const struct i2c_algorithm rk3x_i2c_algorithm = {
.master_xfer = rk3x_i2c_xfer, //iic传输
.functionality = rk3x_i2c_func, //返回iic支持的一些功能
};
static int rk3x_i2c_xfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)
clk_enable(i2c->clk);
clk_enable(i2c->pclk); //使能clk
for (i = 0; i < num; i += ret) //开始信息处理,可以一次性处理多个信息
ret = rk3x_i2c_setup(i2c, msgs + i, num - i);//设置从机地址以及读写位
rk3x_i2c_start(i2c); //使能iic中断,发送iic起始信号
val |= REG_CON_EN | REG_CON_MOD(i2c->mode) |REG_CON_START;//通过配置寄存器实现起始信号的发送(Write 1 to I2C_CON[3], the controller will send I2C start command.)
timeout = wait_event_timeout(i2c->wait, !i2c->busy, //睡眠,这里主机发送了一个起始信号后,从机不是立刻马上就能给主机应答,主机需要等待一段时间才能收到从机发来的应答信号,在裸机程序中这个过程可以通过while循环进行等待,这种通过等待轮询的方式在linux中不适用,所以这里通过调用wait_event_timeout将当前这个进程睡眠,让出cpu给其他进程,并等待中断的唤醒,当从机有应答时,会触发主机的中断函数
rk3x_i2c_irq,这这个中断函数中就会重新唤醒这个进程,继续下面的操作。
总结:rk3x_i2c_xfer 完成了主机对从机的起始信号发送,而后就进入睡眠等到从机返回应答信号触发主机中断进入中断处理函数中继续完成接下来的消息传输工作。
7、IIC从机设备驱动(触摸屏驱动)
这里以之前分析过的触摸屏驱动作为例子,rk开发板用的触摸屏是汇顶科技的gt1x型电容式触摸屏,驱动代码位于/driver/input/touchscreen/gt1x/gt1x.c,电容触摸屏通过IIC总线与SOC进行通信,利用其自带的触摸IC完成坐标计算后通过IIC将坐标信息传输给SOC,坐标的计算过程不需要SOC的参与,从这个角度上来说,电容触摸屏就是一个挂载到SOC上的IIC slave设备,与通常所说的Sensor是一样的性质。我们完成电容触摸屏的驱动就是在IIC总线模型下完成其驱动。
static int __init gt1x_ts_init(void) //驱动入口
return i2c_add_driver(>1x_ts_driver); //添加一个iic设备驱动(触摸屏其实就是一个iic设备)
i2c_register_driver(THIS_MODULE, driver) //i2c-core.c提供的iic从设备驱动注册函数,用于注册一个iic从设备驱动
driver->driver.bus = &i2c_bus_type; //指定触摸屏驱动的总线类型是 IIC
INIT_LIST_HEAD(&driver->clients); //初始化链表
res = driver_register(&driver->driver); //注册设备驱动,需要先确定驱动使用的总线
gt1x_ts_driver
static struct i2c_driver gt1x_ts_driver = {
.probe = gt1x_ts_probe,
.remove = gt1x_ts_remove,
.id_table = gt1x_ts_id,
.driver = {
.name = GTP_I2C_NAME,
#ifdef GTP_CONFIG_OF
.of_match_table = gt1x_match_table,
#endif
#if !defined(CONFIG_FB) && defined(CONFIG_PM)
.pm = >1x_ts_pm_ops,
#endif
},
};
根据前面i2c-core.c中i2c_device_match 的分析可以知道,iic设备的driver 和 device的匹配关键要看driver.id_table.name 和 client->name 是否匹配,当两者匹配之后就会执行driver.probe。在这个函数中就时关于触摸屏驱动的方面了。与iic框架没有太大关系。
8、总结
一个SOC中一般会存在好几个iic控制器,每一个iic控制器都有它自己的寄存器地址,每一个IIC控制器在linux iic驱动框架下都对应着一个适配器即 struct i2c-adapter 结构体,在i2c-core.c中完成了该结构体的填充和注册。其实,IIC 控制器在linux IIC子系统中被视为了一个platform设备,在i2c-core.c通过platform平台总线完成了IIC 控制器驱动与设备的匹配而后调用probe函数,IIC控制器driver的probe函数中完成了adapter的结构体成员填充,和注册。
一个IIC控制器下可以挂载多个IIC从设备,它们通过不同的ID加以区分,IIC从机设备通过调用i2c-core提供的rk3x_i2c_xfer函数完成一个start信号的发送(包含从机设备地址),发送完这个start信号后,IIC控制器进入睡眠,让出CPU控制权,IIC总线上的从机设备都会收到它们绑定的IIC控制器发来的start信号(这个过程称为广播),从机设备将收到的信号中的地址与自身的地址进行对比,两者一致就会向总线上回复一个ack信号,该信号会触发IIC控制器中断,在IIC控制器中断的处理函数中会调用后判断IIC所处的的state,根据state进行下一步信号的传输,并在最后唤醒刚才睡眠的进程。
IIC的控制器的驱动是使用Platform总线架构,IIC控制器下挂载的IIC从设备的驱动是使用的IIC总线架构。Linux内核中构建了许多的总线,但并不是都是真实的线,像platfomr总线就是虚拟出来的,IIC总线就是有真是存在的总线作为基地;但是,但是在linux系统中的总线通常都是分出两个分支的,一个是设备device、另一个是驱动driver;它们都是通过链表挂接在一个相同的总线当中。当我们像某个总线注册一个设备的时候,总线会通过挂载在它上面的驱动链表去遍历其驱动,当有一个驱动的name或者id_table中的信息与刚注册的设备的name信息匹配时,就会将该设备与该驱动匹配上,一旦设备和驱动匹配上后就会调用驱动中的probe函数,通常驱动的probe函数都会区初始化刚匹配上的设备以及构建设备节点等。