3、Verilog-AMS入门:语言基础、模块定义、数据类型、离散时间与连续时间建模

好,咱们开始聊Verilog-AMS。说实话,我第一次接触这玩意儿的时候,心里是有点抵触的。你想啊,搞数字的用Verilog,搞模拟的用SPICE,各玩各的多好。但后来在项目里被逼着做数模混合仿真,才发现——嗯,真香。

Verilog-AMS,说白了就是Verilog的超级增强版。它不光能描述数字电路那种0和1的跳变,还能描述模拟信号那种连续变化的电压、电流。我习惯把它叫做「一统江湖」的语言。今天咱们就把它最核心的几个概念捋清楚。

3.1 语言基础:它到底是个啥?

Verilog-AMS = Verilog + Verilog-A + 混合信号扩展。Verilog-A是专门搞模拟的,Verilog是搞数字的,合在一起就成了AMS。

它的基本结构跟普通Verilog很像,但多了几个关键词。比如:

  • module/endmodule:模块定义,跟数字Verilog一样
  • electrical:声明一个模拟节点(比如一根导线上的电压)
  • analog:描述连续时间行为的块
  • digital:描述离散时间行为的块(其实还是用always块)

我刚开始学的时候,最不习惯的就是它同时支持两种时间域。你想想看,一个模块里既有@(posedge clk)这种事件驱动的写法,又有V(out) <+ V(in);这种连续赋值的写法。刚开始确实容易晕。

3.2 模块定义:从数字到模拟的跨越

模块定义的基本骨架,跟Verilog一样。但端口声明多了个花样。

module resistor (p, n);
    inout p, n;
    electrical p, n;
    parameter real R = 1.0;

    analog begin
        I(p, n) <+ V(p, n) / R;
    end
endmodule

你看,这里用了electrical来声明端口。这跟数字的wirereg完全不同。electrical表示这个节点是模拟的,可以承载连续的电压、电流值。

我有个小建议:模拟端口尽量用inout类型。为什么?因为模拟信号是双向的,电流可以流进流出。你想想看,一个电阻的两个端口,你能说哪个是输入哪个是输出吗?不能。所以用inout最合适。

避坑指南:我曾经在定义模拟端口时用了input,结果仿真器报了一堆警告。后来才明白,模拟端口用inout才是标准做法。数字端口才用input/output

3.3 数据类型:数字的归数字,模拟的归模拟

Verilog-AMS的数据类型,我习惯分成两派:

类型 说明 例子
wire, reg, integer 离散时间 跟标准Verilog一样 reg [7:0] data;
real 连续时间 模拟信号的值 real v_out;
electrical 连续时间 模拟节点(电压/电流) electrical net;
nature 连续时间 定义物理量类型 nature voltage;

这里有个容易混淆的点:realelectrical的区别。我打个比方你就明白了:real就像你写在纸上的一个数值,它不会自动变化;而electrical就像电路板上的一根导线,仿真器会自动计算它上面的电压。

说白了,electrical是「活的」,real是「死的」。在模拟块里,你只能用electrical来连接电路元件。

3.4 离散时间建模:数字的老本行

离散时间建模,其实就是咱们熟悉的数字Verilog。用always块、@(posedge clk)这些。但在Verilog-AMS里,它被叫做「离散时间域」。

module counter (clk, rst, count);
    input clk, rst;
    output reg [3:0] count;

    always @(posedge clk or posedge rst) begin
        if (rst)
            count <= 0;
        else
            count <= count + 1;
    end
endmodule

这段代码你肯定很眼熟。在Verilog-AMS里,它依然有效。但要注意:离散时间块和连续时间块不能直接混用变量。你不能在always块里直接赋值给electrical节点,也不能在analog块里直接赋值给reg

那怎么交互呢?用V()I()函数来读取模拟值,用$bound_step()来控制仿真步长。嗯,这个后面再细聊。

3.5 连续时间建模:模拟的灵魂

连续时间建模,这才是Verilog-AMS的精髓。它用analog块来描述电路的行为。

module capacitor (p, n);
    inout p, n;
    electrical p, n;
    parameter real C = 1e-12;

    analog begin
        I(p, n) <+ C * ddt(V(p, n));
    end
endmodule

你看,这里用了ddt()函数,表示对电压求导。电流等于电容乘以电压的变化率——这不就是电容的V-I特性嘛。

我刚开始写模拟块的时候,最不习惯的就是那个<+符号。它不是赋值,是「贡献」。意思是把电流贡献到这个节点上。如果有多个元件连接到同一个节点,它们的贡献会自动叠加。

核心概念<+是模拟块的赋值运算符,表示「贡献」。它跟数字的=完全不同。数字赋值是覆盖,模拟贡献是叠加。

3.6 混合信号建模:把两个世界连起来

真正的挑战来了——怎么把数字和模拟连在一起?

Verilog-AMS提供了几个桥梁:

  • $bound_step():告诉仿真器,模拟步长不能超过这个值。我习惯在数字信号跳变的地方用这个,保证仿真精度。
  • V() / I():在数字块里读取模拟节点的电压或电流
  • cross() / above():检测模拟信号是否越过某个阈值

举个例子,一个简单的模数转换器:

module adc (vin, clk, dout);
    input clk;
    inout vin;
    electrical vin;
    output reg [7:0] dout;

    real v_sample;

    analog begin
        @(cross(V(vin) - 0.5, +1));  // 检测上升沿
        v_sample = V(vin);
    end

    always @(posedge clk) begin
        dout <= $floor(v_sample * 255 / 3.3);
    end
endmodule

这里用了cross()函数来检测模拟信号的变化。当电压超过0.5V时,触发采样。然后数字块在时钟上升沿读取采样值,转换成数字码。

注意cross()函数很强大,但也很耗仿真资源。我曾经在一个项目里用了太多cross(),结果仿真跑了一整天都没结束。后来改用$bound_step()加定时采样,速度快了10倍。所以我的建议是:能用定时采样就别用cross()

3.7 实战经验:从零搭一个简单的混合信号模型

咱们来搭一个RC低通滤波器,后面接一个比较器。这个例子我当年做项目时经常用,用来验证数字模块的输入信号质量。

module rc_comparator (vin, vout);
    inout vin;
    output vout;
    electrical vin, vout;
    electrical vcap;

    parameter real R = 1e3;
    parameter real C = 1e-9;

    // RC低通
    analog begin
        I(vin, vcap) <+ (V(vin) - V(vcap)) / R;
        I(vcap, 0) <+ C * ddt(V(vcap));
    end

    // 比较器
    analog begin
        if (V(vcap) > 1.0)
            V(vout) <+ 3.3;
        else
            V(vout) <+ 0.0;
    end
endmodule

你看,这个模型里既有电阻电容的连续行为,又有比较器的阈值判断。仿真器会自动处理时间步长,保证精度。

我个人的习惯是:先搭模拟部分,再搭数字部分,最后用cross()$bound_step()连接。这样调试起来思路清晰,不会乱。

3.8 小结

Verilog-AMS入门,说白了就是三件事:

  1. 分清域:离散时间用always,连续时间用analog
  2. 分清类型:模拟节点用electrical,数字信号用reg/wire
  3. 分清赋值:数字用=/<=,模拟用<+

记住这三条,你就能看懂大部分Verilog-AMS代码了。剩下的就是多写、多跑、多踩坑。嗯,踩坑是难免的,但每踩一次,你对混合信号的理解就深一层。

下一章咱们聊「行为级建模与结构级建模」,到时候我会分享一个我当年调了三天三夜的bug案例——保证让你印象深刻。