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中进行软件设计

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

之后出现如下界面:

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

在 Vitis 中,Domain = 处理器核 + 操作系统(BSP) 的组合,即一个软件执行环境。它定义了:

  • 程序跑在哪个核上(Processor)
  • 跑在什么系统上(Operating System)

为什么要分 Domain?

因为 Zynq 这类 SoC 往往有多个核(如双核 Cortex-A9,ZynqMP 还有 A53+R5)。不同的核可以同时跑不同的系统——比如一个核跑 Linux,另一个核跑裸机或 FreeRTOS。每一个”核 + OS”的组合就是一个 domain。

简单总结:这一步就是让你决定——这个应用工程要跑在哪个核上、用裸机(standalone)还是 FreeRTOS。

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

在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划分不是一个概念。

GPIO的Bank分组也许大概可以参考STM32的GPIOA、GPIOB、GPIOC、GPIOD等的分类。

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 抄到你的代码里。

上述代码的详细解析:

  • 第1部分:头文件包含(对应STM32的“#include stm32f10x.h”)
1
2
3
4
5
6
7
#include <stdio.h>
// 相当于STM32的"stm32f10x.h",定义了所有外设的硬件基地址(比如GPIO的地址0xE000A000)
#include "xparameters.h"
// 相当于STM32的"stm32f10x_gpio.h",里面声明了GPIO的初始化、读写函数
#include "xgpiops.h"
// 相当于"delay",提供sleep(1)秒级延时
#include "sleep.h"
  • 第2部分:宏定义(对应STM32的“GPIO_Pin_7”和“RCC_APB2Periph_GPIOA”)
1
2
#define GPIO_DEVICE_ID	XPAR_XGPIOPS_0_DEVICE_ID
#define MIO_LED0 7

对应STM32:在STM32里你会写 #define LED_PIN GPIO_Pin_7,并且要在 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); 里告诉系统开启GPIOA的时钟。

Zynq的不同点:XPAR_XGPIOPS_0_DEVICE_ID 是系统自动生成的外设ID号(相当于告诉驱动“我要操作第0号GPIO外设”)。注意!Zynq这里没有像STM32那样显式开启时钟的代码,因为Zynq的PS(处理系统)部分时钟默认由FSBL(一级引导程序)在芯片启动时就已经全部打开了,裸机程序直接配置寄存器即可。

  • 第3部分:定义变量(对比STM32的“GPIO_InitTypeDef GPIO_InitStructure”)
1
2
XGpioPs_Config* ConfigPtr;  // 配置结构体指针
XGpioPs Gpio; // GPIO器件的驱动实例

对应STM32:GPIO_InitTypeDef GPIO_InitStructure; 你定义一个结构体用来存放要配置的参数(速度、模式、推挽等)。

Zynq的区分:ConfigPtr:这是一个只读的配置查找表,里面存放着硬件的基地址、中断号等。它类似于STM32里系统自带的 &GPIOA 这个地址宏。Gpio:这是驱动实例对象(类似于面向对象的实体),所有后续的操作(读写、方向设置)都要带上这个实例,告诉驱动“我要操作的是这个已初始化的GPIO”。

  • 第4部分:初始化(重中之重,对比STM32的“RCC + GPIO_Init”)

① 查找配置信息

1
ConfigPtr = XGpioPs_LookupConfig(GPIO_DEVICE_ID);

对应STM32:STM32不需要这一步,因为你直接写 GPIOA 就是基地址了。但在Zynq中,为了兼容不同芯片型号,需要通过ID去系统配置表里查找出这个GPIO外设的物理基地址(比如0xE000A000)。

② 驱动实例初始化

1
XGpioPs_CfgInitialize(&Gpio, ConfigPtr, ConfigPtr->BaseAddr);

对应STM32的:RCC_APB2PeriphClockCmd(..., ENABLE); + GPIO_Init(GPIOA, &GPIO_InitStructure);

这一行做了两件事:

  1. 把查到的基地址(ConfigPtr->BaseAddr)绑定到 Gpio 实例上(相当于告诉驱动:以后操作 Gpio 就是操作物理地址0xE000A000)。
  2. 对内部的寄存器映射做软件层面的初始化。

③ 设置方向

1
XGpioPs_SetDirectionPin(&Gpio, MIO_LED0, 1);

对应STM32:STM32用 GPIO_Mode_Out_PP 把方向设为推挽输出。

Zynq的参数:第三个参数 1 代表输出方向(0 代表输入)。注意:Zynq的每个引脚方向是独立设置的,不像STM32可以一次设置16个引脚。

④ 设置输出使能(这是Zynq独有的,STM32没有这一步!)

1
XGpioPs_SetOutputEnablePin(&Gpio, MIO_LED0, 1);

对应STM32:STM32里,当你把模式设为输出时,输出驱动器自动就使能了。

Zynq的特殊性:因为Zynq的MIO引脚是多功能复用的(可能是UART、SPI、GPIO)。哪怕你设置了方向为输出,还必须单独开启输出缓冲器(Output Enable),否则引脚内部是断开的,电平输不出去。这一步就相当于STM32里把GPIO配置为复用功能时的“重映射”开关。

几点说明:

  • (1)XPAR_XGPIOPS_0_DEVICE_ID是自动生成的吗?在哪个头文件里?

    • 是的,它是系统根据你在Vivado中的硬件设计自动生成的,不需要你手动定义。
    • 这个宏定义在 xparameters.h 头文件中。当你用Vivado搭建完硬件并导出到SDK/Vitis后,这个文件就会由工具自动生成。
  • (2)这个ID的本质是什么?Zynq-7010有几个GPIO外设ID?

    • ID的本质
      • XPAR_XGPIOPS_0_DEVICE_ID 本质上是一个数字常量,代表PS(Processing System)中GPIO控制器的唯一编号。
      • 其他外设同理:UART控制器可能叫 XPAR_XUARTPS_0_DEVICE_ID、XPAR_XUARTPS_1_DEVICE_ID;IIC可能有 XPAR_XIICPS_0_DEVICE_ID。每个独立的外设硬件模块都有自己的ID。
  • (3)这个ID的底层本质是什么?为什么要用ID而不用基地址?

    • 底层本质

      • 它就是一个数字编号,用来在配置表中“查户口”。这个“户口本”是 XGpioPs_Config 结构体数组:

        1
        2
        3
        4
        5
        6
        7
        // 在 xgpiops_g.c 中自动生成
        XGpioPs_Config XGpioPs_ConfigTable[] = {
        {
        .DeviceId = 0, // 这就是你的 XPAR_XGPIOPS_0_DEVICE_ID
        .BaseAddr = 0xE000A000, // GPIO 硬件的物理基地址
        }
        };
    • STM32的做法:你在代码里直接写 GPIO_Init(GPIOA, &GPIO_InitStructure);(这个GPIOA代表微控制器上的 GPIO 端口 A‌的硬件寄存器地址)

    • Zynq的做法:GPIO的基地址 0xE000A000 虽然是固定的,但Zynq的驱动库需要统一管理所有PS外设。用 DeviceId 这个抽象层,驱动代码可以更通用:

      1
      2
      // Zynq:驱动不直接依赖基地址,而是通过ID去查表
      ConfigPtr = XGpioPs_LookupConfig(GPIO_DEVICE_ID); // 返回 {ID=0, BaseAddr=0xE000A000}
  • (4)XGpioPs_LookupConfig() 查到的“配置信息”是什么?

    • 回答你的困惑:“配置不是要程序员手动配置吗?”,这里有两个完全不同的“配置”概念:

    • XGpioPs_LookupConfig() 查询的是前者——硬件在系统中的“门牌号”。后者才是你需要通过 XGpioPs_SetDirectionPin() 等函数手动设置的。

    • 如果想改配置怎么办?

      • 若改基地址:改Vivado硬件设计,重新导出。
      • 若改引脚功能:用 XGpioPs_SetDirectionPin()、XGpioPs_WritePin() 等函数在代码里配置。

核心总结:

  1. XPAR_XGPIOPS_0_DEVICE_ID 是工具自动生成的数字常量,定义在 xparameters.h 中。
  2. 它代表 GPIO控制器在系统中的唯一编号,Zynq-7010的PS端只有1个GPIO控制器。
  3. 它的底层本质是查表索引,用来从配置表中找到硬件的基地址。
  4. Zynq使用ID而非直接地址,是为了驱动代码的统一和通用。
  5. XGpioPs_LookupConfig() 查的是硬件静态信息(基地址),不是你的运行时配置。
  6. 你的运行时配置(方向、电平)依然需要你通过API手动设置。

4 EMIO-GPIO学习

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

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

在大多数情况下,PS端经由EMIO引出的接口会直接连接到PL端的器件引脚上,通过IO管脚约束来指定所连接PL引脚的位置。通过这种方式,EMIO可以为PS端实现额外的64个输入引脚或64个带有输出使能的输出引脚。EMIO还有一种使用方式,就是用于连接PL内实现的功能模块(IP 核),此时PL端的IP作为PS端的一个外部设备,如上图所示。

  • 为什么说“EMIO主要用于连接PL中的IP模块”?
    • 这是EMIO最核心且不可替代的价值所在,而非仅仅是“扩展引脚数”。
    • 场景设想:你在PL里设计了一个高速FFT(快速傅里叶变换)处理核。PS端需要把采集到的数据实时发送给这个FFT核进行运算。
    • 如果不用EMIO:数据必须从PS -> MIO -> 外部PCB走线 -> 再绕回PL引脚,慢且占板面积大。
    • 使用EMIO:你将PS的SPI或GPIO接口配置为EMIO模式,这些数据信号线直接通过内部总线“走捷径”灌入PL的FFT核。这实现了“片内异构核”的无缝数据流,这是Zynq架构的精髓所在。

另外可以发现这个框图里面,有些部分是有方框的,比方说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基础概念

4.1.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.1.2 MIO与EMIO究竟是什么

MIO和EMIO都是芯片内部实实在在物理存在的一坨电路(金属导线和晶体管) 。

MIO 后面直接就是“真实的芯片 I/O 电路和物理引脚”,所以输入/输出/三态这些东西在 MIO 内部就处理了;EMIO 后面不是物理引脚,而是 PL,所以 PS 必须把 GPIO 的“输入数据 I、输出数据 O、三态控制 TN”三组信号暴露给 PL,让 PL 决定后面怎么连接。

这也是为什么在3.3.2小节的图里看到 EMIOGPIOI/O/TN 三组信号,而看不到 MIOGPIOI/O/TN 三组信号。

4.2 GPIO连接MIO和EMIO差异

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

上图中针对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的。所以说一般也不需要我们程序再去做一个特殊的处理。

  • IOBUF 到底是什么?
    • 你可以把 FPGA 的 IOBUF 暂时理解成:把 FPGA 内部的“输入、输出、三态控制”与芯片外部的“一根双向物理线”连接起来的硬件。
    • AMD 对 7-series/Zynq-7000 的 IOBUF 定义也是这个意思:它是一个双向 I/O Buffer,包含输入路径和三态输出 Buffer,IO 是双向端口,T 是三态控制。

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端按键加入进来。

前面的工程创建部分,请参考视频:第4.2讲 GPIO之EMIO按键控制LED灯-程序设计(上)- 正点原子 - Bilibil

工程搭建好之后,首先实现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_TYPE、INT_POLARITY、INT_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_MASK和INT_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
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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
//****************************************Copyright (c)***********************************//
//技术支持:www.openedv.com
//淘宝店铺:http://openedv.taobao.com
//关注微信公众平台微信号:"正点原子",免费获取ZYNQ & FPGA & STM32资料。
//版权所有,盗版必究。
//Copyright(C) 正点原子 2018-2028
//All rights reserved
//----------------------------------------------------------------------------------------
// File name: main.c
// Last modified Date: 2023年2月9日下午17:51:55
// Last Version: V1.0
// Descriptions: 使用MIO按键中断控制LED灯
//----------------------------------------------------------------------------------------
// Created by: 正点原子
// Created date: 2023年2月9日下午17:51:55
// Version: V1.0
// Descriptions: The original version
//
//----------------------------------------------------------------------------------------
//****************************************************************************************//

/***************************** Include Files *********************************/

#include "xparameters.h"
#include "xgpiops.h"
#include "xscugic.h"
#include "xil_exception.h"
#include "xplatform_info.h"
#include <xil_printf.h>
#include "sleep.h"

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

//以下常量映射到xparameters.h文件
#define GPIO_DEVICE_ID XPAR_XGPIOPS_0_DEVICE_ID //PS端GPIO器件ID
#define INTC_DEVICE_ID XPAR_SCUGIC_SINGLE_DEVICE_ID //通用中断控制器ID
#define GPIO_INTERRUPT_ID XPAR_XGPIOPS_0_INTR //PS端GPIO中断ID

//定义使用到的MIO引脚号
#define KEY 11 //KEY 连接到 MIO11
#define LED 0 //LED 连接到 MIO0

/************************** Function Prototypes ******************************/

static void intr_handler(void *callback_ref);
int setup_interrupt_system(XScuGic *gic_ins_ptr, XGpioPs *gpio, u16 GpioIntrId);

/**************************Global Variable Definitions ***********************/

XGpioPs gpio; //PS端GPIO驱动实例
XScuGic intc; //通用中断控制器驱动实例
u32 key_press; //KEY按键按下的标志
u32 key_val; //用于控制LED的键值

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

int main(void)
{
int status;
XGpioPs_Config *ConfigPtr; //PS 端GPIO配置信息

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

//根据器件ID查找配置信息
ConfigPtr = XGpioPs_LookupConfig(GPIO_DEVICE_ID);
if (ConfigPtr == NULL)
{
return XST_FAILURE;
}
//初始化Gpio driver
XGpioPs_CfgInitialize(&gpio, ConfigPtr, ConfigPtr->BaseAddr);

//设置KEY所连接的MIO引脚的方向为输入
XGpioPs_SetDirectionPin(&gpio, KEY, 0);

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

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

//中断触发时,key_press为TURE,延时一段时间后判断按键是否按下,是则反转LED
while (1)
{
if (key_press)
{
usleep(20000);
if (XGpioPs_ReadPin(&gpio, KEY) == 0)
{
key_val = ~key_val;
XGpioPs_WritePin(&gpio, LED, key_val);
}
key_press = FALSE;
XGpioPs_IntrClearPin(&gpio, KEY); //清除按键KEY中断
XGpioPs_IntrEnablePin(&gpio, KEY); //使能按键KEY中断
}
}
return XST_SUCCESS;
}

// 中断处理函数
// @param CallBackRef是指向上层回调引用的指针
static void intr_handler(void *callback_ref)
{
XGpioPs *gpio = (XGpioPs *) callback_ref;

//读取KEY按键引脚的中断状态,判断是否发生中断
if (XGpioPs_IntrGetStatusPin(gpio, KEY))
{
key_press = TRUE;
XGpioPs_IntrDisablePin(gpio, KEY); //屏蔽按键KEY中断
}
}

//建立中断系统,使能KEY按键的下降沿中断
// @param GicInstancePtr是一个指向XScuGic驱动实例的指针
// @param gpio是一个指向连接到中断的GPIO组件实例的指针
// @param GpioIntrId是Gpio中断ID
// @return 如果成功返回XST_SUCCESS, 否则返回XST_FAILURE
int setup_interrupt_system(XScuGic *gic_ins_ptr, XGpioPs *gpio, u16 GpioIntrId)
{
int status;
XScuGic_Config *IntcConfig; //中断控制器配置信息

//查找中断控制器配置信息并初始化中断控制器驱动
IntcConfig = XScuGic_LookupConfig(INTC_DEVICE_ID);
if (NULL == IntcConfig)
{
return XST_FAILURE;
}

status = XScuGic_CfgInitialize(gic_ins_ptr, IntcConfig, IntcConfig->CpuBaseAddress);
if (status != XST_SUCCESS)
{
return XST_FAILURE;
}

//设置并使能中断异常
Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_INT, (Xil_ExceptionHandler) XScuGic_InterruptHandler, gic_ins_ptr);
Xil_ExceptionEnable();

//为中断设置中断处理函数
status = XScuGic_Connect(gic_ins_ptr, GpioIntrId, (Xil_InterruptHandler) intr_handler, (void *) gpio);
if (status != XST_SUCCESS)
{
return status;
}

//使能来自于Gpio器件的中断
XScuGic_Enable(gic_ins_ptr, GpioIntrId);

//设置KEY按键的中断类型为下降沿中断
XGpioPs_SetIntrTypePin(gpio, KEY, XGPIOPS_IRQ_TYPE_EDGE_FALLING);

//使能按键KEY中断
XGpioPs_IntrEnablePin(gpio, KEY);

return XST_SUCCESS;
}

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_aclk是S_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.1.4 EMIO与AXI

