当前位置: 首页 > news >正文

Linux驱动开发初识

Linux驱动开发初识

文章目录

  • Linux驱动开发初识
    • 一、驱动的概念
      • 1.1 什么是驱动:
      • 1.2 驱动的分类:
    • 二、设备的概念
      • 2.1 主设备号&次设备号:
      • 2.2 设备号的作用:
    • 三、设备驱动整体调用过程
      • 3.1 上层用户操控设备的流程:
      • 3.2 Linux驱动的运行方式:
    • 四、基于框架编写驱动代码
      • 4.1 基本字符设备驱动框架:
      • 4.2 驱动代码的编译:
      • 4.3 驱动的加载&卸载:
      • 4.4 驱动的测试:
    • 五、树莓派IO口驱动的编写
      • 5.1 BCM2835芯片手册导读:
      • 5.2 Pin4引脚定位:
      • 5.3 根据驱动框架编写树莓派Pin4引脚驱动:
      • 5.4 编译测试Pin4引脚驱动:

一、驱动的概念

1.1 什么是驱动:

Linux内核驱动:是指一段代码,这段代码可以驱动底层硬件,即驱动就是对底层硬件设备的操作进行封装,并向上层提供函数接口。

1.2 驱动的分类:

Linux驱动分为三个基础大类:字符设备驱动块设备驱动网络设备驱动

  • 字符设备(Char Device):
  1. 字符设备是个能够像字节流(类似文件)一样被访问的设备
  2. 对字符设备发出读/写请求时,实际的硬件I/O操作一般紧接着发生
  3. 字符设备驱动程序通常至少要实现openclosereadwrite系统调用
  4. 比如我们常见的lcd、触摸屏、键盘、led、串口等等,他们一般对应具体的硬件都是进行出具的采集、处理、传输
  • 块设备(Block Device)
  1. 一个块设备驱动程序主要通过传输固定大小的数据(一般为512或1k)来访问设备
  2. 块设备通过buffer cache(内存缓冲区)访问,可以随机存取,即:任何块都可以读写,不必考虑它在设备的什么地方
  3. 块设备可以通过它们的设备特殊文件访问,但是更常见的是通过文件系统进行访问
  4. 只有一个块设备可以支持一个安装的文件系统
  5. 比如我们常见的电脑硬盘、SD卡、U盘、光盘等
  • 网络设备(Net Device)
  1. 任何网络事务都经过一个网络接口形成,即一个能够和其他主机交换数据的设备
  2. 访问网络接口的方法仍然是给它们分配一个唯一的名字(比如eth0),但这个名字在文件系统中不存在对应的节点
  3. 内核和网络设备驱动程序间的通信,完全不同于内核和字符以及块驱动程序之间的通信,内核调用一套和数据包传输相关的函(socket函数)而不是readwrite
  4. 比如我们常见的网卡设备、蓝牙设备

在这里插入图片描述

二、设备的概念

  • 在学习驱动和其开发之前,首先要知道所谓驱动,其对象就是设备

2.1 主设备号&次设备号:

在Linux中,各种设备都以文件的形式存在**/dev目录下**,称为设备文件最上层的应用程序可以打开,关闭,读写这些设备文件,从而完成对设备的操作

为了管理这些设备,系统为设备编了号,每个设备都拥有主设备号次设备号主设备号用于区分不同种类的设备,而次设备号用于区分同一类型的多个设备。(对于常用的设备如硬盘,Linux赋予的主设备号一般是3)

  • 在**/dev目录下输入ls -l**,就可以看到设备文件对应的主次设备号:

在这里插入图片描述

2.2 设备号的作用:

在了解了什么是主次设备号之后,就要了解设备号的用处:

  • 用户态中:当用户调用了如open, read, write等函数想要操作设备文件时,需要两个参数,第一个是文件名,第二个就是设备号
  • 内核态中:存在着一个驱动链表,用于管理所有设备的驱动,而驱动在链表中的位置就由设备号来检索

三、设备驱动整体调用过程

3.1 上层用户操控设备的流程:

  1. C语言上层调用open函数。open(“/dev/pin4”,O_RDWR);调用/dev下的pin4以可读可写的方式打开。对于上层open调用到内核时会发生一次软中断中断号是0X80,从用户空间进入到内核空间

  2. open会调用到system_call(内核函数)system_call会根据/dev/pin4设备名,去找出需要的设备号。

  3. 再调到虚拟文件VFS ,调用VFS里的sys_opensys_open会找到在驱动链表里面,根据主设备号和次设备号找到引脚4里的open函数,引脚4里的open是对寄存器操作及对硬件的操作

在这里插入图片描述

