シリアル-LANアダプタ


トラ技99年7月号を参照せよ

 技術力や資金や時間や気分など様々なものが邪魔をして一向に進みませんが、「なんとか古いマシンをネットワークにつなげないものか」という気持ちは頭の隅から離れません。そんなある日、といっても99年6月ですが、トランジスタ技術誌7月号の特集「インターネット時代のハード制御」を見つけました。記事を見てみれば、自分の技術力は棚に置くとして、それほど複雑なことは語られていない内容なのですが、具体的な実現例として大いに参考になるものがあります。


組込み制御の最近のトレンド

 昔は、コンピュータでデータ収集を行うためのI/FはGP-IBと相場が決まっていました。あるいは、イーサネットというものはコンピュータ同士の通信の手段であるというイメージが最近固まりつつありました。しかし、想像以上にLANが一般化している事実があります。
 まず、計測器に常備するI/FにLANが加わりました。GP-IBも同様に備えてはいますが、データを収集する側であるコンピュータにGP-IBを備えるものが少なくなり、形骸化しているとも言えます。
 コンピュータを開発するための機器、ICEとかJTAGプローブとかいうもののI/FはすでにLANが一般的です。元々シリアルでは速度の足らないI/Fなので専用のボードを介して制御するものが多かったのですが、LANを採用することでそのI/Fも簡素化しています。
 テレヴォックスに始まる遠隔制御、データロガーによる無人またはそれに近いサイトでのデータ収集など、電話回線やICカードによるデータのやり取りもLANを採用することでI/Fを簡素化し、通信費を抑えることができます。なにせ、インターネットを介せば地球の裏側だって制御できるのです。
 ですが、機器の更新がままならないところ、あるいはまだLANを内蔵していない機器だって多数あります。でもLANはなくてもRS232Cなら内蔵しているかもしれません。そんな機器と、LANとを接続するためのアダプタというものがいくつか発売されているのです。


ちょっと用途にあわない…

 昔のパソコンでも、シリアルポートを備えたものはたくさんありました。これと例のアダプタをつないでしまえば、8bitマシンでもLANに参加する事ができるような気がします。でも、広告で見るそのようなアダプタはちょっと違うのです。上記雑誌にも記事を寄稿しているメーカーの製品は、TELNETやFTPのサーバにはなってもクライアントにはならないようなのです(実際にはユーザプログラムの組み込みは可能なので、クライアント機能も入れることはできるみたいですが、まぁ標準機能の話として、ということですね)。それも当然の話で、想定している機器はパソコンではなくて周辺機器、つまりパソコン側からアプローチをかけるアクセス手順を踏むものばかりなのですから、贅沢は言えません。
 が、このトラ技にはブレークスルーがありました。なんと秋月のAKI-80とISAのNICを接続し、そこにIPとUDPとICMPの各プロトコルを実装して、専用のクライアントをパソコンに用意して市販品と遜色ないアダプタを製作しているのです。記事としては他のものと同様なのですが、コントローラはZ80ですから、開発も格段にやりやすかろうというものです。これにTCPも実装して、クライアント機能も入れられれば、私の求めるものが実現するはずなのです。


いろいろ勉強してみたい

 TCP/IPは会社のお金で行かせてもらった講習会である程度勉強したのですが、さすがに実装できるほど濃い内容ではありませんでした。そこで、今ちょっとプロトコルのお勉強をしているところです。
 実装したいプロトコルはIPとTCP。IPはすでにお手本がありますのでほぼそのまま使えるでしょう。TCPを実装するのはTELNETを使いたいため。TELNETが使えるようになればHTTPも実現性が高くなります(本当にやるならFTPもいりますが。パソコン側のソフト次第なんですよね)。
 ただ、TCPはコネクションが複雑でUDPほど簡単には組めないプロトコルです(と聞いています)。アセンブラで組むにはちと骨かな?とするなら、Cで組みたいところ。どうせならついでにKC160も勉強してしまえ!てな感じで、元の記事ではAKI-80を使用していたところにイエローソフトのYK30-1というKL5C16030ボードを使い、同時にCやアセンブラも買ってしまおうと計画しました。性能的にどの程度のものか、またどれだけのメモリが必要になるのかつかみきってないので、試作は高い性能のCPUを使い、完成したらAKI-80へのデチューンを検討しようと考えたのです。
 TELNETクライアント機能は、別の記事にもあったのですが、このアダプタにモデムのふりをさせれば従来の通信ソフトが使用できることになり、ネットワークへの親和性が高くなると思います。ネットワークドライブという考え方のない時代ですから、こうするより他にないとも言えますが。なお、普通のモデムにあるボーレート自動検出はマイコン単体ではむつかしそうなのでパスするでしょう。まぁそれでも今はNIFTYやサンデーネットがTELNETでつながりますので、かなりの使い勝手があると思いません?


ここからプロジェクトの格上げ

 いくつか部品も揃い始めてきましたので、「構想プロジェクト」から「進行中プロジェクト」に格上げです。


