新竹县网站建设_网站建设公司_电商网站_seo优化
2026/1/8 9:53:10 网站建设 项目流程

目录

0. 先把对象讲清:你在给谁写软件?

1. ROM / 卡带:为什么“同一台机器”却有几百种内存布局?

1.1 iNES / NES 2.0:模拟器里常见的 ROM 容器格式

1.2 PRG-ROM / CHR-ROM / CHR-RAM 的软件含义

1.3 Mapper(映射器)到底在帮你干嘛?

2. CPU 侧:2A03 程序怎么“活过来”并以帧为单位运行?

2.1 CPU 内存映射:你写的每个地址都可能“不是内存”

2.2 RESET / NMI / IRQ:为什么游戏离不开中断?

2.3 帧预算:你每帧到底有多少 CPU 时间?

3. PPU 侧:为什么背景/精灵这么“规矩”,滚屏还这么麻烦?

3.1 PPU 的 VRAM 空间与你能写的东西

3.2 图块与 pattern table:8×8、2bpp 是一切的原子

3.3 Nametable + Attribute table:背景不是“随便上色”的

3.4 PPU 寄存器:$2000-$2007 的“镜像与锁存”

3.5 滚屏(Scrolling):Loopy 寄存器与“分屏技巧”的根

3.6 精灵(Sprites)与 OAM:64 个精灵,但每条扫描线最多 8 个

3.7 OAM DMA:为什么大家都用 $4014 一次性搬运精灵?

4. 输入:手柄是“串行读”的,不是直接读 8 个 bit

5. 声音:APU 五声道与“音频驱动”的程序组织

5.1 游戏里的常见音频架构

6. “每帧一循环”的软件骨架:FC 游戏为什么都长得像这样?

6.1 Reset 初始化阶段(只跑一次)

6.2 主循环(Game Loop)

6.3 NMI 处理(每帧 VBlank)

7. 为什么滚屏关卡要用“元图块 metatile”、还要做“列/行流式更新”?

8. Mapper 深一点:为什么 MMC3 这类板卡能做出更“现代”的效果?

8.1 更灵活的 bank switching

8.2 扫描线 IRQ / 分屏

9. 性能与坑点清单(非常“软件原理”)

9.1 时间预算与 DMA

9.2 PPU 寄存器的锁存与读写副作用

9.3 精灵限制与闪烁策略

9.4 NMI 的边界行为

10. 开发与调试:现代人写 FC 游戏通常怎么落地?

11. 一份“写红白机游戏软件”的知识点 Checklist(写引擎前先对齐)

12. 推荐你下一步怎么学(最有效的路径)


下面这篇是把“红白机游戏软件”从ROM 格式 → CPU 程序 → PPU 出图 → APU 出声 → 手柄输入 → 卡带映射器(Mapper)串起来的一套知识点框架。你读完后,基本就能理解:为什么 FC 游戏代码看起来像“在一台很奇怪的机器上做实时控制”,以及商业游戏常见的程序结构为什么是那样。


0. 先把对象讲清:你在给谁写软件?

红白机游戏软件不是运行在“通用电脑”上,而是和硬件绑定得非常紧:

  • CPU(RP2A03 / 2A03):基于 6502 内核,NTSC 机型约1.79MHz,且没有十进制模式。nesdev.org

  • PPU(2C02):负责图像合成输出;屏幕按扫描线工作;NTSC 一帧 262 条扫描线,其中约 20 条处于 VBlank。nesdev.org

  • APU:音频合成(方波/三角波/噪声/DMC);帧计数器由 $4017 控制并驱动包络/扫频/长度计数等。nesdev.org

  • 卡带(Cartridge):里面的PRG-ROM/CHR-ROM/CHR-RAM/PRG-RAM,以及最关键的Mapper(映射器)决定“CPU/PPU 看到的地址空间长什么样”。nesdev.org+2nesdev.org+2

  • 手柄:通过移位寄存器串行读按键,写 $4016 的 strobe 进行锁存,然后从 $4016/$4017 逐位读取。nesdev.org

结论:FC 游戏更像在写一套实时固件:你要在每帧固定预算内更新画面/声音/逻辑,而且很多 I/O 只能在特定时机做(尤其是 PPU)。


1. ROM / 卡带:为什么“同一台机器”却有几百种内存布局?

1.1 iNES / NES 2.0:模拟器里常见的 ROM 容器格式

