最近在做那个SB2000 Mapper4游戏镜像修改工具的时候让WorkBuddy帮忙搜了很多资料,虽然近一段时间不会再继续跟进做SB2000相关项目了,但相关资料感觉应该放在这里存档,一方面方便别人查阅,另一方面将来万一自己要用,也会来这里找找看。下面就是一些纯粹的文档了,因为是WorkBuddy搜索来的,包括逆向分析也是WorkBuddy在做,所以不一定全都正确,使用的时候需要进一步验证和勘误。
SB2000机型开发资料来源
· SB2000开发资料汇编
· nesdev_NES20_Mapper602_SB2000
· DPCM技术调研
· 贴吧_说说SB2000的图片_原文存档
· 贴吧_小霸王SB2000编程技术_原文存档
SB2000开发资料汇编
SB2000 机型开发资料
建立 2026-10-01 | 本版修订 2026-10-03(第二版,剔除已证伪结论)
本文档只收录已确认可信的结论。 探索过程中被推翻的模型一律不再保留,
踩坑经验压缩为「规则」形式记录在第十四章,不复述过程。
可信度标记:
已证实(源码级 + 实测互证) | 源码级确定(读源码可定论,未实测) |
存疑 | 未验证
配套文档:
VirtuaNES编译指南.md(重编译补丁版模拟器)、
SB2000应用软件镜像分析.md(官方 100 张应用盘)、
贴吧_小霸王SB2000编程技术_原文存档.md、贴吧_说说SB2000的图片_原文存档.md
──────────────────────────────────────────────────
目录
1. 资料来源
2. 硬件规格
3. CPU 内存映射与分页
4. 启动与装载约定
5. BIOS 调用约定
6. 外设端口总表
7. DMA 传送
8. TIMER 与中断
9. 音频(DAC)
10. 显示系统
11. 素材与数据格式
12. 输入与其他外设
13. 工具链
14. 已验证成果
15. 模拟器补丁与自动验证
16. 工程规则与踩坑清单
17. 未解决问题与下一步
18. 附录:一句话速查
──────────────────────────────────────────────────
一、资料来源
# | 资料 | 类型 | 评价 |
1 | 百度贴吧《小霸王SB2000编程技术》(楼主 VB步步高,2025-06-27) | 爱好者逆向笔记 | 主体可靠、细节有笔误。全文见 贴吧_小霸王SB2000编程技术_原文存档.md |
2 | 官方《SB-2000系列多媒体学生电脑 使用手册》 | 原厂手册 | 最高可信度 |
3 | 官方宣传彩页 Flyer | 原厂资料 | 规格可信,勿作性能依据 |
4 | 本机 rev/vnes_src/emulator-bbk-sb2k/(VirtuaNES BBK/SB2K 分支,已打补丁) | 模拟器源码 | 本项目最实用的「硬件事实」代理 |
5 | docs/SB2000资料/SB2000应用软件镜像分析.md | 我方实测 | 官方 100 张应用盘清点 + API 定位 |
6 | BootlegGames Wiki: Subor SB-2000 | 百科 | 软件清单有用 |
7 | 机核《小霸王SB-2000:最强8bit学习机》 | 科普 | 自述「可能有所谬误」 |
8 | helloacm《Introduction to 8-bit Famicom Clone – Subor – SB2000》 | 博客 | CPU 型号写错(作 UM6561),应以 UM6576 为准 |
9 | caa-ins《数字媒体的实验考古(六)》 | 实验考古 | 提到镜像写法与 demoscene |
本机 curl 直连百度被风控拦截;archive.org 在当前网络不可达。
官方手册 PDF 需在浏览器中打开 https://cors.archive.org/download/sb-2000-manuals/SB-2000%20Manual.raw_text.pdf 下载。
──────────────────────────────────────────────────
二、硬件规格
项目 | 结论 | 依据 |
CPU | UM6576(联华电子),6502 内核,集成 PPU + APU + 键鼠接口 + FDC 控制 | 手册;模拟器变体名 NESVARIANT_UM6576 |
分辨率 | 256×240,32×30 字符 | 彩页;PPU_UM6576 |
ROM/BIOS | 512 KB(CPU 窗口 $8000-$FFFF) | 手册 |
主 RAM | 512 KB(EPRAM[512K]),bank $80-$FF | 模拟器源码 |
显存 | 512 KB 扩展 EVRAM(16 页 × 32 KB)+ PPU 内置 CRAM 32 KB | 手册;模拟器 |
调色板 | 64 项,UM6576 变体下位于 $2040-$207F | 模拟器源码 |
语音 | LPC 合成(德州仪器)+ PCM | 手册;彩页 |
软驱 | 3.5″ 1.44 MB,FAT12,与 PC 完全兼容 | 手册;本机实测 |
接口 | AT 键盘、RS232 鼠标、Centronics 并口、60 针卡带槽、2 手柄 | 手册 |
操作系统 | SB-DOS(含 AUTOEXEC.BAT)+ SB-Win98 | 手册 |
──────────────────────────────────────────────────
三、CPU 内存映射与分页
3.1 地址空间
地址 | 内容 | 说明 |
$0000-$1FFF | 工作 RAM 8 KB | 仅 UM6576 变体下为 8 KB;原版模拟器走 STD 时只有 2 KB |
$2000-$203F | PPU 图形端口 | 见第十章 |
$4000-$4FFF | PSG / 外设端口区 | 实际端口在 $4020-$43FF |
$5000-$57FF | 工作 RAM 2 KB | 8 KB RAM 的镜像 |
$6000-$7FFF | 外部 RAM 8 KB(ERAM) | |
$8000-$FFFF | 程序 bank 窗口,8 × 4 KB | 贴吧写作「8000-BFFF」有误,实际到 FFFF |
$0000-$1FFF 与 $5000-$57FF 不是镜像。模拟器实测:写 $0300 后读 $5300 无变化,
反向亦然。两段是独立 RAM。真机分布待实物验证。
3.2 程序分页寄存器 `$4040-$4047`
写入值 data(00-FF)→ bank 号
data < $80 → BIOS ROM:BIOS[data * $1000]
data >= $80 → 主 RAM :EPRAM[(data & $7F) * $1000]
· $4040→$8000-$8FFF,…,$4047→$F000-$FFFF
· 复位默认 bank[i] = i ⇒ $C000-$FFFF(bank 4-7)开机常驻 BIOS
· 软件起始 bank = $80,可用至 $EF = 112 × 4 KB = 448 KB(与贴吧「最大 448KB」一致)
窗口 ↔ 端口对照表(bank = m_bPbank[(addr>>12)&7],实测确认):
CPU 窗口 | 分页端口 | CPU 窗口 | 分页端口 |
$8000-$8FFF | $4040 | $C000-$CFFF | $4044 |
$9000-$9FFF | $4041 | $D000-$DFFF | $4045 |
$A000-$AFFF | $4042 | $E000-$EFFF | $4046 |
$B000-$BFFF | $4043 | $F000-$FFFF | $4047 |
极易踩坑:$4045 控制的是 $D000,不是 $8000。
若想读另一个 RAM bank 的数据(如音频/图案缓冲),必须用 $D000 窗口,
而 $8000 窗口里跑着程序自身 —— 读 $8000 只会拿到自己的机器码。
LDA #$84
STA $4040 ; $8000-$8FFF 映射到 RAM 偏移 ($84 & $7F)*$1000 = $4000
3.3 `$4300` / `$4301`
端口 | 功能 |
$4300 | 显存 bank(m_bVbank = data & 15,每页 32 KB,共 16 页) |
$4301 | 写 $55 = RAM 引导 / 软复位(清 IRQ、定时器、键盘、DAC、PPU 变体)——BIOS 独占的软盘 NES 模式切换口,三层结论见下表 |
$4301 三层结论(2026-10-05 定案):
层 | 结论 | 证据 |
硬件 / BIOS | BIOS 运行时使用:游戏装入 DRAM 后 STA $4301 ($55) 把 CPU 取指源切到 RAM(m_bRamBoot),同时抹掉全部 UM6576 扩展态(IRQ 全屏蔽 / 定时器 / 键盘 / DAC / 扩展手柄归零)→ 回到正常 NES 模式 | ① SB2K_BIOS.NES 全文唯一一处 STA $4301(偏移 0x5E894):LDA #$55 / STA $5006 / STA $4301 / JSR $2000=「装载完毕→复位→跳入新代码」交接序列;② MapperSB2K.cpp 补丁注释原文 __ITALIC__"$4301 is the soft-reset port and the BIOS writes it at run time"__ITALIC__;③ nesdev 原文 __ITALIC__"Used to run NES games from floppy disk"__ITALIC__(见附录 A.1) |
官方应用软件 | 不写。100 张盘提取的 EXE(含 29 张游戏盘 GAME.EXE、054_FC.EXE、SB-WIN.EXE)搜 STA/STY/STX $4301 与 LDA #$55 → STA $43xx 均 0 命中 | 与 $4031 同一设计原则:模式切换由 BIOS 独占,应用层不碰(《SB2000应用软件镜像分析》§五) |
模拟器 | 只做了一半:ReadHigh 的 m_bRamBoot RAM 取指切换已实现;但 PPU 变体已不再在 $4301 处切换(线程安全补丁固定在 NES::Reset() 一次性决定)⇒ RAM 引导生效、显示不回切标准 NES,「软盘跑 FC 游戏」路径尚不完整 | MapperSB2K.cpp case 0x4301;补丁注释:SetVariant() 跨线程 DELETEPTR(ppu) 会 use-after-free(即「未知错误」弹窗来源) |
官方软盘 FC 游戏的数据本体是否为原生 NES ROM 映像 = 未验证(GAME.EXE 只是小加载器,
游戏数据在裸扇区且带 key=$02 盘防,未解密逐字节比对)。
「官方软盘 FC 游戏 = 走 $4301 RAM 引导」目前是 BIOS 代码 + nesdev 原文支撑的强推断,非逐字节实证。
──────────────────────────────────────────────────
四、启动与装载约定
4.1 EXE 程序头(16 字节,不能省)
.BYTE $4C,$10,$80,$AA,$55,$00,$00,$00
.BYTE $00,$00,$00,$00,$00,$00,$00,$00
· $00 必须是 JMP(4C),$01/$02 = 跳转目标(惯例 $8010)
· $03/$04 = AA 55 = SB2000 软件专用标识
SB-DOS 加载器自检 $8000 处的 JMP $8010 与 AA 55,不匹配即拒绝加载。
tools/sb2k_mkdisk.py 输出的「程序头自检(4C 10 80): FAIL」即为明确信号。
不要把 .GAM 容器头(4D 59 "MY")误当作软件头。
4.2 AUTOEXEC.BAT
结论 | 说明 |
GAME.EXE 非必需 | 盘中只有 XXX.EXE + AUTOEXEC.BAT 即可自启 |
程序名任意、可带后缀 | 应用盘 AUTOEXEC = sb-win.exe |
行尾必须 CRLF | 只写 memdump(无 \r\n)→「命令输入错」 |
多行批处理可用 | dir\r\nmemdump\r\n 依次执行 |
──────────────────────────────────────────────────
五、BIOS 调用约定
5.1 调度器入口
项 | 约定 |
调用方式 | JSR $FFF0(20 F0 FF) |
功能号 | 零页 $063E |
通用参数 | 零页 $063F |
LDA #$19
STA $063E
LDA #$04
STA $063F
JSR $FFF0
调度器结构($FFF0 -> $FD8E,反汇编实测):
· fn < $10:查 $05FB 三字节表(向量 + bank)
· fn $10-3A:查 $E082 双字节表
· fn $47-82:分段映射 BIOS ROM 16 KB 窗($ED94 连设 $4040-$4043)再 JMP ($0010)
· fn $80:查 $0391 标志选驱动体
· 出口自动恢复 $4040-$4043(从 $0639-$063C)——自研软件无需关心 bank
5.2 底层块读写(fn `$80`)
完整调用序列(逐条抄自官方 GAME.EXE,缺一步即挂死):
1 $063E=$80, $063F=$00, JSR $FFF0 ; FDC 初始化
2 $063E=$7C, $063F=$03, JSR $FFF0 ; 取会话密钥 -> $6300
3 key = $6300; $6300 = 0
4 $6207 = $80 ; 非 $80 时 BIOS 内部无限重试
5 $6200 = 磁头 ^ key ; ┐ CHS 寻址
$6201 = 扇区 ^ key ; │ 磁头 0-1
$6202 = 磁道 ^ key ; │ 扇区 1-18
$6206 = 0 ; │ 磁道 0-79
$6205 = (512B扇区数) ^ key ; ┘ 18×2×80 = 2880 = 1.44MB
$6205 ×8 (ASL/ROL ×3) ; 4KB 块 -> 512B 扇区数
6 $6203 = 目标 RAM bank (不 XOR)
$6204 = 0 ; 零页指针增量,普通用法恒 0
7 $063E=$80, $063F=$04, JSR $FFF0 ; 执行读写 -> 状态在 $6208
关键语义:
项 | 结论 |
参数是 CHS,不是线性扇区 | $6200=磁头(0-1)、$6201=扇区(1-18)、$6202=磁道(0-79)。证据:BIOS $8F9B 范围检查 + $901D 进位顺序 |
状态/错误码在 $6208 | $00=成功、$10=参数越界、$18=读失败、$1A=重试耗尽。不是 $6300($6300 恒 0,易造成「成功假象」) |
数据落点 | $6203 指定 RAM bank 偏移 0 起,每 8 个扇区(4 KB)bank+1($8FE8) |
防拷贝密钥 | 官方盘 key=$02(参数表密文反推),自研盘 key=0(不 XOR)即可 |
5.3 读盘前置条件(模拟器实测,缺一即挂死)
# | 条件 | 违反后果 |
1 | CLI + $4032 位3 = 0(如 $4032=$00) | FDC IRQ 被门控 → 驱动超时/挂死。 BIOS 磁盘驱动是中断驱动 |
2 | 调用期间不得破坏 $4044-$4047 | 调度器出口只恢复 $4040-$4043。启动实测值 $4044=$04 / $4045=$05(ROM 块,磁盘驱动本体) |
3 | 文件名必须带 "A:\" 前缀 | 裸名令路径解析挂死 |
4 | FDC 芯片仿真在 $4200-$4207 | — |
实测矩阵:SEI+$4032=$FF → init 挂死;CLI+$4032=$FF → init 成功但 open 挂死;CLI+$4032=$00 → 全链路成功。
2026-10-05 重大更正:上面这条"全链路成功"是误判!
这套 $7B/$7C + $6543/$6560/$6561/$6569/$656B 的接口
从未真正交付过数据。当时判定"成功"的依据是
open 返回码为 0、以及 A/B 对照中 dump 增量恰好等于基线,
但那两者都无法区分"读到了数据"与"什么都没做"。
真正的读软盘 API 见 §5.4「读软盘到内存:BIOS 功能号 $80」。
本节保留仅为记录这段弯路。
5.3.1 `$4045` 的双用途陷阱(2026-10-04 实测,踩了 7 轮)
$4045 既是 CPU $D000-$DFFF 的 bank 窗口,又是磁盘驱动本体所在的映射页
(启动实测值 $4045=$05)。两个用途共用一个寄存器,后果是:
做法 | 后果 |
写 $4047=$07 让 $FFF0 指向 BIOS | open 单次调用能返回 0(假成功),但后续 info/read 全部挂死 |
写满 $4040-$4047=$00-$07 | 立即弹「发生未知错误」,DOS 环境被搅坏 |
读盘时把 $4045 设成目标 bank | BIOS 跳错代码,dump 增量为 0(一个样本都读不出) |
$4040-$4047 一个字节都不动 | 唯一正确做法 |
正确模型:
$656B = 磁盘 API 的目标 bank(另一套独立编号)
$4045 = CPU $D000 窗口的 bank
读盘前: $4045 必须保持 $05(BIOS 调用期间)
读盘后: 想看数据才临时 $4045 = $656B
看完: 立刻 $4045 = $05 还回去, 再调下一次 BIOS
probe17 实测:$656B=$8A + $4045=$8A 才能在 $D000 读到数据;
漏掉"还回去"这一步,下一次读盘必挂。
缓冲区 bank 用 $8A 一类低位编号(probe17/VCAL46 实测有效);
试过 $E0 段,第 2 块起读到的是垃圾。
5.3.2 DAC 开启后不能读盘(VCAL58 实测)
阶段序列 | 结果 |
open → info → read → read(完全不碰 DAC) | 全部成功(VCAL57) |
open → info → 写 $4034 开 DAC → read | 读盘挂死 |
原因:$4034 bit7 打开后模拟器 DAC 线程开始工作,与 FDC 磁盘传输争用
emulated CPU,磁盘例程无法完成。这是模拟器实现限制,真实硬件不存在。
⇒ 读盘与送样必须交替进行(读一块立刻播一块),不能"先全读再全播";
也不能"播完一块再读下一块"而中间不还 $4045。
5.4 读软盘到内存:BIOS 功能号 $80( 原 §5.4 的 $656B 方案是错的 )
本节是2026-10-05 的重大更正。
之前我们记录的「高层文件 API」($7B 打开 / $7C 信息 / $7C/$0F 读块,
参数在 $6560/$6561/$6569/$656B,文件名走 $6543)——这一整套从未被实证,
用它写的 12 个探针(VCAL46~VCAL79)全部读不到数据。
正确做法:BIOS 功能号 `$80`(原帖第5楼 + MTV 播放器实证)
贴吧《小霸王SB2000编程技术》第 5 楼「BIOS调用」:
BIOS功能号=80 功能=读软盘到内存
调用序列(照搬 MTV 播放器 738 字节机器码 8030-809D):
; ---- 1. 功能号与固定参数 ----
LDA #$80
STA $063E ; BIOS 功能号 = 80
LDA #$04
STA $063F ; 原帖:"为0读1盘"(MTV 实际写 $04)
LDA #$80
STA $6207 ; 固定值 $80(原帖勘误明确说"6207固定为80")
; ---- 2. CHS 定位( 按物理扇区,不是文件系统!)----
LDA #$00
STA $6200 ; 磁头 H
LDA #$01
STA $6201 ; 首扇区 S(1-based,范围 1-18)
STA $6202 ; 磁道 C
; ---- 3. 目标 bank 与长度 ----
LDA #<起始BANK>
STA $6203 ; 本次写入的目标起始 bank
STA $00F3 ; 当前 bank 指针(MTV 循环时每轮 += 步长)
LDA #<BANK数>
STA $00F4 ; 步长 = 读入的 bank 数
STA $6205 ; 16 位数的低字节
LDA #$00
STA $6206 ; 高字节
; MTV 的做法:$6205:$6206 = $00F4 << 3(16 位左移 3 次)
; 因为 $00F4 << 3 = bank 数 × 8
LDA $6205
ASL A
ROL $6206
ASL $6205
ROL $6206
ASL $6205
ROL $6206
; 原帖:"04和05通常为00,06不能为00"
; ⇒ 步长不能太小。STEP=32 → $6206=$01(非0)最安全
LDA #$00
STA $6204
; ---- 4. 调用 ----
JSR $FFF0 ; 一次性读入 $00F4 个 4KB bank
参数语义(从 MTV 反汇编逐条确认)
地址 | 含义 | MTV 实例 |
$063E | BIOS 功能号,$80 = 读软盘到内存 | $80 |
$063F | 参数 | $04 |
$6200 | 磁头 H | $00 |
$6201 | 首扇区 S(1-based) | $01 |
$6202 | 磁道 C | $01 |
$6203 | 目标起始 bank | $80(随循环变化) |
$6204 | 通常 $00 | $00 |
$6205/$6206 | bank 数 ×8(16 位) | $00/$04 |
$6207 | 固定 $80 | $80 |
$00F3 | 当前 bank 指针,每轮 $00F3 += $00F4 | 循环 $80→$00 |
$00F4 | 步长 = 读入 bank 数 | $80(128 bank = 512KB) |
LBA 换算(1.44MB 标准几何)
LBA = 磁道C × 36 + 磁头H × 18 + (首扇区S − 1)
例:读簇 2(我的镜像 data_sec=33)
C=0, H=1, S=16($10) → 0×36 + 1×18 + 15 = 33
这解释了此前所有失败
19. $656B 那套参数根本不是读软盘 API。它可能是 SB-DOS 的更高层封装,
依赖 DOS 已初始化状态;我们从 AUTOEXEC 里直接调,DOS 上下文不完整 ⇒ 静默失败。
20. fn=$80 是裸扇区读,绕过文件系统。所以
· 不需要文件句柄、不需要文件名
· 读进来的数据按 bank 顺序连续排列
· 播放器必须自己知道数据在软盘的哪个位置(原帖:$4800 是 WAV,$84800 起是图片)
21. VCAL77 崩溃的原因:bank 数 = 1 ⇒ $6205/$6206 = $00/$00,
违反「$06 不能为 00」。
仍未解决
VCAL76~VCAL79 用这套 API 读盘后,目标 bank 里仍未观察到数据(屏幕纯黑 = DATAV $00)。
原版 MTV 机器码在我们制盘的镜像上也是立即返回 DOS 提示符。
⇒ 下一步应逐字节对照原帖的磁盘布局(EXE 在 $4200、autoexec 在 $4600、
WAV 在 $4800、图片在 $84800)重建镜像,再跑MTV 原版机器码。
MTV 的 $67FB 参数表实测值是 00 01 01 80 FF。
5.4 高层文件 API
步骤 | 调用 | 参数区 |
打开 | fn $7B/$09 | 名 → $6543("A:\名.EXT"),模式 → $6560(0 读) |
读句柄 | — | 返回 $6561 |
信息 | fn $7C/$0D | 长度 → $6568-6A |
读数据 | fn $7C/$0F | 目标 bank → $656B |
关闭 | fn $7B/$0A | 错误码 → $6300 |
每次调用只读 256 字节到 4 KB RAM bank。读 512 字节必须指定两个不同 bank,
否则第二次覆盖第一次(VCAL47 实测踩过)。
写入路径的两个已知怪癖(V2 实测,根因未查):
22. 关闭时把目录 size 与 FAT 链写坏(数据本身按线性落盘完好);
23. 首次 4096 B 大块写的首个 512 B 落盘为零。
⇒ 大批量写盘应用底层 fn $80/$05 直写 + 制盘器预建 FAT 文件。
5.5 BIOS RAM 工作区(重要边界)
区域 | 用途 |
$0700-$0713 | 显示 / 零页辅助,安全 |
$0718-$074F | BIOS fn $80/$04/$05 的 RAM 工作区 —— 禁止写 |
$0780-$07AF | 程序状态,安全 |
实测:变量放 $0744+ → 首次写盘即 ST=$A9;放 $0714-$0721 → 前 117 bank 后在 $F6 冻结;
放 $0780+ → 512 KB 全程 ST=$00 通过。
5.6 PRECALL / POSTCALL 顺序
· fn $80/$00(init)之后不能立刻 POSTCALL:此刻 SNAPH 未快照,会把垃圾写进
$4044-$4047 破坏 BIOS ROM 窗 → 首个写盘 ST=$85
· 正确顺序:PRECALL → init → 读 $6300 → 快照 $4044-$4047 → 之后才配对 POSTCALL
5.7 `$4031` UM6576 模式序列
LDA #$65
STA $4031
LDA #$76
STA $4031 ; 触发 SetVariant(NESVARIANT_UM6576)
副作用:SetVariant 会 delete 并重建 PPU 对象,调色板与 PPU 寄存器全部回默认值
(CRAM 内容保留)⇒ 切换后必须重写调色板与 $2000/$2001/$2008。
在原版(未重编译)模拟器上,EXE 运行期写 $4031 无法推进到 UM6576 模式,
因为 Nes::Reset() 强制回 STD 变体。补丁版已绕过此限制,见第十、十五章。
──────────────────────────────────────────────────
六、外设端口总表
端口 | 功能 |
$4020-$4022 | 键盘(时序控制 / 输出移位寄存器 / 时钟与方向) |
$4023 | 外设选择:0=手柄,1=鼠标 |
$4024/$4025 | 鼠标数据 |
$4026 | 扩展 I/O |
$4027 | DAC 数据(详见第九章) |
$4014 | 精灵 OAM DMA( 实测有效;贴吧作「$2014」是笔误) |
$4031 | UM6576 模式序列($65→$76) |
$4032 | IRQ 屏蔽:位7=TIMER、位5=键盘、位3=FDC。写 1 = 屏蔽,写 0 = 允许并清状态 |
$4033 | IRQ 状态(只读):位7=TIMER、位5=键盘、位3=FDC |
$4034/$4035/$4036 | 定时器控制 / 重载值 / 当前计数 |
$4040-$4047 | 程序分页(各 4 KB) |
$4048-$404F | DMA(详见第七章) |
$4200-$4207 | FDC 软驱控制器 |
$4300 | 显存 bank(0-15) |
$4301 | 写 $55 = RAM 引导 / 软复位(BIOS 独占,官方软盘 FC 游戏用此进 NES 模式,三层结论见 §3.3) |
$4302 | 语音口(LPC/PCM) |
贴吧已勘误的三处
贴吧说法 | 实际 |
置 $4033 位7=1 启用 TIMER(A9 80 8D 3340) | $4033 只读。正确 = $4034 启动 且 $4032 解除 bit7 屏蔽 |
BIOS 检查 $4033 位8 | 应为位7 |
$4035=$FF 是 8000 bps 档 | 按源码公式只有 ≈406 Hz(见第九章) |
──────────────────────────────────────────────────
七、DMA 传送 `$4048-$404F`
源码 MapperSB2K::WriteLow 语义,与贴吧逐字段吻合 :
src_addr = ((Reg1 & 15) << 15) | ((Reg3 & 0x7F) << 8) | Reg2;
p_src = ((Reg1 & 0x10) ? EPRAM : BIOS) + src_addr; // bit4 选源
dst_addr = (Reg5 << 8) | Reg4; // 16 位
len = (Reg7 << 8) | Reg6;
if (len & 1) len++; // 奇数自动 +1
if (Reg0 & 0x20) { /* Work RAM 模式 */ }
else {
if (dst_addr & 0x8000) p_dst = EVRAM + (dst_addr & 0x7FFF) + m_bVbank * 0x8000;
else p_dst = CRAM + dst_addr;
memcpy(p_dst, p_src, len);
}
Reg | 端口 | 含义 |
0 | $4048 | bit7=1 启动(必须最后写);bit5=0→VRAM,=1→Work RAM |
1 | $4049 | bit4=1→源为主 RAM EPRAM,=0→BIOS ROM;bit3-0 = 32 KB bank 号 |
2/3 | $404A/B | 源偏移低/中字节 |
4/5 | $404C/D | 目的地址低/高字节(bit15 决定 CRAM/EVRAM) |
6/7 | $404E/F | 长度(字节,偶数向上取整) |
· 源 bank 是 32 KB 粒度:$00-$0F = BIOS ROM 512 KB,$10-$1F = 主 RAM 512 KB
· 源 bank 换算:CPU 窗口 $90-$97 = EPRAM $10000-$17FFF ⇒ Reg1=$12 对应 EPRAM+$10000
· 应用套路:配合 $4300 切显存 bank 循环 16 次,可把软盘读出的 512 KB 图片数据
一次一张铺满 512 KB 显存(= 16 张 32 KB 全屏图)
标准写法(VRAM 模式,32 KB):
LDA #$00 ; Reg0: bit5=0(VRAM), 启动位留后
STA $4300 ; 选显存页
LDA #$10 ; Reg1: bit4=1(EPRAM) + bank0
STA $4049
LDA #<DATA ; Reg2
STA $404A
LDA #$00 ; Reg3
STA $404B
LDA #$00 ; Reg4 -> 目的 $8000
STA $404C
LDA #$80 ; Reg5 -> bit15=1 ⇒ 写 EVRAM
STA $404D
LDA #$00 ; Reg6 -> 长度 $0400
STA $404E
LDA #$04 ; Reg7
STA $404F
LDA #$80 ; Reg0: 启动,必须最后写
STA $4048
DMA 走 MapperSB2K::ExWrite,完全不受 $20xx 地址掩码影响,
是三条写入路径中最可靠的一条。
──────────────────────────────────────────────────
八、TIMER 与中断
8.1 寄存器
端口 | 含义 |
$4034 | bit7=启动、bit6=连续(自动重载)、bit5=时基(0=CPU 1.66 MHz,1=15.625 kHz)、低 4 位=预分频 (n+1) |
$4035 | 重载值(写入同时装入当前计数) |
$4036 | 当前计数值(可读) |
8.2 频率公式
递减率 = 1662607 / (($4034 & $0F) + 1) ← 只由预分频决定,与 reload 无关
IRQ 频率 = 1662607 / (($4034 & $0F) + 1) × ($4035 + 1)
实测:$4034=$CF / $4035=$F0 ⇒ 16 × 241 = 3856 周期 ≈ 431.2 Hz(MTV 程序所用档)。
8.3 IRQ 屏蔽语义
$4032 位7 = 1 屏蔽 / 0 允许(位5=键盘,位3=FDC 同理)。两重证据:
24. 探针实测:解除屏蔽后 IRQ 计数 +2;
25. 官方应用软件的 DAC 播放例程在每次等待下一次 IRQ 前坚持
$4032=$FF(全屏蔽=应答)→ $4032=$7F(bit7=0 → 只允许 TIMER IRQ)。
若 bit7 是「允许」,写完 $7F 后不可能再等到 IRQ,必然死锁 ⇒ 官方代码本身即极性证明。
8.4 BIOS TIMER 分发
BIOS IRQ handler 检查 $4033 后调用零页 ($24) 指针程序(音乐程序写 $24/$25,
CLI 开 IRQ)。BIOS 黑盒未完全逆向,但官方应用软件的 IRQ 驱动 DAC 播放例程印证了该结构。
8.5 TIMER 使用的两个陷阱
26. $4036 只能读一次 —— 比较与更新基准若用两次读数,中间跨过重载会永久丢失该轮回绕
(实测漏 5/40 轮)。正确做法:单次读数,复用同一个 A。
27. 停表后必须停止轮询 —— $4034=0 后 $4036 冻结,新值==旧值 会被误判为「回绕」
而虚增计数。用软件标志(如 TACT)在轮询入口直接返回。
──────────────────────────────────────────────────
九、音频(DAC)
9.1 两条完全独立的音频路径(关键认知)
路径 A(NES 主音频 / APU):
apu->Process() → DirectSound 缓冲 → g_WaveRec.Out() ← 内置录音抓这条
位置:EmuThread.cpp:1156
路径 B(SB2000 DAC):
$4027 → dpp → pcmBuf → wavDAC.Write() → CWaveOut ← DAC 走这条
位置:MapperSB2K.cpp(独立线程 DACThreadProc)
⇒ VirtuaNES 的 WAV 录音功能抓不到 SB2000 DAC 的声音。
实测 wave/SB2K_BIOS.wav(624,750 字节)中 624,706 个样本全部等于 128(8-bit 静默值)。
这解释了「用户能听到蜂鸣,但录音全静音」的矛盾。
但路径 A(APU)已实测可用(2026-10-04,VCAL60)。
APU 走 apu->Write() 完全不经过 mapper171,与 FDC 无争用,
内置录音能抓到,详见 §12.4。
⇒ 建议:新的音频需求优先走 APU,而不是 DAC。
DAC 这条路在模拟器上有并发争用问题(§5.3.2),且只能靠补丁版的
dump_dac.pcm 观察,用户无法直接试听。
APU 侧现已实测打通两条路:
- 合成器通道(方波/三角波/噪声)→ §12.4
- DPCM 采样回放(可播原始音频数据)→ §12.5
⇒ 音频播放器的推荐实现:DPCM 回放。 它既能播放任意音频素材,
又完全避开 DAC 的所有坑。
9.2 采样率与分频
// MapperSB2K::ExWrite(0x4027) —— 首次写时锁存
div = ((m_bReg4034 & 15) + 1) * (m_bReg4035 + 1);
m_nDACRate = 1662607 / div;
wavDAC.Open() 会向上表 {8000, 11025, 16000, 22050, 32000, 44100, 48000}
取最接近的标称率打开。
$4034 | $4035 | div | 实际率 | 标称率 | 误差 |
$80 | $28 | 41 | 40551 Hz | 44100 | −8.0% |
$80 | $4A | 75 | 22168 Hz | 22050 | +0.5% |
$80 | $67 | 104 | 15986 Hz | 16000 | +0.09% 本项目采用 |
$80 | $95 | 150 | 11084 Hz | 11025 | +0.5% |
$80 | $CF | 208 | 7993 Hz | 8000 | −0.1% |
$81 | $C0 | 386 | 4307 Hz | 44100 | — |
贴吧「$CF/$FF = 8000 bps 档」是错的:16×256 = 4096 ⇒ 仅 ≈406 Hz。
实物 MTV 程序用 $CF/$F0(≈431 Hz)。而 $80/$67 档可达 ≈16 kHz,
足够播放音乐(见 9.6)。
9.3 送样率必须与播放率匹配
CPU 实际送样速率由 cycles 决定:
CPU 送样率 = 1662607 / (每样本 cycles)
wavOut 率 = 1662607 / div
失配 → 缓冲区欠载/过载,听感是断续、变调、爆音
匹配条件:每样本 cycles == div
送样循环 cycles 拆解(参考实现):
PL_LOOP:
LDA BEATL ; 3
BNE PL_NZ ; 2
LDA BEATH ; 3
BNE PL_NZ ; 2
DEC BEATL ; 5
LDA PHASE ; 3 ┐
BNE PL_HI ; 2/3 ┤ 状态读取
LDA LO ; 2 ┘ 必须是立即数 LDA #LO
STA $4027 ; 4 写样本
JSR SAMPDLY ; 6
DEC CNT ; 5
BNE PL_LOOP ; 3
SAMPDLY:
LDX DLOOP ; 2
SD1: DEX ; ┐
BNE SD1 ; ┘ 5/轮
LDX DNOP ; 2
SD2: DEX ; ┐
BNE SD2 ; ┘ 5/轮
RTS ; 6
每样本 div = 33 + 5 × DLOOP + 5 × DNOP
最低 div = 33(DLOOP=0, DNOP=0),最高送样率约 50 kHz
音高公式:方波频率 = 送样率 / (2 × BLK)
9.4 DAC 输出格式与 dpp
· 输出 pcmBuf[n] = (data ^ 0x80) << 8,即无符号 8 位 → 有符号 16 位,零点在 128
· DAC 是逐样本原样输出,不做任何重采样 ⇒ 素材采样率必须等于 1662607/div,否则变调
· dpp 不是重采样器,而是消爆音峰值保持滤波器;连续累积 > DAC_FADE_THRESHOLD(512)
会整块丢弃 ⇒ 单次连续喂入不宜超过 512 字节,不能靠连续狂写 $4027
· CPU 写入是阻塞式(DAC 线程缓冲满时 STA $4027 自然阻塞),无法超量预填
· $4034 bit7 = DAC 开/关;TimerTick 在 DAC 开启时把 tick ×1.5(模拟器防欠载)
· IsThrottleRequired() 在 m_bDACOn 时返回 TRUE ⇒ 写 DAC 的程序在模拟器里跑得很慢
9.5 送样率匹配实例
VCAL39($4034=$80 / $4035=$67 ⇒ div=104):
项 | 值 |
wavOut 播放率 | 15,986 Hz |
CPU 实际送样率 | 15,960 Hz(16,000 样本 / 1,703,936 cycles) |
失配 | 0.16% |
LDX #$08 / DEX / CPX #$00 / BNE(8 轮)+ 循环固定开销 = 104 cycles/样本,与 div 精确对齐。
9.6 分块播放架构(VCAL47,已实现)
BIOS 文件 API:每次读 256 字节到 4 KB RAM bank
每帧 (1/50 s = 20 ms) 需 15986/50 = 320 样本
=> 每缓冲 512 样本(2 bank × 256)= 32 ms 播放时间,留 12 ms 余量给读盘
双缓冲:A($8A + $8C) / B($8B + $8D) 轮换
离线模拟结果:
项 | 值 |
BIOS API | open(OK) → info(len=220500B) → read(bank=$8A) → read(bank=$8C) |
DAC | $4034=$80 $4035=$67 → div=104 → 15986 Hz |
送样 | 193,546 样本 ≈ 12.11 秒 |
波形 | $87,$85,$84,$83,$82,$81,$80,$7F —— 真实音乐波形 |
素材必须预重采样到目标档位:
文件 | 采样率 | 大小 | 时长 |
assets/audio/still_alive.pcm | 11084 Hz | 221,680 B | 20.00 s |
assets/audio/still_alive_11025.pcm | 11025 Hz | 220,500 B | 20.00 s |
assets/audio/still_alive_15986.pcm | 15986 Hz | 319,720 B | 20.00 s |
9.7 RAM bank 窗口机制
// MapperSB2K.cpp:1343
bank = m_bPbank[(addr >> 12) & 7];
if (bank < 0x80) return BIOS[bank*0x1000 | (addr&0x0FFF)];
else return EPRAM[(bank & 0x7F)*0x1000 + (addr & 0x0FFF)];
$4045 写入的是 m_bPbank[5],控制 CPU $8000-$8FFF 的 4 KB 窗口。
磁盘 API 的 $656B 是另一套 bank 号(目标 RAM bank),两者独立。
──────────────────────────────────────────────────
十、显示系统
本章是全项目最容易出错的地方,请务必先读 10.1。
10.1 首要事实:显示模型取决于模拟器构建版本
同一份 SB2000 程序,在原版 EXE 与补丁版 EXE 下走的是两条完全不同的渲染路径:
原版 VirtuaNES.exe | 补丁版(重编译,mapper171 强制 UM6576) | |
PPU 类 | PPU_STD | PPU_UM6576 |
$20xx 地址 | 被 & 0xE007 掩码塌缩 | 完整地址 |
调色板 | $3F00-$3FFF(标准 FC 语义) | $2040-$207F(64 项) |
$2008 | 不存在(被掩码成 $2000) | 色彩模式寄存器(bit7=16 色) |
色彩模式 | 仅 2 色 BG | 4 色 / 16 色 |
命名表 | $2000/$2400/$2800/$2C00,1 字节/格 | $0000/$0800/$1000/$1800,2 字节/格 |
图案区写入 | $2007 写 $0000-$1FFF 被丢弃 | $2007 可写 CRAM $0000-$7FFF |
BG 使能位 | $2001 bit3 | $2001 bit3(一致 ) |
原版走 PPU_STD 的原因(源码级定案):
位置 | 代码 | 含义 |
Nes.cpp:194 | m_Variant = NESVARIANT_STD; | 默认就是 STD |
Nes.cpp:236 | ppu = new PPU_STD(this); | 初始实例是 STD |
Nes.cpp:1141 | $4031 状态机 | 唯一能触发 SetVariant 的路径 |
Nes.cpp:488 | Nes::Reset() 强制回 STD | 任何 Reset 都退回 PPU_STD |
⇒ EXE 运行期写 $4031=$65/$76 无法推进到 UM6576 模式,
所以 PPU_UM6576 从未被实例化,addr & 0xE007 掩码始终生效。
掩码塌缩表(原版,写调色板 = 乱写控制寄存器):
写入 | 变成 | 后果 |
$2008 | $2000 | UM6576 特有寄存器,标准 PPU 无此寄存器 |
$2040 | $2000 | 命名表基址被污染 |
$2041 | $2001 | PPUMASK 被写(破坏性最大) |
$2043 | $2003 | OAM 地址指针 |
$2044 | $2004 | OAM 数据口 |
$2045 | $2005 | 滚动寄存器被污染 |
$2046 | $2006 | GA 高字节被污染 → 之后所有 $2007 写到错误地址 |
$2047 | $2007 | VRAM 数据口 |
该掩码已在原版二进制中确认(VirtuaNES.exe offset 0x094511:AND EDX,0xE007,
紧邻 MOV EDX,EDI = addr)。
这也是「改了调色板却看到画面变化」的真相 —— 那不是颜色生效,
而是控制寄存器被改的副作用。「整屏纯白」的经典伪证据同理:
$2041→$2001 把 BG 关掉,整屏填 BGPAL[0],而 BGPAL[0] 恰好被改成白色。
判断调色板是否生效,绝不能只看画面颜色。
10.2 原版 EXE:GA 可写性总表(`PPU_STD`)
MapperSB2K::Reset() 的 bank 映射 + PPU::Reset() 的 bVromWe = 0 共同决定:
GA 范围 | bank | 映射目标 | 类型 | $2007 可写 |
$0000-$1FFF | 0-7 | EVRAM | BANKTYPE_VROM | 写入被丢弃 |
$2000-$2FFF | 8-11 | CRAM | BANKTYPE_CRAM | |
$3F00-$3FFF | — | 直接改 BGPAL/SPPAL | — |
// PPU_STD::Write 的 $2007 分支
if( PPU_MEM_TYPE[bank] != BANKTYPE_VROM ){ PPU_MEM_BANK[bank][vaddr] = data; }
else if( bVromWe ) { ... } // ← bVromWe=0 时也不写
其余语义(标准 FC 路径):
· GA 只有 14 位($0000-$3FFF),$4000 及以上被截断/镜像
· 调色板在 $3F00-$3FFF
· $2006 是标准 FC 的 loopy 路径:第一次写高 6 位,第二次写低 8 位
· $2005 是纯 X/Y 滚动寄存器
⇒ 原版 EXE 下,图案数据只能靠 DMA 写 EVRAM。 这不是可选加速通道,
而是当时唯一能把图形数据送进图案区的手段。
10.3 补丁版 EXE:UM6576 显示模型(`PPU_UM6576::Scanline`)
命名表(每格 2 字节,行跨度 64 字节):
tile_base = (m_bReg2000 & 3) * 2048;
if (tile_y >= 32) { // 纵向第二页
tile_y -= 32;
if (m_bReg2000 & 2) tile_base -= 4096; // 2,3 -> 0,1
else tile_base += 4096; // 0,1 -> 2,3
}
if (tile_x & 0x20) tile_addr = tile_base + 2048 + tile_y * 32 * 2 + (tile_x & 31) * 2;
else tile_addr = tile_base + tile_y * 32 * 2 + tile_x * 2;
tile_name = PPU_Read(tile_addr + 1) * 256 + PPU_Read(tile_addr);
$2000 & 3 | 屏0(上左) | 屏1(上右) | 屏2(下左) | 屏3(下右) |
0 | $0000 | $0800 | $1000 | $1800 |
1 | $0800 | $1000 | $1800 | $2000 |
2 | $1000 | $1800 | $0000 | $0800 |
3 | $1800 | $2000 | $0800 | $1000 |
每屏 32×32 格 × 2 字节 = 2048 字节;可见 240 行 ⇒ 30 个 tile 行($0000-$077F),
第 30/31 行是滚动余量。
tile 寻址:
// 4 色模式($2008 bit7 = 0)
tile_bank = tile_name >> 12; // = attr 高 4 位 -> 调色板组 0..15
tile_offset = (tile_name & 0xFFF) * 16; // 低 12 位 -> 图案地址
plane[0] = PPU_Read(tile_offset + 0 + y); // y = (scanline + PosY) & 7
plane[1] = PPU_Read(tile_offset + 8 + y);
color = m_bPalette[tile_bank * 4 + index]; // index = plane0 | plane1<<1
// 16 色模式($2008 bit7 = 1)
tile_bank = (tile_name >> 12) & 0x0C; // 4 plane 共用一组 4 色
tile_offset = (tile_name & 0xFFE) * 16; // 16 字节对齐
plane[0..3] = PPU_Read(tile_offset + 0/8/16/24 + y);
$2007 写入路径(与原版截然不同):
// PPU_UM6576::Write case 0x2007
nes->mapper->PPU_Write(GA, data);
if (m_bReg2000 & 4) GA += 32; else GA += 1;
// MapperSB2K::PPU_Write
if (addr < 0x8000) CRAM[addr] = data; // 可写
else if (addr >= 0x8000) EVRAM[m_bVbank*0x8000 + (addr & 0x7FFF)] = data; // 可写
⇒ 补丁版下 $2007 可直接写图案区与命名表,无需 DMA。
(但 tile_offset = (tile_name & 0xFFF) * 16 上限 $1FF0,仍落在 CRAM 内。)
其他寄存器:
端口 | 语义 |
$2000 | bit0-1 选命名表页,bit2 控制 $2007 写入后 GA 步进(1→+32,0→+1) |
$2001 | bit3 = BG 使能(if (m_bReg2001 & 0x08)),bit4 = 精灵 |
$2002 | 读才复位 toggle(m_bW2005Toggle = m_bW2006Toggle = 0) |
$2006 | 16 位 GA,先高后低(与 loopy 无关,无「卷动原点污染」) |
$2008 | bit7 = 4 色 / 16 色 |
$2040-$207F | 64 项调色板 m_bPalette[0..63] = 16 组 × 4 色 |
10.4 显示编写规则(两版通用部分)
# | 规则 |
1 | 每笔成对写 $2006/$2005 之前,必须先 LDA $2002 复位 toggle(读才复位,写不复位) |
2 | BG 使能 = $2001 = $08(bit3)。写 $01 会因 BG 未开而整屏填 BGPAL[0] ⇒ 表现为「灰色无变化」 |
3 | 启动后 CLI 开中断,否则 FDC IRQ 会锁死 |
4 | DMA 启动位 $4048 bit7 必须最后写 |
5 | 屏 0 全部格填同一 tile 号时,全屏共用该 tile 的图案(纯色背景极省事,只需 1 个 tile 的图案字节) |
6 | 要逐格不同图案,必须让各格引用不同 tile 号 |
7 | 命名表字节序 = [tile 号][attr](补丁版),attr 高位决定调色板组与图案区高段 |
8 | 补丁版下图案区放 tile $200 以上(GA $2000+),避开命名表 $0000-$1FFF |
10.5 截图判读规范
VirtuaNES 窗口 532×543,布局:
区域 | 坐标 |
标题栏 | y = 0 … 32 |
菜单栏 | y = 33 … 52 |
画面 | (10, 55) – (521, 532),512×478(≈256×239 @2x) |
裁剪矩形必须用上述范围。直接用 tools/sb2k_imgstat.py(已内置正确矩形 + 2× 降采样)。
10.6 PAL 50 Hz 实机时序
VirtuaNES 以 PAL 50 Hz 运行所有 SB2000 程序,这就是实机的实际运行速度。
此前按 NTSC 1.789773 MHz 估算会产生 7.6% 偏差。
项 | 值 |
晶振(Base clock) | 26.6017 MHz(经 ÷12 分频得 CPU 时钟) |
CPU 时钟 | 1.662607 MHz |
帧率 | 50 Hz |
每帧 cycles | 33,252(1,662,607 / 50) |
1662607 就是 PAL 制式 6502 的真实频率,不是 1.66 GHz。
交叉验证:33248 × 50 = 1,662,400,与 1,662,607 差 0.0125%
制式 | 晶振 | CPU 频率 |
NTSC | 21.477272 MHz | 1.789773 MHz |
PAL | 21.318144 MHz | 1.662607 MHz ← 本项目 |
Dendy | — | 1.773447 MHz |
延时量重算表:
延时子程序 | cycles | NTSC(旧值) | PAL(正确) |
DELAY(双字节计数) | 1,178,118 | 0.658 s | 0.709 s |
WAIT5 = 5×DELAY | 5,890,590 | 3.29 s | 3.54 s |
WAIT8 = 8×DELAY | 9,424,944 | 5.27 s | 5.67 s |
WAIT12 = 12×DELAY | 14,137,416 | 7.90 s | 8.50 s |
引导(BIOS+AUTOEXEC+装载) | — | 9~12 s | 10~13 s |
10.7 显示通路已验证 ,但「长期独占 PPU」仍未定案
已证实(VCAL49 实测,2026-10-03):程序完全掌控显示链路。
out/probe/VCAL49.IMG 运行截图清楚显示 顶部 4 行蓝色 tile 条
((42,105,223) × 3072 像素 = 4 行 × 32 格),下方是 SB-DOS 启动画面
(All rights reserved / A:\>vcal49.exe)。
⇒ 确认以下全部可用(UM6576 语义):
项 | 结论 |
命名表基址 $0000、每格 2 字节 [tile][attr] | 可写、可见 |
attr 选调色板组(attr=2 → 蓝色) | 正确 |
字模放 tile $200(GA $2000+) | 避开命名表,无冲突 |
调色板 $2040-$204F | 可写 |
$2001 = $08 开 BG | 生效 |
仍未定案:t≥26 秒画面变全黑。即程序后续执行时画面消失。
两种可能:
28. 程序自己关掉了显示;
29. SB-DOS / BIOS 在 EXE 运行期间仍会重绘 PPU。
原先「SB-DOS 只在启动阶段作画」的结论建立在「整屏纯白」伪证据上(见 10.1 末),
必须用本节的实证方法重验:写入可辨识图案(而非纯色),长时间观测其是否被覆盖。
若确为 BIOS 抢占,则需要在 EXE 里周期性重绘,或接管 IRQ。
──────────────────────────────────────────────────
十一、素材与数据格式
11.1 软盘镜像格式(实测)
标准 PC 1.44 MB FAT12。实测 BPB:
字段 | 值 |
字节/扇区 | 512 |
扇区/簇 | 1 |
保留扇区 | 1 |
FAT 数 × 扇区/FAT | 2 × 9 |
根目录项 | 224 |
总扇区 | 2880 |
引导扇区尾 | 55 AA @ offset 510 |
OEM | IHC(卷标 NO NAME) |
根目录极简:只有 GAME.EXE(3-5 KB 小加载器)+ AUTOEXEC.BAT。
游戏数据不在 FAT 里,在裸扇区/隐藏区。
开发提示:改造盘时只需替换裸数据 + 改 GAME.EXE,不必理会 FAT。
11.2 `.GAM` 游戏容器格式
从 Nes.cpp 的 NEStoGAM/MakeGAM 完整解出。头部大小随容量变化:
PRG≤128K+CHR≤256K → 0x49;PRG 256K → 0x69;PRG 512K → 0xA9。
偏移 内容
0x00 魔数 'M','Y' (4D 59)
0x02 GAM 总长度, 24 位小端: size = h[04]<<16 | h[03]<<8 | h[02]
0x0C 固定 $03FE
0x10 PROM 装载起始 bank = 256 - PRG/16K (128K→$F8, 256K→$F0, 512K→$E0)
0x18 固定 $001A
0x1A 与 h[0x08] 相同 (= PRG/4K + 34)
hs-2..1 标题长度×3 (16 位小端); titlelen = 该值/3
hs 3 份标题数据, 每份 titlelen 字节
0x200 CHR 数据 (VROMsize = CHR_8K数 × $2000); 无 CHR 时填 $20000 字节 $20
0x200+ PROM, 每个 16K 块 i 占 $4006 字节:
6 字节 MMC3 序列 {0x0B, h[0x10]+i, 0x00, 0x80, 0x00, 0x40} + $4000 PRG
长度自检(两份金样本吻合):
样本 | 公式 | 结果 |
golden_bonk.GAM(PRG 256K/CHR 128K) | 0x20200 + 16×$4006 | 0x60260 |
golden_dd2.GAM(PRG 128K/CHR 128K) | 0x20200 + 8×$4006 | 0x40230 |
11.3 DRW 图片格式
· 结构 = 3 字节标识 "ZXM"(5A 58 4D) + 16 字节调色板 + 22,400 字节像素 = 22,419 字节
· 验证:100 张镜像中 28 个 DRW 全部 size=22419,26 个头为 "ZXM"
· 像素 = 700 tile(28×25,224×200)× 32 字节;tile 内平面优先:偏移 = 平面 z×8 + 行 j
· 字节 bit7→0 = 该行 8 像素左→右;索引 bit z = 位平面 z(4 bpp 16 色)
· 调色板字节取值 $00-$3F(64 色寄存器值),下标 ↔ 调色板寄存器一一对应
· PC 转换要点:转 224×200 16 色 BMP 时调色板必须取自系统 64 色,否则自动转色多数出错
python tools/sb2k_png2drw.py 输入.png 输出.DRW --size 224x200 --group 0 --view 预览.png
11.4 官方「有声播放图片」.EXE(第 15 楼)
· 文件布局:$0000 机器码(装载至 $8000)+ $1000-$7FFF = 28 KB WAV 音乐
(4000 bps 8 位单声道 7 秒,不填则静音)+ $8000 起 = 13 张图片 × 32 KB
· 13×32K + 32K = 448 K = EXE 最大体积(与 bank $80-$EF 一致)
· 图片区格式同 DRW 但 256×240(960 tile);播放速度字节 D2(4 kbps,8 kbps 改 E0/E2)
· 制作流程:WinHex 合成 → 存 EXE → WinImage 加入软盘镜像
11.5 系统 64 色 RGB 表
楼主给出 64 组 (B,G,R,0) 四元组 BASIC 数组,完整表见
贴吧_说说SB2000的图片_原文存档.md。中段数值需实测校准(探针:写 $2040-$207F
各值 → 截图取色 → PC 端比对)。
11.6 未知格式
PIC(50) / XBW(72) / TKF(32) / IDX(306) / BIN(555) —— 沿用 DRW 的
「猜排布 → 渲染 → 肉眼判读」流程。
──────────────────────────────────────────────────
十二、输入与其他外设
12.1 键盘 `$4020-$4022`
$4020 = 时序控制(写)
$4021 = 键盘输出移位寄存器
$4022 bit7 = clk,bit0 = dir(1=写)
上升沿且 dir=1 时把 $4021 发给键盘
bit2=1 时 1 ms 后产生 IRQ(经 $4033 bit5)
键盘数据就绪由 nes->pad->ExRead(0) 轮询,就绪后读 ExRead(1)
12.2 鼠标 `$4023-$4025`
$4023 选择外设(0=手柄,1=鼠标),$4024/$4025 为数据口。
手柄在 BIOS 层已确认可用;键盘/鼠标尚无自写探针实测。
12.3 精灵 / OAM (部分)
· 标准 $4014 OAM DMA 有效;写 $2014 无效(实测 OAM 无变化)
· 贴吧「2014 端口,和 FC 一样」是笔误/误传
· 精灵调色板在 UM6576 下读 m_bPalette[16 + (attr&3)*4 + index]
· 完整精灵显示未实测
12.4 PSG / APU **已实测发声(2026-10-04,VCAL60)**
$4000-$4017 是标准 2A03 APU。Mapper 不接管,Nes::WriteReg() 对 $4000-$4013
直接调 apu->Write(),完全不经过 mapper171 硬件。
实测结论:APU 通道在 SB2000 上正常工作,能发出可辨识的乐音。
项 | 实测值 |
触发方式 | $4015 bit0=SQ1, bit1=SQ2, bit2=TRI, bit3=NOI |
内置录音 | 抓得到(E:\Games\VirtuaNES\wave\SB2K_BIOS.wav,路径硬编码,不接受自定义名) |
WAV 规格 | 22050 Hz、8-bit 无符号、中点 128(头里的 bits/align 字段不可信,按 8-bit 解析) |
VCAL60 实测 | 5.2 s 处起声,365–570 Hz,落在设计的 C4–C5 音阶区间内 |
波形 | 方波 + 三角波叠加,峰值 44、RMS 10.5 |
这是绕过 DAC 的关键路径:DAC 走独立线程、与 FDC 争用 emulated CPU,
且内置录音抓不到;APU 走 apu->Process() → DirectSound → g_WaveRec.Out(),
与 FDC 无争用,且内置录音直接可听。
寄存器用法(标准 2A03):
LDA #$05 ; bit0=SQ1, bit2=TRI
STA $4015 ; 使能
LDA #$BF ; $4000: 音量 15, 无包络
STA $4000
LDA #$08 ; $4001: 关闭扫描器
STA $4001
LDA #$8C ; $4002: 周期低 8 位
STA $4002
LDA #$F9 ; $4003: [len|Thigh|flag] T=396 -> C4
STA $4003
频率公式:f = 1662607 / (16 × (T + 1));三角波 f = 1662607 / (32 × (T + 1))。
内置录音的保存路径是硬编码的 wave\<ROM名>.wav,
GetSaveFileName 弹窗里填别的名字无效。自动化脚本要用
tools/sb2k_apurec.py,它在录音后自动去 wave\ 目录取文件。
12.5 DPCM / DMC 采样回放 **已实测(2026-10-04,VCAL61)**
$4010-$4013 + $4015 bit4。DMC 是 APU 里唯一能直接播放内存中原始采样数据的通道:
// APU_INTERNAL::RenderDPCM()
ch4.cur_byte = nes->Read( ch4.address ); // ← 直接从 CPU 地址空间取样
⇒ 素材只要在程序自身内存里即可,完全不涉及 DAC 路线上那个未解的 $656B bank 语义问题。
寄存器语义(与直觉差异很大,务必照抄)
APU_INTERNAL::WriteDPCM 的实际实现(addr & 3 分支):
寄存器 | 实现 | 说明 |
$4010 | freq = dpcm_cycles_pal[data & 0x0F];looping = data & 0x40 | 周期和循环位都在这里 |
$4011 | dpcm_value = (data & 0x7F) >> 1 | 初始电平 |
$4012 | cache_addr = 0xC000 + (data << 6) | 是 <<6 不是 <<8 |
$4013 | cache_dmalength = ((data << 4) + 1) << 3 | 只是长度高位 |
完整采样地址 = 0xC000 + ($4012 << 6) + ($4010 & 0x3F)
⇒ 三个关键约束:
30. 循环位是 $4010 bit6,不是 $4013 bit7(后者只管长度)
31. $4012 左移 6 位 ⇒ 地址低 6 位由 $4010 补齐,素材必须 64 字节对齐
32. DMC 只能访问 $C000-$FFFF ⇒ 素材若在 $8000 段,
需临时把 $4044 设成 $80 让 $C000 段也映射到同一 EPRAM;
播放期间必须保持(DMC 每次取样都实时查 m_bPbank[4])
采样率:f = 1662607 / (16 × (T+1)) [PAL]
· T=15 → 6494 Hz → 811 字节/秒
· T=14 → 6927 Hz → 865 字节/秒(VCAL61 用这个)
VCAL61 实测结果
项 | 值 |
素材 | MUSIC.dpcm 2501 字节(由 sb2k_pcm2dpcm.py 从 16 kHz PCM 编码) |
播放 | 6.25 s 起声,持续到 22.75 s 录音结束,16 秒无间断 |
RMS | 稳定 10.5–10.8(每 0.25 s 采样,16 个采样点全部有声) |
主频 | 594–662 Hz |
循环 | $4010 bit6 硬件循环生效 |
LDA #$80
STA $4044 ; 让 $C000 段也映射到 EPRAM
LDA #$01 ; offset >> 6
STA $4012
LDA #$31
ORA #$4E ; bit6=循环, bit0-3=T=14
STA $4010
LDA #<LEN
LSR A
LSR A
LSR A
LSR A
ORA #$80
STA $4011 ; 长度使能 + 长度/16
LDA #$10 ; $4015 bit4 = DMC 使能
STA $4015
分段续播 (VCAL63 实测)
$4013 上限只有 4081 字节((L<<4)+1,L 仅 7 位),所以 16 KB 素材
必须软件分段。实测方案:
· 块 A 在 EPRAM 0x0072 → $4044=$80, $4012=hi=1, $4010&0x3F=lo=$32
· 块 B 在 EPRAM 0x1012 → $4044=$81, hi=0, lo=$12
· 轮询 $4015 bit4 检测播完 → 切 $4044 + $4012/$4013 → 立即重启
· 实测 5.5 s 起声,连续 17.0 秒无间断,主频呈周期性变化
(496→1038→1252→593 Hz)= 块交替特征,证明热切换无缝。
详细调研文档
见 docs/SB2000资料/DPCM技术调研.md —— 含 nesdev 权威寄存器规格、
位序问题(真机 LSB / 模拟器 MSB 相反)、16 档采样率对照表、
容量约束分析、以及 5 项音质优化方向(低通预滤波、直流偏置移除、
增益选择、段间对齐、平方根 delta 压缩)。
编码工具
tools/sb2k_pcm2dpcm.py:8-bit PCM → 1-bit DPCM。
--table 打印 16 档采样率对照表;--lsb-first 输出真机格式。
不能用"相邻样本差分符号" —— DPCM 是积分器,直接取差分会得到极高频抖动(听感白噪声)。
必须用贪心跟踪:每步选让积分器电平最接近目标值的 bit。
python tools/sb2k_pcm2dpcm.py assets/audio/still_alive_15986.pcm out/MUSIC.dpcm \
--period 14 --gain 4.0 --max 20000 --loop
DPCM 只有 7-bit 动态范围 + 1-bit 积分,音质是芯片音乐/电子琴风格,
不可能还原 PCM 原样,但足以听出旋律与节奏。
汇编器 EQU 不支持 | 运算,$40 | $0E 要写成 $4E。
12.6 LPC 语音 `$4302`
仅源码注释,无探针。
──────────────────────────────────────────────────
十三、工具链
13.1 汇编与反汇编
脚本 | 用途 |
tools/asm6502.py | 6502 汇编器:标签/.ORG/.BYTE/.WORD/.STR/.FILL/.INCBIN,.INCLUDE、前向引用;zp/abs 由书写位数判定。CLI:asm6502.py <src> <org> [out.bin] |
tools/disasm6502.py | 6502 反汇编器。 参数是 fn base start end,argv[3] = 起始偏移,不是指令条数 |
tools/sb2k_mtv_disasm.py | MTV 程序 738 B 反汇编 + 27 项关键点自动核对 |
汇编器注释只认 ;(行首 # 会被当助记符 → ValueError: bad number)。
零页间接 (zp) 不存在(真 6502 也没这模式),合法替代是 STA (zp),Y。
前向引用用 0x200 占位估长度以避免 zp/abs 长度漂移。
13.2 制盘与镜像
脚本 | 用途 |
tools/sb2k_mkdisk.py | 自研程序制盘(GAME.EXE + AUTOEXEC + 测试 pattern + 预建 FAT 文件)。 --auto 内容必须带 CRLF |
tools/sb2k_mkprog.py | 简化版制盘 |
tools/probe_build_img.py | 探针制盘:重建 FAT12 + TEST.DAT + 裸扇区图块 |
tools/sb2k_fat_extract.py | 按 FAT 链从镜像提取文件 |
tools/sb2k_disk_fat_list.py | 解析 FAT12 根目录并 dump 文件头 |
tools/sb2k_fatinfo.py | FAT 链 / 簇信息查询 |
tools/ima_extract.py | IMA 镜像提取 |
tools/sb2k_header_probe.py | 探测 .GAM/.IMG 头部签名 |
13.3 模拟器自动运行与判读
脚本 | 用途 |
tools/probe_emurun.py | 自动运行:启动 → 拖盘(带挂载确认重试,标题栏出现盘名才继续)→ 多时点截图 |
tools/sb2k_emurun2.py | 同上 + 可配置时间点截图(结尾 WM_CLOSE 优雅关闭——强杀进程会丢失全部写盘!) |
tools/sb2k_imgstat.py | 截图判读:内置正确裁剪 (10,55)-(521,532) + 2× 降采样,输出 md5 / 白像素比 / 主色直方图 |
tools/sb2k_ocr.py | 截图离线 OCR:8×8 字模模板匹配,把截图还原成文本(确定性证据,不依赖在线服务) |
tools/probe7_decode.py / probe13_decode.py / probe18_decode.py | 长度编码条 → 状态/数值解码 |
tools/sb2k_pal_usage.py / sample_colors*.py | 调色板取色分析 |
13.4 离线模拟(零人工介入验证)
脚本 | 用途 |
tools/sb2k_sim.py | CPU 级模拟器(py65 内核):位精确复算,内置 TIMER/PPU 模型,可离线还原屏幕文本、逐步观察变量、记录 BIOS 调用参数块 |
tools/sb2k_ppusim.py | 离线复现 PPU_UM6576::Scanline() 渲染,秒级验证显示模型 |
tools/sb2k_audiosim.py | DAC 离线验证:py65 跑程序 + 照抄模拟器 dpp 预处理,输出统计与 WAV |
tools/sb2k_audiosim2.py | 音频播放器完整模拟:BIOS 文件 API + RAM bank 窗口 + dpp,按重采样步长比对送样数据 |
tools/sb2k_cycles.py | cycles 精确计算与 DAC div 配置。写任何定时/音频程序前必用 |
tools/sb2k_daccheck.py | 分析 dump_dac.pcm:有声判定 / 分段 / 主频 / 直流偏置 / 与参考 PCM 逐样本比对 |
tools/sb2k_audiocheck.py | 模拟器 WAV 录音自动化(仅 APU 有效,抓不到 DAC) |
# 音频播放器完整离线验证
python tools/sb2k_audiosim2.py out/probe/VCAL47.BIN \
--pcm assets/audio/still_alive_15986.pcm --rate 15986 --max 6000000
# DAC 录音分析
python tools/sb2k_daccheck.py dump_dac.pcm --div 104 \
--ref assets/audio/still_alive_15986.pcm
13.5 素材转换
脚本 | 用途 |
tools/sb2k_png2drw.py | PNG → DRW(224×200 / 256×240,16 色,含调色板转换) |
tools/sb2k_wav2pcm.py | WAV → 原始 8 bit 单声道,重采样到 DAC 档位(--list 可列所有档位) |
tools/sb2k_font8.py | 生成 8×8 ASCII 字模(plane0/plane1,Consolas 渲染) |
tools/build_font.py | 字模构建辅助 |
13.6 分析辅助
脚本 | 用途 |
tools/sb2k_apifind.py | 在应用 EXE 中定位 BIOS 调用并 dump 上下文反汇编 |
tools/sb2k_appimg_inventory.py | 100 张应用盘清点(FAT12/主程序/MD5 指纹),产物 out/appimg_inventory.txt |
tools/sb2k_dump_check.py | MEMDUMP 转储校验(提取 LBA 1024 起的 512 KB) |
tools/sb2k_genmemdbg*.py | MEMDUMP 辅助生成 |
tools/sb2k_disasm.py | 通用反汇编封装 |
13.7 py65 移植要点(踩坑记录)
# | 坑 | 正解 |
1 | mpu.instruct 是 list(按 opcode 索引),不是 dict | tbl[0x8D] = fn |
2 | inst_0x8d 里 self.opSTA(...) 是绑定方法调用 | 替换的函数只能收 1 个参数 |
3 | opSTA 被 abs 与 zp 共用,但操作数长度不同 | 包装 instruct 表里 14 条 STA/STX/STY,各按正确长度解析 |
4 | step() 里 pc 已 +1(指向操作数低字节) | 钩子内直接读 mpu.pc |
5 | inst_0x20(JSR) 里 stPushWord((pc+1)) | 用 mpu.stPushWord(pc+1) 而非 pc+3 |
6 | hook LDA abs,X($BD) 时漏了 indexed | mk_load(nbytes, indexed, reg) 三参数齐全 |
7 | 闭包内赋值遮蔽模块级常量 | 用局部别名 |
坑 3 最隐蔽:若统一按绝对寻址解释,STA $10 会把下一条指令的操作码当地址高字节,
导致零页变量写到错误地址 —— 程序行为完全错乱且无任何报错。
13.8 本地测试环境
项 | 值 | ||
模拟器 | E:\Games\VirtuaNES\VirtuaNES.exe | ||
BIOS | E:\Games\VirtuaNES\SB2K_BIOS.NES(iNES 头 NES\x1A,PRG = 32×16 KB = 512 KB,CHR = 0,mapper 171 variant 1) | ||
运行制式 | PAL 50 Hz | ||
引导期 | 零按键约 10~13 秒(否则打断 AUTOEXEC.BAT) | ||
挂盘失败判据 | 窗口标题栏不显示盘名(相对路径多跳一级会静默失败) | ||
一键验证 | `bash tools/sb2k_verify.sh [audio\ | display]` | |
──────────────────────────────────────────────────
十四、已验证成果
14.1 MEMDUMP V3 —— 全主存转储
· 功能:把 512 KB 主 RAM(bank $80-$FF = 128 banks × 4 KB)全部写入软盘
· 盘:mkdisk --pre-out RAMDUMP.BIN 1024 预建 FAT12 文件
(簇 7-1030 = LBA 38-1061,size=524288,链完整)
· 程序(tools/sb2k_memdump3.asm):每 bank(4 KB = 8 扇区)fn $80/$05 直写,
$6203 = 源 bank(FDC 每 8 扇区自动 bank+1),LBA→CHS 运行时 16 位除法(÷36 取道、余数分头/扇)
· 结果:ST=$00 全程;目录 size=524288、FAT 链 1024 簇完整;
bank $80 开头 = 程序本体 4C 10 80 AA 55
· 交叉验证:V2(高层写)vs V3(底层写)两份 512 KB 转储仅差 0.45%(2349 B,
全部位于 bank $80 程序自身与 bank $F5 BIOS 暂存区——两处本就应不同),
其余 126 bank 逐字节一致
· RAM 分布:非零仅 bank $80(程序)、$F5(BIOS 暂存区)、$F8-$FB(系统工作区),其余全零
14.2 PI100 —— 100 位圆周率
· 算法:Machin 公式 PI = 16·arctan(1/5) − 4·arctan(1/239),交错泰勒级数
· 算术:Q4.368 定点(46 字节大数),DIVBIG = 368 步移位减法(unrolled 46×ROL 链);
x=5 收敛 79 项 / x=239 23 项,除数 ≤159
· 计时:TIMER 纯轮询 $4032=$A0(屏蔽 IRQ 但计数照常)、$4034=$CF、$4035=$FF
⇒ 一轮 = 16×256 = 4096 cycles
· 结果:真机 TIME=17182 MS / CYCLES=30738848,屏幕 5 行 × 20 位输出
100 位小数全部正确;Python 复算 NB=368 时 100 位全对
早期 PI(Q8.40 定点版)也已完成:输出 PI = 3.1415926535,误差 1.8e-12。
14.3 音频播放器 VCAL47 (离线验证通过)
· 分块播放架构见 9.6,20 秒素材、12.11 秒有效播放
· 修掉的三个真 bug:
33. PLAYBUF 循环计数错误(INX/TXA/AND #$01 在 X=0 时立即退出,只播 1 个样本)
→ 改成外层 LDY #2 × 内层 LDX #0..255
34. READ512 两块写同一 bank → 第二次覆盖第一次 → 改成两个不同 bank
35. CURBUF 与 BANK 不同步 → 统一用 BANK 作唯一真值
14.4 图片查看器 VCAL48
DRW → DMA → 显示链路已实现(tools/sb2k_vcal48.asm,成品 out/probe/VCAL48.IMG)。
显示效果待补丁版模拟器编译后确认(见 10.7)。
14.5 MTV 官方播放器反汇编 (存档参考)
MTV播放器_反汇编.txt / mtv_player_738.bin。结构全部来自实际反汇编,非推测:
地址 | 内容 |
$8000 | 头 16 B:4C 10 80 + AA 55 符合装载约定 |
$8010 | 自搬运 $8000→$6700、$8100→$6800、JSR $8280 搬 $8200→$6A00,再 JMP $6730 |
$6730 | 读软盘图像:参数表 01 1D 01 80 FF;$6207=$80、$063E=$80、$063F=$04 → JSR $FFF0 |
$6800 | 读软盘声音:参数表 00 01 01 80 FF |
$687E | DMA 子程序:10 00 80 00 70 00 10 + A0 → RAM bank0 $0000-$0FFF → $7000(音频窗口) |
$6A10 | 显示初始化:$4031=$65→$76、$2008=$80、16 字节调色板到 $2040-$204F、$2006/$2007 铺命名表 |
$6AB0 | 启动音乐:零页 $24/$25 = $68A8、$4035=$F0、$4034=$CF、CLI、死循环等 IRQ |
$68A8 | 音乐播放程序(IRQ 入口):4000 bps 双倍播放技巧 E6 AA / A5 AA / 29 01 / D0 40 |
$68F0 | 采样输出:AD 00 70 8D 27 40 60 = LDA $7000 / STA $4027 / RTS |
对本项目的价值:「BIOS 读盘 + DMA 铺显存 + TIMER 播音」三件套的完整可运行参考实现。
该程序是手打录入的论坛代码,多处与源码/实测矛盾
($4035 实际用 $F0 而非帖称 $FF;$4032 写入方向与 IRQ 语义冲突;
实测 738 字节而非 768)。可借鉴架构,不可直接照抄代码。
──────────────────────────────────────────────────
十五、模拟器补丁与自动验证
15.1 已应用的补丁
# | 文件 | 内容 | 备份 |
1 | NES/Nes.cpp | mapper 171 强制 SetVariant(UM6576) + 绕过 0xE007 地址掩码(Read 与 Write 两侧一致) | Nes.cpp.orig |
2 | NES/Mapper/MapperSB2K.cpp | #define DUMP_DAC_DATA → 导出 CPU 写 $4027 的原始样本到 dump.pcm | .orig |
3 | NES/Mapper/MapperSB2K.cpp | DACRec_Open/Write/Close(CRITICAL_SECTION 线程安全)→ dpp 后真正播放的电平写入 dump_dac.pcm | .bak2 |
补丁 1 使 mapper 171 在首次访问 $20xx 时自动切到 UM6576 变体,
绕过了 $4031 状态机与 Nes::Reset() 的限制。
副作用:切换会重建 PPU 对象,调色板与 PPU 寄存器回默认值,须重写(见 5.7)。
编译产物验证通过(2026-10-03):VirtuaNES.exe(879616 B)中
0x57B99 处 cmp dword [eax+0xC8],0xAB + je +0x0C 直接跳过
0x57BAC 的 AND EDX,0xE007 ⇒ mapper 171 走完整地址;Read 路径同理。
补丁 3 的位宽陷阱(已修,待重编译):
pcmBuf 实际是 short[](16-bit),wavDAC.Write(pcmBuf, 1024) 收的是
1024 字节。原补丁按 BYTE* 写 512 字节 ⇒ 拿到
[低字节,高字节,...] 交错数据,症状为
直流偏置 ≈ -65、削顶 ~52%、与 8-bit 素材匹配 ≈ 0%。
正确做法:传 1024 字节,由 DACRec_Write 内部只取每个 short 的高字节。
这也说明分析 dump 前必须先确认位宽,否则会误判为「波形损坏」。
15.2 两个 dump 文件的分工
文件 | 内容 | 时机 | 用途 |
dump.pcm | CPU 写 $4027 的原始字节 | 同步 | 验证「DAC 链路有没有被写」 |
dump_dac.pcm | dpp 处理后真正播放的电平(8-bit,中点 128) | 512 样本一批 | 验证「DAC 播放链路通不通」 |
两者都落在 EXE 所在目录(fopen 用相对路径),
不是项目目录。sb2k_verify.sh 需据此复制。
判读要点:
· dump.pcm 有但 dump_dac.pcm 空 ⇒ dpp 把样本全丢了(波形太平)
· dump.pcm 空 ⇒ 没有任何程序写 $4027
· dump_dac.pcm 有非 128 样本 ⇒ DAC 播放链路通
15.2.1 dump 无法区分「用户程序」与「BIOS」——重要限制
实测定案(2026-10-03):DUMP_DAC_DATA 记录的是全局 $4027 写入流,
不区分来源。SB-DOS / BIOS 自身在开机与提示过程中也会写 $4027,
这些声音会一并被记录。
决定性证据:
镜像 | 程序内容 | dump.pcm | MD5 |
VCAL46.IMG | 磁盘 API 探针,完全不送音频 | 81234 B | 835ee52e7f65c9d8af7d81201410c54f |
VCAL47.IMG | 音频播放器,送 219k+ 样本 | 81234 B | 835ee52e7f65c9d8af7d81201410c54f |
两个毫不相干的程序产生逐字节完全一致的 dump,
且 dump 的前 32 字节在镜像文件任何位置都搜不到。
⇒ 因此:
用途 | 能否用 dump 判定 |
DAC 播放链路是否通 | 可以(看有非 128 样本、有声率) |
用户程序送样是否正确 | 不可以(必然只有 ~1% 素材匹配,属预期行为) |
验证用户程序音频正确性的唯一可靠方法 = 离线模拟
(tools/sb2k_audiosim2.py),它能区分 BIOS 调用与用户 $4027 写入。
真实听感验证只能由用户在模拟器中试听确认。
15.3 一键验证流水线
bash tools/sb2k_verify.sh # 音频 + 显示
bash tools/sb2k_verify.sh audio # 只音频
bash tools/sb2k_verify.sh display # 只显示
流程:运行镜像 → 检查 dump_dac.pcm 是否生成 → 未生成则明确提示编译指南;
已生成则复制 + 分析。已实测:未打补丁时能正确检出并给出指引。
15.4 编译方式
tools/sb2k_build.bat + docs/SB2000资料/VirtuaNES编译指南.md。
沙箱内直接调用 MSBuild 会被安全策略拦截(判为 LOLBin),
cmd.exe 与 Start-Process -Verb RunAs(UAC)亦被拦截。
需用户在管理员 cmd 执行一条命令。
15.5 编译后可直接验证的镜像
镜像 | 内容 | 验证 |
out/probe/VCAL47.IMG | 音频播放器(20 秒素材) | sb2k_verify.sh audio(只能判链路通不通) |
out/probe/VCAL48.IMG | 图片查看器(PIC.DRW) | sb2k_verify.sh display |
out/probe/VCAL46.IMG | 磁盘 API 探针(条形图) | 截图判读 |
out/probe/MEMDUMP.IMG | 全 RAM 转储 | sb2k_dump_check.py |
out/probe/PI.IMG / PI100.IMG | 圆周率基准 | 截图 OCR |
15.6 补丁版实测状态(2026-10-03)
项目 | 状态 |
补丁 1(mapper171 → UM6576 + 绕掩码) | 已进二进制并验证 |
补丁 2/3(双 dump) | 生效 |
补丁 3 位宽修复 | 已改源码,待重编译 |
JSR $7B/$7C → JSR $FFF0 | 已修 15 处(VCAL45-48) |
VCAL47 音频播放 | 程序自启成功,真实听感待用户确认 |
VCAL48 图片显示 | 离线模拟通过,模拟器内仍全黑(见下) |
VCAL48 剩余问题:READB 每次 BIOS 调用只返回 256 字节(硬件限制),
但主循环每次只读 1 次却 DMA 512 字节 ⇒ 数据量不足一半,
且 bank 只在 $8A/$8B 间轮换。需改为每块 2 次 READB + 2 次 DMA。
──────────────────────────────────────────────────
十六、工程规则与踩坑清单
16.1 6502 编程陷阱
# | 陷阱 | 规则 |
1 | 多字节加/减/移位必须【低字节 → 高字节】 | 进位/借位只能向高位传播;从高往低做会让借位错位一字节 |
2 | CPX/CPY/CMP 都会改写进位 C | 多字节进位链的循环不能用 CPX/CPY 判结束(链会断裂)。用内存计数器 + DEC,或 PHP/PLP 保护 |
3 | 符号常量用于立即数必须写 LDA #SYM | 漏掉 # 会静默变成零页读取(A5 vs A9),无任何报错 |
4 | 嵌套循环不能用同一寄存器 | 内外层用不同寄存器,或改 CPX #$00 式比较。DEX/DEY 从 0 绕回 $FF ⇒ 无限循环 |
5 | PLP 会同时恢复 Z | 分支逻辑要绕开 |
16.2 SB2000 编程约束
# | 约束 | ||||
1 | EXE 头 16 字节(4C xx xx AA 55)不能省,否则 SB-DOS 拒绝加载 | ||||
2 | AUTOEXEC.BAT 内容必须以 CRLF 结束 | ||||
3 | 启动后 CLI 开中断,否则 FDC IRQ 锁死 | ||||
4 | BIOS 变量禁放 $0718-$074F;安全区 $0700-$0713 / $0780-$07AF | ||||
5 | fn $80/$00 init 后先快照 $4044-$4047,之后才配对 POSTCALL | ||||
6 | 读盘前置:CLI + $4032 位3=0 + 不破坏 $4044-$4047 + 文件名带 "A:\" | ||||
7 | 每笔成对写 $2006 前先 LDA $2002 复位 toggle | ||||
8 | BG 使能 = $2001 = $08(bit3) | ||||
9 | DMA $4048 启动位最后写 | ||||
10 | DAC 送样 cycles 必须 == div,素材必须预重采样到 1662607/div | ||||
11 | 单次连续喂 $4027 不宜超过 512 字节(dpp 峰值检测) | ||||
12 | 一次 BIOS 读 API 只返回 256 字节到一个 4 KB bank | ||||
13 | BIOS 调用只有 JSR $FFF0 一个入口。写 JSR $7B/JSR $7C 会跳进零页、真机必崩 | ||||
14 | DMA 源偏移 `src_addr = ((Reg1&15)<<15) \ | ((Reg3&$7F)<<8) \ | Reg2,对应 EPRAM[(bank&$7F)*$1000] ⇒ Reg1 = $10\ | ((bank&$7F)>>2)、Reg3 = ((bank&$7F)<<4)&$7F` | |
15 | 补丁版(UM6576)DMA 目的写 CRAM(dst < $8000);写 $8000+ 进 EVRAM,但 tile 读取只扫 CRAM ⇒ 写了也读不到 | ||||
16.3 方法论(血泪教训,压縮版)
# | 规则 |
1 | 源码模型必须先在模拟器上验证,再批量实验。凡是「实测与源码推导不符」,第一反应应是源码推导的前提错了 |
2 | 地址掩码这类「横切」行为(影响所有 $20xx 写入)必须一开始就查,不能等逐个寄存器试错 |
3 | 裁剪矩形等测量基准必须先核准。错误的判读基准会让整轮实验全部作废 |
4 | 单探针只测一个变量,多阶段必须让每阶段画面自带签名(不同纯色/不同标记) |
5 | 画面 md5 比对是判断「两阶段是否真不同」的快速手段 |
6 | 截图时间窗必须按程序实际节奏排期(先算延时量再排时间点);同一次运行同时拍早期细节和晚期稳定性 |
7 | 优先离线验证,再上模拟器。py65 + 照抄外设逻辑可秒级定位;反汇编交叉验证一眼看出 A5 vs A9 |
8 | 人工试听是最后手段 —— 只能告诉你「有声/无声」,无法定位原因 |
9 | 屏幕看不见时用磁盘变量转储(PC 端读 IMG 分析,对显示复位免疫);卡死时先离线 CPU 仿真复现 |
10 | 空白/噪声画面先怀疑图案区与命名表地址重叠,而不是地址猜错 |
11 | 离线模拟器只应 hook 真实硬件入口。给 $7B 也挂桩会让 JSR $7B 这类致命错误在离线阶段「看起来正常」,导致 bug 长期存活 |
11b | 改完源码必须重新制盘,并核对机器码。.bin/.lst 比源码旧 ⇒ 镜像里仍是旧代码,且「程序头自检 OK」也查不出来。校验:grep -c "20 F0 FF" out/probe/vXX.lst 应等于调用数 |
11c | 调用子程序前确认寄存器归属。主循环用 X 作索引、JSR 延时子程序又用 X ⇒ 索引被重置,循环失控(实测送样值恒定)。内外层延时必须用不同寄存器 |
12 | 分析 dump 前先确认位宽与来源。16-bit 数据按 8-bit 读 ⇒ 偏置异常 + 削顶,会被误判为「波形损坏」;全局写入流 ⇒ BIOS 声音会被误判为「用户程序输出」 |
13 | 模拟器补丁会改变运行时行为,探针必须按新模型重写。PPU_STD → UM6576 使 DMA 目的地、命名表基址、调色板窗口全部改变 |
14 | 制盘后必须校验关键文件内容(用 sb2k_fat_extract.py 回读),不要只看「根目录项存在」——占位内容也会让程序读到错误数据 |
──────────────────────────────────────────────────
十七、未解决问题与下一步
17.1 能力完备度
能力域 | 状态 | 说明 |
引导 / 装载 | EXE 头、AUTOEXEC、CRLF 全部实测 | |
CPU / 内存 / 分页 | 模拟器源码 + 自研程序全程使用 | |
存储(FAT12 / 底层 CHS 读写) | MEMDUMP V3 512 KB 逐字节验证 | |
存储(高层文件 API 读) | probe 全通 | |
存储(高层文件 API 写) | 可用但有怪癖(见 5.4) | |
显示(补丁版) | 模型已源码级定案(10.3),待补丁版实测确认 | |
显示(原版 EXE) | 灰屏/伪白等问题已解释(10.1/10.2),但能力受限,建议直接用补丁版 | |
DMA 传送 | 源码级 | 语义完全清楚,自写探针实测仍缺 |
$4300 显存 bank 切换 | 仅 MTV 反汇编支持,无实测 | |
音频 DAC | 离线 | 离线验证链路完整;模拟器内实测待编译补丁版 |
DAC 真实输出抓取 | 补丁已就绪,待编译 | |
精灵 / OAM | 仅定案 $4014 有效 | |
键盘 / 鼠标 | 源码有实现,无探针 | |
PSG / APU | VCAL60 实测发声, 见 §12.4 | |
DPCM / DMC | VCAL61 实测循环播放, 见 §12.5 | |
LPC 语音 $4302 | 仅源码注释 | |
中文显示 | 见 17.3 | |
$2008 bit0 单/多屏 | 模拟器 TODO 未实现,真机不可得 → 维持存疑 |
17.2 优先级路线图
优先级 | 事项 | 依赖 |
P0 | 编译补丁版模拟器(一条管理员命令) | 用户操作 |
P0 | 补丁版上复验显示链路(VCAL42 DMA 写 EVRAM → VCAL48 图片查看器) | 上项 |
P0 | 复验「EXE 能否长期独占 PPU」(10.7,需重验) | 上项 |
P1 | DAC 真实输出抓取验证(sb2k_verify.sh audio) | 上项 |
P1 | DMA 自写探针闭环(RAM → VRAM 32 KB + $4300 切 bank 显示) | 上项 |
P1 | 键盘探针(轮询 $4020,按键回显扫描码) | 无 |
P2 | $4300 显存 bank 切换实测 | 无 |
P2 | 精灵 $4014 OAM DMA + 显示 | 无 |
P2 | 系统 64 色 RGB 表校准 | 无 |
P3 | PSG/APU 方波探针 | 已完成 (VCAL60) |
P3 | DPCM 采样回放 | 已完成 (VCAL61) |
P3 | LPC 语音探针 | 无 |
P3 | 未知格式(PIC/XBW/TKF/IDX/BIN)破解 | 无 |
— | 官方手册 PDF 下载 | 用户在浏览器操作 |
— | 贴吧 6/7/8/13 楼补截图 | 用户操作 |
17.3 中文显示线索
扫描 100 张应用盘发现:
· WBX.BIN / BXM.BIN(各 65536 B)出现在绝大多数应用盘 —— 头 32 字节是
6502 代码(AD 3F 06 / 6C 10 00),不是字库,实为五笔输入法模块:
BXM.BIN 偏移 $0800 起出现 61 C8 E7 CB FD D3 D6… = ASCII 'a' + GB2312 汉字串
· 推论:汉字字库不在软盘上,应在 512 KB BIOS ROM 内
⇒ 下一课题:在 BIOS_PRG.BIN 中搜索 16×16 点阵区(连续 32 字节块 + GB2312 顺序),
并定位 BIOS 取字模的功能号。应用盘内已知 GB2312 字符串可直接用作「明文-字模」对齐基准。
──────────────────────────────────────────────────
附录:一句话速查
== 装载 ==
软件头 4C xx xx AA 55 00 00 ...(16 字节,SB-DOS 自检,缺则拒绝加载)
装载 $8000,bank $80 起,上限 $EF → 448 KB
AUTOEXEC.BAT:GAME.EXE 非必需,程序名任意,行尾必须 CRLF,可多行
== CPU / 内存 ==
CPU 窗口 $8000-$FFFF = 8 个 4KB bank,端口 $4040-$4047
bank < $80 → BIOS ROM[bank*$1000];bank >= $80 → EPRAM[(bank&$7F)*$1000]
$4300 = 显存 bank(0-15,每页 32KB)
$4301 写 $55 = RAM 引导/软复位 → 正常 NES 模式(BIOS 独占使用,官方软盘 FC 游戏走此路;官方 EXE 从不写;模拟器只做了 RAM 取指切换、PPU 不回切 ⇒ 该路径未完整)
$0000-$1FFF 工作 RAM(UM6576 才是 8KB) $5000-$57FF RAM 镜像(非 $0000 段镜像)
$6000-$7FFF ERAM 8KB
== BIOS 调用 ==
功能号 → $063E,参数 → $063F,然后 JSR $FFF0
调度器出口自动恢复 $4040-$4043(不管 $4044-$4047)
底层读盘 $80/$04: $6200磁头(0-1) $6201扇区(1-18) $6202磁道(0-79) ^key
$6203=目标bank $6205/06=512B扇区数(×8) $6207=$80
状态在 $6208(非 $6300): 00成功/10越界/18失败/1A重试耗尽
数据每 8 扇区(4KB) bank+1
高层文件API: 名→$6543("A:\名.EXT") 模式→$6560(0读)
$7B/$09开 → 句柄$6561 → $7C/$0D信息→$6568-6A → $656B=目标bank
→ $7C/$0F读(单次仅 256B!) → $7B/$0A关,错误码 $6300
读盘前置: CLI + $4032位3=0 + 不破坏 $4044-$4047 + 文件名带 "A:\"
init 后先快照 $4044-$4047 再 POSTCALL
RAM 工作区: 禁写 $0718-$074F;安全 $0700-$0713 / $0780-$07AF
== DMA $4048-$404F(不受 $20xx 掩码影响,最可靠)==
4048 bit7启动(最后写) bit5=0→VRAM/1→WorkRAM
4049 bit4=1→源EPRAM/0→BIOSROM,bit3-0=32KB bank号
404A/B 源偏移 404C/D 目的(bit15=1→EVRAM) 404E/F 长度(奇数+1)
源bank 32KB 粒度: $00-$0F=ROM512K, $10-$1F=RAM512K;$12 → EPRAM+$10000
标准 32KB 写 EVRAM: 4049=$10 404A=lo 404B=$00 404C=$00 404D=$80 404E=$00 404F=$04 → 4048=$80
== TIMER / IRQ(PAL)==
CPU = 1662607 Hz(不是 1.66GHz),50Hz 帧,每帧 33252 cycles
$4034 bit7启动 bit6连续 bit5时基 低4位预分频;$4035=重载 $4036=当前值
IRQ 频率 = 1662607 / ((4034&15 + 1) × (4035+1))
$4032 = 屏蔽(1屏蔽,0允许) bit7=TIMER bit5=键盘 bit3=FDC
$4033 = 状态(只读) 同位序
$4036 只读一次(两次读之间跨重载会丢回绕);停表后停止轮询
== DAC ==
写 $4027 即出声;采样率在首次写时锁存 = 1662607/div
$4034=$80 $4035=$67 → div=104 → 15986Hz(标称16000,误差+0.09%)
送样 cycles 必须 == div,否则断续/变调/爆音
输出 (data^0x80)<<8,PCM 零偏移=128;无重采样 → 素材必须预重采样
单次连续喂 ≤512 字节(dpp 峰值保持 512 超限整块丢弃)
方波音高 = 送样率 / (2×BLK)
录音抓不到 DAC(两条独立音频路径),需 DUMP_DAC_DATA / dump_dac.pcm
== 显示(先确认模拟器版本!)==
【原版 EXE】PPU_STD:$20xx 被 &0xE007 掩码 → $2040-$204F 塌缩成 $2000-$2007
⇒ 调色板只能走 $3F00-$3FFF;GA 14 位($0000-$3FFF)
⇒ 图案区 $0000-$1FFF 是 BANKTYPE_VROM,$2007 写入【被丢弃】→ 必须用 DMA 写 EVRAM
"改了调色板画面就变"是控制寄存器被污染的假象;"整屏纯白"是 BG 被误关
【补丁版 EXE】UM6576:完整地址
调色板 $2040-$207F(64项=16组×4色);$2008 bit7=4色/16色
命名表 $0000/$0800/$1000/$1800($2000&3 选屏),每格 2 字节 [tile][attr]
图案 offset=(tile_name&$FFF)*16(16色:(tile_name&$FFE)*16)
4色: plane0=off+0..7, plane1=off+8..15;色 = palette[tile_bank*4 + index]
16色: plane0..3 = off+0/8/16/24;tile_bank=(tile_name>>12)&0x0C
屏0 全填同一 tile 号 → 全屏共用该 tile 图案(纯色背景只填 1 个 tile)
图案区放 tile $200 以上(GA $2000+),避开命名表 $0000-$1FFF
【通用】BG 使能 = $2001 bit3(写 $01 会整屏填 BGPAL[0] = 灰屏)
每笔成对写 $2006 前先 LDA $2002 复位 toggle(读才复位)
$2006 先高后低;启动后 CLI 否则 FDC IRQ 锁死
截图裁剪 (10,55)-(521,532) 512×478;引导 10~13s;DELAY≈0.709s WAIT8≈5.67s
== 素材格式 ==
DRW = "ZXM"(3) + 调色板(16) + 像素(22400) = 22419 字节
700 tile(28×25)×32B,平面优先,offset = 平面z×8 + 行j
.GAM = 'MY' 容器(mapper4 专用),头 0x49/0x69/0xA9 随容量
盘 = 1.44MB FAT12,512B/扇区,2880 扇区;GAME.EXE 只是加载器,数据在裸扇区
== 6502 ==
多字节加减/移位: 低字节→高字节;CPX/CPY/CMP 会破坏 C(循环判终用内存计数器+DEC)
立即数必须写 LDA #SYM(漏 # 变零页读取,静默出错)
嵌套循环内外层用不同寄存器(DEX/DEY 从 0 绕回 $FF)
──────────────────────────────────────────────────
附录 A:外部权威资料索引(2026-10-05 检索)
A.1 NES 2.0 Mapper 602 = Subor SB-2000(nesdev 官方 wiki)
URL: https://www.nesdev.org/wiki/NES_2.0_Mapper_602
SB2000 在 nesdev 上被正式收录为 NES 2.0 Mapper 602。原文要点:
The first 512 KiB of its PRG address space are PRG-ROM, the second 512 KiB are PRG-DRAM.
There are 256 KiB of CHR-RAM, selectable via an outer bank register in 32 KiB amounts.
PRG 地址空间布局:
范围 | 内容 |
前 512 KiB(4 KiB bank $00-$7F) | PRG-ROM(即 BIOS) |
后 512 KiB(4 KiB bank $80-$FF) | PRG-DRAM(共 128 个 4 KiB bank) |
扩展寄存器:
地址 | 功能 |
$4202 | Digital Output Register (R) |
$4204 | Main Status Register (R) |
$4205 | Data Register (R/W) |
$4207 | Digital Input Register (R) / Configuration Control (W) |
$4300 | 外层 32 KiB CHR-RAM bank(共 256 KiB) |
$4301 | 复位寄存器:写 $55 让 UM6576 回到正常 NES 兼容模式(用于从软盘运行 NES 游戏——原文即如此,并非"未使用";BIOS 独占使用与模拟器半成品状态见 §3.3 三层结论) |
$4302 | LPC 语音芯片,位定义同 mapper 518 |
软驱控制器是 Intel 82077兼容。
这条官方定义印证了我们实测的 EPRAM 映射规则:
bank < $80→BIOS,bank >= $80→DRAM[(bank & $7F)]。
也确认了 bank $80 是 DRAM 的第一个 4 KiB 页——
我们的程序就装在那里,所以往 $80 读盘会覆盖自己。
A.2 贴吧《小霸王SB2000编程技术》(中文社区,最实用)
出处:百度贴吧「步步高多媒体学生电脑吧」帖子 tieba.baidu.com/p/9825415521
作者:ID 名为「钳工87430545」——正是这几年提取 SB2000 BIOS、制作模拟器的那位。
这是目前唯一一份系统的 SB2000 编程技术文档,价值极高。
注意:帖子很长,抓取到的只是前两节(内存、显存),
磁盘 API 一节(很可能就是 $656B 的说明)尚未取到。
一、内存(原文摘录):
小霸王SB2000多媒体学生电脑内存有512K(ROM)+512K内存(RAM)+8K+2K。
512K(地址:8000-BFFF)+ 8K(6000-7FFF)+ 2K(0000-07FF)。
其中,512K 内存可以通过 4040-4047 这8个端口切BANK到BANK8-BANKF。
只支持4K切。BANK号=00-FF(16进制),其中 00-7F为ROM,即固化的BIOS。
开机后,BANK[04-07]这16K常驻C000-FFFF。
进入后DOS运行的软件加载到RAM8000-BFFF,第一个BANK号=80。
软件最大支持448KB,加载后BANK号为[80, 81...EF]。
软件没有文件头,仅要求头16个字节 = 4C XX XX AA 55 00 00 ....。
第一个字节就是软件的第一条指令,必须为JMP指令。
$03、$04 = AA 55 是SB2000的软件的专用标识。
前16K字节4个4K的BANK不用切BANK,可以直接使用。地址=8000-BFFF。
要使用之后的数据就需要切BANK了。比如第5个BANK要使用,就写 A9 84 8D 40 40。
这时8000-8FFF的内存变为第5个BANK,即用户软件的$4000-4FFF地址的内容。
写 A9 84 8D 41 40,切到9000-9FFF。写 A9 84 8D 42 40,切到A000-AFFF。
写 A9 84 8D 43 40,切到B000-BFFF。
**C000-FFFF一般情况下为BIOS,可以通过4044-4047切到BANK C,D,E,F。
普通情况下不要这样切。除非不需要调用BIOS的MMC3游戏模式才需要切CDEF。**
这段话直接解释了 VCAL46/64/65/66 的崩溃根因:
「C000-FFFF 一般情况下为 BIOS……普通情况下不要这样切」。
二、显存(要点):
· 显存512K(VRAM)+ PPU 自带 10K
· VRAM 地址支持 0000-27FF 与 8000-FFFF 两段
· 扩展 VRAM 分 16 个 32 KiB bank,$4300 切 bank
· 颜色模式端口是 $2008 的 bit7:$00=4 色,$80=16 色
· 命名表在 0000-1FFF,分 4 块(0000-07FF / 0800-0FFF / 1000-17FF / 1800-1FFF),
由 $2000 的 bit0+bit1 选择
· SB2000 无属性表(与 FC 不同)。每个字符号 2 字节 16 位,
第 2 字节高 2 位 = 属性(选 4 组背景调色板之一),
低 4 位 ×256 + 第 1 字节 8 位 = 12 bit 字符名(0000-0FFF)
· 4 色模式:字符名 ×$10 = 图形表地址
· 16 色模式:字符名 ×$20 = 图形表地址,且字符名必须为偶数
这解释了为什么资料 §12 强调 PPU_UM6576 与 PPU_STD 必须区分——
命名表结构、属性、颜色模式全都不一样。
A.3 SB2000 软盘镜像与工具资源
来源:博客 lainzy.net/post/239.html(作者 lain,即本项目相关社区成员)
· 已有 22 张 SB2000 官方软盘镜像(未压缩 IMG,可直接用 WinImage / 仿真软驱写入)
· SB2K_GAMEDISK_MAKER_V2.1 —— 自制游戏软盘工具(第三方制作)
· SB2000NES.zip —— NES 游戏转磁盘镜像工具
· Bad Apple 音乐欣赏盘(网友自制)—— 这正是"MTV 程序"的现成实现!
· 完整镜像清单:00系统演示 / 01WPS / 02FBASIC程序设计 / 03少儿英语 /
04诗词鉴赏 / 05神奇画笔 / 06家政管家 / 07成语大观 / 08拼音乐园 /
09数学方阵 / 10仓库乐园 / 11拼图游戏 / 12跟我学WIN95提高篇 /
13趣味24点 / 14跟我学Word97 / 15跟我学Foxpro初级篇 / 16五笔学习 /
17打印机设置 / 18-21 游戏集锦
· yqs2000dxl 另帖提到:555.img 是 SB2000 专用软盘,存图片,
用软盘中的画笔软件观看
下一步高价值动作:下载 04诗词鉴赏、08拼音乐园 与
Bad Apple 音乐欣赏盘的镜像,直接逆向它们的音频实现。
这是「前人留下的研究成果」,比继续猜 $656B 更高效。
A.4 图片格式 DRW(贴吧「说说sb2000的图像」)
2000的图片有两种格式,bmp和drw。drw是学习机专用图片文件格式,
可以和bmp文件在学习上相互转换。
drw文件的大小固定为3+16+22400字节。
前3个字节是图片标识,16字节是16色调色板,
16色调色板不必象普通FC那样分成4组。不存在4个相同背景色的问题。16色可以完全不同。
22400是224__ITALIC__200个像素的数据。每8__ITALIC__8像素组成一个32个字节的title字模。
字模分4组,每组8字节。
偏移 | 长度 | 内容 |
0 | 3 | 图片标识 |
3 | 16 | 16 色调色板(不分4 组,16 色可完全不同) |
19 | 22400 | 224×200 像素数据,每 tile 32 字节(4 组× 8 字节) |
这与我们 PPU 文档里的「16 色模式每 tile 32 字节」完全吻合,
且证实「16 色时 4 组调色板可以各不相同」。
A.5 其他值得一提的资料
资源 | 说明 |
https://compumuseum.gitbook.io/hands-on-digital-media-archaeology/vintage-pc | 英文「数字媒体考古」手册,介绍在 SB2000 上用 CajoNES tracker 创作芯片音乐。频道名直接对应 NES 的 A/B/C/D(方波×2 + 三角 + 噪声),说明 SB2000 的音乐软件就是按 APU 四通道设计的 |
https://zhihu.com/video/1465428109269348352 | 在 SB2000 上运行海外 demoscene demo 的演示 |
bootleggames.fandom.com/wiki/Subor_SB-2000 | 列出 SB-Win98 内置 15 个程序,含「音乐欣赏」(10 首曲,播完一遍会停下来问是否重播,菜单音乐复用「卡拉OK」的程序)。这条直接证明原厂音乐软件是循环播放单首曲目,与我们的播放器行为一致 |
gcores.com/articles/143163 | 中文长文,提到 SB2000「支持 PCM」,初学机阶段附赠朗诵古诗的软件 |
──────────────────────────────────────────────────
附录 B: 从官方程序逆向出的真实 BIOS 函数表(2026-10-05)
B.1 突破口:官方音乐程序 `ENG-04.EXE`
在 小霸王SB2000软盘镜/ 的 37 张原版镜像里,扫描根目录找到官方的音乐/英语学习程序:
镜像 | 文件 | 说明 |
070.IMA | ENG-04.EXE 49152 B + ENG-04A~D.DAT(98304/114688/147456/131072 B) | 英语学习,DAT 就是音频数据 |
056.ima | ENG-10.EXE 49152 B + ENG-10A~D.DAT | 同结构 |
072/073/074.IMA | ENG-05/09/12.EXE + 对应 DAT | 同结构 |
055.ima | DICT.EXE + ALL0.DAT + ANI*.IDX/DAT | 电子词典 |
003.IMA | 画笔 + 01~NN.DRW(每个 22419 B)+ OPEN.WAV | DRW 格式实例 |
070.IMA 的完整物理布局(data_sec=33):
文件 | 簇 | 字节 | LBA 范围 |
LABEL.EXE | 2 | 2055 | 33–37 |
SB-WIN.EXE | 7 | 36864 | 38–109 |
AUTOEXEC.BAT | 79 | 12 | 110 |
OPEN.WAV | 105 | 27504 | 136–189 |
ENG-04A.DAT | 235 | 98304 | 266–457 |
ENG-04B.DAT | 427 | 114688 | 458–681 |
ENG-04C.DAT | 651 | 147456 | 682–969 |
ENG-04D.DAT | 939 | 131072 | 970–1225 |
SOFT.XBW | 1195 | 682 | 1226–1227 |
ENG-04.EXE | 1197 | 49152 | 1228–1323 |
B.2 `ENG-04.EXE` 用到的 BIOS 功能号(推翻此前所有认知)
ENG-04.EXE 有 82 次 JSR $FFF0,82 次写 $063E,82 次写 $063F
(每个功能调用都是「写 $063E → 写 $063F → JSR $FFF0」的三段式)。
功能号 $063E 的全部取值(LDA #imm / STA $063E 模式):
功能号 | 次数 | 功能号 | 次数 | 功能号 | 次数 | 功能号 | 次数 |
$10 | 5 | $1B | 2 | $41 | 2 | $5A | 1 |
$12 | 6 | $1C | 6 | $45 | 1 | $5D | 1 |
$13 | 3 | $1D | 2 | $5F | 3 | $65 | 5 |
$17 | 1 | $29 | 2 | $6B | 12 | $6C | 9 |
$2C | 1 | $30 | 8 | $7C | 3 | $7E | 2 |
$32 | 3 | $3D | 2 |
子功能号 $063F:$00(27) $01(6) $02(12) $03(10) $04(4) $05(5)
$06(1) $0A(4) $0F(1) $80(1) $82(1)
**重大发现:官方程序里完全没有 $7B(open)、$7C(info/read)、
也没有原帖说的 $80(读软盘到内存)**!
我们此前 12 个探针(VCAL46~VCAL79)全部基于错误的 API 假设。
$7B/$7C 那套极可能根本不存在(我们早期从某处误记)。
官方最高频的三个是 $6B(12次)、$6C(9次)、$30`(8次)。
$6B+$1C 是一对(见下)。
B.3 读文件的真实调用序列(`ENG-04.EXE` $801C 起,反汇编实证)
801C: A9 6B LDA #$6B
801E: 8D 3E 06 STA $063E ; 功能号 $6B
8021: A9 02 LDA #$02
8023: 8D 3F 06 STA $063F ; 子功能 $02
8026: 20 F0 FF JSR $FFF0 ; 第 1 次调用
8029: A9 1C LDA #$1C
802B: 8D 3E 06 STA $063E ; 功能号 $1C
802E: BA TXA
802F: 8D 3F 06 STA $063F ; 子功能 = A(第二段代码用 `LDA #$89`)
8032: AD 39 06 LDA $0639
8035: 8D 42 06 STA $0642 ; 参数搬运
8038: A9 8C LDA #$8C
803A: 85 0C STA $0C ; 文件名段 1 指针
803C: A9 86 LDA #$86
803E: 85 0D STA $0D ; 文件名段 2 指针
8040: 20 F0 FF JSR $FFF0 ; 第 2 次调用(读文件)
文件名的传入方式(最关键的发现)
不是拷到 $6543,而是通过两个零页指针 $0C / $0D 传入。
从 SOFT.XBW 读到官方实际文件名格式:
@34: "a://eng-04.exe" ← 小写、a:// 前缀、无空格、CRLF 结尾
"a://eng-04.exe" 共 13 字节,官方分成两段:
指针 | 指向 | 内容 |
$0C | $8C8C | a://eng-04(前 8 字节) |
$0D | $8D86 | .exe + NUL(后 5 字节) |
⇒ 文件名分两段,每段最多 8 字节。这正好对应 FAT 8.3 的两段结构。
我们此前把完整文件名拷到 $6543 的做法与官方约定不符。
参数区
地址 | 官方用法 | 出现次数 |
$0639 → $0642 | 每次读文件都搬运 | 21 |
$0646 | — | 4 |
$0647 | — | 5 |
B.4 `$6B` / `$6C` 的可能语义
; $6C 的用法($8113 起,两次连用)
8113: A9 80 8D 52 06 LDA #$80 / STA $0652
8116: A9 78 8D 53 06 LDA #$78 / STA $0653
8119: A9 6C 8D 3E 06 LDA #$6C / STA $063E ; 功能 $6C
811C: A9 04 8D 3F 06 LDA #$04 / STA $063F ; 子功能 $04
811F: 20 F0 FF JSR $FFF0
8120: A9 6C 8D 3E 06 LDA #$6C / STA $063E ; 立刻再调一次
8123: A9 02 8D 3F 06 LDA #$02 / STA $063F ; 子功能 $02
8126: 20 F0 FF JSR $FFF0
8128: AD 46 06 8D F8 98 LDA $0646 / STA $98F8 ; 读回结果
812B: AD 47 06 8D F9 98 LDA $0647 / STA $98F9 ; 读回结果
$0646/$0647 是读回参数(可能是读入的字节数或状态)。
⇒ $6C 很可能是「读文件到内存」,子功能 $04 = 打开/准备,$02 = 读。
$6B 可能是「设置当前目录 / 打开文件」。
B.5 VCAL80 实测结果(照搬官方序列)
用 $6B/$02 → $1C/$00 + $0C/$0D 文件名指针,读 a://test.dat
(首字节设为 $5A 橙色以便判读):
· 不崩溃
· 屏幕纯黑 ⇒ DATAV = $00 ⇒ 没读到数据
⇒ 序列本身安全,但数据仍未落到预期位置。
下一步需确认:读入的数据落在哪个地址范围($6C 的目标地址由什么指定)。
B.6 提取的官方文件(本项目 `out/mtv/`)
文件 | 大小 | 用途 |
070_IMA_ENG-04EXE.e | 49152 | 官方音乐/英语程序,反汇编主要对象 |
056_ima_ENG-10EXE.e | 49152 | 同结构 |
070_IMA_SOFTXBW.bw / 056_ima_SOFTXBW.bw | 682/685 | 配置文件,含 a://eng-04.exe 等文件名 |
SOFT.XBW 格式:
偏移 | 内容 |
0x00 | 签名 Soft.Xbw + 0x0A |
0x0A | 24 字节压缩/编码的目录数据 |
0x22 | 0x0A + 第一个文件名 a://eng-04.exe + 0x0A |
后续 | 更多文件名(本次只取到 1 个) |
──────────────────────────────────────────────────
附录 C: 原版官方程序实测成功(2026-10-05 里程碑)
C.1 实验:`ENG-04.EXE` 在补丁版模拟器上完美运行
把原版镜像 小霸王SB2000软盘镜/070.IMA 的 AUTOEXEC.BAT 从
sb-win.exe 改成 eng-04.exe,其余一个字节都不动:
src[clus(79):clus(79)+len(b'eng-04.exe\r\n')] = b'eng-04.exe\r\n'
# 只改 AUTOEXEC 的内容与目录项长度,簇链/程序/数据全部保持原位
运行结果(out/probe/shot_ENG04RUN.png):
画面完全正常 —— 彩色图形、SB2000 专有中文字库、位图插画、
16 色调色板,全部正确显示。程序已进入交互界面(显示"英语日常用语 初级(4)"
与 5 个对话选项 + "退出"按钮)。
⇒ 模拟器对官方商业软件的兼容性已验证。 之前所有的读盘/音频失败
都是我们自己写错了 API,不是模拟器的问题。
C.2 为什么录音是静音的(不是程序失败)
原帖第 8 楼明确说明 SB2000 播放 WAV 的机制:
$4033 bit7 = 1 启用 TIMER
$4034 bit7 = 1 启用 bit6 = 1 连续
bit5 = 0 → 1.7898 MHz / bit5 = 1 → 15625 Hz
$4034 低4位 + $4035 间隔时间(数越大越慢)
播放程序指针 → 零页 $24/$25
$24 指向的例程里:AD 00 70 / STA $27 40 ← 读声音采样,送 DAC 端口 $4027
原帖给出的实测值:A9 FF 8D 4035 / A9 CF 8D 4034 是最慢速度 8000 Bps。
关键:本项目的内置 WAV 录音只接 APU 输出,抓不到 DAC($4027)
(见 §9.1)。
⇒ ENG-04.EXE 的音频走 DAC,内置录音必然录到静音。这是预期行为。
⇒ 判断它有没有发声,只能靠音箱,或临时打开补丁的 DUMP_DAC_DATA
转成 WAV(但那个开关每样本fflush,会严重拖慢模拟器)。
C.3 官方音频播放器的技术路线(对我们方案的直接影响)
维度 | 官方 ENG 系列 | 我们的方案 |
音频通道 | DAC $4027 + TIMER IRQ | APU DPCM |
采样率 | 8000 Bps(最慢)~ 11025/22050 | 33252 Hz |
音源格式 | 8-bit PCM WAV | 1-bit DPCM |
音质 | 好(8-bit 线性) | 差(1-bit 积分器) |
播放方式 | TIMER IRQ 例程逐字节 STA $4027 | DPCM 硬件自动取样 |
CPU 占用 | 高(每次中断都要服务) | 极低(硬件自动) |
这是对我们方案的重要修正:官方用的就是 DAC + WAV,
而且原帖第 8 楼还给出了播放 4000 Hz 的技巧(每个采样播两遍)。
之前我们在 DAC 路线上失败($656B 那套错误 API + $4045 冲突),
现在 API 问题已经解决(见附录 B),DAC 路线值得重新评估。
C.4 下一步的两条路
路线 | 依据 | 预期收益 |
A. 复原 DAC 播放器 | 官方 ENG-04.EXE 就是现成范例;原帖第 8 楼给了 TIMER + DAC 的完整寄存器用法与播放例程机器码 | 音质好(8-bit WAV)、可直接播放原版素材 |
B. 继续 DPCM | 已验证 APU DPCM 可播放(VCAL61/63/68) | CPU 占用低、实现简单,但音质差 |
建议先做 A:把 ENG-04.EXE 的音频例程(零页 $24/$25 指向的
那段机器码)反汇编出来,作为编写我们自己的播放器的模板。
原帖第 8 楼已经贴出该例程的机器码片段:
68F0: AD 00 70 8D 27 40 60 ← 读采样,送 $4027,然后 RTS
68A8: 20 F0 68 EE F1 68 D0 1F EE F2 68 AD F2 68 C9 80 D0 15
A9 70 8D F2 68 20 90 6A AD EF 68 D0 05 A9 80 8D EF 68
20 59 6A 58 60
C.5 副产品:16 色显示模式已实测
ENG-04.EXE 的画面证实了 §A.4 描述的显示模式:
· 调色板 16 色,四组可以完全不同(不是 FC 的"四组共用背景色")
· 字符名 ×$20 = 图形表地址(16 色模式)
· 命名表在 $0000-$1FFF 分 4 块
· 画面比例 224×200(不是 256×240)
这是我们 PPU 文档 §A.4 描述的实机验证,之前只有文档描述没有实测。
nesdev_NES20_Mapper602_SB2000
NES 2.0 Mapper 602(Subor SB-2000)— nesdev.org 官方页面存档
· 来源:https://www.nesdev.org/wiki/NES_2.0_Mapper_602
· 抓取日期:2026-10-05
· 用途:本项目的模拟器补丁版把 SB2000 识别为 mapper 171(BBK 内部编号),
而 nesdev 官方把它登记为 NES 2.0 Mapper 602。本页是内存映射与扩展寄存器的权威依据。
──────────────────────────────────────────────────
原文(逐字)
NES 2.0 Mapper 602 denotes the Subor SB-2000 UM6576-based educational computer.
The first 512 KiB of its PRG address space are PRG-ROM, the second 512 KiB are PRG-DRAM.
There are 256 KiB of CHR-RAM, selectable via an outer bank register in 32 KiB amounts.
Contents
36. PRG address space layout
37. Extra Registers
· 2.1 Floppy Disk Controller
· 2.2 Miscellaneous
PRG address space layout
· First 512 KiB (4 KiB PRG bank# $00-$7F): PRG-ROM
· Second 512 KiB (4 KiB PRG bank# $80-$FF): DRAM
Extra Registers
Floppy Disk Controller
· $4202: Digital Output Register (R)
· $4204: Main Status Register (R)
· $4205: Data Register (R/W)
· $4207: Digital Input Register (R), Configuration Control Register (W)
The floppy disk controller is Intel 82077-compatible.
Miscellaneous
· $4300: Outer 32 KiB CHR-RAM bank (out of 256 KiB)
· $4301: Reset register: Writing $55 resets the UM6576 to normal NES-compatible mode.
Used to run NES games from floppy disk.
· $4302: LPC speech chip, same bit assignments as in
mapper 518.
──────────────────────────────────────────────────
与本项目实测的对照
项目 | 官方 wiki | 本项目实测(MapperSB2K.cpp) | 结论 |
bank $00-$7F | PRG-ROM(BIOS) | ReadHigh: bank < 0x80 → BIOS[bank*0x1000 + ...] | 一致 |
bank $80-$FF | PRG-DRAM | bank >= 0x80 → EPRAM[(bank & 0x7F) * 0x1000 + ...] | 一致 |
DRAM 容量 | 512 KiB = 128×4 KiB | EPRAM 512 KB,bank 号 & 0x7F | 一致 |
CHR-RAM | 256 KiB,32 KiB 一档 | EVRAM 512 KB(模拟器给的比官方多,但 $4300 语义一致) | 需核实 |
$4202/$4204/$4205/$4207 | FDC,Intel 82077 | ReadLow/WriteLow 中 addr >= 0x4200 && < 0x4208 → nes->fdc->Read/Write(addr & 7) | 一致 |
$4301 写 $55 复位成普通 NES 模式 | 原文明确 Used to run NES games from floppy disk(官方 wiki 直接给出了用途,并非"未使用") | BIOS 运行时使用:SB2K_BIOS.NES 全文唯一一处 STA $4301(偏移 0x5E894,前置 LDA #$55,紧接 JSR $2000=「装载完毕→复位→跳入新代码」);100 张盘提取的 EXE(含 29 张游戏盘 GAME.EXE、054_FC.EXE)STA/STY/STX $4301 均 0 命中 ⇒ 模式切换由 BIOS 独占(同 $4031) | 模拟器只做了一半:m_bRamBoot 取指切换已实现,但 PPU 变体被线程安全补丁固定在 NES::Reset(),不回切标准 NES 显示模式 ⇒「软盘跑 FC 游戏」路径尚不完整 |
$4302 | LPC 语音芯片(同 mapper 518) | 读取 $4302 返回语音缓冲区状态 | 已有桩实现 |
重要推论(来自 bank 号)
官方明确 bank $80-$FF = DRAM,共 128 个 4 KiB bank。
bank $80 就是 DRAM 的第一个页,而 SB-DOS 加载用户程序时
「第一个BANK号 = 80」(见 贴吧_小霸王SB2000编程技术_原文存档.md)。
⇒ 我们的 EXE(装在 $8000-$BFFF)就占用 bank $80。
往 $80 写数据会直接覆盖正在运行的代码 —— 这是本项目实测踩过的坑
(PLAYER2 首版立即静音)。
相关页面
· NES 2.0 Mapper 518(LPC 语音芯片 $5300 位定义):
https://www.nesdev.org/wiki/NES_2.0_Mapper_518
· The Famicom Disk System(BIOS 磁盘 API 结构最接近 SB2000 的 $80 功能,
可作为 JSR $FFF0 调用约定的参考):
https://www.nesdev.org/wiki/Famicom_Disk_System
DPCM技术调研
DPCM / DMC 技术调研(2026-10-04)
目的:为 SB2000 上的音频播放程序提供 DPCM 编码与回放的权威规格。
来源:nesdev wiki(nesdev.org/wiki/APU_DMC、Chimes/List of games with bit-reversed samples)
+ 本项目补丁版 VirtuaNES 源码实测推算。
所有"模拟器"标注的结论都已在本地用录音验证。
──────────────────────────────────────────────────
一、为什么选 DPCM
NES APU 五个通道里,只有 DMC(Delta Modulation Channel) 能播放
内存中的原始采样数据。其余四个(2×方波、三角波、噪声)都是
按寄存器参数合成固定波形,无法承载任意音频。
对本项目而言,DPCM 还有两个决定性优势:
SB2000 DAC($4027) | APU DMC | |
与磁盘服务(FDC) | 独立线程,争用 emulated CPU(实测:DAC 一开,读盘必挂) | 无争用 |
VirtuaNES 内置录音 | 抓不到 | 抓得到(可直接听) |
采样源 | $4027 逐字节写入,受主循环节奏限制 | 硬件自动 DMA,CPU 无需逐样本干预 |
──────────────────────────────────────────────────
二、寄存器规格(nesdev 权威)
`$4010` — IRQ / 循环 / 速率
bit 7 I---.---- IRQ enabled flag(写 0 清除中断标志)
bit 6 -L--.---- Loop flag
bit 3-0 ----.RRRR Rate index(0..15)
`$4011` — 直接电平写入(7-bit)
bit 6-0 -DDD.DDDD 输出电平 D(无符号 0..127)
· 这是直接模式:写进去立刻成为输出电平,不做 delta 调制。
· 在 delta 模式(DMC)下,它定义积分器的起始电平。
· 推荐中心线 $40($3F 亦可)。nesdev FAQ 明确指出
"proper centreline for a good DMC sample is $40"。
`$4012` / `$4010` — 采样地址
Sample address = %11AAAAAA.AA000000 = $C000 + (A * 64)
· A = $4012 全 8 位。
· 低 6 位(%AA)由 $4010 bit0-5 提供。
· ⇒ 完整地址 = 0xC000 + ($4012 << 6) + ($4010 & 0x3F)
· ⇒ 采样起点必须 64 字节对齐($4012 左移 6 位,低 6 位恒为 0)。
· 可用范围:$C000 – $FFC0。
`$4013` — 采样长度
Sample length = %LLLL.LLLL0001 = (L * 16) + 1 bytes
· L = $4013 低 7 位。
· ⇒ 长度范围 1 – 4081 字节,且总是奇数(低 4 位固定为 0001)。
· 想播 N 字节:L = (N-1) >> 4,实际长度 = (L<<4)+1。
`$4015` — 通道使能
bit 0 = Pulse 1 bit 1 = Pulse 2 bit 2 = Triangle
bit 3 = Noise bit 4 = DMC bit 5 = DPCM IRQ
──────────────────────────────────────────────────
三、 位序:真机与本项目模拟器相反
这是最容易踩、且直接影响音质的坑。
真机 NES(RP2A03/2A07)
DMC 输出单元使用右移寄存器,因此每个字节里 LSB(bit0)先被消费。
nesdev 原文:
"The shift register shifts to the right. This means the channel plays the
least significant bits of each byte before playing the most significant bits.
Nintendo games' samples are encoded with the correct bit order. Some
third-party games have their bits reversed, which makes the audio less clear."
位序错误不会改变波形的整体形状(8 个 bit 里 "+1" 和 "-1" 的总数不变),
但会破坏相邻样本间的连续性,产生高频抖动 → 听起来更模糊。
历史上这是个持续数十年的行业隐患:FamiTracker / Dn-FamiTracker
会自动做 bit-reverse,所以工具把这个 bug 掩盖了。NESdev 有一份
专门统计哪些游戏用了错误位序的页面(*Chimes/List of games with
bit-reversed samples*),收录了 Double Dribble、Gimmick! 等。
本项目的补丁版 VirtuaNES
// APU_INTERNAL::RenderDPCM()
ch4.cur_byte = nes->Read( ch4.address );
if( ch4.cur_byte & (1 << ((ch4.dmalength & 7) ^ 7)) ) // ← 注意这里
dmalength 从 N 递减,首次 n&7 == 0 → 取 bit7,
即 MSB 优先——与真机相反。
目标 | 应使用的位序 |
补丁版 VirtuaNES(我们的开发/验证环境) | MSB 优先(默认) |
真机 SB2000 烧录 ROM | LSB 优先(--lsb-first) |
tools/sb2k_pcm2dpcm.py 两种都支持,默认 MSB。
──────────────────────────────────────────────────
四、采样率
真机公式
f = CPU_clock / (16 × (T + 1))
NTSC(1.789773 MHz):
T | 周期数 | 频率 | T | 周期数 | 频率 |
$0 | 428 | 4181.71 Hz | $8 | 190 | 9419.86 Hz |
$1 | 380 | 4709.93 Hz | $9 | 160 | 11186.1 Hz |
$2 | 340 | 5264.04 Hz | $A | 142 | 12604.0 Hz |
$3 | 320 | 5593.04 Hz | $B | 128 | 13982.6 Hz |
$4 | 286 | 6257.95 Hz | $C | 106 | 16884.6 Hz |
$5 | 254 | 7046.35 Hz | $D | 84 | 21306.8 Hz |
$6 | 226 | 7919.35 Hz | $E | 72 | 24858.0 Hz |
$7 | 214 | 8363.42 Hz | $F | 54 | 33143.9 Hz |
PAL(1.662607 MHz)周期表略有不同:398, 354, 316, 298, 276, 236, 210, 198,
176, 148, 132, 118, 98, 78, 66, 50。
补丁版 VirtuaNES 的实际行为
源码里的表(APU_INTERNAL::dpcm_cycles_pal):
INT APU_INTERNAL::dpcm_cycles_pal[16] = {
397, 353, 315, 297, 265, 235, 209, 198,
176, 148, 131, 118, 98, 78, 66, 50
};
这组值 ≈ nesdev 的 NTSC 周期表 × 0.9289(而 1662607/1789773 = 0.9289)。
推导(本地已验证):
cycle_rate = cpu_clock * 65536 / OUT_RATE (OUT_RATE = 22050)
RenderDPCM 每输出一个采样推一次, 相邻 bit 间隔 cycles 个采样:
gap = cycles * 65536 / cycle_rate
采样率 = OUT_RATE / gap = cpu_clock / cycles
实测对照(--table 可复现):
T | 周期数 | 本模拟器 | nesdev NTSC | 偏差 |
$0 | 397 | 4188 Hz | 4182 Hz | +0.1% |
$7 | 198 | 8397 Hz | 8363 Hz | +0.4% |
$F | 50 | 33252 Hz | 33144 Hz | +0.3% |
⇒ 模拟器的 DMC 实际等价于 NTSC 频率,与 nesdev NTSC 表偏差 <1.5%。
这对我们有利:最高档 33.25 kHz,接近 CD 音质下限,足够承载音乐。
──────────────────────────────────────────────────
五、容量约束
约束 | 数值 | 备注 |
DMC 地址空间 | $C000-$FFFF = 16 KB | 单次播放的素材上限 |
采样起点对齐 | 64 字节 | $4012 << 6 的必然结果 |
单次长度寄存器 | 1–4081 字节 | $4013 低 4 位固定为 0001,无法表达更长区间 |
有效 16 KB 时长 | T=0: 31.3 s T=7: 15.6 s T=$F: 3.9 s | 见下表 |
两个容易误判的约束
1. $4013 上限 4081 字节,不是 16 KB
Sample length = %LLLL.LLLL0001 = (L * 16) + 1
L 只有 7 位可用(bit7 在 delta 模式下不用),最大 L=255 → 4081 字节。
所以即使地址空间有 16 KB,单次连续播放也只能读 4081 字节。
要播更长必须软件分段。
2. EXE 不能装在 $C000
SB-DOS 加载 EXE 时自行决定装入位置(实测落在 $8000 段)。
在源码里写 .ORG $C000 只是让汇编器算地址,**盘上的 EXE 仍被 SB-DOS
装到它认为的地方**,JMP $C010 于是跳进别人的内存 —— 实测表现为
输出断续(13 s / 18.5 s / 21.5 s 才有声),因为程序和数据互相覆盖。
正确做法(VCAL61 已验证):
LDA #$80 ; 让 $C000 段映射到 EPRAM[0x0000]
STA $4044 ; (与 $8000 段用的 $4040 相互独立)
LDA #$01 ; hi = offset >> 6
STA $4012
LDA #$32 ; lo = offset & 0x3F
ORA #$4E ; bit6=循环, bit0-3=T
STA $4010
分段续播的可行性
DMC 每次取样都实时调 nes->Read(ch4.address),而 nes->Read 会查
m_bPbank[4]。理论上播放中切换 $4044 可以热切换 bank,
配合重写 $4012/$4013 实现无缝续播。
VCAL63 已实测验证成功(2026-10-04):
; 块 A: EPRAM 0x0072 -> $4044=$80, hi=1, lo=$32
; 块 B: EPRAM 0x1012 -> $4044=$81, hi=0, lo=$12
LDA #$80
STA $4044
LDA #$01
STA $4012
LDA #$32
ORA #$4E ; bit6=循环, bit0-3=T=$F
STA $4010
LDA #$40
STA $4011 ; 中心线
LDA #$F9 ; L=249 -> 3985 字节
STA $4013
LDA #$10 ; $4015 bit4 = DMC 使能
STA $4015
PPWAIT: LDA $4015 ; 轮询等播完
AND #$10
BNE PPWAIT
LDA #$00 ; 关
STA $4015
; ... 切 $4044 / $4012 / $4013 到下一块, 立即重启
实测结果(内置录音 22 秒):
指标 | 值 |
起声时间 | 5.5 s |
连续有声 | 17.0 秒(34 个 0.5 s 采样段全部有声) |
RMS 范围 | 4.3 – 16.0 |
主频变化 | 496 → 1038 → 1252 → 593 Hz 周期性 |
主频的周期性变化正是块 A / 块 B 交替的特征(每块 0.96 秒 ≈ 2 个
采样段),证明 $4044 热切换 + $4012/$4013 重写在播放中无缝生效,
没有断点。
各档位下 16 KB 素材的可播时长(补丁版 VirtuaNES):
T | 采样率 | 字节/秒 | 16 KB 时长 | 单块(3985B)时长 |
$0 | 4188 Hz | 523.5 | 31.3 s | 7.6 s |
$4 | 6274 Hz | 784.2 | 20.9 s | 5.1 s |
$8 | 9447 Hz | 1180.8 | 13.9 s | 3.4 s |
$C | 16965 Hz | 2120.7 | 7.7 s | 1.9 s |
$F | 33252 Hz | 4156.5 | 3.9 s | 0.96 s |
⇒ 时长与音质此消彼长:低档位慢但能播更久,高档位快但短。
想要长音频就得接受低采样率(低通后听感仍可接受,因为 DPCM 本来
就是低码率格式)。
突破 4081 / 16 KB 双重限制的办法
38. 分段播放(已验证):素材切成 ≤3985 字节的块,轮询 $4015 bit4
检测完成,切换 $4044 + $4012/$4013。
39. CPU 预取下一块:DMC 取样几乎不占 CPU(每字节仅 1–4 cycle 的
DMA 抢占),所以可以在当前块播放期间把下一块 memcpy 到另一个
EPRAM bank,切换时只需改 $4044 一个字节 ⇒ 完全无缝。
40. 总容量:EPRAM 512 KB / 4000 字节 = 最多 128 块 = 512 KB 素材
(T=$F 约 123 秒;T=$8 约 443 秒)。
41. 注意 DMA 抢占:nesdev 指出 DMC 取样会 "steal CPU cycles",
对时序敏感的游戏代码有影响。本项目的程序不依赖精确时序,无影响。
──────────────────────────────────────────────────
六、编码算法
DPCM 格式
· 每字节 8 个 bit,每 bit 代表电平 +1 或 −1
· 1 → 电平 +2(nesdev 描述为 ±2,因为输出级有 ×2 增益)
· 0 → 电平 −2
· 输出电平被钳在 1..63(6-bit 有效范围,0 和 64/127 不可达)
关键:不能直接用"相邻样本差分符号"
DPCM 是积分器。如果直接把 PCM 相邻样本的差分符号编码成 bit,
会得到极高频的抖动(听感接近白噪声),而不是原波形。
正确做法是贪心跟踪(slope coding):每一步都选
"让当前积分器电平最接近目标值的那个 bit"。
cur = center # 初始电平, 建议 0x40
for target in targets:
cand1 = min(cur + 1, 0x3F) # bit=1 => +1
cand0 = max(cur - 1, 1) # bit=0 => -1
bit = 1 if abs(cand1 - target) <= abs(cand0 - target) else 0
cur = cand1 if bit else cand0
音质优化方向(后续可做)
42. 低通预滤波:DPCM 最高档 33 kHz,但 1-bit 积分器对高频极敏感。
素材在编码前先做低通(截止 ~10 kHz)能明显减少混叠。
43. 直流偏置移除:把素材均值对齐到中心线 $40,让积分器从中间起步,
避免起始阶段就已经撞到 0 或 63 的钳位。
44. 增益选择:DPCM 只有 6-bit 动态范围,素材太安静会浪费量化精度,
太响会削顶。建议峰值用到 ±28 左右(留 4 级余量给积分器过冲)。
45. 分段对齐:每段开头插入几个 0 bit,让积分器回到中心线,
避免上一段的累积误差污染下一段。
46. 双通道差分编码(differential across bytes):nesdev 论坛提到
对"平方根压缩 delta"(DPCMTable = 0,1,4,9,16,25,36,49,-64,...)
听感明显优于线性 delta。这需要 4-bit 编码 + 运行时解码表,
属于进阶优化。
──────────────────────────────────────────────────
七、工具
# 打印 16 档采样率对照表
python tools/sb2k_pcm2dpcm.py --table
# 编码 (默认 MSB 优先,适配本项目模拟器)
python tools/sb2k_pcm2dpcm.py in.pcm out.dpcm \
--period 15 --gain 4.0 --center 0x40 --loop
# 编码给真机烧录 (LSB 优先)
python tools/sb2k_pcm2dpcm.py in.pcm out.dpcm --lsb-first
参数 | 说明 |
--period | $4010 的 T(0..15),默认 15(33.25 kHz) |
--gain | 增益倍数,默认 2.0;DPCM 只有 6-bit,建议 2–4 |
--center | $4011 初始电平,默认 0x40 |
--loop | 末尾追加 1 字节,配合 $4010 bit6 循环 |
--lsb-first | 真机 NES 位序(默认 MSB,是模拟器用的) |
--max | 只编码前 N 字节输入 |
--table | 打印采样率对照表 |
──────────────────────────────────────────────────
八、参考资料
· APU DMC — nesdev wiki
寄存器规格、位序、速率表、内存读取时序
· [Chimes/List of games with bit-reversed samples — nesdev wiki]
(https://www.nesdev.org/w/index.php?oldid=24162)
位序问题的历史与影响范围
· [NES Technical/Emulation/Development FAQ — Chris McVell]
(https://www.chrismcovell.com/NESTechFAQ.html)
$4011 中心线应设 $40;DMC 抢占 CPU 周期的说明
· [Voice Outtakes — nesdev forums]
(https://forums.nesdev.org/viewtopic.php?t=4391)
平方根 delta 压缩的实验数据
· [Bugs.md(nes-contra-us)]
(https://github.com/vermiceli/nes-contra-us/blob/main/docs/Bugs.md)
位序问题的实际听感对比
· 本项目源码:
NES/ApuEX/APU_INTERNAL.cpp(RenderDPCM / WriteDPCM / dpcm_cycles_pal)
──────────────────────────────────────────────────
九、实战踩坑记录(2026-10-04,VCAL61→VCAL68)
9.1 静音段必须交替编码,否则积分器饱和
症状:某首曲子播放无声,且模拟器弹 ERROR 对话框。
根因:DPCM 是积分器(bit=1 → +1,bit=0 → −1)。
贪心编码器遇到 target == cur(即 PCM 恒等于中点 128)时,
cand0 与 cand1 到目标的距离完全相同,
而判定用的是 <=,于是永远选 1 —— 整个静音段被编码成 0xFF。
结果:积分器从 center 一路涨到上限 0x3F 再也回不来,
输出恒定在最大值 ⇒ 听感是静音,同时把模拟器拖进异常状态。
对照数据(VCAL63 正常 vs VCAL68 崩溃,EXE 逐字节相同只换数据):
素材 | 唯一字节值 | 0xFF 个数 |
still_alive 段(正常) | 174 | 2153 / 4000 |
曲子开头 3.24 秒(崩溃) | 2 | 3999 / 4000 |
修复(sb2k_pcm2dpcm.py):
if t == cur: # 目标与当前电平完全一致
bit = hold & 1 # 交替 0/1, 积分器停在中心线附近摆动
hold += 1
continue
hold = 0
修复后:唯一值 2 → 180,0xFF 3999 → 79。
推论:任何"开头有一段数字静音"的曲子都会触发此问题。
编码前应先扫描并跳过前导静音。
9.2 EXE 装在 `$8000` 段,只有 32 KB 可寻址
$8000-$FFFF 共 32768 字节,且 SB-DOS 只把 EXE 装进这一段。
所以:
项 | 上限 |
EXE 总长 | 32768 字节(硬上限) |
可用 bank | $8000(代码) + $9000-$FFFF(7 个) |
单块音频 | 4096 字节(须不跨 4KB 段) |
实际可用 | 2 块 × 4000 字节 ≈ 1.9 秒(成功案例 8114 字节 EXE) |
⇒ 单个 EXE 内最多约 2 秒音频。要更长必须读盘,而读盘会崩溃(§9.4)。
9.3 `$4045` 绝不能写
$4045 是磁盘驱动本体所在的映射页(启动实测 $05)。
播放期间虽然不调 BIOS,但为了把 $D000 段也指向目标 bank 而写 $4045,
会让后续任何 BIOS 调用跳进 EPRAM 垃圾 —— VCAL64/65 就是这么崩的。
正确做法:只写 $4044,让每块数据都落在 $C000 段 起始(offset 0),
单块长度 < 4096 字节,绝不触及 $D000 段。
9.4 读盘 API 会让模拟器崩溃
探针 | 是否调 $7C/$0F | 结果 |
VCAL46 | 是 | 崩溃 |
VCAL64 | 是(DMC 播放期间) | 崩溃 |
VCAL65 | 是(预载阶段) | 崩溃 |
VCAL63 / VCAL68 | 否(素材 .INCBIN) | 正常 |
⇒ 播放器素材必须编译进 EXE,播放期完全不碰磁盘。
9.5 定位崩溃的必备手段:截图 + 对话框扫描
只靠录音判断崩溃会误判(VCAL65/66 录音显示"100% 非静音",
实际峰值全是 0)。tools/sb2k_apurec.py 现在会在关窗前:
47. PrintWindow 抓模拟器窗口 → out/probe/shot_<tag>.png
48. 枚举该进程所有 #32770 对话框及子控件文本
VCAL67 第一次跑就靠它捕获到 ERROR 弹窗 + 纯黑屏画面。
9.6 内置录音 WAV 的两个坑
49. 保存路径硬编码为 wave\<ROM名>.wav,
GetSaveFileName 弹窗里填别的路径无效。
目标存在时弹"是否覆盖"且默认选 N ⇒ 自动化流程卡死。
⇒ 录音前先清空 wave\ 目录。
50. WAV 头的 bits/align 字段不可信(声明 8-bit 但 align=1),
必须按 8-bit 无符号解析,否则 RMS 会算出 4.4e11 这种垃圾值。
──────────────────────────────────────────────────
十、读盘崩溃的真相:旧结论是错的(2026-10-05 实证推翻)
10.1 起因
VCAL46/64/65/66 四个探针在调用 BIOS 读盘 API($7C/$0F)时
都弹出模拟器 ERROR 对话框,当时归纳为「读盘必崩」,
并据此把播放器架构限制成「素材必须 .INCBIN 进 EXE,播放期不碰磁盘」。
10.2 剂量实验:把「读盘」单变量隔离
VCAL69 只做 open + N 次连续读盘,无其他任何操作:
NREAD | 结果 | 非静音 | ERROR 对话框 |
8 | 正常 | 29/39 段 | 无 |
16 | 正常 | 25.0% | 无 |
32 | 正常 | 26.4% | 无 |
64 | 正常 | 24.6% | 无 |
⇒ 读盘次数与崩溃无关。
10.3 逐项加入可疑因素(`VCAL70`)
MODE | 场景 | 非静音 | ERROR |
A | 读盘 + bank 逐块递增 $A0..$A7 + 切 $4044 | 26.0% | 无 |
B | A + DMC 播放(带超时保护) | 11.0% | 无 |
C | B + 128000 字节大文件(越界读) | 9.1% | 无 |
精确复现 VCAL65 配置(32 bank + DMC + AUD.DAT):13.0%,无 ERROR。
10.4 最后一个未测场景(`VCAL71`)
DMC 正在播放期间 CPU 调 BIOS 读盘(交错执行)——
这正是 VCAL64 的崩溃写法:
28.4% 非静音,无 ERROR 对话框。
⇒ 交错读写也不崩。
10.5 崩溃真因定位:`$4044` 被改后 BIOS 入口失效
用A/B 对照法找到真正的崩溃机制:
写法 | 结果 |
$4044=$A0 给 DMC + 之后调 BIOS 读盘 | 崩溃(VCAL46/64/65/66) |
$4044=$A0 给 DMC + 不调 BIOS | 正常(VCAL63/68/71) |
保持 $4044=$04 + 调 BIOS 读盘 | 正常(VCAL57/69/70) |
$4044 是 BIOS 磁盘驱动本体所在的映射页(启动实测值 $04)。
一旦改成素材 bank,JSR $FFF0 就跳进 EPRAM 垃圾 ⇒ 模拟器报错/挂死。
⇒ 「读盘必崩」是误判。所有 12 个场景(剂量 4 档 + 三阶段 +
精确复现 + 交错)全部正常,因为它们要么不碰 $4044,要么碰了之后不再调 BIOS。
10.6 顺带发现:BIOS 读盘返回成功但不交付数据
严格 A/B 对照(唯一差异是「读盘」这一个动作):
程序 | 做法 | 非静音 |
VCAL71 | $4044=$A0,不读盘直接播 | 28.4%(播 EPRAM 垃圾值) |
PLAYER2 | $4044=$A0,先读盘到$A0 再播 | 0.0%(全 $00 静音) |
VCAL72 用屏幕颜色进一步确认:读盘后该bank 显示近白色
(≈ $FF,EPRAM 未初始化值),而 TEST.DAT 首字节是 $00(应显示黑色)。
⇒ BIOS 读盘 API 返回成功码,但 $656B 指定的 bank 里没有数据。
$656B 的真实语义至今未实证——最早 probe17 记录「用 $8A 成功」,
但那次从未验证读到的内容是否正确。
这是长音频播放器的最后一道坎。
10.7 架构层面的突破:EXE 不再是瓶颈
流式播放器 PLAYER2.EXE 只有 204 字节,
60 秒音频(247070 字节 DPCM)留在软盘上:
1. $4044 = $04 ← 恢复 BIOS 磁盘驱动页
2. 读一块到 bank $A0 ← 此时 $4044 是 BIOS 值,安全
3. $4044 = $A0 ← 切给 DMC(此期间不调任何 BIOS)
4. 等本块播完(带超时)
5. 回到 1
⇒ EXE 的 32KB 寻址限制不再限制音频长度。
10.8 两条新增铁律
铁律A:素材 bank 绝不能用 $80
程序本身装在 $8000-$8FFF(由 $4040 指向 EPRAM[0x0000]),
所以 bank $80 就是程序自己。往 $80 读盘会直接覆盖正在运行的代码。
(PLAYER2 首版踩过:立即静音。)
铁律B:$4010 bit6 不能开硬件循环
bit6=1 时 DMC 反复重播同一块,$4015 bit4 永不置零,
任何「等播完」的循环必然超时。流式播放每块只播一次,必须 bit6=0。
(VCAL63 能连续播 17 秒是因为它根本不等待。)
10.9 结论
修正后的完整崩溃原因清单(两个独立因素,都已实证):
51. $4044 被改成素材 bank 后又调 BIOS ⇒ JSR $FFF0 跳进 EPRAM 垃圾
(VCAL64/65/66 的 ERROR 弹窗)
52. DPCM 数据里有长段 $FF(编码器把前导数字静音编错)
⇒ 积分器饱和,APU 输出恒定(§9.1,VCAL67 抓到证据)
VCAL65 还有一个独立缺陷:PPWAIT 死等 $4015 bit4 无超时,
一旦某块 DMC 没起振就永久挂起,表现为「程序消失、键盘无响应」。
10.6 架构铁律修正
旧规则(错误) | 新规则(实证) |
素材必须 .INCBIN 进 EXE | 可以留在软盘,边读边播 |
EXE 32KB 限制音频只能 2 秒 | 60 秒音频只需 EXE 240 字节 + 素材 483 簇 |
读盘与 DMC 不能共存 | 可以交错 |
仍然有效的铁律(VCAL68/69/70/71 共同确认):
53. $4045 绝不写(磁盘驱动本体所在页)
54. 每块 < 4096 字节且落在 bank 起始(offset 0)⇒ 不触及 $D000 段
55. $4013 上限 4081 字节
56. 等 DMC 结束的循环必须带超时
57. DPCM 数据里不能有长段连续高位($FF)
──────────────────────────────────────────────────
十一、$656B 语义的推进方向(2026-10-05 检索 + 实验记录)
11.1 外部权威资料已找到
来源 | 内容 |
nesdev NES 2.0 Mapper 602 | SB2000 的官方 wiki 页。确认 bank $00-$7F = ROM/BIOS,$80-$FF = DRAM(128×4KB)。与实测 EPRAM 规则完全一致 |
贴吧《小霸王SB2000编程技术》(p/9825415521) | 目前唯一系统的中文技术文档。作者 ID = 钳工87430545 = 提取 BIOS、做模拟器的那位。强烈怀疑「三、软盘」一节就是 $656B 的权威说明,尚未取到 |
lainzy.net/post/239.html | 22 张官方软盘镜像、SB2K_GAMEDISK_MAKER_V2.1、Bad Apple 音乐欣赏盘 |
11.2 官方数据与我们实测的对照
项目 | 官方 wiki | 我们的实测 | 结论 |
bank $00-$7F | PRG-ROM (BIOS) | $404x < $80 → BIOS | 一致 |
bank $80-$FF | PRG-DRAM | $404x >= $80 → EPRAM[(bank & 0x7F)] | 一致 |
$6000-$7FFF | (wiki 未提) | ReadLow → ERAM[addr & 0x1FFF] 独立 8K | 贴吧文档确认这是独立的 8K |
程序加载位置 | — | $8000-$BFFF,bank 从 $80 起 | 贴吧文档确认 |
11.3 最有价值的新假设
那8K $6000-$7FFF 很可能是 BIOS 磁盘系统的缓冲区。
推理链:
58. 贴吧文档明写内存构成为512K($8000-BFFF) + 8K($6000-$7FFF) + 2K($0000-$07FF)
59. 而软件只加载到 $8000-$BFFF(上限448KB),那 8K 装不了用户软件
60. 模拟器 ReadLow 显示 $6000-$7FFF 走 ERAM(独立的 8K RAM),
不在 512KB EPRAM 里
61. ⇒ BIOS 磁盘例程很可能把读出的数据放在这里,
而 $656B 可能不是"目标 bank"而是"缓冲区内的偏移/页号"
这能解释为什么 $656B 写 $A0/$8A/$E0 都在 $C000-$F000 读不到数据
——我们一直看错了地方。
11.4 本轮实验记录(含失败)
探针 | 目的 | 结果 |
VCAL73 | 穷举 $656B 候选值 + 扫 4 个窗口 | 首版把 $4040-$4043 也改了(那是程序自身的窗口)→ 当场 ERROR。修正后跑通,但引导需 15 秒,截图时序对不上,采样全落在DOS 画面 |
VCAL74 | 改用 DMC 音高编码报告 | 全静音。原因:DPCM 只会忠实播放内存内容,若目标 bank 没数据就是恒定直流 = 静音,恰好掩盖了要测的信号 |
VCAL75 | 改用 APU 方波通道报码 + 扫 $6000-$7FFF | 录音峰值 0,截图显示 A:/>vcal75.exe 已输入但程序未接管。EXE 头标识 4C 10 80 AA 55 校验通过,未定位到原因 |
11.5 教训
62. 不能用 DPCM 播码——它是回放通道,不是信号通道。要报码必须用合成器(方波/三角波),它们是独立振荡器,不受内存内容影响。
63. 截图做判据要求时序可控。引导 15 秒这个事实让密集截图法失效。
更可靠的是「开机即开始循环播报」,让待测状态在整个录音期里持续暴露。
64. 改 bank 窗口前必须自问:这个窗口映射的是不是我自己的代码?
这是同一条铁律的第三次违反(前两次是 $4045、第三次是 $4040-$4043)。
11.6 建议的下一步(按性价比排序)
优先级 | 动作 | 理由 |
P0 | 拿到贴吧那篇帖子的完整版(尤其「三、软盘」之后的章节) | 这是唯一已知的一手技术文档,$656B 的权威说明极可能就在里面 |
P0 | 下载 Bad Apple 音乐欣赏盘 / 04诗词鉴赏 / 08拼音乐园 镜像 | 前人成品。逆向它们的音频实现可直接绕过 $656B 未知问题——看它们怎么用就知道 $656B 怎么用 |
P1 | 静态分析 SB2K_BIOS.NES 的磁盘例程 | BIOS 已提取,$FFF0 → JMP $FD8E 已知。可反汇编 FD8E 找$656B 的引用 |
P1 | 验证 $6000-$7FFF 是否为磁盘缓冲区 | 探针设计需改进:用合成器持续报码 |
P2 | 用「开机即循环报码」模式重做 $656B 穷举 | 解决时序问题 |
P0-2 是性价比最高的一条:官方音乐盘必然要读音频文件,
必然用了正确的 $656B 语义。把它抓下来反汇编,
比我们继续盲试快得多。
贴吧_说说SB2000的图片_原文存档
贴吧原帖存档:说说sb2000的图片
存档说明:本文件为原帖截图的人工转录与整理(2026-10-02 由用户提供的 8 张截图补录)。
截图中部分楼层为缩放显示,低可信字符以【?】标注;两段长代码为逐行辨读,个别字符可能失真,引用前请对照原帖。
本帖技术断言已用本地镜像实测验证,见文末「本地机器验证」一节;甄别意见另见同目录 SB2000开发资料汇编.md §十三。
项 | 内容 |
来源 | 百度贴吧 · 步步高多媒体学生电脑吧 |
标题 | 说说sb2000的图片(全部回复 18) |
楼主 | VB步步高(楼主 Lv6 铁杆吧友,河南) |
帖内时间跨度 | 主楼 2024-09-03;回复 2025-05-03 ~ 2025-11-08 |
抓取方式 | 用户人工截图 8 张(2026-10-02),裁剪放大后转录 |
抓取完整性 | 已取:主楼、第 2/3/4/9/10/11/12/14/15/16/17 楼及楼中楼;缺:第 6/7/8/13 楼(疑为图片楼层) |
──────────────────────────────────────────────────
主楼(VB步步高,2024-09-03,河南)
555.IMG 是 SB2000 专用软盘,其中存放了一些图片,使用软盘中的画笔软件可以观看。2000 的图片有两种格式,bmp 和 drw。drw 是学习机专用图片文件格式,可以和 bmp 文件在学习上相互转换。drw 文件的大小固定为 3+16+22400 字节。前 3 个字节是图片标识,16 字节是 16 色调色板。16 色调色板不象普通 FC 那样分成 4 组,不存在 4 个相同背景色的问题,16 色可以完全不同。22400 是 224\__ITALIC__200 个像素的数据。每 8\__ITALIC__8 像素组成一个 32 个字节的 title 字模。字模分 4 组,每组 8 字节。一个字节 8 位代表一条线的 8 像素。4 组相同位字节的相同位合成为 1 个 4 位 16 数,对应调色板的 16 字节。所以 SB2000 画面软件支持的图片只能是 16 色 4 位色深度 224\*200 像素格式。
PC 上的图片想在小霸王 SB2000 上播放,需要转换成 224\__ITALIC__200 的 16 色 BMP 文件。如果 16 色 BMP 图片超过 224\__ITALIC__200,则只显示图片左上角的 224\*200 个像素部分,多余的看不到。值得注意的是,电脑上转换的 16 色图片的调色板颜色应该转成相近的 FC 专用颜色。不转换的情况下,学习机在播放图片时会自动转换颜色。这个自动转换大多数情况下是错误的,可能是软件设计不精确或者学习机算力低的原因。我发的这个软盘镜像中有一部分是 PC 原图未转色,00.bmp-07.bmp 在学习机上播放颜色是乱的。001-003、5554-556 这 6 张图片是我手工转换了颜色,播放就没有问题了,和在电脑上播放效果一样。
[附:6 张画笔软件播放效果截图(电视剧画面照)]
──────────────────────────────────────────────────
第 2 楼(VB步步高,2025-05-03):drw→PC 显示的 VB 代码
在 PC 上观看小霸王 SB2000 画笔专用图片 drw 文件的 VB 代码:
Private Sub loadDRW()
fm = File1.Path + "\" + File1.FileName
If Dir(fm) = "" Then Exit Sub
If LCase(Right(fm, 4)) <> ".drw" Then Exit Sub
If FileLen(fm) < 3 + 16 + 22400 Then Exit Sub
Open fm For Binary As #13
Dim f(15) As Byte ' 16 字节调色板
Dim r(7, 3, 699) As Byte ' 像素: (行j, 位平面z, tile X)
Get #13, 4, f ' 偏移 4(1基) = 3 → 调色板
Get #13, 20, r ' 偏移 20(1基) = 19 = 3+16 → 像素
Close #13
Dim b(7, 255) As Byte
Dim d(223, 199) As Byte
ReDim xsp2(223, 199)
For Y = 0 To 255
For d = 0 To 7
z = 2 ^ d
If (z And Y) = z Then b(d, Y) = 1 Else b(d, Y) = 0 ' b(位,值)=值的那一位
Next
Next
Dim o(3) As Long
o(0) = 1: o(1) = 2: o(2) = 4: o(3) = 8
Dim i As Long, a(3) As Byte, bd As Byte
xx = 0: yy = 0
For X = 0 To 699 ' 700 个 tile
For j = 0 To 7 ' tile 内 8 行
For d = 0 To 7 ' 行内 8 像素
bd = 0
For z = 0 To 3 ' 4 个位平面
a(z) = r(j, z, X)
bd = bd + o(z) * b(7 - d, a(z)) ' 索引位 z = 平面字节的位(7-d)
Next z
d(xx + d, yy + j) = bd
Next d
Next j
xx = xx + 8
If xx >= 224 Then xx = 0: yy = yy + 8
Next X
Dim bi10 As BITMAPINFO
Dim c(15)
bi10.bmiHeader.biHeight = -200: bi10.bmiHeader.biWidth = 224
For X = 0 To 15
bi10.bmiColors(X).rgbBlue = fxsp(f(X)).b ' fxsp(): SB2000 色号→RGB
bi10.bmiColors(X).rgbGreen = fxsp(f(X)).g
bi10.bmiColors(X).rgbRed = fxsp(f(X)).r
Next
SetDIBits Picture2.hdc, Picture2.Image, 0, 200, d(0, 0), bi10, 0
End Sub
注:VB 多维数组按列主序存取,Get #13, 20, r 读入后文件字节序为
tile X(0-699)→ 位平面 z(0-3)→ 行 j(0-7),即 tile 内偏移 = **z\*8 + j**(已本地渲染验证,见文末)。
──────────────────────────────────────────────────
第 3 楼(VB步步高,2025-05-03):PC 图片→drw 的 VB 代码(makdrw,节选辨读)
Private Sub makdrw()
drwY = 40 + 152: drwX = 216
Dim ztb(699) ... ' 【?】700 个 tile 的结构数组(内含 8行×4平面 字节)
Static p(3, 255) As Byte ' 位分解表: p(平面,色号)=该色号二进制那一位
Static c(3) / o(7, 1) ... ' 【?】
If p(3, 255) = 0 Then
For X = 0 To 15
For Y = 0 To 3
If (X And 2 ^ Y) <> 0 Then p(Y, X) = 1 Else p(Y, X) = 0
Next
Next
For X = 0 To 7: o(1, X) = 2 ^ (7 - X): Next ' 【?】位权重表
End If
For Y = 40 To drwY Step 8
For X = 0 To drwX Step 8
For y1 = 0 To 7
For x1 = 0 To 7
c = dian(X + x1, Y + y1) ' dian(): 1字节=1像素的源图数组
For z = 0 To 3: ... b3(z) = b3(z) + ...: Next ' 【?】按位平面累加
Next x1
For z = 0 To 3
ztb(zw).b(y1, z) = b3(z)
Next
Next y1
zw = zw + 1
Next X
Next Y
I = 0
For Y = 0 To 1
For X = 0 To 1
ztb(1023).b(X, 2 + Y) = pt(I): I = I + 1 ' 【?】末尾附加 4 字节(原文如此)
Next
Next
T = Array(12, 13, 16, 21, 24, 25, 28, 36, 37, 38, 39, 44, 48, 52, 56, 61)
Dim sbtab(15) As Byte
For X = 0 To 15: sbtab(X) = T(X): Next
Open "E:\" & Format(File1.ListIndex, "00000") & ".drw" For Binary As #3
Put #3, 1, Clng(&H4D585A): Put #3, 4, sbtab: Put #3, 20, ztb
' ↑ &H4D585A 小端写出 = 5A 58 4D = "ZXM",与本地 drw 文件头一致(截图末位字符已按文件校正)
Close #3
End Sub
楼中楼(VB步步高,2025-05-03):dian(255,235) 是数组,1 字节-1 像素,取值范围 0—15,对应 16 色调色板。
[附:画笔软件界面及调色板截图]
──────────────────────────────────────────────────
第 4 楼(2025-06-03):调色板截图 + 楼中楼(图形显示能力讨论,2025-06-14)
VB步步高 06-14 13:05:05:SB2000 在图形显示上确实是所有 8 位学习机里最牛的!
一、它真正支持 16 色甚至是 64 色。64 色是指每个 title 虽然是 16 色的,但是不同 title 可以选择不同的 16 色调色板,一共 4 组色盘,所以想显示 64 色图片效果也是可能的。
二、它同屏 16 色或 64 色不需要屏幕分裂技术,纯硬件支持。因为其他 FC 型号的命名表都是 8 位,而它是 12 位,是普通 FC 的 16 倍。普通 FC 由于 title 名只有 0-255 这 256 个选项,而一屏图像是 960 个 title,想显示完全不同的字符图案,就需要分成 4 次——在屏幕上每显示 60 条线时就切换一次 4K 背景图案表(ppu:0000-0FFF 或 1000-1FFF),也就是屏幕 4 分裂。而 SB2000 呢,每个 title 名有 4096 个选项,其实有 1024 个就足够显示 960 完全不同的字模图案了,所以不需要屏幕分裂。
永恒的明天:SB2000 这么牛/牛机,生不逢时!
VB步步高 06-14 13:18:12:三、它充分利用了扩展显存。一个 16 色图需要 32K 显存,远超普通 FC 的 10K 大小的 VRAM。所以 SB2000 支持图形命名表使用扩展地址 &H8000-&HFFFF:比如 titlename 是 &H800,代表图形地址是 &H8000;802 代表 8020,804 代表 8040。在 16 色显示时,由于一个 title 图形是 32 字节,所以 801、803 这样的命名是错误的,在 4 色显示时才有意义。对比之下,步步高软驱一号也有 32K 显存,步步高 98 甚至有 256K 显存。但是步步高图形显示完全和普通 FC 是一样的,所以虽然有 16 色和不用屏幕分裂的硬件基础,但是没利用。小霸王 SB2000 的硬件 6576 确实比普通的 6561 要强大的多。
──────────────────────────────────────────────────
第 9、10 楼(2025-06-14):照片类图片的画笔软件播放效果截图
──────────────────────────────────────────────────
第 11 楼(VB步步高,2025-06-14):DMA 写显存
SB2000 写显存也有独到之处。4048-404F 这 8 个端口配合,可以一次向显存写 32KB 内容,而且是 DMA 方式——只要这 8 个端口分别写一次,指定好内存源地址和显存目标地址及内容大小,一共 16 条指令就够了。简直是牛叉了!说明下,4049 端口写的是内存的 bank 号,这是按 32K 切划分的:比如 &H10 代表 prg RAM 的第一个 32K 区间,即用户编写软件的地址 0000-7FFF;而 &H11 代表 8000-FFFF;&H12=&H10000-17FFF……它一共有 512K 内存(RAM),所以 bank 号为 &H10-&H1F,而 0-F 是 512K ROM 的 bank 号。4048 写 &HC0,404D 写 &H[80-FF] = PPU 地址 = 8000-FFFF,这就是向扩展显存写数据的关键。配合命名表的 800、802、804……FFE 这些编号,就能在屏幕上显示出全屏幕 16 色图像了。
──────────────────────────────────────────────────
第 12 楼(VB步步高,2025-06-14):显存切换与命名表
4300 = 32K 切显存端口,可选 0-15。
SB2000 的命名表存放地址也和 FC 不同,它把本来属于图案表的 0000-1FFF 空间改成了 4 组命名表:0000-07FF=00 屏命名表,0800=01 屏,1000=10 屏,1800=11 屏。显示哪个屏由 CPU 2000 端口的低 2 位指定。
CPU 2000 端口位 7=1 指定 16 色模式,=0 指定 4 色模式。位 0=0 指定命名表地址=0000-1FFF,有 4 块;若位 0=1,则命名表地址=2000-27FF,这样就只有一个屏的命名表可以使用了。
──────────────────────────────────────────────────
第 14 楼(VB步步高,2025-06-14):32K 显存页布局示例
· 0200:0C 0D 10 15 18 19 1C 24 25 26 27 2C 30 34 38 3D = 0 组 16 色调色板,根据图片制作时选择颜色可变。
· 0800:00 80,02 80,04 80,06 80 …… 0E FF = 命名表,固定,1024 字节(前 960 字节可显示 32\*30)。
· 8000: 图案表(32\*960 BYTES,格式)。
· 10000,18000,20000 …… 每 &H8000 字节 = 一幅 32K 图像数据。
──────────────────────────────────────────────────
第 15 楼(VB步步高,2025-06-17):有声播放图片 .exe 的机器码与格式
机器码(人工转录,个别字符可能失真):
4C1080AA55000000000000000000FFA9008D0208D012078EAEAEAEAEAEAA9658D3
140A9768D3140A9808D0820EAA210A000B99080994020C8CAD0F6EAEA4CA0800000000
0AD43808D0043EA6000000A2008E0043A008B987809974088D0F7EE8980E8E00DD0EA8
C0043EAEAA91E8D01204CFD80000000000000000C010008800000008C01100800800800C
0C0D101518191C242526272C3034383DA9088501A90085008D06208D0620A5008D0720A50
18D0720E600E600D008E601A501C910F002D0E64C53808000000000000000000000000AD
00904A8D1140EAA9D285AAE6AAD0FC6000000000000000000000000000000000000002
0D880EED980DDF8EEDA80ADDA80C9A0EAEDEA9908DDA80EE4380AD4380C90DD005A
9008D4380EECB80ADCB80C988D005A9818DCB808D4140EAEA2048804CFD80
这是 SB2000 的有声播放图片 .exe 的主程序机器码。位置:0000,装载至内存 8000。位置 1000-7FFF 可以填充 28KB 的 WAV 音乐,规格是 4000bps、8 位、单声道,时长 7 秒,不填充的话就是静音播放。8000 之后就是 13 张图片,每个图片 32KB(&H8000 大小)。
楼中楼(VB步步高,2025-06-17):
· 内含一组 16 色调色板($90-$9F),不需要 0800 的命名表了,内置自动填充命名表代码。8E0 1【?】:D2 是 4kbps 播放速度控制,若是音乐格式是 8kbps,将 D2 改成 E0 或 E2 即可。如果想增加音乐时长,可以把前几幅图片改成音乐数据,将 $13A 的 &H88 这个数改大即可。音乐数据每加 4K,这个数加 1【原文辨读】。88=28K,90=28K+32K。
· 图片数据格式和 DRW 图片文件格式一样,不过像素宽度从 224 变成 256,高度 200 变成 240,仍是 32 字节 1 字符【?】,从 700 字符变为 960 字符。
· 4300=32K 切显存。512K,取值[$0-$F],每页正好是一幅图片。之所以每个 exe 文件只装 13 张图片,是因为这是 SB2000 的 EXE 文件最大体积,再多就会提示内存不足无法运行了。
· 电脑上用 winhex 程序可以把机器码加音乐图片数据合成,另存为 EXE 文件,加入到软盘或软盘镜像文件(使用 winimage)里,就可以在小霸王 SB2000 或模拟器运行软件。
──────────────────────────────────────────────────
第 16 楼(VB步步高,2025-06-17):小霸王 SB2000 的系统调色板(64 色)
T = Array( _
&H55, &H55, &H55, &H0, &H80, &H20, &H0, &H0, &H80, &H0, &H0, &H80, &H0, &H53, &H0, _
&H80, &H0, &H80, &H0, &H40, &H0, &H80, &H0, &H0, &H0, &H80, &H0, &H0, &H4, &H2E, &H7B, &H0, _
&H0, &H6E, &H6E, &H0, &H0, &H80, &H40, &H0, &H0, &H80, &H0, &H0, &H40, &H80, &H0, &H0, _
&H80, &H80, &H0, &H0, &HA, &HA, &HA, &H0, &H0, &H0, &H0, &H0, &H0, &H0, &H0, &H0, _
……(中间约 40 色省略,详见原帖)……
&HFF, &HBD, &HFF, &H0, &HDE, &HBD, &HFF, &H0, &HBE, &HBE, &HFF, &H0, &HC4, &HD8, &HFD, &H0, _
&HBE, &HFF, &HFF, &H0, &HBD, &HFF, &HDE, &H0, &HBE, &HFF, &HBE, &H0, &HDE, &HFF, &HBD, &H0, _
&HFF, &HFF, &HBD, &H0, &H4B, &H4B, &H4B, &H0, &H0, &H0, &H0, &H0, &H0, &H0, &H0, &H0)
i = 0
For x = 0 To 252 Step 4
sb(i).b = T(x + 0)
sb(i).g = T(x + 1)
sb(i).r = T(x + 2)
i = i + 1
Next
即 64 组 (B,G,R,保留) 四元组,下标 0-63,对应调色板寄存器值 $00-$3F。
楼中楼 1(VB步步高,2025-06-17):制作 SB2000 的 16 色 BMP 格式图片时,必须从上面的 64 色中选择 16 个色的 RGB 值写入 BMP 的调色板,其他 RGB 值无法正常显示。制作 drw 格式图片时比较轻松,只需要在 $03-$12 写入 16 个色号[$00-$3F]即可——因为它的画图软件计算 RGB 值能力差,不能把 BMP 的普通 RGB 值准确转换,有图形,但是颜色基本全错。
楼中楼 2(VB步步高,2025-06-17):如果先制作 drw 格式,再用 2000 的画图另存为 BMP,是一个省力的方法——它会自动把 16 个字节的色号转换成 RGB 值 64 字节存入 BMP 文件的调色板。用普通图片转换软件制作的 16 色图片的颜色 SB2000 识别不正确,显示不正常。
──────────────────────────────────────────────────
第 17 楼(free_dongge,2025-11-08,甘肃)
崇拜滔滔江水连绵不绝般地崇拜
──────────────────────────────────────────────────
本地机器验证(2026-10-02,本项目实测)
样本:小霸王SB2000软盘镜/(100 张 FAT12 镜像)扫描发现 28 个 .DRW 文件(集中于 003.IMA)。
帖内断言 | 验证结果 |
drw 大小固定 3+16+22400 字节 | 28 个文件 全部 size=22419 |
前 3 字节为图片标识 | 26 个为 5A 58 4D("ZXM");01.DRW=A9 30 8D、10.DRW=FF FF FF(个别损坏/空白) |
16 字节调色板,取值 $00-$3F | 如 05.DRW:3F 08 0A 03 02 04 0C 08 32 25 1A 36 12 25 22 30 |
700 个 tile(28×25)×32 字节、4 位平面 | 按此模型渲染出完整图片 |
tile 内字模排布 | 平面优先:tile 内字节偏移 = 平面 z×8 + 行 j;字节 bit7→0 对应该行 8 像素自左向右;索引 bit z = 位平面 z |
第 3 楼 Clng(&H4D585A) | 小端写出 = 5A 58 4D = "ZXM"(截图末位字符依文件头校正) |
有声图片 EXE 头 4C 10 80 AA 55 | 与本项目已验证的 EXE 头 16 字节规范一致 |
验证渲染:out/probe/drw05_z_fast.png(003.IMA 之 05.DRW,灰度索引图,雉鸡图案完整无错位)。
反例记录:错误排布(行优先 j×4+z / 整屏连续平面)均渲染为噪声。
与本项目既有结论的互证:4 组调色板、16 色模式位(CPU $2000 bit7)、命名表 4 组 —— 与《SB2000开发资料汇编》§10.3 及 PPU_UM6576 逆向(attr 高 4 位 = tile_bank → m_bPalette[tile_bank*4+index])完全吻合。
待实测的新线索:① 4048-404F DMA 写显存端口组(与已验证的 4040-4047 窗口模型不同,未探测);② 第 14 楼称 0 组调色板位于显存 $0200(本项目实测调色板在 PPU $2040-$207F,疑为不同屏组/不同表述);③ 64 色系统调色板 RGB 表可整理为查色表供转换器使用。
贴吧_小霸王SB2000编程技术_原文存档
贴吧原帖存档:小霸王SB2000编程技术
存档说明:本文件为原文逐字转录,未做任何改写(含原文的笔误与措辞)。
甄别意见见同目录 SB2000开发资料汇编.md。
项 | 内容 |
来源 | 百度贴吧 · 小霸王吧 |
原帖 URL | https://tieba.baidu.com/p/9825415521 |
标题 | 小霸王SB2000编程技术 |
作者 | VB步步高(铁杆吧友) |
发布时间 | 2025-06-27,IP 属地:河南 |
抓取时间 | 2026-10-01(主楼正文)/2026-10-01(后续楼层,用户截图补录) |
抓取方式 | 渲染抓取(本机 curl 被百度风控 403 拦截,改用可渲染的抓取通道)+ 用户人工截图 |
抓取完整性声明:首次仅取到主楼正文("一、内存""二、显存"两节)。2026-10-01 用户
提供 8 张原帖截图,补录第 3/5/6/8/9 楼及部分回复,即"三、调色板""四、BIOS调用"
"五、DMA传送""六、TIMER的使用"全文及 MTV 播放器实例。至此楼主的技术章节基本齐全;
仍未取得:《说说SB2000的图像》全文,以及两次 WebFetch 截断处之间的零散楼层。
截图中大段机器码 hex 串为人工转录,个别字符可能失真,引用前请对照原帖核实。
2026-10-02 更新:《说说SB2000的图片》一帖已由用户截图补录并单独立档,见
同目录 贴吧_说说SB2000的图片_原文存档.md(其中 DRW 图片格式断言已用本地
小霸王SB2000软盘镜/ 实测验证)。
──────────────────────────────────────────────────
正文
小霸王SB2000编程技术
一、内存
小霸王SB2000多媒体学生电脑内存有512K(ROM)+512K内存(RAM)+8K+2K。512K(地址:8000-BFFF) +8K(6000-7FFF)+2K(0000-07FF )。其中,512K 内存可以通过4040-4047这8个端口切BANK到BANK8-BANKF。只支持4K切。BANK号=00-FF(16进制),其中00-7F为ROM,即固化的BIOS。开机后,BANK[04-07]这16K常驻C000-FFFF。
进入后DOS运行的软件加载到RAM8000-BFFF,第一个BANK号=80.软件最大支持448KB,加载后BANK号为[80,81...EF].软件没有文件头,仅要求头16个字节=[4C XX XX AA 55 00 00 。。。 ]。第一个字节就是软件的第一条指令,必须为JMP指令。$03,$04=AA 55是SB2000的软件的专用标识.
前16K字节4个4K的BANK不用切BANK,可以直接使用。地址=8000-BFFF。要使用之后的数据就需要切BANK了。比如第5个BANK要使用,就写A9 84 8D 40 40.这时8000-8FFF的内存变为第5个BANK,即用户软件的$4000-4FFF地址的内容。写A9 84 8D 41 40,切到9000-9FFF。写A9 84 8D 42 40,切到A000-AFFF。写A9 84 8D 43 40,切到B000-BFFF。C000-FFFF一般情况下为BIOS,可以通过4044-4047切到BANKC,D,E,F。普通情况下不要这样切。除非不需要调用BIOS的MMC3游戏模式才需要切CDEF。
二、显存
SB2000的显存有512K(VRAM)+PPU自带的10K(VRAM)。显存地址支持0000-27FF,8000-FFFF这两段。读写显存可以用FC 通用的2006+2007两个端口,也可以使用2000独有的DMA方式传送。
扩展显存512K地址为8000-FFFF,分为16个BANK,每块BANK为32KB。BANK号为00-0F,切显存端口为4300.
比如,A9 00 8D 00 43,就指定使用显存第的第1个32K,BANK号=0. A9 01 8D 00 43,BANK号=1,显存内容变为第2个32K的数据。。。。。。。A9 0F 8D 00 43,指定使用最后一块32K显存,BANK=0F。
扩展显存里的数据就是像素数据(PGT)。在16色模式下,每32个字节=64个像素=1个TITLE=1个8__ITALIC__8字符。在4色模式下,每16个字节=8个像素。切换颜色模式的端口是2008的位7.如A9 00 8D 08 20,使用4色模式;A9 80 8D 08 20,使用16色模式。4色模式同传统的FC一样,这里不多说了。16色模式下,32个字节每8字节为一组,共4组。一个字节有8位,代表8个2色像素,是屏幕上的一个8__ITALIC__8字符的一条横线,取值0-1。顺序从左到右,即位7=左第1点,位0=第8个点。每组8个字节中,一个字节代表一条线,8个字节代表从上到下8条线。每一组每一个字节的每一位=0或1,四组相同位置的字节的相同位一共有4个0或1,2__ITALIC__2__ITALIC__2__ITALIC__2=16.共16种变化即16色。计算方法是第一组的位[0,1]+2__ITALIC__第二组的位[0,1]+4__ITALIC__第三组的位[0,1]+8__ITALIC__第四组的位[0,1],得到一个0-15的数字,即16色的色号。至于制作和显示这种16色字符的方法 ,详情及程序请参阅我写的《说说SB2000的图像》。
在SB2000中,使用8000-FFFF储存图形库,和FC使用0000-1FFF存储图形库是不一样的。
SB2000使用0000-1FFF存储4个命名表(PNT),分四块:0000-07FF,0800-0FFF,1000-17FF,1800-1FFF,分别代表四块屏幕00,01,10,11的命名表。至于使用哪个命名表,由写端口2000的位0+位1确定。SB2000的PPU2000-27FF也可以存储命名表。2008端口的位0=0,使用0000-1FFFF作4块命名表,4屏幕。位0=1,仅使用2000-27FF这一块命名表,单屏幕。命名表每2个字节代表一个字符号,2K字节共1024个字符号。但只有前960个字节能显示。因为屏幕像素256__ITALIC__240__ITALIC__8__ITALIC__8=32__ITALIC__30=960.
SB2000无属性表,不象在FC 的PPU里每个命名表960字节后接一个64字节的属性表。SB2000的每个字符号是2字节16位,第2个字节的高2位=字符的属性。代表00-03四组背景调色板中的一组。第2个字节低4位(*256)+第1个字节8位共12位,=0000-0FFF ,叫字符名,TITLE NAME。举例:
NAME=00 00,NAME=0,属性=0.
NAME=00 01,NAME=1,属性=0.
NAME=FF 0E,NAME=$EFF,属性=0.
NAME=FF 4E,NAME=$EFF,属性=1.
NAME=FF 8E,NAME=$EFF,属性=2
NAME=FF CE,NAME=$EFF,属性=3.
NAME=FF C0,NAME=$0FF,属性=3
在4色模式中,字符名 __ITALIC__ $10(__ITALIC__16)就是这个字符名对应的图形表地址。
在16色模式中,字符名 __ITALIC__ $20(__ITALIC__32)就是这个字符名对应的图形表地址。
注意,在16色模式中,字符名需要是偶数。如02 08=802,04 08=804,对应地址=8020,8040.如果写成01 08,对应地址=8100,这不是一个32字节图形的开始地址,是错误的。在4色模式中没这个要求。
──────────────────────────────────────────────────
后续楼层(用户截图补录,2026-10-01)
三、调色板(第3楼,2025-06-27)
SB2000的调色板是64字节,使用2040-207F这64个端口分别写调色板的64个字节。字节取值范围=00-3F
在16色模式下,调色板每16个字节为1组16色,共四组16色:00号-03号。字符的属性就是指定这个调色板的组号。如:调色板第1个字节=20=白,第17个字节=3F=黑,
字符属性=0,从字符名+图形表中得到的对应字符的四组图形字节第一个字节为00 00 00 00,则代表这是一条白线。
字符属性=1,从字符名+图形表中得到的对应字符的四组图形字节第一个字节为00 00 00 00,则代表这是一条黑线。
理论上,屏幕可以显示4组调色板中的64色。虽然每个字符只能使用一组调色板的16色,但是不同字符可以任意选择4个调色板组号的任何1个,所以屏幕上可以出现64色。当然了,64色中有几个色号是相同重复的,并不一定真正达到64色。
注意,每组调色板16色可以任意选择,不是从00-0F,10-1F这样的固定组合。要根据图形的颜色制定合适的调色板。
四、BIOS调用(第5楼,2025-06-27)
SB2000的BIOS共512K,但最重要的是第2个16K,即BANK 04-07,固定装入内存C000-FFFF,作为软件应用环境的主要功能。BIOS调用的方式是:20 F0 FF。BIOS的功能号=$063E。比如要调用4号BIOS功能,就写:
A9 04 8D 3E 06 20 F0 FF
BIOS调用不需要带很多参数。最经常使用的参数是$063F。不过有的功能也使用别的内存值当参数。
下面以我研究出来的第一个BIOS功能为例。
BIOS功能号=80 功能=读软盘到内存
$063E=80;功能号
$063F=04,参数,为0读1盘
$F3=00,$F4=80;结束BANK标识号,读盘BANK数
$6200-$6207=01 01 1D 80 00 00 04 80;00=磁头 01=首扇区 02=磁道 03=读盘存储内存起始BANK号 07=BANK数=$80=128 04和05,通常为00,06不能为00
设置以上10个参数加1个功能号后,JSR $FFF0,运行结果是吧软盘$84800起512K的数据读入内存。从80号=FF号128个4KB的bank的512KB数据全部变成软盘读出来的512KB数据。
如把6203从80改成90,则6207得改成70,因为从90BANK开始到内存结尾只有$70个BANK可用。只有从80BANK开始,才有共$80,128个4K的BANK可用。
下面这几个参数加载软盘A6400到80:8000,大小=256KB,在执行BIOS功能前的状态:
6200: 01 11 24 80 00 00 02 80
00F3: C0 40
063E: 80 063F=04
(同楼后续自我勘误回复,2025-06-27):
· 则6207得改成70,这句话有误。6207固定为80
· $00F3=内存目标首BANK号+BANK数=结束标识BANK号,$00F4=BANK数。
· 感觉6207=80意思是应该=内存目标地址8000
五、DMA传送(第6楼,2025-06-27)
精灵使用2014端口DMA传送这里不讲了,和FC一样。
SB2000的4048-404F这8个端口组合,可以快速高效地自动化不消耗CPU性能地把主内存数据传送到显存VRAM或低端内存WORKRAM。
把这8个数C0 10 00 80 00 80 00 80,分别写入4048-404F端口,可以把内存10BANK的8000地址起$8000个字节共32K的数据写入显存8000-FFFF。注意,4048端口最后写,后7个仅是参数,4048才是功能,先写功能再写参数无用。(注意:如果上面读软盘512KB是图片数据,这个功能前加上写4300切显存BANK循环16次,就可以把软盘中读取的512KB数据=16张图片写满512K显存)
把这8个数A0 12 00 80 00 69 00 07,分别写入4048-404F端口,可以把内存12号BANK的8000地址起$0700个字节的数据写入内存6900-6FFF。注意,4048端口最后写,后7个仅是参数,4048才是功能,要先写后7个参数再写4048。
注意,4049写的BANK号不是4K的BANK号,而是32K字计算的BANK号,所以取值范围=00-1F。00-0F等于512的ROM,10-1F才是RAM。所以起始地址是8000-FFFF可用。传输显存时,目标地址范围=0000-FFFF。传输低端内存时,目标=0000-7FFF。
六、TIMER的使用(第8楼,2025-06-28)
SB2000有一个强大的定时器TIMER功能,支持1.7898MHZ和15625HZ两个速率。置4033的位7=1,就启用了TIMER。例如:A9 80 8D 3340,或A9 FF 8D3340。这个端口其他位是键盘和鼠标等IRQ功能。4034高4位是功能控制,位7=1=启用,位6=1=连续,位5=0=1.7898MHZ,位5=1=15625快。4034的低4位和4035的8位是控制间隔时间的,数越大间隔时间越慢,数越小越快。播放8000Bbps的音乐时,A9 FF 8D 4035,A9 CF 8D 4034,即将速度控制到最慢。这是它支持的最慢速度。将这两个数调小可以播放更高速度的WAV,比如11025,22050等。
播放音乐的程序指针要写到零页24和25单元。比如音乐程序位于内存68A8地址,就写A9 68 85 25,A9 A8 85 24,58(启用IRQ),BIOS会自动检查4033端口位8是否等于1,是就根据4034和4035的设置TIMER速度,调用($24)指针的timer程序。这里我将它定为音乐播放程序。
68A8: 20 F0 68 EE F1 68 D0 1F EE F2 68 AD F2 68 C9 80
68B8: D0 15 A9 70 8D F2 68 20 90 6A AD EF 68 D0 05 A9
68C8: 80 8D EF 68 20 59 6A 58 60
68F0: AD 00 70 8D 27 40 60 读声音采样,这声音DAC端口
695A: AD8968186910EA8D8968AD8968D014A9808D8968EE7F68AD7F68C920D005A9108D7F68207E6860
(第8楼回复1,2025-06-28):最慢速率只有8000,播放4000怎么办?有办法:就是每个4000的采样播放两遍,第一次播放的时候正常改采样指针,第二次播放的时候只播放不改指针。在播放程序之前加上E6 AA A5 AA 29 01 D0 40 直接跳过正常播放程序跳到68F0,并利用68F0最后的60指令直接返回BIOS。
(第8楼回复2,2025-06-28):
6A90: EEEF68EEEE68ADEE68D005A9108DEE68C920D005A9108DEE6888D004360
MTV播放器实例(第9楼,2025-06-28/29)
完整机器码(用户 2026-10-01 提供的原帖逐字文本,738 字节):
4C1080AA550000000000000000000000A000B90080990067C8D0F7A000B90081
990068C8D0F72080824C306700000000A9808DF300A000B9FB67C9FFD00160A9
808D0762A9808D3E06A9048D3F06B9FB678D0062C8B9FB678D0262C8B9FB678D
0162C8A9008D0662B9FB678DF4008D05620E05622E06620E05622E06620E0562
2E0662ADF3008D0362A9008D0462ADF300186DF4008DF300EAEAEAEAEA20F0FF
EAEAEAEAEAEAEAEAEAADF0008D0043ADF0008D4940A9008D4A40A9808D4B40A9
008D4C40A9808D4D40A9008D4E40A9808D4F40A9C08D4840E6F0A5F0C920D0C9
20106A4C0068000000000000000000000000000000000000000000011D0180FF
A9808DF300A000B9E168C9FFD00160A9808D0762A9808D3E06A9048D3F06B9E1
688D0062C8B9E1688D0262C8B9E1688D0162C8A9008D0662B9E1688DF4008D05
620E05622E06620E05622E06620E05622E0662ADF3008D0362A9008D0462ADF3
00186DF4008DF300EAEA20F0FF4CB06A0000000000000000000000000000A910
8D4940A9008D4A40A9808D4B40A9008D4C40A9708D4D40A9008D4E40A9108D4F
40A9A08D484060EAE6AAA5AA2901D040F005000000000020F068EEF168D01FEE
F268ADF268C980D015A9708DF26820906AADEF68D005A9808DEF6820596A5860
0000010180FF00000000000000001080AD00708D274060000000000000000000
003F2015080921242517182C30333810A9658D3140A9768D3140A9808D0820EA
A210A000B9006A994020C8CAD0F6EAEAA9088501A90085008D06208D0620A500
8D0720A5018D0720E600E600D008E601A501C910F002D0E660AD8968186910EA
8D8968AD8968D014A9808D8968EE7F68AD7F68C920D005A9108D7F68207E6860
A000B9008299006AC8D0F76000000000EEEF68EEEE68ADEE68D005A9108DEE68
C920D005A9108DEE688D004360000000A9688525A9A88524A9F08D3540A9CF8D
3440A9808D3240EA58EAA9A8D0FE0000EECF6AADCF6AC97FF0016020F0684CB3
6800
说明:以上为原帖 hex 逐字转录(按 32 字节/行折行)。实测 738 字节,与楼主所说
"大小 768 字节"差 30 字节(可能是原帖粘贴时的疏漏,或 768 为约数)。
本程序的反汇编清单见同目录 MTV播放器_反汇编.txt,结构化解读见
SB2000开发资料汇编.md §14.5。
楼主说明(2025-06-28):这是SB2000上的一个软件代码,大小768字节,从DOS运行后自动从软盘加载512KB图像数据,写入6区显存,这16张图片自动轮播。再从软盘加载512KB声音数据(44Kps,8位单位单声道)到内存,循环播放这128秒音乐。构成一个MTV播放程序。WAV数据在软盘$4800,大小$80000。$84800开始是图片,每$8000字节一幅图片。
(回复,2025-06-29):IRQ TIMER用8Kbps的速率播放4Kbps的声音,只是为了多点播放时间。512K内存还是小了。这个软盘镜像在软驱一哥群有下载。镜像里程序、图片数据、音乐数据都有。autoexec.bat作用用开机直接运行软件进入MTV软件$BMTV.EXE(软盘地址=4200)。4600=autoexec.bat 4800之后是数据。