(一)一句话总结

EMIO是PS外设引脚通过PL扩展的通道,AXI是PS和PL之间进行数据和寄存器通信的总线协议。更简洁地说:

  1. EMIO 解决的是“PS 外设引脚怎么到 PL”的问题;
  2. AXI 解决的是“PS 和 PL 之间怎么传数据、访问寄存器、搬内存”的问题。

(二)AXI是什么

AXI是ARM AMBA总线协议中的一种,全称是:Advanced eXtensible Interface。

在Zynq中,AXI用于PS和PL之间,以及PL内部模块之间的高速数据通信。

AXI不是一根简单的信号线,而是一组完整的片上总线协议,包括:① 地址通道;② 数据通道;③ 读写控制;④ 突发传输;⑤ 应答信号;⑥ 握手信号等。

Zynq中常见的AXI 类型有:

AXI 类型 说明
AXI4 完整高性能总线,适合大块数据读写
AXI4-Lite 低速简单寄存器访问总线
AXI4-Stream 流式数据总线,适合连续数据流

在Zynq PS和PL之间,常见AXI接口包括:

接口 说明
GP0 / GP1 General Purpose,通常 PS 作为主设备访问 PL 中的 AXI Slave
HP0 ~ HP3 High Performance,通常 PL 作为主设备访问 PS 的 DDR、OCM 等
ACP Accelerator Coherency Port,用于 PL 与 CPU 缓存一致性访问

