龙芯的官方 EJTAG 调试探头
硬件
龙芯 EJTAG 调试探头(下称 “调试器” )采用了一片 Cypress CY7C68013A 单片机作为 USB HS PHY,并配置成 FIFO 模式,将 FIFO 的读写接口拉出供 FPGA 使用;FPGA 采用了一片 Altera Cyclone-II EP2C5T144,上面实现了对被调试芯片 TAP 的操纵逻辑。
FPGA 上面载有一个 MIPS789 软核,并配有 8KiB 的固件,以此实现 USB 通讯、指令包解析、TAP 控制逻辑等任务。
实现细节
JTAG 时钟配置寄存器 0x81000070
FPGA 上的 MIPS 软核的总线上可以访问 FPGA 上的一些逻辑电路的寄存器,其中 JTAG 时钟配置就是通过使 MIPS 软核写一个寄存器实现的。这个寄存器在 MIPS 软核地址空间中的 0x81000070
地址处,向此地址写入 4 字节宽的整数以调节 JTAG 时钟分频和采样 TDO 时机。
(文档中和程序帮助中写的是 TDI 采样时机,但调试器需要采样的应当只有 TDO,我认为应当是笔误)
寄存器位域 | 读/写 | 含义 |
---|---|---|
[31:16] | 写,读出为0 | 此字段为 0x1 时,低半段表示分频系数; 此字段为 0x2 时,低半段表示 TDO 采样时机。 其他值将被硬件忽略。 |
[15:0] | 写,读出为0 | 表示分频系数 CLKDIV 时,原则上只允许此字段中有 1 个被置位的位(测试中发现如果有多个位被置位,则除最高的那个被置 1 的位以外的位均被视为 0)。此时 JTAG 端口的时钟频率为 $ f_{JTAG} = {15 \over CLKDIV}\text{MHz} $。如果 CLKDIV 被设为 0,JTAG 端口的时钟将停止。表示 TDO 采样时机 SAMPLESEL 时,仅最低 2 位([1:0])有效,因此有效的取值为 0、1、2、3 ;当更大的值被写入时,高位将被硬件所忽略。依照 JTAG 时序,第 n 位输入在 CLK 上升沿被从 TDI 读入时,第 n - 1 位才被从 TDO 移出,因此标准的 TDO 采样时机总是位于滞后于输入数据采样时机 1 个周期的时间点;然而由于信号完整性问题或者芯片设计本身导致等原因,可能需要微调采样时机来获得正确的 TDO 读出值。经初步实验验证,当 SAMPLESEL 为 0 时采样时机提前,为 3 时采样时机滞后,且都最大不超过 TCK 一周期的时间。结合配置文件中基本都会将采样时机配置为 1,可以认为 SAMPLESEL 设为 1 时代表使用标准的采样时机。 |
TAP 参数变量指针
在此版本的调试器固件中,不再写死所使用的 IR 指令、IR 长度等信息(因为它们在不同的硬件、不同的架构如 LoongArch 和 MIPS 中都不相同,甚至不排除龙芯以后继续更改的可能性),而是使用全局变量保存;同时,地址空间的首部放置了指向这些全局变量的指针,这样可以通过上位机软件根据选定的芯片型号修改这些参数。即使全局变量的地址改变,也可以通过这些固定位置(入口点程序由汇编编写的话,很方便就能做到不影响这些指针所处位置)的指针确定需要修改的变量位于哪里。
所有指针指向的数据均为 uint32_t
,修改时直接使用下文 0x01 USB 指令即可。
指针所处地址 | 含义 |
---|---|
0x40 | 指向目标芯片 IR 寄存器长度变量 |
0x44 | 指向应使用的 EJTAG SKIP 指令码 |
0x48 | 指向应使用的 EJTAG CONTROL 指令码 |
0x4c | 指向应使用的 EJTAG DATA 指令码 |
0x50 | 指向应使用的 EJTAG ADDRESS 指令码 |
0x54 | 指向应使用的 EJTAG FASTDATA 指令码 |
例如,上位机命令如 usblooptest ${usblooptest 0x44} 0x0
就等于告诉调试器,之后如果要选中目标芯片 TAP 上的 Skip 寄存器,那么应当向 IR 中写入 0x0。(示例来自上位机软件中附带的 scripts/gdb-la.cmd
文件)
USB 通信
描述符
调试器连接上电脑后是一个 Vendor Class 自定义设备,属于 USB 2.0 HS 设备,VID=0x2961(占了 Miselu 公司的位子),PID=0x6688 。
使用两个 USB 端点(Endpoint)进行 Bulk 传输:端点 2 为 OUT,端点 6 为 IN。
通信协议
上位机(la_dbg_tool_usb
等程序,下同)与调试器遵守基本数据格式约定如下:
每次向调试器发送的字节流中含有一个或多个指令包。上位机会期待有的指令包被执行后,调试器向上位机回复一些回读的数据(否则上位机将无穷地等待下去,且协议中不存在超时重传、错误处理的部分)。一次发送了多个指令包时,调试器将顺序执行、按执行顺序返回数据。
包的一般结构如下:
偏移量 | 长度 | 含义 |
---|---|---|
[9:0] | 10 位 | 每种指令特定的配置(下称 “配置”) |
[15:10] | 6 位 | 指令码 |
[…:16] | 任意 | 负荷数据(可选) |
下面列出的指令码的定义来源于对 FPGA 固件、上位机程序的逆向工程与早年间旧版本 EJTAG 上位机中附带的部分头文件中发掘的信息。
理论上可以依据这些信息制作与官方的调试上位机相兼容的调试探头,但是需要注意的是,官方上位机存在使用 usblooptest
指令覆盖远古版本(准确地说是 2015 年的某个版本)固件中的代码的行为,这使得仅仅依据某个版本固件的逆向工程成果制作的兼容探头或许无法在将来使用。
注:下列定义来源于对一个固件版本为 0x20210129
的调试器的固件的逆向工程。使用了 Ghidra。部分内容同时参考了上位机程序的逆向工程。
-
0x01 读写调试器内存空间(上位机指令:
usblooptest
,带有1个或者2个参数时)-
配置结构:
偏移量 长度 含义 [0:0] 1 位 此次操作为读(1)/写(0) [9:1] 不关心 -
负荷结构:
偏移量 长度 含义 [31:0] 4 字节 需要读/写的地址(调试器中的是一颗 32 位 MIPS-1 软核,故地址长度为32位) [63:32] 4 字节 (可选)写入的数值。 -
返回数据:
偏移量 长度 含义 [31:0] 4 字节 (可选)读的结果。如果此次操作是写,则不会返回任何数据。 PS. 调试器固件就是用这个指令 dump 出来的。而且,如果用 USB Blaster 连板上的 FPGA 的 JTAG 口,会发现是禁用的。
PPS. 这个指令在手册里是用来修改 JTAG 时钟速度和采样点的,实际上是直接写 FPGA 内实现的 JTAG 逻辑电路的硬件寄存器。
-
-
0x03 操作特定调试器 I/O 端口电平(上位机指令:
jtagled
)* 此部分已经过硬件测量验证后重新修订。
-
配置结构:
偏移量 长度 含义 [0:0] 1 位 引脚电平高低 [7:1] 7 位 引脚标号 [8:9] (不关心) 其中,引脚标号定义如下:
引脚标号 上位机指令中的名称 含义 0 led 调试器上的绿色 LED 1 fpga_reset (含义不明,FPGA 的 nCONFIG 引脚并未连接到 FPGA 自身可控制的器件上) 2 oe 板上 74LVC573 触发器的 #OE 引脚 3 trst EJTAG 的 #TRST 信号 4 brst EJTAG 的 #BRST 信号 5 dint EJTAG 的 DINT 信号 6 tap_reset (含义不明,可能是用于清空 FPGA 内 TAP 控制器的内部信号?) - 负荷结构:(无)
- 返回数据:(无)
-
-
0x04 读写 IR
-
配置结构:
偏移量 长度 含义 [6:0] 7 位 JTAG 链上的 CPU 核心数
注:此字段似乎没什么用,因为参考上位机的代码可见读写具体某个核心的 IR 的逻辑应该是上位机处理了[7:7] 1 位 不关心 [8:8] 1 位 是否将读缓冲区的结果发回上位机 [9:9] 1 位 是否将 TDO 移出的数据写入读缓冲区 -
负荷结构:
偏移量 长度 含义 [15:0] 2 字节 将要读/写 IR 数据的位数 […:16] n * 4 字节 将要写入 IR 的数据;即使仅需读取 IR 也需要放入占位数据。长度向上取整到 4 字节的倍数。 -
返回数据:
偏移量 长度 含义 […:0] n * 4 字节 (可选)读的结果。如果没有指定返回读取到的数据,则不会返回任何数据。长度向上取整到 4 字节的倍数。
-
-
0x05 读写 DR
-
配置结构:
偏移量 长度 含义 [7:0] 8 位 JTAG链上的 CPU 核心数
注:此字段似乎没什么用,因为参考上位机的代码可见读写具体某个核心的 DR 的逻辑应该是上位机处理了
注意:此处与 0x04 指令定义产生不同的主要原因是,上位机读写 IR 时额外按位与了0x7f
起到了截取部分位的效果,在读写 DR 时没有写这个逻辑,因此我把除了另两个有意义的位以外的 8 位全部分配给了 CPU 核心数字段。[8:8] 1 位 是否将读缓冲区的结果发回上位机 [9:9] 1 位 是否将 TDO 移出的数据写入读缓冲区 -
负荷结构:
偏移量 长度 含义 [15:0] 2 字节 将要读/写 DR 数据的位数 […:16] n * 4 字节 将要写入 DR 的数据;即使仅需读取 DR 也需要放入占位数据。长度向上取整到 4 字节的倍数。 -
返回数据:
偏移量 长度 含义 […:0] n * 4 字节 (可选)读的结果。如果没有指定返回读取到的数据,则不会返回任何数据。长度向上取整到 4 字节的倍数。
-
-
0x08 回环测试(上位机指令:
usblooptest
,不带有参数时)- 配置结构:(不关心)
-
负荷结构:
偏移量 长度 含义 [31:0] 4 字节 上位机发来的一个随机数。 -
返回数据:
偏移量 长度 含义 [31:0] 4 字节 经过处理返回的随机数。
此处返回数据的处理方式为 (未验证):
uint32_t ejtag_loopback_test(uint32_t input) { uint16_t input_lo = input & 0xFFFF; uint16_t input_hi = (input & 0xFFFF) >> 16; uint16_t ret_lo = (input_lo << (input_hi & 0x1F)); uint16_t ret_hi = (input_lo >> (input_hi & 0x1F)); return (ret_hi << 16) | ret_lo; }
-
0x0b SKIPACC(Skip Access,“跳过”对某 dmseg 地址的内存访问)(似乎已废弃) (上位机指令:
skipacc
)-
配置结构:
偏移量 长度 含义 [6:0] 7 位 JTAG 链上的 CPU 核心数。默认被调试设备上所有的物理 CPU 都有一个 TAP,且它们的 IR、DR 被首尾串联在一起,且数据从 TDI 进入后首先到达 CPU0 的 IR/DR、然后是 CPU1、CPU2、…… [9:7] (不关心) -
负荷结构:
偏移量 长度 含义 [15:0] 2 字节 JTAG 时钟分频系数。 [47:16] 4 字节 要跳过访问的内存地址。调试器将把此地址和 0xff200000
进行按位与,并将这个结果作为真正的要跳过访问的 dmseg 内存地址。 -
返回数据:(无)
此指令码几乎可以肯定已经被废弃且不会被上位机使用(所有附带脚本、可执行程序内部均未见对此功能的实际运用),加之此指令内部写死了 32 位 MIPS 处理器的 dmseg 地址,显然除非完全重写固件,否则它不可能被运用于现今广泛生产的 LoongArch64 硬件上。
为了文档的完整度,故依然在此列出此指令执行的具体操作:先清空 JTAG 控制逻辑的输入输出 FIFO,然后从一个未初始化的栈变量中取出目标 CPU 号,并(1)在“这个 CPU 的 TAP”上选择 ADDRESS 指令。然后再将 Address 寄存器读出,判断是否等于要跳过访问的内存地址。如果等于,则选择 CONTROL 指令,然后向 Control 寄存器中直接写入
PrAcc = 0
完成这次内存访问,然后跳转到(1)处循环;如果不等于,则清空 JTAG 输出 FIFO 并返回。
-
- 0x0c 快速写目标内存(使用 FASTDATA)(包括
put
在内的许多种上位机指令均使用) -
0x0d 快速写目标内存(不使用 FASTDATA)
-
配置结构:
偏移量 长度 含义 [6:0] 7 位 JTAG 链上的 CPU 核心数。当此参数为 0 时,负荷结构中不存在描述目标 CPU 核的字段,并进入一个所谓的 GS232 模式;否则,默认被调试设备上所有的物理 CPU 都有一个 TAP,且它们的 IR、DR 被首尾串联在一起,且数据从 TDI 进入后首先到达 CPU0 的 IR/DR、然后是 CPU1、CPU2、…… [7:7] 1 位 CPU 字长是否为 64 位 [9:8] (不关心) -
负荷结构:
偏移量 长度 含义 [15:0] 2 字节 JTAG 时钟分频系数(设定下载速度用)。 [47:16] 4 字节 写入数据字(Word)数(处理器字长,如 32 位 CPU 就是一个 32 位字,64 位处理器就是一个 64 位字)。 [63:48] 2 字节 (根据配置结构描述,可选)目标 CPU 核。 […] n * 4 字节 将要写入的数据。
注意:此数据序列是经过上位机处理过的。详见快速读写数据格式。 -
返回数据:(无)
0x0c 与 0x0d 指令码均使用以上指令结构,只是根据调试器配置文件指定,选择是否使用 FASTDATA IR 指令以加快数据传输过程。
上位机会先向 RAM 中上传一段机器码,机器码中已蕴含了起始地址指针,并让 CPU 跳转到那里执行。这段代码会从 dmseg 中读取数据然后写入到指定的目标地址,而这次访存操作的结果则将由 EJTAG TAP 提供,以此完成内存写操作。所以,此指令码的负荷中不包含写入的具体地址的信息。
调试器执行此指令时只是简单地执行以下的步骤:
- 向 IR 写入 DATA,然后从 USB 缓冲区读出一个处理器字长的数据,并将之写入 DR(如果有多个核心,需要考虑到 SKIP 位的影响);
- 向 IR 写入 CONTROL,然后向 DR 写入 0x0000C000;
- 将计数器增加 1;如果计数器没有计到指定的多少个字的数据,则回到 1 。
如果使用 FASTDATA,则是:
- 向 IR 写入 FASTDATA,然后从 USB 缓冲区读出一个处理器字长的数据,并在最低位之前插入一个 0 比特,再写入 DR(如果有多个核心,需要考虑到 SKIP 位的影响);
- 将计数器增加 1;如果计数器没有计到指定的多少个字的数据,则回到 1 。
配置结构中提到的 GS232 模式是调试软件中对于这个在调试器上特殊开的洞的称谓(当核心数不为 0 时其称之为 464 模式,但依然支持 32 位字长的 CPU)。简而言之,232 模式下默认目标设备为 32 位 CPU、单核、使用 FASTDATA。在 232 模式下,调试探头不会修改目标的 IR,而只是机械地从 USB 缓冲区中读出“写入数据字(Word)数”个 32 位字,然后将它们写入目标的 DR 中(注意因为 232 模式默认目标可使用 FASTDATA,是会添加一个 SPrAcc 的 0 比特的,实际写入的 DR 串是 33 位长),而在 464 模式下,调试探头是会修改 IR 以选择 FASTDATA,DATA 或者 CONTROL 指令的。
-
- 0x0e 快速读目标内存(使用 FASTDATA)
-
0x0f 快速读目标内存(不使用 FASTDATA)
-
配置结构:
偏移量 长度 含义 [6:0] 7 位 JTAG 链上的 CPU 核心数。默认被调试设备上所有的物理 CPU 都有一个 TAP,且它们的 IR、DR 被首尾串联在一起,且数据从 TDI 进入后首先到达 CPU0 的 IR/DR、然后是 CPU1、CPU2、…… [7:7] 1 位 CPU 字长是否为 64 位 [9:8] (不关心) -
负荷结构:
偏移量 长度 含义 [15:0] 2 字节 JTAG 时钟分频系数(设定读出速度用)。 [47:16] 4 字节 读取数据字(Word)数(处理器字长,如 32 位 CPU 就是一个 32 位字,64 位处理器就是一个 64 位字)。 [63:48] 2 字节 目标 CPU 核。 -
返回数据:
偏移量 长度 含义 […:0] n * 4 字节 调试器返回读取的内存数据。
注意:此数据序列需要经后处理才可以使用。详见快速读写数据格式。
0x0e 与 0x0f 指令码均使用以上指令结构,只是根据调试器配置文件指定,选择是否使用 FASTDATA IR 指令以加快数据传输过程。
上位机会先向 RAM 中上传一段机器码,机器码中已蕴含了起始地址指针,并让 CPU 跳转到那里执行。这段代码会将要读取的内存中的数据读出,然后写入到 dmseg 中指定的目标地址,而这次访存操作的结果则将由 EJTAG TAP 截取,调试器读取 DATA 寄存器,以此完成内存读操作。所以,此指令码的负荷中不包含读取的具体地址的信息。
调试器执行此指令时只是简单地执行以下的步骤:
- 如果还没有读完负荷中指定的长度,那么继续执行;否则命令完成;
- (此时已选择 IR 指令为 DATA)向 DR 中填充 0 并读取 DR 原始的内容,即读出 Data 寄存器的内容(如果使用了 FASTDATA,则填充的 0 还可以顺便清空
SPrAcc
),且读出的数据中还包含了整个 JTAG 链上其他已经被设为 BYPASS 状态的 TAP 的 Bypass 寄存器(如果使用了 FASTDATA,则还包含SPrAcc
); - 将步骤 2 中读出的数据的末尾追加 0 以将其字节数向上取整到 4 的倍数,然后将这些数据发送回上位机。
- 回到 1。
-
-
0x1f 返回调试器固件版本
- 配置结构:(不关心)
- 负荷结构:(无)
-
返回数据:
偏移量 长度 含义 [31:0] 4 字节 BCD 编码的一个 YYYYMMDD 日期。
快速读写数据格式
假设有以下场景:用户使用快速数据读/写指令欲读/写 32 个 u64 的内存数据,被调试的处理器字长为 64 位,拥有 4 个 CPU 核心,配置文件指定使用 FASTDATA,且用户指定使用 CPU2 进行读/写。那么在 USB 上传输的数据流,结构将如下所示:
┌───Padding for aligning to 32-bit words
│
┌──────────────────────┴─────────────────────┐ CPU0 SKIP───────┐
│ │ ┌───CPU2 SPrAcc CPU1 SKIP────┐ │
│ │ ▼ ▼ ▼
┌──┬──┬──┬──┬──┬──┬───────┬──┬──┬──┬──┬──┬──┬──┬──┬─────────────────────────────────┬──┬──┐
For │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 64-bit machine word from CPU2 │ │ │
Machine ├──┴──┴──┴──┴──┴──┴───────┴──┴──┼──┴──┴──┴──┴──┴──┴─────┬───────────────────────────┴──┴──┼───────┐
Word │5F 5E 5D 5C 5B 5A ... ... 49 48│47 46 45 44 43 42 41 40│3F 3E 05 04 03 02 01 00│ Bits │
0 ├─────────────────────┬─────────┼───────────────────────┼───────────┬─────────────────────┼───────┤
│ 0B ...│... 09 │ 08 │ 07 ... │ ... 00 │ Bytes │
└─────────────────────┴─────────┴───────────────────────┴───────────┴─────────────────────┴───────┘
┌──┬──┬──┬──┬──┬──┬───────┬──┬──┬──┬──┬──┬──┬──┬──┬─────────────────────────────────┬──┬──┐
For │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 64-bit machine word from CPU2 │ │ │
Machine ├──┴──┴──┴──┴──┴──┴───────┴──┴──┼──┴──┴──┴──┴──┴──┴─────┬───────────────────────────┴──┴──┼───────┐
Word │5F 5E 5D 5C 5B 5A ... ... 49 48│47 46 45 44 43 42 41 40│3F 3E 05 04 03 02 01 00│ Bits │
1 ├─────────────────────┬─────────┼───────────────────────┼───────────┬─────────────────────┼───────┤
│ 17 ...│... 15 │ 14 │ 13 ... │ ... 0C │ Bytes │
└─────────────────────┴─────────┴───────────────────────┴───────────┴─────────────────────┴───────┘
......
┌──┬──┬──┬──┬──┬──┬───────┬──┬──┬──┬──┬──┬──┬──┬──┬─────────────────────────────────┬──┬──┐
For │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 64-bit machine word from CPU2 │ │ │
Machine ├──┴──┴──┴──┴──┴──┴───────┴──┴──┼──┴──┴──┴──┴──┴──┴─────┬───────────────────────────┴──┴──┼───────┐
Word │5F 5E 5D 5C 5B 5A ... ... 49 48│47 46 45 44 43 42 41 40│3F 3E 05 04 03 02 01 00│ Bits │
31 ├─────────────────────┬─────────┼───────────────────────┼───────────┬─────────────────────┼───────┤
│ 17F ...│... 17D │ 17C │ 17B ... │ ... 174 │ Bytes │
└─────────────────────┴─────────┴───────────────────────┴───────────┴─────────────────────┴───────┘
上位机与探头间传输的其实并不是原始数据本身,而是 DR 串的数值:在使用 FASTDATA 时,是 IR 选择 FASTDATA 而其他 CPU 选择为 SKIP 时的整个 DR 串;在不使用 FASTDATA 时,是目标 CPU IR 选择为 DATA 而其他 CPU 选择为 SKIP 时的整个 DR 串。DR 串本身的长度一般是 (处理器字长 + CPU个数 - 1 + (使用 FASTWRITE ? 1 : 0)) 个比特(多核 CPU 要考虑其他 CPU 调到 SKIP 时的那一个空闲位,使用 FASTWRITE 时要考虑 SPrAcc 的长度)。每一个内存读/写操作对应一个 DR 串。每个 DR 串占据的空间会向上对齐到 32 位(比如 65 位长的 DR 串会占据 12 个字节)。