FPGA线上课程平台|最全栈的FPGA学习平台|FPGA工程师认证培训
登录
首页-技术文章/快讯-技术分享-正文

Verilog Testbench自动化:利用Python脚本生成激励与解析仿真结果

FPGA小白FPGA小白
技术分享
4小时前
0
0
2

在FPGA验证流程中,Testbench的编写与仿真结果分析是耗时且易错的关键环节。传统手动编写激励和人工比对波形的方式,在面对复杂协议、大量测试向量或回归测试时效率低下。本文介绍一种基于Python脚本的自动化方案,旨在构建一个可复用、可扩展的验证框架,显著提升验证效率与可靠性。

Quick Start

  • 步骤一:准备环境。确保系统已安装Python 3.8+、Icarus Verilog(或Modelsim/QuestaSim/VCS)仿真器。
  • 步骤二:创建项目目录结构:project/rtl/存放设计文件,project/tb/存放Testbenchproject/scripts/存放Python脚本。
  • 步骤三:编写一个简单的DUT(Design Under Test),例如一个8位加法器(adder.v)并放入rtl/目录。
  • 步骤四:在scripts/目录下创建Python脚本gen_stimulus.py,用于生成随机或特定模式的测试向量,并输出为stimulus.txt文件。
  • 步骤五:编写顶层Testbench文件tb_adder.v,使用$readmemh$fopen读取stimulus.txt文件中的激励,并应用给DUT。
  • 步骤六:在Testbench中使用$fdisplay$fwrite将DUT的输出(以及时间戳、输入等)写入日志文件sim_output.log
  • 步骤七:创建Python脚本parse_results.py,读取并解析sim_output.log文件,自动计算功能正确性、统计覆盖率或性能指标。
  • 步骤八:编写一个Shell脚本(如run_sim.sh)或直接在Python脚本中调用子进程,顺序执行:生成激励 → 编译仿真 → 运行仿真 → 解析结果。
  • 步骤九:运行自动化流程。在终端执行python scripts/run_flow.py./run_sim.sh
  • 步骤十:查看最终输出。脚本应打印“PASS”或“FAIL”总结,并生成详细的报告文件(如report.txt),包含通过率、错误列表等。

前置条件与环境

项目推荐值/说明替代方案/注意点
Python 环境Python 3.8 或更高版本。需安装标准库:random, subprocess, re, argparsePython 3.6+ 基本可用。复杂解析可安装 pandas, numpy
仿真工具Icarus Verilog (iverilog) + GTKWave。开源、轻量,适合快速原型。商用工具:Modelsim/QuestaSim, VCS, Xcelium。需相应调整编译和运行命令。
设计语言Verilog-2001 或 SystemVerilog (用于更丰富的 Testbench 特性)。VHDL 设计同样适用,但 Python 脚本需生成对应格式的激励文件。
操作系统Linux (Ubuntu 20.04/22.04) 或 WSL2 (Windows)。对文件路径和子进程调用支持最好。macOS 或原生 Windows (需注意路径分隔符和工具链可用性)。
文本编辑器/IDEVS Code 配合 Python 和 Verilog 插件,便于脚本和代码编写。Vim, Emacs, Sublime Text 等均可。
约束文件本流程侧重于功能仿真,不直接需要 FPGA 综合约束文件 (.xdc/.sdc)。若测试涉及时序,可在 Testbench 中使用 #delay 或指定 timescale。
接口依赖Testbench 需具备文件 IO 能力($fopen, $fdisplay, $readmemh)。对于简单测试,也可通过 Python 直接生成包含测试向量的 Verilog 初始化块。
版本控制推荐使用 Git 管理 RTL、Testbench 和 Python 脚本,便于回归测试追踪。需将生成的临时文件(如 stimulus.txt, sim_output.log)加入 .gitignore

目标与验收标准

