Kampis Elektroecke

AVR mit einer SD-Karte erweitern – Teil 3

Im letzten Teil des Tutorials bin ich etwas detaillierter auf die Kommunikation mit der SD-Karte eingegangen. Wir haben gelernt, wie wir einzelne, bzw. mehrere Blöcke einer SD-Karte lesen und schreiben, sowie wie wir bestimmte Status- oder Karteninformationen auslesen können. In diesem Teil des Tutorials werden diese Komponenten genutzt, um das FAT-Modul FatFs nutzen zu können.

Dieses Tutorial baut auf die FatFs-Version R0.13c von Elm Chan auf, welche hier heruntergeladen werden kann. In dem Verzeichnis source des heruntergeladenen Archivs befinden sich alle notwendigen Dateien um die FAT-Unterstützung für den Mikrocontroller zu realisieren. Unsere Aufgabe besteht darin, die Datei diskio.c entsprechend unseres Treibers für SD-Karten anzupassen.

Weiterhin ist noch eine Datei mit dem Namen ffconf.h abgelegt und wird zur Konfiguration des Dateisystems genutzt. Es wurden folgende Konfigurationspunkte geändert:

Konfiguration Neuer Wert Funktion
FF_USE_STRFUNC 1 Aktiviert Funktionen wie f_puts um Zeichenketten in Dateien zu schreiben oder aus Dateien zu lesen.
FF_CODE_PAGE 850 Codepage auf 850 (Latin 1) setzen
FF_USE_LFN 2 Lange Dateinamen unterstützen
FF_FS_TINY 1 Buffergröße verringern
FF_FS_NORTC 1 RTC deaktivieren (weil nicht vorhanden)

Die Funktionsprototypen der Funktionen, die der Anwender in der Datei diskio.c implementieren muss, sind bereits alle angelegt. Unsere Aufgabe besteht also darin die folgenden Funktionen zu schreiben:

Funktion Aufgabe
disk_initialize Speichermedium initialisieren
disk_status Status des Speichermediums auslesen
disk_read Einen oder mehrere Sektoren des Speichermediums auslesen
disk_write Einen oder mehrere Sektoren des Speichermediums schreiben
disk_ioctl Steuerung des Speichermediums

Zusätzlich zeigt Elm Chan, wie mehrere verschiedene Speichermedien mit dem Modul genutzt werden können indem jedes Medium eine eigene Laufwerksnummer bekommt:

/* 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 diesem Tutorial soll nur eine SD-Karte als Laufwerk benutzt werden. Die SD-Karte soll zudem die Nummer 0 bekommen.

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

Die erste Funktion, die wir implementieren wollen ist die Funktion disk_initialize, mit der das angeforderte Speichermedium initialisiert wird. Diese Funktion besitzt lediglich die Laufwerksbezeichnung als Übergabeparameter, welcher mittels einer switch-Anweisung ausgewertet werden muss:

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

Wenn das Laufwerk erfolgreich initialisiert worden ist, wird zudem noch eine entsprechende Statusvariable gesetzt. Diese Variable kann dann mit der Funktion disk_status abgefragt werden:

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

Als nächstes wird die Funktion disk_read implementiert. Diese Funktion übergibt, neben der Laufwerkskennung, einen Zeiger auf einen Datenbuffer, den angeforderten Sektor und die Anzahl an Sektoren, die gelesen werden sollen. Diese Informationen werden ausgewertet und die entsprechenden Funktionen des SD-Kartentreibers aufgerufen:

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

Bei der Funktion disk_write wird ähnlich vorgegangen:

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

Jetzt fehlt nur noch die Funktion disk_ioctl. Diese Funktion hat die Aufgabe ein allgemeines Interface für die SD-Karte bereitzustellen, über das Informationen, wie z. B. die Sektorgröße abgerufen werden können. Das angeforderte Kommando wird als Parameter in die Funktion übergeben und muss dann ausgewertet und ausgeführt werden. Die Funktion der einzelnen Kommandocodes kann in der Dokumentation nachgelesen werden.

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

Bei dem Kommando GET_BLOCK_SIZE wird die Erase Block Size des Speichermediums zurückgegeben. Diese Information lässt sich bei SD-Karten mit Version 2 oder später aus dem Statusregister auslesen (siehe Tutorial Teil 2):

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

Das Kommando GET_SECTOR_COUNT fragt die Anzahl an Sektoren des Speichermediums ab. Diese Information kann dem CSD-Register der SD-Karte entnommen werden. Bei SD-Karten mit Version 2 oder später kann die Anzahl der Sektoren über das CSD-Register berechnet werden.

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

Mit Hilfe des Kommandos GET_SECTOR_SIZE wird die Größe eines Sektors des Speichermediums abgefragt. SD-Karten mit einer Version 2 oder später haben eine feste Sektorgröße von 512 Byte. Daher gibt dieses Kommando lediglich einen konstanten Wert zurück.

Das letzte Kommando namens CTRL_SYNC dient dazu um offene Schreiboperationen des Speichermediums zu beenden. Wenn das Gerät einen Cache hat, muss dieser Cache geleert und in den Speicher geschrieben werden. Bei der SD-Karte wird die Karte abgewählt, wodurch die SD-Karte alle offenen Operationen beendet:

void SD_Sync(void)
{
	SD_Deselect();
}

Damit wären alle notwendigen Funktionen programmiert und die SD-Karte kann beschrieben werden. Dazu soll der folgende Codeschnipsel verwendet werden:

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

Ähnlich wie bei Windows besteht auch bei FatFs der Dateiname aus einer Laufwerksbezeichnung und dem eigentlichen Namen. In diesem Beispiel soll also die Datei File.txt auf dem Laufwerk mit der Kennung 0 gespeichert werden. Dies entspricht der SD-Karte.

Zu allererst wird die SD-Karte über die f_mount-Funktion eingebunden. Über den Parameter 1 wird das Speichermedium direkt eingebunden, sodass man direkt überprüfen kann ob die Speicherkarte erkannt wird.

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

Anschließend soll eine neue Datei mit dem Pfad 0:File.txt angelegt, mit dem Text Hello, World gefüllt und wieder geschlossen werden. Eine bereits vorhandene Datei wird dabei überschrieben.

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

Als letztes soll die erstellte Datei geöffnet, der komplette Inhalt ausgelesen und wieder geschlossen werden.

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

Wenn das Programm nun gestartet wird, wird die SD-Karte eingebunden, die Datei angelegt, beschrieben und ausgelesen und der Inhalt in dem Array ReadData gespeichert. Mit einem Kartenleser kann nun zusätzlich der Inhalt der SD-Karte überprüft werden.

Hat alles funktioniert? Wunderbar! Damit habt ihr euren AVR erfolgreich mit einer SD-Karte und einem FAT-Dateisystem ausgestattet.


Info:

Ich habe den aktuelle Code mit vielen verschiedenen SD-Karten und verschiedenen Speichergrößen (4 – 32 GB) getestet. Dennoch kann es vorkommen, dass einzelne SD-Karten nicht korrekt initialisiert werden. So ist es bei mir vorgekommen, dass eine einzige Karte (8 GB, Klasse 4) der getesteten Karten nicht initialisiert werden kann. Falls es also nicht funktionieren sollte, so solltet ihr als erstes die Karte wechseln und es erneut probieren.


Das komplette Projekt findet ihr in meinem GitLab-Repository.

Viel Spaß mit der SD-Karte und fröhliches beschreiben :)
Daniel

5 Kommentare

  1. Hallo Daniel,
    Super Artikel, einfach erklärt und nachvollziehbar!
    Leider haben sich einige Fehler eingeschlichen:
    – Die typedefs der Kartenregister sind falsch sortiert —> die Karte sendet alles in MSB (zum. auf little endian Hardware)
    – CSD-Register: Unterscheidung V1/V2 fehlt
    – SD_SendCommand Funktion: If Bed. am Ende (write-cmd) ist Fehlerhaft, somit fehlt immer ein Resp.-Byte.

    Weiter bin ich noch nicht gekommen, bleib aber dran :p

    Wenn du fragen hast, schreib mir ne Mail, wenn du Interesse hast, kann ich gerne einen Pull-Request schreiben, sobald ich weiß wie man das macht :)

    Viele Grüße Rouven

    1. Hallo Rouven,

      bei einigen Registern hast du recht mit der falschen Sortierung (OCR und Status waren richtig). Die Karten senden aber immer alles MSB first. Es muss nur auf der Zielplattform unterschieden werden ob die Architektur Litte oder Big Endian ist :)

      Die Fehler (bis auf den in der Funktion “SendCommand) habe ich soweit korrigiert und werde die Änderungen im Laufe des Tages pushen. Ggf. kannst du mir eine Mail schreiben, in der du das Problem mit der Funktion genauer beschreibst? Weitere Probleme kannst du natürlich auch direkt per Mail an mich schicken :)

      Dank dir und viele Grüße!
      Daniel

Schreibe einen Kommentar zu Rouven Antworten abbrechen

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