跳转到内容

超级任天堂编程/加载 SPC700 程序

来自维基教科书,开放的书籍,开放的世界

在本教程中,我们将创建一个 ROM,它将初始化 SPC700 以播放从另一个 SNES 游戏中捕获的歌曲。

为了在 SNES 上发出声音,需要将 DSP 的寄存器设置为适当的值。这意味着,要在 SNES 上播放歌曲,你需要一个用于 SPC700 的程序来操作 DSP,还需要一个用于 65816 的代码,将 SPC700 程序传输到 SPC700。幸运的是,网上有数千个用于 SPC700 的程序,以 SPC 文件的形式免费提供,解决了我们一半的问题。

SPC 文件

[编辑 | 编辑源代码]

SPC 文件包含 SPC700 的状态,通常是在 SNES 游戏中歌曲的开始时。通过在 SPC700 和 DSP 模拟器(又名 SPC 播放器)中恢复状态,你可以无需 SNES ROM 即可收听歌曲。我们也可以使用 SPC 文件在 SNES 内部恢复 SPC 状态以播放歌曲。你可以在 SNESMusic.org 上自己使用 SNES ROM 和模拟器捕获 SPC 文件,或者下载数千个在线文件。

从 SPC 文件中提取 SPC700 状态

[编辑 | 编辑源代码]

SPC700 文件包含 SPC 硬件状态以及各种附加信息,例如标题、游戏名称、作者、捕获者等。有关格式的详细说明可以在 SNESMusic.org 上找到,但与我们目的相关的字段是

偏移量 大小 描述
00025h 2 字节 程序计数器 (PC) 寄存器
00027h 1 字节 A 寄存器
00028h 1 字节 X 寄存器
00029h 1 字节 Y 寄存器
0002ah 1 字节 程序状态字 (PSW) 寄存器
0002bh 1 字节 堆栈指针 (SP) 寄存器
00100h 10000h 字节 64k RAM
10100h 128 字节 128 个 DSP 寄存器的内容

我们需要从 SPC 文件内部获取这些数据并将其放入我们的 ROM 中。你可以编写一个脚本从 SPC 文件中提取这些数据并将其转换为汇编数据指令的文本文件,然后.include在你的汇编文件中。但是,WLA 汇编器中的.incbin指令使此过程简单得多,因为它允许我们直接将二进制文件的一部分包含到我们的 ROM 中。以下是包含上述数据的方法

; The SPC file from which we read our data.
.define spcFile "test000.spc"

dspData:  .incbin spcFile skip $10100 read $0080
audioPC:  .incbin spcFile skip $00025 read $0002
audioA:   .incbin spcFile skip $00027 read $0001
audioX:   .incbin spcFile skip $00028 read $0001
audioY:   .incbin spcFile skip $00029 read $0001
audioPSW: .incbin spcFile skip $0002a read $0001
audioSP:  .incbin spcFile skip $0002b read $0001

请注意,我们没有在上面的数据定义中包含 SPC RAM 数据。由于 SPC RAM 数据 (64k) 大于 SNES 的 ROM 存储块大小 (32k),因此我们需要将其分成两半,并将其存储在两个独立的存储块中,与我们的代码和数据分开。

; The first half of the saved SPC RAM from the SPC file.
.bank 1
.section "musicData1"
spcMemory1: .incbin spcFile skip $00100 read $8000
.ends

; The second half of the saved SPC RAM from the SPC file.
.bank 2
.section "musicData2"
spcMemory2: .incbin spcFile skip $08100 read $8000
.ends

主程序

[编辑 | 编辑源代码]

我们的主程序使用了 SNES 初始化教程中的大部分代码。

Start:
    ; Initialize the SNES.
    Snes_Init

    jsr     LoadSPC

    ; Set the background color to green.
    sep     #$20        ; Set the A register to 8-bit.
    lda     #%10000000  ; Force VBlank and set brightness to 0%.
    sta     $2100
    stz     $2121
    lda     #%11100000  ; Load the low byte of the green background color.
    sta     $2122
    lda     #%00000000  ; Load the high byte of the green background color.
    sta     $2122
    lda     #%00001111  ; End VBlank, setting brightness to 100%.
    sta     $2100

    ; Loop forever.
Forever:
    jmp Forever

