Vengineerの戯言

人生は短いけど、長いです。人生を楽しみましょう!

XilinxのQEMU + SystemC + Verilog HDL (Verilator) のデモの内容を探っていく(その3)

@Vengineerの戯言 : Twitter
SystemVerilogの世界へようこそすべては、SystemC v0.9公開から始まった 

はじめに

XilinxQEMU + SystemC + Verilog HDL (Verilator) のデモの内容を探っていく(その3)。

その1、その2 については、下記のブログを参照してください。
XilinxのQEMU + SystemC + Verilog HDL (Verilator) のデモの内容を探っていく(その1)
XilinxのQEMU + SystemC + Verilog HDL (Verilator) のデモの内容を探っていく(その2)

今日は、SystemC側のモデルで使っている インターコネクト (iconnect.h) の中身を見ていきます。

下記が iconnect クラスの header に相当する class 定義の部分です。iconnect は、N_INITIATORS個の tlm_utils::simple_target_socket_tagged な target port と N_TARGETS個の tlm_utils::simple_initiator_socket_tagged な initiator port を持っています。target port は Zynq や DMA などの initiator を接続し、initiator port は DMAのレジスタなどの target を接続します。
それぞれについて説明します。

template<unsigned int N_INITIATORS, unsigned int N_TARGETS>
class iconnect
: public sc_core::sc_module
{
public:
	struct memmap_entry map[N_TARGETS * 4];

	tlm_utils::simple_target_socket_tagged<iconnect> *t_sk[N_INITIATORS];
	tlm_utils::simple_initiator_socket_tagged<iconnect> *i_sk[N_TARGETS];

	SC_HAS_PROCESS(iconnect);
	iconnect(sc_core::sc_module_name name);
	virtual void b_transport(int id,
				 tlm::tlm_generic_payload& trans,
				 sc_time& delay);

	virtual bool get_direct_mem_ptr(int id,
                                  tlm::tlm_generic_payload& trans,
                                  tlm::tlm_dmi&  dmi_data);

	virtual unsigned int transport_dbg(int id,
					tlm::tlm_generic_payload& trans);

	virtual void invalidate_direct_mem_ptr(int id,
                                         sc_dt::uint64 start_range,
                                         sc_dt::uint64 end_range);

	/*
	 * set_target_offset()
	 *
	 * Used to allow the users to attach an initiator socket
	 * to our target socket that gets all of it's accesses offset by
         * a base before entering the interconnect.  */
	void set_target_offset(int id, sc_dt::uint64 offset);
	int memmap(sc_dt::uint64 addr, sc_dt::uint64 size,
		enum addrmode addrmode, int idx, tlm::tlm_target_socket<> &s);
private:
	sc_dt::int64 target_offset[N_INITIATORS];

	unsigned int map_address(sc_dt::uint64 addr, sc_dt::uint64& offset);
	void unmap_offset(unsigned int target_nr,
				sc_dt::uint64 offset, sc_dt::uint64& addr);

};

target port

target port は、 tlm_utils::simple_target_socket_tagged を使っています。iconnect の target port を見る前に、target デバイスの例である memory.{h,cc}

memory

memory の header (クラス定義部) を下記に示します。

class memory
: public sc_core::sc_module
{
public:
	tlm_utils::simple_target_socket<memory> socket;

	const sc_time LATENCY;

	memory(sc_core::sc_module_name name, sc_time latency, off_t size_);
	virtual void b_transport(tlm::tlm_generic_payload& trans,
					sc_time& delay);
	virtual bool get_direct_mem_ptr(tlm::tlm_generic_payload& trans,
                                  tlm::tlm_dmi& dmi_data);
	virtual unsigned int transport_dbg(tlm::tlm_generic_payload& trans);

private:
	uint8_t *mem;
	off_t size;
};

Constructor 以外には、以下の3つのメソッドがあります。

  • void b_transport(tlm::tlm_generic_payload& trans, sc_time& delay);
  • bool get_direct_mem_ptr(tlm::tlm_generic_payload& trans, tlm::tlm_dmi& dmi_data);
  • unsigned int transport_dbg(tlm::tlm_generic_payload& trans);

この3つのメソッドは、memoryのソースコードで次のように定義されています。

