Here is a English version.

カフェのテーブルの上、ラテとワッフルの横で起動しているDOSCON

前回、本物のV30 CPUをRP2040の「ゆりかご」に乗せてMS-DOS (HIDOS) を起動した。

その記事の最後に「今後の展望」として3つ書いた。

  1. ディスク書き込みができるようにして、マシン上でソフトウエア開発をしたい
  2. RP2350にアップグレードしてメモリを増やしたい
  3. 専用の画面とキーボードをそなえたポータブル端末にしたい

今回、これを全部やった。名前は DOSCON

今回も本物のV30チップが乗っている。単4電池2本で動き、電源を入れると数秒で A> プロンプトが出る。LCDとキーボードが付いていて、単体で日本語の読み書きもできるし、vi (自作) でプログラムを書いてアセンブル・コンパイルして実行、までがこのマシンの中で完結する。

基板 (KiCad)、ケース (3Dプリント)、ファームウエア (C++)、中で動くDOSツールまで、フルスタックで必要なものを自作した。

動機

前作のマシンは、外部との接点がUSBのみだったので、別途PCが要った。でも、V30の何百倍も速いPCを隣に置いて、それをただの端末として使うのは、どうにも本末転倒だ。V30が主役なのだから、V30だけで完結してほしい。だから今回は、スタンドアローンで動作する事を目指した。

そして、スタンドアローンにするなら形は決まっていた。小学生の頃、ゲームボーイを見ながら「これのコントローラの代わりにキーボードが付いていて、プログラムが書けたらいいのに」と夢想していた。あの頃使っていたPC-98に入っていたV30と、あの頃使っていたCコンパイラを、あの頃欲しかった形の箱に入れる。つまりこれは、30年越しの答え合わせだ。

前作から何が変わったか

CPUまわりの構成は前作と同じ「ゆりかご」方式だ。本物のV30がバスマスタで、RP2350がメモリ・I/O・周辺回路のすべてを演じる。RP2350にはコアが2つあるので、分業させている。

  • core1: V30のバス制御専用。ALE/RD/WRを監視して、バスサイクルに合わせてメモリの中身を出し入れする
  • core0: I/Oハンドラ、画面、キーボード、USBの面倒を見る

ケースの丸い窓から見える本物のV30チップ

主役なので、ケースの窓から本物のV30が見えるようにしてある。

前作からの本質的な違いは3つ。

  1. スタンドアローンでバッテリ動作する。 PCに繋がなくても、単4電池2本で完結したマシンとして動く。画面もキーボードも付いた
  2. RAMが増えて、Cコンパイラが動く。 前作の128KBから448KBになり、MASM/LINKどころかCコンパイラまでマシン上で動くようになった
  3. RAMディスクがある。 前作最大の宿題だった「作ったプログラムを保存できない」問題が解決した

前作ではクロックの調節などはPCから繋いだモニタプログラムで行っていたが、スタンドアローンになった今作では、それも本体だけでできるように設定シェルを完備した (後述のBootShell)。

DOSCONの全体像

+---------------------+
|  ILI9488 LCD 320×480|   縦長。テキスト 40×30 (8×16 半角 / 16×16 全角)
|  + 4×12 キーボード   |   ANSI.SYS 互換エスケープを内蔵
+---------------------+
            |
+---------------------+          +----------------+
|  RP2350 firmware    |   bus    |  V30 (実チップ) |
|   - HIDOS host      |<========>|  μPD70116      |
|   - USB CDC × 3     | AD0-15他 +----------------+
+---------------------+   core1がバスを捌く
            |  USB FS
