【Linux内核驱动基础】从零开始手搓一个从上层应用到底层驱动的IO口代码

【Linux内核驱动基础】从零开始手搓一个从上层应用到底层驱动的IO口控制代码

文章目录

  • 【Linux内核驱动基础】从零开始手搓一个从上层应用到底层驱动的IO口控制代码
    • 一、驱动基础认知
      • 1.为什么要学会写驱动
      • 2.文件名与设备号
      • 3.open函数从上层打通到底层硬件的详细过程
    • 二、基于内核驱动框架编写驱动代码
      • 1.编写上层应用代码
      • 2.修改内核驱动框架代码
      • 3.部分代码解读
        • 3.1 static的作用
        • 3.2 结构体成员变量赋值方式(最终加载到内核驱动链表)
        • 3.3 寻找file_operations结构体里的驱动模板函数
        • 3.4 手动生成设备
    • 三、驱动代码编译和测试(内核源码的版本一定要和树莓派内核版本对应,这样交叉编译后的文件才能在树莓派成功运行)
      • 1.驱动框架的模块交叉编译并发送至树莓派
        • 1.1 Makefile内添加生成.o命令
        • 1.2 模块编译生成.ko文件
        • 1.3 把.ko文件发送至树莓派
      • 2.交叉编译上层应用代码并发送至树莓派
      • 3.树莓派装载驱动并运行
        • 3.1 树莓派加载内核驱动(insmod)
        • 3.2 运行上层应用代码(发现无权限访问到设备pin4)
        • 3.3 增加访问权限,再次运行(内核版本一定要对应上)
          • ①解决方法1:加超级用户权限
          • ②解决方法2:增加“所有用户都可以访问的权限”(建议)
          • ③我的报错(可执行文件的权限问题)
        • 3.4 检查是否执行成功:demsg指令查看内核打印信息
        • 3.5 Linux权限拓展
    • 四、驱动阶段性总结
      • 1.内核驱动基本框架
      • 2.内核驱动交叉编译(内核版本要对应)
      • 3.驱动测试步骤
      • 4.验证步骤
    • 五、三种地址介绍
      • 1.总线地址
      • 2.物理地址
      • 3.虚拟地址
        • 3.1 虚拟概念
        • 3.2 虚拟地址的作用:
      • 4.简单了解地址框图与内核的页表映射
    • 六、实战:编写底层IO口驱动代码,输出高 / 低电平
      • 1.树莓派博通BCM2835芯片手册导读
        • 1.1 General Purpose I/O (GPIO)板块
          • ①学会捕捉关键信息
          • ②学会翻译,注重描述或解释
        • 1.2 查看配置引脚功能的寄存器
        • 1.3 查看引脚输出0/1的寄存器
        • 1.4 查看清除引脚0/1状态的寄存器
        • 1.5 关键内容总结
      • 2.配置3个主要的寄存器地址(ioremap函数把volatile的物理地址映射成虚拟地址)
        • 2.1 在原来框架的基础上,添加寄存器的定义
        • 2.2 配置寄存器的地址
      • 3.配置驱动代码的函数功能
        • 3.1 在函数pin4_open中配置pin4为输出引脚
          • 运用与(&) / 或(|)运算进行位操作
        • 3.2 在函数pin4_write中配置pin4输出 0 / 1
          • 根据值来操作IO口
      • 4.解除虚拟地址映射
      • 5.完整驱动代码和应用代码
        • 5.1 内核驱动框架代码pin4driver2.c
        • 5.2 上层应用程序代码pin4test2.c
      • 6.交叉编译并发送至树莓派进行测试
        • 6.1 树莓派上卸载之前的pin4驱动,删除上层应用文件和.ko文件
        • 6.2 驱动框架代码和上层应用程序在Ubuntu中进行交叉编译并发送至树莓派
        • 6.3 装载驱动
        • 6.4 运行上层应用程序文件
      • 7.扩展知识
        • 7.1 简单了解:DMA(direct memory access)(直接存储器访问)
        • 7.2 md5sum检查两个文件是否完全一样
        • 7.3 如何清空vi文件下所有的内容

一、驱动基础认知

1.为什么要学会写驱动

  • 树莓派开发简单是因为有厂家提供的wiringPi库,实现超声波,实现继电器操作,做灯的点亮…都非常简单
  • 但未来做开发时,不一定都是用树莓派,则没有wiringPi库可以用
  • 但只要能运行Linux,linux的标准C库一定有
  • 学会根据标准C库编写驱动,只要能拿到linux内核源码,拿到芯片手册,电路图…就能做开发
  • 用树莓派学习的目的不仅是为是体验其强大便捷的wiringPi库,更要通过树莓派学会linux内核开发,驱动编写等,做一个属于自己的库

2.文件名与设备号

linux一切皆为文件,其设备管理同样是和文件系统紧密结合;各种设备都以文件的形式存放在/dev目录下,称为设备文件。

应用程序可以打开、关闭和读写这些设备文件,完成对设备的操作,就像操作普通的数据文件一样。

在目录/dev下,运行ls命令能看到鼠标,键盘,屏幕,串口等设备文件,硬件要有相对应的驱动

在这里插入图片描述

那么操作文件函数,如open怎样区分这些设备呢?——依靠文件名与设备号

/devls -l可以看到

在这里插入图片描述

设备号又分为:

  • 主设备号用于区别不同种类的设备(如华为,小米,苹果);

  • 次设备号区别同一种类的多个不同类型设备(如苹果13,14)


内核中存在一个驱动链表,管理所有设备的驱动,驱动插入到链表的位置(顺序)由设备号检索

驱动开发无非以下两件事:

  • 添加驱动:编写完驱动程序,加载到内核

