На неделе мне задали вопрос, на который я к своему стыду не смог сразу ответить: "Как стартует RISC-V процессор и как обрабатывает прерывания?". Попробую на него ответить хотя бы сейчас. Но! Мне кажется, чтобы по настоящему разобраться в этом вопросе было бы хорошо пройти отладчиком по шагам весь старт и инициализацию RISC-V SoC. Только так, на деле, можно понять особенности системы команд процессора и нюансы запуска.

Чтобы пройти отладчиком запуск процессора мне нужно этот отладчик подключить к системе на кристалле. Вообще-то я что-то такое уже делал, когда запускал систему с процессором MIPSfpga - тогда к нашей FPGA плате Марсоход3 (Altera MAX10) был подключен дополнительный второй FTDI программатор, через который велась отладка MIPSfpga SoC. Программное обеспечение и HW при этом были в связке GDB -> OpenOCD -> FTDI -> FPGA Altera MAX10.

Честно говоря, сейчас мне не хотелось бы связываться с платами. Могу ли я дебажить с GDB систему RISC-V SoC работающую в симуляторе Verilator? Почему бы и нет? Скорее всего могу. Как это сделать? Пока не знаю, но давайте попробуем.

До сегодняшнего дня мой опыт работы с RISC-V FPGA проектами ограничивался просто портированием системы с одного FPGA чипа на другой. Я запускал проект Syntacore Scr1 на двух наших платах: Марсоход3 (Altera MAX10 50 тысяч логических элементов) и Марсоход3бис (Altera MAX10 8 тысяч логических элементов). При этом для второго проекта пришлось прямо поднапрячься, чтобы сделать минимальную конфигурацию и чтобы она влезла в чип и запустилась.

Кроме этого, я запускал другой RISC-V проект picorv32 от YosysHQ. Это совсем минималистический проект, где исполнение команд может происходить прямо из внешней последовательной флешпамяти, правда тут есть возможность использования своего рода кеширования флешки встроенной статической памятью. Я смог запустить picorv32 на трёх наших платах:

Во всех случаях портирования проектов я ставил себе целью просто запустить проект и увидеть в последовательной консоли приветственное сообщение от SoC типа "Hello Marsohod!". После этого я считал цель достигнутой и, к сожалению, не копал глубже, что там и как работает и как стартует система на кристалле. Сейчас пришло время разобраться.

Как я написал выше, мне бы хотелось подключить отладчик GDB к исследуемой системе. Проект picorv32 в принципе не имеет отладочного интерфейса JTAG, значит вариант с picorv32 сам собой отпадает. Остается надежда на Syntacore Scr1 - тут с отладчиком должно быть всё в порядке, так как интерфейс JTAG присутствует.

Прежде чем изобретать велосипед я хотел посмотреть, что уже по этому поводу сделали другие люди. Конечно, даже беглый поиск в интернете сразу даёт результат: Advanced Debug System, но, к сожалению, это довольно старый проект, которому уже больше 10 лет и скорее всего он заброшен. Так же находятся другие похожие проекты использующие эти же идеи.

Самое свежее, что я нашел это вот это: https://git.sr.ht/~bryanb/jtag-dpi. Это простое элегантное решение, которое содержит два компонента:

  • jtag_dpi_remote_bit_bang.sv - SystemVerilog модуль, который нужно вставить в исследуемый Scr1 проект и подключить его JTAG линии к JTAG линиям RISC-V процессора Scr1;
  • jtag_dpi_remote_bit_bang.c - это SystemVerilog DPI модуль написанный на C. Он создает TCP сервер и ждет подключение OpenOCD драйвера jtag_bitbang. Там очень простой односимвольный протокол для передачи состояний линий JTAG: TRST, TCK, TDI, TDO, TMS. Полученные по сети команды трансформируются в сигналы JTAG и передаются в SystemVerilog модуль уже в виде сигналов для jtag_dpi_remote_bit_bang.

Вообще, я когда-то давно в нашем FPGA блоге писал про связку Verilog+VPI, ну здесь почти то же самое, только связка SystemVerilog+DPI (Direct Programming Interface).

Само интересное, что это найденное в интернете решение практически сразу почти заработало, когда я вставил его в Scr1, но только частично. OpenOCD сразу находил ID чипа, но дальше категорически отказывался идти. Потратил почти пол дня, чтобы победить эту проблему. Оказалось, что всё же нужно частоту на jtag_dpi_remote_bit_bang понижать относительно системной частоты Scr1.

А я уже было начал погружаться в исследование waveform TAP контроллера Scr1. И даже перечитал свою же статью про JTAG, которую я писал аж в 2011 году (хех). Но дело оказалось было просто в тактовой частоте на модуле.

Рискну показать здесь эти два файла (рискну, потому, что к сожалению автор не указал тип лицензии по которой он опубликовал сишный файл), но мне нужно показать эти файлы, так как я исправлял их оба, чтобы а) понизить частоту JTAG и б) убрать отладочные спам сообщения из консоли verilator:

Текс файла jtag_dpi_remote_bit_bang.sv:

//////////////////////////////////////////////////////////////////////
////                                                              ////
////  jtag_dpi.sv, former dbg_comm_vpi.v                          ////
////                                                              ////
////   Copyright (C) 2017 Texas Instruments Incorporated - http://www.ti.com/ ////
////   Nishanth Menon                                             ////
////                                                              ////
////  This file is part of the SoC/OpenRISC Development Interface ////
////  http://www.opencores.org/cores/DebugInterface/              ////
////                                                              ////
////                                                              ////
////  Author(s):                                                  ////
////       Igor Mohor (Этот адрес электронной почты защищён от спам-ботов. У вас должен быть включен JavaScript для просмотра.)                       ////
////       Gyorgy Jeney (Этот адрес электронной почты защищён от спам-ботов. У вас должен быть включен JavaScript для просмотра.)                    ////
////       Nathan Yawn (Этот адрес электронной почты защищён от спам-ботов. У вас должен быть включен JavaScript для просмотра.)                ////
////       Andreas Traber (Этот адрес электронной почты защищён от спам-ботов. У вас должен быть включен JavaScript для просмотра.)                ////
////                                                              ////
////                                                              ////
//////////////////////////////////////////////////////////////////////
////                                                              ////
//// Copyright (C) 2000-2015 Authors                              ////
////                                                              ////
//// This source file may be used and distributed without         ////
//// restriction provided that this copyright statement is not    ////
//// removed from the file and that any derivative work contains  ////
//// the original copyright notice and the associated disclaimer. ////
////                                                              ////
//// This source file is free software; you can redistribute it   ////
//// and/or modify it under the terms of the GNU Lesser General   ////
//// Public License as published by the Free Software Foundation; ////
//// either version 2.1 of the License, or (at your option) any   ////
//// later version.                                               ////
////                                                              ////
//// This source is distributed in the hope that it will be       ////
//// useful, but WITHOUT ANY WARRANTY; without even the implied   ////
//// warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR      ////
//// PURPOSE.  See the GNU Lesser General Public License for more ////
//// details.                                                     ////
////                                                              ////
//// You should have received a copy of the GNU Lesser General    ////
//// Public License along with this source; if not, download it   ////
//// from http://www.opencores.org/lgpl.shtml                     ////
////                                                              ////
//////////////////////////////////////////////////////////////////////

module jtag_dpi_remote_bit_bang
#(
  parameter TCP_PORT      = 9000
)
(
  input     clk_i,
  input     enable_i,

  output    jtag_tms_o,
  output    jtag_tck_o,
  output    jtag_trst_o,
  output    jtag_srst_o,
  output    jtag_tdi_o,
  input     jtag_tdo_i,

  output    blink_o
  );

  import "DPI-C" function void jtag_server_deinit();
  import "DPI-C" function int jtag_server_init(input int port);
  import "DPI-C" function int jtag_server_send(input  bit i_tdo);
  import "DPI-C" function int jtag_server_tick(output bit s_tms,
                                               output bit s_tck,
                                               output bit s_trst,
                                               output bit s_srst,
                                               output bit s_tdi,
                                               output bit s_blink,
                                               output bit s_new_b_data,
                                               output bit s_new_wr_data,
                                               output bit s_new_rst_data,
                                               output bit client_con,
                                               output bit send_tdo);

  logic     tdo_o1;
  logic     tdo_o2;
  logic     tms_o;
  logic     tck_o;
  logic     trst_o;
  logic     srst_o;
  logic     tdi_o;
  logic     blink;

  logic     server_ready;
  reg [31:0] server_port;

  wire      read_tdo;
  reg       rx_jtag_tms;
  reg       rx_jtag_tck;
  reg       rx_jtag_trst;
  reg       rx_jtag_srst;
  reg       rx_jtag_tdi;
  reg       rx_jtag_blink;
  reg       rx_jtag_new_b_data_available;
  reg       rx_jtag_new_wr_data_available;
  reg       rx_jtag_new_rst_data_available;
  /* verilator lint_off UNUSED */
  reg       rx_jtag_client_connection_status;
  /* verilator lint_off UNUSED */
  reg       tx_read_tdo;

  // Handle commands from the upper level
  initial
    begin

    rx_jtag_client_connection_status = 0;
    rx_jtag_new_b_data_available = 0;
    rx_jtag_new_wr_data_available = 0;
    rx_jtag_new_rst_data_available = 0;
    rx_jtag_tms = 0;
    rx_jtag_tck = 0;
    rx_jtag_trst = 0;
    rx_jtag_srst = 0;
    rx_jtag_tdi = 0;
    rx_jtag_blink = 0;
    tx_read_tdo = 0;
    tms_o = 0;
    tck_o = 0;
    trst_o = 0;
    srst_o = 0;
    tdi_o = 0;
    blink = 0;
    tdo_o1 = 0;
    tdo_o2 = 0;
    server_ready = 0;
    server_port = TCP_PORT;
  end

reg [3:0]cnt=0;
  always @(posedge clk_i)