void memory::b_transport(tlm::tlm_generic_payload& trans, sc_time& delay)
{
	tlm::tlm_command cmd = trans.get_command();
	sc_dt::uint64    addr = trans.get_address();
	unsigned char*   ptr = trans.get_data_ptr();
	unsigned int     len = trans.get_data_length();
	unsigned char*   byt = trans.get_byte_enable_ptr();

	if (addr > sc_dt::uint64(size)) {
		trans.set_response_status(tlm::TLM_ADDRESS_ERROR_RESPONSE);
		SC_REPORT_FATAL("Memory", "Unsupported access\n");
		return;
	}
	if (byt != 0) {
		trans.set_response_status(tlm::TLM_BYTE_ENABLE_ERROR_RESPONSE);
		SC_REPORT_FATAL("Memory", "Unsupported access\n");
		return;
	}

	if (trans.get_command() == tlm::TLM_READ_COMMAND)
		memcpy(ptr, &mem[addr], len);
	else if (cmd == tlm::TLM_WRITE_COMMAND)
		memcpy(&mem[addr], ptr, len);

	delay += LATENCY;

	trans.set_dmi_allowed(true);
	trans.set_response_status(tlm::TLM_OK_RESPONSE);
}

bool memory::get_direct_mem_ptr(tlm::tlm_generic_payload& trans,
				tlm::tlm_dmi& dmi_data)
{
	dmi_data.allow_read_write();

	dmi_data.set_dmi_ptr( reinterpret_cast<unsigned char*>(&mem[0]));
	dmi_data.set_start_address(0);
	dmi_data.set_end_address(size - 1);
	/* Latencies are per byte.  Our latency is expressed per access,
	   which are in 32bits so dividie by 4. Is there a better way?.  */
	dmi_data.set_read_latency(LATENCY / 4);
	dmi_data.set_write_latency(LATENCY / 4);
	return true;
}

unsigned int memory::transport_dbg(tlm::tlm_generic_payload& trans)
{
	tlm::tlm_command cmd = trans.get_command();
	sc_dt::uint64    addr = trans.get_address();
	unsigned char*   ptr = trans.get_data_ptr();
	unsigned int     len = trans.get_data_length();
	unsigned int num_bytes = (len < (size - addr)) ? len : (size - addr);

	if (cmd == tlm::TLM_READ_COMMAND)
		memcpy(ptr, &mem[addr], num_bytes);
	else if ( cmd == tlm::TLM_WRITE_COMMAND )
		memcpy(&mem[addr], ptr, num_bytes);

	return num_bytes;
}

b_transport は、blocking アクセスの時のメソッドです。b_transportでは、payload からの各種情報からアクセスチェック後、cmd によって、リード・ライトの区別をして、mem にアクセスしています。
get_direct_mem_ptrの前に、transport_dbg について説明します。ransport_dbg はデバック用のメソッドで、b_transport と同じ感じですがアクセスチェック等は行っていません。

get_direct_mem_ptrは、target に直接アクセスするためのメソッドで、この target が途中のバス等を介さずに直接アクセスできる場合にメモリのポインタを返します。
引数のdmi_data を使って、リード・ライト可能なサイズ(size)なメモリで、リードおよびライトの latency を LATENCY/4 に設定しています。memory が 直接アクセスできる target がどうかを知るには、b_transport にてアクセスしてみて、戻ってきた payload の transのメソッド get_dmi_allowed を実行し、true が返ってきたら、get_direct_mem_ptr メソッドを使って獲得した dmi_data の set_dmi_ptr メソッドにてメモリのポインタを獲得します。


これら3つのメソッドは、constructor にて socket.register_xxxx のようにして socket に登録しています。initiator から b_transport が実行されたときにアクセスするアドレスが memoryの場合は、memory の b_transport メソッドが呼ばれることになります。

memory::memory(sc_module_name name, sc_time latency, off_t size_)
	: sc_module(name), socket("socket"), LATENCY(latency)
{
	socket.register_b_transport(this, &memory::b_transport);
	socket.register_get_direct_mem_ptr(this, &memory::get_direct_mem_ptr);
	socket.register_transport_dbg(this, &memory::transport_dbg);

	size = size_;
	mem = new uint8_t[size];
	memset(&mem[0], 0, size);
}

iconnect の target port

iconnect の

  • void b_transport(tlm::tlm_generic_payload& trans, sc_time& delay);
  • bool get_direct_mem_ptr(tlm::tlm_generic_payload& trans, tlm::tlm_dmi& dmi_data);
  • unsigned int transport_dbg(tlm::tlm_generic_payload& trans);

のメソッドは次のように定義されています。

iconnect では、memory と引数の数が違います。iconnect では、引数(int id) が最初の引数として追加されています。これは target port が複数個あるため、どの target port かを区別するために使われます。各メソッドでは、最初にid が N_INITIATORS (target portの数) 以下かどうかをチェックしています。

