Per l'utilizzo della memoria SD, la scelta del Filesystem è inevitabilmente ricaduta su uno di tipo FAT; in questo modo è possibile
caricare i file direttamente da pc.
Nel file fat16.c è implementato il supporto a FAT16, realizzando la stessa interfaccia di MPFS, in modo tale da essere compatibile con esso (per ora in sola lettura).
Ciò significa che i metodi di questo file hanno lo stesso nome, parametri e comportamento di quelli presenti nel file MPFS.c.
Per meglio comprendere il funzionamento del codice, vediamo com'è organizzata una generica memoria di massa.
Inannzitutto, la memoria è divisa in settori solitamente costituiti da 512byte; più settori insieme formano
un cluster, la cui dimensione viene stabilita durante la creazione della partizione, e scritta in un apposito campo del Filesystem.
Il cluster è l'unità di base della FAT16, ad esempio un file di dimensione nulla, occupa comunque un intero cluster.
Il primo settore è denominato MBR (Master Boot Record), e contiene principalmente codice eseguibile per il lancio del sistema operativo.
Il nostro interesse, però, ricade su una piccola porzione di questo settore, ovvero la Partition Table, nella quale troviamo informazioni sulle partizioni.
In particolare, da un campo di tale tabella si può ricavare la locazione d'inizio della partizione, e quindi del Filesystem.
Offset | Descrizione | Dimensione |
0x000 | Codice eseguibile | 446 Byte |
0x1BE | Informazioni sulla Prima Partizione | 16 Byte |
0x1CE | Informazioni sulla Seconda Partizione | 16 Byte |
0x1DE | Informazioni sulla Terza Partizione | 16 Byte |
0x1EE | Informazioni sulla Quarta Partizione | 16 Byte |
0x1FE | Firma Boot Sector: sempre 0x55AA | 2 Byte |
Tali informazioni sulle partizioni sono così formate:
Offset | Descrizione | Dimensione |
0x00 | Stato della partizione (attivo o inattivo) | 1 Byte |
0x01 | Inizio partizione: Head | 1 Byte |
0x02 | Inizio partizione: Cylinder/Sector | 2 Byte |
0x04 | Tipo della partizione | 1 Byte |
0x05 | Fine partizione: Head | 1 Byte |
0x06 | Fine partizione: Cylinder/Sector | 2 Byte |
0x08 | Inizio della partizione in settori | 4 Byte |
0x0C | Numero di settori nella partizione | 4 Byte |
Quindi il campo che vogliamo leggere è il byte 0x1C6 del primo settore.
A questo punto possiamo leggere il primo settore della FAT16 (detto Boot Record), nel quale troviamo
numerose informazioni che devono essere salvate in alcune varibili.
Offset | Descrizione | Dimensione |
0x000 | Jump Code e NOP | 3 Byte |
0x003 | Nome OEM | 8 Byte |
0x00B | Byte Per Settore | 2 Byte |
0x00D | Settori Per Cluster | 1 Byte |
0x00E | Settori Riservati | 2 Byte |
0x010 | Numero di Aree FAT | 1 Byte |
0x011 | Numero massimo di elementi nella Root Directory | 2 Byte |
0x013 | Numero di settori in partizioni < 32MB | 2 Byte |
0x015 | Tipo di supporto (HardDisk, ecc.) | 1 Byte |
0x016 | Dimensioni in settori della FAT Area | 2 Byte |
0x018 | Settori per Traccia | 2 Byte |
0x01A | Numero di Head | 2 Byte |
0x01C | Numero di settori nascosti | 4 Byte |
0x020 | Dimensione in settori della partizione | 4 Byte |
... | ... | ... |
0x1FE | Firma Boot Sector: sempre 0x55AA | 2 Byte |
Abbiamo già visto il MBR e il FBR, mentre per quando riguarda le altre zone:
- FAT (File Allocation Table): nel Filesystem FAT16, come in molti altri, i cluster che formano un file
non sono necessariamente contigui, ma sono spesso frammentati (cioè sparsi dove capita nel disco); per questo motivo esiste
l'area denominata FAT, la quale contiene liste concatenate che permettono di rimettere in ordine i cluster di un singolo file. - FAT2: è semplicemente una copia di sicurezza della FAT.
- Root Directory: contiene l'elenco dei file contenuti nella directory radice del disco.
- Data Area: qui è dove si trovano i file.
Nel Filesystem MPFS i file sono contigui, quindi per semplificare il codice, nello stack Microchip un file è indentificato
dal suo indirizzo nella memoria (ma viene chiamato handle). Nel caso della FAT16, però, tale informazione non è
sufficiente, o meglio, richiederebbe di effettuare diversi calcoli ogni volta che si deve eseguire una lettura dalla memoria;
per tale motivo, è presente una tabella (denominata handleTable) contenente alcune informazioni. La define MAX_OPENED_FILES
determina la dimensione massima della tabella.
typedef struct { BYTE attrib; WORD cluster; // cluster in DataArea BYTE sector; // sector in cluster WORD byte; // byte in sector; DWORD filesize; } handleEntry; handleEntry handleTable[MAX_OPENED_FILES];
Giunti qui, possiamo vedere l'inizializzazione del driver FAT16:
/* partition data */ DWORD partitionStart; BYTE clusterSize; // sectors per cluster DWORD fatStart; DWORD rootStart; DWORD dataArea; BYTE rootSize; BOOL FATInit() { char* buf_pt = &sd_buffer[0]; char i; sdPresent = SDInit(); if (!sdPresent) return FALSE; SDReadSector(0); partitionStart = *(DWORD*)(buf_pt+0x1C6); SDReadSector(partitionStart); clusterSize = buf_pt[0x0D]; fatStart = partitionStart + *(WORD*)(buf_pt+0x0E); rootStart = fatStart + (*(WORD*)(buf_pt+0x16)) * buf_pt[0x10]; rootSize = *(WORD*)(buf_pt+0x11) * sizeof(dirEntry) / SECTOR_SIZE; dataArea = rootStart + rootSize; for (i = 0; i < MAX_OPENED_FILES; i++) handleTable[i].attrib = FREE_HANDLE; mpfsOpenCount = 0; return TRUE; }
Oltre ad inizializzare la memoria SD e la tabella degli Handle, vengono calcolati alcuni indirizzi caratteristici del Filesystem:
Variabile | Descrizione | Valore |
clusterSize | Numero di settori in un cluster | |
fatStart | Inizio della FAT1 | inzio della partizione + settori riservati |
rootStart | Inizio della Root Directory | Indirizzo della FAT + (numero di FAT per dimensione di una FAT) |
rootSize | Dimensione della Root Directory | Numero di elementi nella directory per dimensione di un elemento |
dataArea | Inizio dell'Area Dati | Inizio della Root Directory + la sua dimensione |
Questi indirizzi e dimensioni sono espressi in settori di 512byte.
Una directory è costituita da elementi della stessa dimensione (32 byte)
che, per quanto concerne i file con nomi corti (8+3), contengono questi campi:
Offset | Descrizione | Dimensione |
0x00 | Nome | 8 Byte |
0x08 | Estensione | 3 Byte |
0x0B | Attributi | 1 Byte |
0x0C | Riservato | 1 Byte |
0x0D | Data e ora della creazione | 7 Byte |
0x14 | Attributi Estesi | 2 Byte |
0x16 | Ora | 2 Byte |
0x18 | Data | 2 Byte |
0x20 | Primo cluster del File | 2 Byte |
0x1C | Dimensione del File | 4 Byte |
In particolare i campi Nome ed Estensione sono sempre scritti in maiscuolo, ed i caratteri non presenti
sono riempiti con il carattere spazio (0x20 in esadecimale). Ad esempio il file Pippo.txt sarà scritto nella forma
PIPPO TXT 50 49 50 50 4F 20 20 20 54 58 54
Il metodo MPFSOpen cerca un file nella memoria, e se lo trova crea un handle e lo restituisce al chiamante:
MPFS MPFSOpen(BYTE* name){ BYTE scount, ecount; dirEntry* entry; char* buf_pt = &sd_buffer[0]; char i; char name_ext[11]; if (!sdPresent) return MPFS_INVALID; for (i=0; i<11; i++) name_ext[i] = 0x20; for (i=0; i<8 && *name!='.' && *name; i++) name_ext[i] = toupper(*name++); if (*name == '.') { name++; for (i=8; i<11 && *name; i++) name_ext[i] = toupper(*name++); } scount = 0; while (scount < rootSize) { SDReadSector(rootStart + scount); ecount = 16; entry = (dirEntry*)buf_pt; while (ecount--) { if (entry->attrib == 0x0F) goto next; // Long File Name if (entry->name_ext[0] == 0x00 || entry->name_ext[0] == 0xE5) goto next; // Empty Entry if (memcmp((void*)name_ext, (void*)entry->name_ext, 11) == 0) { // found! for (i=0; i < MAX_OPENED_FILES && handleTable[i].attrib!=FREE_HANDLE; i++); if (i == MAX_OPENED_FILES) return MPFS_NOT_AVAILABLE; handleTable[i].attrib = USED_HANDLE; handleTable[i].cluster = entry->cluster; handleTable[i].filesize = entry->size; handleTable[i].sector = 0; handleTable[i].byte = 0; mpfsOpenCount++; return i; } next: entry++; } scount++; } return MPFS_INVALID; }
Nella prima parte, il nome del file viene scritto nella forma 8+3 vista in precedenza. In questo modo sarà
possibile confrontarlo con quelli presenti nella Root Directory. A questo punto vengono lette tutte le entry (elementi)
della Directory, partendo dal primo settore ed eventualmente leggendo i settori successivi.
Quando il confronto tra il nome del file cercato e quello letto dalla memoria ha esito positivo, viene cercato un handle libero
nella tabella, quindi vengono salvati alcuni dati:
- filesize: la dimensione del file in byte.
- cluster: il primo cluster del file nella Data Area.
- sector: inizializzato a zero perchè tiene traccia di quale settore stiamo leggendo all'interno dello stesso cluster
- byte: analogamente a sector, tiene traccia dell'ultimo byte letto all'interno del settore.
Con MPFSGetBegin il driver si prepara a leggere il file aperto con MPFSOpen, caricando nel buffer il settore
del file puntato dalle varibili salvate nella tabella:
BOOL MPFSGetBegin(MPFS handle){ if (handle >= MAX_OPENED_FILES || handleTable[handle].attrib != USED_HANDLE) return FALSE; // invalid handle if (handleTable[handle].filesize == 0) return TRUE; _currentHandle = handle; SDReadSector(dataArea + (handleTable[handle].cluster-2)*clusterSize + handleTable[handle].sector); return TRUE; }
Da notare che i cluster iniziano da 2 e non da zero, inoltre il numero di cluster non è un indirizzo assoluto rispetto all'intera memoria
ma è relativo alla Data Area, quindi all'indirizzo da caricare va sommato l'indirizzo della Data Area.
La funzione MPFSGet legge e restituisce un byte del file alla volta. Se tale byte si trova caricato nel buffer
della memoria SD, è sufficiente leggerlo da lì, altrimenti bisogna calcolare il prossimo settore da caricare. Quest'ultimo
se si trova all'interno dello stesso cluster sarà semplicemente il successivo, altrimenti è necessario leggere la FAT per individuare il prossimo cluster.
BYTE MPFSGet() { char* buf_pt = &sd_buffer[0]; BYTE b = 0; WORD tmp; if (_currentHandle == MPFS_INVALID) return 0; if (handleTable[_currentHandle].byte >= SECTOR_SIZE) { handleTable[_currentHandle].byte = 0; handleTable[_currentHandle].sector++; if (handleTable[_currentHandle].sector >= clusterSize) { tmp = handleTable[_currentHandle].cluster; SDReadSector(fatStart + tmp / (SECTOR_SIZE /2) ); // read FAT Area handleTable[_currentHandle].cluster = *(WORD*)(buf_pt + (tmp % (SECTOR_SIZE/2)) * 2); // next cluster in chain handleTable[_currentHandle].sector = 0; } SDReadSector(dataArea + (handleTable[_currentHandle].cluster-2)*clusterSize + handleTable[_currentHandle].sector); } b = buf_pt[handleTable[_currentHandle].byte++]; handleTable[_currentHandle].filesize--; return b; }
Nella FAT, ad ogni parola di 16 bit corrisponde un cluster. Per fare un esempio, se stiamo leggendo il file che inizia al cluster 4 (cioè il 5°),
per conoscere il prossimo cluster del file, sarà sufficiente leggere la parola corrispondente nella FAT, ovvero la 5°. L'ultima parola di una catena
è 0xFFFF.
Essendo le parole di 16bit, in un settore della FAT sono presenti 256 elementi; perciò per sapere quale settore della FAT è necessario
caricare viene eseguito questo calcolo:
cluster_corrente / (SECTOR_SIZE /2)
A questo punto, possiamo conoscere il prossimo cluster leggendo la parola numero
(cluster_corrente % (SECTOR_SIZE/2)) * 2
all'interno di tale settore.
Altri metodi:
void MPFSClose() { mpfsOpenCount--; // decrementa il numero di file aperti handleTable[_currentHandle].attrib = FREE_HANDLE; // libera l'handle } BOOL MPFSIsEOF(void) { return (handleTable[_currentHandle].filesize == -1); // se la dimensione è -1, il file è finito }
Potete realizzarlo da soli (fai-da-te) scaricando tutti i codici sorgenti (Schema elettrico realizzato con Orcad, lista parti, circuito stampato realizzato con Orcad, file gerber, codice sorgente assembler realizzato su piattaforma Microchip).
Il progetto montato e collaudato è disponibile sul nostro store --> FTPmicro