八、Linux——驱动认知

8.1 什么是驱动?

驱动就是对底层硬件设备的操作进行封装,并向上层提供函数接口。

设备分类:

linux系统将设备分成3类:字符设备、块设备、网络设备

  • **字符设备:指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后顺序。**字符设备是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台和LED设备等,字符设备驱动程序通常至少要实现open、close、read和write的系统调用,字符终端(/dev/console)和串口(/dev/ttyS0以及类似设备)就是两个字符设备,它们能很好的说明“流”这种抽象概念。
  • 块设备: 指可以从设备的任意位置读取一定长度数据的设备。块设备包括硬盘、磁盘、U盘和SD卡等。
  • 网络设备: 网络设备可以是一个硬件设备,如网卡; 但也可以是一个纯粹的软件设备,
    比如回环接口(lo).一个网络接口负责发送和接收数据报文。
    在这里插入图片描述

我们来举一个例子来说一下整体的调用过程:

  • 在上层我们调用 c语言 open函数 open(“/dev/pin4”,O_RDWR); 调用/dev下的pin4以可读可写的方式打开,对于上层open调用到内核时会发生一次软中断,中断号是0X80,从用户空间进入到内核空间。
  • open会调用到system_call(内核函数),system_call会根据/dev/pin4设备名,去找出你要的设备号
  • 再调到虚拟文件VFS (为了上层调用到确切的硬件统一化),调用VFS里的sys_open,sys_open会找到在驱动链表里面,根据主设备号和次设备号找到引脚4里的open函数,我们在引脚4里的open是对寄存器操作。

我们写驱动无非就是做添加驱动
添加驱动做哪些事呢?

  1. 设备名
  2. 设备号
  3. 设备驱动函数 (操作寄存器 来驱动 IO口)

综上所述
如果想要打开dev下面的pin4引脚,过程是:用户态调用open(“/de/pin4”,O_RDWR),对于内核来说,上层调用open函数会触发一个软中断(系统调用专用,中断号是0x80,0x80代表发生了一个系统调用),系统进入内核态,并走到system_call,可以认为这个就是此软中断的中断服务程序入口,然后通过传递过来的系统调用号来决定调用相应的系统调用服务程序(在这里是调用VFS中的sys_open)。sys_open会在内核的驱动链表里面根据设备名和设备号查找到相关的驱动函数(每一个驱动函数是一个节点),驱动函数里面有通过寄存器操控IO口的代码,进而可以控制IO口实现相关功能。

8.2 主次设备号

在linux中一切皆文件,各种的文件和设备(比如:鼠标、键盘、屏幕、flash、内存、网卡、如下图所示:)都是文件,那既然是文件了,就可以使用文件操作函数来操作这些设备

有一个问题,open、read等这些文件操作函数是如何知道打开的文件是哪一种硬件设备呢?
①在open函数里面输入对应的文件名,进而操控对应的设备。
②通过 设备号(主设备号和次设备号) 。除此之外我们还要了解这些驱动程序的位置,和如何实现这些驱动程序,每一种硬件设备对应不同的驱动(这些驱动有我们自己来实现)。Linux的设备管理是和文件系统紧密结合的,各种设备都以文件的形式存放在/dev目录下,称为 设备文件。

Linux的设备管理是和文件系统紧密结合的各种设备都以文件的形式存放在/dev目录下,称为 设备文件

应用程序可以打开、关闭和读写这些设备文件,完成对设备的操作,就像操作普通的数据文件一样。
为了管理这些设备,系统为设备编了号,每个设备号又分为主设备号 和 次设备号(如下图所示:)。
在这里插入图片描述

主设备号用来区分不同种类的设备,而次设备号用来区分同一类型的多个设备。
对于常用设备,Linux有约定俗成的编号,如硬盘的主设备号是3。
一个字符设备或者块设备都有一个主设备号和次设备号
主设备号和次设备号统称为设备号

主设备号用来表示一个特定的驱动程序。
次设备号用来表示使用该驱动程序的各设备。

例如一个嵌入式系统,有两个LED指示灯,LED灯需要独立的打开或者关闭。那么,可以写一个LED灯的字符设备驱动程序,可以将其主设备号注册成5号设备次设备号分别为1和2。这里,次设备号就分别表示两个LED灯。

驱动链表

管理所有设备的驱动,添加或查找
添加是发生在我们编写完驱动程序,加载到内核。
查找是在调用驱动程序,由应用层用户空间去查找使用open函数。

驱动插入链表的顺序由设备号检索,就是说主设备号和次设备号除了能区分不同种类的设备和不同类型的设备,还能起到将驱动程序加载到链表的某个位置,在下面介绍的驱动代码的开发无非就是添加驱动(添加设备号、设备名和设备驱动函数)和调用驱动。

