Skip to content

Automount SD cards #10129

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Apr 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions docs/workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,30 @@ The workflow APIs are documented here.
These USB interfaces are enabled by default on boards with USB support. They are usable once the
device has been plugged into a host.

### CIRCUITPY drive
### Mass Storage
CircuitPython exposes a standard mass storage (MSC) interface to enable file manipulation over a
standard interface. This interface works underneath the file system at the block level so using it
excludes other types of workflows from manipulating the file system at the same time.
standard interface. (This is how USB drives work.) This interface works underneath the file system at
the block level so using it excludes other types of workflows from manipulating the file system at
the same time.

CircuitPython 10.x adds multiple Logical Units (LUNs) to the mass storage interface. This allows for
multiple drives to be accessed and ejected independently.

#### CIRCUITPY drive
The CIRCUITPY drive is the main drive that CircuitPython uses. It is writable by the host by default
and read-only to CircuitPython. `storage.remount()` can be used to remount the drive to
CircuitPython as read-write.

#### CPSAVES drive
The board may also expose a CPSAVES drive. (This is based on the ``CIRCUITPY_SAVES_PARTITION_SIZE``
setting in ``mpconfigboard.h``.) It is a portion of the main flash that is writable by CircuitPython
by default. It is read-only to the host. `storage.remount()` can be used to remount the drive to the
host as read-write.

#### SD card drive
A few boards have SD card automounting. (This is based on the ``DEFAULT_SD`` settings in
``mpconfigboard.h``.) The card is writable from CircuitPython by default and read-only to the host.
`storage.remount()` can be used to remount the drive to the host as read-write.

### CDC serial
CircuitPython exposes one CDC USB interface for CircuitPython serial. This is a standard serial
Expand Down
23 changes: 21 additions & 2 deletions extmod/vfs.c
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@
#include "extmod/vfs_posix.h"
#endif


#if CIRCUITPY_SDCARDIO
#include "shared-module/sdcardio/__init__.h"
#endif