(三)EMIO与AXI的联系

EMIO与AXI某种含义上算是有关系,但它们不是同一个层面的东西。它们都服务于 Zynq 的 PS 与 PL 协同工作,但解决的问题不同。它们可以一起使用。例如:

  1. 用 EMIO 把 PS 的 GPIO、I2C、SPI 信号引到 PL;
  2. 用 AXI 让 PS 访问 PL 中的自定义寄存器或数据缓冲区;
  3. 用 AXI DMA 在 PL 和 DDR 之间高速搬数据;
  4. 用 AXI4-Stream 连接 PL 中的高速数据流 IP。

(四)EMIO与AXI的区别

表一:EMIO 和 AXI 的核心区别
对比项 EMIO AXI
本质 PS 外设 I/O 通过 PL 扩展的机制 片上总线通信协议
是否是总线协议 不是完整总线协议 是完整总线协议
主要作用 扩展或路由 PS 外设引脚 在 PS 和 PL 之间传输地址、数据、
控制信息
典型对象 GPIO、SPI、I2C、UART、CAN
等低速外设信号
寄存器访问、DMA、DDR 访问、高
速数据流
速度 一般用于中低速控制信号 可用于高性能数据通路
是否经过地址映射 通常不是作为独立 AXI 地址设备出现 通常有地址映射或流式数据接口
CPU参与方式 通常由 PS 外设控制器直接控制 CPU、DMA 或 PL Master 可通过总
线访问
PL是否需要专门IP 不一定,可以只是信号转接 通常需要 AXI Slave/Master/Stream IP
适用场景 PS 外设引脚不够,或想把 PS 外
设信号送入 PL
PL 需要和 PS 进行寄存器、内存或高
速数据通信
表二:EMIO 和 AXI 不同维度对比

