EmbedFire i.MX 6ull开发板学习笔记(六)---驱动模板

本文介绍了Linux驱动的编译方式,包括编译为模块和编译进内核,并详细讲解了字符设备驱动、platform设备驱动和块设备驱动的概念及应用场景,提供了驱动模板的解析。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、驱动编译

Linux驱动可编译为模块,即.ko文件;也可以编译进内核。每一个驱动几乎都有一个独立的CONFIG配置。例如:在drivers/leds/Makefile中有如下内容:

obj-$(CONFIG_LEDS_GPIO)			+= leds-gpio.o

在make muneconfig配置为m,即是编译为模块。

CONFIG_LEDS_GPIO=m   
obj-m		+= leds-gpio.o

在make muneconfig配置为为y即编译进内核。

CONFIG_LEDS_GPIO=y
obj-y		+= leds-gpio.o

所以:obj-m表示编译为模块;obj-y表示编译进内核。在调试阶段我们一般编译为模块,使用insmod 或modprobe命令加载.ko文件,用rmmod命令卸载.ko文件。

insmod不会加载当前.ko依赖的.ko,加载时.ko文件可在任何目录。modprobe会自动加载当前.ko依赖的.ko,加载时.ko文件必须已在/lib/modules/&(uname -r)/目录下存在。uname -r是当前内核版本。

二、驱动模板

Linux下一切皆文件,加载驱动后会下/dev目录下生成对应的节点,应用程序可以向操作普通文件一样进行open,read,write,close操作。linux驱动分为三大类型,即:字符设备驱动,platform设备驱动和块设备驱动。

1、字符设备驱动:

按字节流顺序读写,如IIC,UART等都是字符设备。Linux下绝大部分驱动都属于此种类型,驱动基本模板如下:

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/cdev.h>
#include <linux/device.h>

#define XXXCHRDEV_CNT			1		  		/* 设备号个数 */
#define XXXCHRDEV_NAME			"xxxchrdev"		/* 名字 */
 
/* xxxchrdev设备结构体 */
struct xxxchr_dev{
	dev_t devid;			/* 设备号。在include/linux/types.h中定义有dev_t(32位无符合整数)来表示设备号。*/
							/* 主设备号为高12位,次设备号为低20位。并提供了宏MAJOR(dev),MINOR(dev),MKDEV(ma,mi) */
							/* 来方便提取主次设备号和组合设备号。*/
	struct cdev cdev;		/* cdev。表示一个字符设备 */
	struct class *class;	/* 类 */
	struct device *device;	/* 设备 */
	int major;				/* 主设备号:具体某个驱动。 */						
	int minor;				/* 次设备号:使用该驱动的设备数量。*/
};

struct xxxchr_dev xxxchrdev;

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

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

/*
 * @description		: 向设备写数据 
 * @param - filp 	: 设备文件,表示打开的文件描述符
 * @param - buf 	: 要写给设备写入的数据
 * @param - cnt 	: 要写入的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t xxx_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
	return 0;
}

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

/* 设备操作函数。与应用程序的open、read,write,close等文件操作接口一一对应。 */
static struct file_operations xxxchrdev_fops = {
	.owner = THIS_MODULE,
	.open = xxx_open,
	.read = xxx_read,
	.write = xxx_write,
	.release = 	xxx_release,
};

/*
 * @description	: 驱动入口函数。使用insmod或modprobe加载.ko文件时调用此函数。
 * @param 		: 无
 * @return 		: 无
 */
