日本語版もあります。

DOSCON booted up on a cafe table, next to a latte and a waffle

Last time, I put a real V30 CPU on an RP2040 “cradle” and booted MS-DOS (HIDOS) on it.

At the end of that article, I listed three things as “future work”:

  1. Add disk write support, so I can develop software on the machine itself
  2. Upgrade to the RP2350 to get more memory
  3. Turn it into a portable terminal with its own screen and keyboard

This time, I did all of them. The result is called DOSCON.

It still carries a real V30 chip. It runs on two AAA batteries, and a few seconds after power-on you get the A> prompt. It has an LCD and a keyboard, it can read and write Japanese text on its own, and you can write a program in vi (self-made), assemble or compile it, and run it — all without leaving the machine.

The PCB (KiCad), the case (3D printed), the firmware (C++), and the DOS tools running inside: I built the whole stack myself.

Motivation

The previous machine’s only connection to the outside world was USB, so it needed a PC. But putting a PC that is hundreds of times faster than a V30 next to it, just to use it as a dumb terminal, felt completely backwards. The V30 is the star of the show, so the V30 should be self-sufficient. This time, the goal was a standalone machine.

And if it was going to be standalone, the shape was already decided. When I was in elementary school, I used to look at the Game Boy and daydream: “What if this had a keyboard instead of a controller, and I could write programs on it?” Take the V30 that was inside the PC-98 I used back then, and the C compiler I used back then, and put them into the box I wanted back then. In other words, this is answering a 30-year-old wish.

What Changed Since Last Time

The CPU-side architecture is the same “cradle” approach as before. The real V30 is the bus master, and the RP2350 plays the role of memory, I/O, and every peripheral. The RP2350 has two cores, so I split the work:

  • core1: Dedicated to V30 bus control. It watches ALE/RD/WR and moves data in and out of memory in step with the bus cycles
  • core0: Handles I/O requests, the screen, the keyboard, and USB

The real V30 chip visible through a round window in the case

Since the V30 is the star, the case has a window so you can see the real chip.

The essential differences from the previous machine are three:

  1. It runs standalone on batteries. No PC required — it works as a complete machine on two AAA batteries, with its own screen and keyboard
  2. More RAM, enough to run a C compiler. Up from 128KB to 448KB. Not just MASM/LINK — even a C compiler runs on the machine now
  3. It has a RAM disk. The biggest piece of unfinished business from last time — “no way to save the programs you write” — is solved

Last time, things like adjusting the clock speed were done from a monitor program on the connected PC. Now that the machine is standalone, it has a full configuration shell so you can do all of that on the device itself (the BootShell, described later).

The Big Picture

+---------------------+
|  ILI9488 LCD 320×480|   Portrait. 40×30 text (8×16 halfwidth / 16×16 fullwidth)
|  + 4×12 keyboard    |   ANSI.SYS-compatible escape sequences built in
+---------------------+
            |
+---------------------+          +----------------+
|  RP2350 firmware    |   bus    | V30 (real chip)|
|   - HIDOS host      |<========>|  μPD70116      |
|   - USB CDC × 3     | AD0-15+  +----------------+
+---------------------+   core1 handles the bus
            |  USB FS
+---------------------+
| /dev/ttyACM0  stdio |   DOS standard I/O (with SJIS↔UTF-8 conversion)
| /dev/ttyACM1  debug |   firmware logs / monitor entry
| /dev/ttyACM2  aux   |   COM1 / AUX (raw bytes, used for YMODEM transfer)
+---------------------+
Item Previous (cradle86) This time (DOSCON)
CPU Real V30 (μPD70116) Same real V30
Cradle RP2040 RP2350 @ 250MHz
Memory 128KB 448KB + 8MB PSRAM
Display Serial connection to a PC Built-in LCD (40×30, Japanese support)
Keyboard The PC’s Built-in, 47 keys
Disk ROM only (no writes) ROM disk + 4MB RAM disk
File transfer XMODEM (monitor feature) YMODEM (DOS commands)
Power USB Two AAA batteries (USB-C also works)

Hardware

PCB

