3、AXI4-Lite接口实战:从PL侧控制PS侧GPIO、寄存器映射机制、自定义AXI4-Lite IP核封装

这一章,我们来啃一块硬骨头——AXI4-Lite。说实话,很多初学者一听到AXI总线就头大,觉得协议复杂、时序难懂。其实AXI4-Lite是AXI家族里最温柔的一个成员了。它没有突发传输,没有乱序响应,说白了就是一套带握手的地址+数据读写通道。我当年第一次在Zynq上跑通PL控制PS的GPIO时,那种感觉就像打通了任督二脉——原来软硬件协同就这么回事。

3.1 为什么需要从PL侧控制PS侧GPIO?

你可能会问:PS本身就有GPIO,为什么还要PL去控制?嗯,这个问题我刚开始也困惑过。后来在做一个高速数据采集项目时,PS的GPIO响应速度跟不上PL的逻辑处理速度,我才意识到——有些场景下,PL需要直接接管控制权。

典型的应用场景包括:

  • 高速握手信号:PL需要快速拉高/拉低某个PS的GPIO,用于触发中断或状态指示
  • 系统级复位控制:PL检测到异常后,直接通过PS的GPIO复位整个系统
  • 多核同步:PL作为主控,统一管理PS侧的多个外设使能信号

说白了,就是让PL和PS之间有一条「高速公路」,而不是绕道DDR或者中断这种慢车道。

3.2 寄存器映射机制——你得懂的内存地图

在Zynq里,PS侧的GPIO被映射到固定的地址空间。具体来说,PS的GPIO控制器基地址是0xE000A000。这个地址,PL通过AXI4-Lite总线可以直接访问。

我习惯把寄存器映射理解成「门牌号系统」。每个寄存器就是一个房间,地址就是门牌号。PL要做的,就是拿着门牌号去敲门,然后读写房间里的数据。

PS GPIO的关键寄存器偏移如下:

寄存器名称 偏移地址 功能描述
GPIO_DATA_RO 0x000 读GPIO输入状态(只读)
GPIO_DATA 0x000 写GPIO输出值(写有效)
GPIO_DIRM 0x204 方向控制寄存器(0=输入,1=输出)
GPIO_OUTEN 0x208 输出使能寄存器
注意:GPIO_DATA_RO和GPIO_DATA虽然是同一个地址,但读写行为不同。读操作返回的是引脚实际电平,写操作设置的是输出寄存器。我在项目中曾因为没区分这个,调试了整整一个下午。

3.3 自定义AXI4-Lite IP核封装——从零开始

Vivado里自带了一个很方便的工具——Create and Package New IP。我个人建议你直接用它生成模板,然后修改。自己手写AXI4-Lite接口?除非你想挑战自己,否则没必要。

具体步骤是这样的:

  1. 打开Vivado,点击Tools → Create and Package New IP
  2. 选择Create a new AXI4 peripheral
  3. 设置IP名称、版本、接口类型(选AXI4-Lite)
  4. 配置寄存器数量(一般4~8个够用)
  5. 生成模板后,修改RTL代码

生成的模板里,核心就是那个axi_lite_ipif模块。它帮你处理了地址译码、握手信号、读写响应。你只需要关注用户逻辑部分。

下面是我常用的一个简化版Verilog代码框架:

module my_axi_gpio_ctrl #
(
  parameter integer C_S_AXI_DATA_WIDTH = 32,
  parameter integer C_S_AXI_ADDR_WIDTH = 4
)
(
  input  wire                        S_AXI_ACLK,
  input  wire                        S_AXI_ARESETN,
  // 写地址通道
  input  wire [C_S_AXI_ADDR_WIDTH-1:0] S_AXI_AWADDR,
  input  wire                        S_AXI_AWVALID,
  output wire                        S_AXI_AWREADY,
  // 写数据通道
  input  wire [C_S_AXI_DATA_WIDTH-1:0] S_AXI_WDATA,
  input  wire                        S_AXI_WVALID,
  output wire                        S_AXI_WREADY,
  // 写响应通道
  output wire [1:0]                  S_AXI_BRESP,
  output wire                        S_AXI_BVALID,
  input  wire                        S_AXI_BREADY,
  // 读地址通道
  input  wire [C_S_AXI_ADDR_WIDTH-1:0] S_AXI_ARADDR,
  input  wire                        S_AXI_ARVALID,
  output wire                        S_AXI_ARREADY,
  // 读数据通道
  output wire [C_S_AXI_DATA_WIDTH-1:0] S_AXI_RDATA,
  output wire [1:0]                  S_AXI_RRESP,
  output wire                        S_AXI_RVALID,
  input  wire                        S_AXI_BREADY,
  // 用户逻辑接口
  output wire [31:0]                 gpio_out,
  input  wire [31:0]                 gpio_in
);

  // 寄存器声明
  reg [31:0] slv_reg0;  // 输出控制寄存器
  reg [31:0] slv_reg1;  // 方向控制寄存器
  reg [31:0] slv_reg2;  // 保留
  reg [31:0] slv_reg3;  // 保留

  // 地址译码逻辑
  always @(posedge S_AXI_ACLK)
  begin
    if (!S_AXI_ARESETN) begin
      slv_reg0 <= 32'h0;
      slv_reg1 <= 32'h0;
    end else begin
      if (S_AXI_AWVALID && S_AXI_WVALID) begin
        case (S_AXI_AWADDR[3:2])
          2'b00: slv_reg0 <= S_AXI_WDATA;
          2'b01: slv_reg1 <= S_AXI_WDATA;
        endcase
      end
    end
  end

  // 读操作
  assign S_AXI_RDATA = (S_AXI_ARADDR[3:2] == 2'b00) ? slv_reg0 :
                       (S_AXI_ARADDR[3:2] == 2'b01) ? slv_reg1 :
                       gpio_in;  // 读GPIO输入状态

  // 输出赋值
  assign gpio_out = slv_reg0;

endmodule
小技巧:地址译码时,我习惯用[3:2]而不是[1:0]。因为AXI4-Lite的地址是以字节为单位的,但数据总线是32位,所以最低两位其实没用。用[3:2]可以避免很多地址对齐的坑。

3.4 实战:PL控制PS的MIO

好了,理论说完了,咱们直接上实战。假设我们要用PL控制PS的MIO7(板载LED)。

第一步,在Vivado里添加我们刚才封装的IP核。然后连接:

  • IP的AXI4-Lite接口连接到Zynq的M_AXI_GP0口
  • IP的gpio_out[0]连接到PS的GPIO输入(通过EMIO)

第二步,在PS端配置EMIO。SDK里写一段简单的初始化代码:

#include "xgpio.h"
#include "xparameters.h"

XGpio ps_gpio;

int main()
{
    // 初始化PS GPIO
    XGpio_Initialize(&ps_gpio, XPAR_PS7_GPIO_0_DEVICE_ID);

    // 设置EMIO方向为输入(从PL接收)
    XGpio_SetDataDirection(&ps_gpio, 2, 0xFF);  // Bank2对应EMIO

    // 读取PL写入的值
    u32 pl_value = XGpio_DiscreteRead(&ps_gpio, 2);

    // 控制板载LED
    XGpio_DiscreteWrite(&ps_gpio, 1, pl_value);

    return 0;
}

第三步,在PL侧通过AXI4-Lite写寄存器。比如我们要让LED闪烁:

// 在PL的testbench或状态机中
// 地址0xE000A000 + 0x000 = GPIO_DATA
// 写入0x00000080 表示MIO7输出高电平
// 写入0x00000000 表示MIO7输出低电平

// 实际通过AXI4-Lite IP核操作
// 写slv_reg0 = 0x00000001  // 让gpio_out[0] = 1
// 等待一段时间
// 写slv_reg0 = 0x00000000  // 让gpio_out[0] = 0
核心要点:PL通过AXI4-Lite写PS的GPIO寄存器,本质上是「远程控制」。PL不需要知道PS内部的具体实现,只需要按照地址映射表操作即可。这就是AXI总线的魅力——解耦。

3.5 避坑指南——我曾经踩过的雷

做这个实验时,我遇到过几个典型问题,分享给你:

  • 地址不对齐:AXI4-Lite要求地址是4字节对齐的。如果你写了个奇数地址,总线会直接返回错误响应。我当时查了三天,最后发现是地址偏移算错了。
  • 握手信号没处理好:AWREADY和WREADY必须同时为高,写操作才能完成。有些新手只拉了一个,结果数据写不进去。
  • 复位状态不确定:PS的GPIO在复位后默认是输入状态。如果你PL一上来就写输出,PS可能不响应。记得先配置方向寄存器。

嗯,这些坑说大不大,但卡住你两三天是没问题的。我建议你调试时,先用ILA抓一下AXI总线的波形,看看地址和数据对不对。很多时候,问题出在你自己都没注意到的细节上。

3.6 知识体系总览

下面这张图,是我自己总结的AXI4-Lite控制PS GPIO的完整数据流。你看一眼,应该就能明白整个链路是怎么走的。

PL 逻辑 自定义AXI4-Lite IP 寄存器映射 gpio_out[0] AXI4-Lite 总线 地址:0xE000A000 数据:32位 PS 处理器系统 GPIO控制器 EMIO接口 MIO7 (LED) 数据流方向:PL → AXI4-Lite → PS GPIO → 物理引脚 寄存器映射关系 PL侧地址偏移 PS侧寄存器 功能 0x00 GPIO_DATA 写输出值 0x204 GPIO_DIRM 方向控制 0x208 GPIO_OUTEN 输出使能

你看,整个链路其实很清晰。PL通过AXI4-Lite总线,把数据写到PS的GPIO寄存器里,PS再把这些数据驱动到物理引脚上。反过来,PL也可以读PS的GPIO输入状态。这就是所谓的「软硬件协同」——PL负责高速逻辑,PS负责管理和调度。

好了,这一章的内容就到这里。你如果跟着做一遍,应该能对AXI4-Lite有更深的体会。记住,纸上得来终觉浅,绝知此事要躬行。去打开Vivado,动手试试吧。

可可精品资料库