Reading FAT data Part 1

OK with the SD card initialized in SPI mode it is finally time to read some data. To do this we need to understand the FAT format and I found the following links helpful as I was getting into the specifics. Note in my implementation I only support FAT32.

As mentioned in the previous posts the data is stored in 512 byte blocks on the SD card, which is the smallest increment of data you can read. To get a block of data from the SD card you can send it command 17 with the argument being the address of the block (4 byte address). Once the command is sent we get the usual R1 response and then have to wait until the card is ready to transmit data, which is indicates by sending the OK token ($FE). The following function accomplishes this (and calls the sd.send_cmd function described in my previous post)

.sd_block_cmd   stx zp_source+0       ; low byte in XR
                sty zp_source+1       ; high byte in YR

                ldy #3                ; move block address to command buffer 
@set_cmd_data   lda (zp_source),y
                sta sd_cmd_dat+1,y 
                dey 
                bpl @set_cmd_data
                
.sd_block_cmdx  lda #17               ; entry point if sd_cmd_dat structure set directly 
                jsr sd.send_cmd       ; command 17 = read a single block (arg = block number)
                bne @end  
                
                ldy #0
@wait_data      jsr spi.get_byte      ; wait until card is ready to send block data 
                cmp #$FF 
                bne @return
                dey
                bne @wait_data
                
@return         cmp #$FE              ; did we get 'ok' token ($FE) or error?
@end            rts    

The function needs a pointer to the 4 byte block address passed in XR/YR and will return ‘equal’ (Zero flag set) on success. Assuming that it returns without an error we are now ready to read 512 bytes of data from the specified block on the SD card. The following function reads the 512 bytes of data into a memory buffer that in my current implementation is $0300 – $04FF. To change it simply set sd_data_buffer to the location in memory you want to use.

sd.get_block    jsr .sd_block_cmd     ; send 'read block' command to SD card          
                bne .sdgb_error

.sd_get_bdata   ldx #0 
                
@first_256      lda VIA1_IFR          ; check IRQ flag
                and #%00000100        ; check for SR flag  
                beq @first_256          
                lda VIA1_SR           ; get data 
                sta sd_data_buffer,x 
                inx 
                bne @first_256
                                
                ; ldx #0              ; read last 256 bytes into buffer 
@last_256       lda VIA1_IFR          ; check IRQ flag
                and #%00000100        ; check for SR flag  
                beq @last_256           
                lda VIA1_SR           ; get data 
                sta sd_data_buffer+$100,x 
                inx 
                bne @last_256
                
                jsr spi.get_byte      ; get CRC 
                jsr spi.get_byte                                        

                sec                   ; set carry on success 
                rts 
                

sd.get_blockx   jsr .sd_block_cmdx    ; address already set in in sd_cmd_dat structure
                beq .sd_get_bdata

.sdgb_error     lda #SD_FILE_ERR      ; return file error code 
                sta sd_cmd_dat+0 
                clc                   ; clear carry on failure  
                rts 

I have chosen not to call my generic spi.get_byte function while reading the data, but implement it inline for speed — and I also have two 256 byte loops to read the 512 bytes of data for the same reason. If you want to optimize for code size you could do this, but I have optimized for speed of reading the data. Note the small optimization of not setting XR = 0 in the second loop, since it will have that value exiting the first loop. Once the 512 bytes are received the card sends a two byte CRC code which I simply ignore, but if you want error correction you could implement logic for that.

The loop takes 22 cycles to complete each pass, which is slightly longer than the 16 cycles it takes to read a byte using the system clock to drive the shift register clock on the VIA. This implementation has the advantage of being able to work at different clock speeds, since it waits for the flag to be set, but if you know you are running at a fixed speed you could optimize it to run ~25% faster, e.g. by the following

@first_256      lda VIA1_SR             
                sta sd_data_buffer,x
                nop        
                inx 
                bne @first_256  

I have not currently tested this and at exactly 16 cycles it might be too tight timing.

