4. 自定义AXI4-Lite IP核封装:使用Vivado IP Packager创建自定义IP、添加用户逻辑寄存器

好,咱们进入第四章。这一章要干的事,说白了就是——把你自己写的逻辑,打包成一个标准的AXI4-Lite IP核。

为什么要干这个?因为Zynq的PS和PL通信,最常用的方式就是通过AXI总线。而AXI4-Lite,是其中最轻量、最容易上手的一种。你写一个自定义IP,挂到AXI总线上,PS端就能像访问内存一样,读写你IP里的寄存器。

我在项目里见过不少新手,直接在顶层文件里写一堆端口,然后手动连AXI接口。结果呢?时序乱套、地址对不上、仿真跑不通……折腾好几天。其实Vivado早就给你准备好了IP Packager这个工具,咱们得学会用它。

本章核心目标:

  • 学会使用Vivado IP Packager创建自定义AXI4-Lite IP
  • 理解AXI4-Lite接口的寄存器映射机制
  • 在IP中添加用户逻辑寄存器,并实现PS端的读写控制

4.1 AXI4-Lite接口的本质

先聊点基础的。AXI4-Lite,是AXI4协议的一个子集。它去掉了突发传输、去掉了数据交织,只保留了最基本的单次读写能力。

你想想看,对于配置寄存器、读取状态这种操作,一次读写一个地址就够了,没必要搞突发。所以AXI4-Lite非常适合用来做控制平面的通信。

特性 AXI4-Full AXI4-Lite AXI4-Stream
突发传输 支持 不支持 支持
数据宽度 可配置 32位固定 可配置
地址总线 独立 独立 无地址
典型用途 DMA、高速数据传输 寄存器配置、状态读取 数据流、视频、音频
接口复杂度 高 低 中

嗯,这张表你得记住。选型的时候,别拿AXI4-Lite去传视频流,那会把自己坑死。

4.2 Vivado IP Packager 工作流程

Vivado的IP Packager,说白了就是一个打包工具。你把RTL代码、约束文件、仿真模型准备好,它帮你生成一个标准的IP核,带AXI接口、带GUI配置界面、带文档模板。

我个人习惯的流程是这样的:

  1. 创建IP工程——在Vivado里选择"Create and Package New IP"
  2. 选择模板——选AXI4-Lite外设模板,省去手写接口的麻烦
  3. 配置接口参数——设置地址范围、寄存器数量、数据宽度
  4. 添加用户逻辑——在生成的模板里,加入你自己的功能代码
  5. 打包IP——生成.xcix文件,供其他工程使用

小技巧:第一次创建IP时,建议选"Edit IP"模式。这样Vivado会打开一个独立的工程,你可以直接修改RTL代码,仿真验证通过后再打包。别一上来就"Package IP",万一代码有bug,后面改起来麻烦。

4.3 理解AXI4-Lite的寄存器映射

这是本章最核心的概念。AXI4-Lite的寄存器映射,其实就是把一组32位的寄存器,映射到一段连续的地址空间上。

PS端通过AXI总线发起读写操作,地址总线告诉IP要访问哪个寄存器,数据总线传输具体的数据。IP内部有一个地址译码器,根据地址选择对应的寄存器。

AXI4-Lite 寄存器映射结构 PS (ARM Cortex-A9) 发起读写请求 AXI4-Lite 总线 地址 + 数据 + 控制 自定义 IP 核 地址译码器 slv_reg0 ~ slv_regN slv_reg0 (地址偏移 0x00) slv_reg1 (地址偏移 0x04) slv_reg2 (地址偏移 0x08) slv_reg3 (地址偏移 0x0C) 用户逻辑 (自定义功能) 关键点 • 每个寄存器32位宽 • 地址偏移按4字节对齐 • 读/写操作独立通道

你看这张图,PS通过AXI总线发送地址和数据,IP内部的地址译码器根据地址偏移,选中对应的寄存器。每个寄存器占4字节(32位),地址偏移依次是0x00、0x04、0x08、0x0C……

4.4 实战:创建自定义AXI4-Lite IP

好,理论说完了,咱们动手。我以Vivado 2023.1为例,带你走一遍完整的流程。

4.4.1 创建IP工程

  1. 打开Vivado,点击 Tools → Create and Package New IP
  2. 选择 Create a new AXI4 peripheral
  3. 设置IP名称:my_axi_gpio,版本 1.0
  4. 接口类型选 AXI4-Lite,数据宽度32位
  5. 寄存器数量设为 4(slv_reg0 ~ slv_reg3)
  6. 选择 Edit IP 模式,点击Finish

注意:寄存器数量不要贪多。每个寄存器占用4字节地址空间,4个寄存器就是16字节。如果你只需要2个寄存器,就设2个,别浪费地址空间。我曾经见过有人设了64个寄存器,结果大部分都没用,白白增加了逻辑资源消耗。

4.4.2 理解生成的模板代码

Vivado会生成一个Verilog文件,里面包含了AXI4-Lite从机接口的完整实现。核心代码是这样的:

