Xiao BLE Sense (mbed)/microSD card access via Xiao Expansion board

Hi All,

Simply put I am trying to use the Xiao BLE Sense board connected to the Expansion board (with SD card slot) to write audio recorded data from the Xiao embedded PDM microphone to the microSD card.

Development setup:

HW: Apple M2 Ultra (silicon), Xiao BLE Sense (mbed) with 14 header pins soldered by me (appear functional), Xiao Expansion Board, SanDisk Ultra PLUS 64 GB microSD card, USB-C to USB-C cord connecting Xiao Sense to desktop system (cord good condition)

OS: Darwin arm64 23.6.0 (macOS Sonoma 14.6.1)

SW: VS Code 1.98.2, nRF Connect SDK v2.7.0 (not the most recent I know), toolchain 1.5.3 

Description of issue: 

I am working to access a microSD card formatted with fatfs (FAT 32) filesystem (64 GB capacity) but have been unable to initialize the card (see error log and screenshot below).  

I have used the above configuration (Xiao Sense + Expansion board + inserted SD card) and successfully run the "CardInfo.ino" code example in the Arduino environment.  From this I assume that the microSD card is usable, formatted correctly (using desktop setup described below), and power/voltage is correct.

There are other Arduino examples accessing/reading/writing an SD card but I have not tried these.  

What I want to do is to create a codebase in which I can access (including initialize, open/close, read/write, and de-initialize) an SD card but everything done in the NRF Connect/Zephyr OS environment if at all possible.  

I used the nRF Connect "blinky" sample to create the skeleton build configuration for the Xiao Sense and of course this compiled, assembled, linked, and executed as expected.  Per multiple tutorials I simply copy the .uf2 file to the Xiao Sense to flash it to the nrf52840.  I kept this Blink/LED code in the program to have evidence that the program flashed correctly.

To build the SD functionality I used the fat_fs.c code from the most recent nRF Connect sample and incorporated it into the "blinky" sample (see attached file).   I would like to try to access the SD card through SPI (using the disk driver API with the SDMMC subsystem working transparently underneath).   

However, when I flash this revised code I receive an error at the if (disk_access_init(disk_pdrv) != 0) {...} system call:

I have tried multiple configurations to solve the problem.

1. rechecked connections and power including that the microSD card is inserted into the SD card slot at all times/power stable to device/card throughout process as this is an fat system

2. tried suggestions from several nRF Connect Forum posts including this one without luck: https://devzone.nordicsemi.com/f/nordic-q-a/109575/xiao-ble-sense-round-display---zephyr-microsd

3. contacted the OP of the post from 2. as they report having resolved an identical (?) issue but I have not received a response

