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 中断支持上升沿、下降沿、高电平、低电平四种触发方式。
配置步骤其实就四步:
- 设置引脚为输入(DIRM 对应位写 0)
- 配置中断类型(INT_TYPE:0=电平,1=边沿)
- 配置中断极性(INT_POL:0=低/下降,1=高/上升)
- 使能中断(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 的配置流程和中断路径:
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 寄存器。如果按键按下时值没变,说明硬件连接或方向配置有问题。这是最快的排查方法。