PL 端的 GPIO IP(AXI GPIO)---PS控制----deepseek

老师,您可以写一份操作ps端gpio的经典例程嘛,比如这个gpio用来给到pl端,与pl端进行数据交互,可以传输数据,控制信号啥的;并每行代码分享注释,具体到底层是如何运行,为什么需要这样写,调用的这些函数每个函数的接口表示的含义,这些函数的功能,这些函数最后解析到底层是对应什么,谢谢啦

同学,坐!看到你要开始写 PS 端控制 GPIO 与 PL 端交互 的经典例程,老师真为你感到高兴。你现在不仅是在写代码,而是在试图理解"上帝是怎么设计这整个世界的"。

在 Zynq 里,GPIO 分为两种:

  1. PS 端自带的 GPIO:直接控制芯片引脚(MIO)。

  2. PL 端的 GPIO IP(AXI GPIO) :这就是我们今天要用的。它挂在 FPGA 内部,PS 通过 AXI 总线去控制它,它再去控制 PL 端的引脚。这就构成了 "软件控制硬件" 的最经典闭环。


🎯 第一步:硬件搭建(Vivado 里的伏笔)

在写 C 代码前,你必须先在 Vivado 里搭好硬件。这决定了你代码里的地址。

  1. 添加 Zynq7 Processing System。

  2. 添加一个 AXI GPIO IP 核(名字设为 axi_gpio_0)。

  3. 双击 AXI GPIO,把 GPIO Width 设为 32(因为我们要传 32 位数据),如果是双向交互,勾选 Enable Dual Channel。

  4. 用 Run Connection Automation 把 Zynq 和 AXI GPIO 连起来。

  5. 在 Address Editor 里,Vivado 会给它分配一个基地址(假设为 0x4120_0000)。

  6. 导出 .xsa 文件,在 Vitis 里新建工程。


🚀 第二步:代码实战与逐行拆解

c

复制代码
#include "xgpio.h"       // AXI GPIO 的底层驱动头文件
#include "xparameters.h" // 包含所有硬件地址的“字典”
#include "xil_printf.h"

// =========================================================
// 宏定义:从字典中提取硬件信息
// =========================================================
// XPAR_AXI_GPIO_0_DEVICE_ID 是系统给这个 IP 分配的编号(通常是 0)
#define GPIO_DEVICE_ID   XPAR_AXI_GPIO_0_DEVICE_ID 

// 声明一个 GPIO 驱动实例
XGpio Gpio;

int main()
{
    int Status;
    u32 send_data = 0xA5A5A5A5; // 我们要发给 PL 端的数据(随便写的)
    u32 recv_data = 0;

    xil_printf("--- PS to PL GPIO Test Start ---\r\n");

    // 【1】初始化 GPIO 驱动
    // XGpio_Initialize:让驱动找到硬件,绑定基地址。
    // 内部机制:这个函数会去查系统字典(xparameters),找到 axi_gpio_0 的物理地址(比如 0x41200000),
    // 然后把这个地址告诉我们的 Gpio 实例。这就相当于给 CPU 配了把去这个外设的钥匙。
    Status = XGpio_Initialize(&Gpio, GPIO_DEVICE_ID);
    if (Status != XST_SUCCESS) {
        xil_printf("GPIO Initialization Failed!\n\r");
        return XST_FAILURE;
    }

    // 【2】设置通道方向
    // XGpio_SetDataDirection:配置是输入还是输出。
    // 第一个参数:GPIO 实例。
    // 第二个参数:通道 1(1 代表 Channel 1)。
    // 第三个参数:0x00 代表全设为输出(0是输出,1是输入)。
    // 底层机制:这行代码会通过 AXI 总线,往 GPIO IP 的“方向寄存器”里写 0。
    // 这相当于告诉 PL 端:“这几根线我要用作输出,你别抢!”
    XGpio_SetDataDirection(&Gpio, 1, 0x00);

    // 【3】PS 发送数据给 PL
    // XGpio_DiscreteWrite:往 GPIO 写数据。
    // 底层机制:这行代码底层其实是一句 Xil_Out32(基地址 + 数据偏移量, send_data)。
    // 数据顺着 AXI 总线狂奔,写入 GPIO IP 的数据寄存器,PL 端的引脚电平瞬间变化。
    XGpio_DiscreteWrite(&Gpio, 1, send_data);
    xil_printf("PS Sent to PL: 0x%08x\r\n", send_data);

    // (在实际工程中,这里通常会加一点延时,或者等 PL 端发一个中断过来)

    // 【4】PS 读取 PL 传来的数据
    // 假设通道 2 是输入(如果在 Vivado 里配置了双通道)
    // XGpio_SetDataDirection(&Gpio, 2, 0xFF); // 设置通道 2 全为输入

    // XGpio_DiscreteRead:读取 PL 端传来的信号。
    // 底层机制:底层是一句 Xil_In32(基地址 + 数据偏移量)。
    // CPU 顺着 AXI 总线去“拿”数据。
    // recv_data = XGpio_DiscreteRead(&Gpio, 2);
    // xil_printf("PS Received from PL: 0x%08x\r\n", recv_data);

    // 【5】反转数据再次发送(做回环测试)
    send_data = ~send_data;
    XGpio_DiscreteWrite(&Gpio, 1, send_data);
    xil_printf("PS Sent reversed data: 0x%08x\r\n", send_data);

    return 0;
}

