Using the FAT filesystem on SD cards with the STM32F4 Processor: Part II

In part I of this tutorial, we created a project for our STM32F4DISCOVERY board that read and wrote sectors to an SD card. In today's installment, we'll add a filesystem on top of that.

The first thing to do is make a copy of the SDIO_Test project and name it FAT_Test. That way, if we really screw things up we can go back to a 'known good' codebase. (Or, you could use source code control. Expect a blog post on that in the near future.)

Let's set to work modifying this project to do filesystem operations. The first thing we're going to do is add the FatFs filesystem driver to our project. In the Project Explorer, under the /src folder, create a folder named FatFs. Download the FatFs source code from the appropriate link at the bottom of this page. (I'm using version R0.10a.) The downloaded .zip file has two folders in it: doc and src. Extract the contents of the archive's src folder to your /src/FatFs folder (including the option subdirectory). In Project Explorer, right click the FatFs folder and select Refresh so Eclipse will "see" the changes we made to the files in the project.

Now we need to configure FatFs. Open ffconf.h in the editor. This is where we choose which features of FatFs we want to enable. Each of the options in this file is briefly explained in the comments, and the FatFs documentation goes into more detail. For this example, let's use these settings:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#define    _FS_TINY        0    /* 0:Normal or 1:Tiny */
#define _FS_READONLY    0    /* 0:Read/Write or 1:Read only */
#define _FS_MINIMIZE    3    /* 0 to 3 */
#define    _USE_STRFUNC    0    /* 0:Disable or 1-2:Enable */
#define    _USE_MKFS    0    /* 0:Disable or 1:Enable */
#define    _USE_FASTSEEK    0    /* 0:Disable or 1:Enable */
#define _USE_LABEL    0    /* 0:Disable or 1:Enable */
#define    _USE_FORWARD    0    /* 0:Disable or 1:Enable */
#define _CODE_PAGE    1252
#define    _USE_LFN    0        /* 0 to 3 */
#define    _MAX_LFN    255        /* Maximum LFN length to handle (12 to 255) */
#define    _LFN_UNICODE    0    /* 0:ANSI/OEM or 1:Unicode */
#define _STRF_ENCODE    3    /* 0:ANSI/OEM, 1:UTF-16LE, 2:UTF-16BE, 3:UTF-8 */
#define _FS_RPATH    0    /* 0 to 2 */
#define _VOLUMES    1
#define _STR_VOLUME_ID    0    /* 0:Use only 0-9 for drive ID, 1:Use strings for drive ID */
#define _VOLUME_STRS    "RAM","NAND","CF","SD1","SD2","USB1","USB2","USB3"
#define    _MULTI_PARTITION    0    /* 0:Single partition, 1:Enable multiple partition */
#define    _MIN_SS        512
#define    _MAX_SS        512
#define    _USE_ERASE    0    /* 0:Disable or 1:Enable */
#define _FS_NOFSINFO    0    /* 0 to 3 */
#define _WORD_ACCESS    0    /* 0 or 1 */
#define    _FS_LOCK    0    /* 0:Disable or >=1:Enable */
#define _FS_REENTRANT    0        /* 0:Disable or 1:Enable */
#define _FS_TIMEOUT        1000    /* Timeout period in unit of time ticks */
#define    _SYNC_t            HANDLE    /* O/S dependent sync object type. e.g. HANDLE, OS_EVENT*, ID and etc.. */

As you can see, I've disabled a number of features of FatFs. Of course, you're free to experiment and enable the features that you need for your application. But a stripped-down version of FatFs without all the bells and whistles seems to be a good starting point.

If you try to build your project at this point, you'll get a compile error that one of the files in the option subdirectory isn't needed. Remove this file from your project by right-clicking on it in the /src/FatFs/option folder and selecting Resource Configurations... Exclude From Build. Check all the checkboxes to tell Eclipse not to build this file into any of your project's configurations. Repeat this exercise until you no longer get these errors. (These errors occur because we selected code page 1252 and these files implement different code pages.)

At some point, your project will refuse to build because a header file #included from diskio.c doesn't exist. Diskio.c is the file that interfaces the filesystem driver with the lower level physical layer. It is here that we'll need to do most of our work.

As I said in part 1 of this tutorial, the real work of this project is simply marrying the calls that FatFs needs with the calls that the SDIO library provides us with. After you strip away all of the false starts I made, doing so is actually stunningly easy. We know that this file is going to implement the SDIO interface, so the first thing we need to do is include the appropriate header file:

1
#include "sdio_high_level.h"

Let's start with disk_initialize, which is going to be the most complex function we'll implement. Even then, it's only a copy-and-paste from the main.c code in the SDIO_Test project. We're first going to ensure that the physical drive referenced is drive 0 (my code only handles one drive), then we set up the NVIC, then call SD_Init(). Here's the whole code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
DSTATUS disk_initialize (
    BYTE pdrv                /* Physical drive nmuber (0..) */
)
{
    if (pdrv != 0)
    {
        return STA_NOINIT;
    }

    NVIC_InitTypeDef NVIC_InitStructure;

    /* Configure the NVIC Preemption Priority Bits */
    NVIC_PriorityGroupConfig (NVIC_PriorityGroup_1);
    NVIC_InitStructure.NVIC_IRQChannel = SDIO_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init (&NVIC_InitStructure);
    NVIC_InitStructure.NVIC_IRQChannel = SD_SDIO_DMA_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_Init (&NVIC_InitStructure);

    SD_Error Status = SD_Init ();
    if (Status != SD_OK)
    {
        return STA_NOINIT;
    }

    return 0;
}

Pretty straighforward, right? We verify that a valid disk (drive zero) is being accessed, then we plug in the code stolen from the SDIO example to initialize the SD card.

The disk_status() function should return to the caller whether or not the physical media is ready. It can indicate that everything is OK, that the media is not initialized, and/or that the media is write-protected. Ideally, for SD media, we'd use the Card Detect pin and the Write Protect pin to track the status of the card. Because I'm a lazy programmer doing prototype work, I took the easy way out: my code always reports that the disk is ready. If you're developing a project where media might be removed by the user while your device is in operation, you might want to respond to this a bit more carefully. Here's my code:

1
2
3
4
5
6
7
8
9
10
11
DSTATUS disk_status (
    BYTE pdrv        /* Physical drive nmuber (0..) */
)
{
    if (pdrv != 0)
    {
        return STA_NOINIT;
    }

    return 0;
}

FatFs defines a number of disk I/O control functions that can be called under certain circumstances. Given all of the features that we've #defined out of existence, only one is necessary in our implementation. We need to implement the CTRL_SYNC command, which needs to ensure that all data is flushed to physical media before it returns. This could be useful in situations where there's a caching layer between FatFs and the physical media. In our case, the SDIO code takes care of this for us. Whenever we write blocks to the physical media, the code waits for the DMA to complete, which indicates that the write has been flushed to the media. So really, our implementation of disk_ioctl doesn't have to do anything!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#if _USE_IOCTL
DRESULT disk_ioctl (
    BYTE pdrv,        /* Physical drive nmuber (0..) */
    BYTE cmd,        /* Control code */
    void *buff        /* Buffer to send/receive control data */
)
{
    if (pdrv != 0)
    {
        return RES_PARERR;
    }

    switch (cmd)
    {
    case CTRL_SYNC:
        //do nothing. By calling SD_WaitReadOperation and
        //SD_WaitWriteOperation we already ensure that operations
        //complete in the read and write functions.
        return RES_OK;
        break;
    default:
        return RES_PARERR;
    }
}
#endif

That leaves only two functions that we need to implement: one to read sectors from the disk, and one to write sectors to the disk. Again, given the support of the SDIO library, there's not much to these functions and there's not much for me to comment on. Here they are:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
DRESULT disk_read (
    BYTE pdrv,        /* Physical drive nmuber (0..) */
    BYTE *buff,        /* Data buffer to store read data */
    DWORD sector,    /* Sector address (LBA) */
    UINT count        /* Number of sectors to read (1..128) */
)
{
    if (pdrv != 0)
    {
        return RES_PARERR;
    }

    uint64_t readAddr = sector * 512;
    SD_Error err = SD_ReadMultiBlocks(buff, readAddr, 512, count);
    if (err == SD_OK)
    {
        err = SD_WaitReadOperation();
        if (err == SD_OK)
        {
            while (SD_GetStatus() != SD_TRANSFER_OK){}
        }
        else
        {
            return RES_ERROR;
        }
    }
    else
    {
        return RES_ERROR;
    }
    return RES_OK;
}


#if _USE_WRITE
DRESULT disk_write (
    BYTE pdrv,            /* Physical drive nmuber (0..) */
    const BYTE *buff,    /* Data to be written */
    DWORD sector,        /* Sector address (LBA) */
    UINT count            /* Number of sectors to write (1..128) */
)
{
    if (pdrv != 0)
    {
        return RES_PARERR;
    }

    uint64_t writeAddr = sector * 512;
    SD_Error err = SD_WriteMultiBlocks(buff, writeAddr, 512, count);
    if (err == SD_OK)
    {
        err = SD_WaitWriteOperation();
        if (err == SD_OK)
        {
            while (SD_GetStatus() != SD_TRANSFER_OK){}
        }
        else
        {
            return RES_ERROR;
        }
    }
    else
    {
        return RES_ERROR;
    }
    return RES_OK;
}
#endif

Lastly, we need a function to return a valid FAT file time. The code below, I believe, returns midnight, Jan. 1, 1980:

1
2
3
4
DWORD get_fattime()
{
    return 0b00000000001000010000000000000000;
}

OK, well, that's it. I'd love to have lots of commentary here expounding on the wonders of this code, but frankly, there's nothing amazing here. The code we added is simple; the amazing parts happen in the SDIO code and the FatFs code, both of which we copied from other sources.