x86系CPUを動かす"マザーボード"を作り、MS-DOSを動かした話
Here is a English version.
モチベーション
PC自作が私の趣味の一つなのだけど、 「マザーボードは自作していないの?」って言われた事があって、 確かに自作した事ないな、作ってみたいと思うようになった。
マイコンボードを作ってる友達は何人もいるが、 マザーボードと言うからには、CPUが取り外せる基板であるべきだし、 (パソコンでなく)”PC”と呼ぶなら、ARMなどではなくX86のアーキテクチャのCPUが乗る物であるべきだ。
実は何年か前に、8088CPUを入手して試したが、 前回試みた時は、うまくできなかった。
今回、前回の反省を元に、再度試す事にした。
- 8086ではなく、V30を使う。 8086はHMOSというNMOS系プロセスが使われているらしく、これはCMOSのICと食い合わせがよくない。 CMOSプロセスではNPNトランジスタとPNPトランジスタを対称に配置して消費電力を減らしたが、 その前に使われていたHMOS(IntelでのNMOSの事らしい)では、 NPNトランジスタのみを使っていて、電流をたくさん消費するようだ。 信号がCMOSのチップからNMOSに送ろうとすると、 消費電流が大きすぎてCMOSチップではNMOSチップをドライブできないようだ。 そういや、ファンアウトとかファンインとか習ったな。 今回はCMOSプロセスで作られている8086クローンであるV30を使う事にした。
- クロックを一定間隔で、ある程度より早い速度で生成する。 前回試した時は、クロックはいくら遅くしても、 また、一定の間隔でなくても動作すると思い込んでいた。 そのため、途中でクロックを止めてデバッグ出力したりしていた。 しかし、識者に聞いた所、クロックにはいくつかの制限があり、 特にNMOS系プロセスではクロックとクロックの間が長くなりすぎると問題になるとの事だった。 ハードウエアPWMを使って、一定間隔のクロックを作り、 また、その速度もできる限り早くできるように心掛けて作った。
- 生成AIの利用 データシートを細かく読み込んで、回路を考えたり、制御ソフトウエアを作るのは大変だ。 今回は、生成AIを利用して回路や制御ソフトウエアを作った。
ハードウエア構成と設計
メインのCPUには、V30(型番μPD70116)を使った。 V30はNECが作った8086互換CPUで、昔のPC-98に乗っていて、小学生の頃使っていた。 Aliexpressで1つ2ドルで買った。
前述したように、このCPUはCMOSプロセスで作られているので、 NMOSのCPUよりはクロック間隔が長くなる事に対して耐性があるという事でこれを使う事にした。 また、PWM生成器を使いクロックを生成する事で、一定間隔でクロックを生成する。
また、前回は、回路の電圧レベルで苦労した。 この頃のCPUは5Vで動作しているが、 最近の回路は3.3Vやそれ以下の電圧で動作している。 前回はこれをレベルコンバーターを挿入して解決しようとしたが、 うまく解決できなかった。 改めて調べた所、この5V用のCPUを3.3Vで動作させても動いたという報告を多数見た。 回路全体が3.3Vで動作していれば、電圧差で困る問題は解決する。 3.3VはV30のデータシートで保証されている範囲外だが、 動けばもうけものという事で、今回はこれを真似しようと思う。
システムの面倒を見る部分は、 専用ICかロジックICやASICなどで組む場合が多いと思うが、 今回はサボってRP2040で面倒を見させる事にした。 つまり、このプロジェクトでは本物のV30を使うが、 CPU以外のものは全てPR2040によって実装している。 このRP2040側の事を「ゆりかご」と呼んでいる。
回路設計は、geminiに相談して決めた。 どのコンポーネントをどのコンポーネントにつなぐかを考えてもらってテーブルとして出力(ネットリスト形式相当)してもらい、 それを手作業でKiCadに写経した。 たぶん、自動変換するスクリプトを出したり、ネットリストファイルを出力させる事も可能ではあると思う。
コンポーネントの配置や、基板サイズは、AIを使わず指定した。 サイズはクーポンが使える最大サイズである10cm四方にし、 各コンポーネントは十分に余白をつけて配置した。 自分はこの作業をやる時は、もし回路からやりなおした時に楽なように、 普段からPythonスクリプトを書き、KiCAD APIを経由して行っている。 たぶん、AIにこのスクリプトを書かせる事は簡単ではないかと思う。
ネットリストを物理的な配線に変換する作業は、Freeroutingの自動配線で行った。
追加で、Nano bananaを用いて、 プロジェクトのロゴとか、手書き風に「マザーボード」と書いてもらい、 それを余白にシルクとして配置した。

