上篇blog中记录了DDR3 AXI4接口的IP配置详情,这一文章则是记录自己在项目工程以及学习中对于DDR3的读写测试。先讲一下大概的工程架构:产生16位的自加数写进写FIFO中,当FIFO中的数达到一次突发长度后将其全部读出写进DDR3中,再检测到DDR3中数达到1024之后全部读出写入到读FIFO中,最后在顶层的读使能信号作用下将读FIFO的数全部读出,查看写入的自加数与读出的数是否符一直,符合则实验成功。
可能有的读者最开始会疑问为什么会用到两个异步FIFO,这个自己在最开始学的时候也在想不用行不行,你不用FIFO直接写入数据再读出肯定也是可以的,但是考虑到实际项目需求以及IP核封装出的用户接口,用两个FIFO既可以解决数据位宽不同步以及突发传输的问题,并且复用性更强。比如我自己的一个项目就是数据采集相关,ADC是16位,转换出的数据存入DDR3之前先写进fifo,再读出128位的数据匹配ip 核的读写数据端口。读fifo也是同理。
工程结构
axi_test_ddr_top:顶层模块,主要为产生自加数、列化子模块、时钟生成和生成vio复位调试。
ax_ddr_top:封装好的DDR使用模块,可通过修改顶层参数实现突发长度、数据位宽、地址长度等参数的修改。
axi_fifo_ctrl:将读写fifo封装进子模块,引出fifo所需要相关信号。
axi_wr:axi4接口的写模块,实现AXI4相关写时序并且将数据写进DDR3。
axi_rd:axi4接口的读模块,实现AXI4相关读时序并且将数据从DDR3读出。
mig_7series_0:例化的ip核模板。
DDR型号为MT41K256M16XX-125,单片容量4Gb(512MB)。
具体代码分析
axi_test_ddr_top:
首先我测试用的板卡自带的是200M的差分晶振时钟,为了方便测试,直接用锁相环生成clk_adc、clk_pci与clk_200m。clk_adc是产生自加数的时钟,也是写fifo的写时钟。clk_pci是读fifo的读时钟,读fifo的读使能采用的是当读fifo内可读个数达到900时拉高读使能信号将数据读出。
vio模块是为了产生复位信号(FPGA_RST_N)方便抓取信号时调试。
`timescale 1ns / 1ps
module axi_test_ddr_top
(
input clk_200m_p, //50M
input clk_200m_n, //50M
// input FPGA_RST_N,
//ddr3相关
inout [15:0] ddr3_dq ,
inout [1:0] ddr3_dqs_n ,
inout [1:0] ddr3_dqs_p ,
output [14:0] ddr3_addr , //hang地址线
output [2:0] ddr3_ba , //bank线
output ddr3_cas_n , // 列使能信号,低电平有效
output ddr3_ck_n , //差分时钟
output ddr3_ck_p , //差分时钟
output ddr3_cke , //ddr3时钟使能信号
output ddr3_ras_n , // 行使能信号,低电平有效
output ddr3_reset_n , //复位
output ddr3_we_n , //写使能
output ddr3_cs_n , //片选
output [1:0] ddr3_dm , //掩码
output ddr3_odt
);
reg [15:0] ai_fifo_data_i ;
reg ai_fifo_wr ;
wire ai_fifo_full ;
reg ai_pcie_fifo_rd ;
wire [15:0] ai_pcie_fifo_data ;
wire [12:0] ai_pcie_fifo_rd_data_count ;
wire init_calib_complete ;
wire locked ;
wire clk_adc,clk_pci;
wire clk_200m ;
vio_0 u_vio_0 (
.clk(clk_200m), // input wire clk
.probe_out0(FPGA_RST_N) // output wire [0 : 0] probe_out0
);
clk_wiz_0 clk_wiz_0_inst
(
// Clock out ports
.c0(clk_200m), // output c0 200
.c1(clk_adc), // output clk_out3
.c2(clk_pci), // output clk_out4
// Status and control signals
.locked(locked), // output locked
// Clock in ports
.clk_in1_p(clk_200m_p), // input clk_in1_p
.clk_in1_n(clk_200m_n)); // input clk_in1_n
always@ (posedge clk_pci)
begin
if (!FPGA_RST_N) begin
ai_pcie_fifo_rd <= 1'b0;
end else if(ai_pcie_fifo_rd_data_count >=13'd900) begin
ai_pcie_fifo_rd <= 1'b1;
end else begin
ai_pcie_fifo_rd <= 1'b0;
end
end
always@ (posedge clk_adc)
begin
if (!FPGA_RST_N) begin
ai_fifo_data_i <= 64'd0;
ai_fifo_wr <= 1'd0;
end else begin
if(init_calib_complete & !ai_fifo_full) begin
ai_fifo_data_i <= ai_fifo_data_i +1'b1;
ai_fifo_wr <= 1'b1;
end else begin
ai_fifo_data_i <= ai_fifo_data_i;
ai_fifo_wr <= 1'b0;
end
end
end
axi_ddr_top
#(
. DDR_WR_LEN ( 10'd128 ),
. DDR_RD_LEN ( 10'd128 ),
. DATA_WIDTH ( 128 ),
. DDR_COUNT ( 1024 ),
. DDR_ADDR_LIMIT (32'h0fff_fff0),
. DDR_CNT_LIMIT (32'h8000_0000 )
)
axi_ddr_top_inst
(
. adc_clk_i (clk_adc ),
. ddr3_clk_i (clk_200m),
. pci_clk_i (clk_pci),
. rst_n_i (FPGA_RST_N),
.ai_fifo_data_i (ai_fifo_data_i ),
.ai_fifo_wr (ai_fifo_wr ),
.ai_fifo_full (ai_fifo_full ),
.ai_pcie_fifo_rd (ai_pcie_fifo_rd ),
.ai_pcie_fifo_data (ai_pcie_fifo_data ),
.ai_pcie_fifo_rd_data_count (ai_pcie_fifo_rd_data_count ),
.init_calib_complete (init_calib_complete ),
// Output Ports - Single Bit
.ddr3_cas_n (ddr3_cas_n),
.ddr3_ck_n (ddr3_ck_n),
.ddr3_ck_p (ddr3_ck_p),
.ddr3_cke (ddr3_cke),
.ddr3_cs_n (ddr3_cs_n),
.ddr3_odt (ddr3_odt),
.ddr3_ras_n (ddr3_ras_n),
.ddr3_reset_n (ddr3_reset_n),
.ddr3_we_n (ddr3_we_n),
// Output Ports - Busses
.ddr3_addr (ddr3_addr ),
.ddr3_ba (ddr3_ba),
.ddr3_dm (ddr3_dm),
// InOut Ports - Single Bit
// InOut Ports - Busses
.ddr3_dq (ddr3_dq ),
.ddr3_dqs_n (ddr3_dqs_n ),
.ddr3_dqs_p (ddr3_dqs_p )
);
endmodule
axi_ddr_top:
此模块主要例化三个子模块,包括fifo控制模块、axi读与写模块以及ip核例化模板。
为了方便调试时控制IP核的工作,例化一个vio控制IP核的复位,方便在抓取信号时,先对IP核复位再产生数据写入fifo中。
总体顶层信号需要自己产生的就是自加数、写fifo的写使能与读fifo的读使能信号,其余相关联信号在fifo控制模块中引出。具体的可查看代码。
`timescale 1ns / 1ps
module axi_ddr_top #
(
parameter DDR_WR_LEN = 8'd128, //写突发长度 最大128个128bit
parameter DDR_RD_LEN = 8'd128, //写突发长度 最大128个128bit
parameter DATA_WIDTH = 128, //读写数据位宽
parameter DDR_COUNT = 1024, //DDR内部存在数据个数
parameter DDR_ADDR_LIMIT = 32'h0fff_ffff , //2^10*2^15*2^3=2^28=268,435,456=32'h0fff_ffff
parameter DDR_CNT_LIMIT = 32'h0fff_ffff // 4*1024^3bit / 16bit = 268,435,456 = 32'h0fff_ffff
)
(
//50m的时钟与复位信号
input ddr3_clk_i, //200m
input adc_clk_i, //64M
input pci_clk_i, //
input rst_n_i , //外部复位
inout [15:0] ddr3_dq , //数据线
inout [1:0] ddr3_dqs_n , //数据选取脉冲差分信号
inout [1:0] ddr3_dqs_p , //数据选取脉冲差分信号
output [14:0] ddr3_addr , //地址线
output [2:0] ddr3_ba , //bank线
output ddr3_ras_n , //行使能信号,低电平有效
output ddr3_cas_n , //列使能信号,低电平有效
output ddr3_we_n , //写使能信号,低电平有效
output ddr3_reset_n , //ddr3复位
output [0:0] ddr3_ck_p , //ddr3差分时钟
output [0:0] ddr3_ck_n , //ddr3差分时钟
output [0:0] ddr3_cke , //ddr3时钟使能信号
output [0:0] ddr3_cs_n , //ddr3片选信号
output [1:0] ddr3_dm , //ddr3掩码
output [0:0] ddr3_odt , //odt阻抗
input [15:0] ai_fifo_data_i,
input ai_fifo_wr ,
output ai_fifo_full ,
input ai_pcie_fifo_rd ,
output [15:0] ai_pcie_fifo_data,
output [12:0] ai_pcie_fifo_rd_data_count,
output init_calib_complete
);
//wire define
//axi写通道写地址
wire [3:0] M_AXI_WR_awid; //写地址ID,用来标志一组写信号
wire [28:0]M_AXI_WR_awaddr; //写地址,给出一次写突发传输的写地址
wire [7:0] M_AXI_WR_awlen; //突发长度,给出突发传输的次数
wire [2:0] M_AXI_WR_awsize; //突发大小,给出每次突发传输的字节数
wire [1:0] M_AXI_WR_awburst;//突发类型
wire [0:0] M_AXI_WR_awlock; //总线锁信号,可提供操作的原子性
wire [3:0] M_AXI_WR_awcache;//内存类型,表明一次传输是怎样通过系统的
wire [2:0] M_AXI_WR_awprot; //保护类型,表明一次传输的特权级及安全等级
wire [3:0] M_AXI_WR_awqos; //质量服务QoS
wire M_AXI_WR_awvalid;//有效信号,表明此通道的地址控制信号有效
wire M_AXI_WR_awready;//表明“从”可以接收地址和对应的控制信号
//axi写通道读数据
wire [DATA_WIDTH-1 :0]M_AXI_WR_wdata; //写数据
wire [DATA_WIDTH/8-1 :0] M_AXI_WR_wstrb; //写数据有效的字节线
//用来表明哪8bits数据是有效的
wire M_AXI_WR_wlast; //表明此次传输是最后一个突发传输
wire M_AXI_WR_wvalid; //写有效,表明此次写有效
wire M_AXI_WR_wready; //表明从机可以接收写数据
//axi写通道读应答
wire [3:0] M_AXI_WR_bid ; //写响应ID TAG
wire [1:0] M_AXI_WR_bresp ; //写响应,表明写传输的状态
wire M_AXI_WR_bvalid ; //写响应有效
wire M_AXI_WR_bready ; //表明主机能够接收写响应
//axi读通道写地址
wire [3:0] M_AXI_RD_arid; //读地址ID,用来标志一组写信号
wire [28:0]M_AXI_RD_araddr; //读地址,给出一次写突发传输的读地址
wire [7:0] M_AXI_RD_arlen; //突发长度,给出突发传输的次数
wire [2:0] M_AXI_RD_arsize; //突发大小,给出每次突发传输的字节数
wire [1:0] M_AXI_RD_arburst;//突发类型
wire [0:0] M_AXI_RD_arlock; //总线锁信号,可提供操作的原子性
wire [3:0] M_AXI_RD_arcache;//内存类型,表明一次传输是怎样通过系统的
wire [2:0] M_AXI_RD_arprot; //保护类型,表明一次传输的特权级及安全等级
wire [3:0] M_AXI_RD_arqos; //质量服务QOS
wire M_AXI_RD_arvalid;//有效信号,表明此通道的地址控制信号有效
wire M_AXI_RD_arready;//表明“从”可以接收地址和对应的控制信号
//axi读通道读数据
wire [3:0] M_AXI_RD_rid; //读ID tag
wire [DATA_WIDTH-1:0] M_AXI_RD_rdata; //读数据
wire [1:0] M_AXI_RD_rresp; //读响应,表明读传输的状态
wire M_AXI_RD_rlast; //表明读突发的最后一次传输
wire M_AXI_RD_rvalid; //表明此通道信号有效
wire M_AXI_RD_rready; //表明主机能够接收读数据和响应信息
//
wire ui_clk ; //800 /4 = 200m
wire ui_rst ;
wire ddr3_rst_n;
wire locked ;
wire ai_fifo_rd ;
wire ai_fifo_prog_full;
wire [127:0]wr_fifo_data;
wire [127:0]rd_fifo_data;
wire [31:0] ddr_count ;
wire ddr_prog_full ;
wire ddr_cnt_full ;
wire [12:0]rd_data_count;
wire ai_pcie_fifo_prom_full;
wire [9:0] wr_fifo_rd_data_count;
/* ila_1 u_ila_1 (
.clk(ddr3_clk_i), // input wire clk
.probe0({ M_AXI_WR_awid, M_AXI_WR_wdata , M_AXI_WR_bid , M_AXI_RD_arid , M_AXI_RD_rid ,
M_AXI_WR_awaddr , M_AXI_WR_wstrb , M_AXI_WR_bresp, M_AXI_RD_araddr , M_AXI_RD_rdata ,
M_AXI_WR_awlen , M_AXI_WR_bvalid, M_AXI_RD_arlen , M_AXI_RD_rresp ,
M_AXI_WR_awsize , M_AXI_WR_wlast , M_AXI_WR_bready, M_AXI_RD_arsize , M_AXI_RD_rlast ,
M_AXI_WR_awburst , M_AXI_WR_wvalid, M_AXI_RD_arburst , M_AXI_RD_rvalid,
M_AXI_WR_awlock , M_AXI_WR_wready, M_AXI_RD_arlock , M_AXI_RD_rready,
M_AXI_WR_awcache , ai_fifo_data_i, M_AXI_RD_arcache ,
M_AXI_WR_awprot , ai_fifo_wr , M_AXI_RD_arprot ,
M_AXI_WR_awqos , ai_fifo_full , M_AXI_RD_arqos ,
M_AXI_WR_awvalid , init_calib_complete, M_AXI_RD_arvalid ,
M_AXI_WR_awready , M_AXI_RD_arready }
) // input wire [511:0] probe0
);
vio_1 u_vio_1 (
.clk(ddr3_clk_i), // input wire clk
.probe_out0(ddr3_rst_n) // output wire [0 : 0] probe_out0
);
axi_fifo_ctrl#
(
.DDR_COUNT (DDR_COUNT ),//DDR内部存在数据个数
.DDR_CNT_LIMIT (DDR_CNT_LIMIT) // 1024^3 *8 bit / 16bit = 536870912 = 32'h2000_0000
)(
. ddr3_clk_i (ddr3_clk_i ) , //200m
. adc_clk_i (adc_clk_i ) , //64M
. pci_clk_i (pci_clk_i ) , //
. rst_n_i ( rst_n_i ) , //外部复位
. ai_fifo_data_i (ai_fifo_data_i ) ,
. ai_fifo_rd (ai_fifo_rd ) ,
. ai_fifo_wr (ai_fifo_wr ) ,
. ai_fifo_full (ai_fifo_full ) ,
. wr_fifo_data (wr_fifo_data ) ,
. M_AXI_RD_rvalid (M_AXI_RD_rvalid ) ,
. rd_fifo_data (rd_fifo_data ) ,
. rd_fifo_we (rd_fifo_we ) ,
. ai_pcie_fifo_rd (ai_pcie_fifo_rd ) ,
. ai_pcie_fifo_data (ai_pcie_fifo_data ) ,
. ai_pcie_fifo_rd_data_count (ai_pcie_fifo_rd_data_count ) ,
. ai_pcie_fifo_prom_full (ai_pcie_fifo_prom_full ) ,
. wr_fifo_rd_data_count (wr_fifo_rd_data_count ) ,
. ddr_count (ddr_count ) ,
. ddr_prog_full (ddr_prog_full ) ,
. ddr_cnt_full (ddr_cnt_full )
);
axi_wr
#(
.DATA_WIDTH(DATA_WIDTH),
.DDR_ADDR_LIMIT(DDR_ADDR_LIMIT)
)
u_axi_wr
(
.ARESETN (rst_n_i ), //axi复位
.ACLK (ui_clk ), //axi总时钟
.M_AXI_AWID (M_AXI_WR_awid ), //写地址ID
.M_AXI_AWADDR (M_AXI_WR_awaddr ), //写地址
.M_AXI_AWLEN (M_AXI_WR_awlen ), //突发长度
.M_AXI_AWSIZE (M_AXI_WR_awsize ), //突发大小
.M_AXI_AWBURST(M_AXI_WR_awburst), //突发类型
.M_AXI_AWLOCK (M_AXI_WR_awlock ), //总线锁信号
.M_AXI_AWCACHE(M_AXI_WR_awcache), //内存类型
.M_AXI_AWPROT (M_AXI_WR_awprot ), //保护类型
.M_AXI_AWQOS (M_AXI_WR_awqos ), //质量服务QoS
.M_AXI_AWVALID(M_AXI_WR_awvalid), //有效信号
.M_AXI_AWREADY(M_AXI_WR_awready), //握手信号awready
.M_AXI_WDATA (M_AXI_WR_wdata ), //写数据
.M_AXI_WSTRB (M_AXI_WR_wstrb ), //写数据有效的字节线
.M_AXI_WLAST (M_AXI_WR_wlast ), //表明此次传输是最后一个突发传输
.M_AXI_WVALID(M_AXI_WR_wvalid ), //写有效
.M_AXI_WREADY(M_AXI_WR_wready ), //表明从机可以接收写数据
.M_AXI_BID (M_AXI_WR_bid ), //写响应ID TAG
.M_AXI_BRESP (M_AXI_WR_bresp ), //写响应
.M_AXI_BVALID(M_AXI_WR_bvalid ), //写响应有效
.M_AXI_BREADY(M_AXI_WR_bready ), //表明主机能够接收写响应
.wr_len (DDR_WR_LEN ),
.wr_fifo_rd_data_count( wr_fifo_rd_data_count),
.wr_fifo_data (wr_fifo_data ),
// .ai_fifo_wr (wr_fifo_re ),
.ai_fifo_prog_full(ai_fifo_prog_full),
.ddr_cnt_full (ddr_cnt_full ),
.ai_fifo_rd (ai_fifo_rd ),
.wr_done ( )
);
axi_rd
#(
.DATA_WIDTH(DATA_WIDTH),
.DDR_ADDR_LIMIT(DDR_ADDR_LIMIT)
)
u_axi_rd
(
. ARESETN (rst_n_i),
. ACLK (ui_clk),
. M_AXI_ARID (M_AXI_RD_arid ), //读地址ID
. M_AXI_ARADDR (M_AXI_RD_araddr ), //读地址
. M_AXI_ARLEN (M_AXI_RD_arlen ), //突发长度
. M_AXI_ARSIZE (M_AXI_RD_arsize ), //突发大小
. M_AXI_ARBURST(M_AXI_RD_arburst), //突发类型
. M_AXI_ARLOCK (M_AXI_RD_arlock ), //总线锁信号
. M_AXI_ARCACHE(M_AXI_RD_arcache), //内存类型
. M_AXI_ARPROT (M_AXI_RD_arprot ), //保护类型
. M_AXI_ARQOS (M_AXI_RD_arqos ), //质量服务QOS
. M_AXI_ARVALID(M_AXI_RD_arvalid), //有效信号
. M_AXI_ARREADY(M_AXI_RD_arready), //握手信号arready
. M_AXI_RID (M_AXI_RD_rid ), //读ID tag
. M_AXI_RDATA (M_AXI_RD_rdata ), //读数据
. M_AXI_RRESP (M_AXI_RD_rresp ), //读响应,表明读传输的状态
. M_AXI_RLAST (M_AXI_RD_rlast ), //表明读突发的最后一次传输
. M_AXI_RVALID(M_AXI_RD_rvalid), //表明此通道信号有效
. M_AXI_RREADY(M_AXI_RD_rready), //表明主机能够接收读数据和响应信息
// . rd_start ( ), //读突发触发信号
. rd_len (DDR_RD_LEN), //长度
// . rd_addr ( ), //地址
. rd_fifo_we (rd_fifo_we ), //连接到读fifo的写使能 O
. rd_fifo_data (rd_fifo_data ), //连接到读fifo的写数据
. rd_done ( ), //完成一次突发
. ddr_prog_full(ddr_prog_full ),
. ddr_count(ddr_count),
. ai_pcie_fifo_prom_full(ai_pcie_fifo_prom_full)
);
mig_7series_0 U_mig_7series_0
(
.ddr3_dq (ddr3_dq ), //数据线
.ddr3_dqs_n(ddr3_dqs_n), //数据选取脉冲差分信号
.ddr3_dqs_p(ddr3_dqs_p), //数据选取脉冲差分信号
.ddr3_addr (ddr3_addr ), //地址线
.ddr3_ba (ddr3_ba ), //bank线
.ddr3_ras_n(ddr3_ras_n), //行使能信号,低电平有效
.ddr3_cas_n(ddr3_cas_n), //列使能信号,低电平有效
.ddr3_we_n (ddr3_we_n ), //写使能信号,低电平有效
.ddr3_reset_n(ddr3_reset_n), //ddr3复位
.ddr3_ck_p (ddr3_ck_p ), //ddr3差分时钟
.ddr3_ck_n (ddr3_ck_n ), //ddr3差分时钟
.ddr3_cke (ddr3_cke ), //ddr3时钟使能信号
.ddr3_cs_n (ddr3_cs_n ), //ddr3片选信号
.ddr3_dm (ddr3_dm ), //ddr3掩码
.ddr3_odt (ddr3_odt ), //odt阻抗
.sys_clk_i (ddr3_clk_i), //ip核时钟200M
.sys_rst (ddr3_rst_n), //ip核复位
.ui_clk (ui_clk), //用户端口时钟200M
.ui_clk_sync_rst (ui_rst), //复位
.mmcm_locked ( ),
.aresetn (ddr3_rst_n), //异步复位
.app_sr_req ('b0),
.app_ref_req ('b0),
.app_zq_req ('b0),
.app_sr_active (),
.app_ref_ack (),
.app_zq_ack (),
//axi写通道地址与控制信号
.s_axi_awid (M_AXI_WR_awid ), //写地址ID
.s_axi_awaddr (M_AXI_WR_awaddr ), //写地址
.s_axi_awlen (M_AXI_WR_awlen ), //突发长度
.s_axi_awsize (M_AXI_WR_awsize ), //突发大小
.s_axi_awburst (M_AXI_WR_awburst), //突发类型
.s_axi_awlock (M_AXI_WR_awlock ), //总线锁信号
.s_axi_awcache (M_AXI_WR_awcache), //内存类型
.s_axi_awprot (M_AXI_WR_awprot ), //保护类型
.s_axi_awqos (M_AXI_WR_awqos ), //质量服务QoS
.s_axi_awvalid (M_AXI_WR_awvalid), //有效信号
.s_axi_awready (M_AXI_WR_awready), //握手信号awready
//axi写通道数据
.s_axi_wdata (M_AXI_WR_wdata ), //写数据
.s_axi_wstrb (M_AXI_WR_wstrb ), //写数据有效的字节线
.s_axi_wlast (M_AXI_WR_wlast ), //表明此次传输是最后一个突发传输
.s_axi_wvalid (M_AXI_WR_wvalid ), //写有效,表明此次写有效
.s_axi_wready (M_AXI_WR_wready ), //表明从机可以接收写数据
//axi写通道应答
.s_axi_bid (M_AXI_WR_bid ), //写响应ID TAG
.s_axi_bresp (M_AXI_WR_bresp ), //写响应,表明写传输的状态
.s_axi_bvalid (M_AXI_WR_bvalid ), //写响应有效
.s_axi_bready (M_AXI_WR_bready ), //表明主机能够接收写响应
//axi读通道地址与控制信号
.s_axi_arid (M_AXI_RD_arid ), //读地址ID
.s_axi_araddr (M_AXI_RD_araddr ), //读地址
.s_axi_arlen (M_AXI_RD_arlen ), //突发长度
.s_axi_arsize (M_AXI_RD_arsize ), //突发大小
.s_axi_arburst (M_AXI_RD_arburst), //突发类型
.s_axi_arlock (M_AXI_RD_arlock ), //总线锁信号
.s_axi_arcache (M_AXI_RD_arcache), //内存类型
.s_axi_arprot (M_AXI_RD_arprot ), //保护类型
.s_axi_arqos (M_AXI_RD_arqos ), //质量服务QOS
.s_axi_arvalid (M_AXI_RD_arvalid), //有效信号
.s_axi_arready (M_AXI_RD_arready), //握手信号arready
//axi读通道数据,包括应答
.s_axi_rid (M_AXI_RD_rid ), //读ID tag
.s_axi_rdata (M_AXI_RD_rdata ), //读数据
.s_axi_rresp (M_AXI_RD_rresp ), //读响应,表明读传输的状态
.s_axi_rlast (M_AXI_RD_rlast ), //表明读突发的最后一次传输
.s_axi_rvalid (M_AXI_RD_rvalid ), //表明此通道信号有效
.s_axi_rready (M_AXI_RD_rready ), //表明主机能够接收读数据
.init_calib_complete(init_calib_complete) //ip核初始化完成
);
endmodule
axi_fifo_ctrl:
这里比较重要的是代码30-58行代码,这里需要引出三个信号:ddr_count、ddr_prog_full、ddr_cnt_full。分别为ddr内部存在的数据个数、超过1024的满标志信号、超出ddr内部最大极限个数的标志信号。
顺便提一下38、40行代码:在写fifo读使能有效之后ddr_count加8与DDR读出有效后减8的原因:前文提到的AXI4总线的位宽选择的是128 ,在每次axi握手成功后写入ddr内部的数据个数应该是(128/16=8),读出也是同理。
`timescale 1ns / 1ps
module axi_fifo_ctrl#
( parameter DATA_WIDTH = 128,
parameter DDR_COUNT = 1024, //预设DDR存1024个数
parameter DDR_CNT_LIMIT = 32'h0fff_ffff // 4*1024^3bit / 16bit = 268,435,456 = 32'h0fff_ffff
)(
input ddr3_clk_i, //200m
input adc_clk_i, //64M
input pci_clk_i, //
input rst_n_i , //外部复位
input [15:0] ai_fifo_data_i,
input ai_fifo_rd ,
input ai_fifo_wr ,
output ai_fifo_full ,
output [DATA_WIDTH-1 :0 ] wr_fifo_data ,
input M_AXI_RD_rvalid,
input [DATA_WIDTH-1 :0 ] rd_fifo_data,
input rd_fifo_we,
input ai_pcie_fifo_rd ,
output [15:0] ai_pcie_fifo_data,
output [12:0] ai_pcie_fifo_rd_data_count,
output ai_pcie_fifo_prom_full,
output [9:0] wr_fifo_rd_data_count,
output reg [31:0] ddr_count , //DDR内部存在的数据个数
output reg ddr_prog_full ,
output reg ddr_cnt_full
);
always@ (posedge ddr3_clk_i or negedge rst_n_i)
begin
if (!rst_n_i) begin
ddr_count <= 32'd0;
ddr_prog_full <= 1'b0;
ddr_cnt_full <= 1'b0;
end else begin
if(ai_fifo_rd)begin //fifo开始读出数据到ddr3
ddr_count <= ddr_count + 32'd8;
end else if(M_AXI_RD_rvalid) begin //DDR读出有效 && fifo非满 时ddr内的数据被读出
ddr_count <= ddr_count - 32'd8;
end else begin
ddr_count <= ddr_count;
end
if(ddr_count >= DDR_COUNT)begin // 超过1024满标志信号
ddr_prog_full <= 1'b1;
end else begin
ddr_prog_full <= 1'b0;
end
if(ddr_count > DDR_CNT_LIMIT)begin // 超过 ddr_cnt_limit 满标志信号
ddr_cnt_full <= 1'b1;
end else begin
ddr_cnt_full <= 1'b0;
end
end
end
data_2_ddr3_16x64_8192 adc_2_ddr3_16x64_8192 (
.rst (!rst_n_i), // input wire rst
.wr_clk(adc_clk_i), // input wire wr_clk
.rd_clk(ddr3_clk_i ), // input wire rd_clk
.din (ai_fifo_data_i), // input wire [15 : 0] din
.wr_en(ai_fifo_wr), // input wire wr_en
.rd_en(ai_fifo_rd), // input wire rd_en
.dout(wr_fifo_data), // output wire [127 : 0] dout 等于app_wdf_data
.almost_full (ai_fifo_full), // output wire almost_full
.full ( ), // output wire full
.empty(ai_fifo_empty), // output wire empty
.rd_data_count(wr_fifo_rd_data_count), // output wire [9 : 0] rd_data_count
.prog_full(ai_fifo_prog_full) // output wire prog_full
);
ddr3_2_pci_64x32_2048 ddr3_2_pci_64x16_8192 (
.rst (!rst_n_i), // input wire rst
.wr_clk(ddr3_clk_i), // input wire wr_clk
.rd_clk(pci_clk_i), // input wire rd_clk
.din (rd_fifo_data), // input wire [127 : 0] din
.wr_en (rd_fifo_we), // input wire wr_en
.rd_en (ai_pcie_fifo_rd), // input wire rd_en
.dout (ai_pcie_fifo_data), // output wire [15: 0] dout
.full (), // output wire full
.almost_full( ), // output wire almost_full
.empty( ), // output wire empty
.rd_data_count(ai_pcie_fifo_rd_data_count), // output wire [12 : 0] rd_data_count
.prog_full(ai_pcie_fifo_prom_full) // output wire prog_full
);
endmodule
axi_wr:
此为axi4的写时序模块,主要实现axi4的写时序。 主要将IP核输出的axi4写通道地址通道、写通道数据通道与写通道应答通道对应输入输出。
其中写突发触发信号设计的是当写fifo内部可读数据大于一次突发长度进入写地址等待状态((wr_fifo_rd_data_count+10'd2)>=wr_len );这里我在开始测试时写条件是当写fifo将满且ddr内部没满时进入写地址等待状态,但是由于进入到写数据循环状态还有三个状态,在这期间写fifo还在写入数据,这样最后读出的数据就不对。所以改为((wr_fifo_rd_data_count+10'd2)>=wr_len;感兴趣的读者可以试一下把将满信号(ai_fifo_prog_full)设置的小一点,只要保证fifo没有溢出数据,再接收到axi握手成功信号(ai_fifo_rd = M_AXI_WREADY & reg_wvalid) 后才能将数据正确读出并通过AXI4总线写入到DDR中。
代码注释较为详尽,状态机部分主要还是根据AXI4的读写时序图设计,具体可参考下图:主要关注AWADDR信号、ARVALID+ARREADY这一组握手信号;在每一次突发传输之前,先判断ARREADY信号,当该信号为高时,代表从机已经准备好接收新的地址信息,否则主机就不能发送地址信息给从机。
发送写数据时候主机必须在WREADY为高的时候,将WVALID信号拉高,同时将数据发送给从机,当主机发送最后一个数据时,将WLAST信号拉高一个周期,告诉从机这是最后一个数据,当从机收到WLAST信号为高时,就会将收到的数据按照指定的地址写入DDR。
当主机发送完写数据后的一个时钟周期,IP核会输出BVALID信号表示写响应有效,逻辑部分需要输出BREADY信号表明能够接收写响应。在实际代码中我直接将M_AXI_BREADY = M_AXI_BVALID,此时只需观察 Bresp信号有没有拉高。
时序部分主要是状态机控制,控制ARVALID信号的输出以及声明的一些寄存器变量,具体可查看代码注释。
AXI4的相关时序有空再写一下,网站上也有许多优秀的博主写了blog都可以参考。
突然想到还有一个关键的点没有说道:在对写地址的确定中为什么是加16(即wr_addr_reg <= wr_addr_reg + 16);首先要明确的一个点是AXI4总线只需要一个地址,最高可以突发传输256个字的数据。 每一个地址对于一个字节的数据,128位的数据位宽对于16字节,所以一组数据写入完成之后的地址增量=突发长度x16。
`timescale 1ns / 1ps
module axi_wr
#(
parameter DATA_WIDTH =128,
parameter DDR_ADDR_LIMIT = 32'h0fff_ffff //2^10*2^15*2^3=2^28=268,435,456=32'h0fff_ffff
)
(
input ARESETN , //axi复位
input ACLK , //axi总时钟
//axi4写通道地址通道
output [3:0] M_AXI_AWID , //写地址ID,用来标志一组写信号
output [28:0] M_AXI_AWADDR , //写地址,给出一次写突发传输的写地址
output [7:0] M_AXI_AWLEN , //突发长度,给出突发传输的次数
output [2:0] M_AXI_AWSIZE , //突发大小,给出每次突发传输的字节数
output [1:0] M_AXI_AWBURST, //突发类型
output M_AXI_AWLOCK , //总线锁信号,可提供操作的原子性
output [3:0] M_AXI_AWCACHE, //内存类型,表明一次传输是怎样通过系统的
output [2:0] M_AXI_AWPROT , //保护类型,表明一次传输的特权级及安全等级
output [3:0] M_AXI_AWQOS , //质量服务QoS
output M_AXI_AWVALID, //有效信号,表明此通道的地址控制信号有效
input M_AXI_AWREADY, //表明“从”可以接收地址和对应的控制信号
//axi4写通道数据通道
output [DATA_WIDTH-1 :0] M_AXI_WDATA , //写数据
output [DATA_WIDTH/8-1:0] M_AXI_WSTRB , //写数据有效的字节线
output M_AXI_WLAST , //表明此次传输是最后一个突发传输
output M_AXI_WVALID , //写有效,表明此次写有效
input M_AXI_WREADY , //表明从机可以接收写数据
//axi4写通道应答通道
input [3:0] M_AXI_BID , //写响应ID TAG
input [1:0] M_AXI_BRESP , //写响应,表明写传输的状态
input M_AXI_BVALID , //写响应有效
output M_AXI_BREADY , //表明主机能够接收写响应
//用户端信号
input [9:0] wr_fifo_rd_data_count, //写突发触发信号
input [7:0] wr_len , //写突发长度
input [DATA_WIDTH-1:0] wr_fifo_data , //连接到写fifo的读数据
input ai_fifo_prog_full,
input ddr_cnt_full ,
output ai_fifo_rd //连接到写fifo的读使能
);
localparam S_WR_IDLE = 3'd0;//写空闲
localparam S_WA_WAIT = 3'd1;//写地址等待
localparam S_WA_START = 3'd2;//写地址
localparam S_WD_WAIT = 3'd3;//写数据等待
localparam S_WD_PROC = 3'd4;//写数据循环
localparam S_WR_WAIT = 3'd5;//接受写应答
localparam S_WR_DONE = 3'd6;//写结束
localparam DATA_NUM = (DATA_WIDTH == 512) ? 6 :
(DATA_WIDTH == 256) ? 5 :
(DATA_WIDTH == 128) ? 4 :
(DATA_WIDTH == 64) ? 3 : 2;
//reg define
reg [2:0] wr_state ; //状态寄存器
reg [28:0] reg_wr_adrs; //写地址寄存器
reg reg_awvalid; //地址有效握手信号
reg reg_wvalid ; //数据有效握手信号
reg reg_w_last ; //传输最后一个数据
reg [7:0] reg_w_len ; //突发长度128
reg [31:0] wr_addr_cnt; //写地址计数器
reg [31:0] wr_data_cnt; //写数据计数器
reg [31:0] wr_addr_reg; //写地址寄存器
//写fifo的读使能为axi握手成功 == M_AXI_WREADY + M_AXI_WVALID
assign ai_fifo_rd = M_AXI_WREADY & reg_wvalid
//只有一个主机,可随意设置
assign M_AXI_AWID = 4'b1111;
//把地址赋予总线
assign M_AXI_AWADDR = reg_wr_adrs ;
//一次突发传输1长度 (0 至 wr_len-1'd1)
assign M_AXI_AWLEN[7:0] = wr_len-'d1;
//表示AXI总线每个数据宽度是16字节,128位。 2^(4)=16byte
assign M_AXI_AWSIZE = DATA_NUM;
//01代表地址递增,10代表递减
assign M_AXI_AWBURST[1:0] = 2'b01; //突发类型递增,增量突发,后续数据的地址在初始地址的基础上进行递增,递增幅度与传输宽度相同。
assign M_AXI_AWLOCK = 1'b0;
assign M_AXI_AWCACHE[3:0] = 4'b0010;
assign M_AXI_AWPROT[2:0] = 3'b000;
assign M_AXI_AWQOS[3:0] = 4'b0000;
//地址握手信号AWVALID
assign M_AXI_AWVALID = reg_awvalid;
//写完成信号的写状态完成
assign wr_done = (wr_state == S_WR_DONE);
//fifo数据赋予总线
assign M_AXI_WDATA = wr_fifo_data;
assign M_AXI_WSTRB = 16'hFFFF; //写数据全部有效
//写到最后一个数据
//assign M_AXI_WLAST =(wr_addr_cnt == reg_w_len)? 'b1:'b0;
assign M_AXI_WLAST =reg_w_last;
//数据握手信号WVALID
assign M_AXI_WVALID = reg_wvalid;
//这个信号是告诉AXI我收到你的应答
assign M_AXI_BREADY = M_AXI_BVALID;
//axi写过程状态机
always @(posedge ACLK ) begin
if(!ARESETN) begin
wr_state <= S_WR_IDLE;
reg_wr_adrs <= 29'd0;
reg_awvalid <= 1'b0; //地址
reg_wvalid <= 1'b0; //数据
reg_w_last <= 1'b0;
reg_w_len <= 8'd0;
wr_addr_cnt <= 32'd0;
wr_data_cnt <= 32'd0;
wr_addr_reg <= 32'd0;
end else begin
case(wr_state)
S_WR_IDLE: begin //写空闲
// if(ai_fifo_prog_full & !ddr_cnt_full) begin //写fifo将满且ddr内部没满时进入写地址等待状态
if((wr_fifo_rd_data_count+10'd2)>=wr_len )begin//fifo数据长度大于一次突发长度
wr_state <= S_WA_WAIT;
reg_wr_adrs <= wr_addr_reg ;
end
reg_awvalid <= 1'b0;
reg_wvalid <= 1'b0;
reg_w_len <= 8'd0;
end
S_WA_WAIT :begin
wr_state <= S_WA_START;
end
S_WA_START :begin //写地址开始拉高地址有效与数据有效
wr_state <= S_WD_WAIT;//写数据等待一个时钟周期
reg_awvalid <= 1'b1; //拉高地址有效信号
end
S_WD_WAIT :begin
reg_wvalid <= 1'b1;//拉高数据有效信号
if(M_AXI_AWREADY) //等待AXI写地址就绪信号将对应地址发给从机
wr_state <= S_WD_PROC;
reg_awvalid <= 1'b0; //ready有效后拉低valid信号
reg_w_len <= wr_len-'d1; //127代表128个长度,0代表1个长度
end
S_WD_PROC :begin
if(M_AXI_WREADY)begin //等待写数据就绪
if(wr_addr_cnt == reg_w_len)begin //突发写入完成
reg_wvalid <= 1'b0; //写入完成后拉低数据有效信号;此信号告诉AXI总线我正在写数据有效
wr_state <= S_WR_WAIT;
// reg_w_last <= 1'b1; //表示传输最后一个数据
end else begin
wr_addr_cnt <= wr_addr_cnt + 1'b1;
end
if(wr_addr_cnt == reg_w_len - 1)begin //突发写入完成
reg_w_last <= 1'b1; //传输到最后一个数据时对应拉高
end else begin
reg_w_last <= 1'b0;
end
if(wr_addr_reg == DDR_ADDR_LIMIT)begin
// reg_wr_adrs <= 29'd0;
wr_addr_reg <= 32'd0;
end else begin
wr_addr_reg <= wr_addr_reg + 16;//一组数据写入完成之后的地址增量=突发长度x16,128位等于16字节
//reg_wr_adrs <= reg_wr_adrs + 8;
end
end else begin
//reg_wr_adrs <= reg_wr_adrs;
wr_addr_reg <= wr_addr_reg;
wr_addr_cnt <= wr_addr_cnt;
end
if(ai_fifo_rd)begin
wr_data_cnt <= wr_data_cnt +1'b1;
end else begin
wr_data_cnt <= wr_data_cnt ;
end
end
S_WR_WAIT :begin
reg_w_last<='b0; //只持续一个时钟周期
//M_AXI_BVALID拉高表示写成功,然后状态机完成一次突发传输
if(M_AXI_BVALID) begin
wr_state <= S_WR_DONE;
end
end
S_WR_DONE :begin
wr_state <= S_WR_IDLE;
wr_addr_cnt<=32'd0;
wr_data_cnt<=32'd0;
end
default: begin
wr_state<= S_WR_IDLE;
end
endcase
end
end
endmodule
axi_rd:
此模块主要也是实现AXI4接口的读时序,先上时序图:
读时序相对简单,重点也是关注读地址ARADDR与握手信号ARVALID+ARREADY。简单说一下就是当地址通道上ARVALID 和ARREADY 同时为高时,地址 A 被有效的传给设备,之后设备输出的数据将出现在读数据通道上。当RREADY 和 RVALID 同时为高的时候表明有效的数据传输。RLAST 信号表示最后一个被传输的数据。
`timescale 1ns / 1ps
module axi_rd
#(
parameter DATA_WIDTH =128,
parameter DDR_ADDR_LIMIT = 32'h0fff_ffff
)
(
input ARESETN , //axi复位
input ACLK , //axi总时钟
//axi4读通道地址通道
output [3:0] M_AXI_ARID , //读地址ID,用来标志一组写信号
output [28:0] M_AXI_ARADDR , //读地址,给出一次写突发传输的读地址
output [7:0] M_AXI_ARLEN , //突发长度,给出突发传输的次数
output [2:0] M_AXI_ARSIZE , //突发大小,给出每次突发传输的字节数
output [1:0] M_AXI_ARBURST, //突发类型
output M_AXI_ARLOCK , //总线锁信号,可提供操作的原子性
output [3:0] M_AXI_ARCACHE, //内存类型,表明一次传输是怎样通过系统的
output [2:0] M_AXI_ARPROT , //保护类型,表明一次传输的特权级及安全等级
output [3:0] M_AXI_ARQOS , //质量服务QOS
output M_AXI_ARVALID, //有效信号,表明此通道的地址控制信号有效
input M_AXI_ARREADY, //表明“从”可以接收地址和对应的控制信号
//axi读通道读数据
input [3:0] M_AXI_RID , //读ID tag
input [DATA_WIDTH-1:0] M_AXI_RDATA , //读数据
input [1:0] M_AXI_RRESP , //读响应,表明读传输的状态
input M_AXI_RLAST , //表明读突发的最后一次传输
input M_AXI_RVALID , //表明此通道信号有效
output M_AXI_RREADY , //表明主机能够接收读数据和响应信息
//用户端信号
// input rd_start , //读突发触发信号
input [7:0] rd_len , //读突发长度
// input [31:0] rd_addr , //地址
output rd_fifo_we , //连接到读fifo的写使能
output [DATA_WIDTH-1:0] rd_fifo_data , //连接到读fifo的写数据
output rd_done , //完成一次突发
input [31:0] ddr_count,
input ddr_prog_full,
input ai_pcie_fifo_prom_full
);
localparam S_RD_IDLE = 3'd0; //读空闲
localparam S_RA_WAIT = 3'd1; //读地址等待
localparam S_RA_START = 3'd2; //读地址
localparam S_RD_WAIT = 3'd3; //读数据等待
localparam S_RD_PROC = 3'd4; //读数据循环
localparam S_RD_DONE = 3'd5; //写结束
localparam DATA_NUM = (DATA_WIDTH == 512) ? 6 :
(DATA_WIDTH == 256) ? 5 :
(DATA_WIDTH == 128) ? 4 :
(DATA_WIDTH == 64) ? 3 : 2;
//reg define
reg [2:0] rd_state ; //状态寄存器
reg [28:0] reg_rd_adrs; //地址寄存器
reg reg_arvalid; //地址有效握手信号
reg [7:0] reg_rd_len ; //突发长度最大256,实测128最佳
reg [31:0] rd_addr_cnt; //读地址计数
reg [31:0] rd_data_cnt; //读数据计数
reg [31:0] rd_addr_reg; //读地址寄存器
//读fifo的写使能为axi准备好
assign rd_fifo_we = M_AXI_RVALID ; //ready + valid
//只有一个主机,可随意设置
assign M_AXI_ARID = 4'b1111;
//把地址赋予总线
assign M_AXI_ARADDR = reg_rd_adrs ;
//一次突发传输1长度 (0 至 wr_len-1'd1)
assign M_AXI_ARLEN[7:0] = rd_len-'d1;
//表示AXI总线每个数据宽度是16字节,128位。 2^(4)=16byte
assign M_AXI_ARSIZE =DATA_NUM;
//01代表地址递增,10代表递减
assign M_AXI_ARBURST[1:0] = 2'b01;//地址递增方式传输
assign M_AXI_ARLOCK = 1'b0;
assign M_AXI_ARCACHE[3:0] = 4'b0011;
assign M_AXI_ARPROT[2:0] = 3'b000;
assign M_AXI_ARQOS[3:0] = 4'b0000;
//地址握手信号AWVALID
assign M_AXI_ARVALID = reg_arvalid;
//写完成信号的写状态完成
assign rd_done = (rd_state == S_RD_DONE);
//fifo数据赋予总线
assign rd_fifo_data = M_AXI_RDATA ;
//数据握手信号RREADY
assign M_AXI_RREADY = M_AXI_RVALID;
//axi读过程状态机
always @(posedge ACLK ) begin
if(!ARESETN) begin
rd_state <= S_RD_IDLE;
reg_rd_adrs <= 29'd0;
rd_addr_cnt <= 32'd0;
rd_data_cnt <= 32'd0;
rd_addr_reg <= 32'd0;
reg_arvalid <= 1'b0; //地址
// reg_rd_last <= 1'b0;
reg_rd_len <= 8'd0;
rd_addr_cnt <=32'd0;
rd_data_cnt <=32'd0;
rd_addr_reg <=32'd0;
end else begin
case(rd_state)
S_RD_IDLE: begin //写空闲
//if(rd_start)
if( ddr_prog_full & !ai_pcie_fifo_prom_full) begin // ddr内部达到1024且读FIFO没满时
rd_state <= S_RA_WAIT;
reg_rd_adrs<= rd_addr_reg ;
end
reg_arvalid <= 1'b0;
reg_rd_len <= 8'd0;
end
S_RA_WAIT :begin
rd_state <= S_RA_START;
end
S_RA_START :begin //读地址
rd_state <= S_RD_WAIT; //读数据等待一个时钟周期
reg_arvalid <= 1'b1; //拉高地址有效信号
end
S_RD_WAIT :begin
if(M_AXI_ARREADY) //等待AXI写地址就绪信号将对应地址发给从机
rd_state <= S_RD_PROC;
reg_arvalid <= 1'b0; //arready有效后拉低arvalid信号,结束写地址,进入到读取数据状态
reg_rd_len <= rd_len-'d1; //127代表128个长度,0代表1个长度
end
S_RD_PROC :begin
if(M_AXI_RVALID & M_AXI_RLAST)begin //等待数据有效(M_AXI_RVALID)将对应数据发给从机
rd_state <= S_RD_DONE;
end
if(M_AXI_RVALID)begin
if(rd_addr_reg == DDR_ADDR_LIMIT)begin
// reg_rd_adrs <= 29'd0;
rd_addr_reg <= 32'd0;
end else begin
// reg_rd_adrs <= reg_rd_adrs + 8;
rd_addr_reg <= rd_addr_reg + 16;
end
if(rd_addr_cnt == reg_rd_len)begin
rd_addr_cnt<= 32'd0;
end else begin
rd_addr_cnt<= rd_addr_cnt + 1'b1;// 地址每次+1
end
end else begin
// reg_rd_adrs <= reg_rd_adrs ;
rd_addr_reg <= rd_addr_reg;
rd_addr_cnt <= rd_addr_cnt;
end
if(M_AXI_RVALID)begin
rd_data_cnt <= rd_data_cnt + 1'b1;
end else begin
rd_data_cnt <= rd_data_cnt;
end
end
S_RD_DONE :begin
rd_state <= S_RD_IDLE;
end
default: begin
rd_state <= S_RD_IDLE;
end
endcase
end
end
endmodule
上板验证结果分析
图1为axi写模块写触发条件设置为if(ai_fifo_prog_full & !ddr_cnt_full) 时抓取的从写fifo读出的数据,可以看出数据有误。
图1
图2为抓取的写时序的封面图,可以看出在 axi握手成功后即ai_fifo_rd拉高后写fifo数据连续读出并通过axi总线写进ddr。当时忘记截取写入与读出的数据对比图了,但是大概就是这个样子,最后只要写入与读出的自加数一致则实验成功。
只有后续有机会再继续补充验证结果图了。
图2
欢迎大家一起交流,文中如有错误地方还请指出,大家一起讨论。另外本文逻辑代码有参考《升腾Pro《FPGA Verilog开发实战指南——基于Xilinx Artix7》,感谢观看!文章来源:https://www.toymoban.com/news/detail-635894.html
源码我已上传我的主页,可以直接下载。文章来源地址https://www.toymoban.com/news/detail-635894.html
到了这里,关于DDR3 AXI4 IP核读写仿真实验(2)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!