🎯 第三步:扒开底层的黑盒(这些函数到底干了啥?)

同学,你可能会问:这些 XGpio_xxx 函数看起来很高级,它们到底是什么?其实,它们全都是一层层包装好的"马甲"。

咱们以 XGpio_DiscreteWrite 为例,扒开它的源码看看(路径通常在 xgpio.c):

c

复制代码
void XGpio_DiscreteWrite(XGpio *InstancePtr, unsigned Channel, u32 Data) {
    // 找到要写的寄存器的绝对地址
    UINTPTR RegBase = InstancePtr->BaseAddress + XGPIO_DATA_OFFSET + (Channel - 1) * XGPIO_CHAN_OFFSET;
    
    // 使用最底层的内存映射函数,直接把数据砸进去
    XGpio_WriteReg(RegBase, 0, Data); 
}

你再往下挖,XGpio_WriteReg 最终会变成什么?

在 xil_io.h 里,它变成了这句极其硬核的 C 语言代码:

c

复制代码
*(volatile u32 *)RegBase = Data;

大白话翻译 :"把 RegBase 这个内存地址,强制转换成一个 32 位的指针,然后把 Data 赋值给这个地址。"

这就是软件控制硬件的终极真相! 所有的驱动库(HAL、BSP),只不过是在帮你算地址而已。只要你知道地址,你完全可以在 main.c 里直接写 *(volatile u32 *)0x41200000 = 0xA5A5A5A5;,连库都不用调!


🎯 第四步:PS 与 PL 的数据交互灵魂

上面的代码实现了 单向 传输。在真实的工业应用中,PS 和 PL 的数据交互有几个核心机制:

  1. 轮询(死等) :PS 读完数据后,在 while(1) 里不断看 GPIO 的某个位(比如"数据有效"引脚)有没有拉高。

    • 缺点:极度浪费 CPU。
  2. 中断(最优解) :PL 端准备好数据后,拉高 GPIO 的一个引脚,这个引脚连到了 Zynq 的 pl_ps_irq,触发 CPU 中断。CPU 在中断里调用 XGpio_DiscreteRead 读取数据。

    • 优势:CPU 在平时可以去干别的事,数据到了自动处理。
  3. AXI DMA(大批量数据) :如果是几百兆的数据,千万别用 GPIO 传。要用 AXI DMA 走 High Performance 接口。


💡 老师的总结与忠告

同学,你发现了吗?你看懂了内存映射,GPIO 就再也藏不住秘密了。

为什么还需要写这么复杂的库函数?

因为你写的不仅是你自己的代码,而是整个团队的代码。用库函数,你的同事一看就懂;直接写地址,几个月后你自己都不知道 0x41200000 是什么鬼。这就是咱们之前讲的**"分层解耦"**。

实战中的坑 :

你在 Vivado 里可能配了 64 位的 GPIO,但 Zynq 是 32 位的 ARM,所以一次只能读写 32 位。分两次传就行。

去把这段代码敲进 Vitis,用万用表或者逻辑分析仪量一下 PL 端的引脚。当你看到自己用 C 代码轻轻敲下一行,真实世界的电平随之跳变时,那就是你成为 Zynq 高手的美妙瞬间!老师等你实验成功的捷报!


老师,来一个ps与pl,通过AXI GPIO 双向交互的经典例程吧,并每行代码分享注释,具体到底层是如何运行,为什么需要这样写,调用的这些函数每个函数的接口表示的含义,这些函数的功能,这些函数最后解析到底层是对应什么,谢谢啦

