If you wanted me to demo CP/M running on an emulated Altair 8800, I’d pull out a tiny board from my pocket. You might wonder how I wound up with an Altair 8800 that runs CP/M (even WordStar), that fits in your pocket and cost less than $10. Turns out it’s a story that goes back to 1975.

When the Altair 8800 arrived back in 1975, I wanted one. Badly. I’d been reading about computers but had no hands-on experience. But back then, as far as I was concerned, the $400 price tag might as well have been a million bucks. I was working for no real pay in my family’s store, though in all fairness, adjusted into today’s money that was about $2,000.

I’d love to buy one now, but a real Altair costs even more today than it did back then. They also take up a lot of desk space. Sure, there are replicas and I’ve had a few. I even helped work the kinks out of Vince Briel’s clone which I’ve enjoyed. However, the Briel computer has two problems. First, it takes a little work to drive a serial port (it uses a VGA and a PS/2 keyboard). Second, while it’s smaller than a real Altair, it is still pretty large — a byproduct of its beautiful front panel.

So to quickly show off CP/M to someone, you need to haul out a big box and find a VGA monitor and PS/2 keyboard — both of which are becoming vanishing commodities. I made some modifications to get the serial port working, but it is still a lot to cart around. You could go the software route with a simulator like SIMH or Z80pack, but now instead of finding a VGA monitor and a PS/2 keyboard, you need to find a computer where you can install the software. What I really wanted was a simple and portable device that could boot CP/M.

The ESP32 Solution

The FABGL library lets the EPS32 drive a VGA monitor and also provides terminal-like capabilities using a PS/2 keyboard. It also covers quite a few other functions, like working with a flash file system or an external memory card. One of the examples is an Altair 8800 emulation adapted from another open source project. The VGA32 board is made to work with the library and is very inexpensive. It took a little work, but the Altair emulation worked fine on the board, so I wound up with a $10 replacement for the Briel computer that fits in my pocket. The only problem was it still needed a VGA monitor and a PS/2 keyboard.

I wanted to change the code a bit so that I could use the serial port and because of the nice design of the emulator, it turned out to not only be relatively easy but also pretty simple to allow for both modes — you can drive a VGA or use it via the USB cable with a normal serial terminal program.

The Emulation

Before I get into the changes, let’s look at the emulation as it existed before I started tinkering with it. Fabrizio — the FAB in FABGL — had certainly come up with a nice design. The main Arduino Sketch file organized the configuration of the Altair, and it included disk images that you can mount as read only or read write.

The only issue I ran into was that the emulation used some features that don’t appear to be in the library the Arduino IDE installs via the library manager. The manager claims to have version 1.8 which matches what’s on GitHub, but there were still unresolved symbols when building the example.

The solution was to remove the existing library, download the entire GitHub repository as a ZIP file, and then ask the library manager to install from the archive. After that, everything went fine. If you want to do the install, you might as well start with my fork, so you can get the updated Altair example code.

The next layer of abstraction down is in the /src directory’s machine.cpp file. This file has a hardware abstraction layer for the actual CPU emulations in a different file. The machine.cpp file contains the code to manage memory, disk drives, I/O devices, and actually run programs.

The CPU code is in two parts. There’s an 8080 emulation originally from Viacheslav Slavinsky and a Z-80 emulation by Lin Ke-Fong. There were modifications, but overall, these files are just the emulation logic.

When you run the code it tries to find an SD card, but if it doesn’t find one, it will use internal flash for the disk drive emulation. It has all the bootloader code and pretty much operates like a real Altair, albeit without a front panel, of course. There is an emulation menu that you can bring up with the Pause key on the keyboard. Though that won’t do us much good without a keyboard connected. Though obviously that won’t do us much good without a PS/2 keyboard connected.

From the emulation menu, you can dump and read data from the punch and reader device. However, the CP/M disks also have utilities you use to XModem files back and forth to your PC. You can also do things like select the 8080 vs Z80 CPU and set the emulation speed.

The Investigation

I started digging around the code to see how hard it would be to add the serial port. Turns out, the configuration has the serial port setup but uses it as the CP/M punch/reader device. In the code, there are three lines that attach different streams to the SIO0, SIO1, and SIO2 devices. The console devices (SIO0 and SIO1) are set to the terminal stream that FABGL provides. SIO2 uses the standard serial stream.

That was a good sign, and sure enough, the terminal stream is compatible with the serial one. As a simple experiment, I just changed the terminal references to serial. The system booted up and gave me a CP/M prompt over the serial terminal.

That left a few minor problems. Without a PS/2 keyboard, there’s no way to get to the emulation menu. In addition, the emulation menu was hardwired to the terminal. It was reasonably easy to fix both of these problems.

Fixing the Emulation Menu

There’s only one user button on the VGA32 board, so it made sense to use it to call up the emulation menu on the serial port. It wasn’t hard to change the code that calls it (in machine.cpp):

IRAM_ATTR int Machine::nextStep(CPU cpu)
{ auto keyboard = fabgl::PS2Controller::instance()->keyboard(); static int inmenu=0; if (m_menuCallback && keyboard->isVKDown(VirtualKey::VK_PAUSE)) m_menuCallback(0); // check for menu if (!digitalRead(USER_BUTTON)) { if (inmenu==0) { inmenu=1; m_menuCallback(1); inmenu=0; } }

With this change, the emulation menu callback gets an extra argument and that argument is set depending on the entry method. The problem is, you have to change in the main file. I factored out the code that the menu uses to read the keyboard to a getChar function. It appears the terminal blocks until you press a key, but the serial port does not, so it took a little change to make that work right.

To minimize the code changes, I changed all the references to Terminal to CONSOLE and added this line to the start of the emulator_emu function:

Stream &CONSOLE=stream?(Stream &)Serial:(Stream &)Terminal;

Using a reference means I didn’t have to change all the dots to arrows which would have been the case had I used a pointer.

Dual Personality

The final change I wanted to make was to allow the original code to work so you could still use the VGA and keyboard. I thought about making a new item on the emulation menu but decided to use the user button again, instead. On startup, the code looks at the state of the button. With the button up, the board starts in serial mode. If the button is down, the board sets up the VGA terminal and waits for the button to release:

if (!digitalRead(USER_BUTTON))
{ streamsel=0; // TTY SIO0.attachStream(&Terminal); // CRT/Keyboard SIO1.attachStream(&Terminal);</pre> // Serial SIO2.attachStream(&Serial);
}
else
{ streamsel=1; // TTY SIO0.attachStream(&Serial); // CRT/Keyboard SIO1.attachStream(&Serial); // Serial SIO2.attachStream(&Serial);
}
// wait for button release
while (!digitalRead(USER_BUTTON));

Open Source Fun

This is a great example of how you can build on other’s work. The emulators were from different sources, and Fabrizio did a great job of providing reusable components. It was very straightforward to modify the code to do what I wanted to do.

Am I writing this post using WordStar? Maybe. Just remember, you’ll either need the right terminal emulation or you’ll want to install the WordStar disks to a read write disk so you can run WSChange and pick the terminal type you have.

For a few bucks, this was a great way to have a bootable CP/M computer with a lot of options. The idea isn’t limited to the ESP32 either, as you could certainly put something together using the Arduino. It might not be quite the same as building the real thing from scratch, but it’s certainly a lot cheaper.