I2C子系统

I2C协议

image-20240802091426295

串行、半双工总线,主要用于低速、近距离的芯片之间的通信

I2C信号

**开始信号(S):**SCL为高电平时,SDA由高电平向低电平跳变

**结束信号(P):**SCL为高电平时,SDA由低电平向高电平跳变

**响应信号(ACK):**接收器在接收到8位数据后,在第九个时钟周期拉低SDA

SDA上传输的数据必须在SCL为高电平期间保持稳定,SDA上的数据只能在SCL为低电平期间变化

image-20240802092244068

数据格式

写操作

白色:主→从 灰色:从→主

image-20240802092349788

方向:读/写(0:写 1读)

读操作

image-20240802092525515

具体实现

芯片可以通过SDA线来传输数据,也可以通过它读取数据,那么主设备与从设备肯定不能同时控制SDA线

在9个时钟里(数据+回应):

  1. 前8个时钟由主设备发送数据的话,第9个时钟就由从设备发送数据
  2. 前8个时钟由从设备发送数据的话,第9个时钟就由主设备发送数据

如何才能做到互不影响?

两端使用三极管来实现,如下图所示

image-20240802093303846

image-20240802093408889

A驱动cmos管,接地,SDA输出低电平

A、B同时不驱动,由于上拉电阻的存在,SDA输出高电平

只要有一方驱动三极管,那么SDA都会输出低电平

SMBus协议

IIC (二) – SMBus协议和基础知识介绍-CSDN博客

SMBus:系统管理总线

SMBus是基于I2C协议的,是I2C协议的子集,但是相较于I2C协议要求更加的严格

例如:(重复发送S信号)

读取EEPROM时,涉及两个操作:

  1. 把存储地址发送给设备
  2. 读数据

这两步之间的衔接原本需要发送一个P信号停止,再发送一个S信号重新开始

而SMBus对此进行了改进:两步之间的衔接可以直接发送S信号

除此之外,

对于I2C协议,它只定义了怎么传输数据,但是并没有定义数据的格式,这完全由设备来定义

对于SMBus协议,它定义了几种数据格式

I2C子系统的框架

软件框架

image-20240802094457219

应用程序可以通过自己编写的设备驱动访问I2C控制器,从而访问I2C设备,也可以通过内核自带的i2c-dev,c驱动程序访问I2C控制器(I2C_Tools),从而访问I2C设备

设备驱动框架

image-20240802130959818**I2C适配层:**设备树的I2C控制器节点会被转换为platform_device结构体,与I2C控制器驱动进行匹配,若匹配成功则调用控制器驱动的probe函数,对I2C控制器进行初始化,生成i2c_adapter结构体,再调用i2c_add_adapter,将其添加到I2C控制器链表中

**I2C总线核心层:**遍历adapter对象的子节点并生成i2c_client对象,将i2c_client对象挂载到adapter的client链表中,如果有i2c_driver注册,内核会遍历adapter链表中每一项下的client链表进行匹配,若匹配成功则调用i2c_driver的probe函数,并返回client对象

**I2C从机设备驱动层:**定义驱动程序并注册

要编写设备驱动程序:我们需要做的仅仅只是定义并注册驱动程序

重要的结构体

I2C总线

struct bus_type i2c_bus_type = {
	.name		= "i2c",
	.match		= i2c_device_match,
	.probe		= i2c_device_probe,
	.remove		= i2c_device_remove,
	.shutdown	= i2c_device_shutdown,
	.pm		= &i2c_device_pm_ops,
};

i2c_device_match:I2C设备与I2C驱动是否匹配

i2c_device_probe:如果匹配则调用,进而调用I2C驱动的probe函数

I2C驱动

struct i2c_driver {
	int (*probe)(struct i2c_client *, const struct i2c_device_id *); //probe函数
	struct device_driver driver;                                 //表明这是一个驱动
	const struct i2c_device_id *id_table;                        //要匹配的从设备信息(名称)
	int (*detect)(struct i2c_client *, struct i2c_board_info *); //设备探测函数
	const unsigned short *address_list;                          //设备地址
	struct list_head clients;                                    //设备链表
};

对应I2C驱动程序

I2C设备

struct i2c_client {
	unsigned short addr;         //设备地址
	char name[I2C_NAME_SIZE];    //设备名称
	struct i2c_adapter *adapter; //I2C控制器
	struct i2c_driver *driver;   //设备对应的驱动
	struct device dev;           //表明这是一个设备
	int irq;                     //中断号
	struct list_head detected;   //节点
};

对应I2C设备

adapter是这个设备所属的I2C控制器结构体

I2C控制器

通过I2C控制器,I2C驱动可以与I2C设备进行通信

