3、Hello World 的进阶:从打印语句到第一个可复用的验证函数,理解入口函数与模块化。

好,我们正式开始写代码了。

很多新手学验证,上来就写一个 print("Hello World")。然后呢?然后就没有然后了。说实话,这跟验证工程师的工作差得有点远。你想想看,真正的验证环境里,没人会写一个孤零零的打印语句就完事。我们需要的是——可复用的函数清晰的入口,以及模块化的思维

这一章,我们就从那个经典的 Hello World 出发,一步步把它改造成一个真正能用的验证函数。我个人习惯,每学一个新语言或新框架,都会先搭一个最小的可运行骨架。这个骨架就是我们的“入口函数”。

3.1 从打印语句到函数封装

先看最原始的写法:

print("Hello World, Verification!")

嗯,能跑。但问题来了——如果我想打印不同的消息呢?如果我想在多个地方打印呢?复制粘贴?别闹了,那是新手干的事。

我们把它包成一个函数:

def hello_verification(message):
    """打印验证相关的问候信息"""
    print(f"Hello Verification: {message}")

# 调用
hello_verification("开始检查时序约束")
hello_verification("开始检查复位信号")

看到了吗?一次定义,多次调用。这就是复用的雏形。我在项目中遇到过很多次,有人把同样的打印逻辑写了十几遍,后来要改格式,改到崩溃。你想想看,如果一开始就封装成函数,改一处就全改了,多省心。

核心思想: 函数是代码复用的最小单元。别重复你自己(DRY原则)。

3.2 理解入口函数:为什么需要 main?

好,函数有了。但怎么组织执行顺序呢?

很多 Python 脚本会这样写:

def hello_verification(message):
    print(f"Hello Verification: {message}")

hello_verification("开始检查时序约束")
hello_verification("开始检查复位信号")

这也能跑。但如果你把这个脚本 import 到别的文件里呢?糟糕了——那两行调用会立刻执行!

为什么会这样?因为 Python 解释器是从上往下执行的。它看到函数定义就记住,看到函数调用就执行。所以我们需要一个“入口函数”,也就是 main 函数:

def hello_verification(message):
    """打印验证相关的问候信息"""
    print(f"Hello Verification: {message}")

def main():
    """主入口函数"""
    hello_verification("开始检查时序约束")
    hello_verification("开始检查复位信号")
    hello_verification("开始检查电源域")

if __name__ == "__main__":
    main()

这里有个关键点:if __name__ == "__main__"。它的意思是——只有当我直接运行这个脚本时,才执行 main()。如果我是被别的文件 import 的,那就不执行。这样,你的代码既可以作为独立脚本运行,也可以作为模块被导入。

我的习惯: 每个 Python 脚本都写一个 main() 函数。哪怕只有一行代码。这就像验证环境里的 testbench 顶层,总得有个明确的起点。

3.3 第一个可复用的验证函数

好了,现在我们把 Hello World 升级成真正的验证函数。假设我们要检查一个信号是否在预期时间内稳定:

def check_signal_stable(signal_name, expected_value, timeout_ns=100):
    """
    检查信号是否在超时时间内稳定到预期值
    
    参数:
        signal_name: 信号名称
        expected_value: 预期值 (0 或 1)
        timeout_ns: 超时时间,单位纳秒
    
    返回:
        bool: 是否稳定
    """
    print(f"[CHECK] 检查信号 {signal_name} 是否稳定到 {expected_value}")
    print(f"[CHECK] 超时时间: {timeout_ns} ns")
    
    # 这里只是演示框架,实际会调用仿真器 API
    # 模拟检查过程
    import time
    time.sleep(0.1)  # 模拟仿真时间
    
    result = True  # 假设检查通过
    if result:
        print(f"[PASS] 信号 {signal_name} 稳定")
    else:
        print(f"[FAIL] 信号 {signal_name} 未稳定")
    
    return result

def main():
    """验证流程主入口"""
    print("=== 开始验证流程 ===")
    
    # 检查复位信号
    check_signal_stable("rst_n", 1, timeout_ns=1000)
    
    # 检查时钟信号
    check_signal_stable("clk", 1, timeout_ns=500)
    
    print("=== 验证流程结束 ===")

if __name__ == "__main__":
    main()

这个函数已经有点意思了。它有明确的输入参数,有返回值,还有清晰的日志输出。我在项目中遇到过很多类似的场景——检查信号稳定、检查协议握手、检查数据一致性。把这些检查逻辑封装成函数,就是验证组件的基础。

我曾经踩过的坑: 一开始把超时时间写死在函数里。后来换了个设计,时序要求不同,只能复制整个函数去改。那叫一个痛苦。所以,参数化是复用函数的关键。能传参就别写死。

3.4 模块化:把函数放到该放的地方

函数多了怎么办?全塞在一个文件里?那文件会越来越臃肿。我们需要模块化。

在 Python 里,一个 .py 文件就是一个模块。我们可以把相关的函数放到同一个模块里:

模块名 功能 包含函数示例
signal_checks.py 信号级检查 check_signal_stable(), check_signal_toggle()
protocol_checks.py 协议级检查 check_axi_handshake(), check_apb_write()
data_checks.py 数据一致性检查 check_data_integrity(), check_crc()
utils.py 通用工具函数 log_message(), format_time()

然后,在主脚本里这样导入:

from signal_checks import check_signal_stable
from protocol_checks import check_axi_handshake
from data_checks import check_data_integrity

def main():
    check_signal_stable("rst_n", 1)
    check_axi_handshake("axi_master")
    check_data_integrity("dma_data")

if __name__ == "__main__":
    main()

你看,主脚本变得非常清晰。它只负责“编排”验证流程,具体的检查逻辑都在各自的模块里。这就是模块化的好处——各司其职,互不干扰

模块化的三个好处:
  • 可维护性: 改一个模块不影响其他模块
  • 可复用性: 写好的模块可以在不同项目间复用
  • 可读性: 主流程一目了然,不用看几百行的细节

3.5 避坑指南与最佳实践

最后,分享几个我这些年总结的经验:

  1. 函数命名要见名知意check_signal_stablefunc1 好一万倍。你想想看,三个月后你回来看代码,看到 func1 是什么感觉?
  2. 每个函数只做一件事:如果一个函数既检查信号又打印日志又写文件,赶紧拆。我曾经见过一个函数写了 200 行,什么活都干,改一次崩一次。
  3. 写文档字符串:就是函数定义下面的那个三引号注释。别偷懒。Python 的 help() 函数会直接显示它,团队协作时特别有用。
  4. 入口函数保持简洁main() 只做流程编排,不做具体实现。具体逻辑都放到函数里。
一个小技巧: 写函数时,先写调用代码,再写函数实现。也就是“从使用端出发”。这样你自然会把函数设计得易用、参数合理。这叫“测试驱动开发”的简化版。

好了,这一章的内容就到这里。我们从一句简单的 print 出发,走到了函数封装、入口函数、模块化设计。说白了,这就是验证脚本的骨架。后面的所有内容,都会在这个骨架上生长。

下一章,我们会开始接触真正的验证环境搭建。做好准备。