+---------------------+
| /dev/ttyACM0  stdio |   DOS の標準入出力 (SJIS↔UTF-8 変換つき)
| /dev/ttyACM1  debug |   firmware のログ / モニタ起動
| /dev/ttyACM2  aux   |   COM1 / AUX (生バイト。YMODEM転送に使う)
+---------------------+
項目 前作 (cradle86) 今作 (DOSCON)
CPU 本物のV30 (μPD70116) 同じく本物のV30
ゆりかご側 RP2040 RP2350 @ 250MHz
メモリ 128KB 448KB + PSRAM 8MB
画面 PCにシリアル接続 内蔵LCD (40×30、日本語対応)
キーボード PCの物 内蔵47キー
ディスク ROMのみ (書き込み不可) ROMディスク + 4MB RAMディスク
ファイル転送 XMODEM (モニタ機能) YMODEM (DOS上のコマンド)
電源 USB 単4電池2本 (USB-Cも可)

ハードウエア

基板

届いたDOSCONの基板2枚。表にはDOSCONのロゴのシルク印刷

KiCadで設計して、RP2350 + ILI9488 + キースイッチ + USB-C + 電池ボックスを1枚に載せた。

前作同様、部品配置はPythonスクリプトでKiCad APIを叩いて生成している。キーマトリクスのような規則的な配置は、手で並べるよりコードで書いた方が楽だし、やり直しも効く。

失敗: SDカードの配線を間違えた

前作は「工場から届いた基板が全く動かない」だったが、今回の失敗はSDカードだった。基板にはSDカードスロットを載せていて、RP2350のSPIモジュールに繋ぐつもりだったのだが、クロックとデータが別々のSPIモジュールのピンに割り当たってしまっていた。RP2350のGPIOはピンごとに使えるSPIチャネルが決まっているので、これではハードウエアSPIとして使えない。

AIのおすすめのピン割り当てに合わせたら失敗した、という点で前作の「AIにデータシートのタイミングチャートは読めない」事件の続編でもある。一応ピン名を軽くチェックはしたのだが、見つけられなかった。AIの提案する配線は、ピンマルチプレクサの表ときちんと突き合わせるべきだった。

このミスが発覚したのは、ファームウエアを書き始めてからだった。基板はとっくに発注済み。基板を発注する前に、ファームウエアを先に作っておくべきだった。

幸いRP2350にはPIOがあるので、ソフトウエアでSPIを生やして救済できそうではある。これは今後の課題として、現状このマシンのストレージはSDカードなしの構成 (Flash上のROMディスク + PSRAMのRAMディスク + YMODEM転送) に落ち着いた。コードを書いてコンパイルするという目標は、SDカードなしで達成できた。

キーボード

4行×12列で47キー。面白いのは読み取り方で、普通のマトリクススキャンだと4+12 = 16本のGPIOが要るところを、「倍マトリクス」と呼んでいる配線で、10本のGPIOで読んでいる

10本を行4本+列6本に分けると、普通のマトリクスでは4×6 = 24キーしか読めない。そこで各交点に、ダイオードの向きを逆にした2つのキーをぶら下げて、「列を駆動して行を読む」「行を駆動して列を読む」の両方向でスキャンする。これで交点1つから2キー、合計48ポジションが読める。物理的には4行×12列に見えるキーボードの左半分と右半分が、電気的には同じ交点を逆向きに共有している形だ。

倍マトリクスの配線図。R1〜R4とC1〜C6の各交点に、向きの違うキーが2つずつ付く

なにしろV30のバス (AD0-19 + ALE/RD/WRなどの制御線) だけでGPIOを25本近く持っていかれる上に、LCDのSPIやPSRAMのCSもあるので、キーボードに回せるピンは限られていた。

電源

単4電池2本を3.3Vに昇圧して、RP2350もV30も3.3Vで動かしている。昇圧は市販の昇圧モジュールをそのまま載せた。「5V用のV30を3.3Vで動かしても動く」というのは前作で確認済みなので、今回は最初から3.3V一本の設計にした。

後から、リチウムイオン電池にしようかとも考えた。しかし、フル充電のリチウムイオン電池は4.0Vを超えるので、この構成では壊れそうだ。というわけで乾電池にしている。

電池2本を繋いでDOSが起動しているDOSCONの基板

電池側で消費電流を実測してみたら約250mAだった。単4のニッケル水素電池 (750mAhくらい) なら、単純計算で3時間くらいは動く事になる。もっと動かしたいなら、単3 (2000mAhくらい) にすれば8時間動くはずだし、2本を4本にしたら、更に倍の時間動くはずだ。