Two freshly arrived DOSCON boards, with the DOSCON logo in silkscreen

Designed in KiCad: RP2350 + ILI9488 + key switches + USB-C + battery holder, all on one board.

As before, component placement is generated by a Python script driving the KiCad API. For regular patterns like a key matrix, writing code beats placing parts by hand, and it makes redoing things painless.

Failure: I Miswired the SD Card

Last time the failure was “the board from the fab doesn’t work at all.” This time it was the SD card. The board has an SD card slot that was supposed to connect to the RP2350’s SPI module, but the clock and data lines ended up assigned to pins of two different SPI modules. On the RP2350, each GPIO pin can only serve specific SPI channels, so this cannot work as hardware SPI.

I had followed the AI’s recommended pin assignment, which makes this a sequel to last time’s “AI can’t read the timing charts in datasheets” incident. I did give the pin names a quick check, but I didn’t catch it. Wiring proposed by an AI should be carefully cross-checked against the pin multiplexing table.

The mistake surfaced only after I started writing the firmware. The board had long since been ordered. I should have written the firmware before ordering the board.

Fortunately the RP2350 has PIO, so it should be possible to rescue it with a software SPI. That’s future work; for now the machine’s storage is an SD-less setup (ROM disk in flash + RAM disk in PSRAM + YMODEM transfer). The goal — write code and compile it on the machine — was achieved without the SD card.

Keyboard

47 keys in 4 rows × 12 columns. The interesting part is how they’re read: a normal matrix scan would need 4+12 = 16 GPIOs, but this uses what I call a “double matrix”, which reads everything with 10 GPIOs.

Split 10 pins into 4 rows + 6 columns and a normal matrix gives you only 4×6 = 24 keys. So instead, each intersection gets two keys with their diodes facing opposite directions, and the scan runs both ways: “drive columns, read rows” and “drive rows, read columns.” That’s 2 keys per intersection, 48 positions total. The keyboard looks like 4×12 physically, but electrically its left half and right half share the same intersections in opposite directions.

Double matrix wiring diagram: each intersection of R1–R4 and C1–C6 has two keys with opposite diode orientations

After all, the V30 bus alone (AD0-19 plus control lines like ALE/RD/WR) eats close to 25 GPIOs, and then there’s the LCD’s SPI and the PSRAM’s CS. Not many pins were left for the keyboard.

Power

Two AAA batteries are boosted to 3.3V, and both the RP2350 and the V30 run at 3.3V. The boost converter is an off-the-shelf module. “A 5V-era V30 runs fine at 3.3V” was already confirmed last time, so this time the whole design is 3.3V from the start.

I later considered switching to a lithium-ion battery, but a fully charged li-ion cell exceeds 4.0V, which would probably fry this setup. Hence, dry cells.

The DOSCON board running DOS with two batteries connected

Measured at the battery, the current draw is about 250mA. With AAA NiMH cells (around 750mAh), that’s roughly 3 hours by simple arithmetic. If you want more, AA cells (around 2000mAh) should give 8 hours, and doubling from 2 cells to 4 should double that again.

Case

3D printed. The modeling is not done in a CAD GUI but written as code — JavaScript for replicad. The keycaps are self-made too; the legends are SVGs generated by my own script and printed in via multi-color 3D printing. Looking back at the commit log, late April through May went almost entirely into tweaking the case and keycaps.

Why does the case take so long? Because you can’t run an AI agent loop on it. For firmware, the loop of “AI writes code → tests fail → AI fixes it” runs on its own. For a case, the feedback loop is: print it, fit it against the real thing, adjust dimensions, print again… and a human has to do that with their body. One iteration takes hours, and you can’t write tests for it. In the AI era, the enclosure is the biggest bottleneck in electronics projects.

The finished DOSCON in its black 3D-printed case and keycaps

Carrying HIDOS Around on Batteries

The OS is HIDOS, same as last time. HIDOS, created by my senior hdk, is an environment for self-hosting builds of the MS-DOS 2.11 source that Microsoft open-sourced, and it comes with a minimal VM configuration (hidosvm): one do-everything virtual device, plus a BIOS with exactly one call that invokes it.

