基于GTX 8B10B编码的自定义PHY发送模块(高速收发器十二)

50 篇文章 14 订阅
38 篇文章 58 订阅

  点击进入高速收发器系列文章导航界面


  为了熟悉该IP的使用,当然不是只用下官方提供的示例工程收发数据就算完成了,如果自己能够设计一个自定义的PHY协议,然后验证收发是否正确,才是掌握这个IP的最快方式。

  自定义收发数据的一帧数据格式如下所示,在每次发送数据之前,会发送两个逗号字符,之后发送一个字节的起始位8’hfb,然后发送数据,数据全部发完后,需要发送停止位8’hfd。注意逗号中的8’hBC和起始位、停止位的数据都是K码。

在这里插入图片描述

图1 自定义PHY格式

  在设置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位组成。

在这里插入图片描述

图2 状态转换图

  在发送数据状态下,需要注意最后一个数据可能并不是所有字节都是有效的,需要根据AXI的数据掩码信号判断有效字节。掩码位置不同会影响该状态下发送数据个数,进而影响计数器cnt的最大计数值,因此可以根据下图进行分析。

  如果需要发送数据个数为wr_len,keep用户需要发送最后一个数据的掩码信号状态,cnt_num表示计数器cnt在发送数据状态下的最大计数值,则有下表关系。

表1 掩码信号与计数器最大值的关系
keepcnt_num
4’b1000wr_len - 3
4’b1100wr_len - 3
4’b1110wr_len - 2
4’b1111wr_len - 2

  解释一下上表中计数器为什么是-2和减3,首先cnt_num表示状态机在发送数据状态需要发送的数据个数。那么就要去除在起始位和结束位发送的数据,当最后一个数据需要发送字节数小于3时,最后的数据将会和停止位一起发送(起始位的时候发送了24位数据,导致之后每次发送数据都要从后一个数据取24位拼接,如果最后一个数据需要发送的有效数据小于3字节,那么就会和前一个数据剩余的低8位数据、停止位组成最后一个数据发送个IP)。又由于计数器从0开始计数器,所以计数器最大值是发送数据个数减3。

  如果最后一个数据有效字节数大于等于3,那么最后一个数据发完之后,下一个数据才会是停止位和部分数据,因此需要多一个时钟周期。

  其余设计就比较简单了,需要用一个计数器计数用户发送数据的个数,并且将最后一个数据掩码信号保存,FIFO采用超前模式,尽量减小输出数据延时。

在这里插入图片描述

图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是小端对齐。

在这里插入图片描述

图4 最后数据单字节有效仿真

  下图是用户发送最后一个数据高两字节均有效的仿真结果,此时停止位会包含3字节数据,只发送了2字节的8’h05,仿真正确。

在这里插入图片描述

图5 最后数据两字节有效仿真

  下图是用户发送最后一个数据高三字节均有效的仿真结果,停止位不包含数据,最后发送了三字节8’h55,仿真正确。

在这里插入图片描述

图6 最后数据三字节有效仿真

  下图是用户发送最后一个数据均节均有效的仿真结果,停止位包含1字节数据,一帧数据发送了四字节8’h55,仿真正确。

在这里插入图片描述

图7 最后数据均有效仿真

  经过上述仿真,初步判断该模块功能正常,在空闲状态也插入了LFSR编码,而这个编码与m序列的原理基本类似,也比较简单。

  在接收模块设计完成后,整个工程一起上板进行测试,本工程暂时还不能上板测试。


  如果对文章内容理解有疑惑或者对代码不理解,可以在评论区或者后台留言,看到后均会回复!

  如果本文对您有帮助,还请多多点赞👍、评论💬和收藏⭐!您的支持是我更新的最大动力!将持续更新工程!

评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

电路_fpga

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值