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来声明端口。这跟数字的wire、reg完全不同。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; |
这里有个容易混淆的点:real和electrical的区别。我打个比方你就明白了: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入门,说白了就是三件事:
- 分清域:离散时间用
always,连续时间用analog - 分清类型:模拟节点用
electrical,数字信号用reg/wire - 分清赋值:数字用
=/<=,模拟用<+
记住这三条,你就能看懂大部分Verilog-AMS代码了。剩下的就是多写、多跑、多踩坑。嗯,踩坑是难免的,但每踩一次,你对混合信号的理解就深一层。
下一章咱们聊「行为级建模与结构级建模」,到时候我会分享一个我当年调了三天三夜的bug案例——保证让你印象深刻。