From 87bf6b4ebb7bb47446aafdee9b76ec490cea6e31 Mon Sep 17 00:00:00 2001 From: Aidan MacDonald Date: Tue, 30 Dec 2025 00:40:35 +0000 Subject: [PATCH] firmware: add sdmmc_host storage driver sdmmc_host is a portable driver for targets with SD/MMC storage. It handles all the logic needed to initialize and access SD/MMC devices which is common to all targets. Targets only need to implement functions to issue SD/MMC commands, manage clocks & power, and managing the insert state of the card (if it is hotswappable). This vastly reduces the work needed to get a new SD/MMC based target up & running. At present it's only written for and tested with SD cards, as I don't have access to an MMC-based target to test on. Change-Id: I6a0d7747113c11a3697ae20cbb551bef8bfd1292 --- firmware/SOURCES | 3 + firmware/drivers/sdmmc_host.c | 783 ++++++++++++++++++++++++++++++++++ firmware/export/config.h | 9 + firmware/export/sdmmc_host.h | 291 +++++++++++++ firmware/export/storage.h | 5 + firmware/storage.c | 21 + 6 files changed, 1112 insertions(+) create mode 100644 firmware/drivers/sdmmc_host.c create mode 100644 firmware/export/sdmmc_host.h diff --git a/firmware/SOURCES b/firmware/SOURCES index 46918a23b2..b1221df5ef 100644 --- a/firmware/SOURCES +++ b/firmware/SOURCES @@ -2155,6 +2155,9 @@ drivers/ft6x06.c #ifdef HAVE_CW2015 drivers/cw2015.c #endif +#ifdef HAVE_SDMMC_HOST +drivers/sdmmc_host.c +#endif #endif /* firmware/kernel section */ diff --git a/firmware/drivers/sdmmc_host.c b/firmware/drivers/sdmmc_host.c new file mode 100644 index 0000000000..e7110c64d1 --- /dev/null +++ b/firmware/drivers/sdmmc_host.c @@ -0,0 +1,783 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * $Id$ + * + * Copyright (C) 2025 Aidan MacDonald + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ +#include "sdmmc_host.h" +#include "system.h" +#include "storage.h" +#include "disk_cache.h" +#include "debug.h" +#include "panic.h" +#include "logf.h" +#include + +/* + * SD/MMC host interface + */ + +#ifdef CONFIG_STORAGE_MULTI +# define DECLARE_FIRST_DRIVE(decl) static decl +#else +# define DECLARE_FIRST_DRIVE(decl) static const decl = 0 +#endif + +#if CONFIG_STORAGE & STORAGE_SD +static struct sdmmc_host *sdmmc_sd_hosts[SDMMC_HOST_NUM_SD_CONTROLLERS]; +DECLARE_FIRST_DRIVE(int sdmmc_sd_first_drive); +static volatile long sdmmc_sd_last_activity; +#endif + +#if CONFIG_STORAGE & STORAGE_MMC +static struct sdmmc_host *sdmmc_mmc_hosts[SDMMC_HOST_NUM_MMC_CONTROLLERS]; +DECLARE_FIRST_DRIVE(int sdmmc_mmc_first_drive); +static volatile long sdmmc_mmc_last_activity; +#endif + +static int sdmmc_host_get_logical_drive(struct sdmmc_host *host) +{ + switch (host->config.type) + { +#if CONFIG_STORAGE & STORAGE_SD + case STORAGE_SD: + return sdmmc_sd_first_drive + host->drive; +#endif + +#if CONFIG_STORAGE & STORAGE_MMC + case STORAGE_MMC: + return sdmmc_mmc_first_drive + host->drive; +#endif + + default: + return 0; + } +} + +void sdmmc_host_init(struct sdmmc_host *host, + const struct sdmmc_host_config *config, + const struct sdmmc_controller_ops *ops, + void *controller) +{ + struct sdmmc_host **array = NULL; + size_t array_size = 0; + + switch (config->type) + { +#if CONFIG_STORAGE & STORAGE_SD + case STORAGE_SD: + array = sdmmc_sd_hosts; + array_size = ARRAYLEN(sdmmc_sd_hosts); + break; +#endif + +#if CONFIG_STORAGE & STORAGE_MMC + case STORAGE_MMC: + array = sdmmc_mmc_hosts; + array_size = ARRAYLEN(sdmmc_mmc_hosts); + break; +#endif + + default: + panicf("%s: bad type", __func__); + return; + } + + if (!ops->submit_command || !ops->set_bus_clock) + { + panicf("%s: missing ops", __func__); + return; + } + + for (size_t i = 0; i < array_size; ++i) + { + if (array[i] == NULL) + { + memset(host, 0, sizeof(*host)); + + host->config = *config; + host->drive = i; + host->ops = ops; + host->controller = controller; + host->present = !config->is_removable; + mutex_init(&host->lock); + + array[i] = host; + return; + } + } + + panicf("%s: too many controllers", __func__); +} + +#ifdef HAVE_HOTSWAP +void sdmmc_host_init_medium_present(struct sdmmc_host *host, bool present) +{ + if (!host->config.is_removable) + panicf("%s called for non-removable drive", __func__); + + host->present = present; +} + +void sdmmc_host_set_medium_present(struct sdmmc_host *host, bool present) +{ + long event = present ? Q_STORAGE_MEDIUM_INSERTED : Q_STORAGE_MEDIUM_REMOVED; + + if (!host->config.is_removable) + panicf("%s called for non-removable drive", __func__); + + storage_post_event(event, sdmmc_host_get_logical_drive(host)); +} +#endif + +#ifdef CONFIG_STORAGE_MULTI +static int sdmmc_host_count_drives(struct sdmmc_host **array, + size_t array_size) +{ + size_t num_drives = 0; + for (; num_drives < array_size; ++num_drives) + { + if (array[num_drives] == NULL) + break; + } + + return num_drives; +} +#endif + +/* + * Changes the bus power state if the target supports it. + */ +static void sdmmc_host_set_power_enabled(struct sdmmc_host *host, bool enabled) +{ + if (host->powered != enabled) + { + if (host->ops->set_power_enabled) + host->ops->set_power_enabled(host->controller, enabled); + + host->powered = enabled; + } +} + +/* + * Resets the bus by powering it down and clearing all device-related + * state. The bus must be powered back on manually before trying to + * reinitialize the SD/MMC device. + */ +static void sdmmc_host_bus_reset(struct sdmmc_host *host) +{ + sdmmc_host_set_power_enabled(host, false); + + host->need_reset = false; + host->initialized = false; + host->is_hcs_card = false; + memset(&host->cardinfo, 0, sizeof(host->cardinfo)); +} + +#ifdef HAVE_HOTSWAP +static void sdmmc_host_hotswap_event(struct sdmmc_host *host, bool is_present) +{ + /* Ignore spurious events */ + if (host->present == is_present) + return; + + /* Update medium presence flag so I/O threads see removals */ + host->present = is_present; + + /* Handle sudden medium removal */ + if (!is_present) + { + if (host->ops->abort_command) + host->ops->abort_command(host->controller); + + mutex_lock(&host->lock); + sdmmc_host_bus_reset(host); + mutex_unlock(&host->lock); + } + + /* Broadcast hotswap event to the system */ + queue_broadcast(is_present ? SYS_HOTSWAP_INSERTED : SYS_HOTSWAP_EXTRACTED, + sdmmc_host_get_logical_drive(host)); +} +#endif + +static bool sdmmc_host_medium_present(struct sdmmc_host *host) +{ +#ifdef HAVE_HOTSWAP + return host->present; +#else + return true; +#endif +} + +/* + * Submit one command to the host controller. + */ +static int sdmmc_host_submit_cmd(struct sdmmc_host *host, + const struct sdmmc_host_command *cmd, + struct sdmmc_host_response *resp) +{ + /* + * TODO: generic SD/MMC error detection & recovery, examples: + * + * - timeout to abort the command if controller is hung + * - automatic retrying of commands if CRC error occurs + */ + + return host->ops->submit_command(host->controller, cmd, resp); +} + +/* + * Execute an SD APP_CMD + */ +static int sdmmc_host_submit_app_cmd(struct sdmmc_host *host, + const struct sdmmc_host_command *cmd, + struct sdmmc_host_response *resp) +{ + struct sdmmc_host_response aresp; + struct sdmmc_host_command acmd = { + .command = SD_APP_CMD, + .argument = host->cardinfo.rca, + .flags = SDMMC_RESP_SHORT, + }; + + int rc = sdmmc_host_submit_cmd(host, &acmd, &aresp); + if (rc) + { + logf("%s: cmd55 err %d", __func__, rc); + return rc; + } + + /* Check that card accepted the APP_CMD */ + if ((aresp.data[0] & SD_R1_APP_CMD) == 0) + { + logf("%s: cmd55 not acked", __func__); + return SDMMC_STATUS_ERROR; + } + + return sdmmc_host_submit_cmd(host, cmd, resp); +} + +static int sdmmc_host_cmd_go_idle_state(struct sdmmc_host *host) +{ + struct sdmmc_host_command cmd = { + .command = SD_GO_IDLE_STATE, + .flags = SDMMC_RESP_NONE, + }; + + return sdmmc_host_submit_cmd(host, &cmd, NULL); +} + +static int sdmmc_host_cmd_send_if_cond(struct sdmmc_host *host) +{ + struct sdmmc_host_response resp; + struct sdmmc_host_command cmd = { + .command = SD_SEND_IF_COND, + .argument = 0x1aa, + .flags = SDMMC_RESP_SHORT, + }; + + int rc = sdmmc_host_submit_cmd(host, &cmd, &resp); + switch (rc) + { + case SDMMC_STATUS_OK: + /* Valid response means it's an HCS card */ + if ((resp.data[0] & 0xff) == 0xaa) + { + host->is_hcs_card = true; + break; + } + // fallthrough + + case SDMMC_STATUS_TIMEOUT: + host->is_hcs_card = false; + return SDMMC_STATUS_OK; + + default: + return rc; + } + return 0; +} + +static int sdmmc_host_cmd_send_app_op_cond(struct sdmmc_host *host) +{ + struct sdmmc_host_response resp; + struct sdmmc_host_command cmd = { + .command = SD_APP_OP_COND, + .flags = SDMMC_RESP_SHORT | SDMMC_RESP_NOCRC, + }; + + /* Add operating voltages */ + cmd.argument |= host->config.bus_voltages; + cmd.argument &= (0x1FFu << 15); + + /* Set HCS bit for SDHC/SDXC */ + if (host->is_hcs_card) + cmd.argument |= SD_OCR_CARD_CAPACITY_STATUS; + + /* + * Maximum wait time to initialize is 1 second according + * to the SD specs. + */ + int timeout = HZ; + for (; timeout > 0; timeout--) + { + int rc = sdmmc_host_submit_app_cmd(host, &cmd, &resp); + if (rc) + return rc; + + /* Exit when card says it's initialized */ + if (resp.data[0] & (1u << 31)) + break; + + /* Card not initialized yet, wait & try again */ + sleep(1); + } + + if (timeout == 0) + return SDMMC_STATUS_TIMEOUT; + + return SDMMC_STATUS_OK; +} + +static int sdmmc_host_cmd_all_send_cid(struct sdmmc_host *host) +{ + struct sdmmc_host_response resp; + struct sdmmc_host_command cmd = { + .command = SD_ALL_SEND_CID, + .flags = SDMMC_RESP_LONG, + }; + + int rc = sdmmc_host_submit_cmd(host, &cmd, &resp); + if (rc) + return rc; + + for (int i = 0; i < 4; ++i) + host->cardinfo.cid[i] = resp.data[i]; + + return rc; +} + +static int sdmmc_host_cmd_send_rca(struct sdmmc_host *host) +{ + struct sdmmc_host_response resp; + struct sdmmc_host_command cmd = { + .command = SD_SEND_RELATIVE_ADDR, + .flags = SDMMC_RESP_SHORT, + }; + + int rc = sdmmc_host_submit_cmd(host, &cmd, &resp); + if (rc) + return rc; + + host->cardinfo.rca = resp.data[0] & (0xFFFFu << 16); + return rc; +} + +static int sdmmc_host_cmd_send_csd(struct sdmmc_host *host) +{ + struct sdmmc_host_response resp; + struct sdmmc_host_command cmd = { + .command = SD_SEND_CSD, + .argument = host->cardinfo.rca, + .flags = SDMMC_RESP_LONG, + }; + + int rc = sdmmc_host_submit_cmd(host, &cmd, &resp); + if (rc) + return rc; + + for (int i = 0; i < 4; ++i) + host->cardinfo.csd[i] = resp.data[i]; + + sd_parse_csd(&host->cardinfo); + return rc; +} + +static int sdmmc_host_cmd_select_card(struct sdmmc_host *host) +{ + struct sdmmc_host_command cmd = { + .command = SD_SELECT_CARD, + .argument = host->cardinfo.rca, + .flags = SDMMC_RESP_SHORT | SDMMC_RESP_BUSY, + }; + + return sdmmc_host_submit_cmd(host, &cmd, NULL); +} + +static int sdmmc_host_cmd_clr_card_detect(struct sdmmc_host *host) +{ + struct sdmmc_host_command cmd = { + .command = SD_SET_CLR_CARD_DETECT, + .argument = 0, + .flags = SDMMC_RESP_SHORT, + }; + + return sdmmc_host_submit_app_cmd(host, &cmd, NULL); +} + +static int sdmmc_host_cmd_set_bus_width(struct sdmmc_host *host, + uint32_t bus_width) +{ + struct sdmmc_host_command cmd = { + .command = SD_SET_BUS_WIDTH, + .flags = SDMMC_RESP_SHORT, + }; + + if (bus_width == SDMMC_BUS_WIDTH_1BIT) + cmd.argument = 0; + else if (bus_width == SDMMC_BUS_WIDTH_4BIT) + cmd.argument = 2; + else + return SDMMC_STATUS_ERROR; + + return sdmmc_host_submit_app_cmd(host, &cmd, NULL); +} + +static int sdmmc_host_cmd_switch_freq(struct sdmmc_host *host, + uint32_t clock) +{ + struct sdmmc_host_command cmd = { + .command = SD_SWITCH_FUNC, + .flags = SDMMC_RESP_SHORT | SDMMC_DATA_READ, + .nr_blocks = 1, + .block_len = 64, + }; + + if (clock == SDMMC_BUS_CLOCK_25MHZ) + cmd.argument = 0x80fffff0; + else if (clock == SDMMC_BUS_CLOCK_50MHZ) + cmd.argument = 0x80fffff1; + else + return SDMMC_STATUS_ERROR; + + /* + * We'll just assume the disk cache will have a free buffer. + * Since the disk isn't mounted, we should have at least one + * free that would otherwise be used by the FAT filesystem. + */ + cmd.buffer = dc_get_buffer(); + if (!cmd.buffer) + panicf("%s: OOM", __func__); + + int rc = sdmmc_host_submit_cmd(host, &cmd, NULL); + + dc_release_buffer(cmd.buffer); + return rc; +} + +static int sdmmc_host_cmd_set_block_len(struct sdmmc_host *host, int len) +{ + struct sdmmc_host_command cmd = { + .command = SD_SET_BLOCKLEN, + .argument = len, + .flags = SDMMC_RESP_SHORT, + }; + + return sdmmc_host_submit_cmd(host, &cmd, NULL); +} + +static void sdmmc_host_set_controller_bus_width(struct sdmmc_host *host, uint32_t width) +{ + if (host->ops->set_bus_width) + host->ops->set_bus_width(host->controller, width); +} + +static void sdmmc_host_set_controller_bus_clock(struct sdmmc_host *host, uint32_t clock) +{ + host->ops->set_bus_clock(host->controller, clock); +} + +/* + * Initialize the SD/MMC device and make it ready to access. + * On success, sets host->initialized to true and returns 0. + * Returns nonzero on error. If an error occurs the bus must + * be reset before retrying. + * + * Preconditions: + * - host->initialized is false + * - device related state is reset to default + */ +static int sdmmc_host_device_init(struct sdmmc_host *host) +{ + /* TODO: MMC initialization */ + if (host->config.type != STORAGE_SD) + panicf("%s: TODO mmc init", __func__); + + /* Initialize bus */ + sdmmc_host_set_controller_bus_width(host, SDMMC_BUS_WIDTH_1BIT); + sdmmc_host_set_controller_bus_clock(host, SDMMC_BUS_CLOCK_400KHZ); + sdmmc_host_set_power_enabled(host, true); + + /* Handle SD initialization */ + int rc = sdmmc_host_cmd_go_idle_state(host); + if (rc) + { + logf("sdmmc_host_cmd_go_idle_state: %d", rc); + return rc; + } + + rc = sdmmc_host_cmd_send_if_cond(host); + if (rc) + { + logf("sdmmc_host_cmd_send_if_cond: %d", rc); + return rc; + } + + rc = sdmmc_host_cmd_send_app_op_cond(host); + if (rc) + { + logf("sdmmc_host_cmd_send_app_op_cond: %d", rc); + return rc; + } + + rc = sdmmc_host_cmd_all_send_cid(host); + if (rc) + { + logf("sdmmc_host_cmd_all_send_cid: %d", rc); + return rc; + } + + rc = sdmmc_host_cmd_send_rca(host); + if (rc) + { + logf("sdmmc_host_cmd_send_rca: %d", rc); + return rc; + } + + rc = sdmmc_host_cmd_send_csd(host); + if (rc) + { + logf("sdmmc_host_cmd_send_csd: %d", rc); + return rc; + } + + rc = sdmmc_host_cmd_select_card(host); + if (rc) + { + logf("sdmmc_host_cmd_select_card: %d", rc); + return rc; + } + + rc = sdmmc_host_cmd_clr_card_detect(host); + if (rc) + { + logf("sdmmc_host_cmd_select_card: %d", rc); + return rc; + } + + /* + * All SD cards must support 1-bit and 4-bit bus widths, + * so we only need to check the controller capabilities. + */ + if (host->config.bus_widths & SDMMC_BUS_WIDTH_4BIT) + { + rc = sdmmc_host_cmd_set_bus_width(host, SDMMC_BUS_WIDTH_4BIT); + if (rc) + { + logf("sdmmc_host_cmd_set_bus_width: %d", rc); + return rc; + } + + sdmmc_host_set_controller_bus_width(host, SDMMC_BUS_WIDTH_4BIT); + } + + /* + * Switch to highest clock frequency supported by both + * card and controller. 25MHz must always be supported. + */ + if (host->cardinfo.sd2plus && + (host->config.bus_clocks & SDMMC_BUS_CLOCK_50MHZ)) + { + rc = sdmmc_host_cmd_switch_freq(host, SDMMC_BUS_CLOCK_50MHZ); + if (rc) + { + logf("sdmmc_host_cmd_switch_freq: %d", rc); + return rc; + } + + sdmmc_host_set_controller_bus_clock(host, SDMMC_BUS_CLOCK_50MHZ); + } + else + { + sdmmc_host_set_controller_bus_clock(host, SDMMC_BUS_CLOCK_25MHZ); + } + + host->initialized = true; + return 0; +} + +static int sdmmc_host_transfer(struct sdmmc_host *host, + sector_t start, int count, void *buf, + uint32_t data_dir) +{ + int rc = -1; + + mutex_lock(&host->lock); + + if (!sdmmc_host_medium_present(host)) + goto out; + + if (host->need_reset) + { + /* Automatically clears need_reset flag */ + sdmmc_host_bus_reset(host); + } + + if (!host->initialized) + { + rc = sdmmc_host_device_init(host); + if (rc) + { + host->need_reset = true; + goto out; + } + } + + if (count < 1) + goto out; + if (start + count > host->cardinfo.numblocks) + goto out; + + while (count > 0) + { + /* TODO: multiple block transfers */ + int xfer_count = 1; + + /* Set block length for non-HCS cards */ + if (!host->is_hcs_card) + { + rc = sdmmc_host_cmd_set_block_len(host, SD_BLOCK_SIZE); + if (rc) + goto out; + } + + struct sdmmc_host_command cmd = { + .buffer = buf, + .nr_blocks = 1, + .block_len = SD_BLOCK_SIZE, + .flags = SDMMC_RESP_SHORT | data_dir, + }; + + if (data_dir == SDMMC_DATA_WRITE) + cmd.command = SD_WRITE_BLOCK; + else + cmd.command = SD_READ_SINGLE_BLOCK; + + if (host->cardinfo.sd2plus) + cmd.argument = start; + else + cmd.argument = start * SD_BLOCK_SIZE; + + rc = sdmmc_host_submit_cmd(host, &cmd, NULL); + if (rc) + goto out; + + buf += xfer_count * SD_BLOCK_SIZE; + start += xfer_count; + count -= xfer_count; + } + +out: + mutex_unlock(&host->lock); + return rc; +} + +/* + * SD storage API + */ + +#if CONFIG_STORAGE & STORAGE_SD +int sd_init(void) +{ + return 0; +} + +#ifdef CONFIG_STORAGE_MULTI +int sd_num_drives(int first_drive) +{ + sdmmc_sd_first_drive = first_drive; + return sdmmc_host_count_drives(sdmmc_sd_hosts, ARRAYLEN(sdmmc_sd_hosts)); +} +#endif + +tCardInfo *card_get_info_target(int drive) +{ + /* FIXME: this API is racy, no way to fix without refactoring */ + return &sdmmc_sd_hosts[drive]->cardinfo; +} + +long sd_last_disk_activity(void) +{ + return sdmmc_sd_last_activity; +} + +int sd_event(long id, intptr_t data) +{ +#ifdef HAVE_HOTSWAP + if (id == Q_STORAGE_MEDIUM_INSERTED || + id == Q_STORAGE_MEDIUM_REMOVED) + { + bool present = (id == Q_STORAGE_MEDIUM_INSERTED); + + sdmmc_host_hotswap_event(sdmmc_sd_hosts[IF_MD_DRV(drive)], present); + return 0; + } +#endif + + return storage_event_default_handler(id, data, + sdmmc_sd_last_activity, STORAGE_SD); +} + +int sd_read_sectors(IF_MD(int drive,) sector_t start, int count, void* buf) +{ + struct sdmmc_host *host = sdmmc_sd_hosts[IF_MD_DRV(drive)]; + + sdmmc_sd_last_activity = current_tick; + + return sdmmc_host_transfer(host, start, count, buf, SDMMC_DATA_READ); +} + +int sd_write_sectors(IF_MD(int drive,) sector_t start, int count, const void* buf) +{ + struct sdmmc_host *host = sdmmc_sd_hosts[IF_MD_DRV(drive)]; + + sdmmc_sd_last_activity = current_tick; + + return sdmmc_host_transfer(host, start, count, (void *)buf, SDMMC_DATA_WRITE); +} + +#ifdef HAVE_HOTSWAP +bool sd_removable(IF_MD_NONVOID(int drive)) +{ + struct sdmmc_host *host = sdmmc_sd_hosts[IF_MD_DRV(drive)]; + + return host->config.is_removable; +} + +bool sd_present(IF_MD_NONVOID(int drive)) +{ + struct sdmmc_host *host = sdmmc_sd_hosts[IF_MD_DRV(drive)]; + + return sdmmc_host_medium_present(host); +} +#endif /* HAVE_HOTSWAP */ +#endif /* CONFIG_STORAGE & STORAGE_SD */ + +/* + * TODO: add MMC storage API; right now nothing needs it + */ diff --git a/firmware/export/config.h b/firmware/export/config.h index 7e12d97d57..89b698bdfe 100644 --- a/firmware/export/config.h +++ b/firmware/export/config.h @@ -1448,6 +1448,15 @@ Lyre prototype 1 */ # define HAVE_PERCEPTUAL_VOLUME #endif +#if defined(SDMMC_HOST_NUM_SD_CONTROLLERS) || \ + defined(SDMMC_HOST_NUM_MMC_CONTROLLERS) +# define HAVE_SDMMC_HOST +#endif + +#if defined(HAVE_SDMMC_HOST) && defined(HAVE_HOTSWAP) +# define HAVE_HOTSWAP_IN_THREAD +#endif + /* * Turn off legacy codepage handling in the filesystem code for bootloaders, * and support ISO-8859-1 (Latin-1) only. This only affects DOS 8.3 filename diff --git a/firmware/export/sdmmc_host.h b/firmware/export/sdmmc_host.h new file mode 100644 index 0000000000..7aa48e5f9e --- /dev/null +++ b/firmware/export/sdmmc_host.h @@ -0,0 +1,291 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * $Id$ + * + * Copyright (C) 2025 Aidan MacDonald + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ +#ifndef __SDMMC_HOST_H__ +#define __SDMMC_HOST_H__ + +/* + * Generic SD/MMC host driver which implements the storage + * API for targets with SD/MMC based storage. Targets only + * have to implement a simplified controller interface to + * process SD/MMC commands, and manage clocks/power. + * + * sdmmc_host supports hotswappable storage, but the target + * is responsible for detecting insertion and removals, and + * informing sdmmc_host. + * + * To enable this driver add the following defines in your + * target config.h to set the number of SD/MMC controllers + * on the system: + * + * - SDMMC_HOST_NUM_SD_CONTROLLERS + * - SDMMC_HOST_NUM_MMC_CONTROLLERS + * + * You also need to implement `struct sdmmc_controller_ops` + * for your hardware, and initialize the host drivers for + * each storage device from the target-specific function + * `sdmmc_host_target_init()`. + */ + +#include "storage.h" +#include "sdmmc.h" +#include "mutex.h" +#include +#include +#include + +/* Response length flags */ +#define SDMMC_RESP_NONE 0x00 +#define SDMMC_RESP_SHORT 0x01 +#define SDMMC_RESP_LONG 0x02 +#define SDMMC_RESP_LENGTH(flags) ((flags) & 0x03) + +/* Response requires busy polling */ +#define SDMMC_RESP_BUSY 0x04 + +/* Response has no CRC */ +#define SDMMC_RESP_NOCRC 0x08 + +/* Data present & direction of data transfer */ +#define SDMMC_DATA_READ 0x10 +#define SDMMC_DATA_WRITE 0x30 +#define SDMMC_DATA_PRESENT(flags) ((flags) & 0x10) +#define SDMMC_DATA_DIR(flags) ((flags) & 0x30) + +/* Status codes returned by submit_command() callback */ +#define SDMMC_STATUS_OK 0 +#define SDMMC_STATUS_ERROR (-1) +#define SDMMC_STATUS_TIMEOUT (-2) +#define SDMMC_STATUS_INVALID_CRC (-3) + +struct sdmmc_host_command +{ + uint16_t flags; + uint16_t command; + uint32_t argument; + uint16_t nr_blocks; + uint16_t block_len; + void *buffer; +}; + +struct sdmmc_host_response +{ + uint32_t data[4]; +}; + +/* Possible bus voltages */ +#define SDMMC_BUS_VOLTAGE_2V7_2V8 (1 << 15) +#define SDMMC_BUS_VOLTAGE_2V8_2V9 (1 << 16) +#define SDMMC_BUS_VOLTAGE_2V9_3V0 (1 << 17) +#define SDMMC_BUS_VOLTAGE_3V0_3V1 (1 << 18) +#define SDMMC_BUS_VOLTAGE_3V1_3V2 (1 << 19) +#define SDMMC_BUS_VOLTAGE_3V2_3V3 (1 << 20) +#define SDMMC_BUS_VOLTAGE_3V3_3V4 (1 << 21) +#define SDMMC_BUS_VOLTAGE_3V4_3V5 (1 << 22) +#define SDMMC_BUS_VOLTAGE_3V5_3V6 (1 << 23) + +/* Possible bus widths */ +#define SDMMC_BUS_WIDTH_1BIT (1 << 0) +#define SDMMC_BUS_WIDTH_4BIT (1 << 1) +#define SDMMC_BUS_WIDTH_8BIT (1 << 2) + +/* Possible bus clock speeds */ +#define SDMMC_BUS_CLOCK_400KHZ (1 << 0) +#define SDMMC_BUS_CLOCK_25MHZ (1 << 1) +#define SDMMC_BUS_CLOCK_50MHZ (1 << 2) + +/** + * Callbacks for implementing an SD/MMC host controller. + * + * Most functions here are called with the sdmmc_host lock + * held which prevents multiple threads from accessing the + * same controller. + */ +struct sdmmc_controller_ops +{ + /** + * \brief Enable/disable power supply of the SD/MMC device + * \param controller Controller pointer + * \param enable Whether power should be enabled or not + * + * sdmmc_host will try to power cycle the device after + * non-recoverable errors. Removable devices will also + * have bus power enabled/disabled when the device is + * inserted or removed. + * + * Even if the target can't physically power cycle the + * card, a reset of the SDMMC controller is often good + * enough. + * + * Optional. Not implementing this function does not + * change any behavior of sdmmc_host -- it will still + * try to recover from errors by reinitializing the + * SDMMC device. + */ + void (*set_power_enabled) (void *controller, bool enable); + + /** + * \brief Set the bus width (number of active data lines) + * \param controller Controller pointer + * \param width One of the SDMMC_BUS_WIDTH constants + * + * Optional. If not implemented then the bus width is + * limited to 1 bit wide. + */ + void (*set_bus_width) (void *controller, uint32_t width); + + /** + * \brief Set the frequency of the clock provided to the SD/MMC device + * \param controller Controller pointer + * \param clock One of the SDMMC_BUS_CLOCK constants + * + * All controllers must implement this funtion and support + * at least 400 KHz and 25 MHz rates. + */ + void (*set_bus_clock) (void *controller, uint32_t clock); + + /** + * \brief Submit a command to the controller and wait for the response + * \param cmd Command descriptor + * \param resp Response data buffer; may be NULL + * + * \retval SDMMC_STATUS_OK if command completed successfully + * \retval SDMMC_STATUS_TIMEOUT if failed due to command timeout + * \retval SDMMC_STATUS_INVALID_CRC if failed due to an invalid CRC + * \retval SDMMC_STATUS_ERROR if failed in any other way + * + * Note that if `resp` is NULL, the controller must still use + * the response format specified in `cmd->flags`; the response + * data, if any, should be discarded. + */ + int (*submit_command) (void *controller, + const struct sdmmc_host_command *cmd, + struct sdmmc_host_response *resp); + + /** + * Abort command processing. Any in-progress call to + * submit_command() must be made to return as soon as + * possible with a nonzero status code. + * + * If no command is in progress then this function + * must do nothing. + * + * Optional. If not implemented then the controller is + * responsible for ensuring that submit_command() will + * eventually return even in case of device removal or + * a command unexpectedly hanging. + * + * May be called without the sdmmc_host lock held. + */ + void (*abort_command) (void *controller); +}; + +struct sdmmc_host_config +{ + /* Storage type: either STORAGE_SD or STORAGE_MMC */ + int type; + + /* Supported voltages, bus widths, and clock speeds */ + uint32_t bus_voltages; + uint32_t bus_widths; + uint32_t bus_clocks; + + /* Set to true if the device is removable at runtime */ + bool is_removable; +}; + +struct sdmmc_host +{ + /* Static configuration */ + struct sdmmc_host_config config; + + /* Physical drive number */ + int drive; + +#ifdef HAVE_HOTSWAP + /* Medium presence flag */ + volatile int present; +#endif + + /* + * Lock which protects most controller operations + * and mutable sdmmc_host state. + */ + struct mutex lock; + + /* Bus & device state flags; must only be accessed with lock held */ + bool need_reset : 1; + bool powered : 1; + bool initialized : 1; + bool is_hcs_card : 1; + + /* Controller implemented by the target */ + const struct sdmmc_controller_ops *ops; + void *controller; + + /* Card info probed from card */ + tCardInfo cardinfo; +}; + +/** + * Called during storage_init() to add SD/MMC host controllers. + * The target should call sdmmc_host_init() for each controller + * in the system. + */ +void sdmmc_host_target_init(void) INIT_ATTR; + +/** + * \brief Initialize an SD/MMC host controller + * \param host Host driver state, allocated by the target + * \param config Static configuration for host controller + * \param ops Controller callbacks + * \param controller Opaque pointer passed to ops callbacks + * + * Physical drive numbers will be assigned in the order of + * initialization, eg. the first host will be drive 0, the + * second will be drive 1, and so forth. + */ +void sdmmc_host_init(struct sdmmc_host *host, + const struct sdmmc_host_config *config, + const struct sdmmc_controller_ops *ops, + void *controller) INIT_ATTR; + +/** + * \brief Set initial SD/MMC medium presence + * \param host Host controller + * \param present True if medium is present + * \note Only use this function from `sdmmc_host_target_init()` to set + * the initial medium presence state. Only needs to be called for + * drives that support removable media; non-removable drives will + * always be considered inserted. + */ +void sdmmc_host_init_medium_present(struct sdmmc_host *host, bool present) INIT_ATTR; + +/** + * \brief Set SD/MMC medium presence state + * \param host Host controller + * \param present True if medium is present + * \note Do not use this function from `sdmmc_host_target_init()` since + * it relies on the storage queue being available. You must instead + * call `sdmmc_host_init_medium_present()` to set the initial state. + */ +void sdmmc_host_set_medium_present(struct sdmmc_host *host, bool present); + +#endif /* __SDMMC_HOST_H__ */ diff --git a/firmware/export/storage.h b/firmware/export/storage.h index 86dfde5002..b0a791fe7f 100644 --- a/firmware/export/storage.h +++ b/firmware/export/storage.h @@ -58,6 +58,10 @@ enum #ifdef STORAGE_CLOSE Q_STORAGE_CLOSE, #endif +#ifdef HAVE_HOTSWAP_IN_THREAD + Q_STORAGE_MEDIUM_INSERTED, + Q_STORAGE_MEDIUM_REMOVED, +#endif }; #define STG_EVENT_ASSERT_ACTIVE(type) \ @@ -118,6 +122,7 @@ struct storage_info int storage_init(void) STORAGE_INIT_ATTR; void storage_close(void); +void storage_post_event(long event, intptr_t data); #ifdef HAVE_HOSTFS #include "hostfs.h" diff --git a/firmware/storage.c b/firmware/storage.c index 2d4490369e..341c007f7e 100644 --- a/firmware/storage.c +++ b/firmware/storage.c @@ -25,6 +25,10 @@ #include "disk.h" #include "pathfuncs.h" +#ifdef HAVE_SDMMC_HOST +# include "sdmmc_host.h" +#endif + #ifdef CONFIG_STORAGE_MULTI #define DRIVER_MASK 0xff000000 @@ -181,6 +185,13 @@ static void NORETURN_ATTR storage_thread(void) } break; +#ifdef HAVE_HOTSWAP_IN_THREAD + case Q_STORAGE_MEDIUM_INSERTED: + case Q_STORAGE_MEDIUM_REMOVED: + storage_event_send(DRIVE_EVT, ev.id, ev.data); + break; +#endif + #if (CONFIG_STORAGE & STORAGE_ATA) case Q_STORAGE_SLEEP: storage_event_send(bdcast, ev.id, 0); @@ -255,6 +266,12 @@ void storage_close(void) } #endif /* STORAGE_CLOSE */ +void storage_post_event(long event, intptr_t data) +{ + if (storage_thread_id) + queue_post(&storage_queue, event, data); +} + static inline void storage_thread_init(void) { if (storage_thread_id) { @@ -273,6 +290,10 @@ int storage_init(void) { int rc=0; +#ifdef HAVE_SDMMC_HOST + sdmmc_host_target_init(); +#endif + #ifdef CONFIG_STORAGE_MULTI int i; num_drives=0;