首页 » 软件优化 » 树莓派高级开发——“IO口驱动代码的编写“(地址寄存器内存总线映射)

树莓派高级开发——“IO口驱动代码的编写“(地址寄存器内存总线映射)

少女玫瑰心 2024-10-22 17:22:22 0

扫一扫用手机浏览

文章目录 [+]

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

页表(MMU的单元)

分页管理:

内存分页其实就是我们所说的4G空间,内存的所有内存被操作系统内核以4G为每页划分开,当我们程序运行时会被加载到内存中的4G空间里,其实说是有4G其实并没有真正在的4G空间,4G空间中有一小部分被映射到了物理内存中,或者被映射到了硬盘的文件上(fopen),或者没有被映射,还有一部分在内存当中就会被划分栈,堆,其中有大片大片的内存是没有被映射的,同样物理内存也是被分页了用来与虚拟内存产生映射关系。
将虚拟地址映射为物理地址有一个算法(页表)决定了将虚拟地址映射到物理地址的哪个位置,页表是通过MMU(分页内存管理单元)来管理的,就是设计完页表后通过MMU来执行将虚拟地址映射为物理地址。
其实真正情况下只有3G用户空间,假如你的内存是4G的那么其中有1G是给操作系统内核使用的,所谓的4G空间只是操作系统基于虚拟内存这种拆东墙补西墙的形式给你一种感觉每个进程都有4G的可用空间一样!
这里来说一下拆东墙补西墙,当我们程序被加载进4G空间时其实根本用不了所谓的4G空间,其中有大片内存被闲置,那么这个时候呢,其他程序被加载进来时发现内存不够了,就把其他程序里的4G空间里闲置部分拿出来给这个进程用,换之这个进程内存不够时就会把其他进程里闲置的空间拿过来给该进程使用。
银行也是如此!
当我们要对物理地址做操作时比如if语句要根据CPU的状态标志寄存器来做不同的跳转,那么这个时候就要对CPU额状态寄存器做操作了就必须知道它的物理地址,内存中有一个电子元件叫MMU负责从操作系统已经初始化好的内存映射表里查询与虚拟地址对应的物理地址并转换,比如mov 0x4h8这个是虚拟地址,当我们要对这个虚拟地址里写数据时那么MMU会先判断CPU的分页状态寄存器里的标志状态是否被设定,如果被设定那么MMU就会捕获这个虚拟地址物理并在操作系统内核初始化好的内存映射表里查询与之对应的物理地址,并将其转换成真正的实际物理地址,然后在对这个实际的物理地址给CPU,在由CPU去执行对应的命令,相反CPU往内存里读数据时比如A进程要读取内存中某个虚拟地址的数据,A进程里的指令给的是虚拟地址,MMU首先会检查CPU的分页状态寄存器标志位是否被设置,如果被设置MMU会捕获这个虚拟地址并将其转换成相应的物理地址然后提交给CPU,在由CPU到内存中去取数据!

更详细的地址问题看这里

树莓派高级开发——“IO口驱动代码的编写“(地址寄存器内存总线映射) 软件优化
(图片来自网络侵删)
BCM2835芯片手册

下面截取树莓派芯片手册的一张图:

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

通过芯片手册了解树莓派的GPIO:有54条通用I/O GPIO行,分为两行,备用功能通常是外围IO并且可以在每个银行中出现一个外围设备,以允许灵活地选择IO电压。
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)下图的地址是:总线地址(并不是真正的物理地址)FSELn表示GPIOn, 下图给出第九个引脚的功能选择示例,对寄存器的29-27进行配置,进而设置相应的功能。
根据图片下方的register 0表示0~9使用的是register 0这个寄存器。
输出集寄存器用于设置GPIO管脚。
SET{n}字段定义,分别对GPIO引脚进行设置,将“0”写入字段没有作用。
如果GPIO管脚为在输入(默认情况下)中使用,那么SET{n}字段中的值将被忽略。
然而,如果引脚随后被定义为输出,那么位将被设置根据上次的设置/清除操作。
分离集和明确功能取消对读-修改-写操作的需要。
GPSETn寄存器为了使IO口设置为1,set4位设置第四个引脚,也就是寄存器的第四位。
输出清除寄存器用于清除GPIO管脚。
CLR{n}字段定义要清除各自的GPIO引脚,向字段写入“0”没有作用。
如果的在输入(默认),然后在CLR{n}字段的值是忽略了。
然而,如果引脚随后被定义为输出,那么位将被定义为输出根据上次的设置/清除操作进行设置。
分隔集与清函数消除了读-修改-写操作的需要。
GPCLRn是清零功能寄存器。

配置树莓派的pin4引脚为输出引脚:

功能选择 输出/输入(GPIO Function Select Registers)32位14-12 001 = GPIO Pin4 is an output

只需要将GPFSL0这个寄存器的14~12位设置为001就可以了。
只需要将0x6(对应的2进制是110)左移12位·然后取反再与上GPFSL0就可以将13、14这两位配置为0,然后再将0x6(对应2进制110)左移12位,然后或上GPFSL0即可将12位置1。

可使用copy_from_user()这个函数在驱动代码里面读取用户输入的指令,使用copy_to_user()这个函数让引脚反馈现在的状态,也就是让用户读取到。

若想找树莓派引脚点这里

树莓派IO操控驱动代码:ioremap、iounmap:

一. 一般我们的外设都是通过读写设备上的寄存器来进行的,通常包括控制寄存器、状态寄存器、数据寄存器三大类。
外设的寄存器通常被连续编址,并且根据CPU的体系架构不同CPU对IO端口的编制方式有两种:

IO映射方式(IO-mapped):比较典型的有X86处理器为外设专门实现了一个单独的地址空间,称为“IO端口空间”或者“IO地址空间”,此时CPU可以通过专门的指令(比如X86的IN和OUT)来访问这个“IO端口空间”。
内存映射方式(memory-mapped):RISC指令系统的CPU一般只实现一个物理地址空间,外设IO端口成为内存的一部分。
此时CPU可以访问外设的IO端口,就像访问自己的内存一样方便,不必再设置专门的指令来访问。
在驱动开发过程中一般使用内存映射方式。

二、 在驱动开发过程中,一般来说外设的IO内存资源的物理地址是已知的,由硬件的设计决定。
但是CPU不会为这些已知的外设IO内存资源预先指定虚拟地址的值,所以驱动程序不可以直接就通过外设的物理地址访问到IO内存,而必须要将其映射到虚拟地址空间(通过页表),然后才能根据内核映射过后的虚拟地址来通过内存指令访问这些IO内存,并对其进行操作。

三、 在Linux内核的io.h头文件中声明了ioremap()函数,用来将IO内存资源映射到核心虚拟地址空间(3Gb~4GB)中,当然不用了可以将其取消映射iounmap()。
这两个函数在mm/ioremap.c文件中:

开始映射:void ioremap(unsigned long phys_addr , unsigned long size , unsigned long flags)//用map映射一个设备意味着使用户空间的一段地址关联到设备内存上,这使得只要程序在分配的地址范围内进行读取或写入,实际上就是对设备的访问。
第一个参数是映射的起始地址第二个参数是映射的长度第二个参数怎么定啊?====================这个由你的硬件特性决定。
比如,你只是映射一个32位寄存器,那么长度为4就足够了。
(这里树莓派IO口功能设置寄存器、IO口设置寄存器都是32位寄存器,所以分配四个字节就够了)比如:GPFSEL0=(volatile unsigned int )ioremap(0x3f200000,4); GPSET0 =(volatile unsigned int )ioremap(0x3f20001C,4); GPCLR0 =(volatile unsigned int )ioremap(0x3f200028,4);这三行是设置寄存器的地址,volatile的作用是作为指令关键字确保本条指令不会因编译器的优化而省略,且要求每次直接读值ioremap函数将物理地址转换为虚拟地址,IO口寄存器映射成普通内存单元进行访问。
解除映射:void iounmap(void addr)//取消ioremap所映射的IO地址比如: iounmap(GPFSEL0); iounmap(GPSET0); iounmap(GPCLR0); //卸载驱动时释放地址映射
树莓派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");
1. 设置寄存器的地址

设置寄存器的地址,但是这样写是有问题的,我们上面讲到了在内核里代码和上层代码访问的是虚拟地址(VA),而现在设置的是物理地址,==必须把物理地址转换成虚拟地址==

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

我们先把地址初始

volatile unsigned int GPFSEL0 = NULL;volatile unsigned int GPSET0 = NULL;volatile unsigned int GPCLR0 = NULL;

在初始化int __init pin4_drv_init(void) //真实的驱动入口里赋值。

//整数11 //0xb 11 00010001 即便是16进制也是整数,左边是volatile unsigned int GPFSEL0 右边也强制转换成(volatile unsigned int)

volatile的作用是作为指令关键字,确保本条 ==指令不会因编译器的优化而省略==,==且要求每次直接读值== 因为它是地址我希望它是无符号的unsigned

我们在编写驱动程序的时候,IO空间的起始地址是0x3f000000,加上GPIO的偏移量0x2000000,所以GPIO的物理地址应该是从0x3f200000开始的

然后在这个基础上进行Linux系统的MMU内存虚拟化管理,映射到虚拟地址上。
用到了一个函数ioremap

