MSXの王道:H.TIMI割込みの話
H.TIMIとは
H.TIMIはHook TIMe Interruptの略です。
ブラウン管のテレビの時代ではブラウン管に電子ビームを上から下に当てて画面を表示していました。日本ではNTSCという規格があって、この上から下にビームを当てながら画面を表示するのに1/60秒(たぶん厳密にはもうちょっと短いけどまあそんくらい)の時間を必要としています。1秒だと60回分ですね。これを"60fps"(1秒あたり60回表示する)とかって言ったりするみたいです。VDPは画面表示の開始のタイミングでCPUに対して「今から画面描くよ!1/60秒の間は何も表示を変えれないからよろしく!」というサインをMSXでは1/60秒毎にCPUに送っていて、これが"H.TIMI割込"と呼ばれています。VDPはその合図の直後に画面にVRAMの情報(パターンネームテーブルとかスプライトとか)を出力しますが、出力してる間にCPUはまた別の処理をして次の1/60秒のタイミングまでに次に表示する情報をVDPが画面を刷新する作業と並行して作成していく。というイメージです。
1/60秒が基準の世界
この1/60秒というのがキモで、この期間内にCPU側でのVRAM作成処理が間に合わないと画面表示が崩れたりします。これを"テアリング"といいます。
他にもCPU側のVRAM作成処理にものすごく時間がかかると表示がカクカクしたりします。これを"処理落ち"といいます。例えば1/60秒の間に間に合わなかった場合、VRAMの作成に1/30秒かかったとすると、通常の倍の速度で画面が切り替わることになります。つまり見た目が遅くなる。という感じ(1/30は1/60の倍ですよね)。シューティングゲーム系でよく処理落ちは起きてました。
"テアリングが起きないように"、"処理落ちが起きないように"いい感じの速度で、VDPが画面を刷新してる1/60秒の間に次の画面表示のVRAM情報を作らなければいけない。ということになりますね。
さて、今までのサンプルではこの"割り込み"はまったく使っていませんでした。今回のサンプルコードからはこの"割り込み"を使うことにします。
と、いうことでさっそくサンプルコードをゲットだぜ!
https://github.com/sailorman-msx/games/tree/main/src/sample012
割込み処理の流れ
割り込みを使った処理は、おおまかには
VDPからの割込を待つ
↓
割り込みが来たら自分のメイン処理をCALL
↓
次の割込を待つ
という流れです。
これで、どんなメリットがあるの?というと
1/60秒という一定間隔が保証されるので、○秒に1回処理する(例えばストップウォッチみたいなやつとか、カウントダウンとか)が実現できるようになるわけです。ちなみにMSXには"タイマー"といった機能は付いていません。なのでこの割り込みでしか秒数を測ることしか出来ないのです。とても大切な機能です。
H.TIMIを書き換える
H.TIMIはワークエリアのFD9FHになっていて割り込み時(VDPからの合図をCPUが受け取ったとき)このアドレスがCPUからCALLされます。FD9FHからFDA3Hまでの5バイトがH.TIMIの命令格納エリアになっていますが、割り込み時にCALLされるので通常はRET命令だけが書き込まれています。この部分を書き換えて「JP 自分の処理」と書き換えるとあら不思議。割り込み毎に自分の処理が呼び出されることになるわけです。通常、自分の処理にジャンプするコードに書き換える前に、元あった5バイトをどこかに退避して、自分の処理が終わったらその退避してる場所にジャンプしてあげるようにします。これは周辺機器とかがついていたらRETだけじゃないコードが入ってることがあるから。という理由らしいです。
この仕組みを使うといくつもの処理を数珠つなぎにして、割込時に複数の処理を連続させることが可能になるのです。
それと、割り込みは他にも使い勝手がありま~す♩
BGMを出す
投稿を避けていた"MSXで音楽を鳴らす"テーマです。趣味でギターを弾いたり歌ったりするけどこのMSXでの音作りだけはちょっと難しそうだなあと思いながらの食わず嫌いでした。でも、記事を書くまでにワタシもだいぶ学びました。そしてその奥深さを知りました。
さて、過去記事で説明してる通り、MSX1にはYAMAHAのサウンドチップが入っていてそいつに対して命令を送ると音が出る。という仕組みになっています。このチップのことをPSG(Programmable Sound Generator)と呼びます。
このPSGはVDP同様、CPUとは分離しているので、CPUから「音を出せ」という命令があったら「音を止めろ」という命令が出るまでずーっと音を鳴らし続けます。つまりCPUは自分のタイミングだけ気にしてPSGに命令を出すだけで済むということです。CPUとVDPとのお付き合いは1/60秒の純情な感情ですが、PSGに対してはCPUは亭主関白な感じです。
MSXのPSGでは
3つのトーンジェネレータ(いわゆる3チャンネルっていうやつ)
1つのノイズジェネレータ
1つのチャンネルミキサー
1つのエンベロープジェネレータ
3つのボリュームアンプとスピーカー
で構成されていています。
トーンジェネレータは音階の数値を受け取るとその音階の音をミキサーに放り込みます。音階はご存じのドレミですが、MSX(というか音楽全般)ではCDEFGABでドレミファソラシを表現します。また、世間にはMMLというマークアップ言語が存在しています。MMLでは例えば
T120 O5 C4D4E4
というような表記をします。
この表記でテンポ120の音階5の4分音符でドレミ〜♩
と鳴ります。O5のO(おー)はオクターブのオーです。
MSX-BASICで以下のように書くと音が鳴りますよ。
PLAY "T120O5C4D4E4"
話がソレてしまいました。
さて、チャンネルミキサーには3チャンネル分(3和音)が放り込まれてノイズジェネレータでザーとかガーとかいう音に合成されます。
その音がさらにエンベロープジェネレータで波形を加工されます。ここでの加工はピコピコとかシュゥゥ、、とかみたいな感じです。音量を大きくしたり小さくしたり、音を消したりする機能みたいなものです。ビブラートとかそういうやつ。
その結果がボリュームアンプに渡されてピコピコ音が鳴る。という仕組みです。シンセサイザーっていうやつですね。
さて、テンポ120というのはどういう意味でしょう?
答えは、1分間に4分音符が120回鳴る。ということを意味するのがテンポ120(120BPM:120 beat per minute)です。
1分間。。。
ほらここにも1/60秒の規則に関係するキーワードが出てきましたよ。
サウンドドライバで音を出してみる
自力で音を出すプログラム作るのはしんどいので(いつ出来るかもわからないので)今回は、あぶりさん(@aburi6800)謹製のmsx-PSGSoundDriverというプログラムを使うことにします。あぶりさん、使用許可ありがとうございます。
もともとはZCCというCコンパイラでビルドするように出来てたのですが、Z80ASMでアセンブルできるように、若干の改変はワタシのほうでしています。このサウンドドライバはエンペロープ以外のことが処理できるようになっています。詳しくはサウンドドライバのソースコードを参照ください。
また、音の速さを表すのにBPMという単位を使いますが、MSXではテンポ(T)と表記するので以降、BPMのことをテンポと表記します。
サウンドドライバ概要
サウンドドライバはH.TIMIの割込を契機として音を鳴らす処理が呼び出されます。
サウンドドライバでは1/60秒毎に指定されたトーンで音階を、指定された音長、つまり指定された期間だけ鳴らします。
例としてテンポ120のCの音を出すために必要な音長の計算は以下の通りです。
テンポ120の4分音符は1分間に120回の4分音符を鳴らすということなので
秒に換算すると、120÷60=2、なので2回の4分音符を1秒で鳴らす。ということになります。
さらにサウンドドライバは1/60秒で処理されるので、60÷2=30が音長になるということになります。60/60秒は1秒ですよね。なので30/60秒毎に音を1つ鳴らせば1秒(60/60秒)で2回鳴る。という仕組みです。
8分音符は4分音符の倍速なので30をさらに2で割るとその音の長さが分かるということになります。ワタシは8分音符が好きです。(どうでもいい)
テンポ120で4分音符Cの音を鳴らす場合は、データに音階Cの数値と音長30の数値を用意します。サウンドドライバ側ではそのデータをもとに最初に音長30という数値をワークアドレスにセットして1/60秒毎にその数字をデクリメントしていきます。0になったら次の音。つまりふたたび音長30のCを鳴らせば4分音符のCが1秒間で2回鳴る。という仕組みです。
イメージできますか?
なお、このへんの技術(音を綺麗に出したり繋いだりする技術)は超絶に難しいみたいで何十年もみんな苦労してるみたいですね。ハマるひとはハマるジャンルのようですが、当noteでは軽く伝える程度にします。
できたやつはこれ。カーソルキーだけではなくジョイスティックにも対応しているのでスマホでも動きます♩
半タイル処理はどうしたの!?
さて予告していた半タイル処理ですが、ワタシが間違っておりました。進行カウンタで半タイル処理を制御する予定だったのですが、VDPが画面表示を刷新する頃にはすべてのテキキャラの移動が終わっていて、人間の見た目的にもビューン!とテキキャラがワープしてるように見える始末。なので、割り込み処理の中にテキキャラ移動その他のメイン処理を閉じ込めることで、○秒ごとに半タイル移動、というようなことが実現できるように考えかたを改めました。ということで、半タイル処理はまた別記事とさせていただきます。。
なお、今回のサンプルコードでは割込処理の中にメイン処理を閉じ込めています。INIT_H_TIMI_HANDLERはinterval.asmにサブルーチンが書かれていて1/60秒おきに、VSYNC_WAIT_CNTが0になります。0になったらGameProcサブルーチン(メイン処理)が呼び出される。という流れです。
なお、BGM演奏のメイン処理はINIT_H_TIMI_HANDLERの中でSOUNDDRV_INITという処理を呼び出して、割込み処理の数珠つなぎを構成しています。
(sample012.asm 抜粋)
call INIT_H_TIMI_HANDLER
;--------------------------------------------
; BGM演奏開始
;--------------------------------------------
ld hl, BGM_00
call SOUNDDRV_BGMPLAY
MainLoop:
; VSYNC_WAIT_FLGの初期化
; この値は以下の制御を行うために使用する:
; - メインロジック開始時に 0 に設定
; - H.TIMI割り込み処理の中でデクリメント (1/60秒ごとに呼び出し)
; - メインロジックの最後に、キャリーがONになるまで待機
ld a, 1
ld (VSYNC_WAIT_CNT), a
; ゲーム処理呼び出し
call GameProc
VSYNC_Wait:
; 垂直帰線待ち
ld a, (VSYNC_WAIT_CNT)
or a
jr nz,VSYNC_Wait
jr MainLoop
DIとEI
割り込みが起きてほしくない場合の措置として、DI(Disable Interrupt)という命令があります。この命令をCPUに与えるとCPUはVDPからの割り込み合図を無視するようになりH.TIMIはCALLされなくなります。ふたたび割り込みを許可する場合にはEI(Enable Interrupt)命令で割り込みが行われるようになります。よくある使いかたは1/60秒で呼び出される割込み処理の先頭でDIして、処理が終わったらEIで割り込みを再度許可する。という感じです。割込み処理の実行中に割り込みがかかって、また割込み処理の先頭にジャンプすると困るのでそういう時に使います。VDPはCPUが「割り込みを受け取ったよー」と返事するまではずーっと割り込み合図を送り続けてきます。なので、処理が終わってEIを実行すると直後に割り込み処理が呼ばれる仕組みになっています。
もう、あなたはゲームを作れます
ここまでの連載記事の知識の組み合わせで、もはや、あなたはゲームを作れます。とりあえず次回は半タイル移動の予定です。
では、また!ノシ