Linux中I2C架构, 如何让编写I2C驱动? (三合一光照传感器,ap3216c)

linux内核将I2C驱动分为:I2C总线驱动(包括核心层和总线驱动层)I2C设备驱动两部分,我们编写的是设备驱动。
以编写一个三合一光照传感器(ap3216c)的I2C驱动为例:​ AP3216C 模块的核心就是这个芯片本身。这颗芯片集成了光强传感器(ALS: Ambient Light Sensor),接近传感器(PS: Proximity Sensor),还有一个红外LED(IR LED)。这个芯片设计的用途是给手机之类的使用,比如:返回当前环境光强以便调整屏幕亮度;用户接听电话时,将手机放置在耳边后,自动关闭屏幕避免用户误触碰。该芯片通过I2C接口作为slave与主控制器相连,支持中断。

修改设备树

首先修改设备树,在 iomuxc节点中添加一个新的子节点借助Pictrl子系统来描述三合一传感器所使用的 IIC引脚,也就是根据硬件原理图配置引脚的复用功能和电气属性(一般已经被厂商配置好了)。
以i2c1为例

I2C_SCL: UART4_TXD 复用 ALT2
I2C_SDA: UART4_RXD 复用 ALT2

&iomuxc {
	pinctrl-names = "default";
	pinctrl-0 = <&pinctrl_hog_1>; 
	imx6ul-evk {
			…………(省略)
		pinctrl_i2c1: i2c1grp {
			fsl,pins = <
				MX6UL_PAD_UART4_TX_DATA__I2C1_SCL 0x4001b8b0
				MX6UL_PAD_UART4_RX_DATA__I2C1_SDA 0x4001b8b0
			>;
		};
			…………(省略)
	}

在I2C节点下添加设备子节点,重要的是compatible属性和设备的器件地址reg,用于和设备驱动匹配。

&i2c1 {
	clock-frequency = <100000>;
	pinctrl-names = "default";
	pinctrl-0 = <&pinctrl_i2c1>;
	status = "okay";

	ap3216c@1e {
		compatible = "alientek,ap3216c";
		reg = <0x1e>;
	};
};

reg内容查看ap3216c中的i2c从机地址 i2c slave address
此地址由厂商设置,用于开机设备树匹配从机,需要保证设备树reg和厂商一致

i2c匹配

定义 I2C 驱动之前,用户首先要定义变量 of_device_id 和 i2c_device_id。(虽然传统匹配表和设备树匹配表只需要配置一个就可以匹配成功,但是在使用设备树匹配的时候需要同时实现传统匹配表(主要依靠compatible匹配),不然无法匹配成功,无法运行probe函数,详细参考:i2c中probe函数实现必须要传统匹配和设备树匹配同时实现)

//传统匹配表
static const struct i2c_device_id ap3216c_id[] = {
	{"alientek,ap3216c", 0},
	{}
};
//设备树匹配表
static const struct of_device_id ap3216c_of_match[] = {
	{.compatible = "alientek,ap3216c"},
	{/* Sentinel */}
};

iic_driver结构体

在I2C设备驱动程序中,首先构建一个iic_driver结构体,实现probe/remove函数以及指定驱动和设备的匹配方法。

static struct i2c_driver ap3216c_driver = {

	.probe = ap3216c_probe,
	.remove = ap3216c_remove,
	.driver = {
		.name = "ap3216c",
		.owner = THIS_MODULE,
		.of_match_table = of_match_ptr(ap3216c_of_match),
	},
	.id_table = ap3216c_id,
};

配置成功后使用i2c_add_driver函数
在调用 i2c_add_driver 注册 I2C 驱动时,会遍历 I2C 设备,如果该驱动支持所遍历到的设备,即匹配成功后,则会调用该驱动的=== probe ==函数。

i2c_add_driver(&ap3216c_driver) -> .probe = ap3216c_probe

在驱动加载的时候遇到同名的i2c_board_info就会将i2c_client和driver绑定,并且执行driver的probe函数。

iic_probe

在probe函数中,执行就是字符设备那一套。
1)、分配设备号。
2)、初始化 cdev 结构体(cdev_init)【字符设备结构体】然后使用 cdev_add 函数,向Linux 系统添加这个字符设备。
3)、自动创建设备节点:a、创建一个 class 类:class_create(owner, name)在这个类下创建设备:b、device_create(struct class *class,struct device *parent……)
这样,在用户空间的dev目录下,就会生成一个设备节点,通过对这个设备节点进行操作,尽可以实现对底层硬件的操作。
定义ap3216c结构体,将需要用到的数据全部放进去。