cnt <= cnt+1;
// On clk input, check if we are enabled first always @(posedge clk_i)
if(cnt==4'hF) begin if (enable_i && server_ready) begin if ( 0 != jtag_server_tick(rx_jtag_tms, rx_jtag_tck, rx_jtag_trst, rx_jtag_srst, rx_jtag_tdi, rx_jtag_blink, rx_jtag_new_b_data_available, rx_jtag_new_wr_data_available, rx_jtag_new_rst_data_available, rx_jtag_client_connection_status, tx_read_tdo) ) begin $display("Error recieved from the JTAG DPI module."); $finish; end if (rx_jtag_new_b_data_available) begin blink <= rx_jtag_blink; end //rx_jtag_new_b_data_available if (rx_jtag_new_wr_data_available) begin tms_o <= rx_jtag_tms; tck_o <= rx_jtag_tck; tdi_o <= rx_jtag_tdi; end //rx_jtag_new_wr_data_available if (rx_jtag_new_rst_data_available) begin trst_o <= rx_jtag_trst; srst_o <= rx_jtag_srst; end //rx_jtag_new_rst_data_available if (tx_read_tdo) begin tdo_o1 <= tdo_o1?0:1; end // tx_read_tdo end //enable_i && server_ready // Start the jtag server on assertion of enable if (enable_i && server_ready == 0) begin if (0 != jtag_server_init(server_port)) begin $display("Error initiazing port"); $finish; end // server init server_ready <= 1; $display("JTAG Bitbang port=%d: Open", server_port); end //enable_i && !server_ready // Stop the server on deassertion of enable if (server_ready && enable_i == 0) begin server_ready <= 0; jtag_server_deinit(); $display("JTAG Bitbang port=%d: Closed", server_port); end //server_ready && !enable_i end // posedge clk_i always @(negedge clk_i) begin if (enable_i && server_ready && read_tdo) begin tdo_o2 <= tdo_o2?0:1; if (0 != jtag_server_send(jtag_tdo_i)) begin $display("Error recieved from the JTAG DPI module while Tx."); $finish; end //server_send end //enable_i && read_tdo end //neg edge clk_i assign jtag_tms_o = tms_o; assign jtag_tck_o = tck_o; assign jtag_trst_o = trst_o; assign jtag_srst_o = srst_o; assign jtag_tdi_o = tdi_o; assign blink_o = blink; assign read_tdo = tdo_o1^tdo_o2; endmodule // vim: set ai ts=2 sw=2 et:

 

Текст файла jtag_dpi_remote_bit_bang.c

//   Copyright (C) 2017 Texas Instruments Incorporated - http://www.ti.com/
//   Nishanth Menon

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <string.h>

/* *INDENT-OFF* */
#ifdef __cplusplus
extern "C" {
#endif
/* *INDENT-ON* */

/*XXX: Define TEST_FRAMEWORK as 0 */
#ifndef JTAG_BB_TEST_FRAMEWORK
#define JTAG_BB_TEST_FRAMEWORK 0
#endif

/*
 * Protocol: http://repo.or.cz/openocd.git/blob/HEAD:/doc/manual/jtag/drivers/remote_bitbang.txt
 * implementation: https://github.com/myelin/teensy-openocd-remote-bitbang/blob/master/remote_bitbang_serial_debug.py
 */
#define RB_BLINK_ON	'B'
#define RB_BLINK_OFF	'b'
#define RB_READ_REQ	'R'
#define RB_QUIT_REQ	'Q'
/*	Write requests:		TCK	TMS	TDI */
#define RB_WRITE_0  '0'		/*      0       0       0   */
#define RB_WRITE_1  '1'		/*      0       0       1   */
#define RB_WRITE_2  '2'		/*      0       1       0   */
#define RB_WRITE_3  '3'		/*      0       1       1   */
#define RB_WRITE_4  '4'		/*      1       0       0   */
#define RB_WRITE_5  '5'		/*      1       0       1   */
#define RB_WRITE_6  '6'		/*      1       1       0   */
#define RB_WRITE_7  '7'		/*      1       1       1   */
/*	Reset requests:		TRST	SRST	    */
#define RB_RST_R    'r'		/*      0       0           */
#define RB_RST_S    's'		/*      0       1           */
#define RB_RST_T    't'		/*      1       0           */
#define RB_RST_U    'u'		/*      1       1           */

#define MODULE_NAME "jtag_dpi_remote_bb: "
#if JTAG_BB_TEST_FRAMEWORK
#define DEBUG_PRINT(...) printf(MODULE_NAME "DEBUG: "  __VA_ARGS__)
#else
#define DEBUG_PRINT(...)
#endif
#define INFO_PRINT(...) printf(MODULE_NAME "INFO: "  __VA_ARGS__)
#define ERROR_PRINT(...) printf(MODULE_NAME "ERROR: "  __VA_ARGS__)

#ifndef BB_JTAG_INACTIVITY_THRESH1
#define BB_JTAG_INACTIVITY_THRESH1	10
#endif

#ifndef BB_JTAG_INACTIVITY_THRESH2
#define BB_JTAG_INACTIVITY_THRESH2	100
#endif

#ifndef BB_JTAG_INACTIVITY_THRESH3
#define BB_JTAG_INACTIVITY_THRESH3	500
#endif

#ifndef BB_JTAG_INACTIVITY_THRESH4
#define BB_JTAG_INACTIVITY_THRESH4	1000
#endif

#define ABSOLUTE_MAX_THRESHOLDS 4

#ifndef BB_JTAG_INACTIVITY_MAX_THRESHOLDS
#define BB_JTAG_INACTIVITY_MAX_THRESHOLDS ABSOLUTE_MAX_THRESHOLDS
#else
#if BB_JTAG_INACTIVITY_MAX_THRESHOLDS > ABSOLUTE_MAX_THRESHOLDS
#error "Max thresholds exceeded"
#endif /* Max thresh check */
#endif /* BB_JTAG_INACTIVITY_MAX_THRESHOLDS */

/**
 * struct inactivity_info - Inactivity information
 * @current_tidx:	current threshold
 * @max_tidx:		Maximum thresholds
 * @num_inactive_ticks:	Number of inactive ticks so far
 * @threshold:		Array containing for each state, a residency
 *			latency after which a network check will be performed.
 *			idx[0] indicates steady state, others are incrementally
 *			slower polling of network status.
 */
struct inactivity_info {
	uint8_t current_tidx;
	uint8_t max_tidx;
	uint16_t num_inactive_ticks;
	uint16_t threshold[BB_JTAG_INACTIVITY_MAX_THRESHOLDS];
};

/**
 * struct server_info - Maintains the server params
 * @jp_got_con:		Are we connected?
 * @jp_server_p:	The listening socket
 * @jp_client_p:	The socket for communicating with remote
 * @socket_port:	What port to hook server to?
 */
struct server_info {
	uint8_t jp_got_con;
	int jp_server_p;	/* The listening socket */
	int jp_client_p;	/* The socket for communicating with Remote */

	int socket_port;

};

/* Server information instance */
static struct server_info si;

/* Inactivity information */
static struct inactivity_info ii;

/**
 * server_tick_activity_init() - Initialize basic struct info
 */
static void server_tick_activity_init(void)
{
	ii.current_tidx = 0;
	ii.num_inactive_ticks = 0;
	ii.max_tidx = BB_JTAG_INACTIVITY_MAX_THRESHOLDS - 1;
	ii.threshold[0] = BB_JTAG_INACTIVITY_THRESH1;
#if BB_JTAG_INACTIVITY_MAX_THRESHOLDS > 1
	ii.threshold[1] = BB_JTAG_INACTIVITY_THRESH2;
#endif
#if BB_JTAG_INACTIVITY_MAX_THRESHOLDS > 2
	ii.threshold[2] = BB_JTAG_INACTIVITY_THRESH3;
#endif
#if BB_JTAG_INACTIVITY_MAX_THRESHOLDS > 3
	ii.threshold[3] = BB_JTAG_INACTIVITY_THRESH4;
#endif
}

/**
 * server_tick_mark_active() - Mark myself active
 *
 * In active state, we will check for data every clk cycle.
 * but if the next clk cycle has no data, then, we check
 * based on threshold, and delay between checks increase till
 * the delays reach max threshold.
 */
static void server_tick_mark_active(void)
{
	ii.current_tidx = 0;
	/* Next tick will be an network op */
	ii.num_inactive_ticks = 0;

	DEBUG_PRINT("Detected network activity..\n");
}

/**
 * server_tick_is_idle() - Is this an cycle where no network ops todo?
 *
 * This is the core logic of activity management. we will do two things here:
 * a) if we are marked active (inactive_ticks = 0) then we need to do network
 *    operations - this is necessary to maintain some performance.
 * b) if we have been idle enough, do a simple ladder governor logic to
 *    check even later.
 *
 * NOTE: we dont do a "residency" state, we are more interested in either:
 *  i) lots of activity
 *  ii) or the other extreme of lots of inactivity.
 *
 * Return: 0 if we are idle cycle, else if we have to do network op,
 * return 1
 */
static int server_tick_is_idle(void)
{
	int ret = 0;

	/* Trigger a network op please */
	if (!ii.num_inactive_ticks)
		ret = 1;

	ii.num_inactive_ticks++;
	if (ii.num_inactive_ticks > ii.threshold[ii.current_tidx]) {
		/* Crossed threshold, Next tick will be an network op */
		ii.num_inactive_ticks = 0;
		if (ii.current_tidx < ii.max_tidx) {
			ii.current_tidx++;
			DEBUG_PRINT("Switched to INA state[%d]- check @%d\n",
				    ii.current_tidx,
				    ii.threshold[ii.current_tidx]);
		}
	}
	return ret;
}

/**
 * server_socket_open() - Helper function to open a server socket.
 *
 * Return: 0 if all goes good, else corresponding error
 */
static int server_socket_open()
{
	struct sockaddr_in addr;
	int ret;
	int yes = 1;

	si.jp_got_con = 0;

	addr.sin_family = AF_INET;
	addr.sin_port = htons(si.socket_port);
	addr.sin_addr.s_addr = INADDR_ANY;
	memset(addr.sin_zero, '\0', sizeof(addr.sin_zero));

	si.jp_server_p = socket(PF_INET, SOCK_STREAM, 0);
	if (si.jp_server_p < 0) {
		ERROR_PRINT("Unable to create comm socket: %s\n",
			    strerror(errno));
		return errno;
	}

	if (setsockopt(si.jp_server_p, SOL_SOCKET, SO_REUSEADDR,
		       &yes, sizeof(int)) == -1) {
		ERROR_PRINT("Unable to setsockopt on the socket: %s\n",
			    strerror(errno));
		return -1;
	}

	if (bind(si.jp_server_p, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
		ERROR_PRINT("Unable to bind the socket: %s\n", strerror(errno));
		return -1;
	}

	if (listen(si.jp_server_p, 1) == -1) {
		ERROR_PRINT("Unable to listen: %s\n", strerror(errno));
		return -1;
	}

	ret = fcntl(si.jp_server_p, F_GETFL);
	ret |= O_NONBLOCK;
	fcntl(si.jp_server_p, F_SETFL, ret);

	INFO_PRINT("Listening on port %d\n", si.socket_port);
	return 0;
}

/**
 * client_recv() - Actual processing of rx data from remote bitbang client
 * @jtag_tms:	comm data with verilog
 * @jtag_tck:	comm data with verilog
 * @jtag_trst:	comm data with verilog
 * @jtag_srst:	comm data with verilog
 * @jtag_tdi:	comm data with verilog
 * @jtag_blink:	comm data with verilog
 * @bl_data_avail:	comm data with verilog
 * @wr_data_avail:	comm data with verilog
 * @rst_data_avail:	comm data with verilog
 * @send_tdo:	comm data with verilog
 *
 * Return: 0 if all goes good, else corresponding error
 */
static int client_recv(unsigned char *const jtag_tms,
		       unsigned char *const jtag_tck,
		       unsigned char *const jtag_trst,
		       unsigned char *const jtag_srst,
		       unsigned char *const jtag_tdi,
		       unsigned char *const jtag_blink,
		       unsigned char *const bl_data_avail,
		       unsigned char *const wr_data_avail,
		       unsigned char *const rst_data_avail,
		       unsigned char *const send_tdo)
{
	uint8_t dat;
	int ret;

	ret = recv(si.jp_client_p, &dat, 1, 0);

	/* Check connection abort */
	if ((ret == -1 && errno != EWOULDBLOCK) || (ret == 0)) {
		ERROR_PRINT("JTAG Connection closed\n");
		close(si.jp_client_p);
		return server_socket_open();
	}
	/* no available data */
	if (ret == -1 && errno == EWOULDBLOCK) {
		return 0;
	}
	server_tick_mark_active();

	DEBUG_PRINT("Data: %c\n", dat);
	switch (dat) {
	case RB_BLINK_ON:
	case RB_BLINK_OFF:
		*jtag_blink = (dat == RB_BLINK_ON) ? 1 : 0;
		*bl_data_avail = 1;
		break;

	case RB_READ_REQ:
		DEBUG_PRINT("TX\n");
		*send_tdo = 1;
		break;

	case RB_WRITE_0:
	case RB_WRITE_1:
	case RB_WRITE_2:
	case RB_WRITE_3:
	case RB_WRITE_4:
	case RB_WRITE_5:
	case RB_WRITE_6:
	case RB_WRITE_7:
		DEBUG_PRINT("Write %c\n", dat);
		dat -= RB_WRITE_0;
		*jtag_tdi = (dat & 0x1) >> 0;
		*jtag_tms = (dat & 0x2) >> 1;
		*jtag_tck = (dat & 0x4) >> 2;
		*wr_data_avail = 1;
		break;

	case RB_RST_R:
	case RB_RST_S:
	case RB_RST_T:
	case RB_RST_U:
		DEBUG_PRINT("RST %c\n", dat);
		dat -= RB_RST_R;
		*jtag_srst = (dat & 0x1) >> 0;
		*jtag_trst = (dat & 0x2) >> 1;
		*rst_data_avail = 1;
		break;

	case RB_QUIT_REQ:
#if JTAG_BB_TEST_FRAMEWORK
		/* Shut down sim */
		return 1;
#else
		close(si.jp_client_p);
		return server_socket_open();
#endif
	default:
		DEBUG_PRINT("Unknown request: '%c'\n", dat);
		/* Fall through */
	}
	return 0;
}

/**
 * client_check_con() - Checks to see if we got a connection
 *
 * Return: 0 if all goes good, else corresponding error
 */
static int client_check_con()
{
	int ret;

	if ((si.jp_client_p = accept(si.jp_server_p, NULL, NULL)) == -1) {
		if (errno == EAGAIN)
			return 1;

		DEBUG_PRINT("Unable to accept connection: %s\n",
			    strerror(errno));
		return 1;
	}
	/* Set the comm socket to non-blocking. */
	ret = fcntl(si.jp_client_p, F_GETFL);
	ret |= O_NONBLOCK;
	fcntl(si.jp_client_p, F_SETFL, ret);
	/*
	 * Close the server socket, so that the port can be taken again
	 * if the simulator is reset.
	 */
	close(si.jp_server_p);

	DEBUG_PRINT("JTAG communication connected!\n");
	si.jp_got_con = 1;
	return 0;
}

/**
 * jtag_server_init() - Called during enable to startup a server port
 * @port:	Network port number
 *
 * Return: 0 if all goes good, else corresponding error
 */
int jtag_server_init(const int port)
{
	si.socket_port = port;

	/* Check if we get data in next tick as well */
	server_tick_activity_init();

	return server_socket_open();
}

/**
 * jtag_server_deinit() - Shutdown the network server
 *
 */
void jtag_server_deinit(void)
{
	close(si.jp_server_p);
	close(si.jp_client_p);
	si.jp_got_con = 0;
}

/**
 * jtag_server_tick() - Called for every clock cycle if server is enabled.
 * @jtag_tms:	comm data with verilog
 * @jtag_tck:	comm data with verilog
 * @jtag_trst:	comm data with verilog
 * @jtag_srst:	comm data with verilog
 * @jtag_tdi:	comm data with verilog
 * @jtag_blink:	comm data with verilog
 * @bl_data_avail:	comm data with verilog
 * @wr_data_avail:	comm data with verilog
 * @rst_data_avail:	comm data with verilog
 * @send_tdo:	comm data with verilog
 *
 * Return: 0 if all goes good, else corresponding error
 */
int jtag_server_tick(unsigned char *const jtag_tms,
		     unsigned char *const jtag_tck,
		     unsigned char *const jtag_trst,
		     unsigned char *const jtag_srst,
		     unsigned char *const jtag_tdi,
		     unsigned char *const jtag_blink,
		     unsigned char *const bl_data_avail,
		     unsigned char *const wr_data_avail,
		     unsigned char *const rst_data_avail,
		     unsigned char *const jtag_client_on,
		     unsigned char *const send_tdo)
{

	*rst_data_avail = 0;
	*wr_data_avail = 0;
	*bl_data_avail = 0;
	*send_tdo = 0;

	/* If I have nothing to do in this tick, then return */
	if (!server_tick_is_idle())
		return 0;

	if (!si.jp_got_con) {
		if (client_check_con()) {
			*jtag_client_on = si.jp_got_con;
			return 0;
		}
		server_tick_mark_active();
	}
	*jtag_client_on = si.jp_got_con;

	return client_recv(jtag_tms, jtag_tck, jtag_trst, jtag_srst,
			   jtag_tdi, jtag_blink, bl_data_avail,
			   wr_data_avail, rst_data_avail, send_tdo);
}

/**
 * jtag_server_send() - Called if data has to be transmitted to remote client
 * @jtag_tdo:	TDO data
 *
 * NOTE: This assumes that it was invoked as response to send_tdo being set
 * in tick function.
 *
 * Return: 0 if all goes good, else corresponding error
 */
int jtag_server_send(unsigned char const jtag_tdo)
{
	uint8_t dat;

	dat = '0' + jtag_tdo;
	DEBUG_PRINT("Read = '%c'\n", dat);
	send(si.jp_client_p, &dat, 1, 0);
	return 0;
}


/* *INDENT-OFF* */
#ifdef __cplusplus
}
#endif
/* *INDENT-ON* */

/* vim: set ai: ts=8 sw=8 noet: */

 

Итак, что нужно сделать, чтобы запустить отладку GDB системы RISC-V Scr1 работающей в симуляторе Verilator:

1) положить файл jtag_dpi_remote_bit_bang.sv в папку scr1/src/tb

2) добавить строку с именем этого файла в файл scr1/src/axi_tb.files
git diff axi_tb.files
diff --git a/src/axi_tb.files b/src/axi_tb.files
index 383dc9b..9ee78ce 100644
--- a/src/axi_tb.files
+++ b/src/axi_tb.files
@@ -1,3 +1,4 @@
core/pipeline/scr1_tracelog.sv
tb/scr1_memory_tb_axi.sv
-tb/scr1_top_tb_axi.sv
\ No newline at end of file
+tb/scr1_top_tb_axi.sv
+tb/jtag_dpi_remote_bit_bang.sv

