AVRでファミコン風音源YMZ294を使う

AVRでファミコン風音源YMZ294を使う

YMZ294とは

YMZ294は,ヤマハが提供する(orしていた?)SSG(Software-controlled Sound Generator)音源IC.PSG音源と言われるファミコンやMSXなどのレトロゲーム/PCに搭載されていた音源と同系列の音が出る.いわゆるピコピコシンセサイザー的な発音である.

入手

秋月で,クリスタル込み500円で入手できる.
ヤマハ音源IC(YMZ294)

周辺回路

周辺に用意すべき回路は,クリスタルを除けば基本的にはない.非常に単純な回路構成で使用することができる.ただし,出力にはノイズが乗りやすいので,フィルタを噛ませて出力するとよい.
今回は,AVRマイコンATmega328Pを使って,このICを制御してみる.

ライブラリ公開

検索エンジン等からたどり着いてライブラリだけ欲しい方はこちらからどうぞ.

制御方法

YMZ294は,レジスタに値を書きこむことで音の周波数や音量,エンベロープなどを設定できるようになっている.レジスタに値を書き込むには,WR(書き込み許可),CS(チップセレクト),A0(アドレス指定)の3つの設定ピンのHIGH/LOWを切り替えて,D0〜7に値をセットする必要がある.
以下に手順を示す.表は,上から順に時系列順になっている.

レジスタの値をセットする

  • 書き込み先アドレスのセット
    1. WR,CS,A0がすべてLOWの状態から始まる
    2. D0〜7に書きこむレジスタのアドレスをセットする
    3. WRとCSをHIGHにして,アドレスのセットを完了する
フェーズ WR CS A0
初期化 L L L
D0〜7にデータセット L L L
アドレスセット完了 H H H
  • 書き込みデータのセット
    1. 書きこみ先アドレスをセットしたあとの状態から始まる
    2. WRとCSをLOWに,A0をHIGHにする
    3. D0〜7に書きこむデータをセットする
    4. WR,CS,A0をHIGHにして,データのセットを完了する
    5. WR,CS,A0をすべてLOWにする
フェーズ WR CS A0
初期状態 H H H
データ書き込み用に設定 L L H
D0〜7にデータセット L L H
アドレスセット完了 H H H
初期化 L L L

各レジスタの説明

レジスタ番号 説明文
00 チャンネルAの周波数情報下位8bit
01 チャンネルAの周波数情報上位4bit
02 チャンネルBの周波数情報下位8bit
03 チャンネルBの周波数情報上位4bit
04 チャンネルCの周波数情報下位8bit
05 チャンネルCの周波数情報上位4bit
06 ノイズ音の周波数情報(5bit)
07 ミキサーの設定情報(6bit)
08 チャンネルAの音量コントロール情報(5bit)
09 チャンネルAの音量コントロール情報(5bit)
0A チャンネルAの音量コントロール情報(5bit)
0B エンベロープの周波数情報下位8bit
0C エンベロープの周波数情報上位8bit
0D エンベロープ形状(4bit)

00〜05 楽音の周波数情報
レジスタ00から05には,楽音の周波数情報を格納する.

レジスタ番号 D7 D6 D5 D4 D3 D2 D1 D0
00 or 02 or 04 TP7 TP6 TP5 TP4 TP3 TP2 TP1 TP0
01 or 03 or 05 TP11 TP10 TP9 TP8

TP0〜TP11は12bitの整数TPの各ビットを表す.このとき,発生する矩形波の周波数は,

ft=1/(8TP)

06 ノイズ音の周波数情報
ノイズの周波数は5bitの整数で表す.

レジスタ番号 D7 D6 D5 D4 D3 D2 D1 D0
06 NP4 NP3 NP2 NP1 NP0

このとき発生するノイズの周波数は,

fn=1/(8NP)

07 ミキサーの設定情報

レジスタ番号 D7 D6 D5 D4 D3 D2 D1 D0
07 NC NB NA TC TB TA

