1、自定义PHY顶层模块
前文设计了64B66B自定义PHY的发送模块,本文完成自定义PHY剩余的模块的设计,整体设计框图如下所示。
其中phy_tx是自定义PHY的发送数据模块,scrambler是加扰模块,rx_slipbit是接收端手动对齐模块,descrambler是解扰模块,phy_rx是自定义PHY的接收数据模块。其中加扰和解扰直接使用官方示例工程的模块。
图1 整体设计框图
当接收数据同步完成之后,接收数据模块才会解码接收的数据,转换成axi_stream流数据输出给用户。
PHY顶层模块的参考代码如下所示:
//例化PHY接收对齐模块
phy_rx_slipbit u_phy_rx_slipbit(
.rx_clk ( rx_clk ),//系统时钟信号;
.rx_rst ( rst ),//系统复位信号,高电平有效;
.header ( rx_header ),//头部2位数据,等于2'b01或者2'b10时有效;
.header_vld ( rx_header_vld ),//头部数据有效指示信号,高电平有效;
.slipbit ( rx_slipbit ),//滑块信号,高电平有效;
.sync ( sync ) //同步成功指示信号,高电平有效;
);
//解扰模块;
descrambler u_descrambler_0(
.clk ( rx_clk ),//系统时钟信号;
.rst ( rx_rst ),//系统复位信号,高电平有效;
.rx_header_i ( rx_header ),//2位的同步头数据,不需要进行解扰;
.rx_header_vld_i ( rx_header_vld ),//同步头有效指示信号,高电平有效;
.rx_data_i ( rx_data ),//需要解码的接收数据;
.rx_data_vld_i ( rx_data_vld ),//接收数据有效指示信号;
.rx_header_o ( rx_header_de ),//2位的同步头数据,不需要进行解扰;
.rx_header_vld_o ( rx_header_vld_de ),//同步头有效指示信号,高电平有效;
.rx_data_o ( rx_data_de ),//解扰后的输出数据;
.rx_data_vld_o ( rx_data_vld_de ) //解扰后的输出数据有效指示信号;
);
//例化PHY接收模块;
phy_rx u_phy_rx(
.rx_clk ( rx_clk ),//系统时钟信号;
.rx_rst ( rx_rst ),//系统复位信号,高电平有效;
.sync ( sync ),//同步成功指示信号,高电平有效;
.rx_data ( rx_data_de ),//GTX的接收数据信号;
.rx_data_vld ( rx_data_vld_de ),//GTX的接收数据有效指示信号;
.rx_header ( rx_header_de ),//GTX的接收头部数据信号;
.rx_header_vld ( rx_header_vld_de ),//GTX的接收头部数据有效指示信号;
.m_axi_data ( m_axi_data ),//数据信号;
.m_axi_keep ( m_axi_keep ),//数据掩码信号;
.m_axi_last ( m_axi_last ),//一帧数据的结束数据;
.m_axi_valid ( m_axi_valid ) //数据有效指示信号;
);
//例化PHY发送模块;
phy_tx u_phy_tx(
.tx_clk ( tx_clk ),//系统时钟信号;
.tx_rst ( tx_rst ),//系统复位信号,高电平有效;
.s_axi_valid ( s_axi_valid ),//数据有效指示信号,高电平有效;
.s_axi_last ( s_axi_last ),//帧结束指示信号,高电平有效;
.s_axi_data ( s_axi_data ),//数据信号;
.s_axi_keep ( s_axi_keep ),//数据掩码信号;
.s_axi_ready ( s_axi_ready ),//接收数据应答信号;
.tx_data ( tx_data_0 ),//GTX需要发送的数据;
.tx_header ( tx_header_0 ),//GTX需要发送的头部数据;
.tx_sequence ( tx_sequence_0 ) //GTX外部计数器;
);
//通道0的加扰模块;
scrambler u_scrambler_0(
.clk ( tx_clk ),//系统时钟信号;
.rst ( tx_rst ),//系统复位信号,高电平有效;
.tx_header_i ( tx_header_0 ),//2位的同步头数据,不需要进行加扰;
.tx_sequence_i ( tx_sequence_0 ),//外部计数器;
.tx_data_i ( tx_data_0 ),//需要加码的接收数据;
.tx_header_o ( tx_header ),//2位的同步头数据,不需要进行接扰;
.tx_sequence_o ( tx_sequence ),//外部计数器;
.tx_data_o ( tx_data ) //加扰后的输出数据;
);
2、接收端同步模块设计
该模块通过接收到的同步头(rx_headr)的状态去拉高slipbit,调节串并转换的起始位置,达到接收数据对齐的目的。
当接收通道复位完成之后,开始检测接收到的同步头(rx_headr)的状态,如果rx_headr不等于2'b01或者2'b10,则表示接收的数据没有对齐,把slipbit信号拉高一个时钟,等待32个时钟周期后,再次检测rx_headr的状态,直到连续64个时钟rx_headr均为2'b01或者2'b10时,表示同步完成。
上述是官方示例工程接收端同步数据的思路,以这个方式设计模块或者直接使用官方示例工程的同步模块,在实际上板测试时,可能会存在一些问题。
我在设计该模块时,还增加了一个条件,正常情况下,一帧数据应该只有在帧头和帧尾才会存在控制位,其余时间全是数据位。
因此rx_headr应该不会连续三个时钟为2'b10,因此如果检测到连续三个时钟均为2'b10,则认为同步失败,需要继续拉高slipbit来调整串并转换的起始位置。
下面是该模块的具体设计,首先是端口信号,sync为高电平表示接收的数据同步完成,之后接收数据模块才能正常工作。计数器cnt用于计数连续检测正确数据的个数,计数器计数到最大值时,表示同步完成。
//该计数器用于记录连续检测正确数据的个数,达到指定个数时表示同步成功;
always@(posedge rx_clk)begin
if(rx_rst)begin//
cnt <= 'd0;
end
else if(slipbit)begin//通过滑块信号进行同步时,表示检测到错误数据,计数器清零。
cnt <= 'd0;
end
else if(add_cnt)begin
if(end_cnt)
cnt <= 'd0;
else
cnt <= cnt + 'd1;
end
end
//头部数据有效且是控制帧或者数据帧且不处于同步过程中;
assign add_cnt = (header_vld && ((header == 2'b01) || (header == 2'b10)) && (~slipbit_flag));
assign end_cnt = add_cnt && cnt == SH_CNT_MAX - 1;//连续采集指定个正确的数据;
当不处于同步调整状态,且接收的有效同步头是错误时,表示同步失败,拉高slipbit调整串并转换的起始位置。
//当头部数据有效指示信号位高电平且头部是错误数据并且不处于同步过程中时,表示检测的数据有误。
assign header_invld = (header_vld && (header != 2'b01) && (header != 2'b10) && (~slipbit_flag));
//当采集数据有误时,将滑块信号拉高,进行校准;
always@(posedge rx_clk)begin
if(rx_rst)begin//初始值为0;
slipbit <= 1'b0;
end
else begin//当检测的信号有错误且此时并没有进行校正时有效;
slipbit <= ((~slipbit) && (header_invld || syc_error));
end
end
每次slipbit拉高之后,需要等待32个时钟之后,才能进行下次检测,因此需要一个计数器slipbit_cnt来计数这个状态slipbit_flag。
//正在校准的指示信号,初始值位0,当滑块信号位高电平时开始计数,当计数器计数结束时拉低;
always@(posedge rx_clk)begin
if(rx_rst)begin//初始值为0;
slipbit_flag <= 1'b0;
end
else if(end_slipbit_cnt)begin//当计数器拉高时有效;
slipbit_flag <= 1'b0;
end
else if(slipbit)begin//滑块信号有效时拉高;
slipbit_flag <= 1'b1;
end
end
//记录滑块信号拉高后的一段时间,这段时间内IP会对采集位置进行调整,期间不需要判断输出头部数据的正确性;
always@(posedge rx_clk)begin
if(rx_rst)begin//
slipbit_cnt <= 'd0;
end
else if(add_slipbit_cnt)begin
if(end_slipbit_cnt)
slipbit_cnt <= 'd0;
else
slipbit_cnt <= slipbit_cnt + 'd1;
end
end
assign add_slipbit_cnt = (slipbit || slipbit_flag);//当滑块信号或者指示信号有效时计数;
assign end_slipbit_cnt = add_slipbit_cnt && slipbit_cnt == SLIPBIT_GAP - 1;//当计数到指定时钟个数时清零;
生成同步完成指示信号sync,当slipbit为高电平时,表示在调节同步的位置,此时sync为低电平。当计数器cnt计数结束时,表示同步完成,此时sync拉高。
//生成同步完成指示信号。
always@(posedge rx_clk)begin
if(rx_rst)begin//初始值为0;
sync <= 1'b0;
end
else if(slipbit)begin//当滑块信号位高电平时,表示检测到错误数据,表示当前数据没有同步成功;
sync <= 1'b0;
end
else if(end_cnt)begin//当连续检测到指定个正确数据时,表示同步成功;
sync <= 1'b1;
end
end
最后将输入信号通过两组移位寄存器暂存,如果同步后的有效同步头rx_headr连续三个时钟均为2'b10,则表示同步失败,将sync_error拉高,会重新调整同步位置。
//将头部数据和头部数据有效指示信号延迟两个时钟。
always@(posedge rx_clk)begin
{header_r[1],header_r[0]} <= {header_r[0],header};
header_vld_r <= {header_vld_r[0],header_vld};
end
//检测头部数据,防止出现连续的2'b10,从而导致同步错误。
always@(posedge rx_clk)begin
if(rx_rst)begin//初始值为0;
syc_error <= 1'b0;
end
else begin//当不处于同步调整状态下时,连续三个时钟检测到头部数据为2'b10时拉高,表示接收的同步数据错误,需要继续调整同步。
syc_error <= (~slipbit_flag) && (header == 2'b10) && (header_r[0] == 2'b10) && (header_r[1] == 2'b10) && header_vld && (&header_vld_r);
end
end
3、接收数据模块设计
64B66B编码的接收模块设计逻辑很简单,只需要从指定起始位和停止位之间解析出数据输出到用户的axi_stream端口即可。
首先是端口信号列表,包含解扰后的输入数据、同步头、同步完成指示信号SYNC,还有输出给用户的axi_stream主机端口信号,因为模块内部不包含FIFO等缓冲结果,因此不需要从机提供应答信号,直接将解析的数据输出。
通过一组移位寄存器将输入信号暂存,便于后续逻辑使用。当接收端的数据同步完成,同步头为2'b10且接收第一个数据的第一个字节数据为8'h78时,sof_flag拉高表示检测到帧头。
//当检测到帧头时拉高,其余时间为低电平。
always@(posedge rx_clk)begin
if(rx_rst)begin//初始值为0;
sof_flag <= 1'b0;
end
else if(sync)begin//检测到帧头时拉高;
sof_flag <= (rx_header_vld && (rx_header == 2'b10) && rx_data_vld && (rx_data[7:0] == 8'h78));
end
end
而帧尾有8种情况,当eof_flag拉高时,表示检测到帧尾,同时需要记录帧尾包含几个字节的有效数据,与输出给用户的尾端数据掩码信号有关。
Rx_flag为高电平表示正在接收一帧数据,当检测到帧头时拉高,输出用户最后一帧的最后一个数据时拉低,其余时间保持不变。
//接收数据标志信号,初始值为0,当检测到起始帧时拉高,检测到结束帧时拉低;
always@(posedge rx_clk)begin
if(rx_rst)begin//初始值为0;
rx_flag <= 1'b0;
end
else if(m_axi_last)begin//当一帧数据接收完成时拉低;
rx_flag <= 1'b0;
end
else if(sof_flag)begin//当检测到帧头时拉高;
rx_flag <= 1'b1;
end
end
然后是生成用户数据,由于停止位的最低字节不是数据,因此在拼接时需要舍弃。因为GTX的数据是小端对齐的,而输出给用户的数据采用大端对齐,因此输出的数据需要将高低字节数据转换。
//生成用户数据信号;
always@(posedge rx_clk)begin
if(rx_rst)begin//初始值为0;
m_axi_data_r <= 'd0;
end
else if(eof_flag)begin
m_axi_data_r <= {rx_data_r[0][15:8],rx_data_r[1][63:8]};
end
else if(eof_flag_r)begin
m_axi_data_r <= {rx_data_r[0][15:0],rx_data_r[1][63:16]};
end
else if(rx_flag)begin
m_axi_data_r <= {rx_data_r[0][7:0],rx_data_r[1][63:8]};
end
end
//将输出数据大小端翻转;
assign m_axi_data = {m_axi_data_r[7:0],m_axi_data_r[15:8],m_axi_data_r[23:16],m_axi_data_r[31:24],m_axi_data_r[39:32],m_axi_data_r[47:40],m_axi_data_r[55:48],m_axi_data_r[63:56]};
之后生成用户数据掩码,初始值8'hff,表示所有数据均有效。根据帧尾的有效字节数确定尾端掩码的数值,比如帧尾有1字节有效数据时,则用户数据的尾端所有字节的数据均有效,为8'hff,其余七种情况需要全部考虑。
该模块的设计到此结束,用户在设计时只需要注意帧尾有效字节数,与尾端掩码的对应关系即可,其余设计都比较简单。
4、仿真PHY顶层模块
加扰和解扰模块使用官方示例工程相关模块,但在模块内部添加了对其他信号的延时设计,确保数据对齐,加扰和解扰模块参考代码如下所示。
加扰模块参考代码:
//--###############################################################################################
//--#
//--# File Name : scrambler
//--# Designer : 数字站
//--# Tool : Vivado 2021.1
//--# Design Date : 2024.4.3
//--# Description : 通过x^58+x^39+1对接收数据进行加扰。
//--# 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)
//--#
//--###############################################################################################
module scrambler #(
parameter TX_DATA_WIDTH = 64 //需要加扰的数据位宽;
)(
input clk ,//系统时钟信号;
input rst ,//系统复位信号,高电平有效;
input [1 : 0] tx_header_i ,//2位的同步头数据,不需要进行加扰;
input [6 : 0] tx_sequence_i ,//外部计数器;
input [TX_DATA_WIDTH - 1 : 0] tx_data_i ,//需要加码的接收数据;
output reg [1 : 0] tx_header_o ,//2位的同步头数据,不需要进行接扰;
output reg [6 : 0] tx_sequence_o ,//外部计数器;
output reg [TX_DATA_WIDTH - 1 : 0] tx_data_o //加扰后的输出数据;
);
integer i ;
reg [57 : 0] poly ;
reg [57 : 0] scrambler ;
reg [TX_DATA_WIDTH - 1 : 0] tempData ;
reg xorBit ;
always@(scrambler,tx_data_i)begin
poly = scrambler;
for(i=0 ; i<=(TX_DATA_WIDTH-1) ; i=i+1)begin
xorBit = tx_data_i[i] ^ poly[38] ^ poly[57];
poly = {poly[56:0],xorBit};
tempData[i] = xorBit;
end
end
//加扰输出数据;
always@(posedge clk)begin
if (rst)begin
tx_data_o <= 'h0;
scrambler <= 58'h155_5555_5555_5555;
end
else if(tx_sequence_i)begin
tx_data_o <= tempData;
scrambler <= poly;
end
end
//加码数据相对于其他信号延迟一个时钟,为了对齐,把其他信号延迟一个时钟后输出。
always@(posedge clk)begin
tx_header_o <= tx_header_i;
tx_sequence_o <= tx_sequence_i;
end
endmodule
解扰模块参考代码:
//--###############################################################################################
//--#
//--# File Name : descrambler
//--# Designer : 数字站
//--# Tool : Vivado 2021.1
//--# Design Date : 2024.4.3
//--# Description : 通过x^58+x^39+1对接收数据进行解扰。
//--# 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)
//--#
//--###############################################################################################
module descrambler #(
parameter RX_DATA_WIDTH = 64 //需要解扰的数据位宽;
)(
input clk ,//系统时钟信号;
input rst ,//系统复位信号,高电平有效;
input [1 : 0] rx_header_i ,//2位的同步头数据,不需要进行解扰;
input rx_header_vld_i ,//同步头有效指示信号,高电平有效;
input [RX_DATA_WIDTH - 1 : 0] rx_data_i ,//需要解码的接收数据;
input rx_data_vld_i ,//接收数据有效指示信号;
output reg [1 : 0] rx_header_o ,//2位的同步头数据,不需要进行解扰;
output reg rx_header_vld_o ,//同步头有效指示信号,高电平有效;
output reg [RX_DATA_WIDTH - 1 : 0] rx_data_o ,//解扰后的输出数据;
output reg rx_data_vld_o //解扰后的输出数据有效指示信号;
);
integer i ;
reg [57 : 0] descrambler ;
reg [57 : 0] poly ;
reg [RX_DATA_WIDTH - 1 : 0] tempData ;
reg xorBit ;
//解扰运算;
always@(descrambler,rx_data_i)begin
poly = descrambler;
for(i=0;i<=(RX_DATA_WIDTH-1);i=i+1)begin
xorBit = rx_data_i[i] ^ poly[38] ^ poly[57];
poly = {poly[56:0],rx_data_i[i]};
tempData[i] = xorBit;
end
end
//解扰将数据输出;
always@(posedge clk)begin
if(rst)begin
rx_data_o <= 'h0;
descrambler <= 58'h155_5555_5555_5555;
end
else if(rx_data_vld_i)begin
rx_data_o <= tempData;
descrambler <= poly;
end
end
//解码数据相对于其他信号延迟一个时钟,为了对齐,把其他信号延迟一个时钟后输出。
always@(posedge clk)begin
rx_header_o <= rx_header_i;
rx_header_vld_o <= rx_header_vld_i;
rx_data_vld_o <= rx_data_vld_i;
end
endmodule
自定义PHY顶层模块的仿真激励代码如下所示:
//--###############################################################################################
//--#
//--# File Name : tb_phy_module
//--# Designer : 数字站
//--# Tool : Vivado 2021.1
//--# Design Date : 2024.4.07
//--# 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_module();
localparam CYCLE = 10 ;//系统时钟周期,单位ns,默认10ns;
localparam RST_TIME = 10 ;//系统复位持续时间,默认10个系统时钟周期;
localparam TX_KEEP = 8'b1111_1100 ;//发送最后一个数据的有效位数,大端对齐;
reg clk ;//系统时钟,默认100MHz;
reg rst_n ;//系统复位,默认低电平有效;
reg [4 : 0] send_value ;
reg s_axi_valid ;//数据有效指示信号,高电平有效;
reg s_axi_last ;//帧结束指示信号,高电平有效;
reg [63 : 0] s_axi_data ;//数据信号;
reg [7 : 0] s_axi_keep ;//数据掩码信号;
wire rx_data_vld ;//GTX的接收数据有效指示信号;
wire rx_header_vld ;//GTX的接收头部数据有效指示信号;
wire rx_slipbit ;//滑块信号,高电平有效;
wire [63 : 0] tx_data ;//GTX需要发送的数据;
wire [1 : 0] tx_header ;//GTX需要发送的头部数据;
wire [6 : 0] tx_sequence ;//GTX外部计数器;
wire s_axi_ready ;//接收数据应答信号;
assign rx_data_vld = (tx_sequence != 32);
assign rx_header_vld = rx_data_vld;
phy_module u_phy_module(
.rst ( ~rst_n ),
.rx_clk ( clk ),//系统时钟信号;
.rx_rst ( ~rst_n ),//系统复位信号,高电平有效;
.rx_data ( tx_data ),//GTX的接收数据信号;
.rx_data_vld ( rx_data_vld ),//GTX的接收数据有效指示信号;
.rx_header ( tx_header ),//GTX的接收头部数据信号;
.rx_header_vld ( rx_header_vld ),//GTX的接收头部数据有效指示信号;
.rx_slipbit ( rx_slipbit ),//滑块信号,高电平有效;
.tx_clk ( clk ),//系统时钟信号;
.tx_rst ( ~rst_n ),//系统复位信号,高电平有效;
.tx_data ( tx_data ),//GTX需要发送的数据;
.tx_header ( tx_header ),//GTX需要发送的头部数据;
.tx_sequence ( tx_sequence ),//GTX外部计数器;
.s_axi_valid ( s_axi_valid ),//数据有效指示信号,高电平有效;
.s_axi_last ( s_axi_last ),//帧结束指示信号,高电平有效;
.s_axi_data ( s_axi_data ),//数据信号;
.s_axi_keep ( s_axi_keep ),//数据掩码信号;
.s_axi_ready ( s_axi_ready ),//接收数据应答信号;
.m_axi_data ( ),//数据信号;
.m_axi_keep ( ),//数据掩码信号;
.m_axi_last ( ),//一帧数据的结束数据;
.m_axi_valid ( ) //数据有效指示信号;
);
//生成周期为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
s_axi_data = 64'd0;
s_axi_keep = 8'd0;
s_axi_last = 1'd0;
s_axi_valid = 1'd0;
wait(rst_n);//等待复位完成;
repeat(10) @(posedge clk);
phy_tx_task(5);
repeat(100) @(posedge clk);
repeat(5) begin
phy_tx_task(5);
end
@(posedge s_axi_ready);
end
//发送数据的任务;
task phy_tx_task(
input [7 : 0] len
);
begin : phy_tx_task_0
integer i;
s_axi_data <= 64'd0;
s_axi_keep <= 8'hff;
s_axi_last <= 1'd0;
s_axi_valid <= 1'd0;
send_value <= 5'd1;
@(posedge clk);
wait(s_axi_ready);
@(posedge clk);
for(i=0 ; i<len ; i=i+1)begin
s_axi_data <= {{send_value[4:0],3'd0},{send_value[4:0],3'd1},{send_value[4:0],3'd2},{send_value[4:0],3'd3},{send_value[4:0],3'd4},{send_value[4:0],3'd5},{send_value[4:0],3'd6},{send_value[4:0],3'd7}};
if(i == len - 1)begin
s_axi_last <= 1'b1;
s_axi_keep <= TX_KEEP;
end
else begin
s_axi_last <= 1'b0;
s_axi_keep <= 8'hff;
end
s_axi_valid <= 1'b1;
send_value <= send_value + 1;
@(posedge clk);
end
s_axi_data <= 64'd0;
s_axi_keep <= 8'hff;
s_axi_last <= 1'd0;
s_axi_valid <= 1'd0;
@(posedge clk);
end
endtask
endmodule
将用户发送数据的尾端掩码设置为8'hc0,抓到发送数据和接收数据的时序如下图所示,一帧数据最后一个有效数据是8'h29,收发一致,证明发送数据、接收数据、加扰、解扰的逻辑设计均没有问题。
图2 自定义PHY的仿真时序
将尾端数据掩码信号修改为8'hf0,对应的仿真时序如下图所示。接收和发送的最后一个有效字节数据均为8'h2b,表示收发数据均正确。
图3 自定义PHY仿真时序
将发送数据的尾端掩码修改为8'hfc,对应的仿真时序如下图所示,接收和发送数据帧的最后一个有效字节数据均为2'h2d,表示收发数据时序均正确。
图4 自定义phy仿真时序
其余情况有兴趣的可以自行仿真,在设计时对所有情况都做过仿真。因为本文并没有加入高速收发器进行联调,所以其余情况就不全部列出了。下文将所有模块联合并上板,到时候在将全部情况进行仿真。