Now that we can send and receive bytes using the SPI protocol it is time to begin accessing the SD card itself. In this post I will go through what it needed to initialize the SD Card and in the next post go into the FAT format so we can read files from the SD Card. My implementation is not fully general, but should support most newer cards and you can find the entire code here.
I found this series of blog posts from Lucky Resistor helpful in understanding the SD commands, responses, and data format. The pictures were especially helpful in getting an overview of what is being sent back and forth and I would highly encourage you to read Part 2 and Part 3 in combination with this post of mine. There is also a great overview here that would be a good pre-read to this post and the whole initialization process is summarized in this flow chart.
The procedure to initialize the SD Card is as follows
- Put card into SPI mode, by sending at lest 74 clock pulses
- Send Command 0 repeatedly until card is idle (resets)
- Send Command 8 and check for the right response
- Send AMCD41 command to initialize newer / high capacity cards
- Send Command 58 to check working voltage range
- Finally send Command 16 to ensure block size is 512 bytes
The first step is sending at least 74 clock pulses (I send 80 as the shift register triggers 8 clock pulses every time we send a byte). For this first step you have to set CS (slave select for the SD card) high and MOSI should also be 1 during this procedure. For all “normal” commands you should of course select the SD card by pulling CS low, but the first step is different. The follow code achieves this (see SPI Part 2 for spi.send_byte etc. or you can find the full code here)
sd.init jsr spi.set_output ; setup for SPI output lda #(SPI_OUTPUT+SPI_NOSS) ; set SR to output and SD card CS = 1 (special first time init) sta VIA1_PORTB ldx #10 ; send 80 clock signals to initialize SPI mode for SD card @init0 lda #$FF ; MOSI = 1 and CS = 1 jsr spi.send_byte dex bne @init0
Note that this initialization should be run at between 100-400 kHz. You can subsequently (on newer cards) increase the speed up to about 25 MHz. Check my previous post for notes on how to vary SPI clock speed and setting it to the max of 1/2 the system clock speed.
Sending a command to the SD card follows a 6 byte format. The first byte is the command number with bit 7 = 0 and bit 6 = 1. E.g. for command 8 the first byte is $48. This is then followed by 4 bytes that are the argument for the command, and finally a CRC (error checking) byte. The CRC byte is optional for SPI mode, with some exceptions that we cover later. Before sending a command you should check that the SD card is idle, indicated by receiving $FF. After sending the command the SD card will send one of several responses, depending on the command. The first byte of the response (at least for the commands used here) will contain several flags and have bit 7 = 0. For many commands this is the only response (denoted R1).
sd.send_cmd ora #$40 ; set bit 6 = 0 for a command sta sd_cmd_dat+5 ; store at end of command data (we send from end due to dex) @set_crc ldx #$FF ; standard CRC for most commands cmp #$48 ; cmd8 has a different CRC bne @store_crc ldx #$87 ; crc for cmd8 @store_crc stx sd_cmd_dat+0 ; store at start of command data (we send from end due to dex) @check_idle jsr spi.set_input jsr spi.get_byte cmp #$FF ; idle = $FF bne .sdcmd_error ; clear carry and return @wait lda VIA1_IFR ; check IRQ flag and #%00000100 ; check for SR flag beq @wait sd.send_cmdx jsr spi.set_output ; setup SPI to output ldx #5 @send_cmd lda sd_cmd_dat,x jsr spi.send_byte dex bpl @send_cmd sd.get_response jsr spi.set_input ldx #10 ; wait for response (should be within 8 bytes) @get_byte jsr spi.get_byte bpl .sdcmd_return ; first bit of first byte of response will always be 0 dex ; retry bne @get_byte .sdcmd_error ora #$80 ; return negative value on error .sdcmd_return sta sd_cmd_dat+0 ; save return value for later retrieval rts
Couple of comments on the above. Since a valid R1 response from the SD card always has bit 7 = 0 I return a negative value in case of a failure. Also in the above you will notice that the command bytes are stored in the “opposite” sequence (e.g. the first byte to send is stored in the 6th byte of the command data). I do this in general to optimize my indexed loops, so I dont have to do a compare at the end. Doing “dex, bne” saves both time and space over “inx, cpx #val, bne”. Most of the times it is possible to design your code so a loop runs from max value down to zero.
With the code for sending a command to the SD card in place we can now proceed with the actual initialization process. Firstly (and perhaps most importantly) we send command 0 repeatedly until the card is in idle state. I found that if a previous command was not completed this can take quite a number of retries. For instance if resetting the computer while there are still 100’s of bytes left to receive from a read command command 0 needs to be sent dozens of times to get to idle state.
Since the card might not initially be in idle mode we cant wait for the idle byte to be received, as we generally do when sending commands. Therefore I jump to sd.send_cmdx to skip this step in the code below. Also for the initialization process it is recommended to send the correct CRC byte ($95) for command 0.
ldy #100 ; number of retries to get card in idle state @cmd0 lda #0 ; command 0 (go to idle) - send without waiting for idle byte! sta sd_cmd_dat+4 sta sd_cmd_dat+3 sta sd_cmd_dat+2 sta sd_cmd_dat+1 ldx #$95 ; crc for cmd0 stx sd_cmd_dat+0 ; store at start of command data (we send from end due to dex) ora #$40 ; set the command bit sta sd_cmd_dat+5 ; store at end of command data (we send from end due to dex) jsr sd.send_cmdx ; call send command w/o checking idle cmp #$01 beq @cmd8 ; returns 1 when i idle state dey ; retry cmd0 bne @cmd0 beq .init_sd.error
Next step is to “send if cond” or command 8 with a special argument of $000001AA and the correct CRC code of $87. The $AA is a pattern that the card should repeat back on success. It could be any pattern, but $AA is the standard one ($AA = %10101010). If we have a type 2+ card the 4th byte of the response should be our pattern (response to command 8 is the R1 byte plus 4 more). If we have an older card then the initialization process is slightly different and I have chosen not to support this (at least for now).
@cmd8 ;lda #$00 ;sta sd_cmd_dat+4 ;sta sd_cmd_dat+3 lda #$01 sta sd_cmd_dat+2 lda #$AA ; pattern for response sta sd_cmd_dat+1 lda #8 ; command 8 = send if cond jsr sd.send_cmd ;cmp #$01 ; skip error checking as response will trigger error if there is one ;bne @error jsr spi.get_byte ; response should be $00, $00, $01, $AA jsr spi.get_byte jsr spi.get_byte jsr spi.get_byte cmp #$AA bne .init_sd.error
Since the command data already contains zeros in the 3rd and 4th location from command 0 I skip setting them. It’s just second nature for me to optimize for size and speed in 6502 assembly, but I leave the code commented for readability and if the proceeding code should change and the assumption is no longer valid. I also skip checking of we got the right R1 response (should be $01) since if there is an error then we wont get the right pattern back either and that will trigger the error. Again just second nature to save bytes and cycles.
With a newer card we then need to send it a special application command to indicate we support SDHC and SDCX cards and enable high speed transmission. Command 55 tells the card that the next command is an application command. The application command we need is AMC41 with bit 30 set in its argument ( $40000000). This sequence might also have to be repeated a number of times until the SD card has executed the command. When the card is ready the AMC41 command will return zero.
ldy #200 ; number of retries until card is ready @cmd55 lda #$00 ; command arguments sta sd_cmd_dat+4 sta sd_cmd_dat+3 sta sd_cmd_dat+2 sta sd_cmd_dat+1 lda #55 ; command 55 = application command jsr sd.send_cmd ;cmp #$01 ;bne @error @amc41 lda #$40 ; command arguments sta sd_cmd_dat+4 ;lda #$00 ;sta sd_cmd_dat+3 ;sta sd_cmd_dat+2 ;sta sd_cmd_dat+1 lda #41 ; AMC41 when following command 55 jsr sd.send_cmd beq @cmd58 dey ; retry until card is ready bne @cmd55
The final steps are to send command 58 (read OCR) to check that the card support the right voltage supply ranges. This is optional and could be skipped entirely if your supply voltage is 2.7V to 3.6V. I still send the command just in case, but ignore the response. Lastly we need to send command 16 to set the block size to 512 bytes to ensure compatibility with FAT. This is done by setting the argument to $00000200 = 512.
@cmd58 lda #$00 sta sd_cmd_dat+4 ;sta sd_cmd_dat+3 ;sta sd_cmd_dat+2 ;sta sd_cmd_dat+1 lda #58 ; command 58 = read OCR jsr sd.send_cmd bne .init_sd.error jsr spi.get_byte ; dont use this response for anything jsr spi.get_byte ; is used to check that voltage is ok for this card jsr spi.get_byte jsr spi.get_byte @cmd16 ;lda #$00 ;sta sd_cmd_dat+4 ;sta sd_cmd_dat+3 ;sta sd_cmd_dat+1 lda #$02 ; 512 bytes size = $00000200 sta sd_cmd_dat+2 lda #16 ; command 16 = set block size jsr sd.send_cmd bne .init_sd.error
With this the card should be initialized and ready to read and write to using the FAT file format. I repeat this initialization each time I begin an operation with the SD card, just in case the card has been removed and/or replaced with another one. There might be a way of detecting that a card has been ejected so that you can limit the initialization to once. If someone knows how to do this I would love to hear it in the comments.
The full code incl. reading files in the FAT format can be found here. Feel free to skip ahead if you cant wait for the next post on the FAT file format.
One Reply to “Accessing the SD Card”