FAT filesystems

FAT filesystems

1. Introduction

We have seen how to use NitroFS to be able to go over the limits of the RAM available in the DS. NitroFS is super useful for graphics and music for a game, for example, because it tends to be pre-converted for extra speed. However, what if you want to let the user provide files that your application needs to use? For example, what if you want to support user avatars? You could tell the user to add a PNG file to the SD card, next to the homebrew ROM, and load it. Also, what about saving game data? NitroFS is read-only.

In order to support that kind of scenarios (and many more) there is code in BlocksDS to allow you to access the following filesystems:

  • The SD card of your flashcart (Slot-1 or Slot-2). The only requirement is that it has a DLDI driver.

  • The SD card slot of the DSi.

It’s also possible to access the internal NAND storage of the DSi, but this is heavily discouraged. Use the SD cards unless you have a very good reason to use the NAND.

All the filesystems mentioned before are formatted as FAT. BlocksDS doesn’t implement its own FAT driver due to the amount of effort it takes to do it. Instead, it uses ChaN’s FatFs, which is widely used in all sorts of projects (even commercial products), so it’s very well-tested.

2. DLDI

DLDI stands for “Dynamically Linked Device Interface”. It is a system that Nintendo DS homebrew applications use to be able to read and write data from the SD card of flashcarts.

Essentially, the application contains a filesystem driver with some “read” and “write” functions that are empty. When you load the ROM, the loader patches the empty functions by the ones corresponding to your flashcart. That way one single ROM can work in all flashcarts without actually needing to contain all drivers of all flashcarts.

Most flashcarts autopatch NDS ROMs with the right DLDI driver, but some (the very old ones) don’t do it automatically, and they force the user to do it manually. BlocksDS comes with dldipatch, which you can use to patch your ROMs. Check this archive and look for your flashcart, then use dldipatch like this:

$BLOCKSDS/tools/dldipatch/dldipatch patch dldi_driver.dldi rom.nds

It’s very unlikely you will ever need to do this, but keep it in mind of you find issues later when initializing the filesystems.

3. Initializing the filesystems

Before you can access any file you need to initialize the filesystems. The usual way to do this is to put something like this at the beginning of your application:

#include <fat.h>

int main(int argc, char *argv[])
{
    bool init_ok = fatInitDefault();
    if (!init_ok)
    {
        // Handle error
    }

    // You can initialize NitroFS now if you want
    // init_ok = nitroFSInit(NULL);
    // ...

    // Rest of the code
}

In DS consoles this will try to initialize the DLDI driver. If that fails, fatInitDefault() will return false.

In DSi consoles it tries to initialize the DLDI driver, the internal SD slot and the internal NAND memory (NAND is initialized in read-only mode to prevent accidental writes). It will only return false if the filesystem where the ROM is located can’t be initialized. For example, if your ROM is in the internal SD slot, but it can’t be initialized, it will return false even if the other filesystems can be initialized.

If you want more information about possible failures, you can use errno:

#include <errno.h>
#include <stdio.h>

#include <fat.h>

int main(int argc, char *argv[])
{
    bool init_ok = fatInitDefault();
    if (!init_ok)
    {
        // Handle error

        consoleDemoInit();

        // perror() uses the value of errno to print an error message. The
        // string passed to perror() is added to the error message. A common
        // approach is to pass the name of the function that has failed.
        perror("fatInitDefault");

        // You can also do something like this:
        printf("fatInitDefault() failed: %d", errno);

        while (1)
            swiWaitForVBlank();
    }

    // You can initialize NitroFS now if you want (and use errno or perror())
    // init_ok = nitroFSInit(NULL);
    // ...

    // Rest of the code
}

All the filesystem functions in BlocksDS set errno to values in a way that is as close as possible to Linux and similar operative systems. However, there may be situations in which the error codes of FatFs don’t translate directly to the right errno value.

Finally, you can also use nandInit() to initialize NAND. This is only required if you want to enable write access to it (if not, fatInitDefault() is enough). To disable read-only mode, call nandInit(false). You should check for errors the same way as with fatInitDefault() and nitroFSInit().

4. Accessing the filesystems

When you initialize multiple filesystems it’s important to know what’s the default filesystem. For example, if you call fopen("file.txt", "rb") it will try to open the file in the current working directory, not in the root of the filesystem.

In DS mode, fatInitDefault() sets DLDI as the main filesystem. In DSi mode, the internal SD slot, DLDI or NAND depending on where your ROM is stored.

If you call nitroFSInit() it will set NitroFS as default.

nandInit() never changes the default filesystem.

The filesystems can be accessed with the following drive names:

  • "fat:/": DLDI drive (slot-1 or slot-2 flashcart).
  • "sd:/": Internal SD of the DSi.
  • "nand:/": Main partition of the NAND of DSi.
  • "nand2:/": Second partition of the NAND of DSi (used to store photos).
  • "nitro:/": NitroFS.

If you want to manually check if one specific filesystem is available you can do it this way:

#include <unistd.h>

int main(int argc, char *argv[])
{
    // ...

    if (access("sd:/", F_OK) != 0)
    {
        // We can't access the internal SD slot
    }

    // ...
}

You can freely switch the current working directory using chdir():

#include <stdio.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    // ...

    if (chdir("sd:/data/mygame") != 0)
    {
        // We can't switch to that directory
    }

    // This will open "sd:/data/mygame/save.bin"
    FILE *f = fopen("save.bin", "rb");

    // ...
}

If you want to know the location of the NDS ROM you can check argv[0]. This relies on having a loader that provides the application with that information, of course:

#include <stdio.h>

int main(int argc, char *argv[])
{
    consoleDemoInit();

    for (int i = 0; i < argc; i++)
        printf("argv[%d] = %s\n", i, argv[i]);

    while (1)
        swiWaitForVBlank();
}

