Linux设备驱动之IIC驱动

2023-09-22 11:52:35

Linux设备驱动之I2C驱动

I2C是一种半双工串行通信总线,使用多主从架构,总线上会挂载设备,设备通信就会涉及协议,下面一起看看I2C通信协议是怎样的,在Linux系统上软件又是如何驱动的。

I2C通信协议

硬件连接

I2C串行总线一般有两根信号线,一根是双向数据线SDA,另一根是时钟线SCL,数据线即用来表示数据,时钟线用于数据收发同步。所有接到I2C总线设备上的串行数据SDA都接到总线的SDA上,各设备的时钟线SCL接到总线的SCL上。

总线通过上拉电阻接到电源。当I2C设备空闲时,会输出高阻态,而当所有设备都空闲,都输出高阻态时,由上拉电阻把总线拉成高电平。I2C有三种传输模式:标准模式为100kbit/s ,快速模式为400kbit/s ,高速模式下可达3.4Mbit/s,但目前大多I2C 设备尚不支持高速模式。

总线的运行(数据传输)由主机控制。所谓主机是指启动数据的传送(发出启动信号)、发出时钟信号以及传送结束时发出停止信号的设备,通常主机都是微处理器。被主机寻访的设备称为从机。为了进行通讯,每个接到I2C总线的设备都有一个唯一的地址,以便于主机寻访。主机和从机的数据传送,可以由主机发送数据到从机,也可以由从机发到主机。

I2C bus

数据传输

I2C的协议定义了通讯的起始和停止信号、数据有效性、响应、仲裁、时钟同步和地址广播等环节。

在I2C 器件开始通信(传输数据)之前,串行时钟线 SCL 和串行数据线SDA 线由于上拉的原因处于高电平状态,此时I2C总线处于空闲状态。如果主机想开始传输数据,**只需在 SCL 为高电平时将 SDA 线拉低,产生一个起始信号,**从机检测到起始信号后,准备接收数据,当数据传输完成,主机只需产生一个停止信号,告诉从机数据传输结束,停止信号的产生是在 SCL 为高电平时,SDA 从低电平跳变到高电平,从机检测到停止信号后,停止接收数据。I2C 整体时序如下图。起始信号之前为空闲状态,起始信号之后到停止信号之前的这一段为数据传输状态,主机可以向从机写数据,也可以读取从机输出的数据,数据的传输由双向数据线(SDA)完成。停止信号产生后,总线再次处于空闲状态。

IIC

我们在起始信号之后,主机开始发送传输的数据;在串行时钟线 SCL 为低电平状态时,SDA 允许改变传输的数据位(1 为高电平,0 为低电平),在SCL 为高电平状态时,SDA 要求保持稳定,相当于一个时钟周期传输 1bit 数据,经过8 个时钟周期后,传输了 8bit 数据,即一个字节。第8 个时钟周期末,主机释放SDA 以使从机应答,在第 9 个时钟周期,从机将 SDA 拉低以应答;如果第 9 个时钟周期,SCL 为高电平时,SDA 未被检测到为低电,视为非应答,表明此次数据传输失败。第 9 个时钟周期末,从机释放 SDA 以使主机继续传输数据,如果主机发送停止信号,此次传输结束。我们要注意的是数据以8bit 即一个字节为单位串行发出,其最先发送的是字节的最高位。

IIC sequence

器件地址(也称从机地址,SLAVE ADDRESS):每个I2C 器件都有一个器件地址,有些 I2C 器件的器件地址是固定的,而有些 I2C 器件的器件地址由一个固定部分和一个可编程的部分构成。当主机想给某个器件发送数据时,只需向总线上发送接收器件的器件地址即可。 进行数据传输时,主机首先向总线上发出开始信号,对应开始位S,然后按照从高到低的位序发送器件地址,一般为 7bit,第 8bit 位为读写控制位R/W,该位为 0 时表示主机对从机进行写操作,当该位为1 时表示主机对从机进行读操作,然后接收从机响应

I2C写时序图如下

IIC write

I2C读时序图如下

IIC read

Linux I2C

上面简单介绍了I2C协议的内容,在软件端又是如何驱动呢?下面以Linux5.15全志平台为例进行介绍。

