Far memory routines have now been implemented in z88dk for the Z88. This document describes how you can write C programs using these routines, and also gives details of how the scheme is implemented for anyone interested.
The document is divided into the following sections:
The Z88 far memory model used by the Z88 Development Kit uses a 3-byte far pointer size. Near pointers can easily be cast as far pointers, so it is easy to write programs using a mixture of near and far memory. It is obviously not possible to cast far pointers as near pointers, however.
There is no limit to the size of the far memory heap (other than the 4Mb limit imposed by the Z88 architecture!), and allocations of any size can be made, provided your heap is large enough and the host Z88 has sufficient RAM available. Very large heaps will require more of your bad application RAM to be reserved for memory management, however, and full details of this are given in the next section.
The far memory model is only supported by the application target type in Small C; so it is not possible to write programs running under BASIC using far data. Due to the memory overheads required by the allocation routines (see implementation details if you're interested), any application which uses this model will be bad.
Packages may use far pointers as parameters passed to them by applications, but since each application has its own far heap managed through its static workspace, the following limitations apply:
Using the far memory allocation words in your Z88 applications is quite straightforward. Firstly, you must signal that you will be using the far memory functions with the following line:
#define FARDATA 1
This line should be before any includes, as the standard library header files use it to determine whether to make far memory versions of the library functions available.
Secondly, specify the size of the far heap your application requires with a line as follows:
#pragma -farheap=65536
The above line gives a heapsize of 64K. You can specify any size up to a maximum of 4194304 (4Mb). Bear in mind the following points:
For example, if you have a maximum heapsize of 65536 bytes, the amount of static workspace used will be 999 bytes. A 4Mb heap would require about 32.5K of static workspace!
Within your C application, you can now use the following functions:
You can use various string functions on far memory pointers; where functions take two parameters, you can also provide near pointers as parameters (remembering to cast them as far!) The following functions are currently available for far pointers: strchr, strrchr, strcpy, strncpy, strcat, strncat, strlen, strlwr, strupr.
For more information, take a look at the example in examples/app/farmall/.
Knowledge of the way far memory is implemented on the Z88 may be helpful to anyone wishing to write a similar library for another machine, or for those hoping to port the method to their own (non-C) environment.
Also, if you are planning to rewrite the Z88 operating system (!) or replace any of the memory-allocation calls using the pkg_ozcr call, you should take careful note of the assumptions I have made here, and try not to break them ;-)
Note that all the assumptions made below are true for all versions of OZ up to and including v4.0
Whenever far memory support is required in an application, a certain amount of bad application workspace is automatically reserved. This consists of:
This structure, as noted above, is used to hold the pool handles that have been opened by the application. Initially, it is filled with nulls, indicating that no pools are open.
This is a table of 1-byte entries, each corresponding to a bank in the Z88's memory map. As banks 0-31 are always ROM (internal), the first entry corresponds to bank 32, with the final entry corresponding to bank 255. Each entry holds either 0 (indicating no pool open for that bank), or a value which relates directly to the pool handle returned by OZ for that bank.
Three important assumptions are made when constructing and using this table:
The first two assumptions mean that we only need 224 table entries (one for each bank) no matter how much memory we allocate. The last assumption means that each entry only needs to be one byte in size (we shift the handle value 4 bits to the right), and a zero value can indicate no pool.
Each entry in the malloc table consists of two bytes, and corresponds to an allocation of 256 bytes of far memory.
The first byte contains the bank number containing the allocation (0 indicates no allocation has been made for this entry), and the second byte contains the high byte of the address of the memory allocation (always in segment 1).
The low byte of the address is not required, since for allocations of 256 bytes, OZ always returns an address with a low byte of zero; thus this is always assumed.
The first stage in memory allocation is to locate a section of the malloc table which has not yet been allocated (ie contains nulls) and which is large enough for the required allocation. An extra 2 bytes are always added to the amount of memory required, to hold the number of pages (256-byte allocations) in the allocated block.
When a suitable region has been located (malloc always uses the smallest suitable region), we start making allocations and filling the malloc table. This is done as follows:
If all pools are exhausted and no further ones can be opened before all memory is allocated, the memory allocated so far is freed, and malloc returns an error.
Once the allocations are completed, the number of 256-byte pages used is stored in the first two bytes, and a far pointer following this value is returned.
Deallocating is much more straightforward. The pointer is decremented twice, and the value fetched is thus the number of 256-byte pages that were allocated.
A loop is entered which frees each 256-byte allocation, and zeros the corresponding entry in the malloc table.
Note that no pool handles are freed during this process, but they become usable again when further mallocs are performed. All handles in the pool table are freed by the freeall function, which is automatically called on exit from the application.