1 初始Vitis

前面应该学习过ZYNQ的PL端的开发,还不涉及PS端。ZYNQ实际上是以PS端为核心,PL端作为它的外设,来去比方说做一个硬件加速了或者是对PS端做一个外设的扩展,那么要学习ZYNQ,一定是要学习PS端开发的。

前面所学的FPGA开发是通过Verilog语言,是偏底层硬件的开发和设计。而这里所学习的一个Vitis软件是开发它的PS端,使用C或者是C++。

另外需要说明的是,ZYNQ是PS+PL,而这个MPSoC也是PS+PL,他们只不过是ARM处理器以及PL端的一些资源等等有些差异,但他们的开发很类似,所以说下面的教程以ZYNQ为主要目标来去讲解,那么针对它们的差异地方也会去做一些说明。

1.1 Vitis是什么

AMD Vitis软件平台是一款开发环境,主要用于开发包括FPGA架构,ARM处理器子系统和AI引擎在内的设计。Vitis 工具与Vivado设计套件相结合,可以为开发提供更高层次的抽象设计。

说起这个Vitis,如果之前看过比较旧一点的资料的话,会发现它的一个嵌入式裸机开发工具是SDK,那么这个软件在升级之后就叫Vitis了。其实他俩开发方式有很多类似的地方,像这个SDK呢,它原来是集成在Vivado软件当中的,而后面这个SDK独立出来了,那么也就叫Vitis。

1.2 为什么学习Vitis

目的:为了发挥出ZYNQ/MPSoC软硬件协同设计的优势。

因为ZYNQ包含PS和PL的,并且以PS为核心,它俩是通过AXI接口进行互联的。在很多的使用场景中,单独用FPGA不好实现,单独用纯软件也不好实现,所以就需要它俩结合到一起。

那么在学习Vitis的时候,一方面学习它的PS开发,另一方面也是学习PS跟PL它俩是怎么协同设计的。

1.3 嵌入式系统的开发流程

上图是嵌入式部分的流程图。首先第一步就是创建Vivado工程,这个过程其实就跟我们前面做FPGA开发是一样的,就是通过Vivado软件创建一个工程。在创建好之后,就需要使用IP的集成器来创建一个PS系统,这个IP集成器是塞灵斯提供了的一个工具,在里面可以调用赛灵斯的官方IP核,然后像搭积木一样的方式去搭建整个系统.

接下来是生成这个顶层的HDL,就是说通过这个软件去创建一个顶层的模块,对前面所搭建的系统进行例化。那么接下来就是生成比特流然后把硬件导出,可以发现这个生成比特流打了个括号,意思是它是一个可选的,如果这个系统只有PS端,那么是没有必要生成这个比特流(也生成不了,因为里面是没有包含PL端的,因为比特流实际上是PL端的一个配置文件,如果说这个系统里面包含了PL,那么你在后面导出硬件之前就要生成比特流文件)。

接下来就进入Vitis的一个开发了,它是基于前面所搭建的硬件平台进行一个开发,那么最后就是进行板级验证。

1.4 ZYNQ/MPSoC最小系统

下面再来看一下ZYNQ的一个最小系统,下图是给出了ZYNQ的一个最小系统(ZYNQ的最小系统只包含了PS,不包含PL):

最小系统这里给出了两个条件:① 它是系统正常工作的最小条件;② 它是其他系统建立的基础。

最小系统中,至少包含一个ARM A9处理器,然后它还有一个DDR3存储控制器,与外部的DDR3存储芯片连接,程序是要运行在这个DDDR3上面的,另外有一些数据也可以存储在里面。接着还需要一个UART控制器,它通过MIO外设接到了USB_UART。那么为什么需要这个串口呢?其实大家如果说按照我们的文档操作的话呢,应该是所有历程都会包含这个UART,这个UART在我们后面开发过程当中会比较频繁的用到,可以通过它打印一些信息。

另外还有一点需要跟大家强调一下,这个DDR3控制器它并不是必须的,因为也可以把程序运行在这个OCM(片上存储器)里面,只不过这种程序运行在OCM上不典型,那么为什么不典型呢?主要是他的这个存储容量比较小,跟外置的DDR3肯定没法比。

1.5 参考资料

  • 正点原子文档:
    • 嵌入式Vitis开发指南
  • Xiinx官方手册:
    • Zynq-7000 SoC技术参考手册(UG585)
    • Zynq UIltraScale+技术参考手册(UG1085)

2 HelloWorld-ZYNQ最小系统搭建

2.1 创建Vivado工程

然后,会让你选择工程所在的路径。然后起一个工程名,这里就叫Hello_world。注意要选择Creat project subdirectory,让Vivado帮助我们管理工程目录。

然后选择创建RTL工程,注意选择Do not specify at this time,也就是我们在创建工程过程中不指定RTL文件。

然后点击下一步,选择开发板上的芯片型号。点击完成。

2.2 使用IP Integrator创建PS

双击ZYNQ打开PS端的配置:

首先是DDR配置:

接着是串口UART配置:

方法1,如下图所示:

方法2:如下图所示

配置串口的参数:

好到这里,DDR3和UART的引角包括配置就完成了。

在上讲解了怎么在Diagram的画布里面添加这个ZYNQ的IP核,添加IP核实际上就是对PS做一个配置,而并不是通过FPGA去搭建一个软核。

下面取消一些多余的接口,可以发现图中有个M_AXI_GP0_ACLK、M_AXI_GP0,它两前面的名称是一样的,那么它们就属于同一组类型的接口信号,那么这个M_AXI_GP0就是PS跟PL交互的一个GP0的接口,而M_AXI_GP0_ACLK就是对应接口的时钟,本次实验不包含PL端,那么肯定这个GP接口也不需要,所以可以把这个接口给它取消。步骤是:双击打开ZYNQ,按照图中操作

下图可以发现,这两接口就消失了。

图中还有个FCLK_CLK0是PS提供给PL端的一个时钟,同样双击打开ZYNQ,按照图中操作就可以去掉这个时钟了:

另外针对clock的配置,再展开说一下,其中最上面的Processor/Memory Clocks是处理器/memory的时钟,点开后可以发现CPU的时钟是666.666MHz,这个时钟来自ARM PLL,DDR的时钟频率是533.333MHz,这个时钟来自DDR PLL。

它们这些不同的时钟频率的最原始的时钟都是来自于图中上面的Input Frequency(MHz)输入时钟33.333MHz,也就是PS端的晶振。

图中还有个FCLK_RESET0_N是PS提供给PL端的一个复位信号,,同样双击打开ZYNQ,按照图中操作就可以去掉这个复位了:

到这里,不需要的接口就都取消了,变成如下的情况:

下面需要把有用的接口引出来。不需要我们手动引出,上图中,左上角有关Run Block Automation,点击之后,在弹出的页面点击OK,即可得到下面引出引脚的图:

到这里,这个block design就搭建好了。如何确认搭建的对不对呢?可按照上图中红色字提醒的操作进行验证。

下面就来让它产生输出的结果或者是输出的设计,操作如下图所示:

2.3 生成顶层HDL

如何生成顶层HDL呢?如下图所示:

点击OK之后,软件会自动生成一个Verilog代码,可以打开看一下:

另外前面在产生输出的结果(Generate Output Products)的时候,它实际上会产生一些网表文件、一些综合文件,包括一些IO约束,也就是在下图IP source里:

2.4 生成Bit流并导出Hardware

针对本次实验是不包含PL端的,所以说不需要生成Bit流,下面就直接导出硬件Hardware。如下图:

点击下一步之后,会继续弹出一个框,用于选择导出的这个Hardware文件的路径和名称,之后点击下一步,点击Finish导出即可。

导出之后,会产生一个.xsa文件,这个文件全称为Xilinx Support Archiver可以理解为是一个硬件平台文件(软件的设计会基于这个硬件平台文件来开发),后面在做Vitis开发的时候,或者是Linux的时候都会用到这个文件。它里面包含了一些硬件信息,比方说包含了处理器配置信息,还有一些外设的连接信息和器件初始化代码等等。