ソフトウエア構成
V30上で動くソフトウエアや、 ゆりかご側で動くソフトウエアを用意する。
こちらもAIとVibeコーディングで書いてもらった。 空のディレクトリでgemini-cliを起動してアレコレ頼んだ。
ゆりかご側の制御は、 PicoSDKを使いC++のコードを書いてもらった。
例えば、CPUがメモリを読もうとしてる事をゆりかごで監視し、 メモリからデータを読み出して、CPUに渡している。
このバス制御はタイミングがかなりシビアになっているが、 一方でデバッグにUSB-CDCのコンソールを使っているため、 USBの相手をする割り込みが入り、破綻する。
これを何とかするために、AIに相談したところ、 RP2040にある2つ目のコアを活用すると良いと教えてもらった。 1つ目のコアでPCとのやり取りをし、 2つ目のコアはV30の制御に集中させている。
コア間の通信はRP2040の同期キューを使うべき、とAIが言っているが、 単にvolatileを付けたグローバル変数じゃ駄目なのか?と思ったが口を出さない事にした。 「動けばいい」の、vibeな精神で深く考えずに行きたい。
AIとしては、バス制御にはPIOを使って高速化するべき、と言われたが、 俺が読めない物になってしまうと困るので、とりあえず、全体をC++で書いてもらった。
RP2040の264KBあるRAMのうち128KBをV30用に割りあて、 V30からのメモリアクセスが来たらここから出し入れする構成にしている。
また、メモリアクセスはログとして記録しておき、 PCからのリクエストでログを表示できる機能をつけてもらった。
せっかくなので、AIに頼んで、アセンブラと逆アセンブラを書いてもらった。 パーサもマシンコード生成もAIがCのコードで書いてくれた。 このような、複雑ではないが、量的に多くて面倒なコーディング作業はAIにまかせると良い感じかもしれない。
全体は、モニタプログラムのようになっていて、端末から操作できる。 次のようなコマンドを実装してもらった。
d <addr> [len] : Dump memory
e <addr> <val> : Edit memory
f [val] : Fill memory with byte (default F4)
a <addr> : Assemble interactively
l <addr> [len] : Disassemble
r [cycles] : Run & Log for specified cycles (0 or omit for infinite)
i [cycles] : Run & Log IO only for specified cycles (0 or omit for infinite)
g : Run Loop (Key stop)
c <kHz> : Set V30 clock speed
xr/xs : XMODEM Recv/Send RAM
xl : XMODEM Send Log
v : Version
autotest [io] : Full auto test (Rx -> Run -> Tx Log)
b : Reboot to BOOTSEL mode
デバッグ

