いろいろとお小言をいただく前に言い訳をしておきます。。。

NMIによる計時処理

XENESISシリーズのボードでは、計時機能を実現するために、PICのタイマーで作成したインターバル信号を各マイクロプロセッサのNMIに接続して計時処理を実現しています。

一般的に考えると、タイマー割込みごときをノンマスカブルな割り込みに入れるな!という皆さんが多いのではないでしょうか。私もそう思います。すみません。

ただ、そうは思いつつも、XENESISシリーズでは以下のような理由でNMIへのタイマー割込みで計時処理を行っています。

  • そもそも計時処理は、ないよりはあったほうが絶対に良い。
  • マイクロプロセッサで初めてコンピュータに触れた人が主流だった1970-80年代はまだしも、すでに現在のPCやマイクロコントローラなども使った人たちにとっては、タイマーと割り込みを使わないコンピュータはコンピュータじゃない(。。。と思う)。
  • レベルで割込みを検知するIRQを使おうとすると、割り込み要因を調査し、該当の割り込みをリセットするための仕組みとして、入出力ポートと関連制御回路が必要になり、ボードのハードウェアが中途半端に肥大化してお洒落じゃなくなる。
  • エッジで割込みを検知するNMIで、しかも間隔がかなり長いタイマー割込みであれば、ハードもソフトもお気軽に作れる。
  • ホビー用のお気楽なボードなので、そもそもNMIで処理すべき、システムに致命的な事象なんてない。
  • どうしてもお気に召さないなら、ジャンパーのショートピンを抜いてNMI割り込みの接続を切ればよい!

。。。という確固たる理由で、NMIでタイマー割込みを使用することにしました。すみません。

計時機能をサポートするハードウェア(というよりはPICのプログラム設定なのでソフトウェア?)を実際にちょっと整備し、それを動かすプログラムをモニタや言語システムに組み込みました。当初の目論見通り、1つのICも追加せずに計時機能を実現しています。実際の機能の強化は大したことないのですが、私のシステムに対する印象は大きく変わりました。

NMIのインタラプトハンドラ

PICによる割り込み間隔は10msに設定しています。

NMIの割り込み処理ルーチンでは、4バイトの整数値のカウントアップを行っています。また、カウンタの値を参照するルーチンが、カウンタ値がNMIで変更されたことを識別できるように、フラグと使用しているメモリをクリアするようにしています。NMIの処理は、上記の最小限の処理で終わらせています。

脱線です。NMIの割り込み処理ルーチンは、なぜ0066Hというわけのわからないアドレスから始まっているのだろう。RSTXXのアドレス配置との整合性もないし。どなたかご存じですか?

計時機能の精度

計時機能の中核となるタイマにはPICのタイマを使っているので、計時機能の精度は、PICのクロックの精度によります。システムの用途的に高精度のクロックは必要ないと考えていたため、クリスタルを使用したクロック発信ではなくて、クリスタルを使わないPIC内蔵の発信源HFINTOSC (High-Frequency Internal Oscillator)を使用しています。

当初はあまり気にしていなかったのですが、ベンチマークテストに計時機能を使い始めると、その誤差が少し気になり始めました。PICの固体や気温などの環境にも影響されるのでしょうが、いくつかのボードでの実測では、1時間で10-20秒ぐらいの誤差があります。

実測によるHFINTOSCの誤差は0.5%(もちろんクリスタルと比べると数桁劣っているのですが)ぐらいで、データシートに書かれている1%の誤差内には収まっています。これだけを見ると誤差はそれほど悪くないように思うのですが、計測時間が長引くと誤差が累積するのでかなり気になる誤差量になります。

ただし、ベンチマークテストでの計時時間はほとんどの場合長くても5分程度ですので、0.5%程度の誤差は1-2秒の誤差に収まります。手持ちのストップウォッチなどを使用して計時することを考えると、1-2秒の誤差は許容範囲だと考えられますが、どう思われますか?


計時機能の弊害:スタックポインタの流用

計時機能が悪いのではなく、その利用を阻害するプログラミング手法の話です。

悲報:6800のBASICやVTLでは、(そのままでは)計時機能を利用できない。(ただし、TSC MICRO BASIC PLUSは、問題ありません。)

