写在开头

本文正式对 ZynqMP Ultrascale+ 的重点特性—— PL 协同 PS 共同工作进行了初步探索。具体为在 PL 段烧写两个 GPIO 控制器来控制 LED 灯和五个按钮,并编写 PS段程序实现按钮通过中断来控制 LED 灯。

探索过程极其曲折,以至于笔者不得不用一张图来表达现在的心情:

王雷

Vivado

准备工作

  • 工作环境 vivado 2024.1, ubuntu 22.04.3。

  • 创建 vivado 工程,例如叫做 ZCU102-PSPL-LED。

  • 和 Helloworld 一样在 Block Design 对 ZynqMP 芯片初步配置。

GPIO配置

  • 点击 + 号新增两个 GPIO

    GPIO

    GPIO全拼叫General Purpose Input Output(通用输入输出)简称IO口也叫总线扩展器,这里我们加入的 GPIO 将可以让 PL段的 LED灯、按钮得以通过 GPIO 连接到 AXI 进而和 PS 进行交互。

  • GPIO 0 配置 LED 输出

    GPIO0

    Xilinx 提供的 AXI GPIO IP 核有两条通道,因此其实可以使用一个 GPIO 同时管理 LED 和按钮,但为了简化,我们就只使用一通道。

  • GPIO 1 配置 Button 输入,并在下方配置中断

    GPIO1

    对比一下 GPIO0 就会发现模块上多了一条信号线 ip2intc_irpt。 GPIO 通过此来发送中断信号。

  • 点击 Run Connection Automation 并全选,自动将 MPsoc 和两个 GPIO 连接起来。

    Connect

    最终效果类似下图:

    连接效果

PS 中断配置

至此,我们可以看到图中的 ip2intc_irpt 还没有连线,我们需要对此进行配置。

  • 重新配置 MPSOC,在 PS-PL Configuration 下找到 Interrupts -> PL to PS -> IRQ0,Select 改为 1。

    IRQ0

    可以看到 MPsoc 芯片上也多了一条信号,叫 pl_ps_irq0

  • 在图上拖动鼠标,将 ip2intc_irptpl_ps_irq0 连接起来即可。

    中断连线

    这样的连线表明,gpio1产生的中断将通过 pl_ps_irq0 传入到 ps 中,进而传入到arm的中断控制器 GIC 中。

Debug 检查(选做)

为了更好的理解工作原理,我非常建议对中断信号进行一个 debug 处理,即引入一个 ILA 来监测信号变化。

  • 点击 + 号添加一个 ILA。

    ila

  • 双击 ILA 配置如下。

    ila配置

    • Sample Data Depth 可以任选,这代表了可以一次捕捉多少波形,越大在综合时消耗的板上资源越多,所以酌情处理。

    • Number of Probes 代表了探测多少信号,这里我们只探测一个中断信号。

    • Capture Control 需要选上,这样我们可以设置捕捉什么样电位的信号。

  • Probe0ip2intc_irpt 连接起来。

  • 如果配置了此部分,我们可以在之后运行程序时,打开 Vivado 的 Hardware Manager 来捕捉中断信号的波形。

    HM

后续工作

和 helloworld 类似,在生成比特流成功后导出 xsa,这次由于在 PL 实现了两个 GPIO 控制器,因此一定要勾选 Include BitStream。

Bit

Vitis

准备工作

和 helloworld 一样,基于导出的 xsa 生成 platform。

创建 APP

这次我们由于要自己编写程序,所以理应是创建 Empty Application,但是这样会需要自己导入头文件目录,比较麻烦,所以我们继续创建一个 Helloworld 的 Example Application。

修改文件

