Z88 Developers' Notes
Previous Contents Next

24. BBC BASIC and the in-line assembler

BASIC Summary

The Z80 in-line assembler is not covered explicitly in the Z88 User Guide, so a short explanation is provided here. The definitive source of information about the assembler, and BBC BASIC in general, is the BBC BASIC (Z80) manual by Richard Russell, which is available from M-Tec. Before moving on to the assembler properly, we cover a few unusual features of BBC BASIC. Some useful BASIC commands are:
 

LOAD "filename"         loads a BBC BASIC program
SAVE "filename"         saves a BASIC program
NEW                     clears out BASIC workspace for a new program
OLD                     attempts to recover a program lost through NEW
RUN                     execute a BASIC program
LIST                    list out a BASIC program to the screen
RENUMBER [a,[b]]        renumbers the lines of a BASIC program
DELETE a,b              delete the lines between a and b
CALL x                  execute a machine code routine at address x


BASIC's Workspace

In its Z88 incarnation, BASIC occupies a memory map with the following form:
 

$0000 - $1FFF           Operating system use (and occasional application stack)
$2000 - $3FFF           BASIC program/workspace
$4000 - $BFFF           (additional 32K of program/workspace, expanded Z88)
$C000 - $FFFF           BBC BASIC interpreter application

BASIC's program/workspace is arranged in the following manner:
 
            +-------------------+   $FFFF
            | BASIC interpreter |
            .                   .
            .                   .
HIMEM       +-------------------+   $BFFF or $3FFF
            | Stack             |
            +-------------------+   Current limit of the stack
            .                   .   (Stack expands downwards)
            . Unused memory     .
            .                   .
            +-------------------+   Current limit of the heap
            | Heap              |   (Heap expands upwards)
LOMEM       +-------------------+
            .                   .
            .                   .
TOP         +-------------------+
            | Program           |
PAGE        +-------------------+   $2300
            | Workspace for     |
            | interpreter       |
            +-------------------+   $2000
            | Operating system  |
            | system usage      |
            +-------------------+   $0000
HIMEM, LOMEM and PAGE are pseudo variables whose values can be read and also set. TOP is a read-only pseudo-variable. If you intend to change the values of these pseudo-variables, then you must bear in mind that the "Unused Memory" area in BASIC's memory map will not necessarily be constant. If task switching occurs, this memory may be used for other purposes and when you return to BASIC its contents may have changed. Therefore any memory which you want to stay constant through task switching must either be in or below this heap. This memory is readily allocated within the heap using the DIM statement, explained below. LOMEM, the start of the heap, defaults to the value of TOP, the end of the program, although it is possible to increase LOMEM, thus providing some safe memory between the end of the program and the start of the variables. The value of PAGE is always set on a page (256 byte) boundary, ie. the less significant byte is ignored.

By changing the value of PAGE, it is possible to have more than one program resident at once, within the same instantiation of BASIC. The difficulty with this technique on the Z88 is the inconstancy of the unused memory. It is possible to set LOMEM to some high value, but extreme care is required, because whenever a program is RUN, LOMEM is reset to TOP. TOP, however, is not reset when PAGE is changed unless part of the program is altered or OLD is used. If you attempt this technique, keep in mind that whenever task switching could occur, all the memory used for programs should be allocated to BASIC and therefore safe.
 

Numbers and Indirection Operators

BBC BASIC provides the '&' symbol to prefix a hexadecimal number, as distinct from the more usual '$' used in these notes, which is used in BBC BASIC for string indirection. For printing an expression in hex form, prefix it with '~', eg.

            PRINT &AF               gives 175
while
            PRINT ~27               gives 1B

The indirection operators provided by BBC BASIC are used to find the contents of a cell or cells at a given address. The ? (query) operator represents byte indirection, so:
            ?pointer
represents the byte addressed by 'pointer'. This can be used either on the right or left of an expression; the statement:
            ?address = contents
