Vengineerの戯言

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

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

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

はじめに

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

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

今回は、ZynqMP モデルを見ていきます。ZynqMPモデルはデモではなく、libsystemctlm-soc
xilinx-zynqmp.{h,cc} です。

xilinx_zynq

xilinx_zynqmp は下記のように、remoteport_tlm を継承しています。

class xilinx_zynqmp
: public remoteport_tlm
{
private:
        途中略

public:
};

remoteport_tlm は、ここ にヘッダファイルがあります。

remoteport_tlm の constructor は下記のようになっています。第2引数の fd が -1 で無いときは、第3引数の sk_descr で sk_socket で通信用ソケットをオープンします。

第4引数 sync は はソフトウェア側(QEMU)との同期に使うもので、 this->sync に設定します。sync が nullptr の時は remoteport_tlm_sync_loosely_timed_ptr を this->sync に設定します。
ここ にあるように、remoteport_tlm_sync_loosely_timed_ptr またはremoteport_tlm_sync_untimed_ptr が設定できるようです。

process を SC_THREAD で登録し、blocking_socket が false の時は thread_trampoline 関数 を pthread として起動します。

remoteport_tlm::remoteport_tlm(sc_module_name name,
			int fd,
			const char *sk_descr,
			Iremoteport_tlm_sync *sync,
			bool blocking_socket)
	: sc_module(name),
	  rst("rst"),
	  blocking_socket(blocking_socket),
	  rp_pkt_event("rp-pkt-ev")
{
	this->fd = fd;
	this->sk_descr = sk_descr;
	this->rp_pkt_id = 0;

	this->sync = sync;
	if (!this->sync) {
		// Default
		this->sync = remoteport_tlm_sync_loosely_timed_ptr;
	}

	memset(devs, 0, sizeof devs);
	memset(&peer, 0, sizeof peer);

	dev_null.adaptor = this;

	if (fd == -1) {
		printf("open socket\n");
		this->fd = sk_open(sk_descr);
		if (this->fd == -1) {
			printf("Failed to create remote-port socket connection!\n");
			if (sk_descr) {
				perror(sk_descr);
			}
			exit(EXIT_FAILURE);
		}
	}

	pthread_mutex_init(&rp_pkt_mutex, NULL);
	SC_THREAD(process);

	if (!blocking_socket)
		pthread_create(&rp_pkt_thread, NULL, thread_trampoline, this);
}

SC_THREADに登録する process は下記のようなメソッドです。sync.reset() 後、rst 信号が High => Low になるまで待って、rp.say_hello() にてソフトウェア側(QEMU)と通信をして、whileループで rp_process(true) を実行します。rp_process メソッドは後程説明します。

void remoteport_tlm::process(void)
{
	adaptor_proc = sc_get_current_process_handle();

	sync->reset();
	wait(rst.negedge_event());

	rp_say_hello();

	while (1) {
		rp_process(true);
	}
	sync->sync();
	return;
}
<||

pthread として起動した thread_trampoline 関数は下記のようになっています。引数 arg は remoteport_tlm の this を上記の pthread_create 関数で設定していますので、キャストしています。
その後、t->rp_pkt_main() を呼び出しています。

>|c++|
static void *thread_trampoline(void *arg) {
        class remoteport_tlm *t = (class remoteport_tlm *)(arg);
        t->rp_pkt_main();
        return NULL;
}

rp_pkt_main 関数は下記のように socket から select でチェックし、アクセスがある場合は rp_pkt_event.notify(SC_ZERO_TIME) を呼び出しています。

void remoteport_tlm::rp_pkt_main(void)
{
	fd_set rd;
	int r;

	while (true) {
		FD_ZERO(&rd);

		FD_SET(fd, &rd);
		pthread_mutex_lock(&rp_pkt_mutex);
		r = select(fd + 1, &rd, NULL, NULL, NULL);
		if (r == -1 && errno == EINTR)
			continue;

		if (r == -1) {
			perror("select()");
			exit(EXIT_FAILURE);
		}

		rp_pkt_event.notify(SC_ZERO_TIME);
		pthread_mutex_unlock(&rp_pkt_mutex);
	}
}

rp_process

process メソッドの中で呼ばれる rp_process メソッドは下記のようになっています。blocking_socket が false の時は wait(rp_pkt_event) を呼び出します。rp_pkt_event は rp_pkt_main がソフトウェア側(QEMU)から パケットが送られた時に、rp_pkt_event.notify(SC_ZERO_TIME) によって通知されます。rp_read によってソフトウェア側からのパケットを読み込み、rp_decode_hdrでヘッダを解析し、受信パケットを rp_read で読み出し、rp_decode_payload で payload を解析しています。

if(pkt_rx.pkt->hdr.flags & RP_PKT_FLAGS_response) が成り立つ条件の時は、XXXをします。