3.2 Linux驱动的运行方式:

  1. 驱动编译进 Linux 内核中,当 Linux 内核启动的时就会自动运行驱动程序
  2. 驱动编译成模块(Linux 下模块扩展名为.ko),并在Linux 内核启动以后使用相应命令加载驱动模块

四、基于框架编写驱动代码

4.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协议

4.2 驱动代码的编译:

  • 进入Linux源码树目录下的驱动目录,因为驱动的是字符设备,所以进入的是驱动目录下的char目录。/home/shiyahao/SYSTEM/linux-rpi-4.19.y/drivers/char

在这里插入图片描述

  • 在这个路径下创建一个新的C文件:pin4driver.c,内容为我们刚刚的字符设备驱动:

在这里插入图片描述

在这里插入图片描述

  • 修改当前路径(字符设备驱动)下的Makefile,确保这个新的驱动会被编译到:

在这里插入图片描述

  • 回到linux内核源码的路径,运行以下指令尝试编译:
ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make modules
  • 将编译好的驱动模块传到树莓派中:
scp ./drivers/char/pin4driver.ko pi@192.168.31.123:/home/pi

在这里插入图片描述

4.3 驱动的加载&卸载:

由于现在刚刚把驱动编译成了.ko的模块,所以需要运行以下指令来加载驱动模块:

sudo insmod pin4driver.ko		//加载驱动模块
sudo rmmod  pin4driver.ko 		//卸载驱动模块,此时驱动名字后不用加".ko"

运行成功后,就可以在**/dev**下看到生成的设备文件“pin4”了:

在这里插入图片描述

使用ls -l指令查看这个设备的主设备号&次设备号,和框架代码中的设置一样:

在这里插入图片描述

给pin驱动加权限:

sudo chmod 666 /dev/pin4

4.4 驱动的测试:

在树莓派下写一个测试驱动的C代码:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{int fd;fd = open("/dev/pin4", O_RDWR);                 //打开GPIO4口设备文件if(fd < 0){printf("open pin4 failed\n");}else{printf("open pin4 success\n");}write(fd, "1", 1);                              //输出高电平return 0;
}

执行测试程序后用dmesg 查看内核打印信息发现打印了驱动函数的信息:

在这里插入图片描述

可见内核也按照框架代码中的printk成功打印了信息!驱动测试成功!

同时,结果也再次印证了:当用户在最上层对 驱动文件 调用C库的open函数后,最后的结果还是调用最底层 驱动文件里实现的open驱动函数

五、树莓派IO口驱动的编写

​ 前面我们通过一个基本的字符设备驱动框架来测试了驱动的运行,但是在“pin4_open”和“pin4_write”这两个驱动函数的函数体里只写了一句内核打印的代码,作为一个真正的驱动文件这显然是不够的。

​ 同时,在之前就提到过,驱动位于内核态的最底层,其下方就直接是硬件,所以驱动函数的目标就是直接操控硬件,也就是直接操控寄存器。在我的pin4驱动函数中应该添加的也就是根据函数功能,操作寄存器从而实现I/O口操控的代码。

5.1 BCM2835芯片手册导读:

明确了目标后,就产生了这个问题:我怎么知道应该使用哪些寄存器,又应该怎么使用呢?

答案是:根据开发平台的芯片手册/电路图来找到具体的描述由于我是在树莓派3B上玩驱动的开发,所以我应该查阅这款树莓派的芯片,也就是BCM2835的芯片手册。

此处我只使用了芯片手册就定位了寄存器,而没有用电路图,原因是树莓派的这个芯片手册已经把用什么寄存器写的很清楚了

在BCM2835芯片手册的第六章描述了General Purpose I/O (GPIO)外设相关寄存器。这里驱动pin4引脚需要用到的寄存器有:

  1. GPIO Function Select Registers (GPFSELn) 功能选择寄存器

    在这里插入图片描述

    该寄存器共有五组,每个寄存器都有32位,以GPIO Alternate function select register 0为例,其中:
    29-0位 :每三位对于一个引脚,比如29-27对应的是GPIO Pin 9,26-24对应的是GPIO Pin 8,且这三位取不同的值代表该三位对应的引脚选择不同的功能。比如,当29-27位为000时表示GPIO Pin 9是输入功能,29-27位为001时表示GPIO Pin 9是输出的功能。

  2. GPIO Pin Output Set Registers (GPSETn) 置位寄存器

    在这里插入图片描述

    该寄存器共两组,每个寄存器都有32位,将寄存器某一位置1即将对应的引脚置1。

  3. GPIO Pin Output Clear Registers (GPCLRn) 清0寄存器

    与置位寄存器用法一至,将对应位数引脚置0。

  4. 所需寄存器的地址说明:

​ 在编写驱动程序时,IO空间的起始地址位0X3F000000,加上GPIO的偏移量0X200000,因此GPIO的物理地址是从0X3F200000开始的,而编程所需的地址是虚拟地址,需要通过MMU内存虚拟化管理将地址映射到虚拟地址上。

5.2 Pin4引脚定位:

Pin4引脚指的是BCM4号,对应WiringPi库第7号,物理引脚的7脚:

在这里插入图片描述

在这里插入图片描述

5.3 根据驱动框架编写树莓派Pin4引脚驱动:

#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关键字
volatile unsigned int *GPFSEL0 = NULL;
volatile unsigned int *GPSET0 = NULL;
volatile unsigned int *GPCLR0 = NULL;static int pin4_open(struct inode *inode,struct file *file)
{printk("pin4_open\n");  //内核的打印函数和printf类似//配置引脚4的寄存器,将其配置为输出模式,即将GPFSEL0寄存器的第14-12位配置成001*GPFSEL0 &= 0XFFFF9FFF;  //将第14,13位置0*GPFSEL0 |= 0X00001000; //将第12位置1return 0;
}static ssize_t pin4_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos)
{int usercmd;printk("pin4_write\n");copy_from_user(&usercmd,buf,count);//获取应用层write函数写入的内容if(usercmd == 1){printk("set 1\n");*GPSET0 |=(0x1 << 4); //将Pin4引脚置1}else if (usercmd == 0){printk("set 0\n");*GPCLR0 |=(0X1 << 4);//将Pin4引脚置0}else{printk("undo\n");}return 0;
}static struct file_operations pin4_fops = {.owner = THIS_MODULE,.open  = pin4_open,//当应用层调用open函数时,内核会调用pin4_open..write = pin4_write,//当应用层调用write函数时,内核会调用pin4_write.
};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);//需要将物理地址映射位虚拟地址 ipremap第一个参数需要被映射的物理地址。第二个参数位映射的字节数GPSET0  = (volatile unsigned int *)ioremap(0X3f20001C,4);//通过芯片手册可以看到该寄存器在基础地址上偏移了1CGPCLR0  = (volatile unsigned int *)ioremap(0X3f200028,4);//通过芯片手册可以看到该寄存器在基础地址上偏移了28return 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);  //入口
module_exit(pin4_drv_exit);
MODULE_LICENSE("GPL v2");

然后在树莓派上编写测试代码:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char const *argv[])
{int fd,cmd;fd = open("/dev/pin4",O_RDWR);printf("input 0 ro 1 , 0 :Pin4 Set 0,1:Pin4 Set 1\n");scanf("%d",&cmd);printf("cmd = %d \n",cmd);write(fd,&cmd,1);return 0;
}

5.4 编译测试Pin4引脚驱动:

  • 将驱动代码编译后生成驱动模块放置在树莓派上进行测试:
ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make modules

在这里插入图片描述

  • 将生成的驱动模块拷贝至树莓派:
scp ./drivers/char/pin4driver.ko pi@192.168.31.123:/home/pi
  • 在树莓派上安装驱动并给驱动权限:
sudo insmod pin4driver.ko
sudo chmod 666 /dev/pin4

运行测试程序:
在这里插入图片描述

在这里插入图片描述


http://www.mrgr.cn/news/35516.html

相关文章:

  • Docker 命令总结:从入门到入土
  • 万字长文解读深度学习——ViT、ViLT、DiT
  • 七大经典基于比较排序算法【Java实现】
  • Linux(CentOS)安装 Nginx
  • Anacondan安装opencv
  • node.js安装和配置教程
  • mysql学习教程,从入门到精通,SQL RIGHT JOIN语句(24)
  • Robot Operating System——多边形数据
  • [大语言模型-论文精读] Diffusion Model技术-通过时间和空间组合扩散模型生成复杂的3D人物动作
  • Thread , ThreadLocal , ThreadLocalMap , Entry 之间的关系?
  • 宝塔部署vue项目出现的各种问题
  • 【算法】模拟:(leetcode)6.Z 字形变换(medium)
  • 光子架与电子架 -- 主从子架
  • 小程序面板开发教程|开发照明 Matter 面板步骤(一)
  • WebGL阴影与后期处理
  • Taro多端统一开发解决方案
  • 多线程:死锁
  • 从 Oracle 集群到单节点环境(详细记录一次数据迁移过程)之二:生产服务器的备份操作
  • 前端读取PDF和DOCX文件(干货分享)
  • 【C++】Eclipse技巧汇总
  • ATTCK实战系列-Vulnstack靶场内网域渗透(二)
  • [docker][软件]docker快速安装rabbitmq
  • 每日一练:二叉树的层序遍历
  • 并发编程。
  • 【学习复盘】相关资料
  • QT 自定义可拖动缩放的无边框窗口,可用于mainmindow, widget