riscv-debug

Overview

Debug Transport Module(DTM)

Debug Transport Modules provide access to the DM over one or more transports (e.g. JTAG or
USB) .

这个模块是和外部打交道,把外部信号转成DMI,送给DM。从协议上这句话也可以看出,外部接口可能有JTAG或者USB。目前协议暂时只涉及JTAG。下面也只介绍JTAG。

TAP : Test Access Port

IR: Instruction register

DR: Data register

协议规定DTM TAP IR至少有5bit,也就是必须要实现以下寄存器。

对应RTL层次结构如下。JTAG信号进到 JtagTapController, 然后根据address分到对应的chain上。

其中IDCODE寄存器是个只读寄存器,从代码上看,被设成了0x2000_0001。

Openocd 启动后,在复位之后会来扫描这个IDCODE寄存器。

下一个寄存器是dtmcs,

Openocd 下一步是初始化DAPs,会来读这个dtmcs,从version中知道是哪个版本,从而调用不同版本的处理,从log也看到这里用的是0.13版本的c程序;然后从abits里面知道dmi的address size。

到这步,Openocd已经知道DTM的所有信息了,就可以通过dmi寄存器来像DMI接口上发送请求了。写这个寄存器时,根据op可以决定是要读还是写DMI;读这个寄存器时根据op的值可以知道上一笔操作是成功、失败还是仍在处理中。

Debug Module Interface(DMI)

DTM 通过DMI 连接 DM,DTM是master,DM是slave。DMI使用7到32bit地址位宽。支持读写操作。接口上的信号就是上面dmi寄存器的分解。

Debug Module (DM)

DM提供的功能有:

  1. 给debugger关于implementation的信息
  2. 允许任意一个hart halt和resume
  3. 提供hart是否halt的状态
  4. 提供abstract access,从而访问已经halt的hart的GPRs
  5. 提供复位信号的访问,并能让debugger从复位后第一条指令开始控制
  6. 提供一种机制允许hart立即从reset中出来,不管复位信号。(可选)
  7. 提供abstract access hart非GPR寄存器。(可选)
  8. 提供program buffer以强制hart执行任意代码。(可选)
  9. 允许多个hart同时hart,resume,复位。(可选)
  10. 允许memory access任意hart可以看到的地址。(可选)
  11. 允许直接系统总线访问(system bus access,SBA)。(可选)

register

dmcontrol(0x10)

dmstatus(0x11)

Openocd下一步会创建一个DM,然后来激活它。248行先把dmactive写0,再250行把dmactive写1。从而复位了整个DM。

253行把dmcontrol的hasel, halsello, halselhi, dmactive都写1。

256行读回来,发现只有hasel, hartsello有值。hasel为1表示可以有多个hart被选中。这里的hartsello=31,表示支持32个hart。

259行读dmstatus,得到0xc0a2,也就是264行显示的那些bit拉高了。从而判断出hartsellen是5。

Hart Info (0x12)

Openocd 下一步继续读Hart Info,这个有什么用,还不清楚。

System Bus Access Control and Status(sbcs) (0x38)

Openocd下一步读sbcs,读出来是0,表示不支持SBA。

Abstract Control and status(abstractcs) (0x16)

Openocd 下一步读取abstractcs,得到progbuffersize,和datacount。

Openocd下一步会通过选择每一个hart,然后读出它的状态,来判断当前系统有多少个harts。第一次选择hart0,判断出状态没有noexist。

然后递增的选择hart,直到最后,发现了nonexistent,就知道了总共有多少了harts了。

选中单个hart

利用dmcontrol中的hasel填0,hartsel填index,就能选中单个hart。hartsel是index形式。比如把hasel填0,hartsel填3,那就是选中hart3。hartsel最多20bit,也就意味着最大可以支持2^20个harts。

选中多个hart

选中多个hart是通过hawindowsel和hawindow,再加上hasel来实现的。

Hart Array Window Select(hawindowsel) (0x14)

Hart Array Window(hawindow) (0x15)

这里hawindow是bitmap形式的,一bit代表一个hart。hawindowsel是序号。比如把hawindowsel设置为1,hawindow设置为0b10,那选中的hart就是33。也就是这里最多可以有2^15 * 32 = 2^20个harts,和前面选单个harts是匹配的。

对于硬件来说,如果有2^20个hart,那就需要有2^20 bit 寄存器来保存这些信息。对于用户来说只要操作这两个寄存器就能够把每一个hart都访问到。

正常操作应该是

  1. write hawindowsel 1 (确定范围是32~63)

  2. write hawindow 3 (选中33号和32号hart,并把这两个值保存到mask中)

  3. write hasel 1 (确定选中多个hart,可以从mask中拿到选中了hart32,33)

如果要选中多个,又在不同范围的,只要多写几次就可以了

  1. write hawindowsel 0 (确定范围是0~31)
  2. write hawindow 3 (选中0号和1号hart,并把这两个值保存到mask中)
  3. write hawindowsel 1 (确定范围是32~63)
  4. write hawindow 3 (选中33号和32号hart,并把这两个值保存到mask中)
  5. write hasel 1 (确定选中多个hart,可以从mask中拿到选中了hart0,1,32,33)

如果是连续写两次hawindowsel,应该是只有后面那次有效,而且只有在写hawindow时,才会更新全局mask。

  1. write hawindowsel 0 (确定范围是0~31)
  2. write hawindowsel 1 (确定范围是32~63)
  3. write hawindow 3 (选中33号和32号hart,并把这两个值保存到mask中)
  4. write hasel 1 (确定选中多个hart,可以从mask中拿到选中了hart32,33)

halt hart

到目前为止,Openocd应该是拿到了足够多的DM的信息了。可以开始halt动作了。

