为了熟悉该IP的使用,当然不是只用下官方提供的示例工程收发数据就算完成了,如果自己能够设计一个自定义的PHY协议,然后验证收发是否正确,才是掌握这个IP的最快方式。
自定义收发数据的一帧数据格式如下所示,在每次发送数据之前,会发送两个逗号字符,之后发送一个字节的起始位8’hfb,然后发送数据,数据全部发完后,需要发送停止位8’hfd。注意逗号中的8’hBC和起始位、停止位的数据都是K码。
在设置IP时,设置为在任何时候检测逗号,因此在发送数据时,每间隔一定的空闲时钟周期,就会发送一次逗号,便于逗号检测和接收端时钟纠正。
为了减小线路中的EMI问题,在发送端处于空闲时,会发送伪随机序列,避免发送单频信号。
为方便用户使用,开放给用户端口为axi_stream流端口,用户可以通过控制掩码信号发送任意字节的数据。
注意一般axi_stream采用大端对齐,而GTX IP发送通道小端对齐,先发送低字节数据,比如发送32位数据tx_data,先发送tx_data[7:0],最后发送tx_data[31:24]。在接收、发送数据时,需要注意大小端的转换。
1、自定义PHY发送模块的设计思路
首先需要考虑与用户接口对接的接口设计,思路大概有两种。
一种是通过控制应答来达到组帧时间的控制,这种设计比较简单,但是用户数据端口需要等待从机应答信号拉高,才能发送下一个数据,可能需要存储模块对数据暂存。
另一种是使用FIFO来存储用户需要发送的数据,用户则可以直接将数据存入FIFO中,发送完帧头数据后,从FIFO中取出数据发送即可。这种方式对上游数据发送比较友善,因此采用这种设计思路。
设计思路比较简单,以状态机为主体架构,内嵌一个计数器辅助状态跳转。状态转换图如下所示,包含六个状态。因为GT收发器上电后需要初始化,因此有一个初始化状态,发送通道初始化完成后跳转到空闲状态。
在空闲状态下,GT发送通道一直发送32位的LFSR码(伪随机序列)。没经过一段空闲时间,就需要发送一次逗号,便于接收端好做逗号对齐和时钟纠正。
处于发送逗号状态下时,会发送2个32位的逗号数据,如果有数据发送,则跳转到发送起始位状态,否则回到空闲状态。
由于起始位只有8位数据,因此需要从第一个数据中取32位数据拼接后一起发送,下一个时钟周期跳转到发送数据状态。
在发送数据状态下,由于发送起始位时已经发送了第一个数据的高24位(用户端口大端对齐,先发高字节数据),因此每次发送的数据由前一个数据的低8位和后一个数据的高24位组成。
在发送数据状态下,需要注意最后一个数据可能并不是所有字节都是有效的,需要根据AXI的数据掩码信号判断有效字节。掩码位置不同会影响该状态下发送数据个数,进而影响计数器cnt的最大计数值,因此可以根据下图进行分析。
如果需要发送数据个数为wr_len,keep用户需要发送最后一个数据的掩码信号状态,cnt_num表示计数器cnt在发送数据状态下的最大计数值,则有下表关系。
keep | cnt_num |
---|---|
4’b1000 | wr_len - 3 |
4’b1100 | wr_len - 3 |
4’b1110 | wr_len - 2 |
4’b1111 | wr_len - 2 |
解释一下上表中计数器为什么是-2和减3,首先cnt_num表示状态机在发送数据状态需要发送的数据个数。那么就要去除在起始位和结束位发送的数据,当最后一个数据需要发送字节数小于3时,最后的数据将会和停止位一起发送(起始位的时候发送了24位数据,导致之后每次发送数据都要从后一个数据取24位拼接,如果最后一个数据需要发送的有效数据小于3字节,那么就会和前一个数据剩余的低8位数据、停止位组成最后一个数据发送个IP)。又由于计数器从0开始计数器,所以计数器最大值是发送数据个数减3。
如果最后一个数据有效字节数大于等于3,那么最后一个数据发完之后,下一个数据才会是停止位和部分数据,因此需要多一个时钟周期。
其余设计就比较简单了,需要用一个计数器计数用户发送数据的个数,并且将最后一个数据掩码信号保存,FIFO采用超前模式,尽量减小输出数据延时。
为了加快数据发送速率,当检测到输入数据有效时,就开始产生逗号和起始位,但需要注意FIFO存入数据后,一般需要至少两个周期才能读取数据。
上面的设计通过控制计数器最大值控制状态机跳转,这个思路可能在写计数器时需要思考的东西多一点,但是最后在发送数据时相应代码就会简单很多。在写代码之前还是仔细分析一下,找到一些规律后,很可能简化设计,节省后续仿真时间。
2、核心参考代码
//通过移位寄存器将数据暂存,便于后文使用。
always@(posedge clk)begin
fifo_dout_r <= fifo_dout;
axi_s_valid_r <= {axi_s_valid_r[1:0],axi_s_valid};
axi_s_ready_r <= {axi_s_ready_r[1:0],axi_s_ready};
end
assign valid_pos = axi_s_valid && (~axi_s_valid_r[0]);//检测输入数据有效指示信号的上升沿。
//对输入的数据进行计数,便于读FIFO数据时作为参考。
always@(posedge clk)begin
if(rst)begin//初始值为1;
wr_len <= 10'd1;
end
else if(valid_pos)begin//从1开始计数,表示存储到FIFO中的数据个数。
wr_len <= 10'd1;
end
else if(axi_s_valid && axi_s_ready)begin
wr_len <= wr_len + 1;
end
end
//生成AXI接收数据应答信号;
always@(posedge clk)begin
if(rst)begin//初始值为0;
axi_s_ready <= 1'b0;
end
else if(axi_s_last)begin//当接收完最后一个数据时拉低;
axi_s_ready <= 1'b0;
end
else if(state_c == IDLE)begin//当状态机处于空闲状态时拉高。
axi_s_ready <= 1'b1;
end
end
//将发送最后一字节数据的掩码信号暂存。
always@(posedge clk)begin
if(rst)begin//初始值为0;
axi_s_keep_r <= 4'd0;
end
else if(axi_s_last && axi_s_valid && axi_s_ready)begin//将最后一字节数据的掩码信号暂存。
axi_s_keep_r <= axi_s_keep;
end
end
//状态机次态到现态的跳转。
always@(posedge clk)begin
if(rst)begin//初始位于初始化状态。
state_c <= INIT;
end
else begin
state_c <= state_n;
end
end
//状态机次态的跳转。
always@(*)begin
case(state_c)
INIT : begin
if(gt_tx_done)begin//初始化完成后跳转到空闲状态。
state_n = IDLE;
end
else begin
state_n = state_c;
end
end
IDLE : begin
if((axi_s_valid_r[0] && axi_s_ready_r[0]) || end_commoa_cnt)begin//如果写入数据有效或者间隔固定空闲时钟时,跳转到发送同步码状态。
state_n = COMMOA;
end
else begin
state_n = state_c;
end
end
COMMOA : begin
if(end_cnt)begin//同步码发送完毕后,如果有数据需要发送,则跳转到起始位,否则跳转到空闲状态。
if(axi_s_valid_r[2] && axi_s_ready_r[2])
state_n = SOF;
else
state_n = IDLE;
end
else begin
state_n = state_c;
end
end
SOF : state_n = DATA;//直接跳转到发送数据状态;
DATA : begin
if(end_cnt)begin//数据发送完毕,跳转到发送停止位状态。
state_n = EOF;
end
else begin
state_n = state_c;
end
end
EOF : state_n = IDLE;//回到空闲状态;
default : begin
state_n = INIT;
end
endcase
end
//记录空闲状态对应的时钟个数,初始值为0。
always@(posedge clk)begin
if(rst)begin//
commoa_cnt <= 0;
end
else if(add_commoa_cnt)begin
if(end_commoa_cnt)
commoa_cnt <= 0;
else
commoa_cnt <= commoa_cnt + 1;
end
end
assign add_commoa_cnt = (state_c == IDLE);
assign end_commoa_cnt = add_commoa_cnt && commoa_cnt == ICOMMOA_CYCLE - 1;
//计数器cnt,对状态机处于发送同步码和发送数据的状态进行计数。
always@(posedge clk)begin
if(rst)begin//
cnt <= 0;
end
else if(state_c != state_n)begin//状态机跳转时清零。
cnt <= 0;
end
else if(add_cnt)begin
if(end_cnt)
cnt <= 0;
else
cnt <= cnt + 1;
end
end
//当状态机处于发送同步码或者发送数据时对时钟计数。
assign add_cnt = (state_c == COMMOA) || (state_c == DATA);
assign end_cnt = add_cnt && cnt == cnt_num;//当计数到对应数值时清零。
//用于记录状态机在不同状态下计数器的最大值。
always@(posedge clk)begin
if(rst)begin//初始值最好不为0;
cnt_num <= 10'd20;
end
else if(state_c == COMMOA)begin
cnt_num <= 2 - 1;//每次需要发送4字节同步码16'HBC50;
end
else if(state_c == DATA)begin
case (axi_s_keep_r)
4'b1000 : cnt_num <= wr_len - 3;//发送数据状态需要发送数据的个数;
4'b1100 : cnt_num <= wr_len - 3;//发送数据状态需要发送数据的个数;
4'b1110 : cnt_num <= wr_len - 2;//发送数据状态需要发送数据的个数;
4'b1111 : cnt_num <= wr_len - 2;//发送数据状态需要发送数据的个数;
default : cnt_num <= wr_len - 2;//;
endcase
end
end
3、模块仿真
对应的TestBench如下所示,比较简单,通过设置TX_KEEP参数,即可确定最后一个数据的有效字节,用来进行不同字节数的仿真测试。
//--###############################################################################################
//--#
//--# File Name : tb_phy_tx
//--# Designer : 数字站
//--# Tool : Vivado 2021.1
//--# Design Date : 2024.3.17
//--# Description : TestBench
//--# Version : 0.0
//--# Coding scheme : GBK(If the Chinese comment of the file is garbled, please do not save it and check whether the file is opened in GBK encoding mode)
//--#
//--###############################################################################################
`timescale 1 ns/1 ns
module tb_phy_tx();
localparam CYCLE = 10 ;//系统时钟周期,单位ns,默认10ns;
localparam RST_TIME = 10 ;//系统复位持续时间,默认10个系统时钟周期;
localparam TX_KEEP = 4'b1111 ;//发送最后一个数据的有效位数,大端对齐;
reg clk ;//系统时钟,默认100MHz;
reg rst_n ;//系统复位,默认低电平有效;
reg [7 : 0] send_value ;
reg [31 : 0] axi_s_data ;
reg [3 : 0] axi_s_keep ;
reg axi_s_last ;
reg axi_s_valid ;
wire axi_s_ready ;
wire [31 : 0] gt_tx_data ;
wire [3 : 0] gt_tx_char ;
phy_tx u_phy_tx(
.clk ( clk ),//系统时钟信号;
.rst_n ( rst_n ),//系统复位信号,低电平有效;
.axi_s_data ( axi_s_data ),//AXI的数据输入信号,先传输高字节数据;
.axi_s_keep ( axi_s_keep ),//AXI的数据掩码信号,低电平有效;
.axi_s_last ( axi_s_last ),//AXI输入最后一个数据指示信号;
.axi_s_valid ( axi_s_valid ),//AXI输入数据有效指示信号;
.axi_s_ready ( axi_s_ready ),//AXI输入数据应答信号;
.gt_tx_done ( 1'b1 ),//GTX发送部分初始化成功,高电平有效;
.gt_tx_data ( gt_tx_data ),//GTX发送数据,先发送低字节数据;
.gt_tx_char ( gt_tx_char ) //GTX发送数据K码指示信号,高电平有效;
);
//生成周期为CYCLE数值的系统时钟;
initial begin
clk = 0;
forever #(CYCLE/2) clk = ~clk;
end
//生成复位信号;
initial begin
rst_n = 1;
#2;
rst_n = 0;//开始时复位10个时钟;
#(RST_TIME*CYCLE);
rst_n = 1;
end
//生成输入信号din;
initial begin
axi_s_data = 32'd0;
axi_s_keep = 4'd0;
axi_s_last = 1'd0;
axi_s_valid = 1'd0;
wait(rst_n);//等待复位完成;
repeat(10) @(posedge clk);
forever begin
phy_tx_task(5);
end
end
//发送数据的任务;
task phy_tx_task(
input [7 : 0] len
);
begin : phy_tx_task_0
integer i;
axi_s_data <= 32'd0;
axi_s_keep <= 4'd0;
axi_s_last <= 1'd0;
axi_s_valid <= 1'd0;
send_value <= 8'd1;
@(posedge clk);
wait(axi_s_ready);
for(i=0 ; i<len ; i=i+1)begin
send_value <= send_value + 1;
axi_s_data <= {send_value,send_value,send_value,send_value};
if(i == len - 1)begin//最后一个数据时控制掩码信号;
axi_s_last <= 1'b1;
axi_s_keep <= TX_KEEP;
end
else begin
axi_s_last <= 1'b0;
axi_s_keep <= 4'hf;
end
axi_s_valid <= 1'b1;
@(posedge clk);
end
axi_s_data <= 32'd0;
axi_s_keep <= 4'd0;
axi_s_last <= 1'd0;
axi_s_valid <= 1'd0;
@(posedge clk);
end
endtask
endmodule
如下图所示,用户一帧发送5个32位数据,最后一个数据只有最高字节有效。该模块首先输出2个32位的逗号数据,之后发送起始位8’hfb,起始位中包含24位数据,之后每个数据都由前后两个用户数据拼接。最后停止位中会包含2字节的数据,8’hfd是停止位。注意axi_s_data是大端对齐,gt_tx_data是小端对齐。
下图是用户发送最后一个数据高两字节均有效的仿真结果,此时停止位会包含3字节数据,只发送了2字节的8’h05,仿真正确。
下图是用户发送最后一个数据高三字节均有效的仿真结果,停止位不包含数据,最后发送了三字节8’h55,仿真正确。
下图是用户发送最后一个数据均节均有效的仿真结果,停止位包含1字节数据,一帧数据发送了四字节8’h55,仿真正确。
经过上述仿真,初步判断该模块功能正常,在空闲状态也插入了LFSR编码,而这个编码与m序列的原理基本类似,也比较简单。
在接收模块设计完成后,整个工程一起上板进行测试,本工程暂时还不能上板测试。
如果对文章内容理解有疑惑或者对代码不理解,可以在评论区或者后台留言,看到后均会回复!
如果本文对您有帮助,还请多多点赞👍、评论💬和收藏⭐!您的支持是我更新的最大动力!将持续更新工程!