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 #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接受处理中断。