1文字目のNはノイズ,Tは楽音,2文字目はチャンネルを表す.ビットが0の時,該当する音声を出力.

08〜0A 音量コントロール情報

レジスタ番号 D7 D6 D5 D4 D3 D2 D1 D0
08 or 09 or 0A M L3 L2 L1 L0

各チャンネルの音量を設定する.Mが0のとき,L0〜4が表す整数のレベルに設定される.Mが1のときは,エンベロープによって生成された音量に設定される.
なお,音量は対数スケールで設定されるため,自然な減衰音を表現することができる.

0B〜0C エンベロープの周波数情報

レジスタ番号 D7 D6 D5 D4 D3 D2 D1 D0
0B EP7 EP6 EP5 EP4 EP3 EP2 EP1 EP0
0C EP15 EP14 EP13 EP12 EP11 EP10 EP9 EP8

エンベロープの繰り返し周波数を設定する.

fe=1/(128EP)

ただし,実際にエンベロープを生成する過程で使う周波数はこれとは異なる.

0D エンベロープの形状

レジスタ番号 D7 D6 D5 D4 D3 D2 D1 D0
0D CONT ATT ALT HOLD

CONT,ATT,ALT,HOLDの組み合わせによってエンベロープの形状を設定する.
CONTは繰り返しの有無,ATTは立ち上がり(1)と立ち下がり(0),ALTは立ち上がりと立ち下がりを交互にするかどうか,HOLDは1周期分終わったあとに波形を固定するかどうか,をそれぞれ定める.

結局音を流すにはどうすればよいか

  1. ミキサーの設定を行い,発音を有効にする
  2. 音量の設定を行い,音が聞こえるようにする
  3. 各種周波数の設定を行い,音のデータを設定する

→これで音が流れる

AVR向けライブラリの作成

これをもとに,AVR用のライブラリを作成する.その前に,AVRのどのピンをYMZ294のどのピンに割り当てるかを考える.ここでは,次の回路図のようにした.

PORTBのPB0〜PB7をD0〜D7にそれぞれ割り当て,PORTCのPC0をリセットピン(IC)に,PC1をWRに,PC2をCSに,PC3をA0に割り当てた.合計で12本のピンを占有することになる.
リセットピンをLOWにすることでYMZ294をソフトリセットすることができる.AVRが使用している間は常にHIGHに設定しなければならない.また,今回は4MHzのクリスタルを使用しているため,4/6ピンは+5Vにつないでおく.6MHzのクリスタルを使用する場合はGNDにつなげばよい.SOはサウンドの出力ピンである.

以上の仕様を考慮して実装を行った.

ライブラリの仕様

ポートの初期化

void ymz_init(void){
    D_DDR=0b11111111;
    SYS_DDR=0b00001111;
    _delay_ms(100);
    SYS_PORT|=(1<<YRST);
    _delay_ms(200);
}

YMZ294を利用するためのポートを初期化する.

YMZ294のリセット

void ymz_reset(void){
    SYS_PORT&=~(1<<YRST);
    _delay_ms(10);
    SYS_PORT|=(1<<YRST);
}

YMZ294をソフトリセットする.

レジスタへの値の設定

void ymz_setRegister(char addr,char val){
    SYS_PORT&=~((1<<YADR)|(1<<YCS)|(1<<YWR));
    D_PORT=addr;
    SYS_PORT|=(1<<YCS)|(1<<YWR);
    SYS_PORT=(SYS_PORT&~((1<<YCS)|(1<<YWR)))|(1<<YADR);
    D_PORT=val;
    SYS_PORT|=(1<<YADR)|(1<<YCS)|(1<<YWR);
    SYS_PORT&=~((1<<YADR)|(1<<YCS)|(1<<YWR));
}

レジスタaddrにvalを書き込む.応答時間は非常に短いため,先述の順序通りにそのままコードを書けば良い.

楽音周波数の設定

