| /* |
| * Copyright 2022 NXP |
| * |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| |
| |
| #define DT_DRV_COMPAT nxp_imx_usdhc |
| |
| #include <zephyr/kernel.h> |
| #include <zephyr/devicetree.h> |
| #include <zephyr/drivers/sdhc.h> |
| #include <zephyr/sd/sd_spec.h> |
| #include <zephyr/drivers/clock_control.h> |
| #include <zephyr/drivers/gpio.h> |
| #include <zephyr/logging/log.h> |
| #include <soc.h> |
| #include <zephyr/drivers/pinctrl.h> |
| #define PINCTRL_STATE_SLOW PINCTRL_STATE_PRIV_START |
| #define PINCTRL_STATE_MED (PINCTRL_STATE_PRIV_START + 1U) |
| #define PINCTRL_STATE_FAST (PINCTRL_STATE_PRIV_START + 2U) |
| #define PINCTRL_STATE_NOPULL (PINCTRL_STATE_PRIV_START + 3U) |
| |
| LOG_MODULE_REGISTER(usdhc, CONFIG_SDHC_LOG_LEVEL); |
| |
| #include <fsl_usdhc.h> |
| #include <fsl_cache.h> |
| #include <zephyr/irq.h> |
| |
| enum transfer_callback_status { |
| TRANSFER_CMD_COMPLETE = BIT(0), |
| TRANSFER_CMD_FAILED = BIT(1), |
| TRANSFER_DATA_COMPLETE = BIT(2), |
| TRANSFER_DATA_FAILED = BIT(3), |
| }; |
| |
| #define TRANSFER_CMD_FLAGS (TRANSFER_CMD_COMPLETE | TRANSFER_CMD_FAILED) |
| #define TRANSFER_DATA_FLAGS (TRANSFER_DATA_COMPLETE | TRANSFER_DATA_FAILED) |
| |
| /* USDHC tuning constants */ |
| #define IMX_USDHC_STANDARD_TUNING_START (10U) |
| #define IMX_USDHC_TUNING_STEP (2U) |
| #define IMX_USDHC_STANDARD_TUNING_COUNTER (60U) |
| /* Default transfer timeout in ms for tuning */ |
| #define IMX_USDHC_DEFAULT_TIMEOUT (5000U) |
| |
| struct usdhc_host_transfer { |
| usdhc_transfer_t *transfer; |
| k_timeout_t command_timeout; |
| k_timeout_t data_timeout; |
| }; |
| |
| struct usdhc_config { |
| USDHC_Type *base; |
| const struct device *clock_dev; |
| clock_control_subsys_t clock_subsys; |
| uint8_t nusdhc; |
| const struct gpio_dt_spec pwr_gpio; |
| const struct gpio_dt_spec detect_gpio; |
| bool detect_dat3; |
| bool detect_cd; |
| bool no_180_vol; |
| uint32_t data_timeout; |
| uint32_t read_watermark; |
| uint32_t write_watermark; |
| uint32_t max_current_330; |
| uint32_t max_current_300; |
| uint32_t max_current_180; |
| uint32_t power_delay_ms; |
| uint32_t min_bus_freq; |
| uint32_t max_bus_freq; |
| bool mmc_hs200_1_8v; |
| bool mmc_hs400_1_8v; |
| const struct pinctrl_dev_config *pincfg; |
| void (*irq_config_func)(const struct device *dev); |
| }; |
| |
| struct usdhc_data { |
| const struct device *dev; |
| struct sdhc_host_props props; |
| bool card_present; |
| struct k_sem transfer_sem; |
| volatile uint32_t transfer_status; |
| usdhc_handle_t transfer_handle; |
| struct sdhc_io host_io; |
| struct k_mutex access_mutex; |
| sdhc_interrupt_cb_t sdhc_cb; |
| struct gpio_callback cd_callback; |
| void *sdhc_cb_user_data; |
| uint8_t usdhc_rx_dummy[128] __aligned(32); |
| #ifdef CONFIG_IMX_USDHC_DMA_SUPPORT |
| uint32_t *usdhc_dma_descriptor; /* ADMA descriptor table (noncachable) */ |
| uint32_t dma_descriptor_len; /* DMA descriptor table length in words */ |
| #endif |
| }; |
| |
| static void transfer_complete_cb(USDHC_Type *usdhc, usdhc_handle_t *handle, |
| status_t status, void *user_data) |
| { |
| const struct device *dev = (const struct device *)user_data; |
| struct usdhc_data *data = dev->data; |
| |
| if (status == kStatus_USDHC_TransferDataFailed) { |
| data->transfer_status |= TRANSFER_DATA_FAILED; |
| } else if (status == kStatus_USDHC_TransferDataComplete) { |
| data->transfer_status |= TRANSFER_DATA_COMPLETE; |
| } else if (status == kStatus_USDHC_SendCommandFailed) { |
| data->transfer_status |= TRANSFER_CMD_FAILED; |
| } else if (status == kStatus_USDHC_SendCommandSuccess) { |
| data->transfer_status |= TRANSFER_CMD_COMPLETE; |
| } |
| k_sem_give(&data->transfer_sem); |
| } |
| |
| |
| static void sdio_interrupt_cb(USDHC_Type *usdhc, void *user_data) |
| { |
| const struct device *dev = user_data; |
| struct usdhc_data *data = dev->data; |
| |
| if (data->sdhc_cb) { |
| data->sdhc_cb(dev, SDHC_INT_SDIO, data->sdhc_cb_user_data); |
| } |
| } |
| |
| static void card_inserted_cb(USDHC_Type *usdhc, void *user_data) |
| { |
| const struct device *dev = user_data; |
| struct usdhc_data *data = dev->data; |
| |
| if (data->sdhc_cb) { |
| data->sdhc_cb(dev, SDHC_INT_INSERTED, data->sdhc_cb_user_data); |
| } |
| } |
| |
| static void card_removed_cb(USDHC_Type *usdhc, void *user_data) |
| { |
| const struct device *dev = user_data; |
| struct usdhc_data *data = dev->data; |
| |
| if (data->sdhc_cb) { |
| data->sdhc_cb(dev, SDHC_INT_REMOVED, data->sdhc_cb_user_data); |
| } |
| } |
| |
| static void card_detect_gpio_cb(const struct device *port, |
| struct gpio_callback *cb, |
| gpio_port_pins_t pins) |
| { |
| struct usdhc_data *data = CONTAINER_OF(cb, struct usdhc_data, cd_callback); |
| const struct device *dev = data->dev; |
| const struct usdhc_config *cfg = dev->config; |
| |
| if (data->sdhc_cb) { |
| if (gpio_pin_get_dt(&cfg->detect_gpio)) { |
| data->sdhc_cb(dev, SDHC_INT_INSERTED, data->sdhc_cb_user_data); |
| } else { |
| data->sdhc_cb(dev, SDHC_INT_REMOVED, data->sdhc_cb_user_data); |
| } |
| } |
| } |
| |
| static void imx_usdhc_select_1_8v(USDHC_Type *base, bool enable_1_8v) |
| { |
| #if !(defined(FSL_FEATURE_USDHC_HAS_NO_VOLTAGE_SELECT) && \ |
| (FSL_FEATURE_USDHC_HAS_NO_VOLTAGE_SELECT)) |
| UDSHC_SelectVoltage(base, enable_1_8v); |
| #endif |
| } |
| |
| |
| static int imx_usdhc_dat3_pull(const struct usdhc_config *cfg, bool pullup) |
| { |
| int ret = 0U; |
| |
| ret = pinctrl_apply_state(cfg->pincfg, PINCTRL_STATE_NOPULL); |
| if (ret) { |
| LOG_ERR("No DAT3 floating state defined, but dat3 detect selected"); |
| return ret; |
| } |
| #ifdef CONFIG_IMX_USDHC_DAT3_PWR_TOGGLE |
| if (!pullup) { |
| /* Power off the card to clear DAT3 legacy status */ |
| if (cfg->pwr_gpio.port) { |
| ret = gpio_pin_set_dt(&cfg->pwr_gpio, 0); |
| if (ret) { |
| return ret; |
| } |
| /* Delay for card power off to complete */ |
| k_busy_wait(1000); |
| ret = gpio_pin_set_dt(&cfg->pwr_gpio, 1); |
| /* Delay for power on */ |
| k_busy_wait(1000); |
| if (ret) { |
| return ret; |
| } |
| } |
| } |
| #endif |
| return ret; |
| } |
| |
| /* |
| * Reset SDHC after command error |
| */ |
| static void imx_usdhc_error_recovery(const struct device *dev) |
| { |
| const struct usdhc_config *cfg = dev->config; |
| uint32_t status = USDHC_GetPresentStatusFlags(cfg->base); |
| |
| if (status & kUSDHC_CommandInhibitFlag) { |
| /* Reset command line */ |
| USDHC_Reset(cfg->base, kUSDHC_ResetCommand, 100U); |
| } |
| if (((status & (uint32_t)kUSDHC_DataInhibitFlag) != 0U) || |
| (USDHC_GetAdmaErrorStatusFlags(cfg->base) != 0U)) { |
| /* Reset data line */ |
| USDHC_Reset(cfg->base, kUSDHC_DataInhibitFlag, 100U); |
| } |
| } |
| |
| /* |
| * Initialize SDHC host properties for use in get_host_props api call |
| */ |
| static void imx_usdhc_init_host_props(const struct device *dev) |
| { |
| const struct usdhc_config *cfg = dev->config; |
| struct usdhc_data *data = dev->data; |
| usdhc_capability_t caps; |
| struct sdhc_host_props *props = &data->props; |
| |
| memset(props, 0, sizeof(struct sdhc_host_props)); |
| props->f_max = cfg->max_bus_freq; |
| props->f_min = cfg->min_bus_freq; |
| props->max_current_330 = cfg->max_current_330; |
| props->max_current_180 = cfg->max_current_180; |
| props->power_delay = cfg->power_delay_ms; |
| /* Read host capabilities */ |
| USDHC_GetCapability(cfg->base, &caps); |
| if (cfg->no_180_vol) { |
| props->host_caps.vol_180_support = false; |
| } else { |
| props->host_caps.vol_180_support = (bool)(caps.flags & kUSDHC_SupportV180Flag); |
| } |
| props->host_caps.vol_300_support = (bool)(caps.flags & kUSDHC_SupportV300Flag); |
| props->host_caps.vol_330_support = (bool)(caps.flags & kUSDHC_SupportV330Flag); |
| props->host_caps.suspend_res_support = (bool)(caps.flags & kUSDHC_SupportSuspendResumeFlag); |
| props->host_caps.sdma_support = (bool)(caps.flags & kUSDHC_SupportDmaFlag); |
| props->host_caps.high_spd_support = (bool)(caps.flags & kUSDHC_SupportHighSpeedFlag); |
| props->host_caps.adma_2_support = (bool)(caps.flags & kUSDHC_SupportAdmaFlag); |
| props->host_caps.max_blk_len = (bool)(caps.maxBlockLength); |
| props->host_caps.ddr50_support = (bool)(caps.flags & kUSDHC_SupportDDR50Flag); |
| props->host_caps.sdr104_support = (bool)(caps.flags & kUSDHC_SupportSDR104Flag); |
| props->host_caps.sdr50_support = (bool)(caps.flags & kUSDHC_SupportSDR50Flag); |
| props->host_caps.bus_8_bit_support = (bool)(caps.flags & kUSDHC_Support8BitFlag); |
| props->host_caps.bus_4_bit_support = (bool)(caps.flags & kUSDHC_Support4BitFlag); |
| props->host_caps.hs200_support = (bool)(cfg->mmc_hs200_1_8v); |
| props->host_caps.hs400_support = (bool)(cfg->mmc_hs400_1_8v); |
| } |
| |
| /* |
| * Reset USDHC controller |
| */ |
| static int imx_usdhc_reset(const struct device *dev) |
| { |
| const struct usdhc_config *cfg = dev->config; |
| /* Switch to default I/O voltage of 3.3V */ |
| imx_usdhc_select_1_8v(cfg->base, false); |
| USDHC_EnableDDRMode(cfg->base, false, 0U); |
| #if defined(FSL_FEATURE_USDHC_HAS_SDR50_MODE) && (FSL_FEATURE_USDHC_HAS_SDR50_MODE) |
| USDHC_EnableStandardTuning(cfg->base, 0, 0, false); |
| USDHC_EnableAutoTuning(cfg->base, false); |
| #endif |
| |
| #if FSL_FEATURE_USDHC_HAS_HS400_MODE |
| /* Disable HS400 mode */ |
| USDHC_EnableHS400Mode(cfg->base, false); |
| /* Disable DLL */ |
| USDHC_EnableStrobeDLL(cfg->base, false); |
| #endif |
| |
| /* Reset data/command/tuning circuit */ |
| return USDHC_Reset(cfg->base, kUSDHC_ResetAll, 1000U) == true ? 0 : -ETIMEDOUT; |
| } |
| |
| /* |
| * Set SDHC io properties |
| */ |
| static int imx_usdhc_set_io(const struct device *dev, struct sdhc_io *ios) |
| { |
| const struct usdhc_config *cfg = dev->config; |
| struct usdhc_data *data = dev->data; |
| uint32_t src_clk_hz, bus_clk; |
| struct sdhc_io *host_io = &data->host_io; |
| |
| LOG_DBG("SDHC I/O: bus width %d, clock %dHz, card power %s, voltage %s", |
| ios->bus_width, |
| ios->clock, |
| ios->power_mode == SDHC_POWER_ON ? "ON" : "OFF", |
| ios->signal_voltage == SD_VOL_1_8_V ? "1.8V" : "3.3V" |
| ); |
| |
| if (clock_control_get_rate(cfg->clock_dev, |
| cfg->clock_subsys, |
| &src_clk_hz)) { |
| return -EINVAL; |
| } |
| |
| if (ios->clock && (ios->clock > data->props.f_max || ios->clock < data->props.f_min)) { |
| return -EINVAL; |
| } |
| |
| /* Set host clock */ |
| if (host_io->clock != ios->clock) { |
| if (ios->clock != 0) { |
| /* Enable the clock output */ |
| bus_clk = USDHC_SetSdClock(cfg->base, src_clk_hz, ios->clock); |
| LOG_DBG("BUS CLOCK: %d", bus_clk); |
| if (bus_clk == 0) { |
| return -ENOTSUP; |
| } |
| } |
| host_io->clock = ios->clock; |
| } |
| |
| |
| /* Set bus width */ |
| if (host_io->bus_width != ios->bus_width) { |
| switch (ios->bus_width) { |
| case SDHC_BUS_WIDTH1BIT: |
| USDHC_SetDataBusWidth(cfg->base, kUSDHC_DataBusWidth1Bit); |
| break; |
| case SDHC_BUS_WIDTH4BIT: |
| USDHC_SetDataBusWidth(cfg->base, kUSDHC_DataBusWidth4Bit); |
| break; |
| case SDHC_BUS_WIDTH8BIT: |
| USDHC_SetDataBusWidth(cfg->base, kUSDHC_DataBusWidth8Bit); |
| break; |
| default: |
| return -ENOTSUP; |
| } |
| host_io->bus_width = ios->bus_width; |
| } |
| |
| /* Set host signal voltage */ |
| if (ios->signal_voltage != host_io->signal_voltage) { |
| switch (ios->signal_voltage) { |
| case SD_VOL_3_3_V: |
| case SD_VOL_3_0_V: |
| imx_usdhc_select_1_8v(cfg->base, false); |
| break; |
| case SD_VOL_1_8_V: |
| /** |
| * USDHC peripheral deviates from SD spec here. |
| * The host controller specification claims |
| * the "SD clock enable" bit can be used to gate the SD |
| * clock by clearing it. The USDHC controller does not |
| * provide this bit, only a way to force the SD clock |
| * on. We will instead delay 10 ms to allow the clock |
| * to be gated for enough time, then force it on for |
| * 10 ms, then allow it to be gated again. |
| */ |
| /* Switch to 1.8V */ |
| imx_usdhc_select_1_8v(cfg->base, true); |
| /* Wait 10 ms- clock will be gated during this period */ |
| k_msleep(10); |
| /* Force the clock on */ |
| USDHC_ForceClockOn(cfg->base, true); |
| /* Keep the clock on for a moment, so SD will recognize it */ |
| k_msleep(10); |
| /* Stop forcing clock on */ |
| USDHC_ForceClockOn(cfg->base, false); |
| break; |
| default: |
| return -ENOTSUP; |
| } |
| /* Save new host voltage */ |
| host_io->signal_voltage = ios->signal_voltage; |
| } |
| |
| /* Set card power */ |
| if ((host_io->power_mode != ios->power_mode) && (cfg->pwr_gpio.port)) { |
| if (ios->power_mode == SDHC_POWER_OFF) { |
| gpio_pin_set_dt(&cfg->pwr_gpio, 0); |
| } else if (ios->power_mode == SDHC_POWER_ON) { |
| gpio_pin_set_dt(&cfg->pwr_gpio, 1); |
| } |
| host_io->power_mode = ios->power_mode; |
| } |
| |
| /* Set I/O timing */ |
| if (host_io->timing != ios->timing) { |
| switch (ios->timing) { |
| case SDHC_TIMING_LEGACY: |
| case SDHC_TIMING_HS: |
| break; |
| case SDHC_TIMING_DDR50: |
| case SDHC_TIMING_DDR52: |
| /* Enable DDR mode */ |
| USDHC_EnableDDRMode(cfg->base, true, 0); |
| __fallthrough; |
| case SDHC_TIMING_SDR12: |
| case SDHC_TIMING_SDR25: |
| pinctrl_apply_state(cfg->pincfg, PINCTRL_STATE_SLOW); |
| break; |
| case SDHC_TIMING_SDR50: |
| pinctrl_apply_state(cfg->pincfg, PINCTRL_STATE_MED); |
| break; |
| case SDHC_TIMING_HS400: |
| #if FSL_FEATURE_USDHC_HAS_HS400_MODE |
| USDHC_EnableHS400Mode(cfg->base, true); |
| USDHC_EnableDDRMode(cfg->base, true, 0U); |
| USDHC_ConfigStrobeDLL(cfg->base, 7U, 4U); |
| USDHC_EnableStrobeDLL(cfg->base, true); |
| #else |
| LOG_ERR("HS400 not supported for this device"); |
| return -ENOTSUP; |
| #endif |
| case SDHC_TIMING_SDR104: |
| case SDHC_TIMING_HS200: |
| pinctrl_apply_state(cfg->pincfg, PINCTRL_STATE_FAST); |
| break; |
| default: |
| return -ENOTSUP; |
| } |
| host_io->timing = ios->timing; |
| } |
| |
| return 0; |
| } |
| |
| /* |
| * Internal transfer function, used by tuning and request apis |
| */ |
| static int imx_usdhc_transfer(const struct device *dev, |
| struct usdhc_host_transfer *request) |
| { |
| const struct usdhc_config *cfg = dev->config; |
| struct usdhc_data *dev_data = dev->data; |
| status_t error; |
| #ifdef CONFIG_IMX_USDHC_DMA_SUPPORT |
| usdhc_adma_config_t dma_config = {0}; |
| |
| /* Configure DMA */ |
| dma_config.admaTable = dev_data->usdhc_dma_descriptor; |
| dma_config.admaTableWords = dev_data->dma_descriptor_len; |
| #if !(defined(FSL_FEATURE_USDHC_HAS_NO_RW_BURST_LEN) && FSL_FEATURE_USDHC_HAS_NO_RW_BURST_LEN) |
| dma_config.burstLen = kUSDHC_EnBurstLenForINCR; |
| #endif |
| dma_config.dmaMode = kUSDHC_DmaModeAdma2; |
| #endif /* CONFIG_IMX_USDHC_DMA_SUPPORT */ |
| |
| /* Reset transfer status */ |
| dev_data->transfer_status = 0U; |
| /* Reset semaphore */ |
| k_sem_reset(&dev_data->transfer_sem); |
| #ifdef CONFIG_IMX_USDHC_DMA_SUPPORT |
| error = USDHC_TransferNonBlocking(cfg->base, &dev_data->transfer_handle, |
| &dma_config, request->transfer); |
| #else |
| error = USDHC_TransferNonBlocking(cfg->base, &dev_data->transfer_handle, |
| NULL, request->transfer); |
| #endif |
| if (error == kStatus_USDHC_ReTuningRequest) { |
| return -EAGAIN; |
| } else if (error != kStatus_Success) { |
| return -EIO; |
| } |
| /* Wait for event to occur */ |
| while ((dev_data->transfer_status & (TRANSFER_CMD_FLAGS | TRANSFER_DATA_FLAGS)) == 0) { |
| if (k_sem_take(&dev_data->transfer_sem, request->command_timeout)) { |
| return -ETIMEDOUT; |
| } |
| } |
| if (dev_data->transfer_status & TRANSFER_CMD_FAILED) { |
| return -EIO; |
| } |
| /* If data was sent, wait for that to complete */ |
| if (request->transfer->data) { |
| while ((dev_data->transfer_status & TRANSFER_DATA_FLAGS) == 0) { |
| if (k_sem_take(&dev_data->transfer_sem, request->data_timeout)) { |
| return -ETIMEDOUT; |
| } |
| } |
| if (dev_data->transfer_status & TRANSFER_DATA_FAILED) { |
| return -EIO; |
| } |
| } |
| return 0; |
| } |
| |
| /* Stops transmission after failed command with CMD12 */ |
| static void imx_usdhc_stop_transmission(const struct device *dev) |
| { |
| usdhc_command_t stop_cmd = {0}; |
| struct usdhc_host_transfer request; |
| usdhc_transfer_t transfer; |
| |
| /* Send CMD12 to stop transmission */ |
| stop_cmd.index = SD_STOP_TRANSMISSION; |
| stop_cmd.argument = 0U; |
| stop_cmd.type = kCARD_CommandTypeAbort; |
| stop_cmd.responseType = SD_RSP_TYPE_R1b; |
| transfer.command = &stop_cmd; |
| transfer.data = NULL; |
| |
| request.transfer = &transfer; |
| request.command_timeout = K_MSEC(IMX_USDHC_DEFAULT_TIMEOUT); |
| request.data_timeout = K_MSEC(IMX_USDHC_DEFAULT_TIMEOUT); |
| |
| imx_usdhc_transfer(dev, &request); |
| } |
| |
| /* |
| * Return 0 if card is not busy, 1 if it is |
| */ |
| static int imx_usdhc_card_busy(const struct device *dev) |
| { |
| const struct usdhc_config *cfg = dev->config; |
| |
| return (USDHC_GetPresentStatusFlags(cfg->base) |
| & (kUSDHC_Data0LineLevelFlag | |
| kUSDHC_Data1LineLevelFlag | |
| kUSDHC_Data2LineLevelFlag | |
| kUSDHC_Data3LineLevelFlag)) |
| ? 0 : 1; |
| } |
| |
| /* |
| * Execute card tuning |
| */ |
| static int imx_usdhc_execute_tuning(const struct device *dev) |
| { |
| const struct usdhc_config *cfg = dev->config; |
| struct usdhc_data *dev_data = dev->data; |
| usdhc_command_t cmd = {0}; |
| usdhc_data_t data = {0}; |
| struct usdhc_host_transfer request; |
| usdhc_transfer_t transfer; |
| int ret; |
| bool retry_tuning = true; |
| |
| if ((dev_data->host_io.timing == SDHC_TIMING_HS200) || |
| (dev_data->host_io.timing == SDHC_TIMING_HS400)) { |
| /*Currently only reaches here when MMC */ |
| cmd.index = MMC_SEND_TUNING_BLOCK; |
| } else { |
| cmd.index = SD_SEND_TUNING_BLOCK; |
| } |
| cmd.argument = 0; |
| cmd.responseType = SD_RSP_TYPE_R1; |
| |
| if (dev_data->host_io.bus_width == SDHC_BUS_WIDTH8BIT) { |
| data.blockSize = sizeof(dev_data->usdhc_rx_dummy); |
| } else { |
| data.blockSize = sizeof(dev_data->usdhc_rx_dummy) / 2; |
| } |
| data.blockCount = 1; |
| data.rxData = (uint32_t *)dev_data->usdhc_rx_dummy; |
| data.dataType = kUSDHC_TransferDataTuning; |
| |
| transfer.command = &cmd; |
| transfer.data = &data; |
| |
| /* Reset tuning circuit */ |
| USDHC_Reset(cfg->base, kUSDHC_ResetTuning, 100U); |
| /* Disable standard tuning */ |
| USDHC_EnableStandardTuning(cfg->base, IMX_USDHC_STANDARD_TUNING_START, |
| IMX_USDHC_TUNING_STEP, false); |
| USDHC_ForceClockOn(cfg->base, true); |
| /* |
| * Tuning fail found on some SOCs is caused by the different of delay |
| * cell, so we need to increase the tuning counter to cover the |
| * adjustable tuning window |
| */ |
| USDHC_SetStandardTuningCounter(cfg->base, IMX_USDHC_STANDARD_TUNING_COUNTER); |
| /* Reenable standard tuning */ |
| USDHC_EnableStandardTuning(cfg->base, IMX_USDHC_STANDARD_TUNING_START, |
| IMX_USDHC_TUNING_STEP, true); |
| |
| request.command_timeout = K_MSEC(IMX_USDHC_DEFAULT_TIMEOUT); |
| request.data_timeout = K_MSEC(IMX_USDHC_DEFAULT_TIMEOUT); |
| request.transfer = &transfer; |
| |
| while (true) { |
| ret = imx_usdhc_transfer(dev, &request); |
| if (ret) { |
| return ret; |
| } |
| /* Delay 1ms */ |
| k_busy_wait(1000); |
| |
| /* Wait for execute tuning bit to clear */ |
| if (USDHC_GetExecuteStdTuningStatus(cfg->base) != 0) { |
| continue; |
| } |
| /* If tuning had error, retry tuning */ |
| if ((USDHC_CheckTuningError(cfg->base) != 0U) && retry_tuning) { |
| retry_tuning = false; |
| /* Enable standard tuning */ |
| USDHC_EnableStandardTuning(cfg->base, |
| IMX_USDHC_STANDARD_TUNING_START, |
| IMX_USDHC_TUNING_STEP, true); |
| USDHC_SetTuningDelay(cfg->base, |
| IMX_USDHC_STANDARD_TUNING_START, 0U, 0U); |
| } else { |
| break; |
| } |
| } |
| |
| /* Check tuning result */ |
| if (USDHC_CheckStdTuningResult(cfg->base) == 0) { |
| return -EIO; |
| } |
| USDHC_ForceClockOn(cfg->base, false); |
| |
| /* Enable auto tuning */ |
| USDHC_EnableAutoTuning(cfg->base, true); |
| return 0; |
| } |
| |
| /* |
| * Send CMD or CMD/DATA via SDHC |
| */ |
| static int imx_usdhc_request(const struct device *dev, struct sdhc_command *cmd, |
| struct sdhc_data *data) |
| { |
| const struct usdhc_config *cfg = dev->config; |
| struct usdhc_data *dev_data = dev->data; |
| usdhc_command_t host_cmd = {0}; |
| usdhc_data_t host_data = {0}; |
| struct usdhc_host_transfer request; |
| usdhc_transfer_t transfer; |
| int busy_timeout = IMX_USDHC_DEFAULT_TIMEOUT; |
| int ret = 0; |
| int retries = (int)cmd->retries; |
| |
| if (cmd->opcode == SD_GO_IDLE_STATE) { |
| USDHC_SetCardActive(cfg->base, 0xFFFF); |
| } |
| |
| host_cmd.index = cmd->opcode; |
| host_cmd.argument = cmd->arg; |
| /* Mask out part of response type field used for SPI commands */ |
| host_cmd.responseType = (cmd->response_type & SDHC_NATIVE_RESPONSE_MASK); |
| transfer.command = &host_cmd; |
| if (cmd->timeout_ms == SDHC_TIMEOUT_FOREVER) { |
| request.command_timeout = K_FOREVER; |
| } else { |
| request.command_timeout = K_MSEC(cmd->timeout_ms); |
| } |
| |
| if (data) { |
| host_data.blockSize = data->block_size; |
| host_data.blockCount = data->blocks; |
| /* |
| * Determine type of command. Note that driver is expected to |
| * handle CMD12 and CMD23 for reading and writing blocks |
| */ |
| switch (cmd->opcode) { |
| case SD_WRITE_SINGLE_BLOCK: |
| host_data.enableAutoCommand12 = true; |
| host_data.txData = data->data; |
| break; |
| case SD_WRITE_MULTIPLE_BLOCK: |
| if (dev_data->host_io.timing == SDHC_TIMING_SDR104) { |
| /* Card uses UHS104, so it must support CMD23 */ |
| host_data.enableAutoCommand23 = true; |
| } else { |
| /* No CMD23 support */ |
| host_data.enableAutoCommand12 = true; |
| } |
| host_data.txData = data->data; |
| break; |
| case SD_READ_SINGLE_BLOCK: |
| host_data.enableAutoCommand12 = true; |
| host_data.rxData = data->data; |
| break; |
| case SD_READ_MULTIPLE_BLOCK: |
| if (dev_data->host_io.timing == SDHC_TIMING_SDR104) { |
| /* Card uses UHS104, so it must support CMD23 */ |
| host_data.enableAutoCommand23 = true; |
| } else { |
| /* No CMD23 support */ |
| host_data.enableAutoCommand12 = true; |
| } |
| host_data.rxData = data->data; |
| break; |
| case MMC_CHECK_BUS_TEST: |
| case MMC_SEND_EXT_CSD: |
| case SD_APP_SEND_SCR: |
| case SD_SWITCH: |
| case SD_APP_SEND_NUM_WRITTEN_BLK: |
| host_data.rxData = data->data; |
| break; |
| case SDIO_RW_EXTENDED: |
| /* Use R/W bit to determine data direction */ |
| if (host_cmd.argument & BIT(SDIO_CMD_ARG_RW_SHIFT)) { |
| host_data.txData = data->data; |
| } else { |
| host_data.rxData = data->data; |
| } |
| break; |
| default: |
| return -ENOTSUP; |
| |
| } |
| transfer.data = &host_data; |
| if (data->timeout_ms == SDHC_TIMEOUT_FOREVER) { |
| request.data_timeout = K_FOREVER; |
| } else { |
| request.data_timeout = K_MSEC(data->timeout_ms); |
| } |
| } else { |
| transfer.data = NULL; |
| request.data_timeout = K_NO_WAIT; |
| } |
| request.transfer = &transfer; |
| |
| /* Ensure we have exclusive access to SD card before sending request */ |
| if (k_mutex_lock(&dev_data->access_mutex, request.command_timeout) != 0) { |
| return -EBUSY; |
| } |
| while (retries >= 0) { |
| ret = imx_usdhc_transfer(dev, &request); |
| if (ret && data) { |
| /* |
| * Disable and clear interrupts. If the data transmission |
| * completes later, we will encounter issues because |
| * the USDHC driver expects data to be present in the |
| * current transmission, but CMD12 does not contain data |
| */ |
| USDHC_DisableInterruptSignal(cfg->base, kUSDHC_CommandFlag | |
| kUSDHC_DataFlag | kUSDHC_DataDMAFlag); |
| USDHC_ClearInterruptStatusFlags(cfg->base, kUSDHC_CommandFlag | |
| kUSDHC_DataFlag | kUSDHC_DataDMAFlag); |
| /* Stop transmission with CMD12 in case of data error */ |
| imx_usdhc_stop_transmission(dev); |
| /* Wait for card to go idle */ |
| while (busy_timeout > 0) { |
| if (!imx_usdhc_card_busy(dev)) { |
| break; |
| } |
| /* Wait 125us before polling again */ |
| k_busy_wait(125); |
| busy_timeout -= 125; |
| } |
| if (busy_timeout <= 0) { |
| LOG_DBG("Card did not idle after CMD12"); |
| k_mutex_unlock(&dev_data->access_mutex); |
| return -ETIMEDOUT; |
| } |
| } |
| if (ret == -EAGAIN) { |
| /* Retry, card made a tuning request */ |
| if (dev_data->host_io.timing == SDHC_TIMING_SDR50 || |
| dev_data->host_io.timing == SDHC_TIMING_SDR104 || |
| dev_data->host_io.timing == SDHC_TIMING_HS200 || |
| dev_data->host_io.timing == SDHC_TIMING_HS400) { |
| /* Retune card */ |
| LOG_DBG("Card made tuning request, retune"); |
| ret = imx_usdhc_execute_tuning(dev); |
| if (ret) { |
| LOG_DBG("Card failed to tune"); |
| k_mutex_unlock(&dev_data->access_mutex); |
| return ret; |
| } |
| } |
| } |
| if (ret) { |
| imx_usdhc_error_recovery(dev); |
| retries--; |
| } else { |
| break; |
| } |
| } |
| |
| /* Release access on card */ |
| k_mutex_unlock(&dev_data->access_mutex); |
| /* Record command response */ |
| memcpy(cmd->response, host_cmd.response, sizeof(cmd->response)); |
| if (data) { |
| /* Record number of bytes xfered */ |
| data->bytes_xfered = dev_data->transfer_handle.transferredWords; |
| } |
| return ret; |
| } |
| |
| /* |
| * Get card presence |
| */ |
| static int imx_usdhc_get_card_present(const struct device *dev) |
| { |
| const struct usdhc_config *cfg = dev->config; |
| struct usdhc_data *data = dev->data; |
| |
| if (cfg->detect_dat3) { |
| /* |
| * If card is already present, do not retry detection. |
| * Power line toggling would reset SD card |
| */ |
| if (!data->card_present) { |
| /* Detect card presence with DAT3 line pull */ |
| imx_usdhc_dat3_pull(cfg, false); |
| USDHC_CardDetectByData3(cfg->base, true); |
| /* Delay to ensure host has time to detect card */ |
| k_busy_wait(1000); |
| data->card_present = USDHC_DetectCardInsert(cfg->base); |
| /* Clear card detection and pull */ |
| imx_usdhc_dat3_pull(cfg, true); |
| USDHC_CardDetectByData3(cfg->base, false); |
| } |
| } else if (cfg->detect_cd) { |
| /* |
| * Detect the card via the USDHC_CD signal internal to |
| * the peripheral |
| */ |
| data->card_present = USDHC_DetectCardInsert(cfg->base); |
| } else if (cfg->detect_gpio.port) { |
| data->card_present = gpio_pin_get_dt(&cfg->detect_gpio) > 0; |
| } else { |
| LOG_WRN("No card detection method configured, assuming card " |
| "is present"); |
| data->card_present = true; |
| } |
| return ((int)data->card_present); |
| } |
| |
| /* |
| * Get host properties |
| */ |
| static int imx_usdhc_get_host_props(const struct device *dev, |
| struct sdhc_host_props *props) |
| { |
| struct usdhc_data *data = dev->data; |
| |
| memcpy(props, &data->props, sizeof(struct sdhc_host_props)); |
| return 0; |
| } |
| |
| /* |
| * Enable SDHC card interrupt |
| */ |
| static int imx_usdhc_enable_interrupt(const struct device *dev, |
| sdhc_interrupt_cb_t callback, |
| int sources, void *user_data) |
| { |
| const struct usdhc_config *cfg = dev->config; |
| struct usdhc_data *data = dev->data; |
| int ret; |
| |
| /* Record SDIO callback parameters */ |
| data->sdhc_cb = callback; |
| data->sdhc_cb_user_data = user_data; |
| |
| /* Disable interrupts, then enable what the user requested */ |
| USDHC_DisableInterruptStatus(cfg->base, kUSDHC_CardInterruptFlag); |
| USDHC_DisableInterruptSignal(cfg->base, kUSDHC_CardInterruptFlag); |
| if (cfg->detect_gpio.port) { |
| ret = gpio_pin_interrupt_configure_dt(&cfg->detect_gpio, |
| GPIO_INT_DISABLE); |
| if (ret) { |
| return ret; |
| } |
| } else { |
| USDHC_DisableInterruptSignal(cfg->base, kUSDHC_CardInsertionFlag); |
| USDHC_DisableInterruptStatus(cfg->base, kUSDHC_CardInsertionFlag); |
| USDHC_DisableInterruptSignal(cfg->base, kUSDHC_CardRemovalFlag); |
| USDHC_DisableInterruptStatus(cfg->base, kUSDHC_CardRemovalFlag); |
| } |
| |
| if (sources & SDHC_INT_SDIO) { |
| /* Enable SDIO card interrupt */ |
| USDHC_EnableInterruptStatus(cfg->base, kUSDHC_CardInterruptFlag); |
| USDHC_EnableInterruptSignal(cfg->base, kUSDHC_CardInterruptFlag); |
| } |
| if (sources & SDHC_INT_INSERTED) { |
| if (cfg->detect_gpio.port) { |
| /* Use GPIO interrupt */ |
| ret = gpio_pin_interrupt_configure_dt(&cfg->detect_gpio, |
| GPIO_INT_EDGE_TO_ACTIVE); |
| if (ret) { |
| return ret; |
| } |
| } else { |
| /* Enable card insertion interrupt */ |
| USDHC_EnableInterruptStatus(cfg->base, |
| kUSDHC_CardInsertionFlag); |
| USDHC_EnableInterruptSignal(cfg->base, |
| kUSDHC_CardInsertionFlag); |
| } |
| } |
| if (sources & SDHC_INT_REMOVED) { |
| if (cfg->detect_gpio.port) { |
| /* Use GPIO interrupt */ |
| ret = gpio_pin_interrupt_configure_dt(&cfg->detect_gpio, |
| GPIO_INT_EDGE_TO_INACTIVE); |
| if (ret) { |
| return ret; |
| } |
| } else { |
| /* Enable card removal interrupt */ |
| USDHC_EnableInterruptStatus(cfg->base, |
| kUSDHC_CardRemovalFlag); |
| USDHC_EnableInterruptSignal(cfg->base, |
| kUSDHC_CardRemovalFlag); |
| } |
| } |
| |
| return 0; |
| } |
| |
| static int imx_usdhc_disable_interrupt(const struct device *dev, int sources) |
| { |
| const struct usdhc_config *cfg = dev->config; |
| struct usdhc_data *data = dev->data; |
| int ret; |
| |
| |
| if (sources & SDHC_INT_SDIO) { |
| /* Disable SDIO card interrupt */ |
| USDHC_DisableInterruptStatus(cfg->base, kUSDHC_CardInterruptFlag); |
| USDHC_DisableInterruptSignal(cfg->base, kUSDHC_CardInterruptFlag); |
| } |
| if (sources & SDHC_INT_INSERTED) { |
| if (cfg->detect_gpio.port) { |
| ret = gpio_pin_interrupt_configure_dt(&cfg->detect_gpio, |
| GPIO_INT_DISABLE); |
| if (ret) { |
| return ret; |
| } |
| } else { |
| /* Disable card insertion interrupt */ |
| USDHC_DisableInterruptStatus(cfg->base, |
| kUSDHC_CardInsertionFlag); |
| USDHC_DisableInterruptSignal(cfg->base, |
| kUSDHC_CardInsertionFlag); |
| } |
| } |
| if (sources & SDHC_INT_REMOVED) { |
| if (cfg->detect_gpio.port) { |
| ret = gpio_pin_interrupt_configure_dt(&cfg->detect_gpio, |
| GPIO_INT_DISABLE); |
| if (ret) { |
| return ret; |
| } |
| } else { |
| /* Disable card removal interrupt */ |
| USDHC_DisableInterruptStatus(cfg->base, |
| kUSDHC_CardRemovalFlag); |
| USDHC_DisableInterruptSignal(cfg->base, |
| kUSDHC_CardRemovalFlag); |
| } |
| } |
| |
| /* If all interrupt flags are disabled, remove callback */ |
| if ((USDHC_GetEnabledInterruptStatusFlags(cfg->base) & |
| (kUSDHC_CardInterruptFlag | kUSDHC_CardInsertionFlag | |
| kUSDHC_CardRemovalFlag)) == 0) { |
| data->sdhc_cb = NULL; |
| data->sdhc_cb_user_data = NULL; |
| } |
| |
| return 0; |
| } |
| |
| static int imx_usdhc_isr(const struct device *dev) |
| { |
| const struct usdhc_config *cfg = dev->config; |
| struct usdhc_data *data = dev->data; |
| |
| USDHC_TransferHandleIRQ(cfg->base, &data->transfer_handle); |
| return 0; |
| } |
| |
| /* |
| * Perform early system init for SDHC |
| */ |
| static int imx_usdhc_init(const struct device *dev) |
| { |
| const struct usdhc_config *cfg = dev->config; |
| struct usdhc_data *data = dev->data; |
| usdhc_config_t host_config = {0}; |
| int ret; |
| const usdhc_transfer_callback_t callbacks = { |
| .TransferComplete = transfer_complete_cb, |
| .SdioInterrupt = sdio_interrupt_cb, |
| .CardInserted = card_inserted_cb, |
| .CardRemoved = card_removed_cb, |
| }; |
| |
| if (!device_is_ready(cfg->clock_dev)) { |
| LOG_ERR("clock control device not ready"); |
| return -ENODEV; |
| } |
| |
| ret = pinctrl_apply_state(cfg->pincfg, PINCTRL_STATE_DEFAULT); |
| if (ret) { |
| return ret; |
| } |
| USDHC_TransferCreateHandle(cfg->base, &data->transfer_handle, |
| &callbacks, (void *)dev); |
| cfg->irq_config_func(dev); |
| |
| |
| host_config.dataTimeout = cfg->data_timeout; |
| host_config.endianMode = kUSDHC_EndianModeLittle; |
| host_config.readWatermarkLevel = cfg->read_watermark; |
| host_config.writeWatermarkLevel = cfg->write_watermark; |
| USDHC_Init(cfg->base, &host_config); |
| /* Read host controller properties */ |
| imx_usdhc_init_host_props(dev); |
| /* Set power GPIO low, so card starts powered off */ |
| if (cfg->pwr_gpio.port) { |
| ret = gpio_pin_configure_dt(&cfg->pwr_gpio, GPIO_OUTPUT_INACTIVE); |
| if (ret) { |
| return ret; |
| } |
| } else { |
| LOG_WRN("No power control GPIO defined. Without power control,\n" |
| "the SD card may fail to communicate with the host"); |
| } |
| if (cfg->detect_gpio.port) { |
| ret = gpio_pin_configure_dt(&cfg->detect_gpio, GPIO_INPUT); |
| if (ret) { |
| return ret; |
| } |
| gpio_init_callback(&data->cd_callback, card_detect_gpio_cb, |
| BIT(cfg->detect_gpio.pin)); |
| ret = gpio_add_callback_dt(&cfg->detect_gpio, &data->cd_callback); |
| if (ret) { |
| return ret; |
| } |
| } |
| data->dev = dev; |
| k_mutex_init(&data->access_mutex); |
| /* Setup initial host IO values */ |
| data->host_io.clock = 0; |
| data->host_io.bus_mode = SDHC_BUSMODE_PUSHPULL; |
| data->host_io.power_mode = SDHC_POWER_OFF; |
| data->host_io.bus_width = SDHC_BUS_WIDTH1BIT; |
| data->host_io.timing = SDHC_TIMING_LEGACY; |
| data->host_io.driver_type = SD_DRIVER_TYPE_B; |
| data->host_io.signal_voltage = SD_VOL_3_3_V; |
| |
| return k_sem_init(&data->transfer_sem, 0, 1); |
| } |
| |
| static const struct sdhc_driver_api usdhc_api = { |
| .reset = imx_usdhc_reset, |
| .request = imx_usdhc_request, |
| .set_io = imx_usdhc_set_io, |
| .get_card_present = imx_usdhc_get_card_present, |
| .execute_tuning = imx_usdhc_execute_tuning, |
| .card_busy = imx_usdhc_card_busy, |
| .get_host_props = imx_usdhc_get_host_props, |
| .enable_interrupt = imx_usdhc_enable_interrupt, |
| .disable_interrupt = imx_usdhc_disable_interrupt, |
| }; |
| |
| #ifdef CONFIG_NOCACHE_MEMORY |
| #define IMX_USDHC_NOCACHE_TAG __attribute__((__section__(".nocache"))); |
| #else |
| #define IMX_USDHC_NOCACHE_TAG |
| #endif |
| |
| #ifdef CONFIG_IMX_USDHC_DMA_SUPPORT |
| #define IMX_USDHC_DMA_BUFFER_DEFINE(n) \ |
| static uint32_t __aligned(32) \ |
| usdhc_##n##_dma_descriptor[CONFIG_IMX_USDHC_DMA_BUFFER_SIZE / 4]\ |
| IMX_USDHC_NOCACHE_TAG; |
| #define IMX_USDHC_DMA_BUFFER_INIT(n) \ |
| .usdhc_dma_descriptor = usdhc_##n##_dma_descriptor, \ |
| .dma_descriptor_len = CONFIG_IMX_USDHC_DMA_BUFFER_SIZE / 4, |
| #else |
| #define IMX_USDHC_DMA_BUFFER_DEFINE(n) |
| #define IMX_USDHC_DMA_BUFFER_INIT(n) |
| #endif /* CONFIG_IMX_USDHC_DMA_SUPPORT */ |
| |
| |
| |
| #define IMX_USDHC_INIT(n) \ |
| static void usdhc_##n##_irq_config_func(const struct device *dev) \ |
| { \ |
| IRQ_CONNECT(DT_INST_IRQN(n), DT_INST_IRQ(n, priority), \ |
| imx_usdhc_isr, DEVICE_DT_INST_GET(n), 0); \ |
| irq_enable(DT_INST_IRQN(n)); \ |
| } \ |
| \ |
| PINCTRL_DT_INST_DEFINE(n); \ |
| \ |
| static const struct usdhc_config usdhc_##n##_config = { \ |
| .base = (USDHC_Type *) DT_INST_REG_ADDR(n), \ |
| .clock_dev = DEVICE_DT_GET(DT_INST_CLOCKS_CTLR(n)), \ |
| .clock_subsys = \ |
| (clock_control_subsys_t)DT_INST_CLOCKS_CELL(n, name), \ |
| .nusdhc = n, \ |
| .pwr_gpio = GPIO_DT_SPEC_INST_GET_OR(n, pwr_gpios, {0}), \ |
| .detect_gpio = GPIO_DT_SPEC_INST_GET_OR(n, cd_gpios, {0}), \ |
| .data_timeout = DT_INST_PROP(n, data_timeout), \ |
| .detect_dat3 = DT_INST_PROP(n, detect_dat3), \ |
| .detect_cd = DT_INST_PROP(n, detect_cd), \ |
| .no_180_vol = DT_INST_PROP(n, no_1_8_v), \ |
| .read_watermark = DT_INST_PROP(n, read_watermark), \ |
| .write_watermark = DT_INST_PROP(n, write_watermark), \ |
| .max_current_330 = DT_INST_PROP(n, max_current_330), \ |
| .max_current_180 = DT_INST_PROP(n, max_current_180), \ |
| .min_bus_freq = DT_INST_PROP(n, min_bus_freq), \ |
| .max_bus_freq = DT_INST_PROP(n, max_bus_freq), \ |
| .power_delay_ms = DT_INST_PROP(n, power_delay_ms), \ |
| .mmc_hs200_1_8v = DT_INST_PROP(n, mmc_hs200_1_8v), \ |
| .mmc_hs400_1_8v = DT_INST_PROP(n, mmc_hs400_1_8v), \ |
| .irq_config_func = usdhc_##n##_irq_config_func, \ |
| .pincfg = PINCTRL_DT_INST_DEV_CONFIG_GET(n), \ |
| }; \ |
| \ |
| \ |
| IMX_USDHC_DMA_BUFFER_DEFINE(n) \ |
| \ |
| static struct usdhc_data usdhc_##n##_data = { \ |
| .card_present = false, \ |
| IMX_USDHC_DMA_BUFFER_INIT(n) \ |
| }; \ |
| \ |
| DEVICE_DT_INST_DEFINE(n, \ |
| &imx_usdhc_init, \ |
| NULL, \ |
| &usdhc_##n##_data, \ |
| &usdhc_##n##_config, \ |
| POST_KERNEL, \ |
| CONFIG_SDHC_INIT_PRIORITY, \ |
| &usdhc_api); |
| |
| DT_INST_FOREACH_STATUS_OKAY(IMX_USDHC_INIT) |