S02_CH07_ ZYNQ PL中斷請求
7.1 ZYNQ 中斷介紹
7.1.1 ZYNQ中斷框圖
可以看到本例子中PL到PS部分的中斷經過ICD控制器分發器后同時進入CPU1 和CPU0。從下面的表格中可以看到中斷向量的具體值。PL到PS部分一共有20個中斷可以使用。其中4個是快速中斷。剩余的16個是本章中涉及了,可以任意定義。如下表所示。
7.1.2 ZYNQ CPU軟件中斷 (SGI)
ZYNQ 2個CPU 都具備各自16個軟件中斷。
7.1.3 ZYNQ CPU 私有端口中斷
這些中斷都是固定死的,不能修改。這里有2個PL到CPU的快速中斷nFIQ
共享中斷就是PL的中斷可以發送給PS處理。上圖中,黃色區域就是16個PL的中斷,它們可以設置為高電平或者低電平觸發。
7.2 搭建硬件地址
Step1:新建一個名為為Miz_sys的工程,芯片類型根據自身情況設置。
Step2:創建一個BD文件,並命名為system。
Step3:添加 ZYNQ7 Processing System,根據自己的硬件類型配置好輸入時鍾頻率與內存型號。
Step4:在ZYNQ7 Processing System配置窗口中,使能中斷,單擊OK完成配置。
Step5:單擊添加IP按鈕,添加兩個邏輯門模塊和一個concat IP。
Step6:雙擊邏輯門模塊,將其配置為非功能。
Step7:按以下電路,完善整體電路。
Step8:右鍵單擊Block文件,文件選擇Generate the Output Products。
Step9:右鍵單擊Block文件,選擇Create a HDL wrapper,根據Block文件內容產生一個HDL 的頂層文件,並選擇讓vivado自動完成。
Step10:添加一個約束文件,打開對應自己硬件的原理圖,查看按鍵部分引腳連接情況,完成約束。Miz702約束文件如下所示:
set_property PACKAGE_PIN T18 [get_ports {SW1[0]}] set_property IOSTANDARD LVCMOS33 [get_ports {SW1[0]}] set_property PACKAGE_PIN R18 [get_ports {SW2[0]}] set_property IOSTANDARD LVCMOS33 [get_ports {SW2[0]}] |
Step10:生成Bit文件。
7.3 加載到SDK
Step1:導出硬件。
Step2:新建一個空SDK工程,並添加一個main.c的文件。
Step3:在main.c文件中添加以下程序,按Ctrl+S保存后自動開始編譯。
#include <stdio.h> #include "xscugic.h" #include "xil_exception.h" #define INT_CFG0_OFFSET 0x00000C00 // Parameter definitions #define SW1_INT_ID 61 #define SW2_INT_ID 62 #define INTC_DEVICE_ID XPAR_PS7_SCUGIC_0_DEVICE_ID #define INT_TYPE_RISING_EDGE 0x03 #define INT_TYPE_HIGHLEVEL 0x01 #define INT_TYPE_MASK 0x03 static XScuGic INTCInst; static void SW_intr_Handler(void *param); static int IntcInitFunction(u16 DeviceId); static void SW_intr_Handler(void *param) { int sw_id = (int)param; printf("SW%d int\n\r", sw_id); } void IntcTypeSetup(XScuGic *InstancePtr, int intId, int intType) { int mask; intType &= INT_TYPE_MASK; mask = XScuGic_DistReadReg(InstancePtr, INT_CFG0_OFFSET + (intId/16)*4); mask &= ~(INT_TYPE_MASK << (intId%16)*2); mask |= intType << ((intId%16)*2); XScuGic_DistWriteReg(InstancePtr, INT_CFG0_OFFSET + (intId/16)*4, mask); } int IntcInitFunction(u16 DeviceId) { XScuGic_Config *IntcConfig; int status; // Interrupt controller initialisation IntcConfig = XScuGic_LookupConfig(DeviceId); status = XScuGic_CfgInitialize(&INTCInst, IntcConfig, IntcConfig->CpuBaseAddress); if(status != XST_SUCCESS) return XST_FAILURE; // Call to interrupt setup Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_INT, (Xil_ExceptionHandler)XScuGic_InterruptHandler, &INTCInst); Xil_ExceptionEnable(); // Connect SW1~SW3 interrupt to handler status = XScuGic_Connect(&INTCInst, SW1_INT_ID, (Xil_ExceptionHandler)SW_intr_Handler, (void *)1); if(status != XST_SUCCESS) return XST_FAILURE; status = XScuGic_Connect(&INTCInst, SW2_INT_ID, (Xil_ExceptionHandler)SW_intr_Handler, (void *)2); if(status != XST_SUCCESS) return XST_FAILURE; // Set interrupt type of SW1~SW3 to rising edge IntcTypeSetup(&INTCInst, SW1_INT_ID, INT_TYPE_RISING_EDGE); IntcTypeSetup(&INTCInst, SW2_INT_ID, INT_TYPE_RISING_EDGE); // Enable SW1~SW3 interrupts in the controller XScuGic_Enable(&INTCInst, SW1_INT_ID); XScuGic_Enable(&INTCInst, SW2_INT_ID); return XST_SUCCESS; } int main(void) { print("PL int test\n\r"); IntcInitFunction(INTC_DEVICE_ID); while(1); return 0; } |
Step4:右擊工程,選擇Debug as ->Debug configuration。
Step5:選中system Debugger,雙擊創建一個系統調試。
Step6:設置系統調試。
打開系統自帶的窗口調試助手,點擊運行按鈕開始運行程序。
系統運行結果如下圖所示:
7.4 程序分析
接下來,我們對本章節的程序做一個詳細的分析。還是先從main函數開始分析。第一句打印標題我們略過,直接看到這一句,這個函數只帶了一個參數,我們選中這個參數,直接按F3跟蹤一下這個參數。
從上圖可以看到,這個參數是系統的中斷的設備ID基地址的宏定義,也就是中斷的基地址。
我們返回main函數當中,選中這個函數,按F3對其跟蹤,查看一下此函數的定義。
程序一開頭還是定義了一些要用到的指針和變量。接下來是一個跟第二章講過的相似的一個查找設備配置的程序,帶的參數為設備ID,也就是看我們的中斷向量是否存在,感興趣的可以選中這個程序,按下F3查看其定義。
接下來依然是一個狀態檢測,這是xilinx初始化的老套路,當執行完這一句后,系統會對我們的中斷做一些初始化,如果初始化成功,會返回一個XST_SUCCESS的標志。當未檢測到返回到這個初始化成功的標志時,系統會返回一個XST_FAILURE標志。
接下來是一個中斷注冊函數Xil_ExceptionRegisterHandler,按照之前講過的方法,查看其函數定義。
從上面可以看到這個函數是把中斷的句柄和中斷的參數放到了兩個數組當中,選中這個數組按下F3來看看這個數組。
可以看到這個數組的結構如上圖所示,它是由一個結構體定義的,這個結構體定義如下圖所示:
接下來看到這段程序:
通過上圖中的程序,可以連接到我們的中斷,我們查看下其定義。
上圖可以看到方框中的語句把我們的中斷的句柄和一個指針變量傳遞了進來,也就是把XScuGic_Connect函數的最后兩個函數傳遞了進來。此時我們返回繼續查看XScuGic_Connect函數,我們發現中斷的句柄其實是個指針函數,也就是說當程序被執行的時候,其實被調用的是這個指針函數,此時我們跟蹤這個指針函數,查看它具體做了些什么。
通過程序開頭xilinx給出的這個程序的注釋可以知道:這個函數是基本的中斷驅動函數。它必須連接到中斷源,以便在中斷控制器的中斷激活時被調用。 它將解決哪些中斷是活動的和啟用的,並調用適當的中斷處理程序。 它使用中斷類型信息來確定何時確認中斷。首先處理最高優先級的中斷。此函數假定中斷向量表已預先初始化。 它不會在調用中斷處理程序之前驗證表中的條目是否有效。
上面講到的這個中斷向量表其實也就是下圖所示的部分。
這部分在剛才已經進行了講解了,此時我們就可以清楚的知道這就是一個中斷向量表了。
回到基本的中斷驅動函數的分析,看到下面的一段程序:
通過注釋我們知道了這個程序是讀取int_ack寄存器以識別最高優先級的中斷ID,並確保其有效。 讀取Int_Ack將清除GIC中的中斷。然后看看讀出來的中斷ID是否大於最大的中斷值。查看下這個最大的中斷值。
從上圖中圈出的地方可以看到,當使用ZYNQ的時候,最大有95個中斷可以供我們使用。當讀出來的這個中斷值大於95U的話,就直接跳轉到異常處理程序部分:
這里的意思也就相當於恢復中斷寄存器,相當於出棧。
當讀出來的中斷值是正常的話,就會查找這個中斷的中斷向量表,如果這個向量表不是非空的話,就開始處理這個中斷,也就是開始執行之前的連接中斷的函數。此部分程序如下:
上圖中的Tableptr指向的CallBackRef其實就是我們連接中斷函數定義的無符號的數字,如下圖所示。
為了驗證我們的猜想,我們可以把這里的數字改成其他的值進行驗證。
回到主程序當中,接着看到這段函數:
這段程序把中斷的觸發類型設置為了上升沿觸發。
這段程序使能了中斷。
整段程序下來,那么主要是執行了哪個函數呢?通過上面的分析,我們可以判定其實是下面這個函數:
這個函數的方框部分其實是個指針函數,我們可以跟蹤看一下其定義。
一開始,它將傳遞進來的指針傳遞給了sw_id,然后會打印哪個按鈕初始化,其實也就是哪個中斷被觸發了。
接下來,我們再對中斷的一些寄存器做一些分析。在中斷設置里的一些寄存器是比較重要的,我們就來分析一下中斷設置里的寄存器。
將鼠標停留在圖上圈出的函數上,SDK會跳出關於這個函數的信息,在跳出的窗口中左邊是我們圈出的這個函數的定義,右邊則是在執行過程中實際運行的程序。我們拷貝出右邊這個函數來分析一下:
((Xil_In32((((InstancePtr)->Config->DistBaseAddress)) + ((0x00000C00 + (intId/16)*4)))))
紅色部分是一個指針,它調用了config里的一個基地址DisBaseAddress,后半部分我們可以斷定這是一個寄存器地址,因為這個函數就是一個讀取中斷寄存器的函數。此時,我們跟蹤一下這個函數。
此時,我們就知道了第一個參數是一個指向要處理的中斷的指針,第二個是寄存器偏移。我們就來計算一下這個寄存器偏移。首先我們來看看中斷的基地址是多少(也就是紅色部分指向的基地址)。
在xparameters.h中,找到了中斷的基地址,如圖中方框部分,為F8F01000。IntId就是定義的哪個按鈕將被初始化,此處以SW1為例,SW1的ID為,等於61,此時可以算出:寄存器的地址= F8F01000+((0x00000C00+(61/16)*4))= F8F01C0C。打開ug585,查看一下這個寄存器是什么功能。
從圖中我們可以看出這是一個設置中斷觸發方式的寄存器,01的時候,高電平觸發,11的時候,上升沿觸發。從上表中可以看到每個中斷ID都由兩位表示,而寄存器又是32位數據,因此,可以算出總共我們可以設置16個中斷ID,這也是程序中為什么要除以16的原因。接下來看到Intcsetup的下一句。當執行完這一句后,我們來計算一下寄存器地址變為了多少?在前面的定義中,我們找到INT_TYPE_MASK的值,
,因此可以計算出此時寄存器的值為:F8F01C0C &(~(C000000))=F0F01C0C。下一句又是一個運算,這次我們直接計算:F0F01C0C | 3FFFFFF =F3FF1C0C。也就是說最終寫入寄存器的值是這個值。可以對照ug585查看配置的信息。
其他的寄存器設置的分析方法與上面的一致,在此就不再反復講解了。
7.5 本章小結
本章學習了外部中斷,通過PL傳遞開發板按鍵的中斷,然后在PS接受處理中斷。