Linux设备驱动之IIC驱动

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总线的设备都有一个唯一的地址,以便于主机寻访。主机和从机的数据传送,可以由主机发送数据到从机,也可以由从机发到主机。

Linux设备驱动之IIC驱动_第1张图片

数据传输

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

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

Linux设备驱动之IIC驱动_第2张图片

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

Linux设备驱动之IIC驱动_第3张图片

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

I2C写时序图如下

Linux设备驱动之IIC驱动_第4张图片

I2C读时序图如下

Linux设备驱动之IIC驱动_第5张图片

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的用户手册进行分析。

Linux设备驱动之IIC驱动_第6张图片

在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或者逐个字节发送,最后等待发送完成。

Linux设备驱动之IIC驱动_第7张图片

分层框架图

Linux设备驱动之IIC驱动_第8张图片

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详细讲解

你可能感兴趣的:(linux,linux,嵌入式,I2C)