CPU実験室

誰も見向きもしない古いCPUをいじって動かしてみようというプロジェクトです

VGMデータ再生(2)

大域変数cntはいつどこで参照しても起動時からの経過時間(0.72ms単位)を保持していることになります。32ビットがオーバーフローするのには3.2×10^6秒=約36日もかかるので十分な長さです。

さてVGMフォーマットのパーサ処理ですが「Arduino で YAMAHA YM2151 を VGM ファイルで演奏させる」という記事があったので参考にさせてもらいました。

 

まず音符や休符の長さ、テンポを生成するための待ち時間処理pause()は計時開始時刻の退避と実際に待つカウント数を算出します。この処理自身でのタイムラグがなるべく少なくなるようにタイムベースへの換算はシフト処理にしていますがこれが44.1kHzの32分の1にした理由です。

f:id:O3I:20210114161254j:plain

Arduinoではmicros()というμs分解能のタイマ値を返す組込み関数が使えるようですが、このV50ボードでは時間軸のレゾリューションが生データの32分の1になってしまうのでギグシャクした演奏になるかもしれません。少なくともPCM音声の再生はまるでダメでしょう。

次はVGMのパーサルーチン本体vgmplay()です。この処理は常時ループさせておきイベントドリブン的にpauseにより設定された時刻になるとVGMデータからシーケンシャルにコマンドを取り出し、その内容により音源ICにデータを書いたり次のイベント時刻を設定したりします。なにもVGMが規定するすべてのコマンドに対応する必要はなく、YM2151設定0x54、待ち時間系0x61,x062,0x63,0x7X、終了コマンド0x66だけあれば十分です。

 

f:id:O3I:20210114161309j:plain

 

VGMデータ再生

今年最初のCPUボード動かし初め、何をやろうかと考えていたのですが新年なので歌舞音曲の類が良かろうということでFM音源チップが載っているV50ボードを奥から引っ張り出してきました。

f:id:O3I:20210112221649j:plain

すでにYM2151のレジスタ直叩きで波形が出ること、簡単なMML文字列から楽音が鳴らせることは確認していますがまともに曲を再生するまでには至っていません。当初はMIDIから制御することを考えていたのですが単にデモ的に自動演奏するならばVGM(Video Game Music) データを再生することが手っ取り早いことがわかりました。

VGMフォーマットというのは楽音を鳴らしている音源ICへのコマンドをICの直前でログをとったようなもので、付加された時間情報に従って順番にICにコマンドを書き込めばそのまま楽曲が再生されるというもので、処理は簡単そうです。

 

 まず楽曲の進行の元になるクロックを決定します。VGMの基準クロックはBPMではなく44.1kHz固定となっています。これはVGMが音源ICと一緒にD/Aコンバータも制御してPCM音も再生できるためそのサンプリングクロックに合わせているようです。

システム全体で参照できるカウンタを44.1kHzでインクリメントすればよいのですが、ハードのカウンタは16ビット幅しかないのでタイムアウト割込みで32ビット幅変数cntをカウントアップさせています。ただ割込みのオーバーヘッドがあるのでさすがに44.1kHzの割込みは無理なので、その32分の1、1.378kHzでカウントアップさせました。

 

f:id:O3I:20210112204754j:plain

 

仕事納め

Am29030ボードの動作がほぼOKになったので改めて記念撮影です。

 

f:id:O3I:20201226104521j:plain

 

これは設計開始時の3Dイメージ。あたりまえですが寸分違いありません。ただこのときは資料も少なく暗中模索で、ほんとに動くものができるかかなり不安でした。

f:id:O3I:20201226114036j:plain

 

基板裏面。ほとんど改修痕はありません

f:id:O3I:20201226104546j:plain

今回、部品数、配線量、配線密度も大きいのに設計ミスがほとんどなかったことはちょっとがんばりました。

今後もし直すとすれば・・ですが

1.CPUクロック

RAMの読み書き不良はクロックの速さではなくハンダ不良だったようです。いったん14MHzまで落としてしまったクロックを元の20MHz、さらにCPUmaxの33MHzまで上げてみたいところですが今度はほんとにメモリ、周辺が追いつくかというのと表面実装の水晶が外し難いので躊躇してます

2.グルーロジック

アドレスデコーダだけGAL2個に押し込みましたがここはもう少し規模の大きいCPLDにしておき、クロック、リセット等のタイミング信号も引き込んでおけばCPUクロックを上げてもREADY信号の生成でうまいこといったかもしれません。それに並行して進んだプロジェクトでアルテラのCPLDEPM7064の書き込みもできるようになっていたのでこれを採用していれば基板上にPLCC44ピンデバイスが揃って見た目も良かったかと

3.フットプリント

8ピンSOPは完全にやらかしましたが、SRAMの32ピンSOPももう少しパッドがあったほうが良かったようです。いずれにしても現物合わせが大事です。

4.ROMソケットをベージュにすればよかった ←まだ云ってる

 

この後、ボード上で確認したいのはLCDのインターフェースと、GCC-a29kC言語での開発ができるようにしたいところです。

去年の今頃はNS32032ボードがうまく動作しないとオロオロしていたのを思い出すとあっという間でした。今年はコロナ禍で社会がえらいことになって、趣味レベルでも個人輸入がズタボロだったりしましたが、来年は良い年になりますように・・

 

モニタ完成

モニタコマンド分岐先の処理の実体をコーディング完了しました。

L(Load)、E(Exec)、D(Dump)とも引数無し、SRAMが存在する0xFFE00000番地が暗黙で指定されます。DumpコマンドなどはLoadコマンドが正常に動作しているかを確認するだけのデバッグ用ルーチンに過ぎず、開始番地もサイズも固定ですが用は足りています。

f:id:O3I:20201222123014j:plain

ここで最初にやったメモリチェックプログラムを再度実行させてみます。スタート番地を0から0xFFE00000にリロケートさせるのと、プログラム自身がSRAM上にあるので先頭番地からは当然チェックできません。(自分自身を上書きして壊してしまうので。)

チェック領域を0xFFE10000~0xFFFFFFFFとして繰返しやってもエラー無し。最初データバスが浮いている状態で完走してしまうほうがおかしかったのですが。

あとこのチェックではアドレスの重複はわからない(変なアドレスに書いて読んでもOKになる)ので別のチェックプログラムを作成し0xFFE10000にデータ0xE10xFFE200000xE2・・・・0xFFFF00000xFFと予めユニークな値を書いておいてからまとめてベリファイすることで、「そのアドレスが存在している」確認も行い、OKとなりました。

これでSRAMが1Mbitのニセモノでなくちゃんと4Mbitあることがわかりほっとしました。

 

L(Load)、E(Exec)コマンドさえあれば、統合環境cmpldrvへの追加もできるのでこれで一気にデバッグ作業も楽になります。

f:id:O3I:20201222123034j:plain

 

モニタコーディング中

慣れないAm29kアセンブラでプログラムを書いていますがかなり苦戦しています。アドレッシングモードの制限やスタックの扱いなど本来は高級言語コンパイラがサポートする部分なのでしょうが手作業で試行錯誤している状態です。

コーディングしながら気づいた点ですが

1.オペランドの並びが変

アセンブラ言語のソースとディストネーションの並び順は86系と68系では逆だったりしますが、普通は1つのCPUの命令セットの中では統一されています。ところがこのAm29kプロセッサではインストラクションによって順番が変わることがあります。たとえばLOADSTOREはメモリに対して読出し/書込みで作用は逆ですがオペランドの並びもひっくり返ります。


Syntax: LOAD 0, cntl, destination, source
Operation: destination ← EXTERNAL WORD [source]


