ZYNQ学习笔记2-ZYNQ的UART控制器5
2026/9/10 2:51:42 网站建设 项目流程

本文主要以UART测试程序为例分析ZYNQ的中断流程。

一、中断流程整体分析

1.中断产生

ZYNQ的UART中断分为FIFO中断和非FIFO中断两种。每种又有很多种类,比FIFO中断有FIFO空,FIFO满,到达触发值等,非FIFO中断有超市、校验错误等。不过无论具体原因是什么,查阅UG585可知,所有UART中断都属于IRQ中断如下图:

查看ARM手册《ARM® Architecture Reference Manual》(DDI0406C),IRQ中断偏移量为0x18,也就是说,当IRQ中断发生时,CPU会去vect_table偏移0x18位置执行程序。因此IRQ的中断程序必须放在vect_table偏移量为0x18的位置。

根据《ARM® Architecture Reference Manual》(DDI0406C),当出现有效的UART中断后,IRQ信号有效。可知IRQ处理流程如下:

根据上图可知,当IRQ中断发生时,系统首先暂存寄存器当前值,然后给出向量表偏移量24(0x18)(vect_table),然后检测当前是否是安全模式或者虚拟模式,裸机情况下都不是,直接进入IRQ处理模式(默认模式)。在跳转前会对系统进行一些配置如:切换到 IRQ 模式(CPSR.M = '10010')。将 new_spsr_value 写入 SPSR_irq(即 SPSR[]),new_lr_value 写入 LR_irq(即 R[14])。CPSR.I = '1':禁用 IRQ(禁止嵌套)。如果满足特定条件,还会禁用异步中止(CPSR.A = '1')。重置 IT 状态(Thumb-2 条件执行),并根据 SCTLR.TE 设置 CPSR.T(决定后续指令是 ARM 还是 Thumb),最后根据 SCTLR.EE 设置大小端。如果启用了向量中断(SCTLR.VE == 1),则由实现定义的方式跳转(通常由中断控制器提供地址)。否则,跳转到常规 IRQ 向量:ExcVectorBase() + 0x18。ExcVectorBase() 由 SCTLR.V 决定:V=0 时为 0x00000000,V=1 时为 0xFFFF0000。ZYNQ不具备向量中断模式,所以直接执行的就是ExcVectorBase() + 0x18。

如果把整个CPU比作一个大型集团公司的话,vector_table可以说是整个集团公司的董事长,掌管整个集团的事务,把握大方向。当发现出现需要IRQ这个分公司需要处理的业务的时候,一个电话打给IRQ的董事长,让他去处理。

说了一堆总结也是一句话,就是IRQ中断发生时,系统回到中断向量表偏移0x18的位置去执行程序。

2.中断向量表

打开测试工程bsp的asm_vector.S文件,可以看到ZYNQ中断向量表的分配情况如下:

vector_table: B _boot B Undefined B SVCHandler B PrefetchAbortHandler B DataAbortHandler NOP /* Placeholder for address exception vector*/ B IRQHandler B FIQHandler

ZYNQ一个向量占据四个字节的空间,可以看到IRQHandler正好处于是第7个向量。因此地址是24(0x18)。也就是IRQ中断发生时,系统到0x18处执行指令,执行的指令只有一条,那就是跳转到IRQHandler处(B IRQHandler)。那么继续往下看IRQHandler处的代码(也在asm_vector.S文件中):

IRQHandler: /* IRQ vector handler */ stmdb sp!,{r0-r3,r12,lr} /* state save from compiled code*/ #if FPU_HARD_FLOAT_ABI_ENABLED vpush {d0-d7} vpush {d16-d31} vmrs r1, FPSCR push {r1} vmrs r1, FPEXC push {r1} #endif #ifdef PROFILING ldr r2, =prof_pc subs r3, lr, #0 str r3, [r2] #endif bl IRQInterrupt /* IRQ vector */ #if FPU_HARD_FLOAT_ABI_ENABLED pop {r1} vmsr FPEXC, r1 pop {r1} vmsr FPSCR, r1 vpop {d16-d31} vpop {d0-d7} #endif ldmia sp!,{r0-r3,r12,lr} /* state restore from compiled code */ subs pc, lr, #4 /* adjust return */

IRQHandler代码中主要内容可以分为三部分:跳转前,跳转和跳转后。

跳转前:主要工作就是保存当前的工作状态,代码如下:

