I2C子系统

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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值