4. 测试用例设计:五类核心用例

测试用例设计,说白了就是验证的灵魂。你设计得越细,流片回来翻车的概率就越低。我个人习惯把用例分成五类:功能、边界、随机、定向、异常。每一类都有它的用武之地,缺一不可。

核心观点:五类用例不是互斥的,而是互补的。一个成熟的验证环境,应该同时覆盖这五类,才能做到“该测的都测了,不该测的也防了”。

测试用例五类体系 测试用例设计 功能测试 边界测试 随机测试 定向测试 异常测试 各类型核心关注点 • 功能测试:规格书定义的正常行为是否全部实现 • 边界测试:临界值附近是否出现意外行为 • 随机测试:用大量随机组合发现设计盲区 • 定向测试:针对特定场景或已知风险点精准打击 • 异常测试:输入非法、协议违例、资源耗尽等极端情况

4.1 功能测试用例

功能测试是最基础的。说白了,就是拿着规格书一条一条对。芯片该干什么,你就测什么。比如一个 SPI 控制器,你得测单次传输、连续传输、不同数据宽度、不同时钟极性组合——这些都是规格书里白纸黑字写着的。

我建议你这样做:先把规格书里的功能点拆成一张表,每个功能点对应一个或几个用例。别偷懒,这一步做扎实了,后面才能省心。

功能点 用例描述 预期结果
单次写操作 发起一次 SPI 写传输,数据 0xA5 MOSI 输出 0xA5,CS 正常拉高拉低
连续读操作 发起 10 次连续读,间隔 1 个时钟 每次读到的数据与预期一致
时钟极性 CPOL=1 配置 CPOL=1,CPHA=0 进行传输 时钟空闲为高,数据在下降沿采样
数据宽度 16 位 配置数据宽度为 16 位,发送 0x1234 MOSI 输出 16 位数据,顺序正确

我的经验:功能测试用例最好让设计和验证一起评审。我在一个项目中吃过亏——设计对规格书的理解和我有偏差,结果功能用例跑全绿,但实际行为根本不是客户要的。评审一次,能省后面十次 debug。

4.2 边界测试用例

边界测试,专门抓那些“差一点就出事”的场景。你想想看,芯片里到处都是 FIFO 深度、计数器最大值、状态机跳转条件——这些东西在边界附近最容易出 bug。

常见的边界点包括:

  • FIFO 空/满:写满后继续写、读空后继续读
  • 计数器溢出:计数到最大值后加 1,或者减到 0 后再减
  • 地址边界:访问最后一个有效地址和第一个无效地址
  • 时序边界:建立时间/保持时间刚好满足或刚好不满足

我记得有一次,一个同事测 AHB 总线桥,功能用例全过了。结果我让他加一个边界用例——连续写 256 次,刚好把写缓冲填满。你猜怎么着?第 257 次写的时候,状态机直接卡死了。这就是边界测试的价值。

注意:边界测试不是只测“等于边界”的情况。边界两侧各测一个点,比如 FIFO 深度是 16,你要测写 15 次、16 次、17 次。只测 16 次是不够的,因为 bug 往往出现在“刚好满”和“刚溢出”之间。

4.3 随机测试用例

随机测试,说白了就是让工具帮你“瞎蒙”。你定好约束,让仿真器随机生成地址、数据、延迟、中断——各种组合。为什么需要这个?因为人的思维有盲区,你永远想不到设计会在哪个犄角旮旯里藏个 bug。

我习惯的做法是:

  1. 先写一个随机约束块,定义好合法范围
  2. 跑几千到几万个随机种子
  3. 配合覆盖率收集,看哪些场景还没跑到
  4. 针对覆盖率空洞,再补充定向用例
// 一个简单的随机约束示例(SystemVerilog)
class spi_transaction;
  rand bit [7:0] data;
  rand bit [3:0] delay;  // 随机插入延迟
  rand bit       cpol;
  rand bit       cpha;
  
  constraint valid_delay {
    delay inside {[0:15]};
  }
  
  constraint valid_mode {
    cpol inside {0, 1};
    cpha inside {0, 1};
  }
endclass

我的建议:随机测试不是“乱测”。约束一定要写对,否则你测了半天,其实一直在测非法场景。我曾经见过一个团队,随机约束写错了,地址范围设成了 0x0000 到 0xFFFF,但设计只支持 0x0000 到 0x0FFF——结果所有随机用例都在测不存在的地址,白白浪费了三天仿真时间。

4.4 定向测试用例

定向测试,就是“精准打击”。你知道某个场景容易出问题,或者覆盖率报告告诉你某个分支没跑到,那就专门写一个用例去测它。

定向测试的典型场景:

  • 复位后首次操作:芯片刚上电,第一次读写是否正常
  • 中断嵌套:高优先级中断打断低优先级中断处理
  • 时钟切换:动态切换时钟源时,逻辑是否出现毛刺
  • 低功耗模式:进入睡眠后,唤醒条件是否准确

定向测试和功能测试有点像,但更“钻”。功能测试是“这个功能能不能用”,定向测试是“这个功能在某个极端条件下能不能用”。

关键区别:功能测试覆盖“正常情况”,定向测试覆盖“特殊情况”。两者结合,才能做到无死角。

4.5 异常测试用例

异常测试,测的是“芯片会不会死”。你给芯片输入非法指令、非法地址、非法协议序列——看它能不能优雅地处理,而不是直接挂掉。

常见的异常场景:

  • 非法指令:CPU 执行未定义的 opcode
  • 地址越界:访问不存在的内存区域
  • 协议违例:AHB 总线上 burst 长度超过设备支持范围
  • 资源耗尽:连续申请中断,中断控制器溢出
  • 时钟丢失:外部时钟突然停止,芯片是否进入安全状态

我曾经遇到过一个案例:一个 USB 控制器,功能测试全过,随机测试也跑了 10 万次没出问题。结果客户在产线上插拔 USB 时,偶尔会死机。后来一查,是设备在收到一个“长度为零的 SETUP 包”时,状态机直接跳到了非法状态。这就是典型的异常测试没覆盖到。

避坑指南:异常测试的预期行为一定要明确。是返回错误码?是触发中断?还是直接复位?如果预期不明确,你测出来的“异常”可能其实是正常行为。我曾经就因为这个,把一个正确的设计当成 bug 报了,白白浪费了设计同事两天时间。

4.6 五类用例的协同策略

这五类用例不是孤立存在的。我一般这样安排:

阶段 主要用例类型 目的
早期(RTL 刚完成) 功能测试 + 定向测试 快速验证基本功能是否跑通
中期(功能稳定后) 随机测试 + 边界测试 大规模覆盖,找隐藏 bug
后期(临近 freeze) 异常测试 + 回归测试 确保芯片在各种极端条件下不崩溃

你想想看,如果一上来就跑随机测试,万一有个基础功能没通,你根本分不清是设计 bug 还是约束写错了。所以我的习惯是:先功能,再随机,最后异常。这个顺序我用了十几年,基本没出过大问题。

最后一句:测试用例设计没有银弹。五类用例各有各的用处,别偏废。我见过太多团队只跑随机测试,觉得“随机能覆盖一切”——结果流片回来,一个简单的边界条件就翻车了。嗯,希望你别犯同样的错。