3) положить файл jtag_dpi_remote_bit_bang.с в папку scr1/sim/verilator_wrap

4) чтобы этот сишный файл скомпилировался его нужно добавить в scr1/sim/Makefile:
git diff Makefile
diff --git a/sim/Makefile b/sim/Makefile
index 629d43d..7467397 100644
--- a/sim/Makefile
+++ b/sim/Makefile
@@ -17,7 +17,7 @@ ifeq ($(BUS),AHB)
export scr1_wrapper := $(root_dir)/sim/verilator_wrap/scr1_ahb_wrapper.c
endif
ifeq ($(BUS),AXI)
-export scr1_wrapper := $(root_dir)/sim/verilator_wrap/scr1_axi_wrapper.c
+export scr1_wrapper := $(root_dir)/sim/verilator_wrap/scr1_axi_wrapper.c $(root_dir)/sim/verilator_wrap/jtag_dpi_remote_bit_bang.c
endif
export verilator_ver ?= $(shell expr `verilator --version | cut -f2 -d' '`)

5) я собираюсь вести отладку программы scr1/sim/tests/isr_sample, но тестбенч же быстро отрабатывает и закрывается, а мне так не надо, я же не успею даже отладчиком подключиться. Поэтому в самом начале isr_sample.S (это ассемблерный файл) нужно поставить вечный цикл:

.balign 64
_start:
   nop
   la t0,_start
   jr t0

   la t0, machine_trap_entry
   csrw mtvec, t0

Здесь псевдокоманда la загружает в регистр t0 адрес метки _start, а псевдокоманда jr безусловно переходит по этому адресу, который указан в регистре t0. Программа повиснет, но если подключусь отладчиком, то я смогу вручную выйти из этого цикла.

6) Ещё нужно исправить сам тестбенч - у него есть защита от "повисающих" проектов в виде watchdog таймера. Поэтому я просто комментирую приращение watchdog таймера:
git diff ../../../src/tb/scr1_top_tb_runtests.sv
diff --git a/src/tb/scr1_top_tb_runtests.sv b/src/tb/scr1_top_tb_runtests.sv
index aea666e..ecc7cee 100644
--- a/src/tb/scr1_top_tb_runtests.sv
+++ b/src/tb/scr1_top_tb_runtests.sv
@@ -31,7 +31,7 @@ always_ff @(posedge clk) begin
bit test_pass;
bit test_error;
int unsigned f_test;
- watchdogs_cnt <= watchdogs_cnt + 'b1;
+ //watchdogs_cnt <= watchdogs_cnt + 'b1;
if (test_running) begin
test_pass = 1;
rst_init <= 1'b0;

