【SoC FPGA学习】五、基于虚拟地址映射的 Linux 硬件编程,在已有工程上实现按键与LED的联动

参考教程中的以下章节:
在这里插入图片描述
小知识点:

  • hps_0.h 文件是qsys生成的FPGA侧外设
  • 0xfc00 0000~0xffff ffff一共是0xffffffff - 0xfc000000 + 1 = 0x04000000 个字节,即67108864个字节,即67108864 / 1024 /1024 = 64M

一、虚拟地址映射的介绍

传统的单片机得到的是外设寄存器的绝对地址,而在HPS中,得到的是经过MPU转化过后的虚拟地址(虚拟地址映射),这样CPU 访问外设寄存器就像直接访问内存总线上的某个地址一样方便了。这样在编写 Linux 应用程序的时候通过简单的操作完成虚拟地址映射,就能够非常方便的去操作这些外设 IP 了,无需再编写 Linux 内核驱动程序, 降低了开发难度。

基于虚拟地址操作的,在用户空间无法使用中断!!!

在 Linux 系统中实现虚拟地址映射主要包含三个步骤:

  • ①、得到存储器管理单元(MPU)的虚拟地址
  • ②、通过添加h2f_lw_axi_master 桥在 MPU上的偏移地址来得到 h2f_lw_axi_master 桥的虚拟地址
  • ③、添加 FPGA 侧具体外设在 h2f_lw_axi_master 桥上的偏移地址来得到该外设在 Linux 系统中的虚拟地址

示例如下图所示:
在这里插入图片描述

二、虚拟地址映射的实现

1、当 Linux 系统正常工作时,会在 dev 目录下存在一个名为 mem 的设备,该设备就是 MPU 了, 通过 open 函数打开该设备, 得到其文件描述符。程序实现如下所示:

int fd;
if ((fd = open("/dev/mem", ( O_RDWR | O_SYNC))) == -1) {
	printf("ERROR: could not open \"/dev/mem\"...\n");
	return (1);
}

2、使用该文件描述符来完成 h2f_lw_axi_master 总线的虚拟地址映射。程序实现如下所示:

void *periph_virtual_base;	//外设空间虚拟地址
periph_virtual_base = mmap( NULL, HW_REGS_SPAN, ( PROT_READ | PROT_WRITE),
			MAP_SHARED, fd, HW_REGS_BASE);

程序详细说明:首先定义了一个 void 类型的指针,名为 periph_virtual_base, 即外设虚拟地址,然后使用 mmap 函数将 MPU 上偏移地址为 HW_REGS_BASE 的地址映射为虚拟地址,并赋值给periph_virtual_base 指针。 映射的地址空间长度为 HW_REGS_SPAN 大小,映射方式为可读可写。 其中, HW_REGS_BASEHW_REGS_SPAN 为宏定义, 是与 HPS 中硬件外设相关的两个信息。其程序定义如下所示:

#define HW_REGS_BASE (ALT_STM_OFST )	//HPS外设地址段基地址
#define HW_REGS_SPAN (0x04000000 )		//HPS外设地址段地址空间

可以看到 HW_REGS_BASE 又被指向了一个名为 ALT_STM_OFST 的定义,该定义在 hps.h 中有定义:

#define ALT_STM_OFST        0xfc000000

HW_REGS_BASE 实际是被指定了一个确定的地址 0xfc000000。 那么这个地址又具体是什么意义呢? 这就要先从 SoC FPGA 中各个外设的地址分配说起, 在 HPS 侧,有一段地址段被专门用作了外设地址段(peripherals region), 该地址段是 HPS 统一内存空间最顶端的 64MB 空间,即从 0xfc000000~0xffffffff。在这段地址区间内,每一个地址段都被分配给了 HPS MPU 子系统的一个确定的外设, 而在这个地址段中,第一个外设就是 STM 模块,详见下表所示。