我们在初始化教程中的图形代码之前添加了一行,用于调用一个子例程来加载 SPC 数据。将此图形代码包含在内的优点是,在音乐加载后,它会在屏幕上执行一些操作。因此,当我们执行 ROM 时,它会在视觉上告诉我们音乐是否成功加载,或者执行是否在音乐代码中的某个地方停止。

上传 SPC700 状态

[编辑 | 编辑源代码]

SNES 和 SPC700 通过四个字节宽的通道进行通信,我们将其称为 Audio0、Audio1、Audio2 和 Audio3。在 SNES 端,这些由内存映射寄存器 $2140-$2143 表示,而 SPC 将其表示为 $00f4-$00f7。尽管有四个通道,但实际上后台存储了八个值 - 从 SNES 到 SPC 的通道的四个字节,以及从 SPC 到 SNES 的通道的四个字节。例如,当 SNES 将一个值写入其 Audio0 时,该值将保存在一个位置,以便 SPC 可以从其 Audio0 中读取该值。同样,当 SPC 将一个值写入其 Audio0 时,该值将保存在另一个位置,以便 SNES 可以从其 Audio0 中读取该值。因此,从通道的内存映射寄存器中读取的值可能不是最后写入的值。

SPC 的通信例程

[编辑 | 编辑源代码]

当 SNES 重置时,SPC 将一个 64 字节的 ROM 块(称为“IPL ROM”)映射到位置 $ffc0-$ffff 并执行该块。当它映射到该位置时,读取来自此 ROM 而不是正常的 RAM。它执行 SPC 的必要初始化。

  • 将堆栈指针设置为 $01ef。
  • 将内存位置 $0000-$00ef 清零。
  • 等待来自 SNES 的数据。

IPL ROM 例程能够将数据块从 SNES 复制到 SPC 内存,然后从给定位置开始执行。SNES Devkit 的文档 SNES Central 对 SPC 的确切通信协议有点令人困惑。你可以通过反汇编包含在 SNES 或 SPC 模拟器源代码或 SPC 文件本身中的 IPL ROM 字节码来查看例程。但是,以下是 SPC 使用的算法的摘要

  1. 初始化
    • 将 AudioOut0 设置为 $aa,将 AudioOut1 设置为 $bb。
    • 等待 AudioIn0 为 $cc。
  2. 准备复制一个块
    • 从 AudioIn2(低字节)和 AudioIn3(高字节)读取 16 位目标地址。
    • 将 AudioIn0 复制到 AudioOut0。
    • 如果 AudioIn1 为零,则从目标地址开始执行。
  3. 复制一个块
    • 等待 AudioIn0 为零。
    • 将一个字节大小的计数器设置为零。
  4. 复制一个字节
    • 等待计数器大于 AudioIn0。
    • 如果计数器等于 AudioIn0
      • 将一个字节从 AudioIn1 复制到内存位置。
      • 增加计数器和内存位置。
      • 转到步骤 4。
    • 否则(当计数器小于 AudioIn0 时),转到步骤 2。

一个特别精明或偏执的程序员会注意到,当 SPC 的字节大小计数器翻转(从 $ff 增加到 $00)时,它会变得小于 AudioIn0 中的值,这可能会导致错误:除非 SNES 及时更新 AudioIn0,否则 SPC 例程可能会认为块已结束,而 SNES 仍在发送数据。当这种情况发生时,SNES 和 SPC 可能会在协议的不同部分等待彼此,从而冻结系统 (这可能不正确 - 请参阅讨论页面上的注释)

因此,为了防止死锁,SNES 必须尽快更新 Audio0。这意味着将数据预先复制到可用的最快内存中,并在使用 IPL ROM 协议时禁用中断。或者,你可以将自己限制在复制小于 255 字节的块,这样计数器就不会翻转,或者你可以安装一个更好的通信例程在 SPC RAM 中,并使用它。

SNES 的通信例程

[编辑 | 编辑源代码]

既然我们已经从 SPC 的角度检查了协议,那么我们需要一个 SNES 例程来与之交互。首先,我们将检查一个类似于开源演示(以及推测的实际 SNES 游戏)中使用的例程。然后,我们将对其进行一个小的修改,以简化我们的代码。

