| /* Copyright (c) 2015, The Linux Foundation. All rights reserved. |
| * |
| * This program is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License version 2 and |
| * only version 2 as published by the Free Software Foundation. |
| * |
| * This program is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| * GNU General Public License for more details. |
| */ |
| |
| #include <linux/delay.h> |
| #include <linux/highmem.h> |
| #include <linux/io.h> |
| #include <linux/module.h> |
| #include <linux/dma-mapping.h> |
| #include <linux/slab.h> |
| #include <linux/scatterlist.h> |
| #include <linux/platform_device.h> |
| #include <linux/ktime.h> |
| |
| #include <linux/mmc/mmc.h> |
| #include <linux/mmc/host.h> |
| #include <linux/mmc/card.h> |
| |
| #include "cqhci.h" |
| |
| #define DCMD_SLOT 31 |
| #define NUM_SLOTS 32 |
| |
| struct cqhci_slot { |
| struct mmc_request *mrq; |
| unsigned int flags; |
| #define CQHCI_EXTERNAL_TIMEOUT BIT(0) |
| #define CQHCI_COMPLETED BIT(1) |
| #define CQHCI_HOST_CRC BIT(2) |
| #define CQHCI_HOST_TIMEOUT BIT(3) |
| #define CQHCI_HOST_OTHER BIT(4) |
| }; |
| |
| static inline u8 *get_desc(struct cqhci_host *cq_host, u8 tag) |
| { |
| return cq_host->desc_base + (tag * cq_host->slot_sz); |
| } |
| |
| static inline u8 *get_link_desc(struct cqhci_host *cq_host, u8 tag) |
| { |
| u8 *desc = get_desc(cq_host, tag); |
| |
| return desc + cq_host->task_desc_len; |
| } |
| |
| static inline dma_addr_t get_trans_desc_dma(struct cqhci_host *cq_host, u8 tag) |
| { |
| return cq_host->trans_desc_dma_base + |
| (cq_host->mmc->max_segs * tag * |
| cq_host->trans_desc_len); |
| } |
| |
| static inline u8 *get_trans_desc(struct cqhci_host *cq_host, u8 tag) |
| { |
| return cq_host->trans_desc_base + |
| (cq_host->trans_desc_len * cq_host->mmc->max_segs * tag); |
| } |
| |
| static void setup_trans_desc(struct cqhci_host *cq_host, u8 tag) |
| { |
| u8 *link_temp; |
| dma_addr_t trans_temp; |
| |
| link_temp = get_link_desc(cq_host, tag); |
| trans_temp = get_trans_desc_dma(cq_host, tag); |
| |
| memset(link_temp, 0, cq_host->link_desc_len); |
| if (cq_host->link_desc_len > 8) |
| *(link_temp + 8) = 0; |
| |
| if (tag == DCMD_SLOT && (cq_host->mmc->caps2 & MMC_CAP2_CQE_DCMD)) { |
| *link_temp = CQHCI_VALID(0) | CQHCI_ACT(0) | CQHCI_END(1); |
| return; |
| } |
| |
| *link_temp = CQHCI_VALID(1) | CQHCI_ACT(0x6) | CQHCI_END(0); |
| |
| if (cq_host->dma64) { |
| __le64 *data_addr = (__le64 __force *)(link_temp + 4); |
| |
| data_addr[0] = cpu_to_le64(trans_temp); |
| } else { |
| __le32 *data_addr = (__le32 __force *)(link_temp + 4); |
| |
| data_addr[0] = cpu_to_le32(trans_temp); |
| } |
| } |
| |
| static void cqhci_set_irqs(struct cqhci_host *cq_host, u32 set) |
| { |
| cqhci_writel(cq_host, set, CQHCI_ISTE); |
| cqhci_writel(cq_host, set, CQHCI_ISGE); |
| } |
| |
| #define DRV_NAME "cqhci" |
| |
| #define CQHCI_DUMP(f, x...) \ |
| pr_err("%s: " DRV_NAME ": " f, mmc_hostname(mmc), ## x) |
| |
| static void cqhci_dumpregs(struct cqhci_host *cq_host) |
| { |
| struct mmc_host *mmc = cq_host->mmc; |
| |
| CQHCI_DUMP("============ CQHCI REGISTER DUMP ===========\n"); |
| |
| CQHCI_DUMP("Caps: 0x%08x | Version: 0x%08x\n", |
| cqhci_readl(cq_host, CQHCI_CAP), |
| cqhci_readl(cq_host, CQHCI_VER)); |
| CQHCI_DUMP("Config: 0x%08x | Control: 0x%08x\n", |
| cqhci_readl(cq_host, CQHCI_CFG), |
| cqhci_readl(cq_host, CQHCI_CTL)); |
| CQHCI_DUMP("Int stat: 0x%08x | Int enab: 0x%08x\n", |
| cqhci_readl(cq_host, CQHCI_IS), |
| cqhci_readl(cq_host, CQHCI_ISTE)); |
| CQHCI_DUMP("Int sig: 0x%08x | Int Coal: 0x%08x\n", |
| cqhci_readl(cq_host, CQHCI_ISGE), |
| cqhci_readl(cq_host, CQHCI_IC)); |
| CQHCI_DUMP("TDL base: 0x%08x | TDL up32: 0x%08x\n", |
| cqhci_readl(cq_host, CQHCI_TDLBA), |
| cqhci_readl(cq_host, CQHCI_TDLBAU)); |
| CQHCI_DUMP("Doorbell: 0x%08x | TCN: 0x%08x\n", |
| cqhci_readl(cq_host, CQHCI_TDBR), |
| cqhci_readl(cq_host, CQHCI_TCN)); |
| CQHCI_DUMP("Dev queue: 0x%08x | Dev Pend: 0x%08x\n", |
| cqhci_readl(cq_host, CQHCI_DQS), |
| cqhci_readl(cq_host, CQHCI_DPT)); |
| CQHCI_DUMP("Task clr: 0x%08x | SSC1: 0x%08x\n", |
| cqhci_readl(cq_host, CQHCI_TCLR), |
| cqhci_readl(cq_host, CQHCI_SSC1)); |
| CQHCI_DUMP("SSC2: 0x%08x | DCMD rsp: 0x%08x\n", |
| cqhci_readl(cq_host, CQHCI_SSC2), |
| cqhci_readl(cq_host, CQHCI_CRDCT)); |
| CQHCI_DUMP("RED mask: 0x%08x | TERRI: 0x%08x\n", |
| cqhci_readl(cq_host, CQHCI_RMEM), |
| cqhci_readl(cq_host, CQHCI_TERRI)); |
| CQHCI_DUMP("Resp idx: 0x%08x | Resp arg: 0x%08x\n", |
| cqhci_readl(cq_host, CQHCI_CRI), |
| cqhci_readl(cq_host, CQHCI_CRA)); |
| |
| if (cq_host->ops->dumpregs) |
| cq_host->ops->dumpregs(mmc); |
| else |
| CQHCI_DUMP(": ===========================================\n"); |
| } |
| |
| /** |
| * The allocated descriptor table for task, link & transfer descritors |
| * looks like: |
| * |----------| |
| * |task desc | |->|----------| |
| * |----------| | |trans desc| |
| * |link desc-|->| |----------| |
| * |----------| . |
| * . . |
| * no. of slots max-segs |
| * . |----------| |
| * |----------| |
| * The idea here is to create the [task+trans] table and mark & point the |
| * link desc to the transfer desc table on a per slot basis. |
| */ |
| static int cqhci_host_alloc_tdl(struct cqhci_host *cq_host) |
| { |
| int i = 0; |
| |
| /* task descriptor can be 64/128 bit irrespective of arch */ |
| if (cq_host->caps & CQHCI_TASK_DESC_SZ_128) { |
| cqhci_writel(cq_host, cqhci_readl(cq_host, CQHCI_CFG) | |
| CQHCI_TASK_DESC_SZ, CQHCI_CFG); |
| cq_host->task_desc_len = 16; |
| } else { |
| cq_host->task_desc_len = 8; |
| } |
| |
| /* |
| * 96 bits length of transfer desc instead of 128 bits which means |
| * ADMA would expect next valid descriptor at the 96th bit |
| * or 128th bit |
| */ |
| if (cq_host->dma64) { |
| if (cq_host->quirks & CQHCI_QUIRK_SHORT_TXFR_DESC_SZ) |
| cq_host->trans_desc_len = 12; |
| else |
| cq_host->trans_desc_len = 16; |
| cq_host->link_desc_len = 16; |
| } else { |
| cq_host->trans_desc_len = 8; |
| cq_host->link_desc_len = 8; |
| } |
| |
| /* total size of a slot: 1 task & 1 transfer (link) */ |
| cq_host->slot_sz = cq_host->task_desc_len + cq_host->link_desc_len; |
| |
| cq_host->desc_size = cq_host->slot_sz * cq_host->num_slots; |
| |
| cq_host->data_size = cq_host->trans_desc_len * cq_host->mmc->max_segs * |
| (cq_host->num_slots - 1); |
| |
| pr_debug("%s: cqhci: desc_size: %zu data_sz: %zu slot-sz: %d\n", |
| mmc_hostname(cq_host->mmc), cq_host->desc_size, cq_host->data_size, |
| cq_host->slot_sz); |
| |
| /* |
| * allocate a dma-mapped chunk of memory for the descriptors |
| * allocate a dma-mapped chunk of memory for link descriptors |
| * setup each link-desc memory offset per slot-number to |
| * the descriptor table. |
| */ |
| cq_host->desc_base = dmam_alloc_coherent(mmc_dev(cq_host->mmc), |
| cq_host->desc_size, |
| &cq_host->desc_dma_base, |
| GFP_KERNEL); |
| cq_host->trans_desc_base = dmam_alloc_coherent(mmc_dev(cq_host->mmc), |
| cq_host->data_size, |
| &cq_host->trans_desc_dma_base, |
| GFP_KERNEL); |
| if (!cq_host->desc_base || !cq_host->trans_desc_base) |
| return -ENOMEM; |
| |
| pr_debug("%s: cqhci: desc-base: 0x%p trans-base: 0x%p\n desc_dma 0x%llx trans_dma: 0x%llx\n", |
| mmc_hostname(cq_host->mmc), cq_host->desc_base, cq_host->trans_desc_base, |
| (unsigned long long)cq_host->desc_dma_base, |
| (unsigned long long)cq_host->trans_desc_dma_base); |
| |
| for (; i < (cq_host->num_slots); i++) |
| setup_trans_desc(cq_host, i); |
| |
| return 0; |
| } |
| |
| static void __cqhci_enable(struct cqhci_host *cq_host) |
| { |
| struct mmc_host *mmc = cq_host->mmc; |
| u32 cqcfg; |
| |
| cqcfg = cqhci_readl(cq_host, CQHCI_CFG); |
| |
| /* Configuration must not be changed while enabled */ |
| if (cqcfg & CQHCI_ENABLE) { |
| cqcfg &= ~CQHCI_ENABLE; |
| cqhci_writel(cq_host, cqcfg, CQHCI_CFG); |
| } |
| |
| cqcfg &= ~(CQHCI_DCMD | CQHCI_TASK_DESC_SZ); |
| |
| if (mmc->caps2 & MMC_CAP2_CQE_DCMD) |
| cqcfg |= CQHCI_DCMD; |
| |
| if (cq_host->caps & CQHCI_TASK_DESC_SZ_128) |
| cqcfg |= CQHCI_TASK_DESC_SZ; |
| |
| cqhci_writel(cq_host, cqcfg, CQHCI_CFG); |
| |
| cqhci_writel(cq_host, lower_32_bits(cq_host->desc_dma_base), |
| CQHCI_TDLBA); |
| cqhci_writel(cq_host, upper_32_bits(cq_host->desc_dma_base), |
| CQHCI_TDLBAU); |
| |
| cqhci_writel(cq_host, cq_host->rca, CQHCI_SSC2); |
| |
| cqhci_set_irqs(cq_host, 0); |
| |
| cqcfg |= CQHCI_ENABLE; |
| |
| cqhci_writel(cq_host, cqcfg, CQHCI_CFG); |
| |
| mmc->cqe_on = true; |
| |
| if (cq_host->ops->enable) |
| cq_host->ops->enable(mmc); |
| |
| /* Ensure all writes are done before interrupts are enabled */ |
| wmb(); |
| |
| cqhci_set_irqs(cq_host, CQHCI_IS_MASK); |
| |
| cq_host->activated = true; |
| } |
| |
| static void __cqhci_disable(struct cqhci_host *cq_host) |
| { |
| u32 cqcfg; |
| |
| cqcfg = cqhci_readl(cq_host, CQHCI_CFG); |
| cqcfg &= ~CQHCI_ENABLE; |
| cqhci_writel(cq_host, cqcfg, CQHCI_CFG); |
| |
| cq_host->mmc->cqe_on = false; |
| |
| cq_host->activated = false; |
| } |
| |
| int cqhci_suspend(struct mmc_host *mmc) |
| { |
| struct cqhci_host *cq_host = mmc->cqe_private; |
| |
| if (cq_host->enabled) |
| __cqhci_disable(cq_host); |
| |
| return 0; |
| } |
| EXPORT_SYMBOL(cqhci_suspend); |
| |
| int cqhci_resume(struct mmc_host *mmc) |
| { |
| /* Re-enable is done upon first request */ |
| return 0; |
| } |
| EXPORT_SYMBOL(cqhci_resume); |
| |
| static int cqhci_enable(struct mmc_host *mmc, struct mmc_card *card) |
| { |
| struct cqhci_host *cq_host = mmc->cqe_private; |
| int err; |
| |
| if (cq_host->enabled) |
| return 0; |
| |
| cq_host->rca = card->rca; |
| |
| err = cqhci_host_alloc_tdl(cq_host); |
| if (err) |
| return err; |
| |
| __cqhci_enable(cq_host); |
| |
| cq_host->enabled = true; |
| |
| #ifdef DEBUG |
| cqhci_dumpregs(cq_host); |
| #endif |
| return 0; |
| } |
| |
| /* CQHCI is idle and should halt immediately, so set a small timeout */ |
| #define CQHCI_OFF_TIMEOUT 100 |
| |
| static void cqhci_off(struct mmc_host *mmc) |
| { |
| struct cqhci_host *cq_host = mmc->cqe_private; |
| ktime_t timeout; |
| bool timed_out; |
| u32 reg; |
| |
| if (!cq_host->enabled || !mmc->cqe_on || cq_host->recovery_halt) |
| return; |
| |
| if (cq_host->ops->disable) |
| cq_host->ops->disable(mmc, false); |
| |
| cqhci_writel(cq_host, CQHCI_HALT, CQHCI_CTL); |
| |
| timeout = ktime_add_us(ktime_get(), CQHCI_OFF_TIMEOUT); |
| while (1) { |
| timed_out = ktime_compare(ktime_get(), timeout) > 0; |
| reg = cqhci_readl(cq_host, CQHCI_CTL); |
| if ((reg & CQHCI_HALT) || timed_out) |
| break; |
| } |
| |
| if (timed_out) |
| pr_err("%s: cqhci: CQE stuck on\n", mmc_hostname(mmc)); |
| else |
| pr_debug("%s: cqhci: CQE off\n", mmc_hostname(mmc)); |
| |
| mmc->cqe_on = false; |
| } |
| |
| static void cqhci_disable(struct mmc_host *mmc) |
| { |
| struct cqhci_host *cq_host = mmc->cqe_private; |
| |
| if (!cq_host->enabled) |
| return; |
| |
| cqhci_off(mmc); |
| |
| __cqhci_disable(cq_host); |
| |
| dmam_free_coherent(mmc_dev(mmc), cq_host->data_size, |
| cq_host->trans_desc_base, |
| cq_host->trans_desc_dma_base); |
| |
| dmam_free_coherent(mmc_dev(mmc), cq_host->desc_size, |
| cq_host->desc_base, |
| cq_host->desc_dma_base); |
| |
| cq_host->trans_desc_base = NULL; |
| cq_host->desc_base = NULL; |
| |
| cq_host->enabled = false; |
| } |
| |
| static void cqhci_prep_task_desc(struct mmc_request *mrq, |
| u64 *data, bool intr) |
| { |
| u32 req_flags = mrq->data->flags; |
| |
| *data = CQHCI_VALID(1) | |
| CQHCI_END(1) | |
| CQHCI_INT(intr) | |
| CQHCI_ACT(0x5) | |
| CQHCI_FORCED_PROG(!!(req_flags & MMC_DATA_FORCED_PRG)) | |
| CQHCI_DATA_TAG(!!(req_flags & MMC_DATA_DAT_TAG)) | |
| CQHCI_DATA_DIR(!!(req_flags & MMC_DATA_READ)) | |
| CQHCI_PRIORITY(!!(req_flags & MMC_DATA_PRIO)) | |
| CQHCI_QBAR(!!(req_flags & MMC_DATA_QBR)) | |
| CQHCI_REL_WRITE(!!(req_flags & MMC_DATA_REL_WR)) | |
| CQHCI_BLK_COUNT(mrq->data->blocks) | |
| CQHCI_BLK_ADDR((u64)mrq->data->blk_addr); |
| |
| pr_debug("%s: cqhci: tag %d task descriptor 0x016%llx\n", |
| mmc_hostname(mrq->host), mrq->tag, (unsigned long long)*data); |
| } |
| |
| static int cqhci_dma_map(struct mmc_host *host, struct mmc_request *mrq) |
| { |
| int sg_count; |
| struct mmc_data *data = mrq->data; |
| |
| if (!data) |
| return -EINVAL; |
| |
| sg_count = dma_map_sg(mmc_dev(host), data->sg, |
| data->sg_len, |
| (data->flags & MMC_DATA_WRITE) ? |
| DMA_TO_DEVICE : DMA_FROM_DEVICE); |
| if (!sg_count) { |
| pr_err("%s: sg-len: %d\n", __func__, data->sg_len); |
| return -ENOMEM; |
| } |
| |
| return sg_count; |
| } |
| |
| static void cqhci_set_tran_desc(u8 *desc, dma_addr_t addr, int len, bool end, |
| bool dma64) |
| { |
| __le32 *attr = (__le32 __force *)desc; |
| |
| *attr = (CQHCI_VALID(1) | |
| CQHCI_END(end ? 1 : 0) | |
| CQHCI_INT(0) | |
| CQHCI_ACT(0x4) | |
| CQHCI_DAT_LENGTH(len)); |
| |
| if (dma64) { |
| __le64 *dataddr = (__le64 __force *)(desc + 4); |
| |
| dataddr[0] = cpu_to_le64(addr); |
| } else { |
| __le32 *dataddr = (__le32 __force *)(desc + 4); |
| |
| dataddr[0] = cpu_to_le32(addr); |
| } |
| } |
| |
| static int cqhci_prep_tran_desc(struct mmc_request *mrq, |
| struct cqhci_host *cq_host, int tag) |
| { |
| struct mmc_data *data = mrq->data; |
| int i, sg_count, len; |
| bool end = false; |
| bool dma64 = cq_host->dma64; |
| dma_addr_t addr; |
| u8 *desc; |
| struct scatterlist *sg; |
| |
| sg_count = cqhci_dma_map(mrq->host, mrq); |
| if (sg_count < 0) { |
| pr_err("%s: %s: unable to map sg lists, %d\n", |
| mmc_hostname(mrq->host), __func__, sg_count); |
| return sg_count; |
| } |
| |
| desc = get_trans_desc(cq_host, tag); |
| |
| for_each_sg(data->sg, sg, sg_count, i) { |
| addr = sg_dma_address(sg); |
| len = sg_dma_len(sg); |
| |
| if ((i+1) == sg_count) |
| end = true; |
| cqhci_set_tran_desc(desc, addr, len, end, dma64); |
| desc += cq_host->trans_desc_len; |
| } |
| |
| return 0; |
| } |
| |
| static void cqhci_prep_dcmd_desc(struct mmc_host *mmc, |
| struct mmc_request *mrq) |
| { |
| u64 *task_desc = NULL; |
| u64 data = 0; |
| u8 resp_type; |
| u8 *desc; |
| __le64 *dataddr; |
| struct cqhci_host *cq_host = mmc->cqe_private; |
| u8 timing; |
| |
| if (!(mrq->cmd->flags & MMC_RSP_PRESENT)) { |
| resp_type = 0x0; |
| timing = 0x1; |
| } else { |
| if (mrq->cmd->flags & MMC_RSP_R1B) { |
| resp_type = 0x3; |
| timing = 0x0; |
| } else { |
| resp_type = 0x2; |
| timing = 0x1; |
| } |
| } |
| |
| task_desc = (__le64 __force *)get_desc(cq_host, cq_host->dcmd_slot); |
| memset(task_desc, 0, cq_host->task_desc_len); |
| data |= (CQHCI_VALID(1) | |
| CQHCI_END(1) | |
| CQHCI_INT(1) | |
| CQHCI_QBAR(1) | |
| CQHCI_ACT(0x5) | |
| CQHCI_CMD_INDEX(mrq->cmd->opcode) | |
| CQHCI_CMD_TIMING(timing) | CQHCI_RESP_TYPE(resp_type)); |
| *task_desc |= data; |
| desc = (u8 *)task_desc; |
| pr_debug("%s: cqhci: dcmd: cmd: %d timing: %d resp: %d\n", |
| mmc_hostname(mmc), mrq->cmd->opcode, timing, resp_type); |
| dataddr = (__le64 __force *)(desc + 4); |
| dataddr[0] = cpu_to_le64((u64)mrq->cmd->arg); |
| |
| } |
| |
| static void cqhci_post_req(struct mmc_host *host, struct mmc_request *mrq) |
| { |
| struct mmc_data *data = mrq->data; |
| |
| if (data) { |
| dma_unmap_sg(mmc_dev(host), data->sg, data->sg_len, |
| (data->flags & MMC_DATA_READ) ? |
| DMA_FROM_DEVICE : DMA_TO_DEVICE); |
| } |
| } |
| |
| static inline int cqhci_tag(struct mmc_request *mrq) |
| { |
| return mrq->cmd ? DCMD_SLOT : mrq->tag; |
| } |
| |
| static int cqhci_request(struct mmc_host *mmc, struct mmc_request *mrq) |
| { |
| int err = 0; |
| u64 data = 0; |
| u64 *task_desc = NULL; |
| int tag = cqhci_tag(mrq); |
| struct cqhci_host *cq_host = mmc->cqe_private; |
| unsigned long flags; |
| |
| if (!cq_host->enabled) { |
| pr_err("%s: cqhci: not enabled\n", mmc_hostname(mmc)); |
| return -EINVAL; |
| } |
| |
| /* First request after resume has to re-enable */ |
| if (!cq_host->activated) |
| __cqhci_enable(cq_host); |
| |
| if (!mmc->cqe_on) { |
| cqhci_writel(cq_host, 0, CQHCI_CTL); |
| mmc->cqe_on = true; |
| pr_debug("%s: cqhci: CQE on\n", mmc_hostname(mmc)); |
| if (cqhci_readl(cq_host, CQHCI_CTL) && CQHCI_HALT) { |
| pr_err("%s: cqhci: CQE failed to exit halt state\n", |
| mmc_hostname(mmc)); |
| } |
| if (cq_host->ops->enable) |
| cq_host->ops->enable(mmc); |
| } |
| |
| if (mrq->data) { |
| task_desc = (__le64 __force *)get_desc(cq_host, tag); |
| cqhci_prep_task_desc(mrq, &data, 1); |
| *task_desc = cpu_to_le64(data); |
| err = cqhci_prep_tran_desc(mrq, cq_host, tag); |
| if (err) { |
| pr_err("%s: cqhci: failed to setup tx desc: %d\n", |
| mmc_hostname(mmc), err); |
| return err; |
| } |
| } else { |
| cqhci_prep_dcmd_desc(mmc, mrq); |
| } |
| |
| spin_lock_irqsave(&cq_host->lock, flags); |
| |
| if (cq_host->recovery_halt) { |
| err = -EBUSY; |
| goto out_unlock; |
| } |
| |
| cq_host->slot[tag].mrq = mrq; |
| cq_host->slot[tag].flags = 0; |
| |
| cq_host->qcnt += 1; |
| |
| cqhci_writel(cq_host, 1 << tag, CQHCI_TDBR); |
| if (!(cqhci_readl(cq_host, CQHCI_TDBR) & (1 << tag))) |
| pr_debug("%s: cqhci: doorbell not set for tag %d\n", |
| mmc_hostname(mmc), tag); |
| out_unlock: |
| spin_unlock_irqrestore(&cq_host->lock, flags); |
| |
| if (err) |
| cqhci_post_req(mmc, mrq); |
| |
| return err; |
| } |
| |
| static void cqhci_recovery_needed(struct mmc_host *mmc, struct mmc_request *mrq, |
| bool notify) |
| { |
| struct cqhci_host *cq_host = mmc->cqe_private; |
| |
| if (!cq_host->recovery_halt) { |
| cq_host->recovery_halt = true; |
| pr_debug("%s: cqhci: recovery needed\n", mmc_hostname(mmc)); |
| wake_up(&cq_host->wait_queue); |
| if (notify && mrq->recovery_notifier) |
| mrq->recovery_notifier(mrq); |
| } |
| } |
| |
| static unsigned int cqhci_error_flags(int error1, int error2) |
| { |
| int error = error1 ? error1 : error2; |
| |
| switch (error) { |
| case -EILSEQ: |
| return CQHCI_HOST_CRC; |
| case -ETIMEDOUT: |
| return CQHCI_HOST_TIMEOUT; |
| default: |
| return CQHCI_HOST_OTHER; |
| } |
| } |
| |
| static void cqhci_error_irq(struct mmc_host *mmc, u32 status, int cmd_error, |
| int data_error) |
| { |
| struct cqhci_host *cq_host = mmc->cqe_private; |
| struct cqhci_slot *slot; |
| u32 terri; |
| int tag; |
| |
| spin_lock(&cq_host->lock); |
| |
| terri = cqhci_readl(cq_host, CQHCI_TERRI); |
| |
| pr_debug("%s: cqhci: error IRQ status: 0x%08x cmd error %d data error %d TERRI: 0x%08x\n", |
| mmc_hostname(mmc), status, cmd_error, data_error, terri); |
| |
| /* Forget about errors when recovery has already been triggered */ |
| if (cq_host->recovery_halt) |
| goto out_unlock; |
| |
| if (!cq_host->qcnt) { |
| WARN_ONCE(1, "%s: cqhci: error when idle. IRQ status: 0x%08x cmd error %d data error %d TERRI: 0x%08x\n", |
| mmc_hostname(mmc), status, cmd_error, data_error, |
| terri); |
| goto out_unlock; |
| } |
| |
| if (CQHCI_TERRI_C_VALID(terri)) { |
| tag = CQHCI_TERRI_C_TASK(terri); |
| slot = &cq_host->slot[tag]; |
| if (slot->mrq) { |
| slot->flags = cqhci_error_flags(cmd_error, data_error); |
| cqhci_recovery_needed(mmc, slot->mrq, true); |
| } |
| } |
| |
| if (CQHCI_TERRI_D_VALID(terri)) { |
| tag = CQHCI_TERRI_D_TASK(terri); |
| slot = &cq_host->slot[tag]; |
| if (slot->mrq) { |
| slot->flags = cqhci_error_flags(data_error, cmd_error); |
| cqhci_recovery_needed(mmc, slot->mrq, true); |
| } |
| } |
| |
| if (!cq_host->recovery_halt) { |
| /* |
| * The only way to guarantee forward progress is to mark at |
| * least one task in error, so if none is indicated, pick one. |
| */ |
| for (tag = 0; tag < NUM_SLOTS; tag++) { |
| slot = &cq_host->slot[tag]; |
| if (!slot->mrq) |
| continue; |
| slot->flags = cqhci_error_flags(data_error, cmd_error); |
| cqhci_recovery_needed(mmc, slot->mrq, true); |
| break; |
| } |
| } |
| |
| out_unlock: |
| spin_unlock(&cq_host->lock); |
| } |
| |
| static void cqhci_finish_mrq(struct mmc_host *mmc, unsigned int tag) |
| { |
| struct cqhci_host *cq_host = mmc->cqe_private; |
| struct cqhci_slot *slot = &cq_host->slot[tag]; |
| struct mmc_request *mrq = slot->mrq; |
| struct mmc_data *data; |
| |
| if (!mrq) { |
| WARN_ONCE(1, "%s: cqhci: spurious TCN for tag %d\n", |
| mmc_hostname(mmc), tag); |
| return; |
| } |
| |
| /* No completions allowed during recovery */ |
| if (cq_host->recovery_halt) { |
| slot->flags |= CQHCI_COMPLETED; |
| return; |
| } |
| |
| slot->mrq = NULL; |
| |
| cq_host->qcnt -= 1; |
| |
| data = mrq->data; |
| if (data) { |
| if (data->error) |
| data->bytes_xfered = 0; |
| else |
| data->bytes_xfered = data->blksz * data->blocks; |
| } |
| |
| mmc_cqe_request_done(mmc, mrq); |
| } |
| |
| irqreturn_t cqhci_irq(struct mmc_host *mmc, u32 intmask, int cmd_error, |
| int data_error) |
| { |
| u32 status; |
| unsigned long tag = 0, comp_status; |
| struct cqhci_host *cq_host = mmc->cqe_private; |
| |
| status = cqhci_readl(cq_host, CQHCI_IS); |
| cqhci_writel(cq_host, status, CQHCI_IS); |
| |
| pr_debug("%s: cqhci: IRQ status: 0x%08x\n", mmc_hostname(mmc), status); |
| |
| if ((status & CQHCI_IS_RED) || cmd_error || data_error) |
| cqhci_error_irq(mmc, status, cmd_error, data_error); |
| |
| if (status & CQHCI_IS_TCC) { |
| /* read TCN and complete the request */ |
| comp_status = cqhci_readl(cq_host, CQHCI_TCN); |
| cqhci_writel(cq_host, comp_status, CQHCI_TCN); |
| pr_debug("%s: cqhci: TCN: 0x%08lx\n", |
| mmc_hostname(mmc), comp_status); |
| |
| spin_lock(&cq_host->lock); |
| |
| for_each_set_bit(tag, &comp_status, cq_host->num_slots) { |
| /* complete the corresponding mrq */ |
| pr_debug("%s: cqhci: completing tag %lu\n", |
| mmc_hostname(mmc), tag); |
| cqhci_finish_mrq(mmc, tag); |
| } |
| |
| if (cq_host->waiting_for_idle && !cq_host->qcnt) { |
| cq_host->waiting_for_idle = false; |
| wake_up(&cq_host->wait_queue); |
| } |
| |
| spin_unlock(&cq_host->lock); |
| } |
| |
| if (status & CQHCI_IS_TCL) |
| wake_up(&cq_host->wait_queue); |
| |
| if (status & CQHCI_IS_HAC) |
| wake_up(&cq_host->wait_queue); |
| |
| return IRQ_HANDLED; |
| } |
| EXPORT_SYMBOL(cqhci_irq); |
| |
| static bool cqhci_is_idle(struct cqhci_host *cq_host, int *ret) |
| { |
| unsigned long flags; |
| bool is_idle; |
| |
| spin_lock_irqsave(&cq_host->lock, flags); |
| is_idle = !cq_host->qcnt || cq_host->recovery_halt; |
| *ret = cq_host->recovery_halt ? -EBUSY : 0; |
| cq_host->waiting_for_idle = !is_idle; |
| spin_unlock_irqrestore(&cq_host->lock, flags); |
| |
| return is_idle; |
| } |
| |
| static int cqhci_wait_for_idle(struct mmc_host *mmc) |
| { |
| struct cqhci_host *cq_host = mmc->cqe_private; |
| int ret; |
| |
| wait_event(cq_host->wait_queue, cqhci_is_idle(cq_host, &ret)); |
| |
| return ret; |
| } |
| |
| static bool cqhci_timeout(struct mmc_host *mmc, struct mmc_request *mrq, |
| bool *recovery_needed) |
| { |
| struct cqhci_host *cq_host = mmc->cqe_private; |
| int tag = cqhci_tag(mrq); |
| struct cqhci_slot *slot = &cq_host->slot[tag]; |
| unsigned long flags; |
| bool timed_out; |
| |
| spin_lock_irqsave(&cq_host->lock, flags); |
| timed_out = slot->mrq == mrq; |
| if (timed_out) { |
| slot->flags |= CQHCI_EXTERNAL_TIMEOUT; |
| cqhci_recovery_needed(mmc, mrq, false); |
| *recovery_needed = cq_host->recovery_halt; |
| } |
| spin_unlock_irqrestore(&cq_host->lock, flags); |
| |
| if (timed_out) { |
| pr_err("%s: cqhci: timeout for tag %d\n", |
| mmc_hostname(mmc), tag); |
| cqhci_dumpregs(cq_host); |
| } |
| |
| return timed_out; |
| } |
| |
| static bool cqhci_tasks_cleared(struct cqhci_host *cq_host) |
| { |
| return !(cqhci_readl(cq_host, CQHCI_CTL) & CQHCI_CLEAR_ALL_TASKS); |
| } |
| |
| static bool cqhci_clear_all_tasks(struct mmc_host *mmc, unsigned int timeout) |
| { |
| struct cqhci_host *cq_host = mmc->cqe_private; |
| bool ret; |
| u32 ctl; |
| |
| cqhci_set_irqs(cq_host, CQHCI_IS_TCL); |
| |
| ctl = cqhci_readl(cq_host, CQHCI_CTL); |
| ctl |= CQHCI_CLEAR_ALL_TASKS; |
| cqhci_writel(cq_host, ctl, CQHCI_CTL); |
| |
| wait_event_timeout(cq_host->wait_queue, cqhci_tasks_cleared(cq_host), |
| msecs_to_jiffies(timeout) + 1); |
| |
| cqhci_set_irqs(cq_host, 0); |
| |
| ret = cqhci_tasks_cleared(cq_host); |
| |
| if (!ret) |
| pr_debug("%s: cqhci: Failed to clear tasks\n", |
| mmc_hostname(mmc)); |
| |
| return ret; |
| } |
| |
| static bool cqhci_halted(struct cqhci_host *cq_host) |
| { |
| return cqhci_readl(cq_host, CQHCI_CTL) & CQHCI_HALT; |
| } |
| |
| static bool cqhci_halt(struct mmc_host *mmc, unsigned int timeout) |
| { |
| struct cqhci_host *cq_host = mmc->cqe_private; |
| bool ret; |
| u32 ctl; |
| |
| if (cqhci_halted(cq_host)) |
| return true; |
| |
| cqhci_set_irqs(cq_host, CQHCI_IS_HAC); |
| |
| ctl = cqhci_readl(cq_host, CQHCI_CTL); |
| ctl |= CQHCI_HALT; |
| cqhci_writel(cq_host, ctl, CQHCI_CTL); |
| |
| wait_event_timeout(cq_host->wait_queue, cqhci_halted(cq_host), |
| msecs_to_jiffies(timeout) + 1); |
| |
| cqhci_set_irqs(cq_host, 0); |
| |
| ret = cqhci_halted(cq_host); |
| |
| if (!ret) |
| pr_debug("%s: cqhci: Failed to halt\n", mmc_hostname(mmc)); |
| |
| return ret; |
| } |
| |
| /* |
| * After halting we expect to be able to use the command line. We interpret the |
| * failure to halt to mean the data lines might still be in use (and the upper |
| * layers will need to send a STOP command), so we set the timeout based on a |
| * generous command timeout. |
| */ |
| #define CQHCI_START_HALT_TIMEOUT 5 |
| |
| static void cqhci_recovery_start(struct mmc_host *mmc) |
| { |
| struct cqhci_host *cq_host = mmc->cqe_private; |
| |
| pr_debug("%s: cqhci: %s\n", mmc_hostname(mmc), __func__); |
| |
| WARN_ON(!cq_host->recovery_halt); |
| |
| cqhci_halt(mmc, CQHCI_START_HALT_TIMEOUT); |
| |
| if (cq_host->ops->disable) |
| cq_host->ops->disable(mmc, true); |
| |
| mmc->cqe_on = false; |
| } |
| |
| static int cqhci_error_from_flags(unsigned int flags) |
| { |
| if (!flags) |
| return 0; |
| |
| /* CRC errors might indicate re-tuning so prefer to report that */ |
| if (flags & CQHCI_HOST_CRC) |
| return -EILSEQ; |
| |
| if (flags & (CQHCI_EXTERNAL_TIMEOUT | CQHCI_HOST_TIMEOUT)) |
| return -ETIMEDOUT; |
| |
| return -EIO; |
| } |
| |
| static void cqhci_recover_mrq(struct cqhci_host *cq_host, unsigned int tag) |
| { |
| struct cqhci_slot *slot = &cq_host->slot[tag]; |
| struct mmc_request *mrq = slot->mrq; |
| struct mmc_data *data; |
| |
| if (!mrq) |
| return; |
| |
| slot->mrq = NULL; |
| |
| cq_host->qcnt -= 1; |
| |
| data = mrq->data; |
| if (data) { |
| data->bytes_xfered = 0; |
| data->error = cqhci_error_from_flags(slot->flags); |
| } else { |
| mrq->cmd->error = cqhci_error_from_flags(slot->flags); |
| } |
| |
| mmc_cqe_request_done(cq_host->mmc, mrq); |
| } |
| |
| static void cqhci_recover_mrqs(struct cqhci_host *cq_host) |
| { |
| int i; |
| |
| for (i = 0; i < cq_host->num_slots; i++) |
| cqhci_recover_mrq(cq_host, i); |
| } |
| |
| /* |
| * By now the command and data lines should be unused so there is no reason for |
| * CQHCI to take a long time to halt, but if it doesn't halt there could be |
| * problems clearing tasks, so be generous. |
| */ |
| #define CQHCI_FINISH_HALT_TIMEOUT 20 |
| |
| /* CQHCI could be expected to clear it's internal state pretty quickly */ |
| #define CQHCI_CLEAR_TIMEOUT 20 |
| |
| static void cqhci_recovery_finish(struct mmc_host *mmc) |
| { |
| struct cqhci_host *cq_host = mmc->cqe_private; |
| unsigned long flags; |
| u32 cqcfg; |
| bool ok; |
| |
| pr_debug("%s: cqhci: %s\n", mmc_hostname(mmc), __func__); |
| |
| WARN_ON(!cq_host->recovery_halt); |
| |
| ok = cqhci_halt(mmc, CQHCI_FINISH_HALT_TIMEOUT); |
| |
| if (!cqhci_clear_all_tasks(mmc, CQHCI_CLEAR_TIMEOUT)) |
| ok = false; |
| |
| /* |
| * The specification contradicts itself, by saying that tasks cannot be |
| * cleared if CQHCI does not halt, but if CQHCI does not halt, it should |
| * be disabled/re-enabled, but not to disable before clearing tasks. |
| * Have a go anyway. |
| */ |
| if (!ok) { |
| pr_debug("%s: cqhci: disable / re-enable\n", mmc_hostname(mmc)); |
| cqcfg = cqhci_readl(cq_host, CQHCI_CFG); |
| cqcfg &= ~CQHCI_ENABLE; |
| cqhci_writel(cq_host, cqcfg, CQHCI_CFG); |
| cqcfg |= CQHCI_ENABLE; |
| cqhci_writel(cq_host, cqcfg, CQHCI_CFG); |
| /* Be sure that there are no tasks */ |
| ok = cqhci_halt(mmc, CQHCI_FINISH_HALT_TIMEOUT); |
| if (!cqhci_clear_all_tasks(mmc, CQHCI_CLEAR_TIMEOUT)) |
| ok = false; |
| WARN_ON(!ok); |
| } |
| |
| cqhci_recover_mrqs(cq_host); |
| |
| WARN_ON(cq_host->qcnt); |
| |
| spin_lock_irqsave(&cq_host->lock, flags); |
| cq_host->qcnt = 0; |
| cq_host->recovery_halt = false; |
| mmc->cqe_on = false; |
| spin_unlock_irqrestore(&cq_host->lock, flags); |
| |
| /* Ensure all writes are done before interrupts are re-enabled */ |
| wmb(); |
| |
| cqhci_writel(cq_host, CQHCI_IS_HAC | CQHCI_IS_TCL, CQHCI_IS); |
| |
| cqhci_set_irqs(cq_host, CQHCI_IS_MASK); |
| |
| pr_debug("%s: cqhci: recovery done\n", mmc_hostname(mmc)); |
| } |
| |
| static const struct mmc_cqe_ops cqhci_cqe_ops = { |
| .cqe_enable = cqhci_enable, |
| .cqe_disable = cqhci_disable, |
| .cqe_request = cqhci_request, |
| .cqe_post_req = cqhci_post_req, |
| .cqe_off = cqhci_off, |
| .cqe_wait_for_idle = cqhci_wait_for_idle, |
| .cqe_timeout = cqhci_timeout, |
| .cqe_recovery_start = cqhci_recovery_start, |
| .cqe_recovery_finish = cqhci_recovery_finish, |
| }; |
| |
| struct cqhci_host *cqhci_pltfm_init(struct platform_device *pdev) |
| { |
| struct cqhci_host *cq_host; |
| struct resource *cqhci_memres = NULL; |
| |
| /* check and setup CMDQ interface */ |
| cqhci_memres = platform_get_resource_byname(pdev, IORESOURCE_MEM, |
| "cqhci_mem"); |
| if (!cqhci_memres) { |
| dev_dbg(&pdev->dev, "CMDQ not supported\n"); |
| return ERR_PTR(-EINVAL); |
| } |
| |
| cq_host = devm_kzalloc(&pdev->dev, sizeof(*cq_host), GFP_KERNEL); |
| if (!cq_host) |
| return ERR_PTR(-ENOMEM); |
| cq_host->mmio = devm_ioremap(&pdev->dev, |
| cqhci_memres->start, |
| resource_size(cqhci_memres)); |
| if (!cq_host->mmio) { |
| dev_err(&pdev->dev, "failed to remap cqhci regs\n"); |
| return ERR_PTR(-EBUSY); |
| } |
| dev_dbg(&pdev->dev, "CMDQ ioremap: done\n"); |
| |
| return cq_host; |
| } |
| EXPORT_SYMBOL(cqhci_pltfm_init); |
| |
| static unsigned int cqhci_ver_major(struct cqhci_host *cq_host) |
| { |
| return CQHCI_VER_MAJOR(cqhci_readl(cq_host, CQHCI_VER)); |
| } |
| |
| static unsigned int cqhci_ver_minor(struct cqhci_host *cq_host) |
| { |
| u32 ver = cqhci_readl(cq_host, CQHCI_VER); |
| |
| return CQHCI_VER_MINOR1(ver) * 10 + CQHCI_VER_MINOR2(ver); |
| } |
| |
| int cqhci_init(struct cqhci_host *cq_host, struct mmc_host *mmc, |
| bool dma64) |
| { |
| int err; |
| |
| cq_host->dma64 = dma64; |
| cq_host->mmc = mmc; |
| cq_host->mmc->cqe_private = cq_host; |
| |
| cq_host->num_slots = NUM_SLOTS; |
| cq_host->dcmd_slot = DCMD_SLOT; |
| |
| mmc->cqe_ops = &cqhci_cqe_ops; |
| |
| mmc->cqe_qdepth = NUM_SLOTS; |
| if (mmc->caps2 & MMC_CAP2_CQE_DCMD) |
| mmc->cqe_qdepth -= 1; |
| |
| cq_host->slot = devm_kcalloc(mmc_dev(mmc), cq_host->num_slots, |
| sizeof(*cq_host->slot), GFP_KERNEL); |
| if (!cq_host->slot) { |
| err = -ENOMEM; |
| goto out_err; |
| } |
| |
| spin_lock_init(&cq_host->lock); |
| |
| init_completion(&cq_host->halt_comp); |
| init_waitqueue_head(&cq_host->wait_queue); |
| |
| pr_info("%s: CQHCI version %u.%02u\n", |
| mmc_hostname(mmc), cqhci_ver_major(cq_host), |
| cqhci_ver_minor(cq_host)); |
| |
| return 0; |
| |
| out_err: |
| pr_err("%s: CQHCI version %u.%02u failed to initialize, error %d\n", |
| mmc_hostname(mmc), cqhci_ver_major(cq_host), |
| cqhci_ver_minor(cq_host), err); |
| return err; |
| } |
| EXPORT_SYMBOL(cqhci_init); |
| |
| MODULE_AUTHOR("Venkat Gopalakrishnan <venkatg@codeaurora.org>"); |
| MODULE_DESCRIPTION("Command Queue Host Controller Interface driver"); |
| MODULE_LICENSE("GPL v2"); |