美文网首页
关于openocd JTAG driver开发的学习笔记

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

作者: Mum_Chen | 来源:发表于2019-07-31 17:30 被阅读0次

    写在开始

    事情开始于公司需要对芯片定制一个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

    相关文章

      网友评论

          本文标题:关于openocd JTAG driver开发的学习笔记

          本文链接:https://www.haomeiwen.com/subject/iolzrctx.html