☰
Zynq EMIO实战:PS端GPIO扩展控制PL按键与LED
2026/10/13 1:14:04 网站建设 项目流程

1. 项目缘起与整体设计思路

1.1 为什么要在Zynq上折腾EMIO

手里有一块Zynq-7Z035的官方开发板,PL端有几个按键和LED,PS端跑着Linux或者裸机程序。最直接的想法是:按键直接接PL的IO,LED也接PL的IO,然后在PL里写一段纯Verilog逻辑,按键按下就点亮LED,这事就结了。但实际做项目的时候,往往没这么简单——你希望PS端的软件能感知到按键状态,能根据按键做出一系列复杂决策,比如切换工作模式、触发数据采集、保存日志,然后再由软件去控制LED的亮灭。这时候,PL的按键和LED就必须和PS打通。

打通的方式有几种。第一种是PL端把按键状态写进一个寄存器,PS通过AXI总线去轮询读取,LED同理通过AXI寄存器去写。这种方式最灵活,但需要自己写AXI IP,代码量大,调试也麻烦。第二种是用中断,PL检测到按键边沿后产生中断信号,通过IRQ接到PS的GIC,PS在中断服务程序里处理。这种方式实时性好,但中断控制器的配置、中断号的分配、Linux下的中断注册,每一步都有坑。第三种就是EMIO——Extended MIO,把PS端的GPIO控制器通过EMIO接口扩展到PL端,让PS可以直接像操作自己的MIO引脚一样去操作PL端的引脚。这种方式的好处是:不需要写任何AXI IP,不需要自己处理中断控制器,PS端用标准的GPIO驱动就能读写,软件层面几乎零成本。

我选择EMIO方案,核心原因就是软件简洁性。在这个项目里,按键和LED的数量不多,实时性要求也不高,用EMIO把PS的GPIO扩展到PL,PS端直接调用GPIO库函数就能读按键、控LED,省去了大量底层开发工作。对于刚接触Zynq的开发者来说,这也是理解PS-PL交互的一个非常好的切入点。

1.2 EMIO到底是怎么一回事

要理解EMIO,得先搞清楚MIO和EMIO的区别。Zynq的PS端有54个MIO引脚,这些引脚直接属于PS,可以配置成GPIO、UART、SPI、I2C等功能。但MIO的数量是固定的,而且很多已经被开发板上的外设占用了。当你需要更多的GPIO,或者需要把GPIO信号引到PL端去的时候,EMIO就派上用场了。

EMIO本质上是一组从PS的GPIO控制器延伸到PL的接口信号。PS端的GPIO控制器总共支持118个GPIO(MIO 0-53加上EMIO 54-117)。其中EMIO部分(54-117)并不直接对应物理引脚,而是以信号的形式出现在PL端。你在PL的顶层模块里声明这些信号,然后通过管脚约束把它们分配到实际的物理引脚上。这样,PS端操作GPIO 54,实际上就是在操作PL端你指定的那个引脚。

这里有一个关键点:EMIO信号的方向是从PS的角度定义的。当PS端把某个EMIO配置为输出时,PL端收到的就是一个输出信号,你可以把它接到LED上;当PS端把某个EMIO配置为输入时,PL端需要把按键信号驱动到这个EMIO输入上。方向搞反了,信号就传不过去。

1.3 整体方案框架

整个项目的结构可以这样描述:PL端顶层模块声明两组EMIO信号,一组是输入(来自按键),一组是输出(去往LED)。PS端通过配置GPIO控制器,把对应的EMIO编号设置为输入或输出模式。软件运行后,在一个循环里不断读取按键对应的GPIO值,根据按键状态去设置LED对应的GPIO值。

具体来说,我用了PL端的两个按键和两个LED。按键按下时接地,松开时由上拉电阻拉高,所以是低电平有效。LED是阳极接电源、阴极接FPGA引脚,所以FPGA输出低电平时LED亮。这些硬件细节在写约束和软件逻辑时都要考虑到,否则会出现“按键按下去没反应”或者“LED亮灭逻辑反了”的情况。

