Skip to main content

SD Card and the Shared SPI Bus

The SD card shares its SPI bus with the display, which is the kind of decision that either works invisibly or eats a week of bring-up. This chapter explains why it works, then the scanner that turns a card into the library.

Two devices, one bus

SPI is built for sharing: MOSI, MISO, and the clock are common; each device gets its own chip-select (CS), and only the selected device listens or drives. Safi puts the ST7735S and the SD card on SPI2 (display CS on GPIO2, card CS on GPIO5) because the alternative, a second SPI host, would spend three more GPIOs the pin budget did not have, for bandwidth the workload does not need.

Why the workload makes it safe, with numbers: audio playback reads ~16 KB/s sustained from the card; a full display refresh is 40 KB but typical LVGL updates are dirty-region slices, and the bus at 10 MHz moves ~1.2 MB/s. Utilization is a few percent from each device; contention is statistically rare and, when it happens, resolved by the driver.

The part that must be right:

  • Arbitration belongs to the driver, not the application. ESP-IDF's spi_master queues transactions per device and interleaves them safely; both the esp_lcd panel IO and the sdspi host attach as devices on the same host. Earlier code guarded the bus with a hand-rolled mutex; it was deleted in favor of the driver's queue, because a second lock above a locking driver adds deadlock surface and zero safety.
  • The card must stay deselected until spoken to. The 10 kΩ pull-up on the card's CS (documented in wiring) covers the window before GPIO init, when a floating CS could let the card interpret display traffic as commands.
  • max_transfer_sz on the bus is sized for the display's largest flush, since the LCD is the biggest single transaction on the shared host.

The one real coupling that remains: a long display DMA can delay a card read by a millisecond or two. The audio pipeline's buffering absorbs three orders of magnitude more than that.

Mount and lifecycle

The card mounts at boot at /sdcard via sdspi + FAT (FAT because that is what cards ship with and what users' computers write; the power-cut-safety argument that picked LittleFS for the internal storage partition does not apply to a read-mostly removable card). Long filenames are on (CONFIG_FATFS_LFN_HEAP, CONFIG_FATFS_MAX_LFN=255); without them FATFS would expose only 8.3 aliases (ALI_AL~1/018_AL~1.MP3), and a full path such as Ali_Al-Huzeifi/018_Al-Kahf.mp3 would fail to open. Mount failure lands in the boot report and the device continues card-less. Insertion and removal publish EVENT_SD_MOUNTED / EVENT_SD_UNMOUNTED, which trigger a rescan and UI updates; removal mid-playback stops file playback cleanly at the next failed read.

The library scanner

sd_manager_scan_files() walks the card once per mount and builds the index everything else queries:

  • Top-level directories become reciters; files matching the three-digit-prefix rule become surah entries (the user-facing rules).
  • The index is a fixed array of reciter records holding a name and a 114-slot presence bitmap plus filenames. A record is ~19 KB, and the array lives in PSRAM via EXT_RAM_BSS_ATTR: this single structure once sat in internal RAM and was the largest static consumer in the firmware by far; moving it was the cheapest 380 KB this project ever recovered (placement rules).
  • Queries (sd_manager_get_reciter, get_surah_path, surah_exists) copy out records rather than lending pointers, so callers hold no references into a structure the next rescan may rewrite. Copy-out of 19 KB sounds expensive and measures irrelevant at query frequency; dangling-pointer bugs measure worse.

Surah names deliberately do not come from the card: filenames provide numbers only, and the canonical Arabic names ship in flash (surah_names.h), which is why the display renders proper shaped names regardless of how files were labeled.

What deliberately does not exist

No write path (the device never modifies the card; users manage it on a computer, and read-only means no corruption liability), no metadata database on the card, no recursive scan (one level of folders is the documented contract, and enforcing it keeps the scanner and the UI simple). Constraints a user can read in one paragraph beat features that need a repair tool.