以下是开源演示中使用的通用算法

  1. 等待 Audio0 为 $aa,这表示 SPC 已完成其初始化。
  2. 将一个字节大小的计数器初始化为 $cc。
  3. 如果没有更多块要发送
    • 将 16 位执行地址发送到 Audio2 和 Audio3。
    • 将 $00 发送到 Audio1。
    • 将计数器发送到 Audio0。
    • 等待计数器值在 Audio0 上回显。
    • 结束例程。
  4. 否则
    • 将 16 位目标地址发送到 Audio2 和 Audio3。
    • 将 $01 发送到 Audio1。
    • 将计数器发送到 Audio0。
    • 等待计数器值在 Audio0 上回显。
    • 将计数器重置为零。
  5. 如果有字节剩余在当前块中
    • 将当前字节发送到 Audio1。
    • 将计数器发送到 Audio0。
    • 等待计数器值在 Audio0 上回显。
    • 移到下一个字节。
    • 增加计数器。
    • 转到步骤 5。
  6. 否则
    • 将计数器加 $03。如果计数器现在为零,则再加 $03。
    • 继续到下一个块。
    • 转到步骤 3。

(注意:$03 的值没有什么神奇之处。我们可以将几乎任何值加到计数器上 - 重要的是 SNES 发送的计数器值需要大于 SPC 预期的值,这就是 SPC 知道块已结束的方式。)

此例程一次性发送所有块。但是,如果我们有一个例程可以只复制一个块,以便我们可以在传输块之间执行其他操作,那就太好了。如果我们尝试修改上面的例程来做到这一点,我们要么需要事先知道下一个块的地址,要么需要保存上一个块的终止字节并将其与下一个块一起发送。解决此问题的一个简单方法是发送一个块,然后将通信例程的起始地址($ffc9)作为开始执行的地址。这将重置协议状态,因此我们不需要在发送块之间存储任何信息。

汇编中的通信例程

[编辑 | 编辑源代码]

本节详细介绍了我们的汇编例程,用于将 SNES RAM 中的一块内存复制到 SPC RAM。

首先,我们需要考虑例程的参数。我们需要传递源位置、目标位置和要复制的块的长度。由于 SPC RAM 有 64k,因此目标和长度将适合 16 位变量,因此我们可以将它们传递到 X 和 Y 寄存器中。另一方面,源内存位置是 24 位长的,因为我们将从 $7f:0000-$7f:ffff 的扩展 RAM 块中读取。因此,我们在零页中定义了一个位置,在那里我们可以存储指向源数据的三个字节的指针。

.define musicSourceAddr $00fd

当我们使用它时,我们可以定义音频端口和 CPU 标志的值,以便我们的代码使用可识别的标识符而不是十六进制值。

.define AUDIO_R0 $2140
.define AUDIO_R1 $2141
.define AUDIO_R2 $2142
.define AUDIO_R3 $2143

.define XY_8BIT $10
.define A_8BIT  $20

在编写例程时,我们经常会等待 SPC 回显我们刚刚发送到 Audio0 寄存器的值。与其重复编写此代码,我们可以将其放在一个宏中,汇编器将进行必要的替换。

.macro waitForAudio0M
-
    cmp     AUDIO_R0
    bne     -
.endm

有了这些初始定义,我们可以编写我们的例程。这是初始化阶段,它会等待 SPC 准备好接受数据,然后发送目标地址。请注意,我们可以使用单个 16 位写入到 Audio2 来发送目标地址的两个字节。

CopyBlockToSPC:
    ; musicSourceAddr - source address
    ; x - dest address
    ; y - count

    ; Wait until audio0 is 0xbbaa
    sep     #A_8BIT
    lda     #$aa
    waitForAudio0M

    ; Send the destination address to AUDIO2.
    stx     AUDIO_R2

    ; Transfer count to x.
    phy
    plx

    ; Send $01cc to AUDIO0 and wait for echo.
    lda     #$01
    sta     AUDIO_R1
    lda     #$cc
    sta     AUDIO_R0
    waitForAudio0M

    ; Zero counter.
    ldy     #$0000

这是通信例程的主循环,它发送一个字节并等待 SPC 的响应,然后更新内存和计数器值。请注意,即使 a 当时处于 8 位模式,我们也可以使用 xba 操作交换 a 的高字节和低字节。同样,我们使用将 16 位值写入 Audio0 和 Audio1 以在一次操作中发送它们的技巧。

CopyBlockToSPC_loop:
    ; Load the high byte of a with the destination byte.
    xba
    lda     [$fd],y
    xba
    
    ; Load the low byte of a with the counter.
    tya

    ; Send the counter/byte.
    rep     #A_8BIT
    sta     AUDIO_R0
    sep     #A_8BIT

    ; Wait for counter to echo back.
    waitForAudio0M

    ; Update counter and number of bytes left to send.
    iny
    dex
    bne     CopyBlockToSPC_loop

