MIRA Mike Rains gives your Microdrive a random access facility. [ One note to begin with: as this is a program which works with Microdrives, it has been provided on a .mdr file, not a .tzx file as is more usual. If your emulator can't handle Microdrive files, well, you have no use for MIRA anyway... ] One of the main restrictions of the Sinclair Microdrive system is that it is only possible to access data files from Basic which imposes a severe restriction on database and other similar applications which access data files. For example, changing a record in a data file involves opening a new file, copying the preceding records from the old to the new, writing the altered record to the new file, copying the rest of the old file, deleting the original file and renaming the new file as the old file. Even this last is not straightforward as Sinclair Basic lacks a Rename command. In practice, the following code illustrates the type of subroutine that would be required to alter the nth record of a file of 100 records called "data" in which each record is 24 bytes long. The new record is in the string n$. 9000 REM Alter a record (Sinclair BASIC) 9010 OPEN #5;"m";1;"data" 9020 OPEN #6;"m";1;"new.data" 9030 IF n=1 THEN GOTO 9070 9040 FOR p=1 TO n-1 9050 INPUT #5; LINE x$: PRINT #6;x$ 9060 NEXT p 9070 INPUT #5; LINE x$: PRINT #6;n$: REM write new record 9080 IF n=100 THEN GOTO 9120 9090 FOR p=n+1 TO 100 9100 INPUT #5; LINE x$: PRINT #6;x$ 9110 NEXT p 9120 CLOSE #5: CLOSE #6 9130 ERASE "m";1;"data" 9140 MOVE "m";1;"new.data" TO "m";1;"data" 9150 ERASE "m";1;"new.data" 9160 RETURN It should be clear that this is both tortuous and very slow as it involves many operations of the Microdrive, which also causes increased wear and tear on the cartridge and drive. MIRA solves these problems by providing Sinclair Basic with three new commands which together allow the maintenance of random access files similar to those avail- able on micros such as the BBC with disc interface. The new commands (RND#, POINT# and RESTORE#) are all obtained by using existing Sinclair keywords but in new contexts. The use and syntax of the new commands and the theory behind their implementation are explained below but first, as an example, the above subroutine re-written using the MIRA commands. Note that a 24 byte record occupies 25 bytes in the file because of the inclusion of a carriage return (CHR$ 13). 9000 REM Alter a record (MIRA) 9010 OPEN #5;"m";1;"data":RND#5 9020POINT #5,25*(n-1): PRINT #5;n$ 9030 RESTORE #5 9040 RETURN The first of the new commands, RND followed by a stream identifier (e.g. #5) causes the file attached to the given stream to be declared as a random access file. The file must already have been opened with the standard OPEN # command and must be a "read" file. The RND command cannot be used to create or open a file. In the example above, the file "data" would originally need to have been set up with code such as: 100 DIM x$(24) 110 OPEN #5;"m";1;"data" 120 FOR p=1 TO 100: PRINT #5;x$: NEXT p 130 CLOSE #5 POINT followed by a stream identifier and a numeric expression separated by a comma (e.g. POINT #5,n) moves the internal pointer of the file attached to the given stream to the n th byte of the file, where the next INPUT #, PRINT #, or INKEY$# will take place, provided that the file has been opened with OPEN # and made random with RND#. An error will be generated if you try to move the pointer outside the limits of the file. The Basic loader program for MIRA - listing 1 [on the .mdr as "Mira", and also as "run" for your auto-loading convenience] - also defines a function, FN p(x), which returns the current value of the internal pointer of the file attached to stream x - e.g. LET pos=FN p(5). For what it's worth, this will work with any "read" file, not just a random access one. RESTORE followed by a stream identifier (e.g. #5) closes the random access file attached to the given stream. RESTORE # must be used in preference to CLOSE # to correct- ly close a file that has been marked as random with RND#, but may not be used to close any other type of file. The theory of adding new commands to Sinclair extended Basic (i.e., with Interface 1 attached) is fully described by A. Pernell, Master Your ZX Microdrive (Sunshine 1983) and I. Logan, Spectrum Microdrive Book (Melbourne House 1983). In short, the shadow system variable VECTOR (address 23735), which usually contains the address of the ROM error handling routine, must be loaded with the address of the new commands routine which must then perform two distinct functions. First, it must check the syntax of the new commands and cause an error if it is incorrect, and then, at run-time only, execute the command. "Run-time" means during the running of a program or during the execution of a direct command. Lines 9030-9040 of the MIRA Basic loader program alter the value of VECTOR (line 9030 first ensures that the shadow system variables have been created). As well as providing the new commands as described above, MIRA must also provide new routines to replace the standard PRINT and INPUT commands. To demonstrate how this is done and to explain the actual operation of the new commands it is necessary to first describe the structure of a Micro- drive channel. Each Microdrive channel takes up 595 bytes in the area of memory reserved for channel information. The start of this area is contained in the system variable CHANS (address 23631). The actual location of a particular channel is found by adding the displacement contained in the relevant STRMS system variable - two bytes for each of streams -3 to 14 starting at address 23568 - to the value in CHANS and subtracting 1. FN q(x) defined in the Basic loader program performs this function for stream x. The byte allocation within a Microdrive channel is shown in figure 1 and should be referred to during the following discussion of the operation of the various MIRA routines. ___________________________________________________________ Figure 1. Microdrive channel structure. Byte Length Description 0 2 "output" routine 2 2 "input" routine 4 1 channel specifier "M" 5 2 shadow ROM output 7 2 shadow ROM input 9 2 length of this CHANS area (595) 11 2 current buffer position (0-512) 13 1 position of record in file (0-255) 14 10 10 bytes of filename 24 1 bit 0 - reset - "read" file set - "write" file bits 1-7 - unused (all reset) 25 1 drive number (1-8) 26 2 drive map location 28 12 12 bytes of header pre-amble 40 1 bit 0 - set to signal header bits 1-7 - unused 41 1 sector number (0-255) 42 2 unused (see text) 44 10 10 bytes of cartridge name 54 1 header checksum 55 12 12 bytes of data pre-amble 67 1 bit 0 - reset - not a header bit 1 - reset - not end of file bit 1 - set - end of file bit 2 - reset - a PRINT file bit 2 - set - not a PRINT file bits 3-7 - unused (all reset) 68 1 record number (0-255) 69 2 number of bytes in record (0-512) 71 10 10 bytes of filename 81 1 checksum 82 512 buffer data 594 1 data checksum ___________________________________________________________ The RND# command run-time routine performs three actions: # Bit 0 of byte 24 in the relevant channel is set to signify a "write" file. # The addresses of the new output and input routines are loaded into bytes 5-6 and 7-8 of the channel. # Bit 0 of the unused 42nd byte of the channel is used as a flag to signal whether the buffer contents have been altered. RND# resets this bit to signify "unaltered". The following actions are performed by the POINT# command: # The record number in which the desired file position lies is calculated and compared with the value in byte 13 of the channel. # If they do not match, the "write buffer" and "read buffer" sub-routines are called to fetch the required record into the buffer. # The desired position within the record is loaded into channel locations 11 and 12. The RESTORE # calls the "write buffer sub-routine to write the current record if it is flagged as "altered" (i.e.: bit 0 of byte 42 is set), resets but 0 of channel byte 24 to flag the file as "read" and then calls the shadow ROM CLOSE # routine. The new output routine used by all PRINT # statements is entered with the character to be written to the file in the A register. The following actions are performed: # The current buffer position is obtained from bytes 11 and 12 of the channel, added to 82 - the start of the data area - to give the correct address, and the character from the A register stored there. # The buffer position is incremented and re-stored in bytes 11 and 12. # The record is flagged as "altered" by setting bit 0 of byte 42. # The "next record" sub-routine is called if the end of the present record has been reached. The new input routine used by INPUT # and INKEY$# state- ments performs the following actions: # Tests for the end of the current record and calls "next record" if required. # Increments the buffer position in bytes 11 and 12 of the channel. # Using this position - added to 82 as before - reads a character from the buffer into the A register and sets the carry flag to signal a successful read. [ The article ended with some instructions for entry which were jumbled, and neither complete nor correct; they look like they were written for another version. The last remark is correct, though: ] Note that MIRA will need to be re-activated by the use of lines 9020-60 of listing 2 following a NEW command. [ And now a bit on the contents of the .mdr. This includes "Mira" a.k.a. "run" and "MiraLoader" as described above, as well as the "c:mira" machine code file produced by the latter and read by the former. Since Mr. Rains did not provide a sample application of his tool, but I needed one myself to test that I had entered the MC correctly, I've written a very simple one called "Notepad", which I've also provided on the .mdr. Do not be deceived by its triviality; much more compli- cated applications could be written, but I did not have the time to do so for an example program. Finally, after writing this program, I discovered that it did not work, and after some work with a disassembler, I found that the cause of this was not a typo, but the fact that Mr. Rains had not used hook codes to call the Interface 1's internal routines, but absolute addresses. Of course Murphy ensured that my emulator has a different issue IF1 ROM than the one Mr. Rains coded for. Luckily, this was easy to patch: just a few addresses need to be re-POKEd, and it seems that the ones used did not change after the second issue of the IF1. The result of this is the final program on the .mdr, "MiraPatch". It loads the original machine code, backs it up, patches it for newer ROMs, and saves the new version under the old name. In fact, for my own convenience, the "c:mira" that is on the .mdr is the patched version (and the backup is also present). You may have an even newer (or just different) version of the ROM, in which case even this may not work; if so, sorry, I can't help, but you might try finding the correct addresses for your version, using the ones Mira uses, a copy of Gianluca Carri's "Spectrum Shadow Rom Disassembly" (available as a download from World of Spectrum, amongs others), and a disassembly of your own IF1 ROM. You can then enter the right addresses in MiraPatch and create your own version of the code. Richard Bos, May 2012. ]