// For mp_vfs_proxy_call, the maximum number of additional args that can be passed.
// A fixed maximum size is used to avoid the need for a costly variable array.
#define PROXY_MAX_ARGS (2)
Expand All @@ -67,6 +72,10 @@ mp_vfs_mount_t *mp_vfs_lookup_path(const char *path, const char **path_out) {
// path is "" or "/" so return virtual root
return MP_VFS_ROOT;
}
// CIRCUITPY-CHANGE: Try and automount the SD card.
#if CIRCUITPY_SDCARDIO
automount_sd_card();
#endif
for (mp_vfs_mount_t *vfs = MP_STATE_VM(vfs_mount_table); vfs != NULL; vfs = vfs->next) {
size_t len = vfs->len - 1;
if (len == 0) {
Expand Down Expand Up @@ -367,8 +376,18 @@ mp_obj_t mp_vfs_getcwd(void) {
}
MP_DEFINE_CONST_FUN_OBJ_0(mp_vfs_getcwd_obj, mp_vfs_getcwd);

// CIRCUITPY-CHANGE: accessible from shared-module/os/__init__.c
mp_obj_t mp_vfs_ilistdir_it_iternext(mp_obj_t self_in) {
typedef struct _mp_vfs_ilistdir_it_t {
mp_obj_base_t base;
mp_fun_1_t iternext;
union {
mp_vfs_mount_t *vfs;
mp_obj_t iter;
} cur;
bool is_str;
bool is_iter;
} mp_vfs_ilistdir_it_t;

static mp_obj_t mp_vfs_ilistdir_it_iternext(mp_obj_t self_in) {
mp_vfs_ilistdir_it_t *self = MP_OBJ_TO_PTR(self_in);
if (self->is_iter) {
// continue delegating to root dir
Expand Down
13 changes: 0 additions & 13 deletions extmod/vfs.h
Original file line number Diff line number Diff line change
Expand Up @@ -94,19 +94,6 @@ typedef struct _mp_vfs_mount_t {
struct _mp_vfs_mount_t *next;
} mp_vfs_mount_t;

// CIRCUITPY-CHANGE: allow outside use of ilistdir_it_iternext
typedef struct _mp_vfs_ilistdir_it_t {
mp_obj_base_t base;
mp_fun_1_t iternext;
union {
mp_vfs_mount_t *vfs;
mp_obj_t iter;
} cur;
bool is_str;
bool is_iter;
} mp_vfs_ilistdir_it_t;

mp_obj_t mp_vfs_ilistdir_it_iternext(mp_obj_t self_in);
void mp_vfs_blockdev_init(mp_vfs_blockdev_t *self, mp_obj_t bdev);
int mp_vfs_blockdev_read(mp_vfs_blockdev_t *self, size_t block_num, size_t num_blocks, uint8_t *buf);
int mp_vfs_blockdev_read_ext(mp_vfs_blockdev_t *self, size_t block_num, size_t block_off, size_t len, uint8_t *buf);
Expand Down
6 changes: 3 additions & 3 deletions locale/circuitpython.pot
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ msgstr ""
msgid "%q and %q contain duplicate pins"
msgstr ""

#: ports/atmel-samd/common-hal/audioio/AudioOut.c
#: shared-bindings/audioio/AudioOut.c
msgid "%q and %q must be different"
msgstr ""

Expand Down Expand Up @@ -154,7 +154,7 @@ msgstr ""
msgid "%q length must be >= %d"
msgstr ""

#: py/modsys.c py/objmodule.c py/runtime.c
#: py/modsys.c py/runtime.c
msgid "%q moved from %q to %q"
msgstr ""

Expand Down Expand Up @@ -789,7 +789,7 @@ msgid "Cannot record to a file"
msgstr ""

#: shared-module/storage/__init__.c
msgid "Cannot remount '/' when visible via USB."
msgid "Cannot remount path when visible via USB."
msgstr ""

#: shared-bindings/digitalio/DigitalInOut.c
Expand Down
7 changes: 7 additions & 0 deletions ports/raspberrypi/boards/adafruit_fruit_jam/mpconfigboard.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@
#define DEFAULT_DVI_BUS_BLUE_DN (&pin_GPIO18)
#define DEFAULT_DVI_BUS_BLUE_DP (&pin_GPIO19)

#define DEFAULT_SD_SCK (&pin_GPIO34)
#define DEFAULT_SD_MOSI (&pin_GPIO35)
#define DEFAULT_SD_MISO (&pin_GPIO36)
#define DEFAULT_SD_CS (&pin_GPIO39)
#define DEFAULT_SD_CARD_DETECT (&pin_GPIO33)
#define DEFAULT_SD_CARD_INSERTED true

#define CIRCUITPY_PSRAM_CHIP_SELECT (&pin_GPIO47)

// #define CIRCUITPY_CONSOLE_UART_TX (&pin_GPIO44)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,10 @@
#define DEFAULT_DVI_BUS_GREEN_DP (&pin_GPIO16)
#define DEFAULT_DVI_BUS_BLUE_DN (&pin_GPIO13)
#define DEFAULT_DVI_BUS_BLUE_DP (&pin_GPIO12)

#define DEFAULT_SD_SCK (&pin_GPIO34)
#define DEFAULT_SD_MOSI (&pin_GPIO35)
#define DEFAULT_SD_MISO (&pin_GPIO36)
#define DEFAULT_SD_CS (&pin_GPIO39)
#define DEFAULT_SD_CARD_DETECT (&pin_GPIO40)
#define DEFAULT_SD_CARD_INSERTED false
8 changes: 8 additions & 0 deletions shared-bindings/storage/__init__.c
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,14 @@ MP_DEFINE_CONST_FUN_OBJ_1(storage_umount_obj, storage_umount);
//| ) -> None:
//| """Remounts the given path with new parameters.
//|
//| This can always be done from boot.py. After boot, it can only be done when the host computer
//| doesn't have write access and CircuitPython isn't currently writing to the filesystem. An
//| exception will be raised if this is the case. Some host OSes allow you to eject a drive which
//| will allow for remounting.
//|
//| Remounting after USB is active may take a little time because it "ejects" the drive for one
//| query from the host. These queries happen every second or so.
//|
//| :param str mount_path: The path to remount.
//| :param bool readonly: True when the filesystem should be readonly to CircuitPython.
//| :param bool disable_concurrent_write_protection: When True, the check that makes sure the
Expand Down
22 changes: 10 additions & 12 deletions shared-module/os/__init__.c
Original file line number Diff line number Diff line change
Expand Up @@ -90,21 +90,19 @@ mp_obj_t common_hal_os_getcwd(void) {
mp_obj_t common_hal_os_listdir(const char *path) {
mp_obj_t path_out;
mp_vfs_mount_t *vfs = lookup_dir_path(path, &path_out);

mp_vfs_ilistdir_it_t iter;
mp_obj_t iter_obj = MP_OBJ_FROM_PTR(&iter);

if (vfs == MP_VFS_ROOT) {
// list the root directory
iter.base.type = &mp_type_polymorph_iter;
iter.iternext = mp_vfs_ilistdir_it_iternext;
iter.cur.vfs = MP_STATE_VM(vfs_mount_table);
iter.is_str = true;
iter.is_iter = false;
} else {
iter_obj = mp_vfs_proxy_call(vfs, MP_QSTR_ilistdir, 1, &path_out);
vfs = MP_STATE_VM(vfs_mount_table);
while (vfs != NULL) {
if (vfs->len == 1) {
break;
}
vfs = vfs->next;
}
path_out = MP_OBJ_NEW_QSTR(MP_QSTR__slash_);
}

mp_obj_t iter_obj = mp_vfs_proxy_call(vfs, MP_QSTR_ilistdir, 1, &path_out);

mp_obj_t dir_list = mp_obj_new_list(0, NULL);
mp_obj_t next;
while ((next = mp_iternext(iter_obj)) != MP_OBJ_STOP_ITERATION) {
Expand Down
13 changes: 11 additions & 2 deletions shared-module/sdcardio/SDCard.c
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ static mp_rom_error_text_t init_card(sdcardio_sdcard_obj_t *self) {
return NULL;
}

void common_hal_sdcardio_sdcard_construct(sdcardio_sdcard_obj_t *self, busio_spi_obj_t *bus, const mcu_pin_obj_t *cs, int baudrate) {
mp_rom_error_text_t sdcardio_sdcard_construct(sdcardio_sdcard_obj_t *self, busio_spi_obj_t *bus, const mcu_pin_obj_t *cs, int baudrate) {
self->bus = bus;
common_hal_digitalio_digitalinout_construct(&self->cs, cs);
common_hal_digitalio_digitalinout_switch_to_output(&self->cs, true, DRIVE_MODE_PUSH_PULL);
Expand All @@ -309,10 +309,19 @@ void common_hal_sdcardio_sdcard_construct(sdcardio_sdcard_obj_t *self, busio_spi

if (result != NULL) {
common_hal_digitalio_digitalinout_deinit(&self->cs);
mp_raise_OSError_msg(result);
return result;
}

self->baudrate = baudrate;
return NULL;
}


void common_hal_sdcardio_sdcard_construct(sdcardio_sdcard_obj_t *self, busio_spi_obj_t *bus, const mcu_pin_obj_t *cs, int baudrate) {
mp_rom_error_text_t result = sdcardio_sdcard_construct(self, bus, cs, baudrate);
if (result != NULL) {
mp_raise_OSError_msg(result);
}
}

void common_hal_sdcardio_sdcard_deinit(sdcardio_sdcard_obj_t *self) {
Expand Down
2 changes: 2 additions & 0 deletions shared-module/sdcardio/SDCard.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,5 @@ typedef struct {
uint32_t next_block;
bool in_cmd25;
} sdcardio_sdcard_obj_t;

mp_rom_error_text_t sdcardio_sdcard_construct(sdcardio_sdcard_obj_t *self, busio_spi_obj_t *bus, const mcu_pin_obj_t *cs, int baudrate);
122 changes: 122 additions & 0 deletions shared-module/sdcardio/__init__.c
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,125 @@
// SPDX-FileCopyrightText: Copyright (c) 2024 Adafruit Industries LLC
//
// SPDX-License-Identifier: MIT

#include "shared-module/sdcardio/__init__.h"

#include "extmod/vfs_fat.h"

#include "shared-bindings/busio/SPI.h"
#include "shared-bindings/digitalio/DigitalInOut.h"
#include "shared-bindings/sdcardio/SDCard.h"

#include "supervisor/filesystem.h"

#ifdef DEFAULT_SD_CARD_DETECT
static digitalio_digitalinout_obj_t sd_card_detect_pin;
static sdcardio_sdcard_obj_t sdcard;

static mp_vfs_mount_t _sdcard_vfs;
fs_user_mount_t _sdcard_usermount;

static bool _init_error = false;
static bool _mounted = false;

#ifdef DEFAULT_SD_MOSI
static busio_spi_obj_t busio_spi_obj;
#else
#include "shared-bindings/board/__init__.h"
#endif
#endif

void sdcardio_init(void) {
#ifdef DEFAULT_SD_CARD_DETECT
sd_card_detect_pin.base.type = &digitalio_digitalinout_type;
common_hal_digitalio_digitalinout_construct(&sd_card_detect_pin, DEFAULT_SD_CARD_DETECT);
common_hal_digitalio_digitalinout_switch_to_input(&sd_card_detect_pin, PULL_UP);
common_hal_digitalio_digitalinout_never_reset(&sd_card_detect_pin);
#endif
}

void automount_sd_card(void) {
#ifdef DEFAULT_SD_CARD_DETECT
if (common_hal_digitalio_digitalinout_get_value(&sd_card_detect_pin) != DEFAULT_SD_CARD_INSERTED) {
// No card.
_init_error = false;
if (_mounted) {
// Unmount the card.
mp_vfs_mount_t *cur = MP_STATE_VM(vfs_mount_table);
if (cur == &_sdcard_vfs) {
MP_STATE_VM(vfs_mount_table) = cur->next;
} else {
while (cur->next != &_sdcard_vfs && cur != NULL) {
cur = cur->next;
}
if (cur != NULL) {
cur->next = _sdcard_vfs.next;
}
}
_sdcard_vfs.next = NULL;

#ifdef DEFAULT_SD_MOSI
common_hal_busio_spi_deinit(&busio_spi_obj);
#endif
_mounted = false;
}
return;
} else if (_init_error || _mounted) {
// We've already tried and failed to init the card. Don't try again.
return;
}

busio_spi_obj_t *spi_obj;
#ifndef DEFAULT_SD_MOSI
spi_obj = MP_OBJ_TO_PTR(common_hal_board_create_spi(0));
#else
spi_obj = &busio_spi_obj;
spi_obj->base.type = &busio_spi_type;
common_hal_busio_spi_construct(spi_obj, DEFAULT_SD_SCK, DEFAULT_SD_MOSI, DEFAULT_SD_MISO, false);
common_hal_busio_spi_never_reset(spi_obj);
#endif
sdcard.base.type = &sdcardio_SDCard_type;
mp_rom_error_text_t error = sdcardio_sdcard_construct(&sdcard, spi_obj, DEFAULT_SD_CS, 25000000);
if (error != NULL) {
// Failed to communicate with the card.
_mounted = false;
_init_error = true;
#ifdef DEFAULT_SD_MOSI
common_hal_busio_spi_deinit(spi_obj);
#endif
return;
}
common_hal_digitalio_digitalinout_never_reset(&sdcard.cs);

fs_user_mount_t *vfs = &_sdcard_usermount;
vfs->base.type = &mp_fat_vfs_type;
vfs->fatfs.drv = vfs;

// Initialise underlying block device
vfs->blockdev.block_size = FF_MIN_SS; // default, will be populated by call to MP_BLOCKDEV_IOCTL_BLOCK_SIZE
mp_vfs_blockdev_init(&vfs->blockdev, &sdcard);

// mount the block device so the VFS methods can be used
FRESULT res = f_mount(&vfs->fatfs);
if (res != FR_OK) {
_mounted = false;
_init_error = true;
common_hal_sdcardio_sdcard_deinit(&sdcard);
#ifdef DEFAULT_SD_MOSI
common_hal_busio_spi_deinit(spi_obj);
#endif
return;
}

filesystem_set_concurrent_write_protection(vfs, true);
filesystem_set_writable_by_usb(vfs, false);

mp_vfs_mount_t *sdcard_vfs = &_sdcard_vfs;
sdcard_vfs->str = "/sd";
sdcard_vfs->len = 3;
sdcard_vfs->obj = MP_OBJ_FROM_PTR(&_sdcard_usermount);
sdcard_vfs->next = MP_STATE_VM(vfs_mount_table);
MP_STATE_VM(vfs_mount_table) = sdcard_vfs;
_mounted = true;
#endif
}
3 changes: 3 additions & 0 deletions shared-module/sdcardio/__init__.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@
// SPDX-License-Identifier: MIT

#pragma once

void sdcardio_init(void);
void automount_sd_card(void);
17 changes: 12 additions & 5 deletions shared-module/storage/__init__.c
Original file line number Diff line number Diff line change
Expand Up @@ -174,18 +174,25 @@ mp_obj_t common_hal_storage_getmount(const char *mount_path) {
}

void common_hal_storage_remount(const char *mount_path, bool readonly, bool disable_concurrent_write_protection) {
if (strcmp(mount_path, "/") != 0) {
const char *path_under_mount;
fs_user_mount_t *fs_usermount = filesystem_for_path(mount_path, &path_under_mount);
if (path_under_mount[0] != 0 && strcmp(mount_path, "/") != 0) {
mp_raise_OSError(MP_EINVAL);
}

#if CIRCUITPY_USB_DEVICE && CIRCUITPY_USB_MSC
if (!usb_msc_ejected() && storage_usb_is_enabled) {
mp_raise_RuntimeError(MP_ERROR_TEXT("Cannot remount '/' when visible via USB."));
if (!blockdev_lock(fs_usermount)) {
mp_raise_RuntimeError(MP_ERROR_TEXT("Cannot remount path when visible via USB."));
}
#endif

filesystem_set_internal_writable_by_usb(readonly);
filesystem_set_internal_concurrent_write_protection(!disable_concurrent_write_protection);
filesystem_set_writable_by_usb(fs_usermount, readonly);
filesystem_set_concurrent_write_protection(fs_usermount, !disable_concurrent_write_protection);
blockdev_unlock(fs_usermount);

#if CIRCUITPY_USB_DEVICE && CIRCUITPY_USB_MSC
usb_msc_remount(fs_usermount);
#endif
}

void common_hal_storage_erase_filesystem(bool extended) {
Expand Down
Loading