ESP32 Altair Emulator Gets Split Personality

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

36 thoughts on “ESP32 Altair Emulator Gets Split Personality

  1. I think you forgot to do a ^Q-Y on this:

    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.

  2. Don’t worry Steven13, you already look like an insensitive jerk just by posting this. In the end it all evens out. Unless you keep denying that you’re just as insensitive of a jerk as the next person.

    To everyone else: it’s just a phase. The people who are going with it are bound to end up with an identity crisis. And it will pass if they accept that they are not God-like after all.

    Let’s just make sure that their identity crisis won’t spark yet another war, like it often did in the past.

    1. I have done things like this with my projects (for example, the esp8266-based Altair emulator on hackaday.io), although the emulators use “raw” telnet or serial/USB serial. They work well with the free version of Attachmate for IOS on a phone. My code is pretty hacky and rough, but works.

  3. Nice project and nice update too, that makes it even more versatile, thanks!!

    I managed to format drive C and D. And if I change the .ino I can attach one of the other disk images included as a header file after reflashing the device. E.G.:

    //#define DRIVE_B BDSC_dsk // B: read only
    #define DRIVE_B games_dsk // B: read only

    I then can use PIP to copy files to the C and D drive which seem to be persistent.

    But I can’t figure out how to “mount” one of the, as a header file, included disk images under CP/M, any clues?

    1. Look at “Disks configuration” section in “Altair8800.ino”. You have DRIVE_A, DRIVE_B, etc… defines.
      Just set on the right part of the define the disk you want mounted. For example, if you want turbo pascal 3 in DISK_B, set:
      #define DRIVE_B turbopascal3_dsk

      The “turbopascal3_dsk” value comes from “turbopascal3_dsk.h” file. Inspecting that file you will find that there is an array named “turbopascal3_dsk”, which is the image of that disk.

          1. Have the same problem. No matter which _dsk I specify it never changes. I have even tried writing a new example to overwrite this one. Nothing works for me. It’s as though the new compile is ignored and a previously created one always gets loaded.

  4. How’s the VGA implemented? I know there are HDMI transmitter chips out there that take in 24 bits a clock and a sync so as long as there’s no serial DACs involved you might be able to hack in an HDMI interface if the resolution is high enough for anything to recognize. That might solve one of the problems.

    1. VGA signals are generated using DMA device (embedded inside the ESP32). There is a frame buffer in RAM, that is continuously read by the DMA and sent to the output. Of course there is a DAC in order to generate analog VGA.
      The ESP32 (fabgl library) has a set of primitives in order to draw graphics (lines, circles, text, sprites, etc…etc…etc)

  5. Its a lovely emulation. Works like a charm. But I can not get Drive C or D working, so no writes. Tried formatting, creates driveC.DSK on the SD, I am using both Termianla s Development baord. Always Bad sectors error onC and D. Send a disk file (from a working emu, 330K disk) with the Python transdisk.py, success in transfer, still Bad Sectors error. So no write facility?

Leave a Reply

Please be kind and respectful to help make the comments section excellent. (Comment Policy)

This site uses Akismet to reduce spam. Learn how your comment data is processed.