b_transport メソッドでは、trans の get_address メソッドで アドレスを獲得後、id にて target の offset を address を追加しています。この address を使って、map_address メソッドにて target の番号 (target_nr) と offset を獲得します。この offset が target_nr の initiator port へアドレスになるため、trans.set_address(offset) で再設定しています。そして、(*i_sk[target_nr])->b_transport(trans, delay) にて target_nr の initiator port の b_transport を実行しています。最後に、trans.set_address(addr) でアドレスを元に戻しています。

template<unsigned int N_INITIATORS, unsigned int N_TARGETS>
void iconnect<N_INITIATORS, N_TARGETS>::b_transport(int id,
			tlm::tlm_generic_payload& trans, sc_time& delay)
{
	sc_dt::uint64 addr;
	sc_dt::uint64 offset;
	unsigned int target_nr;

	if (id >= (int) N_INITIATORS) {
		SC_REPORT_FATAL("TLM-2", "Invalid socket tag in iconnect\n");
	}

	addr = trans.get_address();
	addr += target_offset[id];
	target_nr = map_address(addr, offset);

	trans.set_address(offset);
	/* Forward the transaction.  */
	(*i_sk[target_nr])->b_transport(trans, delay);
	/* Restore the addresss.  */
	trans.set_address(addr);
}


transport_dbg メソッドは、b_transport と同じ内容になっています。

template<unsigned int N_INITIATORS, unsigned int N_TARGETS>
unsigned int iconnect<N_INITIATORS, N_TARGETS>::transport_dbg(int id,
				tlm::tlm_generic_payload& trans)
{
	sc_dt::uint64 addr;
	sc_dt::uint64 offset;
	unsigned int target_nr;

	if (id >= (int) N_INITIATORS) {
		SC_REPORT_FATAL("TLM-2", "Invalid socket tag in iconnect\n");
	}

	addr = trans.get_address();
	addr += target_offset[id];
	target_nr = map_address(addr, offset);

	trans.set_address(offset);
	/* Forward the transaction.  */
	(*i_sk[target_nr])->transport_dbg(trans);
	/* Restore the addresss.  */
	trans.set_address(addr);
	return 0;
}

get_direct_mem_ptr メソッドでは、initiator port への get_direct_mem_ptr にアクセスして、戻ってきた dmi_data に対して、start_address と end_address を再計算しているだけです。

template<unsigned int N_INITIATORS, unsigned int N_TARGETS>
bool iconnect<N_INITIATORS, N_TARGETS>::get_direct_mem_ptr(int id,
					tlm::tlm_generic_payload& trans,
					tlm::tlm_dmi& dmi_data)
{
	sc_dt::uint64 addr;
	sc_dt::uint64 offset;
	unsigned int target_nr;
	bool r;

	if (id >= (int) N_INITIATORS) {
		SC_REPORT_FATAL("TLM-2", "Invalid socket tag in iconnect\n");
	}

	addr = trans.get_address();
	addr += target_offset[id];
	target_nr = map_address(addr, offset);

	trans.set_address(offset);
	/* Forward the transaction.  */
	r = (*i_sk[target_nr])->get_direct_mem_ptr(trans, dmi_data);

	unmap_offset(target_nr, dmi_data.get_start_address(), addr);
	dmi_data.set_start_address(addr);
	unmap_offset(target_nr, dmi_data.get_end_address(), addr);
	dmi_data.set_end_address(addr);
	return r;
}

map/unmap

それぞれのメソッドで使われた map_address メソッドは下記のようになっています。initiator port に接続している target の map 情報から address がマッチする target id と offset を見つけます。address が マッチしないと、エラーメッセージを出力し、0 を返します。 戻り値 id は 0 も取りうるので、エラーメッセージを見ないとエラーが発生したことが分からないので気を付ける必要がありますね。

template<unsigned int N_INITIATORS, unsigned int N_TARGETS>
unsigned int iconnect<N_INITIATORS, N_TARGETS>::map_address(
			sc_dt::uint64 addr,
			sc_dt::uint64& offset)
{
	unsigned int i;

	for (i = 0; i < N_TARGETS * 4; i++) {
		if (map[i].size
		    && addr >= map[i].addr
		    && addr <= (map[i].addr + map[i].size)) {
			if (map[i].addrmode == ADDRMODE_RELATIVE) {
				offset = addr - map[i].addr;
			} else {
				offset = addr;
			}
			return map[i].sk_idx;
		}
	}

	/* Did not find any slave !?!?  */
        printf("DECODE ERROR! %lx\n", (unsigned long) addr);
	return 0;
}

