Unlike many other 8-bit computers of the era, the MZ-80A is only equipped with one single source of interrupt. Under normal conditions of use this interrupt occurs as part of the Real Time Clock. After 12 hours have elapsed an interrupt happens which takes care of the switch-over from AM to PM. The trigger for this interrupt is the Intel 8253 chip within the MZ-80A and, more specifically, one of the 8253’s counters.
The 8253 chip has three countdown counters. The first counter is used for producing sound (and is a square wave generator, more on this in the ‘Sound’ section of this site). The second and third counters are linked together in hardware and this cannot be undone. The reason for this is that Sharp intend for them to be used just for the RTC. The input clock to the second counter is an unchangeable 31.5Khz. This is divided down to 1Hz by this counter (which is set as a rate generator). The output from this counter is hard-wired to the input for the third, and final, counter. This counter is usually set to a value of 43500 to begin with (which is the number of seconds in 12 hours). As the second counter is effectively giving a 1Hz input to this third counter, we now achieve an interrupt once 12 hours have elapsed. The interrupt occurs because the third counter’s output is gated with the MZ-80A’s interrupt mask and it is also set up to be an “interrupt on terminal count” type of counter.
However, if you are prepared to abandon your need for an RTC (and I’m sure this is not a problem for most people writing games), we can instead commandeer the 8253 to give us the kind of interrupts we might need to cause interesting effects on the CRT screen whilst not interfering with the smooth running of our games.
The only issue with doing this on the MZ-80A is that of synchronisation. Some computers of the era are able to specify an interrupt point during screen frame display, others (like the Amstrad CPC) have several known points during screen draw when interrupts are guaranteed. As the single interrupt we are using on the MZ has nothing to do with the screen, we must anchor it to the start of screen drawing with our only available means. This is the wait for vsync. The only way to achieve this is, as you will see, to ensure that your very first interrupt handler always waits for vsync to occur. This is the only way we can guarantee the kind of timings and positionings on the screen that we need to create stable, guaranteed effects with no loss of position and also no flickering / tearing.
One last point is that the 8253’s counters are very tricky to code for with respect to the screen. They always take one single clock cycle (and this is not the Z80 clock we are talking about, it’s the input clock to the counter) to change their value to a new value. You must take this into account when coding a screen effect with interrupts. Even at the 8253’s fastest possible countdown (with the 2nd counter inputting as quickly as it is possible to the 3rd counter) we are still only able to create a new effect once four scanlines have passed. We cannot do anything earlier than this. You could, of course, switch an effect on for one scanline and off for another in the same interrupt handler but you would still have to wait four scanlines before another interrupt could occur. Unfortunately, there is no easy rule to screen draw-related interrupts on the MZ-80A, it is mostly trial and error. You just have to ensure that your very last interrupt handler happens no later than the last 4 scanlines of display, otherwise when you jump back to your first interrupt handler it will wait for vsync after vsync has already occurred naturally and you will simply see a lot of flickering or other bad visual effects.
Screen draw-related interrupts on the MZ-80A are no simple topic to cover, so apologies for the wall of text here but if you are serious about coding them then this is all very important information. One last point, the vector address for the interrupt is hard-coded to address 0038 with the standard Z80 interrupt mode which the MZ cold starts in. This is a ROM address and you will note that it jumps straight to address 1038 which luckily is in RAM. You will need to modify the address held at 1039 – 103A to go to your own first interrupt handler. Lastly, of course, the Monitor ROM can be switched out with a read of memory mapped I/O address E00C which will plant the Monitor at C000 (which then becomes the ROM area). You can then have free access to address 0038 if you so wish. With no further delay now, here are my example screen display altering interrupts…
Inverse Split Using Interrupts
This routine uses the MZ-80A’s interrupt (triggered by the 8253 chip) to create a single bar (8 pixels high) of inverse at the top of the screen every single frame. The interrupt handlers are very short routines that take very little processing time away from any main program code. The 8253 chip is programmed by sending a relevant control word to memory mapped I/O address E007 which will tell each counter how it should behave. Then counter #2 is loaded with a value (at address E006) and counter #1 is also loaded with a value (at address E005). The interrupt occurs when counter #2 has reached zero. Again, all code shown is in the format acceptable to Avalon’s Zen Assembler and I realise that this program could be written in a far more condensed way but I have made it this way to show exactly what is going on step by step.
DI ; Disable all interrupts LD SP,4000H ; set stack pointer to an arbitrary location ; in memory. Modify this to your own ; requirements LD A,(1039H) ; Preserve Zen's breakpoint LD (3200H),A ; vector at an arbitrary address in memory LD A,(103AH) LD (3201H),A LD HL,1039H ; Set up our own vector LD DE,MYINT ; to our first interrupt handler LD (HL),E INC HL LD (HL),D LD HL,0E007H ; Set 8253 counters to LD (HL),74H ; counter #1 is rate generator LD (HL),0B0H ; counter #2 is interrupt on terminal count DEC HL ; Initialise counter #1 to 0002 and counter #2 LD (HL),01H ; to 0001 and enable interrupts LD (HL),00H DEC HL LD (HL),02H LD (HL),00H EI LOOP: LD A,0F4H ; Test to see if LD (0E000H),A ; Spacebar has been NOP ; pressed. If not then LD A,(0E001H) ; jump back to LOOP AND 1 JR NZ,LOOP DI ; Disable interrupts and LD A,(3200H) ; restore Zen's vector back to its LD (1039H),A ; original location. Finally ensure LD A,(3201H) ; routine finishes with inverse off LD (103AH),A LD A,(0E014H) RET MYINT: PUSH AF ; Preserve all registers. You may PUSH BC ; not need to store all of them. PUSH DE ; But this must be done if integrating PUSH HL ; interrupts into BASIC code. PUSH IX ; Wait for Vsync to occur so PUSH IY ; we know we are synchronised with LD HL,0E002H ; display. Switch on inverse and set LOOP2: LD A,(HL) ; up 8253 counters to wait a large enough RLA ; delay before triggering next JR C,LOOP2 ; interrupt so that we have gone LD A,(0E015H) ; past vsync, through top of screen LD HL,1039H ; and up to end of first 8 scanlines LD DE,MYINT2 ; with inverse on. LD (HL),E INC HL LD (HL),D LD HL,0E006H LD (HL),01H LD (HL),00H DEC HL LD (HL),3FH LD (HL),00H POP IY POP IX POP HL POP DE POP BC POP AF EI RET MYINT2: PUSH AF ; Preserve all registers. PUSH BC ; Wait a very tiny 8-bit counter delay. PUSH DE ; This is for fine tuning of inverse effect PUSH HL ; on->off otherwise we'll see flickering PUSH IX ; and tearing. Then we switch inverse PUSH IY ; off and set up the 8253 counters for LD B,16H ; a much larger delay to help us get LOOP3: NOP ; through most of the scanlines on the DJNZ LOOP3 ; screen before ending up near the end LD A,(0E014H) ; of the screen so we can begin to LD HL,1039H ; plan for triggering the first interrupt LD DE,MYINT3 ; handler again. This takes some planning LD (HL),E ; as the 8253 counters do not change their INC HL ; values immediately when written to. LD (HL),D LD HL,0E006H LD (HL),01H LD (HL),00H DEC HL LD (HL),65H LD (HL),00H POP IY POP IX POP HL POP DE POP BC POP AF EI RET MYINT3: PUSH AF ; Preserve all registers. PUSH BC ; Inverse is already off from the last PUSH DE ; interrupt handler. All we need to do now PUSH HL ; is plan ahead for the wait for vsync PUSH IX ; which happens in the first interrupt PUSH IY ; handler. We have to be as close as possible LD HL,1039H ; to the bottom of the screen for this to LD DE,MYINT4 ; happen. But at this point, even though LD (HL),E ; we have now set the counters to their INC HL ; fastest values, we won't see them change LD (HL),D ; for another clock cycle. So we still have LD HL,0E006H ; some distance to cover on the screen LD (HL),01H ; and this will happen before the 8253 LD (HL),00H ; acknowledges the new values in this handler. DEC HL LD (HL),02H LD (HL),00H POP IY POP IX POP HL POP DE POP BC POP AF EI RET MYINT4: PUSH AF ; Preserve all registers. PUSH BC ; We still have a few scanlines left to PUSH DE ; burn at the end of the screen before PUSH HL ; we re-trigger our first interrupt PUSH IX ; handler (that waits for vsync). If we PUSH IY ; wait slightly too long then we'll cause LD HL,1039H ; flickering of the whole effect LD DE,MYINT5 ; as we wait for vsync after vsync has LD (HL),E ; already occurred. This interrupt has the INC HL ; 8253 set at its fastest rate which LD (HL),D ; gives us 4 scanlines before the next LD HL,0E006H ; interrupt occurs. LD (HL),01H LD (HL),00H DEC HL LD (HL),02H LD (HL),00H POP IY POP IX POP HL POP DE POP BC POP AF EI RET MYINT5: PUSH AF ; Preserve all registers. PUSH BC ; We now burn our final 4 scanlines PUSH DE ; with this interrupt. After the 4 PUSH HL ; scanlines are up we go back to our PUSH IX ; first interrupt handler which carries PUSH IY ; out the sync with the display. LD HL,1039H ; We have now completed our effect LD DE,MYINT ; and taken the least amount of LD (HL),E ; time away from the main program INC HL ; code as we can per frame. LD (HL),D LD HL,0E006H LD (HL),01H LD (HL),00H POP IY POP IX POP HL POP DE POP BC POP AF EI RET
And here is the same routine in BASIC, naturally it strips out a lot of stuff that is unnecessary once you are in the BASIC environment such as setting up a stack (we don’t want to mess with BASIC’s stack) and preserving Zen’s vector at 1039 – 103A as Zen isn’t loaded and, obviously, also the loop that waited for the Spacebar to be pressed before exiting as we want to immediately return to BASIC once the interrupt has been set in motion. Please also note, although there is an impact on the speed of BASIC it is not very noticeable.
5 LIMIT $CF12 10 FOR A=53011 TO 53247 20 READ B 30 POKE A,B 40 NEXT A 50 END 60 DATA 243,33,57,16,17,48,207,115,35 70 DATA 114,33,7,224,54,116,54,176 80 DATA 43,54,1,54,0,43,54,2 90 DATA 54,0,251,201,245,197,213,229 100 DATA 221,229,253,229,33,2,224,126 110 DATA 23,56,252,58,21,224,33,57 120 DATA 16,17,97,207,115,35,114,33 130 DATA 6,224,54,1,54,0,43,54 140 DATA 63,54,0,253,225,221,225,225 150 DATA 209,193,241,251,201,245,197,213 160 DATA 229,221,229,253,229,6,22,0 170 DATA 16,253,58,20,224,33,57,16 180 DATA 17,144,207,115,35,114,33,6 190 DATA 224,54,1,54,0,43,54,101 200 DATA 54,0,253,225,221,225,225,209 210 DATA 193,241,251,201,245,197,213,229 220 DATA 221,229,253,229,33,57,16,17 230 DATA 183,207,115,35,114,33,6,224 240 DATA 54,1,54,0,43,54,2,54 250 DATA 0,253,225,221,225,225,209,193 260 DATA 241,251,201,245,197,213,229,221 270 DATA 229,253,229,33,57,16,17,222 280 DATA 207,115,35,114,33,6,224,54 290 DATA 1,54,0,43,54,2,54,0 300 DATA 253,225,221,225,225,209,193,241 310 DATA 251,201,245,197,213,229,221,229 320 DATA 253,229,33,57,16,17,48,207 330 DATA 115,35,114,33,6,224,54,1 340 DATA 54,0,253,225,221,225,225,209 350 DATA 193,241,251,201
If you’d like a copy of this BASIC program to run on your MZ-80A then please find it in this folder here. Please note it will NOT work in an emulator as I have yet to find an emulator that can handle the kind of display effects we are creating here. As shown in the video above, the file is called BASIC-INT.