news 2026/4/16 21:51:38

FPGA教程系列-Vivado AXI4-Stream自定义IP核

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
FPGA教程系列-Vivado AXI4-Stream自定义IP核

FPGA教程系列-Vivado AXI4-Stream自定义IP核

打包AXI4-Stream MASTER(创建一个自定义IP核)

下一步

1. Package your current project (封装当前工程):将你当前正在 Vivado 中打开的整个 RTL(Verilog/VHDL)工程打包成一个 IP 核。

2. Package a block design from the current project (封装当前工程中的 Block Design):将当前工程中已经画好的 Block Design(.bd 文件,即由多个 IP 连接而成的子系统)封装成一个新的、单一的 IP 核。

3. Package a specified directory (封装指定目录):选择硬盘上的一个特定文件夹,Vivado 会扫描该文件夹内的源文件(代码、约束等),并将其封装为 IP。

4. Create a new AXI4 peripheral (创建一个新的 AXI4 外设):这不是封装现有的代码,而是生成代码模板。它会启动一个向导,帮你生成一个带有 AXI4 总线接口(AXI4-Lite, AXI4-Full, 或 AXI4-Stream)的 Verilog/VHDL 框架代码。

选第四个。设置名称:

选择Stream接口,接口类型选择主机Master,数据位宽32位

直接将IP添加到仓库里,IP打包完成:

默认参数,生成ip即可。source中可以看到该ip的逻辑文件。

同样,生成一个slave文件。

接收代码解读

完整代码不再粘贴,主要是对别人代码的一种学习,最主要的是学习思想,而不是简单的看代码,也是一种加深印象的做法。

模块声明与参数定义

`timescale 1 ns / 1 ps module myip_AXIslave_slave_stream_v1_0_S00_AXIS # ( // Users to add parameters here // User parameters ends // Do not modify the parameters beyond this line // AXI4Stream sink: Data Width parameter integer C_S_AXIS_TDATA_WIDTH = 32 )
  • ** `timescale **:定义仿真时间单位(1ns)和精度(1ps)。
  • module …:定义了模块名称。
  • parameter:定义了一个参数C_S_AXIS_TDATA_WIDTH,默认是 32 位。这意味着这个 IP 默认每次传输 32bit(4字节)的数据。如果你在 Vivado 里改了这个参数,整个模块的位宽会自动调整。

端口定义(输入输出)

