feat(pdb): Add XSPdb, a GDB-like interactive debugger for XiangShan (#4906)

### 1. Purpose of this Pull Request 
XSPdb is a specialized Python pdb-based debugging tool for RISC-V IP
cores, customized for Xiangshan's difftest interface. It provides
GDB-like interactive debugging capabilities, integrating: Terminal
command-line interface, RTL-level waveform toggling, Automated script
replay, System snapshot save/restore, Register initialization
configuration

### 2. Changes Made

* **New Tool (`XSPdb`)**: The core Python scripts for XSPdb have been
added under `scripts/xspdb/`. This tool provides an interactive
debugging console with standard pdb commands (e.g., breakpoints, step,
continue, register inspection).
* **Build System Integration**: A new Makefile, `pdb.mk`, has been
created to define the build targets and logic required for XSPdb.
* **Makefile Modification**: The root Makefile has been updated to
include `pdb.mk`, integrating the new `pdb` target into the main build
system.

### 3. Usage 

The new tool can be built and used via a new `make` target.
1. **Build the XSPdb package**: 
```bash
    make pdb NOOP_HOME=$(pwd)
 ```
2.  **Run a simulation with the XSPdb**:
```bash
   make pdb-run NOOP_HOME=$(pwd)
 ```

---------

Co-authored-by: Zhicheng Yao <yaozhicheng@ict.ac.cn>
This commit is contained in:
SFangYy 2025-08-23 00:59:13 +08:00 committed by GitHub
parent ed04ad453a
commit db3800c43f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 1940 additions and 0 deletions

3
.github/CODEOWNERS vendored
View File

@ -36,3 +36,6 @@ coupledL2/ @linjuanZ @Ivyfeather
huancun/ @linjuanZ @Ivyfeather
src/main/scala/top/ @Tang-Haojin
scripts/pdb-run.py @yaozhicheng @forever043 @SFangYy @Tang-Haojin
scripts/xspdb/ @yaozhicheng @forever043 @SFangYy @Tang-Haojin

View File

@ -21,6 +21,9 @@ RTL_DIR = $(BUILD_DIR)/rtl
# import docker support
include scripts/Makefile.docker
# import pdb support
include scripts/Makefile.pdb
# if XSTopPrefix is specified in yaml, use it.
ifneq ($(YAML_CONFIG),)
HAS_PREFIX_FROM_YAML = $(shell grep 'XSTopPrefix *:' $(YAML_CONFIG))

View File

@ -121,6 +121,39 @@ make emu CONFIG=MinimalConfig EMU_THREADS=2 -j10
./build/emu -b 0 -e 0 -i ./ready-to-run/coremark-2-iteration.bin --diff ./ready-to-run/riscv64-nemu-interpreter-so
```
### Run with xspdb
* Install [picker](https://github.com/XS-MLVP/picker), a verifaction tool that supports high-level languages.
* Run `make pdb` to build XiangShan Python binaries.
* Run `make pdb-run` to run XiangShan binaries.
Example output and interaction:
```bash
$ make pdb-run
[Info] Set PMEM_BASE to 0x80000000 (Current: 0x80000000)
[Info] Set FIRST_INST_ADDRESS to 0x80000000 (Current: 0x80000000)
Using simulated 32768B flash
[Info] reset dut complete
> XiangShan/scripts/pdb-run.py(13)run()
-> while True:
(XiangShan) xload ready-to-run/microbench.bin # Load binary (Tab-compatible)
(XiangShan) xwatch_commit_pc 0x80000004 # set watch point,
(XiangShan) xistep 3 # Step to next three instruction commit, it will stop at watch point
[Info] Find break point (Inst commit), break (step 2107 cycles) at cycle: 2207 (0x89f)
[Info] Find break point (Inst commit, Target commit), break (step 2108 cycles) at cycle: 2208 (0x8a0)
(XiangShan) xpc # print pc info
PC[0]: 0x80000000 Instr: 0x00000093
PC[1]: 0x80000004 Instr: 0x00000113
PC[2]: 0x0 Instr: 0x0
...
PC[7]: 0x0 Instr: 0x0
(XiangShan) xistep 1000000 # Execute to binary end
[Info] Find break point (Inst commit), break (step 2037 cycles) at cycle: 2207 (0x89f)
[Info] Find break point (Inst commit), break (step 2180 cycles) at cycle: 2207 (0x89f)
...
HIT GOOD LOOP at pc = 0xf0001cb0
```
## Troubleshooting Guide
[Troubleshooting Guide](https://github.com/OpenXiangShan/XiangShan/wiki/Troubleshooting-Guide)

View File

@ -115,6 +115,40 @@ make emu CONFIG=MinimalConfig EMU_THREADS=2 -j10
./build/emu -b 0 -e 0 -i ./ready-to-run/coremark-2-iteration.bin --diff ./ready-to-run/riscv64-nemu-interpreter-so
```
### 运行xspdb
* 安装多语言芯片验证辅助工具 [picker](https://github.com/XS-MLVP/picker)
* 运行 `make pdb` 以利用 picker 构建香山的二进制版本
* 运行 `make pdb-run` 来运行香山二进制版本
运行示例:
```bash
$ make pdb-run
[Info] Set PMEM_BASE to 0x80000000 (Current: 0x80000000)
[Info] Set FIRST_INST_ADDRESS to 0x80000000 (Current: 0x80000000)
Using simulated 32768B flash
[Info] reset dut complete
> XiangShan/scripts/pdb-run.py(13)run()
-> while True:
(XiangShan) xload ready-to-run/microbench.bin # 加载需要运行的bin文件
(XiangShan) xwatch_commit_pc 0x80000004 # 设置观察点
(XiangShan) xistep 3 # 执行到下三条指令提交,如设置观察点则执行到观察点
[Info] Find break point (Inst commit), break (step 2107 cycles) at cycle: 2207 (0x89f)
[Info] Find break point (Inst commit, Target commit), break (step 2108 cycles) at cycle: 2208 (0x8a0)
(XiangShan) xpc # 打印pc信息
PC[0]: 0x80000000 Instr: 0x00000093
PC[1]: 0x80000004 Instr: 0x00000113
PC[2]: 0x0 Instr: 0x0
...
PC[7]: 0x0 Instr: 0x0
(XiangShan) xistep 1000000 # 执行到结束
[Info] Find break point (Inst commit), break (step 2037 cycles) at cycle: 2207 (0x89f)
[Info] Find break point (Inst commit), break (step 2180 cycles) at cycle: 2207 (0x89f)
...
HIT GOOD LOOP at pc = 0xf0001cb0
```
## 错误排除指南
[Troubleshooting Guide](https://github.com/OpenXiangShan/XiangShan/wiki/Troubleshooting-Guide)

72
scripts/Makefile.pdb Normal file
View File

@ -0,0 +1,72 @@
#***************************************************************************************
# Copyright (c) 2025 Beijing Institute of Open Source Chip (BOSC)
# Copyright (c) 2025 Institute of Computing Technology, Chinese Academy of Sciences
#
# XiangShan is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
# http://license.coscl.org.cn/MulanPSL2
#
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
#
# See the Mulan PSL v2 for more details.
#***************************************************************************************
PDB_HOME := $(abspath $(BUILD_DIR)/xspdb)
RTL_HOME := $(abspath $(RTL_DIR))
RTL_FILELIST := $(RTL_HOME)/filelist.f
DIFFTEST_HOME := $(abspath $(PDB_HOME)/pydifftest)
DIFFTEST_INCLUDE = $(abspath difftest/src/test/vsrc/common)
PICKER_INCLUDE = $(shell picker --show_xcom_lib_location_cpp | grep include | awk '{print $$2}')
PYTHON_VERSION = $(shell python3 --version | awk '{print $$2}' | cut -d'.' -f1-2)
SIM_LDFLAGS = $(shell cat $(DIFFTEST_HOME)/sim_ld_flags.txt)
PYTHON_DEP_FILE = scripts/xspdb/requirements.txt
PDB_PYTHONPATH := $(PDB_HOME):script/xspdb
PDB_LD_PRELOAD := $(PDB_HOME)/pyxscore/xspcomm/libxspcomm.so.0.0.1 \
$(PDB_HOME)/pyxscore/libdifftest.so
.PHONY: pdb pdb-run check-deps
pdb: $(DIFFTEST_HOME)/_difftest.so $(PDB_HOME)/libUTSimTop.so
$(RTL_FILELIST): sim-verilog
$(DIFFTEST_HOME)/_difftest.so: $(RTL_FILELIST)
$(MAKE) -C ./difftest pydifftest WITH_CONSTANTIN=0 WITH_CHISELDB=0
ln -srf ../pydifftest/_difftest.so $(DIFFTEST_HOME)/libdifftest.so
$(PDB_HOME)/picker.f: $(DIFFTEST_HOME)/_difftest.so
find $(realpath $(RTL_HOME)) -maxdepth 1 \( -name "*.sv" -o -name "*.v" -o -name "*.cpp" -o -name "*.so" \) -not -name "SimTop.sv" -printf "%p\n" > $(PDB_HOME)/picker.f
find $(realpath $(DIFFTEST_INCLUDE)) -maxdepth 1 \( -name "*.v" \) -not -name "SimTop.sv" -printf "%p\n" >> $(PDB_HOME)/picker.f
echo "$(DIFFTEST_HOME)/libdifftest.so" >> $(PDB_HOME)/picker.f
$(PDB_HOME)/libUTSimTop.so: $(PDB_HOME)/picker.f
time picker export $(RTL_HOME)/SimTop.sv \
--rw 1 \
-w $(PDB_HOME)/xs.fst \
--lang python \
--tdir $(PDB_HOME)/pyxscore \
--fs $(PDB_HOME)/picker.f \
-V "--no-timing;--threads;128;+define+DIFFTEST;-I$(NOOP_HOME)/build/generated-src" \
-C "-fPIC -lz -I$(PICKER_INCLUDE) -L$(PDB_HOME)/pyxscore -ldifftest -lpython$(PYTHON_VERSION) $(SIM_LDFLAGS)" \
--autobuild=false
ln -s ../pydifftest/_difftest.so $(PDB_HOME)/pyxscore/libdifftest.so
$(MAKE) -C $(PDB_HOME)/pyxscore
pdb-run: check-deps
LD_PRELOAD="$(PDB_LD_PRELOAD)" \
PYTHONPATH="$(PDB_PYTHONPATH)" \
python3 scripts/pdb-run.py
check-deps:
@[ -f "$(PYTHON_DEP_FILE)" ] || { echo "Error: $(PYTHON_DEP_FILE) not found"; exit 1; }; \
grep -v '^[#[:space:]]*$$' $(PYTHON_DEP_FILE) | while read -r req; do \
ver=$$(pip show "$${req%%>=*}" 2>/dev/null | awk '/^Version:/{print $$2}') || exit 1; \
[ -n "$$ver" ] && [ "$$(printf "$$ver\n$${req##*>=}" | sort -V | head -n1)" = "$${req##*>=}" ] || \
{ echo "Error: Dependency requirement not satisfied, Please run 'pip install -r $(PYTHON_DEP_FILE)'"; exit 1; }; \
done

23
scripts/pdb-run.py Normal file
View File

@ -0,0 +1,23 @@
#***************************************************************************************
# Copyright (c) 2025 Beijing Institute of Open Source Chip (BOSC)
# Copyright (c) 2025 Institute of Computing Technology, Chinese Academy of Sciences
#
# XiangShan is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
# http://license.coscl.org.cn/MulanPSL2
#
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
#
# See the Mulan PSL v2 for more details.
#***************************************************************************************
from xspdb import XSPdb, DUTSimTop
if __name__ == "__main__":
dut = DUTSimTop();
XSPdb(dut).run();

21
scripts/xspdb/__init__.py Normal file
View File

@ -0,0 +1,21 @@
#***************************************************************************************
# Copyright (c) 2025 Beijing Institute of Open Source Chip (BOSC)
# Copyright (c) 2025 Institute of Computing Technology, Chinese Academy of Sciences
#
# XiangShan is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
# http://license.coscl.org.cn/MulanPSL2
#
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
#
# See the Mulan PSL v2 for more details.
#***************************************************************************************
from .xspdb import *
from pyxscore import DUTSimTop
__version__ = "0.9.1"

View File

@ -0,0 +1,2 @@
urwid>=2.6.16
capstone>=5.0.5

View File

@ -0,0 +1,19 @@
#***************************************************************************************
# Copyright (c) 2025 Beijing Institute of Open Source Chip (BOSC)
# Copyright (c) 2025 Institute of Computing Technology, Chinese Academy of Sciences
#
# XiangShan is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
# http://license.coscl.org.cn/MulanPSL2
#
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
#
# See the Mulan PSL v2 for more details.
#***************************************************************************************
from xspdb.xscmd.util import message, error, warn, info, GREEN, RESET
from xspdb.xscmd.util import get_completions, find_executable_in_dirs

View File

@ -0,0 +1,328 @@
#***************************************************************************************
# Copyright (c) 2025 Beijing Institute of Open Source Chip (BOSC)
# Copyright (c) 2025 Institute of Computing Technology, Chinese Academy of Sciences
#
# XiangShan is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
# http://license.coscl.org.cn/MulanPSL2
#
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
#
# See the Mulan PSL v2 for more details.
#***************************************************************************************
import os
from . import message, error, warn, info, GREEN, RESET
class CmdDiffTest:
def __init__(self):
assert hasattr(self, "difftest_stat"), "difftest_stat not found"
self.condition_watch_commit_pc = {}
self.condition_instrunct_istep = {}
self.difftest_ref_so = self.xsp.CString()
self.difftest_ref_is_inited = False
self.difftest_diff_checker = {}
self.difftest_diff_is_run = False
self.istep_last_commit_pc = []
self.data_last_symbol_block = -1
self.data_last_symbol_pc = -1
def api_load_ref_so(self, so_path):
"""Load the difftest reference shared object
Args:
so_path (string): Path to the shared object
"""
if not os.path.exists(so_path):
error(f"file {so_path} not found")
return False
self.difftest_ref_so.Set(so_path)
self.df.SetProxyRefSo(self.difftest_ref_so.CharAddress())
info(f"load difftest ref so: {so_path} complete")
return True
def api_get_ref_so_path(self):
"""Get the path of the difftest reference shared object
Returns:
string: Path to the shared object
"""
return self.difftest_ref_so.Get()
def api_init_ref(self, force=False):
"""Initialize the difftest reference"""
if self.difftest_ref_is_inited:
if not force:
error("difftest reference already inited")
return False
if self.difftest_ref_so.Get() == "":
error("difftest reference so not loaded")
return False
if not self.mem_inited:
error("mem not loaded, please load bin file to mem first")
return False
if force and self.difftest_ref_is_inited:
self.df.finish_device()
self.df.GoldenMemFinish()
self.df.difftest_finish()
self.df.difftest_init()
self.difftest_stat = self.df.GetDifftest(0).dut
self.df.init_device()
self.df.GoldenMemInit()
self.df.init_nemuproxy(0)
self.difftest_ref_is_inited = True
return True
def api_set_difftest_diff(self, turn_on):
"""Initialize the difftest diff"""
if not self.api_init_ref():
return False
checker = self.difftest_diff_checker.get("checker")
if not checker:
checker = self.xsp.ComUseCondCheck(self.dut.xclock)
tmp_dat = self.xsp.ComUseDataArray(4)
checker.SetCondition("diff_test_do_diff_check", tmp_dat.BaseAddr(), tmp_dat.BaseAddr(),
self.xsp.ComUseCondCmp_NE, 4, 0, 0, 0,
checker.AsPtrXFunc(self.df.GetFuncAddressOfDifftestStepAndCheck()),
0)
self.difftest_diff_checker["checker"] = checker
key = "diff_test_do_diff_check"
self.dut.xclock.RemoveStepRisCbByDesc(key)
self.difftest_diff_is_run = False
if turn_on:
self.dut.xclock.StepRis(checker.GetCb(), checker.CSelf(), key)
self.difftest_diff_is_run = True
info("turn on difftest diff")
else:
info("turn off difftest diff")
return True
def api_is_difftest_diff_exit(self, show_log=False):
"""Check if the difftest diff has exited
Returns:
bool: True if exited, False otherwise
"""
if not self.difftest_diff_is_run:
return False
stat = self.df.GetDifftestStat()
if stat == -1:
return False
if show_log:
message(f"{GREEN}Difftest run exit with code: {stat} {RESET}")
return True
def api_is_difftest_diff_run(self):
"""Check if the difftest diff is running"""
return self.difftest_diff_is_run
def do_xload_difftest_ref_so(self, arg):
"""Load the difftest reference shared object
Args:
arg (string): Path to the shared object
"""
if not arg.strip():
error("difftest ref so path not found\n usage: xload_difftest_ref_so <path>")
return
if not self.api_load_ref_so(arg):
error(f"load difftest ref so {arg} failed")
return
def complete_xload_difftest_ref_so(self, text, line, begidx, endidx):
return self.api_complite_localfile(text)
def api_difftest_reset(self):
"""Reset the difftest"""
if self.difftest_ref_is_inited:
if self.api_init_ref(force=True):
info("difftest reset success")
return True
else:
error("difftest reset failed")
return False
return True
def do_xdifftest_reset(self, arg):
"""Reset the difftest
Args:
arg (None): No arguments
"""
self.api_difftest_reset()
def api_commit_pc_list(self):
"""Get the list of all commit PCs
Returns:
list((pc, valid)): List of PCs
"""
index = 0
pclist=[]
while True:
cmt = self.difftest_stat.get_commit(index)
if cmt:
pclist.append((cmt.pc, cmt.valid))
index += 1
else:
break
return pclist
def do_xpc(self, a):
"""Print the current Commit PCs and instructions
Args:
a (None): No arguments
"""
for i in range(8):
cmt = self.difftest_stat.get_commit(i)
message(f"PC[{i}]: 0x{cmt.pc:x} Instr: 0x{cmt.instr:x}")
def api_istep_update_commit_pc(self):
old_p = self.condition_instrunct_istep.get("pc_old_list")
new_P = self.condition_instrunct_istep.get("pc_lst_list")
if not (old_p and new_P):
self.istep_last_commit_pc = []
else:
self.istep_last_commit_pc = []
for i in range(8):
old_pc = int.from_bytes(old_p[i].AsBytes(), byteorder='little', signed=False)
new_pc = int.from_bytes(new_P[i].AsBytes(), byteorder='little', signed=False)
if old_pc != new_pc:
self.istep_last_commit_pc.append(new_pc)
def comuse_checker_is_break(self, checker):
if not checker:
return False
if {k: v for (k, v) in checker.ListCondition().items() if v}:
return True
return False
def api_break_is_instruction_commit(self):
"""check break is instruction commit or not"""
checker = self.condition_instrunct_istep.get("checker")
return self.comuse_checker_is_break(checker)
def api_break_is_watch_commit_pc(self):
"""check break is watch commit pc or not"""
checker = self.condition_watch_commit_pc.get("checker")
return self.comuse_checker_is_break(checker)
def api_get_istep_last_commit_pc(self):
"""Get the last commit PC after instruction step
Returns:
list: List of commit PCs
"""
return self.istep_last_commit_pc.copy()
def api_xistep(self, instr_count):
"""Step through instructions, stop when find instruction commit
Args:
step_count (int): Number of steps to take
Returns:
step_taken (int)
"""
self.api_xistep_break_on()
update_pc_func = self.condition_instrunct_istep["pc_sync_list"]
update_pc_func()
step_taken = 0
for i in range(instr_count):
update_pc_func()
v = self.api_step_dut(10000)
if self.api_dut_is_step_exit():
break
if self.interrupt:
break
elif self.dut.xclock.IsDisable():
self.api_istep_update_commit_pc()
pc = max(self.api_get_istep_last_commit_pc() + [-1])
self.data_last_symbol_block = self.api_echo_pc_symbol_block_change(pc,
self.data_last_symbol_block,
self.data_last_symbol_pc)
self.data_last_symbol_pc = pc
elif v == 10000:
warn("step %d cycles complete, but no instruction commit find" % v)
step_taken -= 1 # ignore record
step_taken += 1
# remove stepi_check
self.api_xistep_break_off()
return step_taken
def api_xistep_break_on(self):
"""Set the instruction step break condition"""
if not self.condition_instrunct_istep:
checker = self.xsp.ComUseCondCheck(self.dut.xclock)
self.condition_instrunct_istep["checker"] = checker
pc_old_list = [self.xsp.ComUseDataArray(8) for i in range(8)]
pc_lst_list = [self.xsp.ComUseDataArray(self.difftest_stat.get_commit(i).get_pc_address(), 8) for i in range(8)]
# sync pc and add checker
for i, opc in enumerate(pc_old_list):
lpc = pc_lst_list[i]
opc.SyncFrom(lpc.BaseAddr(), 8)
checker.SetCondition("stepi_check_pc_%d" % i, lpc.BaseAddr(), opc.BaseAddr(), self.xsp.ComUseCondCmp_NE, 8)
self.condition_instrunct_istep["pc_old_list"] = pc_old_list
self.condition_instrunct_istep["pc_lst_list"] = pc_lst_list
def _update_old_pc():
for i, opc in enumerate(pc_old_list):
lpc = pc_lst_list[i]
opc.SyncFrom(lpc.BaseAddr(), 8)
self.condition_instrunct_istep["pc_sync_list"] = _update_old_pc
cb_key = "stepi_check"
checker = self.condition_instrunct_istep["checker"]
if cb_key in self.dut.xclock.ListSteRisCbDesc():
return
self.dut.xclock.StepRis(checker.GetCb(), checker.CSelf(), cb_key)
def api_xistep_break_off(self):
"""Remove the instruction step break condition"""
cb_key = "stepi_check"
if cb_key in self.dut.xclock.ListSteRisCbDesc():
self.dut.xclock.RemoveStepRisCbByDesc(cb_key)
assert cb_key not in self.dut.xclock.ListSteRisCbDesc()
def do_xistep_break(self, arg):
"""Set the instruction step break condition
Args:
on_or_off (str): "on" or "off"
"""
if arg.strip() == "on":
self.api_xistep_break_on()
elif arg.strip() == "off":
self.api_xistep_break_off()
else:
error("usage: xistep_break <on|off>")
def complete_xistep_break(self, text, line, begidx, endidx):
return [x for x in ["on", "off"] if x.startswith(text)] if text else ["on", "off"]
def do_xistep(self, arg):
"""Step through instructions, stop when find instruction commit
Args:
step_count (int): Number of steps to take
"""
arg = arg.strip()
instr_count = 1
try:
instr_count = 1 if not arg else int(arg)
except Exception as e:
error(f"convert {arg} to number fail: {str(e)}")
return
self.api_xistep(instr_count)
def api_difftest_get_instance(self, instance=0):
"""Get the difftest instance
Args:
instance (number): difftest instance to get, default is 0
"""
return self.df.GetDifftest(instance)

View File

@ -0,0 +1,269 @@
#***************************************************************************************
# Copyright (c) 2025 Beijing Institute of Open Source Chip (BOSC)
# Copyright (c) 2025 Institute of Computing Technology, Chinese Academy of Sciences
#
# XiangShan is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
# http://license.coscl.org.cn/MulanPSL2
#
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
#
# See the Mulan PSL v2 for more details.
#***************************************************************************************
import os
import time
from . import info, error, message, warn, get_completions
class CmdDut:
def __init__(self):
assert hasattr(self, "dut"), "this class must be used in XSPdb, canot be used alone"
self.interrupt = False
self.xdut_signal_breaks = {}
self.api_dut_reset()
def api_xbreak_list(self):
"""List all breakpoints"""
ret = []
checker = self.xdut_signal_breaks.get("checker")
if not checker:
return ret
checked = {k: v for (k, v) in checker.ListCondition().items()}
for k, v in self.xdut_signal_breaks.items():
if not k.startswith("xbreak-"):
continue
ret.append((k, v["sig"].value, v["cmp"], v["val"].value, checked[k]))
ret.sort(key=lambda x: x[0])
return ret
def call_break_callbacks(self):
checker = self.xdut_signal_breaks.get("checker")
cb_count = 0
if not checker:
return cb_count
callbacks = {}
for k, v in self.xdut_signal_breaks.items():
if not k.startswith("xbreak-"):
continue
if not callable(v["cb"]):
continue
callbacks[k] = (v["cb"], v["cb_once"])
if not callbacks:
return cb_count
checked = {k: v for (k, v) in checker.ListCondition().items() if v}
for k, (cb, once) in callbacks.items():
if k not in checked:
continue
cb(self, checker, k, self.dut.xclock.clk, self.xdut_signal_breaks[k]["sig"].value, self.xdut_signal_breaks[k]["val"].value)
if once:
checker.RemoveCondition(k)
info(f"remove signal {k} break, because callback_once is True")
del self.xdut_signal_breaks[k]
cb_count += 1
if not {k: v for (k, v) in checker.ListCondition().items()}:
checker.ClearCondition()
self.dut.xclock.RemoveStepRisCbByDesc("xdut_signal_break")
assert "xdut_signal_break" not in self.dut.xclock.ListSteRisCbDesc()
self.xdut_signal_breaks.clear()
return cb_count
def api_is_xbreak_on(self):
"""Check if the breakpoint is on"""
checker = self.xdut_signal_breaks.get("checker")
if checker:
return True
return False
def api_dut_is_step_exit(self):
"""Check if the step is exit"""
return any([self.api_is_difftest_diff_exit(show_log=False),
self.api_is_hit_good_trap(show_log=False),
self.api_is_hit_good_loop(show_log=False),
])
def api_get_breaked_names(self):
"""Get the names of the breaked names"""
names = []
# api_xbreak_list
names += [v[0] for v in self.api_xbreak_list() if v[4]]
# instrunct_istep
if self.api_break_is_instruction_commit():
names.append("Inst commit")
# watch_commit_pc
if self.api_break_is_watch_commit_pc():
names.append("Target commit")
return ",".join(names)
def api_dut_step_ready(self):
"""Prepare the DUT for stepping"""
self.dut.xclock.Enable()
assert not self.dut.xclock.IsDisable(), "clock is disable"
self.interrupt = False # interrupt from outside by user
def api_step_dut(self, cycle, batch_cycle=200):
"""Step through the circuit
Args:
cycle (int): Number of cycles
batch_cycle (int): Number of cycles per run; after each run, check for interrupt signals
"""
if not self.mem_inited:
warn("mem not inited, please load bin file first")
def check_break():
if self.dut.xclock.IsDisable():
info("Find break point (%s), break (step %d cycles) at cycle: %d (%s)" % (
self.api_get_breaked_names(),
self.dut.xclock.clk - c_count,
self.dut.xclock.clk, hex(self.dut.xclock.clk)))
return True
fc = getattr(self, "on_update_tstep", None)
if fc:
fc()
if self.api_is_difftest_diff_exit(show_log=True):
return True
elif self.api_is_hit_good_trap(show_log=True):
return True
elif self.api_is_hit_good_loop(show_log=True):
return True
elif self.api_is_hit_trap_break(show_log=True):
return True
return False
self.api_dut_step_ready()
batch, offset = cycle//batch_cycle, cycle % batch_cycle
c_count = self.dut.xclock.clk
need_break = False
for i in range(batch):
if self.interrupt:
break
self.dut.Step(batch_cycle)
if check_break():
need_break = True
break
if not self.interrupt and not need_break:
self.dut.Step(offset)
check_break()
if self.dut.xclock.IsDisable():
self.call_break_callbacks()
return self.dut.xclock.clk - c_count
def api_dut_reset(self):
"""Reset the DUT"""
for i in range(8):
cmt = self.difftest_stat.get_commit(i)
cmt.pc = 0x0
cmt.instr = 0x0
self.difftest_stat.trap.pc = 0x0
self.difftest_stat.trap.code = 32
self.difftest_stat.trap.hasTrap = 0
self.dut.reset.AsImmWrite()
self.dut.reset.value = 1
self.dut.reset.AsRiseWrite()
self.dut.reset.value = 1
self.dut.Step(100)
self.dut.reset.value = 0
info("reset dut complete")
def api_dut_flash_load(self, flash_file):
"""Load a bin file into Flash
Args:
flash_file (string): Path to the bin file
"""
assert os.path.exists(flash_file)
self.df.flash_finish()
self.df.InitFlash(flash_file)
self.flash_bin_file = flash_file
self.info_cache_asm.clear()
def do_xreset(self, arg):
"""Reset DUT
Args:
arg (None): No arguments
"""
self.api_dut_reset()
self.api_difftest_reset()
def do_xwatch(self, arg):
"""Add a watch variable
Args:
arg (string): Variable name
"""
key = arg.strip().split()
if not key:
for k, v in self.info_watch_list.items():
message(f"{k}({v.W()}): 0x{v.value}")
return
arb = key[-1]
sig = self.dut.GetInternalSignal(key[0])
if sig:
self.info_watch_list[arb] = sig
def do_xset(self, arg):
"""Set the value of an internal signal
Args:
name (string): Name of the internal signal
value (int): Value of the internal signal
"""
args = arg.strip().split()
if len(args) < 2:
error("need args format: name value")
return
pin_name, pin_value = args[0], args[1]
try:
pin_value = int(pin_value)
except Exception as e:
error(f"convert {args[1]} to number fail: {str(e)}")
return
pin = self.dut.GetInternalSignal(pin_name)
if pin:
pin.AsImmWrite()
pin.value = pin_value
def complete_xset(self, text, line, begidx, endidx):
cmp = get_completions(self.get_dut_tree(), text)
return cmp
def do_xstep(self, arg):
"""Step through the circuit
Args:
cycle (int): Number of cycles
steps (int): Number of cycles per run; after each run, check for interrupt signals
"""
try:
steps = 200
cycle = arg.strip().split()
if len(cycle) > 1:
steps = int(cycle[1])
if len(cycle) > 0:
cycle = int(cycle[0])
else:
cycle = 1
rcycles = self.api_step_dut(cycle, steps)
info(f"step {rcycles} cycles complete" + (f" ({cycle - rcycles} cycle missed)" if rcycles != cycle else ""))
except Exception as e:
error(e)
message("usage: xstep [cycle] [<steps>]")
def do_xprint(self, arg):
"""Print the value and width of an internal signal
Args:
arg (string): Name of the internal signal
"""
sig = self.dut.GetInternalSignal(arg)
if sig:
message(f"value: {hex(sig.value)} width: {sig.W()}")
def complete_xprint(self, text, line, begidx, endidx):
cmp = get_completions(self.get_dut_tree(), text)
return cmp

View File

@ -0,0 +1,135 @@
#***************************************************************************************
# Copyright (c) 2025 Beijing Institute of Open Source Chip (BOSC)
# Copyright (c) 2025 Institute of Computing Technology, Chinese Academy of Sciences
#
# XiangShan is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
# http://license.coscl.org.cn/MulanPSL2
#
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
#
# See the Mulan PSL v2 for more details.
#***************************************************************************************
import os
import bisect
import subprocess
from . import info, error, message, warn, find_executable_in_dirs
class CmdEfl:
"""ELF command class for disassembling data"""
def __init__(self):
self.elf_symbol_dict = None
self.elf_current_exe_bin_is_efl = None
self.flag_trace_pc_symbol_block_change = False
def api_get_elf_symbol_dict(self, elf_file, search_dirs=["./ready-to-run"]):
"""Get the symbol dictionary from an ELF file
Args:
elf_file (string): Path to the ELF file
search_dirs (list): List of directories to search for CMD readelf
"""
if not os.path.exists(elf_file):
error(f"{elf_file} not found")
return None
readelf = find_executable_in_dirs("readelf", search_dirs=search_dirs)
cmd = [readelf, "-sW", elf_file]
try:
output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
lines = output.decode().splitlines()
symbol_value_dict = {}
symbol_name_dict = {}
symbol_type_dict = {}
symbol_gnored = {}
for line in lines[1:]:
parts = line.split()
if len(parts) != 8:
continue
if parts[1].lower() == "value":
continue
value = {"addr": int(parts[1], 16),
"size": int(parts[2], 16),
"type": parts[3],
"bind": parts[4],
"vis": parts[5],
"ndx": parts[6],
"name": parts[7],
}
if value["type"] in ("NOTYPE", "FILE"):
if value["ndx"] not in ("1", "2", ".text", ".data"):
if value["type"] not in symbol_gnored:
symbol_gnored[value["type"]] = 1
else:
symbol_gnored[value["type"]] += 1
continue
symbol_name_dict[value["name"]] = value
if value["type"] not in symbol_type_dict:
symbol_type_dict[value["type"]] = 1
else:
symbol_type_dict[value["type"]] += 1
if symbol_value_dict.get(value["addr"]) is None:
symbol_value_dict[value["addr"]] = [value]
else:
symbol_value_dict[value["addr"]].append(value)
info("Find symbol: %s" % symbol_type_dict)
if len(symbol_gnored) > 0:
warn("Ignored symbol: %s" % symbol_gnored)
return {"addr": symbol_value_dict,
"name": symbol_name_dict,
"sorted_addr": sorted(symbol_value_dict.keys()),
"sorted_name": sorted(symbol_name_dict.keys()),
}
except subprocess.CalledProcessError as e:
error(f"Failed to read ELF file: {e.output.decode()}")
return None
def api_echo_pc_symbol_block_change(self, current_pc, last_block_addr, last_pc):
block_addr = last_block_addr
if current_pc < 0:
return block_addr
if not self.flag_trace_pc_symbol_block_change:
return block_addr
if self.elf_current_exe_bin_is_efl is False:
return block_addr
if self.elf_symbol_dict is None:
return block_addr
symbol_index = bisect.bisect_left(self.elf_symbol_dict["sorted_addr"], current_pc) - 1
if symbol_index < 0:
return block_addr
if symbol_index >= len(self.elf_symbol_dict["sorted_addr"]):
return block_addr
symbol_addr = self.elf_symbol_dict["sorted_addr"][symbol_index]
if symbol_addr == last_block_addr:
return block_addr
# block address changed
symbol = self.elf_symbol_dict.get("addr", {}).get(symbol_addr)
if not symbol:
return block_addr
symbol_name = ','.join([s['name'] for s in symbol])
symbol_pre = self.elf_symbol_dict.get("addr", {}).get(last_block_addr)
symbol_pre_name = "None"
if symbol_pre:
symbol_pre_name = ','.join([s['name'] for s in symbol_pre])
delta_last = last_pc-last_block_addr
delta_curr = current_pc-symbol_addr
message(f"PC block changed({hex(last_pc)} = > {hex(current_pc)}, " +
f"cycle: {self.difftest_stat.trap.cycleCnt}): {symbol_pre_name}({hex(last_block_addr)})+{hex(delta_last)} " +
f"-> {symbol_name}({hex(symbol_addr)})+{hex(delta_curr)}")
return symbol_addr
def complete_xtrace_pc_symbol_block_change(self, text, line, begidx, endidx):
"""Complete the command for tracing PC symbol block change
Args:
text (string): Current text
line (string): Current line
begidx (int): Beginning index
endidx (int): Ending index
"""
return [x for x in ["on", "off"] if x.startswith(text)] if text else ["on", "off"]

View File

@ -0,0 +1,55 @@
#***************************************************************************************
# Copyright (c) 2025 Beijing Institute of Open Source Chip (BOSC)
# Copyright (c) 2025 Institute of Computing Technology, Chinese Academy of Sciences
#
# XiangShan is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
# http://license.coscl.org.cn/MulanPSL2
#
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
#
# See the Mulan PSL v2 for more details.
#***************************************************************************************
import os
from . import info, error, message, warn
class CmdFiles:
def api_dut_bin_load(self, bin_file):
"""Load a bin file into memory
Args:
bin_file (string): Path to the bin file
"""
assert os.path.exists(bin_file), "file %s not found" % bin_file
self.exec_bin_file = bin_file
if self.mem_inited:
self.df.overwrite_ram(bin_file, self.mem_size)
else:
self.api_init_mem()
self.info_cache_asm.clear()
def complete_xflash(self, text, line, begidx, endidx):
return self.api_complite_localfile(text)
def do_xload(self, arg):
"""Load a binary file into memory
Args:
arg (string): Path to the binary file
"""
if not arg:
message("usage: xload <bin_file>")
return
if not os.path.exists(arg):
error(f"{arg} not found")
return
self.api_dut_bin_load(arg)
def complete_xload(self, text, line, begidx, endidx):
return self.api_complite_localfile(text)

View File

@ -0,0 +1,154 @@
#***************************************************************************************
# Copyright (c) 2025 Beijing Institute of Open Source Chip (BOSC)
# Copyright (c) 2025 Institute of Computing Technology, Chinese Academy of Sciences
#
# XiangShan is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
# http://license.coscl.org.cn/MulanPSL2
#
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
#
# See the Mulan PSL v2 for more details.
#***************************************************************************************
import os
from . import error, warn, message
class CmdFlash:
def api_get_flash_init_iregs(self):
"""Get Flash internal registers
Returns:
list(int): Register values
"""
if not self.api_check_if_xspdb_init_bin_loaded():
return []
base_offset = 8
reg_index = self.mpc_iregs
regs = []
for i in range(len(reg_index)):
regs.append((reg_index[i], self.df.FlashRead(base_offset + i*8)))
return regs
def api_get_flash_init_fregs(self):
"""Get Flash floating-point registers
Returns:
list(int): Register values
"""
if not self.api_check_if_xspdb_init_bin_loaded():
return []
base_offset = 8 + 32*8
regs = []
for i in range(len(self.fregs)):
regs.append((self.fregs[i], self.df.FlashRead(base_offset + i*8)))
return regs
def api_set_flash_float_regs(self, regs):
"""Set Flash floating-point registers
Args:
regs (list(float), dict): Register values
"""
if not self.api_check_if_xspdb_init_bin_loaded():
return
base_offset = 8 + 32*8
reg_map = {k: v for v, k in enumerate(self.fregs)}
return self.api_set_flash_data_values(base_offset, self.fregs, reg_map, regs, "fregs")
def api_set_flash_int_regs(self, regs):
"""Set Flash internal registers
Args:
regs (list(int), dict): Register values
"""
if not self.api_check_if_xspdb_init_bin_loaded():
return
base_offset = 8
reg_index = self.mpc_iregs
reg_map = {k: v for v, k in enumerate(reg_index)}
return self.api_set_flash_data_values(base_offset, reg_index, reg_map, regs, "iregs")
def api_check_if_xspdb_init_bin_loaded(self):
"""Check if xspdb_flash_init.bin is loaded
Returns:
bool: Whether it is loaded
"""
if not self.flash_bin_file or self.xspdb_init_bin not in self.flash_bin_file:
error(f"{self.xspdb_init_bin} not loaded")
return False
return True
def api_set_flash_data_values(self, base_offset, reg_index, reg_map, kdata, kname):
"""Set Flash register values
Args:
base_offset (int): Base address of the registers
reg_index (list(string)): List of register names
reg_map (dict): Mapping of register names
kdata (list(int), dict): Register values
kname (string): Register name
"""
if isinstance(kdata, list):
for i, r in enumerate(kdata):
if isinstance(r, str):
r = r.strip()
if r == "-":
continue
r = int(r, 0)
assert isinstance(r, int), f"{kname}[{i}] not number"
self.df.FlashWrite(base_offset + i*8, r)
elif isinstance(kdata, dict):
if "*" in kdata:
v = kdata["*"]
for key in reg_index:
if key in kdata:
v = kdata[key]
self.df.FlashWrite(base_offset + reg_map[key]*8, v)
else:
for key, v in kdata.items():
if key in reg_map:
self.df.FlashWrite(base_offset + reg_map[key]*8, v)
else:
warn(f"{kname}[{key}] not found")
else:
assert False, "regs type error"
# delete asm data in cache
cache_index = self.flash_base - self.flash_base % self.info_cache_bsz
if cache_index in self.info_cache_asm:
del self.info_cache_asm[cache_index]
def api_dut_reset_flash(self):
"""Reset the DUT Flash"""
self.df.flash_finish()
self.df.InitFlash("")
self.flash_bin_file = None
def do_xflash(self, arg):
"""Load a binary file into Flash
Args:
arg (string): Path to the binary file
"""
if not arg:
message("usage: xload <bin_file>")
return
if not os.path.exists(arg):
error(f"{arg} not found")
return
self.api_dut_flash_load(arg)
def do_xreset_flash(self, arg):
"""Reset Flash
Args:
arg (None): No arguments
"""
self.api_reset_flash()

View File

@ -0,0 +1,46 @@
#***************************************************************************************
# Copyright (c) 2025 Beijing Institute of Open Source Chip (BOSC)
# Copyright (c) 2025 Institute of Computing Technology, Chinese Academy of Sciences
#
# XiangShan is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
# http://license.coscl.org.cn/MulanPSL2
#
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
#
# See the Mulan PSL v2 for more details.
#***************************************************************************************
import bisect
from collections import OrderedDict
from . import error, info
class CmdInfo:
"""Info command class
"""
def __init__(self):
assert hasattr(self, "dut"), "this class must be used in XSPdb, canot be used alone"
self.info_cache_asm = {}
self.info_cache_bsz = 256
self.info_cached_cmpclist = None
self.info_watch_list = OrderedDict()
self.info_force_address = None
self.info_last_address = None
def api_info_get_last_commit_pc(self):
"""Get the last commit PC
Returns:
int: Address
"""
istep_pc = self.api_get_istep_last_commit_pc()
if len(istep_pc) != 0:
return max(istep_pc)
valid_pc_list = [x[0] for x in self.api_commit_pc_list() if (x[1] or self.api_is_difftest_diff_run())]
return max(valid_pc_list) if valid_pc_list else self.mem_base

View File

@ -0,0 +1,72 @@
#***************************************************************************************
# Copyright (c) 2025 Beijing Institute of Open Source Chip (BOSC)
# Copyright (c) 2025 Institute of Computing Technology, Chinese Academy of Sciences
#
# XiangShan is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
# http://license.coscl.org.cn/MulanPSL2
#
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
#
# See the Mulan PSL v2 for more details.
#***************************************************************************************
from . import message, warn, GREEN, RESET, info
class CmdTrap:
"""Trap command class
"""
def __init__(self):
assert hasattr(self, "dut"), "this class must be used in XSPdb, canot be used alone"
self.condition_good_trap = {}
self.break_on_trap = {}
def api_is_hit_good_trap(self, show_log=False):
"""Check if the good trap is hit
Returns:
bool: Whether the good trap is hit
"""
trap = self.difftest_stat.trap
if trap.hasTrap != 0 and trap.code == 0:
if show_log:
message(f"{GREEN}HIT GOOD TRAP at pc = 0x{trap.pc:x} cycle = 0x{trap.cycleCnt:x} {RESET}")
return True
return False
def api_is_hit_good_loop(self, show_log=False):
"""Check if the good trap is hit
Args:
show_log (bool): Whether to show the log
Returns:
bool: Whether the good trap is hit
"""
for i in range(8):
cmt = self.difftest_stat.get_commit(i)
if cmt and cmt.valid:
if cmt.instr == 0x6f:
if show_log:
message(f"{GREEN}HIT GOOD LOOP at pc = 0x{cmt.pc:x}{RESET}")
return True
return False
def api_is_hit_trap_break(self, show_log=False):
"""Check if the trap is break
Args:
show_log (bool): Whether to show the log
Returns:
bool: Whether the trap is break
"""
trap = self.difftest_stat.trap
if trap.hasTrap != 0 and self.api_is_trap_break_on():
if show_log:
message(f"{GREEN}HIT TRAP BREAK pc: 0x{trap.pc:x} code: 0x{trap.code:x} hasWFI: {trap.hasWFI}{RESET}")
return True
return False

View File

@ -0,0 +1,100 @@
#***************************************************************************************
# Copyright (c) 2025 Beijing Institute of Open Source Chip (BOSC)
# Copyright (c) 2025 Institute of Computing Technology, Chinese Academy of Sciences
#
# XiangShan is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
# http://license.coscl.org.cn/MulanPSL2
#
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
#
# See the Mulan PSL v2 for more details.
#***************************************************************************************
import os
from . import message, info, error
class CmdTrap:
"""Trap command class
"""
def __init__(self):
assert hasattr(self, "dut"), "this class must be used in XSPdb, canot be used alone"
def api_init_waveform(self):
"""Initialize the waveform (close waveform at beginning)
"""
self.dut.RefreshComb()
self.dut.CloseWaveform()
self.waveform_on = False
def api_is_waveform_on(self):
"""Check if waveform recording is on
Returns:
bool: Whether the waveform is on
"""
return self.waveform_on
def api_waveform_on(self, wave_file=""):
"""Start waveform recording
Args:
wave_file (str): Waveform file path [optional]
"""
if self.waveform_on:
info("waveform is already on")
return True
if wave_file:
if not os.path.isabs(wave_file):
error(f"waveform file[{wave_file}] name must be a ligal path")
return False
self.dut.SetWaveform(wave_file)
self.dut.OpenWaveform()
self.waveform_on = True
return True
def api_waveform_off(self):
"""Close waveform recording"""
if not self.waveform_on:
info("waveform is already off")
return True
self.dut.CloseWaveform()
self.waveform_on = False
return True
def do_xwave_on(self, arg):
"""Start waveform recording
Args:
name (str): Waveform file path [optional]
"""
if self.api_waveform_on(arg):
info("waveform on")
else:
message("usage: xwave_on [waveform file path]")
def do_xwave_off(self, arg):
"""Close waveform recording
Args:
arg (None): No arguments
"""
self.api_waveform_off()
info("waveform off")
def do_xwave_flush(self, arg):
"""Flush waveform recording
Args:
arg (None): No arguments
"""
if not self.waveform_on:
error("waveform is not on")
return
self.dut.FlushWaveform()
info("waveform flush complete")

313
scripts/xspdb/xscmd/util.py Normal file
View File

@ -0,0 +1,313 @@
#***************************************************************************************
# Copyright (c) 2025 Beijing Institute of Open Source Chip (BOSC)
# Copyright (c) 2025 Institute of Computing Technology, Chinese Academy of Sciences
#
# XiangShan is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
# http://license.coscl.org.cn/MulanPSL2
#
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
#
# See the Mulan PSL v2 for more details.
#***************************************************************************************
import os
import inspect
import importlib.util
import sys
import logging
RESET = "\033[0m"
GREEN = "\033[32m"
RED = "\033[31m"
YELLOW = "\033[33m"
_xspdb_enable_log = False
_xspdb_logger = None
def xspdb_set_log(xspdb_enable_log:bool):
global _xspdb_enable_log
global _xspdb_logger
_xspdb_enable_log = xspdb_enable_log
if _xspdb_enable_log and _xspdb_logger is None:
#initialize logger
_xspdb_logger = logging.getLogger("XSPdb")
_xspdb_logger.setLevel(logging.DEBUG)
ch = logging.FileHandler("XSPdb.log")
ch.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(message)s')
ch.setFormatter(formatter)
_xspdb_logger.addHandler(ch)
def xspdb_set_log_file(log_file:str):
global _xspdb_logger
if _xspdb_logger is None:
_xspdb_logger = logging.getLogger("XSPdb")
_xspdb_logger.setLevel(logging.DEBUG)
ch = logging.FileHandler(log_file)
ch.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(message)s')
ch.setFormatter(formatter)
_xspdb_logger.addHandler(ch)
else:
for handler in _xspdb_logger.handlers:
if isinstance(handler, logging.FileHandler):
handler.close()
_xspdb_logger.removeHandler(handler)
ch = logging.FileHandler(log_file)
ch.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(message)s')
ch.setFormatter(formatter)
_xspdb_logger.addHandler(ch)
_XSPDB_LOG_LEVEL = logging.DEBUG
def set_xspdb_log_level(level):
"""
Set the log level for XSPdb.
Args:
level (int): Log level to set. Use logging.DEBUG, logging.INFO, etc.
"""
global _xspdb_logger
if _xspdb_logger is not None:
_xspdb_logger.setLevel(level)
for handler in _xspdb_logger.handlers:
handler.setLevel(level)
def set_xspdb_debug_level(level):
global _XSPDB_LOG_LEVEL
_XSPDB_LOG_LEVEL = level
set_xspdb_log_level(level)
def log_message(*a, **k):
"""Print a message to log"""
if _xspdb_enable_log:
old_level = _xspdb_logger.level
set_xspdb_log_level(logging.INFO)
_xspdb_logger.info(*a, **k)
set_xspdb_log_level(old_level)
def message(*a, **k):
"""Print a message"""
k["flush"] = True
print(*a, **k)
del k["flush"]
log_message(*a, **k)
def info(msg):
"""Print information"""
if _XSPDB_LOG_LEVEL <= logging.INFO:
print(f"{GREEN}[Info] %s{RESET}" % msg, flush=True)
if _xspdb_enable_log:
_xspdb_logger.info("[Info] %s" % msg)
def debug(msg):
"""Print debug information"""
if _XSPDB_LOG_LEVEL <= logging.DEBUG:
print("[Debug] %s" % msg, flush=True)
if _xspdb_enable_log:
_xspdb_logger.debug("[Debug] %s" % msg)
def error(msg):
"""Print error information"""
if _XSPDB_LOG_LEVEL <= logging.ERROR:
print(f"{RED}[Error] %s{RESET}" % msg, flush=True)
if _xspdb_enable_log:
_xspdb_logger.error("[Error] %s" % msg)
def warn(msg):
"""Print warning information"""
if _XSPDB_LOG_LEVEL <= logging.WARNING:
print(f"{YELLOW}[Warn] %s{RESET}" % msg, flush=True)
if _xspdb_enable_log:
_xspdb_logger.warning("[Warn] %s" % msg)
def build_prefix_tree(signals):
tree = {}
for signal in signals:
current = tree
parts = signal.split('.')
for part in parts:
if part not in current:
current[part] = {}
current = current[part]
return tree
def get_completions(tree, prefix):
parts = prefix.split('.')
current_node = tree
for i, part in enumerate(parts):
if i == len(parts) - 1:
break
if part not in current_node:
return []
current_node = current_node[part]
if prefix.endswith('.'):
return [prefix + v for v in current_node.keys()]
elif part in current_node:
return [prefix + "." + v for v in current_node[part].keys()]
else:
if "." in prefix:
prefix = prefix.rsplit(".", 1)[0] + "."
else:
prefix = ""
last_part = parts[-1] if parts else ''
candidates = list(current_node.keys())
completions = [prefix + c for c in candidates if c.startswith(last_part)]
return completions
def find_executable_in_dirs(executable_name, search_dirs="./ready-to-run"):
"""
Search for an executable file in the specified directories. If not found, search in the system path.
Args:
executable_name (str): Name of the executable file
search_dirs (list): List of specified directories
Returns:
str: Path to the executable file, or None if not found
"""
import shutil
for directory in search_dirs:
potential_path = os.path.join(directory, executable_name)
if os.path.isfile(potential_path) and os.access(potential_path, os.X_OK):
return os.path.abspath(potential_path)
return shutil.which(executable_name)
spike_dasm_path = find_executable_in_dirs("spike-dasm", search_dirs=["./ready-to-run"])
if not spike_dasm_path:
info(f"spike-dasm found, use captone to disassemble, this may cannot work for some instructions")
def dasm_bytes(bytes_data, address):
"""Disassemble binary data
Args:
bytes_data (bytes): Binary data
address (int): Starting address
Returns:
list: List of disassembled results
"""
if spike_dasm_path is not None:
# iterate over bytes_data in chunks of 2 bytes (c.instr. 16 bits)
instrs_todecode = []
full_instr = None
for i in range(0, len(bytes_data), 2):
c_instr = bytes_data[i:i+2]
if full_instr is not None:
full_instr += c_instr
instrs_todecode.append((int.from_bytes(full_instr, byteorder='little', signed=False),
full_instr[::-1].hex(), i-2 + address))
full_instr = None
continue
# Is full 32 bit instr
if c_instr[0] & 0x3 == 0x3: # full instr
full_instr = c_instr
continue
# Is compressed 16 instr
instrs_todecode.append((int.from_bytes(c_instr, byteorder='little', signed=False),
c_instr[::-1].hex(), i + address))
import subprocess
result_asm = []
# For every 1000 instrs, call spike-dasm
for i in range(0, len(instrs_todecode), 1000):
instrs = instrs_todecode[i:i+1000]
ins_dm = "\\n".join(["DASM(%016lx)" % i[0] for i in instrs])
# Call spike-dasm
bash_cmd = 'echo "%s"|%s' % (ins_dm, spike_dasm_path)
result = subprocess.run(bash_cmd,
shell=True,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
assert result.returncode == 0, f"Error({bash_cmd}): " + str(result.stderr)
ins_dm = result.stdout.strip().split("\n")
assert len(ins_dm) == len(instrs), "Error(%s): %d != %d\n%s vs %s" % (bash_cmd, len(ins_dm), len(instrs), ins_dm, instrs)
for i, v in enumerate(instrs):
result_asm.append((v[2], v[1], ins_dm[i] if "unknown" not in ins_dm[i] else "unknown.bytes %s" % v[1], ""))
return result_asm
try:
import capstone
except ImportError:
raise ImportError("Please install capstone library: pip install capstone")
md = capstone.Cs(capstone.CS_ARCH_RISCV, capstone.CS_MODE_RISCV32|capstone.CS_MODE_RISCV64|capstone.CS_MODE_RISCVC)
md.detail = True
md.skipdata = True
md.skipdata_setup = (".byte", None, None)
asm_data = []
for instr in md.disasm(bytes_data, address):
asm_data.append((instr.address, instr.bytes[::-1].hex(), instr.mnemonic, instr.op_str))
return asm_data
def register_commands(src_class, dest_class, dest_instance):
"""
Register all commands from src_class to dest_class.
"""
reg_count = 0
for cls_name in dir(src_class):
if not cls_name.startswith("Cmd"):
continue
cmd_cls = getattr(src_class, cls_name)
if not inspect.isclass(cmd_cls):
continue
for func in dir(cmd_cls):
if func.startswith("__"):
continue
func_obj = getattr(cmd_cls, func)
if not callable(func_obj):
continue
if not hasattr(dest_class, func):
setattr(dest_class, func, func_obj)
dest_instance.register_map[func] = src_class.__name__+"."+cls_name
reg_count += 1
elif has_override_tag(func_obj):
# If the function is overridden, replace it
old_func = getattr(dest_class, func)
func_obj.__old_func__ = old_func
setattr(dest_class, func, func_obj)
warn(f"Command {func} overridden in {dest_class.__name__} from {src_class.__name__}.{cls_name}")
else:
warn(f"Command {func} already exists in {dest_class.__name__}, ignoring")
continue
init = getattr(cmd_cls, "__init__", None)
if init:
init(dest_instance)
return reg_count
def load_module_from_file(filepath):
module_name = os.path.splitext(os.path.basename(filepath))[0]
spec = importlib.util.spec_from_file_location(module_name, filepath)
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
spec.loader.exec_module(module)
return module
def load_package_from_dir(dirpath):
module_name = os.path.basename(dirpath)
init_path = os.path.join(dirpath, "__init__.py")
assert os.path.exists(init_path), f"__init__.py not found in {dirpath}"
spec = importlib.util.spec_from_file_location(module_name, init_path)
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
spec.loader.exec_module(module)
return module
# decorators
def override(func):
"""
Decorator to override a function in a class.
"""
func.__is_xspdb_override__ = True
return func
def has_override_tag(func):
"""
Check if a function is overridden.
"""
return getattr(func, "__is_xspdb_override__", False)

258
scripts/xspdb/xspdb.py Normal file
View File

@ -0,0 +1,258 @@
#***************************************************************************************
# Copyright (c) 2025 Beijing Institute of Open Source Chip (BOSC)
# Copyright (c) 2025 Institute of Computing Technology, Chinese Academy of Sciences
#
# XiangShan is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
# http://license.coscl.org.cn/MulanPSL2
#
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
#
# See the Mulan PSL v2 for more details.
#***************************************************************************************
import os
import sys
import inspect
import pkgutil
import signal
import time
import pdb
from bdb import BdbQuit
from pyxscore import DUTSimTop, xsp
from pydifftest import difftest as df
from collections import OrderedDict
from logging import DEBUG, INFO, WARNING, ERROR
from xspdb.xscmd.util import load_module_from_file, load_package_from_dir, set_xspdb_log_level
from xspdb.xscmd.util import message, info, error, warn, build_prefix_tree, register_commands, YELLOW, RESET, xspdb_set_log, xspdb_set_log_file, log_message
class XSPdb(pdb.Pdb):
def __init__(self, dut, default_file=None,
mem_base=0x80000000,
flash_base=0x10000000,
finstr_addr=None,
default_mem_size=1024*1024*1024, # 1GB
default_flash_size=0x10000000,
no_interact=False,
):
"""Create a PDB debugger for XiangShan
Args:
dut (DUT): DUT exported by picker
df (difftest): Difftest exported from DUT Python library
xsp (xspcomm): xspcomm exported from DUT Python library
default_file (string): Default bin file to load
mem_base (int): Memory base address
finstr_addr (int): First instruction address
flash_base (int): Flash base address
default_mem_size (int): Default memory size
default_flash_size (int): Default flash size
no_interact (bool): No interact mode, default is False
"""
super().__init__()
self.dut = dut
self.df = df
self.xsp = xsp
self.mem_base = mem_base
self.finstr_addr = mem_base if finstr_addr is None else finstr_addr
self.flash_base = flash_base
self.flash_ends = flash_base + default_flash_size
self.no_interact = no_interact
self.interrupt_count = 0
signal.signal(signal.SIGINT, self._sigint_handler)
if no_interact:
info("Start XSPdb whit no_interact config, press Ctrl+C will interrupt the program")
self.dut_tree = None
self.prompt = "(XiangShan) "
self.in_tui = False
# Init dut uart echo
self.dut.InitClock("clock")
self.c_stderr_echo = xsp.ComUseEcho(dut.difftest_uart_out_valid.CSelf(), dut.difftest_uart_out_ch.CSelf())
self.dut.StepRis(self.c_stderr_echo.GetCb(), self.c_stderr_echo.CSelf(), "uart_echo")
# Init difftest
self.exec_bin_file = default_file
self.mem_size = default_mem_size
self.mem_inited = False
self.api_update_pmem_base_and_first_inst_addr(self.mem_base, self.finstr_addr)
if self.exec_bin_file:
assert os.path.exists(self.exec_bin_file), "file %s not found" % self.exec_bin_file
info("load: %s" % self.exec_bin_file)
self.api_init_mem()
self.df.InitFlash("")
self.xspdb_init_bin = "xspdb_flash_init.bin"
self.flash_bin_file = None
self.df.difftest_init()
self.difftest_stat = df.GetDifftest(0).dut
self.difftest_flash = df.GetFlash()
self.register_map = OrderedDict()
self.load_cmds()
self.api_init_waveform()
self.init_cmds = []
self.sigint_original_handler = signal.getsignal(signal.SIGINT)
self.sigint_callback = []
self.log_cmd_prefix = "@cmd{"
self.log_cmd_suffix = "}"
def run(self):
try:
self.set_trace()
while True:
dut.Step(1000)
except BdbQuit:
pass
def _sigint_handler(self, s, f):
self.interrupt = True
warn("[Ctrl+C] Interrupted")
for f in self.sigint_callback:
f(self)
if self.no_interact:
warn("No interact mode, exit(-1)")
raise sys.exit(-1)
self.interrupt_count += 1
if self.interrupt_count > 3:
warn("Too many interrupts, force entering pdb")
self.set_trace()
self.interrupt_count = 0
def __del__(self):
"""Destructor"""
if self.sigint_original_handler:
signal.signal(signal.SIGINT, self.sigint_original_handler)
def is_no_interact(self):
"""Check if no interact mode"""
return self.no_interact
def get_dut_tree(self):
"""Get the DUT tree"""
if self.dut_tree is None:
info("First time to use DUTree, build it ...")
self.dut_tree = build_prefix_tree(self.dut.GetInternalSignalList())
return self.dut_tree
def api_init_mem(self):
"""Initialize memory"""
if self.mem_inited:
return
self.df.InitRam(self.exec_bin_file, self.mem_size)
self.mem_inited = True
def api_update_pmem_base_and_first_inst_addr(self, a, b):
"""Set diftest PMEM_BASE and FIRST_INST_ADDRESS
Args:
a (int): PMEM_BASE value
b (int): FIRST_INST_ADDRESS value
Returns:
(PMEM_BASE, FIRST_INST_ADDRESS): current value of PMEM_BASE and FIRST_INST_ADDRESS
"""
if not hasattr(self.df, "Set_PMEM_BASE"):
warn("difftest.Set_PMEM_BASE not found, update your difftest")
warn("ignore memory PMEM_BASE set")
return None, None
if a is not None:
self.df.Set_PMEM_BASE(a)
if b is not None:
self.df.Set_FIRST_INST_ADDRESS(b)
x, y = self.df.Get_PMEM_BASE(), self.df.Get_FIRST_INST_ADDRESS()
if a is not None:
info("Set PMEM_BASE to %s (Current: %s)" % (hex(a), hex(x)))
if b is not None:
info("Set FIRST_INST_ADDRESS to %s (Current: %s)" % (hex(b), hex(y)))
return x, y
def load_cmds(self):
import xspdb.xscmd
cmd_count = self.api_load_custom_pdb_cmds(xspdb.xscmd)
info(f"Loaded {cmd_count} functions from XSPdb.cmd")
def api_load_custom_pdb_cmds(self, path_or_module):
"""Load custom command
Args:
path_or_module (string/Module): Command file path or directory (or python module)
"""
if isinstance(path_or_module, str):
if path_or_module.strip().endswith("/"):
path_or_module = path_or_module.strip()[:-1]
mod = path_or_module
if not inspect.ismodule(path_or_module):
if os.path.isdir(path_or_module):
mod = load_package_from_dir(path_or_module)
elif os.path.isfile(path_or_module):
mod = load_module_from_file(path_or_module)
return register_commands(mod, self.__class__, self)
else:
error(f"Invalid path or module: {path_or_module}")
return -1
# module
cmd_count = 0
for _, modname, _ in pkgutil.iter_modules(mod.__path__):
if not modname.startswith("cmd_"):
continue
submod = __import__(f"{mod.__name__}.{modname}", fromlist=[modname])
cmd_count += register_commands(submod, self.__class__, self)
return cmd_count
# override the default PDB function to avoid None cmd error
def parseline(self, line):
cmd, arg, line = super().parseline(line)
return cmd or "", arg, line
def do_xcmds(self, arg):
"""Print all xcmds
Args:
arg (None): No arguments
"""
cmd_count = 0
max_cmd_len = 0
cmds = []
for cmd in dir(self):
if not cmd.startswith("do_x"):
continue
cmd_name = cmd[3:]
max_cmd_len = max(max_cmd_len, len(cmd_name))
cmd_desc = f"{YELLOW}Description not found{RESET}"
try:
cmd_desc = getattr(self, cmd).__doc__.split("\n")[0]
except Exception as e:
pass
cmds.append((cmd, cmd_name, cmd_desc))
cmd_count += 1
cmds.sort(key=lambda x: x[0])
for c in cmds:
message(("%-"+str(max_cmd_len+2)+"s: %s (from %s)") % (c[1], c[2], self.register_map.get(c[0], self.__class__.__name__)))
info(f"Total {cmd_count} xcommands")
@staticmethod
def api_log_enable_log(enable):
xspdb_set_log(enable)
@staticmethod
def api_log_set_log_file(log_file):
xspdb_set_log(True)
xspdb_set_log_file(log_file)
def api_busy_sleep(self, data, delta=0.1):
for i in range(int(data//delta)):
time.sleep(delta)
if self.interrupt:
return (i+1)*delta
return data
def complete_xset_log_file(self, text, line, begidx, endidx):
return self.api_complite_localfile(text)
def record_cmd(self, cmd):
log_message(self.log_cmd_prefix + cmd + self.log_cmd_suffix)
def onecmd(self, line, log_cmd=True):
"""Override the onecmd to log the command"""
if log_cmd:
self.record_cmd(line)
return super().onecmd(line)