UltraZed-EG PCIe Carrier Card 开发纪录: Hello Cortex-R5

在 UltraZed-EG PCIe Carrier Card 开发纪录: Hello Cortex-A53 中我们题到了如何透过 Vivado 去建立我们的项目,让 UltraZed-EG PCIe Carrier Card 上的处理器系统 (Processing Syste, PS) 里面的 Cortex-A53 可以透过 AXI_GPIO 去对可程序逻辑区 (Programmable Logic, PL) 端的 LEDs D12 ~ D19 进行输出的控制。

在这篇文章,我们要讲的则是如何透过 AXI_GPIO 来处理 输入 的控制,并让 Cortex-R5 根据不同的输入,在 ps_uart1 输出不同的讯息,以及控制不同的 LED 亮暗。

(本文以 Vivado 2018.2 进行开发)

开发目标

和 UltraZed-EG PCIe Carrier Card 开发纪录: Hello Cortex-A53 一文很像,只是这次我们把目标转向 Cortex-R5 ,以及将 GPIO 输出的功能,改成 GPIO 输入。
这次我们将透过 ps_uart1 输出 Cortex-R5 上的讯息,并透过 AXI_GPIO 搭配 interrupt 的使用,去侦测使用者按下可程序逻辑(Programmable Logic, PL) 端的 SW2 ~ SW4 这三个无段按钮。

建立项目

首先让我们打开 Vivado 吧~ 不过在进行这一步之前,请先确定你有依照 让 Vivado 有 UltraZed-EG PCIe Carrier Card 的配置文件 一文的说明,让我们在建立项目的时候可以找到 UltraZed-EG PCIe Carrier Card 这块板子。

启动了 Vivado 后,点选 Create New Project

接下来指定好项目路径和名称

选择 RTL Project ,并将 Do not specify sources at this time 打勾,我们暂时不会汇入已经有的 verilog 程序代码

点选 Boards ,选择 UltraZed-EG PCIe Carrier Card

完成项目的建立

建立 Block Design

和之前的文章一样,我们的项目需要用到 Xilinx 一些预先定义好的 IP, 因此使用 Block Design 来建立我们的设计。
首先点选 IP Integrator -> Create Block Design

接着点选 OK 建立我们的 Block Design

点选 Add IP 按钮去增加我们需要的 IP 核

我们首先寻找 Zynq UltraScale+ MPSoC 并将它加入到我们的 Block Design,并点选 Run BLock Automation 对该 IP 做一些设定

由于预设的 Zynq UltraScale+ MPSoC 并不会打开可程序逻辑 (Programmable Logic, PL) 对应到处理器系统 (Processing System, PS) 的中断控制 (PL-PS interrupt),因此我们要自己打开。
点击 Zynq UltraScale+ MPSoC 两下来对其进行设定,你会看到这样的页面,选择 PS-PL Configuration

接下来,点选 General -> Interrupts -> PL to PS -> IRQ0[0-7] 将其变成 1 ,完成后点选 OK

你会看到我们的 Zynq UltraScale+ MPSoC 增加了 pl_ps_irq0[0:0] 这个输入界面,如果有需要的话则再 Run Block Automation 一次。

接下来,将 Board 里面的 Push buttons 拉到我们的 Diagram 去

目前电路变成这样

接下来,再把 Board 上的 LED 拉到 axi_gpio_0 上面,让整个电路变成这样

对 axi_gpio_0 点击两下,进入到以下设定页面

在这边,我们将 Enable Interrupt 打开,点选 OK 完成设定

我们拉条线将 ip2intc_irpt 接到 pl_ps_irq0[0:0] 上,让 interrupt 可以运作

完成后,点选 Run Connection Automation 进行线路连接,现在电路会变成这样

(注意到 ip2intc_irpt 一定要连接到 pl_ps_irq0[0:0] 上呦,也就是橘色线的这一条)
完成后可以点选 Validate Design 按钮来确认设计没问题

好了,让我们来产生 HDL Wrapper 吧 ~

产生 HDL Wrapper