在Vivado里,需要配置Zynq Processing System IP,使能EMIO GPIO,并设置好EMIO的宽度。然后在PL顶层模块里把EMIO信号引出,加上IBUF和OBUF,再通过XDC约束分配到物理引脚。导出硬件后,在SDK或Vitis里写PS端程序,调用XGpioPs库函数完成初始化和读写。

这个方案的优势在于:PS端软件极其简单,不需要处理AXI总线协议,不需要写中断服务程序,不需要了解PL内部逻辑。对于需要快速验证PS-PL交互、或者PS端需要大量GPIO但MIO不够用的场景,EMIO是一个非常实用的选择。

2. 硬件设计与Vivado工程配置细节

2.1 开发板硬件资源确认

在动手之前,必须先确认开发板上按键和LED的实际连接。不同厂商的Zynq-7Z035开发板,PL端外设的引脚分配可能完全不同。我手里这块板子,PL端有两个用户按键(KEY1、KEY2)和两个用户LED(LED1、LED2)。按键一端接地,另一端通过10k电阻上拉到3.3V,所以按键未按下时引脚为高电平,按下时为低电平。LED的阳极通过限流电阻接到3.3V,阴极接FPGA引脚,所以引脚输出低电平时LED导通发光。

这些信息从哪里来?最可靠的是开发板的原理图。如果没有原理图,可以用万用表测一下按键引脚在按下和松开时的电平变化,LED则可以通过给引脚一个低电平看是否点亮来验证。我强烈建议在开始写代码之前,先花十分钟把硬件确认清楚,否则后面调试时会浪费大量时间在“为什么按键没反应”这种问题上。

2.2 Vivado中Zynq PS的EMIO配置

打开Vivado,创建一个新工程,选择对应的Zynq-7Z035器件。添加Zynq Processing System IP,双击进入配置界面。在“Peripheral I/O Pins”页面,找到GPIO MIO的配置区域。这里需要注意的是,MIO和EMIO是分开配置的。MIO部分根据开发板实际使用情况勾选,EMIO部分需要使能GPIO EMIO。

在“MIO Configuration”里,展开GPIO,你会看到EMIO GPIO的选项。勾选它,然后设置EMIO GPIO的宽度。这个宽度决定了你最多可以使用多少个EMIO引脚。我用了2个输入和2个输出,所以理论上宽度设为4就够了。但实际配置时,我建议把宽度设得稍微宽裕一些,比如8或16,方便后续扩展。宽度设置会影响GPIO编号的分配:EMIO从GPIO 54开始,宽度为N时,可用的EMIO编号是54到53+N。

配置完成后,Zynq IP的引脚列表里会出现GPIO_0接口,这就是EMIO信号。在Block Design里,把GPIO_0接口引出(Make External),然后创建HDL Wrapper。Vivado会自动生成顶层文件,里面包含了EMIO信号的端口声明。

2.3 PL顶层模块的EMIO信号处理

自动生成的顶层文件里,EMIO信号通常是以总线形式出现的,比如gpio_0_tri_io。这个三态总线包含了输入、输出和输出使能信号。对于输入方向的EMIO,PS端会输出一个使能信号,PL端需要根据这个使能来决定是否驱动输入数据。对于输出方向的EMIO,PS端输出数据,PL端直接使用即可。

但实际写代码时,直接操作这个三态总线比较麻烦。我通常会在顶层模块里把它拆分成独立的输入和输出信号。具体做法是:声明两个输入信号(比如key_in_0、key_in_1)和两个输出信号(比如led_out_0、led_out_1),然后在顶层模块里根据EMIO的方向配置,把三态总线里的对应位连接到这些信号上。

这里有一个容易出错的地方:EMIO的方向是在PS端软件里配置的,PL端并不知道哪个EMIO是输入、哪个是输出。所以PL端的逻辑必须和PS端的配置严格对应。我的做法是在PL端固定好:EMIO 54和55作为输入(接按键),EMIO 56和57作为输出(接LED)。然后在PS端软件里,把54和55配置为输入,56和57配置为输出。两边约定好,就不会乱。