void ymz_setFreq(char channel,int freq){
    if(channel>2){ return; }
    ymz_setRegister(channel*2,freq&0b11111111);
    ymz_setRegister(channel*2+1,((freq&0b111100000000)>>8));
}

指定したチャンネルの周波数レジスタに値を書き込む.これを利用して,実際の周波数を書き込む関数を作成する.

void ymz_setRealFreq(char channel,int freq){
    freq=125000/freq;
    ymz_setFreq(channel,freq);
}

ymz_setRealFreq(CH_A,440) などとすれば,実際にその周波数の音声が出力される.

休符の挿入

void ymz_setPause(char channel){
    ymz_setFreq(channel,0);
}

周波数0の音を設定すれば休符となる.

MML風フォーマットからの周波数の取得

const int noteFreq[] = {
0,9,9,10,10,11,12,12,13,14,15,15,16,17,18,19,21,22,23,25,26,28,29,31,33,35,37,39,41,44,46,49,52,
55,58,62,65,69,73,78,82,87,93,98,104,110,117,124,131,139,147,156,165,175,185,196,208,220,233,247,
262,277,294,311,330,349,370,392,415,440,466,494,523,554,587,622,659,699,740,784,831,880,932,988,
1047,1109,1175,1245,1319,1397,1480,1568,1661,1760,1865,1976,2093,2218,2349,2489,2637,2794,2960,
3136,3322,3520,3729,3951,4186,4435,4699,4978,5274,5587,5920,6272,6645,7040,7459,7902,8372,8870,
9397,9956,10548,11175,11840,12544
};

この周波数テーブルを利用する.

int ymz_calcFreqByMML(char octave,char note,char sharp){
    int pos=0;
    switch(note){
        case 'C':
        case 'c':
            break;
        case 'D':
        case 'd':
            pos=2;
            break;
        case 'E':
        case 'e':
            pos=4;
            break;
        case 'F':
        case 'f':
            pos=5;
            break;
        case 'G':
        case 'g':
            pos=7;
            break;
        case 'A':
        case 'a':
            pos=9;
            break;
        case 'B':
        case 'b':
            pos=11;
            break;
        default:
            break;
    }
    return noteFreq[octave*12+pos+(sharp&1)];
}

MML形式「D#6」などをばらして ymz_calcFreqByMML(6,’D’,1) と書けば良い.

ノイズの周波数設定

void ymz_setNoiseFreq(char freq){
    ymz_setRegister(0x06,freq&0b11111);
}
void ymz_setNoiseRealFreq(int freq){
    freq=125000/freq;
    ymz_setNoiseFreq(freq);
}

先述の内容そのまんま.freqに周波数(情報)を設定する.

ミキサーの設定

void ymz_setMixer(char noisemsk,char tonemsk){
    if(noisemsk>0b111||tonemsk>0b111){ return; }
    ymz_setRegister(0x07,(noisemsk<<3)|tonemsk);
}

noisemskでノイズを出力するチャンネルの設定,tonemskで楽音を出力するチャンネルの設定を行う.
noisemsk,tonemskで0にセットされているビットに対応するチャンネルから音が出る.

音量の設定

void ymz_setVol(char env,char channel,char level){
    ymz_setRegister(0x08+channel,(env<<4)|(level&0b1111));
}

env=0のとき音量はlevelになり,env=1のとき音量はエンベロープから出力された値に従う.

エンベロープの周波数設定

void ymz_setEnvFreq(int freq){
    ymz_setRegister(0x0b,freq&0b11111111);
    ymz_setRegister(0x0c,(freq&0b1111111100000000)>>8);
}
void ymz_setEnvRealFreq(int freq){
    freq=125000/freq;
    ymz_setEnvFreq(freq);
}

そのまんま.freqに周波数(情報)を設定する.

エンベロープの形状設定

void ymz_setEnvShape(char cont,char att,char alt,char hold){
    ymz_setRegister(0x0D,(cont<<3)|(att<<2)|(alt<<1)|hold);
}

