DOSCON: A Handheld MS-DOS Machine Running on Two AAA Batteries
日本語版もあります。

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”:
- Add disk write support, so I can develop software on the machine itself
- Upgrade to the RP2350 to get more memory
- 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

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:
- It runs standalone on batteries. No PC required — it works as a complete machine on two AAA batteries, with its own screen and keyboard
- 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
- 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

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.
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.

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.

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.

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.



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.

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

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.

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:


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 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:
- DOSCON: https://github.com/ikeji/doscon
- VI.EXE: https://github.com/ikeji/tiny-vi-clone
- SX.EXE/RX.EXE: https://github.com/ikeji/xmodem
- SHA1SUM.EXE/CR32SUM.EXE: https://github.com/ikeji/sha1sum
- HIDOS (fork): https://github.com/ikeji/HIDOS/tree/doscon
- HIDOS upstream: https://github.com/hdk1983/HIDOS
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
Please submit this form, if you have any comments.