补充:

  1. 每个系统调用都对应一个系统调用号,而系统调用号就对应内核中的相应处理函数。
  2. 所有系统调用都是通过中断0x80来触发的。
  3. 使用系统调用时,通过eax 寄存器将系统调用号传递到内核,系统调用的入参通过ebx、ecx……依次传递到内核
  4. 和函数一样,系统调用的返回值保存在eax中,所有要从eax中取出

8.3 字符设备驱动工作原理

  • 在Linux文件系统中,每个文件都用一个 struct inode结构体来描述,这个结构体记录了这个文件的所有信息,例如文件类型,访问权限等。
  • 在linux操作系统中,每个驱动程序在应用层的**/dev目录或者其他如/sys**目录下都会有一个文件与之对应。
  • 在linux操作系统中, 每个驱动程序都有一个设备号
  • 在linux操作系统中,每打开一次文件,Linux操作系统会在VFS层分配一个struct file结构体来描述打开的文件。

img

(1) 当open函数打开设备文件时,可以根据设备文件对应的struct inode结构体描述的信息,可以知道接下来要操作的设备类型(字符设备还是块设备),还会分配一个struct file结构体。

(2) 根据struct inode结构体里面记录的设备号,可以找到对应的驱动程序。这里以字符设备为例。在Linux操作系统中每个字符设备都有一个struct cdev结构体。此结构体描述了字符设备所有信息,其中最重要的一项就是字符设备的操作函数接口。

(3) 找到struct cdev结构体后,linux内核就会将struct cdev结构体所在的内存空间首地址记录在struct inode结构体i_cdev成员中,将struct cdev结构体中的记录的函数操作接口地址记录在struct file结构体的f_ops成员中。

(4) 任务完成,VFS层会给应用返回一个文件描述符(fd)。这个fd是和struct file结构体对应的。接下来上层应用程序就可以通过fd找到struct file,然后在struct file找到操作字符设备的函数接口file_operation了。

其中,cdev_init和cdev_add在驱动程序的入口函数中就已经被调用,分别完成字符设备与file_operation函数操作接口的绑定,和将字符驱动注册到内核的工作。

8.4 基于框架编写驱动

上层调用的代码(pin4test.c)

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