成功实施本自动化方案后,应达成以下目标:

  • 功能正确性验证:能够对 DUT 进行至少 1000 个随机向量的测试,并自动判断功能是否正确,输出明确的 PASS/FAIL 结论。
  • 流程自动化:通过单条命令(一键脚本)完成从激励生成、仿真运行到结果解析的全流程,无需人工干预。
  • 结果可追溯:生成结构化的报告文件,包含测试时间、测试向量数量、错误详情(如出错的输入、期望输出、实际输出、仿真时间)。
  • 性能基准:流程执行时间应远少于人工验证同等数量测试向量的时间(例如,1000次测试在1分钟内完成)。
  • 可扩展性验收:能够通过修改配置文件或脚本参数,轻松更换测试模式(随机、定向、递增)或调整测试规模。

实施步骤

阶段一:搭建自动化框架与目录结构

清晰的目录结构是自动化流程可维护的基础。

project/
├── rtl/               # 设计文件 (.v)
├── tb/                # Testbench 文件 (.v)
├── scripts/           # Python 脚本
│   ├── gen_stimulus.py
│   ├── parse_results.py
│   └── run_flow.py    # 主控脚本
├── sim/               # 仿真运行目录(可自动生成)
│   ├── stimulus.txt   # 生成的激励
│   ├── sim_output.log # 仿真原始输出
│   └── report.txt     # 最终解析报告
└── run_sim.sh         # 可选的 Shell 入口

常见坑与排查:

  • 坑1:相对路径错误。脚本在不同目录下执行时,文件路径可能失效。
    排查:在 Python 脚本中使用 os.path.dirname(__file__) 获取脚本所在目录,再基于此构建绝对路径。
  • 坑2:仿真生成文件残留。多次运行导致旧数据干扰新测试。
    排查:在 run_flow.py 开始时,清理或重建 sim/ 目录。

阶段二:编写激励生成脚本 (gen_stimulus.py)

此脚本负责创建测试向量。核心是定义数据格式并与 Testbench 约定好读写方式。

import random