接下来我们要将刚刚用 Block Design 建立的电路变成 verilog 程序代码,因此会需要进行产生 HDL Wrapper 这个步骤。
对你的 Block Design 档案点选右键,选择 Create HDL Wrapper ,它会根据你项目设定的语言 (VHDL 或是 Verilog) 来产生相对的 HDL 程序代码。

由于这次我们不需要对产出来的东西进行修改,因此选 Let Vivado manage wrapper and auto-update 即可

好了后,假设你的 Block Design 档案叫做 design_1.bd ,那就会产生 design_1_wrapper.v 或是 design_1_wrapper.vhdl 这样的档案。

产生比特流 (bitstream)

前面的处理都好了后,接下来点选 Program and Debug -> Generate Bitstream 去让 Viavado 将这个项目产生出 比特流 (bitstream) ,Zynq UltraScale+ 会在开机的时候根据 bitstream 的信息对 FPGA 进行设定。

这个产生的过程视你的计算机强度如何而决定花多少时间,总之先来泡杯茶吧~

当 bitstream 完成后,我们准备执行 Xilinx SDK 来透过写 C 语言项目来让 Cortex-R 可以透过 AXI_GPIO 侦测 SW2 ~ SW4 的中断(interrupt) ,并根据不同按钮的触发来对 LED 进行控制。

点选 File -> Export -> Export Hardware 将刚刚产生的硬件信息输出给 Xilinx SDK 去。

确定你有勾选 Include bitstream 后,点选 OK

完成后,执行 Xilinx SDK

建立 Xilinx SDK 项目

启动 Xilinx SDK 后,点选 File -> New -> Application Project 去建立新的项目

这边我命名此一项目为 helloR5 ,并指定为 standalone 的程序,注意到 Processor 要选择 psu_cortex45_0

接下来,我们一样选择 Hello World 来作为我们的项目样板,点选 Finish 完成项目建立。

打开 helloworld.c

点选左边字段的 helloR5 -> src -> helloworld.c 来编辑我们的主程序,你会看到以下的内容
* helloworld.c: simple test application
*
* This application configures UART 16550 to baud rate 9600.
* PS7 UART (Zynq) is not initialized by this application, since
* bootrom/bsp configures it to baud rate 115200
*
* ------------------------------------------------
* | UART TYPE BAUD RATE |
* ------------------------------------------------
* uartns550 9600
* uartlite Configurable only in HW design
* ps7_uart 115200 (configured by bootrom/bsp)
*/

#include
#include "platform.h"
#include "xil_printf.h"

int main()
{
init_platform();

print("Hello World\n\r");

cleanup_platform();
return 0;
}

这个程序默认会直接透过 Xilinx 定义好的 print() 函式透过当前开发板的 ps7_uart 进行输出,以这块板子而言,就是透过 ps_uart0 也就是 Linux 端的 /dev/ttyUSB1 会得到讯息,让我们修改一下预设的输出吧。

设定输出的 UART

在本文一开始,我们题到了我们这次希望透过 ps_uart1 输出,也就是希望 Linux 端的 /dev/ttyUSB0 可以收到讯息,那这样要怎样做呢?

首先点选 Xilinx -> Board Support Packages Settings

选择 helloR5_bsp

点选 Overview -> standalone 设定 stdin 和 stdout 成 ps_uart1 ,变成如下图这样

点选 OK ,完成设定,这样这个项目透过 print() 或是 xil_printf() 输出的讯息就都是从 ps_uart1 也就是 Linux 端的 /dev/ttyUSB0 进行输出啰~

透过 SW 控制 LED (轮询)

由于如果连如何抓 SW2 ~ SW4 的输入都不会的话,中断控制大概也不用提了 (笑)。 因此让我们先用最传统的方式,透过轮询 (polling) 的方式取得当前 SW2 ~ SW4 的状态,并分别控制 LED D12 ~ D14

简单的 SW2 ~ SW4 信息取得

让我们编辑 helloworld.c 将其变成以下
#include
#include "platform.h"
#include "xil_printf.h"
#include "xgpio.h"
#include "sleep.h"

XGpio sw;