Linux IIC驱动代码在drivers/i2c目录下,我们都知道,Linux最擅长的就是分层,通过分层概念,将应用与硬件平台剥离开来,IIC驱动也不例外,也有分层。在介绍软件分层之前,先了解一下IIC软件概念。

  • i2c_adapter:对应物理上的一个适配器,其实就是集成在SOC上的I2C控制器,也就是I2C通信中的主控制器。
  • i2c_algorithm:对应控制器的传输方式,因为每个平台的I2C控制器设置的方式不一样,需要根据自己的硬件特性实现这个传输方式。
  • i2c_client:对应真实的I2C物理设备device,也就是I2C通信中的从设备。
  • i2c_driver:从设备对应的驱动。

I2C驱动初始化

在其他设备需要使用I2C功能的时候,得先有I2C驱动的加载,每个平台驱动加载的方式,全志Linux5.15的驱动在bsp/drivers/twi/twi-sunxi.c。Linux驱动很多都是作为platform driver先行加载,当platform信息匹配(board.dts与driver的信息compatible一致),则将会调用到platform driver的probe函数。sunxi i2c probe函数,主要进行以下操作:

  1. 获取dts配置信息,包括寄存器映射范围、pinctrl、时钟配置、中断信息、是否使用DMA等。这个不同的平台会有差异,但都会有从dts获取配置信息的过程。
  2. 在获取上述配置信息之后,将使能I2C的IO供电、时钟等,还会向系统注册休眠唤醒的相关配置。
  3. 最重要的,初始化i2c_adapter,i2c_adapter中会包含i2c_algorithm,并通过i2c_add_numbered_adapter()函数向系统注册i2c_adapter,注册之后,系统才有I2C功能。

i2c_add_numbered_adapter()函数的操作,除了向内核注册一个device设备以外,还会把设备的bus设置为i2c_bus_type,type赋值为i2c_adapter_type,最后还会通过of_i2c_register_devices()函数注册挂载到该I2C总线上的从设备i2c_client。这个过程,通过查看dts的信息,可以简要理解,一个twi0总线,挂载着eeprom、pcie_usb_phy。

twi0 {
		#address-cells = <1>;
		#size-cells = <0>;
		compatible = "allwinner,sunxi-twi-v101";
		device_type = "twi0";
		reg = <0x0 0x02502000 0x0 0x400>;
		interrupts = <GIC_SPI 10 IRQ_TYPE_LEVEL_HIGH>;
		clocks = <&ccu CLK_TWI0>;
		clock-names = "bus";
		resets = <&ccu RST_BUS_TWI0>;
		dmas = <&dma 43>, <&dma 43>;
		dma-names = "tx", "rx";
        clock-frequency = <400000>;
        pinctrl-0 = <&twi0_pins_default>;
        pinctrl-1 = <&twi0_pins_sleep>;
        pinctrl-names = "default", "sleep";
        twi_drv_used = <1>;
        status = "okay";

        eeprom@50 {
                compatible = "atmel,24c16";
                reg = <0x50>;
                status = "okay";
        };
        pcie_usb_phy@74 {
                compatible = "combphy,phy74";
                reg = <0x74>;
                status = "disabled";
        };
        pcie_usb_phy@75 {
                compatible = "combphy,phy75";
                reg = <0x75>;
                status = "disabled";
        };
}

上面介绍的仅仅是I2C控制器的初始化,但是内核与用户空间分别都是怎么使用I2C的呢,下面继续。

内核空间使用I2C

在内核中,设备与驱动是相互依赖的,仅有驱动,没有设备,无法完成设备模型匹配,从而不能使用相关资源。上一章节已经介绍到,在dts中配置了I2C总线的从设备,i2c_adapter注册时会注册从设备,现在有了设备,驱动又是应该如何操作呢?代码的简要流程如下。

内核使用I2C的方式如下:

  1. 通过i2c_add_driver(i2c_driver)函数向内核注册i2c驱动,当驱动与设备匹配时,将会调用到驱动的probe函数;
  2. 上述在匹配的时候,将会把i2c_bus_type中的所有i2c_adapter设备都拿出来,逐个进行遍历,通过匹配compatible等信息,将i2c_client与i2c_driver绑定起来(i2c_client已经在上面挂载到i2c_adapter中);
  3. 这个时候可以通过i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)进行I2C通信了。