ケース

3Dプリント。モデリングはCADのGUIではなく、replicad用のJavaScriptでコードとして書いている。キーキャップも自作で、刻印は自作のスクリプトでSVGを生成して多色3Dプリントで入れた。コミットログを見返すと4月末〜5月はほぼケースとキーキャップの調整に費やしていた。

なぜケースにこんなに時間がかかるかというと、AIのエージェントループが回せないからだ。ファームウエアなら「AIが書く → テストが落ちる → AIが直す」のループを勝手に回してくれるが、ケースは印刷して、実物に合わせて、寸法を直して、また印刷して…のフィードバックを人間が身体でやるしかない。1周に数時間かかるし、テストも書けない。AI時代の電子工作は「ガワ」が一番のボトルネックだ。

3Dプリントした黒いケースとキーキャップに収まった完成形のDOSCON

HIDOSを電池で持ち歩く

動かしているOSは前作と同じ HIDOS だ。hdk先輩のHIDOSは、Microsoftが公開したMS-DOS 2.11のソースをセルフビルドできる環境で、「何でもできる仮想デバイスが1つ+それを呼ぶBIOSコールが1つだけ」というミニマルなVM構成 (hidosvm) が用意されている。

今回もforkをサブモジュールとして組み込んでいるが、前作で適当に動くようにした修正は、本家で正式に修正されたので、今のforkの差分はビルドスクリプトのカスタマイズだけ。HIDOSのコードには一切手を入れていない。VM_IO.SYSもそのままだ。ゆりかごがRP2040からRP2350になり、画面もキーボードも日本語も付いたのに、DOS側はノータッチで済んだ。「複雑さは全部VM側に置く」というhidosvmの設計の勝利だと思う。

この方式の何が良いって、BIOSも8253タイマも8259 PICもCRTコントローラも一切実装しなくていい事だ。DOSからのコンソール・ディスク・シリアルへのアクセスは、全部I/Oポート経由のシンプルなプロトコルになってRP2350に届く。V30がI/O命令を実行すると、バスを見ているcore1がそれを検出して共有フラグを立て、core0のホストループが拾ってコンソール/ディスク/AUXのハンドラに振り分ける。

メモリマップ

V30から見えるメモリは448KB。前作の128KBから3.5倍だ。メモリの実体はRP2350の内蔵SRAM上の配列で、core1がV30のバスサイクルに合わせて出し入れしている。つまりRP2350が「メモリチップ」の役も演じている。

RP2350の内蔵SRAMは全部で520KBあり、そのうち448KBをV30に渡して、残りをVRAMやバス制御などファームウエア自身のために使っている。

仮想アドレス 用途
0x00000〜 割り込みベクタ
0x00400〜 ユーザプログラム領域
0x10000 VM_IO.SYS (仮想I/Oドライバ) ※初期配置
0x18000 MSDOS.SYS (DOSカーネル) ※初期配置
〜0x6FFFF 空き (DOSアプリはここで動く)
0x70000〜0xFFFEF 何もない
0xFFFF0 リセットベクタ + BIOS (16バイト)

VM_IO.SYSとMSDOS.SYSのアドレスはブート時にロードされる初期値で、HIDOSが起動する過程で自分で移動する。

448KBのRAMの上限は0x6FFFFで、そこからリセットベクタまでの間には何もない。V30が起動直後に読みに来るリセットベクタ (0xFFFF0) 以降の16バイトだけは、core1が特別に応答するようにしてある。この16バイトにリセットベクタとBIOSがまるごと収まっている。

初期化時にメモリ全体を 0xF4 (HLT命令) で埋めておくのは前作のモニタの f コマンドと同じ流儀で、暴走したらすぐ止まる。

ディスク書き込み問題の解決

