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。
我习惯的做法是:
- 先写一个随机约束块,定义好合法范围
- 跑几千到几万个随机种子
- 配合覆盖率收集,看哪些场景还没跑到
- 针对覆盖率空洞,再补充定向用例
// 一个简单的随机约束示例(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 还是约束写错了。所以我的习惯是:先功能,再随机,最后异常。这个顺序我用了十几年,基本没出过大问题。
最后一句:测试用例设计没有银弹。五类用例各有各的用处,别偏废。我见过太多团队只跑随机测试,觉得“随机能覆盖一切”——结果流片回来,一个简单的边界条件就翻车了。嗯,希望你别犯同样的错。