用户空间使用I2C

用户空间需要使用I2C,需要内核配置CONFIG_I2C_CHARDEV,配置该选项之后,才会编译i2c-dev.c。i2c-dev.c主要监听i2c_bus_type总线,当总线有注册/卸载i2c adapters时,都将会调用i2cdev_notifier_call()。i2cdev_notifier_call()的实现,实际上就是注册i2c adapters时,向系统注册一个字符设备,设备节点为 /dev/i2c-X,其中X是I2C总线索引。

注册 /dev/i2c-X字符设备后,用户空间的操作,都将会传递到i2cdev_fops,而用户空间的使用,简略如下:

	/* 打开/dev/i2c-X节点 */
	file = open("/dev/i2c-X", O_RDWR);

	/* 设置从设备地址 */
	ioctl(file, I2C_SLAVE, I2C_ADDRESS)/* 发送i2c_msg */
	struct i2c_rdwr_ioctl_data data;
	struct i2c_msg messages[2];
	char buffer[2];

    buffer[0] = xxx;
    buffer[1] = xx;
    messages[0].addr = I2C_ADDRESS; //从设备地址
    messages[0].flags = 0; //0表示写,1表示读
    messages[0].len = 2; //数据长度
    messages[0].buf = buffer; //传输的数据包
    data.msgs = messages;
    data.nmsgs = 1;
    ioctl(file, I2C_RDWR, &data); 

	/* 关闭节点 */
	close(file);

上面是用户空间的使用,而内核的实现,则与上述内核空间的使用类似。

全志平台I2C数据传输

内核通过i2c_transfer()函数进行I2C数据传输时,传递路径是这样的。i2c_transfer() —> __i2c_transfer() —> adap->algo->master_xfer(),此时不就调用到包含在i2c_adapter的i2c_algorithm的master_xfer()成员函数来进行数据传输,全志平台的实现函数是sunxi_twi_xfer()。

全志平台,I2C传输有两种模式,一种是engine模式,另一种是drv模式。engine模式是每发送一个字节,将会进入中断,配置下一个发送字节,而drv模式,则是仅在开始发送时配置好信息,发送完成后再触发I2C中断。sunxi_twi_xfer()中有区分这两种模式,下面分别介绍。

engine模式

engine模式调用的发送函数是sunxi_twi_engine_xfer(),sunxi_twi_engine_xfer()的实现概括如下:

  1. 使能engine中断;
  2. 通过sunxi_twi_engine_set_start()发送开始信号;
  3. 等待发送完成;

看似该函数没有进行什么实质性的操作,仅仅发出一个开始信号,这个时候,结合全志平台的TWI的用户手册进行分析。

TWI interrupt status

在sunxi_twi_engine_xfer()中,我们使能了中断,并发送了开始信号,往I2C控制器完成开始信号发送时,将会触发中断,而且中断状态寄存器的信息应该是0x08,此时,我们的engine模式中断处理函数sunxi_twi_engine_core_process()将会通过sunxi_twi_engine_addr_byte()函数,发送丛设备地址。接下来就是根据0xd0、0x28、0x40、0x50、0x58等中断状态进行发送和读取数据,

drv模式

drv模式发送函数是sunxi_twi_drv_xfer(),sunxi_twi_drv_xfer()实现的逻辑如下:

  1. 按照i2c_msg的包读写位,分开读和写分msg处理,读调用sunxi_twi_drv_rx_msgs()函数,写调用sunxi_twi_drv_tx_one_msg()函数;
  2. sunxi_twi_drv_rx_msgs()函数中,先写从设备地址,包数量以及数据包有效数据长度。如果发送数据长度超过FIFO大小(32 Bytes),则使用DMA传递,否则使用CPU搬运。DMA搬运的情况下,配置好DMA之后,使能发送、相关中断,等待传输完成。而CPU搬运的情况下,则是使能RX_REQ_INT_EN,等待数据搬运完成。drv的中断状态寄存器如下,当搬运一个包完成,则触发中断(上面已经设置了数据包有效数据长度)。
  3. sunxi_twi_drv_tx_one_msg()函数与读函数类似,设置丛集地址,msg包长度,根据长度配置dma或者逐个字节发送,最后等待发送完成。

