Vengineerの妄想

人生を妄想しています。

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

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

はじめに

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

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

今回は下図の dma_mm2s_A と dma_s2mm_C について説明します。 dma_mm2s_A と dma_s2mm_C の実態は、xilinx-axidma.{h,cc} の axidma_mm2s と axidma_s2mm です。です。dma_mm2s と dma_s2mm は、axidma を継承しています。

f:id:Vengineer:20210327112319p:plain

axidma

axidma の 定義は、下記のようになっています。initiator port (init_socket)、target port (tgt_socket)、割り込み(irq) があります。protected 以降は axidma で使う各種メンバーですね。

class axidma
: public sc_core::sc_module
{
public:
	tlm_utils::simple_initiator_socket<axidma> init_socket;
	tlm_utils::simple_target_socket<axidma> tgt_socket;

	sc_out<bool> irq;
	axidma(sc_core::sc_module_name name, bool use_memcpy = false);
	SC_HAS_PROCESS(axidma);
protected:
	union {
		struct {
			uint32_t cr;
			uint32_t sr;
			uint32_t rsv0[AXIDMA_R_ADDR - AXIDMA_R_SR - 1];
			uint32_t addr;
			uint32_t addr_msb;
			uint32_t rsv1[AXIDMA_R_LENGTH - AXIDMA_R_ADDR_MSB - 1];
			uint32_t length;
		};
		uint32_t u32[AXIDMA_R_MAX];
	} regs;
	// S2M needs to keep track of the number of bytes actually copied.
	uint32_t length_copied;

	bool use_memcpy;

	sc_event ev_update_irqs;
	sc_event ev_dma_copy;
	virtual void do_dma_copy(void) {};
	void do_dma_trans(tlm::tlm_command cmd, unsigned char *buf,
			sc_dt::uint64 addr, sc_dt::uint64 len, sc_time &delay);
	void update_irqs(void);

private:
	virtual void b_transport(tlm::tlm_generic_payload& trans, sc_time& delay);
};

axidma の constructer は下記のようになっています。constructor の第2引数は、内部メソッド(do_dma_copy) で使われるようです。
axidma は target なので、tgt_socket に register_b_transport にて、axidma::b_transport を登録しています。
割り込みの更新用メソッドとして update_irqs を SC_METHOD で登録しています。更新のトリガーは ev_update_irqs イベントです。
do_dma_copy メソッドは、SC_THREAD にて常時動いている感じですね。

axidma::axidma(sc_module_name name, bool use_memcpy)
	: sc_module(name), tgt_socket("tgt-socket"), irq("irq"),
	  use_memcpy(use_memcpy)
{
	tgt_socket.register_b_transport(this, &axidma::b_transport);
	memset(&regs, 0, sizeof regs);

	SC_METHOD(update_irqs);
	dont_initialize();
	sensitive << ev_update_irqs;
	SC_THREAD(do_dma_copy);
}

b_transport

b_trasport メソッドは axidma のレジスタアクセス部分になっています。チェック部分では、transがバイトイネーブルになっているかをチェックしています。その後、転送サイズが4バイト以下になっているかもチェックしています。リードの時は regs の対応するアドレスからコピーするだけですが、ライトの時は4バイト単位のアドレスで AXIDMA_R_SRとAXIDMA_R_LENGTHでは対応処理を行い、それ以外のレジスタへはただ単にライトしているだけです。AXIDMA_R_LENGTHにアクセスすると、ev_dma_copyイベントに対して、notify() を発行します。このイベントは、 axidma_mm2s と axidma_s2mm の do_dma_copy メソッドで使われます。最後に、 ev_update_irqs.notify() で、irq の状態を更新しています。

