carlk3 / no-OS-FatFS-SD-SDIO-SPI-RPi-Pico

A FAT filesystem with SDIO and SPI drivers for SD card on Raspberry Pi Pico
Apache License 2.0
76 stars 15 forks source link

no-OS-FatFS-SD-SDIO-SPI-RPi-Pico

v2.3.2

C/C++ Library for SD Cards on the Pico

At the heart of this library is ChaN's FatFs - Generic FAT Filesystem Module. It also contains a Serial Peripheral Interface (SPI) SD Card block driver for the Raspberry Pi Pico derived from SDBlockDevice from Mbed OS 5, and a 4-bit wide Secure Digital Input Output (SDIO) driver derived from ZuluSCSI-firmware. It is wrapped up in a complete runnable project, with a little command line interface, some self tests, and an example data logging application.

What's new

v2.3.2

Fix initialization problem when multiple SD cards share an SPI bus. The fix puts all cards into SPI mode at driver initialization time (rather than deferring until media initialization time).

v2.3.1

Faster way to read trailing CRC bytes

v2.2.1

Substantial performance improvement for writing large contiguous blocks of data. This is accomplished by avoiding sending "stop transmission" for as long as possible. The improved throughput is especially noticeable on SDIO-attached cards, since the 4 bit wide SD interface is less of a bottleneck than the 1 bit wide SPI.

v2.1.1

Added ability to statically assign DMA channels for SPI. (See SPI Controller Configuration.)

v2.0.1

Fix miscalculation in get_num_sectors.

v2.0.0

For required migration actions, see Appendix A: Migration actions.

Note: v1.2.4 and v1.1.2 remain available on the v1.2.4 branch and the v1.1.2 branch, repectively.

Features:

Resources Used

SPI and SDIO can share the same DMA IRQ.

For the complete examples/command_line application, configured for one SPI attached card and one SDIO-attached card, release build, as reported by link flag -Wl,--print-memory-usage:

Memory region         Used Size  Region Size  %age Used
           FLASH:      167056 B         2 MB      7.97%
             RAM:       17524 B       256 KB      6.68%

A MinSizeRel build for a single SPI-attached card:

Memory region         Used Size  Region Size  %age Used
           FLASH:      124568 B         2 MB      5.94%
             RAM:       12084 B       256 KB      4.61%

A MinSizeRel build configured for three SPI-attached and one SDIO-attached SD cards:

Memory region         Used Size  Region Size  %age Used
           FLASH:      133704 B         2 MB      6.38%
             RAM:       21172 B       256 KB      8.08%

Performance

Writing and reading a file of 200 MiB of psuedorandom data on the same Silicon Power 3D NAND U1 32GB microSD card inserted into a Pico Stackable, Plug & Play SD Card Expansion Module at the default Pico system clock frequency (clk_sys) of 125 MHz, Release build, using the command big_file_test bf 200x, once on SPI and one on SDIO.

Results from a port of SdFat's bench:

Choosing the Interface Type(s)

The main reason to use SDIO is for the much greater speed that the 4-bit wide interface gets you. However, you pay for that in pins. SPI can get by with four GPIOs for the first card and one more for each additional card. SDIO needs at least six GPIOs, and the 4 bits of the data bus have to be on consecutive GPIOs. It is possible to put more than one card on an SDIO bus (each card has an address in the protocol), but at the higher speeds (higher than this implementation can do) the tight timing requirements don't allow it. I haven't tried it. Running multiple SD cards on multiple SDIO buses works, but it does require a lot of pins and PIO resources.

You can mix and match the attachment types. One strategy: use SDIO for cache and SPI for backing store. A similar strategy that I have used: SDIO for fast, interactive use, and SPI to offload data.

Notes about FreeRTOS

This library is not specifically designed for use with FreeRTOS. For use with FreeRTOS, I suggest you consider FreeRTOS-FAT-CLI-for-RPi-Pico instead. That implementation is designed from the ground up for FreeRTOS.

While FatFs has some support for “re-entrancy” or thread safety (where "thread" == "task", in FreeRTOS terminology), it is limited to operations such as:

There does not appear to be sufficient FAT and directory locking in FatFs to make operations like f_mkdir, f_chdir and f_getcwd thread safe. If your application has a static directory tree, it should be OK in a multi-tasking application with FatFs. Otherwise, you probably need to add some additional locking.