そうでない時は、pkt_rx.pkt->hdr.cmd のコマンドの処理を下記のように行っています。

  • RP_CMD_hello は、ソフトウェア側(QEMU)との最初に通信確立の時の処理を行います。process メソッドで rp_say_hello で ソフトウェア側(QEMU)に送信したのに対応した受信側になります。
  • RP_CMD_write は、dev->cmd_write にて ライト処理を行います。
  • RP_CMD_read は、dev->cmd_read にて リード処理を行います。
  • RP_CMD_interrupt は、dev->cmd_interrupt にて 割り込み処理を行います。
  • RP_CMD_sync は、rp_cmd_sync にて 同期処理を行います。
bool remoteport_tlm::rp_process(bool can_sync)
{
	remoteport_packet pkt_rx;
	ssize_t r;

	pkt_rx.alloc(sizeof(pkt_rx.pkt->hdr) + 128);
	while (1) {
		remoteport_tlm_dev *dev;
		unsigned char *data;
		uint32_t dlen;
		size_t datalen;

		if (!blocking_socket)
			wait(rp_pkt_event);

		pthread_mutex_lock(&rp_pkt_mutex);
		r = rp_read(&pkt_rx.pkt->hdr, sizeof pkt_rx.pkt->hdr);
		if (r < 0)
			perror(__func__);

		rp_decode_hdr(pkt_rx.pkt);

		pkt_rx.alloc(sizeof pkt_rx.pkt->hdr + pkt_rx.pkt->hdr.len);
		r = rp_read(&pkt_rx.pkt->hdr + 1, pkt_rx.pkt->hdr.len);
		pthread_mutex_unlock(&rp_pkt_mutex);

		dlen = rp_decode_payload(pkt_rx.pkt);
		data = pkt_rx.u8 + sizeof pkt_rx.pkt->hdr + dlen;
		datalen = pkt_rx.pkt->hdr.len - dlen;

		dev = devs[pkt_rx.pkt->hdr.dev];
		if (!dev) {
			dev = &dev_null;
		}

		if (pkt_rx.pkt->hdr.flags & RP_PKT_FLAGS_response) {
			unsigned int ri;

			if (pkt_rx.pkt->hdr.flags & RP_PKT_FLAGS_posted) {
				// Drop responses for posted packets.
				return true;
			}
			sync->pre_any_cmd(&pkt_rx, can_sync);

			pkt_rx.data_offset = sizeof pkt_rx.pkt->hdr + dlen;

			ri = dev->response_lookup(pkt_rx.pkt->hdr.id);
			if (ri == ~0U) {
				printf("unhandled response: id=%d dev=%d\n",
					pkt_rx.pkt->hdr.id,
					pkt_rx.pkt->hdr.dev);
				assert(ri != ~0U);
			}

			pkt_rx.copy(dev->resp[ri].pkt);
			dev->resp[ri].valid = true;
			dev->resp[ri].ev.notify();
			sync->post_any_cmd(&pkt_rx, can_sync);
			return true;
		}

//		printf("%s: cmd=%d dev=%d\n", __func__, pkt_rx.pkt->hdr.cmd, pkt_rx.pkt->hdr.dev);
		sync->pre_any_cmd(&pkt_rx, can_sync);
		switch (pkt_rx.pkt->hdr.cmd) {
		case RP_CMD_hello:
			rp_cmd_hello(*pkt_rx.pkt);
			break;
		case RP_CMD_write:
			dev->cmd_write(*pkt_rx.pkt, can_sync, data, datalen);
			break;
		case RP_CMD_read:
			dev->cmd_read(*pkt_rx.pkt, can_sync);
			break;
		case RP_CMD_interrupt:
			dev->cmd_interrupt(*pkt_rx.pkt, can_sync);
			break;
		case RP_CMD_sync:
                        rp_cmd_sync(*pkt_rx.pkt, can_sync);
			break;
		default:
			assert(0);
			break;
		}
		sync->post_any_cmd(&pkt_rx, can_sync);
	}
	return false;
}

xilinx_zynqmp の constructor

xilinx_zynqmp の constructor の最初の部分は下記のようになっています。親クラスの remoteport_tlm の constructor の第2引数には -1 を設定しているので、sk_descr で指定したソケット名をオープンすることになります。第3引数の sync、第4引数の blocking_socket はそのまま remoteport_tlm に渡されます。

xilinx_zynqmp::xilinx_zynqmp(sc_module_name name, const char *sk_descr,
				Iremoteport_tlm_sync *sync,
				bool blocking_socket)
	: remoteport_tlm(name, -1, sk_descr, sync, blocking_socket),

ソフトウェア側(QEMU)と initiator port の接続

xilinx_zynqmp の constructor にて、次のように行っています。

ソフトウェア側(QEMU) の .sk を各 initiator port にアサインしているだけです。

>|c++
s_axi_hpm_fpd[0] = &rp_axi_hpm0_fpd.sk;
s_axi_hpm_fpd[1] = &rp_axi_hpm1_fpd.sk;
s_axi_hpm_lpd = &rp_axi_hpm_lpd.sk;
s_lpd_reserved = &rp_lpd_reserved.sk;
|

