Kampis Elektroecke

AVR mit einer SD-Karte erweitern – Teil 1

SD-Karten eignen sich hervoragend dazu, große Datenmengen zu speichern und am Computer wieder abrufen zu können. Dies macht den Einsatz von SD-Karten gerade für Mikrocontrollerprojekte durchaus interessant, da ein Mikrocontroller in der Regel nur ein, vergleichsweise kleines, EEPROM als Datenspeicher bereitstellt.

Beginnend mit diesem Artikel möchte ich zeigen, wie ein Interface für SD-Karten mit FAT Unterstützung auf einem AVR Mikrocontroller (hier ein XMega256A3BU) implementiert werden kann.

Aufbau und Funktionsweise einer SD-Karte:

Eine SD-Karte besteht aus einem Interface, einem Controller, ein paar Registern und dem eigentlichen Speicherelement.

Pin SD SPI
1 DAT3 CS
2 CMD DI
3 VSS VSS
4 VDD VDD
5 CLK SCLK
6 VSS VSS
7 DAT0 DO
8 DAT1 NC
9 DAT2 NC

Als Kommunikationsschnittstelle soll das SPI des Mikrocontrollers zum Einsatz kommen. In diesem Beispiel verwende ich den USART-SPI des XMega-Mikrocontroller. Ein Steuerbefehl für die SD-Karte ist dabei folgendermaßen aufgebaut:

Quelle: http://elm-chan.org/docs/mmc/mmc_e.html

Im SPI-Modus besteht jeder Befehl für die SD-Karte aus einem 8 Bit Kommando, bei dem das letzte Bit auf 0 und das vorletzte Bit 1 gesetzt ist. Die restlichen Bits ergeben sich aus dem Index des Befehls. Danach folgt ein 4 Byte langes Befehlsargument und eine 1 Byte lange CRC (im SPI Modus optional).

Nachdem der Befehl gesendet worden ist, benötigt die SD-Karte eine gewisse Zeit um diesen Befehl zu verarbeiten und darauf zu antworten. Diese Zeit NCR beträgt zwischen 0 und 8 Bytes für SD-Karten, bzw. 1 bis 8 Bytes für MM-Karten. Nach Ablauf der Zeit sendet die Karte die entsprechende Antwort auf den Befehl. Während der NCR-Phase muss permanent ein 0xFF an die Karte gesendet werden, womit DO auf High liegt und die SD-Karte getaktet über SCLK wird.

Für die Kommunikation mit der SD-Karte sind folgende Befehle definiert:

Index Argument Antwort Daten Beschreibung
CMD0 0 R1 Nein Softwareseitiger Reset der Karte
CMD1 0 R1 Nein Karte initialisieren
ACMD41* 0x40000000 R1 Nein Karte Initialisieren (nur SDC)
CMD8 0x1AA R3 Nein Spannung überprüfen (nur SDC V2)
CMD9 0 R1 Ja CSD-Register auslesen
CMD10 0 R1 Ja CID-Register auslesen
CMD12 0 R1b Nein Daten lesen abbrechen
CMD16 Blocklänge R1 Nein Blockgröße ändern
CMD17 Adresse R1 Ja Einzelnen Block lesen
CMD18 Adresse R1 Ja Mehrere Blöcke lesen
CMD23 Anzahl Blöcke R1 Nein Anzahl zu sendender Blöcke setzen (nur MMC)
ACMD23* Anzahl Blöcke R1 Nein Anzahl der zu löschenden Blöcke beim nächsten Multi-Block Schreiben setzen (nur SDC)
CMD24 Adresse R1 Ja Block schreiben
CMD25 Adresse R1 Ja Mehrere Blöcke schreiben
CMD55 0 R1 Nein ACMD-Kommando einleiten
CMD58 0 R3 Nein OCR-Register lesen
*ACMD<n> : Diese Kommandos sind eine Aneinanderreihung von CMD55 und CMD<n>

Die SD-Karte antwortet auf jeden Befehl mit einem R1 oder R3. Eine R1-Antwort besteht aus 8 und eine R3-Antwort aus 40 Bit.

R1-Antwort
7 6 5 4 3 2 1 0
0 Parameter Error Address Error Erase Sequence Error Command CRC Error Illegal Command Erase Reset In Idle State

Die R1-Antwort enthält immer den aktuellen Zustand des Status-Registers der SD-Karte. Ein Wert von 0x00 bedeutet, dass der Befehl erfolgreich verarbeitet worden ist. Der Befehl CMD12 gibt eine R1b Antwort zurück, welche sich nur in einer längeren Zeit NCR von der Antwort R1 unterscheidet.

Eine R3-Antwort beinhaltet immer den aktuellen Wert des Status-Registers (also eine R1-Antwort), sowie den Wert des OCR-Registers.

R3-Antwort
R1 (8 Bit) OCR-Register (32 Bit)

Nach einem Reset (z. B. dem Einstecken in den Kartenleser) muss eine SD-Karte immer erst initialisiert werden. Während der Initialisierung stellt der Host (in diesem Beispiel der Mikrocontroller) außerdem fest, um was für einen Typ von Speicherkarte es sich handelt, sprich ob es eine SD-Karte bis 2 GB Speicherkapazität, eine Karte mit einer Kapazität >2 GB oder um eine Multimediacard (MMC) handelt. Je nach Kartentyp wird eine andere Initialisierung verwendet.

Aus „Physical Layer Simplified Specification Version 4.10 – Figure 7-2“

Implementierung des SPI-Treibers:

Wie bereits erwähnt soll die die Karteim SPI-Modus betrieben und das USART-SPI-Interface des Mikrocontrollers genutzt werden. Natürlich kann auch jede andere SPI-Schnittstelle genutzt werden, indem der Code entsprechend angepasst wird.

Zuerst wird die Schnittstelle initialisiert werden. Laut Spezifikation müssen SD-Karten im SPI-Modus 0 (CPOL = 0, CPHA = 0) betrieben werden. Für die Anbindung der Karte sind folgende I/Os genutzt worden:

Pin Signal
PD1 SCLK
PD2 MISO
PD3 MOSI
PE0 CS

Die I/Os für die Signale SCK, MOSI und CS müssen während der Initialisierung als Ausgang definiert und auf High gesetzt werden. Im Idle-Zustand müssen die Datenleitungen zudem auf High liegen.

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

Bei der Schnittstellenkonfiguration müssen folgende Einstellungen durchgeführt werden:

  • Einstellen des Modus (USART-SPI)
  • Konfigurieren der Clockphase und der Clockpolarität (CPHA = 0, CPOL = 0)
  • Reihenfolge der Daten einstellen (LSB zuerst)
  • Receiver und Transmitter aktivieren
USART_SetMode(&USARTD0, USART_MODE_MSPI);

GPIO_InvDisable(&PORTD, SPI_SCK_PIN);

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

Es wird zudem empfohlen nach dem Abwählen der SD-Karte noch mind. 80 weitere Taktzyklen am SPI zu erzeugen, damit die SD-Karte ihre internen Abläufe abschließen kann. Zusätzlich dazu erzeuge ich noch acht Taktimpulse am SPI bevor ich die Karte anwähle.

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-Karten sollten während der ersten Initialisierung mit einer Taktfrequenz von etwa 100 kHz bis 400 kHz betrieben werden, da diese Taktfrequenz sowohl von alten, als auch neuen Karten unterstützt wird. Neuere Karten können auch 1 MHz oder mehr, weshalb nach der Karteninitialisierung die Taktfrequenz erhöht werden kann.

Nachdem die Schnittstelle und alle GPIOs konfiguriert wurden, kann mit der Karte kommuniziert und die Initialisierung (siehe Bild) abgearbeitet werden.

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

Nach einem Power-On-Reset wird die Karte ausgewählt und anschließend werden mind. 74 Taktpulse erzeugt (das entspricht 10x 0xFF senden). Dadurch wird der native Betriebsmodus der Karte aktiviert, wodurch die Karte die oben genannten Befehle versteht. Danach wird ein CMD0 gesendet, damit die SD-Karte in den Idle-Modus versetzt wird. Sobald die SD-Karte in den Idle-Modus gewechselt hat, anwortet sie mit einer R1 Antwort (0x01 – siehe Übersicht).

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

Wenn die SD-Karte mehrmals nicht auf den Befehl reagiert, wird ein Fehler ausgegeben und die Initialisierung abgebrochen. Nach der Initialisierung wird bei den einzelnen Kommandos zudem keine Checksumme mehr benötigt.

Die Routine zum Senden eines beliebigen Kommandos mit Argumenten und Checksumme sieht folgendermaßen aus:

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

In der Funktion wird zuerst überprüft, ob es sich bei dem zu sendenden Befehl um einen ACMD<n>-Befehl handelt. Sollte dies der Fall sein, so wird zuerst das Kommando 55 gesendet um die Befehlssequenz einzuleiten. Anschließend wird das entsprechende Kommando gesendet.

Im weiteren Verlauf der Funktion wird die Karte angewählt um dann den übergebenen Befehl mit seinen Befehlsargumenten und einer Checksumme zu senden. Danach werden 8 Datenbytes gesendet um die Zeit NCR zu überbrücken und gleichzeitig wird überprüft ob die Karte bereits eine Antwort gesendet hat. Wenn eine Antwort empfangen wurde, wird die Funktion verlassen. Es ist zudem empfehlenswert acht weitere Taktzyklen zu erzeugen (also ein Leerbyte zu senden), nachdem die Antwort der Karte empfangen worden ist. Dadurch bekommt der Controller der SD-Karte etwas zusätzliche Zeit für die Abarbeitung der interne Abläufe.

Bei einem erfolgreichen Wechsel in den Idle-Mode werden die nächsten Schritte aus dem Kontrollflussgraph durchgeführt um die Initialisierung der Karte abzuschließen:

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 dieser Initalisierungsroutine wird zudem auch noch der Kartentyp bestimmt, da eine MMC einen anderen Befehlssatz verwendet als eine SD-Karte. Zudem wird eine Version 1 SD-Karte (< 2 GB) anders initialisiert, als eine Version 2 SD-Karte. Zum Schluss wird die Karte abgewählt und das Antwortbyte zurückgegeben.

Wenn die Karte erfolgreich initialisiert wurde, wird die Geschwindigkeit des SPI auf den ursprünglichen Wert zurückgeändert und damit ist die SD-Karte einsatzbereit und kann gelesen und beschrieben werden. Wie das geht erkläre ich im nächsten Teil.

Ein funktionierendes Beispielprojekt für das XPlainedC3 Demo Board kann in meinem GitHub-Repository heruntergeladen werden.

Viele Grüße
Daniel

5 Kommentare

    1. Hallo Wolfgang,

      der ist gerade fertig geworden :). Ich veröffentliche ihn in ein paar Minuten. Ich habe die SD-Karte nun komplett funktionsbereit. Daher wird der dritte Teil ~nächste bis übernächste Woche kommen.

      Gruß
      Daniel

  1. Vielen Dank für den tollen Artikel, der auch 2023 noch hilft. :-)

    Leider sind im verlinkten GitLab nur die C-Dateien, nicht aber die ebenfalls benötigten Header-Dateien (z.B. Peripheral/SD/SD.h).

Schreibe einen Kommentar

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