かずの不定期便ブログ

備忘録代わりに書きます

FPGAでOPMを鳴らしてみた(シミュレーション編)

目次

FPGAでOPMを鳴らしてみた

 GWの成果として、(若干難はあるものの)FPGAでOPMの互換IPを動かすことが出来ました。jt51を使わさせていただきました。
 jt51のサウンド出力インターフェースはYM2151と異なっており、出力データはパラレル信号になっています。sample信号が"H"のタイミングで取り込めばよいだけです。

OPMドライバと制御用CPUの調達

 OPMドライバの自作は無理なので。。。既にあるものを流用する事にします。68000コアも載せることとしhootのx68k標準ドライバのバイナリそのままを使う事にしました。(当初はMDXドライバを使う事を考えてましたが、X68Kの実行バイナリである.Xのヘッダが理解できなかったので諦めました)
 フリーの68000コアは,(割込みの実装に難があるものの)TG68を使う事にしました。(当初はfx68kで動かそうとしたのですがvivadoのシミュレーターがelab処理で止まってしまうので諦めました)
 hootは以下のページに「hootのx68kの標準ドライバについて」でメモリマップと共に解析結果の記載がありました。解析結果を公開してくださった方に感謝です。非常に分かりやすく記載されており、このページから68000コアのブート手順も把握しました。このページが無かったらおそらく鳴らせなかったでしょう。
"Darkside X68000 (japanese version only)"

論理シミュレーションの方針

 実機でいきなり動かすのはハードルが高すぎるので、まずは論理シミュレーションを行います。以下のモデルが必要です。実際にFPGAに焼いて動作させる場合には、後いくつかの仕掛けが必要です。

  1. 68Kコア(TG68)
  2. YM2151(jt51)
  3. 割込みコントローラ
  4. 68K→YM2151バスブリッジ回路
  5. RAMモデル
  6. テストドライバ

 クロックはテストドライバで作る単一クロックで、TG68、JT51を動かします。JT51は音声出力時にsample信号を駆動しますが、実機の場合にはその先にDACが居て、リアルタイムで音声化されるので正しい周波数(X68K用ドライバなので4MHz)で動作させる必要がありますが、シミュレーションだけであれば、出力データをwaveファイル化してPCで再生させればいいので、本ファイルを62.5KHzの周波数のデータとしてヘッダを作りこめばきれいに再生できます。62.5KHzな理由は、4MHz/2/32で計算できます。動かすと分かりますが、4MHzを2分周したP1クロックに同期して、32クロック(P1で)に1回sample信号が出てきます。


68Kコアのバスインターフェース

以下のデータシートを参照しました。

複雑の様に思えます。クロックの両エッジを使ったインターフェースです。またスレーブがDTACK=0レベルにしないとバスにwaitがかかるようです。4,5のモデルを作成する際はDTACK生成論理も必要です。

JT51のバスインターフェース

「YM2151 データシート」でググると10ページ程度のデータシートが手に入ります。簡単な非同期バスです。バス本数を減らすためか、まずA0="0"としてアドレスをD7-D0端子を使ってライト後、A0="1"でデータを同端子を使ってライトするという2回アクセス方式です。inside X68000を見ると本バスアクセス方式がそのままX68000のOPMのポートアドレスにアサインされてます。(アドレスライト用に0xe90001番地、データライト用に0xe90003番地が割り当てられています。コアのアドレス線bit1がOPMのA0に入力されているのが分かります。また奇数なのはコアのデータバスD7-D0がOPMにそのまま入力されていることも分かります。ビッグエンディアンですねー。AXIバスはリトルエンディアンなので注意です。はまりました。)

