# SystemVerilog : Testbench練習
[code](https://github.com/zBING-QIAN/Algorithm/tree/main/NTT)
這篇主要紀錄一下如何使用OOP的概念去寫testbench,這裡我選擇寫一個testbench驗證 捲積兩個1-D array取模P的演算法
## 演算法概念
兩個長度為n的array捲積操作直觀的來說要O(n^2)的複雜度,利用FFT則可以達到O(nlogn),但是要處理虛數和捨入誤差之類的問題實在太麻煩,且input的數值都是整數的情況可以考慮使用Number Theroem Transform去處理,控制好modulo P的大小(避免overflow)可以使整個操作都在整數域上,且複雜度也是O(nlogn)。
* 這邊我設定array的長度為256,在不同的模數P下捲積(cyclic convolution)。也可以用128長度的array(後面補0)做非循環捲積。
詳細理論推導可以查[wiki](https://en.wikipedia.org/wiki/Discrete_Fourier_transform_over_a_ring)
## Software Simulation
這邊解釋軟體層面驗證算法的流程
### Naive convolution approach
[code](https://github.com/zBING-QIAN/Algorithm/blob/main/NTT/SW/BasicConv.cpp)
就是最原始的捲積,方便後續產生測資和debug
### NTT convolution approach
[code](https://github.com/zBING-QIAN/Algorithm/blob/main/NTT/SW/NTTConv.cpp)
這邊是使用257和12289當作模數,對應的原根為3,11在這個設定之下只要捲積的結果小於257*12289,都可以使用Chinese Remainder Theorem(CRT)還原出一般捲積正確的結果,所以如果長度為256,輸入值域就約只能是111(sqrt(12289*257/256))。
(因為257=256+1, 12289 = 48*256+1,所以選擇他們來當模數,對應的NTT kernel e為3和11^48 ,這樣3^256 = 1(mod 257), 11^(48*256) = 1(mod 12289) 滿足kernel的要求,不同模數元根的暴力求法可以[參考](https://github.com/zBING-QIAN/Algorithm/blob/main/NTT/SW/preset_ntt.cpp))
* 不過為求簡化,hardware實作我就沒有寫CRT,只單純做捲積mod p
### Compile and test - makefile
直接在SW路徑底下run make,會生成10筆測資,並比較暴力產生的結果和NTT的結果有沒有吻合
```
# Compiler and flags
CXX = g++
CXXFLAGS = -std=c++17 -O2 -Wall
# Executables
GEN = gen.exe
RUN = NTTConv.exe
BRUTE = BasicConv.exe
# Source files
GEN_SRC = gen.cpp
RUN_SRC = NTTConv.cpp
BRUTE_SRC = BasicConv.cpp
# Test directory
TEST_DIR = ./test_cases
# Number of test cases
NUM_CASES = 10
# Default target
all: $(GEN) $(RUN) $(BRUTE) test
# Compile rules
$(GEN): $(GEN_SRC)
$(CXX) $(CXXFLAGS) -o $@ $<
$(RUN): $(RUN_SRC)
$(CXX) $(CXXFLAGS) -o $@ $<
$(BRUTE): $(BRUTE_SRC)
$(CXX) $(CXXFLAGS) -o $@ $<
# Create test directory if not exist
# $(TEST_DIR):
# mkdir -p $(TEST_DIR)
# Test rule: generate multiple inputs, run both programs, compare outputs
test: $(GEN) $(RUN) $(BRUTE) $(TEST_DIR)
@echo Running $(NUM_CASES) test cases...
@for /L %%i in (1,1,$(NUM_CASES)) do ( \
echo == Test case %%i == & \
$(GEN) $(TEST_DIR)\input_%%i.txt & \
$(RUN) < $(TEST_DIR)\input_%%i.txt > $(TEST_DIR)\output_run_%%i.txt & \
$(BRUTE) < $(TEST_DIR)\input_%%i.txt > $(TEST_DIR)\output_brute_%%i.txt & \
fc $(TEST_DIR)\output_run_%%i.txt $(TEST_DIR)\output_brute_%%i.txt >nul && \
echo Test %%i: Outputs match! || echo Test %%i: Outputs differ! Check input_%%i.txt \
)
# Clean rule
clean:
rm -f $(GEN) $(RUN) $(BRUTE)
rm -rf $(TEST_DIR)
```
## 哈味 Simulation (Testbench)
SystemVerilog testbench
設計的思路會是使用兩個memory當作輸入,一個memory當作輸出。這邊使用interface去串接DUT和testbench,所以會有三個memory interface和一個DUT interface,另外要check NTT結果是否正確也會有NTT的interface從DUT接去testbench。
### Memory interface and class memory
[code](https://github.com/zBING-QIAN/Algorithm/blob/main/NTT/verification/aux_if.sv)
其中memory_if是為了連接DUT和TB,其中clocking block是為了模擬output delay 5 timescale。
另一個類似的memory_linker_if,主要負責DUT內部memory傳來傳去使用,如果DUT內使用memory_if,modelsim會爆出底下的錯誤
`Variable '/mytb/u_conv/u_ntt/rearrange_memif/valid' driven in a combinational block, may not be driven by any other process. See ./verification/aux_if.sv(17).`
[參考](https://verificationacademy.com/forums/t/driving-with-and-without-clocking-blocks/38008/8) 主要就是clocking block的問題,因為clocking block為了模擬delay所以提供了透過clocking block去drive output的signal,且output在always_comb中被assign(drive),所以會被always_comb檢查出來output在不同地方被drive(always@(\*)則不會),簡而言之always_comb會比較嚴格檢查是不是可合成的,always@(\*)則不完全,所以systemverilog要寫DUT要用always_comb和always_ff(雖然有時候會忘記哈哈)。可以參考[[Behavior difference between always_comb and always@(*)]](https://stackoverflow.com/questions/32778798/behavior-difference-between-always-comb-and-always)
class memory提供一個delay cycle的選項,方便不同memory條件做調整
### DUT/NTT interface
[code](https://github.com/zBING-QIAN/Algorithm/blob/main/NTT/verification/dut_if.sv)
由於主要的結果和資料都是透過memory_if存在memory裡面,所以這邊定一些簡易的控制signal像是clk,rst,enable,done。
### Environment
[code](https://github.com/zBING-QIAN/Algorithm/blob/main/NTT/verification/environment.sv)
負責初始化gen(產生data),drv(負責控制DUT和蒐集結果),scb/scb_ntt(負責計算groundtruth和比較結果)
### Driver(drv)
[code](https://github.com/zBING-QIAN/Algorithm/blob/main/NTT/verification/driver.sv)
這邊定義了整個verification的流程
1.先向gen要test case
2.rest DUT
3.等待第一組array做NTT
4.打包結果送給scb_ntt
5.等待第二組array做NTT
6.打包結果送給scb_ntt
7.等待前兩項結果內積並INTT
8.打包INTT結果送給scb_ntt
9.等待conv的結果
10.打包結果送給scb
### Generator(gen)
[code](https://github.com/zBING-QIAN/Algorithm/blob/main/NTT/verification/generator.sv)
簡單地走個生測資的流程
由於modelsim要使用random()要有licence,但我就是個免費仔只好繞個彎路在[transcation](https://github.com/zBING-QIAN/Algorithm/blob/main/NTT/verification/transaction.sv)自訂義一個random,另外systemverilog的random真的不好用,所以我改用DPI-C調用我用C++寫得函數產生測資。
簡單說一下DPIC定義時除了要定義參數類型,還要定義是input/inout,否則會發生各種問題。
### Scoreboard(scb/scb_ntt)
兩個基本差不多,主要就是用DPI-C去調用之前寫好的[C++函數](https://github.com/zBING-QIAN/Algorithm/blob/main/NTT/verification/NTTConv.cpp)然後驗證答案,優點是就不用重寫一次NTT和convolution,也不受限於開檔讀固定測資,只要`#include "svdpi.h"`並修改一些就可以使用。
```
// DPIC
#include "svdpi.h"
extern "C" bool SW_CONV_check(svOpenArrayHandle *a, svOpenArrayHandle *b, svOpenArrayHandle *a_res, svOpenArrayHandle *b_res, svOpenArrayHandle *res)
{
int N = 256, P = 257, PRT = 3;
// int N = 256, P = 12289, PRT = 11;
int *aptr = (int *)svGetArrayPtr(a);
int *bptr = (int *)svGetArrayPtr(b);
int *aresptr = (int *)svGetArrayPtr(a_res);
int *bresptr = (int *)svGetArrayPtr(b_res);
int *resptr = (int *)svGetArrayPtr(res);
vector<int> atmp(N, 0), btmp(N, 0), restmp(N, 0);
for (int i = 0; i < N; i++)
{
atmp[i] = aptr[i];
btmp[i] = bptr[i];
}
Conv(atmp, btmp, restmp, N, P, PRT);
for (int i = 0; i < N; i++)
{
// if diff -> print error
if (restmp[i] != resptr[i])
{
for (int j = 0; j < N; j++)
{
printf("SW : %d %d %d\n", j, restmp[j], resptr[j]);
}
return 0;
}
}
return 1;
}
extern "C" bool SW_NTT_check(svOpenArrayHandle *a, svOpenArrayHandle *res)
{
int N = 256, P = 257, PRT = 3;
// int N = 256, P = 12289, PRT = 11;
int *aptr = (int *)svGetArrayPtr(a);
int *resptr = (int *)svGetArrayPtr(res);
vector<int> atmp(N, 0), restmp(N, 0);
for (int i = 0; i < N; i++)
{
atmp[i] = aptr[i];
}
NTT(atmp, restmp, N, P, PRT);
for (int i = 0; i < N; i++)
{
// if diff -> print error
if (restmp[i] != resptr[i])
{
for (int j = 0; j < N; j++)
{
printf("%d %d %d\n", j, restmp[j], resptr[j]);
}
return 0;
}
}
return 1;
}
extern "C" void gencase(svOpenArrayHandle *a, svOpenArrayHandle *b)
{
// srand(std::chrono::high_resolution_clock::now().time_since_epoch().count());
int N = 256, P = 257;
// int N = 256, P = 12289, PRT = 11;
int *aptr = (int *)svGetArrayPtr(a);
int *bptr = (int *)svGetArrayPtr(b);
vector<int> atmp(N, 0), restmp(N, 0);
// i< N/2 is for non cyclic convolution, otherwise i<N is for cyclic convolution
for (int i = 0; i < N / 2; i++)
{
aptr[i] = rand() % P;
bptr[i] = rand() % P;
}
cout << "a :\n";
for (int i = 0; i < N; i++)
cout << aptr[i] << " ";
cout << endl;
cout << "b :\n";
for (int i = 0; i < N; i++)
cout << bptr[i] << " ";
cout << endl;
}
```
svOpenArrayHandle : 負責接dpic inout的array handler,要透過svGetArrayPtr轉成int*,這邊只處理1維最簡單的狀態,高維或複雜的資料的處理方式[參考](https://www.doulos.com/knowhow/systemverilog/systemverilog-tutorials/systemverilog-dpi-tutorial/)
另外就是不要想在systemverilog中操作C/C++的物件型態,即便有chandle可以用,chandle也只能傳給另一個DPI-C function去使用,如果想要在systemverilog使用就必須要用systemverilog的型態或結構去接值。
## 哈味 Simulation (Naive Implementation)
這邊說一下Decimation-in-Time(DIT),在C++中要很麻煩的[預處理](https://github.com/zBING-QIAN/Algorithm/blob/4d852c678627dd56d7ff509535acf16daa1ccd38/NTT/SW/NTTConv.cpp#L26)位置,但是如果仔細觀察可以發現其實轉換的位置的binary representation是原位置的顛倒,舉個例子(長度為256的FFT):
```
00000000 -> 00000000 00000100 -> 00100000 ...
00000001 -> 10000000 00000101 -> 10100000 ...
00000010 -> 01000000 00000110 -> 01100000 ...
00000011 -> 11000000 00000111 -> 11100000 ...
```
用硬體描述語言的話這一塊是相對容易寫的,可以在向momory要位置時做mapping:
```
for (i = '0; i < $clog2(DEPTH); i++) begin
addr_r1[i] = addr_r0[$clog2(DEPTH)-1-i];
end
```
其他實作的部分就是很單純的硬寫出來,純苦力沒甚麼重點。
## Murmur
做完一件事真的要寫紀錄會比較好,像是SW模擬時input range老實說我一開始隨便估一下,沒特別考慮random的limit,所以一開始是設定128,為了要在這邊解釋清楚SW的模擬反而需要仔細算,才發現一開始亂設限制其實會錯(在不modulo的情況),只不過random之後比較不會碰到那種爆掉的測資。
systemverilog寫起來就很像多執行續的程式,一開始做的時候常常會搞出deadlock,習慣之後就還可以,不過有可能是我用到的機制還沒有複雜到要處理active region,NBA region之類的dependency來debug,之後再看看會不會用到。