Running Altair BASIC on a Modern Computer, Part 3: Emulating a Teletype

In part two of this series, I described my lib8080 library, which can be used to emulate an Intel 8080 CPU. In this final post, we're going to use lib8080 to emulate an Altair 8800 connected to a teletype machine and get Altair BASIC up and running.

If you just want to see the teletype emulation code, check it out here.

Teletype Machines

Before the advent of cheap video computer terminals, teletype machines served as the primary means of interfacing with a computer. A teletype was an electromechanical typewriter that provided a keyboard for input and output characters on a physical spool of paper. It could be connected to a communications channel and send data input on the keyboard across the channel, while at the same time printing any data received from the channel on the spool of paper.

One of the most popular teletypes was the Model 33; hundreds of thousands were sold. It was also one of the first teletypes to receive and transmit data in the (then newly standardized) ASCII format. Previous teletypes had mainly used Baudot Code to transmit characters.

Most teletypes of the era (including the Model 33) transmitted data through a current loop interface. Current loop encoded data using the presence of current to represent one and the absence of current to represent zero. This type of serial communication differed from the way microcomputers transmitted data, so an expansion card was required for the Altair 8800 to interface with a teletype.

MITS produced two popular serial I/O cards, the 88-SIO and the 88-2SIO, both of which connected to the Altair using its S100 bus. Digging around online, I managed to find reference manuals for both the 88-SIO and the 88-2SIO. I'll discuss both cards in detail below.

Technical Details (88-SIO)

