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.xdnf.cn/news/150490.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

[译] K8s和云原生

本篇内容是根据2019年8月份Kubernetes and Cloud Native音频录制内容的整理与翻译, Johnny 和 Mat 与 Kris Nova 和 Joe Beda 一起探讨了 Kubernetes 和云原生。他们讨论了 Kubernetes 推动的“云原生”应用的兴起、使用 Kubernetes 的合适场合、运行如此大型的开源项目所面临…

云服务器(华为云)安装java环境。

这篇文章主要是介绍如何搭建华为云服务器中的java环境&#xff0c;也就是jdk的安装。 这里华为云服务器使用的是liunx系统。 uname -a Linux操作系统的版本信息。具体来说&#xff0c;它表明使用的是Ubuntu系统&#xff0c;内核版本是5.15.0&#xff0c;构建于2023年1月20日&a…

linux配置git

一、生成新的 SSH 密钥 ssh-keygen -t rsa -b 4096 -C "your_emailexample.com" 按照提示操作&#xff1a; 当提示 Enter file in which to save the key (/root/.ssh/id_rsa): 时&#xff0c;直接按回车键使用默认路径。 当提示 Enter passphrase (empty for no p…

基于Java+Jsp+SpringMVC漫威手办商城系统设计和实现

基于JavaJspSpringMVC漫威手办商城系统设计和实现 &#x1f345; 作者主页 网顺技术团队 &#x1f345; 欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; &#x1f345; 文末获取源码联系方式 &#x1f4dd; &#x1f345; 查看下方微信号获取联系方式 承接各种定制系统 &am…

pycharm下载selenium等软件包时提示下载超时

1.问题描述 我今天在pycharm运行刚写的自动化脚本时&#xff0c;提示selenium模块未导入&#xff08;自动到导入&#xff09;&#xff0c;鼠标移动到【from selenium import webdriver]的selenium时&#xff0c;显示【未存在文档】 2 解决办法 文件--设置--项目&#xff1a;当前…

手写SpringMVC(简易版)

在上一篇博客中说到这里我们要进行手写SpringMVC&#xff0c;因此最好是将上一篇博客中的SpringMVC源码分析那一块部分搞懂&#xff0c;或者观看动力节点老杜的SpringMVC源码分析再来看这里的书写框架。 首先我们要知道对于一个完整系统的参与者&#xff08;即一个完整的web项…

CentOS 安装 JAVA环境(JDK 1.8)

镜像选择 推荐国内镜像直接下载 清华镜像 https://mirrors.tuna.tsinghua.edu.cn/Adoptium 关于重命名 AdoptOpenJDK 镜像为 Adoptium 的通知 编程宝库 http://www.codebaoku.com/jdk/jdk-index.html 这个镜像站&#xff0c;包含Oracle JDK、OpenJDK、AdoptOpenJDK、阿里…

Android平台使用VIA创建语音交互应用

Android平台使用VIA创建语音交互应用 概述 在 Android 平台上开发一款语音助手应用需要整合多种技术,包括语音识别(ASR)、文字转语音(TTS)、以及热词检测(Hotword Detection)。这些技术共同构成了语音助手应用的核心交互方式,使用户能够通过语音命令与设备进行无缝交…

Maya学习笔记:物体的层级关系

文章目录 父子关系设置父子关系同时显示两个大纲视图 组 父子关系 设置父子关系 设置父子物体&#xff1a; 方法1 先选择子物体&#xff0c;按住shift再选中父物体&#xff0c;按P或者G键 方法2 在大纲视图中按住鼠标中间&#xff0c;拖动一个物体到另一个物体上 取消父子关…

公安局软件管理平台建设方案和必要性,论文-———未来之窗行业应用跨平台架构

一、平台方略 由于csdn拦截关键信息&#xff0c;我发发布方案&#xff0c;请留意后面文章

Oracle逻辑备份脚本【生产环境适用】

