Beginning Z80 Machine Code Part Eleven

The last instructions we have to discuss are the group that deal with interrupts. This will of necessity be a brief discussion, as it is a most difficult subject and volumes could be written.

In addition, use of the interrupts are very hardware dependent. For this reason, usually the hardware designers and programmers will agree on a few things at the time of system design. Since we already have the system (if I could find the designers, I might hurt them), we are forced to program in the environment provided. This means that we do not have full use of the Z80 interrupts. But first, a general discussion of interrupts.

Interrupts are exactly what we would assume them to be, from the name. Whatever the CPU is doing, is interrupted. Now what?

Upon receiving an interrupt, the CPU pushes (saves) all the registers onto the stack and a jump is made to the indicated interrupt service routine (a MC program). We’ll find out a little later, how CPU knows where the service routine is.

The first thing most interrupt service routines do is disable the interrupts, to prevent confusion in case another interrupt occurs. Therefore, the service routine must re-enable the interrupt at its conclusion. Since no other interrupts can be accepted until they are re-enabled, the interrupts are commonly re-enabled at the earliest opportunity that won’t confuse our friendly CPU.

Interrupts are as competitive as us humans and each one wants to be serviced first. Since the purpose of the interrupts is to provide service for I/O devices, they are normally assigned a priority (by harware design). This allows for orderly service and nested interrupts, much the same as you would have nested FOR/NEXT loops in Basic.

For example, assume our old friend CPU is busily executing your favorite arcade ‘shoot-em up’ game and you touch the keyboard, here’s what happens:

1) An interrupt is generated of priority B, to service
   the keyboard
2) CPU stops executing your game and jumps to the
   keyboard service routine
3) During the keyboard servicing, the VDU sends an
   interrupt of priority C. Since the interrupts
   have not been re-enabled yet, this interrupt is
   not acknowledged
4) The interrupts are now re-enabled and the printer
   sends an interrupt of priority A
5) CPU stops execution of the keyboard service routine
   and jumps to the printer service routine
6) Assuming no further interruptions, the printer
   service routine finishes, and CPU returns to the
   keyboard service routine
7) CPU finishes the keyboard service routine and then
   returns to your game (you've probably lost a ship
   by this time!)

Remember, this is illustrative only! Our operating system and hardware are not designed in this manner.

Now, a little more practical. There are two classes of interrupts on the Z80, maskable (MI) and non-maskable (NMI). Maskable interrupts can be disabled (masked) with DI and enabled with EI, while non-maskable interrupts cannot be. Let’s discuss the NMI first.

The NMI cannot be masked as DI and EI do not affect it. The reason is that the NMI is a hardware peripheral interrupt that is hard-wired direct to the CPU and is not under software control. Whenever the CPU’s NMI line is grounded, a NMI occurs.

The NMI always jumps to 66h for its service routine. This interrupt is normally used for a keyboard or other device that must be serviced immediately. On the TS1000, the service routine checks for SLOW mode and refreshes the screen accordingly.

However, the NMI is not used on the TS2068. Curiously, a well known bug in the Spectrum ROM was copied over to the TS2068, even though the programmers had obviously tried to make provision for the NMI (see Dr Logan‘s Complete Spectrum Disassembly). The system variables table tells us that address 5CB0h is not used. If the ROM routine functioned properly, 5CB0h would contain the address of a service routine for the NMI.

Note: Aerco users should be able to fix the TS2068 NMI bug by using the extra ram. I have not had time to try this yet. If I do and it works, I will pass the fix along to Time Designs.

The NMI service routine is properly terminated by a RETN instruction. This allows for properly reseting the CPU (actually the interrupt flip-flop but, we won’t discuss that).

The MI is most useful to us. This is a hardware generated interrupt that occurs every 1/60th second in our computers. The TS1000 and TS2068 are initialized in interrupt mode 1 (IM 1) which causes a jump to 38h (RST 38h) for the service routine. The TS1000 uses this service routine to refresh a line of the screen in SLOW mode and the TS2068 uses it to read the keyboard. There is a method to intercept this interrupt, on the TS2068, explained in the Timex Technical Manual (available from Time Designs Magazine).

The Z80 has three MI modes, IM 0, IM 1 and IM 2. The primary difference is the manner in which the address of the service routine is determined. As we learned, IM 1 always uses 38h and is the default mode upon initialization of the system.

IM 0 allows any instruction to be placed on the data bus by the interrupting device. This mode is meant to be used with the restarts (i.e. RST 10h), which allow an otherwise three byte Call address instruction to be given in one byte. As we have previously discussed, we cannot use the restarts as they were intended to be used. Therefore, this mode is of limited value to us.

IM 2 uses the interrupt vector (register I) in addition to the byte placed on the data bus by the interrupting device, to determine the address of the service routine. The I register is the MSB and the device provided byte is the LSB. This forms a 16 bit address that points (vectors) to the address containing the address of the service routine, which is usually in a table that provides for the servicing of more than one device.

Use of IM 2 is limited by the lack of pull-up resistors on the data lines of the TS2068. It can only safely be accomplished by using a ‘kludge’ block of 257 bytes, containing the same data byte. This insures a jump to your vectored address.

Register I is initialized to 1Eh on the TS1000 and is used to determine the character pixel information. The TS2068 initializes I to 3Fh but, I have not determined to what end.

The MI service routine should end with a RETI instruction. This allows for properly returning through nested interrupts and reseting the program counter (PC). You must also re-enable the interrupt with an EI, prior to the RETI, if they have been disabled. The proper sequence of instructions is normally:

        EI          ;re-enable interrupts