But what if you just want to use chdir() to set the working directory to the directory that stores the NDS ROM? There are two additional helpers provided by libnds:

  • fatGetDefaultDrive() returns a const char * that will contain "fat:/ in a DS. In DSi it may contain "fat:/", "sd:/" or "nand:/" depending on where your ROM is located.

    Important: This pointer must not be passed to free().

  • fatGetDefaultCwd() returns a char * that contains the path to the folder that contains the NDS ROM (excluding the name of the ROM). If your ROM is located in sd:/folder/file.nds it will return sd:/folder/. If argv[0] isn’t provided it will return the same value as fatGetDefaultDrive().

    Important: Remember to call free() to free the memory used by this string after you have used it.

5. Example: Filesystem configuration for games

For example, if most of what you do is use NitroFS to load game graphics and audio, but you sometimes write saved data to the same folder that contains your ROM, you can do:

int main(int argc, char *argv[])
{
    bool init_ok = fatInitDefault();
    if (!init_ok)
    {
        // Handle error and hang
    }

    init_ok = nitroFSInit();
    if (!init_ok)
    {
        // Handle error and hang
    }

    // Set NitroFS as the default filesystem
    if (chdir("nitro:/");

    FILE *f = fopen("background/data.bin", "rb");

    // Load game data

    // ...

    // When you want to save data

    char *cwd = fatGetDefaultCwd();
    char path[512];
    snprintf(path, sizeof(path), "%s/savedata.bin", cwd);
    FILE *fout = fopen(path, "wb");
    free(cwd);

    // Now you can write data to fout
}

6. Example: Writing save data to the filesystem

BlocksDS comes with an example of a very basic save data system: examples/filesystem/save_data.

The idea is to have a global struct with the information you want to be able to save and restore from the filesystem. Then, you write/read the struct in one go.

typedef struct {
    uint32_t key_press_count[15];
} game_state_t;

static game_state_t game_state;

static bool load_data(const char *path)
{
    FILE *f = fopen(path, "rb");
    if (f == NULL)
    {
        printf("Can't open file\n");
        return false;
    }

    if (fread(&game_state, sizeof(game_state), 1, f) != 1)
    {
        printf("Can't read file\n");
        return false;
    }

    if (fclose(f) != 0)
    {
        printf("Can't close file\n");
        return false;
    }

    return true;
}

static bool save_data(const char *path)
{
    FILE *f = fopen(path, "wb");
    if (f == NULL)
    {
        printf("Can't open file\n");
        return false;
    }

    if (fwrite(&game_state, sizeof(game_state), 1, f) != 1)
    {
        printf("Can't write file\n");
        return false;
    }

    if (fclose(f) != 0)
    {
        printf("Can't close file\n");
        return false;
    }

    return true;
}

This solution has several issues:

  • It doesn’t support different versions with different structures. If you update your application you will need to handle different (probably incompatible) versions of the structure.
  • It doesn’t support save data that may have a varying size. For example, in a city-building game the player may want to store one city or 20. It doesn’t make sense to always save files with enough space for 20 cities!
  • It won’t work in big-endian machines. Anything bigger than a byte will be read incorrectly in big-endian machines because the DS is little-endian.
  • Different compilers may use different padding in a struct. If your struct contains a u32, u16 and u8 the compiler will probably add a padding of a byte so that the full size is 8 bytes, which is a multiple of 4 (the size of a word on DS, which has a 32-bit CPU). Compilers for a 64-bit architecture may introduce different padding, for example.

In general, it’s better to have a function that manually reads each field of the file. It should be able to handle endianness, and it is free to add padding or not according to your needs because you control every single byte that you read and write, unlike with a struct, which the compiler controls.

7. Example: Loading JPG file from the filesystem

Something you can do is let the user provide additional files to your application. For example, in a photo viewer you need the user to provide their own files and you need to be able to open them and display them.

Example with gbajpeg

You could ask the user to convert them to one of the formats supported by the DS, but that isn’t very user-friendly. Normally you would let the user have files in a common format, like JPG, and the application is in charge of reading the file and converting it.

One of the packages you can install from the BlocksDS package manager is blocksds-gbajpeg:

wf-pacman -Sy blocksds-gbajpeg

You can see the full example (including loading the file from the filesystem) here. All you have to do in your program is to load the whole JPG file to RAM and then call this function:

#include <gba-jpeg-decode.h>

uint16_t buffer[256 * 192];

int main(int argc, char *argv[])
{
    void *jpg_file = NULL;

    // Load all the file into a buffer in RAM and point jpg_file to it

    JPEG_DecompressImage(jpg_file, buffer, 256, 192);

    // Copy the contents of "buffer" to VRAM, for example
}

You will also need to add gbajpeg to the Makefile:

LIBS        := -lgbajpeg -lnds9 -lc
LIBDIRS     := $(BLOCKSDS)/libs/libnds \
               $(BLOCKSDSEXT)/gbajpeg

8. Third-party libraries

The examples shown before are just a small showcase of what you can do. There are plenty of libraries you can use to load files in different formats. Most of them will just work with BlocksDS because BlocksDS implements the most common C library functions, which cross-platform libraries depend on.

This is a list of Some popular libraries you may be interested in:

This tutorial won’t explain how to use any of them. You should check the documentation, examples and tutorials of the libraries you want to use.

Wonderful Toolchain has packages with pre-built versions of FreeType, libpng and zlib:

wf-pacman -Sy toolchain-gcc-arm-none-eabi-freetype
wf-pacman -Sy toolchain-gcc-arm-none-eabi-libpng16
wf-pacman -Sy toolchain-gcc-arm-none-eabi-zlib