树莓派驱动开发--驱动文件代码的浅度分析(以iic的为例)

前言:我使用的代码是正点原子的驱动代码,我们借鉴学习,看多了别人优秀的代码是我们自主完成代码编写的前提!

一. 总体层面梳理

总线-驱动-设备 模型

--把不同功能的外设归类,然后实现统一接口,无法归类的使用虚拟总线来形容,从而实现总线-驱动-设备模型.

--为什么要这样?比如,像iic外设的驱动代码都是一样的,所以内核里保留一套统一的就好,这样既方便了开发者的使用,也统一了形式,使得内核代码美观统一,这就是所谓的平台总线,在iic看来也为iic适配器.

总线:把重复的东西,比如iic的时序实现代码(这样一样的重复的)统一让总线完成,然后提供统一的接口让外面调用.

驱动:新字符设备的那一套东西.

设备:具体的设备在设备树上的描述.

所以,我们的思路是,按照内核规定的统一标准流程来开发,也既按照总线-驱动-设备模型流程.

我们知道,总线是统一的东西,那么内核肯定是已经帮我们写好写死了,我们只需要知道怎么去和它联系怎么和它挂钩就好.注意:总线是连接驱动和设备的桥梁.

驱动无非就是把驱动侧的设备对设备树侧的设备匹配的信息结构体给初始化,然后注册进内核,内核里面也是同样的操作,根据厂家在设备树上描述的外设把内核实现好的总线激活,这样总线则会自动去匹配设备和驱动,匹配后执行probe函数按照新字符设备那套流程向内核注册一个具体的字符设备.外设的驱动和具体设备的驱动流程基本是一样的,只不过外设的驱动是在开发板启动时就完成了,而具体设备的驱动,是我们insmod.ko时才完成的,当然,如果把驱动模块写入内核编译,那也会在启动时完成.(不理解没关系,下面代码会解释)

设备就是修改设备树,把设备信息描述上去.

二. 代码理解

链接:https://pan.baidu.com/s/1nvEqah5w6NlIQ-7RG5vu7g?pwd=hsw1

提取码:hsw1

参考代码在什么链接,一定要去拿到对照学习.

(1)驱动无非就是把驱动侧的设备对设备树侧的设备匹配的信息结构体给初始化,然后注册进内核,内核里面也是同样的操作,根据厂家在设备树上描述的外设把内核实现好的总线激活,这样总线则会自动去匹配设备和驱动,匹配后执行probe函数按照新字符设备那套流程向内核注册一个具体的字符设备.外设的驱动和具体设备的驱动流程基本是一样的,只不过外设的驱动是在开发板启动时就完成了,而具体设备的驱动,是我们insmod.ko时才完成的,当然,如果把驱动模块写入内核编译,那也会在启动时完成.(不理解没关系,下面代码会解释)

(2)总线是连接驱动和设备的桥梁.

按这两句话理顺驱动开发编写流程.

1.把驱动侧的设备对设备树侧的设备匹配的信息结构体给初始化

/* 传统匹配方式ID列表 */
static const struct i2c_device_id ap3216c_id[] = {
	{"alientek,ap3216c", 0},  
	{}
};

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

/* i2c驱动结构体 */	
static struct i2c_driver ap3216c_driver = {
	.probe = ap3216c_probe,
	.remove = ap3216c_remove,
	.driver = {
			.owner = THIS_MODULE,
		   	.name = "ap3216c",
		   	.of_match_table = ap3216c_of_match, 
		   },
	.id_table = ap3216c_id,
};

说白了就是怎么让设备和驱动匹配------compatible属性!或者传统的方式!

匹配后干嘛------执行probe.

.name = "ap3216c", 表示设备树节点下设备名称,传统匹配方法之一就是根据名字

2.注册进内核,总线是连接驱动和设备的桥梁.

/*
 * @description	: 驱动入口函数
 * @param 		: 无
 * @return 		: 无
 */