2.4 管脚约束与电平标准

EMIO信号最终要分配到PL的物理引脚上,这需要在XDC约束文件里完成。对于输入信号(按键),需要约束到按键所在的引脚,并设置正确的电平标准(通常是LVCMOS33)。对于输出信号(LED),约束到LED引脚,同样设置电平标准。

约束文件里还要注意一点:按键输入需要加上拉约束。虽然开发板上已经有外部上拉电阻,但在XDC里显式声明上拉可以避免一些意外情况。语法是set_property PULLUP true [get_ports key_in_0]。对于LED输出,可以设置驱动能力,默认的12mA通常够用,如果LED不够亮可以适当调大。

另外,如果按键信号有抖动,可以在PL端加一个简单的消抖逻辑,也可以在PS端软件里做软件消抖。我倾向于在PL端加硬件消抖,因为这样PS端读到的就是干净的电平信号,软件逻辑更简单。消抖逻辑很简单:用一个计数器,当输入信号稳定超过一定时间(比如10ms)后才更新输出。这个计数器时钟可以用PL的时钟,比如100MHz,10ms对应1,000,000个周期,用一个20位的计数器就够了。

2.5 硬件导出与软件工程创建

Block Design和约束都完成后,生成比特流,导出硬件(Export Hardware),包含比特流文件。然后打开Vitis(或SDK),创建新的应用工程,选择导出的硬件平台。在Vitis里,可以看到PS端的GPIO控制器基地址和EMIO的编号范围。

创建工程后,需要添加XGpioPs库的初始化代码。XGpioPs是Xilinx提供的GPIO驱动库,支持MIO和EMIO的统一操作。初始化流程是:查找GPIO控制器的配置信息,初始化GPIO控制器,然后分别设置每个引脚的方向和输出使能。对于输入引脚,调用XGpioPs_SetDirectionPin设置为输入;对于输出引脚,设置为输出并调用XGpioPs_SetOutputEnablePin使能输出。

这里有一个细节:EMIO引脚的编号是从54开始的。在代码里操作EMIO时,引脚号要写54、55、56、57,而不是0、1、2、3。这个编号是Zynq架构固定的,MIO占0-53,EMIO从54开始。如果搞错了编号,操作的就是MIO引脚,而MIO引脚可能被其他功能占用,导致不可预期的行为。

3. PS端软件实现与核心代码解析

3.1 GPIO控制器的初始化流程

PS端软件的第一步是初始化GPIO控制器。XGpioPs库的初始化函数是XGpioPs_LookupConfig和XGpioPs_CfgInitialize。前者根据设备ID查找配置信息,后者用配置信息初始化控制器实例。设备ID通常是0,因为Zynq只有一个GPIO控制器。

初始化完成后,需要设置每个引脚的方向。对于按键输入(EMIO 54、55),调用XGpioPs_SetDirectionPin(&gpio, 54, 0),第二个参数是引脚号,第三个参数0表示输入。对于LED输出(EMIO 56、57),调用XGpioPs_SetDirectionPin(&gpio, 56, 1)设置为输出,然后调用XGpioPs_SetOutputEnablePin(&gpio, 56, 1)使能输出。

这里有一个常见的坑:只设置方向而不使能输出,LED不会亮。因为Zynq的GPIO输出有一个独立的输出使能位,必须显式使能。我刚开始做的时候,只设置了方向,结果LED死活不亮,查了半天才发现是输出使能没开。这个细节在官方文档里虽然有写,但很容易被忽略。

初始化完成后,可以调用XGpioPs_ReadPin读取按键状态,调用XGpioPs_WritePin设置LED状态。读到的值是0或1,对应引脚的低电平或高电平。由于按键是低电平有效,所以读到0表示按下,读到1表示松开。LED是低电平点亮,所以写0点亮,写1熄灭。

3.2 按键读取与LED控制的逻辑实现

主循环的逻辑很直接:不断读取两个按键的状态,根据按键状态决定LED的亮灭。我实现的功能是:KEY1按下时LED1亮,松开时LED1灭;KEY2按下时LED2亮,松开时LED2灭。这是一个最简单的映射关系,用来验证EMIO通路是否正常工作。