sets the contents of the byte-sized cell addressed by 'address' to the value 'contents'. In most BASIC's this would have been written as:
            POKE address,contents
whereas the statement
            contents = ?address
is equivalent to
            contents = PEEK(address)
The ! ('pling') operator represents word (32bit and not 16bit) indirection, so the expression:
            !pointer
represents the value of the 32bit word, the address of whose first byte is 'pointer'. Thus, !a (on the righthand side of an expression) is equivalent to:
            a?0 + 256*a?1 + 256*256*a?2 + 256*256*256+a?3
Notice that the least significant byte comes first; this is generally true for the Z88 system, an extension to four bytes of the standard two byte Z80 order.

The query and pling operators may also be used in a dyadic context, which is often more natural (cf. array indexing, which actually works similarly)

            base?offset             equivalent of ?(base+offset)
            base!offset             equivalent of !(base+offset)
In this case 'base' must be a simple variable, but 'offset' may be any expression. Thus "!x" will not work. Remember also that the operators in a dyadic context are symmetric, so a!1 addresses a word starting at a+1 and NOT a+4, as some people might expect. Finally, note that ! and ? have the highest level of priority in an expression, equal to unary plus, minus and the logical NOT, above binary arithmetical, relational and logical operators, so for example a?1^2 will be interpreted as (a?1)^2.

The operator $ ('dollar') implements string indirection: $a refers to a string which begins at address 'a' and is terminated by carriage return (CR). Thus:

            $a = "hello"
sets up successive bytes, starting at address 'a', with these five letters and a 13.

Note that the maximum length of a string in BBC BASIC is 255 characters. This is not because the string storage uses a length byte - it does not - but simply for convenience of internal manipulation on an 8bit machine.

Space for a machine code routine, strings and memory to be used with indirection operators may conveniently be reserved using a special form of the DIM statement, without any brackets. The BASIC statement:

            DIM code 255
reserves a block of 256 contiguous bytes of memory and sets the variable 'code' to point to the start of this block. The elements of the block therefore start at address 'code' and finish at 'code+255'. This is quite distinct from the statement:
            DIM code(255)
which reserves space for 256 floating point variables - which will be considerably more than 256 bytes, actually 256*5 bytes!
 

The Assembler

We can now move to the assembler itself. Some oddities worth mentioning are that the brackets around the port number for the IN and OUT Z80 instructions mnemonics are optional (ie. OUT 5,A and OUT (5),a are equivalent). The instruction IN F,(C) is not accepted, but the source code IN (HL),(C) produces the equivalent object code. It is conventional (but not necessary) to use lower case for labels and manifests, as this avoids lexical pitfalls and improves readability. It is also important to put spaces between the instruction mnemonic and its operands.

Assembler source may simply be placed within the BASIC program, surrounded by square brackets. The assembler uses the BASIC variable P% as a program counter, which advances as the assembler moves through the source code (note that P%, as with any BASIC variable with a % suffix, is a 4-byte signed (2. complement) integer rather than a floating point variable). The user must, therefore, set P% to the desired start point for the machine code output before invoking the assembler. The program might look like this:

            10 REM Trivial example of how to use Z80 assembler
            20 DIM code 50
            30 P%=code
            40 [
            50 ld bc, 50
            60 ret
            70 ]
When this BASIC program is RUN, it assembles the two-line assembler program into the first four bytes of the reserved memory, but does not execute the code itself. As the BASIC is RUN, an assembly listing is provided. This may be surpressed by using option flags, set by using the assembler directive OPT <n> at the start of the code; 0 will supress a listing, ie. insert line 45 with:
            45 OPT 0
A number given by any combination of the following bit settings may follow OPT:
            BIT 0 = 1   (1) give a listing
            BIT 1 = 1   (2) report errors
            BIT 2 = 1   (4) place assembled code starting at O% rather than P%
