1 设备的概念
在学习驱动和其开发之前,首先要知道所谓驱动,其对象就是设备。
1.1 主设备号&次设备号:
在Linux中,各种设备都以文件的形式存在/dev目录下,称为设备文件。最上层的应用程序可以打开,关闭,读写这些设备文件,从而完成对设备的操作。
为了管理这些设备,系统为设备编了号,每个设备都拥有主设备号和次设备号。主设备号用于区分不同种类的设备,而次设备号用于区分同一类型的多个设备。(对于常用的设备如硬盘,Linux赋予的主设备号一般是3)
- 在/dev目录下输入“ls -l”,就可以看到设备文件对应的主次设备号
1.2 设备号的用处
在了解了什么是主次设备号之后,就要了解设备号的用处:
- 在用户态中:当用户调用了如open, read, write等函数想要操作设备文件时,需要两个参数,第一个是文件名,第二个就是设备号
- 在内核态中:存在着一个驱动链表,用于管理所有设备的驱动,而驱动在链表中的位置就由设备号来检索
2 驱动的概念
参考:最全Linux驱动开发全流程详细解析(持续更新)-CSDN博客
Q1:什么是驱动?
A:驱动与底层硬件直接打交道,充当了硬件(设备)与应用软件中间的桥梁
Q2:驱动的功能?
- 对设备初始化和释放
- 把数据从内核传送到硬件 & 从硬件读取数据
- 读取应用程序传送给设备文件的数据 & 回送应用程序请求的数据
- 检测和处理设备出现的错误
Q3:驱动的分类?
Linux驱动分为三个基础大类:字符设备驱动,块设备驱动,网络设备驱动。
- 字符设备(Char Device)
- 字符设备是个能够像字节流(类似文件)一样被访问的设备
- 对字符设备发出读/写请求时,实际的硬件I/O操作一般紧接着发生
- 字符设备驱动程序通常至少要实现open、close、read和write系统调用
- 比如我们常见的lcd、触摸屏、键盘、led、串口等等,他们一般对应具体的硬件都是进行出具的采集、处理、传输
- 块设备(Block Device)
- 一个块设备驱动程序主要通过传输固定大小的数据(一般为512或1k)来访问设备
- 块设备通过buffer cache(内存缓冲区)访问,可以随机存取,即:任何块都可以读写,不必考虑它在设备的什么地方
- 块设备可以通过它们的设备特殊文件访问,但是更常见的是通过文件系统进行访问
- 只有一个块设备可以支持一个安装的文件系统
- 比如我们常见的电脑硬盘、SD卡、U盘、光盘等
- 网络设备(Net Device)
- 任何网络事务都经过一个网络接口形成,即一个能够和其他主机交换数据的设备
- 访问网络接口的方法仍然是给它们分配一个唯一的名字(比如eth0),但这个名字在文件系统中不存在对应的节点
- 内核和网络设备驱动程序间的通信,完全不同于内核和字符以及块驱动程序之间的通信,内核调用一套和数据包传输相关的函(socket函数)而不是read、write等
- 比如我们常见的网卡设备、蓝牙设备
回顾上节Linux系统的构造:
可见,驱动位于内核态,上面是系统调用;下面是硬件(设备)。
2.1 上层用户操控设备的流程:
每一个系统调用,在驱动中都有与之对应的一个驱动函数
以open为例:其系统调用为sys_open,那么驱动文件中就会有一个与其对应的“实现open的驱动函数:xxx_open()”,其大致步骤就是:用户层C库的open→系统调用层的open→驱动函数的open
以上是一个我作为初学者总结的大概流程,下图是一个更详细的流程:
2.2 Linux驱动的运行方式
- 将驱动编译进 Linux 内核中,当 Linux 内核启动的时就会自动运行驱动程序
- 将驱动编译成模块(Linux 下模块扩展名为.ko),并在Linux 内核启动以后使用相应命令加载驱动模块
3 驱动开发实战
尝试使用“将驱动编译成模块的方式”来编写“字符设备的驱动”
驱动开发不是一件容易的事情,对于初学者,可以先根据一个固定的框架来学习驱动的开发:
3.1 基本的字符设备驱动框架
注释很重要,认真看
#include <linux/fs.h> //file_operations声明
#include <linux/module.h> //module_init module_exit声明
#include <linux/init.h> //__init __exit 宏定义声明
#include <linux/device.h> //class devise声明
#include <linux/uaccess.h> //copy_from_user 的头文件
#include <linux/types.h> //设备号 dev_t 类型声明
#include <asm/io.h> //ioremap iounmap的头文件
static struct class *pin4_class;
static struct device *pin4_class_dev;
static dev_t devno; //设备号
static int major =231; //主设备号
static int minor =0; //次设备号
static char *module_name="pin4"; //模块名
//_open函数
static int pin4_open(struct inode *inode,struct file *file)
{
printk("pin4_open\n"); //内核的打印函数和printf类似
return 0;
}
//_write函数
static ssize_t pin4_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos)
{
printk("pin4_write\n"); //内核的打印函数和printf类似
return 0;
}
static struct file_operations pin4_fops = { //结构体的类型是“file_operations”,名字可以自定义
//该结构体的成员就包含实现open和write的驱动函数
//当上层用户想要open或者write这个设备时,就会最终跳转到这个驱动代码中实现的open和write操作函数
//此处只赋值了该结构体中的三个成员变量(在keil中是不能这样写的,linux中可以),这个结构体其实有很多成员,如果想要实现更多的驱动函数,可以把更多的该结构体成员赋值并在这段代码中重写
.owner = THIS_MODULE,
.open = pin4_open,
.write = pin4_write,
};
int __init pin4_drv_init(void) //真实驱动入口
{
int ret;
devno = MKDEV(major,minor); //创建设备号
ret = register_chrdev(major, module_name, &pin4_fops); //注册驱动,告诉内核:把这个驱动加入到内核驱动的链表中
//以下两句代码目的是“生成设备文件”,也可以通过“mknod”命令手动生成,但是一般不会这样做
pin4_class=class_create(THIS_MODULE,"myfirstdemo"); //先创建‘类’
pin4_class_dev =device_create(pin4_class,NULL,devno,NULL,module_name); //再创建‘设备’
return 0;
}
void __exit pin4_drv_exit(void)
{
device_destroy(pin4_class,devno); //先销毁‘设备’
class_destroy(pin4_class); //在销毁‘类’
unregister_chrdev(major, module_name); //卸载驱动
}
module_init(pin4_drv_init); //入口,内核加载驱动的时候,这个宏会被调用
module_exit(pin4_drv_exit);
MODULE_LICENSE("GPL v2"); //linux内核遵循GPL协议
在代码中添加大量“static”的原因是:内核的代码太多了,为了防止出现重名导致歧义,加入static可以保证变量的作用域只在当前代码中,从而不会影响到其他的代码。
file_operation 结构体是 Linux 内核驱动操作函数集合
比如,我在写某设备的驱动文件,并想为其实现一个read的驱动函数,那么我就需要:
- 定义一个类型为file_operation的结构体XXX,其中XXX是自定义的结构体名
- 在XXX结构体中使用“.read = XXX_read,”,其中XXX_read是自定义的函数名
- 在驱动代码里重写XXX_read函数,函数的参数格式和file_operation类型结构体中给出的write成员的参数格式保持一致,函数体就是想要实现的具体内容
3.2 驱动的编译
- 打开虚拟机,进入Linux源码的路径:
- 然后进入“drivers/char/”子目录(driver:驱动;char:字符型设备):
- 在这个路径下创建一个新的C文件"mydriver_pin4.c",内容为刚刚的字符驱动框架:
- 修改当前路径下的Makefile,确保这个新的驱动会被编译到:
- 回到linux内核源码的路径,运行以下指令尝试编译:
ARCH=arm CROSS_COMPILE=/home/mjm/ras_CrossCompile/gcc-linaro-5.1-2015.08-x86_64_arm-linux-gnueabihf/bin/arm-linux-gnueabihf- KERNEL=kernel7 make -j4 modules
回顾在前几节学习内核编译时的代码:
ARCH=arm CROSS_COMPILE=/home/mjm/ras_CrossCompile/gcc-linaro-5.1-2015.08-x86_64_arm-linux-gnueabihf/bin/arm-linux-gnueabihf- KERNEL=kernel7 make -j4 zImage modules dtbs
区别就是:代码的最后少了“zImage”和“dtbs”。原因也很简单,现在只需要编译驱动,不需要再次生成镜像文件等其他文件。
- 将编译好的“mydriver_pin4.ko”通过以下的scp命令发送到树莓派:
scp drivers/char/mydriver_pin4.ko pi@192.168.2.26:/home/pi/mjm_code
此时,编译好的驱动文件就出现在树莓派上了:
3.3 驱动的加载(卸载)
由于现在刚刚把驱动编译成了.ko的模块,所以需要运行以下指令来加载驱动模块:
sudo insmod mydriver_pin4.ko
补充:
- 驱动的卸载:
sudo rmmod mydriver_pin4.ko //此时驱动名字后不用加".ko"
- 查看内核模块:
lsmod
运行成功后,就可以在/dev下看到生成的设备文件“pin4”了:
如果用“ls -l” ,就可以看到这个设备文件的主次设备号,和框架代码中的设置相同:
3.4 驱动的测试
在树莓派下写一个测试驱动的C代码:
pin4_test.c:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
int main()
{
int driver_fd;
driver_fd = open("/dev/pin4",O_RDWR); //以可读可写打开的方式打开驱动
if(driver_fd < 0){
perror("fail to open driver file:");
}else{
printf("open driver file success!\n");
}
driver_fd = write(driver_fd,'a',1); //向驱动文件写一个字节
if(driver_fd < 0){
perror("fail to write to driver file:");
}else{
printf("write success!\n");
}
return 0;
}
编译然后运行:
1. gcc pin4_test.c -o pin4_test
2. sudo ./pin4_test
如果加了sudo还是没法运行,可以根据错误提示修改,可能需要给驱动文件一个执行权限:
sudo chmod 666 /dev/pin4 //666代表让所有用户都有所有权限
可见,运行成功,没有报错!此时还可以另开一个窗口输入“dmesg”查看内核打印的信息:
可见内核也按照框架代码中的printk成功打印了信息!驱动测试成功!
同时,结果也再次印证了:当用户在最上层对 驱动文件 调用C库的open函数后,最后的结果还是调用最底层 驱动文件里实现的open驱动函数