Thursday, 27 April 2023

SD Card Access for a Arty A7: Part 9

Foreword

In the previous post we developed a DMA module for transferring a read sector from the FIFO in the SC Card module to the 6502 memory space. We also wrote some 6502 Assembly code for testing this functionality.

In this post we will write some more 6502 Assembly code for reading a file from a FAT32 partition.

32-bit operations

When trying to determine the location of a file on an SD Card, one often needs to work with 32-bit quantities. However, as you know the 6502 only works with 8 bits at a times. So, in order to make life simpler, let us start by writing some Assembly Routines for doing a couple of 32-bit operations.

Core of these routines we will imagine a virtual 32 bit accumulator, which we will store at address C0 hex in memory, and will use little endian format.

The first operation we need to define, is a Load Accumulator, which we will define with the symbol ld32. The address containing the data we want to store in the accumulator, must be stored in the X- and Y-registers:

ld32
     stx $b0
     sty $b1
     ldy #$0
     lda ($b0),y
     sta $c0
     iny
     lda ($b0),y
     sta $c1
     iny
     lda ($b0),y
     sta $c2
     iny
     lda ($b0),y
     sta $c3
     iny
     rts
First of we need to store the address in memory locations b0 and b1, so we can load the data from the memory location in an indexed addressing fashion. Here I do a bit of loop unrolling, saving a bit of CPU cycles. When this routine returns, our accumulator will contain the necessary data in memory locations c0, c1, c2 and c3.

The next routine we will will need is to store the contents of our virtual accumulator to some other memory location:

st32
     stx $b0
     sty $b1
     ldy #$0
     lda $c0
     sta ($b0),y
     iny
     lda $c1
     sta ($b0),y
     iny
     lda $c2
     sta ($b0),y
     iny
     lda $c3
     sta ($b0),y
     iny
     rts
Again, the destination address needs to be stored in registers X and Y, which we store in memory location b0 and b1 at the beginning of the routine.

All these routines so far are little endian. Our SD Card module, however, works with 32-bit LBA numbers are bug endian. So we need another variant of the Store Accumulator which can store the number as big-endian:

st32rev
     stx $b0
     sty $b1
     ldy #$3
     lda $c0
     sta ($b0),y
     dey
     lda $c1
     sta ($b0),y
     dey
     lda $c2
     sta ($b0),y
     dey
     lda $c3
     sta ($b0),y
     iny
     rts
When determining the location of a file, one 32-bit operation that valuable is add. In FAT32 we are presented with both 16-bit and 32-bit numbers to add, so we need routine for both:
 
add32
     stx $b0
     sty $b1
     ldy #0
     clc
     lda $c0
     adc ($b0),y
     sta $c0
     iny
     lda $c1
     adc ($b0),y
     sta $c1
     iny
     lda $c2
     adc ($b0),y
     sta $c2
     iny
     lda $c3
     adc ($b0),y
     sta $c3
     rts

add16
     stx $b0
     sty $b1
     ldy #0
     clc
     lda $c0
     adc ($b0),y
     sta $c0
     iny
     lda $c1
     adc ($b0),y
     sta $c1
     iny
     lda $c2
     adc #0
     sta $c2
     iny
     lda $c3
     adc #0
     sta $c3
     rts

Finding the root cluster

To find a file we need to loop through file entries in the root cluster. To determine the location of the root cluster, we need to load the bootsector of the FAT32 partition, which contains the necessary parameters for calculating this. The following code takes care of this:
 
mbr  equ $200
par1 equ $1be
lbastart equ 8
lbamemaddr equ mbr+par1+lbastart 

       ldx #<lbamemaddr
       ldy #>lbamemaddr
       jsr ld32
       ldx #48
       ldy #0
       jsr st32rev
       LDA #6
       JSR CMD
       LDA #$12
       STA $FB0B
       LDA #$16
       STA $FB0B
Let us start by breaking down the EQU's a bit. $200 is the address in 6502 memory space where we previously downloaded the MBR from the SD Card.

The value $1be is the offset within the MBR containing the first Partition entry. Byte 8 of every partition contains the LBA number of the sector of the partition.

So, basically we need to store this LBA block number to address 48, which contains the LBA address that we will instruct the SD Card module to read from the SD Card. The bootsector will end up at address $400.