int main()
{
init_platform();

// Initialize SW2 ~ SW4
int ret = XGpio_Initialize(&sw, XPAR_GPIO_0_DEVICE_ID);
if (ret != XST_SUCCESS)
return XST_FAILURE;

// Setup gpio direction to IN, SW is at axi_gpio0 channel 1
XGpio_SetDataDirection(&sw, 1, 0xFF);

print("Hello Cortex-R5\n\r");

// Polling the SW2 ~ SW4 input result
while (1) {
int val = XGpio_DiscreteRead(&sw, 1);
switch (val) {
case 0x4:
xil_printf("SW 4 pressed!\n\r");
break;
case 0x1:
xil_printf("SW 3 pressed!\n\r");
break;
case 0x2:
xil_printf("SW 2 pressed!\n\r");
break;
}
// delay a bit here for 10ms
usleep(100 * 1000);
}

cleanup_platform();
return 0;
}

这个程序,基本上和 UltraZed-EG PCIe Carrier Card 开发纪录: Hello Cortex-A53 时对 LED 进行输出控制差不多,只是将原本的输出变成了输入。
特别要注意的事情是,由于轮询 (polling) 的速度很快,因此我们在循环里面加入了 usleep() 来做点延迟。
你可以依照 下载到开发板 (一次性) 上面的下载方式,并得到以下结果 (单击 SW2 ~ SW4 看看)

确认至少 SW2 ~ SW4 的设定没错后,再来看看如何透过中断 (Interrupt) 来取得这些按钮的状态并控制 LED 的亮暗吧 ~

加入 LED 的控制

在上面,我们做到了侦测 SW2 ~ SW4 不同按键按下的状态,这次就根据我们的结果来控制对应的 LED D12 ~ D14 吧,我们在 print("Hello Cortex-R5\n\r"); 后面加入我们对 LED 的初始化~
#define LED_CHANNEL 2
XGpio led;

int main()
{
// skip ....
print("Hello Cortex-R5\n\r");

// Initialize LEDs
ret = XGpio_Initialize(&led, XPAR_GPIO_0_DEVICE_ID);
if (ret != XST_SUCCESS)
return XST_FAILURE;

// Set direction on GPIO_0 channel 2 to output (LED)
XGpio_SetDataDirection(&led, LED_CHANNEL, 0xff);

// polling the SW2 ~ SW4 input result
while (1) {
int val = XGpio_DiscreteRead(&sw, 0x1);
switch(val) {
case 0x4:
xil_printf("SW 4 pressed!\n\r");
XGpio_DiscreteWrite(&led, LED_CHANNEL, 0x4); // D14: ON
break;
case 0x1:
xil_printf("SW 3 pressed!\n\r");
XGpio_DiscreteWrite(&led, LED_CHANNEL, 0x2); // D13: ON
break;
case 0x2:
xil_printf("SW 2 pressed!\n\r");
XGpio_DiscreteWrite(&led, LED_CHANNEL, 0x1); // D12: ON
break;
}
// delay a bit here for 10ms
usleep(100 * 1000);
}
// skip ...
}

在这边,要特别提到我们定义的 LED_CHANNEL 这个宏,它到底是干啥么用的呢? 如果将 LED 相关控制的程序,和我们的电路对照在一起就明显啦 ~

是的,由于我们在建立 axi_gpio_0 的时候,将 LED 用的输出脚放入到了 GPIO_0 的 Channel 2 上,因此就是需要这样设定才能点亮它~

完整程序代码

到目前为止,透过轮询(polling)来取得 SW2 ~ SW4 并分别控制 D12 ~ D14 的 LED 完整程序代码如下:

#include
#include "platform.h"
#include "xil_printf.h"
#include "xgpio.h"
#include "sleep.h"

#define LED_CHANNEL 2

XGpio sw;
XGpio led;