struct i2c_adapter {
	unsigned int id;                  //设备器的编号
	const struct i2c_algorithm *algo; //算法,传输函数
	struct device dev;                //表明这是一个设备
	...
	int nr;                           //哪一条I2C总线
	...
};

数据

在上面的i2c_algorithm结构体中可以看到要传输的数据被称为:i2c_msg

image-20240802100649325

i2c_msg中的flags用来表示传输方向:bit 0等于I2C_M_RD表示读,bit 0等于0表示写

举例:设备地址为0x50的EEPROM,要读取它里面存储地址为0x10的一个字节,应该构造几个i2c_msg?

  • 要构造2个i2c_msg
  • 第一个i2c_msg表示写操作,把要访问的存储地址0x10发给设备
  • 第二个i2c_msg表示读操作
  • u8 data_addr = 0x10;
    i8 data;
    struct i2c_msg msgs[2];
    
    msgs[0].addr   = 0x50;
    msgs[0].flags  = 0;             
    msgs[0].len    = 1;
    msgs[0].buf    = &data_addr;
    
    msgs[1].addr   = 0x50;
    msgs[1].flags  = I2C_M_RD;
    msgs[1].len    = 1;
    msgs[1].buf    = &data;
    

    内核里的传输函数:

    int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num);
    // num:几个i2c_msg
    

    I2C-Tools

    对于I2C设备,内核提供了驱动程序drivers/i2c/i2c-dev.c,通过它可以直接使用下面的I2C控制器驱动程序来访问I2C设备

    使用一句话概括I2C传输:APP通过I2C Controller与I2C Device传输数据

    所以使用I2C-Tolls时也需要指定:

  • 哪个I2C控制器(或称为I2C BUS、I2C Adapter)
  • 哪个I2C设备(设备地址)
  • 数据:读还是写、数据本身
  • I2C-Tools访问I2C设备的两种方式

  • 怎么指定I2C控制器?
  • i2c-dev.c提供为每个I2C控制器(I2C Bus、I2C Adapter)都生成一个设备节点:/dev/i2c-0、/dev/i2c-1等待
  • open某个/dev/i2c-X节点,就是去访问该I2C控制器下的设备
  • 怎么指定I2C设备?
  • 通过ioctl指定I2C设备的地址
  • ioctl(file, I2C_SLAVE, address)
  • 如果该设备已经有了对应的设备驱动程序,则返回失败
  • ioctl(file, I2C_SLAVE_FORCE, address)
  • 如果该设备已经有了对应的设备驱动程序
  • 但是还是想通过i2c-dev驱动来访问它
  • 则使用这个ioctl来指定I2C设备地址
  • 怎么传输数据?
  • 两种方式
  • 一般的I2C方式:ioctl(file, I2C_RDWR, &rdwr)
  • SMBus方式:ioctl(file, I2C_SMBUS, &args)
  • I2C方式:i2ctransfer.c

    image-20240802124015311

    SMBus方式

    image-20240802124234733

    终端直接控制

    1. i2cdetect(检测I2C器件工具) :

      i2cdetect 0 :确定I2C0这条总线上有多少个I2C设备(UU代表这个设备已经存在驱动程序,其他值代表着设备地址)
      
      i2cdetect -y:查询这个板子上有多少条I2C总线
      
    2. i2cdump(查看寄存器值工具) :

      i2cdump -f -y 0 0x68 //读取 I2C 总线 0 上地址为 0x68 的设备寄存器内容
      
    3. i2cget(读取寄存器值工具)

      i2cget -f -y 0 0x68 0x06       //读地址0x06的寄存器值
      
    4. i2cset(设置寄存器值工具)

      i2cset -f -y 0 0x68 0x06 0x18  //设置为18
      
    5. i2ctransfer(数据传输,但是基于最基本的I2C协议,而不是SMBus协议)

      i2ctransfer -f -y 0 w2@0x1e 0 0x4
      i2ctransfer -f -y 0 w1@0x1e 0xc r2
      

    使用示例(AP3216C传感器):

  • 复位:往寄存器0写入0x4
  • 使能:往寄存器0写入0x3
  • 读光强:读寄存器0xC、0xD得到2字节的光强
  • 读距离:读寄存器0xE、0xF得到2字节的距离值
  • 使用SMBus协议:

    i2cset -f -y 0 0x1e 0 0x4
    i2cset -f -y 0 0x1e 0 0x3
    i2cget -f -y 0 0x1e 0xc w
    i2cget -f -y 0 0x1e 0xe w
    

    使用I2C协议:

    i2ctransfer -f -y 0 w2@0x1e 0 0x4
    i2ctransfer -f -y 0 w2@0x1e 0 0x3
    i2ctransfer -f -y 0 w1@0x1e 0xc r2
    i2ctransfer -f -y 0 w1@0x1e 0xe r2
    

    编写APP控制

    函数接口:

    open_i2c_dev(int i2cbus, char * filename, size_t size, int quit)
    // 第几条总线 argv[1]-'0' 数组 数组大小 填0可以打印错误信息
    
    set_slave_addr(file, dev_addr, 1)
    // open_i2c_dev的返回值 设备地址 强制设置
    
    unsigned char *str = argv[3];
    while(*str)
    {
    	i2c_smbus_write_byte_data(file, mem_addr, *str);
    	// 文件索引 寄存器地址 要写入的一字节数据
    	// 写完一个字节后需要等待10ms
    	// struct timespec req;
        // req.tv_sec  = 0;    // 多少秒
        // req.tv_nsec = 20000000;   // 多少纳秒
        naonsleep(&req, NULL);
    	mem_addr++;
    	str++;
    }
    // 字符串结束符
    i2c_smbus_write_byte_data(file, mem_addr, *str); 
    
    i2c_smbus_read_i2c_block_data(file, mem_addr, sizeof(buf), buf);
    buf[32] = '\0';
    printf("get data:%s", buf  );
    //读出32字节数据 文件索引 寄存器地址 数组大小 数据存放的数组
    

    设备驱动

    设备驱动模型

    image-20240802133952834

    i2c_driver

    匹配
    //用于和设备树匹配
    static const struct of_device_id ap3216_dt_match[] = {
    	{ .compatible = "alientek,ap3216c", },
    	{ }, 
    };
    
    //用于和一般的i2c设备匹配,不管i2c设备来自设备树还是手工创建
    static const struct i2c_device_id ap3216_i2c_id[] = {
    	{ "ap3216c", },
    	{ }
    };
    
    static struct i2c_driver ap3216_i2c_driver = {
    	.driver = {
    		.owner = THIS_MODULE,
    		.name = "ap3216",
    		.of_match_table	= ap3216_dt_match,
    	},
    	.probe = ap3216_i2c_probe,
    	.remove = ap3216_i2c_remove,
    	.id_table = ap3216_i2c_id,
    };
    

    i2c_driver表明能支持哪些设备:

  • 使用of_match_table来判断
  • 设备树中,某个I2C控制器节点下可以创建I2C设备的节点
  • 如果I2C设备节点的compatible属性跟of_match_table的某项兼容,则匹配成功
  • i2c_client.name跟某个of_match_table[i].compatible值相同,则匹配成功
  • 使用id_table来判断
  • i2c_client.name跟某个id_table[i].name值相同,则匹配成功
  • i2c_driver跟i2c_client匹配成功后,就调用i2c_driver.probe函数

    常用函数接口
    i2c_add_driver(driver)
    i2c_register_driver(THIS_MODULE, driver)
    // 注册i2c驱动
    
    void i2c_del_driver(struct i2c_driver *driver);
    // 删除i2c驱动
    

    i2c_client

    设备树

    在对应的i2c控制器节点下创建子节点,并将引脚复用为i2c功能

    	i2c1: i2c@400a0000 {
    		/* ... master properties skipped ... */
    		clock-frequency = <100000>;
    
    		flash@50 {
    			compatible = "atmel,24c256";
    			reg = <0x50>;
    		};
    
    		pca9532: gpio@60 {
    			compatible = "nxp,pca9532";
    			gpio-controller;
    			#gpio-cells = <2>;
    			reg = <0x60>;
    		};
    	};
    
    函数
    i2c_new_device
    

    使用示例:

    // 使用 i2c_new_device 函数创建 i2c_client 结构体
    static int __init ap3216_i2c_clien_init(void)
    {
    	int bus = 0;
    	struct i2c_adapter *adapter;
    
    	struct i2c_board_info ap3216_info = {
    		I2C_BOARD_INFO("ap3216c", 0x1e),   // 名字  地址
    	};
    
    	/* get i2c adapter bus*/
    	adapter = i2c_get_adapter(bus);
    	if (!adapter) {
    		pr_err("%s failed to get i2c adapter %d.\n", __func__, bus);
    		return -1;
    	}
    	/* register i2c device */
    	client = i2c_new_device(adapter, &ap3216_info);
    
    	/* release i2c adapter */
    	i2c_put_adapter(adapter);
    	return 0;
    }
    
    static void __exit ap3216_i2c_clien_exit(void)
    {
    	i2c_unregister_device(client);
    }
    
    用户空间
      // 创建一个i2c_client, .name = "eeprom", .addr=0x50, .adapter是i2c-3
      # echo eeprom 0x50 > /sys/bus/i2c/devices/i2c-3/new_device
      
      // 删除一个i2c_client
      # echo 0x50 > /sys/bus/i2c/devices/i2c-3/delete_device
    

    作者:ad_l

    物联沃分享整理
    物联沃-IOTWORD物联网 » I2C子系统详解

    发表回复