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.
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.
... 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.
SEND_CMD: begin state <= WAIT_ACK; end WAIT_ACK: begin state <= ack ? SEND_6502_MEM : WAIT_ACK; endAs can be seen we are only in SEND_CMD for a single clock cycle, before going to WAIT_ACK.
... 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
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
always @(posedge clk) begin if (pos_trigger) begin pause_6502 <= 1; end else if (state == IDLE) begin pause_6502 <= 0; end endWe basically assert the signal upon assertion of the start signal. Only once we are back at the state IDLE we release the assertion.
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 endThis is pretty self explanatory. Capture data when ready and shift out if in state SEND_6502_MEM.
... assign write_6502 = state == SEND_6502_MEM; ... assign read = state == SEND_CMD; ...
Wiring the DMA module to top module
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.
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.
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 endThe key here is write_6502_dma, which is a signal from our DMA module.
Writing some more 6502 Assembly
ldx #7 ldzero lda data+48,X sta 48,X dex bpl ldzeroJust 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.
LDA #0 STA $A0 STA $A1 LDA #6 JSR CMDThe pointer update we only need to do once. Also we don't need to make any changes to our CMD routine.
lda #$20 sta 50 lda #$15 sta 51 LDA #6 JSR CMDThis will read sector 2015(Hex). To do this, we just needed to adjust two bytes in the command entry we store in zero page.
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 DONEI 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).
No comments:
Post a Comment