For the past three years I've been involved in hacking the 8088 card for the Commodore CBM-II computer:
http://www.6502.org/users/sjgray/computer/8088/Basically, it's an add-on card for a 6502-based computer which adds a second CPU (8088) allowing it to run a specially compiled version of MS-DOS 1.25. It was an attempt by Commodore to join the "MS-DOS compatible" club, and as all the other attempts, it failed in the market because it was not IBM PC compatible and no PC applications could run on it. In fact, this card is about as
incompatible as it gets: the 8088 CPU has no access whatsoever to any I/O or video, instead passing all requests via an IPC mechanism to the 6502 CPU which handles everything and passes the results back [it's actually a really interesting configuration, but it's a story for another time].
Anyway, my goal was is to make this hardware setup able to run PC applications by making it as PC compatible as possible using software emulation. Interestingly, it is possible to achieve a very high degree of compatibility with a software-only solution, by (1) reimplementing a set of PC BIOS interrupts as wrappers for appropriate IPC calls to the 6502 and (2) writing a routine that periodically copies screen data from the B000 segment to the 6502 video memory. I am able to run FreeDOS, Turbo Pascal, Norton Commander etc. with this setup.
But there are a lot of PC applications that access the PC hardware directly, so emulating the PC BIOS is not enough to make them work. So I wanted to introduce a higher degree of compatibility, by emulating PC hardware chips... in software too of course
To that end, I added a little circuitry to the board, which generates a NMI signal whenever an I/O device is accessed. There are only two instructions that can be used to access I/O: IN and OUT, and they always operate on the AL register. So the theory is: when one of these instructions is used, NMI is generated and the interrupt handler routine performs appropriate emulation tasks using the value in AL (in case of OUT) or stuffing AL with appropriate value on return (in case of IN).
Well, in theory there is no difference between theory and practice. In practice, however, it turned out that the Intel 8086 datasheet is misleading. It says that when a rising edge is detected on the NMI line, the processor will finish the current instruction and handle the interrupt. This is not true - after a rising edge on the NMI line, the processor finishes the current instruction, executes the next one and only then handles the interrupt (I have tested this on both 8088 and V20).
Of course this poses serious problems with emulation, because if the interrupt occurs after the second instruction, this instruction may very well change the value in the AL register. This is not such a big deal with OUT, as a simple latch can hold the original value that was written to the I/O space. Not so easy with IN, though; a code such as that (which is very common) will not work:
Code:
IN AL, someport
TEST AL, 01
JZ somewhere
The interrupt is handled only after the TEST instruction is executed, and it is too late - the (bogus) value in AL has already been checked.
The solution I came up with is a kludge, but it works in most reasonable scenarios: the interrupt routine gets the return address from the stack, checks if two bytes earlier there is an instruction that operates on AL (currently I am checking for TEST, OR, AND, CMP and MOV) and if so, modifies the return address on the stack so that the interrupt returns to this instruction, which is executed again, this time with a correct value in AL.
There is, of course, a hundred ways it can go wrong, but generally it works for realistic code scenarios. This way I was able to make Turbo Pascal sound() function work, by intercepting accesses to the I/O ports 42h and 61h, and using the values written there to drive the SID chip on the 6502 side. Still, it remains what it is: a kludge, which may or may not work based on many factors.
So, I guess what I really am asking about, is: is this NMI behavior normal on the 8088? And is there anything that could be done to prevent or circumvent it?