同学,坐!你终于问到了"双向交互",这绝对是 Zynq 开发中最有成就感的一步。之前的单向点灯只是"我说你听",而双向交互是**"握手交谈"**,这意味着 PS 和 PL 真正开始协同工作了。

为了保证你能跑通,老师今天不仅给你写上层应用代码,还要先告诉你 Vivado 里该怎么配,因为硬件决定了软件的"门牌号"。


🎯 第一步:Vivado 硬件配置(极其关键)

  1. 添加 AXI GPIO IP:在 Block Design 中双击它。

  2. 开启双通道 :勾选 Enable Dual Channel。

  3. 配置位宽 :把 Channel 1 和 Channel 2 的 GPIO Width 都设为 8(我们做 8 位数据交互实验,简单直观)。

  4. 连线 :让 S_AXI 连上 Zynq 的 M_AXI_GP0。

  5. 分配地址 :在 Address Editor 里,Vivado 会分配一个基地址(假设是 0x4120_0000)。

  6. (脑补操作):把 Channel 1 的引脚连接到 PL 端的 LED 或自定义逻辑的输入;把 Channel 2 的引脚连接到 PL 端的开关或自定义逻辑的输出。


🚀 第二步:Vitis C 代码实战与逐行拆解

c

复制代码
#include "xgpio.h"       // AXI GPIO 的底层驱动头文件
#include "xparameters.h" // 硬件地址字典(包含 DEVICE_ID 和 BASEADDR)
#include "xil_printf.h"

// =========================================================
// 宏定义与变量声明
// =========================================================
#define GPIO_DEVICE_ID   XPAR_AXI_GPIO_0_DEVICE_ID 

XGpio Gpio; // 实例化一个 GPIO 驱动对象

int main()
{
    int Status;
    u8 send_data = 0xAA; // 我们要发给 PL 端的 8 位数据 (10101010)
    u8 recv_data = 0x00;

    xil_printf("--- PS & PL Dual-Channel GPIO Test Start ---\r\n");

    // 【1】初始化 GPIO 驱动 (找门牌号)
    // 功能:将软件对象与底层硬件绑定。
    // 底层操作:查字典拿到基地址(0x41200000),把地址存入 Gpio 结构体中。
    Status = XGpio_Initialize(&Gpio, GPIO_DEVICE_ID);
    if (Status != XST_SUCCESS) {
        xil_printf("GPIO Initialization Failed!\r\n");
        return XST_FAILURE;
    }

    // 【2】配置通道方向 (极其重要的双向设定)
    // XGpio_SetDataDirection(实例, 通道, 方向值)
    // 参数含义:方向值为 1 代表输入,0 代表输出。
    
    // 通道 1 设为全输出 (0x00):PS 发数据给 PL
    // 底层操作:向基地址+0x04 的寄存器写入 0x00。
    XGpio_SetDataDirection(&Gpio, 1, 0x00);

    // 通道 2 设为全输入 (0xFF):PS 从 PL 读数据
    // 底层操作:向基地址+0x08 的寄存器写入 0xFF。
    XGpio_SetDataDirection(&Gpio, 2, 0xFF);

    // 【3】数据发送 (PS -> PL)
    // XGpio_DiscreteWrite(实例, 通道, 数据)
    // 功能:把 8 位数据写到通道 1 的输出引脚上。
    // 底层解析:*(volatile u32 *)(0x41200000 + 0x00) = 0xAA;
    // 硬件动作:AXI 总线把数据搬到 AXI GPIO IP,IP 立刻将引脚拉高/拉低。
    XGpio_DiscreteWrite(&Gpio, 1, send_data);
    xil_printf("PS sent to PL Channel 1: 0x%02x\r\n", send_data);

    // (假装延迟一下,模拟 PL 端处理时间)
    for(volatile int i = 0; i < 1000000; i++);

    // 【4】数据接收 (PL -> PS)
    // XGpio_DiscreteRead(实例, 通道)
    // 功能:读取通道 2 当前引脚上的电平状态。
    // 底层解析:recv_data = *(volatile u32 *)(0x41200000 + 0x08);
    // 硬件动作:CPU 顺着总线去读 GPIO IP 的数据寄存器,拿到外部传入的值。
    recv_data = XGpio_DiscreteRead(&Gpio, 2);
    xil_printf("PS received from PL Channel 2: 0x%02x\r\n", recv_data);

    // 【5】简单验证逻辑
    if (recv_data == 0x55) { // 假设 PL 端设定了只要收到 0xAA,就回传 0x55
        xil_printf("Bidirectional communication SUCCESS!\r\n");
    } else {
        xil_printf("Data mismatch.\r\n");
    }

    return 0;
}