7) Самое главное - это подключение модуля jtag_bit_bang к процессору Scr1:

git diff ./src/tb/scr1_top_tb_axi.sv
diff --git a/src/tb/scr1_top_tb_axi.sv b/src/tb/scr1_top_tb_axi.sv
index 6282fec..8d3bcf2 100644
--- a/src/tb/scr1_top_tb_axi.sv
+++ b/src/tb/scr1_top_tb_axi.sv
@@ -327,6 +327,7 @@ end

`ifdef SCR1_DBG_EN
initial begin
+/*
trst_n = 1'b0;
tck = 1'b0;
tdi = 1'b0;
@@ -335,7 +336,19 @@ initial begin
#800ns tms = 1'b0;
#500ns trst_n = 1'b0;
#100ns tms = 1'b1;
-end
+*/
+end;
+jtag_dpi_remote_bit_bang jtag_dpi_inst(
+ .clk_i( clk ),
+ .enable_i( 1'b1 ),
+ .jtag_tms_o( tms ),
+ .jtag_tck_o( tck ),
+ .jtag_trst_o( trst_n ),
+ .jtag_srst_o(),
+ .jtag_tdi_o( tdi ),
+ .jtag_tdo_i( tdo ),
+ .blink_o()
+ );
`endif // SCR1_DBG_EN

