Simple FAT and SD Tutorial Part 4
In the previous parts of this tutorial, we have built both an FAT library as well as a test program to communicate with the SD card. Now it’s time to wrap it up with final parts of code to read a file from SD card and print it out.
This part will utilize the previously made FAT library with a few tweaks – it turned out that some byte and word-sized values needed explicit casting to unsigned long
so that the calculations worked as they should. The new library and all code shown here can be found from the updated project zip.
Initializing the SD card automatically
Instead of manually pressing 1, 2, and 3, we’ll now write a single function to initialize the SD card to SPI mode. For standard SD (not high capacity SDHC) cards, it’s enough to:
- Send clock pulses for 80 cycles (“read” 10 bytes)
- Send command 0x40 (it should return 1)
- Send command 0x41 until it returns 0 (it returns 1 while the card is busy)
- Send command 0x50 to set read block size (as well as write block size)
Here’s the code to do just that (sd_sector and sd_pos will be used shortly):
unsigned long sd_sector;
unsigned short sd_pos;
char SD_init() {
char i;
// ]r:10
CS_DISABLE();
for(i=0; i<10; i++) // idle for 1 bytes / 80 clocks
SPI_write(0xFF);
// [0x40 0x00 0x00 0x00 0x00 0x95 r:8] until we get "1"
for(i=0; i<10 && SD_command(0x40, 0x00000000, 0x95, 8) != 1; i++)
_delay_ms(100);
if(i == 10) // card did not respond to initialization
return -1;
// CMD1 until card comes out of idle, but maximum of 10 times
for(i=0; i<10 && SD_command(0x41, 0x00000000, 0xFF, 8) != 0; i++)
_delay_ms(100);
if(i == 10) // card did not come out of idle
return -2;
// SET_BLOCKLEN to 512
SD_command(0x50, 0x00000200, 0xFF, 8);
sd_sector = sd_pos = 0;
return 0;
}
Reading data from SD card
Reading data from SD cards (as well as writing it) is done in discrete blocks. The read command is given the offset of data to be read, and it returns X bytes, where X is the size of transfer block set during initialization. For normal SD cards, block size can be set to smaller than the 512 used in the initialization code, but for higher capacity SDHC and SDXC cards the block size is always 512 bytes, so I decided to use that so there would be less code changes in case I wanted to support SDHC cards later on.
Because I wanted to support systems that have only 128 or 256 bytes of SRAM (if you remember from earlier parts, the FAT library uses a 32 byte buffer), reading all 512 bytes into memory is not possible. Instead, we capture a small window of the data. For example, to read bytes 32-63 (zero-based indexing, so 32 bytes starting from 33rd byte) from a 512 byte sector, we issue a read command for the whole sector, but discard the first 32 bytes, then capture the next 32 bytes, and then again skip the remaining 448 bytes plus 2 CRC bytes. Here’s the read command:
// TODO: This function will not exit gracefully if SD card does not do what it should
void SD_read(unsigned long sector, unsigned short offset, unsigned char * buffer,
unsigned short len) {
unsigned short i, pos = 0;
CS_ENABLE();
SPI_write(0x51);
SPI_write(sector>>15); // sector*512 >> 24
SPI_write(sector>>7); // sector*512 >> 16
SPI_write(sector<<1); // sector*512 >> 8
SPI_write(0); // sector*512
SPI_write(0xFF);
for(i=0; i<10 && SPI_write(0xFF) != 0x00; i++) {} // wait for 0
for(i=0; i<10 && SPI_write(0xFF) != 0xFE; i++) {} // wait for data start
for(i=0; i<offset; i++) // "skip" bytes
SPI_write(0xFF);
for(i=0; i<len; i++) // read len bytes
buffer[i] = SPI_write(0xFF);
for(i+=offset; i<512; i++) // "skip" again
SPI_write(0xFF);
// skip checksum
SPI_write(0xFF);
SPI_write(0xFF);
CS_DISABLE();
}
The read command (0x51) is much like all other SD commands and it takes a 32-bit address – we use the sector number and multiply it by 512 (sector<<9
). Note that SDHC cards use sector addressing and not byte addressing, so this would not be needed for SDHC. After sending the command, SD card responds with “0” to indicate the command has been received, and then it sends 0xFF until data is ready, at which point it sends 0xFE, then the 512 bytes of data and finally 2 bytes of checksum. Pretty straightforward!
Providing FAT library disk functions
As you probably recall, we now need to provide FAT library two functions to navigate around the SD card: fat16_seek()
and fat16_read()
. Now that we have our flexible SD_read()
function, it’s really just a matter of keeping two pointers, current SD sector (sd_sector
) and offset within that (sd_offset
), which we just set when the library wants to seek, and pass to SD_read
when the FAT library wants to read bytes (and increment the pointers afterwards). Without further ado, here are the wrapper functions:
void fat16_seek(unsigned long offset) {
sd_sector = offset >> 9;
sd_pos = offset & 511;
}
char fat16_read(unsigned char bytes) {
SD_read(sd_sector, sd_pos, fat16_buffer, bytes);
sd_pos+=(unsigned short)bytes;
if(sd_pos == 512) {
sd_pos = 0;
sd_sector++;
}
return bytes;
}
Note that the read function can be very simple because we know the reads never cross sector boundaries – the FAT library reads 32 bytes at a time, and 512 is a multiple of that. If that was not true, the read function would need some additional logic to deal with it.
Wrapping it all up
We now have all the helper functions we need. We have the UART. We speak through the SPI. We command the SD card. We understand the FAT. Here’s a simple main function to enjoy our accomplishments through reading a file called README.TXT
and displaying it over UART:
int main(int argc, char *argv[]) {
char i, ret;
short offset = 0x1B0;
USARTInit(64); // 20 MHz / (16 * 19200 baud) - 1 = 64.104x
SPI_init();
uwrite_str("Start\r\n");
if(ret = SD_init()) {
uwrite_str("SD err: ");
uwrite_hex(ret);
return -1;
}
if(ret = fat16_init()) {
uwrite_str("FAT err: ");
uwrite_hex(ret);
return -1;
}
if(ret = fat16_open_file("README ", "TXT")) {
uwrite_str("Open: ");
uwrite_hex(ret);
return -1;
}
while(fat16_state.file_left) {
ret = fat16_read_file(FAT16_BUFFER_SIZE);
for(i=0; i<ret; i++)
USARTWriteChar(fat16_buffer[i]);
}
return 0;
}
Here’s the output from our glorious test program:
That’s all! You can now read SD cards with your ATmega. And best of all, you should also understand every nook and cranny of the code used to do that. Pretty cool! If you have a ATmega with 1 kB of SRAM, you could quite easily implement SD writing, or write some nice code for navigating the SD card directories. Or maybe you’d like to upgrade the library to support FAT32 or SDHC cards. The choice is yours!
Thanks for reading the tutorial, I hope you found it useful. Remember to subscribe to the feed for more tutorials and interesting projects in the future. And of course, all feedback is very welcome, too!
29 comments
Jack:
Hi – A link to Part 2 seems to be missing. (I found it anyway, but presumably
a snipe hunt is’nt what you intended…
Useful stuff – Thanks for putting it up.
Jack
jokkebk:
Hmm, there is a link to part 2 in the very beginning in this article, although a bit hidden. It would be nice to have some kind of automatic “table of contents” for the full tutorial set, but so far I have just linked every post to the previous part.
Anyway, now that you reminded me, I’ll also add links to the next part to the end of each part.
David Cook:
Thank you very much, Mr. Pihlajamaa, for this wonderful tutorial.
You save beginners a lot of time by helping them past the sticking points.
Dave Cook
stephen:
good lesson
nyoman yudi:
Thanks … finally i made it work after 2 days
i tweak your example and made attiny2313 play wav
the video is here http://www.youtube.com/watch?v=N-ZO4oiwpXY
once again …THANK YOU VERY MUCH
jokkebk:
You’re welcome. Very cool to see such a project! I haven’t yet tried to output any audio with MCU projects, your example makes me want to try it out. :)
Gokay:
Hi,it is very fruitful tutorial.As to me I have a problem with the writing single block(512 byte)of data to SD card.I initialized SD card (CMD0,0x95)(CMD1,0xFF) and set the block length(CMD16,0xff) successfully.
Below you can see the result.
CMD 40 FF 01 FF FF FF FF FF FF FF FF
CMD 41 FC 07 FF FF FF FF FF FF FF FF
CMD 41 FC 07 FF FF FF FF FF FF FF FF
CMD 41 FF 01 FF FF FF FF FF FF FF FF
CMD 41 FF 00 FF FF FF FF FF FF FF FF
CMD 50 FF 00 FF FF FF FF FF FF FF FF
The problem is that when i send ‘Single Block Write’ Command to SD card I get an 0x00 response.(i know it is OK!) after I send data token which is 0xFE and send 512 bytes chunks of data and send two bytes CRC
consecutively. I cannot get any response whether data is accepted or not.
Here is below you can see the result.
CMD 58 FF 00 FF FF FF FF FF FF FF FF.
And below you can find my shortened code.
response=send_SDCommand(WRITE_SINGLE_BLOCK,startBlock<<9,0xFF);
if(response!=0)
{
return response;
}
SD_CS_ASSERT();// CS Low
//????????????what is going on here
SPI_transmit(0xFE); data token. we have to send it before.
for(i=0;i<512;i++)
SPI_transmit('A');
SPI_transmit(0xFF); // \
SPI_transmit(0xFF); // \ 16 bit dummy CRC
//Every data block written to the card is acknowledged by a data response token. It isone byte long and has the following format
// XXX0SSS1 // S= STATUS bits(3)
//if S's 010= Data accepted.
//if S's 101’—Data rejected due to a CRC error.
response=SPI_transmit(0xFF); //110’—Data Rejected due to a Write Error.
if((response & 0x1F)!=0x05)
{
SD_CS_DEASSERT();
rs232_Stream("0x05 errorrrrrr…");
return 1;
}
Thank you !
ganesh:
thanks very useful work
jokkebk:
You seem to have done quite a good job on the writing part, congratulations for that! Unfortunately I haven’t tried writing SD cards myself, so can’t help you much. Maybe some microcontroller forum would have people who could help?
Only thing that springs to my mind that you might want to check is that if one needs to send clock pulses to the card afterwards so it can do processing, maybe if you send some bogus data after write command, you get a response after a while? It’s been a year since I tinkered with SD and SPI so I’m most likely very off in this one, though…
Ahmed Eldamarawy:
thank you for the nice and organized presentation that you made for all of us.
i hope you can also explain how to write to on FAT16 SDs.
jokkebk:
Thanks! I’ll probably write a post on FAT writing in the summer months, stay tuned. :)
andraz:
Hi. Thank you for this tutorial it has helped a lot. I have a problem though. In fuction sd_init the sentence for(i=0; i<10 && SD_command(0x40, 0x00000000, 0x95, 8) != 1; i++)
does not compile since SD_command is void. We get R1 written in buffer. I allways get the response: FF01FFFFFFFFFFFF Is that correct and what should function SD_command return?
I apologise if I sound stupid, but I am very new to this…
Joonas Pihlajamaa:
No problem. The only issue is that in 30 months, I’ve also mostly forgotten everything about this. The SD_command is covered in previous part of the tutorial, you might want to try that one first, part 3 also covers at least partially the supposed output.
andraz:
I understand. I have read the previous part and SD_command is declared as void so it doesn’t return anything.
Joonas Pihlajamaa:
Ah sorry, my bad. The fat88.c in the updated project zip (link early on in this part 4) contains a version that returns unsigned char.
andraz:
OK great. Thank you.
tz:
I also wrote a SD card driver – mainly for openlog, but I have a few variations. The lib is at https://github.com/tz1/sparkfun/tree/master/fat32lib – but there’s also “hyperlog” that adds an external SPI RAM to the mix since part of the SD card spec is it can take up to 250mS to do housekeeping, so you would lose data at higher speeds.
Omkar:
Please give me the link for part1 and part 2
Also if this code is modified for pic16 wud it work the same?
Joonas Pihlajamaa:
You can find the link to first part in the beginning of this article:
http://codeandlife.com/2012/04/02/simple-fat-and-sd-tutorial-part-1/
In the end of part 1, there is a link to part 2, and so on. :)
Pic16 should work the same, unless its voltages are different from an AVR chip.
Omkar:
Please could u tell me the modifications for a fat32 sd card
Omkar:
The sd_command is a void function so how can it return any value
You have used the return value from the sd_command function in the for loop in the sd_init function.
Please tell me any solution..
Thanks a lott
Joonas Pihlajamaa:
Hi! Good question. I took a look into my source files and there is a version of SD_command in fat88.c which does indeed return a value. So you should check that out. :)
Joonas Pihlajamaa:
I might’ve originally included some links to FAT32, but basically you’ll need to read the FAT32 specs yourself and see what the difference is, I haven’t done that myself. Quick google on “fat32 specification” gave for example this page:
https://www.pjrc.com/tech/8051/ide/fat32.html
Omkar:
You have used the fat16_read function in which there is a variable fat_buffer but it is not defined anywhere.
So where it is defined?
Thank You.
J:
One simple question. It irritaes me, maybe i got something wrong, but why are you defining a “short offset = 0x1B0;” in the main method, that is not used at all?
Joonas Pihlajamaa:
Probably leftover from some earlier iteration of my code. You can (try to) ignore it. :)
Vinod:
Very useful tutorial for beginners. I was just searching for this king of elaborate stuff. I plan to do it on MSP430 chip.
I will start with RAW read writes and later switch over to FAT32 for readability in a PC.
Thanks a lot.
Vinod
Sheza A:
Hello,
I am trying to implement this using an ATmega 2560. I changed the relevant parameters in fat88.c to make it compatible with my device.
However, to run this project as is, what files from your zip would I use? If I use all of them my compiler throws errors since the same functions are defined in multiple files/repeated.
Joonas Pihlajamaa:
You can look at the Makefile inside the zip, it contains instructions which files make up which .hex file (meant for compiling the project from command line):
fat88.elf: fat88.o fat16.o
This means fat88.elf (intermediate file to get the .hex) depends on fat88.o and fat16.o, both of which the make utility can compile from corresponding .c files. So fat88.hex uses fat88.c and fat16.c.
Similarly, test_spi.hex can be compiled from test_spi.c and fat16.c
It’s a long time since I made this, so no guarantees if that will work on a recent Atmel Studio without errors, sorry…