代码结构如下:首先定义引脚号宏,比如#define KEY1_PIN 54、#define LED1_PIN 56,这样代码可读性更好。然后在主循环里,用XGpioPs_ReadPin读取KEY1_PIN,如果返回0(按下),就XGpioPs_WritePin写LED1_PIN为0(点亮);否则写1(熄灭)。KEY2和LED2同理。

这个逻辑虽然简单,但包含了EMIO应用的核心要素:PS端通过GPIO编号访问EMIO引脚,读操作获取PL端输入信号,写操作驱动PL端输出信号。理解了这一点,就可以扩展到更复杂的应用,比如按键长按检测、多按键组合、LED呼吸灯效果等。

3.3 软件消抖与状态机设计

如果PL端没有加硬件消抖,PS端就需要做软件消抖。软件消抖的常用方法是:检测到按键状态变化后,延时一段时间(比如20ms)再次读取,如果状态仍然相同,才确认按键有效。这个延时可以用usleep或忙等待循环实现。

但更优雅的方式是用状态机。我设计了一个简单的按键状态机,每个按键有三个状态:空闲、消抖中、按下确认。在空闲状态,如果读到低电平,进入消抖中状态并记录时间;在消抖中状态,如果持续低电平超过消抖时间,进入按下确认状态,并触发按键事件;如果中途读到高电平,回到空闲状态。在按下确认状态,如果读到高电平,回到空闲状态并触发释放事件。

这个状态机的好处是:不需要阻塞延时,可以在主循环里非阻塞地运行。对于需要同时处理多个按键和LED效果的应用,非阻塞状态机是更好的选择。实现时,可以用一个结构体数组来管理每个按键的状态,每个结构体包含当前状态、计数器、上次读数等字段。

3.4 从裸机到Linux的移植考虑

如果PS端跑的是Linux,EMIO的使用方式会有所不同。Linux下不能直接调用XGpioPs库,而是通过sysfs接口或字符设备接口来操作GPIO。sysfs接口的路径是/sys/class/gpio/,需要先导出GPIO编号,然后设置方向,再读写值。

但这里有一个问题:Linux下的GPIO编号和Zynq的GPIO编号不是一一对应的。Linux的GPIO子系统有自己的编号规则,通常等于Zynq GPIO基础编号 + 引脚号。Zynq GPIO的基础编号可以在设备树里找到,通常是906或者别的值,取决于内核版本和配置。所以操作EMIO 54时,Linux下的编号可能是906+54=960。

设备树里还需要确保GPIO控制器节点使能了EMIO。默认情况下,Zynq的GPIO控制器节点会包含EMIO的范围,但有些板级设备树可能会覆盖这个配置。如果发现sysfs里找不到对应的GPIO,需要检查设备树里的gpio-controller节点和ngpios属性。

从裸机移植到Linux时,最大的变化是不能直接读写寄存器,必须通过标准接口。这增加了开销,但换来了更好的可移植性和系统稳定性。对于实时性要求不高的应用,Linux下的GPIO操作完全够用。

4. 调试过程中遇到的典型问题与排查方法

4.1 按键无反应:从硬件到软件的逐层排查

按键按下后LED没反应,这是最常见的问题。排查要按顺序来,从硬件到软件逐层确认。第一步,用万用表测按键引脚在按下和松开时的电平。如果按下时不是低电平,说明硬件连接有问题,可能是按键坏了或者上拉电阻没焊。第二步,确认XDC约束里的引脚号是否正确。引脚号写错,信号就分配到错误的物理引脚上,按键自然没反应。第三步,检查PS端软件里的GPIO编号是否写对。EMIO从54开始,如果写成0,操作的就是MIO引脚,和按键无关。

