Authors
Publication
Publication Details
Volume: 2 Issue: 3
Date
Pages
TS2192 COMPUTER
The 2068 was introduced to us as a memory manager, capable of handling up to 256 banks of 64K memories, a veritable elephant among small plastic boxes. Close inspection revealed that an item called a BEU (meaning either Bus Expansion Unit or “hello” in Elephant) was necessary to access most of the extra memory banks, but not all. Three banks are native to the machine, almost, so you can have 192K with but a fraction of the trouble that it took to implement the full 64K on a ZX81.
There is the 64K that imitates the Spectrum. That comprises the 16K BASIC ROM and the 48K of RAM and is called the Home bank. The Home bank is in fact Bank FF (255). There is an entire 64K bank called the Dock (Bank 00), which was intended for cartridges, and which can be made active in 8K “chunks” anywhere in the 64K addressing space of the Z80. Finally, there is a slightly crippled bank known as the EXROM bank (Bank FE or 254), which was designed with the same lack of foresight that brought us the 16K-forever TS1000.
There is a hardware problem with the EXROM bank. The MREQ line that enables the EXROM 8K chip is active whether or not the address is to the chip or a multiple of the chip address. Three address lines (A13, A14, and A15) are not interpreted by the circuitry on the board, but if you add the circuitry to interpret them, then you gain access to to the other 56K. That gives you the ability to communicate with three banks of 64K.
Both of the external banks are available either on the dock connectors under the door of the 2068 or on the bus under the slot on the back. The chip select line is EXROM NOT for the EXROM bank and ROS_CS NOT for the Dock bank. If you switch these lines on a cartridge reader board, like the Oliger cartridge board, then you can read from the EXROM bank rather than the DOCK bank.
The 2068’s memory management allows you to activate or enable each 8K chunk of the Z80’s 64K of addresses in any bank that you have attached. However, the Dock and EXROM banks have an unfortunate link: they share the port byte (F4) that describes which chunks are enabled (the “horizontal select” byte). If bit 7 from port FF is high, then the byte from port F4 describes the EXROM bank; if that bit 7 is low, then port F4 describes the Dock. What this means is that the Dock and EXROM are mutually exclusive: you can have a mixture of chunks from Home and Dock, as when you’re running a cartridge, or a mixture from Home and EXROM, as when you run a cassette operation, but you cannot simultaneously mix Dock and EXROM chunks.
With all of this potential in the machine, a hopeful owner might dream of a disk system with CP/M in one bank, a Sinclair-like system in another bank, and a full bank of cartridge software, or perhaps CP/M 3.0 or even MP/M running the whole show. Only Aerco promises to use the Dock bank for its CP/M adapter, and I hope that promise gets filled in our lifetime.
Memory Management Code
My first experiences with the added Timex features were to discover the many ways in which the so-called function dispatcher could fail and crash the machine. Like many, I said “Why couldn’t they just give us a Spectrum in a chiclet box, and maybe a free watchband?” But I got to know the machine by moving HOT Z over from the ZX81, just in case I was missing something. HOT Z took up residence like a large family of relatives, squandering space, scattering new functions all over memory, and generally creating slum conditions (cheerful, but crowded) in my RAM. So I decided to move the whole show to EPROM, where HOT Z could occupy another bank of memory and keep its cotton-picking bytes off my RAM. To do that, I had to get to know the Timex memory management code in the EXROM.
The EXROM bank contains an 8K ROM with the overspill (Save-Load-Verify-Merge) from Mother Time’s messing about with the Cambridge ROM, as well as some extra initialization for the multibank memory management, the more renowned display-mode switches, and finally the source for the RAM-resident code, which normally resides at 6200-6840H.
That RAM-resident code block is generally called the “function dispatcher” simply because that service is the first one of the set. The function dispatcher is an egocentric concept in that it was written in anticipation that programmers would want to cling to the system software that is provided for BASIC. This seems unlikely, in view of the untapped display possibilities of the 2068.
The Timex memory-management code is generally the code that follows the function dispatcher. The technical manual was a revelation, an admission that there were bugs in that code and some suggestions for repairing them. Most of the suggestions worked, at least well enough to get me interested.
Unfortunately, there are mistakes of inattention that come to light even after the published patches. A good single-stepper can sort out the errors. I plodded through, patched up the rough spots, hiccups really, brought on perhaps by the rapid approach of corporate pink slips. When I was finished, the whole plan seemed to work, in so far as I could judge without a pet BEU to trumpet its call.
The rest of this article is a discussion of what I found in the RAM-resident EXROM code and what I’ve learned about how to use the services there. Much of this is for hardened programmers, but you just might need it some day.
The Function Dispatcher
I was initially puzzled as to what possible use the function dispatcher could be when you can simply make a direct call to the address involved. (The addresses for the function dispatcher are given in a pair of tables at the top of the 8K EXROM.) The RAM-res routines only become useful in a banked environment, where they handle bank switching for you.
If you’re writing your code to run in the Home bank, then the function dispatcher is the long way around to do a ROM call. If you want your code to run in a cartridge or in any bank, then you might want to make use of the function dispatcher, though it’s really only necessary if your code is in one of the chunks (0 or 1) shadowed by the ROM. Do not suffer the illusion that the function dispatcher makes the ROM calls any easier to use. Those of you who saw my last article here will recall that many ROM routines are best entered at the second or third instruction, and the dispatcher doesn’t give you this option.
The function dispatcher itself requires a “service code” (16 bits) on the stack. The service codes are listed in detail in the technical manual; the addresses they correspond to are easily read from the jump table at the top of the EXROM (1EDC and above). Three of these addresses (for XFER, CALL_BANK, and GOTO_BANK) are off by a byte in the original EXROM, and none of those for the RAM-resident routines will work if the second display file is active. Finally, because of sloppy register handling, there is no direct access to the cassette functions.
The function dispatcher is a high-level routine that enables you to exchange a block of parameters with a group of routines that can neither receive nor return those parameters. (They are fixed by the addresses burned into the EXROM.) Thus if you care to take a trip on this Rube-Goldberg device, you must push in succession the service code (high bit low for a CALL) and then four bytes (two pushes) of zeroes to indicate no parameters expected in from the stack and none sent out.
Unless you are writing for chunks 0 or 1, there is an easier way to make ROM calls, which is to enable the Home bank, do the call, and then reenable whatever bank you have called from. For example:
LD BC,FFFC
CALL ENAB
CALL XXXX
LD BC,YYZZ
CALL ENAB
where XXXX is the ROM address, YY is the bank number and ZZ is the horizontal select. You can push and pop around the ENAB call if you need BC. If you want full service, then learn how to use the CALL_BANK routine described below.
Moving Code Between Banks
Another high-level service is the XFER bytes routine at FEE2 or 6722. It moves code in blocks via the stack from one bank to another. This routine exercises the stack almost to its limit of 512 bytes. A consequence of this stack passing is that you cannot use XFER to move anything in or out of chunks that are shadowed by the stack.
My attempts to use XFER uncovered more bugs than I care to list, but once fixed it works very well. To use XFER, you must push in order:
- 16 bits that comprise the bank number of the source (high byte) and destination (low)
- the start address of the source to be copied
- the destination address
- the length (16 bits)
- and finally 16 bits of which the high byte is unused and the low one holds 0 to indicate an LDIR or FF for a LDDR.
Don’t pop this stuff off the stack afterwards; that’s done for you.
When it’s up and running, XFER will move any block of memory from within one 8K chunk to the same or any other bank-chunk where there is RAM. You can use it to copy code from a cartridge into RAM, where you can modify the code and reburn it to EPROM. If you learn how to attach RAM to the Dock or EXROM banks, then XFER becomes indispensable for copying code in and out of the Home bank. (SAVE and LOAD work only on the contents of the Home bank, which becomes the portal to the rest of memory.)
Put_Word and Get_Word are simpler move routines that don’t stress the stack. Put an address in HL and a bank number in B; then Get_Word will bring you back the contents of that address and the next in HL. (Like LD HL,(NNNN), but interbank.) Put_Word will put the word in DE at address HL in bank B. (Like LD (HL),E; INC HL; LD (HL),D.)
Bank Enable Routines
The lowest-level RAM-resident routines are Write_Bank_Status_Register and Read_Bank_Status_Register. These routines do the dirty work of feeding the elephant, enabling memory banks and chunks via the BEU. If you are a hardware sleuth, you might be able to reinvent the BEU from these two routines and the initialization that goes on in the EXROM after 099AH.
The rest of us will probably want to use ENAB, which enables chunks of the bank whose number is in B. The enabled chunks are the low bits of C, which holds the “horizontal select” byte. (7F enables only chunk 7, FE enables chunk 0, CF enables chunks 4 and 5, and so on, binarywise.) The protocol is LD BC, Bank-Chunk and CALL ENAB. Thus LD BC, 008F followed by CALL ENAB would enable chunks 4, 5, and 6, because 8F = 10001111 and the zeroes are bits 4, 5, and 6, counting from bit zero on the right. Since B holds the number of the Dock bank (00), the enablement takes place for that bank. All registers are preserved if, contrary to the tech manual, you correct this routine by replacing the push and pop of BC rather than of AF with the recommended DI-EI combination.
Home bank is enabled by default. Chunks not specified will come up as Home bank.
The ENABle routine was written to go along with the lack of decoding for the EXROM and makes it impossible to enable the higher chunks of the EXROM bank. I remedied this by making the EXROM enabling code loop back through the code for the Dock enable, so that the revised routine treats both banks equally. If you decode the address lines for the EXROM then you should also replace the code in the EXROM to be able to use the extra addressing space.
You can compensate for the inability of the function dispatcher to reach routines in the EXROM by enabling the EXROM (LD BC, FEFE and CALL ENAB) and then calling any routine you know how to use down there. (Remember to re-enable the chunks you want active after such a call.) There is also a routine at 6815 or FFD5 (GOEX) that will switch on the EXROM and jump to the address supplied in HL; if you put a return address in Home RAM on the stack and use that GOEX routine to get to a callable routine in EXROM, then the first RET will get you back, but don’t forget to switch out the EXROM afterward.
Finally, there is one way you can avoid the Enable routine and its bugs altogether for the three banks discussed here. The following routine
IN A,(FF)
RES 7, A
OUT (FF),A
XOR A
OUT (F4),A
leaves you in Home Bank and represents another form of enable.
Get_Bank_Number will tell you what bank is enabled for the address in HL. The bank number comes back in A, in case you need to know what bank is active for a particular address. Get_Bank_Status gets the horizontal select byte into C for the Bank whose number you give it in B. Get_Chunk returns a mask in A with the bit set for the chunk containing the address in HL.
Interbank Calls & Jumps
Call_Bank is another high-level routine that allows you to call routines in another bank, pass a number of parameters on the machine stack and to return as from an ordinary call, and have some other number of parameters returned on the machine stack. For example, you might be calling complex cartridge routines from RAM in Home bank. The parameters to be passed go on and come back on the machine stack. These parameters are described by their byte count. The called routine must know how many parameters to expect on the stack, since that information is not passed by the Call_Bank routine. The number of parameters expected back can be found on the bank-switching stack at FD8A or 65CA.
A small error, corrected in the tech manual, caused Call_Bank to crash if you wanted to pass parameters. Once it works, the way to use this routine is as as follows:
- Push the values you want to pass onto the machine stack. The number of bytes you push is called PRM_OUT.
- Push the address of the routine you want to call, followed by the bank number (high byte) and the horizontal select (low byte) that will enable the code for the call.
- Then push the 16-bit number that equals PRM_0 UT. This number should not be larger than about 0180 H (384d) or you will cause the machine stack to overflow its normal limit.
- Push another 16-bit number, PRM_IN, that is the number of bytes you expect back from the called routine. (If the called routine does not for some reason return the correct number of bytes, you will crash.)
- Next, call Call_Bank.
- Finally, pop off the parameters expected back from the called routine. It is the responsibility of the called routine to get the right number of parameters off the stack and return the specified number of bytes. Pop off only PRM_IN bytes, as the rest of the stack is managed by the Call Bank service routine.
If you pass no parameters, then push two words of zeroes for steps 3 and 4, and skip step 6.
Goto-Bank requires a stack consisting of the address to which you want to jump, and a 16-bit number, of which the high byte is the bank and the low byte is the horizontal select. Push the address first, followed by the bank/select bytes. You must make your own arrangements for the return. Again, the pushed values are cleared from the stack for you. Even though you CALL this routine, it amounts to a JP.
A couple of middle-level routines, Save_Bank_Status and Restore_Status, can be useful to get you back to wherever you started switching banks. The first gets the status of every bank and writes it in a byte per bank to the address pointed to by IX and above. The Restore reads the status bytes and enables the originally active chunks. These are really subroutines that require the caller to manage the value in IX. The 2068 routines generally have IX pointing to a gap in the stack, but you can use it as a simple pointer to a storage area.
RAM-Resident Code | Address | Address (High) |
---|---|---|
Function dispatch | 6200 | F9C0 |
INT (for RST 38H) | 62AE | FA6E |
INT NMI | 6307 | FAC7 |
GET WORD | 6316 | FAD6 |
PUT WORD | 633B | FAFB |
WRITE BANK STATUS REG | 635C | FB1C |
READ BANK STATUS REG | 63AD | FB6D |
GET BANK STATUS | 6405 | FBC5 |
GET CHUNK | 644D | FC0D |
GET BANK NUMBER | 645E | FC1E |
BANK ENABLE | 6499 | FC59 |
SAVE STATUS | 651E | FCDE |
RESTORE STATUS | 654A | FD0A |
GOTO BANK | 6572 | FD32 |
CALL BANK | 65D0 | FD90 |
XFER BYTES | 6722 | FEE2 |
GOEX | 6815 | FFD5 |
The high addresses occur when you are using any video mode but the default (32-column).