まずはハードの仕様から

 善は急げと、いや何か違うような気もしますが、イエローソフトYK30-1を注文しました。その仕様は該当ページを参照してもらうとして、これに何を追加するかです。
 まず、IPアドレスやモデムとしての設定を記憶しておくためにEEPROMが必要です。YK30-1にはフラッシュメモリが搭載されていますがプログラム用と考えるべきもので、他にはメモリといえばRAMしかありません。手軽な不揮発メモリがほしいところです。これは当初普通のEEPROMを載せようかと思っていたのですが、いちいちデコードせねばならないのが面倒です。なにせアドレスバスも増えてますしね。そこで、シリアルのEEPROMを採用します。これを同期シリアルポートに接続すれば読み書きできます。
 RS232Cはコネクタがついていてレベルコンバータも通してありますが、見るとRXDとTXDしか出ていません。開発用としては別に困りませんが、8bitパソコン用にはRTSとCTSは必要でしょう。ということで、レベルコンバータを追加します。YK30-1のコネクタには未使用ピンがありますので、それを経由すると新たなコネクタを用意しなくてもよさそうです。
 ISAバスとの接続は、元の記事ではZ80PIOを経由することになっているのですが、せっかくの16bitCPUなので直接接続したいところ。ところが、KC160はI/Oに関しては8bitアクセスしかできないようになっています。まだここの仕様は決めかねているのですが、今のところメモリマップドI/Oにしようかと考えています。元の記事同様内蔵のパラレルI/Oを使えばプログラムの変更の量も少なくて済むはずなのですが、このパラレルI/Oはあくまで単純入出力しかなくて、Z80PIOのようにストローブ入力などというものはありません。だからといって8255などを外付けするのは大掛かりですからねぇ。


いざ、部品集め

 仕様がだいたい決まったところで、日本橋へ部品の買出しです。ここまでに揃っている部品は

です。LANカードは別に手元にあるLaneedのLD-PNE20/TAでもよかったのですが、Neptune-XでもってX68030をLANに接続する予定もありますので、またジャンパSWで任意にアドレスなどを変更できますので、CeLANのE2000Tにしました。いや、なんとなく記事の回路ではISAバスのデコードがおかしいような気がしたもので…。
 ということで日本橋では足らない部品を集めてきました。手に入ったものは次のとおり。

シリアルEEPROMとYK30-1に使われているRS232C用コネクタがどうしても手に入りませんでした。日本橋も以前から比べるとパーツ屋が減ってしまいましたねぇ。探しまわってもたかが知れてます。コネクタはYK30-1の改造も辞さない覚悟で別の仕様のコネクタを買ったのですが、同じ改造なら空き端子に出すほうが楽なので、YK30-1上のコネクタを使用しないことにします。またシリアルEEPROMは、トラ技の広告を探したところ神和電機が取り扱っていることがわかったので、ここの通販を利用することにします。
 なお、メモリマップドI/OにするつもりだったISAバスアクセスは、ここまでに8ビットラッチに変更しました。メモリ上のパケットとNICのメモリとで、16ビットアクセス時に上位と下位のバイトのテレコ(入れ替わり)が心配されたのが理由です。


工作開始

 まずはケースの加工。いつもはオールアルミケースを選ぶのですが、ちょっとした気まぐれもあってプラスチックケースにしました。でも前面と後面のパネルはアルミ板です。前面には10BASE-Tとリンク/送受信ランプ、後面にはRS232CコネクタとDCジャックを配します。よく考えたら電源スイッチがないですが、まぁ必要なら追加すればいいし、電源コードの抜き差しでも代用できるでしょう。
 ケースは収める基板の大きさを元に選んだはずだったのですが、表示されていた寸法は外形のもので、実際は一回り小さくなってしまいました。四隅に柱まであって、相当削らないと収まりません。LANカードのほうを削るわけにもいかないので、そちらは干渉しない位置にずらし、ユニバーサル基板を削ることにします。
 プラスチックケースを選んだ最大の理由は加工のしやすさだったのですが、プラスチックの肉厚が厚く、基板固定用のスペーサのネジが届きません。どうするか考えたのですが、幸いにもコネクタを出した穴がぴったりなのでそれで基板が固定されてしまい、どうにかしてでもネジで固定しないといけないというわけではなくなりました。まぁ製品じゃないのでこんなもんでいいでしょう。例え同人ハードとして頒布することになってもケースは含めませんし。
 YK30-1はPC/104互換のコネクタを使ってスタック(親亀小亀)構成にできるようにしてあるのですが、使われているコネクタがちょっと特殊で、通常64ピンと40ピンの組み合わせを用いるのに、これは64ピンと50ピンで構成されています。普通のコネクタでは高さが低く、部品が干渉しそうだったのでPC/104用を使いたいのですが合いません。仕方がないので64ピンを加工して50ピンにしてしまいます。あと、コネクタが全て部品面に生えているのでユニバーサル基板に載せたときに裏返しになってしまうのがイマイチなところです。見栄えの問題だけ出なく、メモリ構成切り替え用のDIPスイッチやリセットボタンが裏を向いてしまい、操作性に欠けます(だからリセットだけは簡単にできるように、外付けでボタンを取り付けたのです)。
 結局、回路規模の割には時間をかけて、一週間ほどで配線しました。その間に、シリアルEEPROMはパラレルポート経由で接続することへの変更もおこないました(同期シリアルポートの信号でいけると思ったのですが、特にスタートとストップのビットがそのI/Fで動くとは思えなくなったので、タイミングの変更が容易なパラレルポートに変えたのです)。
 なお、出来上がりの写真は後日に。