我们在 helloworld.c 重写自己的逻辑,代码如下

  • main 函数

    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
    int main ()
    {
    // 打印一个字符画,出于美观的原因
    print("ooooo oooooooooooo oooooooooo. ooooooooooooo oooooooooooo .oooooo..o ooooooooooooo\r\n");
    print("`888' `888' `8 `888' `Y8b 8' 888 `8 `888' `8 d8P' `Y8 8' 888 `8\r\n");
    print(" 888 888 888 888 888 888 Y88bo. 888\r\n");
    print(" 888 888oooo8 888 888 888 888oooo8 `Y8888o. 888\r\n");
    print(" 888 888 888 888 888 888 `Y88b 888\r\n");
    print(" 888 o 888 o 888 d88' 888 888 o oo .d8P 888\r\n");
    print("o888ooooood8 o888ooooood8 o888bood8P' o888o o888ooooood8 88888888P' o888o\r\n");



    print("---Entering main---\n\r");

    {
    int status;
    print("\r\nRunning GpioOutputExample for axi_gpio ... \r\n");
    // 调用核心测试函数,传入两个在 xparameters.h 中定义的 GPIO 基地址。
    status = GpioOutputExample(XPAR_AXI_GPIO_0_BASEADDR, XPAR_AXI_GPIO_1_BASEADDR);
    if (status == 0) {
    print("GpioOutputExample PASSED \r\n");
    } else {
    print("GpioOutputExample FAILED \r\n");
    }
    }

    print("---Exiting main---");
    return 0;
    }
  • GpioOutputExample 函数

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
int GpioOutputExample(UINTPTR BaseAddress_0, UINTPTR BaseAddress_1)
{
XGpio_Config *CfgPtr;
int Status;

// 初始化 GPIO 0
CfgPtr = XGpio_LookupConfig(BaseAddress_0);
Status = XGpio_Initialize(&GpioOutput, BaseAddress_0);

if (Status != XST_SUCCESS) {
print("XST FAIL\r\n");
return XST_FAILURE;
}

// 配置 GPIO 0 一通道的所有方向为输出(对齐Vivado配置)
XGpio_SetDataDirection(&GpioOutput, LED_CHANNEL, 0x0);

// 关闭所有 LED 灯
XGpio_DiscreteWrite(&GpioOutput, LED_CHANNEL, 0x0);

// 初始化 GPIO 1
CfgPtr = XGpio_LookupConfig(BaseAddress_1);
Status = XGpio_Initialize(&GpioInput, BaseAddress_1);

if (Status != XST_SUCCESS) {
print("XST FAIL\r\n");
return XST_FAILURE;
}

// 配置 GPIO 1 一通道的所有方向为输入(对齐Vivado配置)
XGpio_SetDataDirection(&GpioInput, BTN_CHANNEL, 0b11111);

// GPIO 使能中断
XGpio_InterruptGlobalEnable(&GpioInput);
XGpio_InterruptEnable(&GpioInput, 0b11111);

printf("config_intrid : %u\r\n",CfgPtr->IntrId);
printf("config_intrparent : %u\r\n",CfgPtr->IntrParent);

// 配置 GIC,绑定中断处理函数 handler_btn(自定义函数)
Status = XSetupInterruptSystem(&GpioInput,
&handler_btn,CfgPtr->IntrId,
CfgPtr->IntrParent,
XINTERRUPT_DEFAULT_PRIORITY);


print("XScuGic Initialized\r\n");

while (1)
{
switch (mode)//选择5种闪烁模式
{
case 0:
XGpio_DiscreteWrite(&GpioOutput, 1, 0b00000001);
usleep(100000);
XGpio_DiscreteWrite(&GpioOutput, 1, 0b00000010);
usleep(100000);
XGpio_DiscreteWrite(&GpioOutput, 1, 0b00000100);
usleep(100000);
XGpio_DiscreteWrite(&GpioOutput, 1, 0b00001000);
usleep(100000);
XGpio_DiscreteWrite(&GpioOutput, 1, 0b00010000);
usleep(100000);
XGpio_DiscreteWrite(&GpioOutput, 1, 0b00100000);
usleep(100000);
XGpio_DiscreteWrite(&GpioOutput, 1, 0b01000000);
usleep(100000);
XGpio_DiscreteWrite(&GpioOutput, 1, 0b10000000);
usleep(100000);
break;
case 1:
XGpio_DiscreteWrite(&GpioOutput, 1, 0b00000011);
usleep(100000);
XGpio_DiscreteWrite(&GpioOutput, 1, 0b00001100);
usleep(100000);
XGpio_DiscreteWrite(&GpioOutput, 1, 0b00110000);
usleep(100000);
XGpio_DiscreteWrite(&GpioOutput, 1, 0b11000000);
usleep(100000);
break;
case 2:
XGpio_DiscreteWrite(&GpioOutput, 1, 0b00001111);
usleep(100000);
XGpio_DiscreteWrite(&GpioOutput, 1, 0b11110000);
usleep(100000);
break;
case 3:
XGpio_DiscreteWrite(&GpioOutput, 1, 0b11111111);
usleep(100000);
XGpio_DiscreteWrite(&GpioOutput, 1, 0b00000000);
usleep(100000);
break;
case 4:
XGpio_DiscreteWrite(&GpioOutput, 1, 0b10000001);
usleep(100000);
XGpio_DiscreteWrite(&GpioOutput, 1, 0b01000010);
usleep(100000);
XGpio_DiscreteWrite(&GpioOutput, 1, 0b00100100);
usleep(100000);
XGpio_DiscreteWrite(&GpioOutput, 1, 0b00011000);
usleep(100000);
break;
default://全熄灭
XGpio_DiscreteWrite(&GpioOutput, 1, 0b00000000);
usleep(100000);
break;
}
}
return XST_SUCCESS;
}
  • handler_btn 函数
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
static void handler_btn(void* Callback) 
{
print("Interrupt Received !\r\n");
// 将 Callback 强制转为 XGpio 结构体,至于为什么传入的一定是这个结构体,这取决于XSetupInterruptSystem的第一个参数。

XGpio* g = (XGpio*)Callback;

//读出按钮状态
u32 r;
r = XGpio_DiscreteRead(g, 1);
printf("%08x\n\r", r);

//设置工作模式
//由于AXI GPIO中断无论接口由0变1还是由1变0都会产生高电平中断给PS,根据当前逻辑不能响应接口由1变0的中断
switch (r)
{
case 0b1:
mode = 0;
break;
case 0b10:
mode = 1;
break;
case 0b100:
mode = 2;
break;
case 0b1000:
mode = 3;
break;
case 0b10000:
mode = 4;
break;
default://多键按下或者GPIO接口由1变0,都不响应
mode = mode;
print("falling edge!\r\n");
break;
}

//清除全部中断
XGpio_InterruptClear(g, 0b11111);
print("========================================\r\n");
}
  • 头文件引入和全局变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <xil_exception.h>