stmdb sp!,{r0-r3,r12,lr} //保存工作寄存器:r0-r3(函数参数/临时变量)、r12(内部调用暂存)、lr(当前模式的链接寄存器) #if FPU_HARD_FLOAT_ABI_ENABLED //如果定义了硬件浮点(FPU_HARD_FLOAT_ABI_ENABLED) vpush {d0-d7} //入栈d0-d7 vpush {d16-d31} //入栈d16-d31 vmrs r1, FPSCR push {r1} //入栈浮点状态控制寄存器(FPSCR) vmrs r1, FPEXC push {r1} //入栈浮点异常寄存器(FPEXC) #endif #ifdef PROFILING //如果定义了性能分析宏(PROFILING) ldr r2, =prof_pc //返回地址(lr)存到一个全局变量 prof_pc subs r3, lr, #0 str r3, [r2] #endif

跳转:保存完之后开始执行长跳转指令,跳转到中断服务函数,这是跳转真正发生的地方。代码如下:

bl IRQInterrupt

跳转后:执行完中断服务程序返回当前位置,开始恢复现场。

#if FPU_HARD_FLOAT_ABI_ENABLED //如果启用了硬件浮点(FPU_HARD_FLOAT_ABI_ENABLED) pop {r1} //FPEXC出栈 vmsr FPEXC, r1 pop {r1} //FPSCR出栈 vmsr FPSCR, r1 vpop {d16-d31} //d16-d31出栈 vpop {d0-d7} //d0-d7出栈 #endif ldmia sp!,{r0-r3,r12,lr} //r0-r3,r12,lr出栈 subs pc, lr, #4 //返回程序原位置

这里拓展说下为什么pc-4就能回中断位置执行程序。ARM手册(DDI0406C)表述如下:

中断发生时,ARM可以保证把当前正在执行的指令完整执行,然后指针指向下一条指令,然后硬件在进入 IRQ 时,LR_irq被设置为PC + 4(即被中断指令的下一条再下一条,实际上多加了 4),所以返回时需要减 4 才能正确指向被中断指令的下一条指令。

至此可以发现,系统进入中断向量表后,通过长跳转指令跳转到IRQInterrupt。

3.IRQInterrupt函数

IRQInterrupt函数很简单,就是一个函数调用

void IRQInterrupt(void) { XExc_VectorTable[XIL_EXCEPTION_ID_IRQ_INT].Handler(XExc_VectorTable[ XIL_EXCEPTION_ID_IRQ_INT].Data); }

其中XIL_EXCEPTION_ID_IRQ_INT值为5,所以简化一下这个函数,可以这样等效:

void IRQInterrupt(void) { XExc_VectorTable[5].Handler(XExc_VectorTable[5].Data); }

也就是调用XExc_VectorTable数组第六个元素的Handler函数,第六个元素Data为参数。找到定义如下。可见默认情况下第六个元素为空,需要用户注册。

XExc_VectorTableEntry XExc_VectorTable[XIL_EXCEPTION_ID_LAST + 1] = { {Xil_ExceptionNullHandler, NULL}, {Xil_UndefinedExceptionHandler, NULL}, {Xil_ExceptionNullHandler, NULL}, {Xil_PrefetchAbortHandler, NULL}, {Xil_DataAbortHandler, NULL}, {Xil_ExceptionNullHandler, NULL}, {Xil_ExceptionNullHandler, NULL}, };

注册在Xil_ExceptionRegisterHandler函数实现,相关程序片段如下

//调用语句,其中XIL_EXCEPTION_ID_INT = 5 Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_INT,(Xil_ExceptionHandler)XScuGic_InterruptHandler,&XScuGic_inst); //函数主体 void Xil_ExceptionRegisterHandler(u32 Exception_id, Xil_ExceptionHandler Handler, void *Data) { #if defined (versal) && !defined(ARMR5) && EL3 /* * Cortexa72 processor in versal is coupled with GIC-500, and GIC-500 supports * only FIQ at EL3. Hence, tweaking this API to always act on FIQ, * ignoring argument passed by user. */ Exception_id = XIL_EXCEPTION_ID_FIQ_INT; #endif XExc_VectorTable[Exception_id].Handler = Handler; XExc_VectorTable[Exception_id].Data = Data; }

通过Xil_ExceptionRegisterHandler函数的函数体可以发现,此函数的功能很简单,就是向XExc_VectorTable数组写入数据。分别向Handler成员和Data成员赋值。根据程序的调用语句调用内容可知,此程序是将向XExc_VectorTable的元素5中的Handler赋值为XScuGic_InterruptHandler,Data成员赋值为XScuGic_inst变量地址。赋值后,再调用IRQInterrupt,实际上应该调用的是:

void IRQInterrupt(void) { XScuGic_InterruptHandler(&XScuGic_inst); }