The 88-SIO exposed two devices (via the 8080's IN / OUT instructions) to the programmer. The exact device numbers were configurable through jumpers on the card, however most software expected device numbers zero and one to be used.

Device zero was referred to in documentation as the "control channel". When read from, it produced one byte which contained flags indicating control data. The exact format of the byte was as follows:

Data Bit Logic Low Level Logic High Level
7 Output device ready
6 Unused Unused
5 Data available (a word of data is in the buffer on the I/O board)
4 Data overflow (a new word of data was received before the previous word was input to the accumulator)
3 Framing error (data word has no valid stop bit)
2 Parity error (received parity does not agree with selected parity)
1 X-mitter buffer empty (the previous data word has been X-mitted and a new data word may be outputted)
0 Input device ready

Use of the control channel can be clearly seen in 4K Altair BASIC 3.2's code. For example, at offset 0x377, the following instructions are used to wait for the teletype to be ready before reading keyboard input.

; Read one byte from the control channel into the accumulator
0x377    db00    in 00

; Mask all but bit 7 (output device ready)
0x379    e680    ani 80

; Jump back to 0x377 if the bit is set
0x37b    c27703  jnz 0377

; ... Code to read input from the teletype

Device one is used to send and receive ASCII data from the teletype. When data was available on the 88-SIO board's buffer, an IN 01 instruction was executed to read the byte into the accumulator. To write an ASCII character to the terminal, its value was loaded into the accumulator and an OUT 01 instruction was executed.

Technical Details (88-2SIO)

The 88-2SIO card functions much like the 88-SIO, providing both a control channel and a data channel for interfacing with a serial device. Like the 88-SIO, the exact device numbers were configurable via jumpers; however, most software of the era expected the 88-2SIO's control and data devices on I/O ports 16 and 17 respectively.

The data returned from the 88-2SIO's control channel was provided in a different format than the 88-SIO. The exact specification is as follows:

Data Bit Name Logic Low Level Logic High Level
7 Interrupt Request (IRQ) Board is currently raising an interrupt request
6 Parity Error (PE) Parity error
5 Receiver Overrun (OVRN) A character or a number of characters were received but not read from the Receive Data Register prior to subsequent characters being received
4 Framing Error (FE) Framing Error
3 Clear To Send (CTS) Modem related logic
2 Data Carrier Detect (DCD) Modem related logic
1 Transmit Data Register Empty (TDRE) The contents of the Transmit Data Register have not been transmitted since the last write Transmit Data Register contents have been transferred and new data may be entered
0 Receive Data Register Full (RDRF) The contents of the Receive Data register have already been read and are thus not current New data is available in the Receive Data Register

It should also be noted that both the 88-SIO and 88-2SIO could generate interrupt requests at specific intervals. This was software configurable by writing a byte to the control channel in a specific format. Altair BASIC didn't make use of this functionality however, so emulating it isn't necessary to run BASIC.

Emulating a Teletype

Emulating a teletype attached to either an 88-SIO or an 88-2SIO card with lib8080 is fairly simple. We just use lib8080's IN and OUT instruction hooking functionality and either print the character (in the case of an OUT instruction) or return the appropriate status byte (in the case of an IN instruction).

For example, to emulate an 88-SIO, we can use the following two I/O handling callbacks:

/*
 * stdin_ready() returns 1 if a character is available
 * on standard input and 0 otherwise.
 *
 * read_tty_char() reads one character from standard
 * input, converts it to upper case, and returns it.
 */

 uint handle_input(struct i8080 *cpu, uint dev) {
   if (dev == 0) {
     /* Set bit 0 if a char is available on stdin. */
     return stdin_ready() ? 0 : 1;
   } else if (dev == 1) {
     /* If available, return a character from stdin. */
     return stdin_ready() ? read_tty_char() : 0x00;
   } else if (dev == 255) {
     /* Altair sense switches */
     return 0x00;
   } else {
     fprintf(stderr, "Unknown device %d\n", dev);
     exit(1);
   }
 }

void handle_output(struct i8080 *cpu, uint dev, uint val) {
  if (dev == 1) {
    /* Limit output to printable ASCII characters */
    if ((val >= ' ' && val <= '~') || val == '\n') {
      printf("%c", val & 0x7F);
    }
    fflush(stdout);
  }
}

You'll notice that when a character is printed, I mask the eighth bit. Since ASCII is a seven-bit code, the MSB of each byte is unused. Altair BASIC set the eighth bit to indicate the last character in a string, thus it should be masked when printing to only print valid ASCII.

You'll also notice that I'm emulating an extra device, device 255. On the Altair 8800, reads from device 255 (IN FF) would return the number keyed in on the Altair's "sense switches", the leftmost 8 switches of the address bus on the front panel of the Altair. These switches were commonly used to configure I/O with software loaded into the Altair, and BASIC was no exception.

From the little information I could find online, Altair BASIC could be configured to use a variety of I/O cards and devices through the Altair's sense switches. From the Altair BASIC 4.1 reference manual, I found the following supported I/O devices and sense switch configurations:

Device Sense Switch Setting Switches To Toggle I/O Device Numbers
2SIO (2 stop bits) 0 none 16, 17
2SIO (1 stop bit) 0x11 A12, A8 16, 17
SIO 0x22 A13, A9 0, 1
ACR 0x33 A13, A12, A9, A8 6, 7
4PIO 0x44 A14, A10 32, 33, 34, 35
PIO 0x55 A14, A12, A10, A8 4, 5
HSR 0x66 A14, A13, A10, A9 38, 39

I'm going to be trying to run a binary for 4K BASIC 3.2 (note this is older than version 4.1 that the table above corresponds to, so the I/O configuration is different). 4K BASIC 3.2 seems to expect an 88-SIO if the sense switches are set to 0x00, so in the code above, I emulated an 88-SIO and made reads from device 255 return 0x00.

This is almost enough to emulate a teletype using lib8080. Just one more thing is required before we load up BASIC, terminal settings.

When you start a Bash/Zsh/Tcsh terminal on a modern Unix system, it's usually configured in a way that breaks teletype emulation.

Firstly, the terminal is almost always configured to echo any characters it reads on standard input. This means that when you type in the letter A, 'A' is displayed on the terminal. On an old teletype, characters were not immediately printed on paper when a key was pressed. Instead, characters were sent to the Altair, processed by BASIC and then sent back to the teletype where they were printed. We need to disable echoing so that characters will not be printed twice (first by the terminal, then by BASIC).

Secondly, terminals usually operate in "canonical mode" by default. This means that text sent to the terminal's standard input is not processed until a newline or EOF is encountered. This contrasts with the way old teletypes operated, as they sent each character to the computer as it was typed, without waiting for a newline.

Luckily, it's very simple to disable these two settings using the termios API on Unix like systems:

struct termios term;

/* Read old terminal attributes into term */
if (tcgetattr(fileno(stdin), &term) != 0) {
  exit(1);
}

/* Disable echoing and canonical mode */
term.c_lflag &= ~(ECHO | ICANON);

/* Set new attributes */
if (tcsetattr (fileno (stdin), TCSAFLUSH, &term) != 0) {
  exit(1);
}

Running 4K BASIC

Finally, we're ready to load up 4K BASIC. Since it's technically still under copyright, I won't host it here, but you should be able to find 4K Altair BASIC 3.2 online easily with a few targeted Google searches.

Loading the binary into my terminal emulator, I get:

MEMORY SIZE?

Hooray, it works!

The first thing BASIC asks for is how much memory it should use. If you just hit return, it autodetects the available memory by reading and writing the bytes 0x36 and 0x37 to higher and higher addresses until it reads back something else, indicating that it hit the end of physical memory.

Hitting return, it autodetects the memory size (this takes a few seconds running the emulator at 2MHz) and asks for the terminal width:

TERMINAL WIDTH?

This defaults to 72 characters if return is hit.

It then asks if you want to load a few optional functions, namely sine, square root and random. If yes is hit for any of them, the remaining functions are loaded automatically.

WANT SIN? Y

Now BASIC prints a startup banner:

62412 BYTES FREE

BASIC VERSION 3.2
[4K VERSION]

Isn't that a beautiful sight?

Now we can start using 4K BASIC. Here's a simple for loop being executed in the emulator:

10 FOR I = 1 TO 10
20 PRINT I
30 NEXT I
LIST

10 FOR I = 1 TO 10
20 PRINT I
30 NEXT I
OK
RUN
 1
 2
 3
 4
 5
 6
 7
 8
 9
 10

OK

Running 8K BASIC

4K BASIC runs just fine, but has quite few limitations due to the requirement that it fit into only four kilobytes of memory, for example: - Variable names can be at most two characters long - No string variables - No ability to create custom functions using the DEF keyword - Matrices (arrays) can only be one dimensional - Limited built in math functions

8K BASIC, as its name would suggest, is eight kilobytes in size. Because of this, it's able provide several more features which address everything in the list above.

Running 8K BASIC after 4K BASIC has successfully run is fairly easy. We just need to emulate a terminal card (I used the 88-2SIO this time) and set the sense switches to return the correct value for our terminal card. Note that this time the I/O configuration information in the "Emulating a Teletype" section's table seems to be correct because the version of 8K BASIC I have is newer than the version of 4K BASIC.

uint handle_input(struct i8080 *cpu, uint dev) {
  if (dev == 17) {
    /* If stdin is ready, return a char from
     * stdin, otherwise, return a null byte.
     */
    return stdin_ready() ? read_tty_char() : 0x00;
  } else if (dev == 16) {
    /* If stdin is ready, set bit 0 to indicate
     * the computer can read. Always set bit 1
     * to indicate BASIC can write to the
     * terminal.
     */
    return stdin_ready() ? 0x03 : 0x02;
  } else if (dev == 255) {
    /* Altair sense switches */
    return 0x00;
  } else {
    fprintf(stderr, "Unknown device %d\n", dev);
    exit(1);
  }
}

Note that the handle_output function is the same as for 4K BASIC.

Now we can load up 8K BASIC (again binaries can be found online):

MEMORY SIZE?
TERMINAL WIDTH?
WANT SIN-COS-TAN-ATN? Y

59011 BYTES FREE
ALTAIR BASIC REV. 4.0
[EIGHT-K VERSION]
COPYRIGHT 1976 BY MITS INC.
OK
PRINT 2+2
 4
OK

And it works! Now we have full access to the features present in 8K BASIC.

I decided to try out a bunch of games from BASIC Computer Games. For example, this Civil War simulation game:

RUN
                          CIVIL WAR
               CREATIVE COMPUTING  MORRISTOWN, NEW JERSEY




DO YOU WANT INSTRUCTIONS? Y
YES OR NO -- ? YES




THIS IS A CIVIL WAR SIMULATION.
TO PLAY TYPE A RESPONSE WHEN THE COMPUTER ASKS.
REMEMBER THAT ALL FACTORS ARE INTERRELATED AND THAT YOUR
RESPONSES COULD CHANGE HISTORY. FACTS AND FIGURES USED ARE
BASED ON THE ACTUAL OCCURRENCE. MOST BATTLES TEND TO RESULT
AS THEY DID IN THE CIVIL WAR, BUT IT ALL DEPENDS ON YOU!!

THE OBJECT OF THE GAME IS TO WIN AS MANY BATTLES AS POSSIBLE.

YOUR CHOICES FOR DEFENSIVE STRATEGY ARE:
        (1) ARTILLERY ATTACK
        (2) FORTIFICATION AGAINST FRONTAL ATTACK
        (3) FORTIFICATION AGAINST FLANKING MANEUVERS
        (4) FALLING BACK
 YOUR CHOICES FOR OFFENSIVE STRATEGY ARE:
        (1) ARTILLERY ATTACK
        (2) FRONTAL ATTACK
        (3) FLANKING MANEUVERS
        (4) ENCIRCLEMENT
YOU MAY SURRENDER BY TYPING A '5' FOR YOUR STRATEGY.

Conclusion

I like low level stuff, and this project certainly gave my fix. It was a lot of fun hunting for PDFs of 40-year-old reference manuals for MITS products and then emulating them based off info in the original documentation. In many ways, this project felt like a treasure hunt, the treasure being technical information on BASIC and the Altair.

I learned a lot about the inner workings of the Altair 8800, something I think is incredibly valuable. The Altair kickstarted the microcomputer revolution and launched Microsoft. If it had never come along, the world of computing today would certainly be different. Similar to World War II or the Space Race, people should know about the Altair because it shaped the modern world.

All of the teletype emulation code from this post is on GitHub if you want to try it out yourself.

© Rhys Rustad-Elliott. Built using Pelican. Best viewed with NCSA Mosaic.