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!

Monday 3 April 2023

SD Card Access for a Arty A7: Part 8

Foreword

In the previous post we did some deeper exploring into FAT32 and write some high level code to read a file from an example FAT32 partition.

In this post we will start to attempt the same exercise, but with the goal of writing the code in 6502 assembly. However, there is one hurdle that is holding us back to jump straight into writing 6502 code for this exercise, and that is that the read sector data lives in a FIFO buffer within the SDCard core, and not within the memory space of the 6502.

Now, I have pointed out in a previous post that the Sd Card core does have a register in which you can access the FIFO buffer 32 bits at a time. It would be possible to write some 6502 for reading the contents of the FIFO buffer and storing it in 6502 memory space, but this would be quite messy.

What I am thinking is rather to write a DMA (Direct Memory Access) module. This module would then call the FIFO read register repeatedly, and then take the 32 bit data and write it in a byte-by-byte fashion to the 6502 memory space. This will simplify the 6502 assembly code somewhat.

So, in this post we will be developing the DMA module. 

The DMA State Machine

As with so many things one develops in Verilog, one often finds the need to implement your requirements by means of a state machine. Our DMA module is no exception to this.

Let us start by listing the states our state machine it needs, listing the order it needs to transition to:

  • IDLE
  • START: This state will be triggered by 6502 assembly code, indicating that the DMA transfer should happen. When the DMA module starts the transfer process, the 6502 CPU needs to be paused via RDY line, to avoid simultaneous writes to memory. In my design I will leave some headroom, waiting a number of cycles after de-assertion of RDY line, before starting the DMA transfer.
  • SEND_CMD: Send read command to SD Card core, to get 32 bits of data from FIFO. The state machine will always remain just one clock cycle within this state
  • WAIT_ACK: Wait for ack signal from SD Card core. This indicates that data is ready and need to be captured by our DMA core
  • SEND_6502_MEM: Send the captured 32 bits of data to the 6502 memory space. While in this state the data is transferred to 6502 memory space one byte at a time. Once all 32 buts transferred, the state machine will transition to either IDLE or SEND_CMD, depending on whether the full 512 bytes has been transferred to 6502 memory space.
This gives us a high level overview of all the states involved. Let us now focus more on each individual state transition.

The transition from IDLE to START should be triggered by the 6502. So, let us start by adding an input port for this to our module:

module dma(
  input wire start
    );
At first sight one might think that one can just change state to START if this port is high. However, the 6502 might not get a chance to set this port low again, because it will be frozen for the duration of the DMA transfer. If, after the transfer the 6502 still cannot in time set it to low, the DMA will initiate another transfer and freeze the 6502 again.

So, it is actually better to rather trigger the transition from IDLE to start only at the point where the input port transition from low to high.

Let us write some code for this:

...
 always @(posedge clk)
 begin
     start_delayed <= start;
 end
...
assign pos_trigger = start && !start_delayed;
always @(posedge clk)
begin
    case (state)
        IDLE: begin
             state <= pos_trigger ? START : IDLE;
           end
        START: begin
             state <= count == 0 ? SEND_CMD : START;
           end
    endcase 
end
...
The START state only transition to the next state if a counter has expired, to give some headroom as I explained earlier. 

Next, let us have a look at the state of sending a read command to SD Card, and waiting for acknowledge when data is ready:

        SEND_CMD: begin
             state <= WAIT_ACK;
           end
        WAIT_ACK: begin
             state <= ack ? SEND_6502_MEM : WAIT_ACK;
           end
As can be seen we are only in SEND_CMD for a single clock cycle, before going to WAIT_ACK.

Finally we have our SEND_6502_MEM state. We basically will linger in this state until all 32 bits are transferred to 6502 memory space a byte at a time. Once these 4 bytes has been transferred, we either jump so state IDLE or to SEND_CMD, depending on whether we have transferred the full 512 bytes of the FIFO buffer.

This branch from the state SEND_6502_MEM require us to maintain two counters, one for keeping track how far we have shifted the 32 bits of data and how much of the 512 bytes of data has been transferred.

... 
always @(posedge clk)
 begin
   if (ack && state == WAIT_ACK)
   begin
     shift_count <= 3;
   end else if (state == SEND_6502_MEM)
   begin
     shift_count <= shift_count - 1;
   end
 end