我遇到过一次,按键按下后LED偶尔闪一下但不稳定。查了半天发现是XDC里没有设置上拉,而开发板上的上拉电阻是10k,加上杜邦线的分布电容,信号上升沿变得很缓,导致PS端读到的电平不稳定。在XDC里加上set_property PULLUP true后问题解决。这个经验告诉我:不要假设开发板上的外部电路一定完美,显式配置内部上拉可以增加冗余。

4.2 LED常亮或常灭:输出使能与逻辑电平的陷阱

LED一直亮着或者一直灭着,不随按键变化。首先检查输出使能是否打开。前面说过,Zynq的GPIO输出有独立的使能位,只设置方向不使能输出,引脚会保持在高阻态,LED可能因为漏电流而微亮或者完全不亮。调用XGpioPs_SetOutputEnablePin使能输出后,问题通常就解决了。

如果输出使能已经打开,LED还是不对,就要检查逻辑电平。LED是低电平点亮,如果软件里写1去点亮,那就反了。另外,如果PL端在EMIO输出信号上加了反相器或者缓冲器,逻辑也会反。我的建议是:先用一个最简单的测试程序,让LED以1Hz频率闪烁,确认输出通路正常,再叠加按键逻辑。这样可以把问题隔离在最小的范围内。

4.3 EMIO编号与MIO冲突:一个隐蔽的坑

有一次我配置了EMIO宽度为4,然后在软件里操作GPIO 54到57。但运行后发现按键读取的值一直是1,LED也控制不了。查了很久才发现,Vivado里Zynq IP的EMIO配置没有保存,实际生成的硬件里EMIO宽度是0。也就是说,GPIO 54到57根本不存在,软件操作的是未定义的寄存器,返回的值没有意义。

这个问题的教训是:每次修改Vivado配置后,一定要重新生成比特流并导出硬件,然后在Vitis里更新硬件平台。Vitis不会自动检测硬件变化,需要手动刷新。另外,可以在软件初始化后打印GPIO控制器的配置信息,确认EMIO的宽度和基地址是否正确。

4.4 常见问题速查表

现象可能原因排查方法解决方案
按键无反应引脚约束错误检查XDC引脚号对照原理图修正
按键无反应GPIO编号错误确认EMIO从54开始修正软件中的引脚号
按键抖动无消抖处理示波器观察波形加硬件或软件消抖
LED常亮输出未使能检查SetOutputEnablePin调用使能函数
LED逻辑反电平极性搞反确认LED接法调整写值逻辑
EMIO无效Vivado配置未生效检查EMIO宽度重新生成硬件
Linux下找不到GPIO设备树未使能查看sysfs修改设备树

4.5 调试工具与技巧

调试EMIO问题时,ILA(Integrated Logic Analyzer)是非常有用的工具。在Vivado里把EMIO信号加到ILA核上,可以实时观察PL端的信号变化。比如按键按下时,ILA应该能看到输入信号从高变低;PS端写LED时,ILA应该能看到输出信号变化。如果ILA里信号正常但实际LED不亮,问题就在PL到引脚的路径上;如果ILA里信号就不对,问题在PS端软件或EMIO配置上。

另一个技巧是用GPIO回环测试。把PS端的一个EMIO输出直接连到另一个EMIO输入(在PL端用导线短接),然后在软件里写输出、读输入,看是否一致。这个测试可以验证EMIO的输入输出通路是否都正常,排除按键和LED本身的问题。我每次调试新的EMIO配置时都会先做这个回环测试,确认通路无误后再接实际外设。

注意:ILA核会占用PL资源,对于资源紧张的设计要谨慎使用。调试完成后记得移除ILA,否则会影响时序和资源利用率。

5. 方案扩展与进阶应用场景

5.1 多按键多LED的扩展方法

当前项目用了2个按键和2个LED,实际应用中可能需要更多。EMIO的宽度最大可以到64(GPIO 54到117),所以理论上可以扩展64个EMIO引脚。但实际能用的数量受限于PL的可用引脚数和开发板的物理接口。扩展时,只需要在Vivado里增大EMIO宽度,在PL顶层模块里增加对应的信号声明和约束,在软件里增加对应的引脚号定义和初始化代码。

