На неделе мне задали вопрос, на который я к своему стыду не смог сразу ответить: "Как стартует 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 на трёх наших платах:
- picorv32 в Марсоход3GW2 (Gowin, GW1NR);
- picorv32 в MCY112 (Altera Cyclone I);
- picorv32 в MCY316 (Altera Cyclone III).
Во всех случаях портирования проектов я ставил себе целью просто запустить проект и увидеть в последовательной консоли приветственное сообщение от 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, то очень многое становится понятным даже без чтения документации по линковщику.
Память начинается с нуля и имеет размер 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 открыт:

Во втором окне терминала мне нужно запустить 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:

При этом, понимаете, что обмен между 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:

Выполняя команду stepi в консоли GDB я могу исполнять пошагово каждую команду процессора.
Дальше я могу дойти до адреса 0x20C, где расположена команда перехода назад "jr t0" и в этот момент я могу исправить ручками содержимое регистра t0 и покинуть этот цикл. Выполним команду
(gdb) set $t0=0x210
(gdb) stepi
И всё, вечный цикл покинули и можно двигаться вперёд и исполнять все подряд команды процессора и смотреть, как она работает.
Все три моих окна: Verilator и тестбенч, 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:

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

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

Подробнее...