SD card support for AVR

A detailed overview of how to expand your MCU with an SD card including basics for a deeper understanding of the functionality of SD cards.

SD cards are suitable for storing large amounts of data and being able to retrieve them on the computer. This makes the use of SD cards just for microcontroller projects quite interesting, since a microcontroller usually provides only, comparatively small, EEPROM as a data store.

A deeper look into the SD card

An SD card consists of an interface, a controller, a few registers and the actual storage element.

As a communication interface, the SPI of the microcontroller should be used. I am using the USART-SPI of the XMega microcontroller in this example. A control command for the SD card is structured as shown:

In SPI mode, each command for the SD card consists of an 8-bit command with the last bit set to 0 and the penultimate bit 1. The remaining bits result from the index of the command. This is followed by a 4-byte command argument and a 1-byte CRC (optional in SPI mode).

After the command has been sent, the SD card takes some time to process and respond to this command. This time NCR is between 0 and 8 bytes for SD cards, and 1 to 8 bytes for MM cards. When the time has elapsed, the card sends the appropriate response to the command. During the NCR phase, a 0xFF must be permanently sent to the card, so that DO is high and the SD card is clocked via SCLK.

The following commands are defined for communication with the SD card:

ACMD<n>: Combination of the command CMD55 and CMD<n>

The SD card responds to each command with a R1 or R3. An R1 response consists of 8 and an R3 response of 40 bits.

An R3 response always includes the current value of the status register (i.e., an R1 response) as well as the value of the OCR register.

After a reset (e.g. after plugging the card into the card reader), an SD card must always be initialized first. During initialization, the host (in this example the microcontroller) also determines what type of memory card it is, whether it is an SD card up to 2 GB, or a card with a capacity of >2 GB or more Multimedia Card (MMC) acts. Depending on the card type, a different initialization procedure is used.

Let´s write the SPI driver

As mentioned above, the card is operated in SPI mode and the USART-SPI interface of the microcontroller is used. Of course, any other SPI interface can be used by adapting the code accordingly.

First, the interface will be initialized. According to the specification, SD cards must be operated in SPI mode 0 (CPOL = 0, CPHA = 0). The following interface connections have been used to connect the card:

The connections for the signals SCK, MOSI and CS must be defined as output during initialization and set to high. In idle state, the data lines must also be high.

GPIO_SetDirection(&PORTDE, 0, GPIO_DIRECTION_OUT);
GPIO_Set(&PORTDE, 0);

GPIO_SetDirection(&PORTD, 1, GPIO_DIRECTION_OUT);
GPIO_Set(&PORTD, 1);
 
GPIO_SetDirection(&PORTD, 3, GPIO_DIRECTION_OUT);
GPIO_Set(&PORTD, 3);
         
GPIO_SetDirection(&PORTD, 2, GPIO_DIRECTION_IN);
GPIO_Set(&PORTD, 2);

The interface configuration requires the following settings:

  • Switch to USART-SPI mode

  • Configure the clock phase and the clock polaritiy (CPHA = 0, CPOL = 0)

  • Set the data order (LSB first)

  • Enable the receiver and the transmitter

USART_SetMode(&USARTD0, USART_MODE_MSPI);

GPIO_InvDisable(&PORTD, SPI_SCK_PIN);

USARTD0.CTRLC &= ~(0x02 | 0x04); 
USARTD0.CTRLB |= USART_RXEN_bm | USART_TXEN_bm;

It is also recommended to generate at least 80 additional clock cycles on the SPI so that the SD card can complete its internal processes after unselecting the SD card. As an addition to that, I generate eight clock pulses on the SPI before I select the card.

static void SD_Select(void)
{
	SPIM_TRANSMIT(&USARTD0, 0xFF);

	GPIO_Clear(&PORTE, 5);
}

static void SD_Deselect(void)
{
	GPIO_Set(&PORTE, 5);

	for(uint8_t i = 0x00; i < 0x0A; i++)
	{
		SPIM_TRANSMIT(&USARTD0, 0xFF);
	}
}