最后,我们结束块并告诉 SPC 从 SPC 通信例程的开头开始执行,重置协议。

    ; Send the start of IPL ROM send routine as starting address.
    ldx     #$ffc9
    stx     AUDIO_R2
    
    ; Clear high byte.
    xba
    lda     #0
    xba

    ; Add a value greater than one to the counter to terminate.
    clc
    adc     #$2

    ; Send the counter/byte.
    rep     #A_8BIT
    sta     AUDIO_R0
    sep     #A_8BIT

    ; Wait for counter to echo back.
    waitForAudio0M

    rts

发送 SPC 状态

[编辑 | 编辑源代码]

SPC 状态由三个部分组成。

  • 内存
  • DSP 寄存器
  • CPU 寄存器

最难恢复的是 CPU 状态,因为它只是通过执行 SPC 通信例程而改变。此外,恢复程序计数器意味着 SPC 然后将执行存储的代码,而不是通信例程,因此我们不能在恢复程序计数器后发送任何其他内容。因此,我们需要在设置好其他一切之后,最后恢复 CPU 状态。我们先恢复内存还是 DSP 寄存器并不重要,但事实证明先恢复内存会很方便。

发送内存状态

[编辑 | 编辑源代码]

早些时候我们注意到,如果 SNES 发送数据不够快,SPC 通信例程可能会冻结。这意味着我们发送的任何数据都需要存储在 RAM 中;它不能直接从 ROM 中的原始位置复制到 SPC。因此,我们使用以下例程将包含 SPC 内存状态的两个 32k ROM 银行组装成 SNES RAM 中的一个 64k 段。

CopySPCMemoryToRam:
    ; Copy music data from ROM to RAM, from the end backwards.
    rep   #$XY_8BIT        ; xy in 16-bit mode.
    ldx.w #$7fff           ; Set counter to 32k-1.
-   lda.l spcMemory1,x     ; Copy byte from first music bank.
    sta.l $7f0000,x
    lda.l spcMemory2,x     ; Copy byte from second music bank.
    sta.l $7f8000,x
    dex
    bpl -
    rts

现在,我们可以使用之前编写的宏来传输 SPC 内存状态。

    ; Copy RAM between 0x0002 and 0xffc0.
    sendMusicBlockM $7f $0002 $0002 $ffbe

我们不传输前两个和最后六十四字节的内存,因为它们被通信例程使用:最后六十四字节包含例程本身,前两个字节在例程期间用于存储目标地址。

大多数 SPC 不会覆盖通信例程,因此我们只需不恢复该部分内存。另一方面,SPC 很可能使用前两个字节的 RAM,因为它们位于零页并且易于访问。因此,我们在恢复 CPU 状态时将设置这些字节,注意不要在恢复 CPU 状态之前覆盖 SNES RAM 中的这些字节。

请注意,因为我们覆盖了块 $f0-$ff,所以我们实际上写入了一些内存映射寄存器。这将恢复计时器状态以及一个 DSP 寄存器。它还会操作 Audio0-3 端口,但这似乎不会干扰内存传输过程,可能是因为 SNES 只在 Audio0 上监听特定值。

发送 DSP 状态

[编辑 | 编辑源代码]

要设置 DSP 寄存器的值,您首先需要将其编号写入 SPC 内存的地址 $f2,然后您需要将其值写入 SPC 内存的地址 $f3。因为这些地址彼此相邻,所以我们可以通过使用我们的内存复制例程将两个字节的块发送到 $f2 来恢复 DSP 寄存器值。我们对 128 个 DSP 寄存器中的每一个都重复此过程。

InitDSP:
    rep     #XY_8BIT            ; x and y in 16-bit mode
    ldx     #$0000              ; Reset DSP address counter.
-
    sep     #A_8BIT
    txa                         ; Write DSP address register byte.
    sta     $7f0100             
    lda.l   dspData,x           ; Write DSP data register byte.
    sta     $7f0101             
    phx                         ; Save x on the stack.

    ; Send the address and data bytes to the DSP memory-mapped registers.
    sendMusicBlockM $7f $0100 $00f2 $0002

    rep     #XY_8BIT            ; Restore x.
    plx

    ; Loop if we haven't done 128 registers yet.
    inx
    cpx     #$0080
    bne     -
    rts

