22

硬件学习之通过树莓派操控 jtag

 4 years ago
source link: https://www.tuicool.com/articles/Yfmqe27
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

作者:Hcamael@知道创宇404实验室
时间:2019年10月21日

最近在搞路由器的时候,不小心把CFE给刷挂了,然后发现能通过jtag进行救砖,所以就对jtag进行了一波研究。

最开始只是想救砖,并没有想深入研究的想法。

救砖尝试

变砖的路由器型号为:LinkSys wrt54g v8

CPU 型号为:BCM5354

Flash型号为:K8D6316UBM

首先通过jtagulator得到了设备上jtag接口的顺序。

正好公司有一个jlink,但是参试了一波失败,识别不了设备。

随后通过Google搜到发现了一个工具叫: tjtag-pi

可以通树莓派来控制jtag,随后学习了一波树莓派的操作。

树莓派Pins

我使用的是rpi3,其接口编号图如下:

2qIvMzr.png!web

或者在树莓派3中可以使用 gpio readall 查看各个接口的状态:

nyUzA3E.png!web

rpi3中的Python有一个 RPi.GPIO 模块,可以控制这些接口。

举个例子:

>>> from RPi import GPIO
>>> GPIO.setmode(GPIO.BCM)
>>> GPIO.setup(2, GPIO.OUT)
>>> GPIO.setup(3, GPIO.IN)

首先是需要进行初始化GPIO的模式,BCM模式对应的针脚排序是上面图中橙色的部门。

然后可以对各个针脚进行单独设置,比如上图中,把2号针脚设置为输出,3号针脚设置为输入。

>>> GPIO.output(2, 1)
>>> GPIO.output(2, 0)

使用output函数进行二进制输出

>>> GPIO.input(3)
1

使用input函数获取针脚的输入。

我们可以用线把两个针脚连起来测试上面的代码。

将树莓派对应针脚和路由器的连起来以后,可以运行tjtag-pi程序。但是在运行的过程中却遇到了问题,经常会卡在写flash的时候。通过调整配置,有时是可以写成功的,但是CFE并没有被救回来,备份flash的数据,发现并没有成功写入数据。

因为使用轮子失败,所以我只能自己尝试研究和造轮子了。

jtag

首先是针脚,我见过的设备给jtag一般是提供了5 * 2以上的引脚。其中有一般都是接地引脚,另一半只要知道4个最重要的引脚。

这四个引脚一般情况下的排序是:

TDI
TDO
TMS
TCK

TDI表示输入,TDO表示输出,TMS控制位,TCK时钟输入。

Eji6J3f.png!web

jtag大致架构如上图所示,其中TAP-Controller的架构如下图所示:

qABnauV.png!web

根据上面这两个架构,对jtag的原理进行讲解。

jtag的核心是TAP-Controller,通过解析TMS数据,来决定输入和输出的关系。所以我们先来看看TAP-Controller的架构。

从上面的图中我们可以发现,在任何状态下,输出5次1,都会回到 TEST LOGIC RESET 状态下。所以在使用jtag前,我们先通过TMS端口,发送5次为1的数据,jtag的状态机将会进入到RESET的复原状态。

当TAP进入到 SHIFT-IR 的状态时, Instruction Register 将会开始接收TDI传入的数据,当输入结束后,进入到 UPDATE-IR 状态时将会解析指令寄存器的值,随后决定输出什么数据。

SHIFT-DR 则是控制数据寄存器,一般是在读写数据的时候需要使用。

讲到这里,就出现一个问题了,TMS就一个端口,jtag如何知道TMS每次输入的值是多少呢?这个时候就需要用到TCK端口了,该端口可以称为时钟指令。当TCK从低频变到高频时,获取一比特TMS/TDI输入,TDO输出1比特。

比如我们让TAP进行一次复位操作:

for x in range(5):
    TCK 0
    TMS 1
    TCK 1

再比如,我们需要给指令寄存器传入0b10:

1.复位

2.进入RUN-TEST/IDLE状态

TCK 0
TMS 0
TCK 1

3.进入SELECT-DR-SCAN状态

TCK 0
TMS 1
TCK 1

4.进入SELECT-IR-SCAN状态

TCK 0
TMS 1
TCK 1

5.进入CAPTURE-IR状态

TCK 0
TMS 0
TCK 1

6.进入SHIFT-IR状态

TCK 0
TMS 0 
TCK 1

7.输入0b10

TCK 0
TMS 0
TDI 0
TCK 1
TCK 0
TMS 1
TDI 1
TCK 0

随后就是进入 EXIT-IR -> UPDATE-IR

根据上面的理论我们就可以通过写一个设置IR的函数:

def clock(tms, tdi):
    tms = 1 if tms else 0
    tdi = 1 if tdi else 0
    GPIO.output(TCK, 0)
    GPIO.output(TMS, tms)
    GPIO.output(TDI, tdi)
    GPIO.output(TCK, 1)
    return GPIO.input(TDO)
