2、PLL测试环境搭建:验证平台结构、时钟生成模块、复位逻辑、配置接口(SPI/I2C)

好,咱们直接进入正题。PLL的测试环境搭建,说白了就是给你的DUT(待测设计)搭个台子,让它能跑起来、能被控制、能被观察。我做了这么多年验证,发现很多新手一上来就急着写测试用例,结果环境没搭稳,后面debug debug到怀疑人生。所以这一章,咱们把地基打牢。

2.1 验证平台整体结构

一个典型的PLL验证平台,我习惯把它分成这么几块:

  • 时钟生成模块:给PLL提供参考时钟,以及给整个平台提供工作时钟。
  • 复位逻辑:确保DUT和整个平台在正确的时间点被复位。
  • 配置接口:通过SPI或I2C协议,往PLL的寄存器里写配置字。
  • DUT本身:就是咱们要测的PLL模块。
  • 监测与比对模块:抓输出时钟,算频率、测抖动、看锁定时间。

你想想看,这五块缺一不可。少了时钟,PLL没法工作;少了复位,初始状态不确定;少了配置接口,你连让它输出多少MHz都控制不了。

核心思路:验证平台要能模拟真实芯片的工作环境,同时又要提供比真实环境更强的可控性和可观测性。

我个人习惯用SystemVerilog搭建这个平台,配合UVM的agent和monitor结构。但今天咱们先不扯UVM那套复杂的,先把最核心的模块讲清楚。

PLL验证平台结构图 时钟生成模块 ref_clk / sys_clk 复位逻辑 rst_n 生成与同步 配置接口 SPI / I2C DUT: PLL 待测设计 clk_out / lock 监测与比对 频率测量 / 抖动分析 锁定时间检测 ref_clk rst_n sclk/sda 或 scl/sda clk_out, lock 图例: 时钟信号 复位信号

2.2 时钟生成模块

时钟生成模块,我通常叫它 clk_gen。它的任务有两个:一是给PLL提供参考时钟(ref_clk),二是给验证平台的其他组件提供工作时钟(sys_clk)。

这里有个坑,我刚开始做的时候踩过。参考时钟的频率和精度必须跟芯片的规格书一致。比如规格书上说参考时钟是25MHz,精度±100ppm,那你就不能随便写个25.001MHz出来。为什么?因为PLL的输出频率 = 参考时钟 × (N/M),参考时钟偏一点,输出就偏一大截。

小技巧:我习惯在时钟生成模块里加一个enable控制。这样可以在仿真过程中随时关掉时钟,模拟时钟失效的场景,测试PLL的hold行为。

下面是一个简单的时钟生成代码示例:

// 时钟生成模块
module clk_gen #(
    parameter REF_CLK_PERIOD = 40.0,  // 25MHz -> 40ns
    parameter SYS_CLK_PERIOD = 10.0   // 100MHz -> 10ns
)(
    output reg ref_clk,
    output reg sys_clk,
    input      clk_enable
);

    initial begin
        ref_clk = 0;
        sys_clk = 0;
    end

    always #(REF_CLK_PERIOD/2.0) begin
        if (clk_enable) ref_clk = ~ref_clk;
    end

    always #(SYS_CLK_PERIOD/2.0) begin
        if (clk_enable) sys_clk = ~sys_clk;
    end

endmodule

嗯,这里要注意,#(REF_CLK_PERIOD/2.0) 这种写法在有些仿真器里可能不支持实数延迟。我建议用 #(REF_CLK_PERIOD*500) 然后配合时间单位来搞定,或者直接用 #(20ns) 这种硬编码。不过为了可复用性,还是用参数化好一些。

2.3 复位逻辑

复位逻辑,说白了就是让DUT和平台在正确的时间点进入已知状态。PLL的复位通常有两种:异步复位和同步释放。

我个人习惯用异步复位、同步释放的方式。为什么?因为PLL对复位时序比较敏感,异步复位能保证复位信号第一时间生效,同步释放能避免复位释放时出现亚稳态。

警告:千万不要在PLL还没锁定时就给它发配置命令。我曾经见过一个项目,复位释放后立马写寄存器,结果PLL还在自校准过程中,配置根本没写进去。后来查了三天才找到原因。

复位逻辑的代码示例:

