FAT basic routines
Introduction
There is a set of routines that help to read from a FAT volume. Each function deals only with one aspect of accessing a FAT volume. It has grown over the time and it has undergone many changes. At the moment these are read-only routines, that is there is no support to create new files or to expand existing files.
The functions are written to support FAT-16 and FAT-32 volumes at the same time. At the moment we only support one mounted volume. This will most likely change in order to support multiple volumes.
Note that I tried to impose some register conventions. E.g. the temp
register
as well as register r0
and r1
are never preserved. The Carry
bit should
always be cleared on success and set on error. If required the error code is
returned in the register named char
Later I have also started to never preserve registers r4
, r5
, r6
, r7
as
they are often used as a temporary 32-bit integer. All other registers must
always be preserved.
Sometimes we will also use the EQ
flag to return a condition in case we just
need to know if something happend or not and there is no possibility for an
error. I.e. when we follow linked cluster we either have a new cluster (NE)
or we have reached then end of the cluster list (EQ)
Open Directory
It is not very surprising that you first need to be able to read the directory
of a FAT volume. OpenDir
initialises all fields in directory data structure.
As input the function requires a pointer to a directory data cluster with the
P_Cluster offset set to the first cluster of the directory. In fact directories
are just files on a FAT volume and the disk space allocated to a directory is
managed exactly the same way as for normal files. The only exception is the root
directory of a FAT-12/16 volume. This is a fixed junk of space allocated in the
partition and is also the reason why there is only a limited number of files
you can maintain in the root directory. FAT-32 is very clever, the only thing
that is stored in the volume control block is the start cluster of the root
directory, this allows to expand the root directory of a FAT-32 volume as long
as there is enough space.
You must call OpenDir
before you can sequentially read a directory. Setting
the start cluster to zero
instructs the function to open the root directory
of the volume, this works for FAT-16 and FAT-32 volumes the same way.
;--------------------------------------------------------------------------
;
; Open Directory
;
; Input:
; Y pointer to datastrcucture with P_Cluster set to the first cluster
; of the sub-directory or 0 to read the root directory
;
; Registers:
; r0, temp
;
; Completioncode:
; CS
; CC
;
Read Directory
Once a directory has been opened successfully the control block is initialised
in a way that we can now call ReadDir
to sequentially read each directory entry.
Each call of ReadDir
will update the control block to point to the next entry.
If no more entries are found then the carry will be set. Note that directory
entries have one of three possible status. Initially all directory entries
are marked as free. When you create a file then the directory entry is active
and when you later delete the file the directory entry is marked as deleted.
Directory entries are never freed. ReadDir will just skip deleted entries and
will stop at either the end of the directory or at the first entry still marked free.
;--------------------------------------------------------------------------
;
; Read Directory Entry and returns a pointer either to the next active
; or free directory entry if one exists. If no free directory entry
; exists the pointer is set to zero. To read a directory first call
; OpenDir with the P_Cluster set to the first cluster of the directory
; file (or 0 in case you want to read the root directory). OpenDir and
; ReadDir automatically handle the special case of FAT16 root directory.
;
; Input:
; Y Pointer to the data structure for directory
; P_LongFileN must be set to a buffer that will hold the
; long filename
; Output:
; P_Entry copy of the primary directory entry
;
; Completioncode:
; CC Active Entry found
; CS Free or no entry found
;
; Registers:
; r0, temp
;
Match File Name
One option after calling ReadDir
is to match the file name of the directory entry
against a given name.
;--------------------------------------------------------------------------
;
; Match filename
; Y Pointer to datastructure setup to read a directory
; entry (e.g. after calling ReadDir)
; NameBuffer null terminate filename
;
; Completioncode:
; CC entry matches filename
; CS entry does not match
;
; Registers:
; temp
;
;
; Alternate entry:
;
; This entry will copy back the found real filename to the buffer which
; pointer is stored at P_DirName of the datastructure. The name is not
; zero terminated, so in case you need a zero terminated string you must
; clear the buffer in advance. This feature is normally used by the
; Name2DirEntry function to update inline the given path with the real
; filenames. This is because filenames are case insensitive but sometimes
; we want to give feedback to the user with the as it is stored in the
; directory with the correct case. One example is the "pwd" print working
; directory command.
;
Copy Name Partially
When finding a file based on the path we need to step through all elements
of the file path. Copy Name will copy a filename or directoryname from the
string pointed to by the X register to the global buffer called NameBuffer
.
;--------------------------------------------------------------------------
;
; CopyName
;
; Copy file/directory name from buffer at X to NameBuffer. The buffer at
; X is assumed to contain a combined path to a file or a directory with
; file/directory name separators. CopyName will copy up to the next
; separator, null or carriage return, whichever occurs first. This
; function is supposed to be called successively to scan and process
; a path.
;
; Input:
; X Pointer to string
;
; Output:
; X Pointer after terminating character
; NameBuffer Name Element, null terminated string
; Char Terminating Character
;
; Conditions:
; CS Result has length 0
; CC Result has length >0
;
; Registers
; temp
;
Translate a Name to a Directory Entry
Name2DirEntry
combines all of the above routines to one single function. It
accepts as input the pointer to a directory control block and a pointer to a
filename. It will first copy a partial name and try to find it in the directory
described by the directory control block. It also handles all occurencees of ..
and .
in the input string. When a partial name matches the name of a directory
name it will then open this directory and repeat the above steps. It will
iterate until the last partial name has matched the directory entries. If a
match has been found the Carry
bit is cleared and the control block will
be filled with the necessary data to identify the corresponding directory entry.
Else the Carry
bit will be set.
;--------------------------------------------------------------------------
;
; Name2DirEntry
;
; This function takes a pointer to a path name. It walks through the
; path and checks if the name exists in the current directory.
; - If the name is not found it will return an error.
; - If the name found is a normal filename and the name is not the
; last name in the path it will return an error. Else it will
; return success
; - If the name found is a directory it will take the next name and
; search for it in the new directory.
; - If the end of the path has been reached it will return success
;
; Input:
; Y Pointer to data structure for directory IO
; X Pointer to zero terminated path
;
; Output:
; Directory structure updated
; Y+P_StartCluster start cluster of file/directory
; Y+P_DirPointer points to the directory entry
; Char the terminating character
;
; Conditions:
; CS file not found
; CC file found
;
; Registers
; temp
;
Translate Cluster to Sector
In many cases we need to translate a cluster to a sector. For example when we
want opened a file, or when we have reached the end of a cluster when reading
a file only now the cluster-id for the next block. In case of a recently opened
file this is the first cluster and when we have reached the end of a cluster we
will first only be able to find the linked cluster-id using LinkedCluster
but
to retrieve the real data of the file we need to translate this cluster-id to
the physical sector number on the SD-Card of the first sector/block within this
cluster.
Build Fragment List
For block IO files we typically want to be able to translate a logical block number
to a physical block number. To speed this process the function BuildFragList
can
create a linked list of fragment descriptors. This will later allow to translate
a logical block number within a file to the physical block number on the SD-Card
without having to consult the FAT table. Hence this translation will always be very
fast. It takes less than 100 cycles per fragment. So even in a highly fragmented file
with 10 fragments it will take only about 50usec to translate a logical block to a
physical SD-Card block number.
Before you call this routine you need to allocate a file control block and copy all necessary information from the found directory entry to the file control block.
The result will be an update file control block with a queue of file control blocks added to it.
;--------------------------------------------------------------------------
;
; Create a fragment list of a file. The fragmentlist consists of
; tuples of fragmentsize and fragmentstart. Each tuple is a 32-bit
; integer. This is to support direct block IO to disk images without
; the need to read additional sectors from the device.
;
; size the size of this fragments in sectors
; start the number of the first sector covered by this fragment
;
; When we now need to read or write a specific sector of a file we need
; to find the corresponding entry. For this we compare the sector we
; want with the size of the fragment. If it is lower then we found the
; fragment. Else we subtract the size from the sector which can be looked
; at as the offset into the next fragment. If the calculated value is
; less then the size of the next fragment we are done. Else we continue
; until the number of sector is negative (if a file is 4 sectors long
; the sectors we can read are numbered 0,1,2,3. When we subtract the
; legnths of all fragements from the sector to read the result is -1)
;
; When we found a fragment we just need to add the (remaining) sector
; offset to the start sector number of this fragment. The results is
; then the absolute sector on the device.
;
; Input:
; Y Pointer to file control block
;
; Output:
; Fragmentlist created
;
; Registers:
; r4, r5, r6, r7, temp
;
; Completioncode:
; CS The file has more fragments than we can store in memory
; CC Created complete fragment list
;
;
; 2019-01-03 Use linked fragment list. That is we first allocate a
; fragment descriptor buffer and link it to the previous
; one (or in case if it is the first we link it to the
; fragment descriptor queue head of the IO block).
; We support as many fragments as we have free memory.
;
;
BuildFragList:
Translate Logical Block Number to Physical Block
This routine is called for block IO files to translate the logical block number, i.e. the block number within the file, to a physical block number on the SD-Card. This routine will scan the fragment queue list and find the corresponding entry. Then it will use the entry to translate the logical block number to a block number within the partition and then will add the partition offset. This will allow to immediately call the SD_CARD_READ or SD_CARD_WRITE function afterwards to retrieve or store the corresponding block on the SD-Card.
;--------------------------------------------------------------------------
;
; Translate logical block to physical sector using the fragment list
; of the file entry
;
; 2019-07-26 Input changed from P_Sector to P_Cluster so we can keep the
; logical block number in P_Cluster. As this is only used for
; block IO files P_Cluster is not used as we have a fragment
; list to access the sectors of the file.
;
; Input:
; Y Pointer to data structure for file IO
; Y+P_Cluster Logical Block Number
;
; Output;
; Y+P_Sector Physical Sector Number
;
; Condition:
; CC Translation was successfull
; CS Translation failed
;
; Registers:
; temp
;
; 2019-01-03 Use linked fragment list
; 2019-07-24 Input is now expected in P_Cluster
; 2019-07-27 Save and restore registers
;
Logical2Physical:
Get next linked Cluster
The FAT on a MS-DOS volume is just a collection of linked clusters. Therefore if you
reached the end of a cluster within a file you need to find the next cluster. The
function LinkedCluster
finds the next cluster using the FAT.
;--------------------------------------------------------------------------
;
; Subroutine for LinkedCluster
; Converts Cluster to sector offset in FAT adds fat1start and
; checks if the sector is already in pfatbuffer. Else it reads
; the sector to pfatbuffer. Then it calculates the offset into
; the sector and reads the linked cluster and checks if the end
; of the linked list has been reached.
; Input:
; Y datastructure of file
;
; Output:
; P_Cluster updated with linked cluster
;
; Completioncode:
; EQ end of file reached no more clusters
; NE new cluster stored int P_Cluster
;
; Registers:
; r0, r4, r5, r6, r7, temp
;
LinkedCluster:
Create a Path using the Current Working Directory and a File Name
We always keep track of the current working directory so in case operations that use a filename with a relative path need to be able to combine the current directory with the filename. Also in Case the filename is absolute we need to ignore the current path. This function will handle all the possible cases and translate the two entities “current working directory” and a given filename to a new working directory.
;--------------------------------------------------------------------------
;
; 2019-01-06 Added CreatePath
;
; Combine the current working path with a new path/file name to form
; a fully qualified path. It is assumed that both string are valid and
; existing paths or files respectively. E.g. have been checked using
; Name2DirEntry, i.e. both, the working path and the new path/file
; name must exist.
; The function will scan the new path/file for the delimiter. If the
; substring corresponds to ".." then it will remove the top element
; from the working path. If it is a filename it will be added to the
; working path.
; 2019-07-18 Paths preceeded with DELIM are now handled as absolute paths
; Use pointer and not location Path
;
; Input:
; X Pointer to the new path/file zero terminated string
; Z Pointer to the current working path
;
; Output:
; Working path updated with
;
ReadFileOpen
You can open a file for read using the ReadFileOpen routine. This routine expects a pointer to the current directory entry in memory and allocates a file-control block and a sector buffer and initialises the fields in the file control block to sequentially read bytes from the file.
;--------------------------------------------------------------------------
;
; Open File for Read access
;
; Create a file control block with IO buffer, initialise the file
; control block and read first sector/block
;
;
; Input
;
; Z Pointer to directory entry
;
; Output
;
; CS Error
; char Error Code
;
; CC Success
; Y Pointer to file control block
;
; fcb_sector
; fcb_cluster
; fcb_address
; fcb_secinclst
; fcb_position
; fcb_fileszie
; fcb_byteinsec
;
; Registers
;
; temp, r24, r25
;
ReadFileByte
Once you have opened a file using ReadFileOpen you can sequentially retrieve the bytes from the file. ReadFileByte just returns the next byte. In case of error or when it has reached the end of file it will set the carry bit
;--------------------------------------------------------------------------
;
; Read One Byte from open file
;
; Get next byte from file.
;
; - Return 0x1a (^Z for end-of-file) if already all bytes read
; - Update file pointer
; - read next sector in cluster if last byte of sector already read
; - link to next cluster if all bytes in cluster already read
;
; Input
;
; Y file control block
;
; Output
;
; CS Error
; char Error Code, most important 0x1a for end-of-file
;
; CC Error
; char Byte
;
; Registers
;
; temp, r4, r5, r6, r7
;
ReadFileClose
As you may expect there is also a file close routine. This routine must be called to free all allocated buffers.
;--------------------------------------------------------------------------
;
; Close File
;
; Input
;
; Y file control block
;
The three ReadFile routines are mainly used to read text-files from the FAT volume. These routines have been written to read the INI file for the RLV12 emulator but are kept as general as possible so you can read any file using these three routines.