Spectranet: Tutorial 7

From Spectrum
Revision as of 18:26, 25 October 2009 by Winston (talk | contribs) (→‎The "Hello World" of ROM modules)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

As well as the parts required to support the W5100 ethernet interface, the Spectranet contains extra memomory. Some of this is used to support the socket library and provide required functions to make a useful ethernet interface, but most of the memory is free for the programmer.

This includes most of the flash ROM chip. Only 16K is reserved for the Spectranet (pages 0x00 - 0x03). The remaining flash pages, 0x04 to 0x1F, or 112K of ROM, are available to the user. The great thing about this flash memory is that it is nonvolatile so the code or data within it remains available between power cycles.

You may have already seen that the memory on the Spectranet is organized into 4K pages. Therefore, it's probably no surprise that ROM modules are organized as 4K pages too.

When the Spectrum is reset or powered up, the last thing the Spectranet does before starting the Spectrum's main ROM is to examine each page of flash ROM to see if it contains executable code. This is done by examining the first byte of each page to see if it contains the byte 0xAA. If this is the case, it examines the second byte, which contains the ROM's identity and stores this in a table. It then checks to see whether the ROM has a valid RESET vector, and if it does, it calls the routine that this vector points to. This allows a module to perform some initialization. For example, tbe built-in utility ROM uses this to start the DHCP client. A ROM that extends BASIC can use the reset vector to add its extensions.

To allow access to ROM modules from assembly language or C programs, each ROM has a unique ID number (if you're writing a ROM module, ask me for an ID and I'll allocate one), and there is a call dispatch routine which allows this ID to be used to pass control to the ROM module. This system of using an ID means the ROM doesn't have to be in a certain fixed page for a routine to find it and call it.

Each ROM module, therefore, has a certain format to tell the Spectranet to run the code. The format begins with a table that looks like this:

  • Byte 0: Page signature. 0xAA for a module, 0xFF for empty, anything else for a data page.
  • Byte 1: ROM ID
  • Bytes 2,3: RESET vector (an address)
  • Bytes 4,5: MOUNT vector
  • Bytes 6-13: Reserved (will be used as vectors)
  • Bytes 14,15: Address of the ROM's identity string

Code is kept in byte 16 onwards. The code that starts at 16 should handle calls to this module using the MODULECALL entry point (more on that later). After that, what the module contains is up to the programmer.

ROM modules get paged into paging area B, therefore ORG should always be set to 0x2000.

Installing ROM modules into the Spectranet's flash

A standard Spectranet utility is the ROM programmer. It allows you to program the flash over the network. Access this by pressing the NMI button and selecting "Add and remove ROM modules". When you choose Add Module, the Spectrum will listen for data on tcp/2000. Use the ethup utility to send the code to the Spectrum. It will then be programmed into the flash chip.

The "Hello World" of ROM modules

For the first example, I will present the simplest case, a program that just prints "Hello world" on the screen at boot time, and when called via MODULECALL, the string "Hello World Module". This demonstrates clearly a simple case of a reset routine, and a simple case of a function called by MODULECALL.

As I have already said, the first part of a ROM module is a table. We will write this first:

    org 0x2000          ; ORG address for *all* ROM modules
    defb 0xAA           ; This is a code module.
    defb 0xFE           ; This module has the identity 0xFE.
    defw INITROUTINE    ; The RESET vector - call a routine labeled INITROUTINE.
    defw 0xFFFF         ; MOUNT vector - not used by this module
    defw 0xFFFF         ; Reserved
    defw 0xFFFF         ; Address of NMI routine
    defw 0xFFFF         ; Address of NMI menu string
    defw 0xFFFF         ; Reserved
    defw STR_identity   ; Address of the identity string.

This table has brought the address up to 0x2010 - the 16th byte of the module. As I said earlier, this is where the MODULECALL routine begins. Our trivial module call routine prints "Hello World Module". We'll use the Spectranet's 42 column print routine to display the string:

MODULECALL
    ld hl, STR_hellomod
    call PRINT42
    ret
STR_hellomod defb "Hello World Module\n",0

After this, we´ll put our reset vector routine:

INITROUTINE
    ld hl, STR_helloworld
    call PRINT42
    ret
STR_helloworld defb "Hello, world!\n",0

Then, at the end, we'll put the identity string. This is a C string (i.e. terminated with a NULL, 0x00) and is displayed in the ROM utility. Make sure you don't forget the NULL, or you might cause yourself lots of difficulty when you next go to the ROM programming utility!

STR_identity defb "Hwllo world ROM",0

Assemble this code, and upload it to the Spectrum with ethup. Then reset the Spectrum. You'll see the message "Hello, world!" displayed just after the DHCP client runs (and just before the BASIC ROM starts). It stays up just long enough to see!

Using MODULECALL

