[水] 活用Verilog中的Generate功能

前几天猛然发现自己的网站已经挂了,在法师帮助下修好之后意识到自从6月毕业以来就基本没再发过什么东西。当然主要还是因为自己作为失学儿童这段时间把精力放到补习基础知识和生活技能上去了…

言归正传,作为存活证明,今天来聊聊Verilog里Generate这功能。虽然我一直不是很喜欢Verilog,因为这个语言相比于VHDL去除了很多对可读性有较大帮助的功能,同时仍然很啰嗦:阅读OpenSparc的代码几乎让我吐了出来,每个模块所实现的逻辑很简单,但这些逻辑却被埋没在海量的胶水代码里了……吐槽放一边,由于传播和交流上的方便,Verilog仍然是现在最流行的语言,尤其是在国内。虽然一些如Bluespec这样的语言已经大大改善了Verilog的别扭之处,很多时候还是直接用Verilog比较顺手。为了写出更漂亮的,更有复用性的代码,能把Generate和System Task/Function (以后有机会再说)这样强有力的武器用起来就再好不过了。

国内网站上似乎没有太多实际使用的例子,可能主要还是因为大部分博文关注的还是更高级的功能组合的实现过程。而Generate这个功能通常是在一些比较底层的地方使用,用来减轻重复敲代码的麻烦。最重要的是它可以很轻松地实现一些参数化设计(Parameterized Design)。我曾经见过有人在Virtuoso里徒手画了一个32位的独热码转二进制电路,这个工作量想想就很不健康……如果换作Generate,这样的电路可以仅用数行代码就实现。

例一:桶式移位寄存器

桶式移位寄存器可以在 log2N 个MUX延迟内实现 N 位数的 N 次位移。在高性能处理器的执行单元,浮点数加法/归一化单元中都是必须的。Verilog中当然可以写下i <= i<<k;这样的代码,但是取决于工艺库是否支持这种功能,这句话大概率是不能直接综合的。有些工艺库中会提供桶式移位器的Layout,更多时候则没有,要自己去实现或者去买。

如果自行实现的话通常是使用MUX21作为最小单元,组织成如下图所示的结构:

A[7]-Ys_0[4]-------.   
                    D--Ys_1[9]-------.   
                 .-'                  D--Ys_2[B]-------.
A[6]-Ys_0[3]-----+-.              .--'                  D--Ys_3[F]-Y[7]
                    D--Ys_1[8]----|--.              .--'
                 .-'              |   D--Ys_2[A]----|--.
A[5]-Ys_0[2]-----+-.             .|--'              |   D--Ys_3[E]-Y[6]
                    D--Ys_1[7]---|+--.             .|--'
                 .-'             |    D--Ys_2[9]---||--.
A[4]-Ys_0[1]-----+-.            .|---'             ||   D--Ys_3[D]-Y[5]
                    D--Ys_1[6]--|+---.            .||--'
                 .-'            |     D--Ys_2[8]--|||--.
A[3] Ys_0[0]-----+-.           .|----'            |||   D--Ys_3[C]-Y[4]
                    D--Ys_1[5]-|+----.           .|||--'
                 .-'           |      D--Ys_2[7]-|||+--.
A[2]-Ys_0[3]-----+-.           |  .--'           |||    D--Ys_3[B]-Y[3]
                    D--Ys_1[4]-+--|--.           |||.--'
                 .-'              |   D--Ys_2[6]-||+|--.
A[1]-Ys_0[2]-----+-.             .|--'           || |   D--Ys_3[A]-Y[2]
                    D--Ys_1[3]---|+--.           ||.|--'
                 .-'             |    D--Ys_2[5]-|+||--.
A[0]-Ys_0[1]-----+-.            .|---'           | ||   D--Ys_3[9]-Y[1]
                    D--Ys_1[2]--|+---.           |.||--'
                 .-'            |     D--Ys_2[4]-+|||--.
     Ys_0[0]-----+             .|----'            |||   D--Ys_3[8]-Y[0]
                       Ys_1[1]-|+                .|||--'
                               |         Ys_2[3]-|||+
                               |                 |||
                    |  Ys_1[0]-+      |          |||    |
                    |                 |  Ys_2[2]-||+    |
                    |                 |          ||     |
                    |                 |          ||     |
                    |                 |  Ys_2[1]-|+     |
                    |                 |          |      |
                    |                 |          |      |
                    |                 |  Ys_2[0]-+      |
                    |                 |                 |
                    L0                L1                L2

每个D代表一个MUX。这是一个8bit位宽,3bit深度的桶式移位器,可以实现2^3=8bit的位移。因为要在不同场合用很多次,如果能参数化就最好了。因为我采用了如上图所示的命名风格。令从下往上数的2^L(L为级别)个没有连接的输入端一般连接到0(代数位移之类的特殊需求另说),Verilog在综合时综合器会自动对其进行优化。

