Kampis Elektroecke

AVR mit einer SD-Karte erweitern – Teil 2

Im ersten Teil des AVR SD-Karten Tutorials habe ich gezeigt, wie die SD-Karte mit einem Mikrocontroller verbunden wird und wie die Kommunikation mit einer SD-Karte aufgebaut ist. Anschließend habe ich gezeigt, wie die Karte initialisiert wird, sodass der Mikrocontroller mit ihr kommunizieren kann. Dieser Teil des Tutorials soll sich nun etwas tiefer mit der Kommunikation der SD-Karte beschäftigen. Es wird gezeigt, wie die Hardwareinformationen der SD-Karte gelesen, bzw. wie einzelne Blöcke oder Sektoren der Karte gelesen und beschrieben werden können, wodurch es dem Mikrocontroller ermöglichen werden soll, auf das Dateisystem und die Dateien, die auf der SD-Karte gespeichert sind, zuzugreifen und diese zu bearbeiten.

Die Realisierung des Zugriffs auf das Dateisystem wird im nächsten Teil vorgestellt. Ausgangsbasis dafür ist die FatFs-Bibliothek von ElmChan. Diese Bibliothek stellt eine allgemeine Schnittstelle für ein FAT-Dateisystem mit Support für verschiedene Plattformen bereit. Alles was der Anwender entwickeln muss, ist die Schnittstelle zum Speichermedium (hier die SD-Karte).

Bevor Daten von der SD-Karte ausgelesen werden können, muss zuerst ein entsprechendes Kommando (wie z. B. CMD9) an die Karte gesendet werden. Die SD-Karte bestätigt dieses Kommando mit einer R1-Antwort. Anschließend beginnt die Karte mit der Übertragung der angeforderten Daten.

Das gesendete Datenpaket besteht aus einem Token, einen bis zu 2048 Byte langen Datenblock und einer 2 Byte langen CRC.

Token Beschreibung
0xFC Token für CMD17/18/24
0xFD Token für CMD25
0xFE Stop Token für CMD25

Aus dem gezeigten Ablauf ergibt sich die folgende Empfangsroutine:

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;
}

Über diese Funktion kann nun z. B. das STATUS-Register der SD-Karte ausgelesen werden. Dazu wird das Kommando ACMD13 an die Karte gesendet. Die SD-Karte antwortet auf diesen Befehl mit einem 64 Byte großen Datenpaket, welches die Statusinformationen der SD-Karte beinhaltet.

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;
}

Aus Gründen der Übersicht habe ich die Statusinformationen in einer Struktur hinterlegt, sodass die empfangenen Daten direkt identifiziert werden können.

 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;

Es ergibt sich damit der folgende Aufruf:

SD_Status_t Status;
SD_Error_t Error;

Error = SD_GetStatus(&Status);

Aus den Statusinformationen lässt sich z. B. der Kartentyp oder die Klasse der SD-Karte kodiert.

Klasse Beschreibung
0x00 Klasse 0
0x01 Klasse 1
0x02 Klasse 2
0x03 Klasse 3

Auf ähnliche Weise kann nun das CID oder das CSD-Register der SD-Karte ausgelesen werden.

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);

Über das CID-Register kann die Karte identifiziert werden. Das Register beinhaltet u. a. eine Hersteller- und eine OEM-ID, sowie die Seriennummer der SD-Karte.

Damit das Dateisystem Daten auf die SD-Karte schreiben, bzw. Daten von der SD-Karte lesen kann, werden Funktionen benötigt, die einzelne Datenblöcke lesen, bzw. schreiben. Die SD-Karte bietet dafür die folgenden Befehle an:

Befehl Funktion
CMD17 Lese einzelnen Block
CMD18 Lese n Blöcke
CMD24 Schreibe einzelnen Block
CMD25 Schreibe n Blöcke

Ein Lese- bzw. Schreibzugriff auf einen einzelnen Datenblock sieht dabei folgendermaßen aus.

Für einen lesenden Datenzugriff kann die bereits vorgestellte ReadBlock-Funktion verwendet werden. Die Schreibfunktion sieht dann folgendermaßen aus:

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;
}

Zusammen mit der SendCommand-Funktion werden aus der ReadBlock– und WriteBlock-Funktion die vollständigen Funktionen zum Lesen, bzw. Schreiben eines Datenblocks.

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;
}

Mit der SD_ReadDataBlock-Funktion können einzelne Sektoren, wie z. B. der erste Sektor der SD-Karte, ausgelesen werden.

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

Die gelesenen Daten können überprüft werden, indem die letzten beiden Stellen des Arrays betrachtet werden. Bei einem gültigen FAT-Dateisystem sind dort die Werte 0x55 und 0xAA gespeichert.


Info:

Bei einem FAT-Dateisystem handelt es sich bei dem ersten Sektor immer um den Bootsektor für das Betriebssystem.

Dieser Sektor ist immer 512 Byte groß, egal was für ein Speichermedium genutzt wird. Das BIOS ließt diesen ein und führt den darin enthaltenen Code aus um das Betriebssystem zu laden. 


Für große Datenmengen ist es sinnvoll, sämtliche Daten in einem einzigen Durchlauf zu übertragen. Speziell für die Übertragung von mehreren Datenblöcken besitzen SD-Karten die Befehle CMD18 und CMD25, mit denen mehrere Blöcke gelesen oder geschrieben werden können.

Einmal eingeleitet sendet, bzw. empfängt die SD-Karte so lange Daten bis der Host über ein Stop-Signal die Übertragung beendet. Dies geschieht bei einem Lesevorgang über das Kommando CMD12 und bei einem Schreibvorgang durch ein Stop-Token (siehe Tabelle) in einem leeren Datenpaket.

Zum Senden, bzw. Empfangen der einzelnen Datenblöcke können die bereits erstellten Funktionen genutzt werden, sodass die Funktionen zum Senden, bzw. Empfangen von mehreren Datenblöcken leicht programmiert werden können.

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;
}

Die grundlegenden Schreib- und Lesefunktionen sind nun fertig programmiert. Wir sind damit in der Lage Daten zwischen der SD-Karte und dem Mikrocontroller auszutauschen und die Daten permanent auf der SD-Karte zu speichern.

Im nächsten Teil soll es dann darum gehen, wie die Unterstützung eines FAT-Dateisystems umgesetzt wird. Das komplette Beispielprojekt kann in meinem GitLab-Repository heruntergeladen werden.

Viel Spaß beim Ausprobieren und vielen Grüße
Daniel

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.