一个字符设备或者块设备都有一个主设备号和次设备号。主设备号和次设备号统称为设
备号。主设备号用来表示一个特定的驱动程序。次设备号用来表示使用该驱动程序的各
设备。
主设备号用来区分不同种类的设备(华为),而次设备号用来区分同一类型的多个设备(华为mate40)。
从用户态空间贯穿到底层驱动
Linux系统中对于文件的所有操作离不开三个通用的API函数:open、read、wirte,
例如使用函数open打开一个引脚4文件 pin4 到与底层硬件交互时,一共会经过以下三步:
- 在用户态空间调用函数 OPEN 触发进程 open("/dev/pin4",O_RDWR),产生一个软中断(中断号
0x80)进入内核空间; - 内核会调用函数 sys_call(System call interface),从sys_call到sys_open中间实际上经过一层虚拟文件系统VFS,做到了不同文件系统但有了统一的接口, 根据设备的文件名在内核的驱动链表中寻找设备的设备号(分为主设备号、次设备号);
- 在寻找到设备号之后调用函数sys_open,再调用对应的驱动文件里的open,最终实现寄存器的电平操作。「在最终的硬件动作方面,单片机与Linux本质上是一样的
注意:
- 内核代码的编写与上层代码的编写有不同之处。内核里的文件代码是一个异常巨大的结构,若你希望在内核里的驱动链表里添加驱动文件,那就必须遵循链表的操作规则,即
驱动框架。 - 上层与底层的代码实际上是一一对应的,上层opne("/dev/pin4",O_RDWR)底层一定有pin4_open,操作函数read、write同理。
- 对于其他底层的驱动文件也是类似的,open、read、write都是被放在大的结构体之后才加入到驱动链表中。
- 不同于系统的上层文件,底层的驱动框架文件中对于变量的声明有大量的static。之所以大量充斥着该函数,是因为整个内核文件数量庞大足足有上万个之多,容易与其他文件的函数命名冲突,函数static限定了变量的作用域仅在该文件中。
驱动
两个部分,第一部分为驱动的加载部分,第二部分为驱动加载完成后,驱动的使用时的调用过程。
驱动加载:
1、将驱动源代码编译后,生成ko文件,这是将要加载的驱动模块。2、调用命令insmod加载模块,首先会找代码里边固定的宏moudle_init()来找到驱动中的初始化函数这里是s5pv210_led_init()和退出函数s5pv210_led_exit()。
3、调用s5pv210_led_init()来进行设备号注册,和设备添加。MKDEV是一个宏,可以通过移位把主设备号和次设备号进行处理,生成一个32位的数据。调用register_chrdev_region()注册设备号,linux驱动根据散列hash表来建立设备描述cdev结构体的索引,当hash表的index冲突时,采用链表的方式避免冲突,这样可以通过设备号快速找到cdev结构体的地址。
4、调用cdev_init()来初始化结构体cdev,最重要的是将file_operations保存在cdev中,file_oerations里边有本地实现的open release ioctl等具体功能的函数指针,这样可以在使用驱动的时候找到相应的实现函数。
5、调用cdev_add()来添加设备结构体cdev到hash表中,根据参数设备号可以找到注册设备号时在hash表中的位置,然后将cdev结构体地址添加进去
6、映射io端口,即映射io端口的物理地址为虚拟地址,因为在系统运行时,外设的I/O内存资源的物理地址是已知的,由硬件的设计决定,可以在数据手册中找到。但是CPU通常并没有为这些已知的外设I/O内存资源的物理地址预定义虚拟地址范围,驱动程序并不能直接通过物理地址访问I/O内存资源,而必须将它们映射到核心虚地址空间内,然后才能根据映射所得到的核心虚地址范围,通过访问内存指令访问这些I/O内存资源。
7、使用命令mknod 添加/dev目录下设备描述文件,其实主要就是描述了我们输入的三个参数,首先c代表字符型设备,500代表主设备号,0代表次设备号。三个参数的用法在下边的流程描述。
8、调用rmmod命令后,卸载驱动,找到驱动中moudle_exit()宏来找到卸载驱动的退出函数,在这个里边调用cdev_del()和unregister_chrdev_region()删除设备并且去掉设备号的注册,相反的过程,不用赘言。
驱动使用:
1、在驱动测试文件中,首先打开了/dev目录下的设备文件,但是这个文件只是设备基本信息的描述,没有实质的动作,具体的作用可以看作为设备的索引,通过打开文件可以找到设备驱动的位置。这里分析我们输入的三个参数,c表示字符型设备open系统调用中拿到c就知道要去找字符型设备的结构体。主设备号和次设备号用来索引hash散列表,找到cdev结构体的地址,而cdev中保存有file_operations的地址,就可以找到驱动的具体实现函数,就是驱动加载的逆过程。而open执行完之后,返回一个文件描述符,这个描述符中就带有找到的cdev地址。
2、Open函数根据得到的cdev找到file_operations 中的.open对应的的函数指针,调用这个函数来初始化驱动,这里可以做io端口控制寄存器的设置,将相关的端口设置为输出。
3、调用ioctl通信,ioctl是io管道管理函数,是linux系统封装用来给驱动用的通信函数,方便用户使用,不用关心通信的实现方式,也不用考虑通信是否跨线程或者跨进程,可以看作是一个通道,在使用时塞入数据,在驱动里边写好拿出数据并做相应的处理及可以,感觉非常像socket套接字。
4、测试文件中调用ioctl传入数据,ioctl根据传入的文件描述符参数中的cdev找到file_operations 中的. unlocked_ioctl对应的的函数指针,在这个函数里边调用copy_from_user(),可以取出传入的参数,根据参数做驱动对应的动作。
5、退出驱动后,跟2-4同样的道理找到.release执行,释放驱动。
学习链接:https://blog.csdn.net/weixin_46959681/article/details/117962761
上层空间到底层内核驱动的逻辑调用
修改引脚驱动文件 pin4Driver.c(GPIO)
演示代码: raspberryGPIO.c
#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"; //模块名
//函数volatil确保指令不被编译器优化,且要求每次直接读取寄存器里的器。
volatile unsigned int* GPFSEL0 = NULL;
volatile unsigned int* GPSET0 = NULL;
volatile unsigned int* GPCLR0 = NULL;
//pin4_open函数
static int pin4_open(struct inode *inode,struct file *file)
{
printk("pin4_open\n");
*GPFSEL0 &= ~(0x6 << 12);//将寄存器12-14位置001
*GPFSEL0 |= (0x1 << 12);//将寄存器12位置1
return 0;
}
//函数pin4_write
static ssize_t pin4_write(struct file *file1,const char __user *buf,size_t count, loff_t *ppos)
{
int cmd;
printk("pin4_write\n");
//函数copy_from_user —— 获取上层应用空间的数据。
copy_from_user(&cmd,buf,count);
if(cmd == 1){
printk("set 1.\n");
*GPSET0 |= 0x1 << 4;//将引脚4设置为1
}else if(cmd == 0){
printk("set 0.\n");
*GPCLR0 |= 0x1 << 4;//将引脚4设置为0
}else{
printk("error.\n");
}
printk("%d\n",cmd);
return 0;
}
static struct file_operations pin4_fops = {
.owner = THIS_MODULE,
.open = pin4_open,
.write = pin4_write,
};
int __init pin4_drv_init(void) //真实驱动入口
{
int ret;
printk("insmod driver pi4 success.\n");
//1.创建主设备、次设备号。
devno = MKDEV(major,minor);
printk("drive init succeed\n");
//2.注册驱动,告诉内核,把这个驱动加入到驱动链表。
ret = register_chrdev(major,module_name,&pin4_fops);
//3.加载驱动代码文件时自动操作路径“/root/dev”下生成驱动设备。
pin4_class = class_create(THIS_MODULE,"myfirstdriver");
//4.创建设备文件
pin4_class_dev = device_create(pin4_class,NULL,devno,NULL,module_name);
//函数 ioremap —— 将IO的物理地址映射内存的虚拟地址。
GPFSEL0 = (volatile unsigned int *)ioremap(0x3f200000,4);
GPSET0 = (volatile unsigned int *)ioremap(0x3f20001C,4);
GPCLR0 = (volatile unsigned int *)ioremap(0x3f200028,4);
return 0;
}
void __exit pin4_drv_exit(void)
{
//函数iounmap —— 与装载驱动的IO映射相对应,卸载驱动时必须解除映射。
iounmap(GPFSEL0);
iounmap(GPSET0);
iounmap(GPCLR0);
//1.销毁设备
device_destroy(pin4_class,devno);
//2.销毁类文件
class_destroy(pin4_class);
//3.卸载驱动
unregister_chrdev(major, module_name);
}
module_init(pin4_drv_init);
module_exit(pin4_drv_exit);
MODULE_LICENSE("GPL v2");
修改上层应用的测试代码 pin4Test2.c
上层测试代码: pin4Test2.c
#include <stdio.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int fd;
int cmd;
fd = open("/dev/pin4",O_RDWR);
if(fd < 0){
perror("Open failed:");
}else{
printf("Open success.\n");
}
printf("Please input number 0/1, 0 - Low Level, 1 - high Level.\n");
scanf("%d\n",&cmd);
if(cmd == 0){
printf("pin4 is low level.\n");
}
if(cmd == 1){
printf("pin4 is high level.\n");
}
write(fd,&cmd,4);
return 0;
}
编译两个文件
- 引脚驱动文件编译。 将引脚的驱动文件放在系统的字符驱动文件目录下
home/xxx/linux-rpi-4.14.y/drivers/char
,直接 vi 修改引导配置文件Makefile,在文件中直接插入字段obj-m += pin4Driver.o
(前者代表编译的方式,后者代表编译生成的文件。) - 模块化编译。 在第一步的前提下跳转回到系统源码目录下
home/xxx/linux-rpi-4.14.y/
,输入指令进行交叉编译:ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-KERNEL=kernel7 make -j4 modules
。等待编译完成后再次打开字符设备驱动文件,编译成功则目录底下会生成新的文件pin4Driver.ko。“j4”表示使用4个核进行编译工作,“modules”表示生成的驱动模块 - 上层应用测试代码编译。上层的文件处理比较简单,直接对其进行交叉编译处理即可,输入指令:
arm-linux-gnueabihf-gcc pin4Test.c -o pin4Test
。
在树莓派内加载驱动
- 驱动加载
输入指令:sudo insmod pin4Driver.ko
。查看驱动文件是否加载成功有两个方面:
输入指令:ls /dev/pin4 -l
,查看设备文件下是否有设备文件;
输入指令:lsmod
,查看内核是否挂载设备驱动文件pin4Driver.ko
。 - 设备权限设置
输入指令:sudo chmod 666 /dev/pin4
【666 ,表示所有人都可以访问该设备。】 - 测试
在树莓派主目录 pi@home 下运行文件./pin4Test
。注意,此时上层界面是没有任何显示的,真正的运行现场是在内核空间。输入指令dmesg | grep pin4
,可以查看内核中调用函数 printk() 成功,完成了从最上层到最底层的逻辑调用。
树莓派gpio readall
查看引脚4的模式,电平变化。