struct ap3216c_dev{
	dev_t devid;			/* 设备号 	 */
	struct cdev cdev;		/* cdev 	*/
	struct class *class;	/* 类 		*/
	struct device *device;	/* 设备 	 */
	int major;				/* 主设备号	  */
	int minor;				/* 次设备号   */
	struct device_node	*nd; /* 设备节点 */
	void *private_data;
	unsigned short ir, als, ps;
};
static int ap3216c_probe(struct i2c_client *client, const struct i2c_device_id *id)
//i2c_client 结构体   该结构体定义了挂载在I2C总线下的slave设备,一个结构体对象代表一个slave设备 ,而此函数参数有两个表明i2c_device_id必须实现
{
	
	printk("ap3216c probe \r\n");	//如果不执行则没有匹配成功
	/* 注册字符设备驱动 */
	/* 1、创建设备号 */
	if (ap3216cdev.major) {		/*  定义了设备号 */
		ap3216cdev.devid = MKDEV(ap3216cdev.major, 0);
		register_chrdev_region(ap3216cdev.devid, AP3216C_CNT, AP3216C_NAME);
	} else {						/* 没有定义设备号 */
		alloc_chrdev_region(&ap3216cdev.devid, 0, AP3216C_CNT, AP3216C_NAME);	/* 申请设备号 */
		ap3216cdev.major = MAJOR(ap3216cdev.devid);	/* 获取分配号的主设备号 */
		ap3216cdev.minor = MINOR(ap3216cdev.devid);	/* 获取分配号的次设备号 */
	}
	printk("ap3216cdev major=%d,minor=%d\r\n",ap3216cdev.major, ap3216cdev.minor);	

	/* 2、初始化cdev */
	ap3216cdev.cdev.owner = THIS_MODULE;
	cdev_init(&ap3216cdev.cdev, &ap3216c_fops);

	/* 3、添加一个cdev */
	cdev_add(&ap3216cdev.cdev, ap3216cdev.devid, AP3216C_CNT);

	/* 4、创建类 */
	ap3216cdev.class = class_create(THIS_MODULE, AP3216C_NAME);
	if (IS_ERR(ap3216cdev.class)) {
		return PTR_ERR(ap3216cdev.class);
	}

	/* 5、创建设备 */
	ap3216cdev.device = device_create(ap3216cdev.class, NULL, ap3216cdev.devid, NULL, AP3216C_NAME);
	if (IS_ERR(ap3216cdev.device)) {
		return PTR_ERR(ap3216cdev.device);
	}
	
	ap3216cdev.private_data = client;
	return 0;
}

其中在初始化cdev中会初始化操作函数

cdev_init(&ap3216cdev.cdev, &ap3216c_fops);

static struct file_operations ap3216c_fops = { 		//操作函数,打开读写操作
 	.owner = THIS_MODULE,
 	.open = ap3216c_open,
 	.read = ap3216c_read,
 	.release = ap3216c_release,
 };

读写操作

match过程

