前文
STEP BY STEP設計一個RISC-V仿真器之二:OpenOCD適配新的設備驅動
STEP BY STEP設計一個RISC-V仿真器之一:OpenOCD開發環境搭建
分享了OpenOCD開發環境搭建以及如何添加驅動。
RISCV DEBUG系列之二:基于JTAG的RISCV的DM操作數據流
RISCV DEBUG系列之三:RISCV的DM操作Jtag底層驅動實現
RISCV DEBUG系列之一:JTAG簡介
分享了實現JTAG硬件具體的操作。
現在就差來實現二者之間的橋梁了,即OpenOCD驅動的具體操作通過通訊接口發送給仿真器硬件,仿真器硬件來實現具體硬件操作。
二者之間的橋梁需要具體的通訊鏈路一般是USB簡單點實現也可以用串口,在此之上還需要設計通訊協議,用于交流進行什么操作返回操作結果。
這一篇開始就來實現這個橋梁。主要分為兩部分,
一部分需要知道OpenOCD依賴哪些具體的操作,實現相關的操作
一部分是要基于這些依賴操作實現合適通訊鏈路以及之上的協議,用于驅動和仿真器硬件進行交互。
OpenOCD已經對相關的依賴進行了抽象,只需要驅動實現即可,
我們之前添加驅動時的驅動實例
struct adapter_driver xxlink_adapter_driver的成員
.jtag_ops = &xxlink_interface,即為要實現的jtag操作接口
struct jtag_interface只有兩個成員
成員變量supported表示是否支持對應能力,目前只有一個bit
define DEBUG_CAP_TMS_SEQ(1 << 0)成員函數execute_queue,是核心,就是執行依賴的命令序列
int (*execute_queue)(struct jtag_command *cmd_queue);cmd_queue是一個鏈表
其每個節點定義如下
struct jtag_command {union jtag_command_container cmd;enum jtag_command_type type;struct jtag_command *next;};
其中enum jtag_command_type type;表示命令類型
支持以下命令
enum jtag_command_type {JTAG_SCAN = 1,/* JTAG_TLR_RESET's non-minidriver implementation is a* vestige from a statemove cmd. The statemove command* is obsolete and replaced by pathmove.** pathmove does not support reset as one of it's states,* hence the need for an explicit statemove command.*/JTAG_TLR_RESET = 2,JTAG_RUNTEST = 3,JTAG_RESET = 4,JTAG_PATHMOVE = 6,JTAG_SLEEP = 7,JTAG_STABLECLOCKS = 8,JTAG_TMS = 9,};
而不同命令類型,對應不同的參數
union jtag_command_container cmd;/*** Defines a container type that hold a pointer to a JTAG command* structure of any defined type.*/union jtag_command_container {struct scan_command *scan;struct statemove_command *statemove;struct pathmove_command *pathmove;struct runtest_command *runtest;struct stableclocks_command *stableclocks;struct reset_command *reset;struct end_state_command *end_state;struct sleep_command *sleep;struct tms_command *tms;};
前面可以看到enum jtag_command_type即需要實現的操作,以下做一個簡單的總結, 具體功能,參數代表什么含義,可以參考其他仿真器的實現。

其實核心的就是需要兩個功能,狀態轉移JTAG_TMS=9和IR/DR寄存器的操作JTAG_SCAN=1
這其實就是JTAG最基礎的操作。
這樣設計則只依賴于JTAG本身,和TARGET無關,這樣TARGET相關處理是OpenOCD實現的,不是仿真器硬件,這樣一個仿真器硬件可支持不同的平臺TARGET。
SCAN命令是最重要的命令, 即進行IR/DR的移入移出操作, 所以這里重點介紹下,其他命令參考其他仿真器的實現即可。
可以回顧下JTAD的狀態機
RISCV DEBUG系列之一:JTAG簡介
JTAG基本的行為就是將數據移入移出DR或者IR寄存器, SCAN命令即對應該行為,SCAN也是比較形象的。
其命令類型為
enum jtag_command_type type;JTAG_SCAN = 1,
對應的參數是
struct scan_command *scan;struct scan_command {/** instruction/not data scan */bool ir_scan;/** number of fields in *fields array */unsigned int num_fields;/** pointer to an array of data scan fields */struct scan_field *fields;/** state in which JTAG commands should finish */enum tap_state end_state;};
其中ir_scan表示是操作IR還是DR寄存器,為true表示操作IR寄存器。
struct scan_field *fields; 描述一個最小的SCAN操作, 后面再介紹
unsigned int num_fields; 表示有上述多少個最小SCAN操作。
enum tap_state end_state; 表示SCAN結束之后最終要位于什么狀態
我們繼續來看最小的一筆SCAN操作struct scan_field
struct scan_field {/** The number of bits this field specifies */unsigned int num_bits;/** A pointer to value to be scanned into the device */const uint8_t *out_value;/** A pointer to a 32-bit memory location for data scanned out */uint8_t *in_value;/** The value used to check the data scanned out. */uint8_t *check_value;/** The mask to go with check_value */uint8_t *check_mask;};
其中unsigned int num_bits 表示要SCAN多少bit
const uint8_t *out_value; 用于存儲需要移入到設備的數據。
uint8_t *in_value; 表示存儲從設備移出的數。 注意這里的out和in是針對buffer而言的不是針對設備而言的。
uint8_t *check_value;和
uint8_t *check_mask;
用于判斷設備中移出的值是否符合預期, 假設in_value & check_mask = check_value則表示符合預期。 這個用于做執行結果判斷。