//物理地址转换成虚拟地址,io口寄存器映射成普通内存单元进行访问GPFSEL0=(volatile unsigned int )ioremap(0x3f200000,4);GPSET0 =(volatile unsigned int )ioremap(0x3f20001C,4);GPCLR0 =(volatile unsigned int )ioremap(0x3f200028,4); //4是4个字节 2. 配置pin4引脚为输出引脚

配置pin4引脚为输出引脚 bit 12-14 配置成001

31 30 ······14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 0 ······0 0 1 0 0 0 0 0 0 0 0 0 0 0

//配置pin4引脚为输出引脚 bit 12-14 配置成001 GPFSEL0 &=~(0x6 <<12); // 把bit13 、bit14置为0 //0x6是110 <<12左移12位 ~取反 &按位与 GPFSEL0 |=~(0x1 <<12); //把12置为1 |按位或

忘记按位与 按位或 点这里

3. 获取上层write函数的值,根据值来操作io口,高电平或者低电平

用copy_form_user(char buf , user_buf , count)获取上层write函数的值

int usercmd;copy_from_user(&usercmd,buf,count); //将应用层用户输入的指令读如usercmd里面 //根据值来操作io口,高电平或者低电平 printk("get value\n"); if(usercmd == 1){ printk("set 1\n"); //置1 GPSET0 |= 0x01 << 4; //用 | 或操作 目的是不影响其他位 //写1 是让寄存器 开启置1 让bit4为高电平 } else if(usercmd == 0){ printk("set 0\n"); //清0 GPCLR0 |= 0x01 << 4; //用 | 或操作 目的是不影响其他位 //写1 是让清0寄存器 开启置0 让bit4为低电平 } else{ printk("undo\n"); //提示不支持该指令 }4. 解除映射

解除映射:void iounmap(void addr);//取消ioremap所映射的IO地址

void __exit pin4_drv_exit(void){ iounmap(GPFSEL0); //解除映射 GPFSEL0 iounmap(GPSET0); //解除映射 GPSET0 iounmap(GPCLR0); //解除映射 GPCLR0 device_destroy(pin4_class,devno);//先销毁设备 class_destroy(pin4_class);//再销毁类 unregister_chrdev(major, module_name); //卸载驱动}

上层测试代码:

#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}驱动卸载

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

IO口驱动代码编译首先在系统目录/SYSTEM/linux-rpi-4.14.y下使用指令:ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make modules对驱动模块进行编译生成.ko文件.然后将编译后的驱动发送到树莓派:scp ./drivers/char/pin4driver.ko pi@192.168.0.104:/home/pi,然后再将上层代码进行编译:arm-linux-gnueabihf-gcc pin4test.c -o realtest,然后再将测试代码传到树莓派:scp realtest pi@192.168.43.136:/home/pi/然后在树莓派上面使用指令:insmod pin4drive.ko进行加载驱动(然后lsmod即可查看到该驱动),然后使用指令:sudo chmod 666 /dev/pin4给予pin4这个设备可访问权限,还可以在虚拟机上面使用mk5sum查看驱动文件的值,并在树莓派上面使用该指令进行查看该驱动文件的值,看是否一致。
dmesg查看内核打印的信息,如下图所示:然后运行测试代码,在新建一个窗口,使用指令gpio readall可以看到BCM下面的4号引脚模式是输出模式,电平是低电平或高电平(根据输入的上层代码而定,输入0就是低电平,输入1就是高电平),这里我输入的是0,如下图所示:

有关驱动代码里面GPIO口地址的问题:

有关驱动代码里面GPIO口地址的问题:

7Ennnnn意思是7E00000到7EFFFFFF,F2000000是3F000000映射的虚拟地址,然后7E00000和F200000对应,芯片手册里面使用的是和虚拟地址F200000有着对应关系的地址——7E00000,芯片手册上面地址偏移多少物理地址就偏移多少根据上方图片描述,外设的物理地址范围是m 0x3F000000 to 0x3FFFFFFF,所以你看到的7E200000对应的实际物理地址应该是0x3F000000 + (7E200000-7E000000)GPFSEL0=(volatile unsigned int )ioremap(0x3f200000,4); GPSET0 =(volatile unsigned int )ioremap(0x3f20001C,4); GPCLR0 =(volatile unsigned int )ioremap(0x3f200028,4);0x3f200000,0x3f20001C,0x3f200028是物理地址,树莓派的外设空间的起始地址是0x3f000000,根据芯片手册可知对应寄存器的偏移量为,比如GPFSEL0寄存器的实际地址是0x3f200000=0x3F000000 + (7E200000-7E000000)然后通过数据手册可以看到,树莓派相关寄存器的总线地址(和映射的虚拟地址有某种对应关系的地址)进而可得知偏移量,如下图所示:

相关文章