发送 SPC 初始化例程

[编辑 | 编辑源代码]

我们的 SPC 初始化例程恢复了 SPC 状态的那些部分,这些部分只是通过运行 SPC 的通信例程而被改变。在完成所有复制操作后,我们将 SPC 的控制权交给初始化例程,例程的最后一步将跳转到保存的程序计数器位置。以下是我们初始化例程需要做的事情。

  • 恢复 RAM 的前两个字节。
  • 恢复堆栈指针 (S)。
  • 将恢复的 PSW 寄存器压入堆栈。
  • 恢复 A 寄存器。
  • 恢复 X 寄存器。
  • 恢复 Y 寄存器。
  • 将 PSW 寄存器值弹出到其寄存器。
  • 跳转到保存的程序计数器位置。

我们将从(任意)内存位置 $7f0000 开始编写例程。由于我们将需要该位置的前两个字节(我们到目前为止一直小心地不覆盖这些字节),因此我们将首先将它们保存到堆栈中。由于我们将首先恢复第一个字节,因此我们最后将其压入。

MakeSPCInitCode:
    sep     #A_8BIT

    ; Push [01] value to stack.
    lda.l   $7f0001
    pha

    ; Push [00] value to stack.
    lda.l   $7f0000
    pha

接下来,我们编写代码来恢复第一个字节。在 SPC 参考中查找 mov dp,#imm 操作码,我们看到操作码字节为 $8f,因此我们写入它,紧随其后是第一个参数字节(imm - 要恢复的值),然后是第二个参数字节(dp - 要写入字节的地址)。

    ; Write code to set [00] byte.
    lda     #$8f        ; mov dp,#imm
    sta.l   $7f0000
    pla
    sta.l   $7f0001
    lda     #$00
    sta.l   $7f0002

我们对第二个内存字节做同样的事情。

    ; Write code to set [01] byte.
    lda     #$8f        ; mov dp,#imm
    sta.l   $7f0003
    pla
    sta.l   $7f0004
    lda     #$01
    sta.l   $7f0005

由于没有操作码直接写入 S 值,因此我们首先将堆栈值移动到 X 中 - mov x, #imm ($cd) - 然后我们将 X 移动到堆栈寄存器中 - mov sp, x ($bd)。

    ; Write code to set s.
    lda     #$cd        ; mov x,#imm
    sta.l   $7f0006
    lda.l   audioSP
    sta.l   $7f0007
    lda     #$bd        ; mov sp,x
    sta.l   $7f0008

现在我们编写代码将程序状态字 (PSW) 寄存器值压入堆栈,以便我们以后可以将其弹出。我们需要在恢复其他寄存器之前压入值,因为我们在压入值的過程中會覆寫 X。我们不能在恢复其他寄存器之前弹出值,因为我们用来恢复它们的 mov 指令会改变 PSW 寄存器。

    ; Write code to push psw
    lda     #$cd        ; mov x,#imm
    sta.l   $7f0009
    lda.l   audioPSW
    sta.l   $7f000a
    lda     #$4d        ; push x
    sta.l   $7f000b

这里我们编写代码来恢复寄存器。

    ; Write code to set a.
    lda     #$e8        ; mov a,#imm
    sta.l   $7f000c
    lda.l   audioA
    sta.l   $7f000d

    ; Write code to set x.
    lda     #$cd        ; mov x,#imm
    sta.l   $7f000e
    lda.l   audioX
    sta.l   $7f000f

    ; Write code to set y.
    lda     #$8d        ; mov y,#imm
    sta.l   $7f0010
    lda.l   audioY
    sta.l   $7f0011

编写代码将 PSW 从堆栈中恢复相当简单。

    ; Write code to pull psw.
    lda     #$8e        ; pop psw
    sta.l   $7f0012

最后,我们编写代码将控制权发送到保存的程序计数器位置。

    ; Write code to jump.
    lda     #$5f        ; jmp labs
    sta.l   $7f0013
    rep     #A_8BIT
    lda.l   audioPC
    sep     #A_8BIT
    sta.l   $7f0014
    xba
    sta.l   $7f0015
    rts

因此,在调用此例程后,区域 $7f0000-$7f0015 包含初始化例程,因此使用我们的通信例程发送它相当简单。但是,我们必须在 SPC RAM 中有某个地方来放置它。在这里,我们赌博认为 IPL ROM 代码之前的内存区域没有使用。

