Their default mode is high-speed through 4-bit wide port but we're going to be working with the "legacy" SPI (two-wire) mode.
In SPI mode, the master device (our microcontroller) talks to the slave device (the sd card) using a data and a clock line. Every time the clock line goes from low-to-high (or, if you prefer, from high-to-low - you can change this to suit the application needs) the receiving device looks at the data line. If it's high, it receives the single-bit value 1, if it's low, zero.
in this example, when the clock line goes from low-to-high (sometimes called a rising edge trigger) as denoted by the red vertical lines in the clock timing diagram, the state of the data line is converted into a value
The great thing about SPI is that it's not time dependent. Because the master device sends the clock line along with the data, it can be speeded up and slowed down (this is not possible using methods such as UART/serial, which has a fixed data rate; ie. the data has to be moved within a specific time period).
Using this clock-and-data method, we can send commands to the sd card, to tell it to do specific things. So we can send a specific value (the sd format sets out specific values to send for specific commands) to get it to reset, for example.
When an SD card has received and understood a command, it can remain in a busy state for quite some time. It is important to wait until the card has finished doing whatever you asked it to do, before blasting more data or commands at it. To do this, we poll the card (continuously ask it for data) until it gives us a "ready" token.
SPI is actually a data exchange mechanism. There's no difference between reading and writing bytes between the devices. As one device sends a byte of data, so the other transmits one. After sending a single byte of data from an SPI buffer another (possibly different) byte may appear in it's place - this is the byte that has been received.
So to read a byte from the sd card, we have to send it a byte too. It's normal to send either all zeros (0x00) or all ones (0xFF in hex is 255 in decimal, which is 11111111 in binary) for "don't care" bytes - so if you're just reading data from the other device, and it's not important what data you send, it's common to either send 0x00 or 0xFF.
Whenever a command is sent to an SD card, it follows a specific format:
There is a single command byte
There are four "parameter" bytes - data which tells the recipient how to perform the command requested
There is a single CRC (checksum) byte to prove that the previous bytes have been transmitted correctly.
To send data to our SD card, we need a couple of functions:
UInt8 sdSpiByte(UInt8 data){
ssp1buf = data;
while(!(ssp1stat & (1<<BF)));
}
static inline void sdSendCommand(UInt8 cmd, UInt32 param){
UInt8 send[6];
send[0] = cmd | 0x40;
send[1] = param >> 24;
send[2] = param >> 16;
send[3] = param >> 8;
send[4] = param;
send[5] = (sdCrc7(send, 5, 0) << 1) | 1;
for(cmd = 0; cmd < sizeof(send); cmd++){
sdSpiByte(send[cmd]);
}
}
This provides us with a simple method of sending commands (and their parameters) to the SD card.
The first function simply puts a value into the hardware SPI buffer then waits for the SPI busy register value to go "not-busy" before returning the value it finds in the buffer (which is now the value received from the other device - data exchange remember!)
The second function actually sends commands to the sd card. Every command byte sent to an SD must have bit 6 set (so the device can recognise it as a command and not some data).
Bit 6 in binary is 01000000 which is 64 in decimal or 0x40 in hex.
So to make sure that every command byte has bit 6 set, we always OR the command byte with 0x40 (so when we send command zero, for example, it's actually transmitted as 0x40, command one is sent as 0x41 and so on).
But every time we send a command (and it's parameters), we have to wait for a not-busy response from the SD card. While the SD card is busy, it holds its "output pin" high - the data clocked out of it is always 11111111 (or 0xFF) So we build these little functions:
static inline UInt8 sdReadResp(void){
UInt8 v, i = 0;
do{
v = sdSpiByte(0xFF);
}while(i++ < 128 && (v == 0xFF));
return v;
}
static UInt8 sdCommandAndResponse(UInt8 cmd, UInt32 param){
sdSpiByte(0xFF);
sdSendCommand(cmd, param);
ret = sdReadResp();
return ret;
}
This sdCommandAndResponse function sends a "dummy byte" to the sd card.
It then sends the command byte, followed by the 4-byte parameter value(s).
Next it calls the read-response function, which continuously sends the dummy byte 0xFF to the sd card, until the response back is not busy. When the response goes not busy, the response value is returned to the sdCommandAndResponse function. The response could either be "all ok" or it may be some kind of error code to explain why the command given could not be completed.
One last function to mention is the CRC generating function.
Strictly speaking, once we've told our card to work in SPI legacy mode, we don't actually need to generate the CRC values, but it's included here for completeness.
Note: We didn't actually create this function, we ported it from another sd card library for another platform:
static UInt8 sdCrc7(UInt8* chr,UInt8 cnt,UInt8 crc){
UInt8 i, a;
UInt8 Data;
for(a = 0; a < cnt; a++){
Data = chr[a];
for(i = 0; i < 8; i++){
crc <<= 1;
if( (Data & 0x80) ^ (crc & 0x80) ) {crc ^= 0x09;}
Data <<= 1;
}
}
return crc & 0x7F;
}
Before we can actually start sending data over SPI, we need to set up the PIC to use the hardware SPI peripheral. This means writing some values to particular registers in the chip. The names of these registers should be similar across different PIC models, but may not be exactly the same if you're using a different chip:
static void sdSpiInit(void){
ssp1add = 21; //slow clock down to < 400khz
ssp1con1 = 0b00101010; //spi master, clk=timer2
ssp1stat = 0b11000000; //CPHA = 0
}
The important registers here are the SSP1ADD (multiplier value) ad SSP1CON1 register.
When the last four bits of SSP1CON1 are 1010 this slows down the SPI clock speed - by how much depends on the value in the SSP1ADD register (and the actual speed, as in time taken to send each clock pulse, is dependent on the overall processor clock speed so will change with each chip model). When the last four bits are set to 0000, the SPI clock changes on every instruction cycle. When it's set to 1010, it changes on every x clock cycles, where x is the value held in the SSP1ADD register.
The SD card initialisation routine after powering up is:
- Set the clock speed to less than 400khz
- Hold the chip select line on the card low and send in about 80 clock pulses
- Pull the chip select line high to tell the card we're talking to it
- Send in the "soft reset" command (CMD0)
- Wait for the sd card to respond "ok" with the value 0x01
- Send in the "initialise card" command (CMD1)
- Repeat sending CMD1 until the card responds with an ok value of 0x00
- Set the sector size using CMD16 with a parameter 512 (each sector is 512 bytes)
- Turn of the CRC requirement by sending CMD59
- (all future transmissions do not require a valid crc value)
- The next time the card responds with an "ok" value, it has been initialised and we can ramp the clock up to full speed.
All of the initialisation routines have to be carried out at the relatively slow clock speed of not more than 400khz. So we need a couple of extra functions to enable us to set the clock speed and enable the chip select line:
static void sdClockSpeed(Boolean fast){
if(fast){
ssp1con1 = 0b11110000
}else{
ssp1con1 = 0b11111010
}
}
void sdChipSelect(bool active){
portc.3 = !active;
}
For debugging, we've written a simple "fatal error" function to let us know which part of the initialisation failed (if there are any problems). When a fatal error is hit, this function reports it, then puts the microcontroller into "sleep mode" so that the program flow is permanently interrupted.
void fatal(UInt8 val){ //fatal error: flash LED then go to sleep
UInt8 i, j, k;
for(j = 0; j < 5; j++){
for(k = 0; k < val; k++)
{
delay_ms(100);
P_LED = 1;
delay_ms(100);
P_LED = 0;
}
UARTLog("Error",val);
delay_ms(250);
delay_ms(250);
}
while(1){
asm sleep
}
}
By using all these functions together, we can now write our sdInit routine:
Boolean sdInit(){
UInt8 v, tries = 0;
Boolean SD;
SD = false;
sdSpiInit(); // initialise SPI
sdClockSpeed(false); // slow clock
sdChipSelect(false); // CS inactive
for(v = 0; v < 20; v++) {
sdSpiByte(0xFF); //lots of clocks to give card time to init
}
sdChipSelect(true); // CS active
v = sdCommandAndResponse(0, 0, true);
if(v != 1){fatal(2);}
v = sdCommandAndResponse(1, 0, true);
for(int i=0;v != 0 && i<50;++i){
delay_ms(50);
v = sdCommandAndResponse(1, 0, true);
}
if(v){fatal(3);}
v = sdCommandAndResponse(16, 512, true); //sec sector size
if(v){fatal(5);}
v = sdCommandAndResponse(59, 0, true); //crc off
if(v){fatal(6);}
// now set the sd card up for full speed
sdClockSpeed(true);
return true;
}
If this function successfully returns true, the SD card has been successfully initialised and the SPI lines set to run at maximum speed for the fastest (spi-based) data transfer.
Thank you sharing the post. I got many help in the post.
ReplyDeleteGet our services….
Creator of clipping path