// AXI4-Lite 从机接口模板 (简化版)
module my_axi_gpio_v1_0_S00_AXI #(
    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 [C_S_AXI_DATA_WIDTH/8-1:0] S_AXI_WSTRB,
    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
);

    // 用户寄存器
    reg [C_S_AXI_DATA_WIDTH-1:0] slv_reg0;
    reg [C_S_AXI_DATA_WIDTH-1:0] slv_reg1;
    reg [C_S_AXI_DATA_WIDTH-1:0] slv_reg2;
    reg [C_S_AXI_DATA_WIDTH-1:0] slv_reg3;

    // 地址译码逻辑
    localparam ADDR_LSB = (C_S_AXI_DATA_WIDTH/32) + 1;
    localparam OPT_MEM_ADDR_BITS = 1;

    // 写操作
    always @(posedge S_AXI_ACLK) begin
        if (!S_AXI_ARESETN) begin
            slv_reg0 <= 0;
            slv_reg1 <= 0;
            slv_reg2 <= 0;
            slv_reg3 <= 0;
        end else begin
            if (S_AXI_WVALID && S_AXI_AWVALID) begin
                case (S_AXI_AWADDR[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB])
                    2'h0 : slv_reg0 <= S_AXI_WDATA;
                    2'h1 : slv_reg1 <= S_AXI_WDATA;
                    2'h2 : slv_reg2 <= S_AXI_WDATA;
                    2'h3 : slv_reg3 <= S_AXI_WDATA;
                endcase
            end
        end
    end

    // 读操作
    always @(*) begin
        case (S_AXI_ARADDR[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB])
            2'h0 : reg_data_out <= slv_reg0;
            2'h1 : reg_data_out <= slv_reg1;
            2'h2 : reg_data_out <= slv_reg2;
            2'h3 : reg_data_out <= slv_reg3;
            default : reg_data_out <= 0;
        endcase
    end

endmodule

这段代码,你仔细看。它实现了:

  • 写操作:当AWVALID和WVALID同时有效时,根据地址把WDATA写入对应的寄存器
  • 读操作:根据ARADDR,把对应寄存器的值送到RDATA上
  • 地址译码:用地址的低2位(因为4个寄存器)来选择目标

4.4.3 添加用户逻辑

现在,咱们在寄存器的基础上,加点实际功能。比如做一个可配置的PWM发生器:

  • slv_reg0:PWM周期(PS写入)
  • slv_reg1:PWM占空比(PS写入)
  • slv_reg2:控制寄存器(使能位、极性位)
  • slv_reg3:状态寄存器(当前计数值、溢出标志)
// 在模板中添加用户逻辑
// PWM 发生器
reg [31:0] pwm_counter;
reg        pwm_out;

always @(posedge S_AXI_ACLK) begin
    if (!S_AXI_ARESETN) begin
        pwm_counter <= 0;
        pwm_out     <= 0;
    end else begin
        if (slv_reg2[0]) begin  // 使能位
            if (pwm_counter >= slv_reg0) begin
                pwm_counter <= 0;
            end else begin
                pwm_counter <= pwm_counter + 1;
            end

            if (pwm_counter < slv_reg1) begin
                pwm_out <= 1'b1;
            end else begin
                pwm_out <= 1'b0;
            end
        end else begin
            pwm_counter <= 0;
            pwm_out     <= 0;
        end
    end
end

// 更新状态寄存器
always @(posedge S_AXI_ACLK) begin
    if (!S_AXI_ARESETN) begin
        slv_reg3 <= 0;
    end else begin
        slv_reg3[31:1] <= pwm_counter[31:1];
        slv_reg3[0]    <= (pwm_counter == slv_reg0) ? 1'b1 : 1'b0;  // 溢出标志
    end
end

// 输出PWM信号
assign pwm_output = pwm_out;

经验之谈:写用户逻辑时,一定要把寄存器读写和用户功能分开。寄存器读写用模板里的always块,用户功能自己单独写always块。这样代码清晰,调试也方便。我早期犯过一个错,把PWM逻辑直接塞进了寄存器写操作的case语句里,结果地址译码和功能逻辑搅在一起,改一个bug引出三个新bug。

4.5 打包与验证

代码写完后,在IP工程里做两步:

  1. 综合检查:跑一遍Synthesis,确保没有语法错误和综合警告
  2. 行为仿真:写一个简单的testbench,验证寄存器读写和PWM功能

确认无误后,点击 Tools → Package IP。Vivado会生成一个.xcix文件,这就是你的自定义IP核。

步骤 操作 预期结果
1 综合检查 无错误,无严重警告
2 行为仿真 寄存器读写正确,PWM波形符合预期
3 打包IP 生成 .xcix 文件,可在IP Catalog中看到
4 导入工程 在Block Design中可添加该IP

4.6 避坑指南

我曾经踩过的坑:

  • 地址对齐问题:AXI4-Lite的地址必须是4字节对齐的。如果你在PS端写了一个非对齐地址,总线会直接返回错误。解决办法:在PS端用Xil_Out32这类函数,它们会自动处理对齐。
  • 寄存器复位值:模板里寄存器复位值是0。如果你的逻辑需要非零初始值,记得在复位逻辑里改。我见过有人忘了改,结果PWM使能位默认是0,PS端写1之前,PWM一直不工作。
  • WSTRB信号:这个信号是写选通,用来控制写入哪些字节。如果你只写32位数据,WSTRB固定为4'b1111就行。但如果你做字节级操作,必须正确处理WSTRB,否则会写错数据。

4.7 小结

这一章,咱们从AXI4-Lite的接口本质讲起,到Vivado IP Packager的完整流程,再到添加用户逻辑寄存器,最后打包成可复用的IP核。

核心就三句话:

  • AXI4-Lite是控制通道,不是数据通道——别拿它传大块数据
  • 寄存器映射是核心——地址偏移、数据宽度、读写时序,必须搞清
  • IP Packager是工具,不是魔法——它帮你省去手写接口的麻烦,但用户逻辑还得你自己写