Accessing the disk system is slightly more difficult than tape, but then it is a more complex storage system. As far as tape is concerned, you can only read a file & store it. With disks, not only can you read files, but access tracks & sectors and even read & write different disk formats. Again a majority of the work in talking to the disk system is handled by the Dragon's DOS and all you have to do is call the appropriate ROM routine.
All the 'legal' ROM routines are stored in a jump table at the start of the DOS ROM at $C004-$C02E. This means that no matter which DOS you are using, calling these routines will have the same effect. This table contains addresses which point to the relevent DOS routines, and should be called from assembler using an indirect jump (using square or round brackets) eg.
This is as far as compatability goes between different DOS's. Even the workspace differs between different variants. If you jump into the ROM directly, then it is highly likely that whilst the routine you have called will work, executing it on another DOS will cause it to crash.
The top level of the disk system, is the DOS filing system. This manages all the files stored on the disk, and you need know nothing about tracks & sectors in order to use it. There is a host of ROM routines to manage this, but the primary ones are as follows:
[$C008] - validate a filename [$C00A] - 'open' a file [$C00C] - create a file [$C012] - close a file [$C014] - load some file data [$C016] - write some data to a file
The Dragon's DOS system uses a number to represent each file that is open from 1 to 10.
Using the calls is fairly straightforward. First, you format a filename in memory (using the same format as normal when referring to DOS files), then call [$C008] to validate it:
FILENAME FCC \2:TESTFILE.DAT\ DEFEXT FCC \DAT\ FCBN RMB 1 TESTDATA FCC \THIS IS A TEST DISK FILE\ START LDX #FILENAME - X points to the name LDB #14 - B = filename length LDY #DEFEXT - default extension JSR [$C008] - validate filename BNE ERROR - if an error occurs
The Y register can point to a default file extension, in this example DAT. If you then specify a filename without an extension (as you would when loading BASIC files) this extension is assumed (if you don't want a default extension, then point the Y register to a block of 3 spaces ie. FCC \ \.) When the routine is called, it sets the zero flag if an error occurs, which can be acted upon by the BNE instruction to an error handler. If an error has occured, it puts the BASIC error number into the B register, and jumping to $8344 will generate this error eg.
ERROR JMP $8344 - generate the BASIC error.
After the filename is validated, call [$C00A] to open the file. This should always be the case, even if the file doesn't exist yet. This routine will allocate your file it's own number. As before, the routine sets the zero flag if an error occurs, but you need to be careful if you're writing a new file, as it will indicate an error if the file doesn't yet exist:
JSR [$C00A] - open the file BEQ OPENOK - opened successfully CMPB #$A0 - did routine return ?NE ERROR. BEQ OPENOK - yes, so OK BRA ERROR - generate an error otherwise
If the file is opened successfully, this routine will return the number assigned to the file (the File Control Block Number). Up to 10 disk files can be open at a time on the Dragon, and each is given a number from 1 to 10 by the DOS. From now on, all the disk operations are performed simply by referring to this File Control Block Number:
OPENOK STA FCBN - store the Control Block No. returned in A register
The first thing to do when writing a new file, is to create it, by calling [$C00C]. This requires the File Control Block Number in the A register, and since we have just loaded it:
JSR [$C00C] - create a new file BNE ERROR - deal with any error
This routine also deals with the renaming of files to .BAK if the file already exists.
In order to write some data, use the [$C016] call:
LDA FCBN - our file control block no LDX #TESTDATA - point to data to write LDU #24 - no of bytes to write LDY #0000 - Y & B registers hold the CLRB - file pointer JSR [$C016] - write the data BNE ERROR - handle any error
The inputs to this routine are straightforward, except for Y & B which are the point in the file you wish to write to. Unlike tape, where you must write data sequentially, byte after byte, with disk you can write anywhere in the file. In this case, we want to start at the beginning, so set both values to 0.
Now the file can be closed:
LDA FCBN - file to close JSR [$C012] - close the file BNE ERROR - any error RTS
It is important that all files are closed, otherwise the disk will become corrupt. This call is responsible for updating all the directory information & file sizes. Because of this, it is worthwhile modifying your error routine to close the file if an error occurs:
ERROR PSHS B - preserve the error no LDA FCBN JSR [$C012] - close the file PULS B - restore error no JMP #8344 - generate the error
If you were now to run this code, and directory the disk in drive 2, you should find a file 24 bytes long named TESTFILE.DAT.
Having written the test file, it can be read back from disk using a similar method. The following sample program demonstrates this, taking much of the code from the previous article:
FILENAME FCC \2:TESTFILE.DAT\ DEFEXT FCC \DAT\ FCBN RMB 1 TEXTSCR EQU $400 - address of the text screen LOADER LDX #FILENAME - point to filename LDY #DEFEXT - default extension LDB #14 - filename length JSR [$C008] - validate filename BNE ERROR JSR [$C00A] - open file BNE ERROR - note we don't need to check for ?NE error since this is a read operation only. STA FCBN - store File Control No. LDY #24 - 24 bytes to load LDX #TEXTSCR - load it onto the text screen LDU #0000 - file read pointer CLRB - starting at file start JSR [$C014] - load the file BNE ERROR LDA FCBN JSR [$C012] - close the file BNE ERROR RTS ERROR JMP $8344 - error handler
Running this program, will display the text THIS IS A TEST DISK FILE in the top left hand corner of the screen.
Whilst this is all well & good, these programs are rather too simplistic for everyday use. Often as not, programs will not have a fixed filename, you won't know how big the file is or even where to start reading or writing from. Again making use of the Dragon's DOS & ROM makes life significantly easier.
For example there are standard ROM calls which can convert a filename passed as a string to the format which the validate filename [$C008] routine needs. This would enable you to specify the filename as part of an EXEC call eg.
EXEC 21480,"2:TESTFILE.DAT" or FI$="2:TESTFILE":EXEC 21480,FI$
The code to accomplish this is as follows:
JSR $89AA - skip the comma JSR $8887 - Get String Calls JSR $8877 LDX >$52 - $52 points to string descriptor LDB ,X - byte 0 contains length LDX 2,X - byte 2,3 points to string LDY #DEFEXT - Y still point to def extn JSR [$C008] - validate filename BNE ERROR
The open file call [$C00A] also returns some additional information, apart from the File Control Block Number. Location $F1 also contains the File Control Block Number and is used by DOS to store the current control block in use. Location $EB contains the drive number of the current file, but most importantly the X register is set to point into the Control Block of the file.
Each file in use has it's own Control Block which consists of 31 bytes stored in the DOS's workspace between 1536-3072 in memory. Since 10 files can be open at once, there are 10 of these blocks. Each File Control Block contains details about the file in use, such as it's name, drive number, details of tracks/sectors, directory data etc. etc. most of which is not particularly useful to us. However it does contain the current read & write pointers of the current file.
These pointers tie up with the registers we have to load when calling the write a block of data [$C016] or read a block of data [$C014] routines. As a file is read or written, they are automatically updated with whereabouts you are in the current file. Since for most applications, you won't want to manipulate these pointers yourself, you can let the system do it for us.
The X register passed back from the open call [$C00A] points to the current read pointer in your file control block. This is a bit of a tricky concept, it makes my head spin if I'm not fully awake as it is. The read pointer is 3 locations in memory which tell you where you are in the current file. The X register contains the location in memory where you can find these 3 locations, so it effectively 'points' to the read pointer.
As an example, suppose you wanted to read in 20 bytes from a file, process them, then read another 20 bytes etc. etc. You could make use of the read pointer as follows:
-- get the filename, validate it -- JSR [$C00A] - open file STA FCBN - store control block no STX READPTR - preserve X register READLP LDA FCBN - file no to read from LDX #BUFFER - buffer for data to read LDY #20 - want to get 20 bytes LDU READPTR - fetch the pointer to the read pointer LDB 2,U - fetch the lower part of the read pointer LDU ,U - fetch the upper part of the read pointer JSR [$C014] - read the block BNE ERROR -- process data -- BRA READLP - back to get next block
Writing a file is equally straightforward except you need to add 4 to the X register to get the pointer to the write pointer:
-- get the filename, validate it -- JSR [$C00A] - open file STA FCBN - store control block no LEAX 4,X - add 4 to get write ptr STX WRITEPTR - preserve X register WRITLP LDA FCBN - file no to write to LDX #BUFFER - buffer for data to write LDU #20 - want to store 20 bytes LDX WRITEPTR - fetch the pointer to the write pointer LDB 2,X - fetch the lower part of the write pointer LDX ,X - fetch the upper part of the write pointer JSR [$C016] - write the block BNE ERROR -- fetch next 20 bytes into buffer -- BRA READLP - back to get next block
The 2 programs show up another problem - when to stop reading & writing. Presumably, the write routine will stop when the 'fetch next 20 bytes into buffer' routine identifies there is no more data to write, but the read routine needs some way of detecting when the end of file has been reached.
Again, the DOS comes to the rescue again, with another call:
[$C00E] - get file length
This will return the length of the file in the same format used to store the pointers. It can therefore be called after opening the file:
FILELEN RMB 3 - storage for file length LDA FCBN - control block no of file JSR [$C00E] - get file length BNE ERROR - any error STU FILELEN - upper part of file len STA FILELEN+2 - lower part of file len
This can then be compared with the read pointer at the required time:
LDU READPTR - get pointer to read ptr LDB 2,U - get lower part of ptr LDU ,U - get upper part of read ptr CMPU FILELEN - compare upper part of pointer with upper part of file length BHI EOF - exceeded file length BLO MOREDATA - still more data
at this point we know that the upper part of the write pointer = upper part of the file length, so now check the lower part.
CMPB FILELEN+2 - compare with lower part BHI EOF - exceeded file length MOREDATA
-- continue to read next data chunk --
So far I've managed to cover accessing the Dragon's disk system from machine code, making use as far as possible of what DragonDOS does automatically. Whilst it is fine to read & write your own format files, there may come a time when you want to read or write a recognized DOS file such as a BASIC or binary file. If you attempt to LOAD the TESTFILE.DAT file we wrote several articles back, then the system will refuse to do so, returning an ?FM ERROR. Even renaming the file to .BAS or .BIN will make no difference.
A recognized DOS file is identified by the first 9 bytes of the file. You may notice that whenever you save a .BIN file that 9 extra bytes are added to it. For example, just saving a 1 byte file eg
will result in a 10 byte file created on the disk.
These 9 bytes are used to identify the file, and therefore LOAD the file correctly. For example, the loading of a BASIC file is different from that of a binary file. The 9 byte header structure is defined as follows:
Byte 0 - always hex $55 Byte 1 - file type: 01 = BASIC 02 = Binary Bytes 2,3 - Load Address Bytes 4,5 - Length Bytes 6,7 - EXEC address Byte 8 - always hex $AA
Data files (.DAT) have no header and just contain the data written to them.
Knowing this you can successfully read DOS structure files. More likely, is the need to read binary files, and therefore a simple piece of code to achieve this:
-- assume file already opened -- LDA FCBN - file control block no LDX #BUFFER - buffer for header info LDY #9 - 9 bytes to read LDU #0000 - from start of file LDB #0 JSR [$C014] - read header BNE ERROR LDA BUFFER+1 - get the file type CMPA #2 - is it binary BNE FMERR - no, generate an FM error
proceed to load the file based on the header information:
LDA FCBN LDX BUFFER+2 - get the start address LDY BUFFER+4 - get the length LDU #0000 LDB #9 - start from byte 9 JSR [$6014] - load the file into memory BNE ERROR LDX BUFFER+6 - load the EXEC address STX 157 - store in the EXEC vector -- close file & quit -- FMERR LDB $2C - code for ?FM ERROR BRA ERROR
Writing binary files is similarly straight forward. Simply build the 9 byte header in memory, write it out then dump the file out. In addition this format can also be used if you want to create user defined disk formats. For example, for my sampler system I defined a type 04 file, which uses exactly the same 9 byte structure except the start and length fields contain a multiple of 256 bytes. Hence a file length of 4 indicates 4*256 = 1K. (I didn't use type 3 files because this is actually defined as another DOS file type Segmented Binary which I've never used and don't know of anything that does).
Finally in this section on the DOS file structure, some other ROM calls useful for manipulating files:
[$C01A] - KILL a file eg. LDA FCBN JSR [$C01A] - kill the current file BNE ERROR - handle an error [$C01C] - set/reset file protection eg. LDA FCBN CLRB - B = 0 Protect off B = 1 Protect on JSR [$C01C] - protect off then BNE ERROR [$C01E] - rename a file
You should use this in conjunction with the [$C008] validate filename call. Open the file as usual, then validate the new filename & call the rename routine:
-- assume file already open - LDX #NEWFILE - new file name LDB #NEWFILELEN -new filename length LDY #DEFEXT JSR [$C008] - validate filename BNE ERROR LDA FCBN - control no JSR [$C01E] - perform rename BNE ERROR
The level below DOS's filing system concerns tracks & sectors directly. The Dragon's disk system implements this in a fairly straight forward manner, making the stepping up from 40 track single sided disks to 80 track double sided disks an easy process.
The specifications for a standard Dragon disk are as follows:
256 bytes per sector 18 sectors per track (1 to 18) 40 tracks per disk (0 to 39)
Double sided disks are handled by increasing the number of sectors per track visible to the user, not complicating issues with side numbers, so for a double sided disk:
256 bytes per sector 36 sectors per track (1 to 36) 40 tracks per disk
Any combination of disk can be used, double sided 40 disks, single sided 80 etc. The Dragon provides track & sector access through the following ROM calls in the jump table:
[$C004] - controlling 'processor' [$C026] - read absolute sector [$C028] - write absolute sector [$C02A] - verify absolute sector - later DOSs only.
The read & write sector calls are the most useful and I'll concerntrate on those first. They both take 2 register inputs, the X register points to where the sector is to be read from/written to and the Y register contains the absolute sector to read/write. In addition, location $EB should b set to the drive number required.
The absolute sector is calculated as follows:
Absolute sector = sectors per track * track no. + sector no. - 1
Therefore to write track 2 sector 1:
Abs sector = 18 * 2 + 1 - 1 = 36
so the assembler code is:
BUFFER RMB 256 WRTSCT LDA #1 - drive 1 STA $EB LDX #BUFFER - sector to write LDY #36 - abs sector no. JSR [$C028] - write the sector BNE ERROR RTS ERROR JMP $8344 - BASIC error handler :
and it's here that the problem emerges. By assuming you are using a single sided disk and putting 18 sectors per track into the formula will mess up anyone attempting to use a double sided disk, since for these disks:
Abs sector = 36 * 2 + 1 - 1 = 72
The other problem arises if you use this call immediately after you have put a different format disk in the drive or directly after you have switched the Dragon on, because the machine doesn't know what format the new disk is.
The answer to both these problems is to tell the system what the disk is, then use the data held by DOS to work out what the disk type is.
The best way to tell DOS what the disk is, is simply to do a DIR on the new disk. This will read the information off the disk into it's workspace. You can then refer to this when calculating how many sectors per track are in use:
This routine writes the BUFFER to the DRIVE, TRACK, SECTOR specified in memory. It uses the Drive Descriptor table which consists of 25 bytes per drive to fetch the number of sectors per track.
DRVTBL EQU $6A7 - drive descriptor for drv 1 DRVSIZE EQU 25 - no. of bytes in table BUFFER RMB 256 - buffer to write SECTOR RMB 1 - sector no. to write TRACK RMB 1 - track no. to write DRIVE RMB 1 - drive no. to write WRTTRK LDA DRIVE - drive no required STA $EB DECA - set drive range from 0-3 LDX #DRVTBL - point to drive table LDB #DRVSIZE - setup for multiply MUL - do (DRIVE-1) * 25 to select which drive table LEAX D,X - point to the physical table in memory LDA ,X - read the sectors/track LDB TRACK - load the track number MUL - do sectors per track * TRACK ADDD SECTOR - add the sector number SUBD #1 - and subtract 1 TFR D,Y - Y register ready for call LDX #BUFFER - point to buffer JSR [$C028] - write the sector BNE ERROR RTS
A similar routine can be used for reading a sector, simply changing the call to [$C026].
The lowest level interface DOS provides to the disk controller is through the Disk Operation Processor call. This provides all the track/ sector read/write calls and formatting calls. In most instances there are ways around using this call, reading & writing sectors for instance can be handled a lot easier via the Read & Write absolute sector calls detailed last time.
The disk operation processor indirect jump table interface is:
[$C004] - Basic disk operation processor [$C006] - operation block address
The operation block address [$C006] points to where the data is held for the [$C004] call. In almost all instances it points to location $00EA, and you can probably get away with assuming that it does. The structure of this block is as follows:
$EA - Basic disk operation number $EB - current drive number $EC - track number $ED - sector number $EE:EF - disk buffer address $F0 - status $F1 - current File Control Block Number
The majority of this data is self explanatory. In order to use the disk operation processor, you set up this data and call [$C004]. The operation codes to store in location $EA are:
0 - restore to track 0 1 - seek track 2 - read sector 3 - write sector, verify if VERIFY ON 4 - write sector, no verify 5 - write track 6 - read track address 7 - read sector to verify 8 - read track address into locations 18-23
Codes 0-4 are straightforward, the remainder slightly more unusual and I'll cover these only briefly.
The following example demonstrates the usage of this call, in reading track 3, sector 4 into a buffer.
BUFFER RMB 256 - buffer for sector LDA #1 STA $EA - prepare for seek STA $EB - on drive 1 LDA #3 STA $EC - seek for track 3 LDA #4 STA $ED - setup for sector 4 JSR [$C004] - perform the seek BCS ERROR LDA #2 STA $EA - prepare for read LDX #BUFFER STX $EE - point to our buffer JSR [$C004] - perform the read BCS ERROR RTS ERROR JMP $8344
Note that the [$C004] operation sets the carry flag if an error occurs, instead of the zero flag, hence the need for a BCS instead of a BNE instruction on testing for an error. The seek command is used to move to different tracks, therefore there is no need to call seek when reading sectors from the same track.
And thats just about all there is to it. The remaining codes 5-8 are as follows:
5 - Write track. This is a call to format the current track. However, you can't simply call the ROM routine and expect to get a Dragon formatted track. The disk buffer ($EE:EF) should point to the data required to encode a Dragon format track. In addition, some more of the workspace pointed to by [$C006] needs data to be set up. I wrote an article a while back for UPDATE which details using this call to format 1 track of a disk. Suffice to say, the only other use for this call would be in formatting non Dragon disks (more next time!).
6 - Read address of track. This is used as a kind of 'disk status' call. It returns 6 bytes of data into the buffer pointed to by $EE:EF. It's principal use is to find out which track number the disk head is on, in order to prevent unecessary seeks. The 6 byte structure is as follows:
0 - track number 1 - side number (0=side 1, 1=side 2) 2 - sector number* 3 - sector length ( 1=256 bytes/sector) 4 - sector CRC 1* 5 - sector CRC 2*
* The only reliable information you can get from this call is the track number & side number. The sector number returned is whatever sector the disk head happens to be over at the time the operation is called. Bytes 4 & 5 are the sector's checksum values, used for data integrity.
7 - Read sector to verify. This reads the sector, and discards all but the first 2 bytes which it stores into temporary workspace locations 79,80.
8 - Read address of track into locations 18-23. As per call 6, except the 6 bytes are placed into temporary workspace.
After the Disk Command Processor, the next level is talking to the disc controller chip directly. After that, its off into the realms of disc encoding methods which is way beyond the scope of most of us. In fact, there are very few reasons you'd want to talk to the disc chip directly anyway. To do so means that you're seriously into disc formats or that you're more than probably a trifle nuts. If you want to persist, your best bet is to get hold of a WD2797 (disk controller chip) data sheet and spend a few hours one evening having a quiet read. Therefore, in this last but one article I'll briefly touch on how to access the chip through the Dragon and how to go about writing code to do it. The only gain to be made in accessing the disk controller chip directly, is the ability to read in alien disk formats (remember PC disks & BBC Micro disks are not alien, and the standard sector calls can handle these) which the controller cannot access normally. Any disc which is written using standard double or single density methods should be accessable then by the Dragon. However, you need to know a lot about the internal disc structure (see later) to understand the data you'll get back. The only other possible gain, is in performing some sort of specialized disc operation - I wrote an experimental program once to attempt to play samples direct off disc. The disc controller chip itself, is mapped into the IO map, like all the other IO devices, at address $FF40 as follows:
$FF40 - Command/Status register $FF41 - Track register $FF42 - Sector register $FF43 - Data register
in addition, location $FF48 is an external latch chip, which is used to switch on the disc motors, select the drive & select density required on the disc controller. On the face of it then, accessing the disc is straightforward. Simply turn the drive motors on, select the drive (thru $FF48). Then put the track number & sector number required into $FF41, $FF42, put the required command into $FF40 (read, write etc.) and read or write the data from $FF43. When all is done, check the status in $FF40 and take any necessary actions. And don't forget to switch the motors off.
In fact, that is all there is to it, except that there is a major problem. The data comes across so fast, that the Dragon can't keep up with it.
Therefore, a rather strange method of extracting the data has to be used which makes use of both of the two main interfacing methods.
The first method, is that of polled IO. The Dragon uses this a lot, because it's simple and easy to implement. The idea, involves asking each device if it wants some more data, or has got some more data to pass back. Both the keyboard & the printer are polled on the Dragon. Whilst polled IO is simple, it's also slow & time wasting. The processor spends a lot of time asking a device if it's got some data, and 90% of the time it will say no. Like the keyboard, for example where although your typing may be pretty impressive, it's no match for the machine itself. Taking the other extreme, if a device on the system (like the disc controller) is chucking out data so fast, it's highly likely that whilst the processor is off asking your keyboard if it's got some data, that the disc controller has just dumped half a sector down a black hole.
The way around this, is to use interrupt IO. This is where a device tells the processor that it wants attention, by pulsing one of the interrupt lines. The processor can then stop whatever its doing and go off and deal with the interrupt. The 6809 in the Dragon has 3 interrupt lines Interrupt Request (IRQ), Fast Interrupt Request (FIRQ), and Non-Maskable Interrupt (NMI). The Dragon itself, though makes very little use of these interrupts. On a no-frills D32, for example the only interrupt to occur normally is an IRQ 50 times a second to update the TIMER variable.
In an interrupt driven system, when an interrupt occurs, the processor stops what it is doing and looks to see what caused the interrupt (there may be more than 1 device on an interrupt line). When it has located the device requesting attention, the data then be handled, before the processor resumes what it was doing. This is ideally, how the disc system should work. On the BBC for example, an NMI goes off every time a byte is sent/received from the disc controller, whereupon the processor deals with it. Unfortunatly, on the Dragon there is a problem. The act of answering an interrupt involves dumping all the processor registers (A,B,X,Y,U,PC,DP,CC) onto the stack in order that when the interrupt has finished it can pull them all off and carry on with what it was doing. However, pushing registers onto the stack is the most time consuming operation the 6809 can do and by the time it's done this and gone off to service the interrupt, the data (like the last bus) has been and gone and where were you anyway?
So the Dragon, has to use polled/interrupt IO to read & write it's disks. This bears some explaining. The disk controller has 2 interrupt lines connected through the cartridge port, one connects to the NMI line on the processor, the other to the CART line, which connects to a PIA control line. The PIA, when programmed correctly will generate an FIRQ interrupt whenever the CART line is pulsed. Every time a byte is sent to the computer or written to the disk controller, the CART line gets pulsed, which in turn causes an FIRQ. When the current operation is complete, an NMI is generated.
The process works on the Dragon, by masking all interrupts setting up the disk for reading or writing and then going into a tight loop similar to the following:
READLP SYNC LDA >$43 LDB >$22 STA ,X+ BRA READLP
The SYNC instruction simply waits for an interrupt to occur (an FIRQ from the CART line) to indicate a byte has arrived. However, the interrupt will not be acted upon because it has been masked (via an ORCC #$50 command previously). Instead, when the FIRQ occurs, it leaves the SYNC instruction & proceeds to read the byte returned by the disk controller in $FF43. Prior to this loop being called, the direct page register will have been setup to point to the top page $FF to enable these instructions to execute faster. The FIRQ status is then cleared by reading $FF22, and the byte read is stored into a buffer previously setup. The loop then returns to the SYNC instruction ready for the next byte.
When the operation is completed, the disk controller generates an NMI to break out of the loop. By its definition (Non-Maskable) this interrupt is not masked by the ORCC instruction and therefore executes normally. Its operation, is merely to discard what had just been pushed onto the stack, and return whatever called the disk routine.
From this, it is clear how the Dragon can read different length sectors (eg. the 512 byte ones on a PC disk). The machine has no control over how much data is received, the disk controller just transfers however much data it has to send.
Of course, this whole data transfer system is very heavy handed on the machine and is the cause of a lot of agravation. The first of these, is that in order to succeed the only source of interrupts during a disk operation can be the disk controller. Any other device causing an interrupt will break the SYNC instruction and cause the next data byte to be read or written when the controller isn't ready, with the resulting disk corruption to follow. Therefore all interrupts must be switched off, not by masking them (ORCC) but by physically deactivating (on the PIA or ACIA). Whilst the disk system handles those that it expects on the Dragon (like the TIMER interrupt) anything additional you may have added, or the serial chip on the D64 must be disabled by yourself prior to a disk operation.
The problems really show themselves up under operating systems like OS9, where switching off interrupts & masking them is a definite no-no. Everything under OS9 is interrupt driven, even the polling of the keyboard is put into the 50Hz IRQ routine enabling keystrokes to be buffered. This has the advantage, that you can type away whilst the machine is doing something else, and when it has finished, your text is displayed on screen. Of course, with the disk system constantly messing about with interrupts, you typing something like 'CHD /D1' whilst it's scrolling a directory, will probably turn into 'H D1'. It also makes putting things involving disk IO in background a waste of time, since every time the disk gets accessed everything freezes.
However, like it or not (and I don't) we're stuck with it. Providing you access the data in this way, all should go well.
The chip itself excepts 11 commands into it's Command Register ($FF40), most of these tie up with the Disk Command Processor commands and you can use that instead:
1. Restore to track 0
3. Step - this steps the disk head 1 track in the direction it was last stepped.
4. Step in - this steps the disk head 1 track towards track 00.
5. Step out - this steps the disk head 1 track towards track 79 (or 39).
6. Read sector
7. Write sector
8. Read address off track
9. Read track (see below)
10. Write track (format)
11. Force interrupt - this code is used to force an NMI interrupt and therefore abort whatever the controller is doing at the time. This also provides a way of resetting the chip.
The values to input into the command register for these commands is documented in the data sheet, and rather than detail them all here, I'll use one of the commands as an example.
On first glance, it would appear that the most useful command not implemented by the Disk Operation Processor [$C004] is the Read Track call. This provides the opposite to the write track command (surprise) and reads back the information written to the disk when it was formmatted. However, there is a good reason for it not being implemented. When formatting the disc (Write Track) certain bytes written to the Data Register have special meanings and tell the chip to perform some special disk operation (synchronize data, special timing etc.) During the Read Track call any data bytes encountered which match these special codes will cause them to be acted upon. This means that valid data can get interpreted as a control code with the resulting data being corrupt. Usually, the data gets shifted by 1 or 2 bits. However, I've included a listing of the code I used to discover this purely for demonstrational purposes as to how to use the disk chip. This call, will not only return all the sectors contained on the specified track, but all the information about these sectors written at format time and not normally accessable.
Assuming we've already SEEKed to the required track (as an example track 16), the format of the read track call is as follows:
binary# 7 6 5 4 3 2 1 0 1 1 1 0 0 E U 0
E = head settling time. If set to 1 the disk head is allowed 30MS to settle down, otherwise not. Since we know no better, setting it to 1 should prevent any problem.
U = Update side select output. If set to 0, output for side 1, or if set to 1, output for side 2. Assume single sided disks for the moment. Therefore READ TRACK = 228.
The following program issues the read track command to the disk controller & reads in whatever track the disk head happens to be over (ie. no seek). The data is stored in the graphics workspace from 3072 onwards, and you should PCLEAR 5 prior to running it.
*DISK READ TRACK DEMO *INTERRUPTS OFF START ORCC #$50 *CLEAR LAST DISK STATUS LDA $FF40 *FORCE INTERRUPT (NOIRQ) LDA #$D0 STA $FF40 *DISK MOTOR TIMEOUT STA $605 *SELECT DRV 0: BITS 0,1:0 *MOTORS ON : BIT 3 :1 *ENABLE NMI : BIT 5 :1 LDA #36 STA $FF48 *COPY OF DRIVE SEL LATCH STA $607 *SWITCH IRQS OFF ON ALL PIAS PIA 0:SIDE A LDA $FF01 PSHS A ANDA #$FC STA $FF01 *PIA 0:SIDE B LDA $FF03 PSHS A ANDA #$FC STA $FF03 *PIA 1:SIDE A LDA $FF21 PSHS A ANDA #$FC STA $FF21 *PIA 1:SIDE B LDA $FF23 PSHS A ANDA #$FC STA $FF23 *ENABLE FIRQ ON PIA 1:SIDE B ORA #$37 STA $FF23 *USE EXISTING NMI ROUTINE *SETUP FOR DIRECT PAGE $FF ADDRESSING LDA #$FF TFR A,DP *READ WHATEVER TRACK WE'RE ON BSR READTRK *NMI PUTS CONTROL BACK HERE *PUT DP BACK CLRA TFR A,DP *RETURN PIA CONTROL REG SETTINGS PULS A STA $FF23 PULS A STA $FF21 PULS A STA $FF03 PULS A STA $FF01 *ALL DONE RTS *BUFFER FOR TRACK DATA READTRK LDX #3072 *O/P READ TRACK CALL, HEAD LOADING DELAY, SIDE 0 LDA #228 STA >$40 *WAIT FOR DRIVE READY NRDY LDB >$23 BMI READ2 BRA NRDY *READ TRACK, NMI EXITS LOOP READ1 SYNC READ2 LDA >$43 LDB >$22 STA ,X+ BRA READ1
The program first masks interrupts, and issues a Force Interrupt command to the disk controller. This effectively resets the chip. It then masks all the interrupts on the Dragon's PIA chips, preserving the original settings on the stack. The drive is then selected, and the motors switched on. Location $605 is used by all DOSs I believe as a counter to switch of the motors by the Dragon's normal IRQ routine so we do not need to switch them off ourselves. Once the PIAs have been reset, FIRQs are enabled on the second PIA which connects to the disk controller (via the CART line). The direct page is setup to the IO area, the subroutine to actually read the track is then called. This issues the command & waits for the drive to send the first byte. Ideally, there should be a timout counter here so that if a problem occurs the machine doesn't wait forever. When the first byte is received, the routine goes to the SYNC loop & reads the track. The systems NMI vector routine will return us to the main program when all the data is read, the PIAs are returned to normal and the program finishes.
If you type PRINT PEEK(&HFF40) after the operation, this will tell you if the data has become corrupt due to a special control byte being read. If it is 0, then the data has read in okay.
What you get back is about 6K of data, which all appears pretty meaningless. There is about 100 bytes of track header data, followed by the first sector. This too has some header information, the track number, side (0=side 1), sector no, sector length (01=256 bytes/sector) plus about 40 more bytes of data before the actual sector. Then there some sector footer data. This lot repeats for each sector (the Dragon also interleaves sectors, so sector won't come after sector 1), and at the end of the last sector is some final padding out data to finish the track off.
I've found one use for this call! I've managed to write samples (yet another sampler utility!) directly to tracks using the Write Track command, and use the Read Track call to get them back so they can be played off disk real time.
One final word of warning. DOS likes to know exactly what is going on disk wise, and that why it keeps copies of the disk controller registers in its own workspace, as an example to avoid seeking to a track when you are already over it. Using your own routines, which will almost certainly not update the DOS variables, can leave the system in a bit of a dodgy state. Therefore, its worth testing your routines on scrap disks (as always) and saving the assembler routines to an unimportant disk. Even when you've got it working, its worth using the routine and then resetting the machine fully.