SD cards should be operated at a clock rate of approximately 100 kHz to 400 kHz during initial initialization, as this clock rate is supported by both old and new cards. Newer cards can also be 1 MHz or more, which is why the clock frequency can be increased after card initialization.

Once the interface and all GPIOs have been configured, the card can be communicated and the initialization (see picture) can be processed.

const SD_Error_t SD_Init(void)
{
	OldFreq = SPIM_GET_CLOCK(&USARTD0, 32000000);
	SPIM_SET_CLOCK(&USARTD0, 100000, 32000000);

	ErrorCode = SD_SoftwareReset();
	if(ErrorCode != SD_SUCCESSFULL)
	{
		return ErrorCode;
	}

	ErrorCode = SD_InitializeCard();
	if(ErrorCode != SD_SUCCESSFULL)
	{
		return ErrorCode;
	}

	SPIM_SET_CLOCK(&USARTD0, OldFreq, 32000000);
	
	return ErrorCode;
}

After a power-on reset, the card is selected and then at least 74 clock pulses are generated (equivalent to 10x send 0xFF). This activates the card's native operating mode, which means that the card understands the above commands. Then, a CMD0 is sent to put the SD card in idle mode. As soon as the SD card has switched to idle mode, it responds with an R1 response (0x01 - see overview).

static const SD_Error_t SD_SoftwareReset(void)
{
	uint8_t Repeat = 0x00;
	
	for(uint8_t i = 0x00; i < 0x0A; i++)
	{
		SPIM_TRANSMIT(&USARTD0, 0xFF);
	}
	
	while(SD_SendCommand(SD_ID_TO_CMD(SD_CMD_GO_IDLE), 0x00) != SD_STATE_IDLE)
	{
		if(Repeat++ == 0x0A)
		{
		        SD_Deselect();

			return SD_NO_RESPONSE;
		}
	}
	
	SD_Deselect();

	return SD_SUCCESSFULL;
}

If the SD card does not respond to the command several times, an error will be issued and initialization aborted. After initialization, no checksum is required for the individual commands.

The routine for sending any command with arguments and checksum looks like this:

static const uint8_t SD_SendCommand(const uint8_t Command, const uint32_t Arg)
{
	uint8_t Response = 0x00;
	uint8_t CommandTemp = Command;

	// Dummy CRC + Stop
	uint8_t Checksum = 0x01;

	SD_Select();

	// Send ACMD<n> command
	if(CommandTemp & 0x80)
	{
		// Clear ACMD-Flag
		CommandTemp &= 0x7F;
		
		Response = SD_SendCommand(SD_ID_TO_CMD(SD_CMD_APP_CMD), 0x00);
		if(Response > 0x01)
		{
			return Response;
		}
		
	}

	SPIM_TRANSMIT(&USARTD0, CommandTemp);
	SPIM_TRANSMIT(&USARTD0, (Arg >> 0x18) & 0xFF);
	SPIM_TRANSMIT(&USARTD0, (Arg >> 0x10) & 0xFF);
	SPIM_TRANSMIT(&USARTD0, (Arg >> 0x08) & 0xFF);
	SPIM_TRANSMIT(&USARTD0, Arg);

	if(CommandTemp == SD_ID_TO_CMD(SD_CMD_GO_IDLE))
	{
		// Valid CRC for CMD0(0)
		Checksum = 0x95;
	}
	else if(CommandTemp == SD_ID_TO_CMD(SD_CMD_IF_COND))
	{
		// Valid CRC for CMD8(0x1AA)
		Checksum = 0x87;
	}

	SPIM_TRANSMIT(&USARTD0, Checksum);

	if(CommandTemp == SD_ID_TO_CMD(SD_CMD_STOP_TRANSMISSION))
	{
		// Skip stuff byte when transmission stop
		SPIM_TRANSMIT(&USARTD0, 0xFF);
	}

	// Wait for the response (0 - 8 bytes for SD cards and 1 - 8 bytes for MMC)
	for(uint8_t i = 0x00; i < 0x08; i++)
	{
		uint8_t DataIn = SPIM_TRANSMIT(&USARTD0, 0xFF);
		if(DataIn != 0xFF)
		{
			// 8 dummy cycles if the command is a write command
			if(SD_ID_TO_CMD(SD_CMD_WRITE_SINGLE_BLOCK) || SD_ID_TO_CMD(SD_CMD_WRITE_MULTIPLE_BLOCK))
			{
				SPI_TRANSMIT(&USARTD0, 0xFF);
			}
	
			return DataIn;
		}
	}

	return SD_NO_RESPONSE;
}