RETI ;return from interrupt service

EI does not re-enable the interrupts until after execution of the instruction following EI. This is to prevent another interrupt from occuring so quick that it confuses the CPU.

Working with the MI is the instruction HALT. Halt does just what you would expect, it suspends CPU’s operation until the next interrupt. The CPU will actually be executing NOP’s to keep the memory refreshed.

You now have an extremely over-simplified explanation of interrupts and their insructions. The interrupt system is unusable on the TS1000 due to the manner in which the hardware uses it to refresh the screen. The TS2068 provides limited, but useful, access to interrupts.

Since we have learned quite a few new instructions in the above discussion, I have summarized them in the following chart.

Instr ! Function
------!----------------------------------------------
DI    ! Disables the MI
EI    ! Enables the MI, after next instruction
RETI  ! Return from a MI
RETN  ! Return from a NMI
IM 0  ! Select interrupt mode 0, forcing service from one of
      ! of the restarts
IM 1  ! Select interrupt mode 1, forcing service from 38h
IM 2  ! Select interrupt mode 2, forcing service from the
      ! vector indicated by interrupt vector and byte on the
      ! data bus
HALT  ! Suspend CPU operations by executing NOP's until next
      ! interrupt

I hope this discussion gives some of you hardware hackers the information you need for a nifty project. I’ll look forward to seeing your ideas in Time Designs.

You now have all the Z80 instructions. This series has been fun and hope has helped you get started. You can use this series as a MC dictionary but, you need to write programs in order to become conversant in the language.

Next issue I will answer as many questions as I can, as a wrap-up to these lessons. Please send them quickly and direct to me, so that I can have enough time to give a meaningful answer. If I get too many for the next issue, I will answer them individually but, not as quickly.

Syd Wyncoop
[address omitted]
Portland, Or 97233


;******************************
;* Demonstration Routines *
;******************************
;
;The following are assorted useful routines that can be
;adapted for use in your programs.
;
;
;Convert Ascii to Binary
;-----------------------
;
;Entry:A=Ascii character
;
;Exit:A=Binary number
;
Asc2Bin Sub "0" ;remove Ascii bias
Cp 0Ah ;is it larger than 9?
Jr C,EndConv ;if is, we are done
Sub 07h ;else remove more bias
EndConv Ret ;A=Binary number
;
;
;16 Bit Comparison
;-----------------
;
;Entry:HL=first number
; DE=second number
;
;Exit:Flags are set as --- zero sign carry condition
; 1 0 0 HL=DE
; 0 0 0 HL>DE
; 0 1 1 HL<DE
;
;This routine always sets zero if HL=DE. Carry indicates which
;is larger, if set then DE>HL, else HL>DE. Sign will indicate
;which is larger, if are signed integers. If sign set, then
;DE>HL, else HL>DE.
;
Comp16 Or A ;clear carry
Sbc HL,DE ;is HL=DE?
Ret PO ;return if so
Ld A,H ;invert sign bit if overflowed
Rra ;save carry in bit 7
Xor 40h ;complement sign bit 6
Scf ;insure answer <> 0
Adc A,A ;restore carry and complimented sign
Ret
;
;
;String Comparison
;-----------------
;
;Entry:HL=Base of String 1
; DE=Base of String 2
;
;Exit:Flags are set as --- zero carry condition
; 1 0 HL=DE
; 0 0 HL>DE
; 0 1 HL<DE
;
;This routine always sets zero if HL=DE. Carry indicates which
;is larger, if set then DE>HL, else HL>DE. The strings are
;assumed to be of length <=255 with a preceding byte that
;contains the length.
;
CompStg Ld A,(DE) ;get length of string 2
Cp (HL) ;compare length of string 1
Jr C,BegComp ;string 2 is shorter
Ld A,(HL) ;string 1 is shorter, get length
BegComp Or A ;test if length=0
Jr Z,ChkLen
Ld B,A ;B=# bytes to compare=length of
;shorter string
Ex DE,HL ;swap pointers
Loop Inc DE ;adjust pointers for next byte
Inc HL
Ld A,(DE) ;get next byte in string 1
Cp (HL) ;compare corresponding byte in
;string 2
Ret NZ ;return if strings are not equal
Djnz Loop ;loop until done
ChkLen Ld A,(string1) ;get length of string 1
Ld HL,string2 ;point to length of string 2
Cp (HL) ;set or clear flags
Ret
;
;
;2068 MC Pause
;-------------
;
;This routine assumes you are using the Rom keyscan and
;will read the system variable that indicates a new key was
;pressed. The only exit from loop is to press a key.
;
Pause Bit 5,(flags) ;will be set by Rom keyboard routine
Ret NZ ;someone pressed a key!
Jr Pause ;loop until key pressed
;
;
;Create IM 2 Kludge Block
;------------------------
;
;This sets up the kludge block referred to in the lesson. It
;fills the area between FE00h and FF00h with the byte FDh.
;
;You must place a Jp Address instruction at location FDFDh,
;as that is where the IM 2 vector will point.
;
Kludge Ld HL,FE00h ;base of kludge block
Ld BC,00FDh ;load counter and data
KLoop Ld (HL),C ;do fill
Inc HL ;adjust pointer
Djnz KLoop ;loop to do first 256 bytes
Ld (HL),C ;and do last byte
Ld A,FEh
Ld I,A ;put MSB in interrupt vector
Im 2 ;change interrupt mode
Ret

Note: Type-in program listings on this website use ZMAKEBAS notation for graphics characters.