Trapping execution

From Spectrum
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, set to 0xFF for current page B
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. If this is set to 0xFF, then the current memory paged into paging area B is used. This is useful for ROM modules since it frees the module from the task of finding which ROM number in which it is installed.

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

     jp TRAPRETURN            ; restore the stack and unpage the Spectranet

The TRAPRETURN routine pops AF, BC, DE and HL, and returns via the unpage address in the Spectranet ROM. You may need to do something slightly different. 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 TRAPRETURN routine will do this for you.

Sometimes you might want to do something else, for example, return to a different address, or return with the registers changed from their stacked values. You can base a custom return routine on the following code:

return      
     ; 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

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, your code would look like this:

return
     pop af
     pop bc
     pop de
     pop hl
     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 or SRAM

Code that runs from flash memory is generally run from paging area B (0x2000-0x2FFF), so the initial NMI handler will page the requested page here. Setting up the trap is exactly the same as in the above example, except you specify the memory page you want in byte 0 of the block passed to settrap, and you'll generally have a call address in the range 0x2000-0x2FFF.

If, for example, the code you want to call when the trap fires is in page 4 of flash memory, and at address 0x2200 - and you want to trap execution at 0x028E (the KEY_SCAN routine in the Spectrum ROM), you would set up the block to pass to settrap as follows:

trapblock     defb 0x04      ; page 4
              defw 0x2200    ; address of routine to call
              defw 0x290     ; address on stack at time of NMI
              defw 0x28E     ; address at which to fire the NMI

Alternatively, if the trap setup is being done while the page with the target code is actually paged in, the trap block can be written like this:

trapblock     defb 0xFF      ; current page in paging area B
              defw 0x2200    ; address of routine to call
              defw 0x290     ; address on stack at time of NMI
              defw 0x28E     ; address at which to fire the NMI

Since the instruction at 0x28E in ROM is LD L, 0x2F - the value pushed on the stack for the return address will be the trap address plus 2, 0x290.

The NMI handler will call your routine in page 4 (which is page 4 of flash memory), address 0x2200. When it does this, it will push the currently mapped page onto the stack. This means you need a different way of returning from your handler routine. The added complication is that the top entry in the stack is the previous page in area B. If you just call POPPAGEB at this stage, your program will probably crash - because the old page will come back into the paging area you are currently running code from, and it will probably not do what you want! This is just one of the comnplications of bank switching.

The simplest way to do it is to just jump to the base ROM call pagetrapreturn. This runs in the fixed ROM page in the lowest 4K of address space, and so is unaffected by changes to what's paged into area B. This takes care of restoring page B to its original contents, restoring the stack, and then executing RETN. So your code at 0x2200 would look like this:

0x2200       ....your code here....
             ....finished...
             jp PAGETRAPRETURN  ; restore page B, pop items off stack, and return control to ZX ROM

Note that pagetrapreturn unpages Spectranet memory from the lower 16K, so the Spectrum ROM is paged back in. If you need to do something else, you will need to write the code to do it, and put it somewhere in memory other than 0x2000-0x2FFF. The best way of doing this in most cases is to copy your return code to some temporary workspace in the fixed static RAM page (0x3000-0x3FFF). If you need to do this, your return code should look something like this:

return
     ld hl, 10                  ; return address is 10 bytes up the stack
     add hl, sp                 ; set hl to point at the return address
     ld (hl), RETURN_LSB        ; LSB of the new return address
     inc hl
     ld (hl), RETURN_MSB        ; MSB of the new return address
     call POPPAGEB              ; Restore page B
     pop af
     pop bc
     pop de
     pop hl
     retn

There are other ways of writing the return routine - it really depends on what you need to do. The example above replaces the NMI return address with an address of your choice, and does not page out the Spectranet ROM. To do the same and page out the Spectranet ROM, restoring the Spectrum ROM, your return routine would look like this:

return
     ld hl, 10                  ; return address is 10 bytes up the stack
     add hl, sp                 ; set hl to point at the return address
     ld (hl), RETURN_LSB        ; LSB of the new return address
     inc hl
     ld (hl), RETURN_MSB        ; MSB of the new return address
     call POPPAGEB              ; Restore page B
     pop af
     pop bc
     pop de
     ld hl, PAGEOUT             ; Spectranet pageout address
     ex (sp), hl                ; RETN will return via pageout
     retn

Just LDIR this code into a suitable place in 0x3000-0x3FFF. To be safe, you will want to LDIR your code into RAM every time your routine is invoked - because something else could have overwritten your return routine in the meantime.

In many cases you can avoid writing a return routine that needs to be copied elsewhere - just tweak values on the stack before calling pagetrapreturn instead - indeed, in the case immediately above (return and unpage the Spectranet), it would be the best course of action. You can do the same with pagetrapreturn as follows:

return
     ld hl, 10
     add hl, sp
     ld (hl), RETURN_LSB
     inc hl
     ld (hl), RETURN_MSB
     jp PAGETRAPRETURN

This way you don't need to copy anything into SRAM.

Some limitations of trapping and bank switching

While the programmable trap allows you to trap any address, there are some limitations. On an 8 bit machine with a 16 bit address space like the Spectrum, to access more than 64K of address space, bank switching must be used. On the Spectrum, this all happens in the lowest 16K. If you have a Spectrum +3, any one of four Spectrum ROMs may be paged in here, as well as the Spectranet's memory. So if you need to trap an address in the lower 16K you need to choose an address that hopefully will only be trapped in the ROM that you're interested in - or else you might find unusual things happening! If the address you need to trap can potentially be executed in more than one ROM, you'll have to examine the Spectrum's system variables to see what ROM is paged in, and if it's the wrong one, return control back to that ROM (in most cases, just using the PAGETRAPRETURN or TRAPRETURN routine is enough).

Further complications can arise if there's another peripheral which has paged ROMs, such as the DivIDE. Since the Spectranet will nail the A15 line high when the NMI is intercepted (preventing a downstream peripheral from paging in), your code will get invoked, however, the state of the downstream peripheral may be affected if it also has a code trap at the same address (a good example would be implementing tape traps when there's a DivIDE attached to the back of the Spectranet).

In summary, there are potential minefields when trapping addresses in the lower 16K. You have been warned.