嵌入式linux I2C驱动

         在裸机当中,我们需要编写4个文件bsp_i2c.c,bsp_i2c.h,bsp_ap216c.c,bsp_ap326c.h,前两个也就是IIC的驱动接口驱动,后两个就是ap3216c的设备驱动文件,相当于有两部分驱动。

  1. I2C主机驱动
  2. I2C设备驱动

        对于I2C主机驱动,一旦编写完成就不需要再做修改,其他的I2C设备直接调用主机驱动提供的API函数完成读写操作即可,这也就正好符合了linux的分离和分层的思想,因此linux内核也将I2C分成两个部分。

  1. I2C总线驱动,也就是SOC的I2C控制器驱动,也叫作I2C适配器驱动。
  2. I2C设备驱动,I2C设备驱动是专门针对I2C设备而写的驱动。

I2C总线驱动

        I2C总线驱动的重点是I2C适配器驱动,这里需要用到两个重要的数据结构:i2c_adapter和i2c_algorithm,linux内核将SOC的I2C适配器抽象成i2c_adapter,结构体定义如下:

         第501行,i2c_algorithm类型的指针algo,对于一个I2C适配器,肯定要对外提供读写API函数,设备驱动程序可以使用这些API函数来完后读写操作。i2c_algorithm就是I2C适配器与IIc设备进行通讯的方法。i2c_algorithm结构体如下:             第398行,master_xfer就是I2C适配器的传输函数,可以通过此函数来完成与IIC设备之间的通讯。

        第400行,smbus_xfer就是SMBUS总线的传输函数。

        综上可知,I2C总线驱动,或者说I2C适配器驱动的主要工作就是初始化i2c-adapter结构体变量,然后设置i2c_algorithm 中的 master_xfer 函数,完成后就可以通过i2c_add_numbered_adapter或者i2c_add_adapter这两个函数向系统注册设置好的i2c_adapter,这两个函数的原型如下:

        这两个函数的区别在于 i2c_add_adapter 使用动态的总线号,而 i2c_add_numbered_adapter使用静态总线号。函数参数和返回值含义如下:

        adapter和adap:要添加到linux内核的i2c_adapter,也就是I2C适配器。

        返回值:0成功,负值失败

        删除I2C适配器的话使用i2c_del_adapter函数即可,函数原型如下:

         I2C 适配器驱动 NXP 已经编写好了,这个不需要用户去编写。

I2C设备驱动

        I2C设备驱动重点关注两个数据结构,i2c_client和i2c_driver,i2c_client就是描述设备信息的(将设备树节点转化为i2c_client),i2c_driver描述驱动内容,类似于platform。

1,i2c_client结构体

        i2c_client结构体定义如下:

         一个设备对应一个 i2c_client,每检测到一个 I2C 设备就会分配一个i2c_client

2,i2c_driver结构体

        i2c_driver类似于platform_driver,是我们编写I2C设备驱动重点要处理的内容,结构体定义如下:      

         第170行,I2C 设备和驱动匹配成功以后 probe 函数就会执行,和 platform 驱动一样。

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

        189 行,id_table 是传统的、未使用设备树的设备匹配 ID 表。

        我们的重点工作就是设备驱动的编写,也就是说重点工作是构建i2c_driver,构建完成以后需要向linux内核注册这个i2c_driver。i2c_driver注册函数为i2c_register_driver原型如下                 owner:一般为THIS_MODULE

        driver:要注册的i2c_driver

        返回值:0成功,负值失败

        另外也可以用i2c_add_driver注册,与上面的区别就是少了一个owner参数。

         注销I2C设备驱动的时候,我们需要将前面注册的i2c_driver从linux内核注销掉,需要用到i2c_del_driver函数。

        i2c_driver注册过程如下,真的很类似于platform驱动 

        设备和驱动的匹配过程是由I2C总线完成的,I2C总线的数据结构为i2c_bus_type,定义如下

         .match 就是 I2C 总线的设备和驱动匹配函数,在这里就是 i2c_device_match 这个函数,此函数内容如下:

         很容易看出是2c_client与i2c_driver相匹配。上面的匹配方法也是与platform的相似。

 I2C设备驱动编写流程

        1,未使用设备树

        首先肯定是要描述I2C设备节点信息,在未使用设备树的时候需要在 BSP 里面使用 i2c_board_info 结构体来描述一个具体的 I2C 设备。i2c_board_info 结构体如下:

        type 和 addr 这两个成员变量是必须要设置的,一个是 I2C 设备的名字,一个是 I2C 设备的 器件地址。打开 arch/arm/mach-imx/mach-mx27_3ds.c 文件,此文件中关于 OV2640 的 I2C 设备信息描述如下:

         2,使用设备树

        使用设备树的时候 I2C 设备信息通过创建相应的节点就行了,比如 NXP 官方的 EVK 开发板在 I2C1 上接了 mag3110 这个磁力计芯片,因此必须在 i2c1 节点下创建 mag3110 子节点,然后在这个子节点内描述 mag3110 这个芯片的相关信息。打开 imx6ull-14x14-evk.dts 这个设备树 文件,然后找到如下内容:    

        第7~11行,向i2c1添加mag3110子节点,第7行“mag3110@0e”是子节点名字,“@”后面的0e就是mag3110的器件地址,第8行设置compatible属性值为“fsl,mag3110”,第9行的reg属性就是器件地址,I2C设备节点的创建重点是compatible属性和reg属性来设置的。一个用于匹配驱动,一个用于设置器件地址。

 I2C设备数据的收发流程

        I2C设备驱动首要做的就是初始化i2c_driver并向linux内核注册,当设备与驱动匹配之后,probe函数就会执行,probe函数里面所做的就是字符设备注册的那一套了。一般需要在probe函数里面初始化I2C设备,要初始化I2C设备就必须对I2C设备的寄存器进行读写操作。这里就要用到 i2c_transfer 函数了。i2c_transfer 函数最终会调用 I2C 适配器中 i2c_algorithm 里面的 master_xfer 函数,对于 I.MX6U 而言就是i2c_imx_xfer 这个函数。i2c_transfer 函数原型如下:  

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

        msgs:I2C要发送一个或多个信息。

        num:消息数量,也就是msgs的数量

        返回值:负值失败,非负值,发送信息的数量

        Linux 内核使用 i2c_msg 结构体来描述一个消息。在   include/uapi/linux/i2c.h 文件中,结构体内容如下:

         使用i2c_transfer函数发送数据之前要先构建好i2c_msg,事例代码如下:

static int ap3216c_read_regs(struct ap3216c_dev *dev, u8 reg, void *val, int len)
{
	int ret;
	struct i2c_msg msg[2];
	struct i2c_client *client = (struct i2c_client *)dev->private_data;

	/* msg[0]为发送要读取的首地址 */
	msg[0].addr = client->addr;			/* ap3216c地址 */
	msg[0].flags = 0;					/* 标记为发送数据 */
	msg[0].buf = ®					/* 读取的首地址 */
	msg[0].len = 1;						/* reg长度*/

	/* msg[1]读取数据 */
	msg[1].addr = client->addr;			/* ap3216c地址 */
	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 {
		printk("i2c rd failed=%d reg=%06x len=%d\n",ret, reg, len);
		ret = -EREMOTEIO;
	}
	return ret;
}

static s32 ap3216c_write_regs(struct ap3216c_dev *dev, u8 reg, u8 *buf, u8 len)
{
	u8 b[256];
	struct i2c_msg msg;
	struct i2c_client *client = (struct i2c_client *)dev->private_data;
	
	b[0] = reg;					/* 寄存器首地址 */
	memcpy(&b[1],buf,len);		/* 将要写入的数据拷贝到数组b里面 */
		
	msg.addr = client->addr;	/* ap3216c地址 */
	msg.flags = 0;				/* 标记为写数据 */

	msg.buf = b;				/* 要写入的数据缓冲区 */
	msg.len = len + 1;			/* 要写入的数据长度 */

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

        在设备结构体里面添加一个执行void的指针成员变量private_data,这个变量的作用就是先在probe函数里面接收client结构体的地址,然后在读写函数里面将设备信息(设备地址)转换出来使用。

        因为I2C读取数据的时候要先发送要读取的寄存器地址,然后在读取数据,所以需要准备两个i2c_msg,一个用于发送寄存器地址,一个用于读取寄存器的值。对于 msg[0],将 flags 设置为 0,表示写数据。msg[0]的 addr 是 I2C 设备的器件地址,msg[0]的 buf成员变量就是要读取的寄存器地址。对于 msg[1],将 flags 设置为 I2C_M_RD,表示读取数据。 msg[1]的 buf 成员变量用于保存读取到的数据,len 成员变量就是要读取的数据长度。调用 i2c_transfer 函数完成 I2C 数据读操作。

        I2C设备收发数据所使用的API函数是i2c_transfer,这是下面两个函数的集合,能同时实现收发的功能,首先看一下数据发送函数i2c_master_send,函数原型如下:

        client:I2C设备对应的i2c_client

        buf:要发送的数据

        count:要发送的数据字节数,要小于64kb,因为i2c_msg的len成员变量是一个u16(无符号16位)数据类型

        返回值:负值代表失败,非负值代表字节数

        I2C数据接收函数为i2c_master_recv,函数原型如下:

  • 2
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值