• 【Verilog我思我用】-generate


    【Verilog我思我用】-generate

    bc38969d38e860a74361a8d993080bef.png

    在使用xilinx官方例程《XAPP585》实现CameraLink接口发送或者接收数据时,有个程序还是值得学习的,下面把这段程序截出来:

    1. genvar i ;
    2. genvar j ;
    3. generate
    4. for (i = 0 ; i <= (N-1) ; i = i+1)
    5. begin : loop0
    6. serdes_7_to_1_diff_sdr #(
    7.        .D   (D),
    8.        .DATA_FORMAT  (DATA_FORMAT))
    9. dataout (
    10.  .dataout_p    (dataout_p[D*(i+1)-1:D*i]),
    11.  .dataout_n    (dataout_n[D*(i+1)-1:D*i]),
    12.  .clkout_p    (clkout_p[i]),
    13.  .clkout_n    (clkout_n[i]),
    14.  .txclk      (txclk),
    15.  .pixel_clk      (pixel_clk),
    16.  .reset     (reset),
    17.  .clk_pattern    (clk_pattern),
    18.  .datain    (datain[(D*(i+1)*7)-1:D*i*7]));  
    19. end
    20. endgenerate

    主要是generate的用法,整个文件的功能是实现可选多通道数据发送,我们知道Cameralink中对于多通道传输时有一部分功能代码时相同的,只不过需要多通道复用,我们知道generate有一个功能就是重复操作多个模块的实例引用,当然就适合本例程。

    下面我们先讲一讲generate的用法再结合代码简单讲解一下,对于generate其实很好理解,只不过写出来比较难。

    generate用法

    关键字generate和endgenerate(和begin / end类似)作为使用语法的起点,有三种衍生结构,分别为:

    • generate - for 语句结构

    • generate - if 语句结构

    • generate - case 语句结构

    使用generate的情况主要如下:

    • 使用 for 循环对模块进行多次相似实例化

    • 使用参数更改模块的结构或设计

    • 使用带有断言语句进行功能和形式验证

    b7f0d59c61bbe85bc73f2daa8ab8ea55.png

    在这里我们思考一下,generate是在运行中构造重复模块吗??

    答案是否定的,generate语句不是运行时构造。如果你想一想,这个generate结构实际上是在创建一个重复电路,我们不能即时添加或删除硬件电路,所以generate在综合过程中其实是重复构造相似电路,而不是在运行时构造。

    下面先按照generate结构分别举例,然后举例几个常用案例。

    generate - for语句结构

    在使用generate - for语句之前,我们需要先声明一个变量genvar,用于for循环语句进行判断。

    下面举两个不同应用的例子:

    d56aac19b6fa742220b67159dd2c82a4.png

    0a54c37f3f0aeade23fa008dc89bb3fc.png

    上面两个模块功能一样,第一个是对always 块进行了循环;第二个则是对实例化时的模块进行了循环。xorLoop 是 generate 语句模块名,目的是通过它对循环语句进行层次化引用,所以在上面栗子中的 xorLoop 模块相对层次名为 xorLoop[0].u_xor(后面会举例说明)

    这里在对比两个常见的例子:

    1f7fd5615a740f817fe39c25c18fe941.png

    上面的例子功能也一样,一个使用generate...for语句一个使用for语句,关于这两者区别我会在文章最后总结里说明,大家可以自己先思考。

    generate - if语句结构

    generate -if 语句结构比较宽松,即不需要对不需要对generate语句进行命名(generate...for主要是对循环语句进行层次化引用) ,也不需要变量genvar。由于 generate - if 语句结构是通过判断语句执行代码块,这就决定了每次最多执行一个代码块,这种情况下,可以对各个代码块使用相同命名是合法的,且有助于保持对代码的层次化引用。

    需要注意的一点是,在 generate 块中的判断条件必须是常量!

    4363d8c7ffb94bc84813d96753e1c155.png

    generate - case

    generate - case 语句和 generate - if 语句核心思想都是进行条件判断,用法基本一致。

    和 generate - if 语句一样,case 判断条件必须是常量。

    1a1a588ff73d2177b05a43db3ee3c9f5.png

    下面按照应用场景举例:

    循环生成构造

    循环生成构造提供了一种简单而简洁的方法来创建模块项的多个实例,例如模块实例、分配语句、断言、接口实例等。你可以把它想象成一台“克隆机”。

    本质上,它是一种特殊类型的for循环,其循环索引变量为 datatype genvar。这是一个有趣的事实- genvar它是一个整数数据类型,仅在综合时存在并在运行时消失。

    我们看到的《XAPP585》的例程就是这种运行结构,下面再举例看下该语句的特点:

    1. /** Example 1 */
    2. /**
    3.  * 16 input mux
    4.  *
    5.  * Example of how to use Loop Generate Construct
    6.  */
    7. module mux_16(
    8.     input  logic [0:15] [127:0] mux_in,
    9.     input  logic [3:0select,
    10.     output logic [127:0] mux_out
    11. );
    12.     logic [0:15] [127:0] temp;
    13.     // The for-loop creates 16 assign statements
    14.     genvar i;
    15.     generate
    16.         for (i=0; i < 16; i++) begin
    17.             assign temp[i] = (select == i) ? mux_in[i] : 0;
    18.         end
    19.     endgenerate
    20.     assign mux_out = temp[0] | temp[1] | temp[2] | temp[3] |
    21.                      temp[4] | temp[5] | temp[6] | temp[7] |
    22.                      temp[8] | temp[9] | temp[10] | temp[11] |
    23.                      temp[12] | temp[13] | temp[14] | temp[15];
    24. endmodule: mux_16

    仿真文件如下:

    1. `timescale 1ns/1ps
    2. /**
    3.  * Testbench to exercise the mux_16 module.
    4.  * Here we instantiate the mux 4 times. Each instance is
    5.  * fed a different input with different input `select` and
    6.  * the output is observed.
    7.  */
    8. module tb_mux_16;
    9. logic               clk;
    10. logic [0:15][127:0] test_in[4];
    11. logic [3:0]         test_select[4];
    12. logic [127:0]       test_out[4];
    13. int i, j, k;
    14. initial begin
    15.     clk = 0;
    16.     forever #1ns clk = ~clk;
    17. end
    18. initial begin
    19.     // Set inputs
    20.     for (i=0; i < 4; i++) begin
    21.         for (j=0; j < 16; j++) begin
    22.             test_in[i][j] = 127'habcd_0000 + (i << 8) + j;
    23.         end
    24.         test_select[i] = i;
    25.     end
    26.     #2ns;
    27.     // Print outputs
    28.     for(k=0; k < 4; k++) begin
    29.         $display("test_out[%0d] = 0x%x", k, test_out[k]);
    30.     end
    31.     #2ns;
    32.     // Change input select 
    33.     for (i=0; i < 4; i++) begin
    34.         test_select[i] = 10 + i;
    35.     end
    36.     #2ns;
    37.     // Print outputs again
    38.     for(k=0; k < 4; k++) begin
    39.         $display("test_out[%0d] = 0x%x", k, test_out[k]);
    40.     end
    41.     #10ns;
    42.     $finish;
    43. end
    44. genvar m;
    45. generate
    46.     for (m=0; m < 4; m++) begin: MUX
    47.        mux_16 imux_16 (
    48.            .mux_in(test_in[m]),
    49.            .select(test_select[m]),
    50.            .mux_out(test_out[m])
    51.        ); 
    52.     end
    53. endgenerate
    54. endmodule: tb_mux_16

    我们还可以嵌套generate...for 循环。只需确保genvars将外部循环和内部循环分开使用,并在嵌套的 for 循环中引用这些变量时要小心,这是一个经常犯错误的地方。

    条件生成构造

    条件生成构造允许根据在模块实例化期间传递的参数值更改设计结构。这在为设计创建参数化通用 RTL 模块时非常有用。

    一个简单的例子:

    1. /** Example 2.1 */
    2. /**
    3.  * A simple generate example. This paramerter OPERATION_TYPE,
    4.  * passed when this module is instantiated, is used to select
    5.  * the operation between inputs `a` and `b`.
    6.  */
    7. module conditional_generate
    8.     #(parameter OPERATION_TYPE = 0)
    9.     (
    10.         input  logic [31:0] a,
    11.         input  logic [31:0] b,
    12.         output logic [63:0] z
    13.     );
    14.     // The generate-endgenerate keywords are optional.
    15.     // It is the act of doing a conditional operation
    16.     // on a parameter that makes this a generate block.
    17.     generate
    18.         if (OPERATION_TYPE == 0) begin
    19.             assign z = a + b;
    20.         end
    21.         else if (OPERATION_TYPE == 1) begin
    22.             assign z = a - b;
    23.         end
    24.         else if (OPERATION_TYPE == 2) begin
    25.             assign z = (a << 1) + b; // 2a+b
    26.         end
    27.         else begin
    28.             assign z = b - a;
    29.         end
    30.     endgenerate
    31. endmodule: conditional_generate

    另一个例子 - 我们需要创建一个通用 CRC 生成器的任务。团队中的其他设计人员应该能够在 3 个多项式中选择 1 个进行 CRC 计算。

    这是一种方法 - 提供一个名为 CRC_SEL 的参数,该参数在此模块实例化时使用,此CRC_SEL参数用来选择在模块中生成哪个 CRC 函数。通过使用generate而不是简单的多路复用器,可以节省一堆门电路和触发器,因为不需要的 CRC 函数不会被实例化。

    fc8e42703e7fb0b3e73c5d8fd8121d86.png

    完整代码如下:

    1. /**
    2.  * CRC generator module. Select the desired polynomial
    3.  * using the CRC_SEL parameter.
    4.  * 
    5.  * Default polynomial : x^16 + x^15 + x^2 + 1 
    6.  * CRC_SEL = 0        : x^16 + x^1 + 1
    7.  * CRC_SEL = 1        : x^16 + x^12 + x^5 + 1
    8.  *
    9.  * USAGE:
    10.  * + Strobe `start` when driving the first valid byte
    11.  * + Strobe `done` one clk after driving the last valid byte
    12.  * + The final CRC is available 1 clk after the last valid byte
    13.  *   is driven. This is the same cycle you'll drive `done`.
    14.  *
    15.  */
    16. module crc_gen
    17.     #(parameter CRC_SEL = 0)
    18.     (
    19.         input  logic clk,
    20.         input  logic rst,
    21.         input  logic start,
    22.         input  logic done,
    23.         input  logic [7:0] data_in,
    24.         input  logic [15:0] crc_in,
    25.         output logic [15:0] crc_out
    26.     );
    27.     logic [7:0]  data_in_d;
    28.     logic [15:0] crc_in_d;
    29.     assign crc_in_d = (start | done) ? 16'd0 : crc_in;
    30.     assign data_in_d = (done) ? 8'd0 : data_in;
    31.     always_ff @(posedge clk) begin
    32.         if (rst) begin
    33.             crc_out <= 'd0;
    34.         end
    35.         else begin
    36.             // Generate blocks are always assigned a name. If
    37.             // you don't name the generate block, it will be
    38.             // given a default auto generated name.
    39.             //
    40.             // To invoke a function within a generate block,
    41.             // hierarchically call it 
    42.             // .
    43.             crc_out <= crc_poly.nextCRC16_D8(data_in_d, crc_in_d);
    44.         end
    45.     end
    46.     // Once again the generate-endgenerate keywords are optional
    47.     // It is the act of using a parameter, CRC_SEL, in the case
    48.     // statement that makes it a generate block
    49.     //
    50.     // Also notice how all the generate blocks are given the same
    51.     // name `crc_poly` and all the function names are the same
    52.     // `nextCRC16_D8`. This is correct because only one of the
    53.     // function declarations is compiled in during elaboration
    54.     // phase.
    55.     generate
    56.     case (CRC_SEL)
    57.         0
    58.         begin: crc_poly
    59.             // polynomial: x^16 + x^1 + 1
    60.             // data width: 8
    61.             // convention: the first serial bit is D[7]
    62.             function automatic [15:0] nextCRC16_D8;
    63.             
    64.                 input [7:0] Data;
    65.                 input [15:0] crc;
    66.                 reg [7:0] d;
    67.                 reg [15:0] c;
    68.                 reg [15:0] newcrc;
    69.                 d = Data;
    70.                 c = crc;
    71.                 
    72.                 newcrc[0] = d[0] ^ c[8];
    73.                 newcrc[1] = d[1] ^ d[0] ^ c[8] ^ c[9];
    74.                 newcrc[2] = d[2] ^ d[1] ^ c[9] ^ c[10];
    75.                 newcrc[3] = d[3] ^ d[2] ^ c[10] ^ c[11];
    76.                 newcrc[4] = d[4] ^ d[3] ^ c[11] ^ c[12];
    77.                 newcrc[5] = d[5] ^ d[4] ^ c[12] ^ c[13];
    78.                 newcrc[6] = d[6] ^ d[5] ^ c[13] ^ c[14];
    79.                 newcrc[7] = d[7] ^ d[6] ^ c[14] ^ c[15];
    80.                 newcrc[8] = d[7] ^ c[0] ^ c[15];
    81.                 newcrc[9] = c[1];
    82.                 newcrc[10] = c[2];
    83.                 newcrc[11] = c[3];
    84.                 newcrc[12] = c[4];
    85.                 newcrc[13] = c[5];
    86.                 newcrc[14] = c[6];
    87.                 newcrc[15] = c[7];
    88.                 nextCRC16_D8 = newcrc;
    89.             endfunction
    90.         end
    91.         1:
    92.         begin: crc_poly
    93.             // polynomial: x^16 + x^12 + x^5 + 1
    94.             // data width: 8
    95.             // convention: the first serial bit is D[7]
    96.             function automatic [15:0] nextCRC16_D8;
    97.             
    98.                 input [7:0] Data;
    99.                 input [15:0] crc;
    100.                 reg [7:0] d;
    101.                 reg [15:0] c;
    102.                 reg [15:0] newcrc;
    103.                 d = Data;
    104.                 c = crc;
    105.                 
    106.                 newcrc[0] = d[4] ^ d[0] ^ c[8] ^ c[12];
    107.                 newcrc[1] = d[5] ^ d[1] ^ c[9] ^ c[13];
    108.                 newcrc[2] = d[6] ^ d[2] ^ c[10] ^ c[14];
    109.                 newcrc[3] = d[7] ^ d[3] ^ c[11] ^ c[15];
    110.                 newcrc[4] = d[4] ^ c[12];
    111.                 newcrc[5] = d[5] ^ d[4] ^ d[0] ^ c[8] ^ c[12] ^ c[13];
    112.                 newcrc[6] = d[6] ^ d[5] ^ d[1] ^ c[9] ^ c[13] ^ c[14];
    113.                 newcrc[7] = d[7] ^ d[6] ^ d[2] ^ c[10] ^ c[14] ^ c[15];
    114.                 newcrc[8] = d[7] ^ d[3] ^ c[0] ^ c[11] ^ c[15];
    115.                 newcrc[9] = d[4] ^ c[1] ^ c[12];
    116.                 newcrc[10] = d[5] ^ c[2] ^ c[13];
    117.                 newcrc[11] = d[6] ^ c[3] ^ c[14];
    118.                 newcrc[12] = d[7] ^ d[4] ^ d[0] ^ c[4] ^ c[8] ^ c[12] ^ c[15];
    119.                 newcrc[13] = d[5] ^ d[1] ^ c[5] ^ c[9] ^ c[13];
    120.                 newcrc[14] = d[6] ^ d[2] ^ c[6] ^ c[10] ^ c[14];
    121.                 newcrc[15] = d[7] ^ d[3] ^ c[7] ^ c[11] ^ c[15];
    122.                 nextCRC16_D8 = newcrc;
    123.             endfunction
    124.         end
    125.         default
    126.             begin: crc_poly
    127.             // polynomial: x^16 + x^15 + x^2 + 1
    128.             // data width: 8
    129.             // convention: the first serial bit is D[7]
    130.             function automatic [15:0] nextCRC16_D8;
    131.             
    132.                 input [7:0] Data;
    133.                 input [15:0] crc;
    134.                 reg [7:0] d;
    135.                 reg [15:0] c;
    136.                 reg [15:0] newcrc;
    137.                 d = Data;
    138.                 c = crc;
    139.                 
    140.                 newcrc[0] = d[7] ^ d[6] ^ d[5] ^ d[4] ^ d[3] ^ d[2] ^ d[1] ^ d[0] ^ c[8] ^ c[9] ^ c[10] ^ c[11] ^ c[12] ^ c[13] ^ c[14] ^ c[15];
    141.                 newcrc[1] = d[7] ^ d[6] ^ d[5] ^ d[4] ^ d[3] ^ d[2] ^ d[1] ^ c[9] ^ c[10] ^ c[11] ^ c[12] ^ c[13] ^ c[14] ^ c[15];
    142.                 newcrc[2] = d[1] ^ d[0] ^ c[8] ^ c[9];
    143.                 newcrc[3] = d[2] ^ d[1] ^ c[9] ^ c[10];
    144.                 newcrc[4] = d[3] ^ d[2] ^ c[10] ^ c[11];
    145.                 newcrc[5] = d[4] ^ d[3] ^ c[11] ^ c[12];
    146.                 newcrc[6] = d[5] ^ d[4] ^ c[12] ^ c[13];
    147.                 newcrc[7] = d[6] ^ d[5] ^ c[13] ^ c[14];
    148.                 newcrc[8] = d[7] ^ d[6] ^ c[0] ^ c[14] ^ c[15];
    149.                 newcrc[9] = d[7] ^ c[1] ^ c[15];
    150.                 newcrc[10] = c[2];
    151.                 newcrc[11] = c[3];
    152.                 newcrc[12] = c[4];
    153.                 newcrc[13] = c[5];
    154.                 newcrc[14] = c[6];
    155.                 newcrc[15] = d[7] ^ d[6] ^ d[5] ^ d[4] ^ d[3] ^ d[2] ^ d[1] ^ d[0] ^ c[7] ^ c[8] ^ c[9] ^ c[10] ^ c[11] ^ c[12] ^ c[13] ^ c[14] ^ c[15];
    156.                 nextCRC16_D8 = newcrc;
    157.             endfunction
    158.         end
    159.     endcase
    160.     endgenerate
    161. endmodule: crc_gen

    下面是仿真文件及结果:

    1. `timescale 1ns/1ps
    2. /**
    3.  * Testbench to exercise the mux_16 module.
    4.  * Here we instantiate the mux 4 times. Each instance is
    5.  * fed a different input with different input `select` and
    6.  * the output is observed.
    7.  */
    8. module tb_mux_16;
    9. logic               clk;
    10. logic [0:15][127:0] test_in[4];
    11. logic [3:0]         test_select[4];
    12. logic [127:0]       test_out[4];
    13. int i, j, k;
    14. initial begin
    15.     clk = 0;
    16.     forever #1ns clk = ~clk;
    17. end
    18. initial begin
    19.     // Set inputs
    20.     for (i=0; i < 4; i++) begin
    21.         for (j=0; j < 16; j++) begin
    22.             test_in[i][j] = 127'habcd_0000 + (i << 8) + j;
    23.         end
    24.         test_select[i] = i;
    25.     end
    26.     #2ns;
    27.     // Print outputs
    28.     for(k=0; k < 4; k++) begin
    29.         $display("test_out[%0d] = 0x%x", k, test_out[k]);
    30.     end
    31.     #2ns;
    32.     // Change input select 
    33.     for (i=0; i < 4; i++) begin
    34.         test_select[i] = 10 + i;
    35.     end
    36.     #2ns;
    37.     // Print outputs again
    38.     for(k=0; k < 4; k++) begin
    39.         $display("test_out[%0d] = 0x%x", k, test_out[k]);
    40.     end
    41.     #10ns;
    42.     $finish;
    43. end
    44. genvar m;
    45. generate
    46.     for (m=0; m < 4; m++) begin: MUX
    47.        mux_16 imux_16 (
    48.            .mux_in(test_in[m]),
    49.            .select(test_select[m]),
    50.            .mux_out(test_out[m])
    51.        ); 
    52.     end
    53. endgenerate
    54. endmodule: tb_mux_16
    55. Footer
    56. © 2022 GitHub, Inc.
    57. Footer navigation
    58. Terms
    59. Privacy
    60. Security
    61. Status
    62. Docs
    63. Cont
    5c10dfbe7bd21b7ec43ed7e7c4025c34.png

    断言和形式验证

    generate - case 语句结构在编写断言时也非常有用,这反过来有助于形式验证。

    如果对形式验证有任何经验,那么就会知道形式工具在尝试证明属性时很快就会遇到计算界限。因此,重要的是要保持属性简短而简单。

    例如,如果有一个具有 8 个 REQquest 输入和 8 个 ACK 输出的仲裁器块,那么与其编写单个断言来覆盖所有 8 个 REQ/ACK 对,不如将其分解为具有 1 个 REQ/ACK 的 8 个单独的断言对。

    1. /** Example 3.1 */
    2. genvar k;
    3. generate
    4.     for (k=0; k < 8; k++) begin
    5.         req_a: assert property (req[k] |=> ack[k]);
    6.     end
    7. endgenerate

    分层访问生成块

    绊倒人们的一件事是如何访问位于生成块内的模块项。

    生成块有一个名字。如果不为其命名,编译器将自动分配一个通用名称,例如genblk01,genblk02通常必须转储 wave 并查看Visualizer工具以查看分配了哪些名称。

    要访问生成块中的模块项,必须分层访问它.

    这是来自 SystemVerilog LRM 1800-2012 的一个很好的示例(示例 4 第 27.5 节)。查看如何访问块中定义的任务和模块实例。

    分层实例名称为:

    1. memory.word16[3].p, memory.word16[2].p,
    2. memory.word16[1].p, memory.word16[0].p,
    1. /** Example 4 */
    2. module dimm(addr, ba, rasx, casx, csx, wex, cke, clk, dqm, data, dev_id);
    3.     parameter [31:0] MEM_WIDTH = 16, MEM_SIZE = 8;
    4.     ... 
    5.     genvar i;
    6.     case ({MEM_SIZE, MEM_WIDTH})
    7.         {32'd8, 32'd16}: // 8Meg x 16 bits wide
    8.         begin: memory
    9.             for (i=0; i<4; i=i+1) begin:word16
    10.                 sms_08b216t0 p(.clk(clk), .csb(csx), .cke(cke),.ba(ba),
    11.                     .addr(addr), .rasb(rasx), .casb(casx),
    12.                     .web(wex), .udqm(dqm[2*i+1]), .ldqm(dqm[2*i]),
    13.                     .dqi(data[15+16*i:16*i]), .dev_id(dev_id));
    14.                 // The hierarchical instance names are:
    15.                 // memory.word16[3].p, memory.word16[2].p,
    16.                 // memory.word16[1].p, memory.word16[0].p,
    17.                 // and the task memory.read_mem
    18.             end
    19.             task read_mem;
    20.                 input [31:0] address;
    21.                 output [63:0] data;
    22.                 begin // call read_mem in sms module
    23.                     word[3].p.read_mem(address, data[63:48]);
    24.                     word[2].p.read_mem(address, data[47:32]);
    25.                     word[1].p.read_mem(address, data[31:16]);
    26.                     word[0].p.read_mem(address, data[150]);
    27.                 end
    28.             endtask 
    29.         end
    30.     ...
    31.     endcase
    32. endmodule

    总结

    这篇文章是在阅读《XAPP585》代码时候看着generate语法极其方便,所以引出了该篇文章,下面说下generate...for和for的区别:

    • 首先第二个代码时错误的!

    只有当 for 循环在 generate 中时,才能将 always 放在 for 循环中!

    • generate for 循环和常规 for 循环之间的主要区别在于 generate for 循环是为每次迭代生成一个实例。这意味着在示例中将有 3 个 always 块(与常规循环情况下的 1 个块相反)。

    一个更好的例子是:

    1. module A();
    2. ..
    3. endmodule;
    4. module B();
    5. parameter NUM_OF_A_MODULES = 2// should be overriden from higher hierarchy
    6. genvar i;
    7. for (i=0 i1) {
    8.   A A_inst();
    9. }
    10. endmodule;

    在此示例中,常规 for 无法完成创建 NUM_OF_A_MODULES 实例的工作。

    参考

    https://blog.csdn.net/weixin_42150654/article/details/123132249

    夏宇闻. Verilog数字系统设计教程(第三版)[M]. 北京: 北京航空航天大学出版社, 2013: 68.

    数字IC小站, Verilog中generate的使用[EB/OL], https://zhuanlan.zhihu.com/p/107047600, 2020-02-15.

    Formal Verification - Erik Seligman, et al.

    题目练习

    HDLbits上有两道使用Generate的题目,如下:

    https://hdlbits.01xz.net/wiki/Adder100i

    https://hdlbits.01xz.net/wiki/Bcdadd100

    无需注册,可以练习Generate的语法使用,同时还有仿真。后续我会在《HDLBits: 在线学习 SystemVerilog》系列解析这两个题目。 

    0b5bab174ec0651155eaa472305e37dc.gif

  • 相关阅读:
    compose——布局居中
    c#手动签字实现
    Docker swarm 管理 secrets
    iRDMA Flow Control Introduction
    pgsql在navicate导出sql再去其他库导入时会遇到的小问题
    1、Mybatis简介
    CSS 对齐、组合选择符、伪类、伪元素、导航栏
    Python(三)数据类型转换
    分页条件搜索
    软件测试的调用接口怎么调用,逻辑是什么?
  • 原文地址:https://blog.csdn.net/Pieces_thinking/article/details/127081651