Trapping execution

From Spectrum
Revision as of 21:03, 6 July 2008 by Winston (talk | contribs) (New page: The Spectranet CPLD has a number of fixed traps, for paging in its own ROM and doing various things, such as initializing on reset, handling an NMI, extending BASIC etc. All these traps ar...)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

The Spectranet CPLD has a number of fixed traps, for paging in its own ROM and doing various things, such as initializing on reset, handling an NMI, extending BASIC etc. All these traps are hardwired into the CPLD, and occur when the Spectrum's Z80 performs an instruction fetch at the relevant address, and cause the Spectrum ROM to get paged out and the Spectranet ROM to get paged in.

One trap, however, is programmable and can be set to any address in memory - including RAM. It's intended to be used for things such as tape traps (for example, to allow a TAP file to be loaded over the network). But it can be used for many other things, too. The trap is controlled by a pair of registers in the CPLD, which contain the address that should be trapped, and also a flag in the CPLD control register that allows the trap to be enabled or disabled. Three ROM functions are used to control the trap: settrap sets the address where a trap should occur, and the routine that should be called, and enables the trap. The disabletrap routine disables the programmable trap (without changing the trap address), and enabletrap enables it.

When a trap occurs, unlike the fixed traps in the CPLD (which page the ROM without disturbing the Z80's program counter), an NMI is fired. This means when the instruction at the trap address completes, the CPU jumps to the NMI handler and disables interrupts. This in turn causes the fixed NMI trap to occur, paging the Spectranet ROM. The NMI handler examines the return address on the stack, and if it's the address specified in the initial call to settrap, it jumps to the routine that was associated with the trap. If any other address is seen, the 'NMI button pressed' routine is run instead.

Setting the trap

The trap is set up with a call to the settrap routine. This routine is called with HL pointing to a block of memory containing data on how the trap should be handled when it occurs. The block looks like this:

Byte 0  : Memory page to page into paging area B. Set this to 0 to do no paging.
Byte 1,2: Address to call when the trap is handled
Byte 3,4: Address that the trap comes from (i.e. the address that will be on the stack)
Byte 5,6: Address that should be trapped

Byte 0 of the data structure is a 4K page in Spectranet memory. It can be any page - pages 0x01-0x1F are in flash memory, pages 0x80-0x88 are ethernet buffers, and pages 0xC0 to 0xDF are in the static RAM. In practise, you will be using pages in the flash memory or the static RAM, since it doesn't make an awful lot of sense to run code out of the ethernet buffer.

This controls the page in paging area B (0x2000-0x2FFF). If the code you want to run is elsewhere, such as the Spectrum's main memory or in the RAM area from 0x3000-0x3FFF, byte 0 should be left set to 0, to avoid putting a new page into paging area B.

Bytes 1,2 contain the address to call when the trap is handled. This is the address of your handler. The code here is essentially called with the CALLEE convention - i.e. you have to fix the stack before returning. It's done like this to give you the most flexibility in handling how the code exits. The examples in this page should make this clearer.

Bytes 3,4 contain the 'come from' address. This will be 1 to 4 bytes beyond the address that should be trapped. When an NMI is fired, the Z80 finishes the instruction at which the NMI occurred, so the address on the stack when the NMI handler is called will be up to 4 bytes after the trapped address. When you set an address to be trapped, you therefore must find out how long the instruction is at the trap address so you can set this.

Bytes 5,6 contain the address that should fire the trap.

As you would expect, all two byte values are little endian.

When your trap handler is called, there are certain things that have been done already - the main registers all have been pushed on the stack. They are pushed in the order hl, de, bc, af. There are many ways in which you can examine the stack - for example, you can also push ix to preserve it, set ix to the stack pointer and use ix+d to examine what hl, de, bc and af were set to at the point the NMI handler was entered. Another thing to note is that the Spectrum ROM is paged out and the lower 16K currently has the Spectranet mapped into it, so you may need to unpage the Spectranet on exit.

Example 1 - a simple trap in RAM

You may want a trap in RAM if you need to debug some of your code, and you've got no emulator to use for debugging (it will probably be a while before any emulators emulate the Spectranet hardware) - so you want to set up a trap at a point in RAM to find out the value of the CPU's registers are at a certain point in your program.

The following example shows how a trap is set up in RAM.

     include "spectranet.asm" ; Include Spectranet ROM call symbols
     org 0x8000               ; 32768
     ld hl, trapblock         ; trapblock = address of a block of memory containing the trap details
     ld ix, SETTRAP           ; Call the SETTRAP routine
     call IXCALL
     ret                      ; return to BASIC

trapblock    defb 0           ; We don't want to page anything in
             defw EXECTRAP    ; The routine we want to call
             defw 0x9001      ; The address that will be on the stack after the NMI gets fired
             defw 0x9000      ; The address that should fire the trap

You'll note that bytes 4 and 5 are set to 1 byte after the trap address. This is because the instruction at 0x9000 is one byte long, and so when the NMI is fired, 0x9001 will be the address that's pushed onto the stack. Now the program continues, with your actual handler:

EXECTRAP
     ; .... your code goes here
     
     ; This code handles fixing the stack on exit.
     ; At the very least, hl, de, bc and af are on the stack.
     pop af
     pop bc
     pop de
     ; Now the return address must be fixed, with HL preserved at
     ; its value when the NMI was called. This is quite easy to do...
     ld hl, PAGEOUT           ; 0x007C
     ex (sp), hl              ; Set HL to its stacked value and put 0x007C on the stack (the pageout address)
     retn

As mentioned earlier, your trap handler is called with the 'callee' convention - i.e. you need to handle cleaning up the stack. This means at the very least restoring af, bc, de and hl, and then returning to the correct address. The above example simply returns back to the point at which the NMI occurred and continuing. You may not necessarily want to do the pageout, for example - if your program already has the Spectranet memory paged into the lower 16K. If you don't want to page out the Spectranet memory, just POP HL and RETN.

At this stage, it's important to note that you must exit the handler with RETN. The Spectranet CPLD decodes the RETN instruction so that it knows when the NMI handler is complete. Also, the Z80 restores the state of maskable interrupts when it executes the RETN instruction. If you use RET instead of RETN, no further traps will fire (because the Spectranet CPLD thinks the NMI routine is still running), the NMI button will no longer work, and the state of maskable interrupts may not be what you expect.

Example 2 - Calling a routine in flash memory