こうして、必要な物が一通り完成したので、 PCBの作成を専門の業者にお願いした。
できあがった物を半田付けして組み立て、 ファームウエアを書き込んでみたが、 全く動作しなかった。
動かなかった事を、AIに教えた所、 回路を変えて再試行する事を提案してきた。
AIは、 海外の工場にお願いする事や、 手作業で半田付けする事の大変さを理解していないのだと思う。 これだから身体性が無いAIはだめだな。 2026年はフィジカルAIの年になると言ってる人がいるが、 AIが現実世界のコストをきちんと理解しない限り、 難しいのではないだろうか?
仕方ないので、手作業でデバッグ作業をする事になった。 ちょうど家にあったロジアナを接続し、 デバッグを行ったが、 CPUを起動すると、ロジアナごと暴走する。
半日ほどいろいろ試した結果わかったのは、 タイミングの問題だった。
V30(や8086)では、アドレスラインとデータラインを同じ配線で共用し、 タイミングで切り替えているが、 CPUがメモリからデータを読むというRD信号が来たとき、 次のクロックで、入出力が切り替わるが、 AIが書いたコードでは、RD信号が来た瞬間にデータを遅りつけていた。 このため、データ同士が衝突して、ショートしていた。
USB接続した「マザーボード」がショートしてしまったため、 ホスト側が保護のためにUSB電源を切断し、 同じくUSBに接続しているロジアナが巻き込まれて切断されていたというオチだった。
この辺については、データシート(80年代に書かれた物をスキャンしたPDF)に書かれた タイミングチャートを読めばわかるはずだが、 AIには、まだ図の読解は少し難しいのかもしれない。
うっかりAIに電源をショートさせるような制御をさせるべきではないのかもしれない。
どうしたらいいかわからないので、RD命令が来た時は、少しウエイトを入れてから処理を続行するようにしたところ、 単純な足し算などが正常に動作するようになった。
もっと複雑な例を試したところ、 奇数番地に置かれている命令を実行しようとすると、 暴走してしまう事を発見した。
これについても調べた所、バグが見つかった。 8086は、奇数番地用のメモリチップと、偶数番地用のメモリチップを別のチップにし、 どちらのチップからデータを読むかをBHE制御線で制御しているとのことだった。 しかし、AIはこの辺がよくわからなかったらしく、 うまく制御できていなかった。
こちらも手で修正した所、正常に動くようになった。
ハンド(AI)アセンブルしたコードの実行
mon> e 100 B8
Updated.
mon> e 101 01
Updated.
mon> e 102 00
Updated.
mon> e 103 A3
Updated.
mon> e 104 00
Updated.
mon> e 105 02
Updated.
mon> e 106 f4
Updated.
mon> a FFFF0 EA
FFFF0: .
mon> e FFFF0 EA
Updated.
mon> e FFFF1 00
Updated.
mon> e FFFF2 01
Updated.
mon> e FFFF3 00
Updated.
mon> e FFFF4 00
Updated.
mon> e FFFF5 00
Updated.
mon> r
Running V30 (Logging, Infinite cycles). Press any key to stop...
--- Log (10 bus cycles executed, 101474 us) ---
ADDR |B|TY|DATA
FFFF0|B|RD|00EA
FFFF2|B|RD|0001
FFFF4|B|RD|0000
FFFF6|B|RD|F4F4
00100|B|RD|01B8
00102|B|RD|A300
00104|B|RD|0200
00106|B|RD|F4F4
00200|B|WR|0001
00108|B|RD|F4F4
モニタプログラムでアセンブルしたコード
mon> a 100
00100: mov ax, 1 -> B8 01 00
00103: mov bx, 2 -> BB 02 00
00106: add ax, bx -> 01 D8
00108: mov [200], ax -> A3 00 02
0010B: db f4 -> F4
0010C: .
mon> a FFFF0
FFFF0: jmp far 0000:0100 -> EA 00 01 00 00
FFFF5: .
mon> r
Running V30 (Logging, Infinite cycles). Press any key to stop...
--- Log (13 bus cycles executed, 101537 us) ---
ADDR |B|TY|DATA
FFFF0|B|RD|00EA
FFFF2|B|RD|0001
FFFF4|B|RD|F400
FFFF6|B|RD|F4F4
00100|B|RD|01B8
00102|B|RD|BB00
00104|B|RD|0002
00106|B|RD|D801
00108|B|RD|00A3
0010A|B|RD|F402
0010C|B|RD|F4F4
00200|B|WR|0003
0010E|B|RD|F4F4
mon>
gnu assembler でアセンブルしたコード
; V30 (8086) Test Program for Pico Monitor
; Calculates 1+2 and stores the result '3' at address 0x0100.
cpu 8086 ; Specify 8086 mode
org 0 ; Assume code is loaded at address 0
; ==========================================
; Main Program (at 0x0000)
; ==========================================
start:
mov al, 1 ; Load 1 into AL register
add al, 2 ; Add 2 to AL (result is 3)
mov [0x0100], al ; Write the result to memory address 0x0100
; This will appear in the bus log as a WR cycle.
out 5, al ; Output the result to IO port 5
hlt ; Halt the CPU. The Pico will detect this via timeout.
; ==========================================
; Padding up to the reset vector
; ==========================================
; Fill the space from the current address ($) up to just before
; the reset vector (0x1FFF0) with NOP (0x90) instructions.
; `$$` is the start of the section (0), so `$` is the current offset.
times 0x1FFF0 - ($ - $$) db 0x90
; ==========================================
; Reset Vector (at 0xFFF0)
; ==========================================
; The V30 CPU starts execution here (CS:IP = FFFF:0000) after a reset.
; This location is mapped to 0x1FFF0 in our 128KB RAM simulation.
reset_vec:
jmp 0x0000:0x0000 ; Jump to address 0 (where `start` is)
; The resulting machine code is: EA 00 00 00 00
; ==========================================
; Fill the rest of the file to make it exactly 128KB
; ==========================================
times 0x20000 - ($ - $$) db 0x90
mon> r
Running V30 (Logging, Infinite cycles). Press any key to stop...
--- Log (13 bus cycles executed, 101558 us) ---
ADDR |B|TY|DATA
FFFF0|B|RD|00EA
FFFF2|B|RD|0000
FFFF4|B|RD|9000
FFFF6|B|RD|9090
00000|B|RD|01B0
00002|B|RD|0204
00004|B|RD|00A2
00006|B|RD|E601
00008|B|RD|F405
00100|-|WR|0003
0000A|B|RD|9090
00005|B|IW|0300
0000C|B|RD|9090
mon>
bccでコンパイルしたコード
// V30 (8086) Test Program for Pico Monitor, written in C.
// This version includes stack initialization and is K&R-compatible for bcc.
// Forward declaration for the main logic.
void main();
// The _start function will be the entry point for the program.
void _start() {
// Use bcc's inline assembler to set the stack pointer to a safe address.
// as86 (bcc's assembler) uses '#' for immediate values.
asm("mov sp, #0x8000");
// After setting up the stack, call the main C function.
main();
}
void hlt(){
asm("hlt");
}
unsigned char out_mem;
void out(unsigned char value){
out_mem = value;
asm("mov al, _out_mem");
asm("out 5, al");
}
// Main C logic, same as before.
void main() {
// Declare all variables at the top of the function.
unsigned char a;
unsigned char b;
unsigned char c;
unsigned short d;
// Assign values to the variables.
a = 1;
b = 2;
// Perform the calculation and store the result.
c = a + b;
out(c);
hlt();
}
mon> r
Running V30 (Logging, Infinite cycles). Press any key to stop...
--- Log (81 bus cycles executed, 104253 us) ---
ADDR |B|TY|DATA
FFFF0|B|RD|00EA
FFFF2|B|RD|0000
FFFF4|B|RD|1A00
FFFF6|B|RD|1A1A
00000|B|RD|00BC
00002|B|RD|5580
00004|B|RD|E589
00006|B|RD|5657
07FFE|B|WR|500F
00008|B|RD|61E8
0000A|B|RD|5E00
07FFC|B|WR|0000
0000C|B|RD|5D5F
07FFA|B|WR|0038
0000E|B|RD|F4C3
07FF8|B|WR|000B
0006C|B|RD|8955
0006E|B|RD|57E5
00070|B|RD|8356
07FF6|B|WR|7FFE
00072|B|RD|FAC4
00074|B|RD|01B0
07FF4|B|WR|0000
07FF2|B|WR|0038
00076|B|RD|4688
00078|B|RD|B0FB
0007A|B|RD|8802
0007C|B|RD|FA46
07FF1|B|WR|0100
0007E|B|RD|468A
00080|B|RD|30FB
07FF0|-|WR|0002
00082|B|RD|02E4
00084|B|RD|FA46
07FF1|B|RD|0102
00086|B|RD|D480
00088|B|RD|8800
07FF0|-|RD|0102
0008A|B|RD|F946
0008C|B|RD|468A
0008E|B|RD|30F9
07FEF|B|WR|0300
00090|B|RD|50E4
00092|B|RD|7CE8
07FEF|B|RD|0390
00094|B|RD|44FF
00096|B|RD|E844
07FEA|B|WR|0003
00098|B|RD|FF75
07FE8|B|WR|0095
00011|B|RD|55C3
00012|B|RD|E589
00014|B|RD|5657
07FE6|B|WR|7FF6
00016|B|RD|468A
00018|B|RD|A204
07FE4|B|WR|0000
0001A|B|RD|00A4
07FE2|B|WR|0038
0001C|B|RD|A4A0
07FEA|-|RD|0003
0001E|B|RD|E600
000A4|-|WR|0003
00020|B|RD|5E05
00022|B|RD|5D5F
000A4|-|RD|9003
00024|B|RD|55C3
00005|B|IW|0300
00026|B|RD|E589
07FE2|B|RD|0038
07FE4|B|RD|0000
00028|B|RD|5657
07FE6|B|RD|7FF6
07FE8|B|RD|0095
00095|B|RD|44FF
00096|B|RD|E844
00098|B|RD|FF75
0009A|B|RD|C483
07FEA|B|WR|009A
0000F|B|RD|F4C3
00010|B|RD|55C3
mon>
MSDOS(HIDOS)サポート
ここまでは、このマシン専用に作ったソフトウエアを実行した。 せっかくx86互換CPUを選んだので、自分で作ったものではなく、 既存のソフトウエアを動作させたいと思った。 それも、マイナーなものではなく、皆が実行した事があるソフトウエアを実行したい。
探すと、MicroSoftが、MS-DOSのソースコードとバイナリをオープンソースで公開していた。 これのCOMMAND.COMというファイルをダウンロードして実行しようと考えた。
最初は、プログラムローダと標準入出力ファンクションコールに相当するものAIに作ってもらい、 COMMAND.COMを起動しようと考えた。
実際やってみると、AIはセグメントレジスタについてうまく理解していないようで、 正常に動作するコードを書かせるのはなかなか難しかった。
qemuを使ってデバッグを行ない、 ユニットテストを作らせてグラウンディングを繰り返す事で、 なんとか上記の機能を実装したが、 COMMAND.COMは、自分をリロケートしたりしていて、なかなか複雑で、 他にもメモリ管理、ディスクIOなどを作らないと動作しないようで、 プロンプトを表示する所まで処理が進まなかった。
例えば、DEBUG.COMコマンドは複雑な事をしないようで、 この構成で起動する事ができた。
MSDOSのファンクションコールをどんどん実装していけば、 いつかはCOMMAND.COMを実行できるまで実装していく事は可能であるかもしれないが、 それは大変であきらめる事にした。
他のアイディアとしては、COMMAND.COM単体ではなく、 IO.SYSとMSDOS.SYSを使い、COMMAND.COMを動かすという手法がある。
しかし、例えば、IBM PC用のIO.SYSを実行するという事は、 IBM PCが持っているBIOSやIOシステムを実装する必要がある。 前述した通り、BIOSをAIに書かせるのは現在時点では なかなか困難がありそうでこれは避けたい。
困っていたところ、先輩に、HIDOSというのを教えてもらった。
HIDOSは自分の理解では、MSDOSをセルフビルドできる、MSDOS環境で、 実機やDOSBOXで動作するMSDOSの上で、HIDOS環境が提供するスクリプトなどを使ってMSの提供するMSDOSソースコードをビルドする事ができる。 それのみならず、hidosvmというミニマルなVMも用意されていて、 このhidosvmの上で(改造した)MSDOSを動かして、 その中でMSDOSのビルドができる環境も用意されている。
hidosvmは何でもできる複雑なデバイスが1つだけ定義されていて、 そのデバイスを呼ぶというBIOSコールが1つだけある数バイトのBIOSがついている。
hidosvmはBIOSコールが1つだけしかないので、 BIOSを移植するのはとても簡単なはずだ。 もちろん、この1つのhidosハードウエアの実装はとても複雑だが、 このハードウエアはゆりかご側(RP2040側)で実装される。 ゆりかご側は、普通のC++コードであるからAIに書かせる事が可能だ。
実際にやってみると、 V30実機の上でhidosvm用のMSDOSを起動する事ができ、 カーネル、シェル、ファイルアクセス、MSDOSに添付されたソフトウエアの起動などを確認できた。
起動画面
mon> h
Loaded boot.img (131072 bytes) into RAM at address 0x00000.
Start embedded HIDOS machine
Microsoft MS-DOS version 2.11
Copyright 1981,82,83 Microsoft Corp.
Command v. 2.11
Current date is Tue 1-01-1980
Enter new date:
Current time is 0:00:12.60
Enter new time:
A>dir
Volume in drive A has no label
Directory of A:\
COMMAND COM 15957 1-04-26 1:38a
CHKDSK COM 6468 1-04-26 1:38a
DEBUG COM 12146 1-04-26 1:38a
DISKCOPY COM 1409 1-04-26 1:38a
EDLIN COM 8176 1-04-26 1:38a
EXE2BIN EXE 1649 1-04-26 1:38a
FC EXE 2585 1-04-26 1:38a
FIND EXE 6331 1-04-26 1:38a
FORMAT COM 4344 1-04-26 1:38a
HRDDRV SYS 486 1-04-26 1:38a
MORE COM 4364 1-04-26 1:38a
PRINT COM 3808 1-04-26 1:38a
PROFIL COM 1779 1-04-26 1:38a
RECOVER COM 2295 1-04-26 1:38a
SORT EXE 1632 1-04-26 1:38a
SYS COM 922 1-04-26 1:38a
MASM EXE 77440 1-04-26 1:38a
LINK EXE 42368 1-04-26 1:38a
18 File(s) 622592 bytes free
A>chkdsk
1015808 bytes total disk space
393216 bytes in 18 user files
622592 bytes available on disk
131056 bytes total memory
100160 bytes free
A>
フリーソフト
Vectorには今でもMSDOSの頃のフリーソフトがまだダウンロードできるようになっている。 PC-98用やIBM用と書かれているソフトウエアはこのマシンでは動作しないが、 DOS汎用と書かれているソフトウエアは動作するはずだ。
実際に試してみると、DOS汎用と書かれたソフトウエアでも、 内部でPC-98なのかIBMなのかを判定して、機種依存コードを切り替える仕組みになっているものや、 タイマなど実装してない機能を使っているソフトウエア、 そもそも128KBしかメモリがないマシンでは動かないものも多いので、 動かないものが多い。
例: CPUCHK
https://www.vector.co.jp/soft/dl/dos/hardware/se008107.html
例えばここからダウンロードできる、CPU判別ソフトウア「CPUCHK」を試してみると、 このPCがV30であると正常に判定できた。
A>edlin cpu.doc
End of input file
*1,6p
1:
2: CPU 判別関数
3:
4: ■ 概要
5:
6:* CPUの種類を判別します。以下の種類のCPU判別が可能です。
*q
Abort edit (Y/N)? y
A>chkcpu
CPU : NEC V30
A>
(ただし、LHAが動かなかったので、解凍はLinux側で行って実行ファイルを実機に送った)
今後の展望
ディスク書き込み
次にどうしても挑戦したいのが、 このマシン上でのソフトウエア開発だ。
edlin.comは起動するし、nasm.exeもlink.exeも動作するはずだが、 ディスクへの書き込みをする方法が現状ないため、 作ったプログラムを保存し実行する方法がない。
メモリに余裕はないから、RAMディスクのようなものを使るのは難しそう。
例えばUSB経由でLinux機にファイルを保存できるようにする方法があるが、 シリアルポートは画面のために使ってしまっているので、 多重化するか別の方法を考えないといけない。
メモリ
128kbのメモリは足りないんじゃないかと思う。 RP2040ではこれが限界である感じなので、 例えば、RP2350にアップグレードしてメモリを増やしたい。
画面とキーボード
PCに繋いで表示させていると感動も薄い。 専用の画面とキーボードをそなえたポータブル端末にするとデモするのに良いかもしれない。
おわり
いろいろ勉強になりました。
先人の知恵に感謝します。
- 似たプロジェクトを作った先人達
- 昔のCPU設計者
- クロックが遅いと動かないと教えてくれた同僚
- MS-DOSをオープンソースにしたMSの人
- HIDOSを作ったhdk先輩
- gemini-cli
Please submit this form, if you have any comments.