The above options may be combined. The last option means that the code is actually placed starting at O%, but labels have values as if the code started at P% (see below for details of labels declarations). This allows one to assemble code into on space which is designed to fit somewhere else. For instance, in the following code fragment:
            100 DIM code 50
            110 P%=&C000: O%=code
            130 [
            140 OPT 6
            150 .codestart
            160 dec a
            170 cp (hl)
            180 jp codestart
            ..
            200 ]
then although the code will actually go into the 'code' array, the label 'codestart' has the value $C000, and so the address in the JP statement will appear as such. This facility could be used to assemble code that will ultimately appear in an application card.

Comments may be inserted in the assembler source by preceeding them with either semicolon or backslash, viz:

            42 ; This is a comment
            55 \ and so is this
Note, however, that a comment ends at the end of a BASIC statement. This will normally be the end of the line, but a colon will have the same effect. Hence any characters after a colon will be regarded as a new assembler statement:
            54; The following will be regarded as an assembler statement: RST 0
This practice is, of cource, very confusing and is not recommended.

Labels may be used in the assembler code; simply precede each label with a full stop (the full stop is NOT part of the label). A label may or may not be followed by an assembler statement on the same line, but if it is, then at least one space must be left between them, eg.:

            10 ld c,15
            20 .loop1 ld b,30
            30 .loop2
            40 call misc
            50 djnz loop2
            60 call wrn1
            70 dec c
            80 jr nz, loop1
When the assembler encounters a label, it sets a BASIC variable of that name to the current value of P%. Assembler labels and BASIC variables are thus interchangeable; so the assembler code could use:
            JP code
to jump back to the very start of the program (beginning of the allocated area by the DIM statement). Also, this allows BASIC variables to be used to define manifest constants for use in the assembler listing:
            5 maxsize = 62
            .
            .
            40 [
            .
            .
            56 cp maxsize
            .
            .
            80 ]
The assembler simply passes once through the source from start to finish, and so will not know the values of labels before they are defined. It would be inconvenient to have to define every label before it is used, so the way around this problem is to make two passes through the code. The first will, in general, encounter errors, so set OPT 0 to suppress their reporting. This pass will set up all the labels correctly, so that a second pass (with OPT 2, or OPT 3 if a listing is desired, to make sure there are no 'genuine' errors) will complete the assembly process. For example:
            100 DIM code 100
            110 FOR pass%=0 TO 2 STEP 2
            120 P%=code
            130 [
            140 OPT pass%
            150 ld bc,13
            160 jr label
            170 ld bc,26
            180 .label
            190 ret
            200 ]
            210 NEXT pass%
Two rough edges in the assembler regarding labels are:
  1. If a label is re-declared, no warning is given.
  2. If a label which is undeclared is the operand of a JR (jump relative) instruction, then the error issued is 'out of range' rather than 'label not found'.
In-line data definition is possible in the assembler source using the directives DEFB (define byte), DEFW (define word) and DEFM (define message).
DEFB &12    ; sets up a byte of storage and initialises it to 12H (18 decimal)

DEFW 16385  ; sets up a 16bit word of storage and initialises it to 123, less
            ; significant byte first, eg.: 1,64

DEFM "hey"  ; sets up space initialised with this string, one character per
            ; byte
The DEFM directive does not introduce any magic characters, so if you want a string to be null or carriage return terminated, you must explicitly append the terminator byte(s), eg.:
            .pointer
            DEFM "This string is null-terminated"
            DEFB 0
Contrast the BASIC string indirection which when used thus:
            $pointer = "This string is CR-terminated"
will automatically carriage-return (13) terminate the string.

Unfortunately, there is no define-storage directive. A second DIM statement may be used, or small spaces, one could use DEFM with a dummy string. This may conveniently be done as follows:

            DEFM STRING$(100,"A")
This demonstrates a useful consequence of the close intertwining of the assembler with BASIC: the arguments to assembler operands and pseudo-operands may include many forms of BASIC expressions (though brackets may lead to ambiguity as they often indicate an extra level of indirection in Z80 assembler). Two other handy incidences of this are the use of ASC"x" as a character constant, viz:
            ld  a, ASC"Q"
