linux驱动(驱动编译、字符设备驱动框架、交叉编译树莓派驱动、树莓派驱动本地编译)

什么是驱动:

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

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

  • 字符设备:指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后顺序。字符设备是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台和LED设备等。
  • 块设备: 指可以从设备的任意位置读取一定长度数据的设备。块设备包括硬盘、磁盘、U盘和SD卡等。
  • 网络设备: 网络设备可以是一个硬件设备,如网卡; 但也可以是一个纯粹的软件设备, 比如回环接口(lo).一个网络接口负责发送和接收数据报文。

用户态:

  • 是指用户编写程序、运行程序的层面,用户态在开发时需要C的基础和C库,C库讲到文件,进程,进程间通信,线程,网络,界面(GTk)。C库(是linux标准库一定有):就是Clibary,提供了程序支配内核干活的接口,调用的open,read,write,fork,pthread,socket由此处封装实现,由写的应用程序调用,C库中的各种API调用的是内核态,支配内核干活。

内核态:

  • 用户要使用某个硬件设备时,需要内核态的设备驱动程序,进而驱动硬件干活,就比如之前文章里面所提到的wiringPi库,就是提供了用户操控硬件设备的接口,在没有wiringPi库时就需要自己实现wiringPi库的功能,就是自己写设备驱动程序。这样当我们拿到另一种类型的板子时,同样也可以完成开发。
  • 在linux中一切皆文件,各种的文件和设备(比如:鼠标、键盘、屏幕、flash、内存、网卡、如下图所示:)都是文件,那既然是文件了,就可以使用文件操作函数来操作这些设备。有一个问题,open、read等这些文件操作函数是如何知道打开的文件是哪一种硬件设备呢?①在open函数里面输入对应的文件名,进而操控对应的设备。②通过设备号(主设备号和次设备号)。除此之外我们还要了解这些驱动程序的位置,和如何实现这些驱动程序,每一种硬件设备对应不同的驱动(这些驱动有我们自己来实现)。
    在这里插入图片描述
  • Linux的设备管理是和文件系统紧密结合的,各种设备都以文件的形式存放在/dev目录下,称为设备文件。应用程序可以打开、关闭和读写这些设备文件,完成对设备的操作,就像操作普通的数据文件一样。为了管理这些设备,系统为设备编了号,每个设备号又分为主设备号和次设备号(如下图所示:)。主设备号用来区分不同种类的设备,而次设备号用来区分同一类型的多个设备。对于常用设备,Linux有约定俗成的编号,如硬盘的主设备号是3。 一个字符设备或者块设备都有一个主设备号和次设备号。主设备号和次设备号统称为设备号。主设备号用来表示一个特定的驱动程序。次设备号用来表示使用该驱动程序的各设备。例如一个嵌入式系统,有两个LED指示灯,LED灯需要独立的打开或者关闭。那么,可以写一个LED灯的字符设备驱动程序,可以将其主设备号注册成5号设备,次设备号分别为1和2。这里,次设备号就分别表示两个LED灯。
    在这里插入图片描述
  • 驱动链表:管理所有设备的驱动,添加或查找, 添加是发生在我们编写完驱动程序,加载到内核。查找是在调用驱动程序,由应用层用户空间去查找使用open函数。驱动插入链表的顺序由设备号检索,就是说主设备号和次设备号除了能区分不同种类的设备和不同类型的设备,还能起到将驱动程序加载到链表的某个位置,在下面介绍的驱动代码的开发无非就是添加驱动(添加设备号、设备名和设备驱动函数)调用驱动
  • 综上所述:如果想要打开dev下面的pin4引脚,过程是:用户态调用open(“/de/pin4”,O_RDWR),对于内核来说,上层调用open函数会触发一个软中断(系统调用专用,中断号是0x80,0x80代表发生了一个系统调用),系统进入内核态,并走到system_call,可以认为这个就是此软中断的中断服务程序入口,然后通过传递过来的系统调用号来决定调用相应的系统调用服务程序(在这里是调用VFS中的sys_open)。sys_open会在内核的驱动链表里面根据设备名和设备号查找到相关的驱动函数(每一个驱动函数是一个节点),驱动函数里面有通过寄存器操控IO口的代码,进而可以控制IO口实现相关功能。
  • system_call函数是怎么找到详细的系统调用服务例程的呢? 通过系统调用号查找系统调用表sys_call_table!软中断指令INT 0x80运行时,系统调用号会被放入 eax 寄存器中,system_call函数能够读取eax寄存器获取,然后将其乘以4,生成偏移地址,然后以sys_call_table为基址。基址加上偏移地址,就能够得到详细的系统调用服务例程的地址了!然后就到了系统调用服务例程了。

