This is part of a series of posts analysing the Chip-8 interpreter on the RCA COSMAC VIP computer. These posts may be useful if you are building a Chip-8 interpreter on another platform or if you have an interest in the operation of the COSMAC VIP. For other posts in the series refer to the index or instruction index.
In this post I’m taking a break from analysing the Chip-8 interpreter so I can look at the interrupt routine in the operating system. This is a convenient point to take this detour as several of the Chip-8 instructions still to be covered are affected by the interrupt routine.
In an earlier post, I discussed how the video circuitry is enabled during the Chip-8 initialisation sequence.
On the COSMAC VIP the interrupt mechanism is used by the CDP1861 video display interface to indicate when it wants to begin a display cycle. Normally the 1802 will execute two machine cycles for each instruction: a fetch cycle and an execute cycle. The processor indicates which cycle it is in by setting or resetting two state control lines (SC0 and SC1). The possible states for these lines is shown below:
There are a few instructions that require three machine cycles (they have a second S2 cycle), but these cannot be used when interrupts are enabled. When the 1861 is ready to start a display cycle it pulls the Interrupt control line low to indicate an interrupt request to the 1802. This will be honoured, provided the interrupt enable flip flop is set.
When the 1802 receives the request it waits until S1 for the current instruction has been completed and then it enters an interrupt response cycle (S3). Several things happen at this point:
- the interrupt enable flip flop is reset (this prevents further interrupts from occurring while the current interrupt is being serviced).
- the interrupt is acknowledged by setting the two state code control lines high
- the 1802 saves the program counter (P) and indirect register addressing (X) registers in a temporary register (T)
- it then sets P to 1 and X to 2
Normal service is then resumed with the execution of the next S0 cycle. However, note that what is now being executed is the interrupt service routine pointed to by R1. This is shown in the diagram below.
Although the 1861 uses interrupts to indicate to the 1802 that it is about to refresh the display, it uses direct memory access (DMA) to actually read the display memory. When it is ready to display a row of pixels the 1861 takes the DMA-OUT control line low. When the 1802 receives this signal it waits until the current instruction has finished executing (the end of the S1 cycle) and then enters an S2 DMA cycle. The 1802 will execute DMA cycles until the DMA-OUT line goes high again. The 1861 will use this feature to execute 8 DMA cycles to read an entire line of pixels (8 bytes, which is 64 pixels). During a DMA cycle the following happens:
- the 1802 acknowledges the DMA transfer by setting SC0 low and SC1 high
- the address in R0 is placed onto the address bus and a memory read is requested
- the memory responds by placing the required byte on the data bus
- the 1861 reads the byte from the data bus and uses the data to drive the display (displaying a sequence of 8 pixels)
- R0 is incremented
This is shown in the diagram below:
The COSMAC VIP supports a maximum screen size of 128 pixels high by 64 pixels wide. Left to its own devices the 1861 will quite happily display the 8192 bytes starting at whatever address is in R0. In an earlier post I said that the screen size for Chip-8 programmes was only 32 pixels high by 64 pixels wide. So what happens to the other 96 vertical pixels? The answer is that all 128 rows are shown, but the pixel data is only changed after every four rows. Effectively each row of pixels is drawn four times. Although you might think this would result in tall and narrow pixels, in fact the pixels at the maximum screen size are long and thin, so drawing each row four times has the effect of making the pixels more square.
To understand how each line gets drawn four times, it is necessary to look at what happens when the screen is being refreshed. The clock frequency of the COSMAC VIP (1.76064 Mhz) is not arbitrary. It is set so that, at 60 frames per second, a single TV scan line lasts for exactly 14 machine cycles, and a single TV field lasts for exactly 3668 machine cycles. When the scan position is just about to reach the scan line that is two lines above where the display will start, the 1861 generates an interrupt. This gives the COSMAC VIP operating system exactly 29 cycles before the scan position reaches the first scan line on which drawing occurs. During this time the interrupt routine is initialised and is able to take care of some essential housekeeping before the 1861 starts reading display memory. Because the display only occupies the central portion of each scan line, there are two machine cycles at the start of each scan line before the 1861 starts its DMA access. The DMA access requires eight machine cycles. Then there are a further a further four machine cycles until the end of the scan line. The COSMAC VIP operating system uses this time to reset R0 to the start of the current row of pixels. It does this three times so that the same row of pixels is displayed four times on successive scan lines. This is illustrated in the diagram below:
A flowchart for the interrupt routine is shown below:
Here is the code for this part of the operating system:
|Address (hex)||Code (hex)||Labels||Assembly||Comments|
|8143||7A||TURN_ Q_ OFF:||REQ||Reset Q. This stops the COSMAC VIP from generating a tone|
|8144||42||RETURN_ FROM_ INTERRUPT:||LDA 2||Pop the saved value of the accumulator off the stack|
|8145||70||RET||Return from the interrupt routine. This works by popping the saved value of the T register and using this to restore P and X. It also re-enables interrupts|
|8146||22||INTERRUPT_ ROUTINE:||DEC 2||This is the entry point for the interrupt routine
Decrement stack pointer ready for a push
|8147||78||SAV||Push T onto stack (This is a temporary register that stores the values of P and X at the point the interrupt occurred)|
|8148||22||DEC 2||Decrement stack pointer ready for a push|
|8149||52||STR 2||Push the accumulator onto the stack|
|814A||C4||NOP||Do nothing for three cycles (required for timing the screen refresh correctly)|
|814B||19||INC 9||Increment the random number seed (R9)|
|814C||F8 00||LDI 0x00||This will form the low-order byte of the DMA address for the screen refresh|
|814E||A0||PLO 0||Load this into the DMA pointer (R0)|
|814F||9B||GHI B||Get the high-order byte of the display memory address|
|8150||B0||PHI B||Load this into the DMA pointer (R0)
The DMA pointer now points to the first byte of the display memory
|8151||E2||SEX 2||X is already 2, so this is effectively a two machine cycle NOP
Again, these are necessary for accurate timing of the screen refresh
|8152||E2||SEX 2||(Two cycle NOP – see note in 8151)|
|8153||80||DISPLAY_ NEXT_ ROW:||GLO 0||Get low order byte of current DMA address into accumulator|
|8154||E2||SEX 2||(Two cycle NOP – see note in 8151)
8 DMA cycles occur between this instruction and the next one. (During these cycles the CDP 1861 video interface reads a single row of pixels in the display memory and outputs it to the monitor, or to a TV via a modulator)
|8155||E2||SEX 2||(Two cycle NOP – see note in 8151)|
|8156||20||DEC 0||Decrement DMA pointer. This is necessary because the DMA pointer will have advanced to the next page after displaying any of the last four lines of the display. If it has, this will ensure the high order byte is reset to the correct page|
|8157||A0||PLO 0||Restore the low-order byte of the DMA pointer (this moves it back to the start of the current pixel row)
The second DMA access (8 DMA cycles) occurs between this instruction and the next one
|8158||E2||SEX 2||(Two cycle NOP – see note in 8151)|
|8159||20||DEC 0||Decrement DMA pointer (see note in 8156)|
|815A||A0||PLO 0||Restore DMA pointer (see note in 8157)
The third DMA access (8 DMA cycles) occurs between this instruction and the next one
|815B||E2||SEX 2||(Two cycle NOP – see note in 8151)|
|815C||20||DEC 0||Decrement DMA pointer (see note in 8156)|
|815D||A0||PLO 0||Restore DMA pointer (see note in 8157)
The fourth and final 8 DMA cycles for the current pixel row will occur between this instruction and the next one
|815E||3C 53||BN1 DISPLAY_ NEXT_ ROW||If there are more rows to display (which is indicated by EF1 being low), branch back to start of loop|
|8160||98||GHI 8||Get the general purpose timer (R8.1)|
|8161||32 67||BZ CHECK_ SOUND_ TIMER||If it’s zero, then we don’t need to do anything with it|
|8163||AB||PLO B||Temporarily copy the timer into RB.0 (This is necessary because we can’t decrement the high order byte of R8 directly without using subtraction. This would affect DF, which we need to preserve in case it is needed by the interpreter instructions to be executed after the interrupt)|
|8164||2B||DEC B||Decrement it|
|8165||8B||GLO B||Get the new timer value|
|8166||B8||PHI 8||Put it back into the general purpose timer|
|8167||88||CHECK_ SOUND_ TIMER:||GLO 8||Get the sound timer (R8.0)|
|8168||32 43||BZ TURN_ Q_ OFF||If it’s zero, we need to make sure that Q is reset|
|816A||7B||SEQ||Set Q (This causes the COSMAC VIP to emit a tone)|
|816B||28||DEC 8||Decrement the sound timer (Note that we can be sure this will have no inadvertent affect on the general purpose timer at R8.1, because this instruction can only ever be executed if R8.0 is not zero)|
|816C||30 44||BR RETURN_ FROM_ INTERRUPT||Branch to the instructions that return from the interrupt routine|
Here’s a summary of the timing of the interrupt routine for all possible conditions:
|General purpose timer (R8.0)||Sound timer (R8.1)||Machine cycles|
|zero||zero||807 (3663.78 microseconds)|
|zero||non-zero||811 (3649.5 microseconds)|
|non-zero||zero||815 (3700.1 microseconds)|
|non-zero||non-zero||819 (3718.26 microseconds)|
You need to add the overhead of 1024 DMA cycles (4648.96 microseconds) to these figures. When you consider that the display is refreshed 60 times a second, you can see that almost half of the available CPU time is consumed by servicing the display interrupt and waiting for DMA access to complete!
Note that this routine is also responsible for:
- Incrementing the random number seed
- Updating the general purpose timer
- Updating the sound timer and turning the COSMAC VIP’s tone generator on or off accordingly
Note that, although three-cycle instructions are generally not to be used when the COSMAC VIP display is enabled, an exception is the single three-cycle NOP instruction at 0x814A. This is required because there are an odd number of machine cycles between the start of the interrupt routine and the first DMA request.
A programmer of a modern interpreter probably doesn’t have to work quite so hard to refresh the display, but must, at the very least, update the timers and tone generation sixty times a second.