Skip to main content
© TLR Designs

 "Jesus is coming... Look busy!"

                YOUR SECOND GFA BASIC 3.XX MANUAL
                             - or -
        HOW I LEARNED TO STOP WORRYING AND LOVE GFA-BASIC
                             PART 12
                     CHAPTER ELEVEN - FILES
                          by Han Kempen

Floppy Write Test

 You  are advised to switch the Write Verify test off by  setting
the system-variable _fverify (at address &H444) to 0:

     SPOKE &H444,0       ! test off
     SPOKE &H444,1       ! test on (default)

 According  to  experts like Dave Small and  Bill  Wilkinson  the
Verify test is a complete waste of valuable time if you write  to
a disk.

Step Rate

 You will find the current step-rate of drive A with:

     PRINT DPEEK(&H440)            ! system-variable seekrate

 The following values are possible:

     0 -  6 ms
     1 - 12 ms
     2 -  2 ms
     3 -  3 ms (default)

 For an external 5.25"-drive you probably have to use 6 or 12 ms.
From TOS 1.4 onwards you can use XBIOS 41:

     PRINT XBIOS(41,drive,-1)      ! current step-rate (0=A, 1=B)
     ~XBIOS(41,drive,rate)         ! new step-rate

RAM-disk

 Drive D is often reserved for a RAM-disk.  GFA will recognize  a
RAM-disk with DFREE(4) only if it was already present at the time
the interpreter was loaded.

 After  switching  off your 1040 ST you should wait at  least  15
seconds before switching on again.  Otherwise an old RAM-disk (or
something else in RAM, e.g. a virus...) may still be present when
you switch your computer on again.

 If  a  RAM-disk is not installed properly  after  a  reset,  the
reason  could be hidden in the drive bit-table at &H4C2  (system-
variable _drvbits).  The old TOS does not clear this table and  a
RAM-disk can only be installed as drive D if bit 3 is cleared. By
the way, use drive C (bit 2) only for a harddisk.

DFREE

 DFREE(0)  could give the wrong number of bytes for  the  current
drive if you use a cache, but this method always works:

     PRINT DFREE(drive)                      ! 1 - 15
     PRINT DFREE(ASC(drive$)-64)             ! "A" - "O"

 You  probably missed drive P here.  According to  my  GFA-manual
DFREE can't be used for drive P, but I have not been able to test
this.

DIR$()

 Use  GEMDOS 25 (Dgetdrv) to find the current drive  and  combine
this with DIR$ to find the current path:

     path$=CHR$(GEMDOS(25)+65)+":"+DIR$(GEMDOS(25)+1)+"\"

 With  DIR$(0)  you'll  find the last used path  of  the  current
drive.  DIR$(1) returns the path of drive A,  DIR$(2) the path of
drive B,  etc. GEMDOS kindly remembers the last used path for all
available drives.

 If  you  run GFA-Basic from the main directory and load  a  GFA-
program from a folder,  DIR$(0) will return the nullstring  (""),
not  the  folder.  After using CHDIR with  the  folder-name,  the
correct path will be returned.  I use CHDIR in the  Shell-program
START.GFA,  so the Standard Global default.path$ will contain the
correct  path of the GFA-program.  This makes life easier if  you
want to load data-files from the same folder,  but don't know the
precise path when you write the program.  It would be nice if you
could  determine  the  path of the  running  GFA-program  in  the
program itself.

 You can make an array-table of available drives with the aid  of
BIOS 10 (Drvmap) and system-variable _nflops (&H4A6):

     DIM drive!(15)
     SELECT DPEEK(&H4A6)
     CASE 1
       drive!(0)=TRUE
     CASE 2
       drive!(0)=TRUE
       drive!(1)=TRUE
     ENDSELECT
     table%=BIOS(10)
     FOR n=2 TO 15
       IF BTST(table%,n)
         drive!(n)=TRUE
       ENDIF
     NEXT n

 You can check if a harddisk is connected with:

     IF LPEEK(&H472)<>0       ! system-variable hdv_bpb
       harddisk!=TRUE
     ENDIF

 My harddisk-driver sets the system-variable hdv_bpb to 0,  so  I
can't use this method myself. Is there a better way to check if a
harddisk is available?

DIR and FILES

 After DIR,  FILES,  TRON or DUMP you can slow down the scrolling
with  <CapsLock>.  You  can  temporarily stop  the  scrolling  by
holding down the right <Shift>-key.

 The output after DIR fits on any screen, but the FILES-output is
too  wide  for Low resolution.  DIR will show only files  in  the
current directory.  FILES will also show folders (marked with *).
With  DIR (and FILESELECT) you will not be able to  see  "hidden"
files or "system" files,  but FILES will show all files.  You can
also  search  for  hidden and/or system files  with  FSFIRST  and
FSNEXT  by setting bit 1 and/or bit 2 of the attribute-byte  (see
paragraph 'FSFIRST and FSNEXT').

 Perhaps you have noticed that after the command FILES the  first
two lines are peculiar if you happen to be in a folder. The first
"name" in a folder is always '.' (one dot) and the second  always
'..' (two dots). Time and date are incorrect, because the authors
of  (the old) TOS forgot to convert these to  MS-DOS  format.  In
case of nested folders,  the operating system finds the preceding
folder through a pointer of the '..'-file. That's why you can use
'CHDIR ..' to return to the preceding folder.  Don't try this  in
the main directory,  or you'll get an error. Talking about CHDIR,
never  use  'CHDIR  ""'  in  a  compiled  program,   because  the
nullstring will cause a crash.

 You can direct the FILES-output to a file with:

     FILES path$ TO file$

 The  directory can then be loaded with RECALL.  You'll  probably
want to manipulate the format of the FILES-output:

     offset

       0       - "*" if a folder, otherwise a space
       1       - filename (with extension)
      13       - a space
      14       - file-length
      22       - a space
      23       - time as "hh:mm:ss"
      31       - a space
      32       - date as "dd.mm.yyyy"

FSFIRST and FSNEXT

 The  DTA-buffer is usually found at address  BASEPAGE+128  (it's
always  there  after start-up and after DIR or  FILES),  but  you
should  not  count  on it.  Use FGETDTA()  to  find  the  current
address, before FSFIRST:

     dta.adr%=FGETDTA()            ! current address of
                                        DTA-buffer
     e%=FSFIRST(format$,attr)      ! search for first file/folder

 FSFIRST returns -33 if no file has been found.  FSNEXT returns -