Теперь всё уже практически готово для для отладки.

Немного нужно пояснить про структуру тестовой программы ./scr1/sim/tests/isr_sample/isr_sample.S - именно её я собираюсь запускать. Программа написана на ассемблере в инструкциях RISC-V. При сборке задействуется линковщик, который использует скрипт /scr1/sim/tests/common/link.ld

Если открыть этот файл и посмотреть на него, то на первый взгляд покажется, что это какие-то древние индейские письмена на скалах и ничего не понятно:

Тут написано, что-то вот такое:

MEMORY {
    RAM (rwx) : ORIGIN = 0x0, LENGTH = 64K
}

STACK_SIZE = 1024;

CL_SIZE = 32;

SECTIONS {

/* code segment */
.text.init 0 : {
    FILL(0);
    . = 0x100 - 12;
    SIM_EXIT = .;
    LONG(0x13);
    SIM_STOP = .;
    LONG(0x6F);
    LONG(-1);
    . = 0x100;
    PROVIDE(__TEXT_START__ = .);
    *(.text.init)
} >RAM
...

Однако, если посмотреть на этот файл внимательнее и одновременно на исходный файл ./scr1/sim/tests/isr_sample/isr_sample.S и еще одновременно на дамп результирующей программы после компиляции /scr1/build/verilator_AXI_MAX_imc_IPIC_1_TCM_1_VIRQ_1_TRACE_1/isr_sample.dump, то очень многое становится понятным даже без чтения документации по линковщику.

link.ld 

Память начинается с нуля и имеет размер 64 килобайта. По адресу 0x100-12 = 0xF4 объявляется метка SIM_EXIT и там располагается 32х битное слово 0x13, которое обозначает команду процессора nop. По следующему адресу 0xF8 объявляется метка SIM_STOP и там будет располагаться слово 0x6F, которое обозначает команду процессора безусловного перехода на самого себя (на адрес 0xF8) - вечный цикл. Дальше слово (-1), которое 0xFFFFFFFF и следующий адрес это 0x100. По этому адресу располагается код описанный в ассемблерном файле isr_sample.S. А он в свою очередь имеет строку org (64*3) то есть смещает адрес еще дальше на 0xC0 и следом, получается уже по адресу 0x1C0 идет таблица векторов прерываний, которых 16 штук, каждый вектор занимает одно 32х битное слово и каждый вектор это безусловный переход по своему адресу обработчика. Таблица векторов закончится перед адресом 0x200. Уже с этого адреса 0x200 идет метка _start: с которой процессор и начинает работу сразу после системного сброса.

Это подтверждается с другой стороны чтением исходников SystemVerilog файла ./scr1/src/tb/ scr1_top_tb_axi.sv

localparam ADDR_START = 'h200;
localparam ADDR_TRAP_VECTOR = 'h240;
localparam ADDR_TRAP_DEFAULT = 'h1C0;

Старт программы с адреса 0x200 и адрес таблицы векторов по умолчанию это 0x1C0.
Кстати, первое, что делает программа isr_sample.S по адресу _start это устанавливает регистр mtvec, который хранит это значение адреса таблицы прерываний. Ну и, конечно, таблицу прерываний можно изначально иметь пустую, а потом после старта процессора динамически формировать её записывая в таблицу векторов 32х битные вектора нужные переходы "j isr_subroutine".

Теперь приступим к отладке системы.
Нужно открыть 3 окна терминала. Я вообще подключаюсь к своей Ubuntu машине тремя сессиями ssh и оттуда буду всё запускать.
В первом окне я запускаю симулятор Verilator:
> make run_verilator CFG=MAX BUS=AXI TARGETS="isr_sample" TRACE=1

Напомню, что тест не заканчивается, но как бы повисает, ведь я по адресу _start поставил вечный цикл сам на себя и выключил watchdog таймер в тестбенче. За то в окне мы можем увидеть сообщение от jtag сервера, что порт 9000 открыт:

verilator

Во втором окне терминала мне нужно запустить OpenOCD. Я честно говоря не знаю подходит ли стандартный OpenOCD из поставки Ubuntu. Я использую OpenOCD, который я взял из github репозитория Syntacore https://github.com/syntacore/openocd/tree/syntacore. И я выбрал бранч syntacore и собирал из исходников, чтобы при сборке указать
>./configure --enable-syntacore-extension --enable-remote-bitbang

Для openocd мне нужно подготовить еще два конфигурационных файла, первый это jtag_bb.cfg, который описывает подключение OpenOCD по сети к удаленному "серверу JTAG":

adapter driver remote_bitbang
remote_bitbang host localhost
remote_bitbang port 9000
adapter speed 1000
jtag_rclk 1000
reset_config trst_only

Второй конфигурационный файл это mik32.cfg (взял его на просторах интернета, он для российского RISC-V микроконтроллера, который видимо использует проект Scr1 или его разновидность):

proc my_init_proc { } { echo "Disabling watchdog..." }

proc init_targets {} {
reset_config trst_and_srst
set _CHIPNAME riscv
set _CPUTAPID 0xdeb11001
#set _SYSTAPID 0xfffffffe
jtag newtap $_CHIPNAME cpu -irlen 5 -ircapture 0x1 -irmask 0x1f -expected-id $_CPUTAPID
#jtag newtap $_CHIPNAME sys -irlen 4 -ircapture 0x05 -irmask 0x0F -enable -expected-id $_SYSTAPID -ignore-bypass
set _TARGETNAME $_CHIPNAME.cpu
target create $_TARGETNAME riscv -endian little -chain-position $_TARGETNAME -coreid 0
riscv expose_csrs 2016=mcounten
riscv.cpu configure -event reset-init my_init_proc
}
poll_period 200
init
riscv.cpu arm semihosting enable
puts "init done"

Создаю такие файлы и теперь запускаю openocd:
>src/openocd -s ./tcl/ -f ./tcl/interface/jtag_bb.cfg -f ./mik32.cfg

Это прямо очень важный этап, у меня долго не проходил этап examine, пока не снизил скорость JTAG относительно системной частоты процессора. И важно увидеть, что GDB сервер ждет на порту 3333:

openocd

При этом, понимаете, что обмен между RISC-V SoC Scr1 и OpenOCD уже вовсю идет, ведь OpenOСD уже прочитал ID чипа, и обнаружил ядро процессора RISC-V. И весь этот обмен идет через протокол jtag_bitbang до DPI модуля jtag_dpi_remote_bit_bang.c, который связывается с SystemVerilog модулем jtag_dpi_remote_bit_bang.sv и далее передает сигналы JTAG в ядро процессора. И всё это крутится в симуляторе Verilator.

В третьем терминале всё просто - запускаем gdb. Но берем его из тулчейна sc-dt/riscv-gcc/bin/riscv64-unknown-elf-gdb
После запуска отладчика GDB запускаю подключение к openocd:
(gdb) target remote localhost:3333

После подключения к openocd набираю еще две команды:
(gdb) layout asm
(gdb) stepi

И вот я уже вижу текущее исполнение своего вечного цикла по адресу старта программы isr_sample.S где-то в районе адреса 0x200:

gdb layout asm

Выполняя команду stepi в консоли GDB я могу исполнять пошагово каждую команду процессора.
Дальше я могу дойти до адреса 0x20C, где расположена команда перехода назад "jr t0" и в этот момент я могу исправить ручками содержимое регистра t0 и покинуть этот цикл. Выполним команду
(gdb) set $t0=0x210
(gdb) stepi

И всё, вечный цикл покинули и можно двигаться вперёд и исполнять все подряд команды процессора и смотреть, как она работает.

Все три моих окна: Verilator и тестбенч, OpenOCD, GDB выглядят вместе и взаимодействуют друг с другом вот так:

scr1 openocd gdb

С помощью отладчика GDB я могу смотреть содержимое всех (или почти всех?) регистров процессора. Например, можно посмотреть содержимое служебного регистра mtvec, который указывает на таблицу векторов прерываний:
(gdb) info registers mtvec
mtvec 0x1c0 448

Есть множество служебных регистров (до 4096 штук), они называются Control&Status регистры, доступ к ним особый с помощью специальных команд csrr для чтения, csrrw для записи таких регистров. Наиболее важные CSR регистры (с точки зрения обработки прерываний) это 

  • mstatus (0x300): Регистр машины состояний, в том числе статус разрешения или запрещения прерываний;
  • mie (0x304): Регистр разрешения прерываний от разных источников;
  • mtvec (0x305): Регистр базового адреса таблицы прерываний;
  • mepc (0x341): Регистр, который хранит адрес возврата из прерывания
  • mcause (0x342): Регистр хранит причину прерывания.

Изначально я думал, что я смогу вот так в отладчике GDB пройти всю программу isr_sample.S от начала до конца и в том числе я надеялся увидеть прерывание программы и переход к обработчику прерывания. Но я ошибался. Скорее всего подключение GDB каким-то образом маскирует или запрещает прерывания.

Там в программе isr_sample.S, которая является частью тестбенча процессора есть такие строки:

   sh t1, (t0) //send command to generate external interrupt on line 9 to testbench
   nop
   nop
   nop
   nop //wait for external interrupt

Прерывание вызывается искусственно и я думал, что буду проходить во время команд nop где-то должен был бы перейти по вектору обработчика внешнего прерывания, но не перешёл. Немножко досадно.

Но всё равно поизучать обработчик прерывания можно. Например, можно вечный цикл поставить не в начале программы у метки _start, а уже в самом обработчике прерываний прямо перед выходом из обработчика. Как-то вот так:

vec_machine_ext_handler:
    csrr a1, mcause
    li a5, MCAUSE_EXT_IRQ //0x8000000B -- mcause = ext.irq
    ......
    csrr t1,mepc
qqq:
    nop
    la t0,qqq
    jr t0
    mret

Весь эксперимент можно повторить заново, перезапустить тестбенч, он повиснет уже прямо в обработчике прерываний на вечном цикле по адресу метки qqq. Подключиться дебагером, посмотреть содержимое регистра mepc (или t1 - я в него считываю значение регистра mepc командой csrr) - это адрес возврата из прерывания, выйти из вечного цикла перезаписав регистр t0 и пройти команду mret - убедиться, что исполнение программы вернётся по адресу возврата, который был в регистре mepc:

mepc reg

Я всё это проделал и оказалось, что так это и работает! 

В общем, я убедился, что отладка с помощью GDB это мощнейший инструмент для изучения процессора RISC-V. Изучать процессор таким способом гораздо проще и многое сразу становится понятней, если выполнять программу по шагам, а не просто смотреть на неё в редакторе.

Общая схема описанного в этой статье эксперимента вот такая:

Schema

И, как видите, подключаться отладчиком GDB можно даже к виртуальной системе RISC-V Scr1 работающей в симуляторе Verilator.

Попробуйте повторить мой эксперимент - у вас получится!

 

 


Комментарии  
0 #1 valerysmd 08.07.2025 05:01
Отличная тема!
Добавить комментарий