橫看成嶺側成峰,遠近高低各不同。 不識廬山真面目,隻緣身在此山中。 —— 蘇轼《題西林壁》
研究RISC-V系統的debug有很多角度、很多内容,涉及很多軟硬體工具如GDB、OpenOCD、adapter,晶片裡的JTAG、DM子產品、處理器的支援以及RISC-V external debug協定等等。如果單陷入某個方面,則如盲人摸象,很難了解全局的運作原理,我們有必要跳到系統外面來,捋一捋整個流程到底是怎麼運轉起來的。值得一提的是,在沒有晶片、沒有開發闆的情況下,我們可以通過仿真的方式(各種仿真工具包括verilator)來接入GDB。
1. RISC-V Debug系統元件
在了解Verilated RISC-V系統如何debug之前,我們先來看一下真實晶片是如何debug的。假設我們使用PC上的Linux裡的OpenOCD和GDB對一顆RISC-V SoC晶片進行debug,這顆RISC-V晶片在一塊開發闆上,開發闆和PC之間使用某型号的下載下傳器,這個下載下傳器一端連接配接開發闆上連出的JTAG端口,一端連接配接PC上的USB接口。我們來看看這其中的每個部件都負責什麼工作。GDB、OpenOCD、下載下傳器(adapter)和target端的連接配接關系可以參考下圖。
- RISC-V SoC晶片:裡面有RISC-V core,和SoC中的debug module相連,這個debug module對内控制RISC-V core、讀取寄存器、通路memory,對外和JTAG控制器連接配接,JTAG控制器的接口連到晶片的PAD。
- Adapter:負責協定轉換:把USB的JTAG控制資訊按JTAG協定轉換輸出,滿足協定定義的電氣特性
- Linux系統:負責提供USB驅動,由OpenOCD所調用。
- OpenOCD:負責把GDB的進階别指令轉換成JTAG指令,并通過特定下載下傳器的要求進行打包,準備調用OS提供的USB驅動由USB發送出去。GDB和OpenOCD之間使用TCP協定進行連接配接。
- GDB:這個GDB并非是Linux系統下調試host系統(可能是x86,可能是ARM或者其他)的GDB,而是交叉編譯工具提供的調試非host系統的RISC-V裝置的GDB。順便提一句,telnet也可以用來連接配接OpenOCD,不過既然是簡單介紹,就提GDB一個好了。
假設GDB準備把一段risc-v的代碼Load到開發闆上的risc-v soc上去執行。GDB通過TCP連到OpenOCD的GDB server。OpenOCD這邊收到指令後進行解析,根據target類型、adapter型号、使用的協定等等翻譯成符合target debug協定的指令,調用底層的驅動(USB之類)發送出去。USB adapter可能還要再轉成JTAG格式傳給闆子上的裝置。
傳到闆子上的risc-v裝置後,晶片裡的幾個夥計要忙活了。對于符合SIFIVE的debug spec的裝置來講,分成DTM、DMI和DM幾個部分。這裡先祭上事實上的RISC-V标準中的external debug連接配接關系圖,這張圖資訊量很大。裡面的每個部分負責什麼工作?如何實作呢?這裡的細節不細說了,又可以單獨成文了,感興趣的同學可以自行研讀spec。
最終,DM(Debug module)依靠前面各位朋友的幫助把抽象的GDB指令翻譯成一個個的訪存、執行或者讀CPU寄存器的操作,拿到資料後原路傳回。
2. 把GDB接入仿真
還是再來看一下這張block diagram。
test 在仿真時risc-v platform裡面的實際都是仿真器在解釋RTL代碼,PC部分也是和仿真器在一個系統裡運作(比如伺服器或者虛拟機裡的Linux),我們留意到這張圖裡adapter本來是個硬體部分,我們隻要把它做成個model便可以在仿真裡把PC和RISC-V platform連接配接起來了。當然了這時候我們不用USB接口了,直接使用作業系統的TCP服務,至于OpenOCD的運作在TCP上的debug協定我們使用簡單的 remote bitbang協定。順便提一下,如果DTM部分RTL沒有也是可以的,直接做在這個model裡就可以,直接drive DMI,PULP平台就是這麼做的。值得一提的是,用GDB調試Spike上跑的程式也是這個套路,即使Spike是個純C的model,連RTL也不是。
至于仿真器是什麼并不重要,隻要支援DPI即可,畢竟這個model除了接口之外我們是準備用C寫的。當然了,Ibex的PULP平台裡有現成的實作,除了接口部分需要适配一下,其他的直接可以拿過來用了。感謝LowRISC的開發者。OpenTitan項目順理成章也是使用PULP平台上的這套實作機制。它的RISC-V platform這部分的microarchitecture如圖。
就拿OpenTitan作為例子,用verilator仿真的時候可以看到下面的列印資訊:
$build/lowrisc_systems_top_earlgrey_verilator_0.1/sim-verilator/Vtop_earlgrey_verilator --meminit=rom,build-bin/sw/device/boot_rom/boot_rom_sim_verilator.elf --meminit=flash,build-bin/sw/device/examples/hello_world/hello_world_sim_verilator.elf
Simulation of OpenTitan Earl Grey =================================
JTAG: Virtual JTAG interface dmi0 is listening on port 44853. Use OpenOCD and the following configuration to connect: interface remote_bitbang remote_bitbang_host localhost remote_bitbang_port 44853
這是adapter+DTM model裡的DPI調用TCP server在44853端口上開了個服務,等待host的到來。
這時候我們另起個中斷運作OpenOCD,當然要選verilator适配的config。
$ /tools/openocd/bin/openocd -s util/openocd -f board/lowrisc-earlgrey-verilator.cfg
Info : Listening on port 6666 for tcl connections
Info : Listening on port 4444 for telnet connections
Info : Initializing remote_bitbang driver
Info : Connecting to localhost:44853
Info : remote_bitbang driver initialized
Info : This adapter doesn't support configurable speed
Info : JTAG tap: riscv.tap tap/device found: 0x04f5484d (mfg: 0x426 (Google Inc), part: 0x4f54, ver: 0x0)
Info : datacount=2 progbufsize=8
Info : Examined RISC-V core; found 1 harts
Info : hart 0: XLEN=32, misa=0x40101104
Info : Listening on port 3333 for gdb connections
我們可以看到OpenOCD順利連上了44853端口,和仿真中的DMI接洽上了。它自己監聽3333端口看看有否GDB連接配接的請求。要是我們在另一個終端打開GDB:
$ riscv32-unknown-elf-gdb -ex "target extended-remote :3333" -ex "info reg" build-bin/sw/device/examples/hello_world/hello_world_sim_verilator.elf
這時候OpenOCD這邊會顯示:
Info : accepting 'gdb' connection on tcp/3333
GDB那邊的輸出:
Reading symbols from build-bin/sw/device/examples/hello_world/hello_world_sim_verilator.elf...done. Remote debugging using :3333
我們來看一眼OpenOCD把DM啟動起來的瞬間是啥樣:
我們看到DMI上先向0x40(
dmi_req_addr
=0x10)寫了0x0來reset,随即寫了0x1把DM啟動起來,接着還是往0x40寫了0x7FFFFC1然後随即讀這個位址看看它到底實作了哪些寄存器位。我們雖然從waveform看不出OpenOCD到底發了什麼過來,但是我們可以知道它想讓DM做什麼事情。
OK,下面就是使用GDB debug的時間了,略去不表。比如我們想看看0x2000034a位址的值:
(gdb) x 0x2000034a 0x2000034a <ibex_mcycle_read+12>: 0xb80027f3
當然,跳過OpenOCD理論上也是可行的,我們可以直接解析GDB的指令,并轉換成DMI上的請求。理論上這種方式還可以提高仿真速度,但使用OpenOCD更貼近實際硬體運作狀況,并且OpenOCD已經幫我們做了很多指令解析的工作,大大減少了開發的負擔。
3. 關鍵代碼解析
毫無疑問,GDB或者說OpenOCD接入仿真最關鍵的部分是adapter和DTM的model,OpenOCD裡面使用PULP的solution,使用DPI C程式來模拟jtag到DMI的資料傳輸。看一下子產品間的連接配接:
dmidpi
表面上隻有和DM之間的DMI接口,實則還有和OpenOCD之間的TCP資料交換。這是因為
dmidpi
裡面開了一個TCP server。在
dmidpi.sv
裡面
initial
時便調用DPI建立TCP server,仿真結束時關掉:
initial begin
ctx = dmidpi_create(Name, ListenPort);
end
final begin
dmidpi_close(ctx);
ctx = 0;
end
那DPI裡面是怎麼實作的呢?我們看
dmidpi.c
:
struct dmidpi_ctx {
struct tcp_server_ctx *sock;
struct jtag_ctx jtag;
struct dmi_sig_values sig;
};
void *dmidpi_create(const char *display_name, int listen_port) {
// Create context
struct dmidpi_ctx *ctx =
(struct dmidpi_ctx *)calloc(1, sizeof(struct dmidpi_ctx));
assert(ctx);
// Set up socket details
ctx->sock = tcp_server_create(display_name, listen_port);
printf(
"n"
"JTAG: Virtual JTAG interface %s is listening on port %d. Usen"
"OpenOCD and the following configuration to connect:n"
" interface remote_bitbangn"
" remote_bitbang_host localhostn"
" remote_bitbang_port %dn",
display_name, listen_port, listen_port);
return (void *)ctx;
}
TCP送來了remote bitbang協定的JTAG command,一系列的函數對其進行解析,并轉換成DMI上的資料讀寫請求。在
dmidpi.sv
裡面每個cycle對DMI interface上的信号值進行更新,包括從DM收過來以及送到DM去。
always_ff @(posedge clk_i, negedge rst_ni) begin
dmidpi_tick(ctx, dmi_req_valid, dmi_req_ready, dmi_req_addr, dmi_req_op,
dmi_req_data, dmi_rsp_valid, dmi_rsp_ready, dmi_rsp_data,
dmi_rsp_resp, dmi_rst_n);
end
使用了
dmidpi
子產品,就意味着我們放棄了真實的DTM,在verilator仿真頂層
top_earlgrey_verilator
裡面
dmidpi
直接bind到
rv_dm
子產品裡面。
bind rv_dm dmidpi u_dmidpi (
.clk_i,
.rst_ni,
//......
4. 小結
Debug系統是SoC中非常重要的一個部分,也是開發中非常費時的一個部分。Debug系統能在仿真階段來debug,本身也是很有意義也很挑戰的工作。這篇小文裡也隻是粗略解析了系統大體的運作過程以及仿真階段的處理。其實還有很多debug相關的知識點沒有包括,比如:
- hardware breakpoint和software breakpoint各是如何工作的?
- 如何對Flash及ROM裡的代碼設定breakpoint?
- JTAG标準是怎麼樣的?如何應用在debug系統裡的?
- EBREAK的exception handler都做了什麼事情?