该随笔是在使用XLINX自定义IP核的功能中所记录,内容包含了如何自定义IP核,以及如何在XLINX的官方代码基础上进行修改,达到符合我们需求的目的。
背景如下,自定义IP核是为了在使用block design时,将rtl代码能够用图形化的方式加入到整个block design中从而能够更方便的使用,同时为了方便PS端的调用,我们尽量使用带AXI4的接口来构成交互接口。
本随笔使用的AXI4-stream的Master接口,目的是为了让PS端从该自定义IP核中直接快速读取数据,读取的数据来自于使用jesd204b协议栈接受的来自AD9695的采样数据,但本随笔不包含jesd204b和AD9695的代码分析。
本随笔参考如下:
正点原子P15开发指南/2_【正点原子】MPSoC-P15之嵌入式Vitis开发指南_V1.0.pdf中第八章 自定义 IP 核-呼吸灯实验
如何自定义IP核,请大家参考上述的正点原子P15开发指南的第八章,链接如下:(http://www.openedv.com/docs/boards/fpga/MPSoC_P15.html)
创建好后,sources列表如下,ip_demonstration_v1_0可以视为例化文件,ip_demonstration_v1_0_M00_AXIS是源文件,主要工作都在该文件中。我们打开ip_demonstration_v1_0_M00_AXIS源文件。

复制所有代码,先交给deepseek全部分析一下主体逻辑,这里直接贴出结果,建议大家带着代码去看,代码从vivado做到这一步都能点开文件看到,这边不再贴出。




需要注意的是这几个地方,首先是传递的数据位宽,这里默认的是32位,在创建自定义IP时,该位宽是默认的,这里用32位做演示

第二个是等待计数核,这里默认是32,意味着状态机在INIT_COUNTER阶段要等待32个时钟周期后才能进入SEND_STREAM,即开始传输阶段。

第三个是总共传输的数据个数,这里默认是8,意味着传输了8个数据之后,状态机会从SEND_STREAM状态结束,回到IDLE状态,并在一个时钟之后到达INIT_COUNTER阶段

下面看一下需要更改的地方,首先是状态机结构部分,这里的parameter建议改成localparam,因为状态机状态参数属于内部参数,当然不改这里,综合时只是会有一些warrnings提示

然后看状态机部分,与deepseek所说的一致,IDLE阶段直接到INIT_COUNTER阶段,在等到“count == C_M_START_COUNT - 1”条件达成后跳到SEND_STREAM状态开始传输数据,等到“tx_done为1”时结束传递并回到IDLE状态
这里有一个地方可试着更改,也可不改,就是在IDLE阶段重新置0 count,因为原先的example design只是传输一次数据,无法多次传输,所以这里没有再IDLE阶段重新置0 count,当然这里不改也是可以的,结果就是第二次传输的时候从IDLE阶段到INIT_COUNTER阶段再到SEND_STREAM状态只用了两个周期,让INIT_COUNTER阶段无用了



根据上面的状态机,看一下其中关键信号的代码,下面是tx_done的代码,简单来讲,当“read_pointer == NUMBER_OF_OUTPUT_WORDS”条件达成后,tx_done保持1不变,又read_pointer只会在“tx_en为1”时加1,而tx_en为1的其中一个条件就是状态在SEND_STREAM状态,故tx_en信号无需我们关注。但这里面因为tx_done变回1之后,没有变回0的代码,故当前代码只支持一次数据传输,不满足多次传输的要求,这点要注意。

这里提供一个支持多次传输的代码示例,实测可用,仅供参考

最后看一下数据部分,这边需要特别注意的是stream_data_out初始值为1,且值只会在tx_en有效后更新,且第一次更新的值还是1,这会导致在tx_en第一次从0到1的时候,该周期和下一个周期产生连续两个1的值,然而exapmele
design中比较混蛋的是M_AXIS_TVALID虽然是axis_tvalid延迟了一拍得到的信号,然而“axis_tvalid = ((mst_exec_state == SEND_STREAM) && (read_pointer < NUMBER_OF_OUTPUT_WORDS))”这行代码表明了axis_tvalid 在进入NUMBER_OF_OUTPUT_WORDS状态之后基本就等于1了,这导致的结果就在于外部输入的M_AXIS_TREADY必须要先于M_AXIS_TVALID准备好,即从机要先于主机准备好。不然传输就会出错。
而这里的M_AXIS_TLAST信号也是有问题的,其同M_AXIS_TVALID类似,是axis_tlast的延后一拍信号,而 “assign axis_tlast = (read_pointer == NUMBER_OF_OUTPUT_WORDS-1) ”则表明 axis_tlast在倒数第二个数就为1了,虽然M_AXIS_TLAST延后了一拍,但这并不能掩盖,如果正好最后一个数传输的时候M_AXIS_TREADY置0了,那么最后一个数的传输就出错了



这里贴出测试代码和modelsim仿真图,众所周知,AXI4协议里,只有valid和ready同时为1的时候,数据才有效,仿真图里对应了这两个错误发生的情况,分别是数据传输错误(1、2、3、4、5、6、7、8变成了1、1、2、3、4、5、6、7)和包传输错误(talst为1的时候没采到)
点击查看代码
`timescale 1ns / 1ps
//////////////////////////////////////////////////////////////////////////////////
// Company:
// Engineer:
//
// Create Date: 2025/03/24 21:36:19
// Design Name:
// Module Name: tb_ip_demonstration
// Project Name:
// Target Devices:
// Tool Versions:
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//////////////////////////////////////////////////////////////////////////////////
module tb_ip_demonstration(
);
localparam CLK_PERIOD = 10;
wire M_AXIS_ACLK ;
wire M_AXIS_ARESETN;
reg M_AXIS_TREADY ;
reg clk;
reg rst_n;
assign M_AXIS_ACLK = clk;
assign M_AXIS_ARESETN = rst_n;
always #(CLK_PERIOD/2) clk=~clk;
initial begin
#1 rst_n<=1'bx;clk<=1'bx;M_AXIS_TREADY <= 1'b0;
#(CLK_PERIOD*3) rst_n<=1;
#(CLK_PERIOD*3) rst_n<=0;clk<=0;
repeat(5) @(posedge clk);
rst_n<=1;
#(CLK_PERIOD*100) M_AXIS_TREADY <= 1'b1;
#(CLK_PERIOD*8) M_AXIS_TREADY <= 1'b0;
#(CLK_PERIOD*8) M_AXIS_TREADY <= 1'b1;
#(CLK_PERIOD*35) M_AXIS_TREADY <= 1'b0;
#(CLK_PERIOD*8) M_AXIS_TREADY <= 1'b1;
end
wire M_AXIS_TVALID;
wire [31:0] M_AXIS_TDATA ;
wire M_AXIS_TSTRB ;
wire M_AXIS_TLAST ;
ip_demonstration#(
.C_M_AXIS_TDATA_WIDTH ( 32 ),
.C_M_START_COUNT ( 32 )
)u_ip_demonstration(
.M_AXIS_ACLK ( M_AXIS_ACLK ),
.M_AXIS_ARESETN ( M_AXIS_ARESETN ),
.M_AXIS_TVALID ( M_AXIS_TVALID ),
.M_AXIS_TDATA ( M_AXIS_TDATA ),
.M_AXIS_TSTRB ( M_AXIS_TSTRB ),
.M_AXIS_TLAST ( M_AXIS_TLAST ),
.M_AXIS_TREADY ( M_AXIS_TREADY )
);
endmodule

这两个错误是独立的,外部输入的M_AXIS_TREADY先于M_AXIS_TVALID准备好的情况下,包传输错误依然会发生,仿真图如下

唯一正确传输的情况如下,即外部输入的M_AXIS_TREADY先于M_AXIS_TVALID准备好,且数据包传输过程中M_AXIS_TREADY一直为1

浙公网安备 33010602011771号