and the use of user-defined functions to provide macro and conditional assembly facilities. Suppose we are using the (non-local) variable 'pass' to represent the current assembler option. Then:
            OPT pass
will have no effect. Taking this a stage further, if the user-defined function 'macro' always evaluates to 'pass', then:
            OPT FNmacro(arguments...)
will have no effect, except that it will execute the body of the function, if any. For instance, suppose we define:
            DEF FNsave_regs(savearea)
            [
            OPT pass
            ld (savearea),hl
            ld (savearea+2),de
            ld (savearea+4),bc
            ]
            =pass
then including:
            OPT FNsave_regs(space)
in the main code would reproduce the above three lines of code with 'savearea' set to 'space'. Another useful example is a macro to automatically generate a DEFB or a DEFW depending on the size of an operating system call code. This would look like this:
            DEF FNcall_oz(arg)
            IF arg>255 THEN [OPT pass: RST &20: DEFW arg] :=pass
            [OPT pass: RST &20: DEFB arg] :=pass
Note that an OPT must reappear in the function and that closing square bracket in the function body does not exit assembler mode in the main program.

Finally, to call the machine code program once it has been assembled, do:

            CALL code
or
            a = USR(code)
which returns the contents of the HL main and alternate register pairs set at termination (H most significant; L least).

There is a mechanism for initialising the contents of registers from the CALL or USR statements: the registers A, F, B, C, D, E, H, L on entry are set to the values of the BASIC variables A%, F%, B%, C%, D%, E%, H% and L% respectively.

The CALL statement also allows the user to set up a parameter block on entry by appending the required parameters to the CALL statement, ie.:

            CALL code, par1, par2, par3, ... parx
On entry IY is identical to the calling address, and IX will point to a parameter block with the following format:
1 byte      number of parameters
1 byte      parameter 1 type
2 bytes     parameter 1 address
1 byte      parameter 2 type
2 bytes     parameter 2 address
... ...
1 byte      parameter x type
2 bytes     parameter x address
The parameter type byte may be any of:
0           byte, eg. ?x
4           32bit word, eg. !x or x%
5           Floating point (40bit), eg. x
128         String eg. "Hello"
129         Four byte string descriptor containing the current length of the
            string, the number of bytes allocated to the string and its address


Using system calls from BBC BASIC

When using system calls from BBC BASIC there are various important things to bear in mind. The main restart code itself needs to page memory banks in and out of the address space; for instance one of the first things it does is bind bank 0 (which contains the vector table for the OZ calls) to segment 3; but it expects the stack to perform properly when it has done so. So in general, any program, the BASIC interpreter included, should not place its stack where it is liable to be paged out. To be safe, it should be in the bottom 8K of the logical address space, which is never paged out. The BBC BASIC application stack has to be fairly large as it is used for parameter-passing during BASIC execution, and so cannot be placed in the bottom 8K by default. It is in a very vulnerable position, typically at the top of segment 2, and so it is advisable to select a safer stack if any system calls are to be used. This may reliably be done by loading the stack pointer from the location $1FFE. For example, if the main user code starts at 'main' then the program as a whole might look like:

            exx                     ; use alternate registers
            ld   hl,0               ; to preserve hl
            add  hl,sp              ; get current stack pointer in HL
            ld   sp,(&1FFE)         ; load new (safe) stack pointer
            push hl                 ; preserve old stack pointer on new stack
            exx                     ; back to main registers
            call main               ; execute main machine code
            exx                     ; back to alternate registers
            pop  hl                 ; get old BASIC stack pointer
            ld   sp,hl              ; and restore it
            exx                     ; back to main registers
            ret                     ; return to BBC BASIC interpreter

.main       ...                     ; main machine code...
            ...
            ret
The use of the alternate register set avoids corrupting the main set, thus allowing parameter passing with HL (by using the BASIC variables H% and L%), but this may be dispensed with, if the contents of HL are not important. The old stack pointer is pushed onto the new stack so it can be re-called at the end. The old stack pointer could also be saved in a static memory location and this technique is used in some of the other examples.
 