i2c_add_driver–>i2c_register_driver–>i2c_bus_type–>.match->i2c_device_match–>of_driver_match_device/i2c_match_id
(比较i2c_driver->id_table->name和client->name,如果相同,则匹配上,匹配上之后,运行driver_register调用driver_probe_device进行设备与驱动绑定。

i2c_client实例化过程

在Linux启动的时候会将信息进行收集,i2c适配器会扫描已经静态注册的i2c_board_info,通过调用i2c_register_board_info函数将包含所有I2C设备的i2c_board_info信息的i2c_devinfo变量加入到__i2c_board_list链表中,并调用i2c_new_device为其实例化一个i2c_client。

i2c_client						代表一个挂载到i2c总线上的i2c从设备,包含该设备所需要的数据
{					
	struct i2c_adapter *adapter			该i2c从设备所依附的i2c控制器 
	struct i2c_driver *driver 			该i2c从设备的驱动程序
	addr 								该i2c从设备的访问地址
	name								该i2c从设备的名称
}								

用户只需要提供相应的 I2C 设备信息,Linux 就会根据所提供的信息构造 i2c_client 结构体。

字符设备文件操作函数集合的初始化,也就是read\write等函数,具体实现就是:根据三合一光照传感器的寄存器地址,借助I2C 适配器中 i2c_algorithm结构体里面的收发(master_xfer)函数,实现对设备IIC设备的初始化和读写操作;
根据i2c读操作流程第一次从机地址,发送写操作,寄存器地址;第二次为发送从机地址,读操作,读数据。
i2c读操作

static int ap3216c_read_regs(struct ap3216c_dev *dev, u8 reg, void *val, int len)  //dev要操作的结构体,reg寄存器首地址,保存到val,读len个寄存器
{
	struct i2c_client *client = (struct i2c_client*)dev->private_data;//之前定义了将client放在private_data中,
	struct i2c_msg msg[2] = {
		{	
			.addr = client->addr,			//msg0 是发送要读取寄存器的首地址,即ap3216c的地址
			.flags = 0,						//表示要发送数据
			.buf = &reg,					//要发送数据的寄存器地址
			.len = 1,						//要发送的寄存器地址长度为1
		},{	
			.addr = client->addr,			//msg1 读取寄存器的首地址,即ap3216c的地址
			.flags = I2C_M_RD,				//表示要发送数据
			.buf = val,						//接收到从机发送的数据
			.len = len,						//要读取的寄存器长度
		}				
	};				//定义了两个msg ,将i2c的读操作分为两个阶段,i2c流程!!!
	return i2c_transfer(client->adapter, msg, 2);
}
函数操作流程:
 return i2c_transfer(client->adapter, msg, 2);
		->i2c_adapter
			->i2c_algorithm
				->master_xfer(adap,msgs,num);

写操作
Linux中I2C架构, 如何让编写I2C驱动? (三合一光照传感器,ap3216c)_第1张图片

static int 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);
	msg.addr = client->addr;			//msg0 是发送要读取寄存器的首地址,即ap3216c的地址
	msg.flags = 0;						//表示要发送数据
	msg.buf = b;						//要发送数据,寄存器地址+实际数据
	msg.len = len + 1;					//要发送的数据长度,寄存器地址长度+实际数据长度
	return i2c_transfer(client->adapter, &msg, 1);
}

流程同上
接下来就是读写操作函数,对于ap3216c来说最主要的是打开初始化和读数据并显示初始化流程如下:1. 复位(设置0X00寄存器为0X04)2. 设置工作模式(如0X03,开启ALS+PS+IR)

static int ap3216c_open(struct inode *inode, struct file *filp)
{
	unsigned char value = 0;
	filp->private_data = &ap3216cdev; /* 设置私有数据 */
	printk("ap3216c_open\r\n");
	//初始化ap3216c
	ap3216c_write_reg(&ap3216cdev, AP3216C_SYSTEMCONG, 0X4);		//复位	
	mdelay(50);								/* AP33216C复位至少10ms */
	ap3216c_write_reg(&ap3216cdev, AP3216C_SYSTEMCONG, 0X3);//判断是否初始化成功
	value = ap3216c_read_reg(&ap3216cdev, AP3216C_SYSTEMCONG);
	printk("AP3216C_SYSTEMCONG = %#x\r\n", value);
	return 0;
}

下面是数据读取
ap3216c硬件图,其中SDA和SDL为I2C信号线,INT为中断信号线,可以不接
Linux中I2C架构, 如何让编写I2C驱动? (三合一光照传感器,ap3216c)_第2张图片

ap32316c的寄存器列表,以及说明
Linux中I2C架构, 如何让编写I2C驱动? (三合一光照传感器,ap3216c)_第3张图片

寄存器的名字和地址现在 ap3216c.h 中定义

#define AP3216C_SYSTEMCONG	0x00	/* 配置寄存器 			*/
#define AP3216C_INTSTATUS	0X01	/* 中断状态寄存器 		*/
#define AP3216C_INTCLEAR	0X02	/* 中断清除寄存器 		*/
#define AP3216C_IRDATALOW	0x0A	/* IR数据低字节 			*/
#define AP3216C_IRDATAHIGH	0x0B	/* IR数据高字节 			*/
#define AP3216C_ALSDATALOW	0x0C	/* ALS数据低字节 	        */
#define AP3216C_ALSDATAHIGH	0X0D	/* ALS数据高字节			*/
#define AP3216C_PSDATALOW	0X0E	/* PS数据低字节 			*/
#define AP3216C_PSDATAHIGH	0X0F	/* PS数据高字节 			*/