I again use my fork as a submodule, but the quick-and-dirty fixes I made last time have since been properly fixed upstream, so the fork’s only remaining diff is build script customization. Not a single line of HIDOS code is modified. VM_IO.SYS is untouched too. The cradle went from RP2040 to RP2350, and the machine gained a screen, a keyboard, and Japanese text — yet the DOS side needed zero changes. I’d call that a victory for hidosvm’s design of “put all the complexity on the VM side.” The beauty of this approach is that you implement no BIOS, no 8253 timer, no 8259 PIC, no CRT controller — none of it. Every console, disk, and serial access from DOS arrives at the RP2350 as a simple protocol over I/O ports. When the V30 executes an I/O instruction, core1 — which is watching the bus — detects it and raises a shared flag, and core0’s host loop picks it up and dispatches to the console/disk/AUX handlers.

Memory Map

The V30 sees 448KB of memory, 3.5× the previous 128KB. The memory’s real body is an array in the RP2350’s internal SRAM, with core1 moving data in and out in step with the V30’s bus cycles. In other words, the RP2350 also plays the role of the memory chips.

The RP2350 has 520KB of internal SRAM in total; 448KB goes to the V30, and the rest is for the firmware itself — VRAM, bus control, and so on.

Virtual address Purpose
0x00000〜 Interrupt vectors
0x00400〜 User program area
0x10000 VM_IO.SYS (virtual I/O driver) *initial placement
0x18000 MSDOS.SYS (DOS kernel) *initial placement
〜0x6FFFF Free (DOS apps run here)
0x70000〜0xFFFEF Nothing
0xFFFF0 Reset vector + BIOS (16 bytes)

The addresses of VM_IO.SYS and MSDOS.SYS are just where they’re loaded at boot; HIDOS relocates itself during startup.

The 448KB of RAM ends at 0x6FFFF, and there is nothing between there and the reset vector. Only the 16 bytes from the reset vector (0xFFFF0) — which the V30 fetches right after reset — get a special response from core1. The reset vector and the entire BIOS fit in those 16 bytes.

At initialization, all of memory is filled with 0xF4 (the HLT instruction) — same custom as the f command in last time’s monitor. If anything runs wild, it stops immediately.

Solving the Disk Write Problem

Last time I wrote “there’s no way to save the programs you make.” That’s now solved with a two-tier setup:

  • Drive A: A ROM disk baked into the firmware. COMMAND.COM and the full set of DOS tools live here
  • Drive B: A 4MB RAM disk (FAT) in PSRAM. Free to read and write

Last time I wrote “a RAM disk seems hard with so little memory,” but the RP2350 lets you hang 8MB of PSRAM off the secondary interface of XIP (a mechanism that maps external memory directly into the address space for reads and writes), so it turned out to be easy. Being a RAM disk, it vanishes on power-off — but the warm reboot described later, plus YMODEM transfers, soften that considerably.

The Terminal

Last time ended with “showing it on a connected PC isn’t very moving.” This time, the screen and keyboard that DOS sees are implemented entirely in the firmware.

ANSI.SYS-Compatible Escapes

In the DOS world, screen control conventionally goes through ANSI.SYS escape sequences, so I implemented the usual set: CUP (cursor movement), ED/EL (erase), SGR (colors), DECTCEM (cursor visibility). With this, DOS full-screen apps (like the vi described later) just work.

Japanese: Shift_JIS, Fonts, UTF-8 Conversion

  • The LCD carries an 8×16 halfwidth font for English text and a 16×16 fullwidth font for Japanese, used as a 40-column × 30-row text screen. The fonts are converted from bitmap images to C++ source by a Ruby script
  • The DOS side’s character encoding is, of course, Shift_JIS. The LCD interprets SJIS directly
  • Meanwhile, the standard I/O over USB (/dev/ttyACM0) is converted bidirectionally between SJIS and UTF-8, so a modern terminal emulator reads and writes Japanese as-is. The JIS X 0208 ↔ Unicode conversion tables are also script-generated

Existing Japanese-language software runs as-is.