添加驱动做的几件事:

①设置设备名;②设置设备号;③编写设备驱动函数

  • 调用驱动:用户空间open后,调用驱动程序

    (驱动程序就是操作寄存器来驱动IO口,单片机51,32就是这种操作)

3.open函数从上层打通到底层硬件的详细过程

  • 用户空间调用open(比如open(“/dev/pin4”,O_RDWR)),系统产生一个软中断(中断号是0x80),进入内核空间调用sys_call,这个sys_call在内核里面是汇编的

  • sys_calll真正调用的是sys_open(属于VFS层虚拟文件系统,因为下层的文件系统不一样,为了实现上层应用统一化),根据你的设备名比如:pin4,去到内核的驱动链表,根据其主设备号与次设备号找到相关驱动函数。

  • 调用驱动函数里面的open,这个open就是对寄存器的操作,从而设置IO口引脚电平。这件事类似C51单片机的下面指令,特别容易,就两句话搞定:

    sbit pin4 = P1^4;
    pin4=1;
    

在这里插入图片描述

二、基于内核驱动框架编写驱动代码

目的是用简单的例子展示从用户空间到内核空间的整套流程

1.编写上层应用代码

在上层访问一个设备跟访问普通的文件没什么区别,下面试写一个简单的open和write去操作设备"pin4"

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>int main()
{int fd;fd = open("/dev/pin4",O_RDWR);if(fd < 0){printf("open failed\n");perror("reson");}else{printf("open success\n");}fd = write(fd,'1',1);//写一个字符'1',写一个字节return 0;
}

根据上面提到过的驱动认知,有个大致的概念,以open为例子:
上层opensys_callsys_open→内核驱动链表节点→执行节点里的open

当然,没有装载驱动的话,这个程序执行一定会报错。

只有在内核装载了驱动并且在/dev下生成了“pin4”这样一个设备才能运行。

接下来介绍最简单的字符设备驱动框架。

2.修改内核驱动框架代码

所谓框架,就是在往驱动链表里面加驱动的时候要符合的内核规则,它是定死的东西,基本的语句必须要有,少一个都不行。

虽然有这么多的代码,但核心运行的就两个printk。

