139.Linux下IIC驱动框架

1.IIC物理总线

I2C是一种常见的串行通信接口,用来连接各种外设与传感器,由一条时钟线SCL,负责数据收发同步,以及一条数据线,用来传输具体的数据,支持一主多从,各设备的地址独立,标准传输速率为100kbit/s,快速模式为400kbit/s

2. I2C驱动框架简介

Linux内核将IIC驱动框架分为三个部分:

I2C总线驱动:SOC的I2C控制器(适配器)驱动,SOC厂商提供,用于I2C读写时序的主要数据结构:I2C_adapter、i2c_algorithm

I2C设备驱动:具体的I2C设备驱动,通过I2C适配器与CPU进行数据交换,主要数据结构:i2c_driver和i2c_client

I2C核心:提供I2C总线驱动和设备驱动的注册,注销方法,与I2C通信硬件无关的代码

3. I2C总线驱动

I2C总线驱动的重点是I2C适配器驱动,即SOC的I2C控制器驱动,Linux内核将I2C控制器驱动抽象成i2c_adapter,定义在include/linux/i2c.h文件中,内容为:

struct i2c_adapter {
    
    
	struct module *owner;
	unsigned int class;		  /* classes to allow probing for */
	const struct i2c_algorithm *algo; /* the algorithm to access the bus */
	void *algo_data;

	...
};

i2c_algorithm类型的指针变量algo,对于一个I2C适配器,对外提供读写API函数,设备驱动程序使用这些API函数进行读写操作,i2c_algorithm 就是 I2C 适配器与 IIC 设备进行通信的方法 ,定义在include/linux/i2c.h文件中

struct i2c_algorithm {
    
    
	int (*master_xfer)(struct i2c_adapter *adap, struct i2c_msg *msgs,
			   int num);
	int (*smbus_xfer) (struct i2c_adapter *adap, u16 addr,
			   unsigned short flags, char read_write,
			   u8 command, int size, union i2c_smbus_data *data);

	/* To determine what the adapter supports */
	u32 (*functionality) (struct i2c_adapter *);

};
  • master_xfer是I2C适配器的传输函数,可以通过此函数来完成IIC设备之间的通信,用来产生I2C通信时序
  • smbus_xfer 就是 SMBUS 总线的传输函数

综上所述,编写一个I2C适配器驱动主要是初始化i2c_adapter结构体,然后设置i2c_algorithm的传输函数,完成之后向系统注册设置好的i2c_adapter,这两个函数原型为:

  • 相关API

    • int i2c_add_adapter(struct i2c_adapter *adapter)

      注册一个i2c_adapter ,系统分配编号

    • int i2c_add_numbered_adapter(struct i2c_adapter *adapter)

      注册一个i2c_adapter ,自己指定编号

    • void i2c_del_adapter(struct i2c_adapter * adap)

      注销一个i2c_adapter

关于I2C总线驱动,一般由SOC厂商编写

4. I2C 设备驱动

I2C设备驱动主要由两个数据构成:i2c_client 和 i2c_driver,i2c_client用来描述设备信息,i2c_driver用来描述驱动内容,与platform_driver类似。

4.1 i2c_client

i2c_client 结构体定义在 include/linux/i2c.h 文件中,内容如下:

struct i2c_client {
    
    
	unsigned short flags;			/* div., see below		*/
	unsigned short addr;			/* 设备地址:7位	*/

	char name[I2C_NAME_SIZE];
	struct i2c_adapter *adapter;	/* the adapter we sit on	*/
	struct device dev;				/* 设备结构体		*/
	int init_irq;					/* irq set at initialization	*/
	int irq;						/* 中断	*/
	struct list_head detected;
#if IS_ENABLED(CONFIG_I2C_SLAVE)
	i2c_slave_cb_t slave_cb;	/* callback for slave mode	*/
#endif
};

每检测到一个I2C设备就会给I2C设备分配一个i2c_client

4.2 i2c_driver结构体

i2c_driver 类似 platform_driver,是我们编写 I2C 设备驱动重点要处理的内容, i2c_driver 结构体定义在 include/linux/i2c.h 文件中,内容如下:

struct i2c_driver {
    
    
	unsigned int class;

	/* Standard driver model interfaces */
	int (*probe)(struct i2c_client *, const struct i2c_device_id *);
	int (*remove)(struct i2c_client *);
	...
	struct device_driver driver;
	const struct i2c_device_id *id_table;
	...
}
  • 1.当I2C设备和驱动匹配成功之后probe函数会执行,和platform驱动一样

  • 2.device_driver 驱动结构体,如果使用设备树的话,需要设置 device_driver 的of_match_table 成员变量,也就是驱动的兼容(compatible)属性

  • 3.id_table 是传统的、未使用设备树的设备匹配 ID 表

对于驱动编写人员来说,重点工作是构建i2c_driver,构建完成以后需要向Linux 内核注册这个 i2c_driver。 i2c_driver 注册函数为 int i2c_register_driver,此函数原型如下

#define i2c_add_driver(driver) \
	i2c_register_driver(THIS_MODULE, driver)

driver:要注册的 i2c_driver。

当设备卸载之后要将前面注册的i2c_driver从Linux内核中注销掉,需要调用i2c_del_driver函数,函数原型为:

void i2c_del_driver(struct i2c_driver *driver)

driver:要注销的 i2c_driver。

5. I2C设备和驱动的匹配过程