至此,可总结中断流程如下:UART满足中断条件后会申请IRQ异常,CPU接受到此异常后硬件自动到向量表0x18位置执行程序。0x18出只有一句话即 B IRQHandler,CPU执行此指令跳转到 IRQHandler处。在IRQHandler处,CPU将当前环境入栈暂存,然后执行长跳转到IRQInterrupt函数,IRQInterrupt功能是主要是执行XExc_VectorTableEntry 数据中元素5的的函数。由于例程在主函数中已通过Xil_ExceptionRegisterHandler函数将元素5注册为XScuGic_InterruptHandler函数,因此CPU会执行XScuGic_InterruptHandler函数。执行完成后,CPU回到asm_vector.S将之前入栈的变量出栈,然后回到中断前指令的下一条指令继续执行新指令。

4.XScuGic_InterruptHandler函数

XScuGic_InterruptHandler主要内容如下:

void XScuGic_InterruptHandler(XScuGic *InstancePtr) { //……省略非必要内容 IntIDFull = XScuGic_CPUReadReg(InstancePtr, XSCUGIC_INT_ACK_OFFSET); InterruptID = IntIDFull & XSCUGIC_ACK_INTID_MASK; //……省略非必要内容 //此处为主要功能 TablePtr = &(InstancePtr->Config->HandlerTable[InterruptID]); if (TablePtr != NULL) { TablePtr->Handler(TablePtr->CallBackRef); //……省略非必要内容 }

根据函数体内容可见,此函数先通过读取InterruptID 确定是发生了什么中断,然后根据InterruptID从InstancePtr中加载特定函数。因此CPU执行到XScuGic_InterruptHandler后会执行什么函数,由InstancePtr的内容决定。InstancePtr中到底是什么内容,由主程序函数在执行XScuGic_Connect函数时决定,XScuGic_Connect相关的主要内容如下:

//下为调用语句 XScuGic_Connect(&XScuGic_inst,UART_INTR_S_ID, (Xil_InterruptHandler)XUartPs_InterruptHandler, (void *)&XUartPs_0); //下为函数体 s32 XScuGic_Connect(XScuGic *InstancePtr, u32 Int_Id, Xil_InterruptHandler Handler, void *CallBackRef) { //……此处省略主要内容 InstancePtr->Config->HandlerTable[Int_Id].Handler = (Xil_InterruptHandler)Handler; InstancePtr->Config->HandlerTable[Int_Id].CallBackRef = CallBackRef; }

根据函数体内容可知,执行完此函数后,UART的ID对应的元素的Handler成员被注册为XUartPs_InterruptHandler函数,CallBackRef元素被注册为XUartPs_0变量。

还是把CPU比作一个集团,XScuGic_InterruptHandler就是IRQ分公司的老大,掌管所有IRQ中断。总公司把活分下来之后,XScuGic_InterruptHandler分析分析任务的主要内容,最后发现这个需求需要UART这条“产品线”处理,便把任务再次分给UART产品线。

至此,可以再次总结中断流程如下:UART满足中断条件后会申请IRQ异常,CPU接受到此异常后硬件自动到向量表0x18位置执行程序。0x18出只有一句话即 B IRQHandler,CPU执行此指令跳转到 IRQHandler处。在IRQHandler处,CPU将当前环境入栈暂存,然后执行长跳转到IRQInterrupt函数,IRQInterrupt功能是主要是执行XExc_VectorTableEntry 数据中元素5的的函数。由于例程在主函数中已通过Xil_ExceptionRegisterHandler函数将元素5注册为XScuGic_InterruptHandler函数,因此CPU会执行XScuGic_InterruptHandler函数。XScuGic_InterruptHandler主要功能是根据读到InterruptID 调用InstancePtr对应位置的函数和参数。InstancePtr变量中的函数注册在XScuGic_Connect执行,通过XScuGic_Connect函数,已经将InstancePtr函数注册为XUartPs_InterruptHandler,因此系统会执行XUartPs_InterruptHandler函数。并在执行完成后,回到asm_vector.S将之前入栈的变量出栈,然后回到中断前指令的下一条指令继续执行新指令。

5.XUartPs_InterruptHandler函数

XUartPs_InterruptHandler函数非常复杂,分析函数的具体内容不是本文的主要任务,本文主要是要简明扼要的讲述中断执行的整个流程,力求简洁清晰。在这里仅做XUartPs_InterruptHandler功能简述。简单来说XUartPs_InterruptHandler是通过读取中断源与中断使能来判断要执行什么中断服务程序。以UART举例,中断发生后经过前面一系列的跳转,来到了XUartPs_InterruptHandler函数,CPU读完寄存器来判断,当前是应该执行FIFO空的中断呐,还是接受满的中断,还是超时中断还是校验错误的中断等等。一旦判断完成便进入子服务函数。换句话说,XUartPs_InterruptHandler是起到一个进一步分发的作用。相当于把IRQ进行进一步细分。IRQ区分设备级,比如UART会引发IRQ中断,CAN也能引发IRQ中断,USB也会引发IRQ中断,XScuGic_InterruptHandler就是为了分清楚到底是哪个设备引发的IRQ。但XUartPs_InterruptHandler是已经确认是UART引发的,但是要进一步分清楚UART为什么会引发中断。

如果还是把CPU比作一个集团,XUartPs_InterruptHandler就是UART这个“产品线”的负责人了,或者说某个具体部门的经理,负责分一分这个活到底是谁来干。换句话说XUartPs_InterruptHandler其实还是没有干具体的活,还是把活分下去了。

XUartPs_InterruptHandler具体内容详见之前两篇文章,这里不赘述了。

打开XUartPs_InterruptHandler中断的具体子函数可以发现他其实调用的都是同一个函数,以SendDataHandler函数为例,其主要内容如下:

static void SendDataHandler(XUartPs *InstancePtr, u32 IsrStatus) { //……省略非主要内容 InstancePtr->Handler(InstancePtr->CallBackRef, XUARTPS_EVENT_SENT_DATA, InstancePtr->SendBuffer.RequestedBytes - InstancePtr->SendBuffer.RemainingBytes); //……省略非主要内容 }

可见,最后都是调用InstancePtr->Handler指向的函数,不同的是参数不一样给这个函数传入的参数不一样。很不幸,如果还是把一个CPU比作一个集团,这个函数怕就是我与在座的诸位了😅

就是不管是搬桌子还是擦凳子,干活的永远是这个人,只是上级给的指令不一样,他干的活不一样,简直苦逼😫。那么是谁给他找的这个苦逼的工作呐。这个承担了HR角色的函数就是XUartPs_SetHandler函数。主要内容如下:

//调用语句 XUartPs_SetHandler(&XUartPs_0, (XUartPs_Handler)uart_intr_Handler, &XUartPs_0); //函数体 void XUartPs_SetHandler(XUartPs *InstancePtr, XUartPs_Handler FuncPtr, void *CallBackRef) { //……省略非必要部分 InstancePtr->Handler = (XUartPs_Handler)FuncPtr; InstancePtr->CallBackRef = CallBackRef; }

可见通过该函数,系统将uart_intr_Handler函数注册给了InstancePtr->Handler。最后系统将执行uart_intr_Handler。uart_intr_Handler这个牛马就开始苦哈哈干活了。

至此,整个中断流程分析结束,总结如下:

UART满足中断条件后会申请IRQ异常,CPU接受到此异常后硬件自动到向量表0x18位置执行程序。0x18出只有一句话即 B IRQHandler,CPU执行此指令跳转到 IRQHandler处。在IRQHandler处,CPU将当前环境入栈暂存,然后执行长跳转到IRQInterrupt函数,IRQInterrupt功能是主要是执行XExc_VectorTableEntry 数据中元素5的的函数。由于例程在主函数中已通过Xil_ExceptionRegisterHandler函数将元素5注册为XScuGic_InterruptHandler函数,因此CPU会执行XScuGic_InterruptHandler函数。XScuGic_InterruptHandler主要功能是根据读到InterruptID 调用InstancePtr对应位置的函数和参数。InstancePtr变量中的函数注册在XScuGic_Connect执行,通过XScuGic_Connect函数,已经将InstancePtr函数注册为XUartPs_InterruptHandler,因此系统会执行XUartPs_InterruptHandler函数,在XUartPs_InterruptHandler函数中,CPU读取UART的中断源和使能情况,判断应该响应那个子服务程序。不过不管是哪个子服务程序,系统都将统一执行InstancePtr->Handler所指向的函数。此函数是通过XUartPs_SetHandler来注册的。区别在于每个子服务都有自己独特编号(事件码),用于区分到底是什么中断。此外不同的服务函数还会根据中断源进行少量的不要操作。但InstancePtr->Handler所指向的函数实际上才是是最后由用户来编辑的,具有实际功能的函数。在执行完该函数后,CPU回到asm_vector.S将之前入栈的变量出栈,然后回到中断前指令的下一条指令继续执行新指令。

二、中断流程图

整体程序流程如下图所示:

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询