; The address in SPC RAM where we put our 15-byte startup routine.
.define spcFreeAddr $ffa0

调用例程

    ; Build code to initialize registers.
    jsr     MakeSPCInitCode

    ; Copy init code to some region of SPC memory that we hope isn't in use.
    sendMusicBlockM $7f $0000 spcFreeAddr $0016

启动 SPC 执行

[编辑 | 编辑源代码]

现在我们已经恢复了内存和 DSP 状态,并且我们已经编写了一个初始化例程来完成恢复,我们只需要告诉 SPC 从我们的初始化例程开始执行。我们通过修改我们的通信例程来做到这一点,使其不发送任何块,而是立即从给定地址开始执行。

StartSPCExec:
    ; Starting address is in x.

    ; Wait until audio0 is 0xbbaa
    sep     #A_8BIT
    lda     #$aa
    waitForAudio0M

    ; Send the destination address to AUDIO2.
    stx     AUDIO_R2

    ; Send $00cc to AUDIO0 and wait for echo.
    lda     #$00
    sta     AUDIO_R1
    lda     #$cc
    sta     AUDIO_R0
    waitForAudio0M

    rts

此时,SPC 应该开始播放最初存储在 SPC 文件中的音乐。

这种技术实际上只适用于在 ROM 上播放一首歌曲;在您开始播放 SPC 文件后,很难停止它,上传另一首歌曲或播放音效。这是因为 SPC 中的代码只理解它所捕获的游戏的通信协议。要发现协议,您需要对 SPC 状态或原始 SNES ROM 的代码进行逆向工程,即使这样也无法保证协议会支持您想要执行的操作。为游戏中音频编写自定义协议将是未来教程的一个好主题。

这里描述的恢复方式下,SPC 状态无法播放的原因有很多

  • 原始 SPC 程序使用了 IPL ROM 区域或我们存储初始化代码的区域。如果它使用初始化区域,我们可以将代码写入另一个位置,这应该允许 SPC 播放。恢复 IPL ROM 区域更难,因为您需要 SPC 内存中其他地方的通信例程来允许您执行此操作。无论哪种情况,我们都无法逃避这样一个事实,即 RAM 中的一些空间需要用于初始化,并且与原始 RAM 不匹配。
  • 将控制权传递给原始代码时 SPC 的状态与捕获时不完全匹配,因为 DSP 和定时器在恢复后立即开始更新其值。如果 SPC 正在等待某些状态变化,例如定时器或 DSP 值,那么它可能会错过并锁定。
  • SNES 可以通过实时向 SPC 发送值来完全播放音乐。例如,它可以修改 DSP 寄存器,就像我们在恢复它们时所做的那样,除了它会随着时间的推移修改它们以产生音乐,就像其他 SPC 代码一样。这样,SPC 可以使用 IPL ROM 通信例程作为其内存中唯一的代码来产生音乐。这样的 SPC 文件甚至不能在播放器中播放,因为它们依赖于 SNES 提供信息。

完整源代码