割込みコントローラ

 68KコアのIPL2/1/0n端子を駆動します。タイミング図は先ほどの68Kコアのデータシートのfiguire5-11 Interrupt Acknowledge Cycle Timing Diagramに記載されています。動作理解の助けとしてInside X68000の割り込み章も読むとよいです。IPL2/1/0nは負極性なので3本とも"1"の状態で割込み無しになります。X68Kサウンドドライバを使うのでYM2151の割り込みを模倣しないといけません。Inside X68000によるとYM2151割込みはMFPを通してCPUに通知され、割込みレベルは6(IPL2n=0,IPL1n=0, IPL0n=1)になります(負極性なので数値6を反転)。また割込みベクター番号GPIP3がFM音源に割り当てられているので0x43だと思いますが本割込みテーブルはhootでは未使用でオートベクタのレベル6を使ってるようです。
 TG68ではオートベクタしか実装されていないようです。またFCx端子もありません。どうやって割り込み処理のAcknowledgeを検出すればいいのか分かりません(データシートによるとFC2-0=111となる)。これが検出できないと、どのタイミングで割込みをdisable(IPL2/0/1n=111)にすればいいのかも分かりません。ここはTG68を動かしてから考えます。
 動かしてみた結果、TG68のアドレス線はなぜか32本あるのですが、割り込み処理のAcknowledge時、上位ビットが"1"になったので、正しい処置かどうかは置いておいて、とりあえず動けばいいや精神でA[31:29]の3本をFCx端子の代わりとしました。A[3:1]はAcknowledgeタイミングでレベル6を返してきてます(こっちは正極性なのか値は6です)。なので、A[31:29]==3'b111,A[3:1]=3'b110でリードアクセスが来たら割込みのAcknowledgeと認識してIPL2/0/1n=111とする回路を割込みコントローラとして作成しました。
 しかし本対処だけでは動きません。その時の波形を載せます

f:id:spend-carefree:20200515231122p:plain
割込み発生でアドレスが不定になる
 ついでに割込み発生の様子も波形で分かるようにしておきましたが、CPUが割込みに対するACKを返した後、スタックへ現アドレスを積んでる様子が分かります。これから割込みルーチンへブランチするためのPUSH動作だと思われます。その後、アドレスが"0000007X"と不定値を出して、シミュレーションが停止します。CPUとしてはこの時リード動作ですが、アドレスが不定値の為、入力データであるdata_in[15:0]も不定値になります。 このアドレスですがバンドルを展開してみるとbit1,bit0の2bitのみが不定であることが分かります。
 TG68のソースを追いかけるとtrap_vector(7 downto 2)信号を作成している箇所が不定の原因であると分かります(下位2bitの設定がない)。なので下位2bitに"00"を追加し、
trap_vector(7 downto 0) <= "011"&rIPL_nr&"00";
と修正して対処しました。が、おそらく割込みベクタテーブルが配置されるアドレスは32bitデータが格納されているという事を前提に32bitバス幅のRAMを置くことが正しいのかもしれません。
 修正した後の波形を載せます。
f:id:spend-carefree:20200515235541p:plain
TG68修正後
 割込みレベルを"0"に戻すタイミングが分かるようにしました。0x7a番地のリードタイミングで割込みレベルを戻してます。

68K→YM2151バスブリッジ回路

 68Kコアとjt51を同クロックで確認する際はjt51へアクセスが来たらすぐに返答するDTACKの作成とjt51側のCSを作成するためのアドレスデコーダ以外は直結で問題ないです(アドレス系は1cyc遅延させました)。実機で動かす場合、68Kコアのクロックはjt51より早くするので、非同期乗せ換え及びDTACKによりwait制御が必要なので結構制御が面倒です。シミュレーション時(つまり同クロックで動作)の回路は以下の様な感じに作成すれば動作します。verilog風に書くと