The function first checks whether the command to be sent is an ACMD command. If this is the case, the command 55 is sent first to initiate the command sequence. Then the corresponding command is sent.

In the next part of the function, the card is selected to then send the command transmitted with its command arguments and a checksum. Thereafter, 8 bytes of data are sent to bridge the time NCR and at the same time it is checked whether the card has already sent a reply. When a response has been received, the function is exited. It is also advisable to generate eight more clock cycles (ie to send an empty byte) after receiving the card's response. This gives the controller of the SD card some extra time to process the internal processes.

Upon a successful change to idle mode, the next steps are taken from the control flow graph to complete the initialization of the card:

static const SD_Error_t SD_InitializeCard(void)
{
	uint8_t Response = 0x00;
	uint8_t Buffer[4];
	
	Response = SD_SendCommand(SD_ID_TO_CMD(SD_CMD_IF_COND), 0x1AA);
	for(uint8_t i = 0x00; i < 0x04; i++)
	{
		Buffer[i] = SPIM_TRANSMIT(&USARTD0, 0xFF);	
	}
		
	if(Response == SD_STATE_IDLE)
	{
		uint32_t R3 = ((uint32_t)Buffer[3]) << 0x18;
		R3 |= ((uint32_t)Buffer[2]) << 0x10;
		R3 |= ((uint32_t)Buffer[1]) << 0x08;
		R3 |= ((uint32_t)Buffer[0]);
		
		// Send ACMD41 and check for ready
		Wait = 0x00;
		while((++Wait < 0x2710) && (SD_SendCommand(SD_ID_TO_CMD(SD_CMD_ACMD41), ((uint32_t)0x01 << 0x1E)) != 0x00))
		{
			if(Wait >= 0x2710)
			{
				SD_Deselect();

				return SD_NO_RESPONSE;
			}
		}
	
		// Send CMD58 to read OCR
		Response = SD_SendCommand(SD_ID_TO_CMD(SD_CMD_READ_OCR), 0x00);
		for(uint8_t i = 0x00; i < 0x04; i++)
		{
			Buffer[i] = SPIM_TRANSMIT(&USARTD0, 0xFF);
		}

		if(Response == SD_STATE_SUCCESSFULL)
		{
			R3 = ((uint32_t)Buffer[3]) << 0x18;
			R3 |= ((uint32_t)Buffer[2]) << 0x10;
			R3 |= ((uint32_t)Buffer[1]) << 0x08;
			R3 |= ((uint32_t)Buffer[0]);
		}
		else
		{
			__CardType = SD_VER_UNKNOWN;
		}

		// Check if the CCS bit is set
		if(R3 & ((uint32_t)0x01 << 0x1E))
		{
			__CardType = SD_VER_2_HI;
		}
		else
		{
			__CardType = SD_VER_2_STD;
		}
	}
	else if(Response & SD_STATE_ILLEGAL_COMMAND)
	{
		// Check for version 1 SD card
		Wait = 0x00;
		while(++Wait < 0xFF) 
		{
			if(SD_SendCommand(SD_ID_TO_CMD(SD_CMD_ACMD41), 0x00) == SD_STATE_SUCCESSFULL)
			{
				__CardType = SD_VER_1_STD;

				break;
			}
		}

		// Check for multimedia card
		Wait = 0x00;
		if(Response & SD_STATE_ILLEGAL_COMMAND)
		{
			while(++Wait < 0xFF)
			{
				if(SD_SendCommand(SD_ID_TO_CMD(SD_CMD_SEND_OP_COND), 0x00) == SD_STATE_SUCCESSFULL) 
				{
					__CardType = SD_MMC;
	
					break;
				}
			}
		}
	}
	else
	{
		SD_Deselect();

		return Response;
	}
	
	SD_Deselect();
	
	return SD_SUCCESSFULL;
}

