My RISC-V debug feature part3

Tuesday, April 20, 2021

OpenOCD RISC-VにVJTAGのサポートを行う。 OpenOCDにて、VJTAG on DE10-Liteにアクセスしてみる。

OpenOCDのVJTAGサポート状況

or1kはVJTAGに対応している。 この実装を理解して、OpenOCD RISC-Vへ移植をする。

OpenOCD VJTAG support (or1kの実装を読解する)

まず、or1kでどのようにVJTAGを使っているのか確かめる。 そのために、cfgファイルを読む。

tcl/target/or1k.cfg

set  _ENDIAN big

if { [info exists CHIPNAME] } {
   set _CHIPNAME $CHIPNAME
} else {
   set _CHIPNAME or1k
}

if { [info exists TAP_TYPE] } {
   set _TAP_TYPE $TAP_TYPE
} else {
   puts "You need to select a tap type"
   shutdown
}

# Configure the target
if { [string compare $_TAP_TYPE "VJTAG"] == 0 } {
	if { [info exists FPGATAPID] } {
	   set _FPGATAPID $FPGATAPID
	} else {
	   puts "You need to set your FPGA JTAG ID"
		shutdown
	}

	jtag newtap $_CHIPNAME cpu -irlen 10 -expected-id $_FPGATAPID

	set _TARGETNAME $_CHIPNAME.cpu
	target create $_TARGETNAME or1k -endian $_ENDIAN -chain-position $_TARGETNAME

	# Select the TAP core we are using
	tap_select vjtag
}

tap_select vjtagにてVJTAGをTAPとして選択するようである。 tap_selectコマンドはor1k向けに定義されたものであるため、targetコマンドでor1kを選択してから でないと使用することができない。

tap_select命令をまず、RISC-V向けに移植したい。

tap_select命令の移植

src/target/openrisc/or1k.c