wire ym2151_area = (addr[23:1]==(24'he9_0001>>1)) |
                   (addr[23:1]==(24'he9_0003>>1)) ;
// ym2151 CSX 作成
reg ym2151_csx;
always@(posedge CLK68K or  posedge RST)
    if(RST)
        ym2151_csx <= 1'b1;
    else
        ym2151_csx <= ~ ( (( ~as) & ym2151_area);
reg ym2151_a0;
always@(posedge CLK68K)
    ym2151_a0 <= addr[1];

// ym2151 DTACKX 作成 nowait
always@(posedge CLK68K or  posedge RST)
    if(RST)
        YM2151_DTACKn <= 1'b1;
    else if((~as) & ym2151_area & YM2151_DTACKn )
        YM2151_DTACKn <= #1 1'b0;
    else if((~YM2151_DTACKn) & lds & uds)
        YM2151_DTACKn <= #1 1'b1;
jt51 TG68 or 論理
cs_n ym2151_csx & (~((~as) & ym2151_area))
a0 ym2151_a0
wr_n rw ? 1'b1 : lds
d_in data_out[7:0]

RAMモデル

 本モデルは68Kコアのプログラム格納用(音源ドライバ)、データ格納用(音源データ)、hootドライバと音源ドライバの通信用のメモリです。FPGAに実際に焼く際はこのメモリはSDRAMとすることとしAXIバス(68Kバスとのブリッジ回路を介します)でアクセスします。最初はビヘイビアモデルで簡易化します。RAMモデルのDTACKは、先ほどのjt51同様にno waitで作成します。

wire RAM_AREA0 = (addr[23:21]==3'b000); // 0x00_0000 - 0x1f_ffff  2MB SDRAM
wire RAM_AREA1 = (addr[23:16]==8'hf0); // 0x00f0_0000 ~0x00f0_ffff    SP領域
wire RAM_AREA2 = (addr[23:8]==16'he000); //  0x00e0_0000 ~0x00e0_00ff    hoot work ベンチからライトする
wire RAM_AREA = RAM_AREA0 |RAM_AREA1 |RAM_AREA2 ;
wire RAM_ASX = ~(RAM_AREA & (~as));
reg WR_DTACKX , RD_DTACKX ;
always@(posedge clk or posedge rst)
    if(rst)
        WR_DTACKX <= 1'b1;
    else  if(RAM_ASX )
        WR_DTACKX <= #1 1'b1;
    else if(~rw )
        WR_DTACKX <= #1 1'b0;
always@(posedge clk or posedge rst)
    if(rst)
        RD_DTACKX <= 1'b1;
    else  if(~RAM_ASX  & rw )
        RD_DTACKX <= #1 1'b0;
    else
        RD_DTACKX <= #1 1'b1;

wire RAM_DTACKX = RD_DTACKX & WR_DTACKX;

68KコアへのDTACKX入力

 jt51, RAM用のDTACKXを作成しましたので、各DTACKXを単に論理積&して渡せばよいです。
 注意点として68KコアのバスはDTACKが返ってこないと停止します。上記でDTACKはアドレスをデコードして作成しています。想定外のアドレスが来るとDTACKを生成できないのでハングアップします。

テストドライバ

 メインスレッドではシミュレーション開始時に$readmemhでRAMモデルに作ったメモリイメージ(自分の場合はRAMモデルに、reg [7:0] MEM [0:サイズ分];を定義しています)に音源ドライバ、音楽データを書き込みます。音楽データはADPCMを使ってないデータであってもpcmデータを書き込んであげる必要があるようです。hootのxmlに定義されたファイルをテキスト化したものを$readmemhタスクを使ってoffsetアドレスに従って書き込みます。更に曲番号とplay開始を指示する0xe00001, 0xe00000番地のデータを0初期化します。シミュレーションのある時刻(後述)で0xe00001に曲番号を書き込み、直後に0xe00000番地へ0x01(play)を書き込みます。
 別のinitial begin end スレッドでRAMへのアクセスモニタ(時刻、read,write アドレス、データが分かるもの)を仕込んだ方が良いです。最初は少しだけシミュレーションを動かし、本RAMへのアクセスでhootのwork領域の読み出しを開始する時刻を確認し、そのトランザクションの少し後で、0xe00000番地への0x01書き込みを行うようにメインスレッド側で行います。
 また、更に別のスレッドでjt51の出力sample信号に従ってjt51.xleft, jt51.xright をfileに書き込む仕組みを入れます。2sec分だけ動作して、終了するようにしています。本端子の取り込みはsigned 16bitになります。他にunsigned端子やYM2151のビット深度に合わせた端子があります。wave形式への変換が簡単なsigned 16bitを選択しています。

initial begin
    sample_cnt=0;
    wave_fpt = $fopen ("ym2151_out.txt");
    while(1) begin
        @(posedge jt51.p1);
        if(d_sample) begin
             $fwrite(wave_fpt,"%d,%d\n",jt51.xleft, jt51.xright);
             sample_cnt++;
             if(sample_cnt == 62500*2) break;  // 2sec
        end
   end
   $fclose(wave_fpt);
   $finish;
end

動作確認

 本シミュレーションで生成されたサウンドのテキストファイルにヘッダを付けてwaveファイル化します。waveファイルのヘッダ情報は、「62500Hz 250000Bps 16bit 2ch」になります。本ファイルをダブルクリックすれば2secだけ音が聞こえます。
 2sec程度だと、一瞬すぎますが、明らかな異常な音はおそらくすぐに分かります。自分の場合は1回目で正常っぽい音がしました。まぁ、シミュレーション波形を見てjt51から正常っぽい波形が出ているのを確認していたので、おそらく大丈夫だろうと踏んではいましたが。
 いきなり2sec分を流さないで、少なくともjt51からsample信号が出力され、音声データが不定→中身のある値に変化、そしてjt51の割込み発生、クリアされる動作が数回程度正しく行えるのを確認してから長時間シミュレーションを行います。

次回

実機動作です。

以上です。

前回は
spend-carefree.hatenablog.com