[编辑 | 编辑源代码]
 ; SNES SPC700 Tutorial code
 ; (originally by Joe Lee)
 ; This code is in the public domain.
 
 .include "Header.inc"
 .include "Snes_Init.asm"
 
 ; These definitions are needed to satisfy some lines in "Snes_Init.asm".
 .define BG1MoveH $7E1A25
 .define BG1MoveV $7E1A26
 .define BG2MoveH $7E1A27
 .define BG2MoveV $7E1A28
 .define BG3MoveH $7E1A29
 .define BG3MoveV $7E1A2A
 
 ; Needed to satisfy interrupt definition in "Header.inc".
 VBlank:
   rti
 
 .define AUDIO_R0 $2140
 .define AUDIO_R1 $2141
 .define AUDIO_R2 $2142
 .define AUDIO_R3 $2143
 
 .define XY_8BIT $10
 .define A_8BIT  $20
 
 .define musicSourceAddr $00fd
 
 ; The SPC file from which we read our data.
 .define spcFile "test000.spc"
 
 ; The address in SPC RAM where we put our 15-byte startup routine.
 .define spcFreeAddr $ffa0
 
 ; The first half of the saved SPC RAM from the SPC file.
 .bank 1
 .section "musicData1"
 spcMemory1: .incbin spcFile skip $00100 read $8000
 .ends
 
 ; The second half of the saved SPC RAM from the SPC file.
 .bank 2
 .section "musicData2"
 spcMemory2: .incbin spcFile skip $08100 read $8000
 .ends
 
 .bank 0
 .section "MainCode"
 
 ; The rest of the saved SPC state from the SPC file.
 dspData:  .incbin spcFile skip $10100 read $0080
 audioPC:  .incbin spcFile skip $00025 read $0002
 audioA:   .incbin spcFile skip $00027 read $0001
 audioX:   .incbin spcFile skip $00028 read $0001
 audioY:   .incbin spcFile skip $00029 read $0001
 audioPSW: .incbin spcFile skip $0002a read $0001
 audioSP:  .incbin spcFile skip $0002b read $0001
 
 Start:
     ; Initialize the SNES.
     Snes_Init
 
     jsr LoadSPC
 
     ; Set the background color to green.
     sep     #$20        ; Set the A register to 8-bit.
     lda     #%10000000  ; Force VBlank and set brightness to 0%.
     sta     $2100
     lda     #%11100000  ; Load the low byte of the green background color.
     sta     $2122
     lda     #%00000000  ; Load the high byte of the green background color.
     sta     $2122
     lda     #%00001111  ; End VBlank, setting brightness to 100%.
     sta     $2100
 
     ; Loop forever.
 Forever:
     jmp Forever
 
 .macro sendMusicBlockM ; srcSeg srcAddr destAddr len
     ; Store the source address \1:\2 in musicSourceAddr.
     sep     #A_8BIT
     lda     #\1
     sta     musicSourceAddr + 2
     rep     #A_8BIT
     lda     #\2
     sta     musicSourceAddr
 
     ; Store the destination address in x.
     ; Store the length in y.
     rep     #XY_8BIT
     ldx     #\3
     ldy     #\4
     jsr     CopyBlockToSPC
 .endm
 
 .macro startSPCExecM ; startAddr
     rep     #XY_8BIT
     ldx     #\1
     jsr     StartSPCExec
 .endm
 
 LoadSPC:
     jsr     CopySPCMemoryToRam
 
     stz     $4200   ; Disable NMI
     sei             ; Disable IRQ
 
     ; Copy RAM between 0x0002 and 0xffc0.
     sendMusicBlockM $7f $0002 $0002 $ffbe
 
     ; Build code to initialize registers.
     jsr     MakeSPCInitCode
 
     ; Copy init code to some region of SPC memory that we hope isn't in use.
     sendMusicBlockM $7f $0000 spcFreeAddr $0016
 
     ; Initialize DSP registers.
     jsr     InitDSP
 
     ; Start SPC execution at init code region.
     startSPCExecM spcFreeAddr
 
     cli             ; Enable IRQ
     sep     #A_8BIT ; Enable NMI
     lda     #$80
     sta     $4200
 
     rts
 
 CopySPCMemoryToRam:
     ; Copy music data from ROM to RAM, from the end backwards.
     rep   #XY_8BIT        ; xy in 16-bit mode.
     ldx.w #$7fff           ; Set counter to 32k-1.
 -   lda.l spcMemory1,x     ; Copy byte from first music bank.
     sta.l $7f0000,x
     lda.l spcMemory2,x     ; Copy byte from second music bank.
     sta.l $7f8000,x
     dex
     bpl -
     rts
 
 InitDSP:
     rep     #XY_8BIT            ; x and y in 16-bit mode
     ldx     #$0000              ; Reset DSP address counter.
 -
     sep     #A_8BIT
     txa                         ; Write DSP address register byte.
     sta     $7f0100             
     lda.l   dspData,x           ; Write DSP data register byte.
     sta     $7f0101             
     phx                         ; Save x on the stack.
 
     ; Send the address and data bytes to the DSP memory-mapped registers.
     sendMusicBlockM $7f $0100 $00f2 $0002
 
     rep     #XY_8BIT            ; Restore x.
     plx
 
     ; Loop if we haven't done 128 registers yet.
     inx
     cpx     #$0080
     bne     -
     rts
 
 MakeSPCInitCode:
     ; Constructs SPC700 code to restore the remaining SPC state and start
     ; execution.
 
     ; The code we want to construct:
     ; Move 00 byte to 00.
     ; Move 01 byte to 01.
     ; Move s value into s.
     ; Push PSW value.
     ; Move a value into a.
     ; Move x value into x.
     ; Move y value into y.
     ; Pull PSW value.
     ; Jump to saved program counter location.
 
     sep     #A_8BIT
 
     ; Push [01] value to stack.
     lda.l   $7f0001
     pha
 
     ; Push [00] value to stack.
     lda.l   $7f0000
     pha
 
     ; Write code to set [00] byte.
     lda     #$8f        ; mov dp,#imm
     sta.l   $7f0000
     pla
     sta.l   $7f0001
     lda     #$00
     sta.l   $7f0002
 
     ; Write code to set [01] byte.
     lda     #$8f        ; mov dp,#imm
     sta.l   $7f0003
     pla
     sta.l   $7f0004
     lda     #$01
     sta.l   $7f0005
 
     ; Write code to set s.
     lda     #$cd        ; mov x,#imm
     sta.l   $7f0006
     lda.l   audioSP
     sta.l   $7f0007
     lda     #$bd        ; mov sp,x
     sta.l   $7f0008
 
     ; Write code to push psw
     lda     #$cd        ; mov x,#imm
     sta.l   $7f0009
     lda.l   audioPSW
     sta.l   $7f000a
     lda     #$4d        ; push x
     sta.l   $7f000b
 
     ; Write code to set a.
     lda     #$e8        ; mov a,#imm
     sta.l   $7f000c
     lda.l   audioA
     sta.l   $7f000d
 
     ; Write code to set x.
     lda     #$cd        ; mov x,#imm
     sta.l   $7f000e
     lda.l   audioX
     sta.l   $7f000f
 
     ; Write code to set y.
     lda     #$8d        ; mov y,#imm
     sta.l   $7f0010
     lda.l   audioY
     sta.l   $7f0011
 
     ; Write code to pull psw.
     lda     #$8e        ; pop psw
     sta.l   $7f0012
 
     ; Write code to jump.
     lda     #$5f        ; jmp labs
     sta.l   $7f0013
     rep     #A_8BIT
     lda.l   audioPC
     sep     #A_8BIT
     sta.l   $7f0014
     xba
     sta.l   $7f0015
     rts
 
 .macro waitForAudio0M
 -
     cmp     AUDIO_R0
     bne     -
 .endm
 
 CopyBlockToSPC:
     ; musicSourceAddr - source address
     ; x - dest address
     ; y - count
 
     ; Wait until audio0 is 0xbbaa
     sep     #A_8BIT
     lda     #$aa
     waitForAudio0M
 
     ; Send the destination address to AUDIO2.
     stx     AUDIO_R2
 
     ; Transfer count to x.
     phy
     plx
 
     ; Send $01cc to AUDIO0 and wait for echo.
     lda     #$01
     sta     AUDIO_R1
     lda     #$cc
     sta     AUDIO_R0
     waitForAudio0M
 
     ; Zero counter.
     ldy     #$0000
 
 CopyBlockToSPC_loop:
     ; Load the high byte of a with the destination byte.
     xba
     lda     [musicSourceAddr],y
     xba
     
     ; Load the low byte of a with the counter.
     tya
 
     ; Send the counter/byte.
     rep     #A_8BIT
     sta     AUDIO_R0
     sep     #A_8BIT
 
     ; Wait for counter to echo back.
     waitForAudio0M
 
     ; Update counter and number of bytes left to send.
     iny
     dex
     bne     CopyBlockToSPC_loop
 
     ; Send the start of IPL ROM send routine as starting address.
     ldx     #$ffc9
     stx     AUDIO_R2
     
     ; Clear high byte.
     xba
     lda     #0
     xba
 
     ; Add a value greater than one to the counter to terminate.
     clc
     adc     #$2
 
     ; Send the counter/byte.
     rep     #A_8BIT
     sta     AUDIO_R0
     sep     #A_8BIT
 
     ; Wait for counter to echo back.
     waitForAudio0M
 
     rts
 
 StartSPCExec:
     ; Starting address is in x.
 
     ; Wait until audio0 is 0xbbaa
     sep     #A_8BIT
     lda     #$aa
     waitForAudio0M
 
     ; Send the destination address to AUDIO2.
     stx     AUDIO_R2
 
     ; Send $00cc to AUDIO0 and wait for echo.
     lda     #$00
     sta     AUDIO_R1
     lda     #$cc
     sta     AUDIO_R0
     waitForAudio0M
 
     rts
 
 .ends
华夏公益教科书