补充:

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

基于框架编写驱动代码:

  • 最简单的字符设备驱动框架:
#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 = {.owner = THIS_MODULE,.open  = pin4_open,.write = pin4_write,
};
/*
上面的代码等同于以下代码(但是在单片机的编译环境里面不允许以上写法):
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);  //创建设备号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);  //创建设备文件,先有上面那一行代码,创建一个类然后这行代码,类下面再创建一个设备。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 +设备名字 +设备类型(c表示字符设备驱动) +主设备号+次设备号 b : create a block (buffered) pecial file。 c, u: create a character (unbuffered) special file。 p: create a FIFO,删除手动创建的设备名直接rm就好。如下图所示:
    在这里插入图片描述

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

  • 使用下面的的代码:
#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";   //模块名//led_open函数
static int pin4_open(struct inode *inode,struct file *file)
{printk("pin4_open\n");  //内核的打印函数和printf类似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)
{printk("pin4_write\n");  //内核的打印函数和printf类似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);  //创建设备文件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");
  • 在导入虚拟机的内核代码中找到字符设备驱动的那一个文件夹:/SYSTEM/linux-rpi-4.19.y/drivers/char将以上代码复制到一个文件中,然后下一步要做的是就是:将上面的代码编译生成模块,就是修改Makefile这个文件。文件内容如下图所示:(-y表示编译进内核,-m表示生成驱动模块,CONFIG_表示是根据config生成的),所以只需要将obj-m += pin4drive.o添加到Makefile中即可。
    在这里插入图片描述
  • 然后使用指令:ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make modules进行编译生成驱动模块。然后将生成的.ko文件发送给树莓派:scp pin4drive.ko pi@192.168.43.136:/home/pi
  • 编译生成驱动模块会生成以下几个文件:
    在这里插入图片描述
  • .o的文件是object文件,.ko是kernel object,与.o的区别在于其多了一些sections,比如.modinfo。.modinfo section是由kernel source里的modpost工具生成的,包括MODULE_AUTHOR, MODULE_DESCRIPTION, MODULE_LICENSE, device ID table以及模块依赖关系等等。 depmod 工具根据.modinfo section生成modules.dep, modules.*map等文件,以便modprobe更方便的加载模块。
  • 编译过程中,经历了这样的步骤:先进入Linux内核所在的目录,并编译出pin4drive.o文件,运行MODPOST会生成临时的pin4drive.mod.c文件,而后根据此文件编译出pin4drive.mod.o,之后连接pin4drive.o和pin4drive.mod.o文件得到模块目标文件pin4drive.ko,最后离开Linux内核所在的目录。

操作驱动的上层代码(pin4test):

#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);
}
  • 将以上代码进行交叉编译后发送给树莓派,就可以看到pi目录下存在发送过来的.ko文件和pin4test这两个文件,如下图所示:
    在这里插入图片描述

  • 然后使用指令: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内核源代码),也可以在树莓派上面编译,但在树莓派里编译,效率会很低,要非常久。这篇文章有讲树莓派驱动的本地编译。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/272152.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

docker启动报错  (iptables failed: iptables --wait -t nat -A DOCKER -p tcp -d 0/0 --dport 9876 -j DNAT --

docker启动报错 : (iptables failed: iptables --wait -t nat -A DOCKER -p tcp -d 0/0 --dport 9876 -j DNAT --to-destination 172.17.0.2:9876 ! -i docker0: iptables: No chain/target/match by that name. 解决方案&#xff1a; systemctl restart docker

最详细的docker安装rocketMQ教程来了

RocketMQ是一款分布式、队列模型的消息中间件&#xff0c;是由阿里巴巴设计的&#xff0c;具有以下特点&#xff1a; 支持严格的消息顺序 支持Topic与Queue两种模式 亿级消息堆积能力 比较友好的分布式特性 同时支持Push与Pull方式消费消息 历经多次天猫双十一海量消息考验…

树莓派IO口驱动代码的编写、微机总线地址、物理地址、虚拟地址、BCM2835芯片手册

地址总线&#xff1a; 百度百科解释&#xff1a; 地址总线 (Address Bus&#xff1b;又称&#xff1a;位址总线) 属于一种电脑总线 &#xff08;一部份&#xff09;&#xff0c;是由CPU 或有DMA 能力的单元&#xff0c;用来沟通这些单元想要存取&#xff08;读取/写入&#xff…

夺命雷公狗---DEDECMS----26dedecms面包屑导航的实现

我们在很多项目里面都会用到面包屑导航&#xff0c;而dedecms里面也是给我们封装好面包屑导航的了,如下图所示&#xff1a; 在dede里面实现面包屑导航主要用到{dede:field.position/}标签&#xff0c;我们首先来修改下article_movie.htm内容页的模版文件&#xff1a; 我们修改成…

rust油桶用什么打_草莓用什么膨大素好?草莓膨大剂什么时间打?草莓用什么肥料膨大...

农资365公众号&#xff0c;了解更多生根、根腐、重茬、土传、枯黄萎、根烂病、防治根结线虫、微生物菌肥、膨大坐果、抗病增产的防治方法&#xff01;草莓含有丰富的营养&#xff0c;并且种植效益较高&#xff0c;其种植范围也比较广。草莓种植期间有很多因素影响草莓果实膨大&…

docker安装kafka,超级简单的

简介 kafka是一个分布式消息队列。具有高性能、持久化、多副本备份、横向扩展能力。生产者往队列里写消息&#xff0c;消费者从队列里取消息进行业务逻辑。一般在架构设计中起到解耦、削峰、异步处理的作用。 kafka对外使用topic的概念&#xff0c;生产者往topic里写消息&…

Linux中常见的环境变量笔记

1、变量&#xff1a;BASHBash Shell的全路径比如&#xff1a;echo $BASH2、变量&#xff1a;BASH_VERSIONBash Shell的版本号3、变量&#xff1a;EUID记录当前用户的UID。root用户值为0。4、FUNCNAME在用户函数体内部&#xff0c;记录当前函数体的函数名。5、变量&#xff1a;H…

消防信号二总线有没电压_春晓161#地块人防工程消防电源监控系统的设计与应用...

涂志燕安科瑞电气股份有限公司&#xff0c;上海 嘉定 201801&#xff1b;摘要&#xff1a;本文简述了消防设备电源的组成原理&#xff0c;分析了消防设备电源监控系统在应用中的设计依据和相关规范。通过安科瑞消防设备电源监控系统在春晓161#地块项目的实例介绍&#xff0c;阐…

大学慕课数据结构单元测试——华中科技大学

第一章绪论单元测试 一、单选(2分) 1、​___C__ 是数据的最小单位。 A.信息项 B.数据元素 C.数据项 D.表元素 2、​以下说法不正确的是 ___B___。 A.数据元素是数据的基本单位 B.数据项可由若干个数据元素构成 C.数据可由若干个数据元素构成 D.数据项是不可分割的最小…

RocketMQ同步刷盘和异步刷盘

刷盘机制 同步刷盘和异步刷盘 在broker配置文件里修改参数配置是同步还是异步

vim模式下报错E37: No write since last change No write since last change for buffer “ “

报错如下图所示&#xff1a; 网上的解决方法&#xff1a; 文件为只读文件&#xff0c;无法修改。使用命令:w!强制存盘即可在vim模式下&#xff0c;键入以下命令&#xff1a;:w&#xff01;存盘后在使用vim命令检查是否保存&#xff0c;如未保存&#xff0c;编辑后重复以上操作…

Linux中Shell中取消变量和特殊变量的笔记

1、取消变量取消变量也就是将变量从内存中释放出去&#xff0c;可以使用unset 后面加变量名即可&#xff0c;当然函数的释放同样可以采用该方式处理。比如&#xff1a;name"123"echo ${name}输出&#xff1a;123unset nameecho ${name}输出&#xff1a;#取消函数示例…

光华科技光刻胶_【收藏】6天5板!21只光刻胶概念(名单)“出炉”!

连板数量21家中迪投资5板&#xff0c;宁波联合 神驰机电 神马电力4板&#xff0c;汉缆股份 华盛昌 浙江鼎力3板&#xff0c;海航投资世联行 飞龙股份 安洁科技 京威股份 三丰智能 容大感光 晶瑞股份 奥飞数据 光大嘉宝 电子城博天环境 兆易创新 聚辰股份2板二、科技股&#xff…

20159302 《网络攻击与防范》第四周学习总结

本节学习内容为网络攻击环境的配置。在此过程中&#xff0c;我们至少需要一台靶机&#xff0c;一台攻击机。在此选用windows server 2000为靶机&#xff0c;kali系统为攻击机。 一、系统的安装 根据之前发布的kali系统的安装过程&#xff0c;依据此流程进行windows server的安装…

智能家居项目开发准备工作

智能家居功能细节拆分&#xff1a; 控制端支持语音设备的输入&#xff08;用到之前所学习的LD3320语音识别模块&#xff09;或者是socket客户端&#xff08;这个客户端可以是ftp项目的客户端也可以是Android的app&#xff09;&#xff0c;主控芯片是树莓派&#xff0c;既接收语…

catia曲面扫掠命令详解_Mastercam快捷键命令,附中英文功能讲解!值得收藏!

组合键式快捷键功能快 捷 键功能Alt 0设置Z向控制深度Alt 1设置绘制图形的颜色Alt 2设置当前层Alt 3与Alt 2功能相同Alt 4设置刀具面(Tplane)Alt 5设置绘图面(Cplane)Alt 6设置视图面(Gview)Alt A进入自动存文件对话框快 捷 键功能Alt B工具条的显示/关闭Alt C选择执…

RocketMQ中主从复制

生产者把消息发送到master&#xff0c;不会发送到slave 消费者可以从maste也可以从slave消费消息 如果master挂了&#xff0c;那就从slave消费数据&#xff0c;那么slave怎么拿到master中的数据 这就要用到主从复制

智能家居代码构建编写、简单工厂模式、树莓派摄像头视频监控功能实现

根据上一节内容的代码框架开始编写代码&#xff1a; 首先编写controlDevices.h这个头文件里面的代码&#xff0c;这个是设备工厂每一个结点的结构体类型&#xff0c;而且还要在这个头文件里面进行函数的声明&#xff0c;也就是创建的那些设备.c文件里面的函数&#xff08;为了…

Linux中Shell的命令替换用法笔记

命令替换主要是指将命令的标准输出值赋值给某个变量。命令替换属于Shell编程中非常重要的功能&#xff0c;需要熟悉掌握。 命令替换的方式 1、反引号:命令 2、$()&#xff1a;$(命令) 用法说明&#xff1a;date1date #将date命令值给date1变量 date2$(date) #将date命令值给dat…