其实.xsa文件就是个压缩包,可以用压缩软件打开它,可以发现它里面包含很多文件。其实我们在Vitis软件当中基于这个.xsa文件去创建的时候,它会把这些文件都解压出来。正是因为有了这些文件,我们的软件程序才能够在ZYNQ处理器上面运行。

下面就可以打开Vitis软件进行嵌入式软件开发了。

2.5 在Vitis中进行软件设计

点击下一步之后,出现下面的界面:

之后出现如下界面:

点击下一步后,会出现一个“域”的设置界面:

点击下一步后,出现如下界面:

在src文件夹中,找到源文件helloworld.c,如下图所示:

编译工程:

编译完成后,会生成一个.elf文件,这个文件就是ARM处理器所运行的文件,就像前面PL侧的比特流文件。

接着就可以上板验证了,将板子硬件连接好之后就可以烧录程序了:

程序烧录到板子之后,可以打开串口调试助手,就可以接收到板子发送的代码中的两行字符串了。printf()本质底层是调用了UART串口打印的。

注意:

需要说明的是,可以发现这里其实直接就通过函数发送出去了,并没有发现对UART进行初始化,其实这个初始化以及包括整个DDR的初始化,都会有其他的函数去执行的,这些函数是早于这个main()函数去执行的,那么对UART、DDR等初始化完成之后,才会跳转到main()函数去执行,这些启动的细节有机会再来做一个详细的介绍。

2.6 小结

3 MIO-GPIO学习

3.1 MIO-GPIO简介

MIO:Multiplexed I/O,多路复用IO,MIO将来自PS外设和静态存储器接口的访问多路复用到PS引脚上(功能太多,引脚不够用)。

GPIO: General Purpose I/O,GPIO是ZYNQ的PS端的一个IO外设,用于观测(Input)和控制(Output)器件引脚的状态。

3.2 MIO与EMIO

MIO:Multiplexed I/O,多路复用IO,EMIO:ExtendedMIO,扩展的MIO。

  1. 当PS的引脚不够用的时候,可以使用EMIO进行扩展,从而使用PL侧的IO;
  2. 当某个设备硬件已经连接到PL端,但是通过Verilog代码实现较为复杂的时候,可以使用EMIO让PS来驱动。

图中,中间IOP是一个IO外设,其左侧是跟互联接口相连接的,IOP可以通过MIO连接到PS的引脚,当然很多引脚都是双向的。然后也可以通过EMIO连接到PL端,可以连接到一些用户逻辑或者是一些模块上面,也可以连接到PL端的IO引脚上。

3.3 Bank分组

3.3.1 PS的IO口Bank划分

物理电气分组 (Bank 500, 501, 502, 13, 33, 34, 35..):这源自FPGA的物理设计。为了管理不同的IO电气标准(如LVCMOS、LVDS、HSTL等)和电压(如1.8V,2.5V,3.3V),FPGA的脚被分成了多个物理Bank。

  1. 每个物理Bank有独立的VCCIO供电引脚。同一个物理Bank内的所有引脚必须使用相同的电压标准。
  2. 这套编号系统是所有Xiinx7系列FPGA(包括Artix, Kintex,Virtex, Zynq)通用的。
  3. 对于Zyng-7020芯片,其物理Bank编号通常包括:
    • Bank 500,501,502:这三个Bank对应的是PS部分的MIO引脚。
    • Bank 13,14,33,34,35:这些是PL部分的通用Bank。你的Zynq-7020具体有多少个PL Bank取决于封装型号(如CLG400,CLG484等)。

参考资料:ZYNQ7020 Bank划分 - CSDN

下面以CLG400封装和XC7Z020为例来看一下。

bank502专门用于连接DDR3,Bank500和Bank501是MIO引脚(数一下会发现总共58个,要除去其中的4个引脚,这4个引脚是连接时钟、复位、电源等,所以实际可用的MIO数量为54)

3.3.2 GPIO口Bank的划分

注意,3.3.1小节的Bank划分和3.3.2小节的Bank划分不是一个概念。

3.4 GPIO的特性

  1. 软件通过GPIO外设可以观测和控制54个MIO和64个EMIO(Zynq-7000)
  2. 每个GPIO可以独立且动态的编程,作为输入/输出以及中断模式
  3. GPIO被分成了四个BANK,即四个寄存器组(Zyng-7000)
  4. 软件通过一组存储器映射(memory-mapped)的寄存器来控制GPIO

补充:存储器映射(memory-mapped)

对于所有外部设备而言,CPU一律把它们看作地址空间,如果这些地址空间挂载的是外设(比如GPIO、UART、SPI等),那么CPU的寻址就映射成寄存器;如果挂载的是存储器(比如说DDR3、DDR4),那么CPU的寻址就映射为存储器的地址。

GPIO控制和状态寄存器的存储器映射基地址为0xE000_A000(Zynq-7000系列)

3.5 GPIO寄存器

前面提到了存储器映射的概念,也就是说当对某个外设进行操作的时候,需要提供一个地址才能对它进行读写操作,那么针对这个GPIO外设进行操作的时候,同样也需要提供一个地址,这个地址其实就是一个寄存器的地址。对GPIO所有的配置、操作都是通过操作它的寄存器来去实现的。

本节先不看中断相关的寄存器,先看GPIO读写操作相关的寄存器。

  • 图中可以发现寄存器主要分为四大类:
    • 首先是DATA_RO,这是一个数据读寄存器,简单说就是读取GPIO引脚的电平。
    • 接着是数据寄存器,简单说就是控制GPIO引脚的电平
      • DATA:是数据写入寄存器(32位)
      • MASK_DATA_LSW:是数据写入寄存器(带屏蔽功能,低16位)
      • MASK_DATA_MSW:是数据写入寄存器(带屏蔽功能,高16位)
    • 然后DIRM是方向模式寄存器,控制GPIO方向是输入还是输出。
      • 要注意,输入其实是一直有效的,即使你把GPIO设置为输出模式,也可以读取引脚电平
    • 最后OEN是输出使能寄存器。

3.6 GPIO官方数据手册阅读

…… …… ……

3.7 GPIO的LED实验

3.7.1 实验任务

本节的实验任务是使用PS端的MIO控制两个LED,实现LED闪烁的效果,闪烁间隔为1s。硬件连接如下图所示,这里只用到了下面的PS端的2个LED灯。

3.7.2 系统框图

本次系统框图其实就是在最小系统的基础上,添加一个GPIO外设。总之,本次实验包括A9处理器、DDR3控制器(它连接到外部DDR3存储器芯片,作为程序的运行空间),还需要UART打印信息,这是最小系统,在此基础上添加了GPIO的一个控制器,然后把GPIO连到PS上的两个引脚,来控制两个灯的亮灭。

在了解了这个系统框图之后,下面就可以搭建这个系统了。在此之前,可以先做一个思路的整理:

  • 硬件设计部分
    • 在最小系统的基础上,添加一个GPIO外设就可以了;
  • 软件设计部分
    • 思路1:基于直接操作寄存器的方式
    • 思路2:基于库函数的方式

3.7.3 工程搭建

  • 创建Vivado工程
    • 打开Vivado软件,创建工程,参考上面的2.1小节。
  • 使用IP Integrator创建PS
    • 点击Create Block Design,填写模块名称;
    • 添加ZYNQ的PS的IP核;
    • 配置DDR3、UART;
    • 删除不需要的信号;
    • 在MIO Configuration中配置GPIO外设;
    • 点击Run Block Automation,自动连接端口;
    • 验证设计是否正确。
    • 产生输出的结果(Generate Output Products)
  • 创建顶层HDL
  • 生成Bit流并导出Hardware
  • 打开Vitis开始软件设计

3.7.4 程序编写

(一)思路一:寄存器方式

请参考第3.5讲视频讲解

(二)思路二:库函数方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <stdio.h>
#include "xparameters.h"
#include "xgpiops.h"
#include "sleep.h"

#define GPIO_DEVICE_ID XPAR_XGPIOPS_0_DEVICE_ID
#define MIO_LED0 7 // 要控制的引脚号 MI07

int main()
{
printf("GPIO MIO Test!\n");

XGpioPs_Config* ConfigPtr; // 配置结构体指针,用于从硬件配置信息中读取基地址等只读参数
XGpioPs Gpio; // GPIO器件的驱动实例

/*======== 对GPIO驱动进行初始化 ========*/
// 根据器件ID来去查找器件的配置信息
ConfigPtr = XGpioPs_LookupConfig(GPIO_DEVICE_ID);
// 对GPIO的驱动进行初始化
XGpioPs_CfgInitialize(&Gpio, ConfigPtr, ConfigPtr->BaseAddr);
// 设置引脚的方向
XGpioPs_SetDirectionPin(&Gpio, MIO_LED0, 1);
// 设置输出使能
XGpioPs_SetOutputEnablePin(&Gpio, MIO_LED0, 1);

while(1)
{
// 对引脚输出高电平,点亮LED灯
XGpioPs_WritePin(&Gpio, MIO_LED0, 0x1);
sleep(1);
// 对引脚输出低电平,熄灭LED灯
XGpioPs_WritePin(&Gpio, MIO_LED0, 0x0);
sleep(1);
}
return 0;
}

XGpioPs_LookupConfig 的作用是:软件拿着一个“身份证号码”(ID),去系统数据库里查出这个设备的“家庭住址”(寄存器基地址)。

为了彻底搞清楚,我们需要区分两个概念:“控制器(管理者)的ID”“引脚(具体员工)的编号”MIO7 和 MIO8 的“身份证号码(ID)”是一样的,但它们的“工号(引脚号)”不同。

  1. “身份证号码”到底是谁的?

首先必须纠正一个非常容易产生的误解:

  • 这个 ID 不是给 MIO7 的。
  • 这个 ID 也不是给 MIO8 的。
  • 这个 ID 是给“GPIO 控制器”这个硬件模块的。

想象一家公司:

  • GPIO 控制器 = 部门经理(他负责管理手下的一堆引脚)。
  • MIO7, MIO8 = 部门里的员工

当你调用 XGpioPs_LookupConfig(GPIO_DEVICE_ID) 时,你实际上是在找“部门经理”。

  • 你告诉软件:“我要找 GPIO 0 号部门的经理。”
  • 软件就把“0 号部门经理办公室的地址”(基地址)给你。

无论你想操作员工 MIO7 还是员工 MIO8,因为他们都属于同一个部门(都在 GPIO 0 控制器里),所以你找的“部门经理 ID”是同一个。

  1. 编程时如何写这个 ID?是背下来还是查?

绝对不是背下来,是查出来的! 而且通常不需要你去手动查数字,而是直接复制宏定义名字。

  • 步骤一:找到“花名册”(xparameters.h)

如前所述,这个文件在工程目录的 bsp 文件夹里。打开它,你会看到类似这样的内容:

1
2
/* 定义 GPIO 控制器的 ID */
#define XPAR_XGPIOPS_0_DEVICE_ID 0 <-- 这就是硬件分配的部门ID
  • 步骤二:在代码中“抄写”

你不需要写 0(虽然写 0 也能跑,但不规范)。你需要把这个名字 XPAR_XGPIOPS_0_DEVICE_ID 抄到你的代码里。

4 EMIO-GPIO学习

上面是ZYNQ框图,可以发现IO外设(IO Peripherals)既可以通过MIO来连接到PS的IO,也可以连接到EMIO,从而连接到PL侧的IO。同时,从图中可以看到存储器接口(Memory Interface)只能连接到MIO,不能连接到EMIO。

注意:

其实这个IO外设通过EMIO连接到PL的话呢,它除了能够连接到PL端的引脚,它也支持连接到PL端内部的一个逻辑或者是模块。当然连接到PL端的IO,它的使啊更为更为广泛。

另外可以发现这个框图里面,有些部分是有方框的,比方说MIO或者是XADC,而有些是没有这个方框,比方说这个EMIO和GP(General-Purpose Ports)。带方框表示它是一个功能模块,而没有方框表示它是一个接口,像这个EMIO、GP等,都是属于接口。

上图是展示了PS的IO外设既可以连接到PS的引脚,也可以连接到PL端的引脚。PS I/O外设连接到PS端的引脚是通过MIO的一个多路复用器,而连接到PL端的引脚是通过EMIO的接口。当然了,它除了可以连接到PL端的引脚之外,也可以直接连接到PL端的内部逻辑或者PL端的一些模块上。

4.1 EMIO基础概念

EMIO:ExtendedMIO,扩展的MIO。

EMIO是PS和PL之间的接口,主要用于当PS的引脚不够用的时候,可以使用EMIO来进行扩展,从而使用PL的IO。

下面的表格,给出了针对这个IO外设,它连接到MIO或者是连接到EMIO的差异。

TTC是一个定时器,那么针对每个定时器,它可以引出的端口或者是信号的个数不一样,上面表格中可以看出,针对MIO它是一对,而针对EMIO它是三对。

然后下面是SWDT,它是一个看门狗的定时器,上面表格中可以看出,这个对MIO和EMIO是一样的。

然后下面是SMC(静态存储的控制器),针对MIO这个静态存储控制器是可以连接到SRAM或者NAND Flash,但是它不支持连接到EMIO,所以说在最初设计基于ZYNQ原理图的时候,这些是需要了解的。

还有Quad-SPI是Flash控制器,它支持一个单线双线和四线。但针对EMIO是不支持的。

再往下是一个SDIO的控制器,针对PS端的SDIO控制器有两个,主要是用来连接比方说SD卡或者是EMMC,针对MIO或者是EMO,区别在于它所支持的频率不一样。

然后是本节所需要学习的一个重点,就是针对这个GPIO外设,它连接到MIO是支持到54个IO(使用的GPIO Bank 0和Bank 1),而针对EMIO是支持64个引脚的,然后它俩最重要的差异在于是否支持输入输出,对MIO来说,它的引脚支持输入和输出,就是单根线支持输入和输出,而EMIO它是控制三个引脚,分别是输入、输出和一个三态引脚,也就是说这个GPIO外设是直接引出三根线连接到PL端。

接着是一个USB,MIO支持Host、device、OTG等几种模式,而EMIO不支持USB。

然后再往下面是一个以太网的控制器Ethernet[0,1],因为有了这个以太网的控制器,程序里面对PS网口的控制会变得比较简单。这个以太网的控制器有两个(0、1),MIO支持的这个接口是RGMII,EMIO支持的接口类型是MII或者是GMII,RGMII相比于GMII,它的引脚更少。

这里再来展开给大家讲一下,领航者开发版它是有两个网口,一个是PS的网口,一个是PL端的一个网口,板载的这两个网口的一个芯片的接口类型都是RGMII,包括领航者出厂测试程序,它是支持对这两个网口的控制,它都是使用的这个以太网的控制器。它是有两个嘛,刚好一个接PS,一个接PL,但是板子的这两个网口的以太网接口类型都是RGMII。

这就有问题了,上面图中可以看到,通过EMIO连接到PL端那个网口,它只支持GMII(GMII不支持RGMII),那该怎么处理呢?

其实也比较简单,相当于我这个以太网的控制器Ethernet再接一个FPGA模块,这个模块实现的功能就是GMII到RGMII接口的一个转换。然后这个模块再引到PL端的引脚,那么PL端就是一个RGMII的接口了。