Then, there are the facilities used for mutual exclusion and various ways of waiting (delay, wait for interrupt, etc.). This library uses the Pico SDK facilities (which are oriented towards multi-processing on the two cores), but FreeRTOS-FAT-CLI-for-RPi-Pico uses the FreeRTOS facilities, which put the waiting task into a Blocked state, allowing other, Ready tasks to run.

FreeRTOS-FAT-CLI-for-RPi-Pico is designed to maximize parallelism. So, if you have two cores and multiple SD card buses (SPI or SDIO), multiple FreeRTOS tasks can keep them all busy simultaneously.

Hardware

My boards

Prewired boards with SD card sockets

There are a variety of RP2040 boards on the market that provide an integrated µSD socket. As far as I know, most are useable with this library.

Rolling your own

Prerequisites:

image

Please see here for an example wiring table for an SPI attached card and an SDIO attached card on the same Pico. SPI and SDIO at 31.5 MHz are pretty demanding electrically. You need good, solid wiring, especially for grounds. A printed circuit board with a ground plane would be nice!

image image

Construction

Pull Up Resistors and other electrical considerations

Notes about Card Detect

Firmware

Procedure

Customizing for the Hardware Configuration

This library can support many different hardware configurations. Therefore, the hardware configuration is not defined in the library[^1]. Instead, the application must provide it. The configuration is defined in "objects" of type spi_t (see sd_driver/spi.h), sd_spi_if_t, sd_sdio_if_t, and sd_card_t (see sd_driver/sd_card.h).

Illustration of the configuration dev_brd.hw_config.c

Illustration of the configuration dev_brd.hw_config.c

An instance of sd_card_t describes the configuration of one SD card socket

struct sd_card_t {
    sd_if_t type;
    union {
        sd_spi_if_t *spi_if_p;
        sd_sdio_if_t *sdio_if_p;
    };
    bool use_card_detect;
    uint card_detect_gpio;    // Card detect; ignored if !use_card_detect
    uint card_detected_true;  // Varies with card socket; ignored if !use_card_detect
    bool card_detect_use_pull;
    bool card_detect_pull_hi;
//...
}

An instance of sd_sdio_if_t describes the configuration of one SDIO to SD card interface.

typedef struct sd_sdio_if_t {
    // See sd_driver\SDIO\rp2040_sdio.pio for SDIO_CLK_PIN_D0_OFFSET
    uint CLK_gpio;  // Must be (D0_gpio + SDIO_CLK_PIN_D0_OFFSET) % 32
    uint CMD_gpio;
    uint D0_gpio;      // D0
    uint D1_gpio;      // Must be D0 + 1
    uint D2_gpio;      // Must be D0 + 2
    uint D3_gpio;      // Must be D0 + 3
    PIO SDIO_PIO;      // either pio0 or pio1
    uint DMA_IRQ_num;  // DMA_IRQ_0 or DMA_IRQ_1
    bool use_exclusive_DMA_IRQ_handler;
    uint baud_rate;
    // Drive strength levels for GPIO outputs:
    // GPIO_DRIVE_STRENGTH_2MA 
    // GPIO_DRIVE_STRENGTH_4MA
    // GPIO_DRIVE_STRENGTH_8MA 
    // GPIO_DRIVE_STRENGTH_12MA
    bool set_drive_strength;
    enum gpio_drive_strength CLK_gpio_drive_strength;
    enum gpio_drive_strength CMD_gpio_drive_strength;
    enum gpio_drive_strength D0_gpio_drive_strength;
    enum gpio_drive_strength D1_gpio_drive_strength;
    enum gpio_drive_strength D2_gpio_drive_strength;
    enum gpio_drive_strength D3_gpio_drive_strength;
//...
} sd_sdio_t;

Specify D0_gpio, but pins CLK_gpio, D1_gpio, D2_gpio, and D3_gpio are at offsets from pin D0_gpio and are set implicitly. The offsets are determined by sd_driver\SDIO\rp2040_sdio.pio. As of this writing, SDIO_CLK_PIN_D0_OFFSET is 30, which is -2 in mod32 arithmetic, so:

These pin assignments are set implicitly and must not be set explicitly.

An instance of sd_spi_if_t describes the configuration of one SPI to SD card interface.

