The Annotated VT100 Firmware: Bugs and Oddities

More than twenty years ago, John ‘Sloppy’ Millington wrote a document called VT100 Oddities that detailed some ways of provoking a VT100 into musical and visual feats that should never have been possible. Finding out whether these were true and, if so, why they occurred, was one of my motivations for performing the disassembly at all.

The disassembly provided the answers to some, but not all, of Sloppy’s Oddities and revealed many other bugs and quirks that I’ll take you through in this section. Certainly, without performing a full disassembly, I’d never have discovered the sequence to trigger the Easter egg game of Pong.

Noisy LEDs

The first two of Sloppy’s Oddities concerned the DECLL “Load LEDs” sequence, which could be abused to play music. The User Guide says that the sequence takes a single parameter from zero to four. Zero will turn all LEDs off, and the other numbers will turn on a single one of the four user LEDs.

Unfortunately, the code attempts to rule out incorrect parameters by subtracting 5 and exiting if the result is positive. This goes wrong when a parameter of between 133 and 255 is passed in, because the result of the subtraction is negative, in two’s complement arithmetic.

The code then treats this number as a positive count for rotating a single-bit mask and stores this away to send to the keyboard on every vertical blank, 50 or 60 times a second.

In Sloppy’s two examples:

  1. ESC [ 137 q will repeatedly set the “start scan” bit, which will cause any key that is presssed to seem as if it is auto-repeating, producing a set of key clicks at a rate high enough to be sort of musical. Different keys will produce different tones because they will be detected at different times in the keyboard scan, so are reported fractionally sooner or later from the start of each scan.
  2. ESC [ 145 q sets the “speaker click” bit, which will continuously sound the bell, allowing you to tune your piano’s G string, one octave above middle C. When you realise you can’t stop the sound, you may be tempted to smash your terminal, or piano.

MAME’s VT100 emulator is accurate enough to demonstrate both of these bugs, which I find frankly astonishing.

See the decll_action routine for the details.

Screen corruption

The last of Sloppy’s examples is, he admits, rather tricky to provoke, involving double-width lines, scrolling regions and multiple line feeds. To quote the effects he remembers:

Sometimes this just causes the terminal to reset. Other times, I’ve seen it do much stranger things, like display the same logical line on several different physical lines. The most impressive result is when you get an entire column of 24 cursors, flashing in unison.

My attempts to reproduce this have failed, so far. Trying to unpick the method of provoking it and the claimed effects, I’ll note two things:

Firstly, the VT100 video circuitry doesn’t have a hardware cursor. The cursor is produced by setting and resetting the top-most bit of the byte containing the character under the cursor in video RAM, on a timer. This top bit is called the ‘base attribute’ and produces either an inverse video effect on that character if the block cursor is selected, or an underline if that is selected. Therefore, the only way of producing a screen full of cursors is for the pointers at the end of the cursor line to be pointing back to the same line, making that line the fill line. So, we could be looking for corrupted line endings, and scrolling would certainly be the simplest way of provoking those.

Secondly, I’d guess that double-width lines are involved in some way because of the peculiar way that they are handled by the firmware. On a normal screen full of single-width lines, there are three terminating bytes on each line that point to the address of the next line to be displayed. However, for double-width lines, a second set of terminators is placed halfway across the screen line. The code to maintain these pointers during scrolling is more complicated because one copy is updated outside of the vertical blank interrupt, unlike the normal terminator shuffling. If the two sets of terminators became mismatched, I could imagine corruption occurring.

I have found a small piece of code in which the firmware seems to lose track of what value is in a register on one path through, so that needs investigating, but I haven’t seen any other bug in this area. Of course, it is always possible that the bug might have existed in an early version of the VT100 firmware and was then corrected. There are no version numbers in the firmware by which we could track this, and the Print Set revisions I’ve seen all name the same four ROMs.

Now we’ll move from Sloppy’s bugs to new ones I’ve spotted during this investigation.

Origin Mode

Origin Mode (DECOM) is a private DEC mode that controls whether the cursor position, line and column numbering is constrained by the current top and bottom margins. Setting or resetting this mode will also cause the cursor to move to the home position, which will be the top left of the screen when the mode is reset, or the top left position of the scrolling region when set.

This mode can be set or reset in a list with any other DEC private modes, so that the sequence:

ESC [ ? 6 ; 3 h

is supposed to set Origin Mode (6) and Column Mode (3) at the same time. Column mode will set the screen width to 132 columns.

However, this sequence will not work. Origin Mode will be changed as expected, but Column Mode will be ignored. This is because there is a bug in Origin Mode’s handling of the final “cursor home” sequence, which it invokes by clearing the first two parameter to zero and then invoking the routine to position the cursor. However, clearing the second parameter to zero destroys the second parameter to the Set Mode sequence we are already working through, which stops the screen changing to 132 columns.

Because only the first two parameters are cleared, this sequence:

ESC [ ? 6 ; 0 ; 3 h

will work correctly.

See the decom_mode routine for the details.

Character Set Mapping

This one is quite complicated, and reveals a frailty in pointer arithmetic that is seen elsewhere in the VT100 firmware (without having the same unfortunate consequences.)

On an 8-bit processor, it is tempting to restrict as many arithmetic operations as possible to just 8 bits, even for those involving 16-bit address pointers. If you have a base address in the register pair HL and an offset in A, there are two correct ways to calculate the offset address. Firstly, by using another register pair, which could be BC or DE:

        mov     e,a     ; extended offset to 16 bits
        mvi     d,0
        dad     d       ; HL <- HL + DE

or, if you don’t have a spare register pair available:

        add     l       ; add low byte of address
        mov     l,a
        jnc     skiph   ; no carry - doesn't touch high byte
        inr     h
skiph:  ...

But, if you know that your data structure doesn’t cross a 256-byte boundary, it is tempting to lose that conditional jump and increment of H, and leave your calculations as the straight line:

        add     l
        mov     l,a

I refer to this kind of calculation as a ‘frailty’ because it works right up until you move that data structure and then it silently bites you.

On the VT100, characters are displayed from one of four character sets, known as G0 to G3, that are invoked into the GL area, which is the ASCII codes 020h (SP) to 07eh (‘~’.) Reading the User Guide, you’d form the impression that there are only two character sets, G0 and G1. G2 and G3 are present internally, but there are no sequences to invoke character sets into them, so they always contain the base ASCII or U.K. set, depending on a SET-UP switch.

G2 and G3 can be invoked into GL with the SS2 and SS3 single shift sequences, which are also not mentioned by the User Guide, but merit a passing mention in the Technical Manual.

The table that maps character sets into G0 to G3 is four bytes long, at addresses 20fdh to 2100h. When an offset into this table is made, the calculation contains the frailty described above. If G3 is invoked into GL when a character is displayed, it will always come from the U.K. set, regardless of the SET-UP preference. To demonstrate this, consider these three sequences:

  1. SI # SO # SI # (designations being G0, G1, G0)
  2. SI # ESC N # # (designations being G0, G2, G0)
  3. SI # SO # ESC N # (designations being G0, G1, G3)

With the SET-UP character set preference for U.S. ASCII, these sequences should all produce three hashes ‘#’ on the screen but the third will produce two ‘#’ and a ‘£’.

See the print_char routine for the details.

Parsing of Control Sequence Intermediates

The ANSI X3.64 parser in the VT100 is supposed to correctly parse and discard unsupported sequences. For escape sequences with too many intermediate characters, such as ESC ( ( 0, it will do this correctly. For control sequences with intermediates, like ESC [ ! m, the parser contains a bug that will cause the sequence to terminate early and display the ‘m’ character.

This bug is hard to find because the VT100 doesn’t support any control sequences with intermediate characters at all, so you wouldn’t ordinarily think of testing this aspect of the X3.64 standard. In fact, I spent hours reading the code and constructing test cases for Awnty, wondering why I couldn’t execute this part of the parser before realising that it was buggy.

See the gather_params routine for the details.

VT52 Cursor Positioning

In VT52 mode, the sequence ESC Y row col, where ‘row’ and ‘col’ are both single characters, positions the cursor at the given row and column.

As I documented in A parser for DEC’s ANSI-compatible video terminals, the ANSI X3.64 standard doesn’t define the behaviour for the presence of C0 controls in escape and control sequences. DEC’s approach was to execute them while in the middle of parsing the longer sequence.

The VT52 pre-dates X3.64, of course, and its sequences are generally shorter, but I had long been curious about how C0 controls in the VT52 cursor positioning sequence would be handled, without that curiousity ever reaching the giddy heights of spurring me to find out!

The first experiment is the sequence ESC Y ESC Y 0 0. Either the second ESC Y is going to cancel the first one and the cursor will be moved to the position encoded by ‘0 0,’ which is row 16, column 16, or the terminal will attempt to interpret the second ESC Y as a set of coordinates, for which the row will be invalid, and the cursor will move to column ‘Y’ (i.e. column 57) on the current row.

The result is that the cursor moves to (16, 16). OK, so it looks as if ESC cancels the first sequence.

The second experiment is the sequence ESC Y 0 ESC Y 0 1. If our guess about the behaviour from the first experiment is correct, then this sequence cancels the first sequence after reception of the column and move the cursor to row ‘0’ (16), column ‘1’ (17). In fact, it moves to (16, 16) and displays ‘1’.

Analysis of the code reveals that gathering the coordinates for a VT52 cursor positioning sequence relies on a sub-state flag which I call “vt52_got_row,” and this state is not reset when the ‘Y’ that starts (or restarts) the sequence is recognised. This means that, in this experiment, the second ‘0’ is used as the column number, ending the sequence, and the ‘1’ is displayed because the parser is now back in “ground” state.

Other C0 controls will be interpreted, as expected, during reception of this sequence, so that ESC Y BEL 0 0, for example, will sound the bell and then move the cursor to (16, 16).

See the vt52_get_coord routine for the details.

ANSI Cursor Positioning

When processing the ANSI cursor move sequences CUP and HVP, the firmware loses track of the purpose of register C. It is initialised to contain the origin mode flag but if the flag is unset (i.e. cursor moves are absolute, not constrained by margins), it switches to contain the requested new row number.

The effect of this is that, with origin mode reset (i.e. absolute mode), moving from a row that is double width to a row that is single width will constrain the column number to the right margin (i.e. column 40 or column 66) of the current row, not the requested row, if the requested row is anything other than row 1.

See the curpos_action routine for the details.

NVRAM Addressing

This quirk is entirely invisible to the user and doesn’t affect the correct operation of the terminal at all, but the firmware addresses the NVRAM locations in a non-contiguous order because of a misreading of the specification for the ER1400 NVRAM.

The ER1400 contains 100 14-bit words, addressed by a 20-bit one-of-ten binary coded decimal address, which takes longer to describe than it does to demonstrate. If we ignore the negative logic of this chip and show ‘0’ as ‘-’, here are some 20-bit encoded addresses, according to the datasheet:

The last one is the example given in the datasheet. The first ten bits are the ‘tens’ value of the address and the second ten bits are the ‘units’ value.

The firmware gets this wrong in two ways:

  1. It uses the first ten bits for ‘units’ and the second ten bits for ‘tens’, and
  2. It reverses all ten bits for each half of the address.

As there is a one-to-one mapping between the intended address and the actual one, nothing goes wrong. There are 51 values stored in the NVRAM, from (intended) address 00 to address 50. The checksum is always intended for address 99, so ends up in address 00.