In this initialization routine, the card type is also determined because an MMC uses a different instruction set than an SD card. In addition, a version 1 SD card (<2 GB) is initialized differently than a version 2 SD card. Finally, the card is deselected and the response byte returned. When the card has been successfully initialized, the speed of the SPI will be changed back to the original value, so that the SD card is ready to use and can be read and written.

Before data can be read from the SD card, a corresponding command (such as CMD9) must first be sent to the card. The SD card confirms this command with an R1 response. Then the card begins to transfer the requested data.

The transmitted data packet consists of a token, a data block up to 2048 bytes long and a 2-byte CRC.

The sequence shown results in the following code:

static const SD_Error_t SD_ReadBlock(const uint32_t Length, uint8_t* Buffer)
{
	uint8_t Response = 0xFF;

	Wait = 0x00;
	while((++Wait < 0x2710) && (Response == 0xFF))
	{
		Response = SPIM_TRANSMIT(&USARTD0, 0xFF);
		if(Wait >= 0x2710)
		{
			SD_Deselect();

			return SD_NO_RESPONSE;
		}
	}

	for(uint32_t i = 0x00; i < Length; i++)
	{
		*Buffer++ = SPIM_TRANSMIT(&USARTD0, 0xFF);
	}

	SPIM_TRANSMIT(&USARTD0, 0xFF);
	SPIM_TRANSMIT(&USARTD0, 0xFF);
	
	return SD_SUCCESSFULL;
}

This function allows you to read out the SD card. As an example you can read out the STATUS register of the SD card. For this, the command ACMD13 must be send to the card. The SD card responds to this command with a 64-byte data packet containing the status information of the SD card.

const SD_Error_t SD_GetStatus(SD_Status_t* Status)
{
	uint8_t* Ptr = (uint8_t*)Status;
		
	if((SD_SendCommand(SD_ID_TO_CMD(SD_CMD_ACMD13), 0x00) == SD_SUCCESSFULL) && (SD_ReadBlock(sizeof(SD_Status_t), Ptr) == SD_SUCCESSFULL))
	{
		SD_Deselect();

		return SD_SUCCESSFULL;
	}
		
	SD_Deselect();
	
	return SD_NO_RESPONSE;
}

For the sake of clarity, I have stored the status information in a structure so that the received data can be directly identified.

 typedef struct  
 {
	 uint8_t DAT_BUS_WIDTH:2;
	 uint8_t SECURED_MODE:1;
	 uint16_t Reserved:13;
	 uint16_t SD_CARD_TYPE;	
	 uint32_t SIZE_OF_PROT_AREA;
	 uint8_t SPEED_CLASS;
	 uint8_t PERFORMANCE_MOVE;
	 uint8_t AU_SIZE:4;
	 uint8_t Reserved1:4;	
	 uint16_t ERASE_SIZE;
	 uint8_t ERASE_TIMEOUT:6;
	 uint8_t ERASE_OFFSET:2;
	 uint8_t Zero[11];
	 uint8_t Reserved3[39];
 } __attribute__((packed)) SD_Status_t;

This results in the following function call:

SD_Status_t Status;
SD_Error_t Error;
 
Error = SD_GetStatus(&Status);

You can read out different parameter like the class of the SD card from the status information.

Similarly, the CID or the CSD register of the SD card can be read out.

const SD_Error_t SD_GetCSD(SD_CSD_t* CSD)
{
	uint8_t* Ptr = (uint8_t*)CSD;

	if((SD_SendCommand(SD_ID_TO_CMD(SD_CMD_SEND_CSD), 0x00) == SD_SUCCESSFULL) && (SD_ReadBlock(sizeof(SD_CSD_t), Ptr) == SD_SUCCESSFULL))
	{
		SD_Deselect();

		return SD_SUCCESSFULL;
	}

	SD_Deselect();

	return SD_NO_RESPONSE;
}

