Zynq PL按键通过EMIO中断控制PS LED实战指南
发布时间:2026/10/13 6:32:12 锦皓数字建站

1. 从PL到PS的中断链路为什么不用轮询而选EMIO加中断第一次在Zynq开发板上做PL按键控制PS侧LED的实验时很多人下意识会选轮询方案在PS的裸机程序里写一个while循环不断读取GPIO的数据寄存器检测到按键按下就去翻转LED。这个思路在功能上没问题但实际跑起来问题不少。轮询会持续占用CPU按键响应还依赖循环周期按快一点就容易丢事件。更关键的是轮询模式下PS和PL之间的数据交互是单向的读没有真正用上Zynq这颗芯片异构协同的价值。中断模式就完全不一样了。PL侧的按键信号通过EMIOExtended MIO路由到PS的GPIO控制器再配置成中断源按键一按下GIC通用中断控制器直接把中断请求推给CPUCPU在中断服务程序里处理LED翻转。整个过程CPU平时可以干别的事只有事件发生才被唤醒。这就是典型的事件驱动思路也是嵌入式系统里处理异步输入的标准做法。这个实验的核心价值在于它把Zynq最典型的软硬件协同流程完整走了一遍Vivado里做Block Design、配置EMIO、导出硬件、在SDK或Vitis里写中断服务程序、下载验证。对于刚接触Zynq的开发者来说这是一个非常好的入门级综合练习涉及GPIO、中断控制器、EMIO扩展、软硬件接口等多个知识点。做过一遍之后再看更复杂的AXI GPIO中断、DMA中断思路都是相通的。需要提前说明的是不同厂商的开发板在按键和LED的物理连接上会有差异本文以常见的官方Zynq开发板为参考具体引脚约束需要根据实际板卡的原理图调整。下面我会把整个流程拆开从硬件设计到软件中断处理把每一步的意图和容易踩的坑都讲清楚。2. Vivado端设计EMIO配置与GPIO IP的取舍2.1 为什么按键要走EMIO而不是直接接MIOZynq的PS端有54个MIO引脚这些引脚直接连在PS上可以配置成GPIO、UART、SPI等功能。但MIO的数量是有限的而且很多已经被开发板上的外设占用了。PL侧的引脚资源就丰富得多而且可以灵活分配。EMIO的作用就是把PL侧的信号借道引入PS的GPIO控制器让PS能像操作普通GPIO一样操作PL侧的信号。具体到这个实验按键是接在PL侧的引脚上的所以必须通过EMIO把按键信号引到PS。配置的时候在Zynq Processing System这个IP里找到GPIO MIO配置页面勾选EMIO GPIO然后设置宽度。这里有个细节EMIO GPIO的宽度决定了你能扩展多少个PL侧信号到PS。如果只做一个按键宽度设1就够了如果后面还想扩展更多PL信号可以一次多留几个。我一般习惯多留几个比如设4或8方便后续扩展反正不占额外资源。配置完EMIO宽度后Vivado会自动在PS的GPIO控制器上分配对应的EMIO编号。这个编号很关键后面写软件的时候要用到。EMIO GPIO的编号是从MIO编号之后开始排的比如MIO用了54个那EMIO就从54开始。不过实际编号要看Vivado的分配结果在导出硬件后的xparameters.h里能找到。2.2 Block Design里GPIO IP的两种接法在Block Design里PL侧的按键信号要接到PS的EMIO上有两种常见做法。第一种是直接用GPIO IP。在Block Design里加一个AXI GPIO IP把按键信号接到GPIO IP的输入通道然后GPIO IP的中断输出接到Zynq PS的中断输入。这种做法的好处是AXI GPIO IP自带中断逻辑配置起来比较直观而且可以通过AXI总线读取按键状态。缺点是占用了AXI总线资源而且多了一层IP稍微复杂一点。第二种是直接把PL引脚信号通过约束文件接到EMIO上不经过AXI GPIO IP。这种做法更轻量按键信号直接进入PS的GPIO控制器PS通过EMIO编号直接读取。中断也是PS GPIO控制器自己产生的不需要额外的IP。这个实验我推荐用第二种因为它更纯粹地展示了EMIO的用法而且资源占用最少。不过要注意如果直接用EMIO接按键PL侧的引脚需要做输入约束并且要确保信号电平稳定。按键一般需要做消抖处理可以在PL侧加一个简单的消抖逻辑或者在PS侧软件里做消抖。硬件消抖更可靠但需要写一点Verilog软件消抖简单但会占用中断处理时间。我一般建议在PL侧做一个简单的计数器消抖这样中断触发更干净。2.3 中断信号的连接与约束文件编写如果用AXI GPIO IP的方案中断信号从GPIO IP的ip2intc_irpt引脚出来接到Zynq PS的pl_ps_irq端口。这个端口在Zynq IP的配置里需要使能一般用IRQ_F2PPL到PS的中断。使能之后Vivado会自动分配一个中断号这个中断号在软件里要用到。如果用纯EMIO方案中断是PS GPIO控制器自己产生的不需要额外的中断连接。但需要在Zynq IP的GPIO配置里使能EMIO GPIO的中断功能。具体来说在GPIO MIO配置页面勾选EMIO GPIO后还要确保中断相关的选项是打开的。有些版本的Vivado里EMIO GPIO的中断是默认使能的但最好确认一下。约束文件这块按键引脚要约束成输入并且要指定电平标准。LED引脚约束成输出。如果开发板上有现成的按键和LED直接参考原理图上的引脚编号就行。这里有个容易忽略的点按键引脚最好加上上拉或下拉约束确保空闲时电平稳定否则中断会误触发。很多开发板的按键电路已经带了外部上拉或下拉但如果没有就要在约束文件里加上PULLUP或PULLDOWN。# 按键引脚约束示例 set_property PACKAGE_PIN [引脚编号] [get_ports btn_in] set_property IOSTANDARD LVCMOS33 [get_ports btn_in] set_property PULLUP true [get_ports btn_in] # LED引脚约束示例 set_property PACKAGE_PIN [引脚编号] [get_ports led_out] set_property IOSTANDARD LVCMOS33 [get_ports led_out]约束文件写完后跑一遍综合和实现确认没有时序违例。EMIO路径的时序一般比较宽松因为信号速率不高但养成检查时序的习惯是好的。3. 软件端中断服务程序的完整实现逻辑3.1 中断初始化从GIC到GPIO控制器的配置顺序软件端的初始化顺序很重要顺序错了中断就出不来。正确的顺序是先初始化GIC再配置GPIO控制器最后使能中断。第一步是初始化GIC。在裸机环境下用XScuGic_LookupConfig找到GIC的配置然后XScuGic_CfgInitialize初始化。这一步做完GIC就准备好了但还没有连接任何中断源。第二步是配置GPIO控制器。用XGpioPs_LookupConfig和XGpioPs_CfgInitialize初始化GPIO控制器。然后设置按键引脚的方向为输入LED引脚的方向为输出。这里要注意EMIO引脚的编号和MIO引脚是连续的用XGpioPs_GetBankPin可以获取引脚所在的bank和pin编号。第三步是连接中断。用XScuGic_Connect把GPIO的中断处理函数注册到GIC然后XScuGic_Enable使能这个中断。GPIO控制器的中断号在xparameters.h里有定义一般是XPAR_XGPIOPS_0_INTR。第四步是配置GPIO中断。用XGpioPs_SetIntrType设置中断类型按键一般用下降沿触发或上升沿触发取决于按键电路。然后XGpioPs_IntrEnable使能具体引脚的中断。最后别忘了XScuGic_Enable使能GIC本身以及Xil_ExceptionEnable使能处理器中断。// 中断初始化核心代码结构 XScuGic_Config *IntcConfig; IntcConfig XScuGic_LookupConfig(INTC_DEVICE_ID); XScuGic_CfgInitialize(Intc, IntcConfig, IntcConfig-CpuBaseAddress); XGpioPs_Config *GpioConfig; GpioConfig XGpioPs_LookupConfig(GPIO_DEVICE_ID); XGpioPs_CfgInitialize(Gpio, GpioConfig, GpioConfig-BaseAddr); XScuGic_Connect(Intc, GPIO_INTR_ID, (Xil_ExceptionHandler)GpioHandler, Gpio); XScuGic_Enable(Intc, GPIO_INTR_ID); XGpioPs_SetDirectionPin(Gpio, BTN_PIN, 0); XGpioPs_SetDirectionPin(Gpio, LED_PIN, 1); XGpioPs_SetIntrTypePin(Gpio, BTN_PIN, XGPIOPS_IRQ_TYPE_EDGE_FALLING); XGpioPs_IntrEnablePin(Gpio, BTN_PIN); Xil_ExceptionInit(); Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_INT, (Xil_ExceptionHandler)XScuGic_InterruptHandler, Intc); Xil_ExceptionEnable();3.2 中断服务函数里到底该做什么中断服务函数ISR是中断模式的核心。很多人第一次写ISR的时候会把所有逻辑都塞进去比如读按键状态、消抖、翻转LED、打印信息。这样写功能上能跑但有几个问题ISR执行时间太长会影响系统响应打印信息在ISR里可能引发阻塞消抖如果放在ISR里会占用中断上下文。正确的做法是ISR尽量短。ISR里只做最必要的事确认中断源、清除中断标志、设置一个标志变量或者发送一个信号量。真正的业务逻辑放到主循环里处理。这个实验里ISR可以只做一件事翻转LED然后清除中断标志。翻转LED是一个寄存器写操作很快放在ISR里没问题。清除中断标志这一步非常关键。Zynq的GPIO中断是电平敏感和边沿敏感可配的如果是边沿触发中断标志需要手动清除否则会反复触发。用XGpioPs_IntrClearPin清除对应引脚的中断标志。如果忘了这一步你会发现按一次按键LED翻转好几次或者中断一直进不去。void GpioHandler(void *CallbackRef) { XGpioPs *GpioPtr (XGpioPs *)CallbackRef; // 确认中断源并清除标志 XGpioPs_IntrClearPin(GpioPtr, BTN_PIN); // 翻转LED u32 led_val XGpioPs_ReadPin(GpioPtr, LED_PIN); XGpioPs_WritePin(GpioPtr, LED_PIN, !led_val); }如果要做消抖可以在ISR里记录时间戳然后在主循环里判断时间间隔。或者更简单的方式在PL侧做硬件消抖ISR里就不用管了。3.3 中断号与引脚编号的对应关系怎么查这是新手最容易卡住的地方。EMIO引脚的编号不是随便定的它和Vivado里的配置直接相关。在Vivado里配置完EMIO后导出硬件时会生成xparameters.h文件里面有一堆宏定义。GPIO控制器的设备ID是XPAR_XGPIOPS_0_DEVICE_ID中断号是XPAR_XGPIOPS_0_INTR。EMIO引脚的编号需要自己算。Zynq的GPIO控制器分四个bankbank0是MIO 0-31bank1是MIO 32-53bank2是EMIO 54-85bank3是EMIO 86-117。所以EMIO的第一个引脚编号是54。如果你在Vivado里配置EMIO宽度为1那这个引脚就是54。如果宽度为4那就是54、55、56、57。不过要注意Vivado在分配EMIO编号时可能会根据你的连接顺序调整。最可靠的方法是看xparameters.h里的XPAR_XGPIOPS_0_GPIO_WIDTH和相关的定义或者在Vivado的Address Editor里查看。我一般会在代码里用宏定义把引脚编号写清楚方便修改。#define BTN_PIN 54 // EMIO第一个引脚 #define LED_PIN 55 // 如果LED也走EMIO如果LED是接在MIO上的那LED_PIN就是对应的MIO编号比如MIO 7就是7。这个编号一定要和硬件连接对上否则会出现按键没反应或者LED不亮的情况。4. 实测中那些让人抓狂的坑与排查路径4.1 中断进不去从GIC到GPIO的逐层排查中断进不去是最常见的问题可能的原因有好几层。我的排查顺序是这样的先确认GIC是否使能。在Xil_ExceptionEnable之后读一下处理器的中断使能寄存器确认IRQ是打开的。如果这一步没做所有中断都进不来。再确认GIC里对应的中断是否使能。用XScuGic_Enable使能了GPIO中断后可以读GIC的使能寄存器确认。有些例程里会漏掉这一步只Connect不Enable中断自然进不来。然后确认GPIO控制器的中断是否使能。XGpioPs_IntrEnablePin使能了具体引脚的中断但还要确认GPIO控制器的全局中断使能位。有些版本的驱动里XGpioPs_IntrEnablePin会自动处理全局使能但最好确认一下。最后确认中断类型配置是否正确。如果按键是按下接地那应该配下降沿触发如果是按下接高电平那应该配上升沿触发。配反了中断也会进不来。可以用XGpioPs_GetIntrTypePin读一下当前配置。还有一个隐蔽的坑EMIO GPIO的中断在PS端是共享的多个EMIO引脚可能共用一个中断号。在ISR里需要读中断状态寄存器判断是哪个引脚触发的中断。如果只使能了一个引脚这个问题不明显但多个引脚的时候就要注意。4.2 按键抖动导致LED乱闪硬件消抖与软件消抖的取舍机械按键在按下和释放的瞬间会产生抖动电平会在短时间内反复跳变。如果中断配置成边沿触发抖动会导致多次中断LED就会乱闪。这个问题在实测中非常明显按一次按键LED可能翻转好几次。解决办法有两种。硬件消抖是在PL侧加一个计数器检测到按键电平稳定一段时间后才输出有效信号。比如用50MHz时钟计数到500000就是10ms足够消除大部分抖动。硬件消抖的优点是彻底ISR里不用管缺点是要写Verilog稍微麻烦一点。软件消抖是在ISR里记录时间戳如果两次中断间隔小于某个阈值比如20ms就忽略后一次。或者更简单的方式在ISR里只设置标志主循环里检测标志并延时确认。软件消抖的优点是简单不用改硬件缺点是占用中断处理时间而且如果抖动特别严重还是可能漏掉。我一般推荐硬件消抖因为一劳永逸。如果不想写Verilog可以在Vivado里用GPIO IP自带的消抖功能有些IP有这个选项或者用简单的RC电路。实测下来10ms到20ms的消抖时间对大多数按键都够用。4.3 LED不亮或常亮方向配置与电平逻辑的检查LED不亮或者常亮一般是方向配置或者电平逻辑的问题。先确认LED引脚的方向配置成了输出用XGpioPs_SetDirectionPin设置。如果方向配成了输入写操作是无效的。再确认电平逻辑。有些开发板的LED是低电平点亮有些是高电平点亮。如果写1不亮试试写0。这个要看原理图LED的阳极接电源还是阴极接电源决定了点亮电平。还有一个可能LED引脚被其他功能占用了。比如MIO引脚如果被配置成了UART或SPI就不能当GPIO用。在Vivado里配置Zynq IP的时候要确认LED对应的MIO没有被其他外设占用。如果占用了要么换引脚要么把外设关掉。如果LED走的是EMIO还要确认EMIO的宽度够用并且引脚编号正确。EMIO引脚在PS端看来就是普通的GPIO操作方式和MIO一样但编号从54开始。4.4 中断嵌套与优先级什么时候需要关注这个实验里只有一个中断源不涉及嵌套和优先级。但如果后面要扩展多个中断源比如按键中断加上定时器中断就要考虑优先级了。GIC支持中断优先级配置高优先级的中断可以打断低优先级的中断。在XScuGic_Connect之后可以用XScuGic_SetPriorityTriggerType设置优先级和触发类型。优先级数值越小优先级越高。默认情况下所有中断的优先级是一样的不会嵌套。如果需要嵌套还要在GIC配置里使能嵌套功能。对于这个实验我的建议是先不折腾优先级把基本功能跑通再说。等需要多个中断源的时候再回来配置优先级。过早引入复杂性容易把自己绕进去。5. 从能跑到跑好几个提升稳定性的实操细节5.1 中断标志清除的时机与顺序中断标志清除的时机很讲究。如果清除得太早可能在清除之后、处理逻辑之前又来了一个新中断导致丢失如果清除得太晚可能在处理逻辑执行期间中断一直挂着处理完后再次进入ISR。我的做法是在ISR的开头就清除中断标志然后再执行处理逻辑。这样即使处理逻辑执行期间来了新中断也会被记录为pending状态处理完后再次进入ISR。对于按键这种低速事件这个顺序完全够用。但要注意XGpioPs_IntrClearPin清除的是GPIO控制器里的中断标志GIC里的pending状态还需要额外处理。在ISR返回时GIC会自动处理pending状态。如果发现中断反复进入可能是GIC的pending没有正确清除需要检查GIC的配置。5.2 用volatile修饰共享变量如果ISR和主循环之间通过变量传递状态这个变量一定要用volatile修饰。因为编译器优化的时候可能会把变量缓存在寄存器里导致主循环读不到ISR的更新。加上volatile后编译器每次都会从内存读取确保看到最新值。volatile u32 btn_pressed 0; void GpioHandler(void *CallbackRef) { XGpioPs_IntrClearPin(Gpio, BTN_PIN); btn_pressed 1; } int main() { while (1) { if (btn_pressed) { btn_pressed 0; // 处理按键 } } }这个坑很隐蔽因为不加volatile有时候也能跑但优化等级一变就可能出问题。养成习惯ISR和主循环共享的变量都加volatile。5.3 调试中断的实用手段调试中断的时候点灯和串口打印是最常用的手段。但要注意串口打印在ISR里可能会阻塞因为串口发送是慢速操作。如果非要在ISR里打印用非阻塞的方式或者只打印一个字符。更好的方式是用GPIO翻转来指示。在ISR入口翻转一个调试引脚用示波器或者逻辑分析仪看波形能精确测量ISR的执行时间和触发频率。如果没有仪器可以用另一个LED来指示虽然精度不高但能看出中断有没有触发。还有一个手段是读GIC的中断状态寄存器确认中断是否被GIC正确接收。如果GIC收到了但CPU没响应可能是CPU的中断使能没打开如果GIC没收到可能是GPIO控制器的中断没使能。逐层排查总能找到问题。5.4 从裸机到Linux的中断处理差异这个实验是在裸机环境下做的中断处理相对直接。如果后面要移植到Linux中断处理的方式就完全不一样了。Linux下要用request_irq注册中断处理函数中断上下文有严格的限制不能睡眠、不能调用可能阻塞的函数。而且Linux的GPIO子系统有自己的框架EMIO GPIO需要通过gpiochip来管理。裸机下学到的中断概念在Linux下依然适用但具体的API和限制条件不同。我的建议是先把裸机的中断跑通理解中断的基本流程再去碰Linux的中断。裸机下调试中断更直接没有操作系统的干扰更容易定位问题。6. 这个实验之后还能往哪些方向延伸把PL按键通过EMIO扩展到PS控制LED这个实验跑通之后其实已经打通了Zynq软硬件协同的基本链路。接下来可以往几个方向延伸。第一个方向是增加中断源。比如再加一个按键或者加一个定时器中断练习多个中断源的优先级配置和嵌套处理。这个过程中会接触到GIC的更多配置选项对中断控制器的理解会更深入。第二个方向是把AXI GPIO IP用起来。这个实验用的是纯EMIO方案如果换成AXI GPIO IP中断路径会经过AXI总线涉及到AXI中断和PL中断的转换。这个过程中会接触到AXI总线的基本概念对Zynq的互联结构理解会更清楚。第三个方向是结合DMA。按键触发后通过DMA把一块数据从PL搬到PS的DDR里或者从PS搬到PL。这个过程中会接触到Zynq的DMA控制器和AXI Stream接口是更高级的软硬件协同应用。第四个方向是移植到Linux。在Linux下用GPIO子系统和中断框架重新实现这个功能体会裸机和操作系统下中断处理的差异。这个过程中会接触到设备树、platform驱动、gpiochip等概念是嵌入式Linux开发的入门练习。不管往哪个方向走这个实验打下的基础都是有用的。中断的基本流程、GPIO的配置方法、软硬件接口的调试手段这些在更复杂的项目里都会反复用到。我在实际项目中遇到的中断问题排查思路和这个实验里用的方法基本一致只是规模更大、层次更多。把这个实验吃透后面遇到问题心里就有底了。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。