Saving and loading of machine code in BBC BASIC

The simplest method of loading a machine code is having it stored in DATA statements as byte opcode sequenses. However, this emplies a lot of work, converting an assembled code into DATA statements (you could make a BASIC program that generates a simple text file that later could could be typed in by CLI). Another way is simply to have the in-line assembler source included together with the rest of the BBC BASIC program. However, large assembler source tends to use much memory, especially if it is well-commented. It is usually sufficient with small assembler routines.

When you use pre-compiled code ready to execute, you face another obstacle; you must always place the code at the same location. Using DIM will probably not return the same address of allocated memory, so it is needed to move down HIMEM to ensure a static position for your code. If you have the source in-line then it is no problem, since you always can re-compile for a specifically allocated memory area.

The two BASIC procedures below implements a simple interface to both load and save machine code in binary files. Both procedures use a small machine code routine to save/load the code in BASIC's memory. To ensure compact BASIC procedures, the machine code have been put into DATA statements, placed inside the procedures. The mnemonic assembler source are found below the procedures. Both procedures locate the machine code routine at the bottom of the stack, beginning at $1800. This area is not touched by BBC BASIC and is therefore safe. However, when BBC BASIC has been pre-empted the bottom of the stack might have changed. Please note that both routines null-terminate the filename strings.

65515 DEF PROC_lbytes(f$,addr%)
65516 LOCAL b%,i% 65517 f$=f$+CHR$0
65518 RESTORE 65520: FOR i%=0 TO 66: READ b%: ?(&1800+i%)=b%: NEXT i%
65519 CALL &1800,f$,addr%
65520 DATA &21,0,0,&39,&ED,&7B,&FE,&1F,&E5,&CD,&0F,&18,&E1,&F9,&C9
65521 DATA &DD,&E5,&FD,&E1,&FD,&6E,2,&FD,&66,3,&23,&23,&5E,&23,&56,&62
65522 DATA &6B,1,2,0,&3E,1,&E7,9,&60,&38,&15,&FD,&6E,5,&FD,&66,6
65523 DATA &5E,&23,&56,&21,0,0,1,&FF,&FF,&E7,&45,&E7,9,&62,&C9,&E7,9,&4A,&C9 65524 ENDPROC

65525 DEF PROC_sbytes(f$,addr%,length%)
65526 LOCAL b%,i% 65527 f$=f$+CHR$0
65528 RESTORE 65530: FOR i%=0 TO 73: READ b%: ?(&1800+i%)=b%: NEXT i%
65529 CALL &1800,f$,addr%,length%
65530 DATA &21,0,0,&39,&ED,&7B,&FE,&1F,&E5,&CD,&0F,&18,&E1,&F9,&C9
65531 DATA &DD,&E5,&FD,&E1,&FD,&6E,2,&FD,&66,3,&23,&23,&5E,&23,&56,&62
65532 DATA &6B,1,2,0,&3E,2,&E7,9,&60,&38,&1C,&FD,&6E,5,&FD,&66,6
65533 DATA &5E,&23,&56,&FD,&6E,8,&FD,&66,9,&4E,&23,&46,&EB,&11,0,0
65534 DATA &E7,&45,&E7,9,&62,&C9,&E7,9,&4A,&C9
65535 ENDPROC


MODULE Lbytes
 

include ":*//fileio.def"            ; standard file I/O definitions
include ":*//error.def"             ; error code definitions

ORG $1800                           ; subroutine resided at $1800

;****************************************************************************
; BBC BASIC 'load bytes' machine code routine. The routine always loads the
; complete file (length),
; which is not specified in the CALL parameter list.
;
;           1st parameter is the string pointer
;           2nd parameter is the start address integer