多数模拟器使用iNES或升级版NES 2.0头描述 ROM:

  • iNES 头(16 字节)最核心字段:PRG-ROM 大小(16KB 单位)、CHR-ROM 大小(8KB 单位)、Flags 里包含 mapper 编号低/高位、镜像方式、电池、trainer 等。nesdev.org+1

  • NES 2.0 是对 iNES 的向后兼容扩展,能更准确描述更大的 ROM、子映射器等。nesdev.org

实战意义:你写游戏时要明确自己目标板卡(NROM / MMC1 / MMC3…),因为它决定你怎么组织 PRG/CHR、怎么切银行、怎么做滚屏/中断。

1.2 PRG-ROM / CHR-ROM / CHR-RAM 的软件含义

  • PRG-ROM:放 CPU 执行代码与只读数据(关卡表、脚本、压缩资源等)。

  • CHR-ROM/CHR-RAM:放 PPU 的图块数据(pattern tables)。CHR-ROM 固定图形;CHR-RAM 则允许你在运行时把新图块“写进显存侧”,适合动态字库/特效(代价是需要 VBlank 时间写 VRAM)。PPU 内存通过 $2006/$2007 由 CPU 间接访问。nesdev.org+1

1.3 Mapper(映射器)到底在帮你干嘛?

Mapper 主要提供:

  • PRG bank switching:让 CPU 在 $8000-$FFFF 这段能“看到不同的 PRG-ROM 银行”,从而突破 32KB 限制。

  • CHR bank switching:让 PPU 在 pattern table 地址段能切换不同 CHR 银行。

  • 额外能力:例如 MMC3 常见的扫描线 IRQ 计数器,方便做分屏/状态栏等。

常见 homebrew 入门板卡:NROM(最简单)、UxROM/BxROM(CHR-RAM + PRG 切换)、MMC1/MMC3(更细粒度切换 + 更多功能)。nesdev.org


2. CPU 侧:2A03 程序怎么“活过来”并以帧为单位运行?

2.1 CPU 内存映射:你写的每个地址都可能“不是内存”

理解 FC 软件,第一要背的就是 CPU 地址空间大体结构:

  • $0000-$07FF:2KB 内部 RAM(并在 $0800-$1FFF 镜像重复)

  • $2000-$2007:PPU 寄存器(并在 $2008-$3FFF 镜像,每 8 字节镜像一次)nesdev.org

  • $4000-$4017:APU 与 I/O(含手柄 $4016/$4017、OAM DMA $4014、APU 帧计数 $4017 等)

  • $4020-$FFFF:通常留给卡带映射(PRG-ROM/PRG-RAM/mapper 寄存器等;nesdev 也提示某些解码怪癖导致 $4000-$401F 映射可读内存可能引发 DMA 问题,建议卡带可读映射从 $4020 起)nesdev.org

2.2 RESET / NMI / IRQ:为什么游戏离不开中断?

6502 系列通过固定向量进入中断(NMI/RESET/IRQ)。实际游戏里最常用的是:

  • RESET:开机/重置入口。

  • NMI:PPU 在 VBlank 期间可以触发 NMI(不可屏蔽中断),这是游戏每帧更新 PPU 的黄金时机。PPU 是否输出 NMI 由 PPUCTRL.7 控制,并且 NMI 行为有一些边界细节(比如 VBlank 中切换 NMI 输出可能导致多次 NMI)。nesdev.org

2.3 帧预算:你每帧到底有多少 CPU 时间?

经典“为什么 NES 游戏要把图形更新放 NMI”的答案是:VBlank 很短

  • NTSC:262 扫描线,其中 20 条 VBlank;收到 NMI 后到开始渲染前大约2270 CPU cycles可用于更新调色板、精灵、背景等。nesdev.org

  • 周期参考表还给出:一条扫描线 341 PPU dots ≈ 113⅔ CPU cycles;整帧约 29780.5 CPU cycles;OAM DMA 约 513(或 514)CPU cycles。nesdev.org+1

实战心法:不要把重逻辑塞进 NMI。NMI 主要做“把上一帧准备好的图形/精灵/调色板更新提交给 PPU”,主循环做游戏逻辑与资源准备。


3. PPU 侧:为什么背景/精灵这么“规矩”,滚屏还这么麻烦?

3.1 PPU 的 VRAM 空间与你能写的东西

