---
title: Verilog Testbench自动化：利用Python脚本生成激励与解析仿真结果
url: "https://www.fpgalearn.com/32722.html"
kind: article
id: 32722
excerpt: 在FPGA验证流程中，Testbench的编写与仿真结果分析是耗时且易错的关键环节。传统手动编写激励和人工比对波形的方式，在面对复杂协议、大量测试向量或回归测试…
published_at: 2026-04-15
author: FPGA小白
tags: Python, Testbench, Verilog
word_count: 7007
source: "https://www.fpgalearn.com"
---

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

> 标签：Python、Testbench、Verilog
> 在FPGA验证流程中，Testbench的编写与仿真结果分析是耗时且易错的关键环节。传统手动编写激励和人工比对波形的方式，在面对复杂协议、大量测试向量或回归测试…

> 在FPGA验证流程中，Testbench的编写与仿真结果分析是耗时且易错的关键环节。传统手动编写激励和人工比对波形的方式，在面对复杂协议、大量测试向量或回归测试…

在FPGA验证流程中，Testbench的编写与仿真结果分析是耗时且易错的关键环节。传统手动编写激励和人工比对波形的方式，在面对复杂协议、大量测试向量或回归测试时效率低下。本文介绍一种基于Python脚本的自动化方案，旨在构建一个可复用、可扩展的验证框架，显著提升验证效率与可靠性。
## Quick Start
- 步骤一：准备环境。确保系统已安装Python 3.8+、Icarus Verilog（或Modelsim/QuestaSim/VCS）仿真器。
- 步骤二：创建项目目录结构：`project/rtl/`存放设计文件，`project/tb/`存放Testbench，`project/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`, `argparse`。 | Python 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 (需注意路径分隔符和工具链可用性)。 |
| 文本编辑器/IDE | VS 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
