用 verilator 的目的,就是要驗證 verilog 的實作是正確的,但我們又怎麼知道什麼是正確的呢?
就要準備好 C model 跟 SystemC 的實作了。

C model 其實沒有一定要用 C 寫,只要能夠模擬你的實作、得到標準答案的輸入與輸出,要求速度用 python 實作也 OK, 但選用 C/C++ 的一個優勢在於 verilator 是用 C++ 寫的,所以用 C/C++ 寫的 model 比較能和 verilator 的驗證結合在一起。
SystemC 則是一套 C++ 的函式庫,幫助你實作比較接近底層的硬體行為,把一個大系統拆解成 SystemC 實作的小塊, 在軟體設計時就考慮到硬體實作,減少硬體在未來需要重新設計的風險。

C model v1

依我自己工作時作為 hardware tester 的經驗,我之前公司的 C model 跟電路設計這邊是分開的, 有獨立的 repository 在管理,使用時要獨立把它簽出來編譯成執行檔,編譯之後輸入輸出是透過檔案來進行。

在這個專案設計之初,我是依照這個經驗在設計的,因此我第一版的 C model 是使用 GNU多重精度運算庫 libgmp 來開發,這個 v1 後來被 v2 替換掉了,原因下述。
下面是保留在 github 上面 的檔案內容:

void two_power_mod( mpz_t out, const unsigned power, const mpz_t N) {
  mpz_t base;
  mpz_init_set_ui(base, 1u);

  for (int i = 0; i < power; ++i) {
    mpz_mul_ui(base, base, 2u);
    if (mpz_cmp(base, N) > 0) {
      mpz_sub(base, base, N);
    }
  }
  mpz_set(out, base);
}

void montgomery_base2(mpz_t out, const mpz_t A, const mpz_t B, const mpz_t N) {
  mpz_t round_result; // S in doc
  mpz_init_set_ui(round_result, 0);

  for (int i = 0; i < 256; ++i) {
    bool bit_i = mpz_tstbit(A, i);
    if (bit_i) {
      mpz_add(round_result, round_result, B);
    }
    bool is_odd = mpz_tstbit(round_result, 0);
    if (is_odd) {
      mpz_add(round_result, round_result, N);
    }
    mpz_tdiv_q_ui(round_result, round_result, 2u);
  }
  if (mpz_cmp(round_result, N) > 0) {
    mpz_sub(round_result, round_result, N);
  }
  mpz_set(out, round_result);
}

void lsb_modular_exponentiation(mpz_t out, const mpz_t A,
    const mpz_t B, const mpz_t N) {
  mpz_t square; // T in doc
  mpz_t multiple; // S in doc
  mpz_init_set(square, A);
  mpz_init_set_ui(multiple, 1u);

  for (int i = 0; i < 256; ++i) {
    bool bit_i = mpz_tstbit(B, i);
    if (bit_i) {
      montgomery_base2(multiple, multiple, square, N);
    }
    montgomery_base2(square, square, square, N);
  }

  mpz_set(out, multiple);
}

void rsa(mpz_t out, const mpz_t msg, const mpz_t key, const mpz_t N) {
  mpz_t pack_value;
  mpz_t packed_msg;
  mpz_t crypto_msg;
  mpz_inits(pack_value, packed_msg, crypto_msg, NULL);

  two_power_mod(pack_value, 512, N);
  montgomery_base2(packed_msg, msg, pack_value, N);
  lsb_modular_exponentiation(out, packed_msg, key, N);
}

我並沒有打算解釋為什麼 RSA 是這樣做啦…。
簡單來說最關鍵的是 montgomery algorithm , 因為給定 a, b, N,要計算 $ a \cdot b \bmod N $,在取餘數-也就是除法-會很慢 而 montgomery algorithm 可以很快的算完 $ 2^{-256} \cdot a \cdot b \bmod N $。
因此只要我們先準備好 $ 2^{512} \bmod N $,搭配 montgomery 實作 square and multiply , 就能很快算出 rsa 需要的 modular exponential 了。

並不是我們用了 libgmp 就可以直接去算 $ m^e \bmod N $, 不然 libgmp 甚至有附安全版本的 modular exponential , rsa 一行指令就寫完了;實作要使用 montgomery C-model 就要盡量用 montgomery 去算, 未來在實作 SystemC 和硬體的時候才有標準答案可以驗證。

