关于openocd JTAG driver开发的学习笔记

写在开始

事情开始于公司需要对芯片定制一个openocd driver. 然后我开始了为期两周的JTAG学习之旅.

前提描述

具体的协议内容不是这篇笔记的重点, 就不仔细介绍了. 我把需要的一些要点单独拎出来描述一下.

  • 这次的实际物理器件是ftdi232h
  • (tms, tdi, tck)或者(tdo)都需要一条单独的指令, 每条指令的长度大约为80个比特.

关于这篇笔记的结构

这篇笔记主要是以时间发展顺序的角度进行描述的, 笔记内不介绍特别深入的内容, 主要以导读为主, 记录我理解的要点.
因为工作时每一段时间都有一个工作重心, 所以我在整理笔记时根据当时的工作重心取了不同的副标题.

在这次的开发经历了两个阶段:
最初, 我是基于bitbang_interface实现了read, write, reset三个方法. 这种方式的实现胜在简单, 不需要了解特别多的JTAG协议细节.
后来, 由于使用bitbang效率比较低, 所以需要我针对驱动协议实现了一个特定版本的驱动代码. 这个版本要求对JTAG协议的细节有一定的理解, 不然在开发过程中无法进行常规的debug工作.

第一个阶段对应初探JTAG初探openocd.
第二个阶段对应再探JTAG再探openocd.

一些额外的业务分析(可以忽略)

这次协议慢的原因有两个:

  1. 驱动协议效率本身比较低
    简单描述一下这次所需要的驱动协议的效率.
    假如是一个直接支持ftdi mpsse实现JTAG的: 它的一次tdi/tdo都只需要填充一个单独的bit(mpsse会直接进行tck和tdo的处理).
    而对于我所需要实现的协议而言, 一次发数据的操作需要一次tck的翻转(两个写命令), 一次读数据的操作需要额外加入一次读操作(即为写,读,写三次命令)
    所以对于不同的请求, 我协议本身的损耗是ftdi的160倍(写操作)和240倍(读操作).

  2. usb请求导致的效率低下
    虽然协议本身就很慢, 但是真正拖慢运行效率的实际上是bitbang的接口本身.
    使用bitbang慢的原因和协议本身慢的原因不太一样. 这个瓶颈发生在每一次bitbang的读写操作都需要发起一次新的usb请求, 然后等待响应.
    mpsse支持很大批量的连续读写操作, 这是为什么我可以弃用bitbang而自己实现的原因.

如果对于ftdi或者mpsse有兴趣的可以查询ftdi的官网

初探JTAG: JTAG是什么?

在以前的开发过程中, 用过JTAG, 但是我并不知道JTAG具体是什么. 所以在任务开始的第一个阶段我主要是搜罗资料并建立对JTAG的直观印象.

  1. 参考资料
    下面是我在youtube(不可描述的)网站上搜到视频资料, 视频主要是对JTAG进行了一些导读. 视频的重点主要是关于JTAG的起源与边界扫描.

  2. 课代表时间

  • 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的数据输入和输出都是有延迟的, 延迟取决于整个系统中所有寄存器数目的总和.
JTAG Boundary Scan

边界检查的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版本中, sysfsgpioep93xx这两个驱动都是基于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如何工作的文章很好的解释了我想关注的重点;

  1. 参考资料
  1. 课代表时间
JTAG状态机

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

推荐阅读更多精彩内容