| // 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. |
| |
| // Standard Includes |
| #include <endian.h> |
| #include <inttypes.h> |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <sys/param.h> |
| #include <threads.h> |
| |
| // DDK Includes |
| #include <ddk/binding.h> |
| #include <ddk/device.h> |
| #include <ddk/iotxn.h> |
| #include <ddk/protocol/sdmmc.h> |
| |
| // Zircon Includes |
| #include <zircon/threads.h> |
| #include <sync/completion.h> |
| #include <pretty/hexdump.h> |
| |
| #include "sdmmc.h" |
| |
| // TODO: |
| // * close ended transfers |
| // * HS200/HS400 |
| |
| // Various transfer states that the card can be in. |
| #define SDMMC_STATE_TRAN 0x4 |
| #define SDMMC_STATE_RECV 0x5 |
| #define SDMMC_STATE_DATA 0x6 |
| |
| #define TRACE 0 |
| |
| #if TRACE |
| #define xprintf(fmt...) printf(fmt) |
| #else |
| #define xprintf(fmt...) \ |
| do { \ |
| } while (0) |
| #endif |
| |
| static void sdmmc_txn_cplt(iotxn_t* request, void* cookie) { |
| completion_signal((completion_t*)cookie); |
| }; |
| |
| zx_status_t sdmmc_do_command(zx_device_t* dev, const uint32_t cmd, |
| const uint32_t arg, iotxn_t* txn) { |
| sdmmc_protocol_data_t* pdata = iotxn_pdata(txn, sdmmc_protocol_data_t); |
| pdata->cmd = cmd; |
| pdata->arg = arg; |
| |
| completion_t cplt = COMPLETION_INIT; |
| txn->complete_cb = sdmmc_txn_cplt; |
| txn->cookie = &cplt; |
| |
| iotxn_queue(dev, txn); |
| |
| completion_wait(&cplt, ZX_TIME_INFINITE); |
| |
| return txn->status; |
| } |
| |
| static zx_off_t sdmmc_get_size(void* ctx) { |
| sdmmc_t* sdmmc = ctx; |
| return sdmmc->capacity; |
| } |
| |
| static void sdmmc_get_info(block_info_t* info, void* ctx) { |
| memset(info, 0, sizeof(*info)); |
| // Since we only support SDHC cards, the blocksize must be the SDHC |
| // blocksize. |
| info->block_size = SDHC_BLOCK_SIZE; |
| info->block_count = sdmmc_get_size(ctx) / SDHC_BLOCK_SIZE; |
| } |
| |
| static zx_status_t sdmmc_ioctl(void* ctx, uint32_t op, const void* cmd, |
| size_t cmdlen, void* reply, size_t max, size_t* out_actual) { |
| switch (op) { |
| case IOCTL_BLOCK_GET_INFO: { |
| block_info_t* info = reply; |
| if (max < sizeof(*info)) |
| return ZX_ERR_BUFFER_TOO_SMALL; |
| sdmmc_get_info(info, ctx); |
| *out_actual = sizeof(*info); |
| return ZX_OK; |
| } |
| case IOCTL_BLOCK_GET_NAME: { |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| case IOCTL_DEVICE_SYNC: { |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| default: |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| return 0; |
| } |
| |
| static void sdmmc_unbind(void* ctx) { |
| sdmmc_t* sdmmc = ctx; |
| device_remove(sdmmc->mxdev); |
| } |
| |
| static void sdmmc_release(void* ctx) { |
| sdmmc_t* sdmmc = ctx; |
| free(sdmmc); |
| } |
| |
| static void sdmmc_iotxn_queue(void* ctx, iotxn_t* txn) { |
| if (txn->offset % SDHC_BLOCK_SIZE) { |
| xprintf("sdmmc: 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; |
| } |
| |
| if (txn->length % SDHC_BLOCK_SIZE) { |
| xprintf("sdmmc: 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; |
| } |
| |
| iotxn_t* emmc_txn = NULL; |
| sdmmc_t* sdmmc = ctx; |
| zx_device_t* sdmmc_mxdev = sdmmc->host_mxdev; |
| uint32_t cmd = 0; |
| |
| // Figure out which SD command we need to issue. |
| switch(txn->opcode) { |
| case IOTXN_OP_READ: |
| if (txn->length > SDHC_BLOCK_SIZE) { |
| cmd = SDMMC_READ_MULTIPLE_BLOCK; |
| } else { |
| cmd = SDMMC_READ_BLOCK; |
| } |
| break; |
| case IOTXN_OP_WRITE: |
| if (txn->length > SDHC_BLOCK_SIZE) { |
| cmd = SDMMC_WRITE_MULTIPLE_BLOCK; |
| } else { |
| cmd = SDMMC_WRITE_BLOCK; |
| } |
| break; |
| default: |
| // Invalid opcode? |
| iotxn_complete(txn, ZX_ERR_INVALID_ARGS, 0); |
| return; |
| } |
| |
| if (iotxn_alloc(&emmc_txn, IOTXN_ALLOC_CONTIGUOUS | IOTXN_ALLOC_POOL, txn->length) != ZX_OK) { |
| xprintf("sdmmc: error allocating emmc iotxn\n"); |
| iotxn_complete(txn, ZX_ERR_INTERNAL, 0); |
| return; |
| } |
| emmc_txn->opcode = txn->opcode; |
| emmc_txn->flags = txn->flags; |
| emmc_txn->offset = txn->offset; |
| emmc_txn->length = txn->length; |
| emmc_txn->protocol = ZX_PROTOCOL_SDMMC; |
| sdmmc_protocol_data_t* pdata = iotxn_pdata(emmc_txn, sdmmc_protocol_data_t); |
| |
| uint8_t current_state; |
| const size_t max_attempts = 10; |
| size_t attempt = 0; |
| for (; attempt <= max_attempts; attempt++) { |
| zx_status_t rc = sdmmc_do_command(sdmmc_mxdev, SDMMC_SEND_STATUS, |
| sdmmc->rca << 16, emmc_txn); |
| if (rc != ZX_OK) { |
| iotxn_complete(txn, rc, 0); |
| goto out; |
| } |
| |
| current_state = (pdata->response[0] >> 9) & 0xf; |
| |
| if (current_state == SDMMC_STATE_RECV) { |
| rc = sdmmc_do_command(sdmmc_mxdev, SDMMC_STOP_TRANSMISSION, 0, emmc_txn); |
| continue; |
| } else if (current_state == SDMMC_STATE_TRAN) { |
| break; |
| } |
| |
| zx_nanosleep(zx_deadline_after(ZX_MSEC(10))); |
| } |
| |
| if (attempt == max_attempts) { |
| // Too many retries, fail. |
| iotxn_complete(txn, ZX_ERR_BAD_STATE, 0); |
| goto out; |
| } |
| |
| // Which block to operate against. |
| const uint32_t blkid = emmc_txn->offset / SDHC_BLOCK_SIZE; |
| |
| pdata->blockcount = txn->length / SDHC_BLOCK_SIZE; |
| pdata->blocksize = SDHC_BLOCK_SIZE; |
| |
| void* buffer; |
| size_t bytes_processed = 0; |
| if (txn->opcode == IOTXN_OP_WRITE) { |
| iotxn_mmap(txn, &buffer); |
| iotxn_copyto(emmc_txn, buffer, txn->length, 0); |
| bytes_processed = txn->length; |
| } |
| |
| zx_status_t rc = sdmmc_do_command(sdmmc_mxdev, cmd, blkid, emmc_txn); |
| if (rc != ZX_OK) { |
| iotxn_complete(txn, rc, 0); |
| } |
| |
| if (txn->opcode == IOTXN_OP_READ) { |
| bytes_processed = MIN(emmc_txn->actual, txn->length); |
| iotxn_mmap(emmc_txn, &buffer); |
| iotxn_copyto(txn, buffer, bytes_processed, 0); |
| } |
| |
| iotxn_complete(txn, ZX_OK, bytes_processed); |
| |
| out: |
| if (emmc_txn) |
| iotxn_release(emmc_txn); |
| } |
| |
| // Block device protocol. |
| static zx_protocol_device_t sdmmc_device_proto = { |
| .version = DEVICE_OPS_VERSION, |
| .ioctl = sdmmc_ioctl, |
| .unbind = sdmmc_unbind, |
| .release = sdmmc_release, |
| .iotxn_queue = sdmmc_iotxn_queue, |
| .get_size = sdmmc_get_size, |
| }; |
| |
| static void sdmmc_block_set_callbacks(void* ctx, block_callbacks_t* cb) { |
| sdmmc_t* device = ctx; |
| device->callbacks = cb; |
| } |
| |
| static void sdmmc_block_get_info(void* ctx, block_info_t* info) { |
| sdmmc_t* device = ctx; |
| sdmmc_get_info(info, device); |
| } |
| |
| static void sdmmc_block_complete(iotxn_t* txn, void* cookie) { |
| sdmmc_t* dev; |
| memcpy(&dev, txn->extra, sizeof(sdmmc_t*)); |
| dev->callbacks->complete(cookie, txn->status); |
| iotxn_release(txn); |
| } |
| |
| static void block_do_txn(sdmmc_t* dev, uint32_t opcode, zx_handle_t vmo, uint64_t length, uint64_t vmo_offset, uint64_t dev_offset, void* cookie) { |
| block_info_t info; |
| sdmmc_get_info(&info, dev); |
| |
| if ((dev_offset % info.block_size) || (length % info.block_size)) { |
| dev->callbacks->complete(cookie, ZX_ERR_INVALID_ARGS); |
| return; |
| } |
| uint64_t size = info.block_size * info.block_count; |
| if ((dev_offset >= size) || (length >= (size - dev_offset))) { |
| dev->callbacks->complete(cookie, ZX_ERR_OUT_OF_RANGE); |
| return; |
| } |
| |
| zx_status_t status; |
| iotxn_t* txn; |
| if ((status = iotxn_alloc_vmo(&txn, IOTXN_ALLOC_POOL, vmo, vmo_offset, length)) != ZX_OK) { |
| dev->callbacks->complete(cookie, status); |
| return; |
| } |
| txn->opcode = opcode; |
| txn->length = length; |
| txn->offset = dev_offset; |
| txn->complete_cb = sdmmc_block_complete; |
| txn->cookie = cookie; |
| memcpy(txn->extra, &dev, sizeof(sdmmc_t*)); |
| iotxn_queue(dev->mxdev, txn); |
| } |
| |
| static void sdmmc_block_read(void* ctx, zx_handle_t vmo, uint64_t length, uint64_t vmo_offset, uint64_t dev_offset, void* cookie) { |
| block_do_txn(ctx, IOTXN_OP_READ, vmo, length, vmo_offset, dev_offset, cookie); |
| } |
| |
| static void sdmmc_block_write(void* ctx, zx_handle_t vmo, uint64_t length, uint64_t vmo_offset, uint64_t dev_offset, void* cookie) { |
| block_do_txn(ctx, IOTXN_OP_WRITE, vmo, length, vmo_offset, dev_offset, cookie); |
| } |
| |
| // Block core protocol |
| static block_protocol_ops_t sdmmc_block_ops = { |
| .set_callbacks = sdmmc_block_set_callbacks, |
| .get_info = sdmmc_block_get_info, |
| .read = sdmmc_block_read, |
| .write = sdmmc_block_write, |
| }; |
| |
| static int sdmmc_bootstrap_thread(void* arg) { |
| xprintf("sdmmc: bootstrap\n"); |
| zx_device_t* dev = arg; |
| |
| zx_status_t st; |
| sdmmc_t* sdmmc = NULL; |
| iotxn_t* setup_txn = NULL; |
| |
| // Allocate the device. |
| sdmmc = calloc(1, sizeof(*sdmmc)); |
| if (!sdmmc) { |
| xprintf("sdmmc: no memory to allocate sdmmc device!\n"); |
| goto err; |
| } |
| sdmmc->host_mxdev = dev; |
| |
| // Allocate a single iotxn that we use to bootstrap the card with. |
| if ((st = iotxn_alloc(&setup_txn, IOTXN_ALLOC_CONTIGUOUS, SDHC_BLOCK_SIZE)) != ZX_OK) { |
| xprintf("sdmmc: failed to allocate iotxn for setup, rc = %d\n", st); |
| goto err; |
| } |
| |
| // Reset the card. |
| device_ioctl(dev, IOCTL_SDMMC_HW_RESET, NULL, 0, NULL, 0, NULL); |
| |
| // No matter what state the card is in, issuing the GO_IDLE_STATE command will |
| // put the card into the idle state. |
| if ((st = sdmmc_do_command(dev, SDMMC_GO_IDLE_STATE, 0, setup_txn)) != ZX_OK) { |
| xprintf("sdmmc: SDMMC_GO_IDLE_STATE failed, retcode = %d\n", st); |
| goto err; |
| } |
| |
| // Probe for SD, then MMC |
| if ((st = sdmmc_probe_sd(sdmmc, setup_txn)) != ZX_OK) { |
| if ((st = sdmmc_probe_mmc(sdmmc, setup_txn)) != ZX_OK) { |
| xprintf("sdmmc: failed to probe\n"); |
| goto err; |
| } |
| } |
| |
| device_add_args_t args = { |
| .version = DEVICE_ADD_ARGS_VERSION, |
| .name = (sdmmc->type == SDMMC_TYPE_SD) ? "sd" : "mmc", |
| .ctx = sdmmc, |
| .ops = &sdmmc_device_proto, |
| .proto_id = ZX_PROTOCOL_BLOCK_CORE, |
| .proto_ops = &sdmmc_block_ops, |
| }; |
| |
| st = device_add(dev, &args, &sdmmc->mxdev); |
| if (st != ZX_OK) { |
| goto err; |
| } |
| |
| xprintf("sdmmc: bind success!\n"); |
| |
| return 0; |
| err: |
| if (sdmmc) { |
| free(sdmmc); |
| } |
| |
| if (setup_txn) |
| iotxn_release(setup_txn); |
| |
| return -1; |
| } |
| |
| static zx_status_t sdmmc_bind(void* ctx, zx_device_t* dev, void** cookie) { |
| // Create a bootstrap thread. |
| thrd_t bootstrap_thrd; |
| int thrd_rc = thrd_create_with_name(&bootstrap_thrd, |
| sdmmc_bootstrap_thread, dev, |
| "sdmmc_bootstrap_thread"); |
| if (thrd_rc != thrd_success) { |
| return thrd_status_to_zx_status(thrd_rc); |
| } |
| |
| thrd_detach(bootstrap_thrd); |
| return ZX_OK; |
| } |
| |
| static zx_driver_ops_t sdmmc_driver_ops = { |
| .version = DRIVER_OPS_VERSION, |
| .bind = sdmmc_bind, |
| }; |
| |
| // The formatter does not play nice with these macros. |
| // clang-format off |
| ZIRCON_DRIVER_BEGIN(sdmmc, sdmmc_driver_ops, "zircon", "0.1", 1) |
| BI_MATCH_IF(EQ, BIND_PROTOCOL, ZX_PROTOCOL_SDMMC), |
| ZIRCON_DRIVER_END(sdmmc) |
| // clang-format on |