I2C的设备和驱动的匹配由I2C核心层来完成,匹配过程可以看这篇文章[https://blog.csdn.net/weixin_43824344/article/details/112730735]

在这里插入图片描述

6.I.MX6U的适配器驱动分析

6.1 设备树节点

位置为:arch\arm\boot\dts\imx6ull.dtsi,内容为:

	i2c1: i2c@021a0000 {
    
    
				#address-cells = <1>;
				#size-cells = <0>;
				compatible = "fsl,imx6ul-i2c", "fsl,imx21-i2c";
				reg = <0x021a0000 0x4000>;
				interrupts = <GIC_SPI 36 IRQ_TYPE_LEVEL_HIGH>;
				clocks = <&clks IMX6UL_CLK_I2C1>;
				status = "disabled";
			};

在内核代码中找到compatible属性值,找到的驱动文件为drivers/i2c/busses/i2c-imx.c ,

static struct platform_device_id imx_i2c_devtype[] = {
    
    
 {
    
    
	.name = "imx1-i2c",
	.driver_data = (kernel_ulong_t)&imx1_i2c_hwdata,
	}, {
    
    
	.name = "imx21-i2c",
	.driver_data = (kernel_ulong_t)&imx21_i2c_hwdata,
	}, {
    
    
	/* sentinel */
	}
 };
 MODULE_DEVICE_TABLE(platform, imx_i2c_devtype);

 static const struct of_device_id i2c_imx_dt_ids[] = {
    
    
	{
    
     .compatible = "fsl,imx1-i2c", .data = &imx1_i2c_hwdata, },
	{
    
     .compatible = "fsl,imx21-i2c", .data = &imx21_i2c_hwdata, },
	{
    
     .compatible = "fsl,vf610-i2c", .data = &vf610_i2c_hwdata, },
	{
    
     /* sentinel */ }
 };
 MODULE_DEVICE_TABLE(of, i2c_imx_dt_ids);
......
 static struct platform_driver i2c_imx_driver = {
    
    
	.probe = i2c_imx_probe,
	.remove = i2c_imx_remove,
	.driver = {
    
    
	.name = DRIVER_NAME,
	.owner = THIS_MODULE,
	.of_match_table = i2c_imx_dt_ids,
	.pm = IMX_I2C_PM,
 },
	.id_table = imx_i2c_devtype,
 };

 static int __init i2c_adap_imx_init(void)
 {
    
    
	return platform_driver_register(&i2c_imx_driver);
 }
	subsys_initcall(i2c_adap_imx_init);

 static void __exit i2c_adap_imx_exit(void)
 {
    
    
	platform_driver_unregister(&i2c_imx_driver);
 }
 module_exit(i2c_adap_imx_exit);

可以看出I,MX6U的适配器驱动是一个标准的platform驱动,当设备与驱动匹配之后,会调用probe函数:i2c_imx_probe,

static int i2c_imx_probe(struct platform_device *pdev)
{
    
    
	const struct of_device_id *of_id = of_match_device(i2c_imx_dt_ids,
							   &pdev->dev);
	struct imx_i2c_struct *i2c_imx;
	struct resource *res;
	struct imxi2c_platform_data *pdata = dev_get_platdata(&pdev->dev);
	void __iomem *base;
	int irq, ret;
	dma_addr_t phy_addr;

	dev_dbg(&pdev->dev, "<%s>\n", __func__);

	irq = platform_get_irq(pdev, 0);
	if (irq < 0) {
    
    
		dev_err(&pdev->dev, "can't get irq number\n");
		return irq;
	}

	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	base = devm_ioremap_resource(&pdev->dev, res);
	if (IS_ERR(base))
		return PTR_ERR(base);

	phy_addr = (dma_addr_t)res->start;
	i2c_imx = devm_kzalloc(&pdev->dev, sizeof(*i2c_imx), GFP_KERNEL);
	if (!i2c_imx)
		return -ENOMEM;

	if (of_id)
		i2c_imx->hwdata = of_id->data;
	else
		i2c_imx->hwdata = (struct imx_i2c_hwdata *)
				platform_get_device_id(pdev)->driver_data;

	/* Setup i2c_imx driver structure */
	strlcpy(i2c_imx->adapter.name, pdev->name, sizeof(i2c_imx->adapter.name));
	i2c_imx->adapter.owner		= THIS_MODULE;
	i2c_imx->adapter.algo		= &i2c_imx_algo;
	i2c_imx->adapter.dev.parent	= &pdev->dev;
	i2c_imx->adapter.nr		= pdev->id;
	i2c_imx->adapter.dev.of_node	= pdev->dev.of_node;
	i2c_imx->base			= base;

	/* Get I2C clock */
	i2c_imx->clk = devm_clk_get(&pdev->dev, NULL);
	if (IS_ERR(i2c_imx->clk)) {
    
    
		dev_err(&pdev->dev, "can't get I2C clock\n");
		return PTR_ERR(i2c_imx->clk);
	}

	ret = clk_prepare_enable(i2c_imx->clk);
	if (ret) {
    
    
		dev_err(&pdev->dev, "can't enable I2C clock\n");
		return ret;
	}
	/* Request IRQ */
	ret = devm_request_irq(&pdev->dev, irq, i2c_imx_isr,
			       IRQF_NO_SUSPEND, pdev->name, i2c_imx);
	if (ret) {
    
    
		dev_err(&pdev->dev, "can't claim irq %d\n", irq);
		goto clk_disable;
	}

	
	/* Set up adapter data */
	i2c_set_adapdata(&i2c_imx->adapter, i2c_imx);

	/* Set up clock divider */
	i2c_imx->bitrate = IMX_I2C_BIT_RATE;
	ret = of_property_read_u32(pdev->dev.of_node,
				   "clock-frequency", &i2c_imx->bitrate);
	if (ret < 0 && pdata && pdata->bitrate)
		i2c_imx->bitrate = pdata->bitrate;

	/* Set up chip registers to defaults */
	imx_i2c_write_reg(i2c_imx->hwdata->i2cr_ien_opcode ^ I2CR_IEN,
			i2c_imx, IMX_I2C_I2CR);
	imx_i2c_write_reg(i2c_imx->hwdata->i2sr_clr_opcode, i2c_imx, IMX_I2C_I2SR);

	/* Add I2C adapter */
	ret = i2c_add_numbered_adapter(&i2c_imx->adapter);
	if (ret < 0) {
    
    
		dev_err(&pdev->dev, "registration failed\n");
		goto clk_disable;
	}

	/* Set up platform driver data */
	platform_set_drvdata(pdev, i2c_imx);
	clk_disable_unprepare(i2c_imx->clk);

	/* Init DMA config if supported */
	i2c_imx_dma_request(i2c_imx, phy_addr);

	return 0;   /* Return OK */

}

第1步:获取I2C1的中断号

第2步:调用platform_get_resource函数获取I2C1控制器的基地址,获取到基地址之后使用devm_ioremap_resource对其进行内存映射,得到可以在驱动中使用的虚拟地址

第3步:初始化i2c adapter,这里主要看i2c_imx->adapter.algo = &i2c_imx_algo;申请中断,设置I2C的频率

第4步:初始化IMX_I2C_I2CR和IMX_I2C_I2SR寄存器

第5步:调用 i2c_add_numbered_adapter 函数向 Linux 内核注册 i2c_adapter

第6步:申请dma

6.2 I2C适配器与I2C设备的通信函数

i2c_imx_algo 包含 I2C1 适配器与 I2C 设备的通信函数 master_xfer, i2c_imx_algo 结构体定义如下:

static struct i2c_algorithm i2c_imx_algo = {
    
    
	.master_xfer	= i2c_imx_xfer,
	.functionality	= i2c_imx_func,
};

i2c_imx_func表示I2C适配器驱动支持什么通信协议

static u32 i2c_imx_func(struct i2c_adapter *adapter)
{
    
    
	return I2C_FUNC_I2C | I2C_FUNC_SMBUS_EMUL
		| I2C_FUNC_SMBUS_READ_BLOCK_DATA;
}

重点是使用i2c_imx_xfer,最终就是通过此函数来完成与 I2C 设备通信的

static int i2c_imx_xfer(struct i2c_adapter *adapter,
						struct i2c_msg *msgs, int num)
{
    
    
	unsigned int i, temp;
	int result;
	bool is_lastmsg = false;
	struct imx_i2c_struct *i2c_imx = i2c_get_adapdata(adapter);

	dev_dbg(&i2c_imx->adapter.dev, "<%s>\n", __func__);

	/* Start I2C transfer 开始I2C通信*/
	result = i2c_imx_start(i2c_imx);
	if (result)
		goto fail0;

	/* read/write data */
	for (i = 0; i < num; i++) {
    
    
		if (i == num - 1)
			is_lastmsg = true;

		if (i) {
    
    
			dev_dbg(&i2c_imx->adapter.dev,
				"<%s> repeated start\n", __func__);
			temp = imx_i2c_read_reg(i2c_imx, IMX_I2C_I2CR);
			temp |= I2CR_RSTA;
			imx_i2c_write_reg(temp, i2c_imx, IMX_I2C_I2CR);
			result =  i2c_imx_bus_busy(i2c_imx, 1);
			if (result)
				goto fail0;
		}
		dev_dbg(&i2c_imx->adapter.dev,
			"<%s> transfer message: %d\n", __func__, i);
		/* write/read data */
#ifdef CONFIG_I2C_DEBUG_BUS
		temp = imx_i2c_read_reg(i2c_imx, IMX_I2C_I2CR);
		dev_dbg(&i2c_imx->adapter.dev,
			"<%s> CONTROL: IEN=%d, IIEN=%d, MSTA=%d, MTX=%d, TXAK=%d, RSTA=%d\n",
			__func__,
			(temp & I2CR_IEN ? 1 : 0), (temp & I2CR_IIEN ? 1 : 0),
			(temp & I2CR_MSTA ? 1 : 0), (temp & I2CR_MTX ? 1 : 0),
			(temp & I2CR_TXAK ? 1 : 0), (temp & I2CR_RSTA ? 1 : 0));
		temp = imx_i2c_read_reg(i2c_imx, IMX_I2C_I2SR);
		dev_dbg(&i2c_imx->adapter.dev,
			"<%s> STATUS: ICF=%d, IAAS=%d, IBB=%d, IAL=%d, SRW=%d, IIF=%d, RXAK=%d\n",
			__func__,
			(temp & I2SR_ICF ? 1 : 0), (temp & I2SR_IAAS ? 1 : 0),
			(temp & I2SR_IBB ? 1 : 0), (temp & I2SR_IAL ? 1 : 0),
			(temp & I2SR_SRW ? 1 : 0), (temp & I2SR_IIF ? 1 : 0),
			(temp & I2SR_RXAK ? 1 : 0));
#endif
		if (msgs[i].flags & I2C_M_RD)
			result = i2c_imx_read(i2c_imx, &msgs[i], is_lastmsg);
		else {
    
    
			if (i2c_imx->dma && msgs[i].len >= DMA_THRESHOLD)
				result = i2c_imx_dma_write(i2c_imx, &msgs[i]);
			else
				result = i2c_imx_write(i2c_imx, &msgs[i]);
		}
		if (result)
			goto fail0;
	}

fail0:
	/* Stop I2C transfer */
	i2c_imx_stop(i2c_imx);

	dev_dbg(&i2c_imx->adapter.dev, "<%s> exit with: %s: %d\n", __func__,
		(result < 0) ? "error" : "success msg",
			(result < 0) ? result : num);
	return (result < 0) ? result : num;
}

第一步:调用i2c_imx_start开始进行I2C通信

第2步:如果是从I2C设备中读数据则调用i2c_imx_read(i2c_imx, &msgs[i], is_lastmsg)函数

第3步:若是向i2c中写数据则调用i2c_imx_write,如果需要用到DMA则调用i2c_imx_dma_write函数

第4步:I2C通信完成,调用i2c_imx_stop函数

i2c_imx_start、 i2c_imx_read、 i2c_imx_write 和 i2c_imx_stop 这些函数就是 I2C 寄存器的具体操作函数,函数内容单片机中 I2C 通信基本一样

7. I2C设备驱动的编写

I2C适配器驱动由SOC厂商编写完成,驱动工程师需要编写具体的I2C设备驱动

7.1 I2C设备描述

未使用设备树时:

在未使用设备树时在Linux内核中使用i2c_board_info 结构体来描述一个具体的 I2C 设备 ,定义为:

struct i2c_board_info {
    
    
	char type[I2C_NAME_SIZE]; /* I2C 设备名字 */
	unsigned short flags; /* 标志 */
	unsigned short addr; /* I2C 器件地址 */
	void *platform_data;
	struct dev_archdata *archdata;
	struct device_node *of_node;
	struct fwnode_handle *fwnode;
	int irq;
};

type与addr是必须要设置的,type的I2C设备的名字,addr是I2C的设备地址,在内核中找到arch\blackfin\mach-bf561\boards\acvilon.c文件,此文件包含某一个驱动的I2C设备信息

/* I2C devices fitted. */
static struct i2c_board_info acvilon_i2c_devs[] __initdata = {
    
    
	{
    
    
	 I2C_BOARD_INFO("ds1339", 0x68),
	 },
	{
    
    
	 I2C_BOARD_INFO("tcn75", 0x49),
	 },
};

I2C_BOARD_INFO 是一个宏,定义了某一个I2C设备的名字与地址

​```c
#define I2C_BOARD_INFO(dev_type, dev_addr) \
	.type = dev_type, .addr = (dev_addr)

使用设备树之后抛弃了这种方式描述I2C设备

7.2 使用设备树时

在使用设备树时,I2C设备信息通过创建对应节点就行,NXP的EVK开发板在I2C1上外接mag3110这个设备,所以必须在I2C1下创建mag3110这个设备节点,在这个设备节点中描述mag3110这个芯片相关芯片

&i2c1{
    
    
    ·····
	mag3110@0e {
    
    
 	compatible = "fsl,mag3110";
 	reg = <0x0e>;
	position = <2>;
  };
   ·····
   ·····
   ·····
}

其中mag3110@0e是子节点的名字,@后面的0e就是mag3110的器件地址,设置compatible属性值fsl,msg3110,用来匹配驱动,其中reg属性设置mag3110的IIC地址

7.3 I2C设备数据收发处理流程

在I2C驱动的probe函数里面初始化I2C设备,必须能够对I2C的设备寄存器进行读写操作,这里使用i2c_transfer函数,i2c_transfer函数最终会调用I2C适配器中的i2c_algorithm里面的master_xfer函数,对于 I.MX6U 而言就是i2c_imx_xfer 这个函数。 i2c_transfer 函数原型如下:

int i2c_transfer(struct i2c_adapter *adap,struct i2c_msg *msgs,int num)

参数:

  • adap:所使用的I2C适配器,i2c_client中会保存对应的i2c_adapter

  • msgs:I2C要发送的一个或者多个消息

  • num:消息数量,即msgs的数量

Linux内核使用i2c_msg结构体来描述一个消息,结构定义在include/uapi/linux/i2c.h 中,内容为:

struct i2c_msg {
    
    
	__u16 addr;					/* slave address			*/
	__u16 flags;
#define I2C_M_TEN		0x0010	/* this is a ten bit chip address */
#define I2C_M_RD		0x0001	/* read data, from slave to master */
#define I2C_M_STOP		0x8000	/* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_NOSTART		0x4000	/* if I2C_FUNC_NOSTART */
#define I2C_M_REV_DIR_ADDR	0x2000	/* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_IGNORE_NAK	0x1000	/* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_NO_RD_ACK		0x0800	/* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_RECV_LEN		0x0400	/* length will be first received byte */
	__u16 len;		/* msg length				*/
	__u8 *buf;		/* pointer to msg data			*/
};

addr:从机的I2C地址

flags:读写标志位

len:本msg(消息)的长度

buf:消息数据

7.3.1 读取I2C设备多个寄存器数据

static int xxx_read_regs(struct i2c_client *client, u8 addr,u8 reg, void *val,int len)
 {
    
    
	int ret;
	struct i2c_msg msg[2];

	/* msg[0],第一条写消息,发送要读取的寄存器首地址 */
	msg[0].addr = addr; /* I2C 器件地址 */
	msg[0].flags = 0; /* 标记为发送数据 */
	msg[0].buf = &reg; /* 读取的首地址 */
	msg[0].len = 1; /* reg 长度 */

	/* msg[1],第二条读消息,读取寄存器数据 */
	msg[1].addr = addr; /* I2C 器件地址 */
	msg[1].flags = I2C_M_RD; /* 标记为读取数据 */
	msg[1].buf = val; /* 读取数据缓冲区 */
	msg[1].len = len; /* 要读取的数据长度 */
	ret = i2c_transfer(client->adapter, msg, 2);
	if(ret == 2) {
    
    
		ret = 0;
	} else {
    
    
		ret = -EREMOTEIO;
	}
	return ret;
 }

当需要读取多个寄存器的数据时,需要构建两个消息,一个消息用来发送要读取的寄存器地址,每一个消息要初始化从机地址msg[0].addr,读写标志位msg[0].flags,读取的寄存器首地址msg[0].buf,读取的长度为1,第2条数据需要将flags设置为I2C_M_RD,用来标志读取数据,msg[1].buf用来保存读取的寄存器的值,msg[1].len 保存读取的数据长度,

7.3.2 向I2C设备一个寄存器写入数据

static s32 xxx_write_regs(struct i2c_client *client, u8 reg, u8 data,u8 len)
 {
    
    
	u8 b[2];
	struct i2c_msg msg;

	b[0] = reg; /* 寄存器首地址 */
	b[1] = data; /* 将要发送的数据拷贝到数组 b 里面 */

	msg.addr = addr; /* I2C 器件地址 */
	msg.flags = 0; /* 标记为写数据 */

	msg.buf = (char *)b; /* 要发送的数据缓冲区 */
	msg.len = 2//len + 1; /* 要发送的数据长度 */

	return i2c_transfer(client->adapter, &msg, 1);
 }

xxx_write_regs用于向I2C设备中一个寄存器写数据,指需要一个i2c_msg结构体即可,数组b用于存放寄存器的首地址和要发送的数据,flags设置为0,len设置为2,i2c_transfer直接返回读到的值

7.3.3 16位I2c读写操作

暂定

8.具体的I2C驱动

Linux驱动-IIC驱动(基于AP3216C):https://blog.csdn.net/weixin_43824344/article/details/113061026

猜你喜欢

转载自blog.csdn.net/weixin_43824344/article/details/119922075