C model v2

在我堪堪作完 C model 開始進到 SystemC 之後,因為自己對 SystemC 不熟,多次諮詢強者我同學 johnjohnlin 大大, 後來他看不下去直接跳下來實作了。
也多愧了 johnjohnlin 大大的加入,他對 C++ 就熟稔程度跟我不是同一個檔次的, 本來我是使用 SystemC 內建的 Arbitrary Width Bit Type sc_bv 來實作, johnjohnlin 跳進來直接實作一套模擬 verilog register 的 class,也支援 verilog 的各種運算, 例如位移、邏輯運算、加減乘除等等。
相關的實作可見專案的 common/include/verilog 資料夾 , 至於他因為這個專案又弄出另一套 namedtuple 的 C++ 工具函式庫,又是另一回事了。

有了這套 verilog 的 class 後,rsa 的 C model 也用 vint 改寫,選個 montgomery 的部分來比較看看:

constexpr unsigned kBW_RSA = 256;
typedef verilog::vuint<kBW_RSA> rsa_key_t;

void montgomery_base2(rsa_key_t &out, const rsa_key_t &A, const rsa_key_t &B,
                      const rsa_key_t &N) {
  using extend_key_t = verilog::vuint<kBW_RSA + 2>;
  extend_key_t round{0};
  const extend_key_t extend_B = static_cast<extend_key_t>(B);
  const extend_key_t extend_N = static_cast<extend_key_t>(N);
  for (int i = 0; i < 256; ++i) {
    if (A.Bit(i)) {
      round += extend_B;
    }
    if (round.Bit(0)) {
      round += extend_N;
    }
    round >>= 1;
  }
  if (round > extend_N) {
    round -= extend_N;
  }
  out = static_cast<rsa_key_t>(round);
}

比起 version 1,有更明確的指出輸入輸出,以及中間儲存結果的位元數,這是 libgmp 的 model 做不到的。

在這個階段我跟 johnjohnlin 有爭執過一段時間,兩者分別的主張是:

我的主張 johnjohnlin 的主張
C model 沒必要跟實作綁這麼緊,用 libgmp 實作可以跳過細節,專注在演算法上,模擬硬體行為是之後 SystemC 開發者的責任 與硬體行為相近的實作是必要的,愈是貼近未來在驗證上的阻力就愈小

當然,這兩者的差距在這個 project 上面還看不太出來,畢竟 RSA 稱不上是多複雜的演算法,處理的資料量也很小, 最後就選了 johnjohnlin 的方案,直接在 C model 實作上更貼近最終硬體。

希望大家能在下面留下各位的意見跟經驗了。

C model Test

C model 的開發都透過 google test 進行測試 , 部分子模組的測資是利用 python 去生的,也有一些是數學上的預期結果,例如 $ Montgomery(2^{256}, A) = A $; 另外,當初修課的講義也有留下一些測資。
以下是 rsa256 的其中一組測試:

TEST(RsaTest, test_rsa) {
  // example from tech document or using python hex(m0 ** key % N)
  string str_N("E07122F2A4A9E81141ADE518A2CD7574DCB67060B005E24665EF532E0CCA73E1");
  string str_key("10001");
  string str_m0("412820616369726641206874756F53202C48544542415A494C452054524F50");
  string str_c0("D41B183313D306ADCA09126F3FED6CDEC7DCDCE49DB5C85CB2A37F08C0F2E31");
  rsa_key_t N, key, m0, c0, out;
  from_hex(N, str_N);
  from_hex(key, str_key);
  from_hex(m0, str_m0);
  from_hex(c0, str_c0);

  // check output
  rsa(out, m0, key, N);
  EXPECT_EQ(out, c0);
}

SystemC

就 RSA256 來看,其實 C model 跟 SystemC 的差距真的很小,因為我們在 C model 的部分就把演算法切割得差不多了, 幾個不同的地方像是 SystemC 每個函式還是成包成 class,並且會定義出輸入/輸出的資料型別。
我個人是推薦直接使用 模組名稱 + In模組名稱 + Out 來定義輸入/輸出的資料型別,例如:

struct RSAModIn {
  friend ::std::ostream &operator<<(::std::ostream &os, const RSAModIn &v) {
	os << typeid(v).name() << "{" << v.msg << ", " << v.key << ", " << v.modulus
   	<< ::std::endl;
	return os;
  }
  KeyType msg;
  KeyType key;
  KeyType modulus;
};
using RSAModOut = KeyType;

定義 operator« 是 SystemC 的要求,有考慮過要不要設計一個基礎的型別實作 operator«,輸入輸出都繼承那個基礎型別就好, 但最後沒有這樣設計;另外即便輸出型別就是 KeyType,仍然使用 using 造出別名,以示區隔。

下面就是使用 SystemC 實作的 RSA module,在下一章介紹 verilator framework 會看到,設計硬體模組的時候, 大多會使用所謂的 valid/ready handshake protocol,可以很直接地對應到 SystemC 的 FIFO, 因此模組的介面使用 sc_fifo_in/sc_fifo_out 來參考到外部的 fifo。
相較 C model 可以直接呼叫函式,SystemC 各函式都被包裹在模組之中,位在上層的模組也要規畫好對應使用的 fifo:

SC_MODULE(RSA256) {
  sc_in_clk clk;
  sc_fifo_in<RSAModIn> i_data;
  sc_fifo_out<KeyType> o_crypto;

  RSAMontgomery i_montgomery;
  RSATwoPowerMod i_two_power_mod;
  sc_fifo<RSATwoPowerModIn> two_power_mod_in;
  sc_fifo<RSATwoPowerModOut> two_power_mod_out;
  sc_fifo<RSAMontgomeryModIn> montgomery_in;
  sc_fifo<RSAMontgomeryModOut> montgomery_out;

  SC_CTOR(RSA256)
  	: i_montgomery("i_montgomery"), i_two_power_mod("i_two_power_mod") {
    // two_power_mod
    i_two_power_mod.clk(clk);
    i_two_power_mod.data_in(two_power_mod_in);
    i_two_power_mod.data_out(two_power_mod_out);
    // montgomery
    i_montgomery.clk(clk);
    i_montgomery.data_in(montgomery_in);
    i_montgomery.data_out(montgomery_out);
    SC_THREAD(Thread);
  }

  void Thread();
};

以下是 Thread 函式的實作,變數在未來會對應到所需的 verilog register,每一對 fifo 的 module_in.write 跟 module_out.read, 就是對一個模組寫入輸入並等待輸出,來模擬 verilog module 的讀寫。

void RSA256::Thread() {
  char str[256];
  while (true) {
    const RSAModIn &in = i_data.read();
    const RSATwoPowerModIn::TwoPowerMod_Power_t vuint512{512};
    two_power_mod_in.write({.power = vuint512, .modulus = in.modulus});

    auto pack_value = two_power_mod_out.read();
    montgomery_in.write({.a = in.msg, .b = pack_value, .modulus = in.modulus});
    auto packed_msg = montgomery_out.read();
    const auto &key = in.key;

    KeyType crypto;
    KeyType multiple{1};
    KeyType square{packed_msg};
    for (size_t i = 0; i < kBW; i++) {
      if (key.Bit(i)) {
        montgomery_in.write({.a = multiple, .b = square, .modulus = in.modulus});
        multiple = montgomery_out.read();
      }
      montgomery_in.write({.a = square, .b = square, .modulus = in.modulus});
      square = montgomery_out.read();
    }
    crypto = multiple;
    o_crypto.write(crypto);
  }
}

如此一來,一個複雜的演算法,就能透過 SystemC 拆分成很多小小的模組,提供了實作及模組間連結的參考, 每個小模組就能分別交由下面的人來實作。
例如這版的 RSA 我挑戰一個更激進的目標,我只要用一個 montgomery module,讓我的 RSA 面積降到最小, 不同於我在作業裡用的三個,雖然說作業這樣實作是因為有速度要求,square and multiply 要兩個 montgomery 同時處理才夠快, montgomery 也不能一次 1 個 bit,要一次實作 2 個 bits 的版本才夠快。

以上大概就是 C model 與 SystemC 的概述,準備好我們的 RSA256 C model 和 SystemC,下一回終於要進到我們的主角 verilator 了。