(五)应用举例说明

  • 什么时候用 EMIO?
    • PS 的 MIO 引脚不够用;
    • 想把 PS 内部外设信号引到 PL 引脚;
    • 想让 PS 外设信号先经过 PL 做简单逻辑处理;
    • 低速控制信号,比如 GPIO、I2C、SPI、UART;
    • 不想在 PL 中额外实现 AXI 外设 IP。
1
2
3
PS UART --> EMIO --> PL 引脚
PS SPI --> EMIO --> 外部 SPI 芯片
PS GPIO --> EMIO --> PL 逻辑
  • 什么时候用 AXI?
    • PS 需要读写 PL 中的寄存器;
    • PL 需要访问 PS DDR 内存;
    • PL 需要高速传输数据;
    • 需要 DMA 搬运数据;
    • PL 中有自定义 IP,需要被 CPU 控制;
    • PL 和 PS 之间需要结构化、地址化、可扩展的通信。
1
2
3
4
PS CPU --> AXI4-Lite --> PL 控制寄存器
PL DMA --> AXI HP --> PS DDR
PL 视频采集 --> AXI4-Stream --> 显示/存储
PL 加速器 --> AXI --> DDR 或 PS 内存

(六)用通俗例子理解

例子 1:PS 控制一个 LED,只是想用PS的GPIO控制一个 LED,而这个 GPIO 通过 PL 引脚输出:

1
2
PS GPIO --> EMIO --> PL 引脚 --> LED
// 这时主要用 EMIO,不需要 AXI 总线参与。

例子 2:PS 读取 PL 中自定义寄存器,在 PL 里写了一个控制模块,PS 想通过地址读写它的寄存器:

1
2
PS ARM --> AXI4-Lite --> PL 寄存器模块
// 这时用 AXI,通常是 AXI4-Lite。

例子 3:PL 采集高速 ADC 数据并写入 DDR,在 PL 中有一个 ADC 模块,要把大量数据高速写入 PS 侧 DDR:

1
2
ADC 采集 IP --> AXI4-Stream --> AXI DMA --> AXI HP --> DDR
// 这时用 AXI,通常是 AXI4-Stream 和 AXI HP/DMA。

例子 4:PS 的 I2C 通过 PL 连接外部 I2C 设备,如果 PS 的 I2C 控制器要通过 PL 引脚连接外部 I2C 芯片:

1
2
PS I2C --> EMIO --> PL I/O 引脚 --> 外部 I2C 设备
// 这里 I2C 控制器在 PS 中,EMIO 只是信号路径。

6.1.5 ZYNQ的AIX接口

(一)一句话总结

AXI4、AXI4-Lite、AXI4-Stream 是“协议类型”;GP0/GP1、HP0~HP3、ACP 是 Zynq PS 提供给 PL 的具体“AXI 接口/通道”。