The MODULECALL entry point allows other programs to call your ROM. MODULECALL takes one 16-bit parameter in the HL register. H contains the ROM ID number, in this case 0xFE, and L contains an arbitrary byte. Most modules will use L to provide a function number. However, it's up to the ROM module to make sense of the contents of L. It can be ignored altogether if the ROM only has one function. Alternately, it could be a table index to a function table, allowing each module to have up to 256 different publically accessable calls. Use the L register however you see fit.

Now your ROM module is installed, we'll write a little program to demonstrate the use of MODULECALL. This can exist anywhere in memory, including another module. For this example, we will assemble a program to address 0x8000 (32768 decimal), and use the paging entry point, which is at 0x3FF8. This is defined in spectranet.asm, it is likely you'll want to include this file in your project. Here is the example program:

    include "spectranet.asm"
    org 0x8000
    ld hl, 0xFE00              ; H is 0xFE, the id of our ROM
    call MODULECALL
    ret

That's all there is to it. When you run this program, you should see the string "Hello World Module" printed on the screen.

An important point to note is that "call MODULECALL" is trapped by the Spectranet hardware, and causes the Spectranet memory to get paged in. On return it is automatically paged out. You must use the CALL instruction and only the CALL instruction; CALL cc, JP or JR won't do - the Spectranet decodes the CALL instruction to make this trap happen. (This applies to the IXCALL, PAGEIN and HLCALL entry points, too). The CALL instruction is trapped rather than the execution address because some Spectrum ROMs have executable code right up to the last byte - and execution address trapping here would cause incompatibility problems.

If you are writing a program that runs within Spectranet memory, or that runs when the Spectranet is purposefully paged in, then you need to make a call to MODULECALL without the paging mechanism. This is done with RST 0x28 (RST MODULECALL_NOPAGE if you have included spectranet.asm). For example:

    include "spectranet.asm"
    org 0x8000
    call PAGEIN                ; Page in Spectranet memory
    ld hl, STR_test
    call PRINT42               ; Don't use IXCALL, call the routine directly because the Spectranet is paged in
    ld hl, 0xFE00
    rst MODULECALL_NOPAGE      ; ...and do the same with modulecall, too.
    call PAGEOUT
    ret

Errors when using MODULECALL

There is only one error defined by the system itself, and it occurs when no module is found. If you make a MODULECALL to a ROM id that doesn't exist, when MODULECALL returns the carry flag will be set and the A register will be set to 0xFF. It's suggested that any code that's written to work with MODULECALL flags errors with the carry flag and the return code in the A register (something other than 0xFF to distinguish it from "module not found", but if a call requires different error handling, there's nothing stop the module author from doing something different.

BASIC extensions in a ROM module

For the principles of how BASIC extensions work, you'll want to look at Spectranet: Tutorial 6.

Recalling the last tutorial, you'll observe that a BASIC extension is registered with the Spectranet ROM, and that makes it usable. If you're writing a ROM module that extends BASIC, you put this registration code in a routine called by the reset vector. For this example, we´ll use the following as our vector and identity table in the first 16 bytes of the module:

    include "spectranet.asm"   ; Definitions.
    org 0x2000                 ; All ROM modules start at this addr.
; vector table
    defb 0xAA                  ; Code ROM
    defb 0xFE                  ; ROM ID
    defw F_installcmd          ; reset vector
    defw 0xFFFF                ; the next few vectors are reserved
    defw 0xFFFF
    defw 0xFFFF
    defw 0xFFFF
    defw 0xFFFF
    defw STR_ident             ; Pointer to a string that identifies this mod

The reset vector in this case points to our routine "F_installcmd", which as its name suggests, installs a command that extends BASIC. The code to do this from ROM is entirely standard, except you never use the HLCALL or IXCALL mechanism (since the Spectranet memory is paged in during initialization). Compare this with the example in the last tutorial:

F_installcmd
    ld hl, PARSETABLE	; Address of the command's information
    call ADDBASICEXT	; register the new command.
    ret nc		; and if no error, we're finished.
.error
    ld hl, STR_error
    call PRINT42
    ret

And here's the data structure that gets passed in the call to ADDBASICEXT:

    ; 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 0xFF	       ; 0xFF tells ADDBASICEXT to use the currently paged page
    defw RUNCMD        ; Address of routine to call
CMDSTRING
    defb "*poke",0

Note the defb for the page (bolded in the listing above). This value can be 0x00 for don't page (in this case, both the command string and the routine to run to execute the BASIC extension are not in paged memory), or it can be the page number where the string and code lives, or in this case, it's set to 0xFF. This value tells the ADDBASICEXT routine to see what page is currently in paging area B, and when it installs the structure, to use this page number. That way you don't have to figure out which page your code is in, it's done for you.

The routine that is called when the BASIC extension runs (in this case, when the *poke command is used), is exactly the same as it is for the examples in Tutorial 6. If you want to see it all in context, you can look at an example ROM module that implements the 2 byte *poke command is available from SVN here: [1]