;
; IN: IX = pointer to BASIC parameter block:
;           (IX+0)      number of parameters:               2
;           (IX+1)      type of 1st parameter:              129 (mov. string)
;           (IX+2,3)    address of movable string:          xx
;           (IX+4)      type of 2nd parameter:              4 (32bit integer)
;           (IX+5,6)    address of 2nd parameter:           xx
;           The movable string parameter block:
;           (xx+0)      current length
;           (xx+1)      max. length
;           (xx+2,3)    start address of string
.InitLbytes ld   hl,0
            add  hl,sp              ; get current BASIC stack pointer
            ld   sp,($1FFE)         ; install safe stack pointer
            push hl                 ; preserve BASIC stack pointer
            call Lbytes
            pop  hl
            ld   sp,hl              ; restore BASIC stack
            ret                     ; return to BASIC interpreter

.Lbytes     push ix
            pop  iy                 ; iy points at CALL parameter block
            ld   l,(iy+2)
            ld   h,(iy+3)           ; get pointer to moveable string par. block
            inc  hl
            inc  hl                 ; point at string start address pointer
            ld   e,(hl)
            inc  hl
            ld   d,(hl)

            ld   h,d                ; HL & DE = pointer to start of filename
            ld   l,e
            ld   bc,2               ; local pointer, C = 2 byte scratch buffer
            ld   a, OP_IN
            call_oz(GN_Opf)
            jr   c, file_err        ; Ups, file couldn't be opened

            ld   l,(iy+5)           ; IX = file handle
            ld   h,(iy+6)
            ld   e,(hl)
            inc  hl
            ld   d,(hl)             ; start address to load machine code
            ld   hl,0
            ld   bc, $FFFF          ; max. file length (probably much snaller!)
            call_oz(OS_Mv)          ; load file image at (DE) onwards...
            call_oz(GN_Cl)          ; close file
            ret

.file_err   call_oz(GN_Err)         ; display error box...
            ret

MODULE Sbytes
 

include ":*//fileio.def"            ; standard file I/O definitions
include ":*//error.def"             ; error code definitions

ORG $1800                           ; subroutine resided at $1800

;****************************************************************************
; BBC BASIC 'save bytes' machine code routine.
;           1st parameter is the string pointer
;           2nd parameter is the start address integer
;           3rd parameter is length of memory block
;
; IN: IX = pointer to BASIC parameter block:
;           (IX+0)      number of parameters:               2
;           (IX+1)      type of 1st parameter:              129 (mov. string)
;           (IX+2,3)    address of movable string:          xx
;           (IX+4)      type of 2nd parameter:              4 (32bit integer)
;           (IX+5,6)    address of 2nd parameter:           xx
;           (IX+7)      type of 3rd parameter:              4 (32bit integer)
;           (IX+8,9)    address of 3rd parameter:           xx
;
;           The movable string parameter block:
;           (xx+0)      current length
;           (xx+1)      max. length
;           (xx+2,3)    start address of string
;
.InitSbytes ld   hl,0
            add  hl,sp              ; get current BASIC stack pointer
            ld   sp,($1FFE)         ; install safe stack pointer
            push hl                 ; preserve BASIC stack pointer
            call Sbytes
            pop  hl
            ld   sp,hl              ; restore BASIC stack
            ret                     ; return to BASIC interpreter

.Sbytes     push ix
            pop  iy                 ; iy points at CALL parameter block
            ld   l,(iy+2)
            ld   h,(iy+3)           ; get pointer to moveable string par. block
            inc  hl
            inc  hl                 ; point at string start address pointer
            ld   e,(hl)
            inc  hl
            ld   d,(hl)

            ld   h,d                ; HL & DE = pointer to start of filename
            ld   l,e
            ld   bc,2               ; local pointer, C = 2 byte scratch buffer
            ld   a, OP_OUT
            call_oz(GN_Opf)
            jr   c, file_err        ; Ups, file couldn't be created

            ld   l,(iy+5)           ; IX = file handle
            ld   h,(iy+6)           ; HL = pointer to pointer to start address
            ld   e,(hl)
            inc  hl
            ld   d,(hl)             ; DE = start addr. of memory block to save
            ld   l,(iy+8)
            ld   h,(iy+9)           ; HL = pointer to pointer to length
            ld   c,(hl)
            inc  hl
            ld   b,(hl)             ; BC = length of memory block to save
            ex   de,hl
            ld   de,0
            call_oz(OS_Mv)          ; save memory block from (HL) onwards...
            call_oz(GN_Cl)          ; close file
            ret

