To achieve the goal of making the machine PC-compatible, it is necessary to pretend that it has a set of PC peripherals present, such as the 8253 PIT, 8250 UART, the 6845 CRTC from the MDA card, and so on. Unfortunately, the Commodore CBM-II has none of these. What is one supposed to do then? Well, emulate them in software of course.
To do this, one little modification was added to the board’s hardware. Whenever the
/IOWC lines are asserted (which indicates an I/O port read or write by the CPU), the
NMI line is asserted as well. Therefore, on each I/O port access the CPU is interrupted1; the NMI handler routine can then do all sorts of tricks to pretend that the addressed I/O device actually exists.
Why this works
Interestingly, the 8086 CPU has only four instructions for I/O access (if we ignore the V20’s
IN AL, port
IN AL, DX
OUT port, AL
OUT DX, AL
This basically means that wherever an I/O device is being read or written to, the actual value is always held in the the AL register (if we limit ourselves to 8-bit transfers, which is sufficient for all devices that need to be emulated). Similarly, the address of the I/O port is either encoded in the instruction or held in the DX register.
The NMI handler can thus easily detect which one of these four instructions caused the interrupt, and extract the port number and the value being written to. To make things faster, a hardware latch was added to the board chipset which holds the address and the value, but it is not strictly necessary – things would work well without it, just a bit slower.
After the port address is known, the software can then perform the same actions that the PC hardware would do; for example, writing a value to the 3F8h port (which is the transmit data buffer of the 8250 UART) should cause the character to be passed to the via an inter-processor call to the 6509’s KERNAL to be sent over the RS-232 line. And voila, our machine has virtual hardware chips emulated in software.
Why this can’t work
There is just one little problem with the above scenario. The I/O read line is asserted quite late in the 8086 bus cycle, well after the address bus is latched and stabilized2. This is good for the I/O chips, but bad for generating an NMI signal because by the time it reaches back to the CPU, it is too late – its pipeline is already busy preparing to execute the next instruction.
What this means is that the interrupt does not occur after the
OUT instruction; it occurs only after the next instruction after
OUT. And this is bad. Like, really bad; consider this scenario:
IN AL, port
TEST al, 01
<-- this is there the interrupt happens
By the time the interrupt occurs and fills the AL register with the correct value, the old (wrong) value is already tested and the flags set. The next instruction is a conditional jump which will be performed (or not) according to these incorrectly set flags. Everything goes wrong from there.
Why this still works (most of the time)
Fortunately, there is still a way to fix this in most scenarios. After filling the AL register with the correct value, the interrupt routine must simply restart the instruction that was executed right before the interrupt. To do so, it must look back from the interruption point to see what instruction it was, and determine if it needs to be restarted or not. Generally the instructions that require restarting are those which do something with the value in AL, such as:
MOV somewhere, AL… and so on.
TEST AL, something
AND AL, something
OR AL, something
CMP AL, something
There is no easy way though to determine which instruction was executed before the interrupt. A good heuristic is first to realize that unless they have a memory operand, almost all of these instructions are two bytes long (there are interesting one-byte exceptions though:
XLATB). Then one needs to check if 3 bytes back there is an ECh value (one-byte
IN AL, DX instruction) or 4 bytes back an E4h value (two-byte
IN AL, port instruction). If yes, there is good chance that the next instruction (thus the one that would need to be restarted) is two bytes long and its operand code can be checked to see if it actually needs restarting.
This is of course one big ugly hack. There are many different ways where it can go wrong, depending on what instruction was actually executed after the
IN instruction. But in practice, with real life 8086 code, it turns out that it just works. Or, at least, I still haven’t found a case where it doesn’t.
- Of course, the interrupt should not be generated for the board’s internal chipset registers from the range E0h-EFh. It should also be disabled when accessing the on-board peripheral chips. To achieve this, a bit in the chipset settings either enables the NMI or disables it and enables access to he on-board chips, whose addresses would otherwise conflict with PC peripherals.
- For writes, there is an “advance write” signal which is generated earlier, so this is not a problem