Spectranet: Tutorial 6

From Spectrum
Revision as of 13:00, 9 October 2010 by Winston (talk | contribs)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

The Spectranet ROM provides some facilities for extending the ZX BASIC interpreter.

At present, there is an interpreter extension mechanism, and there are plans to add support for channels and streams at a later date. This tutorial will introduce BASIC extension methods.

How BASIC extensions work

When the ZX BASIC interpreter encounters something it considers an error, it executes the RST 8 instruction with the error code as the byte following the RST 8 instruction. The CPU calls the address at 0x0008, where the error routine examines the stack, and picks off the byte at the return address (the error code). It then processes it in the appropriate way.

The Spectranet traps execution at this address (unless this has been disabled by a hardware jumper or in software). This means the Spectranet ROM code runs at this point, and this gives an opportunity to look at some alternate syntax for new commands. If there's no command to run, control gets passed back to the ZX ROM to handle the error as it would have, had the trap not been present. The Spectranet ROM examines a table of commands, and if the command that the ZX ROM threw out matches one in the Spectranet's table, it calls a routine for the new command. For simple commands, this routine will simply execute the action intended. For more complex commands, there may be further parsing to be done.

When the new command is being executed, the Spectranet ROM is paged in. However, there's a simple mechanism to make calls to the BASIC ROM which will automatically page the Spectranet ROM back in on return. This has been modeled on the way the ZX Interface 1 dispatches calls to the ZX BASIC ROM, to allow commands written to work with the Interface 1 to run on the Spectranet. It also means that the examples in the Complete Shadow ROM disassembly will work with very few modifications.

New commands can be written either in assembly language or C (or a mixture of C and assembly language). The C functions are found in the libspectranet library (which is distinct from the socket library), and is a wrapper around the ROM entry points. Commands can run from within the lower 16K of address space (for example, as part of a Spectranet ROM module or from static RAM), or in the Spectrum's main RAM. Code that's to run from Spectranet memory should be assembled into the area between 0x2000 and 0x2FFF (paging area B).

Adding a simple extension

In this context, a simple BASIC extension is just a very simple command with no arguments. It's the sort of thing you might write if you wanted to simply launch a routine that you had written that takes no arguments and returns nothing. You may use it, for example, to start an IRC client in ROM, or perhaps do something simple like display the IP, netmask, gateway and DNS servers on screen.

The code to make it happen is very simple for both C and assembly language programmers.

Firstly, a structure must be filled which contains the command, the error code that triggers this command (which in the case of a simple command, is always C - Nonsense in BASIC - error code 0x0B), the address of the routine that must be called and what page (if any) should be paged into paging area B for the duration of the command. This is passed to a Spectranet ROM routine which adds the command to the extensions table. In this example, the code for the new command will be in the Spectrum's main RAM.

Below is the example code for adding the extension in C.

#include <stdio.h>
#include <spectranet.h>

// The string that is to be interpreted must live somewhere. We declare it here.
// Typing *simple in BASIC will result in our new command being run.
char *token="*simple";

// Prototype for the call to the command. It'll always be void something()
void simpleCmd();

main()
{
       // This structure is used to install the new command into the Spectranet ROM
       // extension interpreter.
       struct basic_cmd bc;
 
       bc.errorcode=TRAP_NONSENSE;	// Trap nonsense in basic
       bc.command=token;               // A pointer to the string above
       bc.rompage=0;                   // don't do paging - running from main RAM
       bc.function=simpleCmd;          // Pointer to the function that gets run when *simple is entered
 
       if(addbasicext(&bc) < 0)        // The addbasicext() call in libspectranet adds the new command.
       {
               printk("Failed to add extension\n");
               return;
       }
       printk("Added basic extension.\n");
}

Once this code has successfully executed, then typing *simple will execute whatever's in the function void simpleCmd(). The information we suppled to the addbasicext routine all lives in the basic_cmd structure. This consists of the error code when the ZX BASIC ROM executes RST 8 (since it knows nothing of the *simple command, it will trigger RST 8 with a C Nonsense in BASIC error), a pointer to the string for this command, the page in ROM (if any) that the routine lives in - here, it's set to zero as we're running from main RAM, and a pointer to the function to call.

There is some special structure to the simpleCmd function. Unfortunately, it´s not quite as easy as just depositing your code there, you also have to interact a little with the ZX ROM. In the case of a complex command (with parameters) you may need to make several calls to the ZX ROM to verify and fetch these parameters. For a simple command like this, which has no parameters, you don't need to do very much. The other thing to note is when your function finally exits, it must exit via a jump to a ROM routine that arranges everything correctly for the return to BASIC, or the computer will crash.