def reset():
    clock(1, 0)
    clock(1, 0)
    clock(1, 0)
    clock(1, 0)
    clock(1, 0)
    clock(0, 0)
def set_instr(instr):
    clock(1, 0)  
    clock(1, 0)
    clock(0, 0)
    clock(0, 0)
    for i in range(INSTR_LENGTH):
        clock(i==(INSTR_LENGTH - 1), (instr>>i)&1)
    clock(1, 0)
    clock(0, 0)

把上面的代码理解清楚后,基本就理解了TAP的逻辑。接下来就是指令的问题了,指令寄存器的长度是多少?指令寄存器的值为多少时是有意义的?

不同的CPU对于上面的答案都不一样,通过我在网上搜索的结果,每个CPU应该都有一个bsd(boundary scan description)文件。本篇文章研究的CPU型号是 BCM5354 ,但是我并没有在网上找到该型号CPU的bsd文件。我只能找了一个相同厂商不同型号的CPU的bsd文件进行参考。

bcm53101m.bsd

在该文件中我们能看到jtag端口在cpu端口的位置:

"tck              : B46  , " &
"tdi              : A57  , " &
"tdo              : B47  , " &
"tms              : A58  , " &
"trst_b           : A59  , " &


attribute TAP_SCAN_RESET of trst_b                   : signal is true;
attribute TAP_SCAN_IN    of tdi                      : signal is true;
attribute TAP_SCAN_MODE  of tms                      : signal is true;
attribute TAP_SCAN_OUT   of tdo                      : signal is true;
attribute TAP_SCAN_CLOCK of tck                      : signal is (2.5000000000000000000e+07, BOTH);

能找到指令长度的定义:

attribute INSTRUCTION_LENGTH of top: entity is 32;

能找到指令寄存器的有效值:

attribute INSTRUCTION_OPCODE of top: entity is
  "IDCODE       (11111111111111111111111111111110)," &
  "BYPASS       (00000000000000000000000000000000, 11111111111111111111111111111111)," &
  "EXTEST       (11111111111111111111111111101000)," &
  "SAMPLE       (11111111111111111111111111111000)," &
  "PRELOAD      (11111111111111111111111111111000)," &
  "HIGHZ        (11111111111111111111111111001111)," &
  "CLAMP        (11111111111111111111111111101111) " ;

当指令寄存器的值为 IDCODE 的时候,IDCODE寄存器的输出通道开启,我们来看看IDCODE寄存器:

attribute IDCODE_REGISTER of top: entity is
  "0000"             & -- version
  "0000000011011111" & -- part number
  "00101111111"      & -- manufacturer's identity
  "1";                   -- required by 1149.1

从这里我们能看出IDCODE寄存器的固定输出为: 0b00000000000011011111001011111111

那我们怎么获取TDO的输出呢?这个时候数据寄存器DR就发挥作用了。

  1. TAP状态机切换到SHIFT-IR
  2. 输出IDCODE到IR中
  3. 切换到SHIFT-DR
  4. 获取INSTRUCTION_LENGTH长度的TDO输出值
  5. 退出

用代码形式的表示如下:

def ReadWriteData(data):
    out_data = 0
    clock(1, 0)
    clock(0, 0)
    clock(0, 0)
    for i in range(32):            
        out_bit  = clock((i == 31), ((data >> i) & 1))
        out_data = out_data | (out_bit << i)
    clock(1,0)
    clock(0,0)
    return out_data
def ReadData():
    return ReadWriteData(0)
def WriteData(data):
    ReadWriteData(data)
def idcode():
    set_instr(INSTR_IDCODE)
    print(hex(self.ReadData()))

因为我也是个初学者,边界扫描描述文件中的内容并不是都能看得懂,比如在边界扫描文件中并不能看出BYPASS指令是做什么的。但是在其他文档中,得知BYPASS寄存器一般是用来做测试的,在该寄存器中,输入和输出是直连,可以通过比较输入和输出的值,来判断端口是否连接正确。

另外还有边界扫描寄存器一大堆数据,也没完全研究透,相关的资料少的可怜。而且也找不到对应CPU的文档。

当研究到这里的时候,我只了解了jtag的基本原理,只会使用两个基本的指令(IDCODE, BYPASS)。但是对我修砖没任何帮助。

没办法,我又回头来看tjtag的源码,在tjtag中定义了几个指令寄存器的OPCODE:

INSTR_ADDRESS = 0x08
INSTR_DATA    = 0x09
INSTR_CONTROL = 0x0A

照抄着tjtag中flash AMD的操作,可以成功对flash进行擦除,写入操作读取操作。但是却不知其原理。

这里分享下我的脚本:jtag.py

flash文档: https://www.dataman.com/media/datasheet/Samsung/K8D6x16UTM_K8D6x16UBM_rev16.pdf

接下来将会对该flash文档进行研究,并在之后的文章中分享我后续的研究成果。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK