| // Copyright 2017 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| // Notes and limitations: |
| // 1. This driver only uses PIO mode. |
| // |
| // 2. This driver only supports SDHCv3 and above. Lower versions of SD are not |
| // currently supported. The driver should fail gracefully if a lower version |
| // card is detected. |
| |
| // Standard Includes |
| #include <assert.h> |
| #include <fcntl.h> |
| #include <inttypes.h> |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <threads.h> |
| |
| // DDK Includes |
| #include <ddk/binding.h> |
| #include <ddk/device.h> |
| #include <ddk/iotxn.h> |
| #include <ddk/io-buffer.h> |
| #include <ddk/protocol/sdmmc.h> |
| #include <ddk/protocol/sdhci.h> |
| #include <hw/sdmmc.h> |
| |
| // Zircon Includes |
| #include <fdio/watcher.h> |
| #include <zircon/threads.h> |
| #include <zircon/assert.h> |
| #include <sync/completion.h> |
| #include <pretty/hexdump.h> |
| |
| #define SD_FREQ_SETUP_HZ 400000 |
| |
| #define TRACE 0 |
| |
| #if TRACE |
| #define xprintf(fmt...) printf(fmt) |
| #else |
| #define xprintf(fmt...) \ |
| do { \ |
| } while (0) |
| #endif |
| |
| #define HI32(val) (((val) >> 32) & 0xffffffff) |
| #define LO32(val) ((val) & 0xffffffff) |
| |
| typedef struct sdhci_adma64_desc { |
| union { |
| struct { |
| uint8_t valid : 1; |
| uint8_t end : 1; |
| uint8_t intr : 1; |
| uint8_t rsvd0 : 1; |
| uint8_t act1 : 1; |
| uint8_t act2 : 1; |
| uint8_t rsvd1 : 2; |
| uint8_t rsvd2; |
| } __PACKED; |
| uint16_t attr; |
| } __PACKED; |
| uint16_t length; |
| uint64_t address; |
| uint32_t reserved; |
| } __PACKED sdhci_adma64_desc_t; |
| |
| static_assert(sizeof(sdhci_adma64_desc_t) == 16, "unexpected ADMA2 descriptor size"); |
| |
| #define ADMA2_DESC_MAX_LENGTH 0x10000 // 64k |
| #define DMA_DESC_COUNT 8192 // for 32M max transfer size for fully discontiguous |
| |
| typedef struct sdhci_device { |
| // Interrupts mapped here. |
| zx_handle_t irq_handle; |
| // Used to signal that a command has completed. |
| completion_t irq_completion; |
| |
| // Memory mapped device registers. |
| volatile sdhci_regs_t* regs; |
| |
| // Device heirarchy |
| zx_device_t* mxdev; |
| zx_device_t* parent; |
| |
| // Protocol ops |
| sdhci_protocol_t sdhci; |
| |
| // DMA descriptors |
| io_buffer_t iobuf; |
| sdhci_adma64_desc_t* descs; |
| |
| // Held when a command or action is in progress. |
| mtx_t mtx; |
| |
| // Current iotxn in flight |
| iotxn_t* pending; |
| // Completed iotxn |
| iotxn_t* completed; |
| // Used to signal that the pending iotxn is completed |
| completion_t pending_completion; |
| |
| // controller specific quirks |
| uint64_t quirks; |
| |
| // Cached base clock rate that the pi is running at. |
| uint32_t base_clock; |
| // Offset to DMA address |
| // XXX temporary (see ddk/protocol/sdhci.h) |
| zx_paddr_t dma_offset; |
| } sdhci_device_t; |
| |
| // If any of these interrupts is asserted in the SDHCI irq register, it means |
| // that an error has occured. |
| static const uint32_t error_interrupts = ( |
| SDHCI_IRQ_ERR | |
| SDHCI_IRQ_ERR_CMD_TIMEOUT | |
| SDHCI_IRQ_ERR_CMD_CRC | |
| SDHCI_IRQ_ERR_CMD_END_BIT | |
| SDHCI_IRQ_ERR_CMD_INDEX | |
| SDHCI_IRQ_ERR_DAT_TIMEOUT | |
| SDHCI_IRQ_ERR_DAT_CRC | |
| SDHCI_IRQ_ERR_DAT_ENDBIT | |
| SDHCI_IRQ_ERR_CURRENT_LIMIT | |
| SDHCI_IRQ_ERR_AUTO_CMD | |
| SDHCI_IRQ_ERR_ADMA | |
| SDHCI_IRQ_ERR_TUNING |
| ); |
| |
| // These interrupts indicate that a transfer or command has progressed normally. |
| static const uint32_t normal_interrupts = ( |
| SDHCI_IRQ_CMD_CPLT | |
| SDHCI_IRQ_XFER_CPLT | |
| SDHCI_IRQ_BUFF_READ_READY | |
| SDHCI_IRQ_BUFF_WRITE_READY |
| ); |
| |
| static bool sdhci_supports_adma2_64bit(sdhci_device_t* dev) { |
| return (dev->regs->caps0 & SDHCI_CORECFG_ADMA2_SUPPORT) && |
| (dev->regs->caps0 & SDHCI_CORECFG_64BIT_SUPPORT) && |
| !(dev->quirks & SDHCI_QUIRK_NO_DMA); |
| } |
| |
| static zx_status_t sdhci_wait_for_reset(sdhci_device_t* dev, const uint32_t mask, zx_time_t timeout) { |
| zx_time_t deadline = zx_time_get(ZX_CLOCK_MONOTONIC) + timeout; |
| while (true) { |
| if (((dev->regs->ctrl1) & mask) == 0) { |
| break; |
| } |
| if (zx_time_get(ZX_CLOCK_MONOTONIC) > deadline) { |
| printf("sdhci: timed out while waiting for reset\n"); |
| return ZX_ERR_TIMED_OUT; |
| } |
| } |
| return ZX_OK; |
| } |
| |
| static void sdhci_complete_pending_locked(sdhci_device_t* dev, zx_status_t status, uint64_t actual) { |
| // Disable irqs when no pending iotxn |
| dev->regs->irqen = 0; |
| |
| dev->completed = dev->pending; |
| dev->completed->status = status; |
| dev->completed->actual = actual; |
| dev->pending = NULL; |
| |
| completion_signal(&dev->pending_completion); |
| } |
| |
| static void sdhci_cmd_stage_complete_locked(sdhci_device_t* dev) { |
| if (!dev->pending) { |
| xprintf("sdhci: spurious CMD_CPLT interrupt!\n"); |
| return; |
| } |
| |
| iotxn_t* txn = dev->pending; |
| volatile struct sdhci_regs* regs = dev->regs; |
| sdmmc_protocol_data_t* pdata = iotxn_pdata(txn, sdmmc_protocol_data_t); |
| uint32_t cmd = pdata->cmd; |
| |
| // Read the response data. |
| if (cmd & SDMMC_RESP_LEN_136) { |
| if (dev->quirks & SDHCI_QUIRK_STRIP_RESPONSE_CRC) { |
| pdata->response[0] = (regs->resp3 << 8) | ((regs->resp2 >> 24) & 0xFF); |
| pdata->response[1] = (regs->resp2 << 8) | ((regs->resp1 >> 24) & 0xFF); |
| pdata->response[2] = (regs->resp1 << 8) | ((regs->resp0 >> 24) & 0xFF); |
| pdata->response[3] = (regs->resp0 << 8); |
| } else { |
| pdata->response[0] = regs->resp0; |
| pdata->response[1] = regs->resp1; |
| pdata->response[2] = regs->resp2; |
| pdata->response[3] = regs->resp3; |
| } |
| } else if (cmd & (SDMMC_RESP_LEN_48 | SDMMC_RESP_LEN_48B)) { |
| pdata->response[0] = regs->resp0; |
| pdata->response[1] = regs->resp1; |
| } |
| |
| // If this command has a data phase and we're not using DMA, transfer the data |
| bool has_data = cmd & SDMMC_RESP_DATA_PRESENT; |
| bool use_dma = sdhci_supports_adma2_64bit(dev); |
| if (has_data) { |
| if (use_dma) { |
| // Wait for transfer complete interrupt |
| regs->irqen = error_interrupts | SDHCI_IRQ_XFER_CPLT; |
| } else { |
| // Select the interrupt that we want to wait on based on whether we're |
| // reading or writing. |
| if (cmd & SDMMC_CMD_READ) { |
| regs->irqen = error_interrupts | SDHCI_IRQ_BUFF_READ_READY; |
| } else { |
| regs->irqen = error_interrupts | SDHCI_IRQ_BUFF_WRITE_READY; |
| } |
| } |
| } else { |
| sdhci_complete_pending_locked(dev, ZX_OK, 0); |
| } |
| } |
| |
| static void sdhci_data_stage_read_ready_locked(sdhci_device_t* dev) { |
| if (!dev->pending) { |
| xprintf("sdhci: spurious BUFF_READ_READY interrupt!\n"); |
| return; |
| } |
| |
| iotxn_t* txn = dev->pending; |
| sdmmc_protocol_data_t* pdata = iotxn_pdata(txn, sdmmc_protocol_data_t); |
| |
| // Sequentially read each block. |
| for (size_t byteid = 0; byteid < pdata->blocksize; byteid += 4) { |
| uint32_t wrd; |
| const size_t offset = pdata->blockid * pdata->blocksize + byteid; |
| wrd = dev->regs->data; |
| iotxn_copyto(txn, &wrd, sizeof(wrd), offset); |
| txn->actual += sizeof(wrd); |
| } |
| pdata->blockid += 1; |
| if (pdata->blockid == pdata->blockcount) { |
| sdhci_complete_pending_locked(dev, ZX_OK, txn->actual); |
| } |
| } |
| |
| static void sdhci_data_stage_write_ready_locked(sdhci_device_t* dev) { |
| if (!dev->pending) { |
| xprintf("sdhci: spurious BUFF_WRITE_READY interrupt!\n"); |
| return; |
| } |
| |
| iotxn_t* txn = dev->pending; |
| sdmmc_protocol_data_t* pdata = iotxn_pdata(txn, sdmmc_protocol_data_t); |
| |
| // Sequentially write each block. |
| for (size_t byteid = 0; byteid < pdata->blocksize; byteid += 4) { |
| uint32_t wrd; |
| const size_t offset = pdata->blockid * pdata->blocksize + byteid; |
| iotxn_copyfrom(txn, &wrd, sizeof(wrd), offset); |
| dev->regs->data = wrd; |
| txn->actual += sizeof(wrd); |
| } |
| pdata->blockid += 1; |
| if (pdata->blockid == pdata->blockcount) { |
| sdhci_complete_pending_locked(dev, ZX_OK, txn->actual); |
| } |
| } |
| |
| static void sdhci_transfer_complete_locked(sdhci_device_t* dev) { |
| if (!dev->pending) { |
| xprintf("sdhci: spurious XFER_CPLT interrupt!\n"); |
| return; |
| } |
| sdhci_complete_pending_locked(dev, ZX_OK, dev->pending->length); |
| } |
| |
| static void sdhci_error_recovery_locked(sdhci_device_t* dev) { |
| // Reset internal state machines |
| dev->regs->ctrl1 |= SDHCI_SOFTWARE_RESET_CMD; |
| sdhci_wait_for_reset(dev, SDHCI_SOFTWARE_RESET_CMD, ZX_SEC(1)); |
| dev->regs->ctrl1 |= SDHCI_SOFTWARE_RESET_DAT; |
| sdhci_wait_for_reset(dev, SDHCI_SOFTWARE_RESET_DAT, ZX_SEC(1)); |
| |
| // TODO data stage abort |
| |
| // Complete any pending txn with error status |
| if (dev->pending != NULL) { |
| sdhci_complete_pending_locked(dev, ZX_ERR_IO, 0); |
| } |
| } |
| |
| static uint32_t get_clock_divider(const uint32_t base_clock, |
| const uint32_t target_rate) { |
| if (target_rate >= base_clock) { |
| // A clock divider of 0 means "don't divide the clock" |
| // If the base clock is already slow enough to use as the SD clock then |
| // we don't need to divide it any further. |
| return 0; |
| } |
| |
| uint32_t result = base_clock / (2 * target_rate); |
| if (result * target_rate * 2 < base_clock) |
| result++; |
| |
| return result; |
| } |
| |
| static int sdhci_irq_thread(void *arg) { |
| zx_status_t wait_res; |
| sdhci_device_t* dev = (sdhci_device_t*)arg; |
| volatile struct sdhci_regs* regs = dev->regs; |
| zx_handle_t irq_handle = dev->irq_handle; |
| |
| while (true) { |
| wait_res = zx_interrupt_wait(irq_handle); |
| if (wait_res != ZX_OK) { |
| printf("sdhci: interrupt wait failed with retcode = %d\n", wait_res); |
| break; |
| } |
| |
| const uint32_t irq = regs->irq; |
| xprintf("got irq 0x%08x 0x%08x en 0x%08x\n", regs->irq, irq, regs->irqen); |
| |
| // Acknowledge the IRQs that we stashed. IRQs are cleared by writing |
| // 1s into the IRQs that fired. |
| regs->irq = irq; |
| |
| mtx_lock(&dev->mtx); |
| if (irq & SDHCI_IRQ_CMD_CPLT) { |
| sdhci_cmd_stage_complete_locked(dev); |
| } |
| if (irq & SDHCI_IRQ_BUFF_READ_READY) { |
| sdhci_data_stage_read_ready_locked(dev); |
| } |
| if (irq & SDHCI_IRQ_BUFF_WRITE_READY) { |
| sdhci_data_stage_write_ready_locked(dev); |
| } |
| if (irq & SDHCI_IRQ_XFER_CPLT) { |
| sdhci_transfer_complete_locked(dev); |
| } |
| if (irq & error_interrupts) { |
| sdhci_error_recovery_locked(dev); |
| } |
| mtx_unlock(&dev->mtx); |
| |
| // Mark this interrupt as completed. |
| zx_interrupt_complete(irq_handle); |
| } |
| return 0; |
| } |
| |
| static zx_status_t sdhci_start_txn_locked(sdhci_device_t* dev, iotxn_t* txn) { |
| sdmmc_protocol_data_t* pdata = iotxn_pdata(txn, sdmmc_protocol_data_t); |
| |
| volatile struct sdhci_regs* regs = dev->regs; |
| const uint32_t arg = pdata->arg; |
| const uint16_t blkcnt = pdata->blockcount; |
| const uint16_t blksiz = pdata->blocksize; |
| uint32_t cmd = pdata->cmd; |
| |
| zx_status_t st = ZX_OK; |
| |
| #if 1 |
| xprintf("sdhci: start_txn cmd=0x%08x (data %d) blkcnt %u blksiz %u length %" PRIu64 "\n", |
| cmd, !!(cmd & SDMMC_RESP_DATA_PRESENT), blkcnt, blksiz, txn->length); |
| #endif |
| |
| pdata->blockid = 0; |
| txn->actual = 0; |
| |
| // Every command requires that the Command Inhibit is unset. |
| uint32_t inhibit_mask = SDHCI_STATE_CMD_INHIBIT; |
| |
| // Busy type commands must also wait for the DATA Inhibit to be 0 UNLESS |
| // it's an abort command which can be issued with the data lines active. |
| if ((cmd & SDMMC_RESP_LEN_48B) && ((cmd & SDMMC_CMD_TYPE_ABORT) == 0)) { |
| inhibit_mask |= SDHCI_STATE_DAT_INHIBIT; |
| } |
| |
| // Wait for the inhibit masks from above to become 0 before issuing the |
| // command. |
| while (regs->state & inhibit_mask) |
| zx_nanosleep(zx_deadline_after(ZX_MSEC(1))); |
| |
| // This command has a data phase? |
| bool has_data = cmd & SDMMC_RESP_DATA_PRESENT; |
| bool use_dma = sdhci_supports_adma2_64bit(dev); |
| if (has_data) { |
| st = iotxn_physmap(txn); |
| if (st != ZX_OK) { |
| goto err; |
| } |
| iotxn_cacheop(txn, IOTXN_CACHE_CLEAN, 0, blkcnt * blksiz); |
| |
| if (use_dma) { |
| iotxn_phys_iter_t iter; |
| iotxn_phys_iter_init(&iter, txn, ADMA2_DESC_MAX_LENGTH); |
| |
| int count = 0; |
| size_t length; |
| zx_paddr_t paddr; |
| sdhci_adma64_desc_t* desc = dev->descs; |
| for (;;) { |
| length = iotxn_phys_iter_next(&iter, &paddr); |
| if (length == 0) { |
| if (desc != dev->descs) { |
| desc -= 1; |
| desc->end = 1; // set end bit on the last descriptor |
| break; |
| } else { |
| xprintf("sdhci: empty descriptor list!\n"); |
| st = ZX_ERR_NOT_SUPPORTED; |
| goto err; |
| } |
| } else if (length > ADMA2_DESC_MAX_LENGTH) { |
| xprintf("sdhci: chunk size > %zu is unsupported\n", length); |
| st = ZX_ERR_NOT_SUPPORTED; |
| goto err; |
| } else if ((++count) > DMA_DESC_COUNT) { |
| xprintf("sdhci: txn with more than %zd chunks is unsupported\n", length); |
| st = ZX_ERR_NOT_SUPPORTED; |
| goto err; |
| } |
| desc->length = length & 0xffff; // 0 = 0x10000 bytes |
| desc->address = paddr; |
| desc->attr = 0; |
| desc->valid = 1; |
| desc->act2 = 1; // transfer data |
| desc += 1; |
| } |
| |
| #if TRACE |
| desc = dev->descs; |
| do { |
| xprintf("desc: addr=0x%" PRIx64 " length=0x%04x attr=0x%04x\n", desc->address, desc->length, desc->attr); |
| } while (!(desc++)->end); |
| #endif |
| |
| zx_paddr_t desc_phys = io_buffer_phys(&dev->iobuf); |
| dev->regs->admaaddr0 = LO32(desc_phys); |
| dev->regs->admaaddr1 = HI32(desc_phys); |
| |
| cmd |= SDHCI_XFERMODE_DMA_ENABLE; |
| |
| } else { |
| ZX_DEBUG_ASSERT(txn->phys_count == 1); |
| regs->arg2 = iotxn_phys(txn) + dev->dma_offset; |
| } |
| |
| if (cmd & SDMMC_CMD_MULTI_BLK) { |
| cmd |= SDMMC_CMD_AUTO12; |
| } |
| } |
| |
| regs->blkcntsiz = (blksiz | (blkcnt << 16)); |
| |
| regs->arg1 = arg; |
| |
| // Unmask and enable command complete interrupt |
| regs->irqmsk = error_interrupts | normal_interrupts; |
| regs->irqen = error_interrupts | SDHCI_IRQ_CMD_CPLT; |
| |
| // Clear any pending interrupts before starting the transaction. |
| regs->irq = regs->irqen; |
| |
| // And we're off to the races! |
| regs->cmd = cmd; |
| return ZX_OK; |
| err: |
| return st; |
| } |
| |
| static void sdhci_iotxn_queue(void* ctx, iotxn_t* txn) { |
| // Ensure that the offset is some multiple of the block size, we don't allow |
| // writes that are partway into a block. |
| if (txn->offset % SDHC_BLOCK_SIZE) { |
| printf("sdhci: iotxn offset not aligned to block boundary, " |
| "offset =%" PRIu64", block size = %d\n", txn->offset, SDHC_BLOCK_SIZE); |
| iotxn_complete(txn, ZX_ERR_INVALID_ARGS, 0); |
| return; |
| } |
| |
| // Ensure that the length of the write is some multiple of the block size. |
| if (txn->length % SDHC_BLOCK_SIZE) { |
| printf("sdhci: iotxn length not aligned to block boundary, " |
| "offset =%" PRIu64", block size = %d\n", txn->length, SDHC_BLOCK_SIZE); |
| iotxn_complete(txn, ZX_ERR_INVALID_ARGS, 0); |
| return; |
| } |
| |
| sdhci_device_t* dev = ctx; |
| |
| // One at a time for now |
| mtx_lock(&dev->mtx); |
| if (dev->pending != NULL) { |
| mtx_unlock(&dev->mtx); |
| printf("sdhci: only one outstanding iotxn is allowed\n"); |
| iotxn_complete(txn, ZX_ERR_NO_RESOURCES, 0); |
| return; |
| } |
| |
| // Start the txn |
| dev->pending = txn; |
| zx_status_t st; |
| if ((st = sdhci_start_txn_locked(dev, txn)) != ZX_OK) { |
| dev->pending = NULL; |
| mtx_unlock(&dev->mtx); |
| iotxn_complete(txn, ZX_ERR_NO_RESOURCES, 0); |
| return; |
| } |
| |
| mtx_unlock(&dev->mtx); |
| |
| // Wait for completion |
| do { |
| completion_wait(&dev->pending_completion, ZX_TIME_INFINITE); |
| completion_reset(&dev->pending_completion); |
| |
| mtx_lock(&dev->mtx); |
| |
| if (!dev->completed || (dev->completed != txn)) { |
| printf("sdhci: spurious completion\n"); |
| mtx_unlock(&dev->mtx); |
| continue; |
| |
| } else { |
| dev->completed = NULL; |
| mtx_unlock(&dev->mtx); |
| break; |
| } |
| } while (true); |
| |
| iotxn_complete(txn, txn->status, txn->actual); |
| } |
| |
| static zx_status_t sdhci_set_bus_frequency(sdhci_device_t* dev, uint32_t target_freq) { |
| const uint32_t divider = get_clock_divider(dev->base_clock, target_freq); |
| const uint8_t divider_lo = divider & 0xff; |
| const uint8_t divider_hi = (divider >> 8) & 0x3; |
| |
| volatile struct sdhci_regs* regs = dev->regs; |
| |
| uint32_t iterations = 0; |
| while (regs->state & (SDHCI_STATE_CMD_INHIBIT | SDHCI_STATE_DAT_INHIBIT)) { |
| if (++iterations > 1000) |
| return ZX_ERR_TIMED_OUT; |
| |
| zx_nanosleep(zx_deadline_after(ZX_MSEC(1))); |
| } |
| |
| // Turn off the SD clock before messing with the clock rate. |
| regs->ctrl1 &= ~SDHCI_SD_CLOCK_ENABLE; |
| zx_nanosleep(zx_deadline_after(ZX_MSEC(2))); |
| |
| // Write the new divider into the control register. |
| uint32_t ctrl1 = regs->ctrl1; |
| ctrl1 &= ~0xffe0; |
| ctrl1 |= ((divider_lo << 8) | (divider_hi << 6)); |
| regs->ctrl1 = ctrl1; |
| zx_nanosleep(zx_deadline_after(ZX_MSEC(2))); |
| |
| // Turn the SD clock back on. |
| regs->ctrl1 |= SDHCI_SD_CLOCK_ENABLE; |
| zx_nanosleep(zx_deadline_after(ZX_MSEC(2))); |
| |
| return ZX_OK; |
| } |
| |
| static zx_status_t sdhci_set_timing(sdhci_device_t* dev, uint32_t timing) { |
| // Toggle high-speed |
| if (timing != SDMMC_TIMING_LEGACY) { |
| dev->regs->ctrl0 |= SDHCI_HOSTCTRL_HIGHSPEED_ENABLE; |
| } else { |
| dev->regs->ctrl0 &= ~SDHCI_HOSTCTRL_HIGHSPEED_ENABLE; |
| } |
| |
| // Disable SD clock before changing UHS timing |
| dev->regs->ctrl1 &= ~SDHCI_SD_CLOCK_ENABLE; |
| zx_nanosleep(zx_deadline_after(ZX_MSEC(2))); |
| |
| uint32_t ctrl2 = dev->regs->ctrl2 & ~SDHCI_HOSTCTRL2_UHS_MODE_SELECT_MASK; |
| if (timing == SDMMC_TIMING_HS200) { |
| ctrl2 |= SDHCI_HOSTCTRL2_UHS_MODE_SELECT_SDR104; |
| } else if (timing == SDMMC_TIMING_HS400) { |
| ctrl2 |= SDHCI_HOSTCTRL2_UHS_MODE_SELECT_HS400; |
| } |
| dev->regs->ctrl2 = ctrl2; |
| |
| // Turn the SD clock back on. |
| dev->regs->ctrl1 |= SDHCI_SD_CLOCK_ENABLE; |
| zx_nanosleep(zx_deadline_after(ZX_MSEC(2))); |
| |
| return ZX_OK; |
| } |
| |
| static void sdhci_hw_reset(sdhci_device_t* dev) { |
| if (dev->sdhci.ops->hw_reset) { |
| dev->sdhci.ops->hw_reset(dev->sdhci.ctx); |
| } |
| } |
| |
| static zx_status_t sdhci_set_bus_width(sdhci_device_t* dev, const uint32_t new_bus_width) { |
| if ((new_bus_width == SDMMC_BUS_WIDTH_8) && |
| !(dev->regs->caps0 & SDHCI_CORECFG_8_BIT_SUPPORT)) { |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| |
| switch (new_bus_width) { |
| case SDMMC_BUS_WIDTH_1: |
| dev->regs->ctrl0 &= ~SDHCI_HOSTCTRL_EXT_DATA_WIDTH; |
| dev->regs->ctrl0 &= ~SDHCI_HOSTCTRL_FOUR_BIT_BUS_WIDTH; |
| break; |
| case SDMMC_BUS_WIDTH_4: |
| dev->regs->ctrl0 &= ~SDHCI_HOSTCTRL_EXT_DATA_WIDTH; |
| dev->regs->ctrl0 |= SDHCI_HOSTCTRL_FOUR_BIT_BUS_WIDTH; |
| break; |
| case SDMMC_BUS_WIDTH_8: |
| dev->regs->ctrl0 |= SDHCI_HOSTCTRL_EXT_DATA_WIDTH; |
| break; |
| default: |
| return ZX_ERR_INVALID_ARGS; |
| } |
| |
| return ZX_OK; |
| } |
| |
| static zx_status_t sdhci_set_signal_voltage(sdhci_device_t* dev, uint32_t new_voltage) { |
| |
| switch (new_voltage) { |
| case SDMMC_SIGNAL_VOLTAGE_330: |
| case SDMMC_SIGNAL_VOLTAGE_180: |
| break; |
| default: |
| return ZX_ERR_INVALID_ARGS; |
| } |
| |
| volatile struct sdhci_regs* regs = dev->regs; |
| |
| // Disable the SD clock before messing with the voltage. |
| regs->ctrl1 &= ~SDHCI_SD_CLOCK_ENABLE; |
| zx_nanosleep(zx_deadline_after(ZX_MSEC(2))); |
| |
| if (new_voltage == SDMMC_SIGNAL_VOLTAGE_180) { |
| regs->ctrl2 |= SDHCI_HOSTCTRL2_1P8V_SIGNALLING_ENA; |
| // 1.8V regulator out should be stable within 5ms |
| zx_nanosleep(zx_deadline_after(ZX_MSEC(5))); |
| #if TRACE |
| if (!(regs->ctrl2 & SDHCI_HOSTCTRL2_1P8V_SIGNALLING_ENA)) { |
| xprintf("sdhci: 1.8V regulator output did not become stable\n"); |
| } |
| #endif |
| } else { |
| regs->ctrl2 &= ~SDHCI_HOSTCTRL2_1P8V_SIGNALLING_ENA; |
| // 3.3V regulator out should be stable within 5ms |
| zx_nanosleep(zx_deadline_after(ZX_MSEC(5))); |
| #if TRACE |
| if (regs->ctrl2 & SDHCI_HOSTCTRL2_1P8V_SIGNALLING_ENA) { |
| xprintf("sdhci: 3.3V regulator output did not become stable\n"); |
| } |
| #endif |
| } |
| |
| // Make sure our changes are acknolwedged. |
| uint32_t expected_mask = SDHCI_PWRCTRL_SD_BUS_POWER; |
| if (new_voltage == SDMMC_SIGNAL_VOLTAGE_180) { |
| expected_mask |= SDHCI_PWRCTRL_SD_BUS_VOLTAGE_1P8V; |
| } else { |
| expected_mask |= SDHCI_PWRCTRL_SD_BUS_VOLTAGE_3P3V; |
| } |
| if ((regs->ctrl0 & expected_mask) != expected_mask) { |
| xprintf("sdhci: after voltage switch ctrl0=0x%08x, expected=0x%08x\n", regs->ctrl0, expected_mask); |
| return ZX_ERR_INTERNAL; |
| } |
| |
| // Turn the clock back on |
| regs->ctrl1 |= SDHCI_SD_CLOCK_ENABLE; |
| zx_nanosleep(zx_deadline_after(ZX_MSEC(2))); |
| |
| return ZX_OK; |
| } |
| |
| static zx_status_t sdhci_ioctl(void* ctx, uint32_t op, |
| const void* in_buf, size_t in_len, |
| void* out_buf, size_t out_len, size_t* out_actual) { |
| sdhci_device_t* dev = ctx; |
| uint32_t* arg = (uint32_t*)in_buf; |
| |
| switch (op) { |
| case IOCTL_SDMMC_SET_SIGNAL_VOLTAGE: |
| if (in_len < sizeof(*arg)) { |
| return ZX_ERR_INVALID_ARGS; |
| } else { |
| return sdhci_set_signal_voltage(dev, *arg); |
| } |
| case IOCTL_SDMMC_SET_BUS_WIDTH: |
| if (in_len < sizeof(*arg)) { |
| return ZX_ERR_INVALID_ARGS; |
| } else { |
| return sdhci_set_bus_width(dev, *arg); |
| } |
| case IOCTL_SDMMC_SET_BUS_FREQ: |
| if (in_len < sizeof(*arg)) { |
| return ZX_ERR_INVALID_ARGS; |
| } else { |
| return sdhci_set_bus_frequency(dev, *arg); |
| } |
| case IOCTL_SDMMC_SET_TIMING: |
| if (in_len < sizeof(*arg)) { |
| return ZX_ERR_INVALID_ARGS; |
| } else { |
| return sdhci_set_timing(dev, *arg); |
| } |
| case IOCTL_SDMMC_HW_RESET: |
| sdhci_hw_reset(dev); |
| return ZX_OK; |
| } |
| |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| |
| static void sdhci_unbind(void* ctx) { |
| sdhci_device_t* dev = ctx; |
| device_remove(dev->mxdev); |
| } |
| |
| static void sdhci_release(void* ctx) { |
| sdhci_device_t* dev = ctx; |
| free(dev); |
| } |
| |
| static zx_protocol_device_t sdhci_device_proto = { |
| .version = DEVICE_OPS_VERSION, |
| .iotxn_queue = sdhci_iotxn_queue, |
| .ioctl = sdhci_ioctl, |
| .unbind = sdhci_unbind, |
| .release = sdhci_release, |
| }; |
| |
| static zx_status_t sdhci_controller_init(sdhci_device_t* dev) { |
| // Reset the controller. |
| uint32_t ctrl1 = dev->regs->ctrl1; |
| |
| // Perform a software reset against both the DAT and CMD interface. |
| ctrl1 |= SDHCI_SOFTWARE_RESET_ALL; |
| |
| // Disable both clocks. |
| ctrl1 &= ~(SDHCI_INTERNAL_CLOCK_ENABLE | SDHCI_SD_CLOCK_ENABLE); |
| |
| // Write the register back to the device. |
| dev->regs->ctrl1 = ctrl1; |
| |
| // Wait for reset to take place. The reset is comleted when all three |
| // of the following flags are reset. |
| const uint32_t target_mask = (SDHCI_SOFTWARE_RESET_ALL | |
| SDHCI_SOFTWARE_RESET_CMD | |
| SDHCI_SOFTWARE_RESET_DAT); |
| zx_status_t status = ZX_OK; |
| if ((status = sdhci_wait_for_reset(dev, target_mask, ZX_SEC(1))) != ZX_OK) { |
| goto fail; |
| } |
| |
| // allocate and setup DMA descriptor |
| if (sdhci_supports_adma2_64bit(dev)) { |
| status = io_buffer_init(&dev->iobuf, DMA_DESC_COUNT * sizeof(sdhci_adma64_desc_t), IO_BUFFER_RW); |
| if (status != ZX_OK) { |
| xprintf("sdhci: error allocating DMA descriptors\n"); |
| goto fail; |
| } |
| dev->descs = io_buffer_virt(&dev->iobuf); |
| |
| // Select ADMA2 |
| dev->regs->ctrl0 |= SDHCI_HOSTCTRL_DMA_SELECT_ADMA2; |
| } |
| |
| // Configure the clock. |
| ctrl1 = dev->regs->ctrl1; |
| ctrl1 |= SDHCI_INTERNAL_CLOCK_ENABLE; |
| |
| // SDHCI Versions 1.00 and 2.00 handle the clock divider slightly |
| // differently compared to SDHCI version 3.00. Since this driver doesn't |
| // support SDHCI versions < 3.00, we ignore this incongruency for now. |
| // |
| // V3.00 supports a 10 bit divider where the SD clock frequency is defined |
| // as F/(2*D) where F is the base clock frequency and D is the divider. |
| const uint32_t divider = get_clock_divider(dev->base_clock, SD_FREQ_SETUP_HZ); |
| const uint8_t divider_lo = divider & 0xff; |
| const uint8_t divider_hi = (divider >> 8) & 0x3; |
| ctrl1 |= ((divider_lo << 8) | (divider_hi << 6)); |
| |
| // Set the command timeout. |
| ctrl1 |= (0xe << 16); |
| |
| // Write back the clock frequency, command timeout and clock enable bits. |
| dev->regs->ctrl1 = ctrl1; |
| |
| // Wait for the clock to stabilize. |
| zx_time_t deadline = zx_time_get(ZX_CLOCK_MONOTONIC) + ZX_SEC(1); |
| while (true) { |
| if (((dev->regs->ctrl1) & SDHCI_INTERNAL_CLOCK_STABLE) != 0) |
| break; |
| |
| if (zx_time_get(ZX_CLOCK_MONOTONIC) > deadline) { |
| xprintf("sdhci: Clock did not stabilize in time\n"); |
| status = ZX_ERR_TIMED_OUT; |
| goto fail; |
| } |
| } |
| |
| // Enable the SD clock. |
| zx_nanosleep(zx_deadline_after(ZX_MSEC(2))); |
| ctrl1 |= dev->regs->ctrl1; |
| ctrl1 |= SDHCI_SD_CLOCK_ENABLE; |
| dev->regs->ctrl1 = ctrl1; |
| zx_nanosleep(zx_deadline_after(ZX_MSEC(2))); |
| |
| // Cut voltage to the card |
| dev->regs->ctrl0 &= ~SDHCI_PWRCTRL_SD_BUS_POWER; |
| |
| // Set SD bus voltage to maximum supported by the host controller |
| const uint32_t caps = dev->regs->caps0; |
| uint32_t ctrl0 = dev->regs->ctrl0 & ~SDHCI_PWRCTRL_SD_BUS_VOLTAGE_MASK; |
| if (caps & SDHCI_CORECFG_3P3_VOLT_SUPPORT) { |
| ctrl0 |= SDHCI_PWRCTRL_SD_BUS_VOLTAGE_3P3V; |
| } else if (caps & SDHCI_CORECFG_3P0_VOLT_SUPPORT) { |
| ctrl0 |= SDHCI_PWRCTRL_SD_BUS_VOLTAGE_3P0V; |
| } else { |
| ctrl0 |= SDHCI_PWRCTRL_SD_BUS_VOLTAGE_1P8V; |
| } |
| dev->regs->ctrl0 = ctrl0; |
| |
| // Restore voltage to the card. |
| dev->regs->ctrl0 |= SDHCI_PWRCTRL_SD_BUS_POWER; |
| |
| // Disable all interrupts |
| dev->regs->irqen = 0; |
| dev->regs->irq = 0xffffffff; |
| |
| return ZX_OK; |
| fail: |
| return status; |
| } |
| |
| static zx_status_t sdhci_bind(void* ctx, zx_device_t* parent, void** cookie) { |
| sdhci_device_t* dev = calloc(1, sizeof(sdhci_device_t)); |
| if (!dev) { |
| return ZX_ERR_NO_MEMORY; |
| } |
| |
| zx_status_t status = ZX_OK; |
| if (device_get_protocol(parent, ZX_PROTOCOL_SDHCI, (void*)&dev->sdhci)) { |
| status = ZX_ERR_NOT_SUPPORTED; |
| goto fail; |
| } |
| |
| // Map the Device Registers so that we can perform MMIO against the device. |
| status = dev->sdhci.ops->get_mmio(dev->sdhci.ctx, &dev->regs); |
| if (status != ZX_OK) { |
| xprintf("sdhci: error %d in get_mmio\n", status); |
| goto fail; |
| } |
| |
| dev->irq_handle = dev->sdhci.ops->get_interrupt(dev->sdhci.ctx); |
| if (dev->irq_handle < 0) { |
| xprintf("sdhci: error %d in get_interrupt\n", status); |
| status = dev->irq_handle; |
| goto fail; |
| } |
| |
| thrd_t irq_thread; |
| if (thrd_create_with_name(&irq_thread, sdhci_irq_thread, dev, "sdhci_irq_thread") != thrd_success) { |
| xprintf("sdhci: failed to create irq thread\n"); |
| goto fail; |
| } |
| thrd_detach(irq_thread); |
| |
| dev->irq_completion = COMPLETION_INIT; |
| dev->pending_completion = COMPLETION_INIT; |
| dev->parent = parent; |
| |
| // Ensure that we're SDv3 or above. |
| const uint16_t vrsn = (dev->regs->slotirqversion >> 16) & 0xff; |
| if (vrsn < SDHCI_VERSION_3) { |
| xprintf("sdhci: SD version is %u, only version %u and above are " |
| "supported\n", vrsn, SDHCI_VERSION_3); |
| status = ZX_ERR_NOT_SUPPORTED; |
| goto fail; |
| } |
| xprintf("sdhci: controller version %d\n", vrsn); |
| |
| dev->base_clock = ((dev->regs->caps0 >> 8) & 0xff) * 1000000; /* mhz */ |
| if (dev->base_clock == 0) { |
| // try to get controller specific base clock |
| dev->base_clock = dev->sdhci.ops->get_base_clock(dev->sdhci.ctx); |
| } |
| if (dev->base_clock == 0) { |
| xprintf("sdhci: base clock is 0!\n"); |
| status = ZX_ERR_INTERNAL; |
| goto fail; |
| } |
| dev->dma_offset = dev->sdhci.ops->get_dma_offset(dev->sdhci.ctx); |
| dev->quirks = dev->sdhci.ops->get_quirks(dev->sdhci.ctx); |
| |
| // initialize the controller |
| status = sdhci_controller_init(dev); |
| if (status != ZX_OK) { |
| goto fail; |
| } |
| |
| // Create the device. |
| device_add_args_t args = { |
| .version = DEVICE_ADD_ARGS_VERSION, |
| .name = "sdhci", |
| .ctx = dev, |
| .ops = &sdhci_device_proto, |
| .proto_id = ZX_PROTOCOL_SDMMC, |
| }; |
| |
| status = device_add(parent, &args, &dev->mxdev); |
| if (status != ZX_OK) { |
| goto fail; |
| } |
| return ZX_OK; |
| fail: |
| if (dev) { |
| if (dev->irq_handle != ZX_HANDLE_INVALID) { |
| zx_handle_close(dev->irq_handle); |
| } |
| if (dev->iobuf.vmo_handle != ZX_HANDLE_INVALID) { |
| zx_handle_close(dev->iobuf.vmo_handle); |
| } |
| free(dev); |
| } |
| return status; |
| } |
| |
| static zx_driver_ops_t sdhci_driver_ops = { |
| .version = DRIVER_OPS_VERSION, |
| .bind = sdhci_bind, |
| }; |
| |
| // The formatter does not play nice with these macros. |
| // clang-format off |
| ZIRCON_DRIVER_BEGIN(sdhci, sdhci_driver_ops, "zircon", "0.1", 1) |
| BI_MATCH_IF(EQ, BIND_PROTOCOL, ZX_PROTOCOL_SDHCI), |
| ZIRCON_DRIVER_END(sdhci) |
| // clang-format on |