Page 10 of 14

Re: Adventures in FAT32 with 65c02

Posted: Wed Jan 14, 2026 9:47 pm
by barnacle
I think this routine might do the trick, though it looks a bit clunky:
The top four bits might be anything; the bottom seven bits are the sector offset in the sector (four byte records), and we need to maintain the LBA intact, so:
- push the LBA
- AND both variables with $0FFFFF80
- subtract one variable from the other
- test for zero
- pop the LBA, preserving the status to save the zero result

Code: Select all

; u32_samesector
;
; Is the cluster pointed to by Y:A in the sector indicated by lba?
;
; On entry: current FAT sector in lba; next cluster pointed to by Y:A
; On exit: Z set if result true	
;
; The sector number is the same if bits 27-7 are the same in both inputs
u32_samesector:
	sta put_ptr
	sty put_ptr+1		; set parameter pointer
	PUSHLBA
	lda lba+3
	and #$0f
	sta lba+3
	lda lba
	and #$80
	sta lba				; trim the unwanted bits of lba
	lda (put_ptr)
	and #$80
	sta (put_ptr)
	ldy #3
	lda (put_ptr),y
	and #$0f
	sta (put_ptr),y		; and from Y:A parameter
	lda put_ptr
	ldy put_ptr+1		; set Y:A again
	jsr u32_sub			; subtract (Y:A) from lba
	jsr u32_iszero
	php					; save result
	POPLBA				; get our original lba back
	plp
	rts	
Actually, I think I can already see some improvements.

Neil

Re: Adventures in FAT32 with 65c02

Posted: Wed Jan 14, 2026 10:02 pm
by barnacle
This might be better, shorter, faster, half a dozen fewer stack usages. It doesn't damage either of the parameters, so no
need to save them.

Code: Select all

u32_samesector:
	sta put_ptr
	sty put_ptr+1		; set parameter pointer
	ldy #3
	lda lba,y
	eor (put_ptr),y		; zero where bits are the same
	and #$0f			; clear the top nibble
	bne u32_ss_x		; they're different
		dey
		lda lba,y
		eor (put_ptr),y
		bne u32_ss_x
			dey
			lda lba,y
			eor (put_ptr),y
			bne u32_ss_x
				dey
				lda lba,y
				eor (put_ptr),y
				and #$80	; lose the bottom seven bits
u32_ss_x:
	rts	
Neil

edit: corrected 'xor' to 'eor'. :roll:

Re: Adventures in FAT32 with 65c02

Posted: Thu Jan 15, 2026 3:11 pm
by barnacle
Still leading up to the main event, but it's looking hopeful...

Code: Select all

NeoDOS32 v0.0226 
A section (to save space) of transient, viewed after the file 'main.lst' has been located:

Code: Select all

7FA0  4D 41 49 4E 20 20 20 20 4C 53 54 20 00 50 B8 68 MAIN    LST .P.h          
7FB0  2F 5C 2F 5C 00 00 2D 68 2F 5C 13 00 1F 1F 02 00 /\/\..-h/\......          
Which is showing that the allocated clusters start at 00000013 (words at $7FB4 and $7FBA) and a file size of $00021f1f (uint32 at $7FBC).

After the file is 'deleted', it looks like this:

Code: Select all

7FA0  E5 41 49 4E 20 20 20 20 4C 53 54 20 00 50 B8 68 .AIN    LST .P.h          
7FB0  2F 5C 2F 5C 00 00 2D 68 2F 5C 00 00 00 00 00 00 /\/\..-h/\......          
And now both the clusters and filesize are zero, and the first byte of the name at $7FA0 has become $E5, the deleted market. At this point, the directory entry looks as if a file has been created and immediately deleted, so that's a legal filename and I can write the data back to the disk. LBA is still pointing at the sector with the directory in it, so nothing to worry about there.

But while I was zeroing the cluster pointers, I cunningly pushed them onto the stack, lowest byte first so things are in the right order for

Code: Select all

    POPLBA
to get the first allocated cluster into LBA. I can then call f_dealloc() which immediately converts the cluster $00000013 on this disk into a FAT sector number (in LBA) and a pointer to the FAT entry; I can load the sector and proceed.

Code: Select all