PPU 有自己 16KB 的地址空间映射规则(pattern table、nametable、palette 等),并可通过 $2006/$2007 访问。NES 典型有 2KB PPU RAM 用于 nametable 区(但可被卡带改线成不同镜像/四屏等)。nesdev.org

3.2 图块与 pattern table:8×8、2bpp 是一切的原子

FC 背景和精灵都以8×8 tile(character)为基本单位(图案数据来自 pattern table)。你写游戏资源时往往把大图拆成 tile,再通过 nametable 去拼。

这会带来一整套“工具链”:

  • 美术画大图 → 切 tile → 导出 CHR

  • 用 metatile(例如 16×16,由 4 个 tile 组成)作为关卡编辑基本单位

  • 再把 metatile 映射到实际 nametable 更新

3.3 Nametable + Attribute table:背景不是“随便上色”的

一个 nametable 是 1024 字节:

  • 960 字节(32×30)存 tile 索引

  • 64 字节 attribute table 存调色板选择(按区域分配)nesdev.org

调色板 RAM 位于 $3F00-$3F1F:背景与精灵各 4 组调色板,每组 4 色(其中第 0 项有“透明/底色”的特殊语义)。nesdev.org

实战后果:你做背景美术时经常要遵守“一个 16×16(或更大区域)只能用同一子调色板”的限制,所以老游戏会出现非常“规整”的色块分区。

3.4 PPU 寄存器:$2000-$2007 的“镜像与锁存”

PPU 对 CPU 暴露 8 个寄存器,名义上在 $2000-$2007,但因为地址译码不完整,$2008-$3FFF 每 8 字节镜像一次。nesdev.org

此外很多寄存器写入有“二次写锁存”机制(典型是 $2005/$2006 共享内部 w 锁存),写错顺序会导致滚动/地址混乱。nesdev.org+1

3.5 滚屏(Scrolling):Loopy 寄存器与“分屏技巧”的根

滚屏核心是对 $2005(PPUSCROLL)与 $2006(PPUADDR)的一系列写入来控制 v/t 等内部寄存器;想做分屏(上方状态栏固定、下方滚动)需要在特定扫描线时机重载滚动相关寄存器。nesdev.org+1

这也是为什么 MMC3 的扫描线 IRQ 很重要:它能在恰当时机打断 CPU,让你写几条指令改滚动/改 bank,实现稳定分屏。

3.6 精灵(Sprites)与 OAM:64 个精灵,但每条扫描线最多 8 个

  • OAM(Object Attribute Memory)共 256 字节,每个精灵 4 字节,因此总计64 个精灵(这点很多教程会强调)。jianshu.com

  • PPU 有“精灵评估”流程:每条扫描线最多挑出 8 个“命中范围”的精灵进入次级 OAM,超过时会继续扫描并可能置位 sprite overflow flag。nesdev.org

实战后果:

  • 你会看到“闪烁”的精灵(sprite flicker):用软件轮换优先级,让不同帧显示不同精灵,规避 8/scanline 限制。

  • 角色用 metasprite(多个硬件 sprite 拼成一个大角色),这会更快撞上 scanline 限制,所以设计时要控制宽度/高度和同屏敌人数。

3.7 OAM DMA:为什么大家都用 $4014 一次性搬运精灵?

精灵数据更新常用“影子 OAM(CPU RAM 中的 256 字节缓冲)”+ 每帧 VBlank 用 DMA 一把拷到 PPU OAM:

  • 写 $4014 会触发 OAM DMA:暂停 CPU,执行对 256 字节的 get/put,耗时513 或 514 cycles。nesdev.org+1


4. 输入:手柄是“串行读”的,不是直接读 8 个 bit

标准手柄读法要点:

  1. 向 $4016 写入 1 再写入 0,产生 strobe 让手柄把当前按键状态锁存进移位寄存器。

  2. 之后连续读取 $4016(1P)或 $4017(2P)8 次,每次返回一个按键位(顺序一般 A/B/Select/Start/Up/Down/Left/Right)。
    当 strobe 为高时,读取会一直返回第一个键(A)的当前状态;strobe 置低后才会开始移位输出。nesdev.org

实战建议:

  • 每帧只读一次手柄,做“本帧按下/抬起边沿检测”,更容易写出稳定的跳跃/射击手感。

  • 输入读写也尽量固定在帧节奏里(例如 NMI 末尾或主循环帧同步点)。


5. 声音:APU 五声道与“音频驱动”的程序组织