...
 always @(posedge clk)
 begin
   if (state == IDLE)
   begin
       address_6502 <= 0;
   end else if (address_6502 < 512 && state == SEND_6502_MEM) begin
       address_6502 <= address_6502 + 1;
   end
 end
...
With these two counters defined, we can now create the SEND_6502_MEM selector in our state machine:

        SEND_6502_MEM: begin
             if (shift_count == 0 && address_6502 != 511)
             begin
                 state <= SEND_CMD;
             end else if (shift_count != 0)
             begin
                 state <= SEND_6502_MEM;
             end 
             else
             begin
                 state <= IDLE;
             end
           end

The Remaining Verilog bits

The state machine we defined in the previous section forms the heart of our DMA module. However, we still need to write some more Verilog code for this module to glue everything together and to do something useful.

First let us create a complete list of ports our DMA module will need:

module dma(
  input wire [31:0] wb_data,
  input wire clk,
  input wire ack,
  input wire start,
  output wire read, 
  output reg pause_6502 = 0,
  output wire [7:0] o_data,
  output reg [15:0] address_6502,
  output wire write_6502
    );
Here is a quick description of the different ports:

  • wb_data: FIFO read data from the SD Card module. Returned when we issue a read command.
  • ack: Signal from SD Card module, indicating data requested is ready.
  • read: Signals the top module we want to do a dma read from the SD Card module.
  • pause_6502: Pause the 6502 so that we transfer a sector of data
  • o_data: 8 bits of sector data to write to 6502 memory data
  • address_6502: This is in actual fact the counter defined earlier on and is also used in writing data to 6502 memory space.
  • write_6502: perform a write to 6502 memory space. This is accompanied with the ports o_data and address_6502
For the above output ports we need to write some code for populating them with values. Let us start with the port pause_6502:

 always @(posedge clk)
 begin
     if (pos_trigger)
     begin
        pause_6502 <= 1;
     end else if (state == IDLE) 
     begin
        pause_6502 <= 0;
     end
 end
We basically assert the signal upon assertion of the start signal. Only once we are back at the state IDLE we release the assertion.

Next, let us tackle o_data. This port is 8 bits wide, whereas we receive the data in 32-bits, so we will to implement some kind of shift register, which we implement as follows:

 always @(posedge clk)
 begin
   if (ack && state == WAIT_ACK)
   begin
       captured_data <= wb_data;
   end else if (state == SEND_6502_MEM)
   begin
       captured_data <= {captured_data[23:0], 8'h0};
   end
 end
This is pretty self explanatory. Capture data when ready and shift out if in state SEND_6502_MEM.

I would like to point out, though, that it is not enough to capture the data why checking ack alone. This signal is also asserted when the 6502 send commands to the SD Card module as well. Therefore we need to check if we are in state WAIT_CMD as well.

There remains two output ports we need to do: read and write_6502. These ports are relatively straightforward:

...
assign write_6502 = state == SEND_6502_MEM;
...
assign read = state == SEND_CMD;
...

Wiring the DMA module to top module

With the DMA module fully developed, we need to interface it with the rest of the system. First let us have a look at the ports of our 6502:

cpu cpu( .clk(gen_clk), .reset(count_down > 0), .AB(cpu_address), .DI(combined_data), 
  .DO(cpu_data_out), .WE(we_6502), .IRQ(0), .NMI(0), .RDY(!(wait_read || pause_6502) ));
With reference to DMA, we are only interested in the RDY signal. We basically to an OR here with the existing RDY singal in the system, as well as the pause_6502 signal from our DMA module.

Next, let us move onto the effected ports in the SD Card Module:

sdspi  sdspi (
...
            .i_wb_stb(dma_read ? 1 : wb_stb), 
		.i_wb_addr(dma_read ? 2 : cpu_address[3:2]),
...
	);
In both these ports we multiplex via dma_read between read instructions from 6502 and the dma module.

We blindly assert port i_wb_stb when dma_read is true. Also for the port i_wb_addr we assert the address 2 if dma_read is true. Address 2 instructs a read from the FIFO buffer.

Finally we need to modify of our block RAM logic for the 6502 memory space so it can be written to by both the 6502 and the DMA module:

     assign ram_6502_addr = write_6502_dma ? {ignore_reads[4:3], ram_6502_addr_out[8:0]} : cpu_address;

     always @ (posedge gen_clk)
       begin
        if ((we_6502 & cpu_address[15:9] == 0) || write_6502_dma) 
        begin
         ram[ram_6502_addr] <= write_6502_dma ? o_data : cpu_data_out;
         ram_out <= write_6502_dma ? o_data : cpu_data_out;
        end
        else 
        begin
         ram_out <= ram[cpu_address];
        end 
       end 
The key here is write_6502_dma, which is a signal from our DMA module.

ram_6502_addr is the address we use writing to 6502 memory. We will notice that in the DMA version of the write address, we are making use of bits 4 and 3 from the ignore_reads register, a register we developed in a previous post and to which the 6502 can write to.

By writing to these two bits in ignore_reads, we can control where in memory the dma data will end up, but on a 512 byte boundary.

Writing some more 6502 Assembly

With the Verilog changes completed for our DMA, let us write some 6502 code to utilise it.

I want to start off by highlighting a limitation with our current setup. Currently all the sequence of bytes for the different SD Card commands, including the reading of a sector, is stored as table in ROM. For the sector read command, this is problematic since the sector number you want to read is also present in ROM, meaning you cannot read a different sector than currently present in the ROM.

Not very, useful, is it? 😂 To get around this, we will need to copy the sector read entry from the table to RAM, which will allow us to change the sector number for a read command. For this purpose, I am going to use zero page:

       ldx #7
ldzero lda data+48,X
       sta 48,X
       dex
       bpl ldzero
Just to recap from previous posts. data is the beginning of the mentioned table in ROM. The sector read command is entry number 6, and with each entry being 8 bytes, we come up with number 48.

In this code snippet I decided to do the copy in the reverse order. If you copy in ascending byte order, you will need to have an extra compare instruction to test whether X reached the target value. Descending order avoids the compare, and your branch instruction can just keep the loop going until x becomes negative.

Now, as soon as we are past the point of SD Card initialisation and we want to read one or more sectors, we need to change the table pointer from ROM to zero page. You might remember from previous posts that we use address A0 for our table pointer, which will result in the following code:

       LDA #0
       STA $A0
       STA $A1
       LDA #6
       JSR CMD
The pointer update we only need to do once. Also we don't need to make any changes to our CMD routine.

To read a different sector, we can just write code like this:

       lda #$20
       sta 50
       lda #$15
       sta 51
       LDA #6
       JSR CMD
This will read sector 2015(Hex). To do this, we just needed to adjust two bytes in the command entry we store in zero page.

So, with the sector being read and present in the FIFO buffer, we need to write some 6502 code to instruct our DMA to transfer data from FIFO buffer to 6502 memory space. The following snippet gives an example of reading two separate sectors into 6502 memory:

       LDA #6
       JSR CMD
       LDA #$e
       STA $FD0B
       nop
       nop
       lda #$20
       sta 50
       lda #$15
       sta 51
       LDA #6
       JSR CMD
       LDA #$12
       STA $FD0B
       LDA #$16
       STA $FD0B

       nop
       nop

DONE
       JMP DONE

I have bolded the sections that performs the DMA transfers. Let us start by having a look at the first transfer, which is initiated by writing $e to the register $FD0B. Looking at the individual bits, setting bit 2 to one, will initiate the transfer. Bits 3 and 4 gives us the value 01, meaning the transfer will be between addresses 512 and 1024 (e.g. 512 byte page 1).

Let us have a look at the second transfer. Here we see two separate writes to the register $FD0B. Comparing the writes we see that happens is setting bit 2 to zero to then to one. This is to create a positive transition, which triggers the DMA transfer. We see bits 3-4 gives us the value 10 binary, which is 512 byte page two, which is present at addresses 1024 to 1535. So the sectors we read will be next to each other.

You will also note that after every DMA trigger, I am adding two nop instructions. This is because of some anomaly I discovered with the 6502 core and the RDY signal. When our DMA core assert the RDY signal, the 6502 core somehow skips the next byte, which is supposed to be the next instruction opcode. I solved this issue by just adding a nop instruction after the STA $FD0B, so if an opcode byte is skipped, it is at least a meaningless one.

This concludes our discussion on the 6502 that needs to be written for triggering the DMA core.

In Summary

In this post we developed a DMA core for transferring sector data stored in the FIFO buffer of the SD Card module to 6502 memory space.

We also wrote some 6502 code for triggering a DMA transfer.

In the next post we will continue to write 6502 Assembly code for reading a file from a FAT32 partition.

Until next time!