typedef struct sd_spi_if_t {
    spi_t *spi;
    // Slave select is here instead of in spi_t because multiple SDs can share an SPI.
    uint ss_gpio;                   // Slave select for this SD card
    // Drive strength levels for GPIO outputs:
    // GPIO_DRIVE_STRENGTH_2MA 
    // GPIO_DRIVE_STRENGTH_4MA
    // GPIO_DRIVE_STRENGTH_8MA 
    // GPIO_DRIVE_STRENGTH_12MA
    bool set_drive_strength;
    enum gpio_drive_strength ss_gpio_drive_strength;
} sd_spi_if_t;

Static vs. Dynamic Configuration

The definition of the hardware configuration can either be built in at build time, which I'm calling "static configuration", or supplied at run time, which I call "dynamic configuration". In either case, the application simply provides an implementation of the functions declared in sd_driver/hw_config.h.

Customizing the FatFs - Generic FAT Filesystem Module

There are many options to configure the features of FatFs for various requirements of each project. The configuration options are defined in ffconf.h. See Configuration Options. In order to allow applications to customize ffconf.h without modifying this library, I have renamed src\ff15\source\ffconf.h in the FatFs distribution files included in this library. (This is necessary because GCC always looks in the directory of the current file as the first search directory for #include "file".) There is a somewhat customized version of ffconf.h in include\ffconf.h that is normally picked up by the library's src\CMakeLists.txt. An application may provide its own tailored copy of ffconf.h by putting it in the include path for the library compilation. For example, for CMake, if the customized ffconf.h file is in subdirectory include/ (relative to the application's CMakeLists.txt file):

target_include_directories(no-OS-FatFS-SD-SDIO-SPI-RPi-Pico BEFORE INTERFACE
    ${CMAKE_CURRENT_LIST_DIR}/include
)

For an example, see examples/unix_like.

Using the Application Programming Interface

After stdio_init_all(), time_init(), and whatever other Pico SDK initialization is required, you may call sd_init_driver() to initialize the block device driver. sd_init_driver() is now[^2] called implicitly by disk_initialize, which is called by FatFs's f_mount, but you might want to call it sooner so that the GPIOs get configured, e.g., if you want to set up a Card Detect interrupt. You need to call sd_init_driver() before you can use sd_get_drive_prefix(sd_card_t *sd_card_p), so you might need to add an explicit call to sd_init_driver if you want to use sd_get_drive_prefix to get the drive prefix to use for the path parameter in a call to f_mount. There is no harm in calling sd_init_driver() repeatedly.

Messages

Sometimes problems arise when attempting to use SD cards. At the FatFs Application Interface level, it can be difficult to diagnose problems. You get a return code, but it might just tell you FR_NOT_READY ("The physical drive cannot work"), for example, without telling you what you need to know in order to fix the problem. The library generates messages that might help. These are classed into Error, Informational, and Debug messages.

Two compile definitions control how these are handled:

Messages are sent using EMSG_PRINTF, IMSG_PRINTF, and DBG_PRINTF macros, which can be redefined (see my_debug.h). By default, these call error_message_printf, info_message_printf, and debug_message_printf, which are implemented as weak functions, meaning that they can be overridden by strongly implementing them in user code. If USE_PRINTF is defined and not zero, the weak implementations will write to the Pico SDK's stdout. Otherwise, they will format the messages into strings and forward to put_out_error_message, put_out_info_message, and put_out_debug_message. These are implemented as weak functions that do nothing. You can override these to send the output somewhere.

C++ Wrapper

At heart, this is a C library, but I have made a (thin) C++ wrapper for it: include\FatFsSd.h. For details on most of the functions, refer to FatFs - Generic FAT Filesystem Module, "Application Interface".

See examples\PlatformIO\one_SPI.C++\src\main.cpp for an example of the use of this API.

namespace FatFsNs

The C++ API is in namespace FatFsNs, to avoid name clashes with other packages (e.g.: SdFat).

class FatFs

This is a pure static class that represents the global file system as a whole. It stores the hardware configuration objects internally, which is useful in Arduino-like environments where you have a setup and a loop. The objects can be constructed in the setup and added to FatFs and the copies won't go out of scope when control leaves setup and goes to loop. It automatically provides these functions required internally by the library:

Static Public Member Functions:

class SdCard

Represents an SD card socket. It is generalized: the SD card can be either SPI or SDIO attached.

Public Member Functions:

Static Public Member Functions

class File

class Dir

Public Member Functions:

PlatformIO Libary

This library is available at https://registry.platformio.org/libraries/carlk3/no-OS-FatFS-SD-SDIO-SPI-RPi-Pico. It is currently running with

platform = https://github.com/maxgerhardt/platform-raspberrypi.git
board_build.core = earlephilhower

Next Steps

You might also need to pick up the library in CMakeLists.txt:

add_subdirectory(no-OS-FatFS-SD-SDIO-SPI-RPi-Pico/src build)
target_link_libaries(${PROGRAM_NAME} no-OS-FatFS-SD-SDIO-SPI-RPi-Pico) 

(where ${PROGRAM_NAME} is your target project) and #include "ff.h".

Happy hacking!

Future Directions

You are welcome to contribute to this project! Just submit a Pull Request in GitHub. Here are some ideas for future enhancements:

For example, if you were using a hw_config.c containing

static sd_card_t sd_cards[] = {  // One for each SD card
    {   // sd_cards[0]: Socket sd0
        .pcName = "0:",
        .type = SD_IF_SPI,
        // ...

change it to

static sd_card_t sd_cards[] = {  // One for each SD card
    {   // sd_cards[0]: Socket sd0
        .type = SD_IF_SPI,
        // ...

See Customizing for the Hardware Configuration.

If you had any references to pcName in your code, you can replace them with calls to char const *sd_get_drive_prefix(sd_card_t *sd_card_p); (sd_card.h). Note: sd_init_driver() must be called before sd_get_drive_prefix.

Migrating from no-OS-FatFS-SD-SPI-RPi-Pico master branch

The object model for hardware configuration has changed. If you are migrating a project from no-OS-FatFS-SD-SPI-RPi-Pico, you will have to change the hardware configuration customization. The sd_card_t now points to new object that specifies the configuration of either an SPI interface or an SDIO interface, and a new type member that identifies which type of interface. Also, the pcName member of sd_card_t has been removed. See Customizing for the Hardware Configuration.

For example, if you were using a hw_config.c containing

static sd_card_t sd_cards[] = {  // One for each SD card
    {
        .pcName = "0:",   // Name used to mount device
        .spi = &spis[0],  // Pointer to the SPI driving this card
        .ss_gpio = 17,    // The SPI slave select GPIO for this SD card
        // ...

that would now become

static sd_spi_if_t spi_ifs[] = {
    { 
        .spi = &spis[0],          // Pointer to the SPI driving this card
        .ss_gpio = 17,             // The SPI slave select GPIO for this SD card
        //...
static sd_card_t sd_cards[] = {  // One for each SD card
    {
        .type = SD_IF_SPI,
        .spi_if_p = &spi_ifs[0],  // Pointer to the SPI interface driving this card
        //...

Migrating from no-OS-FatFS-SD-SPI-RPi-Pico sdio branch

Instances of the interface classes sd_spi_t and sd_sdio_t are no longer embedded in sd_card_t as spi_if and sdio_if. They are moved to the top level as instances of sd_spi_if_t and sd_sdio_if_t and pointed to by instances of sd_card_t. Also, the pcName member of sd_card_t in the hardware configuration has been removed. For example, if you were using a hw_config.c containing:

static sd_card_t sd_cards[] = {  // One for each SD card
    {
        .pcName = "0:",  // Name used to mount device
        .type = SD_IF_SPI,
        .spi_if.spi = &spis[0],  // Pointer to the SPI driving this card
        .spi_if.ss_gpio = 7,     // The SPI slave select GPIO for this SD card
        //...        

that would become:

static sd_spi_if_t spi_ifs[] = {
    {
        .spi = &spis[0],  // Pointer to the SPI driving this card
        .ss_gpio = 7,     // The SPI slave select GPIO for this SD card
        //...
static sd_card_t sd_cards[] = {  // One for each SD card
    {
        .type = SD_IF_SPI,
        .spi_if_p = &spi_ifs[0],  // Pointer to the SPI interface driving this card     
        //...           

For details, see Customizing for the Hardware Configuration.

Appendix B: Operation of command_line example:

date: Print current date and time

format [<drive#:>]: Creates an FAT/exFAT volume on the logical drive. e.g.: format 0:

mount [<drive#:>]: Register the work area of the volume e.g.: mount 0:

unmount <drive#:>: Unregister the work area of the volume

chdrive <drive#:>: Changes the current directory of the logical drive.

Specifies the directory to be set as current directory. e.g.: chdrive 1: info []: Print information about an SD card cd : Changes the current directory of the logical drive. Specifies the directory to be set as current directory. e.g.: cd /dir1 mkdir : Make a new directory. Specifies the name of the directory to be created. e.g.: mkdir /dir1 rm [options] : Removes (deletes) a file or directory Specifies the path to the file or directory to be removed Options: -d Remove an empty directory -r Recursively remove a directory and its contents cp : Copies to mv : Moves (renames) to pwd: Print Working Directory ls [pathname]: List directory cat : Type file contents simple: Run simple FS tests lliot : !DESTRUCTIVE! Low Level I/O Driver Test The SD card will need to be reformatted after this test. e.g.: lliot 1 bench : A simple binary write/read benchmark big_file_test : Writes random data to file . Specify in units of mebibytes (2^20, or 1024*1024 bytes) e.g.: big_file_test 0:/bf 1 1 or: big_file_test 1:big3G-3 3072 3 cdef: Create Disk and Example Files Expects card to be already formatted and mounted swcwdt: Stdio With CWD Test Expects card to be already formatted and mounted. Note: run cdef first! loop_swcwdt: Run Create Disk and Example Files and Stdio With CWD Test in a loop. Expects card to be already formatted and mounted. Note: Type any key to quit. start_logger: Start Data Log Demo stop_logger: Stop Data Log Demo mem-stats: Print memory statistics help: Shows this command help. ``` ## Appendix C: Adding Additional Cards When you're dealing with information storage, it's always nice to have redundancy. There are many possible combinations of SPIs and SD cards. One of these is putting multiple SD cards on the same SPI bus, at a cost of one (or two) additional Pico I/O pins (depending on whether or you care about Card Detect). I will illustrate that example here. To add a second SD card on the same SPI, connect it in parallel, except that it will need a unique GPIO for the Card Select/Slave Select (CSn) and another for Card Detect (CD) (optional). Name|SPI0|GPIO|Pin |SPI|SDIO|MicroSD 0|MicroSD 1 ----|----|----|----|---|----|---------|--------- CD1||14|19||||CD CS1||15|20|SS or CS|DAT3||CS MISO|RX|16|21|DO|DAT0|DO|DO CS0||17|22|SS or CS|DAT3|CS| SCK|SCK|18|24|SCLK|CLK|SCK|SCK MOSI|TX|19|25|DI|CMD|DI|DI CD0||22|29|||CD| ||||||| GND|||18, 23|||GND|GND 3v3|||36|||3v3|3v3 ### Wiring: As you can see from the table above, the only new signals are CD1 and CS1. Otherwise, the new card is wired in parallel with the first card. ### Firmware * `hw_config.c` (or equivalent) must be edited to add a new instance to `static sd_card_t sd_cards[]` and its interface `sd_sdio_if_t` * Edit `ff14a/source/ffconf.h`. In particular, `FF_VOLUMES`: ```C #define FF_VOLUMES 2 ``` ## Appendix D: Performance Tuning Tips Obviously, if possible, use 4-bit SDIO instead of 1-bit SPI. (See [Choosing the Interface Type(s)](#choosing-the-interface-types)). Obviously, set the baud rate as high as you can. (See [Customizing for the Hardware Configuration](#customizing-for-the-hardware-configuration)). TL;DR: In general, it is much faster to transfer a given number of bytes in one large write (or read) than to transfer the same number of bytes in multiple smaller writes (or reads). The modern SD card is a block device, meaning that the smallest addressable unit is a a block (or "sector") of 512 bytes. So, it helps performance if your write size is a multiple of 512. If it isn't, partial block writes involve reading the existing block, modifying it in memory, and writing it back out. With all the space in SD cards these days, it can be well worth it to pad a record length to a multiple of 512. Generally, flash memory has to be erased before it can be written, and the minimum erase size is the "allocation unit" or "segment": > AU (Allocation Unit): is a physical boundary of the card and consists of one or more blocks and its size depends on each card. The maximum AU size is defined for memory capacity. Furthermore AU is the minimal unit in which the card guarantees its performance for devices which complies with Speed Class Specification. The information about the size and the Speed Class are stored in the SD Status. > -- SD Card Association; Physical Layer Specification Version 3.01 There is a controller in each SD card running all kinds of internal processes. When an amount of data to be written is smaller than a segment, the segment is read, modified in memory, and then written again. SD cards use various strategies to speed this up. Most implement a "translation layer". For any I/O operation, a translation from virtual to physical address is carried out by the controller. If data inside a segment is to be overwritten, the translation layer remaps the virtual address of the segment to another erased physical address. The old physical segment is marked dirty and queued for an erase. Later, when it is erased, it can be reused. Usually, SD cards have a cache of one or more segments for increasing the performance of read and write operations. The SD card is a "black box": much of this is invisible to the user, except as revealed in the Card-Specific Data register (CSD), SD_STATUS, and the observable performance characteristics. So, the write times are far from deterministic. The Allocation Unit is typically 4 MiB for a 16 or 32 GB card, for example. Of course, nobody is going to be using 4 MiB write buffers on a Pico, but the AU is still important. For good performance and wear tolerance, it is recommended that the "disk partition" be aligned to an AU boundary. [SD Memory Card Formatter](https://www.sdcard.org/downloads/formatter/) makes this happen. For my 16 GB card, it set "Partition Starting Offset 4,194,304 bytes". This accomplished by inserting "hidden sectors" between the actual start of the physical media and the start of the volume. Also, it might be helpful to have your write size be some factor of the segment size. There are more variables at the file system level. The FAT "allocation unit" (not to be confused with the SD card "allocation unit"), also known as "cluster", is a unit of "disk" space allocation for files. These are identically sized small blocks of contiguous space that are indexed by the File Allocation Table. When the size of the allocation unit is 32768 bytes, a file with 100 bytes in size occupies 32768 bytes of disk space. The space efficiency of disk usage gets worse with increasing size of allocation unit, but, on the other hand, the read/write performance increases. Therefore the size of allocation unit is a trade-off between space efficiency and performance. This is something you can change by formatting the SD card. See [f_mkfs](http://elm-chan.org/fsw/ff/doc/mkfs.html) and [Description of Default Cluster Sizes for FAT32 File System](https://support.microsoft.com/en-us/topic/description-of-default-cluster-sizes-for-fat32-file-system-905ea1b1-5c4e-a03f-3863-e4846a878d31). Again, there might be some advantage to making your write size be some factor or multiple of the FAT allocation unit. The `info` command in [examples/command_line](https://github.com/carlk3/no-OS-FatFS-SD-SDIO-SPI-RPi-Pico/tree/main/examples/command_line) reports the allocation unit. [File fragmentation](https://en.wikipedia.org/wiki/Design_of_the_FAT_file_system#Fragmentation) can lead to long access times. Fragmented files can result from multiple files being incrementally extended in an interleaved fashion. One commonly used trick is to use [f_lseek](http://elm-chan.org/fsw/ff/doc/lseek.html) to pre-allocate a file to its ultimate size before beginning to write to it. Even better, you can pre-allocate a contiguous file using [f_expand](http://elm-chan.org/fsw/ff/doc/expand.html). (Also, see [FAT Volume Image Creator](http://elm-chan.org/fsw/ff/res/mkfatimg.zip) (Pre-creating built-in FAT volume).) Obviously, you will need some way to keep track of how much valid data is in the file. You could use a file header. Alternatively, if the file contains text, you could write an End-Of-File (EOF) character. In DOS, this is the character 26, which is the Control-Z character. Alternatively, if the file contains records, each record could contain a magic number or checksum, so you can easily tell when you've reached the end of the valid records. (This might be an obvious choice if you're padding the record length to a multiple of 512 bytes.) For SDIO-attached cards, alignment of the read or write buffer is quite important for performance. This library uses DMA with `DMA_SIZE_32`, and the read and write addresses must always be aligned to the current transfer size, i.e., four bytes. (For example, you could specify that the buffer has [\_\_attribute\_\_ ((aligned (4))](https://gcc.gnu.org/onlinedocs/gcc-3.1.1/gcc/Type-Attributes.html).) If the buffer address is not aligned, the library copies each block into a temporary buffer that is aligned and then writes it out, one block at a time. (The SPI driver uses `DMA_SIZE_8` so the alignment isn't important.) For a logging type of application, opening and closing a file for each update is hugely inefficient, but if you can afford the time it can be a good way to minimize data loss in the event of an unexpected power loss or that kind of thing. You can also try to find a middle ground by periodically closing and reopening a file, or switching to a new file. A well designed directory structure can act as a sort of hierarchical database for rapid retrieval of records distributed across many small files. ## Appendix E: Troubleshooting * **Check your grounds!** Maybe add some more if you were skimpy with them. The Pico has six of them. * Turn on `DBG_PRINTF`. (See #messages-from-the-sd-card-driver.) For example, in `CMakeLists.txt`, ```CMake add_compile_definitions(USE_PRINTF USE_DBG_PRINTF) ``` You might see a clue in the messages. * Power cycle the SD card. Once an SD card is in SPI mode, the only way to get it back to SD mode is to power cycle it. At power up, an SD card's CS/DAT3 line has a 50 kΩ pull up enabled in the card, but it will be disabled if the card is initialized, and it won't be enabled again until the card is power cycled. * Try lowering the SPI or SDIO baud rate (e.g., in `hw_config.c`). This will also make it easier to use things like logic analyzers. * For SPI, this is in the [spi_t](#spi-controller-configuration) instance. * For SDIO, this is in the [sd_sdio_if_t](#an-instance-of-sd_sdio_if_t-describes-the-configuration-of-one-sdio-to-sd-card-interface) instance. * Make sure the SD card(s) are getting enough power. Try an external supply. Try adding a decoupling capacitor between Vcc and GND. * Hint: check voltage while formatting card. It must be 2.7 to 3.6 volts. * Hint: If you are powering a Pico with a PicoProbe, try adding a USB cable to a wall charger to the Pico under test. * Try another brand of SD card. Some handle the SPI interface better than others. (Most consumer devices like cameras or PCs use the SDIO interface.) I have had good luck with SanDisk, PNY, and Silicon Power. * Tracing: Most of the source files have a couple of lines near the top of the file like: ```C #define TRACE_PRINTF(fmt, args...) // Disable tracing //#define TRACE_PRINTF printf // Trace with printf ``` You can swap the commenting to enable tracing of what's happening in that file. * Logic analyzer: for less than ten bucks, something like this [Comidox 1Set USB Logic Analyzer Device Set USB Cable 24MHz 8CH 24MHz 8 Channel UART IIC SPI Debug for Arduino ARM FPGA M100 Hot](https://smile.amazon.com/gp/product/B07KW445DJ/) and [PulseView - sigrok](https://sigrok.org/) make a nice combination for looking at SPI, as long as you don't run the baud rate too high. * Get yourself a protoboard and solder everything. So much more reliable than solderless breadboard! * Better yet, go to somwhere like [JLCPCB](https://jlcpcb.com/) and get a printed circuit board! [^1]: as of [Pull Request #12 Dynamic configuration](https://github.com/carlk3/no-OS-FatFS-SD-SPI-RPi-Pico/pull/12) (in response to [Issue #11 Configurable GPIO pins](https://github.com/carlk3/no-OS-FatFS-SD-SPI-RPi-Pico/issues/11)), Sep 11, 2021 [^2]: as of [Pull Request #5 Bug in ff_getcwd when FF_VOLUMES < 2](https://github.com/carlk3/no-OS-FatFS-SD-SPI-RPi-Pico/pull/5), Aug 13, 2021 [^3]: In my experience, the Card Detect switch on these doesn't work worth a damn. This might not be such a big deal, because according to [Physical Layer Simplified Specification](https://www.sdcard.org/downloads/pls/) the Chip Select (CS) line can be used for Card Detection: "At power up this line has a 50KOhm pull up enabled in the card... For Card detection, the host detects that the line is pulled high." However, the Adafruit card has it's own 47 kΩ pull up on CS - Card Detect / Data Line [Bit 3], rendering it useless for Card Detection. [^4]: [Physical Layer Simplified Specification](https://www.sdcard.org/downloads/pls/) [^5]: Rationale: Instances of `sd_spi_if_t` or `sd_sdio_if_t` are separate objects instead of being embedded in `sd_card_t` objects because `sd_sdio_if_t` carries a lot of state information with it (including things like data buffers). The union of the two types has the size of the largest type, which would result in a lot of wasted space in instances of `sd_spi_if_t`. I had another solution using `malloc`, but some people are frightened of `malloc` in embedded systems.