内部信号

内部信号示例

内部信号是指未在模块IO端口中暴露,但在模块内部承担控制、数据传输或状态跟踪等功能的信号。通常,picker在将RTL转换为DUT时只会自动暴露IO端口,内部信号不会被主动导出。

但在需要对模块内部逻辑进行更细致验证,或根据已知bug进一步定位问题时,验证人员往往需要访问这些内部信号。除了传统的verilator和VCS等工具,picker还提供了内部信号提取机制,可作为辅助手段。

动机

以上限计数器为例:


module UpperCounter (
    input wire clk,           
    input wire reset,         
    output reg [3:0] count   
);
    wire upper;

    assign upper = (count == 4'b1111);

    always @(posedge clk) begin
        if (reset) begin
            count = 4'b0000;
        end else if (!upper) begin
            count = count + 1;
        end
    end
endmodule

模块的IO信号指的是直接写在模块定义中的信号,也就是:

module UpperCounter (
    input wire clk,           
    input wire reset,         
    output reg [3:0] count   
);

该部分中的 clk、reset 和 count 是 IO 信号,可以直接暴露访问。而紧接着的 wire upper; 则属于内部信号,其取值由模块输入和模块内部逻辑共同决定。本案例中的计数器逻辑较为简单,但对于更大规模的硬件模块,常常会遇到以下难题:

  • 当模块输出与预期不符时,问题范围较大,难以及时定位,需要有效手段快速缩小排查范围;
  • 模块内部逻辑复杂,理解和分析存在困难,此时也需要借助内部信号作为关键标记,理清模块运行机制。

针对上述问题,访问和分析内部信号是非常有效的手段。传统上,通常借助如 Verilator、VCS 等仿真工具来查看内部信号。为进一步降低验证门槛,picker 还提供了三种内部信号访问方式:DPI 直接导出、VPI 动态访问和直接内存读写。

DPI 直接导出

DPI即Direct Programming Interface,是verilog与其他语言交互的接口,在picker的默认实现中,支持了为待测硬件模块的IO端口提供DPI。在执行picker时,如果添加了--internal 选项,则可同样为待测模块的内部信号提供DPI。此时,picker将会基于预定义的内部信号文件,在将verilog转化为DUT时,同步抽取rtl中的内部信号和IO端口一并暴露出来。

编写信号文件

信号文件是我们向picker指定需要提取的内部信号的媒介,它规定了需提取内部信号的模块和该模块需要提取的内部信号。

示例internal.yaml,内容如下:

UpperCounter:
  - "wire upper"

第一行是模块名称,如UpperCounter,第二行开始是需要提取的模块内部信号,以“类型 信号名”的格式写出。比如,upper的类型为wire,我们就写成“wire upper” (理论上只要信号名符合verilog代码中的变量名就可以匹配到对应的信号,类型随便写都没问题,但还是建议写verilog语法支持的类型,比如wire、log、logic等)

内部信号提取的能力取决于模拟器,譬如,verilator就无法提取下划线_开头的信号。

注:多位宽的内部信号需要显式写出位宽,所以实际的格式是“类型 [宽度] 信号名”

UpperCounter:
  - "wire upper"
  - "reg [3:0] another_multiples" # 本案例中这个信号不存在,只是用于说明yaml的格式

选项支持

写好信号文件之后,需要在运行picker时显式指定内部文件,这通过internal选项完成:

--internal=[internal_signal_file]

完整命令如下:

picker export --autobuild=true upper_counter.sv -w upper_counter.fst --sname UpperCounter \
--tdir picker_out_upper_counter/ --lang python -e --sim verilator --internal=internal.yaml

我们可以找到picker为DUT配套生成的signals.json文件:

{
    "UpperCounter_upper": {
        "High": -1,
        "Low": 0,
        "Pin": "wire",
        "_": true
    },
    "clk": {
        "High": -1,
        "Low": 0,
        "Pin": "input",
        "_": true
    },
    "count": {
        "High": 3,
        "Low": 0,
        "Pin": "output",
        "_": true
    },
    "reset": {
        "High": -1,
        "Low": 0,
        "Pin": "input",
        "_": true
    }
}

这个文件展示了picker生成的信号接口,可以看到,第一个信号UpperCounter_upper就是我们需要提取的内部信号, 其中第一个下划线之前的部分是我们在internal.yaml中的第一行定义的模块名UpperCounter,后面的部分则是内部信号名。

信号访问

picker完成提取之后,内部信号的访问和io信号的访问就没有什么区别了,本质上他们都是dut上的一个XData,使用“dut.信号名”的方式访问即可。

from UpperCounter import *

def test():
    dut = DUTUpperCounter()
    print(dut.UpperCounter_upper.value)

VPI动态访问

VPI(Verilog Procedural Interface)是Verilog语言的一种标准接口,用于在仿真时让C语言等外部程序与Verilog仿真器进行交互。通过VPI,用户可以在C程序中访问、读取、修改Verilog仿真中的信号、变量、模块实例等信息,还可以注册回调函数,实现对仿真过程的控制和扩展。VPI常用于开发自定义系统任务、实现高级验证功能、动态信号访问和波形处理等。VPI 是 IEEE 1364 标准的一部分。

选项支持

picker export --help
...
--vpi Enable VPI, for flexible internal signal access default is OFF

可通过参数--vpi开启VPI支持,例如:

picker export upper_counter.sv --sname UpperCounter --tdir picker_out_upper_counter/ --lang python --vpi

信号访问

开启--vpi后,可通过DUT的接口dut.GetInternalSignalList(use_vpi=True)列出所有内部可访问信号,通过dut.GetInternalSignal(name, use_vpi=True)动态构建XData进行数据访问。

from UpperCounter import *

def test():
    dut = DUTUpperCounter()
    # 列出所有内部信号
    # 或者通过 dut.VPIInternalSignalList()
    dut.GetInternalSignalList(use_vpi=True)
    # 动态构建 XData
    internal_upper = dut.GetInternalSignal("UpperCounter.upper", use_vpi=True)
    # 读访问
    print(internal_upper.value)
    # 写访问 (虽然能写入,但是dut step后值会被覆盖,不建议对非reg类型进行写操作)
    internal_upper.value = 0x1

直接内存读写

无论是基于DPI还是VPI进行内部信号访问都有一定的性能开销,为了实现极致性能体验,picker针对verilator/GSIM仿真器实现了内部信号直接访问。

选项支持

picker export --help
...
--rw,--access-mode ENUM:value in {dpi->0,mem_direct->1} OR {0,1}

可通过参数--rw 1开启针对verilator仿真器内部信号直接读写功能,例如:

picker export upper_counter.sv --sname UpperCounter --tdir picker_out_upper_counter/ --lang python --rw 1

信号访问

开启直接内存读写后,可通过dut.GetInternalSignalList(use_vpi=False)列出所有内部信号,通过dut.GetInternalSignal(name, use_vpi=False)动态构建XData实现信号读写。

from UpperCounter import *

def test():
    dut = DUTUpperCounter()
    # 列出所有内部信号
    dut.GetInternalSignalList(use_vpi=False)
    # 动态构建 XData
    internal_upper = dut.GetInternalSignal("UpperCounter_top.UpperCounter.upper", use_vpi=False)
    # 读访问
    print(internal_upper.value)
    # 写访问 (虽然能写入,但是dut step后值会被覆盖,不建议对非reg类型进行写操作)
    internal_upper.value = 0x1

内部信号访问方法对比

picker提供的每种内部信号访问方法都有各自的优缺点,需要按需要进行选择。

方法名称 开启参数 优点 缺点 访问接口 支持仿真器 适用场景
DPI 直接导出 –internal=cfg.yaml 速度快 需要提前指定信号
信号只读
修改后需要重新编译
无(同普通引脚) verilator、VCS 信号少,不需要写操作
VPI动态访问 –vpi 灵活,信号全
不需要提前指定信号
速度慢 GetInternalSignalList
GetInternalSignal
verilator、VCS 小规模电路或不在意仿真速度
直接内存读写 –rw 1 速度快
灵活
不需要提前指定信号
部分信号可能被优化掉 GetInternalSignalList
GetInternalSignal
verilator、GSIM 大规模电路,例如整个香山核

*注: 上述方法彼此独立,可以混用

最后修改 May 7, 2025: fix(multilang): sync en with cn (963533f)