Here is the simpleCmd function. It will just print "Statement executed" on the screen, and that's it:

void simpleCmd()
{
       statement_end();         // Check for statement end - an error will be returned
                                // to BASIC if any parameters follow, because this command
                                // has none.
       printk("Statement executed.\n");

       // This exits after the command was successfully executed.
#asm
       jp 0x3E99
#endasm
}

There's not much to it, but there are some interesting things to note which will affect how you write the functions that handle your new command.

None of the code that actually does the work of the command should appear before the call to statement_end(). If you put some code before statement_end() it'll get executed twice because the simpleCmd() routine above gets called twice! This is because for each C Nonsense in BASIC error which we handle, the ZX ROM will execute RST 8 twice. The code after statement_end(), however, only gets run once - after all of the command's arguments have been successfully processed into something usable. On the first call to simpleCmd(), the statement_end() function rearranges the stack and returns execution control back to the ZX ROM.

The function finishes with a single instruction enclosed in #asm directives. The indirect call via 0x3E99 handles the proper return of control back to ZX BASIC after your function has completed.

C and basic extensions: Caution!

If you are going to use the socket library in a BASIC extension (or any library that calls the Spectranet ROM) you must use a nonpaging version of the library. The default one pages the Spectranet ROM in and out with each call, and when it has paged the ROM out, you'll have the Spectrum ROM paged in instead. So when you jump to 0x3E99 at the end of the C routine, you'll end up crashing, because the Spectrum ROM will be paged (and 0x3E99 is part of the Spectranet ROM). Use the link option "-llibsocket_np" instead of "-llibsocket".

More complex commands: A command with parameters

In this example, I shall cover the assembly language version first since this has some departures from the normal run-of-the-mill assembly programming - namely how to make calls to the ZX BASIC ROM. Earlier, I mentioned that when our routine is called, it's the Spectranet's memory that is paged in. This means you can't just call a routine in the ZX ROM with a CALL instruction. However, the Spectranet uses the same mechanism as many traditional peripherals that extended ZX BASIC.

The BASIC ROM contains a number of useful routines for checking, parsing, and getting values out of the BASIC program and into registers. In this more complex example, it's easier to see the division in your BASIC extension between syntax and run time (as I wrote earlier, your command routine gets called twice - because a call to 0x0008 happens twice, once at syntax time and once at run time).

This example will be the classic double poke extension - so you can POKE a 16 bit value from BASIC. The new command's BASIC syntax is:

*poke x,y

where X is the address to start poking at, and Y is a 16 bit value.

Here is the code that tells the Spectranet ROM about the new command. It's the same as the previous simple example. It is assembled to 32768:


       include "spectranet.asm"	; Spectranet ROM symbols

       org 0x8000
       ld hl, PARSETABLE		; Pointer to the table entry to add
       ld ix, ADDBASICEXT		; the ADDBASICEXT system call
       call IXCALL			; call it
       jr c, .installerror		; failure to install - tell user

       ld hl, STR_ok			; New command added OK
.exit
       ld ix, PRINT42			; and print the message that it's OK
       call IXCALL
       ret	

.installerror
       ld hl, STR_error
       jr .exit

       ; The following is the data structure that is used by the Spectranet
       ; additional command parser. It's important to note that the
       ; structure itself is copied into the Spectranet's system variables,
       ; but the string is not! So don't overwrite the memory used by the
       ; string.
PARSETABLE
       defb 0x0B			; C Nonsense in BASIC
       defw CMDSTRING			; Pointer to string (null terminated)
       defb 0				; Don't do any memory paging
       defw RUNCMD			; Address of routine to call

CMDSTRING
       defb "*poke",0

Once you've run this with RANDOMIZE USR 32768, the new command is installed. The new command when invoked must check the two 16 bit integers. Either or both may be a ZX BASIC variable, or just a simple integer value. (Or even floating point, so long as it's in the range 0-65535 - it will get rounded to an integer value). Fortunately, you don't need to do an awful lot to get these values - the ZX ROM has to do this kind of thing all the time, so the ZX ROM has routines to do it for you.

However, you need to call the ZX ROM routines - but the Spectranet ROM is paged in. There is a special mechanism to do this. While the Spectranet ROM is paged in, executing RST 0x10 does not print a character to the screen, rather, it makes an entry into the ZX ROM (and returns with the Spectranet ROM paged back in). If you've done much with the Interface 1 this will be very familiar, since the Spectranet's method was deliberately made similar in this regard.

If you're not familiar with the Interface 1, here's an explanation of how to call the ZX ROM while the Spectranet is paged in. The basic model is this:

       rst 0x10
       defw address_to_call