割り込みによる計時機能を実装するかなり以前に、アセンブリ言語で記述された6800用の各種のBASICの6800-COREへの移植を行っていたのですが、どれもこれもが、スタックポインタをデータポインタに流用していてあらためてびっくりしました。理由は単純で、処理速度とコード削減のためですが、現在の感覚でいうととても気持ち悪い。。。というか、恐ろしい悪行の数々を見てしまった感じです。現在このようなプログラミングは、そもそも思いつく人は皆無(スタックポインタの意義や利用法の教育などにより)だと思いますし、それをやったら大目玉を食らうような危険なプログラミングです。

ただし、それを責めるのは酷というもので、当時の感覚やハードウェアリソースを背景とすると、むしろ素晴らしいアイディアであったとは思います。

この様な処理を行ったプログラムは、スタックポインタをデータ処理に流用している間に割り込みが入ると、あっさりと暴走することになります。(実際には暴走というよりは、挙動不審、機能不全といったほうが正しい感じです。)ただし、ほとんどのプログラムはSEI命令を使用して、一時的もしくは全面的にIRQを受け付けないように配慮されているので、ある意味正しいプログラムといえるでしょう。

ちょっと脱線:言語処理系ということで、AhoのCompilersを確認してたら、初版は1971年で6800の時代にはすでに出版されていたんですね。現在は全面改訂された第2版になっていて書店でべらぼうに厚い書籍を購入できますが、WEBで検索すると、驚くべきことにPDF版(1000ページでーす)がダウンロードできます。7章「Run-Time Environments」を見ると、言語処理系がスタックを使用してサブルーチンコールでどのように引数や変数を含めた実行環境の管理を体系的に行うかが書かれています。

話は戻って、すでに記憶があやふやなのですが、高校生の時(45年前)には、私自身も、プログラムを読んでもスタックポインタの流用には全く問題意識を持っていなかったように思います。(私が初めて買ってシステムを構築したマイクロプロセッサが6800でした。)

当時は、個人的にも時代的にも、現在のようなスタックポインタの利用上の規範が定まっていなかったように思いますし、それ以上に6800のレジスタが貧弱すぎたのが問題だと思います。

  • そもそもコンピュータがホビーストに開放されたばかりで、コンピュータサイエンス的に適切なスタックの使用法の知識が広まっていなかった。
    • スタックポインタは、サブルーチンコールなどの実行制御用以外に流用してはならない。
  • 割り込みを実際に使用するアプリケーションがなく、スタックポインタの変則利用に関する問題意識や実害がなかった。
  • そもそも、6800のレジスタが貧弱すぎた。
    • 2つの領域間でデータを移動させる操作はコンピュータにとって基本的な処理で、その基本的な処理を当たり前に行うためには、最低2つのポインタ(アドレスレジスタ:6800ではインデックスレジスタ)が必要なのに1つしか実装されていなかった。
    • Z80では多数の、その原型となった8080でさえ3本のポインタを備えている。
    • 6800の簡略版といわれている6502でさえ、2つのポインタ(Y,Yのインデックスレジスタ)を備えている。もっと言うと、ゼロページの間接アドレッシングを使うと最大128本のポインタを利用できる。

68系のマイクロプロセッサは、「究極の」といわれた6809でやっとインデックスポインタ2本体制のまともな(失礼:現代的な観点でいうところの)マイクロプロセッサに昇格できました。また、6809は6800とバイナリの互換性がありませんが、その互換性を持つ6800の正当な後継者という観点では、インデックスポインタが2本になった68HC11でやっと正常化されました。

マイクロソフトお前もか?!

とは言っても、悪いのはどちらかというとこちらでしょうねえ。。。

残念なことに、マイクロソフトが開発したMITS ALTAIR 680 BASICもNMIの計時機能を有効にすると、挙動がおかしくなってしまいます。コードを確認すると、当時の6800用の他のBASICインタプリタと同様に、スタックポインタの流用を行っていました。ただし、スタックポインタを流用するコードの前後には、割り込みの禁止、許可コードが埋め込まれており、基本的な対策は取られています。(とはいえ、NMIは問答無用で割込みますもんね。)

一方、Z80や6502用のBASICインタプリタは、割り込みによって障害が生じることはありません。また、アセンブリコードを見ても、6800で見られるような問題になるコードはなかったように思います。これは、プログラマがアイディア不足だったりお行儀がよかったりしたわけではなく、Z80や6502では、複数のポインタを無理なく使えたため、6800で行わざるを得なかった(現在の観点では)お行儀の悪いプログラミングをする必要性がなかったということでしょう。