DOSCON running existing Japanese-language display software

PSRAM Scrollback Buffer

The text screen is only 40×30, so I gave it a 4096-line scrollback buffer in PSRAM. Tap the Fn key by itself to enter scroll mode, then move through past output with k/j or Ctrl-F/Ctrl-B. A DOS machine with less-style scrollback is surprisingly comfortable.

Tapping the Sym key toggles a double-size text mode. Kind to aging eyes.

The normal screen

The same screen in double-size mode

Scroll mode, going back through past output

Packing Everything Into 47 Keys

With only 47 keys, I went with the QMK-style layer approach familiar from the custom keyboard community. There are Ctrl / Shift / Sym (symbols) / Num (digits) / Fn modifiers, with priority Ctrl > Fn > Sym > Num > Shift > Base. A key with no definition on a layer falls through to the layer below (like QMK’s _______).

The Fn key additionally gets QMK Layer-Tap-style treatment:

  • Held with another key → Fn layer (F1–F12, cursor keys, PgUp/PgDn)
  • Tapped alone → toggles scroll mode
  • Both left and right Fn together → launches the MonitorShell (described later)

A neat side effect of the layer system: the chord detection needed no special logic — I just placed the shell-entry key at the Fn key positions on the Fn layer.

The Monitor Lives On: BootShell and MonitorShell

The spirit of last time’s monitor program (memory dump, disassembler, XMODEM…) carries over, and the firmware contains two “shells” besides DOS.

The BootShell is a settings menu you enter by holding ESC at power-on. Toggle the CPU clock, whether standard I/O goes to CDC or UART, drive configuration, UTF-8 conversion, and so on, then hit b to boot DOS.

The MonitorShell is entered while DOS is running by pressing both Fn keys (or typing anything into /dev/ttyACM1). It offers memory dump, disassembly, reset, and jumping to BOOTSEL (firmware update mode). Being able to peek into the running V30’s memory from outside is invaluable for debugging the DOS side.

My favorite feature is warm reboot: restart the machine while preserving the contents of the RAM disk (drive B).

It didn’t work on the first try, though. Once implemented, files on the RAM disk came out corrupted across reboots. To find where the corruption happened, I wrote SHA1SUM.EXE, a DOS program, took hashes of files before and after reboot, and narrowed down which data broke when.

The culprit was the XIP cache. The RP2350 accesses PSRAM through the XIP cache, so some of the data “written to the RAM disk” was actually still sitting in dirty cache lines, never having reached the PSRAM itself. Reset in that state and the cache evaporates — that was the punchline. The fix: explicitly flush the cache before jumping into the reboot.

SHA1SUM, born as a debugging tool, now ships on the disk and lives a second life as the file-transfer verification tool.

Talking to a PC: Three USB Serial Ports

Plug in USB-C and three CDC ACM devices appear. Thanks to the pico-sdk, this kind of composite serial device was easy to build.

Device Role
/dev/ttyACM0 DOS standard I/O (parallel with the LCD, with SJIS↔UTF-8 conversion)
/dev/ttyACM1 Firmware debug log + monitor entry
/dev/ttyACM2 Direct line to DOS AUX (COM1), raw bytes

Last time I wrote “the serial port is already taken by the display, so I’ll need multiplexing or something.” A USB composite device solved it: one USB cable carries console, debug, and file transfer simultaneously.

File transfer is YMODEM over AUX. The disk ships with SX.EXE / RX.EXE, and on the Linux side plain lrzsz works.

To send a file from the PC to DOSCON, run RX on the DOSCON side, then sb on the Linux side:

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

And the other way around:

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

For verifying transfers, the checksum programs written for the warm reboot debugging (CRC32SUM.EXE / SHA1SUM.EXE) work as-is. Workflows like last time’s “LHA didn’t run, so unpack on Linux” got much smoother.

Developing on This Machine Alone, With vi and TUT-Code

For the leftover homework of “developing software on the machine itself,” the disk carries the HIDOS build artifacts (COMMAND.COM, EDLIN, DEBUG, MASM, LINK, …) plus my own VI.EXE — a vi-style editor doing full-screen control via ANSI escapes.

