3、GPIO裸机驱动:MIO与EMIO的区别、GPIO寄存器详解、LED灯控制实战

好,咱们今天来啃Zynq GPIO这块硬骨头。说实话,GPIO是嵌入式开发里最基础也最常用的外设,但Zynq的GPIO跟普通单片机不太一样——它分MIO和EMIO两套体系。我刚开始接触时也绕了点弯路,今天把经验掰开揉碎了讲给你。

3.1 MIO与EMIO:两条腿走路

Zynq的GPIO控制器在PS端,一共提供了54个GPIO引脚。但这54个引脚不是都能直接连到芯片外面的。它们分成了三组:

类型 引脚范围 连接方式 特点
MIO GPIO[0:53] 直接连到PS I/O 固定功能、无需PL、延迟低
EMIO GPIO[54:63] 通过PL扩展 灵活配置、需要PL逻辑、延迟稍高

MIO(Multiplexed I/O) 是PS自带的54个引脚。它们直接连到芯片的PS侧焊盘,不经过PL(可编程逻辑)。说白了,你用MIO控制LED,完全不需要配置FPGA逻辑,纯软件操作就行。我在一个工业控制项目里就用MIO接了按键和指示灯,响应速度非常快。

EMIO(Extended MIO) 是GPIO控制器通过PL扩展出来的10个引脚。它们从PS端出来,先经过PL内部布线,再连到PL侧的I/O焊盘。为什么要这么麻烦?因为EMIO可以让你把GPIO信号接到PL内部的其他逻辑模块上,或者通过PL的I/O标准(比如LVDS、DDR)输出。嗯,这里要注意:使用EMIO必须在Vivado里把GPIO接口引出来,并且综合实现后才能用。

核心区别一句话: MIO是PS直连,EMIO是PL中转。MIO适合简单控制,EMIO适合需要PL协同的场景。

3.2 GPIO寄存器详解

Zynq的GPIO控制器有4组寄存器,每组控制一个Bank。每个Bank最多32个引脚。咱们重点看几个关键寄存器:

寄存器 偏移地址 功能
DATA_RO 0x000 读引脚电平(只读)
DATA 0x004 写输出电平(可读可写)
DIRM 0x204 方向控制(1=输出,0=输入)
OEN 0x208 输出使能(1=使能输出)
MASK_DATA_LSW 0x008 带掩码写低16位
MASK_DATA_MSW 0x00C 带掩码写高16位

你可能会问:为什么有DIRM还要OEN?这是Zynq GPIO的一个特点——方向控制和输出使能是分开的。DIRM决定引脚是输入还是输出模式,OEN决定输出是否真正生效。我踩过这个坑:只设了DIRM没设OEN,结果输出死活没反应。

我的习惯: 初始化GPIO时,先设DIRM,再设OEN。如果做输出,两个都写1;如果做输入,DIRM写0,OEN不用管。

3.3 地址映射与基地址

GPIO控制器的基地址是 0xE000_A000。每个Bank的寄存器偏移如下:

  • Bank 0:GPIO[0:31],基地址 + 0x000
  • Bank 1:GPIO[32:53],基地址 + 0x100
  • Bank 2:GPIO[54:63](EMIO),基地址 + 0x200
  • Bank 3:GPIO[64:85](EMIO),基地址 + 0x300

举个例子,如果你想操作MIO的LED(假设接在GPIO[0]),对应的寄存器地址就是:

#define GPIO_BASE       0xE000_A000
#define BANK0_DATA       (GPIO_BASE + 0x000)
#define BANK0_DIRM       (GPIO_BASE + 0x204)
#define BANK0_OEN        (GPIO_BASE + 0x208)

3.4 LED灯控制实战

好了,理论讲完,咱们直接上手。假设LED接在MIO的GPIO[0]上,高电平点亮。

3.4.1 初始化GPIO

void gpio_init(void)
{
    unsigned int *dirm = (unsigned int *)BANK0_DIRM;
    unsigned int *oen  = (unsigned int *)BANK0_OEN;

    // 设置GPIO[0]为输出
    *dirm |= (1 << 0);
    // 使能输出
    *oen  |= (1 << 0);
}

3.4.2 点亮LED

void led_on(void)
{
    unsigned int *data = (unsigned int *)BANK0_DATA;
    *data |= (1 << 0);  // 写1,点亮
}

3.4.3 熄灭LED

void led_off(void)
{
    unsigned int *data = (unsigned int *)BANK0_DATA;
    *data &= ~(1 << 0); // 写0,熄灭
}

3.4.4 闪烁效果

void led_blink(int times)
{
    int i;
    for (i = 0; i < times; i++) {
        led_on();
        delay(500000);  // 简单延时
        led_off();
        delay(500000);
    }
}

我曾经踩过的坑: 写DATA寄存器时,如果只用赋值操作(比如 *data = 0x01),会覆盖其他引脚的状态。正确做法是用位操作(|= 和 &=),或者用MASK_DATA寄存器只修改目标位。

3.5 知识结构图

下面这张图帮你理清GPIO的整体脉络:

Zynq GPIO 知识结构图 Zynq GPIO 知识结构 GPIO 控制器 MIO (0-53) EMIO (54-63) PS 直连,无需 PL 需 PL 逻辑扩展 寄存器组 DATA / DATA_RO DIRM / OEN MASK_DATA 基地址:0xE000_A000

3.6 避坑指南

最后分享几个我实际项目中遇到的坑:

  • 方向寄存器没设对:DIRM和OEN都要配,缺一不可。我有个同事只配了DIRM,折腾了半天。
  • 位操作别用赋值:写DATA寄存器时用 |= 和 &=,别直接 =,否则会误改其他引脚。
  • EMIO需要PL支持:如果你用EMIO,记得在Vivado里把GPIO接口引出来,否则软件配了也没用。
  • 延时别用死循环:生产环境建议用定时器,别用 for 循环做精确延时,CPU会被占死。

嗯,GPIO裸机驱动这块就这些。你想想看,其实核心就是三件事:配方向、写数据、读数据。MIO和EMIO的区别搞清楚了,后面做Linux驱动移植时也会轻松很多。