drv interrupt status

分层框架图

IIC framework

debug

系统节点

查看I2C运行状态,包括控制器的寄存器值:

cat /sys/devices/platform/soc@3000000/2502000.twi0/status

root@TinaLinux:/# cat /sys/devices/platform/soc@3000000/2502000.twi0/status
twi->bus_num = 0
twi->status  = [1] Idle
twi->msg_num   = 0, ->msg_idx = 0, ->buf_idx = 0
twi->bus_freq  = 400000
twi->irq       = 116
twi->debug_state = 0
twi->base_addr = 0x0000000016e8f0ae, the TWI control register:
[ADDR] 0x00 = 0x00000000, [XADDR] 0x04 = 0x00000000
[DATA] 0x08 = 0x00000000, [CNTR] 0x0c = 0x00000000
[STAT]  0x10 = 0x00000000, [CCR]  0x14 = 0x00000000
[SRST] 0x18 = 0x00000000, [EFR]   0x1c = 0x00000000
[LCR]  0x20 = 0x00000000
root@TinaLinux:/#
I2C Tools

在Tina openwrt中,通过make menuconfig,选择i2c-tools这个包,然后编译,系统中将会增加以下命令:

  • i2cdetect:i2c设备查询,比如需要探测i2c-1上挂载的设备,可以执行i2cdetect -y 1,将会得到哪个设备地址有回应。
  • i2cdump:读取i2c设备寄存器,i2cdump -y 1 0x18,读取i2c-1上,从设备地址为0x18的寄存器信息。
  • i2cset:写i2c设备寄存器,i2cset -y 1 0x18 0xf 0x5,向i2c-1上设备地址为0x18的0xf寄存器地址写入值0x5。
  • i2cget:读寄存器数据,i2cget -y 1 0x18 0xf,读取i2c-1上设备地址为0x18的0xf寄存器内容。

FAQ

全志平台上的TWI通信,出现异常的时候,log信息有相关的提示,总体信息如下:

TWI数据未完全发送

问题现象: incomplete xfer。具体的log如下所示:

[ 1658.926643] sunxi_i2c_do_xfer()1936 - [i2c0] incomplete xfer (status: 0x20, dev addr: 0x50)
[ 1658.926643] sunxi_i2c_do_xfer()1936 - [i2c0] incomplete xfer (status: 0x48, dev addr: 0x50)

问题分析:此错误表示主控已经发送了数据(status值为0x20时,表示发送了SLAVE ADDR + WRITE;status值为0x48时,表示发送了SLAVE ADDR + READ),但是设备没有回ACK,这表明设备无响应,应该检查是否未接设备、接触不良、设备损坏和上电时序不正确导致的设备未就绪等问题。

问题排查步骤

步骤1:通过设备树里面的配置信息,核对引脚配置是否正确。每组TWI都有好几组引脚配置。

步骤2:更换TWI总线下的设备为at24c16,用i2ctools读写at24c16看看是否成功,成功则表明总线工作正常;

步骤3:排查设备是否可以正常工作以及设备与I2C之间的硬件接口是否完好;

步骤4:详细了解当前需要操作的设备的初始化方法,工作时序,使用方法,排查因初始化设备不正确导致通讯失败;

步骤5:用示波器检查TWI引脚输出波形,查看波形是否匹配。

TWI起始信号无法发送

问题现象: START can’t sendout!。具体的log如下所示:

sunxi_i2c_do_xfer()1865 - [i2c1] START can't sendout!

问题分析:此错误表示TWI无法发送起始信号,一般跟TWI总线的引脚配置以及时钟配置有关。应该检查引脚配置是否正确,时钟配置是否正确,引脚是否存在上拉电阻等等。

问题排查步骤

步骤1:重新启动内核,通过查看log,分析TWI是否成功初始化,如若存在引脚配置问题,应核对引脚信息是否正确;

步骤2:根据原理图,查看TWI-SCK和TWI-SDA是否经过合适的上拉电阻接到3.3v电压;

步骤3:用万用表量SDA与SCL初始电压,看电压是否在3.3V附近(断开此TWI控制器所有外设硬件连接与软件通讯进程);