void axidma::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*   data = trans.get_data_ptr();
	unsigned int     len = trans.get_data_length();
	unsigned char*   byt = trans.get_byte_enable_ptr();
	unsigned int     wid = trans.get_streaming_width();

	if (byt != 0) {
		trans.set_response_status(tlm::TLM_BYTE_ENABLE_ERROR_RESPONSE);
		return;
	}

	if (len > 4 || wid < len) {
		trans.set_response_status(tlm::TLM_BURST_ERROR_RESPONSE);
		return;
	}

	addr >>= 2;
	if (trans.get_command() == tlm::TLM_READ_COMMAND) {
		memcpy(data, &regs.u32[addr], len);
	} else if (cmd == tlm::TLM_WRITE_COMMAND) {
		uint32_t v;
		memcpy(&v, data, len);
		switch (addr) {
		case AXIDMA_R_SR:
			regs.u32[addr] &= ~(v & AXIDMA_SR_IOC_IRQ);
			D(printf("%s: SR=%x.%x val=%x\n", name(),
				regs.sr, regs.u32[addr], v));
			break;
		case AXIDMA_R_LENGTH:
			length_copied = 0;
			regs.length = v;
			regs.sr &= ~(AXIDMA_SR_IDLE);
			D(printf("%s: write LENGTH %d\n",
				name(), regs.length));
			ev_dma_copy.notify();
			break;
		default:
			/* No side-effect.  */
			regs.u32[addr] = v;
			break;
		}
	}
	ev_update_irqs.notify();
	trans.set_response_status(tlm::TLM_OK_RESPONSE);
}

update_irqs

ev_update_irqs.notify() が実行されると、下記の update_irqs が実行されます。reg.sr と regs.cr の状態から irq (割り込み) をドライブするかどうかを決めてます。

void axidma::update_irqs(void)
{
D(printf("DMA irq=%d\n", regs.sr & regs.cr & AXIDMA_CR_IOC_IRQ_EN));
irq.write(regs.sr & regs.cr & AXIDMA_CR_IOC_IRQ_EN);
}

axidma_mm2s

axdma_mm2s は、iconnect からデータをリードして、AXI Stream に書き込むDMAです。AXI Stream 用に initiator port (stream_sockt) が 追加されたのと、do_dma_copy メソッドが再定義されています。

class axidma_mm2s : public axidma
{
public:
	tlm_utils::simple_initiator_socket<axidma_mm2s> stream_socket;
	axidma_mm2s(sc_core::sc_module_name name, bool use_memcpy = false);
protected:
	virtual void do_dma_copy(void);
private:
	void do_stream_trans(tlm::tlm_command cmd, unsigned char *buf,
			sc_dt::uint64 addr, sc_dt::uint64 len, bool eop, sc_time &delay);
};
<||

do_dma_copy メソッドでは、regs.length レジスタの値が 0 出ないときに、wait(ev_dma_copy)で ev_dma_copy.notify() が発行されるのを待ちます。その後に、constructor の第2引数で指定したuse_memcpy によって、memcpy 関数 または do_dma_trans メソッドを呼びます。do_dma_trans メソッドでは、init_socket に対して transaction を発生し、データをリードします。その次の do_stream_trans メソッドは AXIのStreamに対応のWriteを実行しています。

>|c++|
void axidma_mm2s::do_dma_copy(void)
{
	while (1) {
		unsigned char buf[2 * 1024];
		uint64_t addr;
		sc_time delay = SC_ZERO_TIME;
		unsigned int tlen;
		bool eop;

		if (!regs.length) {
			wait(ev_dma_copy);
		}

		assert(!(regs.sr & AXIDMA_SR_IDLE));
		tlen = regs.length > sizeof buf ? sizeof buf : regs.length;
		eop = tlen == regs.length;

		addr = regs.addr_msb;
		addr <<= 32;
		addr += regs.addr;

		if (use_memcpy) {
			memcpy(buf, (void *) addr, tlen);
		} else {
			do_dma_trans(tlm::TLM_READ_COMMAND, buf, addr, tlen, delay);
		}
		do_stream_trans(tlm::TLM_WRITE_COMMAND, buf, addr, tlen, eop, delay);

		addr += tlen;
		regs.length -= tlen;

		regs.addr = addr;
		regs.addr_msb = addr >> 32;

		if (regs.length == 0) {
			/* If the DMA was running, signal done.  */
			regs.sr |= AXIDMA_SR_IDLE | AXIDMA_SR_IOC_IRQ;
			ev_update_irqs.notify();
		}
	}
}

axidma の do_dma_trans メソッドは、init_socket に対して データのリードあるいはライト(cmd)を行います。

void axidma::do_dma_trans(tlm::tlm_command cmd, unsigned char *buf,
				sc_dt::uint64 addr, sc_dt::uint64 len,
				sc_time &delay)
{
	tlm::tlm_generic_payload tr;

	tr.set_command(cmd);
	tr.set_address(addr);
	tr.set_data_ptr(buf);
	tr.set_data_length(len);
	tr.set_streaming_width(len);
	tr.set_dmi_allowed(false);
	tr.set_response_status(tlm::TLM_INCOMPLETE_RESPONSE);

	init_socket->b_transport(tr, delay);
	if (tr.get_response_status() != tlm::TLM_OK_RESPONSE) {
		printf("%s:%d DMA transaction error!\n", __func__, __LINE__);
	}
}