.file_err   call_oz(GN_Err)         ; display error box...
            ret

Using relocatable code in BBC BASIC

Using machine code in BBC BASIC is best suited with allocating dynamic memory with DIM and then storing (assembling) the code to that area. If you don't assemble but have code ready, eg. loaded directly with PROC_lbytes, it is necessary that your machine code is relocatable, ie. contains no absolute address references (the machine code assumes that it is located at a certain ORG position in memory). You can relocatable code by omitting CALL and JP instructions and only use JR and DJNZ (jump relative) instruction. However, your program cannot be very large since relative jumps only range +/- 128 bytes in either direction from the instruction.

You can make truly relocatable machine code with the Z80asm application that is part of the Z88 Assembler Workbench. By using the '-R' option a small header is generated together with your code. The header contains a relocater routine and a relocation table. When your code is executed, it is automatically relocated to the current position in memory (just once). Subsequent calls to the code will just execute your code and not the relocater. With this option you can always store your machine code utilities with PROC_sbytes and at a later time allocate space with DIM for your code, and just load it into appropriate BBC BASIC memory. The code may be placed anywhere (in RAM).
 

Example program in BBC BASIC's assembler

We present here a short example program in BASIC. The error handler copes with pre-emption, responding to RC_QUIT by calling BASIC's own error handler. This will not close files, filters wildcards, or memory however, so if you use these features you must modify the error handler to close these things first before calling BASIC. The program here does something which BASIC cannot normally do, which is to read the update date of a file. When the program is run it assembles the code and asks for a filename. It attempts to open the file for DOR access, indicating failure with a system error box, and then reads the update date. Finally, having released the DOR handle, the program displays the explicit filename, expanded by GN_Opf, and the update date.

This listing can be 'loaded' by CLI. Mark a block for the program only in column A below, and save it as a text file. Then execute eg. from the FILER. .J