7E00  F8 FF FF 0F FF FF FF 0F F8 FF FF 0F 04 00 00 00 ................          
7E10  05 00 00 00 FF FF FF 0F 07 00 00 00 FF FF FF 0F ................          
7E20  09 00 00 00 0A 00 00 00 0B 00 00 00 FF FF FF 0F ................          
7E30  0D 00 00 00 0E 00 00 00 FF FF FF 0F 10 00 00 00 ................          
7E40  FF FF FF 0F 12 00 00 00 FF FF FF 0F 14 00 00 00 ................          
7E50  15 00 00 00 16 00 00 00 17 00 00 00 18 00 00 00 ................          
7E60  19 00 00 00 1A 00 00 00 1B 00 00 00 1C 00 00 00 ................          
7E70  1D 00 00 00 1E 00 00 00 1F 00 00 00 20 00 00 00 ............ ...          
Cluster $13 points to $14 which points to $15 and so on; the listing file is fairly large.

So the current cluster ($00000013) is copied to a local temp and simultaneously zeroed. I do the same trick of reading and writing the value to get the next cluster in the chain.

Code: Select all

7E00  F8 FF FF 0F FF FF FF 0F F8 FF FF 0F 04 00 00 00 ................          
7E10  05 00 00 00 FF FF FF 0F 07 00 00 00 FF FF FF 0F ................          
7E20  09 00 00 00 0A 00 00 00 0B 00 00 00 FF FF FF 0F ................          
7E30  0D 00 00 00 0E 00 00 00 FF FF FF 0F 10 00 00 00 ................          
7E40  FF FF FF 0F 12 00 00 00 FF FF FF 0F 00 00 00 00 ................          
7E50  15 00 00 00 16 00 00 00 17 00 00 00 18 00 00 00 ................          
7E60  19 00 00 00 1A 00 00 00 1B 00 00 00 1C 00 00 00 ................          
7E70  1D 00 00 00 1E 00 00 00 1F 00 00 00 20 00 00 00 ............ ...          
So now entry $13 has gone away (from $7E4C) and I can continue following the chain. The routine isn't complete yet, but I think I'm getting somewhere.

Neil

Re: Adventures in FAT32 with 65c02

Posted: Thu Jan 15, 2026 5:32 pm
by BigDumbDinosaur
barnacle wrote:
It's getting there, I think.

I agree with you completely about black boxes; the problem is handling black boxes that need two 32-bit input variables...
A set of parameter blocks might be the solution.  Since your code will likely end up with multiple function call levels—black box ‘A’ calls black box ‘B’, etc., you will need enough parameter blocks to handle all call levels.  The parameter blocks can also include temporary storage for the registers, which will help with transparency.  You won’t be able to use true recursion, of course, but it will be possible to minimize collateral effects.

A (slower) alternative would be the use of a software stack for parameter-passing, which, in theory, would give you localization and recursion.  Of course, you’d need code to maintain the stack, which itself will complicate things.

You will also have to enact strict rules about how one black box communicates with the other...effectively imitating in hard-coded memory regions what the 65C816 can do with stack operations.  When I messed with this on the 65C02, I strictly used parameter blocks, even when it was possible to use registers alone.  That imposed code discipline with only a small execution speed penalty—I had surprisingly few clashes.  Performance was nothing to write home about, but was reasonable.

Quote:
Speed is not a _huge_ issue for a deallocate operation; it occurs only when a file is deleted. But I don't want to have to do 128 sector writes on the same sector, as might be the case for a long file...
At some point, you have to buckle down to do longish writes...it’s the nature of your chosen beast (FAT32).  I’m no expert on FAT32, but what I do know about it explains to me why Micro-$oft invested the time required to develop the NTFS filesystem.  Unsurprisingly, NTFS has some UNIX-like features, e.g., hard links and sparse files, as well as a saner method of allocating and freeing storage (but still stubbornly insists on using clusters).  The NTFS filesystem driver isn’t as disk-intensive as the FAT32 equivalent, especially the NTFS version that started shipping with Window$ 8.
Quote:
This may do the trick; I'm still thinking about it so comments only at the moment, not proper code:

Code: Select all