在这里插入图片描述
【上表来自:Cyclone V Device Handbook 第 3 卷第一章的 HPS Address Map节的 HPS Peripheral Region Address Map 小节。】

即STM 模块的基地址也就是整个外设区地址段的基地址,因此在这里将HW_REGS_BASE 定义为 ALT_STM_OFST, 实际上只是借用了 STM 的基地址来表达整个外设地址段的基地址,并不是说特指 STM 模块。

HW_REGS_SPAN 参数则被直接定义为了 0x04000000, 也就是 64MB 的大小,刚好也就是整个外设地址段的地址空间大小。

故,上述程序的含义为:将 MPU 上的整个外设地址段空间,共 64MB 的内容映射为虚拟地址,并赋值给定义的 periph_virtual_base 指针。

对于我们在 paltform designer 中添加的 IP, 都是连接到了 lw_h2f_bridge 上,而 lw_h2f_bridge 本身就是处在这个外设地址段中的, 上表中的LWFPGASLAVES 就是这个桥,该桥的基地址为 0xff000000, 共 2MB 的地址空间。

而每一个连接在 lw_h2f_bridge 上的 FPGA 侧外设都有一个基地址,该地址可以在qsys 生成hps_0.h 的头文件中看到,所以,一个特定外设的地址在 Linux 用户空间的虚拟地址就是:整个外设地址段映射得到的虚拟地址加上 lw_h2f_bridge桥的偏移地址, 再加上 lw_h2f_bridge 上该外设的基地址

对于 GHRD 工程中的 led_pio 核和 button_pio 核,其经过映射后的虚拟地址为:

//映射得到led_pio外设虚拟地址
led_pio_virtual_base = periph_virtual_base
		+ ((unsigned long) ( ALT_LWFPGASLVS_OFST + LED_PIO_BASE)
				& (unsigned long) ( HW_REGS_MASK));
//映射得到button_pio外设虚拟地址
button_pio_virtual_base = periph_virtual_base
		+ ((unsigned long) ( ALT_LWFPGASLVS_OFST + BUTTON_PIO_BASE)
				& (unsigned long) ( HW_REGS_MASK));

其中 LED_PIO_BASEBUTTON_PIO_BASEled_piobutton_pio 外设在 lw_h2f_bridge 上的基地址, 这是两个宏定义,在 hps_0.h 文件中定义, ALT_LWFPGASLVS_OFST 即为 lw_h2f_bridge 在外设地址空间中的基地址。

三、EDS-5导入工程和复制工程

3.1、导入工程现有的PIO工程

依次按照如下步骤进行操作即可:

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

注意:工程路径不要有中文,这里我直接拷贝到了桌面!

右键工程,选择Build Project即可编译工程!
在这里插入图片描述

3.2、复制工程现有PIO工程为test工程

①、右键要复制的工程,选择Clean Project

在这里插入图片描述

②、右键要复制的工程,选择Copy
在这里插入图片描述
③、空白处右击,选择Paste

在这里插入图片描述
④、更改新的工程名字

在这里插入图片描述
⑤、将新工程下的 Debug 目录删除,因为这些文件是之前工程编译出来的,在新工程中既不会被使用,也不会被自动删除,因此需要手动删除。

在这里插入图片描述
在这里插入图片描述
⑥、最后右击工程,选择Build Project编译工程

在这里插入图片描述

四、基于虚拟地址映射的 PIO 编程应用

在 AC501_SoC_GHRD 工程的 qsys 设计文件中,添加了两个 PIO 类型的外设,分别为 2 位的仅输出型 PIO, 用以驱动 LED, 和 2 位的仅输入型 PIO,用以连接轻触按键。 如下图所示:

在这里插入图片描述
在这里插入图片描述

本实验,将针对添加的这两个外设,在 DS-5 中编写应用程序, 首先完成虚拟地址映射,然后通过虚拟地址,读写 PIO 的寄存器,完成按键控制 LED 灯的功能。 通过该实验,展示通过虚拟地址映射方式操作外设的基本方法。

