SystemC のビルド

SystemC はハードウェアを C++ で実装したもので、ソフトウェアロジックとの高い親和性が期待できるものと解釈している。SystemC 自体はライブラリでスタティックライブラリとユーザーが書いた C++ のコードをビルドして、実行バイナリからシミュレーションを行える。(PLD への論理合成はどうやるのか不明)

SystemC はソースが配布されているのでそれをビルドする必要がある。2012年7月に最新版である 2.3.0 が5年ぶりに release されているのでこれを使う。やり方が説明されている 2.2.0 にしたかったが configure が通らなかった。

version 2.3.0 から cygwinmingw もちゃんとビルドできるようになっているので ./configure; make するだけでビルドが通った。

    o Windows XP Prof. SP3 (Cygwin 1.5.25) with GNU C++ compiler
      version gcc-4.3.2
    o Windows XP Prof. SP3 (Msys 1.0.16) with MinGW32
      GNU C++ compiler version gcc-4.5.2 through gcc-4.6.2

今回 SystemC を選んだ理由はビデオコントローラのシミュレーションと画像表示を1つの exe ファイルで完結できる点や、ソフト側での ROM/RAM イメージの高速処理を期待している。

main, sc_main の扱い

SystemC は main() -> ライブラリの初期化 -> sc_main() という流れで実行するのだが、 GUI のウィンドウからシミュレータを呼び出したいので、 main を取られると不便。

これは systemc-2.3.0/src/sysc/kernel/sc_main.cpp に記載されている。

int
main( int argc, char* argv[] )
{
	return sc_core::sc_elab_and_sim( argc, argv );
}

というわけでなので sc_main.cpp をコンパイル,ビルドしないように Makefile.am の CXX_FILES から sc_main.cpp を取り除く。systemC を動かす場合は記載通り sc_core::sc_elab_and_sim() を呼び出せばよい。

ソースを書いてみる

ROM.RAMイメージのバイナリファイルを読み込んで RAM に展開してくれればよいのでそれっぽいソースを書いてみた。

SC_MODULE(Ram)
{
	sc_in m_clk, m_we;
	sc_in m_a;
	sc_in m_d;
	sc_out m_q;
	
	sc_signal *const data;
	void thread()
	{
		while(true){
			wait(m_clk.posedge_event());
			m_q.write(data[m_a.read() & 0x1fff]);
			if(m_we.read() == 1){
				data[m_a] = m_d;
			}
		}
	}
	
	SC_HAS_PROCESS(Ram);
	Ram(sc_module_name n) 
		: sc_module(n), data(new sc_signal[0x2000]), 
		m_clk("clk"), m_a("a"), m_d("d"), m_q("q")
	{
		for(int i = 0; i < 0x2000; i++){
			data[i] = 0x44;
		}
		
		SC_THREAD(thread);
	}
	~Ram()
	{
		delete [] data;
	}
};

RAM の部分は sc_signal *const data; にて構成される。おそらくハード的な SRAM みたいな定義として作るのであればこれは不適切である。アドレスは容量を超えないように 0x1fff で and してあるが、これを抜いて未定義領域にアクセスしたら access violation あたりで落ちる。

thread は verilog でいう always を意識した書き方にした。無限ループに入る前には RAM データを初期化できるのでここにバイナリデータを流し込むメソッドをいれればよいことがわかった。

SC_MODULE(RamController)
{
	sc_in m_clk;
	sc_out m_q;
	sc_signal m_ram_a;
	sc_signal m_ram_we;
	sc_signal m_ram_d, m_ram_q;
	Ram m_ram;
	
	void thread()
	{
		while(true){
			wait(m_clk.posedge_event());
			m_ram_a = m_ram_a.read() + 1;
			m_q.write(m_ram.m_q.read());
		}
	}
	SC_HAS_PROCESS(RamController);
	RamController(sc_module_name n) 
		: sc_module(n), m_clk("clk"), m_q("q"), m_ram("RAM")
	{

		m_ram_a = 0;
		m_ram_d = 0xaa;
		m_ram_we = 0;
		m_ram.m_clk(m_clk);
		m_ram.m_we(m_ram_we);
		m_ram.m_d(m_ram_d);
		m_ram.m_a(m_ram_a);
		m_ram.m_q(m_ram_q);

		SC_THREAD(thread);
	}
};

RAM のアドレスをインクリメントしてデータの変化を見るための上位モジュール。

ここではまったのは下位モジュールに渡す配線をクラス内部の変数にしないといけないこと。最初は thread() やコンストラクタのなかに変数宣言をいれて渡していたのだが、ライブラリ側から配線がないと文句を言ってきたり、それを言う前に落ちたりした。

もう1つ注意をしたい点は m_ram_a のインクリメント処理である。 テンプレート sc_signal には +=, ++ 演算子は定義されていないのでベーシックな書き方をしないといけない。さらにいうと値の読み出しは .read() を使った方がよいらしい(やらなくてもできるけど)。 = 演算子は .write(val) ということらしいが、これも明示した方がよいという記述も見かけた。

ここまでやった感想

C++ で記述できるということは、テンプレートやクラスの扱いをしらないといけない。さらにハード的な記述とソフト的な記述が簡単に混ざる。それに加えてクラスの寿命やポインタの管理、冗長な配線定義など非常にかったるい。

今回は RAM の初期化と GUI との連携ができればそれでよいのでこれ以上 SystemC には触らないようにする。NSL 側から SystemC のソースを吐いてもらって組み合わせられればよい。

これで動作速度が遅かったら SystemC は運用から見送る。