SHARP PC-1350でプログラミング その3

f:id:PocketGriffon:20201205221738j:plain

エミュレータにプログラムを流し込みたい!

PC-1350でのプログラミング…と言いつつ、なぜかエミュレータの開発日記になっているが、まぁそこは置いておいて欲しい。

近い将来、ちゃんとSC61860でプログラム組むから……きっと!(^^;

 

エミュレータを作ってる目的は、自作する予定があるプログラムの開発効率を上げたいからだ。なるべく楽ちんに作りたい。プログラマは自分がラクするために苦労をする生き物なのだ!(^^;;

BASICプログラムにしてもマシン語プログラムにしても、普段から慣れたエディタが使えるメリットは計り知れないし、シリアルで通信してるとトライアンドエラーの時間だってバカにならない。こっちはロードしてる時間分だけ寿命が縮むんだよ!(全員そう)

どうあってもエミュレータへプログラムを流し込む事を実現しなくては!!

 

エミュレータとして動いているPC-1350に、自分で作ったプログラムを流し込むとしたら、どんな方法があるだろうか?実にたくさんありすぎて選択に悩むが、それぞれに一長一短がある。

いくつかの例をそれぞれ見ていってみよう。

 1、直接メモリに値を書き込んで実行させる

2、疑似カセットテープからデータを流し込む

3、疑似シリアル(RS-232C)からデータを流し込む

 

1、直接メモリに値を書き込んで実行させる

 一番てっとり早いのはコレだ。マシン語コードをメモリに読み込ませ、PC(プログラムカウンタ)の値を設定してエミュレータを駆動してやれば良い。

この方法は、エミュレータ開発時に良くやる。ホントに良くやるw

面倒な手続きが必要なく、かつ即座に結果が見られる利点がある。

逆に問題となるのが、実機で実行した時とはいろんな場所のメモリ内容が違ってしまう事だ。実機で「LOAD→RUN」した時とは明らかにメモリの内容が食い違う。これが厄介な問題を引き起こす事も多いので、出来るだけ合わせておきたいのだ。

あとBASICのプログラムは中間言語の形式に変換しなければならないのでだいぶ面倒だ。

 

2、疑似カセットテープからデータを流し込む

2番目のカセットからの読み込みについては、今回はあまり検討しなかった。まずカセットを擬似的に実装するメリットは、CLOADMでマシン語データを直接読み込める点だ。PC-1350の場合、外部とマシン語データを直接やりとり出来るのは、このCLOADM、CSAVEMだけ…だと思われる。

 

逆に私的なデメリットとしては、カセットへの出力フォーマットが全く分からない事だ。資料を探してみたがどうも見つからない。アスキーデータとして出力しているのか、それとも中間言語のまま出力しているのかも分からない。おそらく中間言語のままだろう…推測。

だとすると、外部でテキストを中間言語のバイナリに変換するツールが必要になるのかも知れない…上のメモリ直書きと同じ問題をはらんでいる。コレはさすがにホネが折れそうで、手軽に出来る感じがしない(T-T)

 

3、疑似シリアル(RS-232C)からデータを流し込む

 そして今回は3番目の擬似的なRS-232Cを介してプログラムをエミュレータ内部に流し込む方法にチャレンジしてみた。以下、詳しく書いていきたい。

 

擬似的なシリアル通信

RS-232Cで通信出来るパソコンは、ほぼ例外なく「RS-232Cから1バイト受信する」という関数が用意されている。PC-1350でも$F22A(ROMのバージョンが$FFF0=3の場合。$FFF0=203では$EE27)というアドレスにそのルーチンがある事が分かっている。

これは「シャープポケットコンピュータ 機械語マニュアル」に書かれていた。

f:id:PocketGriffon:20201205225128j:plain

こういう有用な情報をメーカーが出してくれるのは本当にありがたい(^-^)

ひとつ補足をすると、この関数の戻り値(受信したデータ)が入っているのは、本に書いてあるBレジスタでは無く、Aレジスタだw

 

やりたいことの皮算用としてはこんな感じのイメージだ。

・読み込ませたいファイルは、エミュレータ実行時に引数として渡しておく

・「1バイト受信」の関数を改造し、ファイルから読んだデータを1バイトずつ渡す

・PC-1350はまんまと騙されてファイルをメモリに書き込んでいく

 

特にBASICのプログラムは、アスキー形式のままPC-1350に渡すことが出来る上に、PC-1350が内部で中間言語に変換してくれるサービスまでしてくれるので一石二鳥だ。

 

エミュレータに特殊な事をさせたい場合、普段はSC61860の命令をエミューレーションしているところに、何かしらの方法で制御を奪う必要がある。これもいくつかの方法が存在するが、今回はSC61860の命令を新設する事で実現した。

f:id:PocketGriffon:20201205230121j:plain

今回は$CFのコードを独自に拡張をした。「1バイト受信」関数の先頭に$CFを埋めておき、必要なレジスタや状態を真似た上でエミュレータにデータを返してやれば良い。
書くと簡単だが、実際にやってみても大して難しい話ではない(^-^)

 

さぁ、じゃあ実際にやってみよう!

f:id:PocketGriffon:20201205231305j:plain

…LOADって表示されてから先に進まなくなっちゃた…。うーん…通信に必要な初期化はBASIC ROMが事前にやってくれるはずで、エミュレータとしては何も考えなくて良いはずなんだけどなぁ…ぐぬぬ

この先を調べようと思ったら……そう、「1バイト受信する」プログラムの解析が必要だ。

解析しなくても行けちゃわないかと思ったけど甘かった(T-T)

 

割り込みを使わないシリアル通信

この↑の言葉で、いろんな事情を想像出来る人は結構な経験者だ(^^;

ここでシリアル通信とは何かを語るつもりはないので簡素に言うと、少ない信号線を頼りに1ビットずつ送られてくるデータを組み立てるようなプログラムになる。

聞き慣れた「ボーレート、パリティ、ストップビット」などの言葉が、プログラム中にコードとして出てくるのを読み解かねばならない。

 

まずはエミュレータをちょろっと改造して、逆アセンブラを作ってみた。画面で見ているだけでは厳しかったので、プリントアウトして書き込みをしながら解析を進めていった。

f:id:PocketGriffon:20201205232222j:plain

殴り書きなので小さい写真でw

情報として必要なのは、「どこで1バイトの結果を得ているのか」「その時のレジスタの値は?」「メモリの値は?」等々だ。これらを上手に組み立ててPC-1350を騙さねばならない。

 

まずプログラムを大雑把に追い掛けていき、ビット長の確定、スタートビットのチェック、1バイト取り込み、ストップビット判定、パリティチェックなどを洗い出した。この辺りは標準のRS-232Cという事で、基本に忠実に作られていた助かったww

 

しかしまぁすごいプログラムだ!クリティカルな場所では1ステートの戦いとなっている。こっちの分岐でこのステートだからこっちの分岐ではNOPが何個入る…や、おそらく分岐でのステートを計算した上での無駄分岐とか、最適化がされているので読み解くのが簡単ではない!

普段、FIFOバッファと割り込みのあるマシンばかり触っていたので苦労が少なかったが、それらがない状態で通信しようとするとこうなるのか…と先駆者の苦労を垣間見た!

関数のサイズ的には300バイトくらいの大きさなんだけど、その道のプロがガチガチに最適化した300バイトなので解析する方も大変だ!

 

ようやく手当てする箇所がわかり、プログラムを組み込むことに。アセンブラも用意していないので、基本はハンドアセンブラだ。でも大したサイズじゃないので大丈夫(^^)

さあ実行してみよう!

f:id:PocketGriffon:20201205233225j:plain

…あ!!…これはとても厄介な状態!(T-T)

エミュレータで一番怖いのは「それっぽく動いちゃうけどなんか違う」ってヤツだ。今回のERROR 4 というのは「ラインナンバーがない」というエラーだ。データの受信に失敗しているのかと思ってモニタリングしてみたが、データはきちんと読んでいる。だけどダメ。

こりゃ…相当厄介だぞ(T-T)

 

ちなみに送り込んでるBASICプログラムはこんな感じ。

f:id:PocketGriffon:20201205233708j:plain

なんのたわいもない、数行のBASICプログラムだ。

これの行番号が無いとおっしゃりたいのですね、PC-1350さまは(T-T)

さぁこれをデバッグしないといかんぞ。

 

試してみたところ、1行のプログラムだったら問題無く読み込める事が分かった。実行する事も出来るので、アスキー中間言語変換部分はちゃんと動いてると思って良いだろう。

 10 WAIT 0

はOKだけど、

 10 WAIT 0

 20 PRINT "HELLO"

はダメなのだ。

この2つの違いといえば、改行コードが入ってるかどうかの違いだ。

問題はそこにあるに違いない…と思い探していくが、どうしても問題が「そこ」にたどり着けない。ここにきてエミュレータが出力する数十メガバイトの逆アセンブルリストとダンプリストと格闘するハメになった。現象が絞りきれない分、逆アセンブラを見る範囲のリストも長くなる。

 

PC-1350は受信バッファが256バイト用意されている。アドレスとしては$6D00固定であり、これはプログラムにハードコーディングされているため動かせない。実際には専用で用意されているのではなく、通信時のみ別の意図で使われていた領域を間借り(破壊)するのだ。ロード出来る1行のサイズが256バイトに制限されているのは、こういった事情によるものだ。

 

分かってきた事としては、受信バッファの先頭4バイトにゴミデータが入るらしい…という事だった。1行目は問題がなく、2行目の先頭に入るのだ。

そしてそのゴミデータを、BASICの中間言語変換プログラムが読み込んで「行番号がない」というエラーになる…というプロセスだった。

 

問題は「なぜゴミデータが入ってしまうのか」だ。

そもそもエミュレータが完全に動いているわけではないので、もしかしたら実装した命令の解釈が間違っていてメモリ破壊が起きているのかも知れない。逆アセンブラの出力を信用して良いのか?レジスタとメモリの割り付けは本当に正しいのか?…などなど…自分で作ってるがゆえに疑うべきところが無限に見つかるw

こうなってくると記憶すら疑わしくなってきて「あれ?CRって$0Aだっけ?$0Dだっけ?」と基礎的なところまで調べ直すハメになったww

 

そして逆アセンブラリストと格闘する事数時間、ようやく分かった驚愕の事実!

なんと!「1バイトを受信する関数」の、途中のアドレスから飛び込む場合がある事が判明!

ちょっとー!プログラムのお作法としてどうなのコレは?(T-T)

関数の途中から飛び込まれる事を想定していなかったので、レジスタの値やメモリの内容が食い違っていておかしな動作をしていた…というのがバグの原因だった。

これは……さすがに分からない!!(^^;;;

 

結局、最初に考えた「1バイトを受信する関数」の改造法を考え直す必要があり、再度あのコテコテに最適化されたプログラムと格闘するハメになった。難しいのは、途中から飛び込まれるアドレスはずらせないので、その部分は活かしたままで改造する必要がある事だ。ちょっとしたパズルが必要となった(T-T)

その部分を改良し、一通りデバッグを済ませた状態がコレ。

f:id:PocketGriffon:20201206001003j:plainf:id:PocketGriffon:20201206001023j:plain

↑流し込んだプログラムをLISTしたところ

f:id:PocketGriffon:20201206001132j:plainf:id:PocketGriffon:20201206001150j:plain

↑実行してみたところ
当然だが、実機とエミュレータとでは1ドットも変わらぬ同じ結果となる(^^)

 

やっとここまで動いたー!

ちなみにここまでBASICを転送する話に終始していたが、マシン語データはどーするのさ…という疑問が残ったかも知れない。

マシン語のデータは、いきなりメモリに書き込んでやりCALL アドレスとすれば良いのだ。BASICと違い、マシン語は変換も入らなければ下準備もいらない。ちょっと乱暴に扱ってやるくらいが丁度良いのだ!(^-^)

 

ところで…初めてSC61860のコードを解析する事になったのがシリアル通信プログラムだった…これはこれで酷くない??…と自分でも思ってしまうのだが…(^^;;;

 

さてさて、ようやくプログラムも動くようになってきたので自作プログラムを…とはまだまだならない。この先も充実させたい事がいくつかあって、そちらを先にやろうと思う!

もう少し「環境オタク」の大暴走にお付き合いくださいませw

 

ではまた次回!(^-^)ノ