この回路あってる?

 あと困ったのは記事の回路にかなりのバグがあるということ。上のほうで「デコードがおかしいような気がしたもので…」とか書いてますが、特にアドレスバスについては開放されているものが多く、「NICを300hに設定すること」とあるのにどう考えてもうまくいくように思えません。CQ出版社のホームページにも修正情報を含むプログラムソースなどが配布されたのですが、それでも足りません。
 この記事を書いた船田悟史氏PRUG(Packet Radio User's Group)のメンバーのようで、プロジェクトであるPRUG96では氏のページにて記事と似たような回路とドライバを発表されています。当初これを読むことで解決しようとしましたがこちらはアドレスの上位をパラレルポートの出力で決定しているなどさらに違う方式になっていました。思い余って氏にメールでお尋ねしたところ、やはり間違いとの事で以下のように修正すべきと指摘していただきました。

「SA10からSA19はGND, SA8とSA9はVcc,SA5とSA6はGNDにつながなくてはなりません。
このほかにも、IOCS16はプルアップ、MEMRとMEMWとSMEMRとSMEMWはVCC、
62256のCSはZ80のA15をインバートしたものを接続、…」

トラ技誌99年8月号ではこの訂正のうちアドレス周辺のもの以外(つまりホームページにて公開された訂正分)だけしか掲載されませんでした。最初PRUGと船田氏の関連性を知らなかったので、もしそのままなら確認できずに自分の満足する回路にしていたことでしょう。


NE2000のプロービング

 組み立てが終わったら、いよいよソフトの本格的な開発です。元々アセンブラで書いてあるプログラムを同じ動きのC言語に手で翻訳していきます。ソースはCQ出版社のホームページからもらってきたものです。一通り移植したあと(といっても動作確認はまだですし、UDPについては拡張しようとしているので中途半端だったりしますが)まずはハードのチェックです。
 このドライバは初期化する前にNICの存在を確認し、その後MACアドレスを読み出します。これが動けば、あとはほとんどハード的な間違いの可能性は低くなります。これを使って、ハードのデバッグも兼ねようということなのです。
 まずは電源を入れます。LINKランプが点灯しますが、これは初期化が済んでないためでしょう。とりあえず燃えるとか煙が上がるとかないようですし、開発ツールにもSフォーマットローダのメッセージが表示されていますので大丈夫のようです。
 検査ルーチンだけ呼び出すプログラムを作り、開発ツールを通して送り込みましたが最初はさも当然のように動作せず。配線を調べたところデコーダに*RDが入力されていなかったことがわかり、これを直すと様子が変わりました(デバッグ用に読み出しデータなどを表示するようにしていたのですが、'e1'とか出てたのが'0'などに変わったのです)。しかし、検査用にバッファに書きこむデータが全く読み出せず、検査ルーチンとしてはエラーになります。
 一応配線などをチェックしますが、アドレスに関しては固定であり間違ってない様子。それにNICのジャンパをいじってアドレスを変えると反応も変わるので、少なくともデコードはされているようです。もう少し動作の様子を探ろうとロジアナも取り出したのですが、使用頻度が少なくてイマイチ使い方を憶えていないうえに、分解能が20MHzのために満足に波形を取り込めないようです(CPUの動作速度も20MHzですし。分解能としては40MHz欲しいところですね。でもまさかZ80関連の開発で実クロックが20MHzに乗るとは…)。
 ふと、実際にバッファから読み出されるデータを確認しようとデバッグ用printfを仕込んでみると、表示される値はなんとなくMACアドレスみたい…。MACアドレスはバッファメモリのある空間の0番地から並んでいますので、アドレス指定がうまくいってないみたいです。で、読み出しルーチンのパラメータを見ると…型を間違えていました(^_^;)。これを直すと、うまく検査ルーチンを通るようになりました。


NE2000の初期化…?

 検査のあと初期化をするのですが、初期化ルーチンはレジスタ設定のみで特に外部に対するアクションとかレジスタのリアクションとかはありません。一応初期化ルーチンを入れて走らせてみましたが何の反応もなし…。LINKランプだと思っていたLEDはマニュアルの間違いでどうもACTランプのようですし(でも点灯しっぱなしなんですが)、まぁ検査はできているのでレジスタ設定くらいは大丈夫かな?


データリンク層とARP

 手順としては物理層にあたるNE2000アクセスルーチンを確認してから、上位層へ…というのが本当のデバッグなんでしょうが、あらゆる階層のプロトコルによる送受信のできるツールなんて手持ちにはありませんでしたから、思いきって元の記事のプログラムに相当する機能を全て入れ込んでしまおうということにしました。
 で、各モジュールのコンパイルをしてみると…出るわ出るわ、コンパイルエラーの山(^_^;)。それどころか、エラーの数なのか種類なのか、コンパイラが落ちてしまいます(-_-;)。YIDEという統合開発環境があるのですが、コンパイラ自身のエラーによりエラーもワーニングもなかったかのように表示されてしまいます。仕方がないのでコンパイラのエラーが出なくなるまでコマンドプロンプトでデバッグし、それからYIDEに移行するようにしました。それと、思った以上に自分のC言語の知識がないことを痛感(^_^;)。static宣言とextern宣言をごちゃ混ぜで考えていたりとか、やはりX68k用Cコンパイラのマニュアルでは限界があるようです(X68kのはANSI仕様ではないし)。そこで、タイトルは恥かしいがベストセラーの技術評論社「はじめてのC」を購入。さすが、初版から数えて15年もの間売れつづける定番だけあって、とてもわかりやすく書いてあります。
 コンパイルとリンク(YIDEで作業すれば自動でやってくれます)ができたら、早速走らせ、Win98からpingを試しますが…やはり反応なし。いきなりのpingならまずはARPでアドレスの照会をするはずですので、まずはどのようなプロトコルIDを受け取っているのか表示させてみることにしました。すると、なんかよくわからないID。実験中のネットワークにはIPXで通信しているプリンタサーバがありますので、それがDIX仕様でなくIEEE仕様のプロトコルを使うのならそのようなIDもわからなくもないですが、どのパケットもIPでもARPでもないようです。
 では受け取ったパケットの中身はどうだろう?ということでパケットをダンプするルーチンを仕込んで再度挑戦…ん?これはMACアドレスではないか(さっきと変わらんがな)。どうも、パケットを受け取る際のアドレスの指定がおかしいようです。内部バッファのポインタを読み出し開始アドレスとして与えるのですが、それはアドレスの上位8ビットに相当するのにそのまま与えてしまっていたのを直したところ、パケットの認識はできるようになりました。が、pingは通らない…。
 そもそも、ARPに対する返事はできているのかということで、別のマシンのLinuxを立ち上げ、tcpdumpにてパケットを監視。するとやはり、ARPの問い合わせパケットは表示されるのに返事のパケットはありません。パケット送出側のソースを眺めていたところ、パケットをきちんと内部メモリに書きこんでないことが判明、これを修正してようやくARPの返事がtcpdumpの表示に出るようになりました。


Ping

 が、相変わらずpingには反応がありません。Linuxでのpingも同様。でも、その後のarpコマンドの出力ではIPアドレスとMACアドレスとの関連付けができているようなので、ARPパケットとしては正しいようです。
 tcpdumpをアダプタのMACアドレスだけ監視するようにしてpingを試すと、どうもパケットとしてはICMPエコー要求と返答のパケットは流れているようです。でもpingコマンドに結果が現れないことから、受付側でパケットが廃棄されているのだろうと考えました。廃棄される理由はいくつかあるのでしょうが、チェックサムが怪しいです…。
 そこでtcpdumpにパケットの内容を16進ダンプするオプションをつけてみました。これでIPパケットの先頭から64バイト読むことができます。チェックサムが合っているかどうか見つける前に、なんだかパケット長が怪しいのを発見しました。ICMPエコーの要求と返答では同じ内容を返すことになっていますので、それぞれでパケット長が違うのは変です。ソースを調べると…どうも特定のケースにおいて、引き算が足し算にされてしまうようです…。コンパイラのバグかなぁ…。そこは関数のパラメータとしてA-Bというのを書いていたのですが、あらかじめA-Bを計算したものをパラメータに与えることで解決しました。
 さて、チェックサムですが、とりあえずIPパケットのものだけ注目してみます。要求と返答でTTL(Time to Liveかな?ネットワークでのパケットの生存時間で、ゲートウェイを通るたびに1引かれます)の値を合わせて(Linuxの40hに対して、こちらはFFh。ちなみにWin98は20hでした)サムの比較を取りやすくしたところ、同じパケットに対して要求より応答の方が2多い…。計算方法を確かめても、どうも不明。そこで対症療法的に2を減じることにしました。こんなんでいいのか?!いいわけないよな…。
 でも、これによってLinuxのpingが通るようになりました。なるほど、ちゃんとチェックサムを計算してたんですね(って、当たり前か)。返答結果を見ると、200msぐらいはかかってるみたい…遅いかも。
 ところが、Win98からのpingは相変わらず通りません。なんでかな?もしかして速攻で返事を返さないと受け取ってくれない、つまりは200msかけて返事してもだめなんですかね?うーむ。

 …と思ったら、IPではなくICMPのパケットのチェックサムがおかしいのを発見。16ビットから桁あふれした数は下16ビットに足して、その16ビットで1の補数を計算するんですか。これでWin98からもpingが通るようになりました。返答に200msかかるのも、デバッグ用にprintfを入れていたためで、これを抜けば19ms程度に速まりました。ウェイト調整とかもやればもう少し速くなるかもしれないですね。


割り込み駆動

 元の記事と同様の目的に使うのならあとはUDPだけ確認すればいいのですけど、どちらかというとモデムとして動いている方が主になる機械です。みんなポーリングというのでは、うまく動いたとしてもパフォーマンスの点で期待できません。そこで、イーサネット受信については割り込みで動かすこととします。この時点ではポーリングで動いているルーチンを、割り込み処理ルーチンとして定義してあげればいいはずです。
 割り込み処理を実現するには、コンパイル処理(レジスタを退避するとか、RETIで帰るとか)について特別な処置が必要です。具体的には引数なし・帰り値なしの関数を「interrupt void」で宣言してあげます。割り込みベクタなどの定義は、スタートアップルーチンを記述したアセンブラソースがありますので、それを修正することになります。こういうものがあるというのが、やはり組み込み用のコンパイラらしいところですねぇ。
 スタートアップルーチンに割り込みベクタ、割り込み用端子の設定、割り込みコントローラへの割り込み許可の設定を書き込んで、mainルーチンをポーリングから割り込み待ちに変更(何もしない永久ループに変わっただけですが)して、再コンパイル…動かない。
 割り込み処理ルーチンの先頭にprintfを仕込んでみても無反応ということで、そもそも割り込みしてないということがわかりました。割り込みをエッジセンスからレベルセンスに変えても同じ。割り込み入力端子をテスターで測ってみると、割り込み時"H"ということがわかったので"L"にしていたのを修正しましたが結果は同じ。マニュアルとソースを代わりばんこに眺めてもらちがあきません。
 「CPUがIACKを返してるかどうかわかれば、受け付けてるかどうかはわかるのに…」と思ったその時、肝心なことを忘れていたのに気づきました。なんと、割り込み設定終了後に、CPUの割り込み許可(「EI」命令)を実行していなかったのです。これを入れると、ようやく割り込み処理ルーチンまでは飛ぶようになりました。やれやれ、困ったもんだ…(^_^;)。
 割り込みがかかるようになったものの、今度は割り込み処理が終わってもずっと割り込みがかかりっぱなしになり、割り込み処理ルーチンで永久ループになっています。受信ルーチンで割り込みステータスレジスタをクリアしてあったと思ったのにな、レベルセンスでは相性が悪いのかな?ということでエッジセンスに変えたところ、最初の割り込み以外受け付けられなくなりました。やはりこれは割り込みを出す側が要求を取り下げてないということなのでしょう。ソースをもう一度よく見たところ、確かに受信ルーチンでは割り込みステータスレジスタをクリアしていませんでした。これを加えて、ようやく割り込み駆動でARP&pingが通るようになりました。


バッファリングとフラグメント

 ではこのあとTCPに備えてRS-232Cを動かすようにしようか…と思ったその時、tcpdumpなどの日本語訳について教えてもらったtreeさんから、こんな指摘が。

「記事のプログラムどおりだとパケット入力から上位層まで全部呼んでるけど、それで間に合うんだろうか?」

 ふむ、確かに今のプログラムでは、例えばpingによるICMP要求があったとすると、

受信→MAC層入力→IP層入力→ICMP層入力→ICMP返答作成→IPフレーム作成→MACフレーム作成→送信

の一連の動作が全て割り込み処理として実行されてしまうわけですね。割り込みは一度入るとさらなる割り込みは禁止されますから、この間の受信はできなくなることになります(正確には一度だけなら割り込みが遅延するだけなんですが、さらにもう一度入力があると取りこぼすでしょう)。できれば割り込み禁止区間は「受信」だけにしておきたいものです。
 同時にtreeさんは、こんな指摘もしてくれました。

「TCPを実装するなら、IPフラグメントにも対応しないと…。8KBぐらいに対応できれば十分とは思うけど」

 うーむ、ややこしそうなので避けたかったんですが、やっぱりやらないといけないですかねぇ。UDPならともかく、TCPの方がデータグラムを短くしているんじゃないのかな?とかいうことでなくてもいけるかとも考えてたんですが…。
 それでは、各層にバッファを持たせ、そのバッファでもってフラグメント処理をさせることにしましょうか。


作りなおしに近い…

 バッファ導入にあたり、ちょっとリアルタイムOSのような考え方も取り入れることにしました。といっても大げさなものではありませんが。
 各層の処理ルーチンは、下位の層から取り出されたフレームを処理し、より上位の層へその結果を渡すような構造にします。処理といっても上位層別にフレームを振り分けるものからARPやICMPの応答動作までいろいろあります。で、各層では上位層へ渡すデータがあったとしてもその場では呼び出しません。代わりに、結果を残すバッファ(これが今作ろうとしているバッファなんですが)に蓄積したあと、そのバッファに上位層で処理していないデータの量を変数に記録しておきます。メインルーチンでは、その変数が0でない、つまり処理すべきデータが発生したのを判断して処理ルーチンを呼び出します。
 これを、結果を蓄積するバッファを「メッセージバッファ」、各層の処理ルーチンを「タスク」と見なせば、各タスクがメッセージ受信を待ってディスパッチされているイベントドリブンのリアルタイムOSと捉えられなくもないわけです。メインルーチンではイベント待ちしている各「タスク」をずらずら並べるのですが、この並び順がまんまタスクの「優先度」になります。while(1){〜}の永久ループでぐるぐる回すときに、各処理ルーチンをただ並べれば優先度が「ラウンドロビン」で回転し、continue;で頭へ戻ればラウンドロビンなしということになります。とりあえず当面はラウンドロビンにしておきます。
 バッファはそれぞれの層ごとにそれなりの容量が必要でしょうが、フラグメント処理用以外はリングバッファとして構成します。シリアルポートのものだとバッファの残りが足らなくなったら送信禁止(受信拒否)を発するのですが、とりあえずは無しです。それが必要になったら検討しましょう(ったってRTSがあるわけでなし。TCPのウィンドウぐらいしかそんな制御ってないですよね)。
 というわけで各層の処理ルーチンを改造。これまで下位層の受信ルーチンからパケットの先頭アドレスとサイズをもらっていたのを、全て専用バッファ経由に変えます。プロトコルだけ見てても大きさがわからないものもあるので、先頭にサイズを書き込んでおき、処理ルーチンであらかじめ取り出すようにします。あ、念のために説明しますが、リングバッファには読み出しポインタと書き込みポインタの両方を設けて、書くたびに書き込みポインタを進め、読むたびに読み出しポインタを進めます。RS-232Cだと処理単位が1文字なのでそれだけでいいのですが、この場合はパケットなので何バイト処理しないといけないかの指標がいるのです。まぁ総蓄積バイト数を保持する変数までは必要なかったかな…。
 バッファは各層毎に設けるのですが、TCPとUDPについてはフラグメント処理用とアプリケーション層処理用の二つを用意しました。各層のバッファのサイズもあまり根拠がなくて、フラグメント処理用に8192バイト、アプリケーション層用に8192バイト(フラグメント処理されたものはこちらに転送されるので、これくらいの容量が必要になるはず)、その他はたいてい4096バイトとしました。まぁでもICMPに1024バイトしか用意してないのにARPに4096バイトってのはやりすぎですよね…。いずれ512バイトくらいにしておくかな。ちなみに、バッファサイズが2の倍数なのは 

ip_buf[ ( ip_buf_rp + i ) & 0xfff ]

とかやると、forループなんかの中で転送ルーチンを組んだときにポインタがバッファの外へ飛び出さなくて便利だからです。それと、出力用(というかフレーム作成用というか)にはそれ用のバッファを用意して(といっても最上位の層にひとつという感じですが)、各層の処理ルーチンでつぶしあわないように配慮しました。また、NICのバッファに書き込む段階では受信割り込みでレジスタが狂う恐れがあるため、送信終了まで割り込み禁止としました。
 フラグメント処理は、フラグメントのオフセットかフラグが0でない時に行うこととして、それ以外はTCPかUDPのフラグメント処理済バッファへ直接書き出します。フラグメントのあるパケットが来たら、

と処理することにします。フラグメントしているパケットなのに識別コードが異なればそれは送信元で破棄され改めて送り出されたものとして扱う(二種類のフラグメントパケットが同時に来るとは想定しない)とか、ICMPはフラグメントされないとか、期待している仕様もあります。

 で、完成したら早速デバッグ。NE2000アクセスルーチンは修正の対象ではありますが、一度動いたという安心感はあります。でも…動かない。バッファに書き込む内容が統一されてないのでそのパラメータの受け渡しに絡んでミスが発生して、とんでもない内容のデータを渡そうとしたり、そもそも変数名を間違えていてあらぬところに書き込んでいたりとかしましたが、MAC層受信から確認しなおしてようやくpingまで動くようになりました。うーん、作りなおしとは言わないにしても、かなり工程が後退してるよなぁ。
 Win98のpingでは4回しか実行しないので、リングバッファがちゃんと働いているかは確認できません。そこでLinuxで連続pingを実行してみます。…すると、なんと突然応答パケットが消失します(;_;)。また動き出したりしてなんだか様子がおかしい。そこでMAC層のバッファの受信バイト数と書き込みポインタの値を表示させてみると…4096バイトを超えようかというところで表示がなくなります(T_T)。それどころか、どうももっと少ない回数でpingに応答しなくなります。ICMP層に渡すデータはどうなっているのか…と確認したら、なんと1024バイトに制限してあるはずのICMP層バッファに対して、ポインタの修正用の計算は4096バイト想定になっていました。これではあらぬところに受信ICMPを書いてしまい、動くはずもありません。これを修正すると、明らかに1024バイトを越えた受信バイト数でも動くようになりました。でも4096バイト取り込んで止まる…。
 NE2000の内蔵メモリにある受信バッファはリングバッファになっていて、ページをまたいでいるかどうかを判定し適宜それに対応しないといけません。それを読み出して格納するMAC層バッファもまたリングバッファであって、ページ単位とバイト単位の扱いの違いによってその管理は同期するはずもありません。そこでそれぞれの読み出し・書き込みポインタが境界をまたがないかそれぞれチェックしながら読み出すというなかなか複雑なことをしていました。工夫していたはずだったのですが、見落としがありました。
 NE2000に対しては16ビットの幅のデータとして取り扱っています。MAC層バッファが境界をまたぐとして、それが奇数バイトだったときに、果たしてちゃんと読み出してくれるのでしょうか?それを考えると、とても不安になりました(^_^;)。だって、以前のプログラムなら奇数バイトの読み込みだったとして1バイト余計に読んだってそれはなかったことになるんです(読み出しバイト数としてカウントされないから)。でも、MAC層バッファが境界をまたぐときは読み出し動作が二分割され、しかも前半が奇数バイト読み出しだったときに余計に読んでしまう1バイトというのは捨ててはいけないデータです。ということで、MAC層バッファに読み出す前に緩衝用として一段テンポラリのバッファに読み出すことにしました。これで連続動作に耐えるプログラムとなりました。一応これを書いてるウラで1万回ぐらいの連続pingをやってますが、なんとか大丈夫のようです(ってその直前の1800回くらいの連続pingでは7パケット消失、5秒くらい応答できなかった瞬間があったようです。1万回チェックでは発生しなかったから、様子を見ることにしましょう)。
 でも、使用メモリがどかーんと増えちゃったなぁ。AKI-80での実装は無理なんかな?せめてMMUのあるCPUをと考えたときに、雑誌にあるKL5C80A12ボードって、14000円ぐらいするのに気づきました。このKC160ボードと変わらんぞ。


鬼門のシリアルポート

 次は、TCP/IPから離れてシリアルポート周辺を整備します。これはその次にTELNETサーバを入れて、PC側からのアプローチのみではありますが目標としている通信ができるようにしようという目論見があるからです。
 KL5C16030には2つの非同期シリアルポートがあって、このCPUボードではどちらも使えるようにバッファなどがついていて、コネクタもそれぞれに用意されています。このうちポート0はデバッグ用として、また作成したプログラムのアップロード用として既に使われています。そこで今回の設計には空いているポート1を使用します。空いているだけに完全に一からドライバを作らなければなりません。実は昔、MZ-2500で割り込み駆動によるターミナルプログラムの製作に挑戦したことがあったのですが、どうしてもうまく動かすことができませんでした。手本のあるEthernetより、より一般的なシリアルポートのほうが私には難しく感じられてしまうのです。
 と悩んでても完成しませんので、とにかく作ることにします。作ったプログラムは、受信データを割り込みによってバッファに取り込む受信ルーチン、送信データをセットして割り込みをセットする送信ルーチン、シリアルポートの初期化ルーチン、受信可否を相手に通知するルーチンなどです。受信ルーチンには受信バッファが満杯近くになったら受信拒否を通知させるようにします。「満杯」ではなく「満杯近く」なのはXコントロールによるフロー制御の場合自ルーチンでの検知から拒否発効までタイムラグが発生する可能性があるからです。送信ルーチンではまだ送信可能でないのに送ろうとした場合送信バッファに溜めこむようにします。割り込みで送信バッファの文字をひとつずつ送り出し、バッファが空になったら割り込みをリセットします。あとこれらのドライバルーチンと共に、テスト用に単純にエコーバックするルーチンも作りました。これは前述のネットワークドライバと同様にイベントドリブンで駆動するよう、メインルーチンに並べることにします。
 できたところでテスト。テストには対向するMZ-2500とモデムケーブル、それとラインモニタを準備しました。ラインモニタといっても信号の状態が赤か緑で表示される簡単なものですが、たったこれだけのものでもケーブルが正しく接続できているかなど判別することができ、重宝します。
 まず最初はRTSが変化しない(リセット時はoff)ことで悩みました。RTS/CTSに限らずいろいろな機能の信号がパラレルポートと多重化されていて、パラレルポートにするのか専用端子にするのかの選択を初期化ルーチンで設定する必要があります。もちろんやっているつもり(それもNICの割り込みができるようになった時に)なのですが、どうもマニュアルが読みづらく、CTSと共有するパラレルポートを入力にしろとは書いてありますがRTSと共有するものはどうすべきか書いてありません。回路構成にもいろいろあって、例えば昔仕事で触ったモトローラのMC68360では専用端子が出力の機能をもつならパラレルポートも出力の設定をしなければなりません(間違えて記憶してたらスマン)。そこでRTSと共有するポートを出力にしてみましたが、事態は好転しません。
 ふと、ヘッダファイルにあるキーワードを見ると、RTSの制御ビットの位置を間違えているのに気がつきました。でも、これを直しても動きません。なんとなくさっき出力に書き換えたRTSの共有ポートを入力にすると、これでやっと動きました。動かなかった原因は違ったんですけど、パラレルポートの出力とぶつけてしまうとやっぱり正しく動かないんですね。
 これでMZ-2500から送信できるようになったはずなのですが、うまくエコーバックされません。入力した文字につれて変化はあるようなのですが、完全に化けています。そこでさっき作った受信ルーチンに受信した文字を表示するように書き加えてみると、こちらでも化けています。受信の方で既に化けているということで、次に初期化ルーチンを疑いました。見ると…ボーレート設定ルーチンが変です。単純に与えられたボーレートを元に計算で設定する値を決めていたのですが、この計算方法が間違っていて(^_^;)、ボーレートの設定自体できていないようになっていたのです。ここはもう簡単にswitch〜caseで値を決めるように改造すると、ようやく文字がエコーバックされるようになりました。
 次に大量のデータの送受信をテストします。テキストのアップロードをしようとディスクの中身を覗いてみると、優音デバッキングローダを作ったときのドキュメントがありましたのでこれを使うことにします。
 テキストのアップロードは途中までは問題なく動作します。ですが、MZ-2500が次のデータを送るためにディスクにアクセスすると、そこでフリーズしてしまいます。ラインモニタを見るとRTSがoffになっていますので、受信バッファがいっぱいになったのだろうと思われますがCTSはon状態なので送信可能なはずです。送信動作が止まっているということは送信するデータがないということではないかと思いますが、単純エコーバックなので受信バッファがいっぱいの状態で送信データがないというはずはありません。
 この問題の解決に半日もかけてしまいました(暑いから考えがまとまらないのかも。ウルトラセブンのDVDを見ていたというのもあるが…)。単純エコーバックは本物のアプリケーションプログラムではないんだし、当面はこれでいいかも…とかいって納得しかけましたがなんとか踏ん張って解決することにしたのです。
 とにかくデッドロックする条件があるはずです。1文字送信ルーチンでは、送信バッファが空でCTSがonなら送信レジスタにデータをセットし、割り込みを許可します。送信バッファが空でないなら文字が送信待ちで溜まっているということですので、今送ろうとした文字をバッファに追加します。送信割り込みがかかると送信バッファの残りを調べ、まだあるなら送信レジスタに新たな文字をセットし、ないなら送信割り込みを禁止します。割り込みがかからなければバッファに文字があっても送信しませんので、そういう状況になる条件を探せばいいのです。フリーズした時のことを思い出すと、答えが見えてきました。送信バッファが空でもCTSがoffなら送信バッファに文字を溜めるのですが、バッファに溜めるところでは送信割り込みを許可していません。バッファに溜める状況というのはそれ以前に割り込みを許可してあるものという思いこみがあったのです。そこで、バッファに溜めるときに同時に割り込みを許可するように変えてみました。すると、MZ-2500のディスクアクセス後も送受信動作を続けるようになりました。

 ちょっと試しに、連続pingと同時にシリアルルーチンを動かしてみましたが…さすがに化けますな。試しにバッファを増やしてみましたが、おかしな化け方(同じデータが繰り返し出力される)をするようなのでやめました。そろそろデータが64KBに収まらなくなってきたようです。まぁ、これは完全に関連性なくして動かしていますから、今後問題が大きくなるまで置いておくことにしましょうか。


うーん、間に合わないか(って何に?(^_^;))

 シリアルも整備できたことだし、ではTCPを…と取り掛かったものの、いろいろ考えると山の大きさに比べて残り時間が少ない。この残り時間というのは、いわゆる「夏コミ」、コミックマーケット56までの時間のこと。といってもコミケに直接出店するわけではなくて、「サークル星くずばこ」の出店に合わせて開かれる「MZ-MLオフ会」で披露しようという魂胆なだけですけどね。やはり何らかの通信をしているという事実を見せられるほうがインパクトがあるわけですが、もちろんTELNETまで実装するとNIFTYアクセスデモなんてのもできるわけで非常にいいんですが、間に合わなさそうならば方針を変える必要がでてきます。
 そこで当初は予定になかったTFTPを実装することにしました。TFTPはFTPと見た目は似てますが、FTPがTCPを使うのに対してTFTPはUDPを使うという違いがあります。同じファイル転送に関するプロトコルなのですが、接続に際していろいろな部分で保証をするTCPに対して送ったら送りっぱなしのUDPを使うだけに、相当簡易なものになっています。またUDPならデバッグはしていないものの元の記事にも実装されていたものですし、これが実装できればFTPの雛型としても使えるでしょう。

 …が、結局これも見送ってしまいました。いや、かなりの部分は作り込んだのです。モデムとして振舞うATコマンド処理部もTFTPクライアント起動部分だけですが作りましたし(他のコマンドはここからの拡張だけで作っていける)、TFTPクライアント部もGET/PUT処理部以外は作りました。しかし、今の構造では破綻しそうだという予感が頭をもたげ、細かい処理の解決方法が見出せずに、方針の大転換を図るべく今の構造のプログラムでの開発を凍結することにしたのです。
 直接の原因は、ARP処理部にあります。元の記事のようにサーバに徹するなら、ARP処理部は受けたパケットについて自分のARPテーブルに溜めこんでいくだけで良く、自分が何らかのIPパケットを送るときは必ずそのIPアドレスに対応するMACアドレスがテーブルにあるのでわざわざARP要求を出す必要はありません。しかし、クライアントとして接続要求やらデータ送信やらする時は、今までパケットを受けたことのない相手である可能性は非常に高いと考えて差し支えないでしょう。つまりARP要求が必要なのです。
 ここまではごく自然な話で、実際それに備えて自分が出したARP要求に対する応答にある情報をARPテーブルに登録するルーチンは作ってあります。問題は、ARP要求をどのような場面で出すか、です。Ethernet送信ルーチンは、送り先として指示されたIPアドレスからMACアドレスを割り出してEthernetヘッダに書きこむためにARPテーブル探索ルーチンを呼び出します。このルーチンの返り値はMACアドレスへのポインタです。ルーチン内ではまずテーブルからIPアドレスを参照し、指示された物と同じならテーブルにあるMACアドレスのアドレスをポインタとして返すわけですが、なかった場合はARP要求を送信しMACアドレスを得た上でそのポインタを返すようにすれば、呼び出した側では求めるMACアドレスがテーブル内にあろうとなかろうと関係なくすっきりとしたプログラムが書けるわけです。
 ではARP要求に対するARP応答が得られるまでの間、どうしていればいいのでしょう?当然その応答で判明するアドレスを使うEthernet送信は止めないといけません。ただ止まっているだけでは他の処理ができませんので、メインルーチンに戻らないといけません。なぜなら、ある処理で発生した上位へのデータの存在を確認してその次の処理を起動するのはメインルーチンの役割になっているからです。処理と処理の間にバッファを設けて独立性を高めましたが、その独立性を維持するためには各処理への分岐を一ヶ所で制御する必要がありますからね。しかし、メインへ戻るというのは本来ひとつの処理が終わったことを意味します。各処理へ分岐すると、その処理の頭から実行しなおします。ですから、処理が途中なのにメインへ戻るというのは都合が悪いんです。実は現時点ではもっと良くなくて、送信ルーチンの各層の独立性が低く、アプリケーション層からデータリンク層まで一気に駆け下る構造になっているので途中で止められないのです(送信処理が終わったら一気に各層のRETで駆け上がるので、メインに直接戻るルーチンはアプリケーション層ということになります)。これを各層毎に独立させると少しはマシなんでしょうが、ARP絡みで処理を途中で止めるというのには変わりありません。これを解決するにはEthernet送信ルーチン専用の状態遷移管理変数を用意して、(1)初期状態、(2)ARP待ち状態、(3)ARP終了状態についてルーチンの先頭で判断して内部で分岐することになります。
 破綻を予想したさらなる原因は、今述べた状態遷移管理変数はグローバル変数として定義しないといけないというのが挙げられます。TCPについては元々状態遷移を管理しながら接続やら通信やらしないといけないのはわかっていましたが、それを含めた全体の状態遷移や、他の部分の状態遷移などが複雑に絡み、実はいまだに状態を全体で管理すべきかプロトコル毎など部分的に管理すべきかわかっていません。なんとかこのあたりの管理を楽にしたいという希望があります。
 そこで、「本物の」マルチタスクOSを採用して、タスクのディスパッチを利用すればこれらの問題を解決できそうな気がします。つまらない状態遷移はディスパッチされる条件をもって代用できそうです。実はすでにいくつかのルーチン(例えばUDPのエコーポートとか)はARP要求を必要としないような工夫をしていたのですが、それももう少し簡単になりそうな気がします。

 ということで、とりあえず今のプログラムからエラーが出ないようにして、フラッシュROMに書き込んでしまいました。ROMに書くにはROM用セットアップルーチン(RAMに書き込んでた時のものとほとんど同じ)とROM用セグメント定義ファイル(コードセグメントとかデータセグメントの領域を記述したもの)を用意して、コンパイルしなおし、専用書きこみソフトで書けばできあがり。pingにだけ反応する妙な箱ができあがりました。


ここで頓挫か?!

 ということで、仕切り直しです。私がマルチタスクOSとか言い出すとμITRONしかないわけですが、今のところタスクのディスパッチができればいいんじゃないの?という程度の設計方針だったりします。あとメッセージバッファかな。ハードを叩くルーチンをそれぞれひとつに絞ればセマフォはいらないですし(日経エレクトロニクスの短期連載で知った。デバイスはセマフォで管理するんじゃなくて、デバイスドライバに握らせっぱなしにしておき、デバイスドライバへメッセージバッファを通して作業を依頼するのがスマートなんだとか)。とにかくスケジューリングのためだけに使う(ってリアルタイムOSはそれを除くと何も残らないという話もあるが)ので必要最小限でいいわけです。
 これまでのプログラムはここで公開します。って誰に対して公開しているのかわかりませんが、一応ドキュメントも入れてありますし、何かの参考にしてください。本当は回路図なんかも入れるのがいいんでしょうけどねぇ。
 さて、これでこのプロジェクトは一旦クローズです。この後どうなることやら…。


ところで

 せっかくいい記事が出てきたのに、肝心のISAバスのNICが数を減らしてきています(私の分は確保してあるのでいいんですけど)。いいものができても、人に広められないかもしれないです。それとも、簡易なPCIコントローラでも作らないといけないのでしょうか…。
 それと、Neptune-Xで有名なShi-MAD氏のページによると、NE2000(というかコントローラのDP8390かな)はPC/ATで拡張されたコネクタにある"IOCS16"を開放すると8ビットバスモードになるらしいんですけど、だったら16ビットデータを8ビットに畳み込む回路なんて不要じゃないんでしょうかね?今NE2000コンパチボードってどうせREALTEK社のRTL8019ASとかしか載ってないんだし、互換性に問題はないでしょう。それにI/Oは8ビット単位にしかアクセスできないんだから、パフォーマンスは変わらないような気がします。次作る時はそうしようかな。


戻る