`timescale 1ns / 100ps

module sfpu_shifter_uni #(
        parameter WIDTH = 8,
        parameter DEPTH = $clog2(WIDTH)
    )(
        input  [WIDTH-1:0]  A,
        input  [DEPTH-1:0]  S,
        output [WIDTH-1:0]  Y
    );

DEPTH这个参数使用System Function产生,$clog2等于log2(WIDTH)向上取整

    generate
        genvar GENi;
        genvar GENj;
        for(GENi = 0; GENi < DEPTH; GENi = GENi + 1) begin: gscd
            wire [WIDTH - 1 + 2**GENi:0]    As;
            wire [WIDTH - 1          :0]    Ys;
            for(GENj = 0; GENj < WIDTH; GENj = GENj + 1) begin: gscw
                sfpu_shifter_mux21 cellij (
                    .A({As[2**(GENi)+GENj], As[GENj]}),
                    .S(!S[GENi]),
                    .Y(Ys[GENj])
                    );
            end
        end
        assign gscd[0].As = {A, 1'b0};
        for(GENi = 1; GENi < DEPTH; GENi = GENi + 1) begin: gsca
            assign gscd[GENi].As = {gscd[GENi-1].Ys, {(2**GENi){1'b0}} };
        end
        assign Y = gscd[DEPTH-1].Ys; 
    endgenerate

endmodule /* sfpu_shifter_uni */

module sfpu_shifter_mux21 (
        input  [1:0]        A,
        input               S,
        output              Y
    );
    assign Y = (S)?A[1]:A[0];
endmodule /* sfpu_shifter_mux21 */

这就是所有的逻辑了,我在这里所做的事情与前文那张ASCII图片中展示的一样。Verilog在循环的包含关系上有一些比较别扭的限制,因此最后还是把连线和模块例化分开写了。

genvar产生了两个变量GENi和GENj,分别作为深度循环(gscd)和位循环(gscw)的索引。gscd和gscw这两个for循环会产生一系列模块和连线,对这些实体进行索引的方法是gscd[x].yyy,例如assign gscd[0].As = {A, 1'b0};是将GENi=0时生成的wire As连接到输入端。进行mux21的例化与相对应的输入输出接口的声明后再进行连线。实际上Verilog并不要求显式声明wire类型变量:那两句wire完全可以省略,不过有些综合器可能会产生警告。

例二:SAR控制器

SAR这个词语通常和ADC一起出现,逐次逼近寄存器(Successive-Approximation Register)描述的是一种用于逐次逼近式ADC的控制系统。它通过二分搜索的方法在内部产生一个与输入值相同的电压,根据产生这个电压的过程就可以获得输入电压的二进制表示。

SAR ADC结构[1]
SAR的搜索过程[1]

SAR ADC有以下几个非常美妙的特点:

  1. SAR ADC最少只需要“比较大小”这一种判断操作,而不需要收集更多信息
  2. 这种控制系统并不一定要服务于模拟量到数字量的转换。任何需要对系统的某个参数进行二分搜索式的修正的地方都可以使用。
  3. SAR控制逻辑的实现极其简单,一个良好的设计不需要任何独立的状态机控制模块。

其中第二点可以更详细地说一说。以飞行时间激光雷达(TOF LiDAR)中需要测量非常微小的时间差。一种常见的实现方式是利用反相器延迟来产生小于时钟周期的延迟,一长串反相器(有时被称作数字延迟线)的每个节点都被一个寄存器在同一时间捕获,从而根据门延迟推算出脉冲到来的准确时间。

一个假象中的双通道延迟链TDC,实际上会很长

在这个设计中,承担延迟功能的反相器的延迟是不可控的。生产过程中的微小偏差以及用户在使用时,供电电压、温度等因素的微小偏差都会导致该延迟产生很大的偏差。一般来说这类芯片需要出场时写入偏差值,修正因子,或者将这个参数交给用户,让用户在后端处理器中进行校准。一些比较良好的设计会像TCXO那样,通过加入额外的,易受温度等参数影响但变化趋势与待补偿电路相反的电路来抵消影响。

该设计存在一个特殊之处:时钟的频率相对而言比较准确,并且一般来说CMOS电路中产生时钟的PLL采用的压控振荡器是压控反相器环路,意味着很容易分出任意多的,十分准确的多路存在固定相位偏差的时钟。在我们的系统中,我将时钟的频率准确性作为一个不变量——一个基准,对同样是压控反相器构成的延迟链进行校准:

加入自校准功能的双通道TDC

反相器延迟链的控制电压由一个DAC产生;在校准模式下比较两个寄存器中捕获到的时钟到来的时刻的位置应该正好就是一个时钟周期在现实中对应的时间长度,如果长于4个或者短于4个都意味着存在偏差。当然还有一些别的实现方法,包括选一个通道,在片外PCB上拉一根线作为时间基准,总之需要想办法找一个相对稳定的量,然后比较延迟线产生的延迟与理想情况下的差别。

可以想象,取决于功耗等具体设计需求,现实中这样一个延迟链可能会拉的比较长,比如32位或者128位,如果一位一位地试或许不是很科学。使用类似SAR ADC的方法会比较快。

那么SAR部分是怎样实现的呢,看下面这张图:

经典SAR控制逻辑的实现(b,c)[2]

第一次看到这个设计时确实感觉挺惊艳的,更神奇的是对这个设计的诸多改进(包括将输入的信息从1bit变为nbit;防止因工作到一半信号变化而导致死锁的抗死锁电路;将那条最长的反馈路径并行化等)仍然能保持简洁和优雅。

接下来按照上面这张图用Verilog实现一个参数化的SAR控制器:

module sarcell #(parameter init = 0)(
    input       start_n,
    input       clk,
    input       a,
    input       shift,
    input       en_n,
    output reg  k
    );
    reg d;
    always @ * begin
        casex ({en_n, k})
            2'b1x:      d = k;
            2'b01:      d = a;
            2'b00:      d = shift;
            default:    d = 1'bx;
        endcase
    end
    always @ (posedge clk or negedge start_n) begin
        if(!start_n)    k <= init;
        else            k <= d;
    end

endmodule // sarcell
module DFF_ENAR (
    input       D, CLK, EN, RST,
    output reg  QP, QN
    );
    always @ (posedge CLK or posedge RST) if(EN) begin
        if(RST) begin
            QP <= 0;
            QN <= 1;
        end else begin
            QP <= D;
            QN <= !D;
        end
    end
endmodule // DFF_ENAR
module sar_test #(parameter DEPTH = 8)(
    input               start_n,
    input               comp,
    input               clk_sar,
    input               locked,
    output [DEPTH:0]    bits,
    output              stop
    );

    wire [DEPTH:0]  en_n;
    wire term_q;

    DFF_ENAR cell_termination(
        .D  (|{bits[0], term_q, locked}),
        .CLK(clk_sar),
        .EN (1),
        .RST(!start_n),
        .QP (term_q)
        );

    sarcell cell_0 (
        .start_n(start_n),
        .clk    (clk_sar),
        .a      (comp),
        .shift  (bits[1]),
        .en_n   (en_n[0]),
        .k      (bits[0])
    );
    
    generate
    genvar i;
        for(i=1; i<DEPTH; i=i+1) begin: gen_sarcell
            sarcell cell_i (
                .start_n(start_n),
                .clk    (clk_sar),
                .a      (comp),
                .shift  (bits[i+1]),
                .en_n   (en_n[i]),
                .k      (bits[i])
            );
            assign en_n[i] = |{bits[i-1], en_n[i-1]};
        end
    endgenerate
    assign en_n[DEPTH] = |{bits[DEPTH-1], en_n[DEPTH-1]};
    assign en_n[0] = stop;
    assign stop = term_q | locked;

    sarcell #(1) cell_last (
        .start_n(start_n),
        .clk    (clk_sar),
        .a      (comp),
        .shift  (1),
        .en_n   (en_n[DEPTH]),
        .k      (bits[DEPTH])
    );

endmodule // sar_test
`timescale 1ns / 10ps
module sar_test_tb();
    parameter BITS = 8;
    parameter CLK_PERIOD = 667;
    parameter SAR_PERIOD = 667*8;
    parameter HALF_CP = CLK_PERIOD/2;
    reg [BITS-1:0]  target_r;
    wire [BITS-1:0] bits;
    wire [BITS-1:0] time_in;
    wire [BITS:0]   diff;
    assign diff = $signed({1'b0, time_in} - {1'b0, target_r});
    wire locked = (diff == 0);
    wire comp = diff[BITS];

    reg clk_sar_r;
    reg start_nr;
    reg rst_nr;
    wire stop;
    integer bias_r = 10;
    assign time_in = (bits[BITS-1:0]*40*bias_r)/(2**(BITS-1)*10);

    sar_test #(BITS) uut (
        .start_n(start_nr),
        .comp   (comp),
        .clk_sar(clk_sar_r),
        .locked (locked),
        .bits   (bits),
        .stop   (stop)
    );
    initial begin
                    clk_sar_r = 0;
                    target_r = 8'd40;
    #HALF_CP        clk_sar_r = !clk_sar_r;
    #HALF_CP        clk_sar_r = !clk_sar_r;
                    start_nr = 1;
    #1              start_nr = 0;
    #2              start_nr = 1;
    #(SAR_PERIOD-3) bias_r = 40;
    #1              start_nr = 0;
    #2              start_nr = 1;
    #(SAR_PERIOD-3) bias_r = 20;
    #1              start_nr = 0;
    #2              start_nr = 1;
    #(SAR_PERIOD-3) bias_r = 5;
    #1              start_nr = 0;
    #2              start_nr = 1;

    #(SAR_PERIOD-3) $stop();
    end

    always begin
    #HALF_CP    clk_sar_r = !clk_sar_r;
    end
    
endmodule // sar_test_tb

引用

[1]. https://www.cnblogs.com/nevel/p/6151671.html

[2]. S. Lu, T. Xu, J. Chen. An Improved Solution for the Fast-Locking All Digital SAR DLL. TELKOMNIKA, Vol.11, No.4, April 13, pp. 1849-1859

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.