static int __init xxx_init(void)
{
	/* 注册字符设备驱动 */
	/* 1、创建设备号 */
	if (xxxchrdev.major) {			/*  定义了设备号 */
		xxxchrdev.devid = MKDEV(xxxchrdev.major, 0);
		register_chrdev_region(xxxchrdev.devid, XXXCHRDEV_CNT, XXXCHRDEV_NAME);
	} else {						/* 没有定义设备号 */
		alloc_chrdev_region(&xxxchrdev.devid, 0, XXXCHRDEV_CNT, XXXCHRDEV_NAME);	/* 申请设备号 */
		xxxchrdev.major = MAJOR(xxxchrdev.devid);	/* 获取分配号的主设备号 */
		xxxchrdev.minor = MINOR(xxxchrdev.devid);	/* 获取分配号的次设备号 */
	}
	
	/* 2、初始化cdev */
	xxxchrdev.cdev.owner = THIS_MODULE;
	cdev_init(&xxxchrdev.cdev, &xxxchrdev_fops);
	/* 3、向内核添加一个cdev,这两步相当于register_chrdev()函数 */
	cdev_add(&xxxchrdev.cdev, xxxchrdev.devid, XXXCHRDEV_CNT);

	/* 4、创建类 */
	xxxchrdev.class = class_create(THIS_MODULE, XXXCHRDEV_NAME);
	if (IS_ERR(xxxchrdev.class)) {
		return PTR_ERR(xxxchrdev.class);
	}
	/* 5、创建设备。这两步用于在/dev目录下生成/dev/XXXCHRDEV_NAME这个设备节点 */
	xxxchrdev.device = device_create(xxxchrdev.class, NULL, xxxchrdev.devid, NULL, XXXCHRDEV_NAME);
	if (IS_ERR(xxxchrdev.device)) {
		return PTR_ERR(xxxchrdev.device);
	}
	
	return 0;
}

/*
 * @description	: 驱动出口函数。使用rmmod卸载.ko文件时调用此函数。
 * @param 		: 无
 * @return 		: 无
 */
static void __exit xxx_exit(void)
{
	/* 注销字符设备驱动 */
	cdev_del(&xxxchrdev.cdev);/*  删除cdev */
	unregister_chrdev_region(xxxchrdev.devid, XXXCHRDEV_CNT); /* 注销设备号。这两步相当于unregister_chrdev()函数 */

	device_destroy(xxxchrdev.class, xxxchrdev.devid);  /* 注销类 */
	class_destroy(xxxchrdev.class);		/* 注销设备 */
}