エンベロープの形状を設定する.詳しくはデータシートなどの参考資料を参照のこと.

サンプルソース

/*
 * main.c
 *
 * Created: 2012/01/13 3:51:20
 *  Author: Tokoro
 */

#include "libYMZ294.h"

void pauseAll(void);

int main(void)
{
    // 初期化
    ymz_init();

    // ミキサー,ボリュームの設定
    ymz_setMixer(0b111,0b000);
    ymz_setVol(0,CH_A,200);
    ymz_setVol(0,CH_B,200);
    ymz_setVol(0,CH_C,200);

    //ループ
    while(1)
    {
        /*
         * chA: E6
         * chB: F#5
         * chC: D4
         */
        ymz_setRealFreq(CH_A,ymz_calcFreqByMML(6,'E',0));
        ymz_setRealFreq(CH_B,ymz_calcFreqByMML(5,'F',1));
        ymz_setRealFreq(CH_C,ymz_calcFreqByMML(4,'D',0));
        _delay_ms(50);
        pauseAll();
        _delay_ms(50);

        /*
         * chA: E6
         * chB: F#5
         * chC: D4
         */
        ymz_setRealFreq(CH_A,ymz_calcFreqByMML(6,'E',0));
        ymz_setRealFreq(CH_B,ymz_calcFreqByMML(5,'F',1));
        ymz_setRealFreq(CH_C,ymz_calcFreqByMML(4,'D',0));
        _delay_ms(50);
        pauseAll();
        _delay_ms(150);

        /*
         * chA: E6
         * chB: F#5
         * chC: D4
         */
        ymz_setRealFreq(CH_A,ymz_calcFreqByMML(6,'E',0));
        ymz_setRealFreq(CH_B,ymz_calcFreqByMML(5,'F',1));
        ymz_setRealFreq(CH_C,ymz_calcFreqByMML(4,'D',0));
        _delay_ms(50);
        pauseAll();
        _delay_ms(150);

        /*
         * chA: C6
         * chB: F#5
         * chC: D4
         */
        ymz_setRealFreq(CH_A,ymz_calcFreqByMML(6,'C',0));
        ymz_setRealFreq(CH_B,ymz_calcFreqByMML(5,'F',1));
        ymz_setRealFreq(CH_C,ymz_calcFreqByMML(4,'D',0));
        _delay_ms(50);
        pauseAll();
        _delay_ms(50);

        /*
         * chA: E6
         * chB: F#5
         * chC: D4
         */
        ymz_setRealFreq(CH_A,ymz_calcFreqByMML(6,'E',0));
        ymz_setRealFreq(CH_B,ymz_calcFreqByMML(5,'F',1));
        ymz_setRealFreq(CH_C,ymz_calcFreqByMML(4,'D',0));
        _delay_ms(50);
        pauseAll();
        _delay_ms(150);

        /*
         * chA: G6
         * chB: B5
         * chC: G4
         */
        ymz_setRealFreq(CH_A,ymz_calcFreqByMML(6,'G',0));
        ymz_setRealFreq(CH_B,ymz_calcFreqByMML(5,'B',0));
        ymz_setRealFreq(CH_C,ymz_calcFreqByMML(4,'G',0));
        _delay_ms(100);
        pauseAll();
        _delay_ms(300);

        /*
         * chA: G5
         * chB: B5
         * chC: G4
         */
        ymz_setRealFreq(CH_A,ymz_calcFreqByMML(5,'G',0));
        ymz_setRealFreq(CH_B,ymz_calcFreqByMML(5,'B',0));
        ymz_setRealFreq(CH_C,ymz_calcFreqByMML(4,'G',0));
        _delay_ms(100);
        pauseAll();
        _delay_ms(300);
    }
}

void pauseAll(void)
{
        ymz_setPause(CH_A);
        ymz_setPause(CH_B);
        ymz_setPause(CH_C);
}

みなさんご存知のあの曲の冒頭部分っぽいものがループされるサンプルです.

参考にしたサイトなど