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

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


  前文完成了发送模块的设计,本文接着完成接收模块的设计,接收模块相对发送模块会更加麻烦。

1、设计思路

  前文在讲解官方示例工程时,提到GTX IP的接收部分没有做字对齐,需要用户自己编写字对齐模块,由于发送端可能发送任意字节数据,因此停止位也可能出现在一帧数据的任意位置。

  下图是自定义的帧格式,在实际收发数据过程中,部分逗号可能被用于时钟纠正。在检测数据帧时,将24’hbc50fb作为一帧数据的帧头,帧尾依旧是8’hfd。

在这里插入图片描述

图1 帧格式

  由于GTX IP的接收通道没有进行字对齐,接收的帧头和帧尾的数据可能会出现下图中的16种现象。

在这里插入图片描述

图2 帧头帧尾的分布

  起始位8’hfb可能出现在一个数据的任何字节位置,停止位8’hfd也可能出现在一个数据的任意字节位置(因为发送端可能发送任意字节数据)。这些条件会使得输出给用户的最后一个数据的部分数据无效,需要用掩码进行控制。

  蓝色数字用于记录起始位和停止位在所处数据中哪个字节,这与最后一个数据的掩码信号直接相关。

  黄色的掩码表示这个数据会出现在下一个数据中,与停止位对齐的数据都i是有效的,由三种情况出现这种情况。

  可以对图2中的规律进行总结,首先当停止位所在字节数减去起始位所在字节数等于1时,输出给用户的最后一个数据所有字节均有效。

  当起始位所在位置与停止位所在位置相差2时,输出给用户的最后一个数据只有最高字节有效。

  当停止位所在字节数减去起始位所在字节数等于3 或者 起始位所在字节数减去停止位所在字节数等于1时,输出给用户的最后一个数据高两字节数据均有效。

  当起始位和停止位所在的位置相等时,输出给用户最后一个数据的高三字节均有效。

  本次主要思路是使用移位寄存器对GTX IP输出数据进行暂存,然后检测帧头,并且确定起始位和停止位在数据中的位置,便于后续生成数据掩码信号。

  当检测到帧头之后,就可以根据起始位所在位置向用户输出数据了,由于模块内部没有存储结构,因此数据信号不需要等待用户的应答信号。

  本模块设计的难点在于一帧数据的最后一个数据的掩码信号和最后一个数据有效指示信号的生成,其余信号都比较简单。

