在 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 <stdio.h> #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 <stdio.h> #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 相關控制的程式,和我們的電路對照在一起就明顯啦 ~
Code
#define LED_CHANNEL 2 XGpio led; // Initalize GPIO_0 XGpio_Initialize(&led, XPAR_GPIO_0_DEVICE_ID); // Set direction on GPIO_0 channel 2 to output (LED) XGpio_SetDataDirection(&led, LED_CHANNEL, 0xff); // Make D14 ON (GPIO_0 channel 2) XGpio_DiscreteWrite(&led, LED_CHANNEL, 0x4); // D14: ON
AXI_GPIO_0
是的,由於我們在建立 axi_gpio_0
的時候,將 LED 用的輸出腳放入到了 GPIO_0
的 Channel 2
上,因此就是需要這樣設定才能點亮它~
完整程式碼
到目前為止,透過輪詢(polling)來取得 SW2 ~ SW4 並分別控制 D12 ~ D14 的 LED 完整程式碼如下:
#include <stdio.h> #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 <stdio.h> #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 <stdio.h> #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
延伸閱讀
- ECE699: Lecture 4 - Intrrrupts AXI GPIO and AXI Timer.pdf
- AXI GPIO v2.0 LogiCORE IP Product Guide.pdf
- UG643 (v2018.2): Xilinx Standalone Library Documentation - OS and Libraries Document Collection.pdf
- Xilinx Wiki: AXI gpio standalone driver
- UltraZed-EG PCIe Carrier Card 開發紀錄: Hello Cortex-A53
- UG1137 (v8.0): Zynq UltraScale+ MPSoC Software Developer Guide.pdf
- AR# 51763: Zynq-7000 - How do I know the IRQ ID# of F2P_IRQ when I connect interrupt signals from PL to PS?