The first data we want to read from a FAT formatted card is the Master Boot Record (MBR). The MBR is located on block 0. To get the address of any block we have to multiply by the block size (512 or $200) … of course for block 0 this is still $00000000 😉

sd.load_mbr     lda #$00                     ; load block 0 on SD card = MBR 
                sta sd_cmd_dat+4                                        
                sta sd_cmd_dat+3                                        
                sta sd_cmd_dat+2                                        
                sta sd_cmd_dat+1 
                jsr sd.get_blockx

@boot_adr       lda #$00                     ; calculate & store boot sector address (little indean)  
                sta sd_boot_sector+0         ; address = LBA * $200 (sector size)
                lda sd_data_buffer+$1BE+8    ; location of first partition info ($1BE) + LBA (#sectors between MBR and first sector in partition) 
                asl 
                sta sd_boot_sector+1
                lda sd_data_buffer+$1BE+9 
                rol 
                sta sd_boot_sector+2
                lda sd_data_buffer+$1BE+10 
                rol 
                sta sd_boot_sector+3

If you want more info on what is stored on the MBR there is a quick overview here and more info in the Wikipedia article linked above. I am assuming that we only have one partition — or at least my current implementation will only support the first partition on the card — and the only piece of information we are after in the MBR is the location of the boot sector address. The first partition info starts at offset $1BE and the number of blocks/sectors between the MBR and the boot sector is stored as a 4 byte value starting in byte 8 of the partition info. Multiply this value by 512 and you have the boot sector address.

I should really also check that this partition is active and that it is FAT32 formatted, but for now I will leave that up to the reader.

The boot sector contains more information that we will need to read files from the SD card. Most importantly we want to find the location of the File Allocation Table (FAT) and its length. We also need the number of sectors per cluster for the FAT formatting of the card. If you have formatted a USB stick you might have noticed an “allocation unit size” which is the smallest unit size for the file format. It will be a power of 2 multiplier of the SD block size, e.g. an allocation unit size of 4096 bytes is 8 * 512. This means that the smallest size of FAT data you can write to the card in this example is 4096 bytes or 8 blocks of 512 bytes. Every file will be saved as whole number of sectors with any remaining bytes “wasted” — and a 5000 byte file will be 2 sectors long, in this example, with 3,192 bytes unused on the second sector. What a luxury on an 8 bit system, but obviously FAT32 was designed for systems where a couple of thousand bytes can be considered a pittance 🙂

In addition to the location of the FAT we also want to find the location of the root sector where information about file and folder names, etc. is stored. This location is not directly stored in the boot sector, but can be calculated since it starts immediately after the FAT sectors. For error correction etc. there are more than one FAT (typically 2) so you need to multiply the length of a FAT with the number of FAT’s to find the root sector location. Lastly we want the root sector number, which typically is 2 but could be a different value. We need this since the FAT stores the sector numbers of files and we will have to convert those sector numbers to addresses of blocks on the SD Card.

sd.load_bsctr   ldx #<sd_boot_sector       ; get boot sector 
                ldy #>sd_boot_sector 
                jsr sd.get_block
                bcc .stlb_end              ; carry clear = failure         

@sector_size    lda sd_data_buffer+$0B     ; check that sector size is 512 
                bne .init_sd.error                      
                lda sd_data_buffer+$0C           
                cmp #$02
                bne .init_sd.error                      

@reserved       lda sd_data_buffer+$0E     ; sectors to first FAT (number of reserved sectors)
                sta zp_temp+0   
                lda sd_data_buffer+$0F                  
                sta zp_temp+1   
                lda #$00
                sta zp_temp+2   
                jsr m.mult512              ; convert from sector length to address length 
                
                ldx #<sd_boot_sector       ; copy boot sector address to fat sector address... 
                ldy #<sd_fat_sector             
                jsr m.move2w_osd 
                
                ldx #<sd_fat_sector        ; ... and add reserved sector length                            
                ldy #>sd_fat_sector 
                jsr m.add2w

                ldx #3
@fat_length     lda sd_data_buffer+$24,x   ; sector length of FAT (for FAT32)
                sta sd_fat_length,x 
                sta zp_temp,x   
                dex 
                bpl @fat_length
                jsr m.mult512              ; convert from sector length to address length  

                ldx #<sd_fat_sector        ; copy fat sector address to root sector address... 
                ldy #<sd_root_sector            
                jsr m.move2w_osd                

                lda sd_data_buffer+$10     ; number of FAT's 
                sta zp_temp+5
@add_fat_length ldx #<sd_root_sector       ; add FAT length times number of FAT entries                            
                ldy #>sd_root_sector 
                jsr m.add2w
                dec zp_temp+5
                bne @add_fat_length             
                
@cluster_size   lda sd_data_buffer+$0D     ; sectors / cluster 
                sta sd_cluster_size
                               
                ldx #3 
@root_cluster   lda sd_data_buffer+$2C,x   ; root cluster number (typically 2, but is theoretically a 2word)
                sta sd_root_cluster,x 
                dex
                bpl @root_cluster 

I created a couple of helper math routines to help multiplying and adding 4 byte (double word) numbers. Nothing special about these, but just for completion they are listed below.

I will stop this post here. I had intended to cover everything needed to load a file, but it is already getting too long and I am late in publishing. So next blog post will cover the FAT and root sector, and finally we will load a file. However if you are impatient everything is in the complete code listing with a decent number of comments and you can always as questions in the comments.

;--------------------------------------------------------------------------
; copy 2word address to another location, both in os_data range   
; source/dest low byte pointer in XR/YR 
;--------------------------------------------------------------------------

m.move2w_osd    lda #>os_data                           ; both source and destination in the same page range 
m.move2w        sta zp_source+1
                sta zp_dest+1

                stx zp_source+0                         ; low byte of source/dest       
                sty zp_dest+0

                ldy #3
@move           lda (zp_source),y 
                sta (zp_dest),y 
                dey 
                bpl @move
                
                rts 
                
                
;--------------------------------------------------------------------------
; add 2word zp_temp to (zp_source) and store in (zp_source). 
; pointer to other 2word in XR/YR 
;--------------------------------------------------------------------------

m.add2w_512     lda #$00
                sta zp_temp+0                           ; store 512 in zp_temp
                sta zp_temp+2
                sta zp_temp+3
                lda #$02 
                sta zp_temp+1 

                
m.add2w         stx zp_source+0                         ; low byte in XR
                sty zp_source+1                         ; high byte in YR               
                ldy #0                                  ; note: cannot run a loop, since cpy would affect flag 

                clc
                lda (zp_source),y 
                adc zp_temp+0
                sta (zp_source),y 
                iny
                lda (zp_source),y
                adc zp_temp+1
                sta (zp_source),y 
                iny
                lda (zp_source),y
                adc zp_temp+2 
                sta (zp_source),y 
                iny
                lda (zp_source),y
                adc zp_temp+3 
                sta (zp_source),y

                rts 


;--------------------------------------------------------------------------
; left shift 2word in zp_temp. number of shifts in XR 
;--------------------------------------------------------------------------     

m.shift2w       asl zp_temp+0           
                rol zp_temp+1
                rol zp_temp+2
                rol zp_temp+3 
                dex 
                bne m.shift2w 
                rts 

                
;--------------------------------------------------------------------------
; multiply 2word in zp_temp with 512 
;--------------------------------------------------------------------------

m.mult512       asl zp_temp+0           
                rol zp_temp+1
                rol zp_temp+2
                lda zp_temp+2 
                sta zp_temp+3 
                lda zp_temp+1 
                sta zp_temp+2 
                lda zp_temp+0 
                sta zp_temp+1 
                lda #$00                                 
                sta zp_temp+0                                           
                rts

3 Replies to “Reading FAT data Part 1”

    1. I must admit this project has gone into hibernation for me. I am building another project with my sons … but in terms of what is next I would like to design a PCB based on this and build my own custom computer from there. I would consider some type of video output as well.

Leave a Reply

Your email address will not be published.