为了熟悉该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、代码分析
模块对应端口信号如下所示,用到的GT IP信号就只有三个,发送通道初始化完成信号,发送数据信号(小端对齐),K码指示信号。开放给用户的是axi_stream接口,这个接收的数据是大端对齐的,因此在数据发送时需要调整。
module phy_tx (
input clk ,//系统时钟信号;
input rst ,//系统复位信号,高电平有效;
input [31 : 0] axi_s_data ,//AXI的数据输入信号,先传输高字节数据;
input [3 : 0] axi_s_keep ,//AXI的数据掩码信号,低电平有效;
input axi_s_last ,//AXI输入最后一个数据指示信号;
input axi_s_valid ,//AXI输入数据有效指示信号;
output reg axi_s_ready ,//AXI输入数据应答信号;
input gt_tx_done ,//GTX发送部分初始化成功,高电平有效;
output reg [31 : 0] gt_tx_data ,//GTX发送数据,先发送低字节数据;
output reg [3 : 0] gt_tx_char //GTX发送数据K码指示信号,高电平有效;
);
下面是状态机编码及发送逗号的空闲间隔时间,并且通过自定义函数计算该计数器位宽。
localparam ICOMMOA_CYCLE = 500 ;//间隔500个时钟周期后发送一次同步码。
localparam ICOMMOA_CYCLE_W = clogb2(ICOMMOA_CYCLE-1) ;//.
localparam INIT = 6'b00_0001 ;//状态机的初始化状态编码;
localparam IDLE = 6'b00_0010 ;//状态机的空闲状态编码;
localparam COMMOA = 6'b00_0100 ;//状态机的发送同步码状态编码;
localparam SOF = 6'b00_1000 ;//状态机的发送起始位状态编码;
localparam DATA = 6'b01_0000 ;//状态机的发送数据状态编码;
localparam EOF = 6'b10_0000 ;//状态机的发送停止位状态编码;
reg [5 : 0] state_n ;//状态机的次态信号;
reg [5 : 0] state_c ;//状态机的现态信号;
reg [3 : 0] axi_s_keep_r ;//
reg [2 : 0] axi_s_valid_r ;
reg [2 : 0] axi_s_ready_r ;
reg [9 : 0] cnt ;//
reg [9 : 0] cnt_num ;//
reg [9 : 0] wr_len ;//
reg [ICOMMOA_CYCLE_W - 1 : 0] commoa_cnt ;//
reg fifo_rden ;
reg [31 : 0] fifo_dout_r ;
wire add_cnt ;
wire end_cnt ;
wire add_commoa_cnt ;
wire end_commoa_cnt ;
wire valid_pos ;
wire fifo_full ;
wire fifo_empty ;
wire [31 : 0] lfsr_value ;
wire [31 : 0] fifo_dout ;
wire [31 : 0] fifo_dout_big ;
//自动计算位宽函数。
function integer clogb2(input integer depth);begin
if(depth == 0)
clogb2 = 1;
else if(depth != 0)
for(clogb2=0 ; depth>0 ; clogb2=clogb2+1)
depth=depth >> 1;
end
endfunction
下面例化LFSR生成模块和FIFO IP,并且将FIFO输出信号调整为小端模式,将先发送的数据放在低字节,便于后续使用。
//将FIFO读出的数据进行调整,与输出数据保持一致,先发送低字节数据;
assign fifo_dout = {fifo_dout_big[7:0],fifo_dout_big[15:8],fifo_dout_big[23:16],fifo_dout_big[31:24]};
//例化lfsr模块
lfsr_gen #(
.LFSR_INIT (16'hA076 )
)
u_lfsr_gen (
.clk ( clk ),
.rst ( rst ),
.lfsr_value ( lfsr_value )
);
//例化同步FIFO存储数据
fifo_32X1024 u_fifo_32X1024 (
.clk ( clk ),//input wire clk;
.din ( axi_s_data ),//input wire [31 : 0] din;
.wr_en ( axi_s_valid && axi_s_ready ),//input wire wr_en;
.rd_en ( fifo_rden ),//input wire rd_en;
.dout ( fifo_dout_big ),//output wire [31 : 0] dout;
.full ( fifo_full ),//output wire full;
.empty ( fifo_empty ) //output wire empty;
);
注意FIFO在设置时使用模式选择,如下所示,选择First Word Fall Through。
将axi的数据有效指示信号、应答信号暂存、FIFO输出数据通过移位寄存器暂存,便于后文使用。
//通过移位寄存器将数据暂存,便于后文使用。
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
检测输入数据的上升沿,作为用户发送数据个数计数器的置一条件,该计数器当主机和从机握手时加1,表示接收到一个用户数据。
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;//当计数到对应数值时清零。
下面是计数器最大值的设置,在逗号状态下只需要发送两个32位数据,而在发送数据状态下,根据最后一个数据的有效字节数,得到计数器对应的最大值。
//用于记录状态机在不同状态下计数器的最大值。
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
然后是FIFO读使能信号,在逗号发送完且需要发送数据时拉高读使能,根据最后一个数据有效字节数不同,拉低的条件会有区别。
//生成FIFO的读使能信号,初始值为零。
always@(posedge clk)begin
if(rst)begin//初始值为0;
fifo_rden <= 1'b0;
end//如果最后一个数据需要发送多余2字节,则状态机在读数据结束时拉低FIFO读使能。
else if(&axi_s_keep_r[3:1] && (state_c == DATA && end_cnt))begin
fifo_rden <= 1'b0;
end
else if(state_c == EOF)begin//当状态机跳转到发送停止位时拉低,其余时间保持不变。
fifo_rden <= 1'b0;
end
else if((state_c == COMMOA) && end_cnt && axi_s_valid_r[2] && axi_s_ready_r[2])begin//当输出最后一个同步码时,将读使能拉高,下个时钟输出数据。
fifo_rden <= 1'b1;
end
end
最后是发送数据和K码指示信号生成,在发送停止位时,需要根据掩码信号的值确定具体数据和K码指示信号状态,代码的注释都比较详细,不再赘述。在停止发送完毕和空闲状态下,发送LFSR随机序列。
//生成发送数据和K码指示信号,低字节数据先发送。
always@(posedge clk)begin
if(rst)begin//初始值为0;
gt_tx_data <= 32'd0;
gt_tx_char <= 4'd0;
end
else begin
case (state_c)
COMMOA : begin//发送同步码32'hBC50BC50.
gt_tx_data <= 32'h50bc50bc;
gt_tx_char <= 4'b0101;
end
SOF : begin//发送起始码且需要加入3字节数据。
gt_tx_data <= {fifo_dout[23:0],8'hfb};
gt_tx_char <= 4'b0001;
end
DATA : begin//发送数据,由于起始位发送了三字节数据,下次发送时需要进行拼接。
gt_tx_data <= {fifo_dout[23:0],fifo_dout_r[31:24]};
gt_tx_char <= 4'b0000;
end
EOF : begin//在发送结束码状态时,根据需要发送的字节数不同,需要对数据拼接。
case (axi_s_keep_r)
4'b1000 : begin//最后一个数据只有1字节有效时,将上次剩余数据进行拼接后发送。
gt_tx_data <= {lfsr_value[7:0],8'hfd,fifo_dout[7:0],fifo_dout_r[31:24]};
gt_tx_char <= 4'b0100;
end
4'b1100 : begin//最后一个数据只有2字节有效时,将上次剩余数据和停止吗进行拼接后发送。
gt_tx_data <= {8'hfd,fifo_dout[15:0],fifo_dout_r[31:24]};
gt_tx_char <= 4'b1000;
end
4'b1110 : begin//最后一个数据有3字节有效时,状态机跳转时数据就已经发送完成,最需要将停止码发出即可。
gt_tx_data <= {lfsr_value[23:0],8'hfd};
gt_tx_char <= 4'b0001;
end
4'b1111 : begin//最后一个数据均有效时,状态机跳转时会剩余一字节数据没有发送,将剩余数据和停止码拼接发出。
gt_tx_data <= {lfsr_value[15:0],8'hfd,fifo_dout_r[31:24]};
gt_tx_char <= 4'b0010;
end
default : begin//其余时间发送随机数。
gt_tx_data <= lfsr_value;
gt_tx_char <= 4'b0000;
end
endcase
end
default : begin//其余时间发送随机数。
gt_tx_data <= lfsr_value;
gt_tx_char <= 4'b0000;
end
endcase
end
end
上述代码理解难度稍微大一点的就是FIFO读使能和发送数据状态计数器的最大值,其余部分都比较简单,也正是由于这两部分思路绕一点,其他代码也才能够如此简单。有兴趣可以试试其余方法,可能会将难度装一道状态机上面去,导致跳转变得很复杂。
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序列的原理基本类似,也比较简单。
在接收模块设计完成后,整个工程一起上板进行测试,本工程暂时还不能上板测试。
如果对文章内容理解有疑惑或者对代码不理解,可以在评论区或者后台留言,看到后均会回复!
如果本文对您有帮助,还请多多点赞👍、评论💬和收藏⭐!您的支持是我更新的最大动力!将持续更新工程!