写在开始
事情开始于公司需要对芯片定制一个openocd driver. 然后我开始了为期两周的JTAG学习之旅.
前提描述
具体的协议内容不是这篇笔记的重点, 就不仔细介绍了. 我把需要的一些要点单独拎出来描述一下.
- 这次的实际物理器件是
ftdi232h
- 写(tms, tdi, tck)或者读(tdo)都需要一条单独的指令, 每条指令的长度大约为80个比特.
关于这篇笔记的结构
这篇笔记主要是以时间发展顺序的角度进行描述的, 笔记内不介绍特别深入的内容, 主要以导读为主, 记录我理解的要点.
因为工作时每一段时间都有一个工作重心, 所以我在整理笔记时根据当时的工作重心取了不同的副标题.
在这次的开发经历了两个阶段:
最初, 我是基于bitbang_interface实现了read
, write
, reset
三个方法. 这种方式的实现胜在简单, 不需要了解特别多的JTAG协议细节.
后来, 由于使用bitbang效率比较低, 所以需要我针对驱动协议实现了一个特定版本的驱动代码. 这个版本要求对JTAG协议的细节有一定的理解, 不然在开发过程中无法进行常规的debug工作.
第一个阶段对应初探JTAG
和初探openocd
.
第二个阶段对应再探JTAG
和再探openocd
.
一些额外的业务分析(可以忽略)
这次协议慢的原因有两个:
-
驱动协议效率本身比较低
简单描述一下这次所需要的驱动协议的效率.
假如是一个直接支持ftdi mpsse
实现JTAG的: 它的一次tdi/tdo都只需要填充一个单独的bit(mpsse
会直接进行tck和tdo的处理).
而对于我所需要实现的协议而言, 一次发数据的操作需要一次tck的翻转(两个写命令), 一次读数据的操作需要额外加入一次读操作(即为写,读,写三次命令)
所以对于不同的请求, 我协议本身的损耗是ftdi的160倍(写操作)和240倍(读操作). -
usb请求导致的效率低下
虽然协议本身就很慢, 但是真正拖慢运行效率的实际上是bitbang的接口本身.
使用bitbang慢的原因和协议本身慢的原因不太一样. 这个瓶颈发生在每一次bitbang的读写操作都需要发起一次新的usb请求, 然后等待响应.
mpsse支持很大批量的连续读写操作, 这是为什么我可以弃用bitbang而自己实现的原因.
如果对于ftdi
或者mpsse
有兴趣的可以查询ftdi的官网
初探JTAG: JTAG是什么?
在以前的开发过程中, 用过JTAG, 但是我并不知道JTAG具体是什么. 所以在任务开始的第一个阶段我主要是搜罗资料并建立对JTAG的直观印象.
-
参考资料
下面是我在youtube(不可描述的)网站上搜到视频资料, 视频主要是对JTAG进行了一些导读. 视频的重点主要是关于JTAG的起源与边界扫描. -
课代表时间
- JTAG主要起源于PCB板级检查时候的boundary scan.
- 整个系统的有并行连接的TMS(test mode select) 和 TCK(test clock), 以及串行连接的TDI(test data output), TDO(test data input)组成.
- JTAG可以直接控制和扫描芯片周围的pin.
- 通过JTAG控制器, 还可以与芯片内部的设备进行直接通讯.(这就是我们常用gdb over JTAG的实现方法).
- JTAG的数据输入和输出都是有延迟的, 延迟取决于整个系统中所有寄存器数目的总和.
边界检查的GUI
在上面的截图中可以看到, 有些JTAG的连接器可以通过GUI直接将扫描的结果显示出来.
例如: 在某个板子上也许两个设备的pin直接接入了一个反向器, 如果控制某个pin输入/输出时, 则可以检查另一个pin的状态是否正确.
初探openocd: openocd是什么? 怎么实现一个驱动?
openocd的全称是Open On-Chip Debugger.
因为需要针对openocd写一个驱动, 所以我关注的重点主要落在了"我要怎么实现一个驱动"上.
在查阅了openocd 官方文档之后, 我大致上对openocd的使用和开发架构有了基本的了解, 大致上知道在哪里找代码了.
整个openocd的使用层级分为三级:
- interface: 实现JTAG协议的内容, 这也是我所需要关心的重点.
- board: 根据特别的开发板子, 同一个接口可能也有不同的配置选项.
- target: 芯片支持的特定的JTAG指令, 这些一般是芯片厂商提供. 我这次的任务的对象是一个现有的cpu, 所以这一层我不需要关心.
简述bitbang
bitbang接口
的实现在openocd/src/jtag/drivers/bitbang.c
里面, 对于使用这个"框架"的用户而言, 只需要提供三个方法, 就可以实现一个简单的driver.
在我clone下来的openocd版本中, sysfsgpio
和ep93xx
这两个驱动都是基于bitbang
实现的.
/* ep93xx的实现代码 */
/* 提供一个全局可见的结构体, 这个接口填写的内容都是模板化 */
struct jtag_interface ep93xx_interface = {
.name = "ep93xx",
.supported = DEBUG_CAP_TMS_SEQ,
.execute_queue = bitbang_execute_queue, /* bitbang 实现 JTAG命令解析的函数入口 */
.init = ep93xx_init,
.quit = ep93xx_quit,
};
/* 下面是三个需要用户实现的接口 */
static struct bitbang_interface ep93xx_bitbang = {
.read = ep93xx_read,
.write = ep93xx_write,
.reset = ep93xx_reset,
.blink = 0,
};
关于如何添加一个新驱动
当实现了一个驱动之后, 需要手工的将自己的驱动加入编译选项, openocd使用的是automake, 加入一个新驱动的编译选项位置比较多, 所以下面列出需要改动的地方, 便于参考.
下面以sysfsgpio
的配置文件作为蓝本进行介绍.
openocd/configure.ac
: 在项目根目录中加入对编译选项的支持
# ... snip ...
AC_ARG_ENABLE([sysfsgpio],
AS_HELP_STRING([--enable-sysfsgpio], [Enable building support for programming driven via sysfs gpios.]),
[build_sysfsgpio=$enableval], [build_sysfsgpio=no])
# ... snip ...
# 其中关于对bitbang的依赖就是通过 "build_bitbang" 这一行实现的
AS_IF([test "x$build_sysfsgpio" = "xyes"], [
build_bitbang=yes
AC_DEFINE([BUILD_SYSFSGPIO], [1], [1 if you want the SysfsGPIO driver.])
], [
AC_DEFINE([BUILD_SYSFSGPIO], [0], [0 if you don't want SysfsGPIO driver.])
])
# ... snip ...
AM_CONDITIONAL([SYSFSGPIO], [test "x$build_sysfsgpio" = "xyes"])
openocd/src/jtag/drivers/Makefile.am
: 在编译目录是加入对应的源文件
if SYSFSGPIO
DRIVERFILES += %D%/sysfsgpio.c
endif
openocd/src/jtag/interfaces.c
: 注册驱动
/* ... snip ... */
#if BUILD_SYSFSGPIO == 1
extern struct jtag_interface sysfsgpio_interface;
#endif
/* ... snip ... */
struct jtag_interface *jtag_interfaces[] = {
/* ... snip ... */
#if BUILD_SYSFSGPIO == 1
&sysfsgpio_interface,
#endif
/* ... snip ... */
}
/* ... snip ... */
关于bitbang的性能不足(可略)
在开发和测试之后发现, bitbang模式下的驱动可以使用, 但是在调用GDB命令时有十分明显的迟钝感. 所以如果想要提升速度, 偷懒的方法就不好用了, 需要自己全面实现一个驱动.
由上文可知, bitbang框架的命令解析函数是bitbang_execute_queue
.
下面是bitbang对于我所需要实现驱动之所以慢的原因.
int bitbang_execute_queue(void)
{
while (cmd) {
switch (cmd->type) {
case JTAG_SCAN:
bitbang_end_state(cmd->cmd.scan->end_state);
/* INFO: 每次都需要动态分配一段大空间 */
scan_size = jtag_build_buffer(cmd->cmd.scan, &buffer);
type = jtag_scan_type(cmd->cmd.scan);
if (bitbang_scan(cmd->cmd.scan->ir_scan, type, buffer,
scan_size) != ERROR_OK)
return ERROR_FAIL;
if (jtag_read_buffer(buffer, cmd->cmd.scan) != ERROR_OK)
retval = ERROR_JTAG_QUEUE_FAILED;
if (buffer)
free(buffer);
break;
/* ... snip ...*/
}
/* ... snip ...*/
}
static int bitbang_scan(bool ir_scan, enum scan_type type, uint8_t *buffer,
unsigned scan_size)
{
/* ... snip ...*/
size_t buffered = 0;
/* INFO: 每次读写都是单独执行的, usb的反应时间成为了我驱动的瓶颈 */
for (bit_cnt = 0; bit_cnt < scan_size; bit_cnt++) {
/* ... snip ...*/
if (bitbang_interface->write(0, tms, tdi) != ERROR_OK)
return ERROR_FAIL;
if (type != SCAN_OUT) {
if (bitbang_interface->buf_size) {
if (bitbang_interface->sample() != ERROR_OK)
return ERROR_FAIL;
buffered++;
} else {
switch (bitbang_interface->read()) {
case BB_LOW:
buffer[bytec] &= ~bcval;
break;
case BB_HIGH:
buffer[bytec] |= bcval;
break;
default:
return ERROR_FAIL;
}
}
}
if (bitbang_interface->write(1, tms, tdi) != ERROR_OK)
return ERROR_FAIL;
/* ... snip ...*/
}
/* ... snip ...*/
}
再探JTAG: JTAG的协议长什么样?
由于bitbang的实现的版本太慢了, 所以需要自己实现对于JTAG命令的解析. 在这种情况下, 如果不对协议有所了解, 那基本上是无法进行开发和调试的. 这个阶段我主要是先简单读了一下JTAG的协议, 其中fpga4fun中的一篇关于JTAG如何工作的文章很好的解释了我想关注的重点;
- 参考资料
- JTAG spec: IEEE 1149.1
- How JTAG Works // 强势推荐
- 课代表时间
JTAG写IR
下面来简要地介绍一下我从参考资料中捕获的要点:
- 当测试器(驱动程序)控制TMS时, 整个板子中所有连接上TMS的设备都同时进入同一种状态.
- JTAG有DR(data registers)和IR(instruction registers)两种寄存器, 通过对IR和DR进行配置实现与芯片内部JTAG控制器的交互.
- 每种设备的IR指令长度不定, DR寄存器对于不同指令可能长度也不一定.
- 可以把串联的多个设备的寄存器想象成一个queue. 当芯片处于读入或者读出模式时, 需要一次填入或者取出整个queue中的数据.
- 假如一个板子上的设备是静态的, 那JTAG的命令都是可以直接计算出来的, 即可以针对板子上JTAG的TDO和TDI的顺序直接控制对应的器件.
- BYPASS命令在IEEE中强制要求为全1. 通过往IR寄存器里面填入远大于queue深度的1, 就可以确保所有设备都处于BYPASS模式.
- BYPASS模式下, 所有的JTAG设备的DR的长都为1, 所以可以通过, 写入足量的0, 再写入足量的1, 就可以动态地探测出线路上有几个设备.
- IDCODE命令是可选的, 对于支持IDCODE命令的设备而言, reset状态之后IR的指令就是IDCODE. IDCODE指令下DR的长度都是固定的.
- IDCODE带有JTAG设备的信息, 即BYPASS+IDCODE两个命令合在一起可以实现自动探测班上JTAG设备.
再探openocd
关于jtag_command
其实通过上述的JTAG协议的学习, 我大致将JTAG命令的类型分为三类: 状态移动(TMS), 数据写入读出(TDI/TDO), 控制(RST).
其中数据的写入读出的命令长度可能十分的长(而这个是我关注的重点).
openocd中命令类型enum jtag_command_type
的定义如下:
enum jtag_command_type {
JTAG_SCAN = 1,
JTAG_TLR_RESET = 2,
JTAG_RUNTEST = 3,
JTAG_RESET = 4,
JTAG_PATHMOVE = 6,
JTAG_SLEEP = 7,
JTAG_STABLECLOCKS = 8,
JTAG_TMS = 9,
};
其中 JTAG_SCAN
负责数据的写入和读出, 这是我程序改进的关键点. 具体的内容和业务相关度比较高, 这篇笔记内容就不仔细介绍了.
关于GDB到openocd到target的内容
openocd/src/target
目录下的目标需要实现struct target_type
, 向openocd中注册了一簇调试用的方法, 让用户可以通过GDB或者tcl进行调试目标.
这些方法在底层调用了openocd/src/jtag/core.c
函数中的方法(例如jtag_add_ir_scan
), 实现了对驱动的控制.
struct target_type riscv_target = {
.name = "riscv",
.init_target = riscv_init_target,
.deinit_target = riscv_deinit_target,
.examine = riscv_examine,
/* poll current target status */
.poll = old_or_new_riscv_poll,
.halt = old_or_new_riscv_halt,
.resume = old_or_new_riscv_resume,
.step = old_or_new_riscv_step,
.assert_reset = riscv_assert_reset,
.deassert_reset = riscv_deassert_reset,
.read_memory = riscv_read_memory,
.write_memory = riscv_write_memory,
.checksum_memory = riscv_checksum_memory,
.get_gdb_reg_list = riscv_get_gdb_reg_list,
.add_breakpoint = riscv_add_breakpoint,
.remove_breakpoint = riscv_remove_breakpoint,
.add_watchpoint = riscv_add_watchpoint,
.remove_watchpoint = riscv_remove_watchpoint,
.hit_watchpoint = riscv_hit_watchpoint,
.arch_state = riscv_arch_state,
.run_algorithm = riscv_run_algorithm,
.commands = riscv_command_handlers
};
参考资料汇总
[1] boundary scan
[2] JTAG概述 youtube
[3] openocd 官方文档
[4] JTAG spec: IEEE 1149.1
[5] How JTAG Works
网友评论