1000 DIM code 512                   \ space for program
1010 REM
1020 GN_Esp=&4C09                   \ return pointer to system error message
1040 GN_Nln=&2E09                   \ carriage return, linefeed to std. output
1050 GN_Sop=&3A09                   \ output string to std. output
1060 GN_Opf=&6009                   \ open file
1070 OS_Erh=&75                     \ install error handler
1080 OS_Esc=&6F                     \ examine special condition
1090 GN_Err=&4A09                   \ standard system error box
1100 GN_Sdo=&0E09                   \ date and time to standard output
1110 OS_Dor=&87                     \ DOR interface
1120 dr_rd=&09                      \ read DOR record
1130 dr_fre=&05                     \ free DOR handle
1140 op_dor=&06                     \ open file for DOR access
1150 rc_quit=&67                    \ KILL request error code
1160 rc_esc=&01                     \ escape detection error code
1170
1180 FOR pass=0 TO 2 STEP 2
1190 P%=code
1200 [
1210 OPT pass
1220 LD HL,0
1230 ADD  HL,SP                     \ get stack pointer
1240 LD   (bstk),HL                 \ save current BASIC stack pointer
1250 LD   SP,(&1FFE)                \ install safe stack pointer

1260
1270 XOR A
1280 LD B,A
1290 LD   HL,errhan                 \ address of error handler
1300 OPT  FNsys(OS_Erh)             \ install new error handler
1310 LD   (obou),A                  \ save old error handler call level
1320 LD   (oerr),HL                 \ save old error handler address
1330 CALL main                      \ call main routine
1340 .exit
1350 LD   HL,(oerr)                 \ address of old error handler
1360 LD   A,(obou)                  \ old call level
1370 LD B,0
1380 OPT  FNsys(OS_Erh)             \ restore old error handler
1390 LD   SP,(bstk)                 \ install BASIC stack pointer
1400 RET                            \ return to BBC BASIC interpreter
1410
1420 .errhan
1430 RET Z
1440 CP   rc_esc                    \ ESC pressed?
1450 JR NZ,err1
1460 OPT  FNsys(OS_Esc)             \ acknowledge ESC
1470 LD A,rc_esc
1480 OR   A                         \ return rc_esc back to main program
1490 RET                            \ Fc = 0, Fz = 0
1500 .err1
1510 CP   rc_quit                   \ KILL request?
1520 JR NZ,err2
1530 LD   HL,(oerr)                 \ re-install old error handler
1540 LD   A,(obou)                  \ old call level
1550 OPT FNsys(OS_Erh)
1560 LD   SP,(bstk)                 \ install BASIC stack pointer
1570 LD HL,(oerr)
1580 LD   A, rc_quit                \ reload A with RC_QUIT
1590 OR   A                         \ Fz = 0
1400 SCF                            \ Fc = 1
1410 JP   (HL)                      \ jump to BASIC's error handler
1420
1430 .err2                          \ write error message if possible
1440 OR   A                         \ Fc = 0
1450 RET 1460
1470 .bstk DEFW 0                   \ storage for BASIC stack pointer
1480 .obou DEFB 0                   \ storage for old call level
1490 .oerr DEFW 0                   \ storage for old error handler address
1500
1510 \ main routine starts here 1520 .main
1530 LD   HL,scratch_1              \ holds address of file to open
1540 LD   DE,scratch_2              \ explicit name buffer
1550 LD   C,40                      \ size of explicit name buffer
1560 LD   B,0                       \ HL string pointer is local
1570 LD   a, op_dor                 \ get DOR handle
1580 OPT  FNsys(GN_Opf)             \ open...
1590 JR NC,opened_OK
1600 OPT  FNsys(GN_Err)             \ report error in standard window
1610 RET 1620 .opened_OK
1630 LD   A,dr_rd                   \ read DOR record
1640 LD   B,ASC"U"                  \ read update information
1650 LD   C,6                       \ 3 byte internal date, 3 byte int. time
1660 LD   DE,scratch_1              \ store returned information at (DE)
1670 OPT  FNsys(OS_Dor)             \ fetch update date

1680 LD A,dr_fre
1690 OPT  FNsys(OS_Dor)             \ free DOR handle
1700 LD   HL,scratch_2              \ display explicit filename
1710 OPT  FNsys(GN_Sop)             \ to standard output

1720 LD HL,tab_str
1730 OPT  FNsys(GN_Sop)             \ tab to column 40
1740 LD HL,scratch_1
1750 OPT  FNsys(GN_Sdo)             \ output returned update date
1760 OPT  FNsys(GN_Nln)             \ display newline
1770 RET                            \ back to BASIC

1780
1790 .scratch_1 DEFM STRING$(40,"X")
1800 .scratch_2 DEFM STRING$(40,"X")
1810 .tab_str DEFM CHR$1+"2X"+CHR$(32+40)+CHR$0
1820 ]
1830 NEXT pass
1840
1850 CLS
1860 PRINT "Read File Update Date and Time"
1870 INPUT "Filename:"A$
1880 IF LEN(a$)>40 THEN PRINT "String too long": END
1890 A$=A$+CHR$0                    \ null-terminate filename string
1900 $scratch_1=A$
1910 CALL code
1920 END
1930 DEF FNsys(arg)
1940 IF arg>255 THEN [OPT pass: RST &20: DEFW arg] :=pass
1950 [OPT pass: RST &20: DEFB arg] :=pass


Previous Contents Next
Miscellaneous useful information BBC BASIC & in-line assembler Z88 Hardware