module_init(xxx_init);
module_exit(xxx_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Tim_Tian");

2、platform设备驱动:

其实也属于字符设备驱动,只是驱动分离和分层的产物而已。假设SOC某一IIC接口上挂有两个外设:G-Sensor和呼吸灯,如果两个设备都单独去编写一个IIC控制器的驱动,做出IIC接口,然后再去编译与外设有关的驱动部分,工作显然有点重复了,我们为何不把IIC控制器部分的驱动独立出来,作为公共代码,预留与设备有关的参考接口,如IIC地址,G-Sensor和呼吸灯驱动只需调用做好的IIC接口,编写与自己有关的驱动部分即可。这便是总线(bus)、驱动(driver)和设备(device)模型。在上面示例中:IIC即总线(bus),提供统一的API接口;SOC的IIC控制器即driver,由SOC厂商编写,对于挂在该IIC总线上外设来说都是一样的;G-Sensor和呼吸灯就是device,由设备厂自行编写,我们的主要工作就是编写这个设备驱动。
那platform设备驱动是什么呢?由于有的外设没有bus这个概念, 所以linux搞出了platform这个虚拟总线。这个便是platform设备驱动。其实IIC,SPI这些总线也是继承platform而来的,框架是一样的,所以它们其实也是属于platform设备驱动的一种。更建议在platform框架下编写字符设备驱动。

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/platform_device.h>

#define XXXDEV_CNT		    1				/* 设备号个数 	*/
#define XXXDEV_NAME			"xxx_name"	    /* 设备名字 	*/

/* xxx_dev设备结构体 */
struct xxx_dev{
	dev_t devid;			/* 设备号	*/
	struct cdev cdev;		/* cdev。表示一个字符设备 */
	struct class *class;	/* 类 		*/
	struct device *device;	/* 设备		*/
	int major;				/* 主设备号:具体某个驱动。 */						
	int minor;				/* 次设备号:使用该驱动的设备数量。*/
};

struct xxx_dev xxxdev; 	    /* xxx设备 */

/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
 * 					  一般在open的时候将private_data指向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int xxx_open(struct inode *inode, struct file *filp)
{
	filp->private_data = &xxxdev; /* 设置私有数据  */
	return 0;
}
/*
 * @description		: 从设备读取数据 
 * @param - filp 	: 要打开的设备文件(文件描述符)
 * @param - buf 	: 返回给用户空间的数据缓冲区
 * @param - cnt 	: 要读取的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t xxx_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
	return 0;
}
/*
 * @description		: 向设备写数据 
 * @param - filp 	: 设备文件,表示打开的文件描述符
 * @param - buf 	: 要写给设备写入的数据
 * @param - cnt 	: 要写入的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t xxx_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
	return 0;
}

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

/* 设备操作函数。与应用程序的open、read,write,close等文件操作接口一一对应。 */
static struct file_operations xxx_fops = {
	.owner = THIS_MODULE,
	.open = xxx_open,
	.read = xxx_read,
	.write = xxx_write,
	.release = 	xxx_release,
};

/*
 * @description		: flatform驱动的probe函数,当驱动与设备匹配以后此函数就会执行。
					: 相当于原xxxdriver_init()
 * @param - dev 	: platform设备
 * @return 			: 0,成功;其他负值,失败
 */
static int xxx_probe(struct platform_device *dev)
{	
	/* 注册字符设备驱动 */
	/*1、创建设备号 */
	if (xxxdev.major) {		/*  定义了设备号 */
		xxxdev.devid = MKDEV(xxxdev.major, 0);
		register_chrdev_region(xxxdev.devid, XXXDEV_CNT, XXXDEV_NAME);
	} else {						/* 没有定义设备号 */
		alloc_chrdev_region(&xxxdev.devid, 0, XXXDEV_CNT, XXXDEV_NAME);	/* 申请设备号 */
		xxxdev.major = MAJOR(xxxdev.devid);	/* 获取分配号的主设备号 */
		xxxdev.minor = MINOR(xxxdev.devid);	/* 获取分配号的次设备号 */
	}
	
	/* 2、初始化cdev */
	xxxdev.cdev.owner = THIS_MODULE;
	cdev_init(&xxxdev.cdev, &xxx_fops);
	
	/* 3、添加一个cdev */
	cdev_add(&xxxdev.cdev, xxxdev.devid, XXXDEV_CNT);

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

	/* 5、创建设备 */
	xxxdev.device = device_create(xxxdev.class, NULL, xxxdev.devid, NULL, XXXDEV_NAME);
	if (IS_ERR(xxxdev.device)) {
		return PTR_ERR(xxxdev.device);
	}

	return 0;
}

/*
 * @description		: platform驱动的remove函数,移除platform驱动的时候此函数会执行
					: 相当于原xxxdriver_exit()函数
 * @param - dev 	: platform设备
 * @return 			: 0,成功;其他负值,失败
 */
static int xxx_remove(struct platform_device *dev)
{
	cdev_del(&xxxdev.cdev);/*  删除cdev */
	unregister_chrdev_region(xxxdev.devid, XXXDEV_CNT); /* 注销设备号 */
	device_destroy(xxxdev.class, xxxdev.devid);
	class_destroy(xxxdev.class);
	return 0;
}

/* 设备树匹配列表。与设备树种的compatible属性进行比较,相等即匹配成功。 */
static const struct of_device_id xxx_of_match[] = {
	{ .compatible = "device_name" },
	{ /* Sentinel */ }
};

MODULE_DEVICE_TABLE(of, xxx_of_match);

/* platform驱动结构体 */
static struct platform_driver xxx_driver = {
	.driver		= {
		.name	= "driver_name",			/* 驱动名字,用于和设备匹配 */
		.of_match_table	= xxx_of_match,     /* 设备树匹配表 		 */
	},
	.probe		= xxx_probe,
	.remove		= xxx_remove,
};
		
/*
 * @description	: 驱动模块加载函数
 * @param 		: 无
 * @return 		: 无
 */
static int __init xxxdriver_init(void)
{
	return platform_driver_register(&xxx_driver);
}

/*
 * @description	: 驱动模块卸载函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit xxxdriver_exit(void)
{
	platform_driver_unregister(&xxx_driver);
}

module_init(xxxdriver_init);
module_exit(xxxdriver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Tim_Tian");

3、块设备驱动:

由于某些设备,如flash,nand,emmc,机械硬盘等储存设备,不能按字节流顺序读写,只能按块读写,即需一次读写所规定的最小字节数。而且这些储存设备都是有读写次数寿命的,如果按字节流进行读写,太过频繁了,可能用不了多久就寿终正寝了,所以得做个缓存,缓存达到一定大小了再写入,这便能延长设备的使用寿命。这类型设备的驱动便是块设备驱动。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值