前作で「作ったプログラムを保存する方法がない」と書いた問題は、2段構えで解決した。

  • Aドライブ: ファームウエアに焼き込んだROMディスク。COMMAND.COMやDOSツール一式が入っている
  • Bドライブ: PSRAM上の4MB RAMディスク (FAT)。読み書き自由

前作は「メモリに余裕がないからRAMディスクは難しそう」と書いたが、RP2350はXIP (外部メモリをアドレス空間に直接マップして読み書きできる仕組み) のセカンダリインターフェースに8MBのPSRAMをぶら下げられるので、あっさり解決した。RAMディスクなので電源を切ると消えるが、それも後述のwarm rebootとYMODEM転送でだいぶ緩和されている。

ターミナル

前作は「PCに繋いで表示させていると感動も薄い」で終わったが、今回はDOSから見た画面とキーボードを全部ファームウエアで実装した。

ANSI.SYS互換エスケープ

DOSの世界では画面制御にANSI.SYSのエスケープシーケンスを使うのが定番だったので、CUP (カーソル移動) / ED・EL (消去) / SGR (色) / DECTCEM (カーソル表示) あたりを実装した。これでDOS側のフルスクリーンアプリ (後述のvi) がそのまま動く。

日本語: SJIS、フォント、UTF-8変換

  • LCDには8×16の半角と16×16の全角フォントを載せ、40列×30行のテキスト画面として使う。フォントはビットマップ画像からRubyスクリプトでC++ソースに変換
  • DOS側の文字コードは当然Shift_JIS。LCDはSJISを直接解釈する
  • 一方、USBでPCに繋いだときの標準入出力 (/dev/ttyACM0) はSJIS↔UTF-8を双方向変換してから流す。現代の端末エミュレータでそのまま日本語が読み書きできる。JIS X 0208↔Unicodeの変換テーブルもスクリプトで生成した

既存の日本語表示ソフトウエアもそのまま動く。

既存の日本語表示ソフトウエアを動かしているDOSCONの画面

PSRAMスクロールバッファ

テキスト画面は40列×30行しかないので、PSRAMに4096行分のスクロールバッファを持たせた。Fnキーを単独タップするとスクロールモードに入り、k/j やCtrl-F/Ctrl-Bで過去の出力を遡れる。DOS機なのにless的なスクロールバックがあるのはなかなか快適だ。

Symキーのタップで文字を倍角表示にするモードも付けた。老眼にやさしい。

通常の画面

同じ画面を倍角表示にしたところ

スクロールモードで過去の出力を遡っているところ

47キーに全部を詰め込む

47キーしかないので、自作キーボード界隈でおなじみのQMK風レイヤ方式にした。Ctrl / Shift / Sym (記号) / Num (数字) / Fnの修飾キーがあり、優先順位は Ctrl > Fn > Sym > Num > Shift > Base。レイヤ上で定義がないキーは下のレイヤに「透過」する (QMKの _______ と同じ)。

Fnキーはさらに QMKのLayer-Tap相当の判定をしていて:

  • 押しながら他のキー → Fnレイヤ (F1〜F12、カーソル、PgUp/PgDn)
  • 単独タップ → スクロールモードのトグル
  • 左右のFnを同時押し → MonitorShell (後述) 起動

同時押しの検出を専用ロジックにせず「FnレイヤのFnキー位置にシェル突入キーを仕込んでおく」だけで実現できたのは、レイヤ方式のきれいな副産物だった。

モニタは今回も居る: BootShellとMonitorShell

前作のモニタプログラム (メモリダンプ、逆アセンブル、XMODEM…) の精神は今作にも引き継がれていて、ファームウエアにはDOS以外に2つの「シェル」が入っている。

BootShellは起動時にESCを押していると入る設定メニュー。CPUクロック、標準入出力をCDC/UARTどちらに繋ぐか、ドライブ構成、UTF-8変換の有無などをトグルして b でDOSを起動する。

MonitorShellはDOS実行中にFn両押し (または /dev/ttyACM1 に何か入力) で入れるモニタで、メモリダンプ、逆アセンブル、リセット、BOOTSEL (ファームウエア書き込みモード) 突入などができる。実行中のV30のメモリを外から覗けるので、DOS側のデバッグに重宝する。