What happens when RST 0x10 is executed, a call to address 0x0010 is performed as you would expect - and since the Spectranet ROM is currently paged in, this will be in the Spectranet ROM. There is a routine here which gets the next two bytes pointed to by the address on the stack, which would be the return address. However, in this case the return address actually points to the two bytes defined from the DEFW. The return address is therefore updated to be two bytes higher, but the value in the DEFW directive is used as the address to call in the ZX ROM. All CPU registers are preserved, so when you enter the ZX ROM, the registers are all still set to what they were when you executed RST 0x10. The upshot of this, is that RST 0x10 followed by DEFW xxxx has the same effect as CALL xxxx, but also pages the ZX ROM in for the call, and pages the Spectranet ROM back in on return. If you want to know all the gory details, you can examine the source code for the do_callbas routine (invoked by RST 0x10), and do_rst8 routine in trapdispatch.asm

Here is the routine that is invoked when *poke is invoked in BASIC. It is rather simpler than you may imagine, given that it has to syntax check the numbers, and parse them, whether it's in integer value, BASIC variable or floating point value. As has already been said, this is because the BASIC ROM actually does all of the heavy lifting:

RUNCMD
       ; This code is run twice - once at syntax time to check syntax,
       ; and once at runtime. The statement end call will handle returning
       ; to the ZX ROM at syntax time, but will return as normal during
       ; runtime.
       rst CALLBAS			; CALLBAS exit point
       defw NEXT_2NUM			; Call the ZX ROM 'NEXT_2NUM' routine
       call STATEMENT_END		; Check for statement end.

       ; This code does not get called at syntax time, because the
       ; 'call STATEMENT_END' doesn't return conventionally at syntax time
       ; as noted above.
       rst CALLBAS			; Call ZX ROM's
       defw FIND_INT2			; routine that fetches a 16 bit int
       push bc				; which is returned in BC
       rst CALLBAS			; Then do it again to get the 16
       defw FIND_INT2			; bit value for the address
       push bc				; and transfer it to HL
       pop hl
       pop bc				; retrieve the value to be poked
       ld (hl), c			; poke the LSB
       inc hl
       ld (hl), b			; poke the MSB
       jp EXIT_SUCCESS			; and pass control back to ZX ROM

The division of syntax time and run time is quite clear here - as was said for the simple example, everything before call STATEMENT_END gets run twice, once at syntax time and once at runtime. Therefore, the actual meat of your code must appear after the call to STATEMENT_END. The code here calls the ZX BASIC NEXT_2NUM which syntax checks that the next two things in the BASIC command are two numbers (and since the next thing to happen immediately after this is a call to STATEMENT_END, the statement must end after the numbers).

At run time, these two numbers, whether they are in variables or are written out directly, must be moved into registers to do something useful with them. The FIND_INT2 routine in the ZX ROM will do this for us, returning the number in the BC register pair. So, for this command, it gets called twice to get the address and value to be poked. Note that the LAST parameter of the command is pulled out first!

In C

The structure of this program is very similar in C. The full source for the C example may be found here in WebSVN. I'll skip the initialization code - it is identical as for the simple example except for the name of the function to call and the token. The meat of the C version of the 16-bit POKE example follows:

void doublepoke()
{
       unsigned int addr, value;

       // Syntax time
       expect2Num();		// Two numbers separated by commas
       statement_end();         // followed by statement end.

       // Run time
       value=find_int2();	// get the value to be poked
       addr=find_int2();	// get the address to be poked

       // Now do some casting tricks to write to memory.
       // This turns addr from being an ordinary unsigned int to an
       // address in memory to write (with the value of the int, of course).
       // So if addr=16384, the RAM address 16384 will be poked.
       *((unsigned int *)addr)=value;

       // Jump to EXIT_SUCCESS
#asm
       jp 0x3e99
#endasm
}

As you can see it has the same structure as the asm version, except the syntax checking and run time functions are called as C functions. Putting the call to expect2Num() and statement_end() will mean the syntax check will look for two numbers (separated by a comma) followed by the statement's end. The expect2Num() function will handle the return of control to ZX BASIC if there's an error in the parameters. And, as has been mentioned already, statement_end() at syntax time will return control back to ZX BASIC.

The runtime functions are designed to work how a C programmer would expect - they return their values as the appropriate type (in this case, as unsigned int). Then there's a little syntactic trickery to persuade the compiler to make code that pokes the address held in the 'addr' variable with the value. (An alternate way of doing this is to declare addr as an unsigned int * and casting the return value of find_int2() to that type).

Further reading

(todo)