void main()
{
        int fd,data;
        fd = open("/dev/pin4",O_RDWR);
        if(fd<0){
                printf("open fail\n");
                perror("reson:");
        }
        else{
                printf("open successful\n");
        }
        fd=write(fd,'1',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;                //设备号,devno是用来接收创建设备号函数的返回值,销毁的时候需要传这个参数
static int major =231;  		   //主设备号
static int minor =0;			   //次设备号
static char *module_name="pin4";   //模块名

//led_open函数
static int pin4_open(struct inode *inode,struct file *file)
{
    printk("pin4_open\n");  //内核的打印函数和printf类似   
    return 0;
}

//led_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 = {
//static防止其他文件也有同名pin4_fops
//static限定这个结构体的作用,仅仅只在这个文件。
    .owner = THIS_MODULE,
    .open  = pin4_open,
    .write = pin4_write,
};
/*
上面的代码等同于以下代码(但是在单片机keil的编译环境里面不允许以上写法):
里面的每个pin4_fops结构体成员单独赋值
static struct file_operations pin4_fops;  
    pin4_fops.owner = THIS_MODULE;
    pin4_fops.open  = pin4_open;
    pin4_fops.write = pin4_write;
*/
//static限定这个结构体的作用,仅仅只在这个文件。


int __init pin4_drv_init(void)   //真实的驱动入口
{

    int ret;
    devno = MKDEV(major,minor);  //2. 创建设备号
    ret   = register_chrdev(major, module_name,&pin4_fops);  
    //3. 注册驱动  告诉内核,把这个驱动加入到内核驱动的链表中

    pin4_class=class_create(THIS_MODULE,"myfirstdemo");//由代码在dev下自动生成设备,创建一个类
    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);  //入口,内核加载驱动的时候,这个宏(不是函数)会被调用,去调用pin4_drv_init这个函数
module_exit(pin4_drv_exit);
MODULE_LICENSE("GPL v2");

上面的代码中能自动在dev下面生成设备,还可以手动进行生成**(并不建议这么做)**

sudo mknod 设备名 设备类型(b表示块设备 c表示字符设备 p表示命名管道) 主设备号 次设备号

删除手动创建的设备名直接rm就可以

在这里插入图片描述

驱动模块代码编译(模块的编译需要配置过的内核源码,编译、连接后生成的内核模块后缀为.ko,编译过程首先会到内核源码目录下,读取顶层的Makefile文件,然后再返回模块源码所在目录。)

在导入虚拟机的内核代码中找到字符设备驱动的那一个文件夹:/SYSTEM/linux-rpi-4.19.y/drivers/char将以上代码复制到一个文件中,然后下一步要做的是就是:将上面的驱动代码编译生成模块,再修改Makefile。(你放那个文件夹下,就改哪个文件夹下的Makefile)
(-y表示编译进内核,-m表示生成驱动模块,CONFIG_表示是根据config生成的)所以只需要将obj-m += pin4drive.o添加到Makefile中即可。

在这里插入图片描述

然后回/SYSTEM/linux-rpi-4.19.y下使用指令:ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make modules进行编译生成驱动模块。 然后将生成的.ko文件发送给树莓派

在这里插入图片描述

登陆树莓派后,加载内核驱动:使用指令:sudo insmod pin4drive.ko加载内核驱动(相当于通过insmod调用了module_init这个宏,然后将整个结构体加载到驱动链表中)

加载完成后就可以在dev下面看到名字为pin4的设备驱动(这个和驱动代码里面static char *module_name=“pin4”; //模块名这行代码有关),设备号也和代码里面相关

在这里插入图片描述

lsmod可以查看驱动已经装进去了

在这里插入图片描述

使用指令:sudo chmod 666 /dev/pin4为pin4赋予权限,让所有人都可以打开成功。

然后再次执行pin4test表面上看没有任何信息输出,其实内核里面有打印信息只是上层看不到
如果想要查看内核打印的信息可以使用指令:dmesg |grep pin4
如下图所示:表示驱动调用成功

在这里插入图片描述

在装完驱动后可以使用指令:sudo rmmod +驱动名(不需要写ko)将驱动卸载。

  • 为什么生成驱动模块需要在虚拟机上生成?树莓派不行吗?

    生成驱动模块需要编译环境(linux源码并且编译,需要下载和系统版本相同的Linux内核源代码),也可以在树莓派上面编译,但在树莓派里编译,效率会很低,要非常久

8.5 基于框架编写树莓派驱动

地址总线:

地址总线 (Address Bus;又称:位址总线) 属于一种电脑总线 (一部份),是由CPU 或有DMA 能力的单元,用来沟通这些单元想要存取(读取/写入)电脑内存元件/地方的实体位址。通俗来讲,就是cpu能够访问内存的范围。

用一个现象来解释地址总线:
装了32位的win7系统,明明内存条8G,可是系统只识别了3.8G(232 bit),装了64位,才能识别到8G。

物理地址:

物理地址是硬件实际地址或绝对地址

虚拟地址:

基于算法的地址[软件层面的地址:假地址])称为虚拟地址;虚拟地址做为程序员应该听说的最多,不管是在linux还是windows下编程,程序所操作地址都是虚拟地址。虚拟地址是硬件MMU与软件内存管理结合的产物,方便更高效率的使用RAM。

MMU:

在这里插入图片描述

内存分页其实就是我们所说的4G空间,内存的所有内存被操作系统内核以4G为每页划分开,当我们程序运行时会被加载到内存中的4G空间里,其实说是有4G其实并没有真正在的4G空间,4G空间中有一小部分被映射到了物理内存中,或者被映射到了硬盘的文件上(fopen),或者没有被映射,还有一部分在内存当中就会被划分栈,堆,其中有大片大片的内存是没有被映射的,同样物理内存也是被分页了用来与虚拟内存产生映射关系。将虚拟地址映射为物理地址有一个算法(页表)决定了将虚拟地址映射到物理地址的哪个位置,页表是通过MMU(分页内存管理单元)来管理的,就是设计完页表后通过MMU来执行将虚拟地址映射为物理地址。

查看一下树莓派的芯片手册
在这里插入图片描述

BCM2835是树莓派3B CPU的型号,是ARM-cotexA53架构,cpu Bus是地址总线,00000000~FFFFFFFF是CPU寻址的范围(4G)。DMA是高速拷贝单元,CPU可以发动DMA直接让DMA进行数据拷贝,直接内存访问单元。物理地址(PA)1G、虚拟地址(VA)4G
若程序大于物理地址1G,是不是就跑不了了,不是的,它有个MMU的单元,把物理地址映射成虚拟地址,我们操作的代码基本上都是在虚拟地址,它有一个映射页表(上面提及到过)

  • GPIO有41个寄存器,所有访问都是32位的。

  • Description是寄存器的功能描述。

    GPFSEL0(寄存器名)
    GPIO Function Select 0(功能选择:输入或输出);
    GPSET0 (寄存器名)
    GPIO Pin Output Set 0(将IO口置0);
    GPSET1(寄存器名)
    GPIO Pin Output Set 1(将IO口置1);
    GPCLR0(寄存器名)
    GPIO Pin Output Clear 0 (清0)下图的地址是:总线地址(并不是真正的物理地址)

在这里插入图片描述

在这里插入图片描述

例如 我们想把pin4(BCM引脚,并不是wiringPi库的引脚)配置成输出模式,就要把GPFSEL0这个寄存器的14-12位设置成 001

我们在编写驱动程序的时候,IO空间的起始地址是0x3f000000,加上GPIO的偏移量0x2000000,所以GPIO的物理地址应该是从0x3f200000开始的,然后在这个基础上进行Linux系统的MMU内存虚拟化管理,映射到虚拟地址上。

特别注意,BCM2708 和BCM2709 IO起始地址不同,BCM2708是0x20000000,BCM2709是0x3f000000,这是造成大部分人驱动出现“段错误”的原因。树莓派3B的CPU为BCM2709。

树莓派IO口驱动程序

#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";   //模块名

volatile unsigned int* GPFSEL0 = NULL;
volatile unsigned int* GPSET0   = NULL;
volatile unsigned int* GPCLR0   = NULL;
//这三行是设置寄存器的地址
//volatile的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值

//led_open函数
static int pin4_open(struct inode *inode,struct file *file)
{
        printk("pin4_open\n");  //内核的打印函数和printf类似
       
        //配置pin4引脚为输出引脚        
        *GPFSEL0 &=~(0x6 <<12); // 把bit13 、bit14置为0  
        //0x6是110  <<12左移12位 ~取反 &按位与
        *GPFSEL0 |=~(0x1 <<12); //把12置为1   |按位或
        
        return 0;

}
//read函数
static int pin4_read(struct file *file,char __user *buf,size_t count,loff_t *ppos)
{
        printk("pin4_read\n");  //内核的打印函数和printf类似

        return 0;
}

//led_write函数
static ssize_t pin4_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos)
{
        int usercmd;
        printk("pin4_write\n");  //内核的打印函数和printf类似
        
        //获取上层write函数的值                
        copy_from_user(&usercmd,buf,count); //将应用层用户输入的指令读如usercmd里面
        //根据值来操作io口,高电平或者低电平
        if(usercmd == 1){
                printk("set 1\n");
                *GPSET0 |= 0x01 << 4;
        }
        else if(usercmd == 0){
                printk("set 0\n");
                *GPCLR0 |= 0x01 << 4;
        }
        else{
                printk("undo\n");
        }
        return 0;
}