// 复位生成模块 - 异步复位,同步释放
module rst_gen #(
    parameter RST_CYCLES = 10  // 复位保持10个时钟周期
)(
    input  clk,
    input  ext_rst_n,   // 外部复位输入
    output reg rst_n    // 同步复位输出
);

    reg [1:0] rst_sync;

    always @(posedge clk or negedge ext_rst_n) begin
        if (!ext_rst_n) begin
            rst_sync <= 2'b00;
            rst_n    <= 1'b0;
        end else begin
            rst_sync <= {rst_sync[0], 1'b1};
            rst_n    <= rst_sync[1];
        end
    end

endmodule

这里用了两级同步器,把外部异步复位同步到时钟域里。释放的时候,rst_n会在第二个时钟沿之后才变高,这样就避免了亚稳态。

2.4 配置接口:SPI与I2C

配置接口是PLL验证中最关键的一环。大部分PLL芯片都支持SPI或I2C协议来配置内部寄存器。咱们得在验证平台里实现一个对应的master模型。

2.4.1 SPI接口

SPI接口,我习惯用四线制:SCLK、MOSI、MISO、CS_N。PLL通常作为slave,咱们的验证平台作为master。

这里有个经验之谈:SPI的时钟频率不能太高。我见过有人把SPI时钟设到50MHz,结果PLL内部的数字逻辑根本跟不上,数据全错。一般SPI时钟设在10MHz以内比较安全。

SPI配置的时序图大概是这样:

// SPI写操作时序(CPOL=0, CPHA=0)
// SCLK:  __|‾‾|__|‾‾|__|‾‾|__|‾‾|__
// CS_N:  ‾‾‾‾‾|________________|‾‾‾
// MOSI:  XXXX|A7|A6|...|A0|D7|...|D0|XXXX
// MISO:  XXXX|Z |Z |...|Z |Z |...|Z |XXXX

我个人习惯把SPI的驱动封装成一个task,方便在测试用例里调用:

task spi_write_byte(
    input [7:0] addr,
    input [7:0] data
);
    begin
        cs_n = 0;
        // 发送地址字节(最高位为写标志)
        for (int i=7; i>=0; i--) begin
            mosi = addr[i];
            @(posedge sclk);
        end
        // 发送数据字节
        for (int i=7; i>=0; i--) begin
            mosi = data[i];
            @(posedge sclk);
        end
        cs_n = 1;
        @(posedge sclk);
    end
endtask

2.4.2 I2C接口

I2C接口比SPI复杂一些,因为它需要双向数据线(SDA)和时钟线(SCL),还有起始条件、停止条件、应答位这些概念。

我记得第一次写I2C master模型时,被应答位的时序搞得很头疼。后来总结了一个口诀:「发完8位等应答,SCL高时读SDA」。你想想看,I2C的SDA在SCL高电平时被采样,所以应答位必须在SCL高电平时读取。

I2C写操作的流程:

  1. 发送起始条件(SCL高时SDA从高变低)
  2. 发送设备地址 + 写位(7位地址 + 1位写标志)
  3. 等待从设备应答(SCL高时读SDA是否为低)
  4. 发送寄存器地址(8位)
  5. 等待应答
  6. 发送数据(8位)
  7. 等待应答
  8. 发送停止条件(SCL高时SDA从低变高)

关键点:I2C的地址是7位的,但很多PLL芯片的数据手册里写的是8位地址(左移了一位)。比如芯片地址是0x50(7位),写操作时发送0xA0(8位)。这个坑我踩过不止一次,每次都要翻数据手册确认。

下面是一个简化的I2C写操作代码:

task i2c_write_byte(
    input [6:0] dev_addr,
    input [7:0] reg_addr,
    input [7:0] data
);
    begin
        // 起始条件
        sda = 1; scl = 1; #1;
        sda = 0; #1;
        scl = 0; #1;

        // 发送设备地址 + 写位
        send_byte({dev_addr, 1'b0});
        check_ack();

        // 发送寄存器地址
        send_byte(reg_addr);
        check_ack();

        // 发送数据
        send_byte(data);
        check_ack();

        // 停止条件
        sda = 0; scl = 1; #1;
        sda = 1; #1;
    end
endtask

2.5 避坑指南与个人经验

做PLL验证环境搭建,有几个坑我几乎每次都会遇到:

  • 时钟精度问题:仿真用的时钟是理想时钟,但实际芯片里的时钟有抖动。我建议在时钟生成模块里加一个jitter注入功能,模拟真实场景。
  • 复位时序问题:PLL的复位释放后,需要等待一段时间才能开始配置。这个等待时间在数据手册里叫「power-up time」或「lock time」。我习惯在复位释放后加一个固定的延迟,比如100us,再开始发配置命令。
  • 配置顺序问题:有些PLL的寄存器有依赖关系,必须先配A再配B。我建议把配置过程封装成一个函数,按顺序调用,不要分散在测试用例里。

个人习惯:我会在验证平台里加一个「配置检查」功能。每次写完寄存器后,再读回来比对一下。这样能及时发现SPI/I2C通信错误,而不是等到最后看输出频率不对了才回头查配置。

好了,这一章的内容就到这里。环境搭好了,下一章咱们就可以开始写具体的测试用例了。记住,环境搭得越扎实,后面debug越省心。

模块 关键点 常见问题
时钟生成 频率精度、enable控制 时钟频率与规格不符
复位逻辑 异步复位同步释放 复位释放后立即配置
SPI接口 时钟频率、CPOL/CPHA 时钟频率过高导致数据错误
I2C接口 地址左移、应答位时序 7位地址与8位地址混淆

可可精品资料库