49 if no more files are found.

 In  an  accessory  it's safer to create a  new  DTA-buffer  (the
desktop will appreciate all this extra work):

     old.dta%=FGETDTA()            ! old buffer
     dta$=STRING$(44,0)
     dta.adr%=V:dta$
     ~FSETDTA(dta.adr%)            ! new buffer
     (...)                         ! use FSFIRST/FSNEXT
     ~FSETDTA(old.dta%)            ! restore old buffer

 The 44-byte DTA-buffer (Data Transfer Address) contains  several
file-data  after a succesful FSFIRST or FSNEXT.  You can  extract
filename,  filelength,  file-attributes,  time and date from  the
buffer:

 fname$=CHAR{dta.adr%+30}
 flen%=LONG{dta.adr%+26}
 attr|=BYTE{dta.adr%+21}
 time%=CARD{dta.adr%+22}
 ftime$=RIGHT$("0"+STR$(SHR(ftime%,11)),2)+":"
 ftime$=ftime$+RIGHT$("0"+STR$(SHR(ftime%,5) AND &X111111),2)+":"
 ftime$=ftime$+RIGHT$("0"+STR$((ftime% AND &X11111)*2),2)
 fdate%=CARD{dta.adr%+24}
 fdate$=RIGHT$("0"+STR$(fdate% AND &X11111),2)+"."
 fdate$=fdate$+RIGHT$("0"+STR$(SHR(fdate%,5) AND &X1111),2)+"."
 fdate$=fdate$+STR$(SHR(fdate%,9)+1980)

 The DTA-buffer is not an exact copy of the relevant  information
in the directory of the disk.  With a disk-editor you can find  a
slot of 32 bytes for each file or folder in the directory of  the
disk:

     offset

      0- 7     - file- or folder-name (without extension)
      8-10     - extension
        11     - attribute-byte
     12-21     - reserved
     22-23     - time
     24-25     - date
     26-27     - FAT-pointer
     28-31     - file-length

 The  first  byte of the filename has a special  meaning  in  the
following cases:

     &H00      - free slot, never used before
     &HE5      - erased file, now free slot
     &H2E      - subdirectory

 Both  time and date are stored in MS-DOS  format.  Consult  your
GFA-manual for more information.  The FAT-pointer, also in an MS-
DOS format (Intel-format: first low byte, then high byte), points
to the first cluster of the file. If you are looking at a folder,
the  FAT-pointer  points to the cluster where you will  find  the
directory of this folder (subdirectory).  If you are looking at a
subdirectory (i.e.  you are in a folder), the first two slots are
reserved for the files '.' and '..' (&H2E and &H2E2E).  This  has
already been mentioned in the paragraph 'DIR and FILES'. Finally,
the file-length is stored in,  you guessed it, MS-DOS format. You
might  wonder what MS-DOS has to do with  Atari-disks.  Read  the
paragraphs  'Disk  Format' and 'File Allocation  Table'  for  the
explanation.

 Use   the   attribute-byte  &X0  (i.e.   no   bits   set)   with
FSFIRST/FSNEXT to find ordinary files only.

 If  you use the attribute-byte &X10000 with  FSFIRST/FSNEXT  you
will find both folders and files!  If the folders in a  directory
don't have an extension and all files do have an  extension,  you
could find all folders in the main directory as follows:

     e%=FSFIRST("\*",&X10000)      ! and e%=FSNEXT() for next
                                         folders

 If your folders do have an extension,  you can't use this simple
method.  You'll have to check after each FSFIRST/FSNEXT if it's a
folder or a file:

     IF BTST(BYTE{dta.adr%+21},4)
       (...)                       ! yes, it's a folder
     ENDIF

 Use attribute-byte &X1000 to find the disk-name:

     dta.adr%=FGETDTA()
     e%=FSFIRST("\*.*",&X1000)     ! finds disk-name only, not
                                         files
     disk.name$=CHAR{dta.adr%+30}

 You  can read the attributes of a file or folder with GEMDOS  67
(Fattrib):

     attr%=GEMDOS(67,L:V:filename$,0,0)

 If  the file (or folder) is not found,  attr% is -33  (or  -34),
otherwise attr% contains the attributes in the usual format.  You
can  even change the attributes of files (not of folders  or  the
diskname) with:

     r%=GEMDOS(67,L:V:filename$,1,attribute%)

EXIST

 You  can use EXIST to test if a folder exists,  but only if  the
folder contains at least one file:

     IF EXIST("\FOLDER\*.*")
       (...)                  ! folder found
     ELSE
       (...)                  ! folder not found or empty folder
     ENDIF

 If  you  want to include empty folders as well,  you  could  use
FSFIRST:

     e%=FSFIRST("\FOLDER",16)
     IF e%=-33
       (...)                  ! folder not found
     ELSE
       (...)                  ! folder found (could be empty)
     ENDIF

 You can use EXIST also to find the length of a file:

     IF EXIST(file$)
       file.length%=LONG{FGETDTA()+26}       ! read DTA-buffer
     ENDIF

