SNES APU , SPC700の使い方(スーパーファミコンの音源モジュール「SHVC-SOUND」でMIDI演奏 )
注意:このサイトの内容を鵜呑みにし、事故や損失を招いた場合でも当方は一切の責任は負いかねます。自己責任でお願いします。
スーパーファミコン(SFC)にはBGMやSEなどを演奏するための音源モジュール(APU)が入っています。特に前期型のスーファミはAPUが独立して取り外すことができます。
中期型や後期型の場合、APUはメインボードに直付けされているため、演奏目的には向きません。(S-SMPやS-DSPなどの部品を取って、使用する事はできます。QFPのはんだ付けに自信がある方向けです。)
1chipのスーファミの場合、APU内の部品をほぼ1つにまとめたS-APUが使われています。
ただし、1chipのスーファミは中古でも高いので演奏目的で部品を取って使うのはお勧めしません。
メリットは、多数のAPUを基板に乗せるとき、省スペースで済むところでしょうか。
今回は、MIDIを受信してAPUで発音するプログラムを書いてみます。
APUは前期型を用います。
前期型APUにもバージョンの違いがありますが、問題なく使用できると思います。
初期型スーファミのAPU
左が「S-DSP A」バージョン、右が「S-DSP」バージョン
中期型以降のAPUは、メインボードに直付けされています。
APU
スーファミの音源は、あらかじめ記録された音声波形を再生するPCM音源の一種です。
同時発音数は8chで、エンベロープやエコー、ノイズ生成、ピッチモジュレーションなど様々なエフェクトをハードウェアで実現できます。
このスーファミのAPUの最大の特徴は、APU内に独立したCPUを持つことです。
つまり、スーパーファミコンの中には、メインのCPU(5A22)とAPU内のCPU(SPC700)の2つが入っているということです。
ヤマハのYM2413やYM2608、AY-3-8910などのFM音源ICやPSG音源ICなどは、レジスタをたたけば音を出すことができます。しかし、スーファミのAPUは内部にCPU(SPC700)を持つため、まずSPC700用のプログラムを書かなければならず、そう簡単には音は出せません。
マイコンでよく音源ICを使う方々には、手を出しにくい音源でもあります。
手を出しにくい要因はもう一つあります。
それは、APUが再生できる専用のフォーマットの音声波形データを用意することです。
FM音源ICやPSG音源ICの場合、発音波形はパラメータで決まります。
しかし、PCM音源の場合はメモリに蓄えた波形データを再生して発音するため、波形データを用意してあげないと望み通りの音は出ません。
スーファミのAPUの場合、この波形データを独自の圧縮方式で圧縮した「BRRサンプルデータ」でなければいけません。(BRRについては解説しませんので各自調べてみてください。)WAVからBRRデータへ変換するソフトもありますが、ループ点の設定がとても面倒です。
この2つの壁を乗り越えてやっとAPUで音を出す準備が整います。
・APUの回路図
回路は、以下を参考にしました。
SFC Development Wiki - Schematics, Ports, and Pinouts
・APUのブロック図
APUには、CPU(SPC700)やSRAM(64kB)(実際はPSRAMですが便宜上SRAMとしています。)、DSP(発音専用DSP)、DAC、OPAMP等入っています。いわば音を出すことに特化したシングルボードコンピュータといえます。
SRAMには、音声波形データのほかにSPC700プログラムデータなどを入れます。もちろん発音に必要なレジスタ等も同じアドレス空間にあります。
スーファミのメインのCPUである5A22と通信するためのポートレジスタや、CPUの動作を決定するレジスタ、DSPへ指示をだすレジスタなど、様々な種類のレジスタがあります。
APUを扱う上でまず最初の壁となるのは、CPU(SPC700)用のプログラムの作成とプログラムの書き込み(APUのSRAMへプログラムを転送)です。
・SPC700アドレス空間
アドレス | 概要 | 説明 |
---|---|---|
$0000~$00EF | ページ0領域 | ページ0領域です。 ページ0をページ領域に設定すると、SPC700はページ0領域のデータを低サイクルで読み書きすることや、このデータ領域に対して様々な演算命令を実行出来ます。 ※ページ0は、実際$0000~$00FFまでの範囲です。表の都合上分けています。 通常この領域($0000~$00EF)は、SPC700でプログラムを書く時の変数領域として使用します。 |
$00F0 | コンフィグレジスタ | CPUのクロックなどを変更できます。詳細不明です。 |
$00F1 | コントロールレジスタ | IPLの有効無効、タイマーの有効無効、ポートのリセットを行うレジスタです。 |
$00F2 | DSPアドレスレジスタ | DSP内部レジスタへアクセスするときにセットします。 |
$00F3 | DSPデータレジスタ | DSP内部レジスタへアクセスするときにセットします。 書き込みの場合は、「DSPアドレスレジスタ」→「DSPデータレジスタ」の順で書き込みます。 読み込みの場合は、読み込むアドレスを「DSPアドレスレジスタ」へ書き込んでから「DSPデータレジスタ」を読み込みます。 |
$00F4 | ポート0 | ポート0レジスタです。 APUの外部とデータのやりとりする時に使います。 ただし、書き込み先ラッチレジスタと読み込み先ラッチレジスタは別々に用意されているため、注意が必要です。 |
$00F5 | ポート1 | ポート0レジスタです。 APUの外部とデータのやりとりする時に使います。 ただし、書き込み先ラッチレジスタと読み込み先ラッチレジスタは別々に用意されているため、注意が必要です。 |
$00F6 | ポート2 | ポート0レジスタです。 APUの外部とデータのやりとりする時に使います。 ただし、書き込み先ラッチレジスタと読み込み先ラッチレジスタは別々に用意されているため、注意が必要です。 |
$00F7 | ポート3 | ポート0レジスタです。 APUの外部とデータのやりとりする時に使います。 ただし、書き込み先ラッチレジスタと読み込み先ラッチレジスタは別々に用意されているため、注意が必要です。 |
$00F8 | 内部ポート4 | APU内部ポート4レジスタです。 APU回路図中のIC1 S-SMPの27~34番ピンに割り当てられています。 RAMとして振る舞いますが、使わないほうが良さそうです。 |
$00F9 | 内部ポート5 | APU内部ポート5レジスタです。 APU回路図中のIC1 S-SMPの18~25番ピンに割り当てられています。 RAMとして振る舞いますが、使わないほうが良さそうです。 |
$00FA | タイマー0 | 8bitタイマー0の最大値を指定します。 8bitタイマー0がこのレジスタの値になると「カウンタ0」レジスタをインクリメントして「8bitタイマー0」をリセットします。 8bitタイマー0は、125μsおきに自動的にインクリメントされます。 |
$00FB | タイマー1 | 8bitタイマー1の最大値を指定します。 8bitタイマー1がこのレジスタの値になると「カウンタ1」レジスタをインクリメントして「8bitタイマー1」をリセットします。 8bitタイマー1は、125μsおきに自動的にインクリメントされます。 |
$00FC | タイマー2 | 8bitタイマー2の最大値を指定します。 8bitタイマー2がこのレジスタの値になると「カウンタ2」レジスタをインクリメントして「8bitタイマー2」をリセットします。 8bitタイマー2は、15.625μsおきに自動的にインクリメントされます。 |
$00FD | カウンタ0 | 4bitカウンタです。「タイマー0」の値と「8bitタイマー0」が一致すると、このレジスタがインクリメントされます。 ただし、カウントビット幅が4bitなので0x0Fの次は0x00となりますので注意が必要です。 |
$00FE | カウンタ1 | 4bitカウンタです。「タイマー1」の値と「8bitタイマー1」が一致すると、このレジスタがインクリメントされます。 ただし、カウントビット幅が4bitなので0x0Fの次は0x00となりますので注意が必要です。 |
$00FF | カウンタ2 | 4bitカウンタです。「タイマー2」の値と「8bitタイマー2」が一致すると、このレジスタがインクリメントされます。 ただし、カウントビット幅が4bitなので0x0Fの次は0x00となりますので注意が必要です。 |
$0100~$01FF | ページ1領域 | ページ1領域です。 ページ1をページ領域に設定すると、SPC700はページ1領域のデータを低サイクルで読み書きすることや、このデータ領域に対して様々な演算命令を実行出来ます。 このページ1はSPC700のスタック専用領域となっています。 プッシュを介さない書き込みは、推奨しません。 |
$0200~$FFBF | 多目的領域 | SPC700のプログラムやBRRサンプルデータ、BRRサンプルベクタ、エコーバッファなど多目的に使用できます。 ただし、SPC700の命令の一部は、実行できるアドレス空間が細かく限られています。詳細は各自で調べてみてください。 |
$FFC0~$FFFF | 多目的領域、IPL領域 | 通常使用する場合は、「$0200~$FFBF」と同様の多目的領域です。 ただし、コントロールレジスタでIPLを有効にすると、この領域の読み込みがIPL(データ転送用プログラム)のデータとなります。 |
APUへプログラムを書き込む
スーパーファミコンのAPUは、CPU(SPC700)やSRAM(64kB)、DSP(発音専用DSP)等を1つのモジュールに押し込めたものです。
CPUがあるということは、もちろんプログラムも必要になるんですが、残念ながらIPL(ブート用ROM)を除いてプログラムは用意されていません。
良く言えば演奏の自由度がとても高いが、反面、演奏用のプログラムをしっかり組まないと狙った通りに鳴ってくれないということです。
ブートROMには、APUのSRAMへプログラムやPCMデータ(BRR)、演奏データ等をAPU外部のCPU・マイコンから転送できるプログラムが格納されています。
APUへ演奏プログラムを転送して、初めて音を鳴らす準備ができます。
また、SRAMは電源を切るとデータが消滅するため、APUを起動させるごとにデータの転送が必要になります。
では、APU(SPC700&SRAM)へデータを転送する手順を見ていきましょう。
①APUの起動
SPC700が起動またはリセットがかかると、内部にあるブートROM(IPL)内のプログラムが開始します。
ブートプログラムはSPC700のレジスタ等を初期化したのち、初期化終了を通知するため、
SPC700はポート0($F4)に0xAA、ポート1($F5)に0xBBをセットします。
@@@@@@@@@@@@@@@@@@@
ポート SPC700
PORT0 ← 0xAA
PORT1 ← 0xBB
@@@@@@@@@@@@@@@@@@@
②APUへ演奏プログラム&PCMデータの転送開始
SPC700を制御するマイコンは、SPC700初期化終了通知(PORT0=0xAA, PORT1=0xBB)を読み出せたら、APUのSRAMへ演奏データの転送を開始できます。
マイコンはまず、転送したいデータの転送先先頭アドレスとモードをSPC700へ書き込みます。
転送したいデータの転送先先頭アドレスは2バイト($0000~$FFFF)で指定します。
アドレス下位8bit(A7~A0)はポート2へ、上位8bit(A15~A8)はポート3へ書き込みます。
モードは0以外(0x01~0xFF)をポート1へ書き込みます。(0を書き込むとポート2,3に書き込んだアドレスへジャンプし、ユーザープログラムが開始します。)
次に、SPC700への転送先先頭アドレスセット完了を通知するために、マイコン側はポート0へ0xCCを書き込みます。ポート0に0xCCが書き込まれていることをSPC700が確認すると、SPC700は演奏プログラム&PCMデータ転送シーケンスへ移行します。
@@@@@@@@@@@@@@@@@@@
MCU ← (PORT0=0xAA, PORT1=0xBB) チェックOK
↓
MCU ポート
0以外(0x01~0xFF) → PORT1
転送したいデータの転送先先頭アドレス(下位8bit(A7~A0)) → PORT2
転送したいデータの転送先先頭アドレス(上位8bit(A15~A8)) → PORT3
↓
MCU ポート
0xCC → PORT0
@@@@@@@@@@@@@@@@@@@
③APUへ演奏プログラム&PCMデータのブロック転送
ポート0に0xCCが書き込まれていることをSPC700が確認すると、SPC700は演奏プログラム&PCMデータ転送シーケンスへ移行します。
その際、SPC700はポート0へ書き込まれたデータをポート0へエコーバックします。(この場合、SPC700はポート0に0xCCを書き込みます。)
マイコン側は、SPC700ブロック転送開始通知(PORT0=0xCC)を読み出せたら、SPC700は入力待ち状態となり、転送するデータの1つ目をSPC700へ転送できます。
@@@@@@@@@@@@@@@@@@@
ポート SPC700
PORT0 ← 0xCC
↓
MCU ← (PORT0=0xCC) チェックOK
@@@@@@@@@@@@@@@@@@@
--1--
まず、SPC700のポート1に書き込みたいデータの0番目をセットします。
次に、SPC700のポート0に書き込みたいデータが、ブロック転送中何番目のデータなのかをセットします。(この場合、最初のデータなので、SPC700のポート1に0x00を書き込みます。)
@@@@@@@@@@@@@@@@@@@
MCU ポート
DATA[n=0](n=0番目の転送したいデータ) → PORT1
n=0(n=0番目:nは0x00~0xFF) → PORT0
@@@@@@@@@@@@@@@@@@@
--2--
SPC700はポート0へ書き込まれたデータをポート0へエコーバックします。
マイコン側は、SPC700がポート0へエコーバックしたデータが、先ほどポート0へセットした値と一致しているかチェックします。
もし、一致しているならば、SPC700は入力待ち状態となり、マイコン側は次のデータを書き込むことができます。
まず、SPC700のポート1に書き込みたいデータの0番目をセットします。
次に、SPC700のポート0に書き込みたいデータが、ブロック転送中何番目のデータなのかをセットします。(下位8bitのみ書き込みます。例:0x0345のとき→0x45を書き込む)
(ポート0には以前ポート0に書き込んだ値+1を書き込んでください。もし、以前ポート0に書き込んだ値以下ならば、SPC700は入力待ち状態状態を保持します。また、以前ポート0に書き込んだ値+1より大きい値ならば、SPC700はこのブロックの転送を終了します。)
@@@@@@@@@データブロック転送(継続)@@@@@@@@@@
MCU ← (PORT0=(以前ポート0に書き込んだ値)) チェックOK
↓
MCU ポート
DATA[n](n(下位8bit)番目の転送したいデータ) → PORT1
↓
n(n番目:nは0x00~0xFF) → PORT0
@@@@@@@@@@@@@@@@@@@
@@@@@@@データブロック転送(途中でやめる)@@@@@@@@
MCU ← (PORT0=(以前ポート0に書き込んだ値)) チェックOK
↓
MCU ポート
モード(1~255:ブロック転送モード,0:ユーザープログラム開始モード) → PORT1
転送したいデータの転送先先頭または、ユーザープログラム開始アドレス(下位8bit(A7~A0)) → PORT2
転送したいデータの転送先先頭または、ユーザープログラム開始アドレス(上位8bit(A15~A8)) → PORT3
↓
n+1(n+1番目) → PORT0
@@@@@@@@@@@@@@@@@@@
--3--
「--2--」で、以前ポート0に書き込んだ値+1を書き込んだ場合、引き続き次のデータをSPC700へ転送できます。「--2--」へ行きます。
以前ポート0に書き込んだ値+1より大きい値ならば、SPC700はこのブロックの転送を終了します。通常ブロックの転送を終了する場合、以前ポート0に書き込んだ値+2を書きます。
ブロック転送終了前には、ポート2,3にブロック開始アドレスまたはユーザープログラム開始アドレスを、ポート1にモード(1~255:ブロック転送モード,0:ユーザープログラム開始モード)を必ず書き込んでください。
@@@@@@@@@@@@@@@@@@@
例:ポート0にn=[0,1,2,3,4,5,7]の順で書き込んだ場合
SPC700にはDATA[0]~DATA[5]の6バイトが書き込まれます。n=6の書き込みを飛ばしているので、n=5でこのブロック転送を終了します。
このとき、ポート0にn=5を書く前に、ポート2,3にブロック開始アドレスまたはユーザープログラム開始アドレスを、ポート1にモード(1~255:ブロック転送モード,0:ユーザープログラム開始モード)を必ず書き込んでください。
@@@@@@@@@@@@@@@@@@@
③APUへ演奏プログラム&PCMデータのブロック転送終了
ブロックの転送を終了すると、ブロック転送モードまたはユーザープログラム開始モード
へ移行します。
ブロックの転送を終了前に、ポート1に書き込んだモード(1~255:ブロック転送モード,0:ユーザープログラム開始モード)がブロック転送モードの場合、「③SPC700へ演奏プログラム&PCMデータのブロック転送」の「--1--」へ行きます。
このとき、ブロックの転送終了前に転送したいデータの転送先先頭アドレスをポート2,3へ書きこんでおきます。
また、ユーザープログラム開始モードの場合、ブロックの転送終了前にポート2,3へ書き込んだアドレスへジャンプします。
APUへ書き込むプログラムの作成
・Lチカプログラムの作成
では次に、プログラム作成します。
SPC700はスーパーファミコンのメインCPUである5A22(65816)に近い命令群を持っています。
ネット上にはいろいろ情報が出回っており、適時参考にするとよさそうです。
基本的にアセンブラでプログラムを記述します。
この章では、アセンブラに少しだけ慣れてもらうため、Lチカのプログラムを組んでみます。
MIDIのプログラムではないので、興味ない方は読み飛ばしてもよいです。
Lチカは、単にポートのビットを一定時間ごとにHとLを切り替える動作をします。
SPC700はf=1.024MHzで動作するので、待ち時間なしでビット反転するプログラムの場合、人の目ではLEDの点滅がわかりません。そこで、あえてSPC700で無駄な処理を行い、時間稼ぎをして点滅がわかるようにします。
具体的な流れとして、
///////////////////////////////////////////
ポート0のbit0をHにする
↓
無駄な処理
↓
ポート0のbit0をLにする
↓
無駄な処理
↓
ポート0のbit0をHにする
↓
無駄な処理
↓
ポート0のbit0をLにする
↓
無駄な処理
↓
…
///////////////////////////////////////////
を繰り返します。
問題は、「無駄な処理」をどうするかです。
今出回っているほとんどのCPUには「何もしない」という命令が備わっており、実行するとCPUクロック何サイクル分(CPUによって異なります)何もしない時間を稼げます。
ただし、この命令はとても短い時間しか稼げません。なので多く時間稼ぎをしたい場合、この命令を連続して実行する必要があります。
SPC700の場合、「NOP」(オペコード:0x00)がこれに相当します。
サイクル数N(実行に必要なCPUクロック数)は2なので、稼げる時間Twは、
Tw = N * 1 / f
= 2 * 1 / (1.024M)
= 1.953μs
f:SPC700の動作周波数f=1.024MHz
となります。
たとえばT=0.5秒稼ぐとしたら、
Ne = T / Tw
= 0.5 / 1.953μ
= 524.7k
となるので、約52万5000回「NOP」を連続で実行しなければいけません。また、プログラムのサイズも500kB以上必要となり、そもそもSRAM64kB内に収まりません。現実的ではないですね。
そこで、ループを使って待ち時間を稼ぐ手法が、簡単なプログラムでよく使われています。c言語でいうとforとかです。
アセンブラの場合、ループ処理は複数の命令を組み合わせて使用します。
ループ処理で重要になるのが、条件分岐処理です。
具体例を挙げてみます。
/////////////////////////////////////////////////
MOV A, #$07 ;アキュムレータへ0x07を書き込む
loop:
DEC A ;アキュムレータデクリメント
BNE loop ;ゼロフラグがクリアされているときloopへ
/////////////////////////////////////////////////
命令は3つ使用しています。
最初の「MOV A, #$07」はアキュムレータ初期化です。
アセンブラの場合、「$定数」でアドレス[定数(16進数)]内にある値を参照し、「#$定数」で16進定数を表します。
アキュムレータに定数0x07を書き込みます。
このプログラムの場合、条件分岐処理は「BNE loop」です。
「BRA」は-128~+127バイトの範囲で相対アドレスジャンプできます。
分岐の条件は、ゼロフラグがクリアされていることです。ゼロフラグはその名の通り、算術結果等でその値が0になるとHとなるフラグです。
今回は、「BNE loop」の前の命令「DEC A」を実行したとき、アキュムレータの値が0となる場合、ゼロフラグがHとなります。
つまり、アキュムレータを減算していきアキュムレータが0になるまでループする処理となります。
アキュムレータの値の変化は次のようになります。
@@@@@@@@@@@@@@@@@@@
アキュムレータの値の変化
0x07 //初期化
0x06 //3行目 DEC A
0x05 //3行目 DEC A
0x04 //3行目 DEC A
0x03 //3行目 DEC A
0x02 //3行目 DEC A
0x01 //3行目 DEC A
0x00 //3行目 DEC A
処理終了
@@@@@@@@@@@@@@@@@@@
アキュムレータの初期値が0x07の場合、「loop」内を7回実行します。
では、実際にどれくらいの時間がかかるのでしょうか?
まずは、「 MOV A, #$07」です。
この命令はNmova=2サイクルで実行します。
「DEC A」
この命令もNdeca=2サイクルで実行します。
「BNE loop」
分岐しない場合Nbne_nb=2サイクル、分岐する場合Nbne_b=4サイクルで実行します。
実行に必要な時間Twは、
Tw = (Nmova + (Ndeca + Nbne_b)(nloop-1) + (Ndeca + Nbne_nb)) * 1 / f
= (2 + (2 + 4)(7-1) + (2 + 2)) * 1 / (1.024M)
= 41.02μs
nloop:アキュムレータ初期値(ループ回数)
ループ回数と実行に必要な時間の関係をもう少しわかりやすくすると、
Tw = (6 * nloop) / f
= (6 * nloop) / (1.024M)
となります。
アキュムレータ(ループ回数)にセットできる最大値はnloop=0xFF=255なので、このループ処理で稼げる最大時間Twmaxは、
Twmax = (6 * nloop) / f
= (6 * 255) / (1.024M)
= 1.494ms
となります。
たった3つの命令で約1.5ms稼ぐことができました。ですが、1.5msのLEDの点滅ではまだまだ速すぎます。
そこで、ループを重ねてより多く時間を稼ぎます。
/////////////////////////////////////////////////
MOV $00, #$07 ;$0000へ0x07を書き込む
loop1:
MOV A, #$07 ;アキュムレータへ0x07を書き込む
loop2:
DEC A ;アキュムレータデクリメント
BNE loop2 ;ゼロフラグがクリアされているときloop2へ
DEC $00 ;$0000デクリメント
BNE loop1 ;ゼロフラグがクリアされているときloop1へ
/////////////////////////////////////////////////
ループが2つ使われていますね。
基本的にループが入れ子になっている以外、大差はありません。
唯一違うところはカウンタ変数としてSRAM上の領域を使用しているところです。
SPC700のアドレス空間は0x0000~0xFFFFまであります。
そのうち、0x0000~0x00FFをページ0、0x0100~0x01FFをページ1という特別な領域があります。この特別な領域をダイレクトページとすることができます。
ダイレクトページでは算術命令による直接アクセスが可能となります。
さらに、ダイレクトページ同士(ページ0→ページ0 or ページ1→ページ1)の値の出し入れも直接行えます。
ダイレクトページは常にページ0またはページ1のどちらかしか指定できません。
ダイレクトページの指定は「CLRP」命令または「SETP」命令で指定します。
「CLRP」を実行するとページ0(0x0000~0x00FF)をダイレクトページ、「SETP」を実行するとページ1(0x0100~0x01FF)をダイレクトページに設定します。
SPC700の場合、ページ1をスタック領域として設定されているため、ページ0をダイレクトページとしておくことが多いようです。
今回のプログラム例ではページ0(0x0100~0x01FF)をダイレクトページとしています。
この2つのループを処理する時間を計算します。
loop2部分の所要時間Tw_loop2は、
Tw_loop2 = (6 * nloop2) / f
= (6 * nloop2) / (1.024M)
nloop2:アキュムレータ初期値(ループ2の回数)
となります。この部分はループ1個の時と同様です。
loop1の部分の所要時間Tw_loop1はどうなるでしょう?
「 MOV $00, #$05」ではNmovdp=5サイクルで実行します。アキュムレータへ書き込むときより3サイクル増加しています。
「DEC $00」
この命令はNdecdp=4サイクルで実行します。アキュムレータのデクリメントの時より、2サイクル増加しています。
loop1の部分のみ実行に必要な時間Tw_loop1は、
Tw_loop1 = (Nmovdp + (Ndecdp + Nbne_b)(nloop1-1) + (Ndecdp + Nbne_nb)) * 1 / f
= (5 + (4 + 4)(7-1) + (4 + 2)) * 1 / (1.024M)
= 57.62μs
nloop1:アドレス0x0000にある値の初期値(ループ1の回数)
アキュムレータの時より大分時間がかかります。
ループ回数と実行に必要な時間の関係をもう少しわかりやすくすると、
Tw_loop1 = (8 * nloop1 + 3) / f
= (8 * nloop1 + 3) / (1.024M)
となり、二つのループの合計所要時間Twは、
Tw = Tw_loop1 + Tw_loop2 * nloop1
= ((8 * nloop1 + 3) / (1.024M)) + ((6 * nloop2) / (1.024M)) * nloop1
= ((8 * nloop1 + 3) + (6 * nloop2)* nloop1) / (1.024M)
= ((8 * 7 + 3) + (6 * 7)* nloop1) / (1.024M)
= 344.7μs
となります。
アキュムレータ(ループ回数)と$0000にセットできる最大値はnloop1=nloop2=0xFF=255なので、このループ処理で稼げる最大時間Twmaxは、
Twmax = Tw_loop1 + Tw_loop2 * nloop1
= ((8 * nloop1 + 3) + (6 * nloop2)* nloop1) / (1.024M)
= ((8 * 255 + 3) + (6 * 255)* nloop1) / (1.024M)
= 383.0ms
約380msのLEDの点滅なのでLED点滅が十分わかります。
0.5秒で点滅させたい時はさらにループを入れ子にします。
/////////////////////////////////////////////////
MOV $00, #$0A ;$0000へ0x0Aを書き込む
loop1:
MOV $01, #$43 ;$0001へ0x43を書き込む
loop2:
MOV A, #$7E ;アキュムレータへ0x7Eを書き込む
loop3:
DEC A ;アキュムレータデクリメント
BNE loop3 ;ゼロフラグがクリアされているときloop3へ
DEC $01 ;$0001デクリメント
BNE loop2 ;ゼロフラグがクリアされているときloop2へ
DEC $00 ;$0000デクリメント
BNE loop1 ;ゼロフラグがクリアされているときloop1へ
/////////////////////////////////////////////////
ループ処理で稼げる時間Twは、
Tw = Tw_loop1 + (Tw_loop2 + Tw_loop3 * nloop2) * nloop1
= ((8 * nloop1 + 3) +((8 * nloop2 + 3) + (6 * nloop3)* nloop2) * nloop1) / (1.024M)
= ((8 * 10 + 3) +((8 * 67 + 3) + (6 * 126)* 67) * 10) / (1.024M)
= 500.0ms
さて、あとはLEDが接続されたポート0への書き込みと無限ループを組み合わせるだけです。
/////////////////////////////////////////////////
main_loop:
MOV $F4, #$01 ;$00F4(ポート0)へ0x01を書き込む
;0.5秒ウェイト
MOV $00, #$0A ;$0000へ0x0Aを書き込む
loop1A:
MOV $01, #$43 ;$0001へ0x43を書き込む
loop2A:
MOV A, #$7E ;アキュムレータへ0x7Eを書き込む
loop3A:
DEC A ;アキュムレータデクリメント
BNE loop3A ;ゼロフラグがクリアされているときloop3Aへ
DEC $01 ;$0001デクリメント
BNE loop2A ;ゼロフラグがクリアされているときloop2Aへ
DEC $00 ;$0000デクリメント
BNE loop1A ;ゼロフラグがクリアされているときloop1Aへ
MOV $F4, #$00 ;$00F4(ポート0)へ0x00を書き込む
;0.5秒ウェイト
MOV $00, #$0A ;$0000へ0x0Aを書き込む
loop1B:
MOV $01, #$43 ;$0001へ0x43を書き込む
loop2B:
MOV A, #$7E ;アキュムレータへ0x7Eを書き込む
loop3B:
DEC A ;アキュムレータデクリメント
BNE loop3B ;ゼロフラグがクリアされているときloop3Bへ
DEC $01 ;$0001デクリメント
BNE loop2B ;ゼロフラグがクリアされているときloop2Bへ
DEC $00 ;$0000デクリメント
BNE loop1B ;ゼロフラグがクリアされているときloop1Bへ
BRA main_loop
/////////////////////////////////////////////////
「BRA」は無条件分岐命令です。-128~+127バイトの範囲で相対アドレスジャンプできます。
めでたくアセンブラコードが完成しました。
次に機械後へ翻訳します。
アセンブラのニーモニックは機械語のオペコードに対応しています。
では早速変換していきましょう。
変換にはSPC700オペコード表を用います。ネット上に多く情報があるので、各自で調べてみると良いでしょう。
/////////////////////////////////////////////////
main_loop:
"$8F" $F4, #$01 ;$00F4(ポート0)へ0x01を書き込む
;0.5秒ウェイト
"$8F" $00, #$0A ;$0000へ0x0Aを書き込む
loop1A:
"$8F" $01, #$43 ;$0001へ0x43を書き込む
loop2A:
"$E8", #$7E ;アキュムレータへ0x7Eを書き込む
loop3A:
"$9C" ;アキュムレータデクリメント
"$D0" loop3A ;ゼロフラグがクリアされているときloop3Aへ
"$8B" $01 ;$0001デクリメント
"$D0" loop2A ;ゼロフラグがクリアされているときloop2Aへ
"$8B" $00 ;$0000デクリメント
"$D0" loop1A ;ゼロフラグがクリアされているときloop1Aへ
"$8F" $F4, #$00 ;$00F4(ポート0)へ0x00を書き込む
;0.5秒ウェイト
"$8F" $00, #$0A ;$0000へ0x0Aを書き込む
loop1B:
"$8F" $01, #$43 ;$0001へ0x43を書き込む
loop2B:
"$E8" A, #$7E ;アキュムレータへ0x7Eを書き込む
loop3B:
"$9C" ;アキュムレータデクリメント
"$D0" loop3B ;ゼロフラグがクリアされているときloop3Bへ
"$8B" $01 ;$0001デクリメント
"$D0" loop2B ;ゼロフラグがクリアされているときloop2Bへ
"$8B" $00 ;$0000デクリメント
"$D0" loop1B ;ゼロフラグがクリアされているときloop1Bへ
"$2F" main_loop
/////////////////////////////////////////////////
ニーモニックをオペコードへ置き換えました。もはやコメントがないとどんな処理をしているかわからないレベルです。
今度は、このプログラムを羅列します。このとき、オペランドが2バイトある命令は、オペランドの順を入れ替える必要があります。今回は、オペコード"8F"の後2バイトを入れ替えます。
/////////////////////////////////////////////////
main_loop:
8F, 01, F4, 8F, 0A, 00,
loop1A:
8F, 43, 01,
loop2A:
E8, 7E,
loop3A:
9C, D0, loop3A, 8B, 01, D0, loop2A, 8B, 00, D0, loop1A, 8F, 00, F4, 8F, 0A, 00,
loop1B:
8F, 43, 01,
loop2B:
E8, 7E,
loop3B:
9C, D0, loop3B, 8B, 01, D0, loop2B, 8B, 00, D0, loop1B, 2F, main_loop
/////////////////////////////////////////////////
だいぶすっきりしてきました。最後に残っているのは「BNE」、「BRA」命令の相対ジャンプ量です。この数値は、((ジャンプ先の実行したい命令アドレス) - (現在のオペランドアドレス))-1を指定します。
(※「-1」というのは、CPUが命令実行後にプログラムカウンタを自動的にインクリメントするため、あらかじめ1引いています。)
負の相対アドレスを指定する時は2の補数表現を用います。
@@@@@@@@@@@@@@@@@@@@
例:
X0, X1, X2, X3, X4, X5, X6, "BRA", NN, X9, XA, XB, XC, XD, XE
のプログラムがある時
NNに0x01が入った時、"BRA"実行後、"XA"が実行されます。
NNに0x03が入った時、"BRA"実行後、"XC"が実行されます。
NNに0xFA(-6)が入った時、"BRA"実行後、"X3"が実行されます。
NNに0xF8(-8)が入った時、"BRA"実行後、"X1"が実行されます。
@@@@@@@@@@@@@@@@@@@@
相対ジャンプ量-1を計算します。
loop1A = -15 -1 = -16 = 0xF0
loop2A = -8 -1 = -9 = 0xF7
loop3A = -2 -1 = -3 = 0xFD
loop1B = -15 -1 = -16 = 0xF0
loop2B = -8 -1 = -9 = 0xF7
loop3B = -2 -1 = -3 = 0xFD
main_loop = -45 -1 = -46 = 0xD2
/////////////////////////////////////////////////
main_loop:
8F, 01, F4, 8F, 0A, 00,
loop1A:
8F, 43, 01,
loop2A:
E8, 7E,
loop3A:
9C, D0, loop3A, 8B, 01, D0, loop2A, 8B, 00, D0, loop1A, 8F, 00, F4, 8F, 0A, 00,
loop1B:
8F, 43, 01,
loop2B:
E8, 7E,
loop3B:
9C, D0, loop3B, 8B, 01, D0, loop2B, 8B, 00, D0, loop1B, 2F, main_loop
/////////////////////////////////////////////////
↓
/////////////////////////////////////////////////
main_loop:
8F, 01, F4, 8F, 0A, 00,
loop1A:
8F, 43, 01,
loop2A:
E8, 7E,
loop3A:
9C, D0, FD, 8B, 01, D0, F7, 8B, 00, D0, F0, 8F, 00, F4, 8F, 0A, 00,
loop1B:
8F, 43, 01,
loop2B:
E8, 7E,
loop3B:
9C, D0, FD, 8B, 01, D0, F7, 8B, 00, D0, F0, 2F, D2
/////////////////////////////////////////////////
さらに羅列して完成です。
/////////////////////////////////////////////////
8F, 01, F4, 8F, 0A, 00, 8F, 43, 01, E8, 7E, 9C, D0, FD, 8B, 01,
D0, F7, 8B, 00, D0, F0, 8F, 00, F4, 8F, 0A, 00, 8F, 43, 01, E8,
7E, 9C, D0, FD, 8B, 01, D0, F7, 8B, 00, D0, F0, 2F, D2
/////////////////////////////////////////////////
合計46バイトの0.5秒おきにLチカするプログラムが完成しました。
このコードをAPUのSRAMに書き込めば、Lチカプログラムが実行します。
SPC700へプログラムを転送するのにはArduinoを使用します。
・回路図
・Arduinoのプログラム(スケッチ)
//SPC700でLチカプログラム
//©oy
//https://oykenkyu.blogspot.com/2021/10/spc700-midi.html
//ユーザープログラムのサイズ(Byte)
#define PROGSIZE 46
//SPC700プログラム書き始めアドレス
#define WRITE_ADR 0x0200
//SPC700プログラム開始アドレス
#define START_ADR 0x0200
//SPC700Lチカプログラム
unsigned char progdata[PROGSIZE] = {
0x8F, 0x01, 0xF4, 0x8F, 0x0A, 0x00, 0x8F, 0x43, 0x01, 0xE8, 0x7E, 0x9C, 0xD0, 0xFD, 0x8B, 0x01,
0xD0, 0xF7, 0x8B, 0x00, 0xD0, 0xF0, 0x8F, 0x00, 0xF4, 0x8F, 0x0A, 0x00, 0x8F, 0x43, 0x01, 0xE8,
0x7E, 0x9C, 0xD0, 0xFD, 0x8B, 0x01, 0xD0, 0xF7, 0x8B, 0x00, 0xD0, 0xF0, 0x2F, 0xD2
};
//APUのポートから読み込み
unsigned char read_apu(unsigned char adr);
//APUのポートへ書き込み
void write_apu(unsigned char adr, unsigned char writedata);
//APUへデータ転送
void prog_write(unsigned int adr, unsigned char *writedata, int len);
//セットアップ
void setup()
{
PORTB = 0x0F; //pin8~pin9_APU:A0~A1_output,pin10_APU:RD,pin11_APU:WE
DDRB = 0x0F; //pin8~pin9_APU:A0~A1_output,pin10_APU:RD,pin11_APU:WE
pinMode(14, OUTPUT); //pin14_APU_reset
digitalWrite(14, HIGH);//pin14_APU_reset
//SPC700プログラム書き込み
prog_write(WRITE_ADR, progdata, PROGSIZE);
//SPC700ユーザープログラム開始
write_apu(3, START_ADR >> 8);
write_apu(2, START_ADR & 0xFF);
write_apu(1, 0x00);//SPC700ユーザープログラム開始モード
write_apu(0, PROGSIZE + 1);//データ転送終了&ユーザープログラム開始
//Lチカ用にarduinoデータピンを開放、APUポート0を選択、APUのRDを常にL、
DDRB = 0x0F; //pin8~pin11_output,pin12~pin13_(D0~D1)input
DDRD &= ~0xFC;//pin2~pin7_(D2~D7)input
PORTB = (~0x03 & PORTB) | (0 & 0x03);//APU_アドレス0
digitalWrite(10, LOW);//pin10_APU_we
}
void loop()
{
}
//APUのポートから読み込み
unsigned char read_apu(unsigned char adr)
{
unsigned char readdata;
DDRB = 0x0F;//arduino_data_pin_input
DDRD &= ~0xFC;//arduino_data_pin_input
PORTB = (~0x03 & PORTB) | (adr & 0x03); //APU_address_write
digitalWrite(10, LOW);//pin10_APU_RD
delayMicroseconds(1);
readdata = ((PINB & 0x30) >> 4) | (PIND & 0xFC);
digitalWrite(10, HIGH);//pin10_APU_RD
return readdata;
}
//APUのポートへ書き込み
void write_apu(unsigned char adr, unsigned char writedata)
{
DDRB = 0x3F;//arduino_data_pin_output
DDRD |= 0xFC;//arduino_data_pin_output
PORTB = (~0x03 & PORTB) | (adr & 0x03); //APU_address_write
PORTD = (~0xFC & PORTD) | (writedata & 0xFC); //APU_data_write
PORTB = (~0x30 & PORTB) | (((writedata & 0x03) << 4) & 0x30); //APU_data_write
digitalWrite(11, LOW);//pin11_APU_WE
delayMicroseconds(1);
digitalWrite(11, HIGH);//pin11_APU_WE
}
//APUへデータ転送
void prog_write(unsigned int adr, unsigned char *writedata, int len)
{
//APUリセット
digitalWrite(14, LOW);
delay(10);
digitalWrite(14, HIGH);
while(!((read_apu(0) == 0xAA) && (read_apu(1) == 0xBB))){
//APUが初期化を完了するまで待機
}
write_apu(3, adr >> 8);
write_apu(2, adr & 0xFF);
write_apu(1, 1);//APUユーザープログラム転送モード
write_apu(0, 0xCC);
while(read_apu(0) != 0xCC){
//APUが転送準備を完了するまで待機
}
for(unsigned int i = 0;i < len;i++)
{
write_apu(1, writedata[i]);
write_apu(0, i & 0xFF);
while(read_apu(0) != (i & 0xFF)){
//APUが転送データ1バイトを書き込むまで待機
}
}
}
0.5秒おきにLEDが点滅しています。
SNESのAPUでLチカ#SPC700 pic.twitter.com/khgyBjzb5F
— oy (@0x6f_0x79) October 17, 2021
どうでしょう?
人力で機械語に変換するのは、とても大変ですね。
プログラムサイズが大きくなると、アセンブラから機械語の変換は、人力ではもはや手に負えなくなるので、通常はソフトで変換します。
WLA-DXというソフトを使うとこの面倒な作業をやってくれます。
参考
ゲームギア開発:アセンブラで開発できる「WLA-DX」を動かしてみる
;//spc700_LB.asm////////////////WLA-DX SPC700////////////////
.MEMORYMAP
DEFAULTSLOT 0
SLOTSIZE $10000
SLOT 0 $0000
.ENDME
.ROMBANKMAP
BANKSTOTAL 1
BANKSIZE $10000
BANKS 1
.ENDRO
.ORG $200 ;開始アドレス
main_loop:
MOV $F4, #$01 ;$00F4(ポート0)へ0x01を書き込む
;0.5秒ウェイト
MOV $00, #$0A ;$0000へ0x0Aを書き込む
loop1A:
MOV $01, #$43 ;$0001へ0x43を書き込む
loop2A:
MOV A, #$7E ;アキュムレータへ0x7Eを書き込む
loop3A:
DEC A ;アキュムレータデクリメント
BNE loop3A ;ゼロフラグがクリアされているときloop3Aへ
DEC $01 ;$0001デクリメント
BNE loop2A ;ゼロフラグがクリアされているときloop2Aへ
DEC $00 ;$0000デクリメント
BNE loop1A ;ゼロフラグがクリアされているときloop1Aへ
MOV $F4, #$00 ;$00F4(ポート0)へ0x00を書き込む
;0.5秒ウェイト
MOV $00, #$0A ;$0000へ0x0Aを書き込む
loop1B:
MOV $01, #$43 ;$0001へ0x43を書き込む
loop2B:
MOV A, #$7E ;アキュムレータへ0x7Eを書き込む
loop3B:
DEC A ;アキュムレータデクリメント
BNE loop3B ;ゼロフラグがクリアされているときloop3Bへ
DEC $01 ;$0001デクリメント
BNE loop2B ;ゼロフラグがクリアされているときloop2Bへ
DEC $00 ;$0000デクリメント
BNE loop1B ;ゼロフラグがクリアされているときloop1Bへ
BRA main_loop
;/////////////////////////////////////////////////////////////
;//spc700_LB.link///////////////////////////////////////////////
[objects]
C:\Users\hogehoge\spc700_LB.o
;/////////////////////////////////////////////////////////////
上記「spc700_LB.asm」「spc700_LB.link」を任意の場所(例ではC:\Users\hogehoge\)に作成し、「wla-spc700.exe」「wlalink.exe」をダウンロードしてきて同ディレクトリに配置します。
その後次の2つのコマンドをコマンドプロンプトで1つずつ実行します。
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
"C:\Users\hogehoge\wla-spc700.exe" -v "C:\Users\hogehoge\spc700_LB.asm"
"C:\Users\hogehoge\wlalink.exe" -v C:\Users\hogehoge\spc700_LB.link C:\Users\hogehoge\spc700_LB.bin
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Free space at $0000-$01ff.
Free space at $022e-$ffff.
Bank 00 has 65490 bytes (99.93%) free.
65490 unused bytes of total 65536.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
こんな感じの結果が返ってきて「spc700_LB.bin」が作成されたら成功です。
「spc700_LB.bin」のサイズは64Kバイトあり、HEXエディタで覗くと開始アドレス$0200からプログラムが入っているのがわかります。もちろん先ほど行った手動ビルドの結果と同じはずです。
開始アドレスは「spc700_LB.asm」の「.ORG $200」で設定しています。
実際SPC700で実行するときは、Arduino側で開始アドレスを決めて書き込むので「.ORG $00」でも良いです。
・MIDI演奏するためのデータ転送プログラムの作成
さて、本題に戻ってスーパファミコンのAPUで音を出すプログラムを書いていきましょう。
まず、検討するのは、APUで直接MIDIを受信できるかどうかです。
MIDIはB=31250bpsの非同期シリアル通信です。シリアル通信は、0と1の時間変化でデータのやりとりをします。
受信側は1秒間に31250回以上で0,1の状態を読み取る必要があります。
Arduinoなどに使われているATmega328pは、シリアルデータを受信する専用のハードウェアを持っていますが、APUにはありません。プログラムを書いてソフトウェアで読み取る必要があります。
APUのCPUであるSPC700はf=1.024MHzで動作します。このとき、シリアルデータ1bitを読み取るのに余裕があるCPUクロック数Nrmは、
Nrm = f / B = 1.024M / 31250
= 32.77 ≂ 32クロック
です。
SPC700は1命令実行するのに2~8ほどクロックが必要なので、だいぶ余裕がないと分かります。なのでAPUで直接MIDIを受信するのはあきらめましょう。
基本的にAPUを使うには、APUのSRAMへユーザープログラム等のデータを書くための別のマイコンが必要です。
そこで、MIDIの受信はその別のマイコンに任せるとします。
DSPとSRAMのデータをマイコンとのやり取り用としてのみ、SPC700を使います。
その上で、APUのポートの特性をよく理解する必要があります。
・APUのポートの回路図
※回路図上では1ポート分のみ描かれているので、実際はこれがあと3つ加わり合計4組のラッチ回路が構成されています。
APUのポートまわりの回路図を見てもらうと、データを保持するラッチが読み込みと書き込みで別々に用意されています。
つまり、SPC700がポートに値を書き込んだあと、そのデータはSPC700側で読みだすことはできません。
逆にマイコンがAPUのポートに値を書き込んだあと、そのデータはマイコン側で読みだすことはできません。
この回路は、マイコン-SPC700間のデータのやり取りする時、なんか便利そうに見えますが、「SPC700へデータを書き込んだことを知らせる」という点では致命的です。
例えば、マイコンからAPUのポート1へデータを書き込む場合を考えてみましょう。
マイコンがAPUのポート1へ"AA"→"BB"→"CC"→"DD"→"EE"を順に書き込場合、SPC700側はポート1を随時読み取り、ポート1の値の変化から値が書き込まれたことを検知できます。
では、マイコンがAPUのポート1へ"AA"→"AA"→"AA"→"AA"→"AA"を順に書き込場合どうでしょう。SPC700側はポート1の値の変化を読み取れないため、書き込まれたことを検知できません。
そこで、ポート0を用いてSPC700側へデータを書き込んだことを通知します。
マイコン側からポート1へデータを書いたのち、マイコン側でポート0の値を変化させます。
ポート0の特定のビットをフラグとして使用することで、SPC700へ任意のデータを書き込めるようにします。
そこで、問題になるのがデータを保持するラッチが読み込みと書き込みで別々に用意されていることです。
ポート0の特定のビットをフラグとして使用した場合、SPC700はポート1のデータ受け取り後に、ポート0のフラグビットを戻す必要があります。ですがそれがハードウェア上できません。
そこで、ポート0のフラグビットの反転があった時に、SPC700はデータを読み取り&内部SRAMまたはレジスタ書き込みを実行するプログラムとしなければいけません。
・プログラム
SPC700のプログラムは基本的にDSPとSRAMのデータをマイコンとのやり取り用する機能のみとします。ですので、プログラムの規模は小さいです。
コード内のコメントで、どんなを処理をしているかを書いていますが、ニーモニックの詳しい解説はしていませんので各自で調べてみてください。
先ほどのLチカプログラムと大きく違うのは、サブルーチンを使用しているところです。
アセンブラ特有の「スタック」の説明はしないので、各自で調べてみてください。
/////////////////////////////////////////////////
.MEMORYMAP
DEFAULTSLOT 0
SLOTSIZE $10000
SLOT 0 $0000
.ENDME
.ROMBANKMAP
BANKSTOTAL 1
BANKSIZE $10000
BANKS 1
.ENDRO
.org $200
;変数
;$00,$01 アドレス保存
;
;$04 以前のポート0(フラグ用)の状態を保存
;
;$F2 DSPアドレスレジスタ
;$F3 DSPデータレジスタ
;$F4 ポート0_読み書きフラグ用(0,0,L:SRAM/H:DSPアドレス,アドレスオートインクリメント:L-No_H-Yes,読み込み完了,書き込み完了,読み込み要求,書き込み要求)
;$F5 ポート1_データやり取り用
;$F6 ポート2_アドレス下位8bitやり取り用
;$F7 ポート3_アドレス上位8bitやり取り用
MOV $00, #$00 ;$00,$01(アドレス16bit保存用)初期化
MOV $01, #$00 ;
MOV $F5, #$00 ;ポート1_データやり取り用初期化
MOV $F6, #$00 ;ポート2_アドレス下位8bitやり取り用初期化
MOV $F7, #$00 ;ポート3_アドレス上位8bitやり取り用初期化
MOV A, $F4
AND A, #$3F ;上位2ビットは使わないので0
MOV $04, A ;$04(以前のフラグ用保存用)へポート0(フラグ用)を書き込む
MOV $F4, $04 ;ポート0(フラグ用)へ$04(以前のフラグ用保存用)を書き込む
main_loop:
;書き込み要求があるときの判定
MOV A, $F4 ;アキュムレータへポート0(フラグ用)を書き込む
EOR A, $04 ;以前のポート0の状態と今回取得したポート0の状態に差がある時、各ビットが1
AND A, #$01 ;書き込み要求のフラグのみ抽出
BNE write ;ゼロフラグが0(書き込み要求のビット反転があった時)書き込みサブルーチンへ
;読み込み要求があるときの判定
MOV A, $F4 ;アキュムレータへポート0(フラグ用)を書き込む
EOR A, $04 ;以前のポート0の状態と今回取得したポート0の状態に差がある時、各ビットが1
AND A, #$02 ;読み込み要求のフラグのみ抽出
BNE read ;ゼロフラグが0(読み込み要求のビット反転があった時)読み込みサブルーチンへ
BRA main_loop ;メインループ先頭へ
write: ;ポート書き込みルーチン
;アドレスオートインクリメント有効か判定
MOV A, $F4 ;アキュムレータへポート0(フラグ用)を書き込む
AND A, #$10 ;アドレスオートインクリメントのフラグのみ抽出
BEQ no_aai_w ;ゼロフラグが1(アキュムレータが0,オートインクリメント)
;オートインクリメント有効の時
OR $04, #$10 ;$04(以前のフラグ用保存用)のbit4(オートインクリメント有効無効フラグ)をHにする
CALL !write_data ;データ書き込みサブルーチンへ
INCW $00 ;$00,$01(アドレス16bit保存用)インクリメント
BRA aai_exit_w ;aai_exit_wへ
no_aai_w: ;オートインクリメント無効の時
AND $04, #$EF ;$04(以前のフラグ用保存用)のbit4(オートインクリメント有効無効フラグ)をLにする
MOVW YA, $F6 ;Yレジスタとアキュムレータへポート3,4(アドレス)を書き込む
MOVW $00, YA ;$00,$01へポート3,4(アドレス)を書き込む
CALL !write_data ;データ書き込みサブルーチンへ
aai_exit_w:
EOR $04, #$05 ;$04(以前のフラグ用保存用)のbit2(書き込み完了通知)とbit0(書き込み要求)を反転
MOVW YA, $00 ;Yレジスタとアキュムレータへ$00,$01を書き込む
MOVW $F6, YA ;ポート3,4(アドレス)へ$00,$01を書き込む
MOV $F4, $04 ;ポート0(フラグ用)へ$04(以前のフラグ用保存用)を書き込む
BRA main_loop ;メインループ先頭へ
write_data: ;sram/dspデータ書き込みサブルーチン
;書き込み先がSRAM/DSPか判定
MOV A, $F4 ;アキュムレータへポート0(フラグ用)を書き込む
AND A, #$20 ;書き込み先SRAM/DSPのフラグのみ抽出
BEQ sel_ram_dsp_w ;ゼロフラグが1(アキュムレータが0,書き込み先SRAM)
OR $04, #$20 ;$04(以前のフラグ用保存用)のbit5(書み込み先SRAM/DSPのフラグ)をHにする
MOV $F2, $00 ;DSPアドレスレジスタへ$00内にセットされているアドレスを書き込む
MOV $F3, $F5 ;DSPデータレジスタへポート1(データ用)を書き込む
BRA sel_ram_dsp_exit_w
sel_ram_dsp_w:
AND $04, #$DF ;$04(以前のフラグ用保存用)のbit5(書み込み先SRAM/DSPのフラグ)をLにする
MOV Y, #$00 ;Yレジスタへ0x00を書き込む
MOV A, $F5 ;アキュムレータへポート1(データ用)を書き込む
MOV [$00]+Y, A ;$00,$01に内にセットされているアドレスへポート1(データ用)を書き込む
sel_ram_dsp_exit_w:
RET ;サブルーチン終了
read: ;ポート読み込みルーチン
;アドレスオートインクリメント有効か判定
MOV A, $F4 ;アキュムレータへポート0(フラグ用)を書き込む
AND A, #$10 ;アドレスオートインクリメントのフラグのみ抽出
BEQ no_aai_r ;ゼロフラグが1(アキュムレータが0,オートインクリメント)
;オートインクリメント有効の時
OR $04, #$10 ;$04(以前のフラグ用保存用)のbit4(オートインクリメント有効無効フラグ)をHにする
CALL !read_data ;データ書き込みサブルーチンへ
INCW $00 ;$00,$01(アドレス16bit保存用)インクリメント
BRA aai_exit_r ;aai_exit_rへ
no_aai_r: ;オートインクリメント無効の時
AND $04, #$EF ;$04(以前のフラグ用保存用)のbit4(オートインクリメント有効無効フラグ)をLにする
MOVW YA, $F6 ;Yレジスタとアキュムレータへポート3,4(アドレス)を書き込む
MOVW $00, YA ;$00,$01へポート3,4(アドレス)を書き込む
CALL !read_data ;データ書き込みサブルーチンへ
aai_exit_r:
EOR $04, #$0A ;$04(以前のフラグ用保存用)のbit3(読み込み完了通知)とbit1(読み込み要求)を反転
MOVW YA, $00 ;Yレジスタとアキュムレータへ$00,$01を書き込む
MOVW $F6, YA ;ポート3,4(アドレス)へ$00,$01を書き込む
MOV $F4, $04 ;ポート0(フラグ用)へ$04(以前のフラグ用保存用)を書き込む
BRA main_loop ;メインループ先頭へ
read_data: ;sram/dspデータ読み込みサブルーチン
;読み込み先がSRAM/DSPか判定
MOV A, $F4 ;アキュムレータへポート0(フラグ用)を書き込む
AND A, #$20 ;読み込み先SRAM/DSPのフラグのみ抽出
BEQ sel_ram_dsp_r ;ゼロフラグが1(アキュムレータが0,読み込み先SRAM)
OR $04, #$20 ;$04(以前のフラグ用保存用)のbit5(読み込み先SRAM/DSPのフラグ)をHにする
MOV $F2, $00 ;DSPアドレスレジスタへ$00内にセットされているアドレスを書き込む
MOV $F5, $F3 ;ポート1(データ用)へDSPデータレジスタを書き込む
BRA sel_ram_dsp_exit_r
sel_ram_dsp_r:
AND $04, #$DF ;$04(以前のフラグ用保存用)のbit5(読み込み先SRAM/DSPのフラグ)をLにする
MOV Y, #$00 ;Yレジスタへ0x00を書き込む
MOV A, [$00]+Y ;ポート1(データ用)へ$00,$01に内にセットされているアドレス内の値を書き込む
MOV $F5, A ;ポート1(データ用)へアキュムレータの値を書き込む
sel_ram_dsp_exit_r:
RET ;サブルーチン終了
/////////////////////////////////////////
このプログラムをSPC700が実行することで、マイコン側はSPC700を意識せずにSRAMやDSPとデータのやり取りができます。
このプログラムの具体的な機能は、3つあります。
・SRAM上(SPC700上のアドレス$0000~$FFFF)のデータの読み書き
SPC700上のアドレス$0000~$FFFFの読み書きができます。$00F0~$00FFにあるSPC700のレジスタのアクセスも、このSRAMアクセスモードで読み書きします。
($00,$01,$04,$F4,$F5,$F6,$F7,$100~$300はプログラム内で使用しているため書き込み禁止です。)
・DSP上(DSP上のアドレス$00~$FF)のデータの読み書き
本来DSPのアクセスには$F2,$F3レジスタを通さなければいけませんが、演奏する際は頻繁にアクセスするので、直接アクセスできるようにしました。
DSPアクセスモードでアクセスすると、読み書き手順が少なくなり、少しだけ高速に読み書きができます。
・読み書き後、次点読み書きアドレスオートインクリメント機能
有効無効が切り替えられます。連続アドレスのアクセスで使用できます。BRRサンプル書き込み時など、いちいちアドレスの指定をすることなく連続したデータの書き込みができます。
・DSPにデータを書き込む
さて、ようやくAPUのSRAMやDSPにデータをマイコン側から書き込む準備が整いました。
DSPは音を出すための処理をするLSIで、DSPに何かしらの指示を出すと音が出ます。
APU内で言うと音源ICに相当します。(DSPはSRAMから音源データを読み込んで再生するため、厳密にはDSP単体で音源ICとは呼べないでしょう。)
音を出すにはまず、音の波形データが必要です。このデータはサンプリング周波数32KHz・16bitの波形データBRR圧縮した音波形データを入手する必要があります。
BRR圧縮については説明を省きます。「snes brr」等で検索をかけるといろいろ情報があるので各自調べてみてください。
ネット上にはwavをBRR圧縮してくれるソフトを作っている方がいるようなので、BRR圧縮した音波形データの入手には、そのソフトを利用します。
DSP用にBRR圧縮されたデータは9バイトで1ブロックとなっています。1ブロックにはステレオ16bitで8サンプル分のデータとヘッダ情報が含まれています。複数のブロックを連続してSRAM上に配置することで、1つの楽器(音声)のサンプルデータとなります。
・DSPの主な仕様
同時発音数:ステレオ8チャンネル
サンプリング周波数32KHz・16bitでBRR圧縮されたデータの再生
ノイズジェネレータ、ハードウェアエンベロープ、ピッチモジュレーション、エコー等
・DSPのレジスタ
DSPのレジスタは、SPC700の16bitアドレス空間とは別に、8bitアドレス($00~$FF)空間を持っています。
各レジスタの機能
アドレス | レジスタ名 | 機能 |
---|---|---|
$X0 | VOL_X_L | ch_Xの左音量レジスタ |
$X1 | VOL_X_R | ch_Xの右音量レジスタ |
$X2 | PTC_X_L | ch_Xの音程レジスタ(下位8bit) |
$X3 | PTC_X_H | ch_Xの音程レジスタ(上位8bit) |
$X4 | SRCN_X | ch_XのBRRソース番号(楽器サンプル番号)レジスタ(0~255を指定) |
$X5 | ENV1_X | ch_XのエンベロープADレジスタ |
$X6 | ENV2_X | ch_XのエンベロープSRレジスタ |
$X7 | GAIN_X | ch_Xのソフトウェアエンベロープ使用時のゲインレジスタ |
$X8 | DSP_ENV_X | ch_XのDSPエンベロープ制御作業用レジスタ(読み込み専用) |
$X9 | DSP_OUT_X | ch_XのDSP音量制御作業用レジスタ(読み込み専用) |
$0C | MAIN_VOL_L | メイン左音量レジスタ |
$1C | MAIN_VOL_R | メイン右音量レジスタ |
$2C | ECHO_VOL_L | エコー左音量レジスタ |
$3C | ECHO_VOL_R | エコー右音量レジスタ |
$4C | KEY_ON | キーオンレジスタ |
$5C | KEY_OFF | キーオフレジスタ |
$6C | DSP_CONF | DSP設定レジスタ |
$7C | END_SMPL | BRRサンプル終端通知レジスタ |
$0D | ECHO_FB | エコーフィードバックレジスタ |
$2D | PTC_MD | ピッチモジュレーションON/OFFレジスタ |
$3D | NOISE_EN | ノイズON/OFFレジスタ |
$4D | ECHO_EN | エコーON/OFFレジスタ |
$5D | SR_DIR | BRRサンプルデータソースディレクトリレジスタ |
$6D | ECHO_BUF_ADR | エコー用SRAM領域の開始アドレスレジスタ |
$7D | ECHO_DLY | エコー遅延時間設定レジスタ |
$XF | ECHO_FIR_X | ch_XのエコーFIRフィルタ係数設定レジスタ |
レジスタマップ
$X0 | $X1 | $X2 | $X3 | $X4 | $X5 | $X6 | $X7 | $X8 | $X9 | $XA | $XB | $XC | $XD | $XE | $XF | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
$0X | VOL_0_L | VOL_0_R | PTC_0_L | PTC_0_H | SRCN_0 | ENV1_0 | ENV2_0 | GAIN_0 | DSP_ENV_0 | DSP_OUT_0 | MAIN_VOL_L | ECHO_FB | ECHO_FIR_0 | |||
$1X | VOL_1_L | VOL_1_R | PTC_1_L | PTC_1_H | SRCN_1 | ENV1_1 | ENV2_1 | GAIN_1 | DSP_ENV_1 | DSP_OUT_1 | MAIN_VOL_R | ECHO_FIR_1 | ||||
$2X | VOL_2_L | VOL_2_R | PTC_2_L | PTC_2_H | SRCN_2 | ENV1_2 | ENV2_2 | GAIN_2 | DSP_ENV_2 | DSP_OUT_2 | ECHO_VOL_L | PTC_MD | ECHO_FIR_2 | |||
$3X | VOL_3_L | VOL_3_R | PTC_3_L | PTC_3_H | SRCN_3 | ENV1_3 | ENV2_3 | GAIN_3 | DSP_ENV_3 | DSP_OUT_3 | ECHO_VOL_R | NOISE_EN | ECHO_FIR_3 | |||
$4X | VOL_4_L | VOL_4_R | PTC_4_L | PTC_4_H | SRCN_4 | ENV1_4 | ENV2_4 | GAIN_4 | DSP_ENV_4 | DSP_OUT_4 | KEY_ON | ECHO_EN | ECHO_FIR_4 | |||
$5X | VOL_5_L | VOL_5_R | PTC_5_L | PTC_5_H | SRCN_5 | ENV1_5 | ENV2_5 | GAIN_5 | DSP_ENV_5 | DSP_OUT_5 | KEY_OFF | SR_DIR | ECHO_FIR_5 | |||
$6X | VOL_6_L | VOL_6_R | PTC_6_L | PTC_6_H | SRCN_6 | ENV1_6 | ENV2_6 | GAIN_6 | DSP_ENV_6 | DSP_OUT_6 | DSP_CONF | ECHO_BUF_ADR | ECHO_FIR_6 | |||
$7X | VOL_7_L | VOL_7_R | PTC_7_L | PTC_7_H | SRCN_7 | ENV1_7 | ENV2_7 | GAIN_7 | DSP_ENV_7 | DSP_OUT_7 | END_SMPL | ECHO_DLY | ECHO_FIR_7 |
それでは、音を出すための手順をみてみましょう。
まずは、ただ発音出来ればよいのでエコーなどの機能の説明は省きます。
発音手順をおおまかに2つのグループに分けます。1つ目のグループは、一度設定したらそうそう変更しないパラメータで、2つ目のグループはMIDIのノートオン・ノートオフで頻繁に変更があるパラメータです。
1グループ目・APU起動後の発音準備処理(BRRサンプルデータの配置)~各パラメータ設定
①BRR圧縮されたデータをSRAM上に配置②BRRサンプルデータソースディレクトリアドレスに、BRR開始ブロックアドレスとループアドレスを登録(サンプル1開始ブロックアドレス(2Byte),サンプル1ループアドレス(2Byte),サンプル2開始ブロックアドレス(2Byte),サンプル2ループアドレス(2Byte),…,サンプルPCM_N開始ブロックアドレス(2Byte),サンプルPCM_Nループアドレス(2Byte)の順に配置)③BRRサンプルデータソースディレクトリレジスタ(SR_DIR($5D))にBRRサンプルデータソースディレクトリアドレスの上位8bitをセット(下位8bitは$00に固定されています。)④BRRソース番号レジスタ(SRCN_X($X4))にサンプル番号PCM_Nを指定⑤エンベロープ関係のレジスタ(ENV1_X($X5), ENV2_X(&X6), GAIN_X($X7))等の設定⑥メイン音量レジスタ(MAIN_VOL_L($0C), MAIN_VOL_R($1C))の設定⑦DSP設定レジスタ(DSP_CONF($6C))の設定
2グループ目・DSP発音手順
①チャンネル音量レジスタ(VOL_X_L($X0), VOL_X_R($X1))のセット②音程レジスタ(PTC_X_L($X2), PTC_X_H($X3))のセット③キーオンレジスタ(KEY_ON($4C))のセット
詳しく見ていきましょう
APU起動後の発音準備処理(BRRサンプルデータの配置)
①BRR圧縮されたデータをSRAM上に配置
まずはサンプリングされたデータの配置です。
BRR圧縮されたデータは9バイトで1つのブロックなので、このブロックをSRAM上に連続して配置します。配置開始アドレスは基本的に自由に決められますが、$00
00~$01FF(ページ0,1)は、SPC700の変数&スタック領域なので配置できません。
また、 $0200~$0300らへんもユーザープログラムで使用するので配置できません。
例を挙げてみましょう。
BRRデータサイズが72byte(ブロックサイズ8)のサンプリングデータ「サンプル1」を配置します。
SRAM上のアドレス$8000から、サンプル1のBRRデータを書き込み、$8047がデータの終端になります。
同様にBRRデータサイズが36byte(ブロックサイズ4)のサンプリングデータ「サンプル2」を配置します。SRAM上のアドレス$8100から、サンプル2のBRRデータを書き込み、$8124がデータの終端になります。
②BRRサンプルデータソースディレクトリアドレスに、BRR開始ブロックアドレスとループアドレスを登録(サンプル1開始ブロックアドレス(2Byte),サンプル1ループアドレス(2Byte),サンプル2開始ブロックアドレス(2Byte),サンプル2ループアドレス(2Byte),…,サンプルPCM_N開始ブロックアドレス(2Byte),サンプルPCM_Nループアドレス(2Byte)の順に配置)
BRRサンプルデータソースディレクトリアドレスをどこかに決めておきます。このアドレスからのデータは、DSPがBRRサンプリングデータを再生する際、アドレスで指定せずサンプル番号で指定するためのベクタテーブルです。このアドレスの下位8bitは$00で固定となりますので注意してください。
例を挙げてみましょう。
「サンプル1」の開始アドレスは$8000、「サンプル2」の開始アドレスは$8100です。「サンプル1」のループ先は2ブロック目、「サンプル2」のループ先は1ブロック目(先頭)とします。
BRRサンプルデータソースディレクトリアドレスを$0400と決めます。開始ブロックアドレス(2Byte),ループアドレス(2Byte)の順で書き込んでいきます。
(サンプル1開始ブロックアドレス(2Byte),サンプル1ループアドレス(2Byte),サンプル2開始ブロックアドレス(2Byte),サンプル2ループアドレス(2Byte))
[$0400] 00, 80, 09, 80, 00, 81, 00, 81
③BRRサンプルデータソースディレクトリレジスタ(SR_DIR($5D))にBRRサンプルデータソースディレクトリアドレスの上位8bitをセット(下位8bitは$00に固定されています。)
BRRサンプリングデータのベクタ先頭のありかをDSPに教えてあげます。
BRRサンプルデータソースディレクトリレジスタ(SR_DIR($5D))にBRRサンプルデータソースディレクトリアドレスの上位8bitをセット(下位8bitは$00で固定)します。
例を挙げてみましょう。
BRRサンプルデータソースディレクトリアドレス先頭は$0400と決めたので、SR_DIR($5D)には$04をセットします。
SR_DIR($5D) ← $04
④BRRソース番号レジスタ(SRCN_X($X4))にサンプル番号PCM_Nを指定
チャンネルごとに再生したいサンプル番号をDSPに指定します。
BRRソース番号レジスタ(SRCN_X($X4))にサンプル番号を指定します。
例を挙げてみましょう。
チャンネル0の音色をサンプル1に設定、チャンネル1の音色をサンプル2に設定
SRCN_0($04)← $01
SRCN_1($14)← $02
⑤エンベロープ関係のレジスタ(ENV1_X($X5), ENV2_X(&X6), GAIN_X($X7))等の設定
エンベロープ(音量の時間変化)を設定します。
エンベロープのモードには3種類あり、1.「ADSRハードウェアエンベロープ」、2.「単純増減」、3.「ゲインパラメータ直接指定」が利用できます。
1.「ADSRハードウェアエンベロープ」にはアタック(Attack)、ディケイ(Decay)、サスティン(Sustain)、リリース(Release)の4つのパラメータがあります。
アタック(Attack)は、キーオンした時に音量が0からMAXになるまでの設定時間です。
ディケイ(Decay)は、アタックがMAXになった後にサスティン音量になるまでの設定時間です。
サスティン(Sustain)は、ディケイ時間が終了し、一定の音量となる時の設定音量です。
リリース(Release)は、キーオフ後のサスティン音量から0になるまでの設定時間です。
___最大音量/|\
/ | \ _____ __サスティン音量/ | | |\__/ | | | \ _____ ___音量0| | | | |
↑ ↑ ↑
| ディケイ時間 リリース時間
アタック時間| |
↓ ↓ ↓
| | | | |
__| ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄|____________ キー状態
キーオン キーオフ
「ADSRハードウェアエンベロープ」を利用するには、ENV1_X($X5)レジスタのbit7をHにします。
DSPのアタック(Attack)は、ENV1_X($X5)レジスタのbit0~bit3の4ビットで指定します。DSPのディケイ(Decay)は、ENV1_X($X5)レジスタのbit4~bit6の3ビットで指定します。
DSPのサスティン(Sustain)は、ENV2_X($X6)レジスタのbit5~bit7の3ビットで指定します。
DSPのリリース(Release)は、ENV2_X($X6)レジスタのbit0~bit4の5ビットで指定します。
アタック(Attack)、ディケイ(Decay)、リリース(Release)時間は、設定した値が小さいほど時間が長いです。
サスティン(Sustain)は、最大音量との比率で設定し、設定した値が小さいほど音量が小さくなる比率になります。
2.「単純増減」には、「線形増加」、「線形減少」、「曲線増加」、「曲線減少」の4つの種類があります。
「単純増減」を利用するには、ENV1_X($X5)レジスタのbit7をLかつ、GAIN_X($X7)レジスタのbit7をHにします。GAIN_X($X7)レジスタのbit6で増加か減少を選択します。(bit6が1で増加、0で減少)GAIN_X($X7)レジスタのbit5で曲線か線形を選択します。(bit5が1で曲線、0で線形)GAIN_X($X7)レジスタのbit0~bit4の5ビットで、所要時間を指定します。この設定値が小さいほど、音量が最大or0になるまでの時間が長いです。(0にすると無限)3.「ゲインパラメータ直接指定」を利用するには、ENV1_X($X5)レジスタのbit7をLかつ、GAIN_X($X7)レジスタのbit7をLにします。GAIN_X($X7)レジスタのbit0~bit6の7ビットで音量を指定します。(0~127)
例を挙げてみましょう。
チャンネル0のエンベロープを「線形減少」に設定、チャンネル1のエンベロープを「ADSRハードウェアエンベロープ」に設定します。
チャンネル0のエンベロープは有限時間の最長時間に設定します。
チャンネル0のエンベロープはアタック(Attack)=15、ディケイ(Decay)=0、サスティン(Sustain)=3、リリース(Release)=31
ENV1_0($05) ← $00(bit7=L(ADSRエンベロープ無効))
GAIN_0($07) ← $81(bit7=H(単純増減),bit6=L(現象),bit5=L(線形),bit0~bit4=$01(所要時間(最長)))
ENV1_1($15) ← $0F(bit7=L(ADSRエンベロープ有効),bit4~bit6=$00(ディケイ),bit0~bit3=$0F(アタック))
ENV2_1($16) ← $7F(bit5~bit7=$03(サスティン),bit0~bit4=$1F(リリース))
⑥メイン音量レジスタ(MAIN_VOL_L($0C), MAIN_VOL_R($1C))の設定
メイン音量レジスタは、APUから出力される音量を決めるレジスタです。
LとRで別々に設定できるため、左右のバランス調整として使えそうです。設定音量値は符号付き8bit(-128~127)です。
例を挙げてみましょう。左右のメイン音量を127にします。MAIN_VOL_L($0C) ← $7FMAIN_VOL_R($1C) ← $7F
⑦DSP設定レジスタ(DSP_CONF($6C))の設定
DSP設定レジスタは、「リセット」、「ミュート」、「エコー有無」、「ノイズ周波数」の4つの設定ができます。
「リセット」は、DSP_CONF($6C)のbit7をHすると、すべてのチャンネルが、キーオンへの待機中となり、ミュートがかかります。「ミュート」は、DSP_CONF($6C)のbit6をHすると、全てのチャンネルの出力にミュートがかかります。「エコー有無」は、DSP_CONF($6C)のbit5をLすると、エコーがかかります。エコー用に確保されたSRAM領域の読み書きが行われるため、必ず「エコー用SRAM領域の開始アドレスレジスタ:ECHO_BUF_ADR($6D)」でSRAM領域を指定してください。「ノイズ周波数」は、DSP_CONF($6C)のbit0~bit4の5ビット(Nbit=0~31)で設定します。 ノイズは、ドラムのスネアやシンバル系の音の表現などで利用できます。
ノイズ周波数fnは、1~29までは、2 ^ ((1/3)(11+Nbit)) [Hz]Nbit=0:fn=0、Nbit=30:fn=16KHz、Nbit=31:fn=32KHzとなるようです。
例を挙げてみましょう。「リセット」=解除、「ミュート」=解除、「エコー有無」=無効、「ノイズ周波数」=0にします。DSP_CONF($6C) ← $20(bit7=L(リセット解除),bit6=L(ミュート解除),bit5=H(エコー無効),bit0~bit4=$00(ノイズ設定値Nbit=0))
DSP発音手順
①チャンネル音量レジスタ(VOL_X_L($X0), VOL_X_R($X1))のセット
チャンネルごとの音量を設定します。メイン音量の設定と同様、LとRで別々に設定でき、設定音量値は符号付き8bit(-128~127)です。
例を挙げてみましょう。チャンネル0の左右の音量を127に、チャンネル1の左右の音量を63にします。
VOL_0_L($00) ← $7F
VOL_0_R($01) ← $7F
VOL_1_L($10) ← $3F
VOL_1_R($11) ← $3F
②音程レジスタ(PTC_X_L($X2), PTC_X_H($X3))のセット
発音する音のピッチ(PCM再生速度)を設定します。設定値Pは14ビット(0~16383)で、上位8bitはPTC_X_H($X3)レジスタへ、下位8bitはPTC_X_L($X2)レジスタへ入れます。ピッチPは、4096($1000)で32kHz(BRR標準のサンプリング周波数)でサンプルの再生をします。(原音再生)ピッチP、録音サンプリング周波数fs、サンプルされた楽器の音の周波数fとするとき、APUで発音した楽器の音の出力周波数foutは、fout = f * (32000 / fs) * P / (2^12) [Hz]
となります。例えばサンプリング周波数fs=32000Hz、サンプルされた楽器の音の周波数f=440Hz(A4)で録音した場合で、ピッチPを8192($2000)にすると、
fout = f * (32000 / fs) * P / (2^12)
= 440 * (32000 / 32000) * 8192 / (2^12)
= 880 Hz (A5)
となります。
出力周波数foutから、ピッチPを求める場合は、
P = fout * (fs / 32000)* (2^12) / fとなります。サンプリング周波数fs=32000Hz、サンプルされた楽器の音の周波数f=440Hz(A4)で録音した場合のピッチP設定値と音程Tn (Tn=69で440Hz(A4))の関係は、P = (2^12) * (2 ^ ((Tn-69)/12))= (2^7) * (2 ^ ((Tn-9)/12))
= (2 ^ (Tn/12)) * (2 ^ (7 - (9/12)))= (2 ^ (Tn/12)) * 76.1093となります。最大の音程はP=16383,Tn=93 (1760Hz(A6))で、最小音程はP=76でTn=0(27.5Hz(A0))になります。これより高い音程を出したい場合は、録音する時の楽器の音程をあげます。ただし、楽器の音は倍音を含んでいるため、倍音成分の周波数がサンプリング周波数の半部の周波数を超えないようにしなければいけません。楽器にもよりますが、倍音周波数は基音周数の8~10倍付近まであるため、録音時の楽器の音程は、せいぜい+2オクターブが限界でしょう。
例を挙げてみましょう。チャンネル0の音程をA4(Tn=69,P=4096=$1000)に、チャンネル1の音程をE5(Tn=76,P=6137=$17F9)にします。PTC_0_L($02) ← $00PTC_1_L($12) ← $F9
PTC_0_H($03) ← $10
PTC_1_H($13) ← $17
③キーオンレジスタ(KEY_ON($4C))のセット
最終的に音を出すには、キーオンレジスタ(KEY_ON($4C))を操作します。キーオンレジスタの各ビットが、チャンネルのキーオンに対応しています。KEY_ON($4C)のbit0はチャンネル0に割り当てられ、ビットをHにするとキーオンし、音がでます。ほかのチャンネル1~7もKEY_ON($4C)のbit1~bit7に割り当てられています。キーオフしたい場合は、KEY_OFF($5C)の各ビットをHにします。このレジスタのビット割り当てもKEY_ON($4C)と同様です。
例を挙げてみましょう。チャンネル0とチャンネル1をキーオンします。KEY_ON($4C) ← $03(bit0とbit1をキーオン)
チャンネル0とチャンネル1をキーオフします。KEY_OFF($5C) ← $03(bit0とbit1をキーオフ)
これらの手順でDSPのレジスタにデータを書き込むと音が鳴ります。
ただし、これ以外の未使用のレジスタ(エコー系やノイズ系などのレジスタ)については、電源投入時に不定値となるため、初期化が必要です。
初期化を忘れると、音が鳴らなかったり、期待した音が出なかったりします。
今回の例で、未使用のレジスタで音を出すのに直接かかわるものは、具体的に、
・エコー左音量レジスタ(MAIN_VOL_L($0C))
・エコー右音量レジスタ(MAIN_VOL_R($1C))
・ピッチモジュレーションON/OFFレジスタ(PTC_MD($2D))
・ノイズON/OFFレジスタ(NOISE_EN($3D))
・エコーON/OFFレジスタ(ECHO_EN($4D))
などがあります。
エコー音量レジスタは、音量0に、ON/OFFレジスタは各チャンネルに対応するビットをすべて0にすると無効にできます。
例を挙げてみましょう。
MAIN_VOL_L($0C) ← $00(エコー左音量0)
MAIN_VOL_R($1C) ← $00(エコー右音量0)
PTC_MD($2D) ← $00(チャンネル0~7のピッチモジュレーション無効)
NOISE_EN($3D) ← $00(チャンネル0~7のノイズ発音無効)
ECHO_EN($4D) ← $00(チャンネル0~7のエコー無効)
この未使用レジスタの初期化の処理は、「APU起動後の発音準備処理(BRRサンプルデータの配置)」の「⑦DSP設定レジスタ(DSP_CONF($6C))の設定」の前までに行う必要があります。
ようやくAPUで音を出すのに必要な最低限の手順・プログラムが完成しました。まだMIDIを受信して演奏する機能はありませんが、ひとまず音を出してみましょう。
ページサイズが大きすぎてインデックスされない問題があるため、記事を分割しました。
↓MIDI演奏プログラムはこちら
0 件のコメント:
コメントを投稿