PCE ハードウェア研究資料 - PSG

概要

PSG は HuC6280 内のアドレス $0800 〜 $0809 からアクセスできます(MPR0=$FF時)。 5 ビット 32 サンプルの波形メモリを 6 チャンネル (CH0 〜 CH5) 持っています。 ここに 32 サンプルの波形を登録し、音量と周波数をレジスタに設定して発音させます。 なお波形メモリを搭載した音源は、厳密には PSG とは呼ばれないようですが、ここでは便宜上 PSG と呼ぶことにします。

波形メモリの再生以外にもいくつかの機能を持っています。CH0 + CH1 を組み合わせて FM 音源に似た音を出力する LFO モード、波形メモリを使わずに CPU から書き込まれた値を 直接出力する DDA (Direct D/A) モード、疑似ノイズ出力モード (CH4, CH5)があります。

チャンネルごとに設定するレジスタは、まず $0800 に CH 番号 (0 〜 5)を書き込み、 $0802 〜 $0807 に CH のデータを書き込むことによって行います。 波形メモリの再生はすべてのチャンネルで可能なため、波形メモリの再生のみ行う場合は、同じ構造のチャンネルが 6 個あるとみなせます。

レジスタ一覧を示します。$0800, $0801, $0808, $0809 はチャンネル毎ではなく、全体で一つしか ないレジスタです。$0802 〜 $0806 は全てのチャンネルに存在し、$0807 は CH4と CH5 にのみ存在します。

AddressRegister D7D6D5D4D3D2D1D0
$0800Channel Select -----CH SEL
$0801Main Amplitude Level LMALRMAL
$0802Fine Frequency FRQ LOW
$0803Coarse Frequency ----FRQ HIGH
$0804ON, DDA, CH Amplitude ONDDA-AL
$0805L, R Amplitude LALRAL
$0806Waveform ---WAVE DATA
$0807Noise Enable, Noise Freq. NE--NOISE FRQ
$0808LFO Frequency LFO FRQ
$0809LFO Control TRG-----CTRL

レジスタは全て書き込み専用です。書き込んだ値を読み出すことはできません(読み出し値は I/O バッファの値になります)。

音量設定レジスタがいくつかありますが、出力レベルは各レジスタで設定した減衰量 [dB] を合計したものになります。たとえば、LMAL に $E (-3.0[dB]), LAL に $E (-3.0[dB]), AL に $1E (-1.5[dB]) を設定した場合、左音声の出力レベルは -3.0 - 3.0 - 1.5 = -7.5[dB] になります。

PCE 本体は初代(白) から DUO-RX まで数多くの機種が存在し、音声まわりの回路も機種によって多少異なります。 よって、全ての音量設定レジスタを最大に設定した場合、 一部の機種で音割れが発生してしまう可能性があります。このため、 LMAL および RMAL にそれぞれ最大値より 1 低い $E を設定しているゲームが多いです。

周波数設定レジスタに設定した値 12 ビットの値 V と波形の周波数 Fout[Hz] の関係は、 波形メモリに登録された波形が 32 サンプルで 1 周期であるとすると、 だいたい

Fout = 3579545 / 32 / V [Hz]

です。要は、PSG には 3.579545[MHz] が供給されており、1 クロックごとに周波数カウンタ(初期値 V)がカウントされ、 これが expire するごとに波形メモリの位相カウンタが +1 されるしくみです。

しかし実際には、上記とは少し異なります。実機では V に $000 を設定した場合に最も周波数が低くなり、V に $001 を設定した場合に最も周波数が高くなります。これは、どういうことでしょうか?

