寫在開(kāi)始
事情開(kāi)始于公司需要對(duì)芯片定制一個(gè)openocd driver. 然后我開(kāi)始了為期兩周的JTAG學(xué)習(xí)之旅.
前提描述
具體的協(xié)議內(nèi)容不是這篇筆記的重點(diǎn), 就不仔細(xì)介紹了. 我把需要的一些要點(diǎn)單獨(dú)拎出來(lái)描述一下.
- 這次的實(shí)際物理器件是
ftdi232h
- 寫(tms, tdi, tck)或者讀(tdo)都需要一條單獨(dú)的指令, 每條指令的長(zhǎng)度大約為80個(gè)比特.
關(guān)于這篇筆記的結(jié)構(gòu)
這篇筆記主要是以時(shí)間發(fā)展順序的角度進(jìn)行描述的, 筆記內(nèi)不介紹特別深入的內(nèi)容, 主要以導(dǎo)讀為主, 記錄我理解的要點(diǎn).
因?yàn)楣ぷ鲿r(shí)每一段時(shí)間都有一個(gè)工作重心, 所以我在整理筆記時(shí)根據(jù)當(dāng)時(shí)的工作重心取了不同的副標(biāo)題.
在這次的開(kāi)發(fā)經(jīng)歷了兩個(gè)階段:
最初, 我是基于bitbang_interface實(shí)現(xiàn)了read
, write
, reset
三個(gè)方法. 這種方式的實(shí)現(xiàn)勝在簡(jiǎn)單, 不需要了解特別多的JTAG協(xié)議細(xì)節(jié).
后來(lái), 由于使用bitbang效率比較低, 所以需要我針對(duì)驅(qū)動(dòng)協(xié)議實(shí)現(xiàn)了一個(gè)特定版本的驅(qū)動(dòng)代碼. 這個(gè)版本要求對(duì)JTAG協(xié)議的細(xì)節(jié)有一定的理解, 不然在開(kāi)發(fā)過(guò)程中無(wú)法進(jìn)行常規(guī)的debug工作.
第一個(gè)階段對(duì)應(yīng)初探JTAG
和初探openocd
.
第二個(gè)階段對(duì)應(yīng)再探JTAG
和再探openocd
.
一些額外的業(yè)務(wù)分析(可以忽略)
這次協(xié)議慢的原因有兩個(gè):
驅(qū)動(dòng)協(xié)議效率本身比較低
簡(jiǎn)單描述一下這次所需要的驅(qū)動(dòng)協(xié)議的效率.
假如是一個(gè)直接支持ftdi mpsse
實(shí)現(xiàn)JTAG的: 它的一次tdi/tdo都只需要填充一個(gè)單獨(dú)的bit(mpsse
會(huì)直接進(jìn)行tck和tdo的處理).
而對(duì)于我所需要實(shí)現(xiàn)的協(xié)議而言, 一次發(fā)數(shù)據(jù)的操作需要一次tck的翻轉(zhuǎn)(兩個(gè)寫命令), 一次讀數(shù)據(jù)的操作需要額外加入一次讀操作(即為寫,讀,寫三次命令)
所以對(duì)于不同的請(qǐng)求, 我協(xié)議本身的損耗是ftdi的160倍(寫操作)和240倍(讀操作).usb請(qǐng)求導(dǎo)致的效率低下
雖然協(xié)議本身就很慢, 但是真正拖慢運(yùn)行效率的實(shí)際上是bitbang的接口本身.
使用bitbang慢的原因和協(xié)議本身慢的原因不太一樣. 這個(gè)瓶頸發(fā)生在每一次bitbang的讀寫操作都需要發(fā)起一次新的usb請(qǐng)求, 然后等待響應(yīng).
mpsse支持很大批量的連續(xù)讀寫操作, 這是為什么我可以棄用bitbang而自己實(shí)現(xiàn)的原因.
如果對(duì)于ftdi
或者mpsse
有興趣的可以查詢ftdi的官網(wǎng)
初探JTAG: JTAG是什么?
在以前的開(kāi)發(fā)過(guò)程中, 用過(guò)JTAG, 但是我并不知道JTAG具體是什么. 所以在任務(wù)開(kāi)始的第一個(gè)階段我主要是搜羅資料并建立對(duì)JTAG的直觀印象.
-
參考資料
下面是我在youtube(不可描述的)網(wǎng)站上搜到視頻資料, 視頻主要是對(duì)JTAG進(jìn)行了一些導(dǎo)讀. 視頻的重點(diǎn)主要是關(guān)于JTAG的起源與邊界掃描. 課代表時(shí)間
- JTAG主要起源于PCB板級(jí)檢查時(shí)候的boundary scan.
- 整個(gè)系統(tǒng)的有并行連接的TMS(test mode select) 和 TCK(test clock), 以及串行連接的TDI(test data output), TDO(test data input)組成.
- JTAG可以直接控制和掃描芯片周圍的pin.
- 通過(guò)JTAG控制器, 還可以與芯片內(nèi)部的設(shè)備進(jìn)行直接通訊.(這就是我們常用gdb over JTAG的實(shí)現(xiàn)方法).
- JTAG的數(shù)據(jù)輸入和輸出都是有延遲的, 延遲取決于整個(gè)系統(tǒng)中所有寄存器數(shù)目的總和.
在上面的截圖中可以看到, 有些JTAG的連接器可以通過(guò)GUI直接將掃描的結(jié)果顯示出來(lái).
例如: 在某個(gè)板子上也許兩個(gè)設(shè)備的pin直接接入了一個(gè)反向器, 如果控制某個(gè)pin輸入/輸出時(shí), 則可以檢查另一個(gè)pin的狀態(tài)是否正確.
初探openocd: openocd是什么? 怎么實(shí)現(xiàn)一個(gè)驅(qū)動(dòng)?
openocd的全稱是Open On-Chip Debugger.
因?yàn)樾枰槍?duì)openocd寫一個(gè)驅(qū)動(dòng), 所以我關(guān)注的重點(diǎn)主要落在了"我要怎么實(shí)現(xiàn)一個(gè)驅(qū)動(dòng)"上.
在查閱了openocd 官方文檔之后, 我大致上對(duì)openocd的使用和開(kāi)發(fā)架構(gòu)有了基本的了解, 大致上知道在哪里找代碼了.
整個(gè)openocd的使用層級(jí)分為三級(jí):
- interface: 實(shí)現(xiàn)JTAG協(xié)議的內(nèi)容, 這也是我所需要關(guān)心的重點(diǎn).
- board: 根據(jù)特別的開(kāi)發(fā)板子, 同一個(gè)接口可能也有不同的配置選項(xiàng).
- target: 芯片支持的特定的JTAG指令, 這些一般是芯片廠商提供. 我這次的任務(wù)的對(duì)象是一個(gè)現(xiàn)有的cpu, 所以這一層我不需要關(guān)心.
簡(jiǎn)述bitbang
bitbang接口
的實(shí)現(xiàn)在openocd/src/jtag/drivers/bitbang.c
里面, 對(duì)于使用這個(gè)"框架"的用戶而言, 只需要提供三個(gè)方法, 就可以實(shí)現(xiàn)一個(gè)簡(jiǎn)單的driver.
在我clone下來(lái)的openocd版本中, sysfsgpio
和ep93xx
這兩個(gè)驅(qū)動(dòng)都是基于bitbang
實(shí)現(xiàn)的.
/* ep93xx的實(shí)現(xiàn)代碼 */
/* 提供一個(gè)全局可見(jiàn)的結(jié)構(gòu)體, 這個(gè)接口填寫的內(nèi)容都是模板化 */
struct jtag_interface ep93xx_interface = {
.name = "ep93xx",
.supported = DEBUG_CAP_TMS_SEQ,
.execute_queue = bitbang_execute_queue, /* bitbang 實(shí)現(xiàn) JTAG命令解析的函數(shù)入口 */
.init = ep93xx_init,
.quit = ep93xx_quit,
};
/* 下面是三個(gè)需要用戶實(shí)現(xiàn)的接口 */
static struct bitbang_interface ep93xx_bitbang = {
.read = ep93xx_read,
.write = ep93xx_write,
.reset = ep93xx_reset,
.blink = 0,
};
關(guān)于如何添加一個(gè)新驅(qū)動(dòng)
當(dāng)實(shí)現(xiàn)了一個(gè)驅(qū)動(dòng)之后, 需要手工的將自己的驅(qū)動(dòng)加入編譯選項(xiàng), openocd使用的是automake, 加入一個(gè)新驅(qū)動(dòng)的編譯選項(xiàng)位置比較多, 所以下面列出需要改動(dòng)的地方, 便于參考.
下面以sysfsgpio
的配置文件作為藍(lán)本進(jìn)行介紹.
openocd/configure.ac
: 在項(xiàng)目根目錄中加入對(duì)編譯選項(xiàng)的支持
# ... 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 ...
# 其中關(guān)于對(duì)bitbang的依賴就是通過(guò) "build_bitbang" 這一行實(shí)現(xiàn)的
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
: 在編譯目錄是加入對(duì)應(yīng)的源文件
if SYSFSGPIO
DRIVERFILES += %D%/sysfsgpio.c
endif
openocd/src/jtag/interfaces.c
: 注冊(cè)驅(qū)動(dòng)
/* ... 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 ... */
關(guān)于bitbang的性能不足(可略)
在開(kāi)發(fā)和測(cè)試之后發(fā)現(xiàn), bitbang模式下的驅(qū)動(dòng)可以使用, 但是在調(diào)用GDB命令時(shí)有十分明顯的遲鈍感. 所以如果想要提升速度, 偷懶的方法就不好用了, 需要自己全面實(shí)現(xiàn)一個(gè)驅(qū)動(dòng).
由上文可知, bitbang框架的命令解析函數(shù)是bitbang_execute_queue
.
下面是bitbang對(duì)于我所需要實(shí)現(xiàn)驅(qū)動(dòng)之所以慢的原因.
int bitbang_execute_queue(void)
{
while (cmd) {
switch (cmd->type) {
case JTAG_SCAN:
bitbang_end_state(cmd->cmd.scan->end_state);
/* INFO: 每次都需要?jiǎng)討B(tài)分配一段大空間 */
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: 每次讀寫都是單獨(dú)執(zhí)行的, usb的反應(yīng)時(shí)間成為了我驅(qū)動(dòng)的瓶頸 */
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的協(xié)議長(zhǎng)什么樣?
由于bitbang的實(shí)現(xiàn)的版本太慢了, 所以需要自己實(shí)現(xiàn)對(duì)于JTAG命令的解析. 在這種情況下, 如果不對(duì)協(xié)議有所了解, 那基本上是無(wú)法進(jìn)行開(kāi)發(fā)和調(diào)試的. 這個(gè)階段我主要是先簡(jiǎn)單讀了一下JTAG的協(xié)議, 其中fpga4fun中的一篇關(guān)于JTAG如何工作的文章很好的解釋了我想關(guān)注的重點(diǎn);
- 參考資料
- JTAG spec: IEEE 1149.1
- How JTAG Works // 強(qiáng)勢(shì)推薦
- 課代表時(shí)間
下面來(lái)簡(jiǎn)要地介紹一下我從參考資料中捕獲的要點(diǎn):
- 當(dāng)測(cè)試器(驅(qū)動(dòng)程序)控制TMS時(shí), 整個(gè)板子中所有連接上TMS的設(shè)備都同時(shí)進(jìn)入同一種狀態(tài).
- JTAG有DR(data registers)和IR(instruction registers)兩種寄存器, 通過(guò)對(duì)IR和DR進(jìn)行配置實(shí)現(xiàn)與芯片內(nèi)部JTAG控制器的交互.
- 每種設(shè)備的IR指令長(zhǎng)度不定, DR寄存器對(duì)于不同指令可能長(zhǎng)度也不一定.
- 可以把串聯(lián)的多個(gè)設(shè)備的寄存器想象成一個(gè)queue. 當(dāng)芯片處于讀入或者讀出模式時(shí), 需要一次填入或者取出整個(gè)queue中的數(shù)據(jù).
- 假如一個(gè)板子上的設(shè)備是靜態(tài)的, 那JTAG的命令都是可以直接計(jì)算出來(lái)的, 即可以針對(duì)板子上JTAG的TDO和TDI的順序直接控制對(duì)應(yīng)的器件.
- BYPASS命令在IEEE中強(qiáng)制要求為全1. 通過(guò)往IR寄存器里面填入遠(yuǎn)大于queue深度的1, 就可以確保所有設(shè)備都處于BYPASS模式.
- BYPASS模式下, 所有的JTAG設(shè)備的DR的長(zhǎng)都為1, 所以可以通過(guò), 寫入足量的0, 再寫入足量的1, 就可以動(dòng)態(tài)地探測(cè)出線路上有幾個(gè)設(shè)備.
- IDCODE命令是可選的, 對(duì)于支持IDCODE命令的設(shè)備而言, reset狀態(tài)之后IR的指令就是IDCODE. IDCODE指令下DR的長(zhǎng)度都是固定的.
- IDCODE帶有JTAG設(shè)備的信息, 即BYPASS+IDCODE兩個(gè)命令合在一起可以實(shí)現(xiàn)自動(dòng)探測(cè)班上JTAG設(shè)備.
再探openocd
關(guān)于jtag_command
其實(shí)通過(guò)上述的JTAG協(xié)議的學(xué)習(xí), 我大致將JTAG命令的類型分為三類: 狀態(tài)移動(dòng)(TMS), 數(shù)據(jù)寫入讀出(TDI/TDO), 控制(RST).
其中數(shù)據(jù)的寫入讀出的命令長(zhǎng)度可能十分的長(zhǎng)(而這個(gè)是我關(guān)注的重點(diǎn)).
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
負(fù)責(zé)數(shù)據(jù)的寫入和讀出, 這是我程序改進(jìn)的關(guān)鍵點(diǎn). 具體的內(nèi)容和業(yè)務(wù)相關(guān)度比較高, 這篇筆記內(nèi)容就不仔細(xì)介紹了.
關(guān)于GDB到openocd到target的內(nèi)容
openocd/src/target
目錄下的目標(biāo)需要實(shí)現(xiàn)struct target_type
, 向openocd中注冊(cè)了一簇調(diào)試用的方法, 讓用戶可以通過(guò)GDB或者tcl進(jìn)行調(diào)試目標(biāo).
這些方法在底層調(diào)用了openocd/src/jtag/core.c
函數(shù)中的方法(例如jtag_add_ir_scan
), 實(shí)現(xiàn)了對(duì)驅(qū)動(dòng)的控制.
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