那么大家发现没有,其实上面这种解决方式就是前面提到的:IO外设接到FPGA的一个内部的模块,然后转换之后再通过这个IP输出端口连接到PL端引脚。所以说这种方式相当于是EMIO连接到FPGA的一个功能模块上面去的。

后面还有一些什么SPI、CAN、UART等,可以通过看UG58手册去查询它的一个使用和差异点在哪里。后面在使用的过程当中呢,也会去详细的介绍它这个差异。

那针对GPIO的话,主要是知道它所使用的这个GPIO Bank是不一样的。

4.2 GPIO连接MIO和EMIO差异

  • 对于MIO来说,当输出除能时,输出端口处于三态(3-state)
    • 输出除能:也就是说你是取消对这个输出引脚的控制,那么它的整个引脚的状态就是由输入所决定的
    • 三态:指三种状态,分别是高电平、低电平还有高阻态
  • 对于EMIO来说,输出端口不支持三态,因此不受OEN寄存器的影响;
    • EMIOGPIOTN[x] = DIRM[x] & OEN[x],这个EMIOGPIOTN三态控制引脚等于DIRMOEM两个寄存器想与,实现输出的控制。

上图中针对MIO,最右侧是PS的一个IO,下面是一个三态控制引脚,左侧是输出引脚,上面是输入引脚。当这个三态打开的时候,就表示输出是一个高阻状态,所以说此时输出引脚就是断开的。此时就可以把这个IO作为输入去采集IO引脚的电平。而当这个三态关闭的时候,输出引脚就可以控制最右侧IO引脚的状态输出高低电平。

对于EMIO,其输出端口不支持三态,并且不受OEN寄存器的影响。注意这里只是说这个输出端口它不支持3态,其实EMIO它是这三个端口(参考3.3.2小节的图),最终体现在PL端的一个IO是一根线,这根线既支持输入又支持输出。它是怎么做到的呢?这个就用FPGA的一个内部逻辑实现,也就是它有一个专用的I/O Buffer,图中三根引脚连接到I/O Buffer,然后连接到PL端的一个引脚,它就是单根线,是可以支持输入和输出的。一般在Vivado软件里面操作的时候,其实我们只需要把EMIO引到端口,然后工具它会自动添加一个I/O Buffer的。所以说一般也不需要我们程序再去做一个特殊的处理。

4.3 实验任务实现

4.3.1 实验任务

本次的实验任务:使用两个用户按键分别控制PS端的两个灯的一个亮灭。这两个灯都是MIO的一个端口,这两个按键一个是通过MIO连接(相当于是PS端的一个按键),另外一个是通过EMIO连接(相当于PL端的一个按键)。

4.3.2 硬件设计

上图是本次实验的硬件设计部分,从这个原理图中可以发现,无论是PL端的按键还是PS端的按键,在按键没有被按下的时候是高电平,按键按下之后,连接到GND,所以说按下之后就是低电平。

4.3.3 系统框图

上图是本实验的系统框图,从这个框图中可以发现:首先处理器(arm Cortex-A9)、UART、DDR3是一个最小系统所需要包含的。图中两个按键分别是PS端和PL端的,一个连接到MIO,一个连接到EMIO。

注意:本实验用到了PL侧的EMIO,所以需要生成比特流文件

4.3.4 编程实现任务

思路:首先先在3.7小节的基础上,实现PS端按键控制灯亮灭的任务,然后再更改Vivado工程,把PL端按键加入进来。

前面的工程创建部分,请参考视频:https://www.bilibili.com/video/BV1Et421H75m?t=98.6&p=20

工程搭建好之后,首先实现PS端的按键控制灯亮灭的程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <stdio.h>
#include "xparameters.h"
#include "xgpiops.h"
#include "sleep.h"

#define GPIO_DEVICE_ID XPAR_XGPIOPS_0_DEVICE_ID
#define MIO_LED0 7 // 要控制的引脚号 MIO7
#define MIO_KEY0 12 // PS_KEY0

int main()
{
printf("GPIO MIO Test!\n");

XGpioPs_Config* ConfigPtr; // 配置结构体指针,用于从硬件配置信息中读取基地址等只读参数
XGpioPs Gpio; // GPIO器件的驱动实例

/*======== 对GPIO驱动进行初始化 ========*/
// 根据器件ID来去查找器件的配置信息
ConfigPtr = XGpioPs_LookupConfig(GPIO_DEVICE_ID);
// 对GPIO的驱动进行初始化
XGpioPs_CfgInitialize(&Gpio, ConfigPtr, ConfigPtr->BaseAddr);
// 设置LED灯引脚的方向(输出)
XGpioPs_SetDirectionPin(&Gpio, MIO_LED0, 1);
// 设置LED灯引脚的输出使能
XGpioPs_SetOutputEnablePin(&Gpio, MIO_LED0, 1);

// 设置按键PS_KEY0引脚的方向(输入)
XGpioPs_SetDirectionPin(&Gpio, MIO_KEY0, 0);

u32 key0_value;
while(1)
{
// 获取按键值
key0_value = XGpioPs_ReadPin(&Gpio, MIO_KEY0);
// 用按键值控制LED灯
XGpioPs_WritePin(&Gpio, MIO_LED0, ~key0_value);
}
return 0;
}

下面,需要通过EMIO扩展出一个PL端的引脚,此引脚接一个按键实现对PS端灯的控制,所以说下面就返回到Vivado工程,打开框图设计:

双击打开ZYNQ的IP核,对GPIO进行配置:

如图修改完之后,点击OK,在系统框图上就出现了GPIO的一个接口:

展开GPIO_0之后,可以发现有三个端口,GPIO_I表示一个输入端口,GPIO_O表示一个输出端口,GPIO_T表示是一个三态控制(用于控制引脚方向的)。要把它引到PL端的一个端口上面去,方法如下图所示:

图中端口名称GPIO_EMO_KEY是可以在左侧栏进行修改的。

上图中,选择generate output products后,就耐心等待编译完成。编译成功之后,就看一下其顶层模块有没有把这个端口加进来:

这个时候可以把顶层模块这个文件给先关掉,然后把这个顶层模块移除,然后重新去创建这个HDR的一个顶层模块:

现在重新生成了顶层模块,双击打开看一下:

可以发现这里多了一个端口,就是我们刚才那所添加的一个端口——GPIO_EMIO_KEY_tri_io(软件这里重新命名了这个端口)。

然后继续往后面翻的话,如下图:

可以发现他这里加了一个I/O Buffer,它将PS端所输出的三个引脚_T_I_O经过这个IO Buffer之后,它变成了一个双向的IO引脚连接到了这个端口。

下面对这个IO端口进行约束,因为他是要连接到PL端的引脚,所以说下面打开原理图:

根据图中进行设置后,右键RTL ANALYSIS把这个界面关闭掉,然后它会生成一个.xdc文件:

比特流文件编译好了之后呢,我们就把它导出到硬件(选择File -> Export -> Export Hardware)

导出过程,会有一些选项,注意下图中,要选择比特流文件:

接着,回到Vitis软件,我们前面已经编写好了一部分代码,当Vivado软件更新了硬件之后,那么在Vitis软件中怎么把更新后的硬件加进来呢?如下图所示:

点击OK后,提示硬件更新完成。

然后找到板级支持包,对它进行复位:

注意,FSBL也给它复位一下(上图中的蓝色字),这个是跟固化相关的。

复位完成之后,重新去编译一下这个平台工程:

  • PL侧按键控制程序编写

其实EMIO引脚的控制跟MIO是非常类似的,只不过它属于不同的bank,所以说这里的引脚编号有差异。那么这个针对EMIO的编号应该是多少呢?我们可以看一下配置函数XGpioPs_CfgInitialize()里面。如下图,对于ZYNQ的MPSOC(性能强于ZYNQ 7000系列)

那么针对ZYNQ 7000系列呢,如下图:

EMIO是从Bank2开始的,所以说它的标号就是从54开始的,因此这里来定义一个EMIO_KEY0,是PL端的,它的编号就应该是54

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <stdio.h>
#include "xparameters.h"
#include "xgpiops.h"
#include "sleep.h"

#define GPIO_DEVICE_ID XPAR_XGPIOPS_0_DEVICE_ID
#define MIO_LED0 7 // 要控制的LED0引脚号 MIO7
#define MIO_LED1 8 // 要控制的LED1引脚号 MIO8
#define MIO_KEY0 12 // PS_KEY0
#define EMIO_KEY0 54 // PL_KEY0

int main()
{
printf("GPIO EMIO Test!\n");

XGpioPs_Config* ConfigPtr; // 配置结构体指针,用于从硬件配置信息中读取基地址等只读参数
XGpioPs Gpio; // GPIO器件的驱动实例

/*======== 对GPIO驱动进行初始化 ========*/
// 根据器件ID来去查找器件的配置信息
ConfigPtr = XGpioPs_LookupConfig(GPIO_DEVICE_ID);
// 对GPIO的驱动进行初始化
XGpioPs_CfgInitialize(&Gpio, ConfigPtr, ConfigPtr->BaseAddr);
// 设置LED灯引脚的方向(输出)
XGpioPs_SetDirectionPin(&Gpio, MIO_LED0, 1);
XGpioPs_SetDirectionPin(&Gpio, MIO_LED1, 1);
// 设置LED灯引脚的输出使能
XGpioPs_SetOutputEnablePin(&Gpio, MIO_LED0, 1);
XGpioPs_SetOutputEnablePin(&Gpio, MIO_LED1, 1);

// 设置按键PS_KEY0引脚的方向(输入)
XGpioPs_SetDirectionPin(&Gpio, MIO_KEY0, 0);
// 设置按键PL_KEY0引脚的方向(输入)
XGpioPs_SetDirectionPin(&Gpio, EMIO_KEY0, 0);

u32 ps_key0_value;
u32 pl_key0_value;
while(1)
{
// 获取按键值
ps_key0_value = XGpioPs_ReadPin(&Gpio, MIO_KEY0);
pl_key0_value = XGpioPs_ReadPin(&Gpio, EMIO_KEY0);

// 用按键值控制LED灯
XGpioPs_WritePin(&Gpio, MIO_LED0, ~ps_key0_value);
XGpioPs_WritePin(&Gpio, MIO_LED1, ~pl_key0_value);
}
return 0;
}

5 GPIO之MIO按键中断

5.1 中断简介

5.1.1 中断概念

中断(Interrupt):打断CPU执行正常的程序,转而处理紧急程序,然后返回原程序暂停的地方继续运行,就叫中断。

5.1.2 如何实现中断

在ZYNQ里面有一个专业名词叫“GIC”,也就是通用中断控制器。这个中断控制器它就可以监测不同的事件。

针对GPIO的中断,它是怎么实现这个中断功能的呢?

上面这个图在3.5小节展示过,当时主要是学习的下面这部分普通GPIO功能。这个图的上半部分主要是跟中断有关的,那么从上半部分左边的寄存器可以发现INT就是interrupt的缩写。

图中可以发现,input线除了连接到DATA_RO,还向上连接到Interrupt Detecion Logic(中断检测逻辑)。这个中断检测逻辑的功能就是对一个输入信号进行监测,比方说监测一个上升沿或者是下降沿等等,具体是通过INT_TYPEINT_POLARITYINT_ANY这三个寄存器来配置的。

  • INT_TYPE控制是边沿触发还是电平触发中断
  • INT_POLARITY配置中断的极性(正、负),这个极性就是进一步去配置中断检测逻辑,个比方说配置了电平触发,电平触发也分为两种,可以配置为低电平触发还是高电平触发。
  • INT_ANY该寄存器仅在INT_TYPE配置为边沿触发的时候有效,该寄存器就是配置是否是双边沿都触发中断。

图中可以发现,当检测到一个中断的信号之后,会连接到一个DQ触发器,表示中断状态。可以发现INT_STAT中断状态寄存器有一根线Write-1-to-clear指向DQ触发器,如果向INT_STAT中断状态寄存器中写入1,则就可以清除这个中断。

另外,也可以发现DQ触发器的Q端输出的信号也连接到INT_STAT中断状态寄存器,则可以通过读取中断状态的寄存器查看有没有产生中断。

接着,DQ触发器的Q端输出之后,进入一个与门,与门的另一个输入连接左上角的三个寄存器,这三个寄存器是干嘛的呢:

  • INT_MASK:是中断屏蔽寄存器,通过读取这个寄存器判断哪些中断位被开启了,哪些位被屏蔽了。
  • INT_DIS:中断关闭寄存器
  • INT_EN:中断使能寄存器

所以说,如果想要屏蔽掉某个引脚的中断,就设置INT_DIS给它禁止掉,如果想打开某个引脚的中断,就设置INT_EN

如果说某个引脚使能中断,那么检测到中断之后,就会产生一个IRQ(中断请求),这个中断请求给到GIC(通用中断控制器)。那么这个GIC是在哪里呢,如下图:

上图中黑色框内就是GIC,GIC的左侧有一根IRQ(中断请求)线连接过来,这个中断请求可以来自于IO外设,不同的IO外设都可以产生中断。

另外,在前面一张图中,可以发现IRQ #52 to GIC这里有一个#52,这个52是什么意思呢?简单说它是一个中断ID,中断ID的作用是:从图中可以发现,不同的IO外设都可以产生中断信号,这个中断ID就是用来标识是哪一个外设产生的中断。比方说中断ID=52,那么中断就来自于GPIO,如果中断ID等于其他值,那么就来自于其他外设(针对每个IO外设的中断ID是多少,同样也是查看手册)。

此外,不仅仅是IO外设可以产生中断,PL端也可以产生中断,甚至APU内部的CPU也可以产生中断。

以ZYNQ来说,它是54个MIO和64个EMIO,这118个是共享了一个中断ID,也就是#52。那么不同的引脚产生的中断请求的ID都是52,那如何确定这个中断来自于哪个引脚呢?这就要结合INT_MASKINT_STAT这两个寄存器进行判断——首先INT_MASK可以判断哪些中断被屏蔽了,其次可以通过读取INT_STAT寄存器去判断这个中断是来自于哪一个引脚。

5.1.3 实验任务

使用GPIO的MIO中断功能,实现按键控制LED灯的亮灭。

5.1.4 系统框图

5.2 实现实验

5.2.1 搭建工程

打开前面的MIO控制LED灯实验创建的Vivado工程,打开后把这个工程另存为本次实验的工程。本次实验用不到PL侧,所以导出的时候不需要比特流文件。

5.2.2 编写程序

导出完成后,打开Vitis软件,快速创建一个空的工程。然后添加一个main.c文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#include <stdio.h>
#include "xparameters.h"
#include "xgpiops.h"
#include "sleep.h"

#define GPIO_DEVICE_ID XPAR_XGPIOPS_0_DEVICE_ID
#define MIO_LED 0 // 要控制的LED引脚号 MIO0
#define MIO_KEY 11 // 按键
#define GPIO_INTERRUPT_ID XPAR_XGPIOPS_0_INTR // 中断ID(GPIO中断ID实际上就是52)
#define INTC_DEVICE_ID XPAR_SCUGIC_SINGLE_DEVICE_ID

u8 key_press; // 按键被按下的标志

XScuGic Intc = 0; // 中断控制GIC的驱动实例

