18_I2C的使用

介绍如何读取和写入i2c芯片的数据寄存器

Posted by Jerry Chen on August 12, 2019

了解内容

I2C知识

I2C是2线的:SCL和SDA。无数据时SCL线是高电平,有数据时变为正常方波时钟信号。

SCL信号由主机控制。由图可知每一帧时钟保持9个周期,每帧有1字节数据,第9个时钟周期需要ACK(SDA为低)。

  • 同一状态的首帧格式均是:从机地址(高7位)+读写状态(低0位,0为写,1为读);

  • 主机写入n帧的实现,按照如下传输

    1
    
    通信地址(低0位写0) + 数据地址(1字节) + 数据1(1字节,下同)+数据2+...+数据n
    

    这样数据1会写入数据地址a,数据2写入a+1地址,类推。

  • 主机读取n帧的实现,需要分两步

    1. 写入将读取的地址

      1
      
      通信地址(低0位写0) + 数据地址(1字节)
      
    2. 读取(低0位写1),接下来从机依次发送该地址起始的数据,主机ack响应,不响应就停止。

      1
      
      通信地址(低0位写1)
      

关于通信地址是由硬件连接决定的。

比如D7、D6、D5、D4、D3、D2、D1、D0,可能芯片内部已经决定了部分线的高低(ft5x06最高3位固定了3’b011),D0是R/W位,也就是D4、D3、D2、D1是可自定义的地址码(可能是A0,A1,A2这样的命名)。

举例:网上找到的lm75芯片的手册中就有提到通信地址(与本文使用的芯片无关),该设备地址有7位构成,其中高4位是固定的,低3位由设备的三个引脚电平来决定。

Linux I2C知识

和想象中不同的是我们不用根据时序图写i2c的硬件操作函数,这部分已经被硬件实现控制层完成了。设备驱动(driver 驱动层)的工作仅是根据统一的i2c设备操作函数(比如传输函数)完成应用程序调用接口。

Linux中查看i2c总线下的设备信息:

1
ls /sys/bus/i2c/devices/

以“3-0038”举例,表示第3组i2c总线下0038设备地址。

其在平台文件”arch/arm/mach-exynos/mach-itop4412.c”的注册信息如下:

  • 0x70»1即0x38,也就是设备通信地址为0x38;
  • “ft5x0x_ts”就是该设备的name,查看命令cat /sys/bus/i2c/devices/3-0038/name

开始

目标设备是图中左侧的LVDS接口设备。

由于内核中已经申请过3-0038的i2c设备信息,首先需要从内核中取消“TOUCHSCREEN_FT5X0X”,路径是“Device Drivers”–>“Input device support”–>“Touchscreens”–>“FT5X0X based touchscreens”。取消后,编译内核镜像进行烧录会发现ls /sys/bus/i2c/devices/中已经没有“3-0038”设备了。

紧接着添加平台文件mach-itop4412.c中设备信息,模仿ft5x0x_ts平台设备信息添加即可,为了简单实验这里就不用#if条件编译了,也就是始终编译而不用改Kconfig:

这个数组会被i2c_register_board_info(3,…)函数使用,用于注册第3组i2c中的设备信息。

上述完成后,重新编译内核镜像再次烧录

这时可以看到cat /sys/bus/i2c/devices/3-0038/name中打印的是”i2c_demo”:

接下来完成i2c_demo的驱动即可。

一些函数

  1. 注册i2c驱动函数i2c_add_driver,在include/linux/i2c.h中有定义。可类比platform_driver_register函数,性质相同。

    1
    
    int i2c_add_driver(struct i2c_driver *driver);
    

    传参driver,其中包含了I2C设备的名称、probe、remove等接口信息。

  2. i2c数据传输函数i2c_transfer

    1
    
    int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num);
    
    • adap参数,适配器,它是probe函数的参数“client结构体指针”中的成员。

    • msgs参数,一般定义一个数组,每个成员结构体中有用于传输的关键信息;

      写入n个地址的一般声明和赋值方法,下面len改为1+n:

      读取n字节的一般声明和赋值方法:

    • num参数,作用是i2c传输依次使用msgs[0]…msgs[num-1],比如读n个地址,先声明msgs[2],然后这里写2。

  3. 注销i2c驱动函数i2c_del_driver

    1
    
    i2c_del_driver(struct i2c_driver *driver);
    

例程

驱动例程

i2c_demo.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
#include <linux/init.h>
#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/gpio.h>
#include <mach/gpio-exynos4.h>
#include <linux/delay.h>

MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("jerry");

#define DRV_NAME "i2c_demo"

static int demo_probe(struct i2c_client *client, const struct i2c_device_id *id){
	int ret = 0;
	unsigned char buf_r[1] = {0xa6};//寄存器地址:该i2c芯片Firmware ID
	unsigned char buf_w[1] = {0};
	
	struct i2c_msg msgs[] = {
		{
			.addr = client->addr,
			.flags = 0,//写
			.len = 1,
			.buf = buf_w,
		},
		{
			.addr = client->addr,
			.flags = I2C_M_RD,//读
			.len = 1,
			.buf = buf_r,
		},
	};
	
	//首先打印i2c从机地址,会打印0x38
	printk(KERN_EMERG "client addr is 0x%x!\n",client->addr);
	
	ret = i2c_transfer(client->adapter, msgs, 2);
	if(ret < 0)
		printk(KERN_EMERG "read Firmware ID fail.\n");
	else
		printk(KERN_EMERG "Firmware ID is %d.\n",buf_r[0]);
	
	printk(KERN_EMERG "demo_probe ok!\n");
	return 0;
}

static int demo_remove(struct i2c_client *client){
	printk(KERN_EMERG "demo_remove ok!\n");
	return 0;
}

static const struct i2c_device_id demo_id[] = {
	{ DRV_NAME, 0 },
	{ }//最后一个元素需要是空
};

static struct i2c_driver i2c_drv_demo = {
	.probe		= demo_probe,
	.remove		= __devexit_p(demo_remove),
	.id_table	= demo_id,
	.driver	= {
		.name	= DRV_NAME,
		.owner	= THIS_MODULE,
	},
};

static void VccConvEn_ctl(void){
	//1.8V到3.3V芯片的使能脚控制
	int ret;
	ret = gpio_request(EXYNOS4_GPL0(2),"VccConv_En");
	if(ret){
		printk(KERN_EMERG "gpio request fail!\n");
	}
	gpio_direction_output(EXYNOS4_GPL0(2),1);
	gpio_free(EXYNOS4_GPL0(2));
	mdelay(5);
}

static int __init demo_init(void){
	VccConvEn_ctl();
	i2c_add_driver(&i2c_drv_demo);
	return 0;
}
static void __exit demo_exit(void){
	i2c_del_driver(&i2c_drv_demo);
}

//如果写内核中,要使用late_initcall后加载
module_init(demo_init);
module_exit(demo_exit);
测试结果

本文测试结果和测试程序是不完整的,因为需要外接FT5X0X硬件模块才能测试具体的读写。所以测试程序仅在probe中实现了读取数据就没有继续实现了,读懂后继续实现很容易。

编写简单Makefile:参考这里

编译生成模块后,拷贝到开发板中:

  • 首先加载模块后正常执行probe函数,但是没该硬件模块所以i2c读取失败。