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!

No comments:

Post a Comment