int main()
{
printf("GPIO EMIO Test!\n");

XGpioPs_Config* ConfigPtr; // 配置结构体指针,用于从硬件配置信息中读取基地址等只读参数
XGpioPs Gpio; // GPIO器件的驱动实例

/*======== 对GPIO驱动进行初始化 ========*/
// 根据器件ID来去查找器件的配置信息
ConfigPtr = XGpioPs_LookupConfig(GPIO_DEVICE_ID);
// 对GPIO的驱动进行初始化
XGpioPs_CfgInitialize(&Gpio, ConfigPtr, ConfigPtr->BaseAddr);
// 设置LED灯引脚的方向(输出)
XGpioPs_SetDirectionPin(&Gpio, MIO_LED, 1);
// 设置LED灯引脚的输出使能
XGpioPs_SetOutputEnablePin(&Gpio, MIO_LED, 1);

// 设置按键PS_KEY0引脚的方向(输入)
XGpioPs_SetDirectionPin(&Gpio, MIO_KEY, 0);

// GPIO中断配置 SetupInterruptSystem()可以从官方示例中拷贝尽量
SetupInterruptSystem(&Intc, &Gpio, )

u32 ps_key_value;
u8 led_on_off = 0;
while(1)
{
if(key_press == 1)
{
// 按键消抖
usleep(200000); // 延时20ms
ps_key_value = XGpioPs_ReadPin(&Gpio, MIO_KEY);
if(ps_key_value == 0)
{
// 按键确实被按下
led_on_off = ~led_on_off;
XGpioPs_WritePin(&Gpio, MIO_KEY, led_on_off);// 点亮灯或者熄灭灯
}
// 使能GPIO_MIO_KEY引脚的中断
XGpioPs_IntrEnablePin(&Gpio, MIO_KEY);
key_press = 0
}
}
return 0;
}

// 注:下面这个函数可以从官方示例中拷贝进来
int SetupInterruptSystem(XScuGic* GicInstancePtr, XGpioPs* Gpio, u16 GpioIntrId)
{

}

// 设置中断回调函数
void IntrHandler(void* CallBackRef, u32 Bank, u32 Status)
{
XGpioPs* Gpio = (XGpioPs*)CallBackRef;
// 置标志位
key_press = 1;

// 关闭GPIO_MIO_KEY引脚的中断
XGpioPs_IntrDisablePin(Gpio, MIO_KEY);

}

6 AXI-GPIO

6.1 AXI GPIO与AXI接口

6.1.1 AXI GPIO是什么

AXI GPIO是ZYNQ PL部分的一个IP软核,实现了通用输入输出接口的功能,并通过AXI协议实现了与处理系统(PS)的通信,使得开发者可以方便地控制和扩展GPIO接口。

上图是AXI GPIO的IP核(从Vivado的软件中截取的一个界面)。

首先,S_AXI接口可以展开,展开后会有很多个端口信号,简单说这就是一个AXI接口,这个S表示从机的接口。然后s_axi_aclkS_AXI接口的时钟。s_axi_aresetn是一个复位信号。右侧是GPIO的端口,可以实现普通GPIO接口的功能,包括控制引脚,它是支持输入和输出的,还有它也可以直接跟的FPGA的逻辑进行连接。

总之,这个AXI GPIO的IP软核,是使用FPGA可编程的逻辑资源来实现的一个GPIO的功能

6.1.2 GPIO与AXI GPIO的对比

对比项 GPIO AXI GPIO
资源类型 硬核(存在实际的物理电路) 软核(使用PL端资源实现)
连接I/O 可连接到PS和PL端I/O 不能连接PS端I/O,只能连接PL端I/O
连接PL逻辑 支持EMIO 支持
灵活性/拓展性 较低 较高

注意:AXI GPIO是软核,也就是说不存在硬件电路,是官方或者工程师自己通过FPGA程序,利用可编程逻辑资源来去搭建的一个AXI GPIO功能。当然了这个可编程的逻辑,也可以用于实现一个I2C、UART、SPI、傅里叶变换等等都可以。

6.1.3 AXI接口介绍

从上面第4节EMIO-GPIO学习的那个复杂的图中,其实AXI接口就是指GP接口(General-Purpose Ports)HP接口(High-Perormance Ports),外加一个ACP接口,AXI主要就是指这三个接口。下面是一个简化的示意图:

左边是PS右边是PL。从这个图中能够看到PS和PL之间的AXI接口。

首先第一个是GP接口,它是一个通用用途接口。可以发现图中上下有两个GP接口,一个是M_AXI_GP,另一个是S_AXI_GP,其中M表示Master,S表示Slave,这里的M和S它是相对于PS来说的,所以说上面的这个Master它就是表示PS作为主机PL作为从机,下面的Slave它是表示PS作为一个从机PL作为一个主机。我们知道一般一个通信接口作为主机和作为从机的区别是什么——主机发起和控制通信总线而从机机呢一般就是被动地响应。总共有4个GP接口。GP接口的数据总线是32位的,一般的应用场景包括中低速的一些场景上,比如说PS对PL做一些配置等。

当我有一个大量数据搬移的情况时,该使用什么接口。这里提供了一个更高性能的接口,就是下面的HP接口,可以看到它是个S表示一个从机,HP接口总共有四个,这四个HP接口都是PS作为从机,PL作为主机。这个HP接口跟上面两个GB接口最大的一个差异在哪里呀——是不是可以发现有个FIFO,用来缓存数据,所以说PS、PL之间在高速传输数据的时候会经过FIFO进行一个缓存,实现一个大量数据的传输。

比方说我们举个例子,有个摄像头位于PL端,我们知道不同的摄像头,它输出的帧率不一样、分辨率不一样,一般摄像头它是每秒钟源源不断地输出大量的数据,而我们要通过LCD屏或者是HTMI屏显示这个摄像头数据的话呢,我们一般是需要对这个数据做一个缓存,也就是说把这个图像数据缓存到内存当中也就是DDR3当中,那对于我们所使用的ZYNQ来说呢,它的这个DDR3是位于PS端的,所以说是需要把这个图像数据搬移到这个PS内的DDR3当中,那么由于摄像头的数据量很大,所以说我们就可以通过这个HP接口进行一个传输。

若传输少量数据,可以通过GP接口进行传输。

这里还有一个接口,就是这个ACP接口,那这个S也是表示PS作为从机PL作为主机。可以明显发现这个ACP接口跟GP和HP最大的区别是什么——可以发现GP、HP其实都是连接到了一个互联Interconnect模块上面去,我们知道这个互联它其实是相当于一个开关,来去控制/管理这个接口上面的数据传输。而ACP接口它是直接连接到APU里面的SCU上,这个SCU是一致性控制单元,可以作为这个ARM处理器和二级缓存以及OCM的一个桥梁,所以说SCU呢它是比较方便的去访问这个缓存,也就意味着PL端可以直接去访问这个APU里面的缓存。所以说当我们的这个PS端他有一个CPU计算了结果之后呢它可以快速的将这个结果通过ACP接口送到PL端,而同时当PL端做一个硬件加速的时候,ACP可以把他的这个加速的结果快速地发送到这个ARM处理器。

下面给出了一个总结的表格:

AXI它究竟是什么呢?下面就来介绍一下这个接口。这个AXI它首先是这个AMBA这个接口总线的一部分

AMBA它最早是由ARM开发的,主要是用在这个单片机上面,后面对这个总线进行了扩展和修订,那么现在一般就认为在这个片上系统也就是SOC,就是以这个ZYNQ为代表的SCO上面。除此之外,它其实也应用在SOPC也就是纯FPGA的一个芯片上面。

这个AMBA包含多个接口总线,包括APB、AHB、AXI、ATB。AMBA不断升级,现在是到了4.0版本。那AXI也升级到了AXI4。AXI是一个高级可扩展接口或者说是总线。

AXI4-Full表示一个全功能的接口,它比较适合用在一个中高速的一个通信,支持突发传输,比方说主机在发起这个传输的时候,只需要指定一个起始地址,在指定好起始地址之后,后面的数据就可以源源不断的传输,从机它会自动对这个数据放到相应的地址进行累加,所以说它支持的一个传输速度就比较快。

AXI4-Light表示一个轻量级的接口,那它支持的传输速率就比较低了,不支持突发传输,但占用的资源相对较小。