APU 经典 5 声道:2 个 pulse(方波)、1 个 triangle(三角)、1 个 noise(噪声)、1 个 DMC(采样)。很多资料会用这套描述作为入门。阿里云

更“软件驱动层”的核心是:

  • 帧计数器($4017):驱动 pulse/triangle/noise 的 envelope、sweep、length 等单元,NTSC 下大约 240Hz 触发节奏,并可配置 4-step 或 5-step,且 4-step 可选择在末尾产生 IRQ。nesdev.org

5.1 游戏里的常见音频架构

商业游戏往往把声音做成“独立小系统”:

  • 主循环写“事件”(播放音效/切 BGM/改音量)到队列

  • 音频驱动每帧(或更细粒度)更新 APU 寄存器

  • 音符序列(tracker 风格)→ 转成 pulse/triangle/noise 的参数(period、duty、envelope 等)

关键点:音频更新也会吃 CPU 周期,而且 VBlank 很短,所以常见做法是:NMI 中只做必要的 APU 寄存器刷新,把谱面解析、模式推进放在主循环或更低频率的 tick 中。


6. “每帧一循环”的软件骨架:FC 游戏为什么都长得像这样?

下面是一种非常典型、非常“红白机”的结构(用概念描述,不贴具体汇编):

6.1 Reset 初始化阶段(只跑一次)

  • 关渲染、关 NMI(先别让 PPU 打断你)

  • 清 RAM(至少清零页、工作区、OAM shadow)

  • 初始化栈指针、初始化变量、初始化 mapper(如果有)

  • 等待 1~2 次 VBlank(确保 PPU 状态稳定)

  • 加载初始关卡:准备 nametable 数据、调色板、CHR(若 CHR-RAM)

  • 开启渲染与 NMI

6.2 主循环(Game Loop)

  • 等待“本帧结束/下一帧开始”的同步点(很多引擎用 NMI 设置一个 flag)

  • 读取手柄 → 解析输入

  • 游戏逻辑:AI、物理、碰撞、状态机、计分等

  • 生成“渲染命令”:

    • OAM shadow(精灵表)更新

    • 下一帧要写入 PPU 的增量更新列表(例如要改的 tile、要改的调色板)

    • 滚屏参数、bank 切换计划等

  • 音频系统推进(或写事件给音频驱动)

6.3 NMI 处理(每帧 VBlank)

NMI 的目标是:在大约 2270 cycles 的窗口里,把准备好的图形更新提交给 PPU。nesdev.org
典型顺序:

  1. 写入 $2000/$2001 等(必要时关渲染/强制 blanking)

  2. 做 OAM DMA($4014)把 256B 精灵表推给 PPU(成本 513/514 cycles)nesdev.org+1

  3. 执行背景/调色板的增量 VRAM 更新(通过 $2006/$2007)notify-ctrl.github.io+1

  4. 更新滚屏寄存器($2005)与地址锁存的正确写序 nesdev.org+1

  5. 清 NMI 标志/设置“本帧完成”flag,RTI 返回

你会发现:NES 的渲染不是“画图 API”,而是“每帧向 PPU 寄存器与 VRAM 提交一小段补丁”。


7. 为什么滚屏关卡要用“元图块 metatile”、还要做“列/行流式更新”?

因为:

  • 一屏 32×30 tile(960 字节)+ attribute(64 字节),全量写 VRAM 很难在一次 VBlank 内完成。nesdev.org+1

  • 所以滚动关卡通常是:每帧只更新新出现的一列(横向滚)或一行(纵向滚),其余保持不动。

  • 关卡数据常以 metatile(例如 16×16)存储,更节省空间;渲染时把 metatile 展开为 4 个 tile,并同步维护 attribute 分区。

这种结构会自然引出两个技术点:

  1. 增量 VRAM 更新队列:主循环把“要写 VRAM 的地址+数据”写入队列;NMI 里按预算消费。

  2. 双缓冲思路

    • OAM:几乎总是 CPU 侧 shadow + DMA

    • 背景:用“逻辑地图缓冲 + PPU 的 nametable”配合滚动寄存器,让视觉连续


8. Mapper 深一点:为什么 MMC3 这类板卡能做出更“现代”的效果?

Mapper 的高级价值主要体现在两点:

8.1 更灵活的 bank switching

  • 大关卡、多音乐、多敌人、多脚本都需要更大 PRG;动态换 bank 才能装得下。

  • CHR bank switching 还能让你在同一帧不同区域用不同图块(配合 IRQ/分屏)。