🎯 第三步:扒开底层的黑盒(底层是怎么跑的?)

同学,你之前问过:"为什么要这样写?" 其实,这些函数全都是为了掩盖一个极其暴力的底层真相。

1. XGpio_Initialize 底层到底干了啥?

它其实就是个"指针赋值":

c

复制代码
// 源码大概长这样
Gpio->BaseAddress = XPAR_AXI_GPIO_0_BASEADDR; // 比如 0x41200000

然后它会让 AXI GPIO 硬件复位一下。从这以后,你调用的所有函数,其实都在操作 0x41200000 这个地址附近的数据。

2. 为什么双通道在底层能分开?

你看你的这段代码 XGpio_DiscreteWrite(&Gpio, 1, send_data); 和读通道 2。

在底层的芯片手册里,AXI GPIO 这个 IP 内部有四个核心寄存器:

  • 0x00:通道 1 数据寄存器 (GPIO_DATA)

  • 0x04:通道 1 方向寄存器 (GPIO_TRI)

  • 0x08:通道 2 数据寄存器 (GPIO2_DATA)

  • 0x0C:通道 2 方向寄存器 (GPIO2_TRI)

当你写 XGpio_SetDataDirection(&Gpio, 1, 0x00) 时,底层实际上是在往 0x41200004 这个地址写入 0x00。

当你写 XGpio_DiscreteRead(&Gpio, 2) 时,底层是在读 0x41200008 这个地址的当前值。

3. 硬件是怎么配合的?
  • 对于输出(通道 1) :你往 0x00 寄存器写 1,AXI GPIO IP 内部的三态门打开,对应引脚输出 3.3V 高电平;写 0 就输出 0V 低电平。

  • 对于输入(通道 2) :PL 端的外设(或者外部按键)拉低了引脚,这个物理电信号传到了 AXI GPIO IP 的接收端。当你调用 Read 去读 0x08 寄存器时,读到的那一位就是 0。


💡 老师的实战避坑指南(小白必看)

  1. 位宽不一致会出事 :如果你的 Vivado 里配置的是 32 位宽,但你代码里只传了 8 位(比如 0xAA),这没关系,高位补 0 而已。但如果你从 PL 端输入超过 32 位的信号,AHB/AXI 数据线就装不下了,会被截断。这就是为什么位宽配置要匹配的原因。

  2. 方向设反了是致命伤:如果通道 1 是输出,你硬把它配成输入,而你又在驱动它输出,硬件内部会发生总线冲突。严重时甚至可能烧毁芯片引脚(虽然 Zynq 内部有三态保护,但会读不到正确的值)。

  3. 不要用 GPIO 传大规模数据:我们这次做的是 8 位控制信号。如果你要传几百兆的图片,千万不要用这种办法。你要去用 AXI DMA,那才是干大工程的。

同学,你可以把这套代码跑起来,如果你有逻辑分析仪,去抓一下 PL 端的引脚波形,你会看到 0xAA 变成了真实的 01010101 的波形。这就是你亲手打通的软硬边界!去试试吧,有报错随时找老师!

相关推荐
hahaha60164 小时前
3D降噪算法YUV降噪FPGA实现
fpga开发
Hello-FPGA1 天前
基于RFSOC FPGA ZCU216 的CoaXPress 采集demo
fpga开发
9527华安1 天前
FPGA基于ADV7182解码PAL视频,Video Processing Subsystem去隔行,提供6套工程源码和技术支持
fpga开发·音视频·pal·adv7182
hahaha60161 天前
数字图像时域滤波或者3D降噪算法FPGA实现
算法·计算机视觉·fpga开发
GateWorld1 天前
Xilinx 7系列FPGA时钟网络详解
fpga开发·时钟网络·xilinx 7系列
szxinmai主板定制专家2 天前
X86 工控机 vs RK3588 异构主控:AMR 控制器 BOM、散热、长期运维对比
运维·stm32·单片机·嵌入式硬件·fpga开发·am62x
szxinmai主板定制专家2 天前
AMR 项目落地:机器人控制器硬件定制化需求复盘
arm开发·单片机·fpga开发·系统架构·机器人·am62x
NamoAmitabha1283 天前
中断工作机制与全流程---zynq例子---deepseek
zynq--ps
NamoAmitabha1283 天前
适合嵌入式开发工程师看的《微机原理与接口技术》---deepseek
zynq--ps