这里所说的这两个接口AXI4-Full和AXI4-Light它其实都是属于存储器映射接口(什么叫存储器映射接口呢,就说你在发起传输的时候,你需要指定地址)。

AXI4-Stream是一个流的传输,它是不要求指定地址的,所以说是比较适合大量数据的一个高速传输。

6.2 AXI GPIO简介

这一节教学视频中,主要是正点原子讲师带着看了一下Vivado软件中官方IP软核的介绍文档,还是讲了不少的,我这里偷懒了,感觉笔记不太好做,请直接参考原视频吧:AXI GPIO简介 - 正点原子 - Bilibili

6.3 实验任务

通过调用 AXI GPIO IP核,使用中断机制,实现开发板PL 端按键控制PS端LED灯亮灭的功能。

下面是系统框图:

图中,AXI Interconnect也是一个IP核,用于将一个(或多个)AXI存储器映射(memory-mapped)的主器件连接到一个(或多个)存储器映射的从器件。

概念1:互联(Interconnect)—— 互联实际上是一个开关,它管理并指挥所连接的AXI接口之间的通信。

概念2:存储器映射,就是把外设(如按键控制器、LED控制器)的寄存器,安排在内存的地址空间里。CPU 访问外设,就像访问普通内存一样,只需要知道“地址”,然后进行“读”或“写”。

我们可以这样对应理解:

1
2
3
4
5
6
7
主器件(Master): 图中的 ARM Cortex-A9。它是发号施令的人,它发起读写请求。
从器件(Slave): 图中的 AXI GPIO。它是听话的执行者,它等待被访问。
存储器映射的含义:
ARM 想要读取 PL_KEY0(按键)的状态,它不需要发明一种特殊的“读按键指令”。
ARM 只需要执行一条普通的“读内存指令”,目标地址设为 AXI GPIO 的地址(例如 0x41200000)。
AXI Interconnect(交通枢纽) 看到这个地址,就知道:“哦,这个地址不是给 DDR 内存的,是给右边 AXI GPIO 的。” 于是把请求转给 AXI GPIO。
AXI GPIO 收到请求,把按键的状态(0 或 1)作为数据返回给 ARM。

你可能会问,为什么要这么麻烦把外设映射成内存?

  1. 指令简单: CPU 不需要专门学习一套控制外设的指令(如 IN, OUT)。它只需要会用 LOAD(读)和 STORE(写)这两招,就能控制天下所有的外设。

  2. 编程方便:对于写 C 语言程序的程序员来说,控制外设就像操作数组一样简单。比如:int key_status = *(int *)0x41200000; (这就相当于去那个“映射”的地址读了一下按键状态)。

6.4 硬件设计

首先,在Vivado工程中,添加ZYNQ的PS端硬核。

双击打开对其进行配置——① 配置DDR,② 在MIO Configure中配置UART、GPIO MIO(注意电源也要修改)。

其次,添加AXI GPIO软核:

然后,配置完成两个IP核之后,就可以开始连线了。其实这个Vivado软件是支持自动连线的,只有少部分端口需要我们主动去连接:

下图是连接完成后的图:

复位IP核可以简单看一下。首先它有一个同步时钟slowest_sync_clk,然后下面有一些复位的输入信号,比方说一个外部复位输入ext_reset_in、还有一个辅助复位输入aux_reset_in、还有一个debug系统复位mb_debug_sys_rst,还有一个DCM的一个lock的信号dcm_locked。所以说它可以将不同的复位输入同步到这个同步时钟slowest_sync_clk上面,然后再去输出一个针对外设的复位。总之,这个复位IP核主要是用来同步这个复位信号。然后它同步之后的这个复位信号peripheral_aresetn连接到各个不同的功能模块,比方说这里的AXI GPIO以及AXI Interconnection IP核。

AXI Interconnection IP核的功能就是:一个主接口的输入M_AXI_GP0连接到一个从接口S_AXI

最后,会发现AXI GPIO核的中断引脚ip2intc_irpt还没有连线,这个主要是还没有配置ZYNQ硬核的中断:

之后可手动连线:

这就完成了硬件部分的工程搭建,然后先验证一下这个设计是否正确,验证无误后就可以生成输出文件了:

完成之后,就可以产生顶层文件。

然后,还需要做管脚约束 —— 通过PCB原理图,可以发现PL_KEY0对应的是L14:

最后,生成比特流文件,然后导出硬件。

6.5 软件设计

本次实验的功能跟前面所学习的GPIO MIO按键中断的功能是类似的,所以说可以参考前面所学习的MIO按键中断实验。这里先来打开前面学习的MIO按键中断实验,看一下它的C代码,尝试借鉴修改。

首先,打开Vitis软件,创建一个新的工程。创建一个main.c文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
#include "xparameters.h"
#include "xgpiops.h"
#include "xgpio.h"
#include "xscugic.h"
#include "xil_exception.h"
#include "xplatform_info.h"
#include <xil_printf.h>
#include "sleep.h"

/************************** Constant Definitions *****************************/

//以下常量映射到xparameters.h文件
#define GPIOPS_DEVICE_ID XPAR_XGPIOPS_0_DEVICE_ID //PS端GPIO器件ID
#define AXI_GPIO_DEVICE_ID XPAR_AXI_GPIO_0_DEVICE_ID //PL端AXI GPIO器件ID
#define SCUGIC_ID XPAR_SCUGIC_0_DEVICE_ID //通用中断控制器ID
#define AXI_GPIO_INT_ID XPAR_FABRIC_GPIO_0_VEC_ID //PL端AXI GPIO中断ID

#define MIO_LED 0 //LED 连接到 MIO0
#define KEY_CHANNEL1 1 //PL 按键使用 AXI GPIO 通道 1
#define KEY_CH1_MASK XGPIO_IR_CH1_MASK //通道 1的中断位定义

/************************** Function Prototypes ******************************/
void instance_init();
int setup_interrupt_system(XScuGic *gic_inst_ptr, XGpio *axi_gpio_inst_ptr,
u16 AXI_GpioIntrId);
static void intr_handler(void *callback_ref);

/**************************Global Variable Definitions ***********************/
XGpioPs gpiops_inst; //PS端GPIO驱动实例
XGpio axi_gpio_inst; //PL端AXI GPIO驱动实例
XScuGic scugic_inst; //通用中断控制器驱动实例
int led_value=1; //ps端LED2的显示状态

/************************** Function Definitions *****************************/

int main(void)
{
int status;

//初始化各器件驱动
instance_init();

xil_printf("AXI_Gpio interrupt test \r\n");

//设置LED所连接的MIO引脚的方向为输出并使能输出
XGpioPs_SetDirectionPin(&gpiops_inst, MIO_LED, 1);
XGpioPs_SetOutputEnablePin(&gpiops_inst, MIO_LED, 1);
XGpioPs_WritePin(&gpiops_inst, MIO_LED, led_value);

//建立中断,出现错误则打印信息并退出
status = setup_interrupt_system(&scugic_inst, &axi_gpio_inst, AXI_GPIO_INT_ID);
if (status != XST_SUCCESS) {
xil_printf("Setup interrupt system failed\r\n");
return XST_FAILURE;
}

return XST_SUCCESS;
}

//初始化各器件驱动
void instance_init()
{
XScuGic_Config *scugic_cfg_ptr;
XGpioPs_Config *gpiops_cfg_ptr;

//初始化中断控制器驱动
scugic_cfg_ptr = XScuGic_LookupConfig(SCUGIC_ID);
XScuGic_CfgInitialize(&scugic_inst, scugic_cfg_ptr, scugic_cfg_ptr->CpuBaseAddress);

//初始化PS端 GPIO驱动
gpiops_cfg_ptr = XGpioPs_LookupConfig(GPIOPS_DEVICE_ID );
XGpioPs_CfgInitialize(&gpiops_inst, gpiops_cfg_ptr, gpiops_cfg_ptr->BaseAddr);

//初始化PL端 AXI GPIO驱动
XGpio_Initialize(&axi_gpio_inst, AXI_GPIO_DEVICE_ID);
}