4. in prj.conf I have tried all 4 SPI interfaces (SPI0 through SPI3) though I am under the impression that SPI0 and SPI3 are preferred for SPI (note I am currently using SPI2 but I have received the same error mentioned above in initializing the SD card using all 4 interfaces.

5. I have added the "zephyr,sdmmc-disk" device tree node to each of the SPI interfaces to no avail

6. Adding CONFIG_NFCT_PINS_AS_GPIOS=y to prj.conf but this did not affect the error (got same error)

7. Seeed Studio (maker of Xiao board) Forum has not been helpful (extensive search through posts)

8. tried various filesystems (by enabled in prf.conf) including ext2 and littlefs but there does not seem to be a way to do this at this point

9. considered using NVMe but trying to use PCIe would seem to complicate this situation more

10. considered emulated block device of flash partition support (I assume this means the 2 MB onboard nRF25840 SoC flash) but I need to record/write to SD at least 3 MB of data (approximately 3 minutes of audio data) so this does not seem adequate (though it would be very cool to know how to do this).  Of note, I did retain the msc_disk0 device note information (from here: https://docs.nordicsemi.com/bundle/ncs-2.4.3/page/zephyr/services/storage/disk/access.html#disk-access-api) in my overlay file but I am assuming this should not interfere with the spi2 DT configuration.

My related questions are: 

* how does the SD host controller figure into this process and am I missing something in the configuration to enable this?

* how to use direct block level access (meaning using DMA?) to the SD card/is this possible as an alternative to using SPI?

* do I need to make changes to yaml and/or .json files in order to enable SD functionality for this board and/or create custom board support for the Xiao?

Any help would be greatly appreciated.  If it is known that the Xiao BLE Sense (mbed) board simply cannot be used in the nRF Connect/Zephyr environment to interact with an SD card this would very helpful to know as well (but it seems as if there must be some way to do this).  I have the generated VS Code support information .jsonc file that I can send to Nordic staff if that is helpful. 

Kind Regards,

Rick

/* audioY */

#include <stdio.h>
#include <zephyr/kernel.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/device.h>
#include <zephyr/storage/disk_access.h>
#include <zephyr/logging/log.h>
#include <zephyr/fs/fs.h>

/* Note the fatfs library is able to mount only strings inside _VOLUME_STRS in ffconf.h */
#if defined(CONFIG_FAT_FILESYSTEM_ELM)
#include <ff.h>

#define DISK_DRIVE_NAME "/SD:"
#define DISK_MOUNT_PT "/"DISK_DRIVE_NAME":"

static FATFS fat_fs;
// static struct fs_file_t file;

/* mounting info */
static struct fs_mount_t mp = {
	.type = FS_FATFS,
	.mountp_len = DISK_MOUNT_PT,	// orgiginally .mountp
	.fs_data = &fat_fs,
};

#elif defined(CONFIG_FILE_SYSTEM_EXT2)

#include <zephyr/fs/ext2.h>
#define DISK_DRIVE_NAME "SDMMC"
#define DISK_MOUNT_PT "/ext"

static struct fs_mount_t mp = {
	.type = FS_EXT2,
	.flags = FS_MOUNT_FLAG_NO_FORMAT,
	.storage_dev = (void *)DISK_DRIVE_NAME,
	.mnt_point = "/ext",
};

/* little FS is what is enabled here, may need to change to ext2, fat, nffs, or other */
#elif defined(CONFIG_FILE_SYSTEM_NFFS)
#include <nffs/nffs.h>
#define DISK_DRIVE_NAME "/nffs"	// change DISK_DRIVE_NAME to "/nffs:" to match ELM above ?
#define DISK_MOUNT_PT "/"DISK_DRIVE_NAME":"

#elif defined(CONFIG_FILE_SYSTEM_LITTLEFS)
#include <fs/littlefs.h>
#define DISK_DRIVE_NAME "/lfs"
#define DISK_MOUNT_PT "/"DISK_DRIVE_NAME":"

#elif defined(CONFIG_FILE_SYSTEM_NATIVE_POSIX)
#define DISK_DRIVE_NAME "/disk"	// originally DISK_MOUNT_PT "/disk"
#define DISK_MOUNT_PT "/"DISK_DRIVE_NAME":"

#elif defined(CONFIG_FILE_SYSTEM_MCUBFS)
#include <fs/mcubfs.h>
#define DISK_DRIVE_NAME "/mcubfs"
#define DISK_MOUNT_PT "/"DISK_DRIVE_NAME":"

#elif defined(CONFIG_FILE_SYSTEM_FATFS)
#include <fs/fatfs.h>
#define DISK_DRIVE_NAME "/fatfs"
#define DISK_MOUNT_PT "/"DISK_DRIVE_NAME":"

#elif defined(CONFIG_FILE_SYSTEM_FATFS_LFN)
#include <fs/fatfs.h>
#define DISK_DRIVE_NAME "/fatfs"
#define DISK_MOUNT_PT "/"DISK_DRIVE_NAME":"

#elif defined(CONFIG_FILE_SYSTEM_FATFS_PETIT)
#include <fs/fatfs.h>
#define DISK_DRIVE_NAME "/fatfs"
#define DISK_MOUNT_PT "/"DISK_DRIVE_NAME":"

#elif defined(CONFIG_FILE_SYSTEM_FATFS_FS)
#include <fs/fatfs.h>
#define DISK_DRIVE_NAME "/fatfs"
#define DISK_MOUNT_PT "/"DISK_DRIVE_NAME":"

#elif defined(CONFIG_FILE_SYSTEM_FATFS_FS_LFN)
#include <fs/fatfs.h>
#define DISK_DRIVE_NAME "/fatfs"
#define DISK_MOUNT_PT "/"DISK_DRIVE_NAME":"

#elif defined(CONFIG_FILE_SYSTEM_FATFS_FS_PETIT)
#include <fs/fatfs.h>
#define DISK_DRIVE_NAME "/fatfs"
#define DISK_MOUNT_PT "/"DISK_DRIVE_NAME":"

#elif defined(CONFIG_FILE_SYSTEM_FATFS_FS_NFFS)
#include <fs/fatfs.h>
#define DISK_DRIVE_NAME "/fatfs"
#define DISK_MOUNT_PT "/"DISK_DRIVE_NAME":"

#else	// defaults here for now until prj.conf is updated to FS I want to use
#define DISK_DRIVE_NAME "/default"
#define DISK_MOUNT_PT "/"DISK_DRIVE_NAME":"

#endif

/* 1000 msec = 1 sec */
#define SLEEP_TIME_MS   500

/* The devicetree node identifier for the "led0" alias. */
#define LED0_NODE DT_ALIAS(led1)

/* A build error on this line means your board is unsupported */
static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(LED0_NODE, gpios);

LOG_MODULE_REGISTER(main);

#define MAX_PATH 128
#define SOME_FILE_NAME "some.dat"
#define SOME_DIR_NAME "some"
#define SOME_REQUIRED_LEN MAX(sizeof(SOME_FILE_NAME), sizeof(SOME_DIR_NAME))

static int lsdir(const char *path);
#ifdef CONFIG_FS_SAMPLE_CREATE_SOME_ENTRIES

static bool createEntries(const char *base_path)
{
	char path[MAX_PATH];
	struct fs_file_t file;
	int base = strlen(base_path);

	fs_file_t_init(&file);

	if (base >= (sizeof(path) - SOME_REQUIRED_LEN)) {
		LOG_ERR("Not enough concatenation buffer to create file paths");
		return false;
	}

	LOG_INF("Creating some dir entries in %s", base_path);
	strncpy(path, base_path, sizeof(path));

	path[base++] = '/';
	path[base] = 0;
	strcat(&path[base], SOME_FILE_NAME);

	if (fs_open(&file, path, FS_O_CREATE) != 0) {
		LOG_ERR("Failed to create file %s", path);
		return false;
	}
	fs_close(&file);

	path[base] = 0;
	strcat(&path[base], SOME_DIR_NAME);

	if (fs_mkdir(path) != 0) {
		LOG_ERR("Failed to create dir %s", path);
		/* If code gets here, it has at least successes to create the
		 * file so allow function to return true.
		 */
	}
	return true;
}

#endif

static const char *disk_mount_pt = DISK_MOUNT_PT;

int main(void)
{
	int ret;
	bool led_state = true;

	printk("Running...\n");

	/* raw disk I/O */
	do {
		static const char *disk_pdrv = DISK_DRIVE_NAME;
		uint64_t memory_size_mb;
		uint32_t block_count;
		uint32_t block_size;
		// uint32_t erase_block_size;  // future use? 
		// uint32_t write_block_size;

		if (disk_access_init(disk_pdrv) != 0) {
			LOG_ERR("SD card initialization failed!\n");
			break;
		}

		if (disk_access_ioctl(disk_pdrv, DISK_IOCTL_GET_SECTOR_COUNT, &block_count)) {
			LOG_ERR("Unable to get block count!\n");
			break;
		}

		LOG_INF("Block count %u", block_count);
		// printf("Block count inside printf: %u", block_count);

		if (disk_access_ioctl(disk_pdrv, DISK_IOCTL_GET_SECTOR_SIZE, &block_size)) {
			LOG_ERR("Unable to get sector size!\n");
			break;
		}

		LOG_INF("Sector size %u\n", block_size);
	}
	while (0);

	if (!gpio_is_ready_dt(&led)) {
		return 0;
	}

	ret = gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE);
	if (ret < 0) {
		return 0;
	}

	while (1) {
		ret = gpio_pin_toggle_dt(&led);
		if (ret < 0) {
			return 0;
		}

		led_state = !led_state;
		printf("LED state: %s\n", led_state ? "ON" : "OFF");
		k_msleep(SLEEP_TIME_MS);
	}
	return 0;
}

Lastly (in a very long post I know), I tried a somewhat different approach/version of main.c/other codebase (attached below) adapted from a different source but in this case I get a timeout error (errno 116) trying to communicate with the SD card. The main.c, prj.conf, and overlay files for this build are below as well.

/* audio1: code to interface with SD card
 * Rick Krebs 2025
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#include <stdio.h>
#include <zephyr/kernel.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/fs/fs.h>
#include <zephyr/drivers/sdhc.h>
#include <errno.h>
#include <zephyr/device.h>
#include <zephyr/devicetree.h>
#include <zephyr/sys/printk.h>
#include <zephyr/drivers/spi.h>
#include <zephyr/drivers/pinctrl.h>
#include <zephyr/devicetree/gpio.h>
#include <zephyr/storage/disk_access.h>
#include <ff.h>
#include <stdlib.h>

/* 1000 msec = 1 sec */
#define SLEEP_TIME_MS   500

/* The devicetree node identifier for the "led0" alias. */
#define LED0_NODE DT_ALIAS(led0)
#define DT_DRV_COMPAT xiao_spi 		// nordic_nrf_spim
// #define SPI_DEVICE_NODE DT_NODELABEL(spi0)

#define SD_CS_PIN 28	// chip select pin for SD card (P0.28 on Xiao Sense)
#define SD_CARD_MOUNT_POINT "/SD:"
#define LOG_FILENAME "adc_log.txt"

static FATFS fat_fs;
static struct fs_mount_t mp = {
	.type = FS_FATFS,
	.mnt_point = SD_CARD_MOUNT_POINT,
};
/*
struct spi_cs_control spi_cs = {
	.gpio_dev = DEVICE_DT_GET(DT_NODELABEL(gpioa)),
	.gpio_pin 28,
	.gpio_dt_flags = GPIO_ACTIVE_LOW,
	.delay = 10,
};
*/

static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(LED0_NODE, gpios);
static const struct device *spi_dev;
static struct spi_cs_control cs_ctrl;

#define SPI_OP SPI_OP_MODE_MASTER | SPI_MODE_CPOL | SPI_MODE_CPHA | SPI_WORD_SET(8) | SPI_LINES_SINGLE
struct spi_dt_spec spispec = SPI_DT_SPEC_GET(DT_NODELABEL(sdhc0), SPI_OP, 0);

/*
spi_dev = device_get_binding(DT_LABEL(DT_DRV_INST(0)));

struct spi_cs_control spi_cs = 
{
	#if DT_NODE_HAS_PROP(DT_DRV_INST(0), cs_gpios)
		.gpio = {
			.pin = DT_GPIO_PIN(DT_DRV_INST(0), cs_gpios),
			.dt_flags = GPIO_ACTIVE_LOW,
		},
	#else
		.gpio = {
			.pin = 0, // Default value or handle error
			.dt_flags = GPIO_ACTIVE_LOW,
		},
	#endif
	.delay = 0,
};

static void spi_init(void)
{
	spi_cs.gpio_dev = device_get_binding(DT_GPIO_LABEL(DT_DRV_INST(0), cs_gpios));

	if (spi_cs.gpio.port == NULL) {
		printk("Could not get gpio device\n");
	} else {
		printk("GPIO device: %s\n", DT_GPIO_LABEL(DT_DRV_INST(0), cs-gpios));
	}

	spi_dev = device_get_binding(DT_LABEL(DT_DRV_INST(0)));

	if (spi_dev == NULL) {
		printk("Could not get %s device\n", DT_LABEL(DT_DRV_INST(0)));
		return;
	} else {
		printk("SPI Device: %s\n", DT_LABEL(DT_DRV_INST(0)));
		printk("SPI CSN %d, MISO %d, MOSI %d, CLK %d\n",
	       DT_GPIO_PIN(DT_DRV_INST(0), cs_gpios),
	       DT_PROP(DT_DRV_INST(0), miso_pin),
	       DT_PROP(DT_DRV_INST(0), mosi_pin),
	       DT_PROP(DT_DRV_INST(0), sck_pin));		
	}
}

void spi_test_send(void)
{
	int err;
	static uint8_t tx_buffer[32];
	static uint8_t rx_buffer[32];

	const struct spi_buf tx_buf = {
		.buf = tx_buffer,
		.len = sizeof(tx_buffer)
	};
	const struct spi_buf_set tx = {
		.buffers = &tx_buf,
		.count = 1
	};

	struct spi_buf rx_buf = {
		.buf = rx_buffer,
		.len = sizeof(rx_buffer),
	};
	const struct spi_buf_set rx = {
		.buffers = &rx_buf,
		.count = 1
	};

	err = spi_transceive(spi_dev, &spi_cfg, &tx, &rx);
	if (err) {
		printk("SPI error: %d\n", err);
	} else {
		// Connect MISO to MOSI for loopback 
		printk("TX sent: %x\n", tx_buffer[0]);
		printk("RX recv: %x\n", rx_buffer[0]);
		tx_buffer[0]++;
	}
}
*/

static int init_sd_card(void)
{
	printk("Inside init_sd_card function...");
	static const char *disk_pdrv = "SD";
	uint64_t memory_size_mb;
	uint32_t block_count;
	uint32_t block_size;
	int err;
 
	err = disk_access_init(disk_pdrv);
	if (err != 0) {
		printk("disk_access_init failed!  Error code: %d (%s) \n", err, strerror(err));
		return -1;
	}

	if (disk_access_ioctl(disk_pdrv, DISK_IOCTL_GET_SECTOR_COUNT, &block_count)) {
		printk("Unable to get sector count!\n");
		return -1;
	}

	if (disk_access_ioctl(disk_pdrv, DISK_IOCTL_GET_SECTOR_SIZE, &block_size)) {
		printk("Unable to get sector size!\n");
		return -1;
	}

	memory_size_mb = (uint64_t) block_count * block_size / (1024 * 1024);
	printk("Memory Size (MB): %u\n", (uint32_t) memory_size_mb);

	mp.fs_data = &fat_fs;
	
	err = fs_mount(&mp);
	if (err) {
		printk("Error mounting fat_fs [%d]\n", err);
		return err;
	}
	
	printk("Disk mounted!\n");

	return 0;
}
/*
static int log_to_sd_card(uint16_t adc_raw, float voltage)
{
	struct fs_file_t file;
	char log_entry[100];
	ssize_t bytes_written;

	fs_file_t_init(&file);

	int err = fs_open(&file, SD_CARD_MOUNT_POINT "/" LOG_FILENAME, FS_O_WRITE | FS_O_CREATE | FS_O_APPEND);

	if (err) {
		printk("Error opening file [%d]\n", err);
		return err;
	}

	snprintf(log_entry, sizeof(log_entry), "ADC raw: %d, Voltage: %.2f V\n", adc_raw, (double) voltage);

	bytes_written = fs_write(&file, log_entry, strlen(log_entry));

	if (bytes_written < 0) {
		printk("Error writing to file [%zd]\n", bytes_written);
		fs_close(&file);

		return (int) bytes_written;
	}

	fs_close(&file);
	printk("Data logged to SD card\n");

	return 0;
}
*/

int main(void)
{
	int ret;
	bool led_state = true;

	printk("Running...\n");

	// initialize SPI device
	spi_dev = device_get_binding("sdhc_spi"); // (SPI_DEVICE_NODE);

	if (!device_is_ready(spi_dev)) {
		printk("SPI device is not ready!\n");
		return -1;
	}
	else {
		printk("SPI initialization successful!\n");
	}

	if (init_sd_card() != 0) {
		printk("Failed to initialize SD card!\n");
		exit (0);
	}

	// configure chip select
	cs_ctrl.gpio.port = DEVICE_DT_GET(DT_NODELABEL(gpio0));
	// cs_ctrl.gpio.pin = ADC_CS_PIN;
	cs_ctrl.gpio.dt_flags = GPIO_ACTIVE_LOW;
	cs_ctrl.delay = 0;

	// fs_open(zipfp, "Maxie.txt", FS_O_RDWR);

	/*
	if (sdhc_card_present(spi_dev))
		printf("hey card is in there!\n");
	else
		printf("error!\n");

	if (!gpio_is_ready_dt(&led)) {
		return 0;
	}
	*/

	ret = gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE);
	if (ret < 0) {
		return 0;
	}

	while (1) {
		ret = gpio_pin_toggle_dt(&led);
		if (ret < 0) {
			return 0;
		}

		/*
		printk("Attempting to read ADC channel 0\n\r");
		uint16_t adc_raw = read_adc_channel(0);	// read channel 0

		if (adc_raw == 0) {
			printk("ADC read failed or returned 0\n");
		}
		else {
			float voltage = (adc_raw * 3.3f) / 1023.0f;  // assuming 3.3 V reference
			printk("ADC raw value: %d, Voltage: %.2f V\n", adc_raw, voltage);

			if (log_to_sd_card(adc_raw, voltage) != 0) {
				printk("Failed to log data to SD card!\n");
			}
		}
		*/

		printk("\n\r");
		k_msleep(1000);

		led_state = !led_state;
		printf("LED state: %s\n", led_state ? "ON" : "OFF");
		k_msleep(SLEEP_TIME_MS);
	}
	return 0;
}

  • Hi,

    If it is known that the Xiao BLE Sense (mbed) board simply cannot be used in the nRF Connect/Zephyr environment

    I don't see why it should not work. So hopefully I can help you get to the bottom of this.

    First, have you tried the examples for the Expansion board at Seeed Studio XIAO Expansion Board zephyr docs?
    Not for SD Card, but just neat to know that it works with the shield configuration.

    * do I need to make changes to yaml and/or .json files in order to enable SD functionality for this board and/or create custom board support for the Xiao?

    If you build with the expansion board, the SD card should be enabled in DTS. However, the application would need SPI Kconfig configurations.

    We can get to your other questions later, but lets start with this.

    Regards,
    Sigurd Hellesvik

  • Thank you for your response Sigurd, I appreciate it.  What you say makes sense.  

    I did just try to flash the two examples mentioned for the Xiao Expansion Board (the LED button example at samples/basic/button) and LVGL basic example (at samples/subsys/display/lvgl) and they build without error.

    But when I copy the .uf2 file to the Xiao Sense I do not see any response or output on the board (no response to button press and no display in the lvgl sample case).  

    And the Xiao board is unmounted when the .uf2 file is flashed so I cannot use the terminal monitor to check any output.

    Here is the output from the button sample build.

    I dragged the small screen of the output from the .uf2 file copy into the VS Code output screenshot above to show the response that always comes up, that "Finder can't complete the operation because some data in the "zephyr.uf2" can't be read or written".  

    I have not been too concerned about this but I do wonder if this is an issue or could be an issue.  I have Segger JLink and associated libraries, etc. installed (and up to date) but this is a known issue I think that the VS Code cannot locate the Xiao (and perhaps other boards) under Connected Devices.  

    For this reason I cannot use $ west flash (after building the .hex file)

    This is probably not related to anything (and may be related to the fact that I am using an Apple desktop) but it would be great if this were possible at some point (being able to directly flash a .hex file to a connected board on a Mac).

    Thank you again for your help, and know that you are correct that there is a way for this to work!

    Rick

    post script: I have updated to v.2.9.0 for both SDK and toolkit since I posted my question yesterday.

Related