static const struct command_registration or1k_hw_ip_command_handlers[] = {
	{
		.name = "tap_select",
		.handler = or1k_tap_select_command_handler,
		.mode = COMMAND_ANY,
		.usage = "name",
		.help = "Select the TAP core to use",
	},

src/target/openrisc/or1k.c

COMMAND_HANDLER(or1k_tap_select_command_handler)
{
	struct target *target = get_current_target(CMD_CTX);
	struct or1k_common *or1k = target_to_or1k(target);
	struct or1k_jtag *jtag = &or1k->jtag;
	struct or1k_tap_ip *or1k_tap;

	if (CMD_ARGC != 1)
		return ERROR_COMMAND_SYNTAX_ERROR;

	list_for_each_entry(or1k_tap, &tap_list, list) {
		if (or1k_tap->name) {
			if (!strcmp(CMD_ARGV[0], or1k_tap->name)) {
				jtag->tap_ip = or1k_tap;
				LOG_INFO("%s tap selected", or1k_tap->name);
				return ERROR_OK;
			}
		}
	}

	LOG_ERROR("%s unknown, no tap selected", CMD_ARGV[0]);
	return ERROR_COMMAND_SYNTAX_ERROR;
}

tap_selectコマンドは、or1k_tap_select_command_handlerが対処する。 tap_selectコマンドの第一引数にあるインターフェースを使用する。 VJTAGを選択した場合を見てみる。

src/target/openrisc/or1k_tap_vjtag.cで実装されている。 or1k_tap_vjtag_initが初期化用の処理である。

	jtag_add_tlr();

まずは、TAPを初期状態にする。

	uint8_t t[4] = { 0 };
	struct scan_field field;
	struct jtag_tap *tap = jtag_info->tap;

	/* Select VIR */
	buf_set_u32(t, 0, tap->ir_length, ALTERA_CYCLONE_CMD_USER1);
	field.num_bits = tap->ir_length;
	field.out_value = t;
	field.in_value = NULL;
	jtag_add_ir_scan(tap, &field, TAP_IDLE);

その後、sld_hubのIRをUSER1にする。 この操作で、Select_IRを行い、Shift_IRにて、sld_hubのIRにUSER1のコード(0xE, 10 bit)を書き込む。

次に、DR Shiftを行う。 USER1レジスタのフォーマットは以下のようになっている。 ただし、nはCEIL(log2(Number of SLD_nodes + 1))である。 mは各VIRの長さの最大値である。 sld_hubには以下のレジスタがある。

  • SLD HUB IP Configuration Register
    • sld_hubに接続されているsld_nodeの情報や、USER1 DRの寸法がわかる。
  • sld_hubSLD_NODE_INFOレジスタ
    • indexとアドレスのマッピングがわかる HUB_INFO命令を使用し、HUB IP Configuration Registerを読み出す。 また、各sld_nodeのアドレスマッピングを保持している、SLD_NODE_INFOレジスタがある。 ただし、この時点でsld_hubに接続されたsld_nodeの数がわからない。 また、n, mについてもわからない状態である。 そこで、USER1 DRをゼロで埋める。 nがわからないが、64回シフトすれば大体の場合は十分である。
	/* Select the SLD Hub */
	field.num_bits = 64;
	field.out_value = NULL;
	field.in_value = NULL;
	jtag_add_dr_scan(tap, 1, &field, TAP_IDLE);

次に、Select_DRにて、DR(SLD HUB Configuration register)を選択する。

	/* Select VDR */
	buf_set_u32(t, 0, tap->ir_length, ALTERA_CYCLONE_CMD_USER0);
	field.num_bits = tap->ir_length;
	field.out_value = t;
	field.in_value = NULL;
	jtag_add_ir_scan(tap, &field, TAP_IDLE);

	int retval = jtag_execute_queue();
	if (retval != ERROR_OK)
		return retval;

jtag_execute_queueにて、JTAGコマンドを実行する。

次に、SLD HUB Configuration registerを読み出す。 レジスタのフォーマットは以下のとおりである。 8つのnibbleのフィールドからなるレジスタであり、nibble単位での読み出しが必須となる。:w 次のnibbleを読み出す前に、UPDATE_DRをする必要がある。 そこで、読み出しごとにjtag_execute_queueを行う。

	uint8_t nibble;
	uint32_t hub_info = 0;

	for (int i = 0; i < 8; i++) {
		field.num_bits = 4;
		field.out_value = NULL;
		field.in_value = &nibble;
		jtag_add_dr_scan(tap, 1, &field, TAP_IDLE);
		retval = jtag_execute_queue();
		if (retval != ERROR_OK)
			return retval;
		hub_info = ((hub_info >> 4) | ((nibble & 0xf) << 28));
	}

	int nb_nodes = NB_NODES(hub_info);
	int m_width = M_WIDTH(hub_info);

	LOG_DEBUG("SLD HUB Configuration register");
	LOG_DEBUG("------------------------------");
	LOG_DEBUG("m_width         = %d", m_width);
	LOG_DEBUG("manufacturer_id = 0x%02" PRIx32, MANUF(hub_info));
	LOG_DEBUG("nb_of_node      = %d", nb_nodes);
	LOG_DEBUG("version         = %" PRIu32, VER(hub_info));
	LOG_DEBUG("VIR length      = %d", guess_addr_width(nb_nodes) + m_width);

これで、sld_nodeの数、m, nのサイズ、VIRのサイズなどがわかった。

次に、インデックス(VJTAGのインスタンス時にユーザもしくはQuartusが割当)からアドレスへのマッピングを調べる。 SLD_NODE info Registerに情報が格納されており、sld_nodeの数分存在する。 このレジスタも同様に8つのnibbleからなる。 つまり、ノードの数 * (8 * nibble)回シフトが必要である。

	int vjtag_node_address = -1;
	int node_index;
	uint32_t node_info = 0;
	for (node_index = 0; node_index < nb_nodes; node_index++) {

		for (int i = 0; i < 8; i++) {
			field.num_bits = 4;
			field.out_value = NULL;
			field.in_value = &nibble;
			jtag_add_dr_scan(tap, 1, &field, TAP_IDLE);
			retval = jtag_execute_queue();
			if (retval != ERROR_OK)
				return retval;
			node_info = ((node_info >> 4) | ((nibble & 0xf) << 28));
		}

		LOG_DEBUG("Node info register");
		LOG_DEBUG("--------------------");
		LOG_DEBUG("instance_id     = %" PRIu32, ID(node_info));
		LOG_DEBUG("manufacturer_id = 0x%02" PRIx32, MANUF(node_info));
		LOG_DEBUG("node_id         = %" PRIu32 " (%s)", ID(node_info),
						       id_to_string(ID(node_info)));
		LOG_DEBUG("version         = %" PRIu32, VER(node_info));

		if (ID(node_info) == VJTAG_NODE_ID)
			vjtag_node_address = node_index + 1;
	}

	if (vjtag_node_address < 0) {
		LOG_ERROR("No VJTAG TAP instance found !");

これでアドレスがわかった。 次に、sld_nodeのインスタンスのVIR, VDRにアクセスをする。

まず、USER1命令を発行する。 これにより、IR chainが選択される。

	/* Select VIR */
	buf_set_u32(t, 0, tap->ir_length, ALTERA_CYCLONE_CMD_USER1);
	field.num_bits = tap->ir_length;
	field.out_value = t;
	field.in_value = NULL;
	jtag_add_ir_scan(tap, &field, TAP_IDLE);

次に、sld_nodeのVIRに命令を送る。 この場合はDEBUG命令を転送している。 これは、or1kのデバックモードの命令の様子。 なお、転送サイズはアドレスのサイズ+VIRのサイズである。 VIRの前に、Addressを付与する。 フォーマットを以下に示す。

/* Send the DEBUG command to the VJTAG IR */
	int dr_length = guess_addr_width(nb_nodes) + m_width;
	buf_set_u32(t, 0, dr_length, (vjtag_node_address << m_width) | ALT_VJTAG_CMD_DEBUG);
	field.num_bits = dr_length;
	field.out_value = t;
	field.in_value = NULL;
	jtag_add_dr_scan(tap, 1, &field, TAP_IDLE);

最後に、USER0命令を発行して、DR chainに切り替える。 これにより、後続のShift_DEは、USER1命令でセットした、Addrを用いてsld_nodeのインスタンスに転送される。 なお、or1kの場合では、VIRをDEBUGから変えることはないようである。 しかし、VIRを変える場合は、USER1を発行して、ADDR+VIRを転送し、USER0へ切り替えるという手順が必要となる。 この切り替え部分をRISC-VのOpenOCDへ追加する必要がある。 DRのアクセスは切り替えが正しくできていれば、問題ないはずである。

/* Select the VJTAG DR */
	buf_set_u32(t, 0, tap->ir_length, ALTERA_CYCLONE_CMD_USER0);
	field.num_bits = tap->ir_length;
	field.out_value = t;
	field.in_value = NULL;
	jtag_add_ir_scan(tap, &field, TAP_IDLE);

	return jtag_execute_queue();
}

OpenOCD VJTAG support

詳しい追加はkoyamanX/riscv-openocd にある、 src/target/riscv/riscv_tap_vjtag.cを確認してほしい。 初期化用のコードは、or1k_tap_vjtag_initをそのまま流用する。 関数名をriscv_tap_vjtag_initとする。 また、DEBUGレジスタの指定を、DTMCSレジスタの指定に変更する。 これで初期化は十分である。 一部、変数(nb_nodesなど)を雑にグローバル変数にした。 また、VIRの書き込み用の関数を別に定義した。

src/target/riscv/riscv_tap_vjtag.c

int vjtag_vir_scan(struct jtag_tap *tap, uint32_t vir_val)
{
	uint8_t t[4] = { 0 };
	struct scan_field field;

	/* Select VIR chain */
	buf_set_u32(t, 0, tap->ir_length, ALTERA_CYCLONE_CMD_USER1);
	field.num_bits = tap->ir_length;
	field.out_value = t;
	field.in_value = NULL;
	jtag_add_ir_scan(tap, &field, TAP_IDLE);

	/* Set VIR Value to the VIR of sld_node determined by vjtag_node_address */
	int dr_length = guess_addr_width(nb_nodes) + m_width;
	buf_set_u32(t, 0, dr_length, (vjtag_node_address << m_width) | vir_val);
	field.num_bits = dr_length;
	field.out_value = t;
	field.in_value = NULL;
	jtag_add_dr_scan(tap, 1, &field, TAP_IDLE);

	/* Select the VJTAG DR chain */
	buf_set_u32(t, 0, tap->ir_length, ALTERA_CYCLONE_CMD_USER0);
	field.num_bits = tap->ir_length;
	field.out_value = t;
	field.in_value = NULL;
	jtag_add_ir_scan(tap, &field, TAP_IDLE);

	return jtag_execute_queue();
}

src/target/riscv/riscv.cを変更し、targetの初期化の際に、ついでにriscv_tap_vjtag_initを呼び出すことにした。 また、use_vjtagフラグで、VJTAGを使用するかをハードコードした。 本来は、このフラグをtap_selectのような命令で変更できるようにするべきである。 しかし、target固有のコマンドはtargetの初期化が終わっていこうに使えるようになる。 ただし、target初期化の処理は、examine(JTAGでDebug Moduleへアクセスし、dmactiveを取得する)が成功したら 正しく完了したこととなる。 VJTAGを用いたアクセスでないため、必ず失敗する。 そこで、とりあえず、動けばいいという適当な考えで、ハードコードしてしまった。 正しく動くことが確認できたら、しっかりと対応しテストしてPRを出したいね。

通信テスト

VJTAGとOpenOCDでやり取りをする。 適当なDebug Moduleのような回路を書く。 コードはkoyamanX/riscv-openocd にある。 なお、DM(Debug Module)やDTM(Debug Transport Module)はJTAGのTCKのクロックドメインで動作していることに注意。 プロセッサと接続するためには、適切にCDC(Clock Domain Crossing)を行う必要がある。 また、DMに関しては、まだスタブである。

src/dtm.nsl

#ifndef DTM_H
#define DTM_H

/* This module design to run in TCK in JTAG clock domain */
/* m_clock must be connected to TCK of JTAG */

#define IDCODE		5'b00001
#define DTMCS		5'b10000
#define DMI			5'b10001
#define BYPASS		5'b11111

#define DTMCS_VERSION	4'b0001
#define DTMCS_ABITS		6'b100000

#define DMI_NOP			2'b00
#define DMI_READ		2'b01
#define DMI_WRITE		2'b10
#define DMI_RESERVED	2'b11

#define DMI_STAT_SUCCESS	2'b00
#define DMI_STAT_RESERVED	2'b01
#define DMI_STAT_FAILURE	2'b10
#define DMI_STAT_INPROGRESS	2'b11

struct dtmcs_t {
	zero1[14];
	dmihardreset[1];
	dmireset[1];
	zero0[1];
	idle[3];
	dmistat[2];
	abits[6];
	version[4];
};

struct dmi_t {
	addr[32];
	data[32];
	op[2];
};

declare dtm interface {
	input m_clock;
	input p_reset;

	input ir_in[5];
	output ir_out[5];
	input tdi;
	output tdo;
	func_in virtual_state_cdr;
	func_in	virtual_state_sdr;
	func_in	virtual_state_e1dr;
	func_in	virtual_state_pdr;
	func_in	virtual_state_e2dr;
	func_in	virtual_state_udr;
	func_in	virtual_state_cir;
	func_in	virtual_state_uir;

	/* DMI */
	output addr[32];
	input rdata[32];
	output wdata[32];
	func_out read(addr);
	func_out write(addr, wdata);
	func_in ready();

#ifdef DEBUG
	output debug_out[32];
#endif
}

#endif

src/dtm.nsl

#include "dtm.h"

module dtm {
	reg ir[5] = IDCODE;	/* IDECODE (defined by spec) */
	reg idcode[32] = 0x10e31913;	/* same as SiFive's */
	reg bypass = 0;
	
	/* DTMCS register */
	dtmcs_t reg dtmcs = {14'b00000000000000, 1'b0, 1'b0, 1'b0, 3'b000, 2'b00, DTMCS_ABITS, DTMCS_VERSION};
	/* DTMCS internal register for read via dtmcs(Capture-DR) */
	reg dtmcs_dmistat[2] = 0;
	reg dtmcs_idle[3] = 0;
	reg dtmcs_dmireset = 0;
	reg dtmcs_dmihardreset = 0;
	/* DMI register */
	dmi_t reg dmi = 0;
	/* DMI internal register for read via dmi(Capture-DR) */
	reg dmi_op_stat[2] = 0;
	reg dmi_addr[32] = 0;
	reg dmi_data[32] = 0;

	func virtual_state_uir {
		ir := ir_in;
	}
	func virtual_state_cir {
		ir_out = IDCODE;
	}
	func virtual_state_udr {
		/* At this point, we can issue abstarct command */
		any {
			ir == DTMCS: {
				/* TODO: Implement hardreset */
				if(dtmcs.dmihardreset) {
					dtmcs_dmireset := 0;
				} else if(dtmcs.dmireset) {
					dtmcs_dmireset := 0;
					dmi_op_stat := 0;
				}
			}
			ir == DMI: {
				any {
					dmi.op == DMI_NOP: {dmi_op_stat := DMI_STAT_SUCCESS;}
					dmi.op == DMI_READ: {
						dmi_addr := dmi.addr;
						read(dmi.addr);
					}
					dmi.op == DMI_WRITE: {
						dmi_addr := dmi.addr;
						dmi_data := dmi.data;
						write(dmi.addr, dmi.data);
					}
					dmi.op == DMI_RESERVED: {dmi_op_stat := DMI_STAT_FAILURE;}
				}
			}
		}
	}
	func ready {
		dmi_data := rdata;
		dmi_op_stat := DMI_STAT_SUCCESS;
	}
	func virtual_state_cdr {
		any {
			ir == DTMCS: dtmcs := {14'(1'b0), dtmcs_dmihardreset, dtmcs_dmireset, 1'b0, 
									dtmcs_idle, dtmcs_dmistat, DTMCS_ABITS, DTMCS_VERSION};
			ir == DMI: dmi := {dmi_addr, dmi_data, dmi_op_stat};
			ir == IDCODE: idcode := 0x10e31913;
			ir == BYPASS: bypass := 1'b0;
			else: bypass := 1'b0;
		}
	}
	func virtual_state_sdr {
		any {
			ir == DTMCS: dtmcs := {tdi, dtmcs[31:1]};
			ir == DMI: dmi := {tdi, dmi[65:1]};
			ir == IDCODE: idcode := {tdi, idcode[31:1]};
			ir == BYPASS: bypass := tdi;
			else: bypass := tdi;
		}
	}
	any {
		ir == DTMCS: tdo = dtmcs[0];
		ir == DMI: tdo = dmi[0];
		ir == BYPASS: tdo = bypass;
		ir == IDCODE: tdo = idcode[0];
		else: tdo = bypass;
	}

#ifdef DEBUG
	debug_out = dtmcs;
#endif
}

