From: Dmitry Yakovlev Date: Tue, 4 Jul 2017 05:58:36 +0000 (+0800) Subject: driver: SD protocol driver for SPI peripheral X-Git-Tag: v3.1-dev~441^2 X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=e5bb45f3814ac5a94726ea443022fc266272bc4e;p=esp-idf driver: SD protocol driver for SPI peripheral --- diff --git a/components/driver/include/driver/sdspi_host.h b/components/driver/include/driver/sdspi_host.h new file mode 100644 index 0000000000..72bd9f3829 --- /dev/null +++ b/components/driver/include/driver/sdspi_host.h @@ -0,0 +1,156 @@ +// Copyright 2015-2017 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include "esp_err.h" +#include "sdmmc_types.h" +#include "driver/gpio.h" +#include "driver/spi_master.h" +#include "driver/sdmmc_host.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Default sdmmc_host_t structure initializer for SD over SPI driver + * + * Uses SPI mode and max frequency set to 20MHz + * + * 'slot' can be set to one of HSPI_HOST, VSPI_HOST. + */ +#define SDSPI_HOST_DEFAULT() {\ + .flags = SDMMC_HOST_FLAG_SPI, \ + .slot = HSPI_HOST, \ + .max_freq_khz = SDMMC_FREQ_DEFAULT, \ + .io_voltage = 3.3f, \ + .init = &sdspi_host_init, \ + .set_bus_width = NULL, \ + .set_card_clk = &sdspi_host_set_card_clk, \ + .do_transaction = &sdspi_host_do_transaction, \ + .deinit = &sdspi_host_deinit, \ +} + +/** + * Extra configuration for SPI host + */ +typedef struct { + gpio_num_t gpio_miso; ///< GPIO number of MISO signal + gpio_num_t gpio_mosi; ///< GPIO number of MOSI signal + gpio_num_t gpio_sck; ///< GPIO number of SCK signal + gpio_num_t gpio_cs; ///< GPIO number of CS signal + gpio_num_t gpio_cd; ///< GPIO number of card detect signal + gpio_num_t gpio_wp; ///< GPIO number of write protect signal + int dma_channel; ///< DMA channel to be used by SPI driver (1 or 2) +} sdspi_slot_config_t; + +#define SDSPI_SLOT_NO_CD ((gpio_num_t) -1) ///< indicates that card detect line is not used +#define SDSPI_SLOT_NO_WP ((gpio_num_t) -1) ///< indicates that write protect line is not used + +/** + * Macro defining default configuration of SPI host + */ +#define SDSPI_SLOT_CONFIG_DEFAULT() {\ + .gpio_miso = GPIO_NUM_2, \ + .gpio_mosi = GPIO_NUM_15, \ + .gpio_sck = GPIO_NUM_14, \ + .gpio_cs = GPIO_NUM_13, \ + .gpio_cd = SDMMC_SLOT_NO_CD, \ + .gpio_wp = SDMMC_SLOT_NO_WP, \ + .dma_channel = 1 \ +} + +/** + * @brief Initialize SD SPI driver + * + * @note This function is not thread safe + * + * @return + * - ESP_OK on success + * - other error codes may be returned in future versions + */ +esp_err_t sdspi_host_init(); + +/** +* @brief Initialize SD SPI driver for the specific SPI controller +* +* @note This function is not thread safe +* +* @param slot SPI controller to use (HSPI_HOST or VSPI_HOST) +* @param slot_config pointer to slot configuration structure +* +* @return +* - ESP_OK on success +* - ESP_ERR_INVALID_ARG if sdspi_init_slot has invalid arguments +* - ESP_ERR_NO_MEM if memory can not be allocated +* - other errors from the underlying spi_master and gpio drivers +*/ +esp_err_t sdspi_host_init_slot(int slot, const sdspi_slot_config_t* slot_config); + +/** + * @brief Send command to the card and get response + * + * This function returns when command is sent and response is received, + * or data is transferred, or timeout occurs. + * + * @note This function is not thread safe w.r.t. init/deinit functions, + * and bus width/clock speed configuration functions. Multiple tasks + * can call sdspi_host_do_transaction as long as other sdspi_host_* + * functions are not called. + * + * @param slot SPI controller (HSPI_HOST or VSPI_HOST) + * @param cmdinfo pointer to structure describing command and data to transfer + * @return + * - ESP_OK on success + * - ESP_ERR_TIMEOUT if response or data transfer has timed out + * - ESP_ERR_INVALID_CRC if response or data transfer CRC check has failed + * - ESP_ERR_INVALID_RESPONSE if the card has sent an invalid response + */ +esp_err_t sdspi_host_do_transaction(int slot, sdmmc_command_t *cmdinfo); + +/** + * @brief Set card clock frequency + * + * Currently only integer fractions of 40MHz clock can be used. + * For High Speed cards, 40MHz can be used. + * For Default Speed cards, 20MHz can be used. + * + * @note This function is not thread safe + * + * @param slot SPI controller (HSPI_HOST or VSPI_HOST) + * @param freq_khz card clock frequency, in kHz + * @return + * - ESP_OK on success + * - other error codes may be returned in the future + */ +esp_err_t sdspi_host_set_card_clk(int slot, uint32_t freq_khz); + + +/** + * @brief Release resources allocated using sdspi_host_init + * + * @note This function is not thread safe + * + * @return + * - ESP_OK on success + * - ESP_ERR_INVALID_STATE if sdspi_host_init function has not been called + */ +esp_err_t sdspi_host_deinit(); + +#ifdef __cplusplus +} +#endif diff --git a/components/driver/sdspi_crc.c b/components/driver/sdspi_crc.c new file mode 100644 index 0000000000..4b47446596 --- /dev/null +++ b/components/driver/sdspi_crc.c @@ -0,0 +1,53 @@ +// Copyright 2015-2017 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include "rom/crc.h" +#include "sdspi_crc.h" + +static const uint8_t crc7_table[256] = +{ + 0x00, 0x09, 0x12, 0x1b, 0x24, 0x2d, 0x36, 0x3f, 0x48, 0x41, 0x5a, 0x53, 0x6c, 0x65, 0x7e, 0x77, + 0x19, 0x10, 0x0b, 0x02, 0x3d, 0x34, 0x2f, 0x26, 0x51, 0x58, 0x43, 0x4a, 0x75, 0x7c, 0x67, 0x6e, + 0x32, 0x3b, 0x20, 0x29, 0x16, 0x1f, 0x04, 0x0d, 0x7a, 0x73, 0x68, 0x61, 0x5e, 0x57, 0x4c, 0x45, + 0x2b, 0x22, 0x39, 0x30, 0x0f, 0x06, 0x1d, 0x14, 0x63, 0x6a, 0x71, 0x78, 0x47, 0x4e, 0x55, 0x5c, + 0x64, 0x6d, 0x76, 0x7f, 0x40, 0x49, 0x52, 0x5b, 0x2c, 0x25, 0x3e, 0x37, 0x08, 0x01, 0x1a, 0x13, + 0x7d, 0x74, 0x6f, 0x66, 0x59, 0x50, 0x4b, 0x42, 0x35, 0x3c, 0x27, 0x2e, 0x11, 0x18, 0x03, 0x0a, + 0x56, 0x5f, 0x44, 0x4d, 0x72, 0x7b, 0x60, 0x69, 0x1e, 0x17, 0x0c, 0x05, 0x3a, 0x33, 0x28, 0x21, + 0x4f, 0x46, 0x5d, 0x54, 0x6b, 0x62, 0x79, 0x70, 0x07, 0x0e, 0x15, 0x1c, 0x23, 0x2a, 0x31, 0x38, + 0x41, 0x48, 0x53, 0x5a, 0x65, 0x6c, 0x77, 0x7e, 0x09, 0x00, 0x1b, 0x12, 0x2d, 0x24, 0x3f, 0x36, + 0x58, 0x51, 0x4a, 0x43, 0x7c, 0x75, 0x6e, 0x67, 0x10, 0x19, 0x02, 0x0b, 0x34, 0x3d, 0x26, 0x2f, + 0x73, 0x7a, 0x61, 0x68, 0x57, 0x5e, 0x45, 0x4c, 0x3b, 0x32, 0x29, 0x20, 0x1f, 0x16, 0x0d, 0x04, + 0x6a, 0x63, 0x78, 0x71, 0x4e, 0x47, 0x5c, 0x55, 0x22, 0x2b, 0x30, 0x39, 0x06, 0x0f, 0x14, 0x1d, + 0x25, 0x2c, 0x37, 0x3e, 0x01, 0x08, 0x13, 0x1a, 0x6d, 0x64, 0x7f, 0x76, 0x49, 0x40, 0x5b, 0x52, + 0x3c, 0x35, 0x2e, 0x27, 0x18, 0x11, 0x0a, 0x03, 0x74, 0x7d, 0x66, 0x6f, 0x50, 0x59, 0x42, 0x4b, + 0x17, 0x1e, 0x05, 0x0c, 0x33, 0x3a, 0x21, 0x28, 0x5f, 0x56, 0x4d, 0x44, 0x7b, 0x72, 0x69, 0x60, + 0x0e, 0x07, 0x1c, 0x15, 0x2a, 0x23, 0x38, 0x31, 0x46, 0x4f, 0x54, 0x5d, 0x62, 0x6b, 0x70, 0x79, +}; + +// returns the CRC-7 for a message of "length" bytes +uint8_t sdspi_crc7(const uint8_t *data, size_t size) +{ + uint8_t result = 0; + for (size_t i = 0; i < size; ++i) { + result = crc7_table[(result << 1) ^ data[i]]; + } + return result; +} + +/// Return CRC16 of data, in the on-the-wire format used by SD protocol +uint16_t sdspi_crc16(const uint8_t* data, size_t size) +{ + return __builtin_bswap16(crc16_be(UINT16_MAX, data, size) ^ UINT16_MAX); +} diff --git a/components/driver/sdspi_crc.h b/components/driver/sdspi_crc.h new file mode 100644 index 0000000000..775d682bd3 --- /dev/null +++ b/components/driver/sdspi_crc.h @@ -0,0 +1,43 @@ +// Copyright 2015-2017 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#pragma once +#include +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + + +/** + * @brief Return CRC7 of data, in the format used by SD protocol + * @param data array of data used to compute CRC + * @param size size of data in bytes + * @return CRC7 value + */ +uint8_t sdspi_crc7(const uint8_t *data, size_t size); + +/** + * @brief Return CRC16 of data, in the format used by SD protocol + * @param data array of data used to compute CRC + * @param size size of data in bytes + * @return CRC16 value + */ +uint16_t sdspi_crc16(const uint8_t* data, size_t size); + + +#ifdef __cplusplus +} +#endif diff --git a/components/driver/sdspi_host.c b/components/driver/sdspi_host.c new file mode 100644 index 0000000000..11f702e528 --- /dev/null +++ b/components/driver/sdspi_host.c @@ -0,0 +1,815 @@ +// Copyright 2015-2017 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include +#include +#include +#include "esp_log.h" +#include "esp_heap_caps.h" +#include "driver/gpio.h" +#include "driver/sdmmc_defs.h" +#include "driver/sdspi_host.h" +#include "sdspi_private.h" +#include "sdspi_crc.h" + +/// Max number of transactions in flight (used in start_command_write_blocks) +#define SDSPI_TRANSACTION_COUNT 4 +#define SDSPI_MOSI_IDLE_VAL 0xff //!< Data value which causes MOSI to stay high +/// FIXME: this has to be replaced with a timeout expressed in ms, rather in retries +#define SDSPI_RETRY_COUNT 1000 +#define GPIO_UNUSED 0xff //!< Flag indicating that CD/WP is unused +/// Size of the buffer returned by get_block_buf +#define SDSPI_BLOCK_BUF_SIZE (SDSPI_MAX_DATA_LEN + 4) + + +/// Structure containing run time configuration for a single SD slot +typedef struct { + spi_device_handle_t handle; //!< SPI device handle, used for transactions + uint8_t gpio_cs; //!< CS GPIO + uint8_t gpio_cd; //!< Card detect GPIO, or GPIO_UNUSED + uint8_t gpio_wp; //!< Write protect GPIO, or GPIO_UNUSED + /// Set to 1 if the higher layer has asked the card to enable CRC checks + uint8_t data_crc_enabled : 1; + /// Number of transactions in 'transactions' array which are in use + uint8_t used_transaction_count: 3; + /// Intermediate buffer used when application buffer is not in DMA memory; + /// allocated on demand, SDSPI_BLOCK_BUF_SIZE bytes long. May be zero. + uint8_t* block_buf; + /// array with SDSPI_TRANSACTION_COUNT transaction structures + spi_transaction_t* transactions; +} slot_info_t; + +static slot_info_t s_slots[3]; +static const char *TAG = "sdspi_host"; + +/// Functions to send out different kinds of commands +static esp_err_t start_command_read_blocks(int slot, sdspi_hw_cmd_t *cmd, + uint8_t *data, uint32_t rx_length); + +static esp_err_t start_command_write_blocks(int slot, sdspi_hw_cmd_t *cmd, + const uint8_t *data, uint32_t tx_length); + +static esp_err_t start_command_default(int slot, int flags, sdspi_hw_cmd_t *cmd); + +/// A few helper functions + +/// Set CS high for given slot +static void cs_high(int slot) +{ + gpio_set_level(s_slots[slot].gpio_cs, 1); +} + +/// Set CS low for given slot +static void cs_low(int slot) +{ + gpio_set_level(s_slots[slot].gpio_cs, 0); +} + +/// Return true if WP pin is configured and is low +static bool card_write_protected(int slot) +{ + if (s_slots[slot].gpio_wp == GPIO_UNUSED) { + return false; + } + return gpio_get_level(s_slots[slot].gpio_wp) == 0; +} + +/// Return true if CD pin is configured and is high +static bool card_missing(int slot) +{ + if (s_slots[slot].gpio_cd == GPIO_UNUSED) { + return false; + } + return gpio_get_level(s_slots[slot].gpio_cd) == 1; +} + +/// Check if slot number is within bounds +static bool is_valid_slot(int slot) +{ + return slot == VSPI_HOST || slot == HSPI_HOST; +} + +static spi_device_handle_t spi_handle(int slot) +{ + return s_slots[slot].handle; +} + +static bool is_slot_initialized(int slot) +{ + return spi_handle(slot) != NULL; +} + +static bool data_crc_enabled(int slot) +{ + return s_slots[slot].data_crc_enabled; +} + +/// Get pointer to a block of DMA memory, allocate if necessary. +/// This is used if the application provided buffer is not in DMA capable memory. +static esp_err_t get_block_buf(int slot, uint8_t** out_buf) +{ + if (s_slots[slot].block_buf == NULL) { + s_slots[slot].block_buf = heap_caps_malloc(SDSPI_BLOCK_BUF_SIZE, MALLOC_CAP_DMA); + if (s_slots[slot].block_buf == NULL) { + return ESP_ERR_NO_MEM; + } + } + *out_buf = s_slots[slot].block_buf; + return ESP_OK; +} + +static spi_transaction_t* get_transaction(int slot) +{ + size_t used_transaction_count = s_slots[slot].used_transaction_count; + assert(used_transaction_count < SDSPI_TRANSACTION_COUNT); + spi_transaction_t* ret = &s_slots[slot].transactions[used_transaction_count]; + ++s_slots[slot].used_transaction_count; + return ret; +} + +static void release_transaction(int slot) +{ + --s_slots[slot].used_transaction_count; +} + +static void wait_for_transactions(int slot) +{ + size_t used_transaction_count = s_slots[slot].used_transaction_count; + for (size_t i = 0; i < used_transaction_count; ++i) { + spi_transaction_t* t_out; + spi_device_get_trans_result(spi_handle(slot), &t_out, portMAX_DELAY); + release_transaction(slot); + } +} + +/// Clock out one byte (CS has to be high) to make the card release MISO +/// (clocking one bit would work as well, but that triggers a bug in SPI DMA) +static void release_bus(int slot) +{ + spi_transaction_t t = { + .flags = SPI_TRANS_USE_RXDATA | SPI_TRANS_USE_TXDATA, + .length = 8, + .tx_data = {0xff} + }; + spi_device_transmit(spi_handle(slot), &t); + // don't care if this failed +} + +/// Clock out 80 cycles (10 bytes) before GO_IDLE command +static void go_idle_clockout(int slot) +{ + uint8_t data[10]; + memset(data, 0xff, sizeof(data)); + spi_transaction_t t = { + .length = sizeof(data) * 8, + .tx_buffer = data, + .rx_buffer = data, + }; + spi_device_transmit(spi_handle(slot), &t); + // don't care if this failed +} + + +/// Return true if the pointer can be used for DMA +static bool ptr_dma_compatible(const void* ptr) +{ + return (uintptr_t) ptr >= 0x3FFAE000 && + (uintptr_t) ptr < 0x40000000; +} + +/** + * Initialize SPI device. Used to change clock speed. + * @param slot SPI host number + * @param clock_speed_hz clock speed, Hz + * @return ESP_OK on success + */ +static esp_err_t init_spi_dev(int slot, int clock_speed_hz) +{ + if (spi_handle(slot)) { + // Reinitializing + spi_bus_remove_device(spi_handle(slot)); + s_slots[slot].handle = NULL; + } + spi_device_interface_config_t devcfg = { + .clock_speed_hz = clock_speed_hz, + .mode = 0, + // For SD cards, CS must stay low during the whole read/write operation, + // rather than a single SPI transaction. + .spics_io_num = -1, + .queue_size = SDSPI_TRANSACTION_COUNT, + }; + return spi_bus_add_device((spi_host_device_t) slot, &devcfg, &s_slots[slot].handle); +} + +esp_err_t sdspi_host_init() +{ + return ESP_OK; +} + +esp_err_t sdspi_host_deinit() +{ + for (size_t i = 0; i < sizeof(s_slots)/sizeof(s_slots[0]); ++i) { + if (s_slots[i].handle) { + spi_bus_remove_device(s_slots[i].handle); + free(s_slots[i].block_buf); + s_slots[i].block_buf = NULL; + free(s_slots[i].transactions); + s_slots[i].transactions = NULL; + spi_bus_free((spi_host_device_t) i); + s_slots[i].handle = NULL; + } + } + return ESP_OK; +} + +esp_err_t sdspi_host_set_card_clk(int slot, uint32_t freq_khz) +{ + if (!is_valid_slot(slot)) { + return ESP_ERR_INVALID_ARG; + } + if (!is_slot_initialized(slot)) { + return ESP_ERR_INVALID_STATE; + } + ESP_LOGD(TAG, "Setting card clock to %d kHz", freq_khz); + return init_spi_dev(slot, freq_khz * 1000); +} + +esp_err_t sdspi_host_init_slot(int slot, const sdspi_slot_config_t* slot_config) +{ + ESP_LOGD(TAG, "%s: SPI%d miso=%d mosi=%d sck=%d cs=%d cd=%d wp=%d, dma_ch=%d", + __func__, slot + 1, + slot_config->gpio_miso, slot_config->gpio_mosi, + slot_config->gpio_sck, slot_config->gpio_cs, + slot_config->gpio_cd, slot_config->gpio_wp, + slot_config->dma_channel); + + spi_host_device_t host = (spi_host_device_t) slot; + if (!is_valid_slot(slot)) { + return ESP_ERR_INVALID_ARG; + } + + spi_bus_config_t buscfg = { + .miso_io_num = slot_config->gpio_miso, + .mosi_io_num = slot_config->gpio_mosi, + .sclk_io_num = slot_config->gpio_sck, + .quadwp_io_num = -1, + .quadhd_io_num = -1 + }; + + // Initialize SPI bus + esp_err_t ret = spi_bus_initialize((spi_host_device_t)slot, &buscfg, + slot_config->dma_channel); + if (ret != ESP_OK) { + ESP_LOGD(TAG, "spi_bus_initialize failed with rc=0x%x", ret); + return ret; + } + + // Attach the SD card to the SPI bus + ret = init_spi_dev(slot, SDMMC_FREQ_PROBING * 1000); + if (ret != ESP_OK) { + ESP_LOGD(TAG, "spi_bus_add_device failed with rc=0x%x", ret); + spi_bus_free(host); + return ret; + } + + // Configure CS pin + s_slots[slot].gpio_cs = (uint8_t) slot_config->gpio_cs; + gpio_config_t io_conf = { + .intr_type = GPIO_PIN_INTR_DISABLE, + .mode = GPIO_MODE_OUTPUT, + .pin_bit_mask = 1LL << slot_config->gpio_cs, + }; + + ret = gpio_config(&io_conf); + if (ret != ESP_OK) { + ESP_LOGD(TAG, "gpio_config (CS) failed with rc=0x%x", ret); + spi_bus_remove_device(spi_handle(slot)); + s_slots[slot].handle = NULL; + spi_bus_free(host); + return ret; + } + cs_high(slot); + + // Configure CD and WP pins + io_conf = (gpio_config_t) { + .intr_type = GPIO_PIN_INTR_DISABLE, + .mode = GPIO_MODE_OUTPUT, + .pin_bit_mask = 0, + .pull_up_en = true + }; + if (slot_config->gpio_cd != SDSPI_SLOT_NO_CD) { + io_conf.pin_bit_mask |= (1 << slot_config->gpio_cd); + s_slots[slot].gpio_wp = slot_config->gpio_wp; + } else { + s_slots[slot].gpio_wp = GPIO_UNUSED; + } + + if (slot_config->gpio_wp != SDSPI_SLOT_NO_WP) { + io_conf.pin_bit_mask |= (1 << slot_config->gpio_wp); + s_slots[slot].gpio_cd = slot_config->gpio_cd; + } else { + s_slots[slot].gpio_cd = GPIO_UNUSED; + } + + if (io_conf.pin_bit_mask != 0) { + ret = gpio_config(&io_conf); + if (ret != ESP_OK) { + ESP_LOGD(TAG, "gpio_config (CD/WP) failed with rc=0x%x", ret); + spi_bus_remove_device(spi_handle(slot)); + s_slots[slot].handle = NULL; + spi_bus_free(host); + return ret; + } + } + + s_slots[slot].transactions = calloc(SDSPI_TRANSACTION_COUNT, sizeof(spi_transaction_t)); + if (s_slots[slot].transactions == NULL) { + spi_bus_remove_device(spi_handle(slot)); + s_slots[slot].handle = NULL; + spi_bus_free(host); + return ESP_ERR_NO_MEM; + } + + return ESP_OK; +} + + +esp_err_t sdspi_host_start_command(int slot, sdspi_hw_cmd_t *cmd, void *data, + uint32_t data_size, int flags) +{ + if (!is_valid_slot(slot)) { + return ESP_ERR_INVALID_ARG; + } + if (!is_slot_initialized(slot)) { + return ESP_ERR_INVALID_STATE; + } + if (card_missing(slot)) { + return ESP_ERR_NOT_FOUND; + } + // save some parts of cmd, as its contents will be overwritten + int cmd_index = cmd->cmd_index; + uint32_t cmd_arg; + memcpy(&cmd_arg, cmd->arguments, sizeof(cmd_arg)); + cmd_arg = __builtin_bswap32(cmd_arg); + ESP_LOGV(TAG, "%s: slot=%i, CMD%d, arg=0x%08x flags=0x%x, data=%p, data_size=%i crc=0x%02x", + __func__, slot, cmd_index, cmd_arg, flags, data, data_size, cmd->crc7); + + + // For CMD0, clock out 80 cycles to help the card enter idle state, + // *before* CS is asserted. + if (cmd_index == MMC_GO_IDLE_STATE) { + go_idle_clockout(slot); + } + // actual transaction + esp_err_t ret = ESP_OK; + cs_low(slot); + if (flags & SDSPI_CMD_FLAG_DATA) { + if (flags & SDSPI_CMD_FLAG_WRITE) { + ret = start_command_write_blocks(slot, cmd, data, data_size); + } else { + ret = start_command_read_blocks(slot, cmd, data, data_size); + } + } else { + ret = start_command_default(slot, flags, cmd); + } + cs_high(slot); + + release_bus(slot); + + if (ret != ESP_OK) { + ESP_LOGE(TAG, "%s: cmd=%d error=0x%x", __func__, cmd_index, ret); + } else { + // Update internal state when some commands are sent successfully + if (cmd_index == SD_CRC_ON_OFF) { + s_slots[slot].data_crc_enabled = (uint8_t) cmd_arg; + ESP_LOGD(TAG, "data CRC set=%d", s_slots[slot].data_crc_enabled); + } + } + return ret; +} + +static esp_err_t start_command_default(int slot, int flags, sdspi_hw_cmd_t *cmd) +{ + size_t cmd_size = SDSPI_CMD_R1_SIZE; + if (flags & SDSPI_CMD_FLAG_RSP_R1) { + cmd_size = SDSPI_CMD_R1_SIZE; + } else if (flags & SDSPI_CMD_FLAG_RSP_R2) { + cmd_size = SDSPI_CMD_R2_SIZE; + } else if (flags & SDSPI_CMD_FLAG_RSP_R3) { + cmd_size = SDSPI_CMD_R3_SIZE; + } else if (flags & SDSPI_CMD_FLAG_RSP_R7) { + cmd_size = SDSPI_CMD_R7_SIZE; + } + spi_transaction_t t = { + .flags = 0, + .length = cmd_size * 8, + .tx_buffer = cmd, + .rx_buffer = cmd + }; + esp_err_t ret = spi_device_transmit(spi_handle(slot), &t); + return ret; +} + +// Wait until MISO goes high +static esp_err_t poll_busy(int slot, spi_transaction_t* t) +{ + uint8_t t_rx; + uint8_t rd_data; + *t = (spi_transaction_t) { + .tx_buffer = &t_rx, + .rx_buffer = &rd_data, + .length = 8, + }; + esp_err_t ret; + + for (int i = 0; i < SDSPI_RETRY_COUNT; i++) { + t_rx = SDSPI_MOSI_IDLE_VAL; + rd_data = 0; + ret = spi_device_transmit(spi_handle(slot), t); + if (ret != ESP_OK) { + return ret; + } + if (rd_data != 0) { + if (i < SDSPI_RETRY_COUNT - 2) { + i = SDSPI_RETRY_COUNT - 2; + } + } + } + return ESP_OK; +} + +// Wait for response token +static esp_err_t poll_response_token(int slot, spi_transaction_t* t) +{ + uint8_t t_rx; + uint8_t rd_data; + *t = (spi_transaction_t) { + .tx_buffer = &t_rx, + .rx_buffer = &rd_data, + .length = 8, + }; + esp_err_t ret; + + for (int retry = 0; retry < SDSPI_RETRY_COUNT; retry++) { + t_rx = SDSPI_MOSI_IDLE_VAL; + rd_data = 0; + ret = spi_device_transmit(spi_handle(slot), t); + if (ret != ESP_OK) { + return ret; + } + if ((rd_data & TOKEN_RSP_MASK) == TOKEN_RSP_OK) { + break; + } + if ((rd_data & TOKEN_RSP_MASK) == TOKEN_RSP_CRC_ERR) { + return ESP_ERR_INVALID_CRC; + } + if ((rd_data & TOKEN_RSP_MASK) == TOKEN_RSP_WRITE_ERR) { + return ESP_ERR_INVALID_RESPONSE; + } + if (retry == SDSPI_RETRY_COUNT - 1) { + return ESP_ERR_TIMEOUT; + } + } + + return ESP_OK; +} + +// Wait for data token, reading 8 bytes at a time. +// If the token is found, write all subsequent bytes to extra_ptr, +// and store the number of bytes written to extra_size. +static esp_err_t poll_data_token(int slot, spi_transaction_t* t, + uint8_t* extra_ptr, size_t* extra_size) +{ + uint8_t t_rx[8]; + *t = (spi_transaction_t) { + .tx_buffer = &t_rx, + .rx_buffer = &t_rx, + .length = sizeof(t_rx) * 8, + }; + esp_err_t ret; + for (int retry = 0; retry < SDSPI_RETRY_COUNT; retry++) { + memset(t_rx, SDSPI_MOSI_IDLE_VAL, sizeof(t_rx)); + ret = spi_device_transmit(spi_handle(slot), t); + if (ret != ESP_OK) { + return ret; + } + bool found = false; + for (int byte_idx = 0; byte_idx < sizeof(t_rx); byte_idx++) { + uint8_t rd_data = t_rx[byte_idx]; + if (rd_data == TOKEN_BLOCK_START) { + found = true; + memcpy(extra_ptr, t_rx + byte_idx + 1, sizeof(t_rx) - byte_idx - 1); + *extra_size = sizeof(t_rx) - byte_idx - 1; + break; + } + if (rd_data != 0xff && rd_data != 0) { + ESP_LOGD(TAG, "%s: received 0x%02x while waiting for data", + __func__, rd_data); + return ESP_ERR_INVALID_RESPONSE; + } + } + if (found) { + break; + } + if (retry == SDSPI_RETRY_COUNT - 1) { + return ESP_ERR_TIMEOUT; + } + } + return ESP_OK; +} + + +/** + * Receiving one or more blocks of data happens as follows: + * 1. send command + receive r1 response (SDSPI_CMD_R1_SIZE bytes total) + * 2. keep receiving bytes until TOKEN_BLOCK_START is encountered (this may + * take a while, depending on card's read speed) + * 3. receive up to SDSPI_MAX_DATA_LEN = 512 bytes of actual data + * 4. receive 2 bytes of CRC + * 5. for multi block transfers, go to step 2 + * + * These steps can be done separately, but that leads to a less than optimal + * performance on large transfers because of delays between each step. + * For example, if steps 3 and 4 are separate SPI transactions queued one after + * another, there will be ~16 microseconds of dead time between end of step 3 + * and the beginning of step 4. A delay between two blocking SPI transactions + * in step 2 is even higher (~60 microseconds). + * + * To improve read performance the following sequence is adopted: + * 1. Do the first transfer: command + r1 response + 8 extra bytes. + * Set pre_scan_data_ptr to point to the 8 extra bytes, and set + * pre_scan_data_size to 8. + * 2. Search pre_scan_data_size bytes for TOKEN_BLOCK_START. + * If found, the rest of the bytes contain part of the actual data. + * Store pointer to and size of that extra data as extra_data_{ptr,size}. + * If not found, fall back to polling for TOKEN_BLOCK_START, 8 bytes at a + * time (in poll_data_token function). Deal with extra data in the same way, + * by setting extra_data_{ptr,size}. + * 3. Receive the remaining 512 - extra_data_size bytes, plus 4 extra bytes + * (i.e. 516 - extra_data_size). Of the 4 extra bytes, first two will capture + * the CRC value, and the other two will capture 0xff 0xfe sequence + * indicating the start of the next block. Actual scanning is done by + * setting pre_scan_data_ptr to point to these last 2 bytes, and setting + * pre_scan_data_size = 2, then going to step 2 to receive the next block. + * + * With this approach the delay between blocks of a multi-block transfer is + * ~95 microseconds, out of which 35 microseconds are spend doing the CRC check. + * Further speedup is possible by pipelining transfers and CRC checks, at an + * expense of one extra temporary buffer. + */ +static esp_err_t start_command_read_blocks(int slot, sdspi_hw_cmd_t *cmd, + uint8_t *data, uint32_t rx_length) +{ + bool need_stop_command = rx_length > SDSPI_MAX_DATA_LEN; + spi_transaction_t* t_command = get_transaction(slot); + *t_command = (spi_transaction_t) { + .length = (SDSPI_CMD_R1_SIZE + 8) * 8, + .tx_buffer = cmd, + .rx_buffer = cmd, + }; + esp_err_t ret = spi_device_transmit(spi_handle(slot), t_command); + if (ret != ESP_OK) { + return ret; + } + release_transaction(slot); + + uint8_t* cmd_u8 = (uint8_t*) cmd; + size_t pre_scan_data_size = 8; + uint8_t* pre_scan_data_ptr = cmd_u8 + SDSPI_CMD_R1_SIZE; + + + while (rx_length > 0) { + size_t extra_data_size = 0; + const uint8_t* extra_data_ptr = NULL; + bool need_poll = true; + + for (int i = 0; i < pre_scan_data_size; ++i) { + if (pre_scan_data_ptr[i] == TOKEN_BLOCK_START) { + extra_data_size = pre_scan_data_size - i - 1; + extra_data_ptr = pre_scan_data_ptr + i + 1; + need_poll = false; + break; + } + } + + if (need_poll) { + // Wait for data to be ready + spi_transaction_t* t_poll = get_transaction(slot); + poll_data_token(slot, t_poll, cmd_u8 + SDSPI_CMD_R1_SIZE, &extra_data_size); + if (extra_data_size) { + extra_data_ptr = cmd_u8 + SDSPI_CMD_R1_SIZE; + } + release_transaction(slot); + } + + // Arrange RX buffer + size_t will_receive = MIN(rx_length, SDSPI_MAX_DATA_LEN) - extra_data_size; + uint8_t* rx_data; + ret = get_block_buf(slot, &rx_data); + if (ret != ESP_OK) { + return ret; + } + + // receive actual data + const size_t receive_extra_bytes = 4; + memset(rx_data, 0xff, will_receive + receive_extra_bytes); + spi_transaction_t* t_data = get_transaction(slot); + *t_data = (spi_transaction_t) { + .length = (will_receive + receive_extra_bytes) * 8, + .rx_buffer = rx_data, + .tx_buffer = rx_data + }; + + ret = spi_device_transmit(spi_handle(slot), t_data); + if (ret != ESP_OK) { + return ret; + } + release_transaction(slot); + + // CRC bytes need to be received even if CRC is not enabled + uint16_t crc = UINT16_MAX; + memcpy(&crc, rx_data + will_receive, sizeof(crc)); + + // Bytes to scan for the start token + pre_scan_data_size = receive_extra_bytes - sizeof(crc); + pre_scan_data_ptr = rx_data + will_receive + sizeof(crc); + + // Copy data to the destination buffer + memcpy(data + extra_data_size, rx_data, will_receive); + if (extra_data_size) { + memcpy(data, extra_data_ptr, extra_data_size); + } + + // compute CRC of the received data + uint16_t crc_of_data = 0; + if (data_crc_enabled(slot)) { + crc_of_data = sdspi_crc16(data, will_receive + extra_data_size); + if (crc_of_data != crc) { + ESP_LOGE(TAG, "data CRC failed, got=0x%04x expected=0x%04x", crc_of_data, crc); + esp_log_buffer_hex(TAG, data, 16); + return ESP_ERR_INVALID_CRC; + } + } + + data += will_receive + extra_data_size; + rx_length -= will_receive + extra_data_size; + extra_data_size = 0; + extra_data_ptr = NULL; + } + + if (need_stop_command) { + // To end multi block transfer, send stop command and wait for the + // card to process it + sdspi_hw_cmd_t stop_cmd; + make_hw_cmd(MMC_STOP_TRANSMISSION, 0, &stop_cmd); + ret = start_command_default(slot, SDSPI_CMD_FLAG_RSP_R1, &stop_cmd); + if (ret != ESP_OK) { + return ret; + } + spi_transaction_t* t_poll = get_transaction(slot); + ret = poll_busy(slot, t_poll); + release_transaction(slot); + } + return ESP_OK; +} + +static esp_err_t start_command_write_blocks(int slot, sdspi_hw_cmd_t *cmd, + const uint8_t *data, uint32_t tx_length) +{ + if (card_write_protected(slot)) { + ESP_LOGW(TAG, "%s: card write protected", __func__); + return ESP_ERR_INVALID_STATE; + } + spi_transaction_t* t_command = get_transaction(slot); + *t_command = (spi_transaction_t) { + .length = SDSPI_CMD_R1_SIZE * 8, + .tx_buffer = cmd, + .rx_buffer = cmd, + }; + esp_err_t ret = spi_device_queue_trans(spi_handle(slot), t_command, 0); + if (ret != ESP_OK) { + return ret; + } + uint8_t start_token = tx_length <= SDSPI_MAX_DATA_LEN ? + TOKEN_BLOCK_START : TOKEN_BLOCK_START_WRITE_MULTI; + wait_for_transactions(slot); + + while (tx_length > 0) { + + // Write block start token + spi_transaction_t* t_start_token = get_transaction(slot); + *t_start_token = (spi_transaction_t) { + .length = sizeof(start_token) * 8, + .tx_buffer = &start_token + }; + esp_err_t ret = spi_device_queue_trans(spi_handle(slot), t_start_token, 0); + if (ret != ESP_OK) { + return ret; + } + + // Prepare data to be sent + size_t will_send = MIN(tx_length, SDSPI_MAX_DATA_LEN); + const uint8_t* tx_data = data; + if (!ptr_dma_compatible(tx_data)) { + // If the pointer can't be used with DMA, copy data into a new buffer + uint8_t* tmp; + ret = get_block_buf(slot, &tmp); + if (ret != ESP_OK) { + return ret; + } + memcpy(tmp, tx_data, will_send); + tx_data = tmp; + } + + // Write data + spi_transaction_t* t_data = get_transaction(slot); + *t_data = (spi_transaction_t) { + .length = will_send * 8, + .tx_buffer = tx_data, + }; + ret = spi_device_queue_trans(spi_handle(slot), t_data, 0); + if (ret != ESP_OK) { + return ret; + } + + // Write CRC + uint16_t crc = sdspi_crc16(data, will_send); + spi_transaction_t* t_crc = get_transaction(slot); + *t_crc = (spi_transaction_t) { + .length = sizeof(crc) * 8, + .tx_buffer = (uint8_t*) &crc, + }; + ret = spi_device_queue_trans(spi_handle(slot), t_crc, 0); + if (ret != ESP_OK) { + return ret; + } + + // Wait for data to be sent + wait_for_transactions(slot); + + // Check if R1 response for the command was correct + if (cmd->r1 != 0) { + ESP_LOGD(TAG, "%s: invalid R1 response: 0x%02x", __func__, cmd->r1); + return ESP_ERR_INVALID_RESPONSE; + } + + // Poll for response + spi_transaction_t* t_poll = get_transaction(slot); + ret = poll_response_token(slot, t_poll); + release_transaction(slot); + if (ret != ESP_OK) { + return ret; + } + + // Wait for the card to finish writing data + t_poll = get_transaction(slot); + ret = poll_busy(slot, t_poll); + release_transaction(slot); + if (ret != ESP_OK) { + return ret; + } + + tx_length -= will_send; + data += will_send; + } + + if (start_token == TOKEN_BLOCK_START_WRITE_MULTI) { + uint8_t stop_token[2] = { + TOKEN_BLOCK_STOP_WRITE_MULTI, + SDSPI_MOSI_IDLE_VAL + }; + spi_transaction_t* t_stop_token = get_transaction(slot); + *t_stop_token = (spi_transaction_t) { + .length = sizeof(stop_token) * 8, + .tx_buffer = &stop_token, + }; + ret = spi_device_queue_trans(spi_handle(slot), t_stop_token, 0); + if (ret != ESP_OK) { + return ret; + } + wait_for_transactions(slot); + + spi_transaction_t* t_poll = get_transaction(slot); + ret = poll_busy(slot, t_poll); + release_transaction(slot); + if (ret != ESP_OK) { + return ret; + } + } + + return ESP_OK; +} diff --git a/components/driver/sdspi_private.h b/components/driver/sdspi_private.h new file mode 100644 index 0000000000..d28cfcf88c --- /dev/null +++ b/components/driver/sdspi_private.h @@ -0,0 +1,96 @@ +// Copyright 2015-2017 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include "esp_err.h" +#include "freertos/FreeRTOS.h" +#include "freertos/queue.h" + + +/// Control tokens used to frame data transfers +/// (see section 7.3.3 of SD simplified spec) + +/// Token sent before single/multi block reads and single block writes +#define TOKEN_BLOCK_START 0b11111110 +/// Token sent before multi block writes +#define TOKEN_BLOCK_START_WRITE_MULTI 0b11111100 +/// Token used to stop multi block write (for reads, CMD12 is used instead) +#define TOKEN_BLOCK_STOP_WRITE_MULTI 0b11111101 + +/// Data response tokens + +/// Mask (high 3 bits are undefined for data response tokens) +#define TOKEN_RSP_MASK 0b11111 +/// Data accepted +#define TOKEN_RSP_OK 0b00101 +/// Data rejected due to CRC error +#define TOKEN_RSP_CRC_ERR 0b01011 +/// Data rejected due to write error +#define TOKEN_RSP_WRITE_ERR 0b01101 + + +/// Data error tokens have format 0b0000xyzw where xyzw are signle bit flags. +/// MASK and VAL are used to check if a token is an error token +#define TOKEN_ERR_MASK 0b11110000 +#define TOKEN_ERR_VAL 0b00000000 + +/// Argument is out of range +#define TOKEN_ERR_RANGE BIT(3) +/// Card internal ECC error +#define TOKEN_ERR_CARD_ECC BIT(2) +/// Card controller error +#define TOKEN_ERR_INTERNAL BIT(1) +/// Card is locked +#define TOKEN_ERR_LOCKED BIT(0) + + +/// Transfer format in SPI mode. See section 7.3.1.1 of SD simplified spec. +typedef struct { + // These fields form the command sent from host to the card (6 bytes) + uint8_t cmd_index : 6; + uint8_t transmission_bit : 1; + uint8_t start_bit : 1; + uint8_t arguments[4]; + uint8_t stop_bit : 1; + uint8_t crc7 : 7; + /// Ncr is the dead time between command and response; should be 0xff + uint8_t ncr; + /// Response data, should be set by host to 0xff for read operations + uint8_t r1; + /// Up to 16 bytes of response. Luckily, this is aligned on 4 byte boundary. + uint32_t response[4]; +} sdspi_hw_cmd_t; + +#define SDSPI_CMD_NORESP_SIZE 6 //!< Size of the command without any response +#define SDSPI_CMD_R1_SIZE 8 //!< Size of the command with R1 response +#define SDSPI_CMD_R2_SIZE 9 //!< Size of the command with R1b response +#define SDSPI_CMD_R3_SIZE 12 //!< Size of the command with R3 response +#define SDSPI_CMD_R7_SIZE 12 //!< Size of the command with R7 response + +#define SDSPI_CMD_FLAG_DATA BIT(0) //!< Command has data transfer +#define SDSPI_CMD_FLAG_WRITE BIT(1) //!< Data is written to the card +#define SDSPI_CMD_FLAG_RSP_R1 BIT(2) //!< Response format R1 (1 byte) +#define SDSPI_CMD_FLAG_RSP_R2 BIT(3) //!< Response format R2 (2 bytes) +#define SDSPI_CMD_FLAG_RSP_R3 BIT(4) //!< Response format R3 (5 bytes) +#define SDSPI_CMD_FLAG_RSP_R7 BIT(5) //!< Response format R7 (5 bytes) + +#define SDSPI_MAX_DATA_LEN 512 //!< Max size of single block transfer + +void make_hw_cmd(uint32_t opcode, uint32_t arg, sdspi_hw_cmd_t *hw_cmd); + +esp_err_t sdspi_host_start_command(int slot, sdspi_hw_cmd_t *cmd, + void *data, uint32_t data_size, int flags); diff --git a/components/driver/sdspi_transaction.c b/components/driver/sdspi_transaction.c new file mode 100644 index 0000000000..95f98240f9 --- /dev/null +++ b/components/driver/sdspi_transaction.c @@ -0,0 +1,122 @@ +// Copyright 2015-2017 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include "esp_err.h" +#include "esp_log.h" +#include "sys/lock.h" +#include "soc/sdmmc_reg.h" +#include "soc/sdmmc_struct.h" +#include "driver/sdmmc_types.h" +#include "driver/sdmmc_defs.h" +#include "driver/sdmmc_host.h" +#include "sdspi_private.h" +#include "sdspi_crc.h" + +static const char* TAG = "sdspi_transaction"; + +static _lock_t s_lock; +static bool s_app_cmd; + +static uint8_t sdspi_msg_crc7(sdspi_hw_cmd_t* hw_cmd) +{ + const size_t bytes_to_crc = offsetof(sdspi_hw_cmd_t, arguments) + + sizeof(hw_cmd->arguments); /* can't take address of bit fields */ + return sdspi_crc7((const uint8_t *)hw_cmd, bytes_to_crc); +} + +void make_hw_cmd(uint32_t opcode, uint32_t arg, sdspi_hw_cmd_t *hw_cmd) +{ + hw_cmd->start_bit = 0; + hw_cmd->transmission_bit = 1; + hw_cmd->cmd_index = opcode; + hw_cmd->stop_bit = 1; + hw_cmd->r1 = 0xff; + memset(hw_cmd->response, 0xff, sizeof(hw_cmd->response)); + hw_cmd->ncr = 0xff; + uint32_t arg_s = __builtin_bswap32(arg); + memcpy(hw_cmd->arguments, &arg_s, sizeof(arg_s)); + hw_cmd->crc7 = sdspi_msg_crc7(hw_cmd); +} + +esp_err_t sdspi_host_do_transaction(int slot, sdmmc_command_t *cmdinfo) +{ + _lock_acquire(&s_lock); + // Convert the command to wire format + sdspi_hw_cmd_t hw_cmd; + make_hw_cmd(cmdinfo->opcode, cmdinfo->arg, &hw_cmd); + + // Flags indicate which of the transfer types should be used + int flags = 0; + if (SCF_CMD(cmdinfo->flags) == SCF_CMD_ADTC) { + flags = SDSPI_CMD_FLAG_DATA | SDSPI_CMD_FLAG_WRITE; + } else if (SCF_CMD(cmdinfo->flags) == (SCF_CMD_ADTC | SCF_CMD_READ)) { + flags = SDSPI_CMD_FLAG_DATA; + } + + // In SD host, response format is encoded using SCF_RSP_* flags which come + // as part of sdmmc_command_t from the upper layer (sdmmc_cmd.c). + // SPI mode uses different command formats. In fact, most of the commands + // use R1 response. Therefore, instead of adding another parallel set of + // response flags for the SPI mode, response format is determined here: + if (!s_app_cmd && cmdinfo->opcode == SD_SEND_IF_COND) { + flags |= SDSPI_CMD_FLAG_RSP_R7; + } else if (!s_app_cmd && cmdinfo->opcode == MMC_SEND_STATUS) { + flags |= SDSPI_CMD_FLAG_RSP_R2; + } else if (!s_app_cmd && cmdinfo->opcode == SD_READ_OCR) { + flags |= SDSPI_CMD_FLAG_RSP_R3; + } else if (s_app_cmd && cmdinfo->opcode == SD_APP_SD_STATUS) { + flags |= SDSPI_CMD_FLAG_RSP_R2; + } else { + flags |= SDSPI_CMD_FLAG_RSP_R1; + } + + // Send the command and get the response. + esp_err_t ret = sdspi_host_start_command(slot, &hw_cmd, + cmdinfo->data, cmdinfo->datalen, flags); + + // Extract response bytes and store them into cmdinfo structure + if (ret == ESP_OK) { + ESP_LOGV(TAG, "r1 = 0x%02x hw_cmd.r[0]=0x%08x", hw_cmd.r1, hw_cmd.response[0]); + // Some errors should be reported using return code + if (flags & SDSPI_CMD_FLAG_RSP_R1) { + cmdinfo->response[0] = hw_cmd.r1; + if (hw_cmd.r1 == 0xff) { + // No response received at all + } else if (hw_cmd.r1 & SD_SPI_R1_CMD_CRC_ERR) { + ret = ESP_ERR_INVALID_CRC; + } else if (hw_cmd.r1 & SD_SPI_R1_IDLE_STATE) { + // Idle state is handled at command layer + } else if (hw_cmd.r1 != 0) { + ESP_LOGD(TAG, "Unexpected R1 response: 0x%02x", hw_cmd.r1); + } + } else if (flags & SDSPI_CMD_FLAG_RSP_R2) { + cmdinfo->response[0] = (((uint32_t)hw_cmd.r1) << 8) | (hw_cmd.response[0] >> 24); + } else if (flags & (SDSPI_CMD_FLAG_RSP_R3 | SDSPI_CMD_FLAG_RSP_R7)) { + // Drop r1 response, only copy the other 4 bytes of data + // TODO: can we somehow preserve r1 response and keep upper layer + // same as in SD mode? + cmdinfo->response[0] = __builtin_bswap32(hw_cmd.response[0]); + } + } + + // Save a flag whether the next command is expected to be an app command + if (ret == ESP_OK) { + s_app_cmd = (cmdinfo->opcode == MMC_APP_CMD); + } else { + s_app_cmd = false; + } + _lock_release(&s_lock); + return ret; +} diff --git a/components/fatfs/src/esp_vfs_fat.h b/components/fatfs/src/esp_vfs_fat.h index 1ce0b54d3f..278e427c53 100644 --- a/components/fatfs/src/esp_vfs_fat.h +++ b/components/fatfs/src/esp_vfs_fat.h @@ -18,6 +18,7 @@ #include "driver/gpio.h" #include "driver/sdmmc_types.h" #include "driver/sdmmc_host.h" +#include "driver/sdspi_host.h" #include "ff.h" #include "wear_levelling.h" @@ -98,9 +99,9 @@ typedef esp_vfs_fat_mount_config_t esp_vfs_fat_sdmmc_mount_config_t; * @brief Convenience function to get FAT filesystem on SD card registered in VFS * * This is an all-in-one function which does the following: - * - initializes SD/MMC peripheral with configuration in host_config - * - initializes SD/MMC card with configuration in slot_config - * - mounts FAT partition on SD/MMC card using FATFS library, with configuration in mount_config + * - initializes SDMMC driver or SPI driver with configuration in host_config + * - initializes SD card with configuration in slot_config + * - mounts FAT partition on SD card using FATFS library, with configuration in mount_config * - registers FATFS library with VFS, with prefix given by base_prefix variable * * This function is intended to make example code more compact. @@ -109,8 +110,16 @@ typedef esp_vfs_fat_mount_config_t esp_vfs_fat_sdmmc_mount_config_t; * with proper error checking and handling of exceptional conditions. * * @param base_path path where partition should be registered (e.g. "/sdcard") - * @param host_config pointer to structure describing SDMMC host - * @param slot_config pointer to structure with extra SDMMC slot configuration + * @param host_config Pointer to structure describing SDMMC host. When using + * SDMMC peripheral, this structure can be initialized using + * SDMMC_HOST_DEFAULT() macro. When using SPI peripheral, + * this structure can be initialized using SDSPI_HOST_DEFAULT() + * macro. + * @param slot_config Pointer to structure with slot configuration. + * For SDMMC peripheral, pass a pointer to sdmmc_slot_config_t + * structure initialized using SDMMC_SLOT_CONFIG_DEFAULT. + * For SPI peripheral, pass a pointer to sdspi_slot_config_t + * structure initialized using SDSPI_SLOT_CONFIG_DEFAULT. * @param mount_config pointer to structure with extra parameters for mounting FATFS * @param[out] out_card if not NULL, pointer to the card information structure will be returned via this argument * @return @@ -118,11 +127,11 @@ typedef esp_vfs_fat_mount_config_t esp_vfs_fat_sdmmc_mount_config_t; * - ESP_ERR_INVALID_STATE if esp_vfs_fat_sdmmc_mount was already called * - ESP_ERR_NO_MEM if memory can not be allocated * - ESP_FAIL if partition can not be mounted - * - other error codes from SDMMC host, SDMMC protocol, or FATFS drivers + * - other error codes from SDMMC or SPI drivers, SDMMC protocol, or FATFS drivers */ esp_err_t esp_vfs_fat_sdmmc_mount(const char* base_path, const sdmmc_host_t* host_config, - const sdmmc_slot_config_t* slot_config, + const void* slot_config, const esp_vfs_fat_mount_config_t* mount_config, sdmmc_card_t** out_card); diff --git a/components/fatfs/src/vfs_fat_sdmmc.c b/components/fatfs/src/vfs_fat_sdmmc.c index cb8324289f..a712fa9733 100644 --- a/components/fatfs/src/vfs_fat_sdmmc.c +++ b/components/fatfs/src/vfs_fat_sdmmc.c @@ -18,6 +18,7 @@ #include "esp_vfs.h" #include "esp_vfs_fat.h" #include "driver/sdmmc_host.h" +#include "driver/sdspi_host.h" #include "sdmmc_cmd.h" #include "diskio.h" @@ -28,8 +29,8 @@ static char * s_base_path = NULL; esp_err_t esp_vfs_fat_sdmmc_mount(const char* base_path, const sdmmc_host_t* host_config, - const sdmmc_slot_config_t* slot_config, - const esp_vfs_fat_sdmmc_mount_config_t* mount_config, + const void* slot_config, + const esp_vfs_fat_mount_config_t* mount_config, sdmmc_card_t** out_card) { const size_t workbuf_size = 4096; @@ -52,19 +53,29 @@ esp_err_t esp_vfs_fat_sdmmc_mount(const char* base_path, ESP_LOGD(TAG, "could not copy base_path"); return ESP_ERR_NO_MEM; } + esp_err_t err = ESP_OK; + s_card = malloc(sizeof(sdmmc_card_t)); + if (s_card == NULL) { + err = ESP_ERR_NO_MEM; + goto fail; + } - // enable SDMMC - sdmmc_host_init(); - - // enable card slot - esp_err_t err = sdmmc_host_init_slot(host_config->slot, slot_config); + err = (*host_config->init)(); if (err != ESP_OK) { - return err; + ESP_LOGD(TAG, "host init returned rc=0x%x", err); + goto fail; } - s_card = malloc(sizeof(sdmmc_card_t)); - if (s_card == NULL) { - err = ESP_ERR_NO_MEM; + // configure SD slot + if (host_config->flags == SDMMC_HOST_FLAG_SPI) { + err = sdspi_host_init_slot(host_config->slot, + (const sdspi_slot_config_t*) slot_config); + } else { + err = sdmmc_host_init_slot(host_config->slot, + (const sdmmc_slot_config_t*) slot_config); + } + if (err != ESP_OK) { + ESP_LOGD(TAG, "slot_config returned rc=0x%x", err); goto fail; } diff --git a/components/sdmmc/test/test_sd.c b/components/sdmmc/test/test_sd.c index 768fecc258..8d371f67e9 100644 --- a/components/sdmmc/test/test_sd.c +++ b/components/sdmmc/test/test_sd.c @@ -18,6 +18,7 @@ #include "unity.h" #include "driver/gpio.h" #include "driver/sdmmc_host.h" +#include "driver/sdspi_host.h" #include "driver/sdmmc_defs.h" #include "sdmmc_cmd.h" #include "esp_log.h" @@ -30,16 +31,29 @@ TEST_CASE("can probe SD", "[sd][ignore]") { sdmmc_host_t config = SDMMC_HOST_DEFAULT(); sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT(); - sdmmc_host_init(); - sdmmc_host_init_slot(SDMMC_HOST_SLOT_1, &slot_config); + TEST_ESP_OK(sdmmc_host_init()); + TEST_ESP_OK(sdmmc_host_init_slot(SDMMC_HOST_SLOT_1, &slot_config)); sdmmc_card_t* card = malloc(sizeof(sdmmc_card_t)); TEST_ASSERT_NOT_NULL(card); TEST_ESP_OK(sdmmc_card_init(&config, card)); sdmmc_card_print_info(stdout, card); - sdmmc_host_deinit(); + TEST_ESP_OK(sdmmc_host_deinit()); free(card); } +TEST_CASE("can probe SD (using SPI)", "[sdspi][ignore]") +{ + sdmmc_host_t config = SDSPI_HOST_DEFAULT(); + sdspi_slot_config_t slot_config = SDSPI_SLOT_CONFIG_DEFAULT(); + TEST_ESP_OK(sdspi_host_init()); + TEST_ESP_OK(sdspi_host_init_slot(config.slot, &slot_config)); + sdmmc_card_t* card = malloc(sizeof(sdmmc_card_t)); + TEST_ASSERT_NOT_NULL(card); + TEST_ESP_OK(sdmmc_card_init(&config, card)); + sdmmc_card_print_info(stdout, card); + TEST_ESP_OK(sdspi_host_deinit()); + free(card); +} static void do_single_write_read_test(sdmmc_card_t* card, size_t start_block, size_t block_count) @@ -66,7 +80,7 @@ static void do_single_write_read_test(sdmmc_card_t* card, gettimeofday(&t_stop_rd, NULL); float time_rd = 1e3f * (t_stop_rd.tv_sec - t_start_rd.tv_sec) + 1e-3f * (t_stop_rd.tv_usec - t_start_rd.tv_usec); - printf(" | %6.2f | %.2f | %.2fs | %.2f\n", + printf(" | %6.2f | %.2f | %.2f | %.2f\n", time_wr, total_size / (time_wr / 1000) / (1024 * 1024), time_rd, total_size / (time_rd / 1000) / (1024 * 1024)); srand(start_block); @@ -76,16 +90,8 @@ static void do_single_write_read_test(sdmmc_card_t* card, free(buffer); } -TEST_CASE("can write and read back blocks", "[sd][ignore]") +static void read_write_test(sdmmc_card_t* card) { - sdmmc_host_t config = SDMMC_HOST_DEFAULT(); - config.max_freq_khz = SDMMC_FREQ_HIGHSPEED; - sdmmc_host_init(); - sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT(); - sdmmc_host_init_slot(SDMMC_HOST_SLOT_1, &slot_config); - sdmmc_card_t* card = malloc(sizeof(sdmmc_card_t)); - TEST_ASSERT_NOT_NULL(card); - TEST_ESP_OK(sdmmc_card_init(&config, card)); sdmmc_card_print_info(stdout, card); printf(" sector | count | size(kB) | wr_time(ms) | wr_speed(MB/s) | rd_time(ms) | rd_speed(MB/s)\n"); do_single_write_read_test(card, 0, 1); @@ -104,6 +110,35 @@ TEST_CASE("can write and read back blocks", "[sd][ignore]") do_single_write_read_test(card, card->csd.capacity/2, 32); do_single_write_read_test(card, card->csd.capacity/2, 64); do_single_write_read_test(card, card->csd.capacity/2, 128); +} + +TEST_CASE("can write and read back blocks", "[sd][ignore]") +{ + sdmmc_host_t config = SDMMC_HOST_DEFAULT(); + config.max_freq_khz = SDMMC_FREQ_HIGHSPEED; + TEST_ESP_OK(sdmmc_host_init()); + sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT(); + TEST_ESP_OK(sdmmc_host_init_slot(SDMMC_HOST_SLOT_1, &slot_config)); + sdmmc_card_t* card = malloc(sizeof(sdmmc_card_t)); + TEST_ASSERT_NOT_NULL(card); + TEST_ESP_OK(sdmmc_card_init(&config, card)); + read_write_test(card); + free(card); + TEST_ESP_OK(sdmmc_host_deinit()); +} + +TEST_CASE("can write and read back blocks (using SPI)", "[sdspi][ignore]") +{ + sdmmc_host_t config = SDSPI_HOST_DEFAULT(); + config.max_freq_khz = SDMMC_FREQ_HIGHSPEED; + sdspi_slot_config_t slot_config = SDSPI_SLOT_CONFIG_DEFAULT(); + TEST_ESP_OK(sdspi_host_init()); + TEST_ESP_OK(sdspi_host_init_slot(config.slot, &slot_config)); + sdmmc_card_t* card = malloc(sizeof(sdmmc_card_t)); + TEST_ASSERT_NOT_NULL(card); + TEST_ESP_OK(sdmmc_card_init(&config, card)); + read_write_test(card); free(card); - sdmmc_host_deinit(); + TEST_ESP_OK(sdspi_host_deinit()); } + diff --git a/docs/api-reference/storage/sdmmc.rst b/docs/api-reference/storage/sdmmc.rst index 4ea2f62606..ef53263850 100644 --- a/docs/api-reference/storage/sdmmc.rst +++ b/docs/api-reference/storage/sdmmc.rst @@ -10,7 +10,9 @@ SDMMC peripheral supports SD and MMC memory cards and SDIO cards. SDMMC software 2. SDMMC protocol layer (``sdmmc_cmd.h``) — this component handles specifics of SD protocol such as card initialization and data transfer commands. Despite the name, only SD (SDSC/SDHC/SDXC) cards are supported at the moment. Support for MCC/eMMC cards can be added in the future. -Protocol layer works with the host via ``sdmmc_host_t`` structure. This structure contains pointers to various functions of the host. This design makes it possible to implement an SD host using SPI interface later. +Protocol layer works with the host via ``sdmmc_host_t`` structure. This structure contains pointers to various functions of the host. + +In addition to SDMMC Host peripheral, ESP32 has SPI peripherals which can also be used to work with SD cards. This is supported using a variant of the host driver, ``driver/sdspi_host.h``. This driver has the same interface as SDMMC host driver, and the protocol layer can use either of two. Application Example ------------------- @@ -94,3 +96,30 @@ Of all the funtions listed below, only ``sdmmc_host_init``, ``sdmmc_host_init_sl .. doxygenfunction:: sdmmc_host_set_card_clk .. doxygenfunction:: sdmmc_host_do_transaction .. doxygenfunction:: sdmmc_host_deinit + +SD SPI driver APIs +------------------ + +SPI controllers accessible via spi_master driver (HSPI, VSPI) can be used to work with SD cards. In SPI mode, SD driver has lower throughput than in 1-line SD mode. However SPI mode makes pin selection more flexible, as SPI peripheral can be connected to any ESP32 pins using GPIO Matrix. SD SPI driver uses software controlled CS signal. Currently SD SPI driver assumes that it can use the SPI controller exclusively, so applications which need to share SPI bus between SD cards and other peripherals need to make sure that SD card and other devices are not used at the same time from different tasks. + +SD SPI driver is represented using an ``sdmmc_host_t`` structure initialized using ``SDSPI_HOST_DEFAULT`` macro. For slot initialization, ``SDSPI_SLOT_CONFIG_DEFAULT`` can be used to fill in default pin mapping, which is the same as the pin mapping in SD mode. + +SD SPI driver APIs are very similar to SDMMC host APIs. As with the SDMMC host driver, only ``sdspi_host_init``, ``sdspi_host_init_slot``, and ``sdspi_host_deinit`` functions are normally used by the applications. Other functions are called by the protocol level driver via function pointers in ``sdmmc_host_t` structure. + +.. doxygenfunction:: sdspi_host_init + +.. doxygendefine:: SDSPI_HOST_DEFAULT + +.. doxygenfunction:: sdspi_host_init_slot + +.. doxygenstruct:: sdspi_slot_config_t + :members: + +.. doxygendefine:: SDSPI_SLOT_NO_CD +.. doxygendefine:: SDSPI_SLOT_NO_WP +.. doxygendefine:: SDSPI_SLOT_CONFIG_DEFAULT + +.. doxygenfunction:: sdspi_host_set_bus_width +.. doxygenfunction:: sdspi_host_set_card_clk +.. doxygenfunction:: sdspi_host_do_transaction +.. doxygenfunction:: sdspi_host_deinit diff --git a/examples/storage/sd_card/README.md b/examples/storage/sd_card/README.md index 47fb599254..341f3fe125 100644 --- a/examples/storage/sd_card/README.md +++ b/examples/storage/sd_card/README.md @@ -18,19 +18,21 @@ This example demonstrates how to use an SD card with ESP32. Example does the fol To run this example, ESP32 development board needs to be connected to SD card as follows: -ESP32 pin | SD card pin | Notes ---------------|-------------|------------ -GPIO14 (MTMS) | CLK | 10k pullup -GPIO15 (MTDO) | CMD | 10k pullup -GPIO2 | D0 | 10k pullup, pull low to go into download mode -GPIO4 | D1 | 10k pullup; not used in 1-line mode -GPIO12 (MTDI) | D2 | otherwise 10k pullup (see note below!); not used in 1-line mode -GPIO13 (MTCK) | D3 | 10k pullup needed at card side, even in 1-line mode -N/C | CD | -N/C | WP | +ESP32 pin | SD card pin | SPI pin | Notes +--------------|-------------|---------|------------ +GPIO14 (MTMS) | CLK | SCK | 10k pullup in SD mode +GPIO15 (MTDO) | CMD | MOSI | 10k pullup, both in SD and SPI modes +GPIO2 | D0 | MISO | 10k pullup in SD mode, pull low to go into download mode (see note below!) +GPIO4 | D1 | N/C | not used in 1-line SD mode; 10k pullup in 4-line SD mode +GPIO12 (MTDI) | D2 | N/C | not used in 1-line SD mode; 10k pullup in 4-line SD mode (see note below!) +GPIO13 (MTCK) | D3 | CS | not used in 1-line SD mode, but card's D3 pin must have a 10k pullup +N/C | CD | | optional, not used in the example +N/C | WP | | optional, not used in the example This example doesn't utilize card detect (CD) and write protect (WP) signals from SD card slot. +With the given pinout for SPI mode, same connections between the SD card and ESP32 can be used to test both SD and SPI modes, provided that the appropriate pullups are in place. In SPI mode, pins can be customized. See the initialization of ``sdspi_slot_config_t`` structure in the example code. + ### Note about GPIO2 GPIO2 pin is used as a bootstrapping pin, and should be low to enter UART download mode. One way to do this is to connect GPIO0 and GPIO2 using a jumper, and then the auto-reset circuit on most development boards will pull GPIO2 low along with GPIO0, when entering download mode. @@ -63,9 +65,12 @@ By default, example code uses the following initializer for SDMMC host periphera sdmmc_host_t host = SDMMC_HOST_DEFAULT(); ``` -Among other things, this sets `host.flags` to `SDMMC_HOST_FLAG_4BIT`, which means that SD/MMC driver will switch to 4-line mode when initializing the card (initial communication always happens in 1-line mode). If some of D1, D2, D3 pins are not connected to the card, set `host.flags` to `SDMMC_HOST_FLAG_1BIT` — then the SD/MMC driver will not attempt to switch to 4-line mode. -Note that even if D3 line is not connected to the ESP32, it still has to be pulled up at card side, otherwise the card will go into SPI protocol mode. +Among other things, this sets `host.flags` to `SDMMC_HOST_FLAG_4BIT`, which means that SD/MMC driver will switch to 4-line mode when initializing the card (initial communication always happens in 1-line mode). If some of the card's D1, D2, D3 pins are not connected to the ESP32, set `host.flags` to `SDMMC_HOST_FLAG_1BIT` — then the SD/MMC driver will not attempt to switch to 4-line mode. +Note that even if card's D3 line is not connected to the ESP32, it still has to be pulled up, otherwise the card will go into SPI protocol mode. + +## SPI mode +By default, the example uses SDMMC Host peripheral to access SD card. To use SPI peripheral instead, uncomment ``#define USE_SPI_MODE`` in the example code. ## Example output diff --git a/examples/storage/sd_card/main/sd_card_example_main.c b/examples/storage/sd_card/main/sd_card_example_main.c index 03d0ec1310..975fbc4b0c 100644 --- a/examples/storage/sd_card/main/sd_card_example_main.c +++ b/examples/storage/sd_card/main/sd_card_example_main.c @@ -14,15 +14,37 @@ #include "esp_log.h" #include "esp_vfs_fat.h" #include "driver/sdmmc_host.h" -#include "driver/sdmmc_defs.h" +#include "driver/sdspi_host.h" #include "sdmmc_cmd.h" -static const char* TAG = "example"; +static const char *TAG = "example"; + +// This example can use SDMMC and SPI peripherals to communicate with SD card. +// By default, SDMMC peripheral is used. +// To enable SPI mode, uncomment the following line: + +// #define USE_SPI_MODE + +// When testing SD and SPI modes, keep in mind that once the card has been +// initialized in SPI mode, it can not be reinitialized in SD mode without +// toggling power to the card. + +#ifdef USE_SPI_MODE +// Pin mapping when using SPI mode. +// With this mapping, SD card can be used both in SPI and 1-line SD mode. +// Note that a pull-up on CS line is required in SD mode. +#define PIN_NUM_MISO 2 +#define PIN_NUM_MOSI 15 +#define PIN_NUM_CLK 14 +#define PIN_NUM_CS 13 +#endif //USE_SPI_MODE void app_main(void) { ESP_LOGI(TAG, "Initializing SD card"); +#ifndef USE_SPI_MODE + ESP_LOGI(TAG, "Using SDMMC peripheral"); sdmmc_host_t host = SDMMC_HOST_DEFAULT(); // To use 1-line SD mode, uncomment the following line: @@ -32,9 +54,22 @@ void app_main(void) // Modify slot_config.gpio_cd and slot_config.gpio_wp if your board has these signals. sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT(); +#else + ESP_LOGI(TAG, "Using SPI peripheral"); + + sdmmc_host_t host = SDSPI_HOST_DEFAULT(); + sdspi_slot_config_t slot_config = SDSPI_SLOT_CONFIG_DEFAULT(); + slot_config.gpio_miso = PIN_NUM_MISO; + slot_config.gpio_mosi = PIN_NUM_MOSI; + slot_config.gpio_sck = PIN_NUM_CLK; + slot_config.gpio_cs = PIN_NUM_CS; + // This initializes the slot without card detect (CD) and write protect (WP) signals. + // Modify slot_config.gpio_cd and slot_config.gpio_wp if your board has these signals. +#endif //USE_SPI_MODE + // Options for mounting the filesystem. - // If format_if_mount_failed is set to true, SD card will be partitioned and formatted - // in case when mounting fails. + // If format_if_mount_failed is set to true, SD card will be partitioned and + // formatted in case when mounting fails. esp_vfs_fat_sdmmc_mount_config_t mount_config = { .format_if_mount_failed = false, .max_files = 5 @@ -46,11 +81,14 @@ void app_main(void) // production applications. sdmmc_card_t* card; esp_err_t ret = esp_vfs_fat_sdmmc_mount("/sdcard", &host, &slot_config, &mount_config, &card); + if (ret != ESP_OK) { if (ret == ESP_FAIL) { - ESP_LOGE(TAG, "Failed to mount filesystem. If you want the card to be formatted, set format_if_mount_failed = true."); + ESP_LOGE(TAG, "Failed to mount filesystem. " + "If you want the card to be formatted, set format_if_mount_failed = true."); } else { - ESP_LOGE(TAG, "Failed to initialize the card (%d). Make sure SD card lines have pull-up resistors in place.", ret); + ESP_LOGE(TAG, "Failed to initialize the card (%d). " + "Make sure SD card lines have pull-up resistors in place.", ret); } return; } @@ -101,7 +139,7 @@ void app_main(void) } ESP_LOGI(TAG, "Read from file: '%s'", line); - // All done, unmount partition and disable SDMMC host peripheral + // All done, unmount partition and disable SDMMC or SPI peripheral esp_vfs_fat_sdmmc_unmount(); ESP_LOGI(TAG, "Card unmounted"); }