1.驱动
在《嵌入式之嵌入式系统》文章中介绍到驱动用于控制硬件,为操作系统或应用程序提供控制硬件的接口。
2.用户态和内核态
用户态和内核态是操作系统的两种运行级别,它们的主要区别在于权限、系统调用、CPU指令、中断处理、内存访问和运行环境等方面。
用户态 | 内核态 |
---|---|
用户态是最低权限的运行状态。 不能直接访问操作系统内核数据结构和程序。 访问的内存空间和对象受到限制。 占用的CPU可被其他程序抢占。 |
内核态是最高权限的运行状态。 可以访问系统的所有资源。 可以使CPU执行所有的指令。 可以访问所有的内存地址。 |
3.驱动程序和应用程序
应用程序 | 驱动程序 |
---|---|
属于用户态。 通过操作系统或驱动操作硬件。 由用户态函数(如系统调用和库函数)实现:
|
属于内核态。 直接操作硬件。 由内核态函数实现:
|
4.驱动类型
市面上有很多设备,每种设备使用的驱动各不相同,但驱动大致可以分为三类:字符设备驱动、块设备驱动和网络设备驱动。
4.1.字符设备驱动
字符设备,如鼠标、音箱、打印机等,是指以字节(即8个比特位)为单位进行传输的设备,操作字符设备的驱动被称为字符设备驱动。
4.2.块设备驱动
块设备与字符设备正好相反,块设备是以块为单位进行传输的设备,常指存储器设备,如机械硬盘、固态硬盘、eMMC、NAND、SD卡、U盘等,操作块设备的驱动被称为块设备驱动。
块设备在进行数据传输时,会将数据分割成固定大小的块,然后按照块的顺序进行传输。
4.3.网络设备驱动
网络设备是将服务器、PC、手机等终端设备相互链接的设备,如网卡、路由器、交换机等,操作网络设备的驱动被称为块设备驱动。
网络设备驱动负责网络数据的传输和接受。
本文章不对网络设备驱动讲解。
5.设备文件
设备文件用于表示系统中的设备,也可以理解为设备被抽象为文件,这使得设备操作与文件操作相似,以便通过操作文件来操作设备。设备文件通常存在于/dev目录下。
设备文件分为字符设备文件和块设备文件,分别用于表示字符设备和块设备,网络设备没有设备文件。
普通文件有文件类型、权限、硬连接数量、所有者、所属组、文件大小、更新时间、文件名这几个属性。设备文件的属性与普通文件的属性大致相同,但设备文件没有文件大小属性,而有设备号。字符设备文件和块设备文件的文件类型分别由c和b字母表示。
下图为设备文件属性
设备号分为主设备号和从设备号。主设备号用于标识一个驱动程序,可取范围为1至254。从设备号用于标识使用某一驱动程序的各个具体设备,可取范围为0至255。如下图,8是主设备号,用于标识驱动程序;0、1、2、5是从设备号,用于标识使用这一驱动程序的各个储存分区。
其他说明
- 字符设备文件的主设备号和块设备文件的主设备号可重复使用;
- 不同主设备号下的从设备号可重复使用;
- 通过cat /proc/devices命令可查看主设备号的使用情况。
6.可加载内核模块
向内核添加驱动有两种方法。一种方式是在编译内核前,先将驱动程序添加到源码中,然后再编译内核源码并烧写,此种方法实现步骤请查看《物联网之嵌入式系统启动过程》文章的“添加Linux内核功能”章节,此种方法不利于调试。另一种是采用可加载内核模块方法,可加载内核模块,简称LKM,是Linux内核向外部提供的一个接口,使得外部程序能够在系统运行时动态地加载和卸载内核功能单元。这些模块具有独立的功能,可以被单独编译,在系统运行时它们被链接到内核,作为内核的一部分在内核空间运行。
6.1.可加载内核模块的实现
第1步:编写模块代码
- 第①步:创建C源文件,如module.c
- 第②步:在文件中添加<linux/module.h>和<linux/kernel.h>头文件
- 第③步:定义模块的初始化函数,代码如下:
static int __init xxx_init(void)
{
// 初始化代码
return 0; // 返回0表示初始化成功
}
// 说明:
// __init:是一个宏。用于告诉编译器此函数只在模块的加载期间使用
// xxx_init:初始化函数名称。初始化函数名称常以__init结尾
- 第④步:定义模块的清理函数,代码如下:
static int __exit xxx_exit(void)
{
// 清理代码
}
// 说明:
// __exit:是一个宏。用于告诉编译器此函数只在模块的卸载期间使用
// xxx_ exit:清理函数名称。清理函数名称常以__exit结尾
- 第⑤步:注册模块的初始化函数和清理函数,代码如下:
module_init(xxx_init) // module_init是一个宏,用来告诉内核哪个函数作为模块的初始化函数
module_exit(xxx_exit) // module_exit是一个宏,用来告诉内核哪个函数作为模块的清理函数
- 第⑥步:提供有关模块的额外信息,代码如下:
MODULE_LICENSE(module_license) // 指定模块的许可证类型
MODULE_AUTHOR(author_name) // 指定模块的作者
MODULE_DESCRIPTION(module_description) // 指定模块的描述信息
第2步:编译模块
- 第①步:创建一个名为
Makefile
的文件,用于编译模块,代码如下:
# obj-m变量用于指定需要编译为模块的目标文件
# <target_file_name>同C源文件的名称
obj-m := <target_file_name>.o
# KERNELDIR指向Linux内核源码的目录。
# 内核源码中包含了编译内核模块所需的脚本、头文件和Makefile
KERNELDIR = <kernel_directory>
# PWD指向内核模块所在目录。
# 用于告诉内核源代码目录中的Makefile,内核模块的源码在哪个目录下
PWD = $(shell pwd)
default:
# 编译内核模块
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
# 删除中间文件
rm -rf *.order *.mod.* *.o *.symvers
clean:
# 删除<target_file_name>.ko文件
rm -rf *.ko
- 第②步:执行make命令编译模块,编译成功后会产生一个<arget_file_name>.ko文件
第3步:在开发板上加载和卸载内核模块
- 执行insmod <arget_file_name>.ko命令加载模块。
- 执行rmmod <arget_file_name>命令卸载模块。
- 执行modinfo <arget_file_name>.ko命令查看模块的详细信息。
- 执行lsmod命令查看系统中所有已加载的模块。
6.2.可加载内核模块的示例
第1步:编写模块代码
- 第①步:创建module.c文件
- 第②③④⑤⑥步:向module.c文件添加如下代码
#include <linux/module.h>
#include <linux/kernel.h>
static int __init demo_init(void)
{
printk(KERN_INFO "模块被加载进内核\n");
return 0;
}
static void __exit demo_exit(void)
{
printk(KERN_INFO "模块从内核中卸载\n");
}
module_init(demo_init);
module_exit(demo_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Bob");
MODULE_DESCRIPTION("Demo description");
第2步:编译模块
- 第①步:创建名为
Makefile
的文件,代码如下:
obj-m := module.o
KERNELDIR = /home/lg/share/kernel
PWD = $(shell pwd)
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
rm -rf *.order *.mod.* *.o *.symvers
clean:
rm -rf *.ko
- 第②步:执行make命令编译模块,编译成功后会产生一个module.ko文件,如下图:
第3步:在开发板上加载和卸载内核模块
- 执行insmod module.ko命令加载模块
- 执行rmmod module命令卸载模块
7.字符设备驱动开发
虽然字符设备驱动为应用程序提供操作硬件的接口,但应用程序不能直接使用字符设备驱动提供的接口,这是因为应用程序工作在用户态,而驱动程序工作在内核态。应用程序通过系统调用调用驱动程序,系统调用通过file_operations结构体与驱动程序关联起来。
file_operations结构体里面包含一组函数指针,是对系统调用的统一管理,并指向驱动程序里的函数,以使系统调用与驱动程序相互关联起来。file_operations结构体的成员后续介绍。
7.1.字符设备驱动开发过程
7.1.1.过程
第1步:编写字符设备驱动代码
- 第①步:创建C源文件,如driver.c
- 第②步:根据file_operations结构体的成员编写驱动函数
- 第③步:file_operations结构体的相应成员指向驱动函数
- 第④步:通过register_chrdev()函数注册字符设备驱动
- 第⑤步:通过unregister_chrdev()函数注销字符设备驱动
第2步:执行make命令编译字符设备驱动
第3步:在开发板执行如下命令创建设备文件
mknod device_name {b | c} major minor
// 作用:用于创建字符设备文件和块设备文件
// device_name:设备文件名称
// b:表示块设备文件
// c:表示字符设备文件
// major:主设备号
// minor:从设备号
第4步:测试
7.1.2.示例
第1步:编写字符设备驱动代码
- 第①步:创建char_driver.c文件
- 第②③④⑤步:根据file_operations结构体的成员编写驱动函数
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/uaccess.h>
// 编写打开文本文件的驱动函数
static int demo_open(struct inode *pinode, struct file *pfile)
{
printk(KERN_WARNING "我是驱动: 文本文件已打开\n");
return 0;
}
// 编写读取文本文件的驱动函数
static ssize_t demo_read(struct file *pfile, char __user *pbuf, size_t count, loff_t *off)
{
int ret;
char data[] = "我是内核!";
int len = min(count, sizeof(data));
ret = copy_to_user(pbuf, data, len);
return len;
}
// 编写写入文本文件的驱动函数
static ssize_t demo_write(struct file *pfile, const char __user *pbuf, size_t count, loff_t *off)
{
int ret;
char data[100] = "";
int len = min(count, sizeof(data));
ret = copy_from_user(data, pbuf, len);
printk(KERN_WARNING "我是驱动: 收到应用程序的信息是“%s”\n", data);
return count;
}
// 编写关闭文本文件的驱动函数
static int demo_release(struct inode *pinode, struct file *pfile)
{
printk(KERN_WARNING "我是驱动: 文本文件已关闭\n");
return 0;
}
// 通过file_operations结构体指向驱动函数
static struct file_operations fops = {
// 表示当前正在被编译的内核模块拥有fops结构体成员所指驱动函数的操作权限,THIS_MODULE表示当前正在被编译的内核模块。
.owner = THIS_MODULE,
.open = demo_open,
.read = demo_read,
.write = demo_write,
.release = demo_release,
};
static int __init demo_init(void)
{
// 注册字符设备驱动
register_chrdev(240,"char_driver",&fops);
printk(KERN_INFO "我是驱动: 模块加载成功-驱动注册成功\n");
return 0;
}
static void __exit demo_exit(void)
{
// 注销字符设备驱动
unregister_chrdev(240,"char_driver");
printk(KERN_INFO "我是驱动: 驱动注销成功-模块卸载成功\n");
}
module_init(demo_init);
module_exit(demo_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Bob");
MODULE_DESCRIPTION("Demo description");
第2步:执行make命令编译字符设备驱动
第3步:在开发板执行如下命令创建设备文件
mknod /dev/char_driver c 240 88
第4步:测试
- 第①步:创建名为read_app.c的应用程序,代码如下:
#include <stdio.h>