Modeling Digital Buses in Verilog-A

It is generally preferred to use Verilog-AMS when simulating mixed-signal systems. However, sometimes that is not possible. This tutorial demonstrates a reasonably efficient and robust way to convert the value of an electrical bus into an integer. Before starting this tutorial, you should read Functional Modeling.

Input Buses

There are a two challenges that must be overcome to implement electrical digital input buses in Verilog-A:

  1. The threshold crossings must be resolved to avoid undesirable delays in recognizing the value of the bus.

  2. The individual bit values must be combined into a single integer.

Consider the following model of an 8-bit DAC:

module dac (out_p, out_n, in, enable, vdda, gnda);
output out_p, out_n; electrical out_p, out_n;
input [7:0] in; electrical [7:0] in;
input enable; electrical enable;
input vdda; electrical vdda;
input gnda; electrical gnda;
integer code;
real value;
genvar i;

analog begin
    // convert the input to a signed integer
    code = 0;
    for (i = 0; i < 8; i = i + 1) begin
        @(cross(V(in[i]) - V(vdda)/2))
            ;
        if (V(in[i]) > V(vdda)/2)
            code = code + (1 << i);
    if (code >= 128)
        code = code - 256;

    // implement enable
    @(cross(V(enable) - V(vdda)/2))
        ;
    if (V(enable) < V(vdda)/2)
        value = 0;
    else
        value = code/256.0;

    // drive the differential output
    V(out_p) <+ V(vdda)/2 + transition(value/2, 0, 10n);
    V(out_n) <+ V(vdda)/2 - transition(value/2, 0, 10n);
end
endmodule

This model starts by applying a threshold to the individual lines in an electrical array and combining the resulting Booleans into an integer. The electrical array is in and the integer is code. The if statement computes the sign of the integer. It is only needed if in represents a signed number. Specifically, it should be deleted in in is an unsigned number.

The next block of code implements the enable feature and scales the output to the desired range.

The final block of code drives the differential outputs with the computed result. Notice that transition functions are used to smooth the otherwise discontinuous output signal.

The conversion from electrical signal to Booleans includes use of cross functions to assure that the threshold crossings are properly resolved.

The for loop in the first block is genvar loop. Such loops are fully expanded before the simulation begins. That loop expands as follows:

// convert the input to a signed integer
code = 0;
@(cross(V(in[0]) - V(vdda)/2))
    ;
if (V(in[0]) > V(vdda)/2)
    code = code + (1 << 0);
@(cross(V(in[1]) - V(vdda)/2))
    ;
if (V(in[1]) > V(vdda)/2)
    code = code + (1 << 1);

...

@(cross(V(in[7]) - V(vdda)/2))
    ;
if (V(in[7]) > V(vdda)/2)
    code = code + (1 << 7);

This DAC updates immediately after the input changes. It is possible to modify this model so it updates on a clock edge:

module dac (out_p, out_n, in, clock, enable, vdda, gnda);
output out_p, out_n; electrical out_p, out_n;
input [7:0] in; electrical [7:0] in;
input clock; electrical clock;
input enable; electrical enable;
input vdda; electrical vdda;
input gnda; electrical gnda;
integer code, en;
real value;
genvar i;

analog begin
    // convert the input to a signed integer on positive clock edge
    @(cross(V(clock) - V(vdda)/2), +1) begin
        code = 0;
        for (i = 0; i < 8; i = i + 1) begin
            @(cross(V(in[i]) - V(vdda)/2));
            if (V(in[i]) > V(vdda)/2)
                code = code + (1 << i);
        if (code >= 128)
            code = code - 256;
        value = code/256.0;
    end

    // reset output value when disabled
    @(cross(V(enable) - V(vdda)/2))
        ;
    if (V(enable) < V(vdda)/2)
        value = 0;

    // drive the differential output
    V(out_p) <+ V(vdda)/2 + transition(value/2, 0, 10n);
    V(out_n) <+ V(vdda)/2 - transition(value/2, 0, 10n);
end
endmodule

In this model the input in is sampled on the rising edge of the clock and it is the sampled value that drives the output.

Output Buses

The challenges that must be overcome to implement electrical digital output buses in Verilog-A are:

  1. An integer must be decomposed into its individual bits.

  2. The abrupt and discontinuous transitions in the value of the output bits must be smoothed with well controlled.

This process is demonstrated with the following model of an 6-bit ADC:

module adc (out, in, clk);
output [5:0] out; input in, clk;
electrical [5:0] out; electrical in, clk;
parameter real vh = 1;
parameter real vth = vh/2;
parameter real tt = 100n from (0:inf);
integer result;
genvar i;

analog begin
    @(cross(V(clk) - vth, +1)) begin
        result = 64*(V(in)+1)/2;
        if (result > 63) result = 63;
        else if (result < 0) result = 0;
    end

    for (i=0; i<6; i=i+1)
        V(out[i]) <+ transition(result & (1<<i) ? vh : 0, 0, tt);
end
endmodule

The first part of this model is an event block that samples the voltage of the input pin at rising edges of the clock. The actual analog-to-digital conversion is performed by offsetting and scaling the input voltage and then casting the value to an integer. This line converts input values from the range −1 V ⋯ 1 V to integers in the range 0 ⋯ 64. Finally, the integer is then clipped so that its value always falls within the range 0 to 63, the normal operating range of the ADC. You can eliminate the +1 to get a signed output, but you must also change the clipping range.

The second part of the model is a genvar loop that splits result into its individual bits and drives them onto the output bus. This is performed with the help of the left-shift operator, the bitwise and operator, and the inline conditional operator. To see how this works, expand the genvar loop:

V(out[0]) <+ transition(result & (1<<0) ? vh : 0, 0, tt);
V(out[1]) <+ transition(result & (1<<1) ? vh : 0, 0, tt);
V(out[2]) <+ transition(result & (1<<2) ? vh : 0, 0, tt);
V(out[3]) <+ transition(result & (1<<3) ? vh : 0, 0, tt);
V(out[4]) <+ transition(result & (1<<4) ? vh : 0, 0, tt);
V(out[5]) <+ transition(result & (1<<5) ? vh : 0, 0, tt);

Now, evaluate the left shift operator:

V(out[0]) <+ transition(result & ('b000001) ? vh : 0, 0, tt);
V(out[1]) <+ transition(result & ('b000010) ? vh : 0, 0, tt);
V(out[2]) <+ transition(result & ('b000100) ? vh : 0, 0, tt);
V(out[3]) <+ transition(result & ('b001000) ? vh : 0, 0, tt);
V(out[4]) <+ transition(result & ('b010000) ? vh : 0, 0, tt);
V(out[5]) <+ transition(result & ('b100000) ? vh : 0, 0, tt);

Assume that the value of result is 58, which in binary is ‘b111010. Consider bit 0. ‘b111010 & ‘b000001 is ‘b000000, which is a logical false and so the inline conditional operator evaluates to its third argument, 0. Now consider bit 1. ‘b111010 & ‘b000010 is ‘b000010, which is a logical true and so the inline conditional operator evaluates to its second argument, vh. You can follow this logic to its conclusion to see that the values of the output bus will be {vh, vh, vh, 0, vh, 0}.

Finally, the transition function smooths and controls each output transition.