target port とソフトウェア側(QEMU)の接続

xilinx_zynqmp の target port をソフトウェア側(QEMU) に接続するのは、tie_off メソッドで行っています。
xilinx_zynqmp の tie_off メソッドでは、親クラスの tie_off メソッドを呼んでいます。

void xilinx_zynqmp::tie_off(void)
{
	tlm_utils::simple_initiator_socket<xilinx_zynqmp> *tieoff_sk;
	unsigned int i;

	remoteport_tlm::tie_off();

	for (i = 0; i < proxy_in.size(); i++) {
		if (proxy_in[i].size())
			continue;
		tieoff_sk = new tlm_utils::simple_initiator_socket<xilinx_zynqmp>();
		tieoff_sk->bind(proxy_in[i]);
	}
}

remoteport_tlm の tie_off メソッドは自分が持っているすべての devs (remoteport_tlm_dev) の tie_off メソッドを呼んでいるだけですね。

void remoteport_tlm::tie_off(void)
{
	unsigned int i;

	for (i = 0; i < RP_MAX_DEVS; i++) {
		if (devs[i]) {
			devs[i]->tie_off();
		}
	}
}

remoteport_tlm_dev の tie_off メソッドは、下記のように再定義していなければ何もしないですね。

virtual void tie_off(void) {} ; 

xilinx_zynqmp::tie_offの後半では proxy_in[i].size が0の時は、tlm_utils::simple_initiator_socket を new し、bind(proxy_in[i]) を実行しています。
ちなみに、proxy_in は、xilinx_zynqmp.h の ここで tlm_utils::simple_target_socket_tagged の sc_vector で定義されています。

	sc_vector<tlm_utils::simple_target_socket_tagged<xilinx_zynqmp> > proxy_in;

xilinx_zynqmp.cc の constructor の部分で 9個のベクターとして宣言されています。

	  proxy_in("proxy-in", 9),

そして、xilinx_zynqmp.cc の constructor の中で proxy_in に対して、register_b_transport と register_transport_dbg を、proxy_out に対しては、bind を実行しています。

	for (i = 0; i < proxy_in.size(); i++) {
		char name[32];

		sprintf(name, "proxy_in-%d", i);
		proxy_in[i].register_b_transport(this,
						  &xilinx_zynqmp::b_transport,
						  i);
		proxy_in[i].register_transport_dbg(this,
						  &xilinx_zynqmp::transport_dbg,
						  i);
		named[i][0] = &proxy_in[i];
		proxy_out[i].bind(*out[i]);
	}

named[i][0] = &proxy_in[i]; で、named[i][0] に代入しています。named は、下記のように ZynqMP への target port になっています。なので、register_b_transport および register_transport_dbg を実行しています。

	tlm_utils::simple_target_socket_tagged<xilinx_zynqmp> ** const named[] = {
		&s_axi_hpc_fpd[0],
		&s_axi_hpc_fpd[1],
		&s_axi_hp_fpd[0],
		&s_axi_hp_fpd[1],
		&s_axi_hp_fpd[2],
		&s_axi_hp_fpd[3],
		&s_axi_lpd,
		&s_axi_acp_fpd,
		&s_axi_ace_fpd,
	};

最後に、out[i] を proxy_out[i] に bind しています。
proxy_out[i] は、下記のように、tlm_utils::simple_initiator_socket_tagged の sc_vector で、サイズは、proxy_in の size と同じです。

	sc_vector<tlm_utils::simple_initiator_socket_tagged<xilinx_zynqmp> > proxy_out;
	  proxy_out("proxy-out", proxy_in.size()),

out は、下記のように ソフトウェア側(QEMU)との通信用ポート (remoteport_tlm_memory_slave) に接続されています。

	tlm_utils::simple_target_socket<remoteport_tlm_memory_slave> * const out[] = {
		&rp_axi_hpc0_fpd.sk,
		&rp_axi_hpc1_fpd.sk,
		&rp_axi_hp0_fpd.sk,
		&rp_axi_hp1_fpd.sk,
		&rp_axi_hp2_fpd.sk,
		&rp_axi_hp3_fpd.sk,
		&rp_axi_lpd.sk,
		&rp_axi_acp_fpd.sk,
		&rp_axi_ace_fpd.sk,
	};

結果として、ZynqMP の target port は下図のようにソフトウェア側(QEMU)と繋がっています。

f:id:Vengineer:20210328114356p:plain

終わりに

今回は、xilinx_zynqmp および 親クラスとなる remoteport_tlm を見てみました。remoteport_tlm は、xilinx_zynqmp だけでなく、xilinx_zynq や xilinx_versal の親クラスでもあります。
親クラスの remoteport_tlm でソフトウェア側(QEMU)との通信をサポートし、zynqmp, zynq, versal の個別対応を各クラスにて実装する感じになっています。