static struct file_operations pin4_fops = {

        .owner = THIS_MODULE,
        .open  = pin4_open,
        .write = pin4_write,
        .read  = pin4_read,
};

//static限定这个结构体的作用,仅仅只在这个文件。
int __init pin4_drv_init(void)   //真实的驱动入口
{
        int ret;
        devno = MKDEV(major,minor);  //创建设备号
        ret   = register_chrdev(major, module_name,&pin4_fops);  //注册驱动  告诉内核,把这个驱动加入到内核驱动的链表中
        pin4_class=class_create(THIS_MODULE,"myfirstdemo");//让代码在dev下自动>生成设备
        pin4_class_dev =device_create(pin4_class,NULL,devno,NULL,module_name);  //创建设备文件
        
        GPFSEL0=(volatile unsigned int *)ioremap(0x3f200000,4);
        GPSET0 =(volatile unsigned int *)ioremap(0x3f20001C,4);
        GPCLR0 =(volatile unsigned int *)ioremap(0x3f200028,4);

        printk("insmod driver pin4 success\n");
        return 0;
}

void __exit pin4_drv_exit(void)
{

        iounmap(GPFSEL0);
        iounmap(GPSET0);
        iounmap(GPCLR0); //卸载驱动时释放地址映射

        device_destroy(pin4_class,devno);
        class_destroy(pin4_class);
        unregister_chrdev(major, module_name);  //卸载驱动
}
module_init(pin4_drv_init);  //入口,内核加载驱动的时候,这个宏会被调用,去调用pin4_drv_init这个函数
module_exit(pin4_drv_exit);
MODULE_LICENSE("GPL v2");

test.c

#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include <unistd.h>
#include<stdlib.h>
#include<stdio.h>

int main()
{
   int fd;
   int cmd;
   int data;

   fd = open("/dev/pin4",O_RDWR);
   if(fd<0){
           printf("open failed\n");
   }else{
    printf("open success\n");
   }
   
   printf("input commnd:1/0 \n 1:set pin4 high \n 0 :set pin4 low\n");
   scanf("%d",&cmd);

   printf("cmd = %d\n",cmd);
   fd = write(fd, &cmd,4); //cmd类型是int  所以 写4
}

编译完驱动后要pin4这个设备可访问权限 sudo chmod 666 /dev/pin4

运行程序,输入0后运行结果如下

在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值