In this tutorial, we will be learning how to program the PIC18F4550 to perform the raw SD read/write functions on an SD card. To implement this hobby project, we will be using the popular SPI protocol and the software UART to display the output.
Microchip’s PIC18F4550 is a small microcontroller in terms of its functionalities as well as the memory space. To design a code to implement the raw SD read/write using this microcontroller requires a few considerations. In our previous tutorial, we’ve discussed the process of SD card interfacing with microcontrollers.
To begin with, it only has a 32KB program memory and 2048 bytes of data memory. One of the features of SD card is it has a limited memory space is available when coding the program.
We’re going to use UART for debugging the SD card. The Receive pin(Rx) of the UART and Serial Data Out (SDO) pin of SPI are sharing the same port pin of PIC18F4550 and here the software UART is used instead of a hardware interrupt.
Interfacing
The first step is to interface the hardware with the software using the microchip’s library function, OpenSPI( ). The OpenLab uses RB4 as chip select(CS) or the slave select(SS) pin instead of the usual RA5 pin. The direction of RA5 must be set as output and keep it as low in order to apply this change. The RB4 pin must be set as an output pin and should be used as the chip select pin.
You can choose the CS pin as you desire. The SPI pins RB4 (CS), RB1(SCK) and RB0(SDI) are analog pins by default. So the pins configured to digital pins. Set the SPI at mode(0,0) and in the 8-bit asynchronous mode with the input sampled at the middle of the data out. The directions of the SPI pins apart from the adopted RB4 pin are set in the OpenSPI( ) function. The soft_hard_init( ) function shows how the SPI is set up successfully.
void soft_hard_init() { unsigned char sync_mode=0; unsigned char bus_mode=0; unsigned char smp_phase=0; CloseSPI(); ADCON1 = 0X0F; //Make RB4, RB1, RB0 Pins Digital CMCON |= 7; TRISA5 = 0; //Work around for a hardware adjustment in Open Lab LATA5 = 0; TRISB4 = 0; //Chip Select sync_mode = SPI_FOSC_64; // 8 Bit Asynchronous Mode bus_mode = MODE_00; // Mode(0,0) smp_phase = SMPMID; //Input data sample at middle of data out OpenSPI(sync_mode, bus_mode, smp_phase); }
Transmission
Next, we must create the basic functions to carry out the transmission and reception of data and command with the SD card. The function WriteSPI( ) is used to transmit command or data to SD card. The function ReadSPI( ) is used to read every datum or response that is received from the SD card. While receiving a response from the SD card, it is important to make sure that the chip select pin must be low prior to reading.
unsigned char response() { unsigned char buff; LATB4 = 0; //CS low buff=ReadSPI(); return buff; }
Reception
There is an exception while using Microchip’s ReadSPI( ) here.
unsigned char ReadSPI( void ) { unsigned char TempVar; TempVar = SSPBUF; // Clear BF PIR1bits.SSPIF = 0; // Clear interrupt flag SSPBUF = 0x00; // initiate bus cycle //while ( !SSPSTATbits.BF ); // wait until cycle complete while(!PIR1bits.SSPIF); // wait until cycle complete return ( SSPBUF ); // return with byte read }
The above code shows the ReadSPI( ) microchip library function. In line 6, 0x00 byte write to the SSPBUF register to initiate a bus cycle. The microcontroller generates the bus cycle only after the buffer is filled. Writing 0x00 does not fill the buffer. Rather, it clears the buffer. A 0xFF write to SSPBUF register in order to generate a bus cycle before the arriving of response. So we need to use the modified version of the ReadSPI() function in our code.
The command has a fixed length of 6 bytes. A dummy byte (0xFF) is sent in order to generate the bus cycle.
void Command(char cmd, unsigned long int arg, char CRCbits) { unsigned char temp; WriteSPI(0xff); //Dummy Byte WriteSPI(cmd); WriteSPI((unsigned char) (arg >> 24)); WriteSPI((unsigned char) (arg >> 16)); WriteSPI((unsigned char) (arg >> 8)); WriteSPI((unsigned char) arg); WriteSPI(CRCbits); }
First, transmitting the argument ‘ cmd’, which contains a single byte hexadecimal command number. The variable ‘arg’ contains the 4-byte argument, It transmitted as 8 bits starting from the most significant bit (MSB) at a time and finally sents the CRC bit. Before starting the initialization process, we must create a function called dummy_clocks( ) and another function called proceed( ). The purpose of dummy_clocks( ) is to send bus clocks to the SD card.
void dummy_clocks(int n) { int i = 0; for(i=0;i<n;i++) { LATB4 = 1; //CS High WriteSPI(0XFF); //Dummy Byte LATB4 = 0; //CS Low } }
Set the chip select to high to send each dummy byte. Now, the purpose of proceed( ) is to send bus clocks at instances where the buffer of the microcontroller is empty during the execution time.
void proceed() { LATB4 = 0; //CS Low WriteSPI(0XFF); // Give Time For SD_CARD To Proceed LATB4 = 1; //CS High }
As you may notice, there is a difference between the two functions all though they may appear similar. To generate the clock pulses using the function proceed(). Sent the 0xFF byte to the buffer and it does not send to the SD card. To write the 0xFF byte turn off the slave selection.
While in the case of dummy_clocks( ), our purpose is to send clock pulses to the SD card soon after power-up or prior to sending a command, to prepare the SD card for the appropriate communication frequency. At this point, writing a 0xFF does not actually mean anything to the SD card.
For a detailed explanation of the timing and flow diagram of SD card initialization, read and write process, please refer the tutorial: Interfacing Microcontrollers with SD Card
SD Card Initialization
After power up, wait for one second or two seconds. In the initialization process, the first step is to make the card to expose to the frequency at which the data is being sent. And then reset the SPI mode from its default operating mode. For this, send around 75 dummy bits approximately (dummy byte * 10 times = 80 bits) and the card will be ready to communicate in the required frequency.
The first command to send is CMD0. Which resets the SD card and set the card at an idle state.
do{ dummy_clocks(8); // Initial Clock Pulse Command(0X40, 0X00000000, 0X95); proceed(); // Generate Bus Clock do{ buff = response(); count++; }while((buff!=0X01) && (count<10) ); count = 0; }while(buff!=0X01);
The loop do-while continues to runs until the card is inserted into SD card slot. And it receives an idle response (0x01). At this point, the SD card is in an idle state and will only accept the commands, CMD0, CMD1, ACMD41, CMD58 and CMD59.
Sent the command CMD8 is after the checking the version of SD card. If the SD card is Version 2, then continuously sent the CMD1 command after receiving the response of the CMD0 command. Keep sending the CMD1 command till the SD card gets out of its idle mode and returns a 0x00 byte as a response.
do{ Command(0X41, 0X00000000, 0X00); proceed(); //Generate Bus Clock do{ buff = response(); count++; }while((buff!=0X00) && (count<10)); count = 0; }while(buff!=0x00);
To read or write the raw data first accept the command CMD1. There is also the pat.bility that the SDC requires a CMD41 command to initialize the card instead of the CMD1 command. Send the command CMD41 to write a general code. Sent the command CMD1 if the command CMD41 is to reject. Sent the command CMD1 if any rejection of the command CMD41 has occurred.
The CMD<n> command is CMD55 command followed by CMD<n>. So follow the same procedure to send both the commands i.e. CMD55 and wait for a 0x01 response and then CMD41 and wait for a 0x00 response. Send command CMD1 otherwise after a definite amount of time the command will not accept.
Block Length
After the initialization is complete, set the block length. The command to set the block length is CMD16 and it takes the argument as the size of the block. Wait for the command to accept.
Write to SD Card
The initial command is the same to send for raw data read/write. first, we will look at how to write a single block.
First, send the command CMD18 with the argument. The argument contains the address of the sector. The command is to accept with a usual response of 0x00 byte.
Then the SD card is waiting for the microcontroller to give a start token of 0xFE. This indicates the beginning of the block write and the card would be ready to accept a block of data. After block write completes then send two 0xFF bytes as CRC. It is mandatory to send the CMD13 command after the completion of every data block write.
dummy_clocks(8); Command(0X58, 0X00000A00, 0X00); proceed(); do{ buff = response(); }while(buff!=0x00); /*CMD 24 Accepted*/ Delay_s(1); dummy_clocks(8); WriteSPI_(0XFE); //START TOKEN for(i=0;i<512;i++) //DATA BLOCK { WriteSPI_(/*WRITE YOUR DATA BYTE HERE*/); i++; } WriteSPI_(0XFF); //CRC WriteSPI_(0XFF); //CRC Delay_s(1); dummy_clocks(8); Command(0X4D,0X00000000,0XFF); proceed(); do{ buff = response(); }while(buff!=0x00); /*DATA WRITE OVER*/
Read from SD card
Next, we will see how to read a block of data from an SD Card. First, send the command CMD24 and wait for the command to accept. Then wait for its response. The microcontroller immediately receives the command 0xFE from the SD card. It is the indication of the start of data transmission. Set the chip select pin as low and remains the same throughout the process. The command CMD12 indicate the end of the reading process and it sent a block of data at the end.
do{ buff = response(); }while(buff!=0xFE); length = 0; LATB4 = 0; // CS is driven low during the read process while(length<512) { str[length]=response(); //read buffer (R1) should be 0x01 = idle mode length++; } length = 0; WriteUART_(0XFF); //CRC WriteUART_(0XFF); //CRC Delay_s(1); dummy_clocks(8); Command(0X4C,0X00000000,0X00); //COMMAND12. Stop To Read proceed(); do{ buff = response(); }while(buff!=0xFF);
Multi-Block write
To write multi-block use the command CMD25. The 0xFC byte indicating start token indicating in the case of a multi-block write.
while(i!=/*number of blocks*/) { WriteSPI_(0XFC); //START TOKEN i++; for(j=0;j<512;j++) //DATA BLOCK { WriteSPI_(/*your data byte*/); j++; } WriteSPI_(0XFF); // CRC 2 BYTES WriteSPI_(0XFF); Delay_s(1); dummy_clocks(8); }
Finally, once the data write is over, issue a stop token 0x4D to indicate the end of the transmission. In the case of a multi-read, wait till the SD Card issues a 0xFE byte(start read) after accepting the CMD18 command.
Multi-Block read
Now, continuously keep reading the data bytes until the end of the specified number of blocks. The readings are as one block at a time because of space limitation. And collects every block of data as different array in a static declaration. And using a 0x4C as stop token at the end of the reading process.
while(count){ while ( length < 512 ) { str[length] = response(); //Repeat with a different array for each count rd++; length++; } count--; }
Finally, create a switch condition for the user to choose between the different functions of the system.