;	convert cluster to fat record location
;	extract fat record to temp
;	write zero to fat record
;	temp is next in chain; is it in the same sector?
;		if no, write segment back to fat1 and fat2
;	is temp end-of-chain?
;		if no, cluster = temp and repeat
;		else, write segment back to fat1 and fat2
That seems like it could work without constantly thrashing the disk.
Quote:
temp is a 32 bit temporary variable, ideally zero page (though I may have to do some register colouring; I'm already up to $36 plus 27 bytes of string space) and I want to leave as much as possible for user programs. And I hate to think how deep the stack is getting, pushing and popping the 32 bit lba.
At some point, you have to accept the fact the zero page is a limited resource and the slight performance improvement seen with zero-page addressing is less important than conservation for use with instructions that can only operate on zero page.  And, as you noted, you need to avoid what Commodore did in the C-64 and C-128, which was to consume almost all of zero page in BASIC and kernel functions.
Quote:
The line that's going to be tricky is

Code: Select all

;	temp is next in chain; is it in the same sector?

It may well need further variables.

This would be _so_ much easier with a 32-bit processor.
It would be much easier with the 65C816.  :D  Although the 816 can’t handle a 32-bit quantity in one register, it can do so in two and if more than two 32-bit parameters must be passed, there’s the stack and movable direct page.  Those two features allow a parameter passing depth that is limited only by how far down SP can go before there’s a collision with something else.

As I said a few posts back, sometimes your project requires use of a bigger hammer.  :mrgreen:

Re: Adventures in FAT32 with 65c02

Posted: Thu Jan 15, 2026 6:25 pm
by barnacle
tiny-hammer.png
Neil

Re: Adventures in FAT32 with 65c02

Posted: Sat Jan 17, 2026 6:26 pm
by barnacle
But even a tiny hammer, chipping away, gets the job done eventually:

Code: Select all

(10) current cluster: 00000034                                                  
(10) next cluster: 0FFFFFFF                                                     
7E00  F8 FF FF 0F FF FF FF 0F F8 FF FF 0F 04 00 00 00 ................          
7E10  05 00 00 00 FF FF FF 0F 07 00 00 00 FF FF FF 0F ................          
7E20  09 00 00 00 0A 00 00 00 0B 00 00 00 FF FF FF 0F ................          
7E30  0D 00 00 00 0E 00 00 00 FF FF FF 0F 10 00 00 00 ................          
7E40  FF FF FF 0F 12 00 00 00 FF FF FF 0F 00 00 00 00 ................          
7E50  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................          
7E60  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................          
7E70  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................          
7E80  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................          
7E90  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................          
7EA0  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................          
7EB0  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................          
7EC0  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................          
7ED0  00 00 00 00 FF FF FF 0F 37 00 00 00 38 00 00 00 ........7...8...          
7EE0  FF FF FF 0F 00 00 00 00 00 00 00 00 00 00 00 00 ................ 
shows the big hole left in the FAT from a file whose first cluster was $00000013 (at offset $7E4C) which has been deleted.

As yet, there is no writeback to the disc (otherwise I have to copy the file in again every time I 'erase' it, possibly accompanied by formatting first), and I haven't yet checked if it handles a file chain that includes a different sector. Also, the code is full of debug add-ons, and still needs some tidying/factoring, including updating the next_cluster and free_count variables. As intended, it doesn't write back the sector until the chain moves to a new sector (which may well not be sequential) to avoid unnecessary writes.

But we're getting there.

The overall result will be a FAT which fills linearly from the last-written location. As files are deleted, they will leave holes in the FAT. Once the allocation gets to the last cluster in the partition, it will start looking again from the beginning for free clusters and use those. With time, that will lead to a potentially very fragmented disc but the lack of seek time renders that a non-problem.

Neil

Re: Adventures in FAT32 with 65c02

Posted: Sat Jan 17, 2026 8:52 pm
by BigDumbDinosaur
barnacle wrote:
But even a tiny hammer, chipping away, gets the job done eventually:

Code: Select all

(10) current cluster: 00000034                                                  
(10) next cluster: 0FFFFFFF...00 00 00 00 ................ 

shows the big hole left in the FAT from a file whose first cluster was $00000013 (at offset $7E4C) which has been deleted.
“Bloody horrid arrangement,” says I in my best British accent.  I do seem to recall you mentioning something about your wife wondering why you are swearing at Micro-$oft.  :D

Re: Adventures in FAT32 with 65c02

Posted: Sun Jan 18, 2026 4:46 pm
by barnacle
Our Hero Takes His Vorpal Sword in Hand
The jabberwock that is deallocating clusters in the FAT definitely has jaws that bite and claws that catch, and the Vorpal Sword is a necessary implement in cutting it down to a manageable size.

He has not yet written the data back to the CF, but an inspection shows that all the data fields appear to be as they should; the deallocated clusters are zeroed properly (I think: MS say 'preserve the top four bits' AND 'unallocated clusters are always zero' which has the potential for amusement); and the available cluster count is correct.

Code: Select all

NeoDOS32 v0.0249                                                                
Free cluster count: 0001E5CB                                                    
Next free cluster: 00000038                                                     
Press any key to boot:                                                          
BIOS.ASM                                                                        
CF.ASM                                                                          
DOS.ASM                                                                         
FILE_SYS.ASM                                                                    
MAIN.ASM                                                                        
MAIN.HEX                                                                        
MAIN.LST                                                                        
U32MATHS.ASM                                                                    
VARS.ASM                                                                        
deleting main.lst                                                               
(not yet) Writing directory back                                                
End of cluster                                                                  
00000820                                                                        
exit                                                                            
0001E5ED
Here we are listing the root directory after showing the cluster variables (why didn't MS put them in the VIB?); attempting to delete 'main.lst'; and showing some debug messages.

There are four writes to make (or more, depending how things fit in the FAT):
  • The directory sector
  • The FAT sector which we have just zapped; I only write that once if the following cluster record is in a different sector, or we get to the last entry in the chain. This avoids hammering the sector with unnecessary writes at some minor complexity in the deallocator.
  • The second FAT sector, written the same way as the first and as close to the same time as possible
  • The FSI sector, where MS keep the fsi_next_cluster and fsi_free_count variables
I haven't uncommented the actual writes yet since while I've been debugging, every mistake means I have to reformat the disk from scratch...

Neil

ps: Don't ever break the chain

Re: Adventures in FAT32 with 65c02

Posted: Tue Jan 27, 2026 3:45 pm
by barnacle
A minor delay caused by my backup system letting the magic smoke out, but we progress, slightly.

To Deallocate the Clusters Owned by a File
After some consideration (upthread) regarding the 'best' way to reallocate disk space when the system runs out of clusters, I concluded that option (a) 'ignore it' was going to lead to trouble sooner or later, potentially massively underusing the available space; (b) 'wait until we need it and then look for it' lead to recursive nightmares; but (c) 'deallocate the cluster when the file is erased' is moderately quick and leaves the file system in a sane and usable state. You can't undelete any more, but tbh I can't recall the last time I tried to do that...

So the process starts by locating the file to delete, and if it exists, and is neither read-only or a directory, mark its directory entry as having no clusters and of zero size. After that, we can deallocate the clusters:

Code: Select all

	bss
tmpq1:		ds 4			; cluster to delete
tmpq2:		ds 4			; next cluster in chain

	code
f_dealloc:
	LYA tmpq1
	jsr u32_fromlba			; copy the current cluster to tmpq1
	jsr clus_to_pointer		; find the cluster record and sector
	PUSHLBA					; save the current FAT lba
	
	LYA transient
	sta sector_ptr
	sty sector_ptr+1		
	jsr cf_read				; read the FAT sector
		
f_deal_05:
	ldy #0
f_deal_10:
	lda (fat_record),y
	sta tmpq2,y				; save the record
	lda #0
	sta (fat_record),y		; while clearing it
	iny
	cpy #4
	bne f_deal_10			; until all four bytes are done
	
	; every time we deallocate a record, we increment the remaining 
	; cluster count; we won't save it back to disk yet.
	; we do not, however, make any change to fsi_next_free.	
	inc fsi_free_count
	bne f_deal_101
		inc fsi_free_count+1
		bne f_deal_101
			inc fsi_free_count+2
			bne f_deal_101
				inc fsi_free_count+3
f_deal_101:
	; is the record in tmpq2 an end-of-cluster marker?
	; if tmpq2 AND $0FFFFFF8 = $0FFFFFF8, then it is
	lda tmpq2+3
	and #$0f				; ignore top bits
	cmp #$0f
	bne f_deal_11
		lda tmpq2+2
		cmp #$ff
		bne f_deal_11
			lda tmpq2+1
			cmp #$ff
			bne f_deal_11
				lda tmpq2
				and #$f8	; ignore bottom three bits; any are valid
				cmp #$f8
f_deal_11:
	bne f_deal_12
		; if zero, then we are done
		; write the sector back and quit
		POPLBA				; fat sector address
		LYA transient
		sta sector_ptr
		sty sector_ptr+1		
		jsr cf_write		; FAT1
		LYA sectors_per_fat
		jsr u32_add
		LYA transient
		sta sector_ptr
		sty sector_ptr+1		
		jsr cf_write		; FAT2
		jmp f_deal_x		; and quit for cleanup
f_deal_12:
	; there are more links in the chain
	; is the new record in the same FAT sector as the last?
	lda tmpq2+3
	eor tmpq1+3
	and #$0f				; ignore the top four bits
	bne f_deal_20			; zero if they're the same
		lda tmpq2+2
		eor tmpq1+2
		bne f_deal_20
			lda tmpq2+1
			eor tmpq1+1
			bne f_deal_20
				lda tmpq2
				eor tmpq1
				and #$80	; we don't care about the lower 7 bits
				bne f_deal_20	; no, it's not
		; same page, so copy current cluster to old cluster
		ldy #3
f_deal_13:
		lda tmpq2,y
		sta tmpq1,y
		dey
		bpl f_deal_13
		; calculate the new record position
		lda tmpq2
		jsr clus_to_fat_rec
		jmp f_deal_05			; and grab that one
f_deal_20:
	; the next FAT record is not in the same sector as the current one
	; so we're finished with this sector and can rewrite it
	LYA tmpq2
	jsr u32_tolba
	jmp f_dealloc				; start again at the top
	
f_deal_x:
	LYA partition_begin_lba
	jsr u32_tolba
	LYA one
	jsr u32_add
	LYA transient
	sta sector_ptr
	sty sector_ptr+1
	jsr cf_read					; get the FSI sector
	PUSHLBA						; save its address
	LYA fsi_free_count
	jsr u32_tolba
	LYA free_count_ptr
	jsr u32_fromlba				; update the free count in the FSI
	POPLBA
	LYA transient
	sta sector_ptr
	sty sector_ptr+1
	jsr cf_write
	rts
There's a lot of code in there designed to limit the number of writes to the various sectors. In particular, we don't write the FAT sector back until we have either reached the end of the cluster chain, or the chain continues in a different cluster. When we do write the FAT sector back, we also write directly to the second FAT's sector, keeping them both in sync with each other.

We maintain copies of FSI_Next_Cluster (where the system should start looking next time) and FSI_Free_Count (how many clusters remain) in memory, but we _must_ write the FSI_Free_Count cluster back each time we deallocate files.

So there are a total of a minimum of four sectors written to the disc each time we erase a file: the directory entry in a given directory sector; two sectors, one each to FAT 1 and FAT 2; and one final sector to the FSI sector. A longer file will require proportionally more FAT writes: deleting a 4MB file would require a thousand FAT entries, which would require eight FAT sectors to hold them.

This has been tested as far as I can; I need to confirm that it also works in subdirectories (but using cwd should ensure that). It also needs a read_only test. Files written from the laptop are shown correctly, are indicating that they are deleting properly, don't show - as they shouldn't - when the disk is returned to the laptop, and files can be further added after a deletion.

Other than that, I can get on with _allocating_ a cluster, which has not yet been considered - remember that creating a file does not allocate any cluster to it.

Neil

Re: Adventures in FAT32 with 65c02

Posted: Thu Feb 05, 2026 2:15 pm
by barnacle
Allocating a new cluster

(with apologies for the delay; I've been rebuilding a NAS that let the magic smoke out of its controller. I now know a lot more about NASes than I did a couple of weeks ago!)

Allocating a new cluster is one of those fundamental file operations that ought to be simple, easy, and straightforward. Guess what... it's not.

Let's recap the FAT32 file system.
  • The FAT table consists of uint32_t pointers, so four bytes each, in sequential order in the FAT sectors.
  • There is one FAT entry per cluster, whether it's allocated or not.
  • There is a second copy of the FAT, immediately following the first (usually, and in my disks this is so; the parameters can tell you more about this and really you should check them)
  • The position of the entry in the fat tells which cluster it refers to; the value of the entry is either a pointer to the next cluster in the chain, a magic number to say it's the end of the chain, or a zero to indicate that the cluster is not allocated.
  • Every file except the root directory has a directory entry, either in the root directory or in a child of that directory. That directory includes a pointer to the first cluster allocated to it (but no others).
  • When a file is created, no cluster is allocated to it and its allocated cluster number is also zero.
  • When a file is deleted, I deallocate its clusters at the same time - so the process of creating and deleting files leads to a FAT with holes in it; new clusters are initially allocated sequentially in the table while deleted files occur behind that (at lower entry positions).
  • The first sector in the partition - the VIB (Volume Information Block) contains most of the data you need to locate the various structures within the partition including the position of the FSI sector (File System Info) which holds two critical variables: FSI_Next_Cluster and FSI_Free_Count which must be updated after every filesystem write or erasure. The FSI is usually the sector following the VIB (it is on my disks).
    (To avoid too many reads and writes to the disk, keep these two variables in memory).
  • FSI_Next_Cluster appears to hold the last cluster allocated, not the next free space. It's where MS say we should start looking for new space. Actually telling us where it was would be too easy, right?
Bear in mind a couple of points: I am trying to do all the file manipulation (other than file open, read, and write in a program) through a single sector buffer; transient. I am trying to keep the number of 4-byte variables to a minimum. I am also trying to avoid repeated writes to the disk. As a final complication, the 65c02 isn't really suited to the amount of 32-bit arithmetic involved. It's not that it can't, but it's not small and tidy. One irritating thing is that the perfectly reasonable instructions inc (ptr),y and dec (ptr),y don't exist, which complicates adding one.

Better to allocate than never...
This is the procedure you must do to allocate a new cluster. Note that it doesn't matter why you're allocating it, the procedure is always the same. But in some cases, you may already have something in transient, so keep that in mind.
  1. Start by reading the value of FSI_Next_Cluster. This is the cluster number where we start looking.
  2. From that cluster number, obtain the FAT record containing its record; the FAT sector is FSI_Next_Cluster >> 7 and the record number is FSI_Next_Cluster & 0x7f
  3. Examine that 32-bit record value. If it's zero, we're ready to update things. If not...
  4. Increment the record counter to the next record, and go back to [3]. If incrementing the record counter takes us beyond $128, then we reached the end of the sector and we need to get the next sector into transient and try again.
  5. If we get to the end of the FAT without finding a record with value zero, we need to remember that there may be space further back in the FAT, before our starting point. We want to start with the oldest of those, the earliest, so we need to go back to the beginning of the FAT and start looking again. But we must be sure that we only do that once, or we're stuck in a loop...
  6. If by this time, we have not found a zero record, then the disk is full and we cannot allocate more space. Return an error code.
  7. Otherwise, we have found somewhere to allocate. There's an amount of housekeeping necessary to do before we can return the cluster number (which we calculate from the current FAT sector and sector record values).
    - Change the value in the FAT record to indicate an end-of-chain marker
    - Write the FAT record back to the disk
    - Write the FAT record back to the second FAT
    - Decrement FSI_Free_Count in memory
    - Set FSI_Next_Cluster to the new cluster number
    - Write the FSI back to the disk
Point 5 is interesting. Initially, I thought that I would have to compare the FAT sector LBA with the start of the second FAT sector (which I don't have precalculated) and also use some sort of flag to indicate that I was on the second search for earlier deallocated space. But it occurs to me that I _do_ have a precalculated value for the start of the clusters: cluster_begin_lba and that the second FAT is an accurate copy of the first (we haven't updated anything at this point). The clusters, of course, follow directly after the second FAT.

That means that it is only necessary to start at FSI_Next_Cluster in the first fat, and search for zero records until either we find one, or we get to cluster_begin_lba; a much simpler process. So the sixth or seventh rewrite is on its way...

When do we need this?
Two main occasions: when we make a child directory, we need to allocate a cluster to hold that directory; and when we write to an existing file - either one which has just been created, and so has no allocated cluster, or one where we're writing past the end of an existing cluster.

We do need to bear in mind that this allocation routing comprehensively demolishes anything that might be in transient. If we're building a file through a file pointer, we will have a dedicated sector buffer for that file, so that might not be a great issue. However, when we are creating a new directory, we will have to manage - in transient - the sector from the parent's directory containing the reference we will have just inserted to the new directory and the sector from the new directory containing links to the new . and .. directories.

Buckle up. It could get exciting. :mrgreen:

Neil

Re: Adventures in FAT32 with 65c02

Posted: Thu Feb 05, 2026 4:47 pm
by GlennSmith
barnacle wrote:
Buckle up. It could get exciting. :mrgreen:
Lucky that the thread we were hanging on isn't as 'retro' as FAT32. It would have broken long ago !
But I'm still here, following along. Thanks!

Re: Adventures in FAT32 with 65c02

Posted: Thu Feb 05, 2026 8:12 pm
by barnacle
barnacle wrote:
  1. From that cluster number, obtain the FAT record containing its record; the FAT sector is FSI_Next_Cluster >> 7 and the record number is FSI_Next_Cluster & 0x7f
A minor clarification: the calculated FAT sector is relative to the start of the FAT. So for the first FAT, we need

Code: Select all

sector = (FSI_Next_Cluster >> 7) + fat_begin_lba
to get its absolute lba sector.

Using the start at FSI_Next_Cluster and incrementing until we find a zero or cluster_begin_lba, we still need to either note or calculate whether we're in the first or second FAT; when we write the end-of-cluster marker to the record, we either add or subtract sectors_per_cluster to select the other FAT. We need to write both, of course.

Neil

Re: Adventures in FAT32 with 65c02

Posted: Fri Feb 06, 2026 9:56 pm
by barnacle
Allocating a New Cluster - code

This is as yet untested, so probably still full of logic bombs. One major assumption is that the the FSI sector is in the second sector of the partition - sector $801 on my disk - but I'm not going to worry about it.

Testing will be interesting: first that it can do it with a nearly empty disk, when we remain in the first sector of the FAT table; then that we can move to other FAT sectors, and then with deleted files from a full disk...

Note: LYA is a macro that loads Y:A as a pointer to memory, usually a 32-bit int. It gets used _everywhere_...

Code: Select all

;-----------------------------------------------------------------------
; f_alloc
; Allocate a new cluster if one is available.
; Return new cluster number, or E_NO_SPACE if disc is full
; New cluster is returned in fsi_next_free
;
; Clusters are allocated in linear order from the first empty cluster
; following FSI_next_cluster. If we get to the end of the FAT without
; finding one, we start again at the first cluster looking for spaces
; that have been cleared by deleted files.
; After allocation, we need to update fsi_next_cluster, fsi_free_count,
; and update the FSI sector on the disk. And of course both FAT copies.

f_alloc:
	LYA fsi_free_count
	jsr u32_iszero
	bne f_all_01
		; disc is full
		lda #E_NO_SPACE
		jmp f_all_x
f_all_01:
	; free count is not zero, so seek next cluster
	LYA fsi_next_free
	jsr u32_tolba			; cluster is in lba
	jsr clus_to_fat_sector	; see where the FAT sectors are in FAT 1
	LYA tmpq1
	jsr u32_fromlba			; tmpq1 now holds address of current fat sector
	LYA transient
	jsr cf_read				; get the FAT sector into transient
	lda lba
	and #$7f				; isolate the record count
	tax						; and save it
f_all_05:
	jsr clus_to_fat_rec		; sector_ptr points to record in transient
	ldy #1
	lda (sector_ptr)
	ora (sector_ptr),y
	iny
	ora (sector_ptr),y
	iny
	ora (sector_ptr),y		; is this a zero?
	beq f_all_10			; yes!
	; if non-zero, we have to try the next location
	inx
	txa
	bpl f_all_05			; 128 records per sample
		; else we need a new sector. By checking for zeros until either
		; we find one, or we get to the cluster start, we first check
		; for empty space at the end of the disk and if that fails, at
		; the beginning (finding the oldest deallocated cluster first).
		LYA tmpq1				; increment the fat sector
		jsr u32_inc
		jsr u32_tolba			; into lba
		LYA cluster_begin_lba
		jsr u32_cmp				; are we at the clusters yet?
		bne f_all_06			; no, carry on looking
			; else disc is full
			lda #E_NO_SPACE
			jmp f_all_x
f_all_06:
		LYA tmpq1
		jsr u32_tolba
		LYA transient
		jsr cf_read				; read this next FAT sector
		bra f_all_05			; and back for more
f_all_10:
	; if we get here, then we have found a zero.
	; first we calculate the new cluster number. We need to know which 
	; FAT we're in first
	; tmpq1 contains the sector and sector_ptr the offset in transient
	LYA fat_begin_lba
	jsr u32_tolba
	LYA sectors_per_fat
	jsr u32_add					; the start of the second fat
	LYA tmpq1
	jsr u32_cmp					; are we less than that?
	php
	LYA tmpq1
	jsr u32_tolba				; put the FAT sector in lba
	plp							; retrieve the flags from the comparison
	bcc f_all_12				; 
		; ah, we're in FAT 2
		LYA sectors_per_fat
		jsr u32_sub					; so move back to FAT1	
f_all_12:	
	; convert the current fat sector and sector_ptr to the cluster
	lda sector_ptr
	pha
	lda sector_ptr+1			; save sector_ptr; pointer_to_clus
	pha							; thumps it								
	jsr pointer_to_clus
	LYA fsi_next_free			; save the result as next free cluster
	; change the fat entry to end-of-cluster
	; we currently have the address of the FAT 1 sector in lba
	pla
	sta sector_ptr+1
	pla
	sta sector_ptr				; retrieve sector_ptr 
	ldy #0
	lda #$f8					; set record to end-of-chain
	sta (sector_ptr),y
	iny
	lda #$ff
	sta (sector_ptr),y
	iny
	sta (sector_ptr),y
	iny
	lda #$0f
	sta (sector_ptr),y			; record = $0ffffff8
	; transient should now have the modified chain, so get it back in
	; the two FAT copies
	LYA transient
	;jsr cf_write				; FAT 1
	LYA sectors_per_fat
	jsr u32_add
	LYA transient
	;jsr cf_write				; FAT 2
	;but there's still stuff to do
	LYA fsi_free_count
	jsr u32_tolba
	LYA one
	jsr u32_sub	
	PUSHLBA
	LYA fsi_free_count
	jsr u32_fromlba				; decrement fsi_free_count
	LYA partition_begin_lba
	jsr u32_tolba
	LYA lba
	jsr u32_inc					; ASSUME FSI sector is partition + 1
	LYA transient
	jsr cf_read					; load FSI to transient
	LYA fsi_free_count
	jsr u32_tolba
	LYA free_count_ptr
	jsr u32_fromlba				; set updated free count
	LYA fsi_next_free
	jsr u32_tolba
	LYA next_free_ptr
	jsr u32_fromlba				; and updated next pointer
	POPLBA						; get the FSI sector back
	LYA transient
	;jsr cf_write				; and write it to disk
	lda #E_NOERR				; ASSUME it worked

f_all_x:
	rts
One point to note is that this routine only allocates the new cluster and updates the various links and pointers (this would be much easier in C!). When we allocate, either we are allocating for first use, in which case we can substitute the resulting cluster number into the directory entry (which has long since disappeared from transient), or we are extending an existing chain, in which case we have to have remembered where we were when we asked because we need to change the existing link... a pain in either case, but writing FAT32 is a _lot_ harder than reading it.

And we haven't got onto creating directories yet... nor __write or __read...

Neil

Re: Adventures in FAT32 with 65c02

Posted: Sat Feb 07, 2026 12:45 am
by BigDumbDinosaur
barnacle wrote:
And we haven't got onto creating directories yet... nor __write or __read...
Following this topic is like watching a complete rerun of a popular, American soap opera, but instead titled As the Disk Turns:mrgreen:  It also reminds me of a common joke I heard during my US Navy days.

We used to say, when referring to policies and procedures, “There is the right way, the wrong way...and the Navy way.”  With FAT32, it’s “...the right way, the brain-dead way...and the Microsoft way.”  :D  The two latter ways might represent a distinction without a difference...  :roll:

Re: Adventures in FAT32 with 65c02

Posted: Sat Feb 07, 2026 4:14 am
by Martin_H
I let this thread get away from me in December. Today I decided to catch up. It was intimidating but I made it all the way through.

I like the idea of using a compact flash with an IDE adapter as a mass storage medium. Also, Neil you're braver than me writing a FAT32 implementation from scratch for the 6502.