3. Verilog行为级建模:PLL行为级模型的作用

各位同学,今天我们来聊聊PLL的行为级建模。说实话,我刚入行那会儿,总觉得行为级模型就是个“假东西”——又不是真正的电路,写它干嘛?直到有一次,我在一个SoC项目中,顶层仿真跑了整整三天还没跑完,一查原因,罪魁祸首就是那个PLL的晶体管级模型。从那以后,我再也不敢小看行为级模型了。

3.1 为什么需要PLL行为级模型?

PLL行为级模型,说白了就是个“替身”。它不关心内部电荷泵怎么工作、VCO怎么振荡,它只关心一件事:输入多少频率,输出多少频率,锁定需要多久。

我个人习惯,在项目初期就搭好PLL的行为级模型。原因有三:

  • 仿真速度快——行为级模型比晶体管级快几个数量级。你想想看,一个PLL锁定时间可能几百微秒,用晶体管级仿真,等到花儿都谢了。
  • 功能验证早——芯片其他模块需要PLL提供的时钟才能工作。没有行为级模型,你只能干等。
  • 调试方便——行为级模型内部信号都是可见的,出了bug一眼就能看出来。

核心观点:行为级模型不是偷懒,而是聪明的工程选择。用20%的工作量,解决80%的仿真效率问题。

3.2 使用`timescale设置时间精度

写PLL模型,第一件事就是设置时间单位和精度。我见过不少新手在这里翻车——仿真出来的波形对不上,查了半天发现是`timescale设错了。

`timescale 1ns / 1ps

这行代码的意思是:时间单位是1ns,时间精度是1ps。为什么精度要设得比单位小?因为PLL的抖动可能只有几个皮秒,精度不够的话,这些细节就丢了。

注意:不同模块的`timescale设置不一致,会导致仿真结果混乱。我建议整个项目统一使用`timescale 1ns/1ps。

3.3 生成参考时钟

参考时钟是PLL的输入,通常来自晶振。行为级建模时,我们用always块来生成它:

module ref_clk_gen(
    output reg ref_clk
);

parameter REF_FREQ = 50_000_000; // 50MHz
real half_period;

initial begin
    half_period = 1_000_000_000.0 / (REF_FREQ * 2); // 单位:ns
    ref_clk = 0;
    forever #half_period ref_clk = ~ref_clk;
end

endmodule

这里有个小技巧:用real类型来存储半周期时间,避免整数除法带来的精度损失。我曾经在一个项目中,因为用了整数除法,50MHz的时钟生生变成了49.9MHz,仿真跑了半天才发现问题。

3.4 模拟VCO输出

VCO(压控振荡器)是PLL的核心。行为级建模时,我们不需要真的去模拟振荡器电路,只需要根据控制电压输出对应的频率。

module vco_model(
    input vctrl,
    output reg vco_clk
);

parameter VCO_GAIN = 100e6; // 100MHz/V
parameter VCO_CENTER = 500e6; // 500MHz
real freq, half_period;

initial begin
    vco_clk = 0;
end

always @(vctrl) begin
    freq = VCO_CENTER + VCO_GAIN * vctrl;
    half_period = 1_000_000_000.0 / (freq * 2);
end

always begin
    #half_period vco_clk = ~vco_clk;
end

endmodule

嗯,这里要注意:always @(vctrl)块里更新频率,另一个always块里生成时钟。为什么分开写?因为频率变化时,时钟周期要立即更新,不能等到下一个时钟沿。

避坑指南:我曾经在VCO模型里直接用#(half_period),结果发现仿真器报错——因为half_period是变量,不是常量。正确的做法是用#half_period,Verilog允许变量作为延时值。

3.5 分频器行为建模

分频器相对简单,就是把高频时钟变成低频时钟。行为级建模时,我们通常用计数器来实现:

module divider_model(
    input clk_in,
    input rst_n,
    output reg clk_out
);

parameter DIV_RATIO = 8; // 分频比
reg [7:0] cnt;

always @(posedge clk_in or negedge rst_n) begin
    if (!rst_n) begin
        cnt <= 0;
        clk_out <= 0;
    end else begin
        if (cnt == (DIV_RATIO/2 - 1)) begin
            cnt <= 0;
            clk_out <= ~clk_out;
        end else begin
            cnt <= cnt + 1;
        end
    end
end

endmodule

这个模型实现了50%占空比的分频。如果分频比是奇数怎么办?嗯,那就需要稍微复杂一点的逻辑——先上升沿计数,再下降沿计数,最后组合输出。我在一个项目中做过奇数分频,代码大概多了三行,但调试花了我半天。

3.6 完整的PLL行为级模型

把上面几个模块拼起来,就是一个完整的PLL行为级模型了。我习惯用一张图来展示它们的关系:

参考时钟 PFD 鉴频鉴相器 LPF 环路滤波器 VCO 压控振荡器 输出 分频器 (÷N) 反馈路径

这张图展示了PLL的基本结构。行为级模型就是把每个框框用Verilog实现出来,然后连在一起。你想想看,有了这个模型,你在RTL仿真阶段就能验证整个芯片的时钟方案,不用等到后端做完才发现问题。

3.7 行为级模型的验证要点

模型写完了,怎么验证它对不对?我总结了几点:

验证项目 检查内容 常见问题
锁定时间 从复位到输出稳定,时间是否符合预期 锁定时间过长或过短
输出频率 测量输出时钟频率,是否等于参考频率×分频比 频率偏差超过1%
占空比 高电平时间是否等于低电平时间 奇数分频时占空比不是50%
抖动 时钟周期是否稳定 行为级模型通常没有抖动,需要手动添加

个人经验:验证PLL行为级模型时,我习惯写一个自检脚本。让模型跑1000个周期,统计平均频率和周期抖动。如果频率误差在0.1%以内,就算通过。这个脚本我用了好几年,帮我抓到了不少bug。

好了,关于PLL行为级建模,今天就聊到这里。记住一句话:行为级模型是仿真加速器,不是偷懒的借口。写得好的行为级模型,能让你的项目进度快上一倍。

课后练习:试着写一个支持可编程分频比(4/5/6/8)的PLL行为级模型,并用一个测试平台验证它的锁定时间和输出频率是否正确。

可可精品资料库