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提供的功能有:
- 给debugger关于implementation的信息
- 允许任意一个hart halt和resume
- 提供hart是否halt的状态
- 提供abstract access,从而访问已经halt的hart的GPRs
- 提供复位信号的访问,并能让debugger从复位后第一条指令开始控制
- 提供一种机制允许hart立即从reset中出来,不管复位信号。(可选)
- 提供abstract access hart非GPR寄存器。(可选)
- 提供program buffer以强制hart执行任意代码。(可选)
- 允许多个hart同时hart,resume,复位。(可选)
- 允许memory access任意hart可以看到的地址。(可选)
- 允许直接系统总线访问(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都访问到。
正常操作应该是
write hawindowsel 1 (确定范围是32~63)
write hawindow 3 (选中33号和32号hart,并把这两个值保存到mask中)
write hasel 1 (确定选中多个hart,可以从mask中拿到选中了hart32,33)
如果要选中多个,又在不同范围的,只要多写几次就可以了
- write hawindowsel 0 (确定范围是0~31)
- write hawindow 3 (选中0号和1号hart,并把这两个值保存到mask中)
- write hawindowsel 1 (确定范围是32~63)
- write hawindow 3 (选中33号和32号hart,并把这两个值保存到mask中)
- write hasel 1 (确定选中多个hart,可以从mask中拿到选中了hart0,1,32,33)
如果是连续写两次hawindowsel,应该是只有后面那次有效,而且只有在写hawindow时,才会更新全局mask。
- write hawindowsel 0 (确定范围是0~31)
- write hawindowsel 1 (确定范围是32~63)
- write hawindow 3 (选中33号和32号hart,并把这两个值保存到mask中)
- 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有三种方式。
- 使用program buffer,和前面读取misa寄存器一样的方式。
- 使用abstract access memory command
- 使用system bus access
其中两种方式的对比如下
SBA
如果支持SBA,可以通过SBCS、SBADDRESS、SBDATA直接访问memory。
通过sbaccess128/64/32/16/8可以得到硬件支持哪些访问位宽。以64位为例。
写一个地址的流程如下。
- 写SBCS,设置sbasize为3,表示接下来要访问64bit
- 写SBADDR1和SBADDR0,把地址写进去
- 写SBDATA1和SBDATA0,把数据写进去,这时就会触发写入动作了
- 读取SBCS,判断sbbus和sberror,得到是否成功
读一个地址的流程如下。
- 写SBCS,设置sbasize为3,并设置sbreadonaddr,表示写完地址要读数据
- 写SBADDR1和SBADDR0,把地址写进去,这时就会触发读取动作
- 读SBCS,判断sbbus和sberror,得到是否成功
- 写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模式的时候中断没有用。