int main()
{
init_platform();

// Initialize SW2 ~ SW4
int ret = XGpio_Initialize(&sw, XPAR_GPIO_0_DEVICE_ID);
if (ret != XST_SUCCESS)
return XST_FAILURE;

// Setup gpio direction to IN, SW is at axi_gpio0 channel 1
XGpio_SetDataDirection(&sw, 1, 0xff);

print("Hello Cortex-R5\n\r");

// Initialize LEDs
ret = XGpio_Initialize(&led, XPAR_GPIO_0_DEVICE_ID);
if (ret != XST_SUCCESS)
return XST_FAILURE;

// Set direction on GPIO_0 channel 2 to output (LED)
XGpio_SetDataDirection(&led, LED_CHANNEL, 0xff);

// Polling the SW2 ~ SW4 input result
while (1) {
int val = XGpio_DiscreteRead(&sw, 0x1);
switch(val) {
case 0x4:
xil_printf("SW 4 pressed!\n\r");
XGpio_DiscreteWrite(&led, LED_CHANNEL, 0x4); // D14: ON
break;
case 0x1:
xil_printf("SW 3 pressed!\n\r");
XGpio_DiscreteWrite(&led, LED_CHANNEL, 0x2); // D13: ON
break;
case 0x2:
xil_printf("SW 2 pressed!\n\r");
XGpio_DiscreteWrite(&led, LED_CHANNEL, 0x1); // D12: ON
break;
}
// Delay a bit here for 10ms
usleep(100 * 1000);
}

cleanup_platform();
return 0;
}

我们将来看如何透过中断来达到一样的事情~

透过 SW 控制 LED (中断)

了解了如何透过轮询(polling) 的方式来使用 GPIO 相关的函式库后,这次来将刚刚的程序改写成中断 (interrupt) 的版本吧 !

清理目前的程序

我们先将刚刚的轮询的程序清理成这样,好方便后面程序的撰写
#include
#include "platform.h"
#include "xil_printf.h"
#include "xgpio.h"
#include "sleep.h"

#define LED_CHANNEL 2

XGpio sw;
XGpio led;

int main()
{
init_platform();

// Initialize SW2 ~ SW4
int ret = XGpio_Initialize(&sw, XPAR_GPIO_0_DEVICE_ID);
if (ret != XST_SUCCESS)
return XST_FAILURE;

// Setup gpio direction to IN, SW is at axi_gpio0 channel 1
XGpio_SetDataDirection(&sw, 1, 0xff);

print("Hello Cortex-R5\n\r");

// Initialize LEDs
ret = XGpio_Initialize(&led, XPAR_GPIO_0_DEVICE_ID);
if (ret != XST_SUCCESS)
return XST_FAILURE;

// Set direction on GPIO_0 channel 2 to output (LED)
XGpio_SetDataDirection(&led, LED_CHANNEL, 0xff);

/// <--- NOTE: Other Codes will be inserted here !!

// wait here
while (1) ;
cleanup_platform();
return 0;
}

加入中断控制

我们先在 while (1); 前面加入我们对中断控制器初始化用的函式 My_InterruptInitializer()
skip ...

int main()
{
/// <--- NOTE: Other Codes will be inserted here !!
ret = My_InterruptInitialize(XPAR_SCUGIC_0_DEVICE_ID);
if (ret != XST_SUCCESS)
return XST_FAILURE;

// wait here
while (1) ;
cleanup_platform();
return 0;
}

并在程序最前面,加入中断相关的头文件 xscugic.h ,还有一些方便我们撰写程序用的宏
#include "xscugic.h"

#define SW_INT XGPIO_IR_CH1_MASK
#define GPIO_0_INTERRUPT_ID XPAR_FABRIC_AXI_GPIO_0_IP2INTC_IRPT_INTR

XScuGic gic;

在这边, SW_INT 主要是帮助我们知道 axi_gpio_0 的通道 (channel) 屏蔽 (mask) ,好让我们知道当前是 axi_gpio_0 的哪个通道 (channel) 发出了中断 (interrupt)

而 GPIO_0_INTERRUPT_ID 则是对应到 Xilinx SDK 自动帮我们定义好的中断编号,你可以到 helloR5_bsp 里面去找对应的数值。

我们先定义一旦进入到中断时,相对应处理的函式 SW_Irq_Handler() ,在这边,我们做的事情和轮询(polling) 的版本很像,都是抓到 SW 的输入后,让相对应的 LED 进行输出。
不同的地方是,这个函式会在中断被触发的时候执行。

