4. GPIO控制实战:MIO与EMIO配置、LED灯控制、按键中断实验

GPIO 是 Zynq 里最基础也最常用的外设。说白了,就是让芯片跟外界打个招呼——点亮一个灯,读一个按键状态。但你别小看它,MIO 和 EMIO 的区别、中断的配置,这些坑我当年都踩过。今天咱们就把它彻底搞明白。

4.1 MIO 与 EMIO:两条路,两种玩法

Zynq 的 GPIO 控制器挂在 PS 端,总共有 118 个引脚可用。但它们的“出身”不一样:

特性 MIO EMIO
物理位置 PS 端专用引脚(Bank0/1/2) 通过 PL 扩展的引脚
数量 54 个(Bank0:32, Bank1:22) 64 个(输入+输出)
访问速度 直接,无额外延迟 需经过 AXI-GP 桥,有 1-2 周期延迟
电压域 固定 1.8V/2.5V/3.3V(按 Bank) 由 PL 侧的 I/O Bank 决定
典型用途 UART、I2C、SPI、SDIO 等固定功能 自定义 LED、按键、扩展外设

我个人习惯:能用 MIO 的尽量用 MIO,省事、快、不占 PL 资源。但很多时候,MIO 引脚被其他外设占用了,或者你需要更多 GPIO,那就得走 EMIO。

小提示: EMIO 虽然叫“扩展”,但它本质上还是 PS 的 GPIO 控制器在管。你配置的寄存器地址是一样的,只是物理引脚映射到了 PL 侧。所以驱动代码几乎不用改,改的是 Vivado 里的连接。

4.2 点亮 LED:从寄存器到代码

先来个最简单的——点亮一块板子上的 LED。Zynq 的 GPIO 寄存器映射在 0xE000_A000 起始的地址空间。核心寄存器就这几个:

  • DATA_RO:读引脚电平(只读)
  • DATA:写输出电平(可读可写)
  • DIRM:方向控制(0=输入,1=输出)
  • OEN:输出使能(1=使能输出)
  • INTEN / INT_POL / INT_TYPE:中断相关

嗯,这里要注意:DIRM 和 OEN 是两个寄存器。DIRM 决定引脚是输入还是输出,OEN 决定输出是否真正生效。我见过有人只配了 DIRM 没配 OEN,结果灯死活不亮——查了半天才发现。

下面是一段裸机代码,控制 MIO 上的 LED(假设 LED 接在 MIO7):

// 定义 GPIO 寄存器基地址
#define GPIO_BASE_ADDR  0xE000A000

// 寄存器偏移
#define GPIO_DATA       0x0000
#define GPIO_DIRM       0x0204
#define GPIO_OEN        0x0208

// 配置 MIO7 为输出
*(volatile uint32_t *)(GPIO_BASE_ADDR + GPIO_DIRM) |= (1 << 7);
*(volatile uint32_t *)(GPIO_BASE_ADDR + GPIO_OEN)  |= (1 << 7);

// 点亮 LED(输出高电平)
*(volatile uint32_t *)(GPIO_BASE_ADDR + GPIO_DATA) |= (1 << 7);

// 熄灭 LED(输出低电平)
*(volatile uint32_t *)(GPIO_BASE_ADDR + GPIO_DATA) &= ~(1 << 7);
注意: 如果你用的是 EMIO,代码完全一样!只是引脚号从 54 开始算。比如 EMIO 的第 0 脚对应 GPIO 编号 54。别搞混了。

4.3 按键中断:让 CPU 主动响应

轮询按键太浪费 CPU 了。真正项目中,我们都会用中断。Zynq 的 GPIO 中断支持上升沿、下降沿、高电平、低电平四种触发方式。

配置步骤其实就四步:

  1. 设置引脚为输入(DIRM 对应位写 0)
  2. 配置中断类型(INT_TYPE:0=电平,1=边沿)
  3. 配置中断极性(INT_POL:0=低/下降,1=高/上升)
  4. 使能中断(INT_EN 对应位写 1)

然后别忘了在 GIC(通用中断控制器)里使能 GPIO 中断号。Zynq 的 GPIO 中断号是 52(共享外设中断)。

我曾经在一个项目里,按键中断老是触发两次。后来发现是按键没做硬件消抖,边沿中断在抖动时反复触发。解决办法:要么加硬件 RC 滤波,要么在中断服务程序里加软件消抖——我建议两者都做。

下面是一个按键中断的初始化代码片段:

// 假设按键接在 MIO8,下降沿触发
#define GPIO_INTEN      0x020C
#define GPIO_INT_TYPE   0x0210
#define GPIO_INT_POL    0x0214
#define GPIO_INT_STAT   0x0218

// 1. 配置为输入
*(volatile uint32_t *)(GPIO_BASE_ADDR + GPIO_DIRM) &= ~(1 << 8);
*(volatile uint32_t *)(GPIO_BASE_ADDR + GPIO_OEN)  &= ~(1 << 8);

// 2. 边沿触发
*(volatile uint32_t *)(GPIO_BASE_ADDR + GPIO_INT_TYPE) |= (1 << 8);

// 3. 下降沿
*(volatile uint32_t *)(GPIO_BASE_ADDR + GPIO_INT_POL) &= ~(1 << 8);

// 4. 使能中断
*(volatile uint32_t *)(GPIO_BASE_ADDR + GPIO_INTEN) |= (1 << 8);

// 5. 在 GIC 中使能(伪代码)
GIC_EnableInterrupt(52);
关键点: 中断服务程序里,一定要先读 INT_STAT 寄存器确认中断源,然后写 1 清除中断标志。否则会一直进中断,系统直接卡死。

4.4 知识体系:GPIO 控制的核心逻辑

下面这张图帮你理清 MIO/EMIO 的配置流程和中断路径:

GPIO 控制核心逻辑 PS GPIO 控制器 MIO (54 个) EMIO (64 个) 寄存器配置:DIRM → OEN → DATA(输出) / INT_EN(中断) 中断路径:GPIO → GIC → CPU 轮询路径:CPU 直接读 DATA_RO 说明:MIO 直连 PS 引脚,EMIO 需经过 PL 扩展,但寄存器操作完全一致 中断号固定为 52,边沿触发需注意硬件消抖

4.5 避坑指南与实战建议

做 GPIO 控制,看起来简单,但有几个地方特别容易翻车:

  • MIO 电压不匹配: Bank0 是 1.8V,Bank1 是 2.5V 或 3.3V(取决于板子)。我曾经直接把 3.3V 的按键接到 Bank0,结果芯片发热——赶紧断电。一定要查原理图!
  • EMIO 未连接: 在 Vivado 里勾选了 EMIO,但忘了在顶层模块里实例化引脚。结果代码跑起来,灯没反应。检查方法:看 Vivado 的 I/O Ports 窗口,确认 EMIO 引脚有分配。
  • 中断优先级: 多个外设共用中断时,GPIO 中断优先级别设太高。否则频繁的按键中断会饿死其他任务。
  • 软件消抖: 我一般用 10-20ms 的延时消抖。在中断里启动一个定时器,定时到了再读一次引脚状态。别在中断里直接 delay——那是阻塞式的,影响实时性。
调试技巧: 用 XSDB 或者 SDK 的 Memory Monitor 直接看 GPIO 的 DATA_RO 寄存器。如果按键按下时值没变,说明硬件连接或方向配置有问题。这是最快的排查方法。