也就是说,GP、HP、ACP 不是和 AXI4、AXI4-Lite、AXI4-Stream 并列的新协议,而是 Zynq PS 侧暴露出来的一组具体接口端口,这些端口底层使用的是 AXI 系列协议。

(二)AXI4、AXI4-Lite、AXI4-Stream 是什么?

  • AXI4 是功能最完整的一种,通常用于高速、批量、地址化的数据访问。
    • 特点:① 有地址;② 支持突发传输 Burst;③ 支持多笔事务同时进行中;④ 支持读写数据分离;⑤ 适合大块数据访问。
    • 典型用途:① CPU 访问 PL 中的大容量缓冲区;② PL IP 通过 DMA 访问 DDR;③ 视频帧缓存读写;④ 高速数据搬运。
  • AXI4-Lite 是 AXI4 的简化版,主要用于访问寄存器、控制状态端口。
    • 特点:① 有地址;② 一次通常只传一个数据;③ 不支持复杂突发传输;④ 数据位宽通常较窄;⑤ 逻辑资源消耗较少。
    • 典型用途:① CPU 写 PL 中的控制寄存器;② CPU 读 PL 中的状态寄存器;③ 配置 ADC、DAC、电机控制 IP。
  • AXI4-Stream 和 AXI4、AXI4-Lite 很不一样。它通常没有地址概念,不是用来访问某个内存地址的,而是用来连续传输数据流。
    • 特点:① 通常没有地址;② 数据像流水一样一帧一帧或一样本一样本传输;③ 适合连续数据;④ 常配合 DMA、FIFO、视频、ADC、DAC 使用。
    • 典型用途:① ADC 采样数据流;② 视频像素流;③ 网络数据包;④ DMA 数据搬移;⑤ PL 内模块之间高速传输。

(三)GP0/GP1、HP0~HP3、ACP 是什么?

这些是 Zynq-7000 PS 和 PL 之间的具体 AXI 接口端口。它们不是协议本身,而是 PS 内部提供出来的“接口实例”或者“通道”。可以类比为:

AXI4、AXI4-Lite、AXI4-Stream 是“语言”;GP、HP、ACP 是 Zynq PS 上开的几扇“门”。
  • GP0/GP1:通用 AXI 接口

    • 通常特性是:

      • PS 作为 AXI Master;
      • PL 中的 IP 作为 AXI Slave;
      • 适合 PS 主动访问 PL;
      • 常用于控制类寄存器、低速数据访问。
    • 方向可以理解为:

      1
      2
                  GP0 / GP1
      PS ARM CPU ————————————> PL AXI Slave IP
    • 例如:

      1
      2
      3
      PS CPU 通过 GP0 访问 PL 中的 AXI GPIO
      PS CPU 通过 GP0 读写 PL 自定义寄存器
      PS CPU 通过 GP1 配置 PL 中的控制 IP
  • HP0~HP3:高性能 AXI 接口

    • 通常特性是:

      • PL 作为 AXI Master;
      • PS 侧内存子系统作为 AXI Slave;
      • PL 可以通过 HP 接口访问 DDR、OCM 等;
      • 适合高带宽数据搬移。
    • 方向可以理解为:

      1
      2
                         HP0 ~ HP3
      PL AXI Master IP ————————————> PS DDR / OCM / 内存子系统
    • 例如:

      1
      2
      3
      PL 中的 ADC 采集模块通过 AXI DMA 和 HP0 把数据写入 DDR
      PL 中的视频 IP 通过 HP1 读取帧缓存
      PL 加速器通过 HP2 读取 DDR 中的数据并写回结果
  • ACP:缓存一致性接口:ACP 也是给 PL 访问 PS 侧内存用的,但它和 HP 的重点不同

    • ACP 的特点是:

      • PL 可以通过 ACP 访问 CPU 缓存一致性相关的路径;
      • 适合 PL 加速器和 CPU 共享数据;
      • 可以减少某些缓存同步负担;
      • 但使用上比 HP 更复杂,带宽和场景也有限制。
    • 方向可以理解为:

      1
      2
                  ACP
      PL 加速器——————————>PS CPU 缓存一致性路径
    • 典型用途:

      1
      2
      3
      4
      PL 和 CPU 共享一小块数据
      PL 加速器写结果,CPU 很快读到
      CPU 写命令,PL 加速器很快看到
      低延迟共享数据

(四)小结

名称 本质 说明
AXI4 协议类型 高性能内存映射总线
AXI4-Lite 协议类型 简单寄存器访问总线
AXI4-Stream 协议类型 流式数据总线
GP0/GP1 Zynq PS-PL 接口 PS 作为 Master 访问 PL,通常用于控制/普通访问
HP0~HP3 Zynq PS-PL 接口 PL 作为 Master 访问 PS 内存,通常用于高速数据
ACP Zynq PS-PL 接口 PL 访问 PS 缓存一致性路径

6.1.6 补充:接口的概念

在数字系统和 SoC 设计中,接口 Interface 可以理解成两个模块之间的约定。 也就是说,接口不只是“几根线”,而是一整套规则。

(一)接口是一个边界约定

假设有两个模块 A 和 B:模块 A ---- 接口 ---- 模块 B,只要 A 和 B 都遵守同一套接口规则,它们就可以连接。

例如 UART 接口: TX、RX、GND,它的接口约定包括: ① 电压电平是多少?② 波特率是多少?③ 起始位多长?④ 数据位几位?⑤ 停止位几位?⑥ 有没有校验?所以 UART 接口不仅仅是 TX/RX 两根线,而是包括电气、时序、协议含义的一整套约定。

AXI 接口也是边界约定,一个 AXI4-Lite Slave 接口可能包括:

写地址通道 写数据通道 写响应通道 读地址通道 读数据通道
AWADDR
AWVALID
AWREADY
WDATA
WSTRB
WVALID
WREADY
BRESP
BVALID
BREADY
ARADDR
ARVALID
ARREADY
RDATA
RRESP
RVALID
RREADY

这些信号共同组成 AXI4-Lite 接口。

(二)“接口”应该如何理解

  • 第一层:物理/引脚接口
