8.3 隨機測試、記分板與 cocotb 嵌入#
從導向測試到約束隨機#
8.1 節的測試是導向測試(directed test):工程師手寫每一個情境。導向測試的優點是可讀、可預測、失敗時除錯方向明確;缺點在於狀態空間的規模。
一個有 \(n\) 個輸入位元的模組,每個週期的輸入空間大小是 \(2^n\) ;若模組還有 \(m\) 個狀態位元,則整個行為空間是 \(2^{n+m}\) 量級。以一個不起眼的例子估計:32 位元的 ALU 運算,光是三個 32-bit 運算元加一個 3-bit 運算碼,就有
種組合。工程師手寫的情境,哪怕寫上幾百個,也只是這個空間裡極小、且偏向「人想得到的地方」的取樣。經驗規律是:導向測試覆蓋的是工程師預期會出錯的地方,隨機測試覆蓋的是工程師沒想到會出錯的地方——而後者才是流片後最致命的那一類。
SystemVerilog 的回答是約束隨機驗證:rand 欄位 + constraint 區塊 + 求解器,讓工程師宣告約束(「位址必須 4-byte 對齊且在合法範圍內」),由求解器產生滿足約束的隨機值。這是 UVM 能力的核心,也是其複雜度的核心——約束求解是一個深奧的技術領域,把它的語言化帶進 HDL,是 SystemVerilog 語言體積爆炸的重要推手之一(1.2 節)。
Veryl 的立場是:不內建約束求解器。取而代之的是 $tb::random 的基本隨機能力,加上本節要講的「土法煉鋼」組合。這個取捨的適用邊界必須講清楚:模組級驗證足夠;大型 SoC 級驗證仍需 SystemVerilog 生態——這正是第 9 章互操作存在的理由之一。
$tb::random 的完整用法#
三個方法與可重現性#
var r: $tb::random::<bit<12>>;
initial {
r.seed(42);
let x : bit<12> = r.get(); // 12-bit 隨機值
let lo: bit<12> = r.get_range(0, 99); // [0, 99] 範圍內
}
r.seed(n):設定隨機種子。同一個 seed 產生同一組序列。r.get():產生一個型別為T的隨機值(T是宣告時的泛型參數,可以是bit<12>、u32等)。r.get_range(lo, hi):產生範圍內的值。
可重現性(reproducibility)是隨機測試的性命。 隨機測試的價值來自「找到意想不到的失敗」,而找到之後,必須能原樣重跑出同一個失敗才能除錯。實務紀律是:
- 每個測試的 seed 顯式寫死在測試碼中(或由 CI 以固定策略產生並記錄);
- 測試失敗訊息中印出當時的 seed;
- 迴歸報告記錄 seed 與失敗的對應,重現就是「帶著 seed 重跑一次」。
Veryl 把 seed 做成顯式方法而非全域隱含狀態,正是為了讓這條紀律可以機械化執行。
約束隨機的土法煉鋼#
沒有求解器,不等於沒有約束。Veryl 中可以用三個基本件的組合覆蓋大部分需求:
get_range做範圍約束;inside過濾做集合約束(5.3 節);- 手寫加權做分佈控制。
最常見的需求是「產生合法的 AXI 位址」。以 32-bit 位址空間、4-byte 對齊、有效區間 \([0, 4095]\) 為例:
var r_addr: $tb::random::<u32>;
var r_word: $tb::random::<u32>;
function gen_addr() -> u32 {
var base: u32;
loop {
base = r_addr.get_range(0, 4095) & 32'hFFFF_FFFC; // 4-byte 對齊
if base < 4096 {
break;
}
}
return base;
}
& 32'hFFFF_FFFC 把隨機值拉到對齊邊界,if 過濾掉超出範圍的取樣——這就是「範圍約束 + 對齊約束」的最小實現。再加權:邊界值要特別抽樣:
function gen_addr_biased() -> u32 {
let sel: u32 = r_word.get_range(0, 9);
case sel {
0: return 0; // 邊界:最低位址
1: return 4092; // 邊界:最高合法位址
2: return 4092 - 4 * r_word.get_range(0, 4); // 邊界附近
default: return gen_addr(); // 一般均勻取樣
}
}
經驗上,硬體 bug 高度集中在邊界(位址空間的端點、計數器的回繞點、滿/空條件),加權把取樣密度集中到 bug 的高發區。這個「均勻隨機 + 顯式邊界抽樣」的組合,覆蓋了模組級約束隨機的大部分實際需求。
誠實的能力缺口#
必須明說這個缺口的形狀:
| 能力 | UVM(SV constraint solver) | Veryl 土法 |
|---|---|---|
| 簡單範圍與對齊約束 | 有 | 容易實現 |
| 跨欄位關聯約束(欄位 B 依賴欄位 A) | 求解器處理 | 手寫函式處理 |
| 分佈控制 | dist 語法 | 手寫加權 |
| 複雜全域約束(數十變數聯立) | 求解器強項 | 會失控 |
最後一列是真正的分界:當約束是「數十個隨機變數之間的聯立關係」時(典型如複雜匯流排的交易序列與亂序交錯),土法會變成難以維護的糾纏函式。此時的正確選擇是 SystemVerilog 生態——透過 9.1 節的嵌入機制接上既有 UVM 環境,或用本節後半的 cocotb 借 Python 之力。Veryl 的定位是模組級驗證的自足,而非全頻段取代。
記分板(scoreboard)模式#
期望值佇列#
記分板是隨機測試的標準搭檔:隨機激勵產生的行為無法手寫期望值,因此需要一個參考模型與 DUT 並行運作、逐筆比對。其基本結構是「期望值佇列」:
flowchart LR
A["驅動端<br/>產生隨機交易"] --> B["送給 DUT"]
A --> C["期望值推入<br/>queue push_back"]
B --> D["DUT"]
D --> E["監控端<br/>觀測 DUT 輸出"]
C --> F["queue pop_front"]
E --> F
F --> G{"比對"}
G -->|不符| H["斷言失敗"]
G -->|相符| I["繼續"]關鍵性質:佇列維持順序,因此「期望值在第 \(k\) 筆交易產生、輸出在第 \(k\) 筆交易被消費」的對應關係被結構化保存——DUT 若亂序或丟失交易,pop 出的期望值與實際輸出就對不上,斷言立即失敗。
完整範例:FIFO 的記分板#
以一個同步 FIFO 為例。驅動端隨機產生寫入,同時把寫入值推入期望值佇列;觀測到讀出時,從佇列取出期望值比對:
#[test(test_fifo_random)]
module TestFifoRandom {
inst clk: $tb::clock_gen #(period: 10);
inst rst: $tb::reset_gen ( clk );
inst u_fifo: SyncFifo #(
WIDTH: 8,
DEPTH : 16,
) (
clk: clk.clk,
rst: rst.rst,
i_wr: i_wr,
i_data: i_data,
i_rd: i_rd,
o_data: o_data,
o_full: o_full,
o_empty: o_empty,
);
var r_rand : $tb::random::<bit<8>>;
var r_expc : $tb::queue::<u32, 64>;
var r_cnt : u32;
var i_wr : logic;
var i_rd : logic;
var i_data: logic<8>;
var o_data: logic<8>;
var o_full: logic;
var o_empty: logic;
initial {
r_rand.seed(1234);
rst.assert();
i_wr = 0;
i_rd = 0;
clk.next(5);
// 隨機驅動 1000 個週期:寫入同時把期望值入隊 讀出時比對
for i in 0..1000 {
let do_wr: logic = if r_cnt < 16 && r_rand.get_range(0, 3) != 0 { 1 } else { 0 };
let do_rd: logic = if r_cnt > 0 && r_rand.get_range(0, 3) != 0 { 1 } else { 0 };
i_wr = do_wr;
i_rd = do_rd;
i_data = r_rand.get();
if do_wr == 1 {
r_expc.push_back(u32'(i_data));
}
clk.next(); // 交易生效
if do_rd == 1 {
let expect: u32 = r_expc.pop_front();
$assert(o_data == logic<8>'(expect),
"cycle %d: pop got %h, expect %h", i, o_data, expect);
}
}
$finish();
}
}
這個骨架值得留意三點:寫入與讀出的允許條件(r_cnt < 16、r_cnt > 0)由測試端自己維護的影子計數器控制——這是「不對 DUT 的介面狀態輸出(o_full/o_empty)做驅動決策」的常見做法,避免測試行為依賴可能有 bug 的 DUT 輸出;斷言訊息帶週期編號與兩個值,失敗時除錯資訊一次到位;seed 寫死,任何人在任何機器上都能重現。
參考模型:純函數式比對#
對於行為可以純函數描述的模組(ALU、編解碼器、轉換器),比對可以更簡單:用 Veryl 的 function 寫一個無狀態的參考模型,逐筆輸入比對:
function ref_alu(op: logic<2>, a: logic<8>, b: logic<8>) -> logic<9> {
case op {
Op::Add: return 9'(a) + 9'(b);
Op::Sub: return 9'(a) - 9'(b);
Op::And: return 9'(a & b);
default: return 9'(a | b);
}
}
// 隨機測試迴圈中:
for i in 0..500 {
let rand_op: logic<2> = r_rand.get();
let rand_a : logic<8> = r_rand.get();
let rand_b : logic<8> = r_rand.get();
op = rand_op; a = rand_a; b = rand_b;
clk.next();
let expected: logic<9> = ref_alu(rand_op, rand_a, rand_b);
$assert({carry, y} == expected,
"iter %d: dut=%h ref=%h op=%d a=%h b=%h", i, {carry, y}, expected, rand_op, rand_a, rand_b);
}
參考模型用與 DUT 不同的寫法表達同一語意(上例中參考模型不做流水線暫存,DUT 有)——若兩者寫法相同,比對就退化為「自己對自己」,沒有發現錯誤的能力。
cocotb 嵌入#
為什麼需要 Python#
Veryl 原生測試的工具箱到此為止:線性劇本、隨機、佇列、斷言。它缺少的是 Python 生態——數值計算(numpy)、資料結構與演算法、通訊協定封包產生(scapy)、檔案與影像處理。硬體驗證的許多環節(產生一段真實的 Ethernet 幀、用數值方法算出期望值、對 DUT 輸出做統計分析)在這些庫面前,「HDL 內建工具」永遠是玩具。
cocotb(Coroutine-based Co-simulation TestBench)是 Python 端驗證的主流框架:用 coroutine 撰寫 testbench,await 表達時間等待,可讀性極佳:
# cocotb 的典型樣貌
@cocotb.test()
async def test_fifo(dut):
await RisingEdge(dut.i_clk)
dut.i_wr.value = 1
dut.i_data.value = 0xAB
await RisingEdge(dut.i_clk)
assert dut.o_data.value == 0xAB
Veryl 的 embed (cocotb) 機制把 Python 測試碼嵌入 Veryl 專案,由 veryl test 一併執行:
#[test(test_fifo_cocotb, TestFifoTarget)]
module TestFifoTarget {
inst clk: $tb::clock_gen #(period: 10);
// DUT 實例化由 Veryl 負責 Python 端以層級路徑存取訊號
}
embed (cocotb) py{{{
import cocotb
from cocotb.clock import Clock
from cocotb.triggers import RisingEdge, Timer
@cocotb.test()
async def test_fifo_cocotb(dut):
cocotb.start_soon(Clock(dut.i_clk, 10, units="ns").start())
dut.i_rst.value = 1
await RisingEdge(dut.i_clk)
dut.i_rst.value = 0
await RisingEdge(dut.i_clk)
# 寫入
dut.i_wr.value = 1
dut.i_data.value = 0xAB
await RisingEdge(dut.i_clk)
dut.i_wr.value = 0
# 讀出並比對
dut.i_rd.value = 1
await RisingEdge(dut.i_clk)
dut.i_rd.value = 0
assert dut.o_data.value == 0xAB, (
f"fifo pop = {dut.o_data.value}, expected 0xAB"
)
}}};
三個語法要點:
#[test(test_name, TargetModule)]的第二個參數指定測試作用在哪個模組上——cocotb 的dut物件就是該模組實例,Python 端以dut.<訊號名>存取其埠;embed (cocotb) py{{{ ... }}}內嵌 Python 原始碼,三層大括號使內部的{}(f-string、dict)不需要轉義——與 9.1 節sv{{{ ... }}}同一個設計;veryl test統一執行原生測試與 cocotb 測試,CI 不需要區分兩條流程(但 cocotb 需要 Python 環境與 cocotb 套件,7.2 節的 CI 中這是一個額外的安裝步驟)。
混合策略#
原生測試與 cocotb 的分工建議:
| 層級 | 選擇 | 理由 |
|---|---|---|
| 模組級功能測試 | 原生測試 | 快、無外部依賴、與 Veryl 程式碼同源 |
| 協定級測試(握手、亂序、逾時) | cocotb | Python 協程表達時序監控自然 |
| 需要外部資料的測試(封包、影像) | cocotb | Python 生態的資料庫 |
| 需要數值參考模型的測試 | cocotb | numpy |
一個務實的檢驗問題是:「這個測試需要 Python 標準庫以外的東西嗎?」不需要,就用原生測試;需要,就上 cocotb。
SystemVerilog 測試的嵌入#
測試嵌入的最後一塊拼圖是 SystemVerilog 本身。用 embed (inline) 可以把 SV 測試模組原樣放進專案,由外部模擬器執行:
#[test(test1)]
embed (inline) sv{{{
module test1;
// 這裡是完整的 SystemVerilog testbench
// 可以是既有 UVM 環境的進入點
initial begin
$display("hello from SV testbench");
// run_uvm_test("my_uvm_test");
$finish;
end
endmodule
}}};
這讓既有的 UVM 環境可以包進 Veryl 專案:DUT 已遷移到 Veryl,驗證環境維持 UVM,透過 embed 銜接。9.1 節會把 embed 的完整語法(含識別字插值)講透。
覆蓋率的土法實作#
8.1 節展示過 final 區塊的情境統計。把它發展成完整的土法覆蓋率,需要三件套:
// 1. 命中計數器(模組級別的情境編號)
var r_hits: logic<32> [8];
// 2. 模擬中隨交易更新
always_ff {
case i_txn_kind {
Kind::Write: r_hits[0] += 1;
Kind::Read: r_hits[1] += 1;
Kind::WrWhileFull: r_hits[2] += 1;
Kind::RdWhileEmpty: r_hits[3] += 1;
default: {},
}
}
// 3. final 報告
final {
for i in 0..4 {
if r_hits[i] == 0 {
f.write("COVERAGE MISS: scenario %d\n", i);
} else {
f.write("scenario %d: hit %d times\n", i, r_hits[i]);
}
}
}
與正式 coverage 工具(收集 line/toggle/functional coverage 並產生合併報告)的差距要誠實指出:土法只能覆蓋工程師想到要數的情境(功能性覆蓋的子集),無法自動枚舉程式碼行、位元翻轉、狀態機弧。它適合中小專案與 CI 中的快速回饋;簽核級的覆蓋率閉環,仍是商業工具與 SV 生態的領域。
實作練習#
- 為 6.3 節的非同步 FIFO 撰寫隨機 + 記分板測試:寫入域與讀出域用不同的
clk.next節奏推進,期望值佇列仍然按寫入順序消費——觀察非同步讀寫節奏下記分板的對應方式; - 在上述測試中加入邊界抽樣:刻意產生「寫到滿之後繼續寫」「讀空之後繼續讀」的情境,用
$assert驗證o_full/o_empty的正確性; - 用
final區塊輸出情境覆蓋報告到檔案,確認所有情境都被命中;未命中的情境調整加權後重跑; - 為同一個 FIFO 寫一個 cocotb 協定級測試:連續寫入直到
full拉高,驗證滿條件下的寫入被拒絕且資料不損毀; - 把 seed 換掉重跑三次,比較覆蓋報告——體會「單一 seed 的覆蓋不代表隨機測試的總覆蓋」。
本節小結#
- 導向測試覆蓋的是預期中的錯誤;行為空間是 \(2^{n+m}\) 量級,隨機測試是對「想不到的地方」的取樣。
$tb::random的 seed 可重現性是隨機測試迴歸的性命;範圍約束 +inside過濾 + 邊界加權的土法組合,覆蓋模組級約束隨機的大部分需求。- 誠實的缺口:沒有求解器,聯立約束會失控;SoC 級驗證需 SV 生態(第 9 章)或 cocotb。
- 記分板以
$tb::queue為基礎:驅動端 push 期望值、監控端 pop 比對;參考模型應與 DUT 異構,否則比對失去發現能力。 - cocotb 嵌入以
#[test(name, Target)]+embed (cocotb) py{{{ ... }}}實現,veryl test統一執行;模組級用原生、協定級與資料密集型用 Python。 embed (inline) sv{{{ ... }}}讓既有 UVM 環境包進 Veryl 專案;土法覆蓋率用final+ 命中計數,但無法替代正式 coverage 工具。
下一節:第 9.1 節「sv 命名空間、embed 與 include:混合語言設計」