4.1、添加和包含 HPS 库文件

由于第一个实验只是简单的使用 Console 打印了一个“hello world”, 是一个最简单的应用程序,与特定外设硬件没有任何关系,因此在创建该工程时,并未添加任何额外的包含文件。 而在本例操作 led_pio 和 button_pio 时, 就涉及到了 SoC FPGA 中 HPS 的专用外设了,因此需要添加与基本的 HPS 硬件信息相关的头文件。 需要包含的基本头文件主要有 3 个,分别为:

#include "hwlib.h"
#include "socal/socal.h"
#include "socal/hps.h"

由于这些库是 SoC EDS 软件提供的, DS-5 中默认并没有包含该库,所以如果直接在程序中包含这些文件, DS-5 会提示找不到文件,因此需要在工程中设置头文件包含路径。

在 DS-5 中选中上文复制的 test 工程(当然pio工程也是可以的), 鼠标右击选择 Properties,或者直接选中 test 工程后, 输入键盘组合键 Alt+Enter,打开工程属性对话框。如下图所示。

在这里插入图片描述

点击 C/C++General 下的 Paths and Symbols 选项,在右侧的选项中,点击 GNU C 一栏,可以看到已经默认包含了很多的库文件。 点击 Add 按钮。如下图所示:

在这里插入图片描述

上图Include directories中默认加粗的两行为已有工程原作者添加的路径,如果你的路径与其一样,则可以跳过本步骤。

①、在弹出的对话框中输入 hwlib 库的头文件包含路径,例如我的D:\intelFPGA\17.1\embedded\ip\altera\hps\altera_hps\hwlib\include勾选上 Add to all configurationsAdd to all languages 选项, 然后点击 OK 即可,如下图所示:

在这里插入图片描述
②、使用通样的操作,将D:\intelFPGA\17.1\embedded\ip\altera\hps\altera_hps\hwlib\include\soc_cv_av路径添加到包含路径中。 然后点击 OK 即可,添加完成后的效果如下图所示:
在这里插入图片描述
该源码包定义了大量的针对 HPS 硬件的宏定义和底层操作函数, 在基于虚拟地址编程时,可以使用该源码包中的定义和函数来完成各种硬件外设的操作,避免了用户自行编写基于指针的操作函数。

下面就几个重要的 HPS 库文件作下简单介绍:

  • hwlib.h

该文件中主要针对与 HPS 硬件编程相关的一些常量进行了定义,比如各种标志信号,实际在基于 Linux 的应用编程中用户直接使用该文件中的内容较少。 但是该文件中有一个非常重要的条件编译选项需要关注,就是第 54行的

#if !defined(soc_cv_av) && !defined(soc_a10)
#error You must define soc_cv_av or soc_a10 before compiling with HwLibs
#endif

即该文件同时支持 Intel 的 Cyclone V、 Arria V、 Arria 10 SoC FPGA,针对不同的硬件平台, 部分底层硬件定义会有差别,因此需要根据程序选择的器件来选择底层定义。所以,一般在 main 函数所在文件中包含该头文件,用以检查是否制定了特定的硬件平台,如果没有定义,会在编译信息中提示。

在这里插入图片描述
指定专用硬件平台的方法最简单的就是在包含该头文件之前先定义硬件平台,例如在 main文件中, 按照如下顺序来包含该头文件,就能够成功指定硬件平台了:

//hps 厂家提供的底层定义头文件
#define soc_cv_av //定义使用 soc_cv_av 硬件平台

#include "hwlib.h"
#include "socal/socal.h"
#include "socal/hps.h"
  • socal.h

该文件中为一些基本的底层操作函数,如位、字节、 半字、字的读写等。

  • hps.h

该文件中对 HPS 中各种外设地址信息进行了定义,如上述提到的 ALT_STM_OFST。 该文件中的定义在进行虚拟地址映射时有用到。