Now we are ready to calculate the LBA block number of the root cluster. From the previous posts, we basically calculate this with the following formula: Bootsector location + reserved sectors + Number of FATs * Sectors per FAT.

From the previous snippet of code, we still have the Bootsector location stored the virtual accumulator at address $C0, so we can just continue to add the number of reserved sectors and so on to get to the location of the root cluster.

Firstly, adding the reserved sectors:

...
bootsec    equ $400
reservedsec equ bootsec+$e
...
       ldx #<reservedsec
       ldy #>reservedsec
       jsr add16
...
As can be seen, the location of the Reserved Sectors is at $e in the Bootsector and is two bytes, so we need to use add16.

Next we need to add Sectors per fat a number of times as specified by Number of FATs:

...
numfat     equ bootsec+$10
secperfat  equ bootsec+$24
...
       ldx numfat
addfat
       txa
       pha
       ldx #<secperfat
       ldy #>secperfat
       jsr add4
       pla
       tax
       dex
       bne addfat 
...
With this we have the calculated LBA for the root cluster. This number we need to store again at address 48, which will instruct the SD Card core to load the root cluster sectors. We also need to make a backup of this number as well for future calculations:

       ldx #48
       ldy #0
       jsr st32rev
       ldx #$c4
       ldy #0
       jsr st32

Searching for the file

With the root cluster location determined, we now need to loop through all the file entries to find the file we are looking for. If we just have a look at the purpose of all this, we want bootable ROM code of minimum size in block rom, and then load the rest of the boot code from the SD card.

So, we will always load a file with hardcoded filename 'boot.bin'. This filename will form of part of the bootrom in top of memory, defined as:

FILENAME
     .TEXT "BOOT    BIN"
It may look a bot strange with the extra white space between filename and extension, but this is how filenames are stored in file entries in FAT32 partitions. When looping through the file entries we need to compare each filename with the above.

Since we need to do so many compare operations, it makes sense to move the text boot.bin into zero page:

       ldx #10

initfilename
       lda FILENAME,x
       sta $d0,x
       dex
       bpl initfilename 
I have become into the habit of when needing to iterate through a number of memory locations, I am doing it in the reverse order. It just eliminates the need to have a compare operation with every loop iteration.

There is quite a number of things that needs to happen when iterating through file entries. You need to read sector by sector of the root cluster. Then, each sector you need to process all entries. Also, what is complicating things is that a sector is 512 bytes in size, whereas the 6502 works with pages of 256 bytes in size. So, one also needs to keep track of how many times a 256 byte page boundary is crossed to figure out when to load the next sector.

All this calls for a nested loop that is a number of levels deep. Here is some pseudo code for the nested loop:

for sectors = 1 to ...
   read sector
   for page = 0 to 1
      for fileentry = 0 to 7
        get file entry
        do something with file entry
      end
   end
end   
Each file entry is 32 bytes, so in a page of 256 bytes, there is 8 entries. For that reason we are looping from 0 to 7 in innermost loop.

Let us do some initialisation:

nextsec
       LDA #6
       JSR CMD
       LDA #$12
       STA $FB0B
       LDA #$16
       STA $FB0B

       lda #0
       sta $b2
       lda #4
       sta $b3
We start with some code to load a root sector into memory, where the sector number is stored in addresses 48 - 51, as explained previously. The addresses b2/b3 contains the address at which the root sector is stored, which is $0400. We will be incrementing b2/b3 as we loop through the file entries.

Next, let us write some code for looping through the file entries:

nextentry
       clc
       lda $b2
       adc #32
       sta $b2
       bcc inspectfileentry
       inc $b3
       lda #1
       and $b3
       bne inspectfileentry
       inc 51
       jmp nextsec
In this snippet inspectfileentry is where we do something with the current file entry. Basically to get to the next entry we keep adding 32 to the address in b2/b3.

However, we need to mindful of when we cross a page boundary, that is when the carry flag gets set. IN such a case we increment b3 and then inspect bit 0 of b3. When bit 0 is a 1, it means we are at byte 256 of 512 bytes, and we are still good to go.

However when we increment b3 and bit 0 is 0, it means we just passed the 512'th byte of the sector we are reading. In this case it is time to read the next sector from SD card. We do this by incrementing address 51, which is part of the 48-51 LBA number.

Finally, let us implement inspectfileentry:

inspectfileentry
       ldy #11
       lda ($b2),y
       cmp #15
       beq nextentry
loopfilesearch
       dey
       bmi done
       lda ($b2),y
       cmp $d0,y
       beq loopfilesearch
Again, we are working backwards. We start by inspecting the byte following the filename/extension, which contains all the attributes. With this entry we check if this file entry forms part of a long file entry. If it is we skip to the next entry.

We then check the filename entry byte by byte to see if it matches 'boot.bin'. If it matches, we jump to done and load the file into memory.

Loading the file

With the file entry for the file we want, we now have our hands the cluster number where the file starts. This cluster number is located at bytes 26, 27, 20 & 21 of the file entry. With a cluster number we always need to subtract 2 to get the physical cluster position. So, let us load the virtual accumulator with the cluster number and do the subtraction:

DONE
       ldy #26
       sec
       lda ($b2),y
       sbc #2
       sta $c0
       ldy #27
       lda ($b2),y
       sbc #0
       sta $c1
       ldy #20
       lda ($b2),y
       sbc #0
       sta $c2
       ldy #21
       lda ($b2),y
       sbc #0
       sta $c3
In all the code written in this post, we are only doing one subtraction, so I didn't deemed it necessary to create a routine for this process.

A this point we should remember that we have a cluster number and not a sector number. As a cluster contains multiple sectors we need to multiply this number by the number of sectors per cluster, which is byte 13 of the boot sector. In my experience this parameter is usually a power of 2, so we can achieve multiplication by shifting the cluster number by a number of bit positions, with this assumption.

Obviously, we need to determine upfront how many left shifts is required for this operation. We need to do this while the bootsector is still in memory:

addfat
       txa
       pha
       ldx #<secperfat
       ldy #>secperfat
       jsr add32
       pla
       tax
       dex
       bne addfat 

       ldx #48
       ldy #0
       jsr st32rev
       ldx #$c4
       ldy #0
       jsr st32

       lda sectorspercluster
       ldx #0
       clc
shift
       ror a
       bcs endshift
       inx
       bcc shift
endshift
       stx $c8
You will recognise this code from an earlier section, of which I have just appended some extra code. We just keep shifting the parameter right until the carry flag is set, keeping count how many shifts is required. This required number of shifts we store in location $c8.

With this calculated, we can now move back to the spot where we loaded our virtual accumulator with the cluster number of our file.

With this number we do a number of right shifts, implying the multiplication I was referring to earlier:

       clc
       ldx $c8
conv
       rol $c0
       rol $c1
       rol $c2
       rol $c3
       dex
       bne conv

Now we have the relative sector number where our file begins. We still need to add the location of the root cluster number to get the absolute cluster number. We previously stored this number at location $c4, so we can do the addition like this and load the first sector of the file into memory:

       ldx #$c4
       ldy #0
       jsr add32
       ldx #48
       ldy #0
       jsr st32rev
       LDA #6
       JSR CMD
       LDA #$12
       STA $FB0B
       LDA #$16
       STA $FB0B
After loading this sector of the file, one can also jump to it with JMP $400.

Testing

To test that all this functionality really work on a physical board, we can write a 6502 program in boot.bin that flashes an LED. There is some spare bits in the register ignore_reads that we can use. For this purpose we will be using bit 5 of this register. One also need to map this bit via the XDC constraint file, to an led on the board.

The following snippet will do the flashing:

    lda #0
    sta $0
    clc
    ldx #0
    ldy #0
    lda #0
loop
    inx
    bne loop
loop2
    iny
    bne loop
loop3
    adc #1
    cmp #60
    bne loop
    lda #$20
    eor $0
    sta $FB0B
    sta $0
    lda #0
    beq loop
I have added a couple of nested loops to slow down the flashing enough so the flashing can be visible to the human eye. One needs to assemble this snippet and store as boot.bin on the root directory on the SD Card.

I followed this process and can confirm that the LED flashes on my board 😀

In Summary

In this post we wrote some more 6502 assembly code to read the sector of a file stored on a FAT32 partition.

In the next post we will be revisiting our DDR3 core and see if we can get our 6502 based design to use DDR3 RAM rather than block RAM. This will bring us one step closer in trying to run an Amiga core on an Arty A7, using a 6502 to load all the ROM and images into memory.

Until next time!

No comments:

Post a Comment