//建立中断系统,使能KEY按键中断
// @param GicInstancePtr是一个指向XScuGic驱动实例的指针
// @param gpio是一个指向连接到中断的GPIO组件实例的指针
// @param GpioIntrId是Gpio中断ID
// @return 如果成功返回XST_SUCCESS, 否则返回XST_FAILURE
int setup_interrupt_system(XScuGic *gic_inst_ptr, XGpio *axi_gpio_inst_ptr, u16 AXI_GpioIntrId)
{
//设置并使能中断异常
Xil_ExceptionInit();
Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_INT,
(Xil_ExceptionHandler) XScuGic_InterruptHandler, gic_inst_ptr);
Xil_ExceptionEnable();

//设置中断源的优先级和请求类型(优先级为0xA0,高电平请求)
XScuGic_SetPriorityTriggerType(gic_inst_ptr, AXI_GpioIntrId, 0xA0, 0x01);
//为中断设置中断处理函数
XScuGic_Connect(gic_inst_ptr, AXI_GpioIntrId,
(Xil_ExceptionHandler) intr_handler, (void *) axi_gpio_inst_ptr);

//使能来自于axi_Gpio器件的中断
XScuGic_Enable(gic_inst_ptr, AXI_GpioIntrId);

//配置PL端 AXI GPIO
//设置 AXI GPIO 通道 1方向为输入
XGpio_SetDataDirection(axi_gpio_inst_ptr, KEY_CHANNEL1, 1);
XGpio_InterruptEnable(axi_gpio_inst_ptr, KEY_CH1_MASK); //使能通道1的中断
XGpio_InterruptGlobalEnable(axi_gpio_inst_ptr); //使能axi gpio全局中断

return XST_SUCCESS;
}

//中断处理函数
// @param CallBackRef是指向上层回调引用的指针
static void intr_handler(void *callback_ref)
{
XGpio *axi_gpio_inst_ptr = (XGpio *)callback_ref;
usleep(20000); //延时20ms,按键消抖
if (XGpio_DiscreteRead(axi_gpio_inst_ptr, KEY_CHANNEL1) == 0)
{
//按键有效按下
print("Interrupt Detected!\r\n");
led_value = ~led_value;
XGpioPs_WritePin(&gpiops_inst, MIO_LED, led_value); //改变LED显示状态
XGpio_InterruptDisable(axi_gpio_inst_ptr, KEY_CH1_MASK);//关闭 AXI GPIO中断使能
}
XGpio_InterruptClear(axi_gpio_inst_ptr, KEY_CH1_MASK); //清除中断
XGpio_InterruptEnable(axi_gpio_inst_ptr, KEY_CH1_MASK); //使能AXI GPIO中断
}

7 ZYNQ启动流程和程序固化

7.1 启动流程

前面学习了不少历程了,都是通过Jtag的方式下载程序的(也就是使用下载器连接板卡的Jtag接口,然后把程序在线烧录到板卡当中)。通过这种方式比较方便的对代码进行验证,主要是调试功能。其缺点是是当板卡断电之后程序就丢失了。

想让程序断电之后再上电仍然能够运行,那么就需要把程序固化到板卡的片外非易失性存储器当中,比方说像什么Flash等等。这一小节就来教大家怎么把程序固化到板卡当中。

7.1.1 ZYNQ的启动方式

上图总结了ZYNQ系列的启动方式,主要是分为两大类——一个是主模式,另一个是从模式。当然前面通过JTAG的下载方式其实就是从模式。下面重点看一下主模式。

ZYNQ芯片的主模式支持四种:

  • NOR Flash
  • NAND Flash
  • QSPI Flash
  • SD卡

7.1.2 ZYNQ启动方式配置

那么ZYNQ的不同启动方式是怎么配置的呢?板卡上面有一个拨码开关BOOT_CFG,这个拨码开关就是控制它启动方式的。上图是拨码开关的电路图,可以发现,拨码开关BOOT_CFG能够控制BOOT_MODE0BOOT_MODE1的电平高低,根据图中的表格,对应分析就得到了拨码开关BOOT_CFG开关状态对应的启动模式。

7.1.3 ZYNQ的启动过程

下图给出了ZYNQ的一个启动过程。

首先是power on reset,这是一个上电复位,就说这个板卡上电之后,它会一直处于一个复位状态,当上电完成之后,它的这个复位引脚才会被拉高。

在复位完之后,它就进入了一个Stage0:BootROM Executes。这个BootROM是啥呢?可以把它理解成是固化在ZYNQ芯片里面的一段代码(Boot是跟启动相关的,ROM是只读存储器),简单说BootROM是跟启动相关的一段代码,用户不可以对它进行修改。它除了做一些初始化,还会引导Stage1:FSBL的启动。

Stage1:FSBL会对一些关键的外设进行配置,包括DDR、一些MIO引脚等等。

然后才会去引导裸机程序User Application的运行。

  • Power On Reset

一开始是POR(Power On Reset),就说板卡断电然后上电之后呢,先是板子的供电电源有一个启动顺序,然后会拉高这个复位信号,相当于结束复位。结束复位之后,所有的寄存器都恢复原来的状态。然后硬件采集这个模式的引脚(Hardware Samples Mode_Pins),这个模式的引脚就是前面所看到的MIO[2]到MIO[8]这几个与启动相关的引脚(参考视频:https://www.bilibili.com/video/BV1Et421H75m?t=525.2&p=34)。然后JTAG、IO外设、DDR等这些外设都会被Disabled除能。

然后它会做一个选择——就是PLL是否使能(前面不是讲了启动的时候有一个引脚单独控制PLL),那么它会根据这个PLL的配置来去选择是否使能PLL。那么最后就是BootROM的一个执行了。这个是从上电启动到这个BootROM执行的过程啊(上图左半侧部分)

需要说明的是,除了通过断电上电进行复位,还有一个硬复位(PS_POR_B)和软复位(PS_SRST_B)。硬复位(PS_POR_B)和断电上电的复位过程类似,软复位(PS_SRST_B)则有点不同。软复位(PS_SRST_B)是复位大部分的寄存器,但是有部分寄存器是没有复位的,然后它有一个安全模式和非安全模式,这个主要与程序有没有做加密有关,我们学习的时候一般是没有做加密的,可以先不用管它,最后进入到BootROM。

  • Stage0:BootROM

BootROM是固化在ZYNQ芯片内部的一个程序,是APU运行的第一个程序。BootROM的主要任务是配置系统,将FSBL代码从引导设备(片外Flash、SD卡等)</font>拷贝到OCM(片上存储器),引导FSBL代码的执行。此外,在非安全模式下,支持直接在QSPI或者NOR Flash中直接运行程序(XIP,eXecuteIn Place)。

  • Stage1:FSBL

下面就进入到一个FSBL的一个一个阶段了,这个FSBL它是可以由我们程序员创建的,但Xilinx官方提供了一个标志的FSBL程序的模板,大部分情况下可以不用对它做任何修改。那么它所实现的功能包括:

  • User Application

最后一个阶段就是用户的应用程序了。

注意:

这里所说的启动过程主要是针对裸机程序的,那如果是Linux的一个启动过程,肯定是有差异的。

7.2 程序固化实验

7.2.1 实验任务

本章的实验任务是在“AXIGPIO按键控制LED实验”的基础上实现程序上电自启动(程序固化),包括从SD卡启动和从QSPIFlash启动两种方式。

7.2.2 固化实验实操

请直接参考视频吧:实操:ZYNQ开发板固化程序 - 正点原子 - Bilibili

毕竟只是一种固化程序的操作。

参考链接1:ZYNQ学习笔记 程序固化
参考链接2:ZYNQ启动过程
参考链接3:详解zynq的启动和配置过程