( // AXI4Stream sink: Clock input wire S_AXIS_ACLK, // AXI4Stream sink: Reset input wire S_AXIS_ARESETN, // Ready to accept data in output wire S_AXIS_TREADY, // Data in input wire [C_S_AXIS_TDATA_WIDTH-1 : 0] S_AXIS_TDATA, // Byte qualifier input wire [(C_S_AXIS_TDATA_WIDTH/8)-1 : 0] S_AXIS_TSTRB, // Indicates boundary of last packet input wire S_AXIS_TLAST, // Data is in valid input wire S_AXIS_TVALID );

定义了标准的AXI4-Stream 协议信号:

  • S_AXIS_ACLK:时钟信号,所有逻辑都在这个时钟上升沿动作。
  • S_AXIS_ARESETN​:复位信号,低电平有效(名字里最后的N代表 Negative)。
  • S_AXIS_TVALID(输入):主机(Master)说:“我有数据要发”。
  • S_AXIS_TREADY(输出):从机(也就是本模块)说:“我准备好接收了”。只有当TVALID​ 和TREADY同时为 1 时,数据才算传输成功。
  • S_AXIS_TDATA:真正的数据总线。
  • S_AXIS_TLAST:表示这是当前数据包的“最后一个”数据。
  • S_AXIS_TSTRB:字节选通信号(用于指示哪些字节有效),但在本代码逻辑中常被忽略。

辅助函数与常量定义

function integer clogb2 (input integer bit_depth); begin for(clogb2=0; bit_depth>0; clogb2=clogb2+1) bit_depth = bit_depth >> 1; end endfunction localparam NUMBER_OF_INPUT_WORDS = 8; localparam bit_num = clogb2(NUMBER_OF_INPUT_WORDS-1);
  • clogb2函数:这是一个计算“以2为底的对数(向上取整)”的函数。存NUMBER_OF_INPUT_WORDS = 8​ 个数据。为了给这 8 个位置编号(0~7),需要几位二进制?clogb2(7)​ 算出来是3位。所以bit_num= 3,后面定义指针时会用到这个宽度。

状态机定义

parameter [1:0] IDLE = 1'b0, // Initial/idle state WRITE_FIFO = 1'b1; // FIFO written state wire axis_tready; reg mst_exec_state;

定义了两个状态:

  1. IDLE (0):空闲,发呆,等待开始信号。
  2. WRITE_FIFO (1):正在干活,正在把数据写进 FIFO。

mst_exec_state:这是一个寄存器,用来存当前处于哪个状态。

状态机逻辑(FSM)

always @(posedge S_AXIS_ACLK) begin if (!S_AXIS_ARESETN) mst_exec_state <= IDLE; else case (mst_exec_state) IDLE: // 只要看到主机发来了 TVALID,就进入写状态 if (S_AXIS_TVALID) mst_exec_state <= WRITE_FIFO; else mst_exec_state <= IDLE; WRITE_FIFO: // 如果 writes_done (写完了) 信号变高,就回到空闲 if (writes_done) mst_exec_state <= IDLE; else mst_exec_state <= WRITE_FIFO; endcase end
  • 复位时,进入IDLE
  • IDLE:如果你给了TVALID​,下一拍我就跳到WRITE_FIFO准备收数据。
  • WRITE_FIFO:我就一直收,直到writes_done​ 变高(由后面逻辑决定,比如存满了或者收到了 TLAST),然后跳回IDLE

TREADY 信号生成(握手)

assign S_AXIS_TREADY = axis_tready; assign axis_tready = ((mst_exec_state == WRITE_FIFO) && (write_pointer <= NUMBER_OF_INPUT_WORDS-1));

告诉主机什么时候准备好。

  • 条件是:必须处于WRITE_FIFO状态FIFO 还没满(指针小于等于7)。
  • 注意:因为状态机从 IDLE 跳到 WRITE_FIFO 需要一个时钟周期,所以主机拉高TVALID​ 后,这个 IP 至少要晚一个周期才会拉高TREADY

写指针与写完成逻辑(最关键的逻辑)

always@(posedge S_AXIS_ACLK) begin if(!S_AXIS_ARESETN) begin write_pointer <= 0; writes_done <= 1'b0; end else if (write_pointer <= NUMBER_OF_INPUT_WORDS-1) begin if (fifo_wren) // 如果真的写入了数据 begin write_pointer <= write_pointer + 1; // 指针+1 writes_done <= 1'b0; end // 如果刚写的是第8个数据 (指针==7) 或者 收到了 TLAST if ((write_pointer == NUMBER_OF_INPUT_WORDS-1)|| S_AXIS_TLAST) begin writes_done <= 1'b1; // 标记:写完了 end end end // 生成写使能:必须 VALID 和 READY 同时为 1 assign fifo_wren = S_AXIS_TVALID && axis_tready;
  • fifo_wren:这是标准的 AXI 握手逻辑,只有双方都同意,才算写入一次。
  • write_pointer:每写一次,指针加 1。
  • writes_done​:当存满 8 个数,或者收到TLAST信号时,这个信号拉高,通知状态机回到 IDLE。

隐藏的坑:仔细看代码,write_pointer除了复位信号外,没有任何清零的逻辑!

  • 这意味着:这个 IP 接收完这一包数据后,write_pointer​ 就停在末尾了。下一包数据来的时候,因为指针没归零,TREADY​ 永远拉不起来。它是一次性的!如果你想重复使用,必须修改这里,让指针在writes_done后清零。

FIFO 存储实现

generate for(byte_index=0; byte_index<= (C_S_AXIS_TDATA_WIDTH/8-1); byte_index=byte_index+1) begin:FIFO_GEN // 定义存储器数组 reg [(C_S_AXIS_TDATA_WIDTH/4)-1:0] stream_data_fifo [0 : NUMBER_OF_INPUT_WORDS-1]; always @( posedge S_AXIS_ACLK ) begin // 下面这行注释里本来有 S_AXIS_TSTRB,但被屏蔽了 if (fifo_wren)// && S_AXIS_TSTRB[byte_index]) begin // 写入数据 stream_data_fifo[write_pointer] <= S_AXIS_TDATA[(byte_index*8+7) -: 8]; end end end endgenerate
  • 这里用了一个generate循环。对于 32位宽的数据,它循环 4 次(0, 1, 2, 3)。
  • 目的是把 32位的存储器拆成 4 个 8位的存储器。
  • 为什么这么麻烦?主要是为了支持S_AXIS_TSTRB(字节掩码),可以单独写某个字节。

发送代码解读

模块声明与参数

module myip_AXImaster_master_stream_v1_0_M00_AXIS # ( parameter integer C_M_AXIS_TDATA_WIDTH = 32, parameter integer C_M_START_COUNT = 32 )
  • C_M_AXIS_TDATA_WIDTH(32):数据位宽,默认为 32位(4字节)。
  • C_M_START_COUNT(32):启动延迟计数。这个 Master 不会复位后立刻发数据,而是会先等 32 个时钟周期。这是为了防止系统复位未稳时就开始通信。

端口定义

( input wire M_AXIS_ACLK, input wire M_AXIS_ARESETN, output wire M_AXIS_TVALID, output wire [C_M_AXIS_TDATA_WIDTH-1 : 0] M_AXIS_TDATA, output wire [(C_M_AXIS_TDATA_WIDTH/8)-1 : 0] M_AXIS_TSTRB, output wire M_AXIS_TLAST, input wire M_AXIS_TREADY );
  • M_AXIS_TVALID:主机输出。表示“我现在有有效数据要发给你”。
  • M_AXIS_TDATA:主机输出。数据线。
  • M_AXIS_TLAST:主机输出。表示“这是最后一个数据了”。
  • M_AXIS_TREADY主机输入。从机告诉主机“我准备好了”。这是唯一的输入控制信号(除了时钟复位)。

辅助函数与常量

localparam NUMBER_OF_OUTPUT_WORDS = 8;
  • NUMBER_OF_OUTPUT_WORDS(8):这个演示代码只会发 8 个数,发完就停。
  • WAIT_COUNT_BITS​ 和bit_num:计算计数器和指针需要的位宽。

状态机定义 (FSM)

parameter [1:0] IDLE = 2'b00, INIT_COUNTER = 2'b01, SEND_STREAM = 2'b10; reg [1:0] mst_exec_state;
  • IDLE (00):复位后的初始状态。
  • INIT_COUNTER (01):倒计时/等待状态。
  • SEND_STREAM (10):正式发送数据的状态。

状态机逻辑

always @(posedge M_AXIS_ACLK) begin if (!M_AXIS_ARESETN) ... else case (mst_exec_state) IDLE: mst_exec_state <= INIT_COUNTER; // 复位松开立刻进入计数状态 INIT_COUNTER: if ( count == C_M_START_COUNT - 1 ) // 等待计数器数满 mst_exec_state <= SEND_STREAM; // 进入发送状态 else begin count <= count + 1; mst_exec_state <= INIT_COUNTER; end SEND_STREAM: if (tx_done) // 如果数据发完了 mst_exec_state <= IDLE; // 回到 IDLE(注意:回到 IDLE 后会马上再次进入 INIT_COUNTER) else mst_exec_state <= SEND_STREAM; endcase end
  • 流程:复位 -> IDLE -> INIT_COUNTER (等32个周期) -> SEND_STREAM (发8个数) -> IDLE -> INIT_COUNTER …
  • 循环发送:注意,这里的逻辑会让它无限循环发送。每次发完 8 个数,等一会,再发 8 个数。这与 Slave 代码的“一次性”不同。

控制信号生成 (TVALID, TLAST)

assign axis_tvalid = ((mst_exec_state == SEND_STREAM) && (read_pointer < NUMBER_OF_OUTPUT_WORDS)); assign axis_tlast = (read_pointer == NUMBER_OF_OUTPUT_WORDS-1);
  • axis_tvalid​:只有在SEND_STREAM状态且还没发完 8 个数时,才为高。
  • axis_tlast:当读指针指到第 7 个数(最后一个)时,拉高。

关键延迟逻辑(Alignment):

always @(posedge M_AXIS_ACLK) begin ... axis_tvalid_delay <= axis_tvalid; axis_tlast_delay <= axis_tlast; end assign M_AXIS_TVALID = axis_tvalid_delay; assign M_AXIS_TLAST = axis_tlast_delay;

为什么要有 delay?

  • 看后面的数据生成逻辑,数据stream_data_out是在时钟沿更新的(时序逻辑)。
  • 如果不打一拍,控制信号(组合逻辑生成的axis_tvalid)会比数据早一拍到达,导致时序对不齐。
  • 为了让 TVALID、TLAST 和 TDATA 在同一个时钟周期对齐输出,这里故意把控制信号打了一拍。

读指针逻辑

always@(posedge M_AXIS_ACLK) begin ... if (read_pointer <= NUMBER_OF_OUTPUT_WORDS-1) begin if (tx_en) // 握手成功(TREADY && TVALID) begin read_pointer <= read_pointer + 1; tx_done <= 1'b0; end end else if (read_pointer == NUMBER_OF_OUTPUT_WORDS) begin tx_done <= 1'b1; // 发完了 end end
  • tx_en​:assign tx_en = M_AXIS_TREADY && axis_tvalid;只有当从机说 Ready 且主机 Valid 时,指针才加 1。
  • tx_done​:当指针数到 8 时,拉高tx_done,通知状态机切状态。

隐患:这里同样没有显式的指针清零逻辑(除了复位)。但是,因为mst_exec_state​ 会跳回 IDLE,如果需要在下一轮循环中让read_pointer​ 归零,代码里其实缺了一句逻辑!

  • Bug Alert: 仔细看代码,read_pointer​ 只有在!M_AXIS_ARESETN​ 时才清零。这意味着,虽然状态机在循环跑(IDLE->INIT->SEND->IDLE),但read_pointer一直停在 8。
  • 结论:这个 Master 实际上也是一次性的。它发完第一包 8 个数后,虽然状态机会不断尝试进入 SEND 状态,但因为read_pointer​ 已经是 8 了,axis_tvalid永远是 0,数据再也发不出来了。

数据生成(最简单的部分)

always @( posedge M_AXIS_ACLK ) begin if(!M_AXIS_ARESETN) stream_data_out <= 1; else if (tx_en) stream_data_out <= read_pointer + 32'b1; end
  • 初始值:1。

  • 后续值read_pointer + 1​。所以发送的数据序列是:1, 1, 2, 3, 4, 5, 6, 7​。(注意第一拍发出去的是初始值 1,发送同时tx_en​ 有效,下一拍更新为read_pointer(1) + 1 = 2… 稍微有点怪,通常期望发 1~8)。

    • 实际上,由于stream_data_out​ 是时序逻辑更新,而M_AXIS_TDATA直接连它。
    • 第一拍握手时,发出去的是旧值(1)。
    • 握手后,stream_data_out更新。
    • 所以发出的数据大概率是1​ (初始),1​ (ptr=0+1),2​,3… 直到最后。

仿真

Testbench

`timescale 1ns / 1ps module tb_axis_system; // ========================================================================= // 1. 信号定义 // ========================================================================= reg aclk; reg aresetn; // AXI4-Stream 接口连接线 (连接 Master 输出 -> Slave 输入) wire [31:0] axis_tdata; wire [3:0] axis_tstrb; wire axis_tlast; wire axis_tvalid; wire axis_tready; // 定义时钟周期 (100MHz = 10ns) parameter CLK_PERIOD = 10; // ========================================================================= // 2. 模块实例化 // ========================================================================= // 实例化 Master (发送者) myip_AXImaster_0 #( .C_M_AXIS_TDATA_WIDTH(32), .C_M_START_COUNT(10) // 修改参数:缩短启动等待时间,方便仿真查看 ) u_master ( .m00_axis_aclk (aclk), .m00_axis_aresetn (aresetn), .m00_axis_tvalid (axis_tvalid), .m00_axis_tdata (axis_tdata), .m00_axis_tstrb (axis_tstrb), .m00_axis_tlast (axis_tlast), .m00_axis_tready (axis_tready) ); // 实例化 Slave (接收者) myip_AXIslave_0 #( .C_S_AXIS_TDATA_WIDTH(32) ) u_slave ( .s00_axis_aclk (aclk), .s00_axis_aresetn (aresetn), .s00_axis_tvalid (axis_tvalid), .s00_axis_tdata (axis_tdata), .s00_axis_tstrb (axis_tstrb), .s00_axis_tlast (axis_tlast), .s00_axis_tready (axis_tready) ); // ========================================================================= // 3. 时钟生成 // ========================================================================= initial begin aclk = 0; forever #(CLK_PERIOD/2) aclk = ~aclk; end // ========================================================================= // 4. 测试流程控制 // ========================================================================= initial begin // 1. 初始化 aresetn = 0; $display("Simulation Start: Reset Active"); // 2. 保持复位 100ns #100; aresetn = 1; $display("Reset Released. Master should start counting down."); // 3. 等待足够长的时间让传输发生 // Master 设置了 START_COUNT=10,所以复位后约 10 个周期开始传输 // 总共传输 8 个数据,大概需要 20-30 个周期 #500; // 4. 结束仿真 $display("Simulation Finished"); $stop; end // ========================================================================= // 5. 监控与打印 (Monitor) // ========================================================================= // 在时钟上升沿检测握手是否成功 always @(posedge aclk) begin if (aresetn) begin // 当 VALID 和 READY 同时为高时,表示一次成功的数据传输 if (axis_tvalid && axis_tready) begin $display("[%0t ns] Transfer Occurred! Data: 0x%h | Last: %b", $time, axis_tdata, axis_tlast); end // 检测 Slave 内部是否写满了 (通过观察 Slave 是否拉低 Ready) // 注意:因为是 Testbench,我们也可以通过 hierarchy path 偷窥 Slave 内部信号 // 比如: u_slave.write_pointer end end endmodule

仿真结果的思考:

从波形上看,这是一个标准的、成功的 AXI-Stream 数据传输过程:

  • 数据量正确:发送了从 1 到 8 共 8 个数据。
  • 结束信号正确:在发送数据8​ 的时候,axis_tlast拉高了,表示包结束。
  • 握手逻辑正确:数据只有在tvalid​ 和tready同时为高的时候才发生变化(从 1 变 2,2 变 3…)。

上述仿真有点瑕疵,理论应该是这样的,存入01,02,03…08.

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/16 13:35:12

提示工程架构师的核心竞争力:掌握Agentic AI在信息安全中的应用!

提示工程架构师的核心竞争力:用Agentic AI搭建信息安全的“智能防御军团” 关键词 提示工程、Agentic AI、信息安全、智能防御、自主代理、Prompt Engineering、Cybersecurity 摘要 凌晨3点,企业安全运维人员被警报惊醒——数百条恶意流量正攻击核心服务器。他手忙脚乱登…

作者头像 李华
网站建设 2026/4/15 18:43:58

Dify知识库关联Qwen-Image-Edit-2509操作手册实现智能问答

Dify知识库关联Qwen-Image-Edit-2509操作手册实现智能问答 在电商运营的日常中&#xff0c;一个常见的场景是&#xff1a;市场团队需要在凌晨发布“双十一”促销图&#xff0c;但设计师还在休假。过去&#xff0c;这可能意味着紧急联系外包、手动修图数小时&#xff1b;而现在&…

作者头像 李华
网站建设 2026/4/16 14:02:20

微PE官网启发:轻量化系统下运行ACE-Step模型的可能性探索

微PE官网启发&#xff1a;轻量化系统下运行ACE-Step模型的可能性探索 在一台老旧笔记本上插入U盘&#xff0c;启动进入一个只有命令行界面的极简系统——这不是黑客电影的桥段&#xff0c;而是真实可能发生的AI音乐创作场景。设想这样一个画面&#xff1a;没有联网、没有独立显…

作者头像 李华
网站建设 2026/4/16 1:59:46

HashCalculator:文件哈希值批量修改与校验的专业解决方案

HashCalculator&#xff1a;文件哈希值批量修改与校验的专业解决方案 【免费下载链接】HashCalculator 一个文件哈希值批量计算器&#xff0c;支持将结果导出为文本文件功能和批量检验哈希值功能。 项目地址: https://gitcode.com/gh_mirrors/ha/HashCalculator 在现代数…

作者头像 李华
网站建设 2026/4/16 15:54:10

测试工程师的 mentoring 能力:如何培养新人?

在快速迭代的软件测试领域&#xff0c;培养新人不仅是团队发展的核心环节&#xff0c;更是保障产品质量的关键所在。优秀的测试工程师不仅需要精湛的技术能力&#xff0c;更需要成为新人的引路人和能力催化剂。 一、搭建系统化培养框架 1.1 明确能力成长路径 为新人制定清晰…

作者头像 李华
网站建设 2026/4/16 9:02:39

DesktopNaotu桌面脑图工具终极使用指南

DesktopNaotu桌面脑图工具终极使用指南 【免费下载链接】DesktopNaotu 桌面版脑图 (百度脑图离线版&#xff0c;思维导图) 跨平台支持 Windows/Linux/Mac OS. (A cross-platform multilingual Mind Map Tool) 项目地址: https://gitcode.com/gh_mirrors/de/DesktopNaotu …

作者头像 李华