4.2、添加 FPGA 侧外设硬件信息【生成或更新hps_0.h文件】

上述头文件仅仅是针对 HPS 的架构, 没有包含在 Platform Designer中添加的各种 FPGA 侧外设 IP

当我们需要对这些 FPGA 侧添加的外设进行操作时,还需要知道这些外设的硬件信息,如该外设在 lw_h2f_bridge 上的偏移地址。这些信息需要根据 Qsys 文件信息在中用命令脚本生成。

生成方式如下:

①、打开 SoC EDS Command Shell

在这里插入图片描述

②、使用 cd 命令切换到对应的 Quartus 工程目录下

例如本例讲述的AC501_SoC_GHRD 工程在笔者电脑上的位置为:C:/Users/wangxubo/Desktop/AC501_SoC_GHRD

在这里插入图片描述

③、输入./generate_hps_0.sh命令以执行 hps_0.h 文件生成脚本,即可在工程目录下生成或更新名为 hps_0.h 的头文件。

在这里插入图片描述
执行成功,会打印如下提示信息swinfo2header: Creating macro file 'hps_0.h' for module 'hps_0',然后就能在 Quartus 工程根目录下找到生成好的 hps_0.h 文件了。

在这里插入图片描述

④、将该文件复制,然后粘贴到 DS-5 中 test 工程(或者pio)下。双击打开该文件即可查看文件内容了。

在这里插入图片描述
可以看到, 在 Platform Designer 中添加的外设 IP,在该文件中都有相应的硬件信息, 例如 LED_PIO 外设的基地址为 0x10040,在 Patform Designer 中打开 soc_system.qsys 文件, 切换到 Address Map 一栏,可以看到 led_pio.s1 在 hps_0.h2f_lw_axi_master 总线上的地址范围为 0x0001_0040~0x0001_005f,所以 led_pio 外设的基地址为 0x10040, 结束地址为 0x1005f, 地址空间为 32 字节,如下图所示。

在这里插入图片描述

这与 hps_0.h 文件中 pio_led 外设的基地址(LED_PIO_BASE)、结束地址(LED_PIO_END)、 地址空间(LED_PIO_SPAN) 定义的值一致,因此,我们使用 hps_0.h 文件中的这些硬件信息,就能准确的操作 led_pio 外设了。

4.3、PIO IP 核介绍