2、参考代码核心部分

    //当检测到24'hBC50FB时,表示检测到同步码且检测到起始位,同步标志信号与gt_rx_data_r[1]对齐。
    //为了后续方便生成keep信号,只能将数据打两拍在进行检测。
    always@(posedge clk)begin
        if(rst)begin//初始值为0;
            commoa_flag <= 1'b0;
        end//检测到的序列是32'h50bc50bc,32'hXXXXXXfb时拉高,表示检测到同步码。
        else if((gt_rx_data_r[1][31:16] == 16'h50bc && gt_rx_char_r[1][3:2] == 2'b01) && (gt_rx_data_r[0][7:0] == 8'hfb && gt_rx_char_r[0][0] == 1'b1))begin
            commoa_flag <= 1'b1;
        end//检测到的序列是32'hBC50BCXX,32'hXXXXfb50时拉高,表示检测到同步码。
        else if((gt_rx_data_r[1][31:24] == 8'hbc && gt_rx_char_r[1][3] == 1'b1) && (gt_rx_data_r[0][15:0] == 16'hfb50 && gt_rx_char_r[0][1:0] == 2'b10))begin
            commoa_flag <= 1'b1;
        end//检测到的序列是32'h50BCXXXX,32'hXXfb50BC时拉高,表示检测到同步码。
        else if(gt_rx_data_r[0][23:0] == 24'hfb50bc && gt_rx_char_r[0][2:0] == 3'b101)begin
            commoa_flag <= 1'b1;
        end//检测到的序列是32'hBCXXXXXX,32'hfb50BC50时拉高,表示检测到同步码。
        else if(gt_rx_data_r[0][31:8] == 24'hfb50bc && gt_rx_char_r[0][3:1] == 3'b101)begin
            commoa_flag <= 1'b1;
        end
        else begin
            commoa_flag <= 1'b0;
        end
    end

    //接收数据标志信号,与gt_rx_data_r[2]对齐。
    always@(posedge clk)begin
        if(rst)begin//初始值为0;
            rx_data_flag <= 1'b0;
        end
        else if(axi_s_last)begin//当检测到停止位时拉低。
            rx_data_flag <= 1'b0;
        end
        else if(commoa_flag && gt_rx_bytealign)begin//当检测到同步码和起始位后拉高,表示一帧数据的开始。
            rx_data_flag <= 1'b1;
        end
    end

    //检测到起始位标志信号,以及标志信号位于所在数据的字节数,与gt_rx_data_r[2]对齐。
    always@(posedge clk)begin
        if(rst)begin//初始值为0;
            sof_local <= 3'd0;
        end
        else if(commoa_flag && gt_rx_bytealign)begin//检测到同步码和起始位时,记录起始位位于该数据的字节位置。
            if((gt_rx_data_r[1][7:0] == 8'hfb) && gt_rx_char_r[1][0])
                sof_local <= 3'd0;
            else if((gt_rx_data_r[1][15:8] == 8'hfb) && gt_rx_char_r[1][1])
                sof_local <= 3'd1;
            else if((gt_rx_data_r[1][23:16] == 8'hfb) && gt_rx_char_r[1][2])
                sof_local <= 3'd2;
            else if((gt_rx_data_r[1][31:24] == 8'hfb) && gt_rx_char_r[1][3])
                sof_local <= 3'd3;
        end
    end

    //检测到停止位标志信号,以及记录停止位位于该数据的字节位置。
    always@(posedge clk)begin
        if(rst)begin//初始值为0;
            eof_flag <= 1'b0;
            eof_local <= 3'd0;
        end
        else if(rx_data_flag)begin//当接收数据时,检测停止位的位置。
            if((gt_rx_data[7:0] == 8'hfd) && gt_rx_char[0])begin//检测到停止位位于最低字节。
                eof_flag <= 1'b1;
                eof_local <= 3'd0;
            end
            else if((gt_rx_data[15:8] == 8'hfd) && gt_rx_char[1])begin
                eof_flag <= 1'b1;
                eof_local <= 3'd1;
            end
            else if((gt_rx_data[23:16] == 8'hfd) && gt_rx_char[2])begin
                eof_flag <= 1'b1;
                eof_local <= 3'd2;
            end
            else if((gt_rx_data[31:24] == 8'hfd) && gt_rx_char[3])begin//检测到停止位位于最高字节。
                eof_flag <= 1'b1;
                eof_local <= 3'd3;
            end
            else begin
                eof_flag <= 1'b0;
            end
        end
        else begin
            eof_flag <= 1'b0;
        end
    end

    //生成输出数据,由于输出数据是先输出高字节数据,而接收的数据是先发送低字节数据,所以在拼接时需要进行纠正处理。
    always@(posedge clk)begin
        if(rst)begin//初始值为0;
            axi_s_data <= 32'd0;
        end
        else if(rx_data_flag)begin
            case (sof_local)//rx_data_flag与gt_rx_data_r[2]对齐,因此赋值时直接使用gt_rx_data_r[2]的信号。
                2'd0 : axi_s_data <= {gt_rx_data_r[2][15:8],gt_rx_data_r[2][23:16],gt_rx_data_r[2][31:24],gt_rx_data_r[1][7:0]};//将先接收的数据放在高字节处进行输出。
                2'd1 : axi_s_data <= {gt_rx_data_r[2][23:16],gt_rx_data_r[2][31:24],gt_rx_data_r[1][7:0],gt_rx_data_r[1][15:8]};//将先接收的数据放在高字节处进行输出。
                2'd2 : axi_s_data <= {gt_rx_data_r[2][31:24],gt_rx_data_r[1][7:0],gt_rx_data_r[1][15:8],gt_rx_data_r[1][23:16]};//将先接收的数据放在高字节处进行输出。
                2'd3 : axi_s_data <= {gt_rx_data_r[1][7:0],gt_rx_data_r[1][15:8],gt_rx_data_r[1][23:16],gt_rx_data_r[1][31:24]};//将先接收的数据放在高字节处进行输出。
                default : ;
            endcase
        end
    end
    
    //计算最后一个输出数据的掩码信号;
    always@(posedge clk)begin
        if(rst)begin//初始值为0;
            axi_s_keep_r <= 4'hf;
        end
        else if(eof_flag)begin//结束标志有效时,通过起始位和停止位的位置,计算最后一个数据的有效字节位置。
            if(eof_local - sof_local == 3'd1)begin
                axi_s_keep_r <= 4'b1111;
            end
            else if((eof_local - sof_local == 3'd2) || (sof_local - eof_local == 3'd2))begin
                axi_s_keep_r <= 4'b1000;
            end
            else if((eof_local - sof_local == 3'd3) || (sof_local - eof_local == 3'd1))begin
                axi_s_keep_r <= 4'b1100;
            end
            else if(eof_local == sof_local)begin
                axi_s_keep_r <= 4'b1110;
            end
        end
    end

    //生成数据掩码信号;
    always@(posedge clk)begin
        if(rst)begin//初始值为4'hf;
            axi_s_keep <= 4'hf;
        end//当最后一个数据刚好在停止位所在数据时,将计算结果赋值输出。
        else if(((sof_local > 1) || ((sof_local <= 1) && (((eof_local - sof_local < 2) && (eof_local >= sof_local)) || (sof_local > eof_local)))) && eof_flag_r[0])begin
            axi_s_keep <= axi_s_keep_r;
        end
        else if((((sof_local <= 1) &&  (eof_local > sof_local) && (eof_local - sof_local >= 2))) && eof_flag_r[1])begin
            axi_s_keep <= axi_s_keep_r;
        end
        else begin//其余时间输出的数据均有效。
            axi_s_keep <= 4'hf;
        end
    end

    //生成输出最后一个数据的指示信号;
    always@(posedge clk)begin
        if(rst)begin//初始值为0;
            axi_s_last <= 1'b0;
        end
        else if(sof_local > 1)begin
            if((sof_local == 3) && (eof_local == 0))//
                axi_s_last <= eof_flag;
            else
                axi_s_last <= eof_flag_r[0];
        end
        else if(((eof_local - sof_local < 2) && (eof_local >= sof_local)) || (sof_local > eof_local))begin
            axi_s_last <= eof_flag_r[0];
        end
        else if((eof_local > sof_local) && (eof_local - sof_local >= 2))begin
            axi_s_last <= eof_flag_r[1];
        end
        else begin
            axi_s_last <= 1'b0;
        end
    end

3、模块仿真

  将PHY发送和接收模块例化形成PHY顶层模块,对应代码如下所示。

    //例化PHY芯片的发送模块。
    phy_tx u_phy_tx(
        .clk		    ( i_tx_clk      ),//发送部分时钟信号;
        .rst 	        ( i_tx_rst      ),//发送部分复位信号,低电平有效;
        .axi_s_data     ( i_axi_s_data  ),//AXI的数据输入信号;
        .axi_s_keep     ( i_axi_s_keep  ),//AXI的数据掩码信号,低电平有效;
        .axi_s_last     ( i_axi_s_last  ),//AXI输入最后一个数据指示信号;
        .axi_s_valid    ( i_axi_s_valid ),//AXI输入数据有效指示信号;
        .axi_s_ready    ( o_axi_s_ready ),//AXI输入数据应答信号;
        .gt_tx_done     ( gt_tx_done    ),//GTX发送部分初始化成功,高电平有效;
        .gt_tx_data     ( gt_tx_data    ),//GTX发送数据;
        .gt_tx_char     ( gt_tx_char    ) //GTX发送数据K码指示信号,高电平有效;
    );
    
    //例化PHY芯片的接收模块。
    phy_rx u_phy_rx(
        .clk		    ( i_rx_clk          ),//接收部分时钟信号;
        .rst 	        ( i_rx_rst          ),//接收模块复位信号,低电平有效;
        .axi_s_data     ( o_axi_s_data      ),//输出接收的数据,先输出高字节数据;
        .axi_s_keep     ( o_axi_s_keep      ),//输出接收数据有效指示信号;
        .axi_s_last     ( o_axi_s_last      ),//输出一帧数据的最后一个数据指示信号;
        .axi_s_valid    ( o_axi_s_valid     ),//输出数据有效指示信号;
        .gt_rx_bytealign( gt_rx_bytealign   ),//输入数据字节对齐指示信号;
        .gt_rx_data     ( gt_rx_data        ),//输入待解析的数据;
        .gt_rx_char     ( gt_rx_char        ) //输入数据K码指示信号,高电平有效;
    );
    
endmodule

  PHY顶层模块的TestBench如下所示,可以通过修改TX_KEEP和SOF_LOCAL来确定接收模块接收数据的起始位和停止位的位置,进而验证图2中16种数据格式。

//--###############################################################################################
//--#
//--# File Name		: tb_phy_module
//--# 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_module();
    localparam	CYCLE		= 	10		        ;//系统时钟周期,单位ns,默认10ns;
    localparam	RST_TIME	= 	10		        ;//系统复位持续时间,默认10个系统时钟周期;
    localparam  TX_KEEP     =   4'b1111         ;//发送最后一个数据的有效位数,大端对齐;
    localparam  SOF_LOCAL   =   0               ;//起始位位于第几字节。
    
    reg			                clk             ;//系统时钟,默认100MHz;
    reg			                rst_n           ;//系统复位,默认低电平有效;
    reg         [7 : 0]         send_value      ;

    reg         [31 : 0]        i_axi_s_data    ;
    reg         [3 : 0]         i_axi_s_keep    ;
    reg                         i_axi_s_last    ;
    reg                         i_axi_s_valid   ;
    wire                        o_axi_s_ready   ;
    wire        [31 : 0]        gt_tx_data      ;
    wire        [3  : 0]        gt_tx_char      ;

    reg [3 : 0]  gt_tx_char_r;
    reg [31 : 0] gt_tx_data_r;//

    wire [31: 0]    gt_tx_data_o    ;
    wire [3 : 0]    gt_tx_char_o    ;

    phy_module u_phy_module(
        .i_tx_clk       ( clk           ),//发送部分时钟信号;
        .i_tx_rst_n     ( rst_n         ),//发送部分复位信号,低电平有效;
        .i_rx_clk       ( clk           ),//接收部分时钟信号;
        .i_rx_rst_n     ( rst_n         ),//发送模块复位信号,低电平有效;
        .i_axi_s_data   ( i_axi_s_data  ),//AXI的数据输入信号;
        .i_axi_s_keep   ( i_axi_s_keep  ),//AXI的数据掩码信号,低电平有效;
        .i_axi_s_last   ( i_axi_s_last  ),//AXI输入最后一个数据指示信号;
        .i_axi_s_valid  ( i_axi_s_valid ),//AXI输入数据有效指示信号;
        .o_axi_s_ready  ( o_axi_s_ready ),//AXI输入数据应答信号;
        .o_axi_s_data   (               ),//输出接收的数据,先输出高字节数据;
        .o_axi_s_keep   (               ),//输出接收数据有效指示信号;
        .o_axi_s_last   (               ),//输出一帧数据的最后一个数据指示信号;
        .o_axi_s_valid  (               ),//输出数据有效指示信号;
        .gt_tx_done     ( 1'b1          ),//GTX发送部分初始化成功,高电平有效;
        .gt_tx_data     ( gt_tx_data    ),//GTX发送数据;
        .gt_tx_char     ( gt_tx_char    ),//GTX发送数据K码指示信号,高电平有效;
        .gt_rx_bytealign( 1'b1          ),//输入数据字节对齐指示信号;
        .gt_rx_data     ( gt_tx_data_o  ),//输入待解析的数据;
        .gt_rx_char     ( gt_tx_char_o  ) //输入数据K码指示信号,高电平有效;
    );

    always@(posedge clk)begin
        gt_tx_data_r <= gt_tx_data;
        gt_tx_char_r <= gt_tx_char;
    end

    //根据起始位的位置插入不同的数据。
    generate
        if(SOF_LOCAL == 0)begin
            assign gt_tx_data_o = gt_tx_data_r;
            assign gt_tx_char_o = gt_tx_char_r;
        end
        else if(SOF_LOCAL == 1)begin
            assign gt_tx_data_o = {gt_tx_data[23:0],gt_tx_data_r[31:24]};
            assign gt_tx_char_o = {gt_tx_char[2:0],gt_tx_char_r[3]};
        end
        else if(SOF_LOCAL == 2)begin
            assign gt_tx_data_o = {gt_tx_data[15:0],gt_tx_data_r[31:16]};
            assign gt_tx_char_o = {gt_tx_char[1:0],gt_tx_char_r[3:2]};
        end
        else if(SOF_LOCAL == 3)begin
            assign gt_tx_data_o = {gt_tx_data[7:0],gt_tx_data_r[31:8]};
            assign gt_tx_char_o = {gt_tx_char[0],gt_tx_char_r[3:1]};
        end
    endgenerate

    //生成周期为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
        i_axi_s_data  = 32'd0;
        i_axi_s_keep  = 4'd0;
        i_axi_s_last  = 1'd0;
        i_axi_s_valid = 1'd0;
        wait(rst_n);//等待复位完成;
        repeat(10) @(posedge clk);
        forever begin
            phy_tx_task(21);
        end
    end

    //发送数据的任务;
    task phy_tx_task(
        input	[7 : 0]		len
    );
        begin : phy_tx_task_0
            integer i;
            i_axi_s_data  <= 32'd0;
            i_axi_s_keep  <= 4'd0;
            i_axi_s_last  <= 1'd0;
            i_axi_s_valid <= 1'd0;
            send_value <= 8'd1;
            @(posedge clk);
            wait(o_axi_s_ready);
            for(i=0 ; i<len ; i=i+1)begin
                send_value <= send_value + 1;
                i_axi_s_data <= {send_value,send_value,send_value,send_value};
                if(i == len - 1)begin
                    i_axi_s_last <= 1'b1;
                    i_axi_s_keep <= TX_KEEP;
                end
                else begin
                    i_axi_s_last <= 1'b0;
                    i_axi_s_keep <= 4'hf;
                end
                i_axi_s_valid <= 1'b1;
                @(posedge clk);
            end
            i_axi_s_data  <= 32'd0;
            i_axi_s_keep  <= 4'd0;
            i_axi_s_last  <= 1'd0;
            i_axi_s_valid <= 1'd0;
            @(posedge clk);
        end
    endtask

endmodule

  如下图示,起始位和停止位均处于最低字节,输出给用户的最后一个数据里高三字节数据均有效。

在这里插入图片描述

图3 起始位和停止位均处于最低字节

  如下图示,起始位位于最低字节,停止位位于第1字节,那么输出给用户的最后一个数据的所有位均有效,仿真结果正确。

在这里插入图片描述

图4 起始位位于第0字节,停止位位于第1字节

  如下图示,起始位位于最低字节,停止位位于第2字节,那么输出给用户的最后一个数据的只有最高字节有效,仿真结果正确。

在这里插入图片描述

图5 起始位位于第0字节,停止位位于第2字节

  如下图示,起始位位于最低字节,停止位位于第3字节,那么输出给用户的最后一个数据的高两字节均有效,仿真结果正确。

在这里插入图片描述

图6 起始位位于第0字节,停止位位于第3字节

  如下图示,起始位位于第1字节,停止位位于第0字节,那么输出给用户的最后一个数据的高两字节均有效,仿真结果正确。

在这里插入图片描述

图7 起始位位于第1字节,停止位位于第0字节

  如下图示,起始位位于第1字节,停止位位于第1字节,那么输出给用户的最后一个数据的高三字节均有效,仿真结果正确。

在这里插入图片描述

图8 起始位位于第1字节,停止位位于第1字节

  如下图示,起始位位于第1字节,停止位位于第2字节,那么输出给用户的最后一个数据的所有字节均有效,仿真结果正确。

在这里插入图片描述

图9 起始位位于第1字节,停止位位于第2字节

  如下图示,起始位位于第1字节,停止位位于第3字节,那么输出给用户的最后一个数据只有最高字节有效,仿真结果正确。

在这里插入图片描述

图10 起始位位于第1字节,停止位位于第3字节

  如下图示,起始位位于第2字节,停止位位于第0字节,那么输出给用户的最后一个数据只有最高字节有效,仿真结果正确。

在这里插入图片描述

图11 起始位位于第2字节,停止位位于第0字节

  如下图示,起始位位于第2字节,停止位位于第1字节,那么输出给用户的最后一个数据只有最高两字节有效,仿真结果正确。

在这里插入图片描述

图12 起始位位于第2字节,停止位位于第1字节

  如下图示,起始位位于第2字节,停止位位于第2字节,那么输出给用户的最后一个数据只有高三字节均有效,仿真结果正确。

在这里插入图片描述

图13 起始位位于第2字节,停止位位于第2字节

  如下图示,起始位位于第2字节,停止位位于第3字节,那么输出给用户的最后一个数据所有字节均有效,仿真结果正确。

在这里插入图片描述

图14 起始位位于第2字节,停止位位于第3字节

  如下图示,起始位位于第3字节,停止位位于第0字节,那么输出给用户的最后一个数据所有字节均有效,仿真结果正确。

在这里插入图片描述

图15 起始位位于第3字节,停止位位于第0字节

  如下图示,起始位位于第3字节,停止位位于第1字节,那么输出给用户的最后一个数据只有最高字节数据有效,仿真结果正确。

在这里插入图片描述

图16 起始位位于第3字节,停止位位于第1字节

  如下图示,起始位位于第3字节,停止位位于第2字节,那么输出给用户的最后一个数据的高两字节数据均有效,仿真结果正确。

在这里插入图片描述

图17 起始位位于第3字节,停止位位于第2字节

  如下图示,起始位位于第3字节,停止位位于第3字节,那么输出给用户的最后一个数据的高三字节数据均有效,仿真结果正确。

在这里插入图片描述

图18 起始位位于第3字节,停止位位于第3字节

  仿真到此结束,通过修改TestBench中的TX_KEEP和SOF_LOCAL参数,完成了十六种接收格式的仿真,输出给用户的数据均没有问题,仿真结束。

  本模块主要完成输出数据的检测、字对齐、掩码信号的生成,支持接收发送端的任意字节数据。

  PHY顶层模块对应的RTL视图如下所示,由于高速收发器是全双工通信,因此把发送和接收分为两个模块。

在这里插入图片描述

图19 PHY顶层模块

  关于自定义PHY接收模块的设计到此结束了,下文将GT收发器模块、自定义PH发送模块、自定义PHY接收模块综合,然后通过光口对协议进行验证。


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

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

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-06-09 17:14:01       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-06-09 17:14:01       100 阅读
  3. 在Django里面运行非项目文件

    2024-06-09 17:14:01       82 阅读
  4. Python语言-面向对象

    2024-06-09 17:14:01       91 阅读

热门阅读

  1. TypeScript常见面试题第十一节

    2024-06-09 17:14:01       27 阅读
  2. TalkingData数据统计:洞察数字世界的关键工具

    2024-06-09 17:14:01       41 阅读
  3. Django中drf动态过滤查询

    2024-06-09 17:14:01       40 阅读
  4. 006 RabbitMQ

    2024-06-09 17:14:01       36 阅读
  5. 记录一次jlink连不上cpu的情况

    2024-06-09 17:14:01       37 阅读
  6. wordpress网站建设详细过程

    2024-06-09 17:14:01       36 阅读
  7. 移动端前端开发遇到过的Andorid和IOS的差异记录

    2024-06-09 17:14:01       29 阅读
  8. Audio音频资源播放

    2024-06-09 17:14:01       45 阅读
  9. springboot + easyRules 搭建规则引擎服务

    2024-06-09 17:14:01       29 阅读