1 说明 从Oracle10g开始&#xff0c;引入了数据泵&#xff08;Data Pump&#xff09;&#xff0c;是一种高效的数据传输工具&#xff0c;它通过导出&#xff08;Export&#xff09;和导入&#xff08;Import&#xff09;的方式帮助用户迁移数据。 在Oracle的产品设计中&#…

详解机器学习经典模型(原理及应用)——K-Means

一、K-Means算法概念 K-Means 算法是一种经典的聚类分析方法&#xff0c;属于无监督学习的一种。它的目标是将数据集中的样本划分为预定数量的簇&#xff0c;使得簇内的样本尽可能相似&#xff0c;而簇间的样本尽可能不同。K-Means在业务中也有诸多用途&#xff0c;比如在进行探…

Github + Hexo + Shoka搭建个人博客以及遇到的部分问题

博客预览&#xff1a; 主页&#xff1a; 文章&#xff1a; 博客语言链接&#xff1a; 全部分类 |mmjon 不在能知&#xff0c;乃在能行 Shoka官方博客&#xff1a; Yume Shoka 優萌初華 有夢書架 (lostyu.me) 1、准备 1、github账号 &#xff1a;自行去github官网注册…

睡眠监测系统基于边缘计算和微服务缓存

这篇论文的主要内容是关于基于边缘计算和微服务缓存的睡眠监测系统。以下是详细内容概述&#xff1a; 标题 睡眠监测系统基于边缘计算和微服务缓存 作者 Nico Surantha - 东京市立大学&#xff0c;日本David Jayaatmaja - 雅加达Bina Nusantara大学&#xff0c;印度尼西亚S…

Java面向对象(类和对象)(自己学习整理的资料)

目录 一.面向对象思想 二.类和对象 三&#xff1a;定义类的步骤 四.创建对象 五.用Java代码写一个简单的登录系统 练习 六.关于类的方法 七.类的无参无返回值方法 八.方法的返回值 练习 关于方法调用问题 九.全局变量和局部变量 十.笔记 一.面向对象思想 就只关注参…

FDA辅料数据库在线免费查询-药用辅料

在药物制剂的研制过程中&#xff0c;需要确定这些药用辅料的安全用量。而美国食品药品监督管理局&#xff08;FDA&#xff09;的辅料数据库&#xff08;IID&#xff09;提供了其制剂研发中的关键参考资源&#xff0c;使得更多的医药研发相关人员及企业单位节省试验环节及时间成…

快速学会一个算法,BERT

今天给大家介绍一个强大的算法模型&#xff0c;BERT BERT&#xff08;Bidirectional Encoder Representations from Transformers&#xff09;是一种基于 Transformer 架构的深度学习模型&#xff0c;主要用于处理自然语言处理&#xff08;NLP&#xff09;问题。 BERT 由 Goo…

星辰计划-深入理解kafka的消息存储和索引设计

消息存储 提到存储不得不说消息的读写&#xff0c;那么kafka他是如何读写数据的呢&#xff1f; 读取消息 1.通过debug(如何debug) 我们可以得到下面的调用栈&#xff0c;最终通过FileRecords来读取保存的数据 写入消息 1.通过debug(如何debug) 我们可以得到下面的调用栈&am…

模型django封装uvicorn服务器部署实战

Uvicorn 是一个轻量级的 ASGI 服务器&#xff0c;它基于 uvloop 和 httptools 这两个高性能的异步库。Uvicorn 提供了快速的启动时间和低延迟的响应&#xff0c;非常适合用于生产环境。 Django&#xff1a; 是一个开源且强大的Web框架&#xff0c;适用于快速开发和部署Python …

深度学习——线性回归

房价预测 线性模型 单层神经网络 损失函数的均方误差 训练数据 参数学习 显示解 偏导数少了负号 最优解y旁边的X少了转置符号 梯度下降 学习率选择 小批量随机梯度下降 批量规模的选择 总结