LOF

 The length of a file is easily determined with LOF:

     OPEN "I",#1,file$
     file.length%=LOF(#1)
     CLOSE #1

 But  you probably are going to use EXIST anyway to test  if  the
file exists,  so you might as well use the method decribed in the
EXIST-paragraph.

 To  determine the number of records in a random file  you  could
divide the file-length by the total FIELD-length.

TOUCH

 Use this method with TOUCH:

     OPEN "U",#1,file$
     TOUCH #1
     CLOSE #1

NAME

 With the old TOS you can only change the name of files,  not  of
folders.  Even  from the desktop you can't change the name  of  a
folder, so choose it carefully.

KILL

 KILLing a file does not erase it from the disk.  The first  byte
of   the   filename  in  the  directory  is  changed   to   &HE5.
Unfortunately you can't restore a killed file by simply  changing
this byte with a disk-editor.  The operating system will be  able
to find the first cluster of the restored file, because the first
FAT-pointer  is located in the directory.  The next clusters  can
only be found through the FAT (File Allocation Table),  but after
KILL  all pointers to this file are irreversibly erased.  If  you
have  not killed any file on the disk before your fatal  mistake,
you  are  extremely lucky and will find all  clusters  have  been
stored  consecutively.  But after some killing and saving on  the
disk,  the  file could be dispersed over the  entire  disk.  Some
programs  are able to help you,  but you will have  to  recognize
clusters as belonging to the killed file. That's easy with ASCII-
files, but almost impossible with other files.

File Copy

 You  can copy a file source$ to dest$ (use complete  pathnames!)
with:

     OPEN "I",#90,source$
     OPEN "O",#91,dest$
     block%=LOF(#90)
     WHILE block%>31744               ! 31 clusters of 1024 bytes
       PRINT #91,INPUT$(31744,#90);
       SUB block%,31744
     WEND
     PRINT #91,INPUT$(block%,#90);
     CLOSE #90
     CLOSE #91

 There  are slower methods to copy files,  the desktop-copy  (TOS
1.0) being a good example.

 Do  not  copy a file "to itself" on a harddisk.  Thanks  to  yet
another  bug in TOS,  this action could completely wipe  out  the
harddisk.  Or perhaps this should be called a feature of TOS, put
there  to  punish  the crazy user who tries to  copy  a  file  to
itself.

Disk Format

 A disk contains 80 concentric tracks (numbered 0 - 79) or  more.
Sometimes  the expression "cylinder" is used instead of  "track".
Each track is divided into 9,  10 or even 11 sectors.  One sector
can  contain 512 data-bytes.  In order to be compatible with  MS-
DOS,  TOS  formats  a disk with 80 tracks  and  9  sectors/track.
Actually it's easy to fit 10 sectors in one track.  With a little
more effort you can create room for 11 sectors,  but some  drives
run slightly too fast and are not able to read the 11th sector!

 With  a  disk-editor  you can examine the 512  data-bytes  of  a
sector, but you can't examine the sector-layout without accessing
the Floppy Disk Controller (FDC) directly. In that case you would
find the following layout for each sector:

     data-separator (GAP)     - 15 bytes
     ID-Address mark          - 1 byte
     sector-header            - 4 bytes (track, side, sector,
                                         size)
     CRC of sector header     - 2 bytes
     data separator           - 37 bytes
     Data-Address Mark        - 1 byte
     data bytes               - 512 bytes
     CRC of data bytes        - 2 bytes
     data separator           - 40 bytes

 The  data  separator  bytes are there  to  synchronize  the  FDC
properly.  The FDC recognizes the sector-header by the  preceding
ID-Address  mark.  The sector-header itself contains  information
about the current track,  side and sector and also about the size
of the data-field (usually 512 bytes).
 The  FDC  checks both the sector-header and the  data-field  for
corrupted  bytes  by  comparing a computed  "checksum"  with  the
stored CRC-value. The operating system cannot read/write one byte
from/to a sector, only complete sectors are read or written. GFA-
Basic takes care of all the dirty work.

 First some bad news. A CRC-error is not always recognized by the
ROM of 520 ST's and 1040 ST's (bug in XBIOS 8,  Floprd).  If your
palms are now getting sweatty, you could check your most precious
disks  with XBIOS 19 (Flopver).  This function checks  for  'lost
data, RNF- or CRC-errors'. Create a buffer of 1024 bytes and call
XBIOS 19. A sector (512 bytes) is loaded from the disk in drive A
or B (0 or 1) into the second part of the buffer and checked.  If
a bad sector is found,  the sector-number is stored as a word  in
the first part of the buffer.  After checking all sectors in  one
track you have to examine the word-list in the buffer.  Hope  you
will find only &H0000 there.  I leave the writing of this program
as  an  exercise to the reader.  Never thought I would  use  that
phrase myself. Allright, here's something to get you started:

     buffer$=STRING$(1024,0)
     adr%=V:buffer$
     r%=XBIOS(19,L:adr%,L:0,drive,1,track,side,9)  ! if 9 sect/tr

 You should now be able to find out if the track on this side  (0
or 1) is OK. Good luck.

 You  can use BIOS 7 (Getbpb) to examine the disk-format  in  the
so-called BIOS-Parameter-Block (BPB) of the disk:

     bpb.adr%=BIOS(7,drive)   ! address of BPB, or 0 (= error)

 In 9 words you'll find the following information in the BPB:

     offset

        0      - bytes/sector (usually 512)
        2      - sectors/cluster (usually 2)
        4      - bytes/cluster
        6      - number of directory-sectors
        8      - length of FAT
       10      - first sector of second FAT
       12      - first data-sector
       14      - total clusters
       16      - flag (not used)

 Use  GEMDOS  54 (Dfree) to find out how many free  clusters  are
available,  or simply use DFREE if you want to know how many free
bytes are available on a drive.  Due to a bug,  GEMDOS misses the
last  two clusters on a disk.  You can't write to these  clusters
(2048 bytes down the drain...),  but you can read these  clusters
if they do contain data.  That would be a miracle,  or an  MS-DOS
disk.

 You'll  probably  use XBIOS 10 (Flopfmt) to format a  disk  from
GFA-Basic:

r%=XBIOS(10,L:buffer%,L:0,0,spt,track,side,1,L:&H87654321,&HE5E5)

 If  you  do,  use &H0000 as the virgin-value for the  first  two
tracks (18 sectors),  and then &HE5E5 for all  data-sectors.  You
can use either 9 or 10 sectors/track (spt), not 11. If you use 10
spt,  you  could fill the first two tracks with &H0000  and  fill
sectors  19  and  20  with  &HE5E5  afterwards  (read   paragraph
'Sectors'). The buffer should be (at least) 8192 bytes for 9 spt,
probably  9216 bytes for 10 spt.  I don't recommend more than  80
tracks,  certainly not more than 82 tracks. If XBIOS 10 returns a
value other than 0, you'll find a word-list of bad sectors in the
buffer  you used (terminated with &H00).  If one of the first  18
sectors is bad, you can throw the disk away.

 Using XBIOS 10,  the interleave-factor should be 1.  This  means
the sectors on a track are numbered  consecutively:  1,2,3,4,etc.
The  FDC  needs  more  time  to read  a  complete  track  if  the
interleave-factor is not 1.

 TOS loses time if the head moves to the next track.  Because the
Seek with Verify flag is set,  the FDC first verifies the  track-
number  and  then reads the  sector-number.  While  checking  the
track-number,  sector 1 was passing,  so we have to wait for  one
complete spin of the disk (at 300 rpm that's 200 ms,  yawn) until
sector  1  can be read.  One solution is to clear the  Seek  with
Verify  flag,  but that could lead to nasty problems if the  head
still  rattles slightly at the time sector 1 is  read.  The  best
solution is the Twisted format (adopted by Atari from the Mega ST
onwards).  For  a single-sided disk with 9 sectors on  one  track
this means:

     track 0 : sector 1,2,3,4,5,6,7,8,9
     track 1 : sector 8,9,1,2,3,4,5,6,7
     track 2 : sector 6,7,8,9,1,2,3,4,5
     etc.

 Now, sector 1 is encountered almost immediately after the track-
number is verified.  A 1-sector offset is not possible,  but a 2-
sector  offset  is  enough to settle the  rattling  head.  It  is
impossible  to read data faster from a disk!  But I'm afraid  you
can't format a Twisted disk with XBIOS 10 if you have an old TOS.
You  have to use a special format-program.  From the  Blitter-TOS
you  can use XBIOS 10:  if interleave is -1 (instead of  1),  the
third  parameter is used as a pointer to a word-table of  sector-
numbers.

 If you format a disk from the desktop,  bad clusters are flagged
with a special value in the FAT. However, if TOS 1.4 encounters a
bad sector,  something goes wrong and the FAT is  corrupted.  One
more reason to quit smoking,  because smoke-particles  definitely
constitute a serious hazard to the health of your disks.

 You  can  use XBIOS 18 (Protobt) to create a bootsector  on  the
formatted  disk.  Don't worry about the  media-byte  (disk-type),
because TOS doesn't use it. Do use &H1000000 to generate a random
serial number,  because it is very important that different disks
have  different serial numbers!  TOS assumes you are  creating  a
standard  disk,  so you should change the number of  sectors  per
track  (SPT:  default 9) and the total number of sectors  on  the
disk  (NSECTS:  default 9*80 or 9*80*2) in the  sector-buffer  if
necessary:

     POKE buffer%+19,nsects AND &HFF
     POKE buffer%+20,nsects DIV 256
     POKE buffer%+24,spt

 Read the paragraph 'Bootsector' for more information.  Write the
bootsector  to the disk with XBIOS 9 (Flopwr),  not with  BIOS  4
(Rwabs):

     r%=XBIOS(9,L:buffer%,L:0,drive,1,0,0,spt)

 Why are different serial numbers so important? If TOS suspects a
disk-swap,  the serial number is read from the disk.  A disk-swap
can only be recognized if the number on the new disk is different
from  the old number.  If the new disk contains the  same  serial
number,  TOS  uses  the FAT of the previous disk.  Writing  to  a
swapped disk will probably zap it, if you follow me. Disk-copiers
copy everything, including serial numbers. Be careful!

 Although it is possible to format a disk from GFA-Basic, I don't
recommend it (now he tells us...). My favourite format is:

     80 tracks
     10 sectors/track
     Twisted format

 My  double-sided disks contain 807936 bytes.  I use  TWISTER.PRG
(not in the Public Domain, as far as I know), but you could use a
Public  Domain  program  like  DCOPY  (actually  Shareware).  The
desktop  can't  copy a Twisted disk (or  any  other  non-standard
format),  but  file-copy  is always possible.  DCOPY  copies  any
format, including Twisted format.

File Allocation Table (FAT)

 The  first sector on a disk is the boot-sector.  The  next  five
sectors are reserved for the FAT. And the next five for a copy of
the FAT (actually it's the other way around).  Five sectors for a
FAT really is too much, three sectors is sufficient. Finally, the
main  (or root) directory occupies the next 7 sectors  (32  bytes
for  each slot,  16 entries/sector).  Seven sectors for the  main
directory (112 files and/or folders) is quite a lot, five sectors
(80 files/folders) should be enough in practice.  This means that
the first 18 sectors (No.  0-17) of a standard disk are  reserved
for  the  operating  system,  but you could  reduce  this  to  12
sectors. All other sectors are available for storing files.

 The storage-unit for files is a cluster.  A cluster consists  of
two consecutive sectors (1024 bytes). A file that contains only 1
byte  will therefore still occupy 1024 bytes (1 K) on  the  disk.
When  a file is saved,  the operating system looks for the  first
empty cluster,  then the next,  etc.  Information about available
clusters is stored in the FAT as a collection of pointers. Due to
inefficient programming of TOS 1.0,  the search for free clusters
takes  a  long time.  Try DFREE on a harddisk and  you'll  agree.
Install  a program like FATSPEED.PRG (Public  Domain,  by  Ulrich
Kuebler) to speed this up!

 The  first three bytes of the FAT are not used by TOS,  but  are
there to enable MS-DOS to read an ST-formatted disk.  TOS  writes
&HF7FFFF,  where  the  first byte (&HF7) is supposed  to  be  the
media-byte.  Unfortunately  MS-DOS  doesn't understand  this  and
refuses to read the directory properly.  I'm not quite sure  why,
but changing the first three bytes to &H000000 seems to work. You
could  try &HF8FFFF (80 tracks,  9  sectors/track,  single  sided
disk) or &HF9FFFF (double sided disk) instead.  Or you could  use
the  (correct!) media-byte at offset 21 from the  bootsector.  Of
course  the disk really should have 9  sectors/track,  or  MS-DOS
could get confused. To make your MS-DOS friends completely happy,
you  should  change the first three bytes of the  bootsector  to:
&HEB3890.  Sometimes  (from MS-DOS 4.0 ?) you have to change  the
next three bytes as well:  &H49424D (that's IBM in ASCII;  please
watch your language).  Better still,  let them use their own disk
editor  on their MS-DOS computer  (e.g.  Norton  Utilities).  Why
should you do all the work? By the way, your ST should be able to
use an MS-DOS disk without any modifications.

 Each  FAT-pointer consists of one and a halve byte  (3  nibbles,
i.e.  12 bits).  In hexadecimal notation this means three  digits
for one pointer.  The first pointer (No. 0) points to cluster No.
0, the second to No. 1, etc. Because the first two pointers don't
count,  you  have  to  subtract two to find  the  actual  cluster
(pointer  2 points to cluster No.  0,  etc.).  The first  cluster
starts at sector No.  18 (remember, sectors 0-17 are reserved for
bookkeeping),  so  you could find the first sector of  a  cluster
with:

     (pointer - 2) * 2 + 18

 TOS reads the pointers in a peculiar (MS-DOS) way.  Suppose  the
FAT-sector starts with:  F7 FF FF 03 40 00 FF 0F 00

 Without further explanation, this translates to:

     FAT-pointer 0 and 1 are ignored
     FAT-pointer 2 = &H003 (next cluster on sector 20 + 21)
     FAT-pointer 3 = &H004 (next cluster on sector 22 + 23)
     FAT-pointer 4 = &HFFF (last cluster of this file)
     FAT-pointer 5 = &H000 (free cluster)

 In order to understand this,  you have to consult the  following
table:

     &H000         : cluster still available
     &HFF1 - &HFF7 : bad cluster, never available
     &HFF8 - &HFFF : last cluster of this file (End Of File)
     &H002 - &HFF0 : pointer to next cluster

 Assuming  the  FAT-pointer &H002 at offset 26 in  the  directory
(you'll find &H0200 with a disk editor,  that's Intel format with
low byte first again), you should be able to figure out that this
file  will be found in sectors 18 through 23.  Sectors 24/25  are
empty,  so  this  cluster  is  available  for  a  new  file.  Any
questions?  The  one big advantage of all this  is  compatibility
with  MS-DOS  disks.  From  TOS 1.4 your  ST-disks  can  be  made
completely  compatible,  so you don't even have to change  a  few
bytes.

Sectors

 There are two different methods to assign a number to a  sector.
The  first one is to number the "physical" sectors in each  track
from  1 to 9 (assuming 9 sectors/track).  This way you could  say
the  bootsector is sector 1 on track 0 (on side 0 if the disk  is
double sided).  But GEMDOS doesn't care about tracks or sides, it
counts   "logical"  sectors  from  0  to  719   (80   tracks,   9
sectors/track,  one  sided disk) or from 0 to 1439 (double  sided
disk). According to GEMDOS, the bootsector is on sector 0. And on
a double sided disk,  physical sector 1 on track 0 of side 1 (the
other side) would be sector 9.

 With  BIOS 4 (Rwabs) you can read (and write)  complete  logical
sectors:

     buffer$=SPACE$(512)                     ! 512 bytes for 1
                                                        sector
     buffer%=V:buffer$                       ! address of buffer
     r%=BIOS(4,0,L:buffer%,1,sector%,drive%) ! load the sucker

 You would load the bootsector from the disk in drive A with:

     r%=BIOS(4,0,L:buffer%,1,0,0)

 You can use BIOS 4 not only with floppy disks,  but also with  a
harddisk or a RAM-disk. After loading a sector in the buffer, you
can read one byte or word with:

     b|=BYTE{buffer%+i}       ! i from 0 to 511
     w&=WORD{buffer%+i}

 The latter only if the word starts at an even address. Otherwise
you have to use:

     w&=BYTE{buffer%+i+1}+256*BYTE{buffer%+i}

 If necessary, you can speed this up by using the special integer
commands for addition and multiplication.  How about this  Polish
monster:

     DEFFN word(adr%)=ADD(BYTE{SUCC(adr%)},MUL(256,BYTE{adr%}))
     w&=@word(ADD(buffer%,i))

 If  you use BIOS 4 (Rwabs) to write a sector after formatting  a
disk, you should use '3' as a flag (not '1'):

     r%=BIOS(4,3,L:buffer%,1,sector%,drive%)

 You  can  also  read and write physical  sectors  with  XBIOS  8
(Floprd) and XBIOS 9 (Flopwr). With these commands you could even
read/write all sectors on one track. This is the only way to read
a track,  because reading one complete track is impossible due to
a bug in the FDC.

 If you swap a disk after loading/writing a sector you should  be
careful.  Testing with BIOS 9 (Mediach) you could miss the  disk-
swap.  This could be fatal, because TOS uses the FAT of the other
disk!  I think you could use BIOS 7 (Getbpb) in most  cases,  but
use this if in doubt:

     ' Force media change
     x$=SPACE$(12)
     old.vector%=LPEEK(&H47E)      ! system-variable hdv_mediach
     a%=V:x$
     DPOKE a%,&H2B7C
     LPOKE a%+2,old.vector%
     DPOKE a%+6,&H47E
     DPOKE a%+8,&H7002
     DPOKE a%+10,&H4E75
     SLPOKE &H47E,a%
     ~DFREE(0)                     ! current drive

 To  be  more  precise,  BIOS 7 can't be used  in  the  following
situation:

     - TOS reads a sector
     - you swap disks
     - you use BIOS 7 in your program
     - TOS reads a sector

 Now TOS will assume there has been no disk-swap,  because  there
has been no disk-swap after the last BIOS 7 call!  You definitely
need the decribed method here to force a media change before  TOS
reads a sector.

 By  the track,  TOS also ignores disk-swaps during a  screendump
(HARDCOPY),  or while using DMA (harddisk,  laser  printer).  TOS
detects a disk-swap by monitoring the write-protect state. To see
this,  you  should turn all lights off and then watch the  drive-
light closely.  Closer.  You can turn the lights on again. If the
drive is empty,  TOS gets a write-protect signal and assumes  the
user might have swapped disks.  BIOS 9 should return '1' at  this
point.  TOS  checks if you really did swap disks by  reading  the
serial  number  from  the bootsector and comparing  it  with  the
current number.  Only if these numbers are different, a disk-swap
is recognized by TOS (BIOS 9 should return '2' now). You probably
deduced  that  TOS  will read the bootsector also if  you  use  a
write-protected  disk.  Continuously reading the bootsector is  a
waste  of time,  so TOS waits 1.5 seconds before  looking  again.
Never swap disks within 1.5 seconds after a read/write-operation.
The drive keeps spinning for 2 seconds,  so you can't go wrong if
you wait until the drive-light is off before swapping disks.

Bootsector

 In the following table you'll find the lay-out of a  bootsector.
All words are in Intel-format, except CHKSUM. Standard values are
mentrioned between parentheses.

     offset    length    name

        0        2                 &H6038 = branch to bootroutine
        2        6       FILLER    fill-bytes
        8        3       SERIAL    serial-number of disk
       11        2       BPS       bytes/sector (512)
       13        1       SPC       sectors/cluster (2)
       14        2       RES       reserved sectors (1, Bootsec)
       16        1       NFATS     number of FAT's (2)
       17        2       NDIRS     max. entries in main directory
       19        2       NSECTS    total sectors
       21        1       MEDIA     media-byte (not used by TOS)
       22        2       SPF       sectors/FAT (5)
       24        2       SPT       sectors/track
       26        2       NSIDES    sides (1 or 2; no joke here)
       28        2       NHID      hidden sectors, ignored by TOS
       30        2       EXECFLAG  start of bootcode: flag
       32        2       LDMODE    0=load FNAME; <>0=load sectors
       34        2       SSECT     first sector (LDMODE<>0)
       36        2       SECTCNT   number of sectors
       38        4       LDADDR    load at this RAM-address
       42        4       FATBUF    address of FAT-buffer
       46       11       FNAME     filename nnnnnnnneee, LDMODE=0
       57        1       DUMMY     fill-byte
       58                          boot-routine, could be a virus
      510        2       CHKSUM

 TOS  determines  if the bootsector is executable by  adding  all
bytes.  If this sum (AND &HFFFF) equals &H1234, the bootsector is
executable.  If you use GFA-Basic this probably means you have an
ancient ST with TOS in RAM,  or a boot-virus.  A normal  GFA-disk
has only &H00- or &HE5-bytes from the offset 58.

BLOAD

 BLOAD needs an address,  unless you have used BSAVE in the  same
program   before.   In  that  case  the  BSAVE-address  is   used
automatically by BLOAD if you don't specify a new address.

 BLOAD  (BSAVE) is easier to use than BGET  (BPUT),  because  you
don't have to open the file. But with BLOAD you can only load the
entire file, while BGET allows you to load any part of the file.

INP and OUT

 Both  INP  and  OUT can also be used  with  words  and  (4-byte)
integers:

     a|=INP(#n)          ! byte
     a&=INP&(#n)         ! word
     a%=INP%(#n)         ! integer
     OUT #n,a|
     OUT& #n,a&
     OUT% #n,a%

INPUT and LINE INPUT

 Because GFA uses a 4K-buffer for each opened file (from  version
3.07),  reading  data  from a file with (LINE)  INPUT  goes  much
faster.

STORE and RECALL

 For  very fast loading and saving of string-arrays,  you  should
use RECALL and STORE. You can also store (or recall) a part of an
array as follows:

     STORE #1,txt$(),10       ! store elements 0 through 9
     STORE #1,txt$(),5 TO 10  ! store elements 5 through 10

 The correct syntax is:

     STORE #i,x$()[,n[ TO m]]      ! file must be open
     RECALL #i,x$(),n[ TO m],x%    ! use n=-1 for complete array

 If you STORE a text-array,  GFA puts &H0D0A  (CHR$(13);CHR$(10))
after each element.  This is the same ASCII-format as used by 1st
Word Plus (WP Mode off), Tempus and other editors.

 If  you are going to show a few text-lines on the screen  you'll
probably  use  PRINT.  An alternative would be the use  of  DATA-
lines:

     lines=0
     RESTORE txt.data
     READ line$
     REPEAT                             ! count number of lines
       INC lines
       READ line$
     UNTIL line$="***"
     DIM text$(lines-1)
     RESTORE txt.data
     FOR n=0 TO lines-1
       READ text$
       text$(n)=SPACE$(5)+text$   ! add a left margin of 5 spaces
     NEXT n
     txt.data:
     DATA text
     DATA ***

 If you are going to show more text,  I suggest you use 1st  Word
Plus,  or  any other wordprocessor or text-editor that  can  save
your  text as an ASCII-file.  With 1st Word Plus,  I use a  Ruler
length of 70 and the following Page Layout Form:

     Paper length   66
     TOF margin     19
     Head margin     4
     Foot margin     4
     BOF margin     19
     Lines/page     20

 Enter  the  text  and save as an ASCII-file (turn  WP  Mode  off
before saving). In your GFA-Basic program you could load the text
in a string-array:

     DIM text$(lines)
     OPEN "I",#1,file$
     RECALL #1,text$(),-1,lines%        ! lines% ? lines+1
     CLOSE #1

 I like to use a left and right margin of 5 characters, so that's
why  I use 70 characters/line in my wordprocessor.  If the  text-
array is full,  you won't get an error if the file contains  more
text-lines!

 You  could use a 2-dimensional string-array to store  first  and
last names:

     name$(i,0)=first_name$
     name$(i,1)=last_name$

 Again,  STORE and RECALL are very fast.  But it is now necessary
to use exactly the same dimensions with RECALL that you used with
STORE. If the dimensions don't match, the array will be scrambled
after RECALL:

     OPEN "O",#1,file$
     STORE #1,name$(),-1
     CLOSE #1
     '
     OPEN "I",#1,file$
     RECALL #1,name$(),-1,n%
     CLOSE #1

FILESELECT

 Don't   use  the  underscore  '_'  in  the  path-line   of   the
Fileselector,  because a bug in the old TOS will then cause a few
bombs.  Owners  of a Mega ST or TOS-version 1.4 can use  as  many
underscores as they like.

 Changing drives in the Fileselector (old TOS) is not easy. Click
on  the path-line and press <Esc> to clear the  line.  Enter  the
drive,  e.g.  'D:',  and click on the bar under the path-line  in
order  to read the new directory.  Also click on this  bar  after
changing  disks  (in  this  case you would  press  <Esc>  on  the
desktop).  Selecting drive A is easier:  just clear the path-line
and  click on the bar.  The TOS-code that takes care of all  this
work is also known as the bartender.

 If you have changed the extension in the path-line,  you  should
click just below the bar.  If you click on the bar, the path-line
is overwritten with '*.*' ! That same bartender strikes again.

 The correct syntax for calling the Fileselector is:

     FILESELECT [#title$,]path$,default$,file$

 The title (max.  30 characters,  centered automatically) is used
from  TOS-version  1.4 and ignored  in  older  TOS-versions.  The
default$ usually is the null-string (""), but don't use the null-
string for path$ or the Fileselector will freeze.  Use "\*.*"  as
path$ for all files in the main directory.  Do use the  backslash
in the pathname (e.g.  "A:\*.*").  Due to a bug in GEM, the wrong
drive is sometimes used if you forget the backslash ("A:*.*"). If
the  user has selected a file,  file$ will contain the  path  and
filename.  The file$ will be the null-string if the user selected
<Cancel>.  A  third possibility is easily  overlooked:  the  user
could  have selected <OK> without choosing a file.  In that  case
file$ contains the current path, ending with a backslash.

 As  a programmer,  you should take into account the  possibility
that  a  user  might start your program from  drive  A  or  B,  a
harddisk or a RAM-disk.  I use the Standard Global  default.path$
to  remember  where  the  program  was  started  (paragraph  'The
Standard').  If  the  user  changes the  (default)  path  in  the
Fileselector,  you should note the change and use the new path if
the Fileselector is called again.
 If you have a joystick with Auto-Fire on,  you should switch  it
off. The Fileselector doesn't like Auto-Fire. Neither do I.
 The Fileselector will warn you with a modest 'ping' if it counts
more  than  100 files/folders.  It will show only the  first  100
files/folders. I think 45 files in one folder is really the utter
limit for impatient users like me.  More than 100 is a crime that
should be punished with more than just a little 'ping'.  The main
directory  of drive A can't contain more than 112  files/folders,
because the 7 directory-sectors contain 112 32-byte slots.

 Every  time  you  open  a  folder  (in  the   Fileselector,   or
otherwise),  TOS stores information about the folder in a  table.
After  opening/accessing 40 folders,  TOS will  delete  clusters,
cross-link clusters,  and do other nasty things.  Your disk could
be completely destroyed,  thanks to this bug.  Atari enlarged the
buffer  in  TOS  1.2 and fixed the bug in  TOS  1.4.  Atari  also
distributes  the  program FOLDRxxx.PRG to  extend  the  40-folder
limit with 100 (FOLDR100.PRG) or more. Be careful with SHOW INFO,
it's  easy to exceed the 40-folder limit!  You could recognize  a
disaster by one of the following symptoms:

     - unexpected '0 bytes in 0 items' message in directory
     - folder-names trashed (usually lots of Greek letters)
     - Show Info crashes or shows weird information

 Don't be afraid of a new virus. It's only a TOS-bug. Immediately
reset  your  ST,  try to salvage as many files  as  possible  and
reformat the disk.  If all files are lost,  you will have to  use
your  back-up files.  If you don't have back-up files,  you  have
nothing left but my sympathy.

 If  you use FILESELECT in a compiled program you need  at  least
32500 free bytes for the Fileselector.

                     Procedures (CHAPTER.11)

Accessory                                                ACCESORY
 Activate  accessory by changing .ACX to .ACC,  or deactivate  by
changing .ACC to .ACX:
     @accessory(TRUE)    ! activate one or more accessories
 The  user  is given the opportunity to perform a warm  reset  in
order to install all active accessories.

Attr_read                                                ATR_READ
 Read the attributes of a file:
     @attr_read(file$,ok!,read!,hidden!,system!,label!,folder!,
                    archive!)
 The attributes are only valid if ok!=TRUE.

Attr_write                                               ATR_WRIT
 Write attributes to file:
     @attr_write(file$,read!,hidden!system!,archive!,ok!)
 If ok!=TRUE the attributes of file$ have been changed.

Dir_files                                                DIR_FILE
 Put names of all files,  which are in a certain path and have  a
given extension, in a string-array:
     @dir_files("A:\GAMES\","GFA",directory$(),last&)
 The index of the last file is returned in last&.  Any number  of
files could be accepted, the current limit is 100.

Dir_folders                                              DIR_FOLD
 Same as Procedure Dir_files, but for folders:
     @dir_folders("A:","",directory$(),last&)

Directory & Directory_read & Directory_calc              DIRECTRY
 Put all files and folders in certain path in string-array:
     @directory("A:\",d$(),n)
 Not  only  the names (including path)  are  returned,  but  also
length, date and time:
     PRINT "File","Length","Date","Time"
     PRINT d$(0,0),d$(0,1),d$(0,2),d$(0,3)   ! first entry, idx 0
 You  could find the same data using FILES,  but then  you  would
have to write the directory to a file first.

Disk_format                                                FORMAT
 Format a disk in drive A or B:
     @disk_format("A",2,80,9,112,5,"STANDARD.TOS",free%)
 This  is a standard double-sided disk (2  sides,  80  tracks,  9
sectors/track,  112 directory-entries,  5 sectors/FAT) as created
by TOS from the desktop.  There are free% bytes available on  the
formatted disk.

Disk_ibm & Disk_ibm_error                                DISK_IBM
 Convert disk to IBM-format:
     @disk_ibm("A",ok!)

Disk_info                                                DISKINFO
 Return information about disk (from bootsector):
     @disk_info("A",nsides,tracks,spt,spd,spf,ser%,name$,ex!,
                         ibm!,ok!)

Disk_newname                                             DSK_NAME
 Change the name of a disk in drive A or B:
     @disk_newname("A","GFAXPERT.001")
 That sounds easy, but it's quite difficult.

Disk_number                                               DISKNMB
 Change the serial number of a disk:
     @disk_number("A",ok!)

Drive_speed                                              DRVSPEED
 Check the speed of drive A or B
     @drive_speed("A")        ! should be 300 rpm

Drives                                                     DRIVES
 Makes an array-table of available dirves:
     @drives(driv!())
     IF drive!(1)
       ' External drive B available
     ENDIF

File_arch                                                FILEARCH
 Set or clear archive-bit of file:
     @file_arch(file$,TRUE,ok!)         ! set archive-bit

File_copy                                                FILECOPY
 Copy a file:
     @file_copy(FALSE,"C:\TEST.GFA","A:\")   ! copy TEST.GFA
                                                   from C to A
     @file_copy(FALSE,file$,"C:\BACKUP\LASTFILE.GFA")
 If the flag is TRUE,  the source-file is deleted (= move  file).
If the destination-file already exists,  the user may delete that
file or rename it with a BAK-extension.

File_copy_2                                              FILECOP2
 The same as Procedure File_copy,  but uses another  copy-method.
This  Procedure  appears  to  be faster  with  large  files  than
File_copy. Try it.

File_hide                                                FILEHIDE
 Hide file or make file visible:
     @file_hide(file$,TRUE,ok!)         ! hide file

File_load                                                FILELOAD
 Load a file into a byte-array:
     @file_load(file$,music|(),adr%)
 The  start-address of the data is adr%.  ERASE could  result  in
moving the byte-array in memory,  so adr% would contain the wrong
address!

File_read                                                FILEREAD
 Make file read-only or make it read/write:
     @file_read(file$,TRUE,ok!)         ! make file read-only

Fileselect                                               FILE_SEL
 Use Fileselector with title (any TOS-version):
     @fileselect("Load a file...","\*.*","",file$)

Fileselect_margins                                       FILE_SEM
 A Fileselector with some extras, especially for TOS 1.0 and 1.3:
     @fileselector(TRUE,"Choose a file:","*.*","","LEFT","RIGHT",
                              file$)
 Now you can use a title,  just as with TOS 1.4. And you can even
add an optional title in the left and right margin of the screen.
If the flag is TRUE,  the original screen is restored.  Only  for
High and Medium resolution.

Folder                                                     FOLDER
 Create a folder:
     @folder("A:\",TRUE)           ! in main directory of drive A
 If flag is TRUE,  the folder will become the default folder. The
user enters the folder-name through the fileselector.

Force_mediach                                            FMEDIACH
 Force a media change:
     @force_mediach("A")
 According  to Frank Roos this should work,  although the  method
differs from that described earlier.~

Inarray                                                   INARRAY
 Search a string in an array:
     @inarray("GFA",1,0,text$(),column,line)      ! start at (1,0)
 Similar to INSTR this Procedure finds a string in an array.  The
first  column is 1,  the first line is 0 (index 0 of the  array).
The position of the found string is returned in column& and line&
(both 0 if search failed). If you would like to look further, you
should  call the Procedure again,  with column& and line& as  the
new start-column and -line.

Parse_filename                                           PARSFILE
 Parse a file-name completely:
     @parse_filename(file$,d$,p$,f$,e$)
     PRINT "Drive","Path","File","Extension"
     PRINT d$,p$,f$,e$
     PRINT file$
 In  most  cases  you  can  use  the  Functions  File_name$   and
File_path$ instead.

Path_search                                              PATHSRCH
 Find the path of a file:
     @path_search("CHESS.GFA","A",path$)
 In  case  you forgot where a certain file  is  located.  Returns
complete file-path, e.g.: "A:\GAMES\CHESS.GFA"

Text_array_init                                          TEXT_ARR
 Fill a (short) text-array from DATA-lines:
     @text_array_init(text$())

Text_load                                                TEXTLOAD
 Load a text-file (ASCII-text) into an array:
     @text_load(file$,200,text$(),lines%)    ! maximum 200 text-
                                                  lines
 The  actual number of loaded text-lines is returned  in  lines%.
You'll probably use Procedure Text_show next.

Text_save
                                                TEXTSAVE
 Save a text-array (ASCII-text) as a file:
     @text_save(file$,99,text$())       ! line 0-99

Text_show                                                TEXTSHOW
 Show an ASCII-text on the High or Medium screen:
     @text_show("TITLE",text$())
 You'll  see 20 lines of text on the screen.  The following  keys
can be used:
     <Down Arrow>             - page down
     <Up Arrow>               - page up
     <ClrHome>                - go to first page
     <Shift><ClrHome>         - go to last page
     <Control><F>             - find a string (enter string
                                   first)
     <Control><G>             - continue string-search
     <Esc> or <Control><Q>    - exit Procedure
 By  international  agreement you should use the above  keys  for
these commands.

Write_verify                                             WRVERIFY
 Turn Write Verify on or off:
     @write_verify(FALSE)     ! turn Write Verify off

                     Functions (CHAPTER.11)

Archive_bit                                              ARCH_BIT
 Returns TRUE if archive-bit of file is set:
     PRINT @archive_bit(file$)

Boot_check                                               BOOTCHCK
 Checks  bootsector  and warns user if bootsector  is  executable
(could be a virus):
     IF @boot_check("A")
       (...)                  ! no boot-virus on disk in drive A
     ELSE
       (...)                  ! boot-virus?
     ENDIF
  Needless to say,  this routine will tell you there might  be  a
virus on *any* executable disk.


Disk_free                                                DISKFREE
 Returns free bytes on disk:
     free.bytes%=@disk_free(FALSE,"A")
 If the flag is TRUE, the free bytes are shown in an Alert-box.

Disk_name$                                               DISKNAME
 Returns the disk-name (label):
     PRINT @disk_name$("A")
 You can change this name with Procedure Disk_newname.

Disksector_load                                          SECTLOAD
 Loads one disk-sector in an INLINE-line and returns the  address
of that line:
     PRINT "Sector ";s;" of drive ";d;" loaded at: ";
       @disksector_load(d,s)

Drive_write_protected                                    WRITPRO2
 Should return TRUE if drive is write-protected,  but it  doesn't
do that for me. Let me know what's wrong.

File_length                                              FILE_LEN
 Returns file-length:
     PRINT "File-length: ";@file_length(file$)

File_load                                                FILE_MAL
 The  safest way to load a file,  but can be done only once in  a
program:
     adr%=@file_load(file$)
     (...)
     ~MFREE(adr%)        ! do restore memory before leaving the
                                         program
     RESERVE

File_name$                                               FILENAME
 Returns the file-name (without path):
     PRINT @file_name$("A:\GAMES\CHESS.GFA")
 Should return "CHESS.GFA" in this case.

File_path$                                               FILEPATH
 Returns path (without file-name):
     PRINT @file_path$("A:\GAMES\CHESS.GFA")
 Should return "A:\GAMES\" in this case.

Step_rate                                                STEPRATE
Returns the step-rate of drive A:
     PRINT @step_rate

Track_check                                              TRCKCHCK
 Returns TRUE if all sectors on a track are good:
     IF @track_check(0,0,0)   ! drive 0 (A), side 0, track 0
       PRINT "Congratulations"
     ELSE
       PRINT "Where is your back-up disk?"
     ENDIF

Write_protected                                           WRITPROT
 Returns TRUE if disk is write-protected:
     IF @write_protected("A")
       PRINT "Write-protected"
     ELSE
       PRINT "Not write-protected"
     ENDIF
 Should  work on a normal ST and a Mega ST,  but uses an  illegal
PEEK   to  determine  the  write-protect  state.   The   Function
Drive_write_protected doesn't look very legal either (and doesn't
work on my ST), but at least Write_protected works on my ST. 

Disclaimer
The text of the articles is identical to the originals like they appeared in old ST NEWS issues. Please take into consideration that the author(s) was (were) a lot younger and less responsible back then. So bad jokes, bad English, youthful arrogance, insults, bravura, over-crediting and tastelessness should be taken with at least a grain of salt. Any contact and/or payment information, as well as deadlines/release dates of any kind should be regarded as outdated. Due to the fact that these pages are not actually contained in an Atari executable here, references to scroll texts, featured demo screens and hidden articles may also be irrelevant.