static int __init ap3216c_init(void)
{
	int ret = 0;

	ret = i2c_add_driver(&ap3216c_driver);
	return ret;
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit ap3216c_exit(void)
{
	i2c_del_driver(&ap3216c_driver);
}

/* module_i2c_driver(ap3216c_driver) */

module_init(ap3216c_init);
module_exit(ap3216c_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("zuozhongkai");

这里就没必要纠结注册函数为什么是i2c_add_driver,用到的时候查一下即可,毕竟现在的学习资源丰富.

我们在insmod.ko后,总线便会工作,看看有没有驱动和设备树上的设备匹配,一旦有,它就会获取设备树上的设备信息,比如reg描述的设备地址等,存储在内核统一规定的结构体中,比如iic的client结构体,驱动使用client结构体就相当于访问设备信息.所以说总线是连接设备和驱动的桥梁.

结合1 2点,将第一步总结为

让驱动匹配设备,注册进内核,总线实现驱动和设备的匹配并以结构体的形式返回设备信息

3.内核里面也是同样的操作,根据厂家在设备树上描述的外设把内核实现好的总线激活,这样总线则会自动去匹配设备和驱动

内核源码里面有外设的驱动的代码,流程也是结构体初始化然后注册,只不过这些比较复杂统一,是厂家写好的,已经实现好了.它匹配的设备树上的外设也是厂家根据自己板子有的外设来描述的.启动时就已经加载好了.

4.匹配后执行probe函数按照新字符设备那套流程向内核注册一个具体的字符设备

#define AP3216C_CNT	1
#define AP3216C_NAME	"ap3216c"

struct ap3216c_dev {
	dev_t devid;			/* 设备号 	 */
	struct cdev cdev;		/* cdev 	*/
	struct class *class;	/* 类 		*/
	struct device *device;	/* 设备 	 */
	struct device_node	*nd; /* 设备节点 */
	int major;			/* 主设备号 */
	void *private_data;	/* 私有数据 */
	unsigned short ir, als, ps;		/* 三个光传感器数据 */
};

/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
 * 					  一般在open的时候将private_data指向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int ap3216c_open(struct inode *inode, struct file *filp)
{
	filp->private_data = &ap3216cdev;

	/* 初始化AP3216C */
	ap3216c_write_reg(&ap3216cdev, AP3216C_SYSTEMCONG, 0x04);		/* 复位AP3216C 			*/
	mdelay(50);														/* AP3216C复位最少10ms 	*/
	ap3216c_write_reg(&ap3216cdev, AP3216C_SYSTEMCONG, 0X03);		/* 开启ALS、PS+IR 		*/
	return 0;
}

/*
 * @description		: 从设备读取数据 
 * @param - filp 	: 要打开的设备文件(文件描述符)
 * @param - buf 	: 返回给用户空间的数据缓冲区
 * @param - cnt 	: 要读取的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t ap3216c_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off)
{
	short data[3];
	long err = 0;

	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;
}

/*
 * @description		: 关闭/释放设备
 * @param - filp 	: 要关闭的设备文件(文件描述符)
 * @return 			: 0 成功;其他 失败
 */
static int ap3216c_release(struct inode *inode, struct file *filp)
{
	return 0;
}

/* AP3216C操作函数 */
static const struct file_operations ap3216c_ops = {
	.owner = THIS_MODULE,
	.open = ap3216c_open,
	.read = ap3216c_read,
	.release = ap3216c_release,
};

static struct ap3216c_dev ap3216cdev;

 /*
  * @description     : i2c驱动的probe函数,当驱动与
  *                    设备匹配以后此函数就会执行
  * @param - client  : i2c设备
  * @param - id      : i2c设备ID
  * @return          : 0,成功;其他负值,失败
  */
static int ap3216c_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
	/* 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);
	}

	/* 2、注册设备 */
	cdev_init(&ap3216cdev.cdev, &ap3216c_ops);
	cdev_add(&ap3216cdev.cdev, ap3216cdev.devid, AP3216C_CNT);

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

	/* 4、创建设备 */
	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;
}

/*
 * @description     : i2c驱动的remove函数,移除i2c驱动的时候此函数会执行
 * @param - client 	: i2c设备
 * @return          : 0,成功;其他负值,失败
 */
static int ap3216c_remove(struct i2c_client *client)
{
	/* 删除设备 */
	cdev_del(&ap3216cdev.cdev);
	unregister_chrdev_region(ap3216cdev.devid, AP3216C_CNT);

	/* 注销掉类和设备 */
	device_destroy(ap3216cdev.class, ap3216cdev.devid);
	class_destroy(ap3216cdev.class);
	return 0;
}

这里重点讲一下:ap3216cdev.private_data = client;

这句话体现了总线实现驱动和设备的匹配并以结构体的形式返回设备信息

先看看这个结构体

里面的成员便是设备的信息体,这个总线帮我们获取好了,故这里只需要将其赋值给我们编写的字符驱动设备,我们就可以访问它了.

#define AP3216C_NAME "ap3216c"

这个就是我们字符设备在内核的bus下的名称.

file_operations ap3216c_ops是驱动和app的连接桥梁.

结合3 4点,将第二步总结为

在probe中注册字符驱动,并将获取的设备信息给到字符驱动,实现ops打通app和驱动的桥梁.

5.具体设备对应的具体的iic操作实现

/*
 * @description	: 从ap3216c读取多个寄存器数据
 * @param - dev:  ap3216c设备
 * @param - reg:  要读取的寄存器首地址
 * @param - val:  读取到的数据
 * @param - len:  要读取的数据长度
 * @return 		: 操作结果
 */
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;
}