With RAM up to 448KB, a C compiler runs on the machine too. I installed the LSI C-86 trial version. The compiler I used in elementary school, running on the same V30 as back then — but this time on a machine I built myself. It just works.

DOSCON compiling and running hoge.c with LSI C-86

Write FizzBuzz with copy con, compile it, run it. All inside this machine.

Compiling and running a FizzBuzz written via copy con

And perhaps the strangest part of DOSCON is kanji input: instead of an IME, vi has TUT-Code built in — a system where each kanji is typed directly with 2–3 keystrokes. I skipped the IME because there’s no memory for a dictionary. Among direct-input systems, T-Code needs more keys than this keyboard has, so I settled on TUT-Code. No candidate window, no dictionary lookup — a good match for a DOS machine with only 448KB. Japanese display works everywhere in DOS, but input currently works only inside vi.

Typing "Hello world! こんにちは世界" in vi.exe

So now the whole cycle — write source in vi → build with MASM or the C compiler → run → pull the result to a PC over YMODEM — completes on a battery-powered pocket machine, on a real V30.

Here’s Hello World assembled with MASM:

HELLO.ASM source and MASM assembly output on DOSCON

Building HELLO.COM with LINK and EXE2BIN, then running it and getting Hello World

Development Log

Reconstructed from the commit log, it went roughly like this. The previous article was published on February 11 — and by then, I had actually already started designing the next board.

  • Early February: PCB design in KiCad, ordered
  • Mid February: Verified the LCD/keyboard standalone with MicroPython → firmware skeleton in C++
  • February 23: First implementation of the V30 bus interface and I/O handling
  • Late February: HIDOS as a submodule, disk image build, DOS boots
  • March: I/O work, SJIS/UTF-8, scrollback buffer, BootShell/MonitorShell, warm reboot
  • April: DOS-side tools (vi, YMODEM), README
  • Late April–May: Case and keycap tweaking, double-size mode, finishing touches

About four months, 74 commits.

The screen the first time MS-DOS booted on the assembled board

The first time DOS came up on the LCD. From here on it was the fun phase: poking at and extending a working DOS machine.

Since I planned to hand the implementation to AI again, I built tests to excess. Every firmware class is template-parameterized and swappable, with FakeHAL/FakeScreen and friends injected into host-side gtest (no Pico SDK required), so all the logic that’s painful to debug on hardware — escape sequence interpretation, keyboard layer resolution — is verified in tests on a PC. If you build an environment where the AI can run the tests and fix its own code, software development hums along.

On the other hand, as mentioned, you can’t write tests for a case. Software where the AI loop runs, versus an enclosure where it can’t — the time spent on each flipped. That was this project’s lesson.

One more thing: I kept writing design memos in firmware/docs/ before implementing — memory map, logical key mapping, I/O specs, 13 in all. They’re good to look back on, and they became the first draft of this article.

Wrap-up and What’s Next

All three items of last time’s future work (disk writes, memory, screen and keyboard) are done. Japanese display and kanji input came along for the ride.

  • Having every layer from PCB to OS in your own hands means you get to decide where the boundaries between layers go (like throwing out the whole BIOS in favor of a single I/O port protocol). That’s the best part
  • The RP2350 has more than enough power to play “everything except the CPU.” V30 bus control, USB, and the display together still leave headroom
  • MS-DOS 2.11 lives comfortably in 448KB

And above all: the “Game Boy with a keyboard that I can program on” I daydreamed about in elementary school now runs on two batteries, in my hands.

Future work:

  • Rescue the miswired SD card with a PIO-based software SPI
  • And next, I’d like to move away from the V30 and build something different. I originally chose DOS on the theory that I could leverage existing DOS software — but if AI is this good at development, maybe a self-made OS, compiler, or even CPU should be on the table

Repositories:

The End

Once again, I learned a lot.

Thanks to those who came before:

  • hdk, my senior, who created HIDOS
  • The people who devised TUT-Code
  • The custom keyboard culture exemplified by QMK
  • The people still maintaining lrzsz