文章目录
前言
最近跟进实验室项目时,需要在原有工程基础上添加一个由PS端控制的GPIO来充当使能信号,但是由于该工程已经不再是裸机,而且sdk工程中并没有BSP工程(xilinx的基础硬件库),导致很多API并不能直接使用(如下图)。在我尝试为现有的应用工程创建配套的BSP工程并使两者成功链接后,发现链接后的程序无法运行,所以直接换了种思路,即直接通过控制寄存器方式来控制pl端的gpio,不再使用xilinx公司封装好的API。
一、实验环境
zcu102开发板,petalinux2019,vivado2019.1,sdk2019.1,Ubuntu 18.04。
涉及实现该功能的功能模块:
二、mmap
在前面的系列文章ZYNQ学习笔记(三):PL与PS数据交互—— UART串口+AXI GPIO控制DDS IP核输出实验中我们已经学习了如何使用xilinx提供的用于控制 GPIO设备的 API 函数(XGpio_DiscreteWrite等),但是如何在Linux系统下绕过这些API,直接通过寄存器方式来控制pl端的gpio呢——可以使用mmap函数。
2.1、什么是mmap?
mmp函数将一个文件或设备的内容映射到进程的虚拟地址空间中,从而使进程可以通过内存地址直接访问该文件或设备的内容。
简言之,mmap的工作原理如下:
1、内存映射:mmap将一个文件或对象的内容映射到进程的虚拟地址空间中。这意味着文件内容的部分或全部可以直接通过内存地址访问。
2、指针操作:映射完成后,进程可以像操作普通内存一样,通过指针读写这段内存。这种方式比传统的read和write系统调用更高效,因为它减少了数据在用户空间和内核空间之间的拷贝。
3、自动回写:当进程修改了映射的内存区域,系统会自动将这些修改回写到对应的文件磁盘上。因此,进程对内存的修改最终会反映到文件中,而不需要显式调用write系统调用。
4、内核空间修改:如果内核空间修改了这段映射的内存区域,这些修改也会直接反映到用户空间。这使得mmap可以实现不同进程之间的文件共享,因为它们可以通过映射同一个文件来共享数据。
什么是虚拟地址空间和虚拟内存区域?如图:
虚拟地址空间是指操作系统为每个进程提供的一个连续的地址范围。这些地址并不直接对应物理内存,而是通过操作系统的内存管理机制映射到物理内存。虚拟地址空间的目的是为每个进程提供独立的、隔离的内存视图,增强安全性和稳定性,同时简化内存管理。
进程的虚拟地址空间由多个虚拟内存区域组成。每个虚拟内存区域是一个具有相同特性的连续地址范围,即同质区间。虚拟内存区域包括text数据段(代码段)、初始数据段、BSS数据段、堆、栈和内存映射。这些都是独立的虚拟内存区域。
为内存映射服务的地址空间位于堆和栈之间的空余部分。
Linux内核使用vm_area_struct结构来表示一个独立的虚拟内存区域。如图:
由于每个虚拟内存区域的功能和机制不同,进程需要使用多个vm_area_struct结构来表示不同类型的虚拟内存区域。各个vm_area_struct结构使用链表或树形结构链接,以方便进程快速访问这些虚拟内存区域。vm_area_struct结构包含区域的起始和终止地址以及其他相关信息。同时,结构中还包含一个vm_ops指针,指向可以对该区域执行的所有系统调用函数。因此,进程对某一虚拟内存区域的任何操作所需的信息都可以从vm_area_struct中获得。
mmap函数的作用是创建一个新的vm_area_struct结构,并将其与文件的物理磁盘地址相连。这使得文件的内容可以直接映射到进程的虚拟地址空间中,从而实现高效的文件访问。
2.2、mmap函数原型
其函数原型为:
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
参数说明
addr:建议的映射起始地址,通常设为NULL,由内核决定映射的具体地址。
length:要映射的内存区域的长度。
prot:内存保护标志,决定映射区域的访问权限。常见的标志包括:
PROT_READ:页面可读。
PROT_WRITE:页面可写。
PROT_EXEC:页面可执行。
PROT_NONE:页面不可访问。
flags:映射选项,控制映射对象的性质。常见的标志包括:
MAP_SHARED:映射区域的修改会写回到文件,并且对其他进程可见。
MAP_PRIVATE:映射区域的修改不会写回到文件,并且对其他进程不可见(写时复制机制)。
MAP_ANONYMOUS:映射区域不与任何文件关联,通常与MAP_PRIVATE一起使用。
fd:文件描述符,指向要映射的文件。对于匿名映射(MAP_ANONYMOUS),fd设为-1。
offset:指定从文件的哪个位置开始进行映射,指定映射文件开始的位置。
2.3、利用mmap 方法直接控制 PL端GPIO的步骤
1、打开设备文件:在这个文件中,GPIO被映射到了内存地址空间中的一段区域。
2、映射 GPIO 寄存器:通过 mmap 函数将设备文件映射到用户空间的地址空间中。这样就可以直接访问该地址空间,从而访问 GPIO 寄存器。
3、访问 GPIO 寄存器:映射完成后,可以通过读写内存地址的方式来访问 GPIO 寄存器。这些寄存器包含了控制 GPIO 的各种配置和状态信息。
4、配置 GPIO:根据需要可以通过写入相应的寄存器来配置 GPIO 的功能、方向和状态等。
5、操作 GPIO:GPIO 配置完成后,就可以通过读写相应的寄存器来控制 GPIO 的状态,比如设置输出值或者读取输入值。
6、解除映射:最后,使用 munmap 函数解除对设备文件的映射,释放资源。
三、功能实现
3.1、确认设备树配置
因为用的petalinux来根据工程bit文件定制的linux系统,所以我们需要确认设备树中定义了该 GPIO 外设以及查看该外设的信息。
打开petalinux工程中的pl.dtsi文件,我们可以看到以下内容的定义:
gpio@a0002000 {
#gpio-cells = <3>;
clock-names = "s_axi_aclk";
clocks = <&zynqmp_clk 71>;
compatible = "xlnx,axi-gpio-2.0", "xlnx,xps-gpio-1.00.a";
gpio-controller ;
reg = <0x0 0xa0002000 0x0 0x1000>;
xlnx,all-inputs = <0x0>;
xlnx,all-inputs-2 = <0x0>;
xlnx,all-outputs = <0x1>;
xlnx,all-outputs-2 = <0x0>;
xlnx,dout-default = <0x00000000>;
xlnx,dout-default-2 = <0x00000000>;
xlnx,gpio-width = <0x1>;
xlnx,gpio2-width = <0x20>;
xlnx,interrupt-present = <0x0>;
xlnx,is-dual = <0x0>;
xlnx,tri-default = <0xFFFFFFFF>;
xlnx,tri-default-2 = <0xFFFFFFFF>;
};
这个定义告诉我们,AXI GPIO 的基地址是 0xa0002000,寄存器大小是 0x1000 字节。因此我们完全可以使用 mmap 函数将该地址映射到用户空间,并通过操作寄存器来控制 GPIO。
3.2、编写用户空间程序
假设我们通过 mmap 方法直接控制 PL 端的 GPIO,以产生一个周期为 2.4 毫秒(1.2 毫秒高电平和 1.2 毫秒低电平)的信号。
以下是一个简单的示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#define GPIO_BASE_ADDRESS 0xa0002000
#define GPIO_DATA_OFFSET 0x0
#define GPIO_HIGH 0x1
#define GPIO_LOW 0x0
#define GPIO_PERIOD_MICROSECONDS 2400 // 2.4毫秒
int main() {
int fd;
void *gpio_map;
// 打开设备文件
fd = open("/dev/mem", O_RDWR | O_SYNC);
if (fd < 0) {
perror("Failed to open /dev/mem");
exit(EXIT_FAILURE);
}
// 映射 GPIO 寄存器到用户空间
gpio_map = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, GPIO_BASE_ADDRESS);
if (gpio_map == MAP_FAILED) {
perror("Failed to mmap GPIO registers");
close(fd);
exit(EXIT_FAILURE);
}
// 设置 GPIO 为输出模式
*((unsigned int *)gpio_map + GPIO_DATA_OFFSET) |= (1 << 0);
// 产生信号
while (1) {
// 设置 GPIO 输出高电平
*((unsigned int *)gpio_map + GPIO_DATA_OFFSET) |= (1 << 0);
usleep(GPIO_PERIOD_MICROSECONDS / 2); // 高电平持续时间为周期的一半
// 设置 GPIO 输出低电平
*((unsigned int *)gpio_map + GPIO_DATA_OFFSET) &= ~(1 << 0);
usleep(GPIO_PERIOD_MICROSECONDS / 2); // 低电平持续时间为周期的一半
}
// 解除映射并关闭文件
munmap(gpio_map, 4096);
close(fd);
return 0;
}
这段代码将 GPIO 控制器的寄存器映射到用户空间,然后在一个循环中设置 GPIO 引脚的状态,以产生 1.2 毫秒高电平和 1.2 毫秒低电平的信号。这个程序并不复杂,我们可以结合2.2、2.3小节进行对照理解,这里不再赘述。
3.3、实验结果
工程编译后、进行下载验证,结果如图:
在Vivado中我利用15.36MHz的ILA,去观测该GPIO产生的信号,在理论上采样率是15.36MHz、1.2毫秒内的采样点数可以通过以下计算得到:1.2Ms × 15.36MHZ = 18432个采样点,与实验结果对照可见GPIO已经按设置正常输出~
总结
通过该实验自己掌握了一种在带Linux系统的情况下,如何通过mmap 函数将设备文件或设备的内容映射到进程的虚拟地址空间中,从而实现直接访问设备寄存器的方法,这确实也是一种收获~
道阻且长,还在路上~