为了代码的可维护性,我建议用数组和循环来管理多个按键和LED。比如定义const u32 key_pins[] = {54, 55, 58, 59}和const u32 led_pins[] = {56, 57, 60, 61},然后用循环初始化。这样增加引脚时只需要修改数组,不需要改初始化逻辑。读取和写入也可以用循环批量处理,代码更简洁。

5.2 结合中断实现按键事件驱动

轮询方式虽然简单,但会占用CPU时间,而且响应有延迟。如果按键需要触发复杂处理,可以用中断方式。Zynq的GPIO控制器支持中断,EMIO输入也可以配置为中断源。配置步骤是:设置GPIO中断类型(上升沿、下降沿或双边沿),使能GPIO中断,然后在GIC里使能对应的中断号,最后在软件里注册中断服务程序。

中断方式的优势是CPU不需要一直轮询,按键事件发生时自动触发处理。但中断服务程序里不能做耗时操作,通常只是设置一个标志位,主循环检测到标志位后再做具体处理。另外,中断方式需要处理按键抖动,否则一次按下可能触发多次中断。可以在中断服务程序里启动一个定时器,定时时间到后再读取按键状态确认。

5.3 EMIO在高速数据采集中的应用

EMIO不仅可以用于按键和LED,还可以用于更高速的信号。比如PL端做一个数据采集模块,采集到的数据通过EMIO传给PS。但EMIO的带宽有限,每个时钟周期只能传一位(对于GPIO而言),所以不适合高速数据传输。高速数据传输应该用AXI DMA或者AXI Stream接口。

EMIO更适合控制信号和状态信号的传递,比如触发信号、同步信号、状态指示等。在这些场景下,EMIO的简单性和低延迟优势明显。我做过一个项目,PL端采集到外部触发信号后,通过EMIO通知PS端启动数据处理,PS端处理完后再通过EMIO通知PL端切换通道。整个交互逻辑用EMIO实现,代码量不到100行,非常简洁。

5.4 从EMIO到AXI GPIO的选型对比

EMIO和AXI GPIO是两种常见的PS-PL GPIO扩展方式。EMIO的优势是零IP开销、软件简单、延迟低,缺点是引脚数量受限于GPIO控制器(最多64个EMIO)、方向配置不够灵活(需要PS端软件配置)。AXI GPIO的优势是引脚数量灵活、方向可在PL端固定、支持中断,缺点是需要额外的IP核、占用AXI总线带宽、软件需要操作AXI寄存器。

选型时,如果GPIO数量少(几十个以内)、方向固定、不需要频繁中断,EMIO是更好的选择。如果需要大量GPIO、方向需要动态切换、或者需要独立的中断控制,AXI GPIO更合适。我个人的经验是:先用EMIO,不够用了再上AXI GPIO。因为EMIO的开发效率太高了,能省下大量调试AXI总线的时间。

5.5 实际项目中的经验总结

做了几个Zynq项目后,我总结了几条关于EMIO的经验。第一,EMIO的编号一定要在项目文档里写清楚,哪个编号对应哪个功能,不然后面维护时容易搞混。第二,PL端的消抖逻辑尽量加上,硬件消抖比软件消抖更可靠,而且不占用CPU时间。第三,软件初始化后加一个自检流程,比如让所有LED闪烁三次,确认GPIO通路正常后再进入主循环。第四,XDC约束文件要加注释,说明每个约束对应的硬件功能,方便后续修改。

还有一个容易被忽略的点:EMIO信号在PL端需要经过IBUF和OBUF。虽然Vivado通常会自动推断,但在某些情况下(比如信号需要驱动多个负载),显式例化IBUF和OBUF可以避免综合工具的错误推断。我遇到过因为缺少OBUF导致LED驱动能力不足的情况,加上OBUF后问题解决。

这个项目虽然简单,但涵盖了Zynq PS-PL交互的核心概念。把EMIO搞明白了,再去学AXI、DMA、中断,会发现很多底层逻辑是相通的。我建议初学者从这个项目入手,亲手做一遍,把每个环节都搞清楚,比看十篇教程都管用。

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

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

立即咨询