過去に調査した結果、周波数カウンタ FRQ_COUNTER をダウンカウンタと仮定した場合、 FRQ_COUNTER の expire は $001 になったかどうかで判定していると考えられます。

    AT (CLOCK_3579545HZ_POSITIVE_EDGE)
    {
        IF (FRQ_COUNTER == 12'H001)
        {
            INCREMENT_PHASE_COUNTER();
            FRQ_COUNTER = V;
        }
        ELSE IF (ON == 1)
        {
            FRQ_COUNTER = FRQ_COUNTER - 12'H001;
        }
        ELSE
        {
            FRQ_COUNTER = V;
        }
    }

FRQ_COUNTER と V は 12 ビットの unsigned int と考えてください。

FRQ_COUNTER の初期値は V です。V が $000 の場合は、次のクロックで行われる $001 との比較は成立せず、FRQ_COUNTER は $FFF, $FFE, ..., $002, $001 までカウントされてようやく expire します。これが、FRQ_COUNTER が expire するのに最も時間がかかるケースです。また、V が $001 の場合は、次のクロックで行われる $001 との比較が即成立します。これが最短のケースになります。

よって、V と波形の周波数 Fout[Hz] の正確な関係は、

Fout = 3579545 / 32 / (((V-1) & 0xFFF) + 1) [Hz]

になると思います。ただし、この式を使う必要があるのは V が $000 のときだけです。

なぜこんな動作になっているのかというと、おそらく Fout = 3579545 / 32 / V の式を使いたかったからでしょう。FRQ_COUNTER の expire 判定値を単純に $000 とした場合、出力の周波数は Fout = 3579545 / 32 / (V+1) となってしまい、計算が少しややこしくなってしまいます。それよりは、 おそらく使用しないと思われる、 V が $000 の場合を犠牲にして、計算式を単純化したいという判断ではないかと思います。


$0800: Channel Select

チャンネルを選択するレジスタです。ここに書き込まれた値 (0 〜 5) は、 以後 $0802 〜 $0807 書き込み時に有効になります。

$0801: Main Amplitude Level

メイン音量を設定するレジスタです。全体で 1 個だけ存在するレジスタです。 D0 〜 D3 が右音量、D4 〜 D7 が左音量です。左右それぞれ 0 〜 15 の 16 段階の設定が可能で、15 ($F) が -0[dB]、1 ステップごとに -3[dB] 減衰します。 0 では -3*15 = -45[dB] になります。書き込まれた値は即出力に反映されます。

$0802: Fine Frequency

全 12 ビットの周波数設定レジスタの下位 8 ビットです。 $0800 で指定した CH ごとに設定可能です。設定値を大きくすると周波数は低くなります。 書き込まれた値は即出力に反映されます。

$0803: Coarse Frequency

全 12 ビットの周波数設定レジスタの上位 4 ビットです。 $0800 で指定した CH ごとに設定可能です。設定値を大きくすると周波数は低くなります。 書き込まれた値は即出力に反映されます。

$0804: ON, DDA, AL(CH Amplitude Level)

$0800 で指定した CH ごとに設定可能です。 ON に 1 を書き込むと波形メモリの内容が逐次出力されます。 DDA に 1 を書き込むと DDA モードの出力となり、以後 $0806 に書き込んだ値の下位 5 ビットが出力されます。 ON と DDA の両方に 1 を書き込んだ場合は DDA のビットが優先されます。 AL は 5 ビットの音量設定レジスタで、31 が -0[dB]、1 ステップごとに -1.5[dB] ずつ減衰します(0 では -1.5*31 = -46.5[dB])。左右の両方の音量に適用されます。

$0805: LAL, RAL (Left / Right Amplitude Level)

$0800 で指定した CH ごとに設定可能です。 左右の音量をそれぞれ $0 〜 $F に設定可能です。 設定値 を 1 減少させるごとに -3[dB] 減衰します(0 では -3*15 = -45[dB])。

$0806: Waveform

$0800 で指定した CH ごとに設定可能です。 DDA が 0 の場合は波形メモリにデータを書き込みます(32 サンプル)。 DDA が 1 の場合は直接出力する値を書き込みます。書き込みデータは下位 5 ビットのみ有効です。

$0807: NE, NOISE FRQ (Noise Enable, Noise Frequency)

$0800 で $4 または $5 を指定した場合のみ設定可能です。 NE を 1 に設定すると、ノイズの出力を開始します。 ON, DDA, NE ビットの優先順位は、DDA < NE < ON です。 NOISE FRQ に、出力するノイズの周波数を設定します。設定値は $0 〜 $1F で、設定値を大きくすると周波数は低くなります。

$0808: LFO FRQ (LFO Frequency)

LFO の周波数を設定します。

$0809: LFO TRG, CTRL (LFO Trigger, Control)

TRG を 1 に設定すると、CH0 と CH1 で LFO 機能を実行します。 CTRL の効果は確認中です。

波形メモリ再生方法

波形メモリを再生するまでの手順を以下に示します。

  1. $0801 にメイン音量を設定する ($EE がよく使われる)。
  2. $0800 に発音するチャンネルの番号を書き込む (0 〜 5)。
  3. $0805 に $40 → $00 を書き込んで波形メモリの位相カウンタを 0 に戻す。
  4. $0806 に波形をデータを 32 回書き込んで、波形メモリに波形を登録する。
  5. $0802, $0803 に再生周波数を書き込む。
  6. $0805 にチャンネルの L, R 音量を設定する。
  7. $0804 にチャンネルの音量値と ON = 1 を書き込む。

サンプルコード

PSG にアクセスし、波形を再生するサンプルコードを以下に示します。 使用アセンブラは HuC に含まれている pceas です。

 1 ;------------------------------------------------------------------------------
 2 ;	[PsgTest.asm]
 3 ;------------------------------------------------------------------------------
 4
 5 ; output PsgTest.lst 
 6     .list
 7
 8 ; zero-page memory $2000-$20ff (not used)
 9 ;   .zp
10
11
12 ; scratchpad memory $2200-3fff (not used)
13 ;   .bss
14
15     .code
16     .bank 0
17     .org $E000
18
19 _reset:
20     csh
21     cld
22     ldx #$ff
23     txs
24
25     txa
26     tam #0
27
28     lda #$f8
29     tam #1
30
31     stz $800    ; select ch0
32     stz $807    ; noise=off, noise_frq=0
33     stz $808    ; LFO frequency = 0
34     stz $809    ; LFO=OFF, LFO Ctrl=0
35
36 ; set main volume of L and R 
37     lda #$ee
38     sta $801
39
40 ; clear wavetable index 
41     lda #$40
42     sta $804
43     stz $804
44
45 ; write wavetable (square wave) 
46 ;     0              15              31
47 ; $1f ---------------+
48 ;                    |
49 ; $00                +----------------
50 ;
51     ldx #16
52     lda #$1f
53
54 .sound_loop_l16:
55     sta $806
56     dex
57     bne .sound_loop_l16
58
59     ldx #16
60 .sound_loop_h16:
61     stz $806
62     dex
63     bne .sound_loop_h16
64
65 ; set frequency L 
66     stz $802
67
68 ; set frequency H 
69     lda #1
70     sta $803
71
72 ; set LAL = RAL = 15 (max) 
73     lda #$ff
74     sta $805
75
76 ; ON, AL=31
77     lda #$9f
78     sta $804
79
80 .end:
81     bra .end
82
83
84 ; define interrupt vectors 
85     .org  $FFFE
86     .dw _reset

';' 以後は改行するまでコメントになります。

'.' で始まる記述は、6280 ではなく 6280 のアセンブラに対する指示で、 assembly directives などといいます。アセンブラによっては '.' で始まらない場合もありますが、これらは 6280 に対する命令ではない点に注意してください。

6 行目の ".list" は、アセンブラに対して「リスト (*.lst) ファイルを出力するように」と指示しています。 このアセンブリソースファイル名は PsgTest.asm なので、アセンブラは PsgTest.lst を出力します。 PsgTest.lst ファイルには、PsgTest.asm がどのようにアセンブルされたかが記録されているテキストファイルです。 どのコードがどのアドレスに配置されたか等の情報を得ることができます。

9 行目の ".zp" は、ゼロページ(通常 $2000-$20FF)に変数を配置する場合に記述します。

13 行目の ".bss" は、ワークメモリ(通常 $2200-$3FFF)に変数を配置する場合に記述します。

15 行目の ".code" は、ここから 6280 のアセンブリ命令を記述していく領域であることをアセンブラに教えています。

16 行目の ".bank 0" は、ROM の最初のバンクであることをアセンブラに教えています。 pceas の場合、1 バンクは 8kB ($2000 bytes) です。よって .bank 0 を指定すると、そのバンクに記述された内容は ROM の $0000-$1FFF に配置されます。 .bank 1 を指定すると、その内容は ROM の $2000-$3FFF に配置されます。

17 行目の ".org $E000" は、bank 0 を どの論理アドレスでアクセスするかを指定します。 ここでは $E000 を指定していますので、bank 0 は論理アドレス $E000 から開始することになります。 PCE の場合、これは必要な設定になります。PCE では、bank 0 は必ず論理アドレス $E000 から開始するように指定します。PCE はリセット時 MPR7 が $00 に初期化され、なおかつ $FFFE-$FFFF (物理アドレス $001FFE-$001FFF) にリセットベクタを読みにくるためです (MPR7=$00 で論理アドレス $E000-$FFFF が物理アドレス $000000-$001FFF にマッピングされる)。 詳細はメモリマッピングレジスタを参照してください。

19 行目の "_reset:" は、6280 がリセットされるとジャンプしてくるアドレスです。

20 行目の "csh" で、6280 の動作周波数を 1.79[MHz] から 7.16[MHz] に切り替えています。

21 行目の "cld" は、ADC および SBC 命令が BCD モードで行われないように D フラグをゼロにクリアしています。 D フラグはリセットでゼロにクリアされないため、リセット直後に明示的にクリアします。

22-23 行目でスタックポインタ (S) を $FF に初期化しています。スタックポインタへの値のロードは X レジスタを介してしか行えませんので、まず X レジスタに $FF をロードし、TXS 命令で S に $FF をロードしています。

25-26 行目で MPR0 に $FF をロードしています。これで $0000-$1FFF に I/O が配置されます。これで $0800-$0809 から PSG にアクセスできます。 $0000-$1FFF に I/O を配置しないと ST0, ST1, ST2 命令が使えませんので、通常は $0000-$1FFF に I/O を配置します。

28-29 行目で MPR1 に $F8 をロードしています。これで $2000-$3FFF に ワーク RAM (8kB) が配置されます。ゼロページやスタックにアクセスする命令は 論理アドレス $2000-$21FF にアクセスするため、通常は $2000-$3FFF に RAM を配置します。

31-34 行目で PSG の CH0 を選択し、今回使用しないノイズおよび LFO を OFF に設定しています。CH0 はもともとノイズを使用できないのでノイズの設定は不要ですが、 CH0 ではなく CH4 または CH5 を選択したときのために処理を残しています。

37-38 行目で LMAL と RMAL をそれぞれ $E に設定しています。

41-43 行目で波形メモリの位相カウンタをゼロに戻しています。 これによって、波形メモリへの書き込みを先頭から開始することができます。

51-57 行目で波形メモリへ $1f を 16 サンプル書き込んでいます。 X レジスタに 16 をロードしてループカウンタにしています。 A レジスタに $1F をロードして $0806 に書き込みを行っています。

59-63 行目で波形メモリへ $00 を 16 サンプル書き込んでいます。 これで波形メモリへ全 32 サンプルを書き込めた形になります。

66-70 行目で周波数を設定しています。$100 を設定しているので、出力波形の周波数は、 Fout = 3579545 / 32 / (((256-1) & 0xFFF) + 1) ~= 437 [Hz] となります。

73-74 行目で CH0 の LAL と RAL をそれぞれ $F に設定しています。

77-78 行目で ON ビットを 1 に設定し、AL を $1F に設定しています。 今回は DDA モードでの出力を行わないので、DDA ビットは 0 に設定しています。 ここで、CH0 から矩形波が出力されます。

81 行目では、 CH0 の出力に必要な処理が全て終わったので無限ループしています。 PCE がリセットされるまでここでループします。'.' で始まるラベルは、 pceas の場合はローカルラベルとして認識されます。使用範囲は、'.' で始まらないラベルから、次の '.' で始まらないラベルまでです。単に ".loop" などという、どこにでも使われそうなラベル名を複数の箇所で使えるようにするための配慮です。

85-86 行目で $FFFE-$FFFF (つまり bank 0 の最後の 2 バイト) にリセットアドレスを設定しています。17 行目で bank 0 は $E000 から開始するように指定しているため、bank 0 の末尾は $FFFF になります。 6280 はリセット時 bank 0 の $FFFE-$FFFF にリセットベクタを読みにくるため、 ここにリセットアドレスを設定する必要があります。

目次へ戻る


(C) Ki 2010