src/dm.h

#ifndef DM_H
#define DM_H
/* Runs in same clock domain as DTM and output to hart must be 
	synchronized to hart's clock domain */
declare dm interface {
	input p_reset;
	input m_clock;
	/* DMI */
	input addr[32];
	input wdata[32];
	output rdata[32];
	func_in read(addr);
	func_in write(addr, wdata);
	func_out ready();
#ifdef DEBUG
	output debug_out[32];
#endif
}
#endif

src/dm.nsl

#include "dm.h"
module dm {
	reg dmcontrol[32] = 0;
	func write {
		any {
			addr == 0x10: dmcontrol := wdata;
		}
		ready();
	}
	func read {
		any {
			addr == 0x10: rdata = dmcontrol;
		}
		ready();
	}

#ifdef DEBUG
	debug_out = dmcontrol;
#endif
}
#include "vjtag.h"
#include "dtm.h"
#include "dm.h"

declare DE10 {
	input SW[10];
	output LEDR[10];
}
module DE10 {
	vjtag vjtag0;
	dtm riscv_dtm;
	dm riscv_dm;

	riscv_dtm.m_clock = vjtag0.tck;
	riscv_dtm.p_reset = p_reset;
	riscv_dtm.ir_in = vjtag0.ir_in;
	vjtag0.ir_out = riscv_dtm.ir_out;
	riscv_dtm.tdi = vjtag0.tdi;
	vjtag0.tdo = riscv_dtm.tdo;