/*
 * @description	: 向ap3216c多个寄存器写入数据
 * @param - dev:  ap3216c设备
 * @param - reg:  要写入的寄存器首地址
 * @param - val:  要写入的数据缓冲区
 * @param - len:  要写入的数据长度
 * @return 	  :   操作结果
 */
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);
}

/*
 * @description	: 读取ap3216c指定寄存器值,读取一个寄存器
 * @param - dev:  ap3216c设备
 * @param - reg:  要读取的寄存器
 * @return 	  :   读取到的寄存器值
 */
static unsigned char ap3216c_read_reg(struct ap3216c_dev *dev, u8 reg)
{
	u8 data = 0;

	ap3216c_read_regs(dev, reg, &data, 1);
	return data;

#if 0
	struct i2c_client *client = (struct i2c_client *)dev->private_data;
	return i2c_smbus_read_byte_data(client, reg);
#endif
}

/*
 * @description	: 向ap3216c指定寄存器写入指定的值,写一个寄存器
 * @param - dev:  ap3216c设备
 * @param - reg:  要写的寄存器
 * @param - data: 要写入的值
 * @return   :    无
 */
static void ap3216c_write_reg(struct ap3216c_dev *dev, u8 reg, u8 data)
{
	u8 buf = 0;
	buf = data;
	ap3216c_write_regs(dev, reg, &buf, 1);
}

/*
 * @description	: 读取AP3216C的数据,读取原始数据,包括ALS,PS和IR, 注意!
 *				: 如果同时打开ALS,IR+PS的话两次数据读取的时间间隔要大于112.5ms
 * @param - ir	: ir数据
 * @param - ps 	: ps数据
 * @param - ps 	: als数据 
 * @return 		: 无。
 */
void ap3216c_readdata(struct ap3216c_dev *dev)
{
	unsigned char i =0;
    unsigned char buf[6];
	
	/* 循环读取所有传感器数据 */
    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;					
	else 				/* 读取IR传感器的数据   		*/
		dev->ir = ((unsigned short)buf[1] << 2) | (buf[0] & 0X03); 			
	
	dev->als = ((unsigned short)buf[3] << 8) | buf[2];	/* 读取ALS传感器的数据 			 */  
	
    if(buf[4] & 0x40)	/* IR_OF位为1,则数据无效 			*/
		dev->ps = 0;    													
	else 				/* 读取PS传感器的数据    */
		dev->ps = ((unsigned short)(buf[5] & 0X3F) << 4) | (buf[4] & 0X0F); 
}

struct i2c_client *client = (struct i2c_client *)dev->private_data;

这一步体现了我们ap3216cdev.private_data = client;这一步的作用!

函数i2c_transfer(client->adapter, msg, 2); 便是iic在内核的统一对外api,不要纠结我怎么知道是这个,用的时候查一查就好,毕竟这是人家的东西,整个内核复杂要死,一个一个理解不可能,用多了自然会领悟.

结合第5点,将第三步总结为

具体设备iic操作实现

三. 总结

需掌握:总线-驱动-设备架构含义.

心中有流程:

1.让驱动匹配设备,注册进内核,总线实现驱动和设备的匹配并以结构体的形式返回设备信息

2.在probe中注册字符驱动,并将获取的设备信息给到字符驱动,实现ops打通app和驱动的桥梁

3.具体设备操作实现

相关推荐
hhhhhhh_hhhhhh_15 小时前
rk3568制冷项目驱动开发流程汇总(只适用于部分模块CIF DVP等,自用)
驱动开发
icy、泡芙17 小时前
T527-----音频调试
linux·驱动开发·音视频
7yewh1 天前
嵌入式Linux QT+OpenCV基于人脸识别的考勤系统 项目
linux·开发语言·arm开发·驱动开发·qt·opencv·嵌入式linux
疯狂飙车的蜗牛2 天前
从零玩转CanMV-K230(4)-小核Linux驱动开发参考
linux·运维·驱动开发
嵌入式进阶行者2 天前
【驱动开发初级】内核模块静态和动态添加功能的步骤
驱动开发
逝灮2 天前
【蓝桥杯——物联网设计与开发】拓展模块3 - 温度传感器模块
驱动开发·stm32·单片机·嵌入式硬件·物联网·蓝桥杯·温度传感器
__NULL__USER4 天前
petalinux-adi ---添加AD9361驱动(二)
linux·驱动开发
7yewh4 天前
嵌入式驱动RK3566 HDMI eDP MIPI 背光 屏幕选型与调试提升篇-eDP屏
linux·arm开发·驱动开发·嵌入式硬件·嵌入式linux·rk·edp
少年、潜行5 天前
树莓派3B+驱动开发(8)- i2c控制PCF8591
驱动开发·树莓派·3b+
千千道5 天前
深入理解 Linux 内核启动流程
linux·arm开发·驱动开发