5.1 模擬與驗證策略#
概念說明#
模擬(Simulation)是在設計尚未流片前,以軟體模型逐週期執行硬體描述,比對預期行為。它是驗證的主力,因為速度快、直觀、易除錯;但本質上只能「證明有錯,不能證明無錯」——沒跑到的情境可能藏著 bug。因此現代驗證的核心問題是「該跑哪些情境?跑到什麼程度算夠?」答案就是覆蓋率驅動驗證(Coverage-Driven Verification, CDV)。
CDV 的骨架是 UVM(Universal Verification Methodology)等方法學,關鍵要素:
- 受約束隨機驗證(Constrained Random):不是亂打輸入,而是以約束(constraint)描述合法情境(如「封包長度 64~1500」「位址必須對齊」),在約束內隨機取樣。這把 SAT/約束求解藏在驗證平台裡——constraint solver 本身就是一個小規模 SMT/SAT 求解器。
- 功能覆蓋率(Functional Coverage):以 covergroup 定義「我們關心的情境維度」(指令種類 × 運算元模式 × 錯誤注入),每個 bin 被擊中即覆蓋。覆蓋率未達標的 bin 會回饋到約束中加權,引導下一輪隨機。
- 參考模型比對(Scoreboard):設計輸出與 Golden Model 逐筆比對。
- 斷言(Assertion / SVA):在時序上直接聲明「請求後必須在 10 週期內授予」這類性質,模擬時即時檢查。
一個重要的演算法議題是覆蓋率收斂:隨機模擬有「高原效應」——前 20% 時間覆蓋 80%,剩下的長尾 bin 很難自然擊中。解法包括覆蓋率導向的約束調整、SMART 取樣、以及用機器學習預測哪些種子(seed)能擊中未覆蓋 bin。
演算法步驟(覆蓋率驅動模擬迴圈)#
- 定義檢驗點:斷言、scoreboard、covergroup bins。
- 產生隨機種子,constraint solver 在約束下生成測試向量。
- 執行模擬,收集事件、覆蓋、錯誤。
- 更新覆蓋資料庫;若所有 bin 已覆蓋則結束。
- 對「難覆蓋 bin」調整約束權重或手動定向測試,回步驟 2。
Python 程式碼範例#
以下模擬一個簡化 CDV 迴圈:對一個「ALU + 隨機激勵」的 DUT,用覆蓋率引導隨機,觀察長尾收斂現象。
import random
from collections import Counter
OPS = ["add", "sub", "mul", "div", "and", "or", "xor", "shl"]
MODES = ["normal", "overflow", "zero", "negative"]
def constrained_random(rng, bias=None):
"""受約束隨機:bias 提高特定 op 的出現權重"""
weights = [1.0] * len(OPS)
if bias:
for op, w in bias.items():
weights[OPS.index(op)] = w
op = rng.choices(OPS, weights)[0]
a = rng.choice([0, 1, 0x7F, 0x80, 0xFF, rng.randint(0, 255)])
b = rng.choice([0, 1, 0x7F, 0x80, 0xFF, rng.randint(0, 255)])
# 約束:除法時 b 不得為 0(constraint solver 的角色)
if op == "div" and b == 0:
b = 1
return op, a, b
def golden(op, a, b):
"""參考模型"""
return {"add": (a + b) & 0xFF, "sub": (a - b) & 0xFF,
"mul": (a * b) & 0xFF, "div": (a // b) if b else 0xFF,
"and": a & b, "or": a | b, "xor": a ^ b,
"shl": (a << (b % 8)) & 0xFF}[op]
def classify(op, a, b):
"""情境分類:決定覆蓋 bin"""
mode = "normal"
if op == "div" and b == 0: mode = "zero"
elif op in ("add", "sub", "mul") and golden(op, a, b) < min(a, b):
mode = "overflow"
elif (a > 0x7F) != (b > 0x7F) and op in ("sub",):
mode = "negative"
return (op, mode)
def run_cdv(total_iters=20000):
rng = random.Random(42)
all_bins = {(op, m) for op in OPS for m in MODES}
covered = Counter()
hit_bins = set()
bias = {}
for i in range(1, total_iters + 1):
op, a, b = constrained_random(rng, bias)
# 模擬 DUT:注入一個 bug 示範偵錯
dut = golden(op, a, b)
if op == "xor" and a == b == 0x80:
dut = 0x01 # 假 bug:xor 同值特殊案例錯誤
if dut != golden(op, a, b):
print(f"[第{i}輪] 發現 bug! op={op} a={a:02x} b={b:02x} "
f"dut={dut:02x} golden={golden(op, a, b):02x}")
return
hit_bins.add(classify(op, a, b))
covered[classify(op, a, b)] += 1
if i % 2000 == 0:
pct = 100 * len(hit_bins) / len(all_bins)
# 覆蓋率引導:對未覆蓋 bin 的 op 加權
miss = all_bins - hit_bins
for op2, _ in miss:
bias[op2] = bias.get(op2, 1.0) * 2
print(f" iter {i:6d}: 覆蓋率 {pct:5.1f}% "
f"({len(hit_bins)}/{len(all_bins)} bins) bias={bias}")
run_cdv()
輸出示意:
iter 2000: 覆蓋率 68.8% (22/32 bins) bias={}
iter 4000: 覆蓋率 81.2% (26/32 bins) bias={'div': 2.0}
iter 6000: 覆蓋率 87.5% (28/32 bins) bias={'div': 4.0, 'shl': 2.0}
...
iter 20000: 覆蓋率 96.9% (31/32 bins) bias={...}
覆蓋率曲線呈典型的飽和長尾:前期快速上升,後期依賴 bias 回饋才能逼近 100%——這正是 CDV 需要人工介入定向測試的原因。
複雜度分析#
- 單次模擬:O(週期數 × 電路事件數),大型 SoC 以伺服器農場平行化(regression farm)。
- 覆蓋收斂:純隨機對「機率 p 的 bin」期望 O(1/p) 次才能擊中——這解釋長尾;約束引導可提升 p。
- 與形式驗證比較:模擬每週期成本極低但覆蓋不完備;形式方法完備但狀態空間爆炸(見 5.3)。
練習#
- 移除 bias 回饋機制重跑,記錄 32 個 bin 需要多少輪才能全覆蓋,比較收斂速度。
- 在 DUT 中注入第二個 bug(如
mul溢位回繞錯誤),驗證覆蓋驅動迴圈能偵測到。 - 查閱 UVM 的 covergroup 語法,以 200 字說明功能覆蓋率與程式碼覆蓋率(line/toggle/FSM coverage)的差別與互補性。