EmbedFire i.MX 6ull开发板学习笔记(六)---驱动模板
一、驱动编译
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,机械硬盘等储存设备,不能按字节流顺序读写,只能按块读写,即需一次读写所规定的最小字节数。而且这些储存设备都是有读写次数寿命的,如果按字节流进行读写,太过频繁了,可能用不了多久就寿终正寝了,所以得做个缓存,缓存达到一定大小了再写入,这便能延长设备的使用寿命。这类型设备的驱动便是块设备驱动。