	riscv_dm.m_clock = vjtag0.tck;
	riscv_dm.p_reset = p_reset;

	func vjtag0.virtual_state_cdr {
		riscv_dtm.virtual_state_cdr();
	}
	func vjtag0.virtual_state_sdr {
		riscv_dtm.virtual_state_sdr();
	}
	func vjtag0.virtual_state_udr {
		riscv_dtm.virtual_state_udr();
	}
	func vjtag0.virtual_state_cir {
		riscv_dtm.virtual_state_cir();
	}
	func vjtag0.virtual_state_uir {
		riscv_dtm.virtual_state_uir();
	}
	func riscv_dtm.read {
		riscv_dm.read(riscv_dtm.addr);
	}
	func riscv_dtm.write {
		riscv_dm.write(riscv_dtm.addr, riscv_dtm.wdata);
	}
	func riscv_dm.ready {
		riscv_dtm.rdata = riscv_dm.rdata;
		riscv_dtm.ready();
	}

#ifdef DEBUG
	any {
		SW == 0: LEDR = riscv_dtm.debug_out;
		SW == 1: LEDR = riscv_dm.debug_out;
		else:	LEDR = 0xffffffff;
	}
#endif
}

動作確認

koyamanX/riscv-openocd

ツールのビルド

sudo apt install libftdi1-dev
git clone --recursive https://github.com/koyamanX/riscv-debug #riscv-vjtag branch(by default)
cd riscv-debug/riscv-openocd
git submodule update --init --recursive
./bootstrap
mkdir build
cd build
../configure --prefix=/opt/riscv-openocd
make -j $(nproc)
sudo make install

rv32xsoc.cfg

set _ENDIAN little
set _CHIPNAME riscv
set _FPGATAPID 0x031050dd
set _TARGETNAME $_CHIPNAME.cpu

adapter driver usb_blaster
usb_blaster_lowlevel_driver ftdi

jtag newtap $_CHIPNAME cpu -irlen 10 -expected-id $_FPGATAPID
target create $_TARGETNAME riscv -endian $_ENDIAN -chain-position $_TARGETNAME

#use_vjtag

FPGA(DE10-Lite)へ実装

cd riscv-debug/fpga
make all
make download

テスト

sudo /opt/riscv-openocd/bin/openocd -f ../tcl/target/rv32xsoc.cfg

実行結果

dmactiveに成功している。(dmcontrolの0ビット目) dmstatusのリードに失敗しているが、実装していないので期待通り。

次からは、DMの仕様読みと実装を行う。

RISC-VRISC-VJTAGFPGA

My RISC-V debug feature part4

My RISC-V debug feature part2