feat(tools): 仿真调试台适配新版显存,并修好 xsim 里跑 python 的环境问题

tb_saolei.v / tb_saolei_play.v:
- dump 从 1200 字节(1 字节/格)改成 2400 字节(每格 2 字节)
- 变量地址按新 ELF 更新(g_state 0x7AC、mine_bm 0x86C、g_ncells 0x8B4 ...)
- 已揭开格数直接读 opened_cnt(0x7B4),通关目标改成 g_ncells - g_mines

fb2png.py:
- 支持每格 2 字节(byte0 字形[5:0] / byte1 颜色[2:0])
- 字库不再手工抄表,改成直接解析 cpu21_vga.v 的 font_row(),RTL 加字模会自动跟随
- 字形 16..41 显示成 A-Z

play_saolei.tcl:
- 用绝对路径找系统 Python:xsim 的环境里 py 会选中 Vivado 自带的 python3.8,
  报 SRE module mismatch
- 清掉 PYTHONHOME/PYTHONPATH,否则系统 python 也会去 Vivado 的 lib 找模块
- render/save_shot 补 global python

实测:hard 模式 30x16 棋盘在终端正常显示,点击展开的区域横跨屏幕第 13~22 行
(含中线以下的 8 行)-> 证实 FB_WORDS 那个"只能写前 15 行"的 bug 已修好
This commit is contained in:
2026-09-23 21:21:32 +08:00
parent 10d4836ad7
commit 64428bdd85
4 changed files with 130 additions and 74 deletions
+67 -36
View File
@@ -9,43 +9,70 @@
dump 格式(tb_saolei.v 里的 dump_fb 任务生成):
CURSOR <row> <col>
CTRL 0x<byte> # [0] 光标可见 [6:4] 背景色
FB 1200 # 随后 1200 行十六进制字节,按格号 0..1199
FB 2400 # 随后 2400 行十六进制字节:每格 2 字节(字形、颜色)
渲染依据(与 RTL 严格对应,改 RTL 时这两个表要同步):
* 字库 cpu21_vga.v 的 font_row():8x8、bit7 是最左像素,放大 2 倍 = 16x16 一格
渲染依据(与 RTL 严格对应):
* 字库:直接从 cpu21_vga.v 的 font_row() 解析(8x8、bit7 最左,放大 2 倍 = 16x16 一格)
—— RTL 改了字库(比如新增 A-Z)这里会自动跟随,不用手工抄表
* 调色板 cpu21_vga.v 的 pal():3 位 -> 4:4:4 RGB(0xA 分量 = 170)
* 显存每格 2 字节:byte0 = 字形[5:0],byte1 = 颜色[2:0]
* 光标 cpu21_vga.v 的 cursor_box:格子四边一圈 2 像素宽的黄色框(常亮)
* 画面 40x30 格 -> 640x480
另外还会在终端打印一份 ASCII 图,方便在没有图片查看器的地方快速看结果。
"""
import os
import re
import struct
import sys
import zlib
COLS, ROWS = 40, 30
CELL_PX = 16 # 8x8 字模放大 2 倍
FB_BYTES = COLS * ROWS * 2 # 每格 2 字节(字形 + 颜色)= 2400
# --- 字库(8 行/字,bit7 = 最左),抄自 cpu21_vga.v 的 font_row() ---
FONT = {
0: [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], # 空白
1: [0x18, 0x38, 0x18, 0x18, 0x18, 0x18, 0x7E, 0x00], # 1
2: [0x3C, 0x66, 0x06, 0x0C, 0x18, 0x30, 0x7E, 0x00], # 2
3: [0x3C, 0x66, 0x06, 0x1C, 0x06, 0x66, 0x3C, 0x00], # 3
4: [0x0C, 0x1C, 0x3C, 0x6C, 0x7E, 0x0C, 0x0C, 0x00], # 4
5: [0x7E, 0x60, 0x7C, 0x06, 0x06, 0x66, 0x3C, 0x00], # 5
6: [0x1C, 0x30, 0x60, 0x7C, 0x66, 0x66, 0x3C, 0x00], # 6
7: [0x7E, 0x66, 0x0C, 0x18, 0x18, 0x18, 0x18, 0x00], # 7
8: [0x3C, 0x66, 0x66, 0x3C, 0x66, 0x66, 0x3C, 0x00], # 8
9: [0xFF, 0x81, 0xBD, 0xA5, 0xA5, 0xBD, 0x81, 0xFF], # 未揭开
10: [0x40, 0x70, 0x78, 0x70, 0x40, 0x40, 0x40, 0x00], # 旗
11: [0x3C, 0x7E, 0xFF, 0xFF, 0xFF, 0xFF, 0x7E, 0x3C], # 雷
12: [0x81, 0x42, 0x24, 0x18, 0x18, 0x24, 0x42, 0x81], # 爆炸
13: [0x3C, 0x66, 0x06, 0x0C, 0x18, 0x00, 0x18, 0x00], # 问号
14: [0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00], # 横线
15: [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF], # 实心
}
RTL_VGA = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)),
"..", "..", "pipelined-cpu", "srcs", "rtl",
"cpu21_vga.v"))
def load_font_from_rtl(path=None):
"""直接从 cpu21_vga.v 的 font_row() 里解析 8x8 字模,返回 {字形号: [8 行]}。
RTL 里每个字形形如
6'd16: font_row = (r == 3'd0) ? 8'h7C : (r == 3'd1) ? 8'h66 : ... ;
或全部相同行时形如
6'd15: font_row = 8'hFF;
两种情况都能解析。
"""
path = path or RTL_VGA
try:
with open(path, encoding="utf-8", errors="replace") as f:
text = f.read()
except OSError as e:
raise SystemExit("读不到 RTL 字库 %s:%s" % (path, e))
m = re.search(r"function\s*\[7:0\]\s*font_row.*?endfunction", text, re.S)
if not m:
raise SystemExit("%s 里找不到 font_row()" % path)
font = {}
parts = re.split(r"6'd(\d+)\s*:", m.group(0))
for i in range(1, len(parts) - 1, 2): # 奇数下标是字形号,后一格是它的分支体
idx, seg = int(parts[i]), parts[i + 1]
rows = [0] * 8
pairs = re.findall(r"\(r == 3'd(\d+)\)\s*\?\s*8'h([0-9A-Fa-f]{2})", seg)
if pairs:
for k, v in pairs:
rows[int(k)] = int(v, 16)
else: # 形如 "font_row = 8'hFF;":8 行一样
one = re.search(r"font_row\s*=\s*8'h([0-9A-Fa-f]{2})", seg)
if one:
rows = [int(one.group(1), 16)] * 8
font[idx] = rows
if not font:
raise SystemExit("%s 的 font_row() 没解析出任何字形" % path)
return font
# --- 调色板(3 位 -> RGB),抄自 cpu21_vga.v 的 pal(),4 位分量按 *17 扩到 8 位 ---
PAL = [
@@ -98,18 +125,24 @@ def parse_dump(path):
fb.append(int(line, 16))
except ValueError:
pass
if len(fb) != 1200:
raise SystemExit("显存字节数不对:%d(应为 1200)" % len(fb))
if len(fb) != FB_BYTES:
raise SystemExit("显存字节数不对:%d(应为 %d;新版每格 2 字节)"
% (len(fb), FB_BYTES))
return cur, ctrl, fb
def cell_of(fb, idx):
"""第 idx 格 -> (字形, 颜色):byte0 = 字形[5:0],byte1 = 颜色[2:0]。"""
return fb[2 * idx] & 0x3F, fb[2 * idx + 1] & 0x07
def cell_char(g):
"""字形号 -> 一个可见字符(字形 1..8 就是数字 1..8)。"""
if g in GLYPH_CH:
return GLYPH_CH[g]
"""字形号 -> 一个可见字符(1..8 数字,16..41 = A-Z,其余见 GLYPH_CH)。"""
if 16 <= g <= 41:
return chr(ord('A') + g - 16)
if 1 <= g <= 8:
return chr(ord('0') + g)
return ' '
return str(g)
return GLYPH_CH.get(g, '?')
def dump_note(path):
@@ -140,8 +173,7 @@ def draw_term(fb, cur, cursor_box, note=""):
for y in range(ROWS):
cells = []
for x in range(COLS):
cell = fb[y * COLS + x]
g, c = cell & 0xF, (cell >> 4) & 0x7
g, c = cell_of(fb, y * COLS + x)
r, gg, b = PAL[c]
ch = cell_char(g)
if cursor_box and (x, y) == cur:
@@ -151,7 +183,7 @@ def draw_term(fb, cur, cursor_box, note=""):
cells.append("\x1b[38;2;%d;%d;%dm%s" % (r * 17, gg * 17, b * 17, ch))
out.append("%2d " % y + "".join(cells) + "\x1b[0m\n")
out.append("\x1b[0m")
out.append("图例: #=未揭开 P=旗 *=雷 X=爆炸 ?=问号 1..8=数字 空格=已揭开的空地;反色格 = 光标\n")
out.append("图例: #=未揭开 P=旗 *=雷 X=爆炸 A-Z=文字 1..8=数字 空格=已揭开的空地;反色格 = 光标\n")
sys.stdout.write("".join(out))
sys.stdout.flush()
@@ -165,13 +197,13 @@ def main():
if quiet:
args.remove("--quiet")
if quiet: # 只出图不说话(给脚本批量截图用)
import os
sys.stdout = open(os.devnull, "w")
if not args:
raise SystemExit(__doc__)
src = args[0]
dst = args[1] if len(args) > 1 else src.rsplit(".", 1)[0] + ".png"
cur, ctrl, fb = parse_dump(src)
font = load_font_from_rtl()
bg = (ctrl >> 4) & 0x7
cursor_box = bool(ctrl & 1) and cur[0] is not None
@@ -190,9 +222,8 @@ def main():
cy, fy = y // CELL_PX, (y % CELL_PX) // 2
for x in range(COLS * CELL_PX):
cx, fx = x // CELL_PX, (x % CELL_PX) // 2
cell = fb[cy * COLS + cx]
g, c = cell & 0xF, (cell >> 4) & 0x7
px_on = (FONT[g][fy] >> (7 - fx)) & 1
g, c = cell_of(fb, cy * COLS + cx)
px_on = (font.get(g, [0] * 8)[fy] >> (7 - fx)) & 1
col = c if px_on else bg
if cursor_box and (cx, cy) == cur:
# 光标格:四边 2 像素黄色框,与 cpu21_vga.v 的 cursor_box 一致