def generate_random_stimulus(num_tests=1000, data_width=8):
    """生成随机激励文件,格式:每行一个十六进制数"""
    with open('sim/stimulus.txt', 'w') as f:
        for _ in range(num_tests):
            a = random.randint(0, (1<<data_width)-1)
            b = random.randint(0, (1<<data_width)-1)
            # 约定:一行包含两个输入,以空格或逗号分隔
            f.write(f"{a:04x} {b:04x}
")  # 04x表示4位十六进制

def generate_directed_stimulus():
    """生成边界情况或定向测试向量"""
    directed_cases = [
        (0, 0),
        (255, 0),
        (0, 255),
        (255, 255),
        (1, 254)
    ]
    with open('sim/stimulus.txt', 'w') as f:
        for a, b in directed_cases:
            f.write(f"{a:04x} {b:04x}
")

if __name__ == "__main__":
    # 可以添加命令行参数解析,方便选择模式
    generate_random_stimulus(1000)

常见坑与排查:

  • 坑1:数据格式不匹配。Testbench 中的 $readmemh 期望每行一个数,而脚本生成了一行多列。
    排查:仔细核对 Testbench 读取代码与生成文件的前几行内容。确保进制(十六进制/十进制/二进制)一致。
  • 坑2:随机种子不可复现。调试时希望复现某个失败用例。
    排查:在脚本中使用 random.seed(42) 固定随机数种子,确保每次生成的序列相同。

阶段三:编写支持文件读写的 Testbench (tb_adder.v)

Testbench 需要从文件读取激励,并将结果写入另一个文件。

`timescale 1ns/1ps
module tb_adder;
    reg  [7:0] a, b;
    wire [8:0] sum;
    integer i, file_in, file_out;
    reg  [15:0] stimulus [0:999]; // 假设最多1000个激励对

    adder uut (.a(a), .b(b), .sum(sum));

    initial begin
        // 1. 打开激励文件和输出日志文件
        file_in = $fopen("sim/stimulus.txt", "r");
        if (!file_in) begin $display("Error opening stimulus file"); $finish; end
        file_out = $fopen("sim/sim_output.log", "w");
        
        // 2. 读取激励(方法一:逐行读取解析)
        // while (!$feof(file_in)) begin
        //     // 使用 $fscanf 按格式读取
        // end
        
        // 2. 读取激励(方法二:一次性读入数组,需提前知道数量)
        $readmemh("sim/stimulus.txt", stimulus);
        
        // 3. 应用激励并记录结果
        for (i = 0; i < 1000; i = i + 1) begin
            {a, b} = stimulus[i]; // 根据生成格式解包
            #10; // 等待稳定
            // 写入日志:时间、输入a、输入b、输出sum
            $fdisplay(file_out, "%t, %h, %h, %h", $time, a, b, sum);
        end
        
        // 4. 关闭文件并结束仿真
        $fclose(file_in);
        $fclose(file_out);
        $display("Simulation finished. Log saved to sim_output.log");
        $finish;
    end
endmodule

常见坑与排查:

  • 坑1:文件路径权限问题。仿真器无法创建或写入日志文件。
    排查:检查 sim/ 目录是否存在且有写权限。在 Testbench 初始处添加文件打开成功与否的判断。
  • 坑2:仿真不同步。在同一个时间点记录了多个结果,或结果尚未稳定。
    排查:确保在施加激励并经过足够延时(#10)后再采样和记录输出。检查 Testbench 的 timescale 设置是否合理。

阶段四:编写结果解析脚本 (parse_results.py)

此脚本是自动判定的核心,读取仿真日志,根据黄金模型(Golden Model)进行比对。

import re

def parse_and_check(log_file='sim/sim_output.log'):
    error_count = 0
    error_list = []
    
    with open(log_file, 'r') as f:
        for line_num, line in enumerate(f, 1):
            line = line.strip()
            if not line:
                continue
            # 解析日志行,例如:"10000, 0a, 1b, 025"
            # 正则匹配:时间,十六进制a,十六进制b,十六进制sum
            match = re.match(r'(\d+),\s*(\w+),\s*(\w+),\s*(\w+)', line)
            if not match:
                print(f"Warning: Cannot parse line {line_num}: {line}")
                continue
                
            sim_time, a_hex, b_hex, sum_hex = match.groups()
            a = int(a_hex, 16)
            b = int(b_hex, 16)
            sum_sim = int(sum_hex, 16)
            
            # 黄金模型计算(这里是8位加法器)
            sum_expected = a + b
            # 注意:实际硬件是9位输出,我们只取低9位比较
            mask = (1 << 9) - 1
            sum_expected_masked = sum_expected & mask
            
            # 比对
            if sum_sim != sum_expected_masked:
                error_count += 1
                error_list.append({
                    'line': line_num,
                    'time': sim_time,
                    'input_a': a_hex,
                    'input_b': b_hex,
                    'expected': f'{sum_expected_masked:03x}',
                    'got': sum_hex
                })
    
    # 生成报告
    with open('sim/report.txt', 'w') as report:
        report.write(f"=== Simulation Result Report ===
")
        report.write(f"Total tests checked: {line_num}
")
        report.write(f"Errors found: {error_count}
")
        report.write(f"Pass rate: {(line_num - error_count)/line_num*100:.2f}%

")
        
        if error_list:
            report.write("Error Details:
")
            for err in error_list:
                report.write(f"  Line {err['line']} @{err['time']}ps: ")
                report.write(f"a={err['input_a']}, b={err['input_b']}, ")
                report.write(f"expected={err['expected']}, got={err['got']}
")
    
    # 控制台输出摘要
    if error_count == 0:
        print(f"\033[92mPASS! All {line_num} tests passed.\033[0m")
        return True
    else:
        print(f"\033[91mFAIL! {error_count} errors found. See report.txt for details.\033[0m")
        return False

if __name__ == "__main__":
    parse_and_check()

常见坑与排查:

  • 坑1:日志格式解析失败。正则表达式无法匹配所有行,因为 Testbench 中可能混入了其他调试信息。
    排查:在 Testbench 中确保日志格式严格统一。或者在解析脚本中增加更灵活的正则,或先过滤掉非数据行。
  • 坑2:黄金模型与 DUT 位宽不匹配。Python 中整数无溢出,而硬件有固定位宽。
    排查:在 Python 的黄金模型计算中,必须模拟硬件的截断或溢出行为,使用位掩码(& mask)进行处理。

阶段五:编写主控流程脚本 (run_flow.py)

将以上步骤串联起来,实现一键自动化。

import os
import sys
import subprocess
import shutil
from gen_stimulus import generate_random_stimulus
from parse_results import parse_and_check

def run_simulation():
    """调用仿真器编译和运行"""
    # 1. 编译
    compile_cmd = ['iverilog', '-o', 'sim/adder_tb.vvp',
                   '-Irtl', '-Itb', 'rtl/adder.v', 'tb/tb_adder.v']
    print("Compiling...")
    result = subprocess.run(compile_cmd, capture_output=True, text=True)
    if result.returncode != 0:
        print("Compilation failed:")
        print(result.stderr)
        return False
    
    # 2. 运行仿真
    sim_cmd = ['vvp', 'sim/adder_tb.vvp']
    print("Running simulation...")
    result = subprocess.run(sim_cmd, capture_output=True, text=True)
    print(result.stdout)  # 打印仿真器输出信息
    if result.returncode != 0:
        print("Simulation failed:")
        print(result.stderr)
        return False
    return True

def main():
    # 清理并创建仿真目录
    sim_dir = 'sim'
    if os.path.exists(sim_dir):
        shutil.rmtree(sim_dir)
    os
标签:
本文原创,作者:FPGA小白,其版权均为FPGA线上课程平台|最全栈的FPGA学习平台|FPGA工程师认证培训所有。
如需转载,请注明出处:https://z.shaonianxue.cn/32722.html
FPGA小白

FPGA小白

初级工程师
成电国芯®的讲师哦,专业FPGA已有10年。
22219.27W7.09W34.38W
分享:
成电国芯FPGA赛事课即将上线
2026年半导体与硬件技术演进深度观察:从Chiplet到边缘AI的六大关键趋势
2026年半导体与硬件技术演进深度观察:从Chiplet到边缘AI的六大关键趋势上一篇
FPGA实现PCIe 5.0接口:PIPE架构与高速SerDes设计要点下一篇
FPGA实现PCIe 5.0接口:PIPE架构与高速SerDes设计要点
相关文章
总数:260
嵌入式与FPGA哪个更好?从开发到实战全面对比,看完秒懂如何选!

嵌入式与FPGA哪个更好?从开发到实战全面对比,看完秒懂如何选!

从技术原理到实际应用,嵌入式系统和FPGA各有千秋,下面从多个维度拆解它…
技术分享, 行业资讯
1年前
0
0
376
8
成电国芯 FPGA 工程师基础入门课程上线了,现在订购送“板卡 + 证书”

成电国芯 FPGA 工程师基础入门课程上线了,现在订购送“板卡 + 证书”

成电国芯,作为专注于集成电路和工业软件领域的翘楚,一直致力于为学员提供高…
技术分享
1年前
0
0
560
0
锁定高薪职业 FPGA工程师岗位开启等你加入

锁定高薪职业 FPGA工程师岗位开启等你加入

——FPGA企业高薪订单班12月25日重庆启航🚀加速腾飞,FP…
技术分享
2年前
0
0
460
0
评论表单游客 您好,欢迎参与讨论。
加载中…
评论列表
总数:0
FPGA线上课程平台|最全栈的FPGA学习平台|FPGA工程师认证培训
没有相关内容