步骤4:核查引脚配置以及clk配置是否进行正确设置;

步骤5: 测试PIN的功能是否正常,利用寄存器读写的方式,将PIN功能直接设为INPUT功能(echo [reg] [val] > /sys/class/sunxi_dump/write),然后将PIN上拉和接地改变PIN状态,读PIN的状态(echo [reg,reg] > /sys/class/sunxi_dump/dump;cat dump),看是否匹配。

步骤6:测试CLK的功能是否正常,利用寄存器读写的方式,将TWI的CLK gating等打开,(echo [reg] [val] > /sys/class/sunxi_dump/write),然后读取相应TWI的寄存器信息,读TWI寄存器的数据(echo [reg],[len]> /sys/class/sunxi_dump/dump),查看寄存器数据是否正常。

TWI终止信号无法发送

问题现象: STOP can’t sendout。具体的log如下所示:

twi_stop()511 - [i2c4] STOP can't sendout!
sunxi_i2c_core_process()1726 - [i2c4] STOP failed!

问题分析:此错误表示TWI无法发送终止信号,一般跟TWI总线的引脚配置。应该检查引脚配置是否正确,引脚电压是否稳定等等。

问题排查步骤

步骤1:根据原理图,查看TWI-SCK和TWI-SDA是否经过合适的上拉电阻接到3.3v电压;

步骤2:用万用表量SDA与SCL初始电压,看电压是否在3.3V附近(断开此TWI控制器所有外设硬件连接与软件通讯进程);

步骤3:测试PIN的功能是否正常,利用寄存器读写的方式,将PIN功能直接设为INPUT功能(echo [reg] [val] > /sys/class/sunxi_dump/write),然后将PIN上拉和接地改变PIN状态,读PIN的状态(echo [reg,reg] > /sys/class/sunxi_dump/dump;cat dump),看是否匹配;

步骤4: 查看设备树配置,把其他用到SCK/SDA引脚的节点关闭,重新测试I2C通信功能。

TWI传送超时

问题现象: xfer timeout。具体的log如下所示:

[123.681219] sunxi_i2c_do_xfer()1914 - [i2c3] xfer timeout (dev addr:0x50)

问题分析:此错误表示主控已经发送完起始信号,但是在与设备通信的过程中无法正常完成数据发送与接收,导致最终没有发出终止信号来结束I2C传输,导致的传输超时问题。应该检查引脚配置是否正常,CLK配置是否正常,TWI寄存器数据是否正常,是否有其他设备干扰,中断是否正常等问题。

问题排查步骤

步骤1:核实TWI控制器配置是否正确;

步骤2:根据原理图,查看TWI-SCK和TWI-SDA是否经过合适的上拉电阻接到3.3v电压;

步骤3:用万用表量SDA与SCL初始电压,看电压是否在3.3V附近(断开此TWI控制器所有外设硬件连接与软件通讯进程);

步骤4:关闭其他TWI设备,重新进行烧录测试TWI功能是否正常;

步骤5: 测试PIN的功能是否正常,利用寄存器读写的方式,将PIN功能直接设为INPUT功能(echo [reg] [val] > /sys/class/sunxi_dump/write),然后将PIN上拉和接地改变PIN状态,读PIN的状态(echo [reg,reg] > /sys/class/sunxi_dump/dump;cat dump),看是否匹配;

步骤6:测试CLK的功能是否正常,利用寄存器读写的方式,将TWI的CLK gating等打开,(echo [reg] [val] > /sys/class/sunxi_dump/write),然后读取相应TWI的寄存器信息,读TWI寄存器的数据(echo [reg] ,[len]> /sys/class/sunxi_dump/dump),查看寄存器数据是否正常;

步骤7:根据相关的LOG跟踪TWI代码执行流程,分析报错原因。

参考资料

基础通信协议之 IIC详细讲解

更多推荐

解锁网络世界的利器:代理IP与Socks5代理

随着跨界电商、爬虫、网络安全和游戏等领域的不断发展,网络工程师们正面临着越来越多的挑战和机会。在这个信息爆炸的时代,如何更有效地访问、保护和探索网络资源成为了网络工程师们的首要任务。本文将重点介绍代理IP和Socks5代理,它们是网络世界的利器,为网络工程师提供了强大的工具来应对各种技术挑战。代理IP的妙用代理IP是一