お気に入りが warm reboot だ。RAMディスク (Bドライブ) の内容を保持したまま再起動できる。

ただ、これはすんなり動いたわけではなかった。実装してみると、再起動をまたぐとRAMディスク上のファイルが化ける。どこで化けているのかを調べるために、DOS上で動くSHA1SUM.EXE を作った。reboot前後でファイルのハッシュを取って突き合わせ、どのデータがいつ壊れるのかを絞り込んでいった。

犯人はXIPキャッシュだった。RP2350のPSRAMはXIPキャッシュ越しにアクセスするので、RAMディスクに書いたつもりのデータの一部はまだキャッシュ上のダーティラインに居て、PSRAM本体には届いていない。その状態でリセットするとキャッシュごと消える、というオチだ。再起動前にキャッシュを明示的にflushしてから飛ぶようにして解決した。

デバッグのために作ったSHA1SUMは、そのままディスクに同梱してファイル転送の照合ツールとして第二の人生を送っている。

PCとの連携: 3本のUSBシリアル

USB-CでPCに繋ぐとCDC ACMが3本生える。pico-sdkのおかげで、この手のコンポジットシリアルデバイスは簡単に作れた。

デバイス 役割
/dev/ttyACM0 DOSの標準入出力 (LCDと並行、SJIS↔UTF-8変換つき)
/dev/ttyACM1 ファームウエアのデバッグログ + モニタ起動
/dev/ttyACM2 DOSのAUX (COM1) 直結、生バイト

前作で「シリアルポートは画面のために使ってしまっているので多重化するか別の方法を考えないといけない」と書いた問題は、USBのコンポジットデバイスにして解決した。1本のUSBケーブルでコンソール・デバッグ・ファイル転送が同時に使える。

ファイル転送はAUX経由のYMODEM。DOS側にSX.EXE / RX.EXE を同梱していて、Linux側は lrzsz がそのまま使える。

PCからDOSCONにファイルを送るには、DOSCON側で RX を実行してから、Linux側で sb を実行する:

A> RX /Y
$ sb HELLO.TXT < /dev/ttyACM2 > /dev/ttyACM2

逆にDOSCONからPCに送るには:

A> SX /Y HELLO.TXT
$ rb < /dev/ttyACM2 > /dev/ttyACM2

転送後の照合には、warm rebootのデバッグ用に作ったチェックサムプログラム (CRC32SUM.EXE / SHA1SUM.EXE) がそのまま使える。前作でLHAが動かなくてLinux側で解凍した、みたいな運用が今回はだいぶ楽になった。

viとTUT-Codeで、このマシンだけで開発する

前作からの宿題だった「このマシン上でのソフトウエア開発」のために、ディスクにはHIDOSのビルド成果物 (COMMAND.COM、EDLIN、DEBUG、MASM、LINKなど) に加えて、自作のVI.EXE を入れた。vi風のエディタで、ANSIエスケープで全画面制御する。

RAMが448KBに増えたので、Cコンパイラもこのマシンの上で動く。入れたのはLSI C-86 試食版。小学生の頃に使っていたコンパイラが、あの頃と同じV30の上で、今度は自分で作ったマシンで普通に動く。

LSI C-86でhoge.cをコンパイルして実行しているDOSCONの画面

copy con でFizzBuzzを書いて、コンパイルして、実行する。全部このマシンの中だ。

copy conで書いたFizzBuzzをコンパイルして実行しているDOSCONの画面

そしてDOSCONで一番変なところかもしれないのが漢字入力で、IMEを積む代わりに TUT-Code (2〜3打鍵で漢字を直接入力するコード体系) をviに内蔵した。IMEにしなかったのは、辞書を置くメモリがないから。同じ漢直でもT-Codeはこのキーボードではキーが足りないので、TUT-Codeに落ち着いた。変換候補ウィンドウも辞書引きも不要なので、448KBしかないDOSマシンと相性が良い。なお、日本語の表示はDOS全体でどこでもできるが、入力ができるのは今のところviの中だけだ。

