跳轉到內容

超級任天堂程式設計/載入 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
華夏公益教科書