#include <xinterrupt_wrap.h>
#include <xscugic.h>
#include "xparameters.h"
#include "xgpio.h"
#include "stdio.h"
#include "xstatus.h"
#include "xil_printf.h"
#include "sleep.h"
#define LED_CHANNEL 1
#define BTN_CHANNEL 1
#define printf xil_printf

int GpioOutputExample(UINTPTR BaseAddress_0, UINTPTR BaseAddress_1);
int GpioInputExample(UINTPTR BaseAddress, u32 *DataRead);
void GpioDriverHandler(void *CallBackRef);


XGpio GpioOutput;
XGpio GpioInput;
XScuGic ScuGic;
volatile u8 mode = 0;
  • 将上述几段代码拼起来即为全部文件内容。

烧写测试

常规操作

和 helloworld 类似,生成 boot.bin,将 zcu102 启动模式改为 QSPI 启动。烧写完毕后重启板子,等待 GIC 初始化完成后,按动 5个按钮,观察 LED 显示模式是否变化即可。

ILA(选做)

如果之前进行了 ILA 模块的添加和配置,那我们可以观察到按动任意开关时中断信号的变化。

具体方法如下:

  • 连接 jtag 线到电脑

  • 点开 vivado 的 hardware manager。

  • 点击 open target -> auto connect,vivado 将自动检测 jtag,如果检测成功,将会出现我们的开发板。

    ila测试

    并且我们可以看到我们添加的 ila_1。

  • 右键图中的 xzcu9_0 ,点击 program device,选择我们的 bit 流,路径在 工程目录/xx.run/xx.impl下.

    select

  • 点击 program 等待烧写完毕,之后重启板子。

    重启的原因是一般情况下,此时程序已经运行过了 GPIO 的配置,但我们重新烧写了 PL,相当于 reset 了 GPIO,对于中断的配置将失效,此时我们直接抓信号将得不到正确结果。

  • 点击图中的加号,添加仅有的一根信号线进来

    sign

    名字发生变化不奇怪,是因为 Vivado 综合实现可能会进行优化,不会保留原有的名字。

  • 修改此信号的抓取逻辑如下,抓取信号为 高电平(1)的时刻。

    config

  • 点击抓取设置左侧的启动,Ila 进入监测模式,检测到高电平将会抓取并显现出来。

    waiting

  • 此时我们任意按下一个板上按钮,正确情况将会抓取到高电平的中断信号

    result

    这证明了按下按钮,在正确配置的情况下,gpio控制器将拉高中断信号线,ps我们写的handler 对其进行处理后 gpio中断信号线将重新回到低电位。

写在最后

现有的网络教程版本太为落后,xsa已经使用了SDT这种全新的规范,进而导致 vitis 编写和以往大相径庭,笔者已经被坑惨了,所以建议后来者即使是查看这篇 2024 年的文章,如果时间过于久远,版本差距太大,也应该报有着审慎的态度来学习。

谢谢!