爱分析《商业智能最佳实践案例》

近日,国内知名数字化市场研究咨询机构爱分析发布《2023爱分析·商业智能最佳实践案例》,此评选活动面向落地商业智能的各行企业和商业智能厂商,以第三方专业视角深入调研,评选出具有参考价值的创新案例。永达汽车集团与数聚股份合作的数字化智能运营管理平台项目,凭借突出的实践领先性与案例创新性,作为商业智能典型案例实力入选本次报

Android 插件开发框架 总结

1)类转载器ClassLoader:标准的javaSDK中有ClassLoader类,ClassLoader加载类的方式常称为双亲委托,ClassLoader.java具体代码如下:protectedClass<?>loadClass(StringclassName,booleanresolve)throwsClass

物联网的未来:连接的智能世界

物联网(IoT)是引领我们走向未来的一项关键技术。它让物品通过互联网进行连接,交流,开创了智能生活新时代。预计到2025年,全球将拥有超过410亿的IoT设备。在对人类生活的每个方面产生影响的同时,物联网也正在为经济增长、社会进步和环境可持续性开创新的可能性。物联网的最大优势在于其无所不在的连通性。从智能家居到工业自动

HEC-RAS 1D/2D水动力与水环境模拟教程

详情点击公众号技术科研吧链接:HEC-RAS1D/2D水动力与水环境模拟教程前言水动力与水环境模型的数值模拟是实现水资源规划、环境影响分析、防洪规划以及未来气候变化下预测和分析的主要手段。然而,一方面水动力和水环境模型的使用非常复杂,理论繁复;另一方面,免费的水动力和水环境软件往往缺少重要功能,而商业软件则非常昂贵。H

沈阳闪耀“城市之光”,小赢卡贷与民宿创业者共创美好未来!

根据最新发布的《2023年暑期旅游市场趋势报告》显示,旅游市场的复苏势头正迅猛加速,公众对出游的信心也持续恢复,超过70%的受访者表示他们计划在暑假期间进行旅行。沈阳作为一个备受欢迎的暑期旅游目的地,吸引了大量游客的目光,根据携程发布的《2023年五一出游数据报告》,五一期间,沈阳累计接待人次达到611.06万,旅游总

【Linux】【网络】UDP、TCP 网络接口及使用

文章目录socket及相关补充0.netstat--查询当前服务器上网络服务器1.端口号(port)2.网络字节序3.sockaddr结构体一、socket常见APIUDP0.IP地址转化函数1.socket函数:创建socket文件描述符(TCP/UDP,客户端+服务器)2.bind函数:绑定端口号(TCP/UDP,

代码随想录算法训练营Day56 | 动态规划(16/17) LeetCode 583. 两个字符串的删除操作 72. 编辑距离

动态规划马上来到尾声了,当时还觉得动态规划内容很多,但是也这么过来了。第一题583.DeleteOperationforTwoStringsGiventwostringsword1andword2,returntheminimumnumberofstepsrequiredtomakeword1andword2thesa

基于复旦微的FMQL45T900全国产化ARM核心模块(100%国产化)

TES745D是一款基于上海复旦微电子FMQL45T900的全国产化ARM核心板。该核心板将复旦微的FMQL45T900(与XILINX的XC7Z045-2FFG900I兼容)的最小系统集成在了一个87*117mm的核心板上,可以作为一个核心模块,进行功能性扩展,能够快速的搭建起一个信号平台,方便用户进行产品开发。核心

vivo面试-Java

一、JAVA八股1、Java实现线程的三种方式(1)继承Thread类:创建一个新类,该类继承自Thread类,并重写run方法。然后创建该类的实例,并调用它的start方法来启动线程。publicclassMyThreadextendsThread{publicvoidrun(){System.out.println

UML的组成

UML的构造块在UML(统一建模语言)中,事物是指建模中的各种元素、概念和组件,用于描述软件系统的不同方面。以下是一些常见的UML事物:类(Class):用于表示系统中的对象类型或类别,包括属性和方法。对象(Object):表示系统中的实际对象实例。接口(Interface):描述类或组件的合同,规定了可以被其他类或组

热文推荐