寄存器AP3216C_IRDATALOW到AP3216C_PSDATAHIGH一共6个寄存器,将这个6个寄存器的值顺序读取,分别放到ir,ps,als变量中

void ap3216c_readdata(struct ap3216c_dev *dev)
{
	//als:环境光强度	ps:接近距离		ir:红外线强度
	unsigned char buf[6];
	unsigned char i = 0;
	for(i = 0; i < 6; i++){
		buf[i] = ap3216c_read_reg(dev, AP3216C_IRDATALOW + i);
	}
	if(buf[0] & 0X80){ /* IR_OF位为1,则数据无效 */
		dev->ir = 0;
		dev->ps = 0;
	}else{
		dev->ir = ((unsigned short)buf[1] << 2) | (buf[0] & 0x03);
		dev->ps = (((unsigned short)buf[5] & 0x3F) << 4) | (buf[4] & 0x0F); 
	}
	dev->als = ((unsigned short)buf[3] << 8) | buf[2];
}

static ssize_t ap3216c_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off)
{
	long err = 0;
	short data[3];
	struct ap3216c_dev *dev = (struct ap3216c_dev *)filp->private_data;
	ap3216c_readdata(dev);
	data[0] = dev->ir;
	data[1] = dev->als;
	data[2] = dev->ps;
	err = copy_to_user(buf, data, sizeof(data));
	return 0;
}

​ 寄存器AP3216C_IRDATALOW到AP3216C_PSDATAHIGH一共6个寄存器,将这个6个寄存器的值顺序读取,分别放到ir,ps,als变量中。
不同的工作模式读取时间是有差别的,读取的太快可能无法得到当前准确数值。
Linux中I2C架构, 如何让编写I2C驱动? (三合一光照传感器,ap3216c)_第4张图片

最后就是应用

int main(int argc, char *argv[])
{
	int fd;
	int err = 0;
	char *filename;
	unsigned short data[3];
	unsigned short ir, ps, als;
	
	filename = argv[1];
	if(argc != 2) {								//输出参数为2个
		printf("Error Usage!\r\n");
		return -1;
	}
	
	fd = open(filename, O_RDWR);
	if (fd < 0) {
		printf("Can't open file %s\r\n", filename);
		return -1;
	}
	//als:环境光强度	ps:接近距离		ir:红外线强度
	while(1){
		err = read(fd, data, sizeof(data));
		if(err == 0){
			ir = data[0];
			als = data[1];
			ps = data[2];
			printf("ap3216c ir = %d, als = %d, ps = %d\r\n", ir, als, ps);
		}
		usleep(200000);
	}
	close(fd);
	return 0;
}

至此,整个基于i2c的ap3216c的驱动程序就写完了,输出结果如下:

/lib/modules/4.1.15 # depmod
/lib/modules/4.1.15 # modprobe ap3216cplus.ko 
ap3216c probe 
ap3216cdev major=249,minor=0
/lib/modules/4.1.15 # ./ap3216cAPP /dev/ap3216ci2c 
ap3216c_open
AP3216C_SYSTEMCONG = 0x3
ap3216c ir = 0, als = 0, ps = 0
ap3216c ir = 0, als = 501, ps = 136
ap3216c ir = 36, als = 453, ps = 51
ap3216c ir = 0, als = 507, ps = 57
ap3216c ir = 30, als = 516, ps = 94
ap3216c ir = 0, als = 460, ps = 0
ap3216c ir = 25, als = 496, ps = 0
ap3216c ir = 0, als = 451, ps = 67
ap3216c ir = 4, als = 501, ps = 124
ap3216c ir = 12, als = 434, ps = 80
ap3216c ir = 28, als = 444, ps = 0
ap3216c ir = 0, als = 150, ps = 57
ap3216c ir = 5, als = 41, ps = 225
ap3216c ir = 0, als = 380, ps = 133
ap3216c ir = 0, als = 29, ps = 1023
ap3216c ir = 20, als = 0, ps = 1023

你可能感兴趣的:(c语言,linux)