do_dma_copy にてリードしたデータを コピーし、genattr にて AXI Stream 用の拡張機能として tr.set_extension(genattr) にて eop を設定します。eop は最後の転送の時に true になるんでしょうね。

void axidma_mm2s::do_stream_trans(tlm::tlm_command cmd, unsigned char *buf,
				sc_dt::uint64 addr, sc_dt::uint64 len, bool eop,
				sc_time &delay)
{
	tlm::tlm_generic_payload tr;
	genattr_extension *genattr = new genattr_extension();

	tr.set_command(cmd);
	tr.set_address(addr);
	tr.set_data_ptr(buf);
	tr.set_data_length(len);
	tr.set_streaming_width(len);
	tr.set_dmi_allowed(false);
	tr.set_response_status(tlm::TLM_INCOMPLETE_RESPONSE);

	genattr->set_eop(eop);
	tr.set_extension(genattr);

	stream_socket->b_transport(tr, delay);
	if (tr.get_response_status() != tlm::TLM_OK_RESPONSE) {
		printf("%s:%d DMA transaction error!\n", __func__, __LINE__);
	}

	tr.release_extension(genattr);
}

axidma_s2mm

axidma_s2mm は、AXI Stream からデータを読みだして、iconnect へデータを書き込むDMAです。target port として stream_socket が追加されているのと、do_dma_copy メソッド、s_b_transport メソッドが定義されています。

class axidma_s2mm : public axidma
{
public:
	tlm_utils::simple_target_socket<axidma_s2mm> stream_socket;
	axidma_s2mm(sc_core::sc_module_name name, bool use_memcpy = false);
protected:
	virtual void do_dma_copy(void);
private:
	void s_b_transport(tlm::tlm_generic_payload& trans, sc_time& delay);
};

do_dma_copy メソッドは下記のように何もしていません。

void axidma_s2mm::do_dma_copy(void) {}

s_b_transport メソッドは下記のようになっています。DMAがIdle状態の時は、wait(ev_dma_copy) で待ちます。DMAがIdle状態で無いときは、use_memcpy の値によって、memcopy 関数と do_dma_trans メソッドのいずれかを実行します。最後に、ev_update_irqs.notify() にて割り込み信号(irq)の更新を行います。do_dma_trans メソッドでは、init_socket に対して transaction を発生し、データをリードします。

void axidma_s2mm::s_b_transport(tlm::tlm_generic_payload& trans,
				sc_time& delay)
{
	unsigned char *data = trans.get_data_ptr();
	unsigned int len = trans.get_data_length();
	genattr_extension *genattr;
	unsigned int len_to_copy;
	uint64_t addr;
	bool eop = true;

	trans.get_extension(genattr);
	if (genattr) {
		eop = genattr->get_eop();
	}

	if (regs.sr & AXIDMA_SR_IDLE) {
		/* Put back-pressure.  */
		D(printf("%s: DMA IS IDLE length=%x\n",
				name(), regs.length));
		do {
			wait(ev_dma_copy);
		} while (regs.sr & AXIDMA_SR_IDLE);
	}

	addr = regs.addr_msb;
	addr <<= 32;
	addr += regs.addr;

	len_to_copy = regs.length >= len ? len : regs.length;

	if (use_memcpy) {
		memcpy((void *) addr, data, len_to_copy);
	} else {
		do_dma_trans(tlm::TLM_WRITE_COMMAND, data, addr,
				len_to_copy, delay);
	}

	length_copied += len_to_copy;
	addr += len_to_copy;
	regs.length -= len_to_copy;
	regs.addr_msb = addr >> 32;
	regs.addr = addr;

	if (regs.length == 0 || eop) {
		regs.sr |= AXIDMA_SR_IDLE | AXIDMA_SR_IOC_IRQ;
		regs.length = length_copied;
	}

	ev_update_irqs.notify();
	trans.set_response_status(tlm::TLM_OK_RESPONSE);
}

終わりに

axidma, axidma_mm2s, axidma_s2mm では、違うプロトコルの間のデータ転送を行うDMAのモデルです。tlm::tlm_generic_payloadの拡張機能を利用することで、AXI Streamに対応するプロトコルを実装しています。