8.2 扫描线 IRQ / 分屏

MMC3 等 mapper 被总结为“提供细粒度切换和扫描线计数等特性”。nesdev.org
软件层面你会这么用:

  • 游戏上方状态栏固定(不滚动),下方地图滚动

  • 或者实现水面波纹、背景视差(通过 mid-frame 改 scroll 或改 pattern bank)


9. 性能与坑点清单(非常“软件原理”)

9.1 时间预算与 DMA

  • VBlank 预算短(~2270 cycles),OAM DMA 就吃掉 513/514 cycles;所以你很难每帧大量更新背景。nesdev.org+2nesdev.org+2

  • 做复杂特效时经常需要“关渲染/强制 blanking”来延长可写 VRAM 的窗口,但会带来屏幕闪烁或黑屏过渡(商业游戏也常这么做)。nesmaker.nerdboard.nl

9.2 PPU 寄存器的锁存与读写副作用

  • $2005/$2006 的“两次写”与共享锁存(w)导致顺序错了就滚屏乱。nesdev.org+1

  • 访问 VRAM 常通过 $2006/$2007,并且“渲染期间乱写”可能造成明显图形故障,因此通常在 VBlank 做。notify-ctrl.github.io+1

9.3 精灵限制与闪烁策略

  • 每扫描线 8 精灵限制是硬约束,超了就要做软件层面的“轮换显示”。nesdev.org

9.4 NMI 的边界行为

  • NMI 由 vblank_flag 与 PPUCTRL.7(NMI 输出使能)共同决定;在 VBlank 中切换 PPUCTRL.7 且不读取 PPUSTATUS 可能触发多次 NMI,这些都是写引擎时要避开的坑。nesdev.org


10. 开发与调试:现代人写 FC 游戏通常怎么落地?

你可以用:

  • 纯汇编(最贴近硬件、最可控)

  • C(cc65)+ 少量汇编(提高开发效率,但仍需理解 NMI/PPU 细节)
    cc65 工具链通常涉及 cc65/ca65/ld65 三件套,很多教程会强调这一点。知乎专栏

调试上,多数人靠模拟器的:

  • PPU/CPU 断点、内存查看、OAM/nametable 视图

  • 逐帧、看 VBlank 更新是否超预算

  • 观察 sprite overflow、NMI 时序、mapper 寄存器写入等


11. 一份“写红白机游戏软件”的知识点 Checklist(写引擎前先对齐)

如果你打算真的写一个最小可运行 demo(能显示角色、能走、能滚屏、能放音效),建议你逐项确认:

  1. 选板卡/Mapper:先从 NROM 或 UxROM 起步;明确 PRG/CHR 组织。nesdev.org

  2. 搭骨架:RESET 初始化 → 主循环 → NMI 提交渲染。nesdev.org+1

  3. OAM 体系:shadow OAM(256B)+ 每帧 $4014 DMA。nesdev.org

  4. 背景更新策略:只做增量更新;滚屏用“列/行流式写入 + attribute 同步”。nesdev.org+1

  5. 手柄读取:$4016 strobe + 8 次串行读。nesdev.org

  6. 音频驱动:至少能按帧刷新 APU 寄存器;理解 $4017 帧计数器节奏。nesdev.org

  7. 时序预算:明确 VBlank 可用 cycles 与 DMA 成本;NMI 只干提交,主循环干重活。nesdev.org+2nesdev.org+2

  8. 避免坑点:$2005/$2006 锁存写序、NMI 使能边界、精灵 8/scanline。nesdev.org+2nesdev.org+2


12. 推荐你下一步怎么学(最有效的路径)

  • 先做一个NROM小程序:显示背景 → 放一个 metasprite → 手柄移动 → 每帧 DMA 精灵

  • 再做滚屏:用 nametable + $2005/$2006 正确写序,做“每帧一列更新”

  • 最后上Mapper(UxROM/MMC1/MMC3):加大 PRG/CHR,做分屏与更复杂关卡

如果你愿意,我可以按你偏好的方向再给一个“更落地”的版本,例如:

  • 以《魂斗罗》式横版卷轴为目标的引擎结构(含 metatile/碰撞/刷怪/子弹管理)

  • 以 RPG 为目标的引擎结构(对话框/菜单/地图切换/脚本系统)

  • 以音乐为核心(写一个简化 tracker + APU 播放器)

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询