const SD_Error_t SD_GetCID(SD_CID_t* CID)
{
	uint8_t* Ptr = (uint8_t*)CID;

	if((SD_SendCommand(SD_ID_TO_CMD(SD_CMD_SEND_CID), 0x00) == SD_SUCCESSFULL) && (SD_ReadBlock(sizeof(SD_CID_t), Ptr) == SD_SUCCESSFULL))
	{
		SD_Deselect();

		return SD_SUCCESSFULL;
	}

	SD_Deselect();
	
	return SD_NO_RESPONSE;
}

SD_CID_t CID;
Error = SD_GetCID(&CID);
	
SD_CSD_t CSD;
Error = SD_GetCSD(&CSD);

The card can be identified via the CID register. The register contains u. a. a manufacturer and an OEM ID, as well as the serial number of the SD card.

For the file system to write data to the SD card or to read data from the SD card, functions are required that read or write individual data blocks. The SD card offers the following commands:

A read or write access to a single data block looks like this.

For reading data access, the already presented ReadBlock function can be used. The write function looks like this:

static const SD_Error_t SD_WriteBlock(const uint8_t* Buffer, const uint32_t Length, const uint8_t Token)
{
	uint8_t* Buffer_Temp = (uint8_t*)Buffer;

	SPIM_TRANSMIT(&USARTD0, Token);
	if(Token != SD_TOKEN_STOP)
	{
		for(uint32_t i = 0x00; i < Length; i++)
		{
			SPIM_TRANSMIT(&USARTD0, *Buffer_Temp++);
		}

		SPIM_TRANSMIT(&USARTD0, 0xFF);
		SPIM_TRANSMIT(&USARTD0, 0xFF);

		if((SPIM_TRANSMIT(&USARTD0, 0xFF) & 0x1F) != 0x05)
		{
			return SD_NO_RESPONSE;
		}
	}

	while(SPIM_TRANSMIT(&USARTD0, 0xFF) != 0xFF);

	return SD_SUCCESSFULL;
}

Together with the SendCommand function, the ReadBlock and WriteBlock functions become the complete functionset for reading or writing a block of data.

const SD_Error_t SD_ReadDataBlock(const uint32_t Address, uint8_t* Buffer)
{
	if((SD_SendCommand(SD_ID_TO_CMD(SD_CMD_READ_SINGLE_BLOCK), Address) == SD_SUCCESSFULL) && (SD_ReadBlock(SD_BLOCK_SIZE, Buffer) == SD_SUCCESSFULL))
	{
		SD_Deselect();

		return SD_SUCCESSFULL;
	}

	SD_Deselect();

	return SD_NO_RESPONSE;
}

const SD_Error_t SD_WriteDataBlock(const uint32_t Address, const uint8_t* Buffer)
{
	if((SD_SendCommand(SD_ID_TO_CMD(SD_CMD_WRITE_SINGLE_BLOCK), Address) == SD_SUCCESSFULL) && (SD_WriteBlock(Buffer, SD_BLOCK_SIZE, SD_TOKEN_DATA) == SD_SUCCESSFULL))
	{
		SD_Deselect();

		return SD_SUCCESSFULL;
	}

	SD_Deselect();

	return SD_NO_RESPONSE;
}

With the SD_ReadDataBlock function, individual sectors, such as the first sector of the SD card, can be read out.

uint8_t FirstSector[512];
Error = SD_ReadDataBlock(0x00, FirstSector);

For a FAT file system, the first sector is always theboot sector for the operating system. The read data can be checked by looking at the last two digits of the array. For a valid FAT file system, the values ​​0x55 and 0xAA are stored there.

In a FAT file system, the first sector of the storage medium is always 512 bytes in size, no matter what storage media is used. This sector is read in by the BIOS and the code of the bootloader is executed, which then loads the operating system.

For large amounts of data, it makes sense to transfer all data in a single pass. SD cards have the CMD18 and CMD25 commands especially for the transmission of several data blocks, with which several blocks can be read or written.

Once initiated, the SD card sends or receives data until the host ends the transmission via a stop signal. This happens during a read operation via the command CMD12 and during a write operation through a stop token (see table) in an empty data packet.

The already created functions can be used to send or receive the individual data blocks so that the functions for sending or receiving several data blocks can be easily programmed.

const SD_Error_t SD_ReadDataBlocks(const uint32_t Address, const uint32_t Blocks, uint8_t* Buffer)
{
	SD_Error_t ErrorCode;

	if(SD_SendCommand(SD_ID_TO_CMD(SD_CMD_READ_MULTIPLE_BLOCK), Address) != SD_SUCCESSFULL)
	{
		SD_Deselect();

		return SD_NO_RESPONSE;
	}

	for(uint32_t i = 0x00; i < Blocks; i++)
	{
		ErrorCode = SD_ReadBlock(SD_BLOCK_SIZE, Buffer);
		if(ErrorCode != SD_SUCCESSFULL)
		{
			SD_Deselect();

			return ErrorCode;
		}

		Buffer += SD_BLOCK_SIZE;
	}

	SD_SendCommand(SD_ID_TO_CMD(SD_CMD_STOP_TRANSMISSION), 0x00);
		
	SD_Deselect();

	return SD_SUCCESSFULL;
}

const SD_Error_t SD_WriteDataBlocks(const uint32_t Address, const uint32_t Blocks, const uint8_t* Buffer)
{
	SD_Error_t ErrorCode;

	if(SD_SendCommand(SD_ID_TO_CMD(SD_CMD_WRITE_MULTIPLE_BLOCK), Address) != SD_SUCCESSFULL)
	{
		SD_Deselect();

		return SD_NO_RESPONSE;
	}

	for(uint32_t i = 0x00; i < Blocks; i++)
	{
		ErrorCode = SD_WriteBlock(Buffer, SD_BLOCK_SIZE, SD_TOKEN_DATA_CMD25);
		if(ErrorCode != SD_SUCCESSFULL)
		{
			SD_Deselect();

			return ErrorCode;
		}

		Buffer += SD_BLOCK_SIZE;
	}

	SD_WriteBlock(NULL, 0, SD_TOKEN_STOP);

	SD_Deselect();

	return SD_SUCCESSFULL;
}

The basic writing and reading functions are now fully programmed. We are thus able to exchange data between the SD card and the microcontroller and permanently store the data on the SD card.

Give me a file system please!

The basis for the FAT implementation is the FatFs library from ElmChan.

This library provides a common interface for a FAT file system with support for various platforms. All the user has to develop is the interface to the storage medium (here the SD card). This tutorial use the FatFs version R0.13c. The source directory of the downloaded archive contain all necessary files to realize the FAT support for the microcontroller. Our task is to customize the file diskio.c according to our driver for SD cards.

Furthermore, a file with the name ffconf.h is used to configure the file system. The following configuration items have been changed:

The function prototypes of the functions that the user has to implement in the file diskio.c are all already created. Our task is therefore to write the following functions:

In addition, Elm Chan shows how several different storage media can be used with the module by giving each medium its own drive number:

/* Definitions of physical drive number for each drive */
#define DEV_RAM		0	/* Example: Map Ramdisk to physical drive 0 */
#define DEV_MMC		1	/* Example: Map MMC/SD card to physical drive 1 */
#define DEV_USB		2	/* Example: Map USB MSD to physical drive 2 */

In this tutorial, only one SD card will be used as a drive. The SD card should get the number 0. So change the definitions to the following:

#define DEV_MMC		0	/**< Map MMC/SD card to physical drive 0 */

The first function we want to implement is the function disk_initialize, which initializes the requested storage medium. This function has only the drive name as a transfer parameter, which must be evaluated by means of a switch statement:

DSTATUS disk_initialize(
	BYTE pdrv,		/* Physical drive number to identify the drive */
)
{
	switch(pdrv)
	{
		case DEV_MMC:
		{
			if(SD_Init(&__InterfaceConfig) == SD_SUCCESSFULL)
			{
				__MMCStatus &= ~STA_NOINIT;

				return __MMCStatus;
			}
		}
		default:
		{
			return STA_NOINIT;
		}
	}
}

If the drive has been initialized successfully, a corresponding status variable will also be set. This variable can then be queried with the disk_status function:

DSTATUS disk_status(
	BYTE pdrv,		/* Physical drive number to identify the drive */
)
{
	switch(pdrv)
	{
		case DEV_MMC:
		{
			return __MMCStatus;
		}
		default:
		{
			return STA_NOINIT;
		}
	}
}

Next, the function disk_read is implemented. This function passes, in addition to the drive identifier, a pointer to a data buffer, the requested sector, and the number of sectors to be read. This information is evaluated and the corresponding functions of the SD card driver are called up:

DRESULT disk_read (
	BYTE pdrv,		/* Physical drive number to identify the drive */
	BYTE *buff,		/* Data buffer to store read data */
	DWORD sector, /* Start sector in LBA */
	UINT count		/* Number of sectors to read */
)
{
	switch(pdrv)
	{
		case DEV_MMC:
		{
			if(__MMCStatus & STA_NOINIT)
			{
				return RES_NOTRDY;
			}

			if(count == 1)
			{
				if(SD_ReadDataBlock(sector, buff) == SD_SUCCESSFULL)
				{
					return RES_OK;
				}
			}
			else
			{
				if(SD_ReadDataBlocks(sector, count, buff) == SD_SUCCESSFULL)
				{
					return RES_OK;
				}
			}

			return RES_ERROR;
		}
		default:
		{
			return RES_NOTRDY;
		}
	}

	return RES_PARERR;
}

The function disk_write works in the same way:

DRESULT disk_write(
	BYTE pdrv,		    /* Physical drive number to identify the drive */
	const BYTE *buff,	/* Data to be written */
	DWORD sector,		  /* Start sector in LBA */
	UINT count		    /* Number of sectors to write */
)
{
	switch(pdrv)
	{
		case DEV_MMC:
		{
			if(__MMCStatus & STA_NOINIT)
			{
				return RES_NOTRDY;
			}

			if(count == 1)
			{
				if(SD_WriteDataBlock(sector, buff) == SD_SUCCESSFULL)
				{
					return RES_OK;
				}
			}
			else
			{
				if(SD_WriteDataBlocks(sector, count, buff) == SD_SUCCESSFULL)
				{
					return RES_OK;
				}
			}

			return RES_ERROR;
		}
		default:
		{
			return RES_NOTRDY;
		}
	}

	return RES_PARERR;
}

Finally, the function disk_ioctl must be implemented. This function has the task to provide a general interface for the SD card, which can be used to get informations such as the the sector size from the card. The requested command is passed as a parameter to the function and must then be evaluated and executed. The function of the individual command codes can be found in the documentation.

DRESULT disk_ioctl (
	BYTE pdrv,		/* Physical drive number (0..) */
	BYTE cmd,		  /* Control code */
	void *buff		/* Buffer to send/receive control data */
)
{
	BYTE *ptr = (BYTE*)buff;

	switch(pdrv)
	{
		case DEV_MMC:
		{
			switch(cmd)
			{
				case GET_BLOCK_SIZE:
				{
					if(SD_GetEraseBlockSize((uint16_t*)ptr) == SD_SUCCESSFULL)
					{
						return RES_OK;
					}
					
					return RES_OK;
				}
				case GET_SECTOR_COUNT:
				{
					if(SD_GetSectors((DWORD*)ptr) == SD_SUCCESSFULL)
					{
						return RES_OK;
					}
					
					return RES_ERROR;
				}
				case GET_SECTOR_SIZE:
				{
					*(WORD*)buff = SD_BLOCK_SIZE;

					return RES_OK;
				}
				case CTRL_SYNC:
				{
					SD_Sync();

					return RES_OK;
				}
				default:
				{
					return RES_PARERR;
				}
			}
		}	
		default:
		{
			return RES_PARERR;
		}
	}
}

With the command GET_BLOCK_SIZE the Erase Block Size of the storage medium is returned. This information can be read out of the status register on SD cards with version 2 or later

const SD_Error_t SD_GetEraseBlockSize(uint16_t* EraseSize)
{
	if((__CardType == SD_VER_2_STD) || (__CardType == SD_VER_2_HI))
	{
		SD_Error_t Error = SD_GetStatus(&__CardStatus);

		*EraseSize = __CardStatus.ERASE_SIZE;

		return Error;
	}

	SD_Deselect();

	return SD_SUCCESSFULL;
}

The command GET_SECTOR_COUNT queries the number of sectors of the storage medium. This information can be found in the CSD register of the SD card. For SD cards version 2 or later, the number of sectors can be calculated via the CSD register.

const SD_Error_t SD_GetSectors(uint32_t* Sectors)
{
	uint32_t Size;
	uint8_t CSD[16];

	SD_Error_t ErrorCode = SD_GetCSD((SD_CSD_t*)CSD);
	if(ErrorCode != SD_SUCCESSFULL)
	{
		return ErrorCode;
	}

	if((__CardType == SD_VER_2_STD) || (__CardType == SD_VER_2_HI))
	{
		Size = CSD[9] + (CSD[8] << 8) + 0x01;
		*Sectors = Size << 0x0A;
	}

	return SD_SUCCESSFULL;
}

With the help of the command GET_SECTOR_SIZE the size of a sector of the storage medium is queried. SD cards with a version 2 or later have a fixed sector size of 512 bytes. Therefore, this command returns only a constant value.

The last command named CTRL_SYNC serves to terminate open write operations of the storage medium. If the device has a cache, that cache must be flushed and written to memory. For the SD card, the card is deselected, causing the SD card to end all open operations:

void SD_Sync(void)
{
	SD_Deselect();
}

Thus all necessary functions would be programmed and the SD card can be described. For this the following code snippet can be used:

FATFS MicroSD_FS;
FRESULT FileStatus;
FIL LogFile;
UINT Return;
char FilePath[] = "0:File.txt";
char ReadData[13];

FileStatus = f_mount(&MicroSD_FS, "0:", 1);
if(FileStatus == FR_OK)
{
	FileStatus = f_open(&LogFile, FilePath, FA_WRITE | FA_CREATE_ALWAYS);
	FileStatus = f_puts("Hello, World!", &LogFile);
	FileStatus = f_close(&LogFile);
			
	FileStatus = f_open(&LogFile, FilePath, FA_READ | FA_OPEN_EXISTING);
	FileStatus = f_read(&LogFile, ReadData, 13, &Return);
	FileStatus = f_close(&LogFile);
}

Similar to Windows, the file name of FatFs consists of a drive name and the actual name. In this example, the file File.txt should be saved on the drive with the identifier 0. This corresponds to the SD card.

First of all, the SD card is integrated via the f_mount function. The storage medium is integrated directly via parameter 1, so that you can directly check whether the memory card is recognized.

FileStatus = f_mount(&MicroSD_FS, "0:", 1);

Then create a new file with the path 0: File.txt, fill it with Hello, World and close it again. An existing file will be overwritten.

FileStatus = f_open(&LogFile, FilePath, FA_WRITE | FA_CREATE_ALWAYS);
FileStatus = f_puts("Hello, World!", &LogFile);
FileStatus = f_close(&LogFile);

Finally, the created file should be opened, the complete content read out and closed again.

FileStatus = f_open(&LogFile, FilePath, FA_READ | FA_OPEN_EXISTING);
FileStatus = f_read(&LogFile, ReadData, 13, &Return);
FileStatus = f_close(&LogFile);

When the program is executed, the SD card is mounted. After that the file is created, filled with data and read into the ReadData array. Additionally you can check the contents of the SD card by pluging the card into a card reader and read the card with your PC.

Everything works? Wonderful! With this you have successfully equipped your AVR with an SD card and a FAT file system.

The final SD card driver, including an example implementation for a XMega384C3, can be found in my GitLab repository.

Last updated