vi.exeで『Hello world! こんにちは世界』と入力しているところ

これで、viでソースを書く → MASMやCコンパイラでビルド → 実行 →YMODEMでPCに吸い出す、までが電池駆動のポケットマシンで、しかも本物のV30の上で完結するようになった。

MASMでHello Worldをアセンブルして動かした様子:

HELLO.ASMのソースとMASMでのアセンブル結果を表示しているDOSCONの画面

LINKとEXE2BINでHELLO.COMを作って実行し、Hello Worldが表示されたDOSCONの画面

開発の記録

コミットログから振り返ると、だいたいこんな流れだった。前作の記事を書いたのが2月11日。実はその頃には、もう次の基板の設計を始めていた。

  • 2月上旬: KiCadで基板設計、発注
  • 2月中旬: MicroPythonでLCD/キーボードの単体動作確認 →C++でファームウエアの骨組み
  • 2月23日: V30バスインターフェースとI/O処理の初期実装
  • 2月末: HIDOSをサブモジュール化してディスクイメージのビルド、DOS起動
  • 3月: I/Oまわりの整備、SJIS/UTF-8、スクロールバッファ、BootShell/MonitorShell、warm reboot
  • 4月: DOS側ツール (vi、YMODEM)、README整備
  • 4月末〜5月: ケースとキーキャップの調整、倍角モードなど仕上げ

約4ヶ月、74コミット。

組み立てた基板の上で初めてMS-DOSが起動した時の画面

初めてLCDの上でDOSが起動した時の様子。ここから先は「動くDOSマシンをいじって拡張していく」楽しいフェーズだった。

今回もAIに実装を任せるつもりだったので、テストは過剰なぐらいに整備した。ファームウエアの全クラスをテンプレートで差し替え可能にして、FakeHAL/FakeScreenなどを注入したホスト側gtest (Pico SDK不要)を用意し、エスケープシーケンスの解釈やキーボードのレイヤ判定のような「実機だとデバッグが面倒なロジック」は全部PC上のテストで検証できるようにした。AIが自分でテストを回して自分で直せる環境を作れば、ソフトウエアの開発はよく回る。

一方で、前述の通りケースはテストが書けない。AIループが回るソフトと回らないガワで、かかる時間が逆転したのが今回の学びだった。

もうひとつ、実装前に設計メモを firmware/docs/ に書き溜めながら進めるスタイルを取った。メモリマップ、キーの論理化、I/Oの仕様など13本。あとから見返せるし、この記事の下書きにもなった。

まとめと今後

前作の展望3つ (ディスク書き込み・メモリ・画面とキーボード) は全部回収した。ついでに日本語表示と漢字入力まで付いた。

  • 基板からOSまで全レイヤが自分の手元にあると、レイヤ間の「境界」を自分で決められるのが最高に楽しい (BIOSを丸ごと捨ててI/Oポートプロトコル1本にする、など)
  • RP2350は「CPU以外の全部」を演じる筐体として十分すぎる性能。V30のバス制御とUSBと画面を回してもまだ余裕がある
  • MS-DOS 2.11は448KBあれば快適に住める

そして何より、小学生の頃に夢想した「キーボード付きのゲームボーイでプログラムを書く」が、電池2本で手の中にある。

今後の展望:

  • 配線を間違えたSDカードを、PIOでソフトSPIを生やして救済する
  • そして次は、V30からは離れて、もっと別の物を作ってみたい。そもそも今回DOSを選んだのは「既存のDOS資産が活かせるのでは」という目論見だったからだが、ここまでAIの開発力が高いなら、OSもコンパイラも、なんならCPUも、自作を視野に入れていいのかもしれない

リポジトリ:

おわり

今回もいろいろ勉強になりました。

先人の知恵に感謝します。

  • HIDOSを作ったhdk先輩
  • TUT-Codeを考えた人達
  • QMKに代表される自作キーボード文化
  • lrzszを今もメンテしている人達