超级任天堂编程/加载 SPC700 程序
在本教程中,我们将创建一个 ROM,它将初始化 SPC700 以播放从另一个 SNES 游戏中捕获的歌曲。
为了在 SNES 上发出声音,需要将 DSP 的寄存器设置为适当的值。这意味着,要在 SNES 上播放歌曲,你需要一个用于 SPC700 的程序来操作 DSP,还需要一个用于 65816 的代码,将 SPC700 程序传输到 SPC700。幸运的是,网上有数千个用于 SPC700 的程序,以 SPC 文件的形式免费提供,解决了我们一半的问题。
SPC 文件包含 SPC700 的状态,通常是在 SNES 游戏中歌曲的开始时。通过在 SPC700 和 DSP 模拟器(又名 SPC 播放器)中恢复状态,你可以无需 SNES ROM 即可收听歌曲。我们也可以使用 SPC 文件在 SNES 内部恢复 SPC 状态以播放歌曲。你可以在 SNESMusic.org 上自己使用 SNES ROM 和模拟器捕获 SPC 文件,或者下载数千个在线文件。
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 时,它会在视觉上告诉我们音乐是否成功加载,或者执行是否在音乐代码中的某个地方停止。
SNES 和 SPC700 通过四个字节宽的通道进行通信,我们将其称为 Audio0、Audio1、Audio2 和 Audio3。在 SNES 端,这些由内存映射寄存器 $2140-$2143 表示,而 SPC 将其表示为 $00f4-$00f7。尽管有四个通道,但实际上后台存储了八个值 - 从 SNES 到 SPC 的通道的四个字节,以及从 SPC 到 SNES 的通道的四个字节。例如,当 SNES 将一个值写入其 Audio0 时,该值将保存在一个位置,以便 SPC 可以从其 Audio0 中读取该值。同样,当 SPC 将一个值写入其 Audio0 时,该值将保存在另一个位置,以便 SNES 可以从其 Audio0 中读取该值。因此,从通道的内存映射寄存器中读取的值可能不是最后写入的值。
当 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 使用的算法的摘要
- 初始化
- 将 AudioOut0 设置为 $aa,将 AudioOut1 设置为 $bb。
- 等待 AudioIn0 为 $cc。
- 准备复制一个块
- 从 AudioIn2(低字节)和 AudioIn3(高字节)读取 16 位目标地址。
- 将 AudioIn0 复制到 AudioOut0。
- 如果 AudioIn1 为零,则从目标地址开始执行。
- 复制一个块
- 等待 AudioIn0 为零。
- 将一个字节大小的计数器设置为零。
- 复制一个字节
- 等待计数器大于 AudioIn0。
- 如果计数器等于 AudioIn0
- 将一个字节从 AudioIn1 复制到内存位置。
- 增加计数器和内存位置。
- 转到步骤 4。
- 否则(当计数器小于 AudioIn0 时),转到步骤 2。
一个特别精明或偏执的程序员会注意到,当 SPC 的字节大小计数器翻转(从 $ff 增加到 $00)时,它会变得小于 AudioIn0 中的值,这可能会导致错误:除非 SNES 及时更新 AudioIn0,否则 SPC 例程可能会认为块已结束,而 SNES 仍在发送数据。当这种情况发生时,SNES 和 SPC 可能会在协议的不同部分等待彼此,从而冻结系统 (这可能不正确 - 请参阅讨论页面上的注释)。
因此,为了防止死锁,SNES 必须尽快更新 Audio0。这意味着将数据预先复制到可用的最快内存中,并在使用 IPL ROM 协议时禁用中断。或者,你可以将自己限制在复制小于 255 字节的块,这样计数器就不会翻转,或者你可以安装一个更好的通信例程在 SPC RAM 中,并使用它。
既然我们已经从 SPC 的角度检查了协议,那么我们需要一个 SNES 例程来与之交互。首先,我们将检查一个类似于开源演示(以及推测的实际 SNES 游戏)中使用的例程。然后,我们将对其进行一个小的修改,以简化我们的代码。
以下是开源演示中使用的通用算法
- 等待 Audio0 为 $aa,这表示 SPC 已完成其初始化。
- 将一个字节大小的计数器初始化为 $cc。
- 如果没有更多块要发送
- 将 16 位执行地址发送到 Audio2 和 Audio3。
- 将 $00 发送到 Audio1。
- 将计数器发送到 Audio0。
- 等待计数器值在 Audio0 上回显。
- 结束例程。
- 否则
- 将 16 位目标地址发送到 Audio2 和 Audio3。
- 将 $01 发送到 Audio1。
- 将计数器发送到 Audio0。
- 等待计数器值在 Audio0 上回显。
- 将计数器重置为零。
- 如果有字节剩余在当前块中
- 将当前字节发送到 Audio1。
- 将计数器发送到 Audio0。
- 等待计数器值在 Audio0 上回显。
- 移到下一个字节。
- 增加计数器。
- 转到步骤 5。
- 否则
- 将计数器加 $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 状态由三个部分组成。
- 内存
- 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 寄存器的值,您首先需要将其编号写入 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 的控制权交给初始化例程,例程的最后一步将跳转到保存的程序计数器位置。以下是我们初始化例程需要做的事情。
- 恢复 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
现在我们已经恢复了内存和 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