当进入到中断的时候,我们要先关掉该设备的中断,避免受到干扰,而当中断结束后,则是要回复这些设定。

有一点要注意的事情是,在中断处理的函式中,要 尽可能的快速处理 这样才可以避免影响到整体系统的其他程序。
void SW_Irq_Handler(void *gpio)
{
// Disable GPIO Interrupts
XGpio_InterruptDisable(gpio, SW_INT);

// Ignore addition button press
if ((XGpio_InterruptGetStatus(gpio) & SW_INT) != SW_INT)
return;

int val = XGpio_DiscreteRead(gpio, 1);
switch(val) {
case 0x4:
xil_printf("SW 4 pressed!\n\r");
XGpio_DiscreteWrite(&led, LED_CHANNEL, 0x4); // D14: ON
break;
case 0x1:
xil_printf("SW 3 pressed!\n\r");
XGpio_DiscreteWrite(&led, LED_CHANNEL, 0x2); // D13: ON
break;
case 0x2:
xil_printf("SW 2 pressed!\n\r");
XGpio_DiscreteWrite(&led, LED_CHANNEL, 0x1); // D12: ON
break;
}

// Clear the interrupt bit
XGpio_InterruptClear(gpio, SW_INT);
// Enabl GPIO interrupts
XGpio_InterruptEnable(gpio, SW_INT);
}

完成中断的处理函式后,我们需要将它喂给中断控制器,让它知道哪些设备要处理中断,因此让我们来弄我们的 My_InterruptInitialize() 吧
这边的初始化很单存,首先先初始化中断控制器,并将刚刚的 SW_Irq_Handler() 注册给这个控制器后,启用 GPIO 的中断后,就完成了。
int My_InterruptInitialize(u16 DeviceID)
{
// Interrpt controller initizlization
XScuGic_Config *IntcConfig = XScuGic_LookupConfig(DeviceID);
int ret = XScuGic_CfgInitialize(&gic, IntcConfig, IntcConfig->CpuBaseAddress);
if (ret != XST_SUCCESS)
return XST_FAILURE;

// Register Interrupt handler
Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_INT,
(Xil_ExceptionHandler) XScuGic_InterruptHandler,
&gic);
Xil_ExceptionEnable();

// Connect GPIO interrupt to handler
ret = XScuGic_Connect(&gic, GPIO_0_INTERRUPT_ID,
(Xil_ExceptionHandler) SW_Irq_Handler,
(void *) &sw);

if (ret != XST_SUCCESS)
return XST_FAILURE;

// Enable GPIO Interrupts
XGpio_InterruptEnable(&sw, SW_INT);
XGpio_InterruptGlobalEnable(&sw);

// Enable GPIO interrupts in the controller
XScuGic_Enable(&gic, GPIO_0_INTERRUPT_ID);

return XST_SUCCESS;
}

好啦~ 程序完成啦,可以准备下载了。 如果觉的哪些地方很模糊的话,完整的程序代码如下

完整程序代码

完整的程序代码如下:
#include
#include "platform.h"
#include "xil_printf.h"
#include "xgpio.h"
#include "sleep.h"
#include "xscugic.h"

XGpio sw;
XGpio led;
XScuGic gic;

#define LED_CHANNEL 2
#define SW_INT XGPIO_IR_CH1_MASK
#define GPIO_0_INTERRUPT_ID XPAR_FABRIC_AXI_GPIO_0_IP2INTC_IRPT_INTR