往halsel填0,hasel选0,表示选中hart0,haltreq填1,表示发出hart0 halt请求。然后读出status,看到allhalted已经拉高了,表示hart0已经进入halt状态了。

Abstract Command

Abstract Command(0x17)

其中cmdtype

如果cmdtype等于0的话,命令格式如下

如果cmdtype等于1,命令格式如下

如果cmdtype等于2,命令格式如下

再继续看Openocd的动作,下一步尝试读取GPR,往command里面写命令,读status,发现并没有报错,说明支持debugger访问GPR。

然后再尝试读取CSR。发现状态返回2,也就是unsupport。不支持访问CSR,然后disable掉读CSR功能。

但是debugger又想知道misa(Machine ISA Register)寄存器,怎么办呢。这里做了件很巧妙的事情,因为是可以读取GPR的,所以,debugger把一条读取misa的指令扔到hart里面去执行,这条指令把misa读取到s0寄存器里面,然后再读取s0就能得到misa的值了。

417行先把s0寄存器的值读回来,并保存起来。429行和432行把两条指令写到Program buffer里面去,然后434行写了一条命令,把postexec置成了1,表示要执行。然后440行把s0寄存器的值都回来,就拿到了misa。455行再把之前保存的s0寄存器写回去,复原。

其中写进去的指令,反汇编后得到是csrr s0, misa

resume

然后再把hart0 给resume起来。这里仍然是通过dmcontrol和dmstatus来做。

到这里,第一个hart就处理完了。可以用一样的方法,把所有的hart都遍历出来。

Memory Access

Debugger访问memory有三种方式。

  1. 使用program buffer,和前面读取misa寄存器一样的方式。
  2. 使用abstract access memory command
  3. 使用system bus access

其中两种方式的对比如下

SBA

如果支持SBA,可以通过SBCS、SBADDRESS、SBDATA直接访问memory。

通过sbaccess128/64/32/16/8可以得到硬件支持哪些访问位宽。以64位为例。

写一个地址的流程如下。

  1. 写SBCS,设置sbasize为3,表示接下来要访问64bit
  2. 写SBADDR1和SBADDR0,把地址写进去
  3. 写SBDATA1和SBDATA0,把数据写进去,这时就会触发写入动作了
  4. 读取SBCS,判断sbbus和sberror,得到是否成功

读一个地址的流程如下。

  1. 写SBCS,设置sbasize为3,并设置sbreadonaddr,表示写完地址要读数据
  2. 写SBADDR1和SBADDR0,把地址写进去,这时就会触发读取动作
  3. 读SBCS,判断sbbus和sberror,得到是否成功
  4. 写SBDATA1和SBDATA0,读出数据

Breakpoint

断点分为软件断点和硬件断点。

软件断点是在运行起来的程序中设置特征值,也就是把断点处的指令先读回debugger,保存起来,然后把一条特殊指令写到这个地址去,从而在运行时识别。这种方法的优点时数目不受限制,但是由于要写memory,所以不能设置在ROM中。

硬件断点,需要硬件寄存器支持,断点数目受core里面的寄存器个数限制,优点是可以在任意地方设置。

software breakpoint

来看openocd的处理过程。假设是设了一条bp bp 0x20000206 4

首先2178行会查询下所有hart的状态。

然后读取了下stap,发现没读出来。这步有什么用,不确定。

下一步执行了几条指令,这几条指令是fence.i fence ebreak

然后遍历所有核,都发一遍这些指令。

然后把s0和s1的值读出来,保存起来。

然后再把0x20000206和0x20000208地址的值读出来。这也是通过几条指令完成的。

然后把s0和s1的值还原回去,结束了这次操作。

下一步是写0x20000206地址了。又来一遍保存s0和s1,可以看出这里其实有很多重复动作。

然后再次通过指令的方式,把0x00100073写到0x20000206的地址里面去。

从openocd的源码也可以看出,软件断点插入的指令也是ebreak

再把备份的s0和s1给写回去。

然后让所有的hart都执行fence.i fence ebreak

这样,软件断点就设置完了。

恢复执行:当断点命中后,如果直接恢复执行,那会再次触发断点。对于这种情况,大多数调试器的做法都是先单步执行一次,设置单步执行标志,然后恢复执行,将断点所在位置的指令执行完。由于设置了单步标志,CPU执行完断点位置的指令便会再次进入debug模式,这次调试器不会通知用户,而是做一些内部操作后恢复程序的执行。这个过程一般称为“单步走出断点”。如果用户在恢复程序执行前取消了该断点,是不需要单步执行一次的。

hardware breakpoint

设置硬件断点,首先读取tselect。

然后把0再写进tselect

再读一次tselect

再读tdata1

再把1写到tselect里面,再都回来

再去读tdata1

再把2写到tselect里面

再读回来

再读tdata1

再递增写,直到某次读回来的tselect不是写进去的值,这样就知道支持多少个硬件trigger。

然后把所有的hart都遍历一遍。

然后再读tselect,读出来是0,再写tselect为0,再读tdata1,再写tdata1。

再读回来tdata1,再写tdata2,把地址写进去。

然后就可以开始下一个hart了。

hart1的话,写tdata为105c,为什么?

把所有hart做完之后,再写一次tselect

然后就做完了添加硬件bp

在resume的时候,还会更新dcsr的值。把dcsr里的ebreaks, ebreaku,ebreakm都置一。

最后在发resumereq。

resume之后,开始poll_hart。直到某个hart处于halt状态。

然后就可以读取dcsr,从而直到是什么原因。

Debug Control and status(dcsr, at 0x7b0)

Other

Note: 记得在验证JTAG时,要加上一些随机性里面。比如时钟随机,因为JTAG时钟是异步的。比如debug信号给到core的时间点,让core在不定的时间点进debug。debug模式的时候中断没有用。