#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;    //pin4设备变量的定义static dev_t devno;                //设备号
static int major =231;  		   //主设备号
static int minor =0;			   //次设备号
static char *module_name="pin4";   //模块名 上层的名字//pin4_open函数
static int pin4_open(struct inode *inode,struct file *file)
{printk("pin4_open\n");  //内核的打印函数,和printf类似return 0;
}//pin4_write函数  因为上层需要open和write这两个函数 
//            如果上层需要调用read等其他函数,可用vscode去内核源码搜索,照着格式修改即可使用 在file_operations结构体里面,搜索  struct file_operations
static ssize_t pin4_write(struct file *file1,const char __user *buf,size_t count, loff_t *ppos)
{printk("pin4_write\n");return 0;
}// static 限定这个结构体的作用仅仅在当前这个源文件
// 结构体名字 pin4_fops 可修改
static struct file_operations pin4_fops = {//内核定义好的结构体 内核源码里有//就是驱动的结构体 要加载到内核驱动链表.owner = THIS_MODULE, .open  = pin4_open,  //上层有读 底层就要有open的支持.write = pin4_write, //上层有写 底层就要有write的支持
};int __init pin4_drv_init(void)   //1.驱动的真正入口
{int ret;devno = MKDEV(major,minor);//2.创建设备号 //********************注册驱动 加载到内核驱动链表***********//主设备号231 模块名pin4 上面的结构体ret   = register_chrdev(major, module_name,&pin4_fops); //3.注册驱动 告诉内核,把这个驱动加入到内核驱动的链表中// 创建一种设备,并创建设备类型,如华为,然后mate30pin4_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);       //删除设备  /dev底下的 上面也是创建了设备和类class_destroy(pin4_class);              //删除类unregister_chrdev(major, module_name);  //卸载驱动 就是删除链表节点的驱动}module_init(pin4_drv_init);  //入口:内核加载驱动的时候,这个宏(module_init它不是个函数)会被调用,而真正的驱动入口是它里面调用的函数
module_exit(pin4_drv_exit);
MODULE_LICENSE("GPL v2");

3.部分代码解读

3.1 static的作用

内核源码里面运用了大量的static,因为内核源码众多,一万五千多个C文件,很容易造成代码命名的冲突。

内核代码数量庞大,为了防止与其他的文件有变量命名冲突,static限定变量的作用域仅仅只在这个文件。

3.2 结构体成员变量赋值方式(最终加载到内核驱动链表)

这是内核代码中常见的,对结构体操作的方式,单独给指定结构体元素赋值,其他不管。

static struct file_operations pin4_fops = {.owner = THIS_MODULE,.open  = pin4_open,.write = pin4_write,
};

但是,在keil的编译工具环境中不允许这样写,linux可以

3.3 寻找file_operations结构体里的驱动模板函数

可用vscode在内核源码里进行遍寻

在这里插入图片描述

在这里插入图片描述

跳转后,就可以看到整个file_operations结构体的定义,里面有对应的驱动函数声明格式

(我们常用的就是open,read以及write函数)

file_operations,可以发现很多的函数指针,这些函数名跟系统上层文件的操作差不多。

如果上层想要实现read,就复制过来,按照格式改一改来实现下层就能使用。

上层对应底层,上层想要用read,底层就要有read的支持

在这里插入图片描述

3.4 手动生成设备

框架中有自动生成设备的代码,那么手动生成设备是怎么样的呢?(一般不这样干,麻烦,仅作为演示)

  • 进入/dev目录,查看mknod帮助命令可知道创建规则:

    sudo mknod 设备名称 设备类型 主设备号 次设备号

在这里插入图片描述

  • 使用如下命令创建名称为arsen,主设备号为6,次设备号为27的字符设备

  • sudo mknod arsen c 6 27

    • 在这里,参数c表示创建一个字符设备节点。字符设备节点用于与字符设备进行通信,例如键盘、鼠标等。相对应的,还有块设备节点(参数为b),用于与块设备进行通信,例如硬盘、USB设备等。

    在这里插入图片描述

三、驱动代码编译和测试(内核源码的版本一定要和树莓派内核版本对应,这样交叉编译后的文件才能在树莓派成功运行)

1.驱动框架的模块交叉编译并发送至树莓派

  • 在ubuntu中,进入字符设备驱动目录linux-rpi-4.19.y/drivers/char(IO口属于字符设备驱动)

  • 进入源码目录下的原因是,写驱动必须要链接到源码(源码定义好了结构体等等),必须要有源码

  • 拷贝上文分析过的驱动框架代码,创建名为pin4driver.c的文件

    在这里插入图片描述

1.1 Makefile内添加生成.o命令

要进行配置,使得工程编译时可以编译到这个文件

当然不一定要放在/char下

但注意:放在哪个文件夹下,就修改那个文件夹的Makefile即可

vi Makefile

在这里插入图片描述

模仿这些文件的编译方式,以编译成模块的形式(还有一个方式为编译进内核)编译pin4drive.c

在Makefile里面添加( -m就是模块的形式):

obj-m                           += pin4driver.o

在这里插入图片描述

1.2 模块编译生成.ko文件

之前编译内核的时候用的是这个命令:

ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make -j4 zImage modules dtbs
#命令解释
ARCH=arm(指定当前要编译的是arm架构)
CROSS_COMPILE=arm-linux-gnueabihf- (指定编译器是树莓派交叉编译工具链)
KERNEL=kernel7 (指明kernel类型,树莓派1设置为kernel,树莓派2,3设置为kernel7,树莓派4设置为kernel7l)
make -j4:指定用多少电脑资源进行编译(j4是四核的意思,这个要每个人根据自己的电脑的配置以及虚拟机的设置的核数来决定,我之前虚拟机设置的是4核)
zImage:生成内核镜像
modules:要生成驱动模块
dtbs:生成配置文件

现在只需进行模块编译,不需要生成zImage,dtbs文件,回到源码目录/linux-rpi-4.19.y再执行下面指令

ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make modules

注:如果说编译中途提示出错,照着错误提示去修改.c文件即可,和上层编译类似。

编译完成,进入/char目录发现已经生成的一些文件如下(则成功):

在这里插入图片描述

1.3 把.ko文件发送至树莓派

使用远程拷贝命令或者共享文件夹先拷贝到本地再上传给树莓派

(共享文件夹不能用,报错显示没有该文件夹时,可先禁用再重新启用即可)

scp drivers/char/pin4driver.ko pi@192.168.102.74:/home/pi

2.交叉编译上层应用代码并发送至树莓派

拷贝上文分析的上层代码到ubuntu中,此处我命名为pin4test.c

使用交叉编译工具进行编译

arm-linux-gnueabihf-gcc pin4test.c -o pin4test

在这里插入图片描述

发送至树莓派

scp pin4test pi@192.168.102.74:/home/pi

在这里插入图片描述

3.树莓派装载驱动并运行

3.1 树莓派加载内核驱动(insmod)
sudo insmod pin4driver.ko

查看是否加载添加成功

ls /dev/pin4 -l

在这里插入图片描述

看到设备添加成功,主设备号231,次设备号0,和内核里面的驱动代码对应上

在这里插入图片描述

也可以用lsmod查看内核挂载的驱动;如果需要卸载驱动,就sudo rmmod pin4drive

在这里插入图片描述

3.2 运行上层应用代码(发现无权限访问到设备pin4)

试着运行上层应用代码

./pin4test

发现没有访问权限

在这里插入图片描述

3.3 增加访问权限,再次运行(内核版本一定要对应上)
①解决方法1:加超级用户权限
sudo ./pin4test
②解决方法2:增加“所有用户都可以访问的权限”(建议)
sudo chmod 666 /dev/pin4

在这里插入图片描述

③我的报错(可执行文件的权限问题)

操作后一直还是运行不了,结果发现是pin4test文件的访问权限问题,只开了读写,没有执行

之前甚至还一度以为是内核版本,环境变量,或者哪个步骤出错了

解决办法:chmod 711 pin4test

执行完后,会发现它变绿了,说明可以执行了

在这里插入图片描述

3.4 检查是否执行成功:demsg指令查看内核打印信息

用dmesg命令显示内核缓冲区信息,并通过管道筛选与pin4相关信息

dmesg | grep pin4

可以看到这些打印信息,说明内核的printk已经被成功调用,我们已经成功完成了上层对内核的调用 !

在这里插入图片描述

3.5 Linux权限拓展

Linux/Unix 的档案调用权限分为三级 : 档案拥有者(user)、群组(group)、其他(other)。利用 chmod 可以藉以控制档案如何被他人所调用

r 表示可读取,w 表示可写入,x 表示可执行

在这里插入图片描述

r=4,w=2,x=1

若要rwx属性则4+2+1=7;

若要rw-属性则4+2=6;

若要r-x属性则4+1=7。


-rw------- (600) – 只有属主有读写权限。
-rw-r–r-- (644) – 只有属主有读写权限;而属组用户和其他用户只有读权限。
-rwx------ (700) – 只有属主有读、写、执行权限。
-rwxr-xr-x (755) – 属主有读、写、执行权限;而属组用户和其他用户只有读、执行权限。
-rwx–x–x (711) – 属主有读、写、执行权限;而属组用户和其他用户只有执行权限。
-rw-rw-rw- (666) – 所有用户都有文件读、写权限。这种做法不可取。
-rwxrwxrwx (777) – 所有用户都有读、写、执行权限。更不可取的做法。


EG: chmod 744 仅允许用户(所有者)执行所有操作,而组和其他用户只允许读。

四、驱动阶段性总结

1.内核驱动基本框架

驱动代码编写:参考pin4driver.c

2.内核驱动交叉编译(内核版本要对应)

  • 把驱动代码拷贝至 driver/char
  • 修改Makefile ,告诉编译器,要编译该驱动文件,驱动代码文件放在哪个目录下就修改哪个目录下的Makefile文件
  • ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make modules

3.驱动测试步骤

内核驱动装载:

sodu insmod xxx.ko

内核驱动卸载:
sodu rmmod xxx 不需要写ko
查看内核模块:
lsmod

4.验证步骤

  • 装载驱动
  • 驱动装载后生成设备,比如:/dev/pin4,
  • 通过sudo chmod 666 /dev/pin4 添加访问权限
  • 运行测试程./pin4text调用驱动
  • 内核的 printk 是内核层的 printf ,通过dmesg | grep pin4查看并通过管道筛选与pin4相关信息

五、三种地址介绍

写驱动是为了操作IO口,实现自己的wiringpi库(驱动库),跟硬件打交道

首先,要理解3个地址的概念

  • 专业参考博文:

    物理地址、虚拟地址、总线地址
    物理地址和总线地址区别

1.总线地址

通俗来说:cpu能够访问的内存范围(运行内存)

举个例子:

  • 现象:电脑装了32位(bit)的系统,明明内存条有8G,却只能识别3.8G左右
  • 这是因为32位仅能表示/访问232=4,294,967,296bit = 4,194,304Kb = 4096Mb = 4G左右
  • 只有装了64位的,才能够识别到8G(32位、64位是计算机CPU一次处理数据能力的大小)

树莓派装载32位操作系统,寻址自然是4G

  • 树莓派查看内存,大概是925M

  • cat /proc/meminfo
    

    在这里插入图片描述

    在这里插入图片描述

2.物理地址

硬件实际地址或绝对地址

在存储器里以字节为单位存储信息,为正确地存放或取得信息,每一个字节单元给以一个唯一的存储器地址,称为物理地址(Physical Address),又叫实际地址或绝对地址。

3.虚拟地址

3.1 虚拟概念

逻辑地址(基于算法的地址,软件层面的地址,是假地址)便称为虚拟地址

3.2 虚拟地址的作用:

以树莓派为例,总线可以访问4G,物理地址只有1G,但需要运行的程序大于1G,如果把程序全部都加载到内存是不可取的,以下是虚拟地址的作用

  • 物理地址数据的运行真正是拿虚拟地址来操作的,虚拟地址可以大于1G
  • 总线地址(CPU能访问的地址范围)能看到4个G,就可以把1个G的物理地址映射成4个G的虚拟地址
  • 当物理地址不能满足程序运行空间需求时,如果没有虚拟地址,程序就不能正常运行;如单片机51和STM32,如果程序过大,是禁止你烧写的,而在Linux系统环境下是可以的

树莓派3b/3b+的cpu型号查看:

  • 树莓派3b/3b+的cpu型号是BCM2835,它是ARM-cotexA53架构
cat /proc/cpuinfo

在这里插入图片描述

unix设计与实现(主要是内核设计文档)

4.简单了解地址框图与内核的页表映射

在这里插入图片描述

  • BCM2835是树莓派3B CPU的型号,是ARM-cotexA53架构
  • cpu Bus是地址总线,00000000~FFFFFFFF是CPU寻址的范围(4G)
  • DMA是高速拷贝单元,CPU可以发动DMA直接让DMA进行数据拷贝,直接内存访问单元。
  • 物理地址(PA)1G、虚拟地址(VA)4G

若程序大于物理地址1G,那还能跑吗?

答案是可以的(不过仅限Linux),它有个MMU的单元,把物理地址映射成虚拟地址,我们操作的代码基本上都是在虚拟地址,它有一个映射页表(如下)

在这里插入图片描述

将物理地址映射为虚拟地址有一个算法(页表),它决定了将物理地址映射到虚拟地址的哪个位置,页表是通过MMU(分页内存管理单元)来管理的,就是设计完页表后,通过MMU来执行将物理地址映射为虚拟地址

  • 物理地址的1M通过扩充映射成为4M的虚拟地址,这中间有个设计的算法叫页表

  • 这个表决定了这个4M被映射到虚拟内存的哪一个段,通过MMU进行管理

如果想要更多地了解Linux对内存的管理,推荐书《unix设计与实现》,类似内核设计文档,讲的是内核设计的思路,讲代码讲的不多


六、实战:编写底层IO口驱动代码,输出高 / 低电平

之前驱动框架的代码仅仅用来检测、走一遍整个驱动的架构;那么在这一个小节,将着手于从硬件底层实现pin4引脚输出高 / 低电平

驱动开发两大利器:

  • 芯片手册
  • 电路图(电路图主要用来寻找寄存器,树莓派的电路图很难找到,但芯片手册清楚地给出了各个寄存器)

1.树莓派博通BCM2835芯片手册导读

1.1 General Purpose I/O (GPIO)板块

查看芯片手册的目的性很强:做哪一块的开发,就只看那一块,现在要开发的是GPIO,熟悉控制IO口的寄存器最为重要

如果看完这部分的文档,你对于以下几个问题(后面有解析)有清晰的答案,说明你真正读懂了这一部分的开发

  • 操作逻辑:
    • 简言之,就是怎么进行配置相关的寄存器,这些配置步骤和思想其实都很类似
  • 需要重点掌握的寄存器有哪些?
    • 输入 / 输出控制寄存器
    • 输出 0 / 1控制寄存器
    • 清除状态寄存器
①学会捕捉关键信息

在新的平台也要学会捕捉类似的关键信息:选择输入还是输出,0/1,怎么清除,上升沿下降沿等。(配置过32 / 51寄存器的应该对这些很熟悉)

从下图中可以大概了解到所有的IO口被分成了0~5组

有意思的是,下图最第一列的地址Address是树莓派总线地址,一般芯片手册给的都是真正的物理地址。

第二列是寄存器的名字,第三列寄存器功能描述。

在这里插入图片描述

②学会翻译,注重描述或解释

描述部分也很重要,大多涉及使用方法

例如,下面就是对GPFSELn的描述:

在这里插入图片描述

必要时,可以借助AI或者其他翻译工具进行翻译,以作参考

  • AI翻译内容:

    选择寄存器用于定义通用输入/输出引脚(GPIO)的操作。每个54个GPIO引脚都至少有两个可选功能,如第16.2节所定义。FSEL{n}字段确定第n个GPIO引脚的功能。所有未使用的可选功能线均连接到地线,并在选择时输出“0”。所有引脚将重置为正常的GPIO输入操作。

1.2 查看配置引脚功能的寄存器

功能选择引脚一共有6组(0组~5组)

  • pin0-pin9——0组
  • pin10-pin19——1组
  • pin20-pin29——2组
  • pin30-pin39——3组
  • pin40-pin49——4组
  • pin50-pin53——5组

在这里插入图片描述

找到pin4对应的功能选择0组,并查看相关的配置示例(这0~9的IO口属于分组0)

  • 由示例可知,要想让pin4为输出模式,就得把该组寄存器的14-12位置为001

在这里插入图片描述

IO编号要看好,可以使用wiringpi库的命令查看,也可以去看官方手册

gpio readall

在这里插入图片描述

树莓派引脚手册传送门

在这里插入图片描述

1.3 查看引脚输出0/1的寄存器

GPIO引脚输出设置寄存器 /GPIO Pin Output Set Registers (GPSETn) :

来自AI翻译

  • 输出设置寄存器用于设置GPIO引脚。SET{n}字段定义要设置的相应GPIO引脚,将“0”写入该字段不会产生任何效果。如果GPIO引脚被用作输入(默认情况下),则忽略SET{n}字段中的值。但是,如果之后将引脚定义为输出,则该位将根据最后一次设置/清除操作进行设置。将设置和清除功能分开可以避免读取-修改-写入操作的需求

在这里插入图片描述

1.4 查看清除引脚0/1状态的寄存器

GPIO引脚输出清除寄存器 / GPIO Pin Output Clear Registers (GPCLRn):

来自AI翻译

  • 输出清除寄存器用于清除GPIO引脚。CLR{n}字段定义要清除的相应GPIO引脚,将“0”写入该字段不会产生任何效果。如果GPIO引脚被用作输入(默认情况下),则忽略CLR{n}字段中的值。但是,如果之后将引脚定义为输出,则该位将根据最后一次设置/清除操作进行设置。将设置和清除功能分开可以避免读取-修改-写入操作的需求

在这里插入图片描述

1.5 关键内容总结

通过文档阅读,可以整理出如下的关键信息,有3个最基本的要清楚:

  • ①选择IO是 输入 / 输出控制寄存器:GPFSEL
  • ②输出0 / 1寄存器:GPSET
  • ③清除寄存器:GPCLR

操作逻辑:

  • GPFSEL0寄存器举例,引脚pin4对应的分组就是第0组(51单片机引脚也是分成第0组、第1组、第2组、第3组)
  • 只要在这个分组下,把14-12位设置为001,就能配置pin4引脚为输出

总之,还是要自己多多翻阅,这里仅为简单的导读


2.配置3个主要的寄存器地址(ioremap函数把volatile的物理地址映射成虚拟地址)

2.1 在原来框架的基础上,添加寄存器的定义
volatile unsigned int* GPFSEL0 = NULL;
volatile unsigned int* GPSET0  = NULL;
volatile unsigned int* GPCLR0  = NULL;

要想写出上面的代码,要掌握以下几点:

  • ①弄清楚寄存器的分组
    • 其中寄存器的0表示的是分组,目标操作的IO是pin4,由文档可知,属于寄存器分组0
  • volatile的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值(笔试)
    • 防止编译器优化(你给的这个地址编译器可能认为不好,可能会省略,也可能会进行更改)这些寄存器变量;
    • 要求每次直接从寄存器里读值:由于随着程序的执行,可能会改变寄存器当中的数据,而读取的都是内存里面的备份数据,数据的时效性没有那么强,读的可能是一个老数据在内核中对IO口进行操作都要有volatile
2.2 配置寄存器的地址

在添加好寄存器代码的基础上,在驱动的初始化pin4_drv_init函数体中添加寄存器地址配置,并将物理地址通过ioremap函数映射成虚拟地址

GPFSEL0 = (volatile unsigned int *)ioremap(0x3f200000,4);
GPSET0  = (volatile unsigned int *)ioremap(0x3f20001C,4);
GPCLR0  = (volatile unsigned int *)ioremap(0x3f200028,4);

要想写出上面的代码,要掌握以下几点:

  • ①分别找到几个IO寄存器的物理地址(非常易错),弄清楚GPIO的物理地址(真实地址)

    • 并不是用下图这个地址来对应GPIO功能选择寄存器0的地址,否则编译后运行会有段错误

    • 在这里插入图片描述

    • IO口的起始地址是0x3f000000(从网上找的,树莓派手册第一列是总线地址),加上GPIO的偏移量0x2000000,所以GPIO的实际物理地址应该是从0x3f200000开始的,然后在这个基础上进行Linux系统的MMU内存虚拟化管理,映射到虚拟地址上,编程都是操作虚拟地址。

  • ②根据偏移值,弄清楚寄存器的物理地址(真实地址)

    • 可以看到寄存器GPSET0相对于GPIO物理地址的偏移值为1C

      在这里插入图片描述

    • 同样的方法,寄存器GPCLR0的偏移值为28,即0x3f200028;寄存器GPFSEL0的偏移值为0,即0x3f200000

  • ③物理地址转换为虚拟地址:ioremap函数

    因为代码操作的是虚拟地址,代码中直接用物理地址肯定不行,需要进行转换,将IO口寄存器映射成普通内存单元进行访问。

    使用函数ioremap:

    函数原型:

    void *ioremap(unsigned long phys_addr, unsigned long size)
    phys_addr:要映射的起始的IO物理地址;
    size:要映射的空间的大小;

3.配置驱动代码的函数功能

3.1 在函数pin4_open中配置pin4为输出引脚

可以看到只要32位寄存器GPFSEL0的14-12位配置为001,其它位不管,即可配置pin4为输出引脚

在这里插入图片描述

当然直接暴力赋值(0000…001…0000)是不可取的,会把其他的IO口给影响;最好的结果是只改变了14-12位

  • 运用与(&) / 或(|)运算进行位操作
  • *GPFSEL0 &= ~(0x6 << 12);
    *GPFSEL0 |= (0x1 << 12);
    
    • 与运算给指定位(14、13)赋值0,其他不变

      为了方便描述,这里把需要“与”上的数称为 “辅助数”。(寄存器中的数是假设的)

      在这里插入图片描述

      但为了方便(1越少,用计算器换算就越简单)得到这个第13、14位为0的数,选择对辅助数“取两次反“。

      第一次取反为: 00000…110…00000

      用计算器在二进制BIN中输入110(方便就在这,你要是直接在计算器中输入目标辅助数进行换算,数有多少个1都很吃力!!)

      在这里插入图片描述

      0110,向左移12位,低位自动补0,则1 1正好对上14、13位。

      再取反(~),得到一开始想要的让寄存器的数14、13位与上0的辅助数。

    • 或运算给指定位(12)赋值1

3.2 在函数pin4_write中配置pin4输出 0 / 1

获取上层write函数的值:copy_from_user函数

copy_from_user 完成用户空间数据到 内核空间的拷贝:

unsigned long copy_from_user(void *to, const void user *from, unsigned long n)

copy_to_user 完成内核空间到用户空间的拷贝:

unsigned long copy_to_user(void *to, const void __user *from, unsigned long n)

  • unsigned long copy_from_user(void * to, const void __user * from, unsigned long n)copy_from_user(buf, user_buf, buf_size)
    

    此函数将from指针指向的用户空间地址开始的连续n个字节的数据,产送到to指针指向的内核空间地址,简言之,是用于将用户空间的数据传送到内核空间

    如果数据拷贝成功,则返回;否则,返回没有拷贝成功的数据字节数。

  • 第一个参数 to 是内核空间的数据目标地址指针,第二个参数 from 是用户空间的数据源地址指针,第三个参数 n 是数据长度

根据值来操作IO口

说明(这也是操作逻辑的一部分啦):
①这个GPSET0,0指的是分组,不是设置成低电平
②左移4位,是因为GPSET0寄存器的第4位对应pin4,只要把第4位设置为1,表示这个寄存器就对pin4发挥作用,设置成高电平,如果是0则 no effct(手册内容)

在这里插入图片描述

int userCmd;
copy_from_user(&userCmd,buf,count);
if(userCmd == 1){printk("set 1\n");
}
else if(userCmd == 0){printk("set 0");
}
else{printk("undo\n");
}

4.解除虚拟地址映射

退出程序卸载驱动的时候,先解除映射:iounmap函数

  • void iounmap(void* addr)//取消ioremap所映射的IO地址
    
void __exit pin4_drv_exit(void)//可以发现和init刚好是相反的执行顺序。
{iounmap(GPFSEL0);iounmap(GPSET0);iounmap(GPCLR0);device_destroy(pin4_class,devno);class_destroy(pin4_class);unregister_chrdev(major, module_name);  //卸载驱动}

5.完整驱动代码和应用代码

5.1 内核驱动框架代码pin4driver2.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";   //模块名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");  //内核的打印函数,和printf类似//open的时候配置pin4为输出引脚*GPFSEL0 &= ~(0x6 << 12);*GPFSEL0 |= (0x1 << 12);return 0;
}//pin4_write函数
static ssize_t pin4_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos)
{int userCmd;//上层写的是整型数1,底层就要对应起来用int.如果是字符则用charprintk("pin4_write\n");//获取上层write的值copy_from_user(&userCmd,buf,count);//用户空间向内核空间传输数据//根据值来执行操作if(userCmd == 1){printk("set 1\n");*GPSET0 |= 0x1 << 4;}else if(userCmd == 0){printk("set 0\n");*GPCLR0 |= 0x1 << 4;}else{printk("cmd error\n");}return 0;
}//read函数
static ssize_t pin4_read(struct file *file, char __user *buf, size_t size, loff_t *offset)
{printk("pin4_read\n");  //内核的打印函数//可以用copy_to_user()函数读取引脚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 pin4 success\n");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);return 0;
}void __exit pin4_drv_exit(void)//可以发现和init刚好是相反的执行顺序。
{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");
5.2 上层应用程序代码pin4test2.c
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>int main()
{int fd;int cmd;fd = open("/dev/pin4",O_RDWR);if(fd < 0){printf("open failed\n");perror("reson");}else{printf("open success\n");}printf("请输入0 / 1\n 0:设置pin4为低电平\n 1:设置pin4为高电平\n");scanf("%d",&cmd);if(cmd == 0){printf("pin4设置成低电平\n");}else if(cmd == 1){printf("pin4设置成高电平\n");}fd = write(fd,&cmd,1);//写一个字符'1',写一个字节return 0;
}

6.交叉编译并发送至树莓派进行测试

6.1 树莓派上卸载之前的pin4驱动,删除上层应用文件和.ko文件
sudo rmmod pin4driver

lsmod查看是否卸载成功

6.2 驱动框架代码和上层应用程序在Ubuntu中进行交叉编译并发送至树莓派

将完整驱动框架代码拷贝到/SYSTEM/linux-rpi-4.19.y/drivers/char目录下(继续修改一下该char目录下写好的Makefile编译规则),并在内核源码/SYSTEM/linux-rpi-4.19.y目录检查一下工具链版本

vi Makefile

在这里插入图片描述

arm-linux-gnueabihf-gcc -v

在这里插入图片描述

在内核源码/SYSTEM/linux-rpi-4.19.y目录进行模块编译

ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make modules

框架代码交叉编译后:

  • 红色框表示编译生成了需要的模块
  • 而蓝色框的警告可以不用理会(copy_form_user前两个参数传入的是空类型的指针,在框架代码中没有进行强制转换)

在这里插入图片描述

交叉编译应用程序代码

在这里插入图片描述

拷贝至树莓派(可以给一下文件执行权限chmod 711 pin4test2)

在这里插入图片描述

6.3 装载驱动

安装

#安装
sudo insmod pin4driver2.ko    
#查看
lsmod                            

在这里插入图片描述

查看内核后台消息,是否安装成功

dmesg

在这里插入图片描述

给设备权限,并查看pin4权限

sudo chmod 666 /dev/pin4
ls -l /dev/pin4

在这里插入图片描述

6.4 运行上层应用程序文件

运行前,可以用命令检查看当前pin4(BCM4)的引脚设置状态和电平状态,运行后观察它的变化

gpio readall 
./pin4test2

在这里插入图片描述

在这里插入图片描述

同时可查看内核消息

dmesg

在这里插入图片描述

如何驱动失败,要学会调试

当然,很多情况下不是直接就能够成功运行的,学会调试很重要。

出问题先看上层(因为上层简单,好修改),后看底层。

Mode我们配置的是输出模式,如果是IN,或者ALT2等,说明底层的模式配置出了问题,大多是寄存器的移位没搞明白。

多看看内核的打印信息,write打印信息,变量的值等等。

7.扩展知识

7.1 简单了解:DMA(direct memory access)(直接存储器访问)

在这里插入图片描述

大数据的快速拷贝单元

  • 使用cp指令拷贝大文件会很大程度占用CPU资源,DMA是专门用来做协助数据拷贝的微控制器,CPU可以发动DMA使其进行数据的拷贝
7.2 md5sum检查两个文件是否完全一样

多用于检测原来的“同名”文件是否已经被新的替代,也可以用作检查拷贝过程中是否损坏

md5sum file.c

唯一标识,相同则两个文件为同一文件
在这里插入图片描述

7.3 如何清空vi文件下所有的内容

在命令模式(不输入模式)下

  • 首先执行 gg 这里是跳至文件首行
  • 再执行:dG 这样就清空了整个文件

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

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

相关文章

【强训笔记】day4

NO.1 思路&#xff1a;利用滚动数组&#xff0c;迭代一个Fibonacci数列&#xff0c;给出三个值进行循环迭代&#xff0c;当n<c时&#xff0c;说明n在b和c之间&#xff0c;这里只需要返回c-n和n-b的最小值就可以了。 代码实现&#xff1a; #include<iostream>using n…

打印给定数组中每一个数字

如何给定1-10的数字 #include<stdio.h> int main() {int arr[] { 1,2,3,4,5,6,7,8,9,10 };// 0 9//[]--下标引用操作符int i;int sz sizeof(arr) / sizeof(arr[0]);//10个数for (i 0; i < sz; i){printf("%d ", arr[i]);}re…

centos7使用源码安装方式redis

安装编译源码的工具gcc yum install -y gcc下载源码 源码下载地址 https://download.redis.io/releases/ 注意事项 不建议安装最新版本redis&#xff0c;所以我这里选择6.2.6版本 下载 wget https://download.redis.io/releases/redis-6.2.6.tar.gz解压 tar -zxvf redis-…

单片机通讯协议

参考&#xff1a;江科大单片机教程 STM32入门教程-2023版 细致讲解 中文字幕_哔哩哔哩_bilibili IIC通讯协议SPI通信协议UARTCANUSB速度100k-400khz4Mhz-线数2 CLK,DATA4CLK,ENB,IO,OI额外设备一主多从一主多从 一般不用自己写&#xff0c;都有相应的库或官方提供相应的&#…

舌头分割YOLOV8-SEG

舌头分割&#xff0c;基于YOLOV8-SEG&#xff0c;训练得到PT模型&#xff0c;然后转换成ONNX&#xff0c;OPENCV的DNN调用&#xff0c;从而摆脱YOLO依赖&#xff0c;支持C,PYTHON,ANDROID开发 舌头分割YOLOV8-SEG

使用预训练模型构建自己的深度学习模型(迁移学习)

在深度学习的实际应用中&#xff0c;很少会去从头训练一个网络&#xff0c;尤其是当没有大量数据的时候。即便拥有大量数据&#xff0c;从头训练一个网络也很耗时&#xff0c;因为在大数据集上所构建的网络通常模型参数量很大&#xff0c;训练成本大。所以在构建深度学习应用时…

OS对软件的管理,进程,PCB、子进程

进程 可执行程序加载到内存中&#xff0c;操作系统为内个程序都形成一个PCB对象&#xff08;结构体对象&#xff09;&#xff0c;PCB里存放着这个程序的所有的属性。进程可执行程序PCB &#xff0c;CPU执行程序也是先通过该程序的PCB找到相应的程序代码&#xff0c;然后一条一…

ThinkPHP5 SQL注入漏洞敏感信息泄露漏洞

1 漏洞介绍 ThinkPHP是在中国使用极为广泛的PHP开发框架。在其版本5.0&#xff08;<5.1.23&#xff09;中,开启debug模式&#xff0c;传入的某参数在绑定编译指令的时候又没有安全处理&#xff0c;预编译的时候导致SQL异常报错。然而thinkphp5默认开启debug模式&#xff0c…

分享一些实用的工具

1、amCharts5&#xff1a;模拟航线飞行/业务分布图/k线/数据分析/地图等 网址&#xff1a; JavaScript mapping library: amCharts 5https://www.amcharts.com/javascript-maps/ Demo地址&#xff1a;Chart Demos - amChartshttps://www.amcharts.com/demos/#maps 他分为amC…

小龙虾优化算法(Crayfish Optimization Algorithm,COA)

小龙虾优化算法&#xff08;Crayfish Optimization Algorithm&#xff0c;COA&#xff09; 前言一、小龙虾优化算法的实现1.初始化阶段2.定义温度和小龙虾的觅食量3.避暑阶段&#xff08;探索阶段&#xff09;4.竞争阶段&#xff08;开发阶段&#xff09;5.觅食阶段&#xff08…

【誉天战报】3月HCIE战报火热来袭!新增45位同学通过认证!

2024年3月&#xff0c;誉天教育共有45名学员顺利通过了HCIE认证&#xff0c;其中&#xff1a;云计算20人、数通18人、存储5人、云服务2人。让我们一起祝贺他们吧~ 誉天教育是华为优选级授权培训合作伙伴&#xff0c;专业从事华为授权认证课程实战技能培训。连续13年荣获“华为优…

和林曦老师一起读书吧 | 愿我们:只生欢喜不生愁

今天&#xff0c;想和你一起来读书&#xff0c;林曦老师的《只生欢喜不生愁》。    这本书的名字很有意味&#xff0c;它来自于清代《养真集》中的一句话&#xff1a;自古神仙无别法&#xff0c;只生欢喜不生愁。      我们会羡慕这样的状态&#xff1a;只生欢喜不生愁…

018基于SSM的音乐系统网站

018基于SSM的音乐系统/网站 开发环境&#xff1a; Jdk7(8)Tomcat7(8)MysqlIntelliJ IDEA(Eclipse)Maven 数据库&#xff1a; MySQL 技术&#xff1a; SpringSpring mvcMybatisJqueryVideo jsJSPJSTLEasyUI 适用于&#xff1a; 课程设计&#xff0c;毕业设计&#xff0c;学习…

“你需要TrustedInstaller提供的权限才能对此文件进行更改” 解决方案

转载地址 【“你需要TrustedInstaller提供的权限才能对此文件进行更改” 解决方案-CSDN博客】

37.WEB渗透测试-信息收集-企业信息收集(4)

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 内容参考于&#xff1a; 易锦网校会员专享课 上一个内容&#xff1a;36.WEB渗透测试-信息收集-企业信息收集&#xff08;3&#xff09;-CSDN博客 关于主域名收…

Git -- 运用总结

文章目录 1. Git2. 基础/查阅2.1 基础/查阅 - git2.2 仓库 - remote2.3 清理 - rm/clean2.4 版本回退 - reset 3. 分支3.1 分支基础 - branch3.2 分支暂存更改 - stash3.3 分支切换 - checkout 4. 代码提交/拉取4.1 代码提交 - push4.2 代码拉取 - pull 1. Git 2. 基础/查阅 2…

(学习日记)2024.05.07:UCOSIII第六十一节:User文件夹函数概览(uCOS-III->Source文件夹)第七部分

写在前面&#xff1a; 由于时间的不足与学习的碎片化&#xff0c;写博客变得有些奢侈。 但是对于记录学习&#xff08;忘了以后能快速复习&#xff09;的渴望一天天变得强烈。 既然如此 不如以天为单位&#xff0c;以时间为顺序&#xff0c;仅仅将博客当做一个知识学习的目录&a…

RISC-V CVA6 在 Linux 下相关环境下载与安装

RISC-V CVA6 在 Linux 下相关环境下载与安装 所需环境与源码下载 CVA6 源码下载 首先&#xff0c;我们可以直接从 GitHub 一次性拉取所有源码&#xff1a; git clone --recursive https://github.com/openhwgroup/cva6.git如果这里遇到网络问题&#xff0c;拉取失败&#x…

Fluent.Ribbon创建Office的RibbonWindow菜单

链接&#xff1a; Fluent.Ribbon文档 优势&#xff1a; 1. 可以创建类似Office办公软件的复杂窗口&#xff1b; 2. 可以应用自定义主题风格界面

实现 <el-cascader> 组件的回显功能

vue A页面&#xff0c;用户填写了el-cascader多层级数据&#xff0c;层级list数据从接口获取&#xff1b; vue B页面&#xff0c;多层级数据要进行回显&#xff0c;接口给到的数据是value值&#xff1b; 直接看demo <template><div><el-cascaderv-model"…