get_direct_mem_ptr メソッドで start_address, end_address を再計算する時に使う unmap_offset メソッド を下記に示します。対応する target の addrmode の時はマップから再計算しますが、そうでない場合は addr はそのままのです。

template<unsigned int N_INITIATORS, unsigned int N_TARGETS>
void iconnect<N_INITIATORS, N_TARGETS>::unmap_offset(
			unsigned int target_nr,
			sc_dt::uint64 offset,
			sc_dt::uint64& addr)
{
	if (target_nr >= N_TARGETS) {
		SC_REPORT_FATAL("TLM-2", "Invalid target_nr in iconnect\n");
	}

	if (map[target_nr].addrmode == ADDRMODE_RELATIVE) {
		if (offset >= map[target_nr].size) {
			printf("offset=%lx\n", (unsigned long) offset);
			SC_REPORT_FATAL("TLM-2", "Invalid range in iconnect\n");
		}

		addr = map[target_nr].addr + offset;
	} else {
		addr = offset;
	}
	return;
}

iconnect の constructor

iconnect の constructor を見てみましょう。N_INITIATORS個の target port を動的 (new) に生成し、memory と同様に各 target port に対して、register_b_transport、register_transport_dbg, register_get_direct_mem_ptr にて メソッドを登録しています。

initiator port も 動的 (new) に生成し、register_invalidate_direct_mem_ptr メソッドにて、invalidate_direct_mem_ptr を登録しています。

template<unsigned int N_INITIATORS, unsigned int N_TARGETS>
iconnect<N_INITIATORS, N_TARGETS>::iconnect (sc_module_name name)
	: sc_module(name)
{
	char txt[32];
	unsigned int i;

	for (i = 0; i < N_INITIATORS; i++) {
		sprintf(txt, "target_socket_%d", i);

		t_sk[i] = new tlm_utils::simple_target_socket_tagged<iconnect>(txt);

		t_sk[i]->register_b_transport(this, &iconnect::b_transport, i);
		t_sk[i]->register_transport_dbg(this, &iconnect::transport_dbg, i);
		t_sk[i]->register_get_direct_mem_ptr(this,
				&iconnect::get_direct_mem_ptr, i);
	}

	for (i = 0; i < N_TARGETS; i++) {
		sprintf(txt, "init_socket_%d", i);
		i_sk[i] = new tlm_utils::simple_initiator_socket_tagged<iconnect>(txt);

		i_sk[i]->register_invalidate_direct_mem_ptr(this,
				&iconnect::invalidate_direct_mem_ptr, i);
		map[i].size = 0;
	}
}

invalidate_direct_mem_ptr メソッドは下記のようになっています。各 initiator port に対して、invalidate_direct_mem_ptr メソッドを実行しています。

template<unsigned int N_INITIATORS, unsigned int N_TARGETS>
void iconnect<N_INITIATORS, N_TARGETS>::invalidate_direct_mem_ptr(int id,
                                         sc_dt::uint64 start_range,
                                         sc_dt::uint64 end_range)
{
	sc_dt::uint64 start, end;
	unsigned int i;

	unmap_offset(id, start_range, start);
	unmap_offset(id, end_range, end);

	/* Reverse the offsetting.  */
	start -= target_offset[id];
	end -= target_offset[id];

	for (i = 0; i < N_INITIATORS; i++) {
		(*t_sk[i])->invalidate_direct_mem_ptr(start, end);
	}
}

memmap

各 initiator port へのアドレス設定は、memmap メソッドで行います。こちらについては、その2 の「iconnect との接続」にて説明しましたのでここでは省略します。

template<unsigned int N_INITIATORS, unsigned int N_TARGETS>
int iconnect<N_INITIATORS, N_TARGETS>::memmap(
		sc_dt::uint64 addr, sc_dt::uint64 size,
		enum addrmode addrmode, int idx,
		tlm::tlm_target_socket<> &s)
{
	unsigned int i;

	for (i = 0; i < N_TARGETS * 4; i++) {
		if (map[i].size == 0) {
			/* Found a free entry.  */
			map[i].addr = addr;
			map[i].size = size;
			map[i].addrmode = addrmode;
			map[i].sk_idx = i;
			if (idx == -1)
				i_sk[i]->bind(s);
			else
				map[i].sk_idx = idx;
			return i;
		}
	}
	printf("FATAL! mapping onto full interconnect!\n");
	abort();
	return -1;
}

おわりに

今回は、iconnect について見てみました。iconnect は アドレスマップをサポートするxbar の働きをするモジュールです。initiator と target が接続するので、target な役割と initiator な役割をするのでソースコードの中を見ると、両方の役割をどのように実装すればいいかが分かりますね。