Awnty (“Are we nearly there yet?”) was born out of a need to know whether my interpretation of the VT100 firmware was correct. There were many places in the code where it appeared that certain locations in the RAM were written to and then never read and even, in one case, read but never written!
So I created a coverage checker, which would enable me to run the VT100 firmware, inject it with key presses and bytes over the serial port and record which bits of code were run, and the changes made to both RAM and external devices by that code.
I envisaged using an existing 8080 CPU emulator for speed, and running it while reading a control file that I thought would probably just need a few commands:
serial <string of bytes>
– to inject bytes from the “host”key <scan codes>
– to simulate key presses, andpause <cycles>
– to regulate both of the aboveRegarding “pause
,” it is fairly simple to regulate the speed of injected bytes so that they
enter the machine at a rate no greater than (say) 9600 baud, i.e. 1000 characters per second, and also to
clock in key presses at a realistic rate, but there are times when the firmware does a good deal of work,
and the serial mechanism would have to implement XON/XOFF flow control to make sure it didn’t overrun the
serial buffer. Also, I’d like to run my coverage tests slowly enough that I can see what is happening on
the screen at certain points, so a coarse ‘just pause for a second’ command is handy.
As time went on, I realised that this simple set of commands wasn’t going to enable nearly enough coverage of the code to be sure I’d properly documented all memory accesses. At the moment, the following additional commands are used:
rxgap <cycles>
– change the timing of serial injection, in order to
stress the buffer-handling code (losing bytes, potentially)keygap <cycles>
– slow down key injection to make it clearer to see
the effect on the screen and the serial port in real time, because it’s a real slog trying to make sense
of the terminal’s behaviour by running the tests at top speed and just investigating logs afterwards.local
and online
– switch the terminal mode.
I started off by sending the terminal through SET-UP mode with appropriate key presses whenever
I wanted to change mode, but the internal effect is a simple change to one scratch RAM location,
so I’ve simplified things.dump <location, num bytes>
– helps me to tie memory changes to
a particular place in the tests as they run.have <feature>
and missing <feature>
–
lets me specify whether the terminal has the Advanced Video Option (AVO), Graphics Port Option (GPO),
or Standard Terminal Port (STP).bug <fault>
and nobug <fault>
–
exercising the code that detects faults requires, er, faults!
This lets me simulate faulty scratch RAM, nonvolatile RAM (for settings) and PUSART (to enable
testing of framing and parity errors.)reset
– necessitated by the inclusion of have
, missing
and (no)bug
, because certain bugs don’t allow me to inject key presses to take me through
SET-UP mode in order to press the ‘0’ (reset) key.I’ve developed tests to cover particular areas of the terminal’s functionality. Most of them are very simple. For example, here is the command file that runs the modem test:
have loopback pause 1000000 # allow to power up normally serial 1b,"[2;2y" pause 1000000 # Now check without loopback to see failure missing loopback serial 1b,"[2;2y" pause 1000000
By itself, this doesn’t cover much of the code, but it enables me to see that I can trigger certain behaviour. In order to run the complete coverage test, I concatenate all of these individual tests and run them together, which takes somewhere between five and ten minutes. At the moment, this means that some of the individual tests go wrong when I run the large command file. For instance, there is one point in the file where the DECID (Identify Terminal) sequence is injected over the serial port, and the terminal doesn’t produce any output. It also runs correctly elsewhere in the file, so I haven’t diagnosed what is wrong, but there is always the chance that the setup for one test disrupts the next test. Ideally, I should reset the terminal before each test. (More work still to do.)
The heart of Awnty is the 8080 emulator by Nicolas Allemand, which is written in C99. This code has a very small 8080 core, with hooks for customisation of the memory and port accesses. Two modifications to the core have been necessary; one for coverage purposes and one to run the VT100 code correctly.
Firstly, I wanted to distinguish between memory reads made by the processor for instruction fetches and reads of data.
The hook routine to read a byte from memory won’t have this information because one memory read is the same as any other.
However the core itself has two routines that perform instruction fetches: i8080_next_byte()
and
i8080_next_word()
, so marking the fetched locations as “executed” is possible here. These routines then call
the hookable i8080_rb()
and i8080_rw()
routines, so these core routines are modified to also
mark the fetched locations as “read.” The hookable write routines also mark coverage.
The second modification is to do with the interrupt handling. The approach adopted by the core is that
you call i8080_interrupt()
to ask for an interrupt to be serviced, specifying the opcode that will be executed,
which is probably one of the RST (restart) instructions. However, that isn’t the way that interrupt handling works on
the VT100. (In fact, I’m not sure it works like that on any system.)
There are three sources of interrupts on the VT100: the PUSART, keyboard and vertical blanking. These interrupt signals are ORed together into the INT signal to the 8080, to say, effectively, “at least one interrupt is pending.”
If interrupts are currently enabled at the end of the current instruction (or one instruction later if we’ve just executed EI to enable them), the processor raises INTA (interrupt acknowledge), which heads through the 8228 to a latch called E41, which makes up a RST opcode based on the state of all the interrupt request lines. This way, any of the instructions from RST1 to RST7 will be executed, and the firmware itself can prioritise its response with the appropriate interrupt handler.
This all seems simple enough, until we consider that some routines cause misinformation to appear in the coverage map. The power-on self-test (POST) routine checks the ROMs by reading every location in order to calculate checksums, and performs a write-and-readback test to check the RAM. This is not useful information for coverage, so I have to censor these from from the read- and write-byte routines. The writing done by the VT100 firmware’s memset() routine is also censored.
Unfortunately, as this censoring is done in the core itself, there is very little distinction between core and non-core (hookable) functionality any more. As I only need a coverage checker for the VT100 firmware, I haven’t even made an external censor list that could be read, as I’d have to do if I was releasing a general coverage tool.
Having coverage for just “exec,” “read” and “write” would leave some gaps in the final map. The majority of RAM is given over to the video display, which is written by the CPU but read by DMA from the video controller. So, my routine to produce the screen display records its path through memory as DMA accesses.
Some locations in the ROM are data structures rather than code, so I pre-load the coverage map with these structures, based on my latest disassembly, and then these are shown in a different colour when they are read. Code that is thought to be unreachable, and the zero bytes at the end of the final ROM, are also marked in the coverage map at the beginning of testing.
As Awnty runs, it produces an annotated display of the terminal screen so I can tell what is going on:
The annotations to the left of the screen show the line attributes: scrolling region and line size. In the keyboard LEDs region at the bottom, it displays the current number of characters available in the serial buffer, which enabled me to judge how well the terminal could keep up with a continual stream of characters as I adjusted the injection speed. The four switch banks of the “SET-UP B” screen are decoded to the right, at the bottom.
The final output of running all the tests is the coverage map, which is built as the tests are run, and then summarised in text form. The map itself:
The latest (2022-02-18) coverage summary looks like this:
587/ 587 reachable symbols executed uncovered 0028 - 002b ( 4 bytes) uncovered 0038 - 003a ( 3 bytes) restart6 + 8 uncovered 008b - 008f ( 5 bytes) patt_loop + 11 uncovered 00f1 - 00f4 ( 4 bytes) skip_click + 17 uncovered 01a5 - 01aa ( 6 bytes) ansi_app_mode + 7 uncovered 0221 - 0221 ( 1 bytes) uncovered 027e - 0284 ( 7 bytes) have_avo + 21 uncovered 0565 - 0568 ( 4 bytes) nobell + 22 uncovered 060e - 0615 ( 8 bytes) charset_range + 15 uncovered 0622 - 0623 ( 2 bytes) normal_mapping + 12 uncovered 06bd - 06c1 ( 5 bytes) process_keys + 19 uncovered 073d - 0742 ( 6 bytes) check_rpt_slot + 5 uncovered 0830 - 0837 ( 8 bytes) X082b_ + 5 uncovered 08ad - 08b1 ( 5 bytes) note_failure + 6 uncovered 09b9 - 09ba ( 2 bytes) recog_esc + 14 uncovered 0ac1 - 0ac1 ( 1 bytes) execute_seq + 31 uncovered 0cf8 - 0cf8 ( 1 bytes) ansi_identity + 21 uncovered 0d47 - 0d49 ( 3 bytes) curpos_report + 16 uncovered 0e08 - 0e0a ( 3 bytes) try_tab_loc + 6 uncovered 0e1c - 0e1c ( 1 bytes) chk_rmargin + 7 uncovered 11ad - 11af ( 3 bytes) no_shuffle + 13 uncovered 130a - 130f ( 6 bytes) tparm_report + 26 uncovered 131b - 131b ( 1 bytes) skip_odd_even + 11 uncovered 14a0 - 14a1 ( 2 bytes) update_kbd + 13 uncovered 14fb - 14fb ( 1 bytes) curs_was_off + 22 uncovered 1523 - 1523 ( 1 bytes) el_to_end + 14 uncovered 1ee3 - 1ee4 ( 2 bytes) answer_print + 6 uncovered 1f0c - 1f0c ( 1 bytes) c132 + 16 Total uncovered bytes = 96 unread 2012 - 2021 (16 bytes) unwritten 2012 - 2021 (16 bytes) unread 2052 - 2055 ( 4 bytes) unwritten 2064 - 2064 ( 1 bytes) unread 2066 - 2066 ( 1 bytes) unwritten 2066 - 2066 ( 1 bytes) unwritten 206f - 2071 ( 3 bytes) unwritten 2077 - 2077 ( 1 bytes) unread 210f - 2110 ( 2 bytes) unwritten 210f - 2110 ( 2 bytes) unread 213f - 213f ( 1 bytes) unread 2149 - 214a ( 2 bytes) unread 214c - 214d ( 2 bytes) unwritten 214c - 214d ( 2 bytes) unread 2170 - 2170 ( 1 bytes) unwritten 2170 - 2170 ( 1 bytes) unread 21b1 - 21b3 ( 3 bytes) unwritten 21b1 - 21b3 ( 3 bytes) unread 21b6 - 21b7 ( 2 bytes) unwritten 21b6 - 21b7 ( 2 bytes) unread 21ca - 21ca ( 1 bytes) unwritten 21ca - 21cb ( 2 bytes) unread 22bb - 22cf (21 bytes) unwritten 22bb - 22cf (21 bytes)
This summary tells me that only 96 bytes out of the reachable code in the 8192-byte ROM have not been executed. All of them are just a few bytes long, which makes it simple enough to look at the listing and judge whether I need to write any test cases to cover them or whether I’m happy that they could be executed.
The “unwritten/unread” lines refer to locations in scratch RAM. I haven’t matched them to the symbol table from the assembler, although I should do.