一、Linux三大驱动类型:
- 字符设备驱动:字符设备驱动最多,从最简单的点灯到 I2C、 SPI、音频等都属于字符设备驱动的类型。
- 块设备驱动:块设备驱动就是存储器设备的驱动,比如 EMMC、 NAND、 SD 卡和 U 盘等存储设备,因为这些存储设备的特点是以存储块为基础,因此叫做块设备。
- 网络设备驱动:不管是有线还是无线,都属于网络设备驱动。
一个设备可以属于多种设备驱动类型,比如 USB WIFI,其使用 USB 接口,所以属于字符设备,但是其又能上网,所以也属于网络设备驱动。
二、字符设备驱动开发
1.字符驱动设备介绍
字符设备是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。比如我们最常见的点灯、按键、IC、SPI,LCD等等都是字符设备,这些设备的驱动就叫做字符设备驱动。
在Linux中一切皆为文件,驱动加载成功以后会在"/dev"目录下生成一个相应的文件,应用程序通过对这个名为"/dev/xxx"(xxx是具体的驱动文件名字)的文件进行相应的操作即可实现对硬件的操作。
比如现在有个叫做/dev/led的驱动文件,此文件是led灯的驱动文件。应用程序使用open函数来打开文件/devled,使用完成以后使用close函数关闭/dev/led这个文件。open和close就是打开和关闭led驱动的函数,如果要点亮或关闭led,那么就使用write函数来操作,也就是向此驱动写入数据,这个数据就是要关闭还是要打开led的控制参数。如果要获取led灯的状态,就用read函数从驱动中读取相应的状态。
应用程序运行在用户空间,驱动运行于内核空间。
当我们在用户空间想要实现对内核的操作,比如使用open函数打开/dev/led这个驱动,因为用户空间不能直接对内核进行操作,因此必须使用一个叫做“系统调用”的方法来实现从用户空间“陷入”到内核空间,这样才能实现对底层驱动的操作。open.close,write和read等这些函数是由C库提供的,在Linux系统中,系统调用作为C库的一部分。
2.字符驱动开发步骤
2.1 Linux驱动有两种运行方式:
第一种就是将驱动编译进Linux内核中,这样当Linux内核启动的时候就会自动运行驱动程序。
第二种就是将驱动编译成模块(Linux下模块扩展名为.ko),在Linux内核启动以后使用"insmod"命令加载驱动模块。
在调试驱动的时候一般都选择将其编译为模块,这样我们修改驱动以后只需要编译一下驱动代码即可,不需要编译整个Linux代码。
而且在调试的时候只需要加载或者卸载驱动模块即可,不需要重启整个系统。
2.2 模块有加载和卸载两种操作
module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函数
“insmod”命令加载驱动的时候, xxx_init 这个函数就会被调用。
“rmmod”命令卸载具体驱动的时候, xxx_exit 函数就会被调用。
2.3 字符设备驱动模块加载和卸载模板
/* 驱动入口函数 */
static int __init xxx_init(void)
{
/* 入口函数具体内容 */
return 0;
}
/* 驱动出口函数 */
static void __exit xxx_exit(void)
{
/* 出口函数具体内容 */
}
/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(xxx_init);
module_exit(xxx_exit);
驱动编译完成以后扩展名为.ko,
驱动模块的加载使用命令“modprobe ”,比如要加载 drv.ko,命令如下:
modprobe drv.ko”
驱动模块的卸载使用命令“rmmod”即可,比如要卸载 drv.ko,命令如下:
rmmod drv.ko
也可以使用“modprobe -r”命令卸载驱动,比如要卸载 drv.ko,命令如下:
modprobe -r drv.ko
3.字符驱动注册于注销
/*
功能:注册字符设备
参数:
major:主设备号
name:设备名字,指向字符串
fops:结构体 file_operations 类型指针,指向设备的操作函数集合变量。
*/
static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
/*
功能:注销字符设备
参数:
major:主设备号
name:设备名字,指向字符串
*/
static inline void unregister_chrdev(unsigned int major, const char *name)
在 Linux 内核文件 include/linux/fs.h 中有个叫做 file_operations 的结构体,此结构体就是 Linux 内核驱动操作函数集合。
struct file_operations {
......
};
- owner拥有该结构体的模块的指针,一般设置为THISMODULE.
- Ilsek函数用于修改文件当前的读写位置。
- read函数用于读取设备文件。
- write函数用于向设备文件写入(发送)数据。
- poll是个轮询函数,用于查询设备是否可以进行非阻塞的读写。
- unlocked ioctl函数提供对于设备的控制功能,与应用程序中的ioctl函数对应。
- compat ioctl函数与unlocked ioctl函数功能一样,区别在于在64位系统上,32位的应用程序调用将会使用此函数。在32位的系统上运行32位的应用程序调用的是unlocked ioctl
- mmap函数用于将将设备的内存映射到进程空间中(也就是用户空间),一般帧缓冲设备会使用此函数,比如LCD驱动的显存,将帧缓冲(LCD显存)映射到用户空间中以后应用程序就可以直接操作显存了,这样就不用在用户空间和内核空间之间来回复制。
- open函数用于打开设备文件
- release函数用于释放(关闭)设备文件,与应用程序中的close函数对应。
- fasync函数用于刷新待处理的数据,用于将缓冲区中的数据刷新到磁盘中。
- aio_fsync函数与fasync函数的功能类似,只是aio-fsync是异步刷新待处理的数据。
4.字符驱动框架实现模板
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#define CHRDEVBASE_MAJOR 200 /* 主设备号 */
#define CHRDEVBASE_NAME "chrdevbase" /* 设备名 */
static char readbuf[100]; /* 读缓冲区 */
static char writebuf[100]; /* 写缓冲区 */
static char kerneldata[] = {"kernel data!"};
/*
* @description : 打开设备
* @param - inode : 传递给驱动的inode
* @param - filp : 设备文件,file结构体有个叫做private_data的成员变量
* 一般在open的时候将private_data指向设备结构体。
* @return : 0 成功;其他 失败
*/
static int chrdevbase_open(struct inode *inode, struct file *filp)
{
//printk("chrdevbase open!\r\n");
return 0;
}
/*
* @description : 从设备读取数据
* @param - filp : 要打开的设备文件(文件描述符)
* @param - buf : 返回给用户空间的数据缓冲区
* @param - cnt : 要读取的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 读取的字节数,如果为负值,表示读取失败
*/
static ssize_t chrdevbase_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
int retvalue = 0;
/* 向用户空间发送数据 */
memcpy(readbuf, kerneldata, sizeof(kerneldata));
retvalue = copy_to_user(buf, readbuf, cnt);
if(retvalue == 0){
printk("kernel senddata ok!\r\n");
}else{
printk("kernel senddata failed!\r\n");
}
//printk("chrdevbase read!\r\n");
return 0;
}
/*
* @description : 向设备写数据
* @param - filp : 设备文件,表示打开的文件描述符
* @param - buf : 要写给设备写入的数据
* @param - cnt : 要写入的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 写入的字节数,如果为负值,表示写入失败
*/
static ssize_t chrdevbase_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
int retvalue = 0;
/* 接收用户空间传递给内核的数据并且打印出来 */
retvalue = copy_from_user(writebuf, buf, cnt);
if(retvalue == 0){
printk("kernel recevdata:%s\r\n", writebuf);
}else{
printk("kernel recevdata failed!\r\n");
}
//printk("chrdevbase write!\r\n");
return 0;
}
/*
* @description : 关闭/释放设备
* @param - filp : 要关闭的设备文件(文件描述符)
* @return : 0 成功;其他 失败
*/
static int chrdevbase_release(struct inode *inode, struct file *filp)
{
//printk("chrdevbase release!\r\n");
return 0;
}
/*
* 设备操作函数结构体
*/
static struct file_operations chrdevbase_fops = {
.owner = THIS_MODULE,
.open = chrdevbase_open,
.read = chrdevbase_read,
.write = chrdevbase_write,
.release = chrdevbase_release,
};
/*
* @description : 驱动入口函数
* @param : 无
* @return : 0 成功;其他 失败
*/
static int __init chrdevbase_init(void)
{
int retvalue = 0;
/* 注册字符设备驱动 */
retvalue = register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME, &chrdevbase_fops);
if(retvalue < 0){
printk("chrdevbase driver register failed\r\n");
}
printk("chrdevbase init!\r\n");
return 0;
}
/*
* @description : 驱动出口函数
* @param : 无
* @return : 无
*/
static void __exit chrdevbase_exit(void)
{
/* 注销字符设备驱动 */
unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);
printk("chrdevbase exit!\r\n");
}
/*
* 将上面两个函数指定为驱动的入口和出口函数
*/
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);
/*
* LICENSE和作者信息
*/
MODULE_LICENSE("GPL");
MODULE_AUTHOR("muchunpeng");