通过上述操作,我们已经完成了 led_pio`和 button_pio的虚拟地址映射, 这些地址都是该外设的基地址, 那么要如何才能够正确的操作该外设呢?

例如,对于 led_pio,是用来驱动 led 灯的, 而 AC501-SoC 开发板上的两个FPGA 侧用户 LED 灯都是低电平点亮,高电平熄灭的。 所以,我们需要通过设置 led_pio 的某个引脚为低电平来点亮该引脚连接的 led 灯, 设置 led_pio 的某个引脚为高电平来熄灭该引脚连接的 led 灯。

Platform Designer 中提供的 IP 核都对应了有一个 IP 用户手册, 名为《Embedded Peripherals IP User Guide》在该手册中可以查看该 IP 核的相关信息,包括 IP 功能描述、 寄存器映射、寄存器功能等, PIO 核的信息在第 22节, 如下图所示。在该节中,包括了针对该 IP 核的简介(Core Overview)、功能描述(Function Description)、配置示例(Example Configurations)、配置说明(Configuration)、 软件编程模型(Software Programming Model)。使用该 IP前,需要先阅读该手册内容。

在这里插入图片描述
SoC FPGA 芯片中在 FPGA 侧使用可编程逻辑实现的 PIO 外设,其与 HPS 侧自带的外设在特性上有一定的差异, 该差异主要表现在硬件可裁剪上。即对于一个完整功能的 GPIO 来说, 一般支持配置为输入、 输出、 三态、边沿捕获、 中断等, 通过寄存器设置这些功能使能与否。 而 Platform Designer中的 PIO 核,则可以通过配置,选择是否具备这些功能。

例如本例中, 对于led_pio 外设,只需要使用该引脚作为输出控制 LED 灯, 无需使用输入功能,因此就可以将该 IP 核配置为仅输出功能,这样输入或三态功能逻辑就不会加入到 IP 核的逻辑代码中,编译时也就不会占用逻辑资源。 而对于 button_pio 外设,只需作为输入功能,读取按键引脚电平。另外也可以捕获按键信号的边沿,并产生中断。 所以选择使能边沿捕获功能, 下降沿捕获, 并产生边沿中断。 由于功能配置的差异, IP 核中的一些功能寄存器也会有差异, 接下来介绍PIO IP 核的寄存器映射和功能。

4.4、PIO 核寄存器映射

Avalon-MM 主机外设,例如 NIOS II CPU 或 HPS,可以通过 PIO 核提供的 4 个 32 位寄存器来对其实现控制和通信。

在这里插入图片描述

需要说明的是, direction、 interruptmask、 edgecapture、 outset、 outclear 寄存器可能并不存在。 具体根据在 Platform Designer 中添加该 IP 时的设置决定。

  • 如果设置为非三态端口(Bidir), 则 direction 寄存器不存在;
  • 如果设置为仅输出端口,或者设置为含输入功能的端口时没有使能中断,则 interruptmask 寄存器不存在;
  • 如果设置为仅输出端口,或者设置为含输入功能的端口时没有使能边沿捕获功能,则 edgecapture 寄存器不存在;
  • 如果没有勾选 Enable individualbit set/clear output register 选项,则 outset 和 outclear 寄存器将无效。

另外对于 edgecapture 寄存器,如果没有勾选 Enable bit-clearing for edgecapture register 选项,那么往 edgecapture 寄存器写入任意值将清零所有位的捕获状态,否则,往指定位写入 1 将只清零对应位的值

了解PIO IP 核的寄存器映射之后,就可以通过操作虚拟地址加对应寄存器偏移地址的方式来读写对应寄存器了。 接下来示例讲解如何通过具体的 C 语言来检测按键,点亮 LED 灯

4.5、编写按键与LED联动的Linux应用程序

4.5.1、I/O操作基础

①、LED PIO外设

编程时,可以直接使用指针的方式,对 PIO 外设的 data 寄存器对应位写入0,来驱动对应的 I/O 输出低电平,从而点亮 LED 灯, 例如设置 FPGA_LED0点亮, FPGA_LED 1 熄灭, 代码如下所示:

*(led_pio_virtual_base + 0) = 0x2; //LED0 亮, LED1 灭

或者使用数组下标的方式来指向 data 寄存器,代码如下所示:

led_pio_virtual_base[0] = 0x2; //LED0 亮, LED1 灭

0x2 的二进制值为 10b, 即第 0 位值为 0, 第 1 位值为 1, 写入 PIO 核的数据寄存器之后,就会驱动 PIO 核对应的输出端口连接的 I/O 值电平为 0 或 1,从而实现点亮或熄灭 LED 灯的功能。

上述代码往数据寄存器中写入确定的值,一次性操作了所有位的状态。 那么当我们仅希望对其中 1 位数据进行操作,不影响其他位的状态时,有两种方案可以选择:

  • 方案一:首先读取 PIO 核的数据寄存器的值, 到一个临时变量中,然后修改该变量中对应位的值为我们所希望的值,而不改变变量中其他位的值,然后再把修改好的临时变量的值重新写入数据寄存器。
  • 方案二:利用 PIO 核里面的输出设置/输出清零寄存器,在配置 IP 时勾选了 Enable individual bit set/clear output register(使能单个位设置/清除输出寄存器), 则可以通过往 outset 寄存器的对应位写 1来置位(置 1) 输出端口的对应位,或是往 outclear 寄存器的对应位写 1 来清零(置 0) 输出端口的对应位, 而其他位不受任何改变。

例如:仅希望点亮 FPGA_LED1, 而不影响 FPGA_LED 0 的状态, 使用这两种方案的代码分别如下所示:

方案一:数据回写方式代码:

unsigned int data;//定义数据寄存器临时变量
data = *(led_pio_virtual_base + 0); //读取数据寄存器的值到 data 中
data &= ~0x2; //将 data 变量中的 bit1 设为 0,其他位不变
*(led_pio_virtual_base + 0) = data; //将 data 值写回 PIO 核数据寄存器

方案二:使用清零寄存器方式代码:

//向 outclear 寄存器的 bit1 写 1,以清除输出端口中 bit1 的值
*(led_pio_virtual_base + 5) = 0x2;

对应的,如果要设置端口中 bit1 的输出状态为 1, 则可以写 outset 寄存器的 bit1 的值为 1 来实现, 代码如下所示:

//向 outset 寄存器的 bit1 写 1,以置位输出端口中 bit1 的值
*(led_pio_virtual_base + 4) = 0x2;

②、Button PIO外设

而对于 button_pio 来说,是一个仅输入型 PIO, 我们可以通过读取该 PIO的数据寄存器的值来获知连接在该 PIO 端口上的每一个按键的输出电平,从而判断按键有没有被按下。 使用指针方式读取 button_pio 数据寄存器的代码如下所示:

unsigned int button_data;//定义数据寄存器临时变量
button_data= *(button_pio_virtual_base +0);//读取 PIO 数据寄存器以获知按键状态

或者,也可以使用数组下标的方式来指定对应寄存器,代码如下所示:

button_data = button_pio_virtual_base[0];//读取 PIO 数据寄存器以获知按键状态

设计程序时, 在使能了边沿捕获功能的情况下, 也可以通过读取边沿捕获寄存器的值来判断按键有没有被按下过(按键按下过程中会产生下降沿,该下降沿会被边沿捕获功能捕获并存储在边沿捕获寄存器中)。使用指针方式读取button_pio 边沿捕获寄存器的代码如下所示:

unsigned int button_edge;//定义边沿捕获寄存器临时变量
//读取 PIO 边沿捕获寄存器以获知是否有检测到设定的边沿
button_edge = *(button_pio_virtual_base + 3);

或者,也可以使用数组下标的方式来指定对应寄存器,代码如下所示:

unsigned int button_edge;//定义边沿捕获寄存器临时变量
//读取 PIO 边沿捕获寄存器以获知是否有检测到设定的边沿
button_edge = button_pio_virtual_base[3];

对于边沿捕获寄存器中的值, 需要在程序中通过程序手动清除,以保证下一次边沿事件能够被正确捕获。 在勾选了 Enable bit-clearing for the edge captureregister 选项的情况下,向该寄存器中某一位写 1 将清除对应位的值。 如果没有使能该选项,则向该寄存器中写入任意值将清除所有位的值。 使用指针方式清除 button_pio 边沿捕获寄存器中 bit0 的值的代码如下所示:

//清除边沿捕获寄存器的 bit0 的值
*(button_pio_virtual_base + 3) = 0x1;

对于中断信号,由于中断需要在 Linux 内核驱动程序中定义什么的,才能注册使用,因此在基于虚拟地址映射的应用程序开发中无法使用。 所以,如果想使用外设的中断功能,则必须编写 Linux 内核驱动程序。

4.5.2、按键与LED的联动代码

本节将通过一个具体的实例,来完成基于 PIO 核的按键控制 LED 功能。 设计时,每当检测到 FPGA_KEY0 按下事件,就调整 FPGA_LED0 的状态(亮/灭),每当检测到 FPGA_KEY1 按下事件,就调整 FPGA_LED1 的状态(亮/灭)。 以下为完整程序清单:

//gcc标准头文件
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>

//hps 厂家提供的底层定义头文件
#define soc_cv_av	//定义使用soc_cv_av硬件平台

#include "hwlib.h"
#include "socal/socal.h"
#include "socal/hps.h"

//与用户具体HPS应用系统相关的硬件描述头文件
#include "hps_0.h"

#define HW_REGS_BASE (ALT_STM_OFST )	//HPS外设地址段基地址
#define HW_REGS_SPAN (0x04000000 )		//HPS外设地址段地址空间
#define HW_REGS_MASK (HW_REGS_SPAN - 1 )	//HPS外设地址段地址掩码

static volatile unsigned long *led_pio_virtual_base = NULL;	//led_pio虚拟地址
static volatile unsigned long *button_pio_virtual_base = NULL;	//button_pio虚拟地址

int fpga_init(long int *virtual_base) {
	int fd;
	void *periph_virtual_base;	//外设空间虚拟地址

	//打开MPU
	if ((fd = open("/dev/mem", ( O_RDWR | O_SYNC))) == -1) {
		printf("ERROR: could not open \"/dev/mem\"...\n");
		return (1);
	}

	//将外设地址段映射到用户空间
	periph_virtual_base = mmap( NULL, HW_REGS_SPAN, ( PROT_READ | PROT_WRITE),
			MAP_SHARED, fd, HW_REGS_BASE);
	if (periph_virtual_base == MAP_FAILED) {
		printf("ERROR: mmap() failed...\n");
		close(fd);
		return (1);
	}

	//映射得到led_pio外设虚拟地址
	led_pio_virtual_base = periph_virtual_base
			+ ((unsigned long) ( ALT_LWFPGASLVS_OFST + LED_PIO_BASE)
					& (unsigned long) ( HW_REGS_MASK));
	//映射得到button_pio外设虚拟地址
	button_pio_virtual_base = periph_virtual_base
			+ ((unsigned long) ( ALT_LWFPGASLVS_OFST + BUTTON_PIO_BASE)
					& (unsigned long) ( HW_REGS_MASK));
	*virtual_base = periph_virtual_base;	//将外设虚拟地址保存,用以释放时候使用
	return fd;
}

int main(int argc, char ** argv) {

	int fd;
	int virtual_base = 0;	//虚拟基地址
	unsigned int button_edge;	//定义边沿捕获寄存器临时变量

	bool led0 = true, led1 = true;

	//完成fpga侧外设虚拟地址映射
	fd = fpga_init(&virtual_base);

	//清除边沿捕获寄存器的bit0的值
	*(button_pio_virtual_base + 3) = 0x3;

	while (1) {
		//读取PIO边沿捕获寄存器以获知是否有检测到设定的边沿
		button_edge = *(button_pio_virtual_base + 3);

		switch (button_edge) {
		case 0x1:
			printf("FPGA KEY0\r\n");
			led0 = !led0; //对FPGA_LED 的bit0取反
			*(button_pio_virtual_base + 3) = 0x1; //清除边沿捕获寄存器的bit0位
			if (led0)
				*(led_pio_virtual_base + 4) = 0x1; //置位led_pio的bit0输出
			else
				*(led_pio_virtual_base + 5) = 0x1; //清零led_pio的bit0输出
			break;

		case 0x2:
			printf("FPGA KEY1\r\n");
			led1 = !led1; //对FPGA_LED 的bit1取反
			*(button_pio_virtual_base + 3) = 0x2; //清除边沿捕获寄存器的bit1位
			if (led1)
				*(led_pio_virtual_base + 4) = 0x2;	//置位led_pio的bit1输出
			else
				*(led_pio_virtual_base + 5) = 0x2;	//清零led_pio的bit1输出
			break;

		default:
			break;
		}
	}

	//程序退出前,取消虚拟地址映射
	if (munmap(virtual_base, HW_REGS_SPAN) != 0) {
		printf("ERROR: munmap() failed...\n");
		close(fd);
		return (1);
	}

	close(fd); //关闭MPU
	return 0;
}

程序中,通过读取 button_pio 外设的边沿捕获寄存器的值, 判断有无按键被按下,如果有按键按下,则根据边沿捕获寄存器的值确定是哪个按键被按下,然后设置对应的 LED 的状态, 如果是单个按键被按下,则通过写 led_pio外设中单 bit 输出置位和清零寄存器的值来关闭或打开对应的 led 灯。 同时,在每次判断完毕后会及时的清除 button_pio 外设中边沿捕获寄存器的值。

需要注意的是,该示例中, 直接使用的 while(1)的形式来保证程序的持续运行,并未做合理的退出机制。 如果要退出该程序,可在当前终端窗口中通过组合键 Ctrl+C 来完成程序的强制退出。

4.5.3、关于按键抖动需要说明的

机械按键在按下和释放时都存在抖动, 如果不对抖动进行合理的处理,就会影响软件程序的正确识别判定。 在 MCU 中, 一般都使用软件延时的方式来进行抖动滤除,此种方式会占用 CPU 软件资源。而在 FPGA 中,可以使用Verilog 编写数字逻辑完成抖动的滤除得到纯净的按键按下和释放边沿信号, 再将该纯净的边沿信号连接给 PIO 作为输入,从而降低软件编程时的复杂度。

在 AC501_SoC_GHRD 工程中,对输入的两个轻触按键的信号先进行了抖动滤除,然后再连接到 button_pio 的导出引脚上,实现按键输入的功能,如下图所示。因此在编程时, button_pio 的输入信号已经是不含抖动的按键信号了,无需在软件中进行抖动滤除功能,直接读取边沿捕获寄存器的值就能得到准确的按键按下事件。

在这里插入图片描述

4.6、运行按键与LED的联动程序

①、参考Hello World一节正确配置PC和开发板的网络,编写应用程序,右击所在工程,选择Build Project
在这里插入图片描述
②、参考Hello World一节正确配置FileZilla工具,拖曳编译生成的test程序到开发板上!然后在串口终端,为test可执行文件赋予可执行权限。

chmod 777 test

③、运行test可执行文件

./test

FPGA侧的按键及LED位置见下图

在这里插入图片描述

按下最右侧按键,对应KEY0,控制台输出FPGA KEY0,同时最上面的LED也会改变现有状态。

在这里插入图片描述

同理,按下次右侧按键,对应KEY1,控制台输出FPGA KEY1,同时次上面的LED会改变现有的状态。

在这里插入图片描述

4.7 最后需要说明的LED闪烁现象

参考:http://www.corecourse.cn/forum.php?mod=viewthread&tid=27860

AC501开发板上电后,会发现开发板上的两个FPGA侧IO驱动的LED开机后会默认闪烁,当用户做自己的实验时,如果用户希望关闭该程序,可以有两种方式关闭。第一种为临时关闭,该关闭操作会生效持续到下一次重启系统之前。
第二种为可以直接取消该应用程序的开机启动设置。

4.7.1、临时关闭法

1、首先需要知道该进程的名称,该进程的完整名称为/www/pages/cgi-bin/scroll_server
2、需要知道该进程对应的进程号,直接使用ps命令即可查询到该进程的进程编号。如下图所示:

在这里插入图片描述
3、在上图中可以看到,该进程的编号为780,则直接输入kill -9 780即可关闭该进程。(注,命令中的-9为数字九,不是英文字母g)
4、关闭之后,可以看到LED灯停止了闪烁。

4.7.2、彻底关闭法

1、输入以下命令打开vi编辑器编辑启动脚本文件

vi  /etc/rc5.d/S90gsrd_init.sh

2、打开该脚本后,将第9行最前面加上#,注释掉该行即可。如下图所示:

在这里插入图片描述
3、编辑完成后,记得使用“:wq”命令保存并退出

4、使用reboot重启系统,再看LED就不会闪烁了。

©️2020 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页