1
2
3
4
5
举例:
FPGA 引脚
EMIO 引脚
UART TX/RX 引脚
SPI SCK/MOSI/MISO/CS 引脚
  • 第二层:逻辑/协议接口
1
2
3
4
5
6
7
8
9
10
11
12
13
举例:
AXI4 Slave 接口
AXI4-Lite Master 接口
AXI4-Stream Master 接口
UART 接口
SPI 接口

这类接口关心:
信号名
信号方向
握手方式
数据格式
协议规则
  • 第三层:软件/地址接口
1
2
3
4
5
6
7
8
9
10
11
12
举例:
某个 IP 的寄存器基地址
设备树中的地址范围
Linux 驱动访问的寄存器
裸机程序中的 Xil_Out32 地址

这类接口关心:
基地址是多少?
寄存器偏移是多少?
每一位代表什么?
读什么值?
写什么值?

6.1.7 接口、协议、总线、端口概念解析

(一)协议 Protocol

协议是规则,例如:① AXI4 协议;② AXI4-Lite 协议;③ AXI4-Stream 协议;④ SPI 协议;⑤ I2C 协议;⑥ UART 协议等。

协议规定: ① 信号有哪些?② 信号什么时候有效?③ 主从设备如何交互?④ 数据格式是什么?

(二)接口 Interface

接口是模块边界上的具体实现。例如:① 某个 IP 有一个 AXI4-Lite Slave 接口;② 某个 PS 端口有一个 AXI Master 接口;③ 某个模块有一个 UART 接口等。

可以理解为:协议 = 规则 ⇒ 接口 = 按规则做出来的插座/端口

(三)端口 Port

端口是接口在硬件描述中的具体信号集合。例如:

1
2
3
4
5
6
7
// 下面这些信号组成了 GP0 的 AXI 接口。
GP0_AXI_ARADDR
GP0_AXI_ARVALID
GP0_AXI_ARREADY
GP0_AXI_RDATA
GP0_AXI_RVALID
GP0_AXI_RREADY

(四)总线 Bus

总线通常指多个设备共享或互连的通信结构。例如: ① AXI Interconnect;② AXI SmartConnect;③ AXI Crossbar等。

它可以把多个 Master 和多个 Slave 连起来:

1
2
3
4
Master 1 ----|
Master 2 ----|---- AXI Interconnect ---- Slave A
Master 3 ----| Slave B
Slave C

在 Zynq 中,PS 的 GP/HP 接口经常通过 AXI Interconnect 连接到 PL 中的多个 IP。

(五)举例说明

例子1:PS 控制 PL 中的寄存器

假设你在 PL 中做了一个 LED 控制寄存器模块。

1
2
               GP0                       AXI4-Lite
PS ARM CPU ——————————> AXI Interconnect ——————————> PL LED 控制寄存器

这里涉及:

名称 角色
GP0 Zynq PS 提供的 AXI 接口通道
AXI Interconnect AXI 总线互连
AXI4-Lite PL 寄存器模块使用的协议类型
PL LED 控制寄存器 AXI Slave 设备
1
2
3
4
5
6
7
8
9
10
11
12
13
CPU 发起地址写操作
|
v
通过 GP0 进入 PL
|
v
AXI Interconnect 根据地址路由
|
v
LED 控制寄存器收到写数据
|
v
LED 点亮

例子2:PL 高速采集数据写入 DDR

假设 PL 中有 ADC 采集逻辑。

1
2
            AXI4-Stream         AXI Master
ADC 采集 IP ——————————> AXI DMA ——————————> HP0 ——————————> PS DDR

这里涉及:

名称 角色
ADC 采集 IP 数据源
AXI4-Stream 流式数据协议
AXI DMA 把流数据搬到内存
HP0 PL 访问 PS 内存的高性能接口
DDR 最终数据存储位置

这个例子说明:AXI4-Stream 和 HP 不是同一个东西,但可以配合使用。

AXI4-Stream 负责 PL 内部或 PL 模块之间的数据流动;HP 负责把数据送进 PS 内存系统。

GP、HP、ACP 是 Zynq 给你的 AXI 接口入口;AXI4、AXI4-Lite、AXI4-Stream 是这些入口后面可能跑的 AXI 协议类型。

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_MODE0和BOOT_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的启动和配置过程