Syntax: STORE 0, cntl, source, destination
Operation: EXTERNAL WORD [destination] ← source

 

これを他のCPUと同じように代入の矢印の向きはいつも同じと思い込んでアセンブラコードを書くと思った通りのメモリ参照ができず悩むことになります。 

 

2.レジスタ間転送がない(たぶん・・・

Z80LD A,Bとか8086MOV BX,AXのように内容を変更しないでそのままコピーする命令がありません。むりくりやるとすると演算結果が変化しない演算で行うしかないのかもしれません。

ADD destination,source,0    :destination←source+0

とか。

 

3.即値演算のデータ幅が8ビットしかない

Am29kの命令長は32ビット固定で代表的なフォーマットは

OPCODE destination,source1,source2

の形をとります。それぞれのフィールドが8ビット幅なので即値を指定できるsource2も8ビット幅しかなく32ビット幅レジスタにアライメントされるとき上位ビットは勝手に0拡張されてしまいます。これはレジスタの使用効率が悪い(8086ではAXAH/ALに分割して使えるのに・・・って貧乏性かっ!)だけでなくレジスタの上位ビットにゴミが入っていると比較命令でマッチせず嵌まることになります

 

4.即値代入のデータ幅が16ビットしかない

単純にレジスタに即値をセットする場合は定数セット命令

CONST destination,imm16

が使えます。この場合は16ビット幅の即値が使えますがセットされるのは下位16ビットで上位ビットはやはり勝手に0拡張されてしまいます。では上位ビットをセットするのどうするかというと

CONSTH destination,imm16

という別の専用命令を使うことになります。ここで要注意なのは転送先のレジスタは上位下位半分づつ独立してアクセスしているわけではないので

CONSTH destination,imm16H(D31..D16)

CONST destination,imm16L(D15..D0)

の順で実行すると期待する結果になりません。2行目がせっかく1行目でセットした上位16ビットをクリアしてしまいます

 

5.アドレスの即値指定

上に関連しますが引数が32ビット幅アドレス、例えば固定文字列の先頭アドレスをラベルで渡す場合、一度に代入できるのは下位16ビットのみなので上位16ビットはデータが存在するアドレスを探して別途与えなければなりません。アドレス空間は4Gバイトリニアなのに64kbyteのセグメントがあるような感じで何か気持ちが悪いところです。

以下はコーディング中のモニタのソースの一部でオープニングメッセージ文字列のあるアドレスMSG1を文字列出力ルーチンSTROUTに渡している部分で、STROUTルーチン内で上位ビットをセットしています

 

f:id:O3I:20201219113128j:plain


6.スタックの処理

Am29kプロセッサの最大の特徴が潤沢にあるレジスタによる「レジスタウィンドウ」である、とのことですがこれの使い方が理解できていません。スタックの処理もここに含まれていると思われ、サブルーチン(AMDではプロシジャーコールと云っている)の作り方にもかかわってきます。

今の段階では特定レジスタ1個に戻りアドレスを退避してジャンプ、という作りにしていますが当然1個ではサブルーチンの中から別のサブルーチンを呼ぶことができません。苦肉の策として退避レジスタを複数用意してそのサブルーチンのネストの深さ(請負レベル)によってレジスタを使い分けることにしました。

以下はモニタに含まれるダンプ時のヘキサ表示ルーチンですがコール関係は

 モニタプログラムメインルーチン(請負レベル0)

  ↓

 HEX2DSP:HEX2桁表示ルーチン(請負レベル1)・・戻りアドレスは tpc2に退避

  ↓

 HEXDSP:HEX1桁表示ルーチン(請負レベル2) ・・戻りアドレスは tpc1に退避

  ↓

 TXCHAR:1文字出力ルーチン(請負レベル3)  ・・戻りアドレスは tpcに退避

となっています。

f:id:O3I:20201219101313j:plain

とりあえずアセンブラレベルのコーディングではこれで凌いでおいて、スタック処理はGCCが使えるようになったらコンパイラに任せてしまうということにします。

 

 

ハンダ修正

データの化け方をよく見ると規則性があることはすぐ判ります。まず4バイトの周期を持つということは32ビット幅のデータバスに配置した4個の8ビット幅SRAMにそれぞれ対応していることになります。

読出し値がおかしいのは1個目と2個目のRAM:D31-D24D23-D16につながっているRAMと断定できます(Am29kプロセッサは初期状態でビッグエンディアンのため)

そこで入力したキャラクタと読みだされたキャラクタをビット配置で見てみると・・

f:id:O3I:20201214203014j:plain

明らかにD26,D25.D24D17はビットに「0」を書いても「1」を書いても必ず「1」が読みだされてしまっているということになります。

これはもう、ICのハンダ不良ですね。データバスとICのピンがつながっていないようです。データバスはプルアップはしていませんが高インピーダンス状態のバスを読むと前回書込みで充電されていて不安定に「1」が読めてしまうのかもしれません。顕微鏡でRAMのSOパッケージの当該ピンを見ると確かに足が浮いているようにも見えるので少し追いハンダを流して修復、読出し値は書込み値とちゃんと同じになりました。

 

次に1バイトづつインクリメントしたデータを書き込んでみます。
012345678901234567890123456789012345678901234567

890123456789・・・・

 

これを読み出すと
212365674901834527896123456789012345678941238567

290163454789・・・・

なんか微妙に順番が入れ替わってます。これは同じようにRAMのアドレスピンが浮いていてちゃんとアドレスが指定できず重複されて読み出されているのでしょう。どのアドレスピンが歯抜けになっているかはこのデータ列から導くのは至難の業なのでROM-RAMに数珠つなぎになってるアドレスバスを一本一本導通確認しました。

基板のパッドからSOパッケージの足がわずかに浮いている場合、足にテスタ棒を押し当てるとそれでパッドと接触して導通してしまうので要注意です。こんなチェックシートで当たっていくと、これは酷い・・アドレスピン全76か所のうち10か所も非導通(×)です。

f:id:O3I:20201214203028j:plain

バツが付いたところを、これも追いハンダしてすべてOKになりました。

今回、RAMのフットプリントはちょどぴったりのものを選びましたがこれはリフローならいいですが手ハンダだとちょっと厳しいのかもしれません。占有面積の制約もありますがもうすこしフィレットが流せるパッドを選んだ方がいいです。

SRAMデータ化け

1文字入出力ができたのでこれを使って原始モニタのコーディングを始めました。まず実装するのはL(Load)、D(Dump)、E(Exec)の3つのコマンドだけです。

オープニングメッセージ出力、コマンドプロンプト、コマンド解析・分岐はニーモニックこそ違いますが今までのCPUの処理とほとんど同じように書けます。分岐ごとにnopを挿入するのがちょっと煩わしいです。

f:id:O3I:20201213175351j:plain

 

起動は成功、まずLoadコマンドを試してみます。

f:id:O3I:20201213172801j:plain

Loadコマンドはシリアル入力(対ターミナルプログラムではキー入力したキャラクタ)をSRAMの先頭番地0xFFE00000から順番にセットします。

ここでたとえば「0」のキーをしばらく押し続けるとメモリ上にはASCIIコードで「0」すなわち0x30でフィルされ、その後DumpコマンドでSRAMの先頭番地から今度はシリアル出力するとターミナル画面上には「000000000000000000000・・・・・」が出るはずです。(本来はbin2hex変換して2桁のヘキサコードで出すべきですが最初は手抜きでASCIIコードのまま返送。)

 

ところが画面上には「72007200720072007200・・・・・」で文字化け化け。  「1」~「9」でも同様にやってみるとこんな具合。

f:id:O3I:20201213172743j:plain

なんかやらかしてます。