void SW_Irq_Handler(void *gpio)
{
// Disable GPIO Interrupts
XGpio_InterruptDisable(gpio, SW_INT);

// Ignore addition button press
if ((XGpio_InterruptGetStatus(gpio) & SW_INT) != SW_INT)
return;

int val = XGpio_DiscreteRead(gpio, 1);
switch(val) {
case 0x4:
xil_printf("SW 4 pressed!\n\r");
XGpio_DiscreteWrite(&led, LED_CHANNEL, 0x4); // D14: ON
break;
case 0x1:
xil_printf("SW 3 pressed!\n\r");
XGpio_DiscreteWrite(&led, LED_CHANNEL, 0x2); // D13: ON
break;
case 0x2:
xil_printf("SW 2 pressed!\n\r");
XGpio_DiscreteWrite(&led, LED_CHANNEL, 0x1); // D12: ON
break;
}

// Clear the interrupt bit
XGpio_InterruptClear(gpio, SW_INT);
// Enabl GPIO interrupts
XGpio_InterruptEnable(gpio, SW_INT);
}

int My_InterruptInitialize(u16 DeviceID)
{
// interrpt controller initizlization
XScuGic_Config *IntcConfig = XScuGic_LookupConfig(DeviceID);
int ret = XScuGic_CfgInitialize(&gic, IntcConfig, IntcConfig->CpuBaseAddress);
if (ret != XST_SUCCESS)
return XST_FAILURE;

// Register Interrupt handler
Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_INT,
(Xil_ExceptionHandler) XScuGic_InterruptHandler,
&gic);
Xil_ExceptionEnable();

// Connect GPIO interrupt to handler
ret = XScuGic_Connect(&gic, GPIO_0_INTERRUPT_ID,
(Xil_ExceptionHandler) SW_Irq_Handler,
(void *) &sw);

if (ret != XST_SUCCESS)
return XST_FAILURE;

// Enable GPIO Interrupts
XGpio_InterruptEnable(&sw, SW_INT);
XGpio_InterruptGlobalEnable(&sw);

// Enable GPIO interrupts in the controller
XScuGic_Enable(&gic, GPIO_0_INTERRUPT_ID);

return XST_SUCCESS;
}

int main()
{
init_platform();

// Initialize SW2 ~ SW4
int ret = XGpio_Initialize(&sw, XPAR_GPIO_0_DEVICE_ID);
if (ret != XST_SUCCESS)
return XST_FAILURE;

// setup gpio direction to IN
XGpio_SetDataDirection(&sw, 1, 0xff);

print("Hello Cortex-R5\n\r");

// Initialize LEDs
ret = XGpio_Initialize(&led, XPAR_GPIO_0_DEVICE_ID);
if (ret != XST_SUCCESS)
return XST_FAILURE;

// setup gpio direction to OUT
XGpio_SetDataDirection(&led, 2, 0xff);

// Initialize Interrupt
ret = My_InterruptInitialize(XPAR_SCUGIC_0_DEVICE_ID);
if (ret != XST_SUCCESS)
return XST_FAILURE;

// wait for interrupt triggered
while (1) ;
cleanup_platform();
return 0;
}

设定 JTAG 下载

为了透过 Micro USB 连接到 UltraZed-EG PCIe Carrier Card 上的 JTAG 来进行下载,我们需要对 UltraZed-EG上的 SW2 要进行一些调整,变成下图这样。

这样子就可以透过 Micro USB 走 JTAG 下载的路线,将程序下载下去

下载到开发板 (一次性)

和 UltraZed-EG PCIe Carrier Card 开发纪录: Hello Cortex-A53 一文不同的是,这次我们不再分别下载 FPGA 和我们的程序,这次采用一次性下载的方案
点选 Run -> Run Configurations

在 Xilinx C/C++ Application (GDB) 建立新的设定,并设定如下:

点入 Application 确定我们的程序会下载到 psu_cortexr5_0 去

这样就完成啰,点选 Run 就会看到 Xilinx SDK 先刻录 FPGA 再下载这次的程序了~

结果

按照本篇文章的设定,你的 UltraZed-EG PCIe Carrier Card 显示应该如以下影片:

另外,我们也可以透过 minicom, emacs, tio, gtkterm 等终端机软件,连接上 /dev/ttyUSB0 来查看透过 printf() 输出的讯息。

取得程序代码

本文的范例已经上传到 coldnew/ultrazed_pciecc_helloR5 ,你可以透过以下命令获得

git clone https://github.com/coldnew-examples/ultrazed_pciecc_helloR5.git

延伸阅读

本文转载自:coldnew's blog