以下是一些相關的函數與處理
在指定了enum tap_state end_state;參數的對應的命令中
(包括JTAG_RUNTEST,JTAG_TLR_RESET,JTAG_SCAN),其結束狀態應該是某些穩定的狀態,對于JTAG這些穩定狀態包括
TAP_RESET:TAP_IDLE:TAP_DRSHIFT:TAP_DRPAUSE:TAP_IRSHIFT:TAP_IRPAUSE:
函數bool tap_is_state_stable(enum tap_state astate)用于判斷是否是穩定狀態。
上述SCAN命令的參數,struct scan_command描述了一組最小SCAN操作, 但是其參數值等都是使用的指針, 最終要將這些信息通過鏈路轉發給仿真器硬件去執行,所以這里要將相關信息組織成仿真器硬件協議需要的數據包。
這里通過jtag_build_buffer函數來實現
即輸入const struct scan_command *cmd
輸出uint8_t **buffer
int jtag_build_buffer(const struct scan_command *cmd, uint8_t **buffer)該函數返回實際處理的bit數。
實現如下
先獲取scan的bit數, jtag_scan_size
因為有num_fields個struct scan_field,所以就是將num_fields個struct scan_field的num_bits累加。
然后申請空間, 按照8字節向上圓整
*buffer = calloc(1, DIV_ROUND_UP(bit_count, 8));這里是賦值給buffer,buffer是指向指針的指針。
然后遍歷cmd->num_fields處理
然后將需要發送到設備的數據
移動到剛分配的buffer中去
buf_set_buf(cmd->fields[i].out_value, 0, *buffer,bit_count, cmd->fields[i].num_bits);
即源地址cmd->fields[i].out_value處偏移0bit的數據長度 cmd->fields[i].num_bits的數據,復制到*buffer偏移bit_count bit的地方。
buf_set_buf是現實的是按照bit復制。
jtag_read_buffer函數將, buffer中的數據,再返回到const struct scan_command的in_value中去。
所以驅動層需要實現的就是
通過gdb調試,結合增加打印信息, 可以跟蹤到初始化會執行到以下驅動接口
gdb --args openocd.exe -f xxlink.cfg -d4
其中-d4后面的數字表示使能的debug等級,
等級如下
enum log_levels {LOG_LVL_SILENT = -3,LOG_LVL_OUTPUT = -2,LOG_LVL_USER = -1,LOG_LVL_ERROR = 0,LOG_LVL_WARNING = 1,LOG_LVL_INFO = 2,LOG_LVL_DEBUG = 3,LOG_LVL_DEBUG_IO = 4,LOG_LVL_DEBUG_USB = 5,};
設置為4則<=LOG_LVL_DEBUG_IO的日志都會打印出來
比如如下打印
LOG_DEBUG_IO("%s scan end in %s", (cmd->cmd.scan->ir_scan) ? "IR" : "DR",tap_state_name(cmd->cmd.scan->end_state));
我們可以看到基本會設計如下函數調用
xxlink_handle_vid_pid_command() cfg文件中制定了xxlink vid_pid 0x1122 0x3344則會調用對應的命令
xxlink_init()xxlink_khz()xxlink_speed(): cfg文件中adapter speed 1000指定速度則會調用xxlink_khz():xxlink_speed_div():xxlink_execute_queue():xxlink_execute_queue(): cmd->type:4 JTAG_RESETxxlink_execute_queue(): cmd->type:2 JTAG_TLR_RESETxxlink_execute_queue(): cmd->type:1 JTAG_SCANxxlink_execute_queue(): cmd->type:2xxlink_execute_queue(): cmd->type:1
以上只是一個概覽,不同cfg文件內容,不同TARGET都有可能有差異,gdb調試和打印結合去調試分析即可。
以上分析了OpenOCD對JTAG操作的依賴,即知道了需要實現哪些操作,后續就是實現對應的鏈路通訊以及協議,將這些操作高速硬件仿真器去實現,并返回結果即可。