2015-03-31 11:50:46 +00:00
|
|
|
/*-
|
|
|
|
* Copyright 2015 John Wehle <john@feith.com>
|
|
|
|
* All rights reserved.
|
|
|
|
*
|
|
|
|
* Redistribution and use in source and binary forms, with or without
|
|
|
|
* modification, are permitted provided that the following conditions
|
|
|
|
* are met:
|
|
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
|
|
* notice, this list of conditions and the following disclaimer.
|
|
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
|
|
* documentation and/or other materials provided with the distribution.
|
|
|
|
*
|
|
|
|
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
|
|
|
|
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
|
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
|
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
|
|
|
|
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
|
|
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
|
|
|
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
|
|
|
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
|
|
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
|
|
|
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
|
|
|
* SUCH DAMAGE.
|
|
|
|
*/
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Amlogic aml8726-m8 (and later) SDXC host controller driver.
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include <sys/cdefs.h>
|
|
|
|
__FBSDID("$FreeBSD$");
|
|
|
|
|
|
|
|
#include <sys/param.h>
|
|
|
|
#include <sys/systm.h>
|
|
|
|
#include <sys/conf.h>
|
|
|
|
#include <sys/bus.h>
|
|
|
|
#include <sys/kernel.h>
|
|
|
|
#include <sys/module.h>
|
|
|
|
#include <sys/lock.h>
|
|
|
|
#include <sys/mutex.h>
|
|
|
|
#include <sys/resource.h>
|
|
|
|
#include <sys/rman.h>
|
|
|
|
|
|
|
|
#include <sys/gpio.h>
|
|
|
|
|
|
|
|
#include <machine/bus.h>
|
|
|
|
#include <machine/cpu.h>
|
|
|
|
|
|
|
|
#include <dev/ofw/ofw_bus.h>
|
|
|
|
#include <dev/ofw/ofw_bus_subr.h>
|
|
|
|
|
|
|
|
#include <dev/mmc/bridge.h>
|
|
|
|
#include <dev/mmc/mmcreg.h>
|
|
|
|
#include <dev/mmc/mmcbrvar.h>
|
|
|
|
|
|
|
|
#include <arm/amlogic/aml8726/aml8726_soc.h>
|
|
|
|
#include <arm/amlogic/aml8726/aml8726_sdxc-m8.h>
|
|
|
|
|
|
|
|
#include "gpio_if.h"
|
|
|
|
#include "mmcbr_if.h"
|
|
|
|
|
|
|
|
/*
|
|
|
|
* The table is sorted from highest to lowest and
|
|
|
|
* last entry in the table is mark by freq == 0.
|
|
|
|
*/
|
|
|
|
struct {
|
|
|
|
uint32_t voltage;
|
|
|
|
uint32_t freq;
|
|
|
|
uint32_t rx_phase;
|
|
|
|
} aml8726_sdxc_clk_phases[] = {
|
|
|
|
{
|
|
|
|
MMC_OCR_LOW_VOLTAGE | MMC_OCR_320_330 | MMC_OCR_330_340,
|
|
|
|
100000000,
|
|
|
|
1
|
|
|
|
},
|
|
|
|
{
|
|
|
|
MMC_OCR_320_330 | MMC_OCR_330_340,
|
|
|
|
45000000,
|
|
|
|
15
|
|
|
|
},
|
|
|
|
{
|
|
|
|
MMC_OCR_LOW_VOLTAGE,
|
|
|
|
45000000,
|
|
|
|
11
|
|
|
|
},
|
|
|
|
{
|
|
|
|
MMC_OCR_LOW_VOLTAGE | MMC_OCR_320_330 | MMC_OCR_330_340,
|
|
|
|
24999999,
|
|
|
|
15
|
|
|
|
},
|
|
|
|
{
|
|
|
|
MMC_OCR_LOW_VOLTAGE | MMC_OCR_320_330 | MMC_OCR_330_340,
|
|
|
|
5000000,
|
|
|
|
23
|
|
|
|
},
|
|
|
|
{
|
|
|
|
MMC_OCR_LOW_VOLTAGE | MMC_OCR_320_330 | MMC_OCR_330_340,
|
|
|
|
1000000,
|
|
|
|
55
|
|
|
|
},
|
|
|
|
{
|
|
|
|
MMC_OCR_LOW_VOLTAGE | MMC_OCR_320_330 | MMC_OCR_330_340,
|
|
|
|
0,
|
|
|
|
1061
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
struct aml8726_sdxc_gpio {
|
|
|
|
device_t dev;
|
|
|
|
uint32_t pin;
|
|
|
|
uint32_t pol;
|
|
|
|
};
|
|
|
|
|
|
|
|
struct aml8726_sdxc_softc {
|
|
|
|
device_t dev;
|
|
|
|
boolean_t auto_fill_flush;
|
|
|
|
struct resource *res[2];
|
|
|
|
struct mtx mtx;
|
|
|
|
struct callout ch;
|
|
|
|
unsigned int ref_freq;
|
|
|
|
struct aml8726_sdxc_gpio pwr_en;
|
|
|
|
int voltages[2];
|
|
|
|
struct aml8726_sdxc_gpio vselect;
|
|
|
|
struct aml8726_sdxc_gpio card_rst;
|
|
|
|
bus_dma_tag_t dmatag;
|
|
|
|
bus_dmamap_t dmamap;
|
|
|
|
void *ih_cookie;
|
|
|
|
struct mmc_host host;
|
|
|
|
int bus_busy;
|
|
|
|
struct {
|
|
|
|
uint32_t time;
|
|
|
|
uint32_t error;
|
|
|
|
} busy;
|
|
|
|
struct mmc_command *cmd;
|
|
|
|
};
|
|
|
|
|
|
|
|
static struct resource_spec aml8726_sdxc_spec[] = {
|
|
|
|
{ SYS_RES_MEMORY, 0, RF_ACTIVE },
|
|
|
|
{ SYS_RES_IRQ, 0, RF_ACTIVE },
|
|
|
|
{ -1, 0 }
|
|
|
|
};
|
|
|
|
|
|
|
|
#define AML_SDXC_LOCK(sc) mtx_lock(&(sc)->mtx)
|
|
|
|
#define AML_SDXC_UNLOCK(sc) mtx_unlock(&(sc)->mtx)
|
|
|
|
#define AML_SDXC_LOCK_ASSERT(sc) mtx_assert(&(sc)->mtx, MA_OWNED)
|
|
|
|
#define AML_SDXC_LOCK_INIT(sc) \
|
|
|
|
mtx_init(&(sc)->mtx, device_get_nameunit((sc)->dev), \
|
|
|
|
"sdxc", MTX_DEF)
|
|
|
|
#define AML_SDXC_LOCK_DESTROY(sc) mtx_destroy(&(sc)->mtx);
|
|
|
|
|
|
|
|
#define CSR_WRITE_4(sc, reg, val) bus_write_4((sc)->res[0], reg, (val))
|
|
|
|
#define CSR_READ_4(sc, reg) bus_read_4((sc)->res[0], reg)
|
|
|
|
#define CSR_BARRIER(sc, reg) bus_barrier((sc)->res[0], reg, 4, \
|
|
|
|
(BUS_SPACE_BARRIER_READ | BUS_SPACE_BARRIER_WRITE))
|
|
|
|
|
|
|
|
#define PIN_ON_FLAG(pol) ((pol) == 0 ? \
|
|
|
|
GPIO_PIN_LOW : GPIO_PIN_HIGH)
|
|
|
|
#define PIN_OFF_FLAG(pol) ((pol) == 0 ? \
|
|
|
|
GPIO_PIN_HIGH : GPIO_PIN_LOW)
|
|
|
|
|
|
|
|
#define msecs_to_ticks(ms) (((ms)*hz)/1000 + 1)
|
|
|
|
|
|
|
|
static void aml8726_sdxc_timeout(void *arg);
|
|
|
|
|
|
|
|
static void
|
|
|
|
aml8726_sdxc_mapmem(void *arg, bus_dma_segment_t *segs, int nseg, int error)
|
|
|
|
{
|
|
|
|
bus_addr_t *busaddrp;
|
|
|
|
|
|
|
|
/*
|
|
|
|
* There should only be one bus space address since
|
|
|
|
* bus_dma_tag_create was called with nsegments = 1.
|
|
|
|
*/
|
|
|
|
|
|
|
|
busaddrp = (bus_addr_t *)arg;
|
|
|
|
*busaddrp = segs->ds_addr;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
aml8726_sdxc_power_off(struct aml8726_sdxc_softc *sc)
|
|
|
|
{
|
|
|
|
|
|
|
|
if (sc->pwr_en.dev == NULL)
|
|
|
|
return (0);
|
|
|
|
|
|
|
|
return (GPIO_PIN_SET(sc->pwr_en.dev, sc->pwr_en.pin,
|
|
|
|
PIN_OFF_FLAG(sc->pwr_en.pol)));
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
aml8726_sdxc_power_on(struct aml8726_sdxc_softc *sc)
|
|
|
|
{
|
|
|
|
|
|
|
|
if (sc->pwr_en.dev == NULL)
|
|
|
|
return (0);
|
|
|
|
|
|
|
|
return (GPIO_PIN_SET(sc->pwr_en.dev, sc->pwr_en.pin,
|
|
|
|
PIN_ON_FLAG(sc->pwr_en.pol)));
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
aml8726_sdxc_soft_reset(struct aml8726_sdxc_softc *sc)
|
|
|
|
{
|
|
|
|
|
|
|
|
CSR_WRITE_4(sc, AML_SDXC_SOFT_RESET_REG, AML_SDXC_SOFT_RESET);
|
|
|
|
CSR_BARRIER(sc, AML_SDXC_SOFT_RESET_REG);
|
|
|
|
DELAY(5);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
aml8726_sdxc_engage_dma(struct aml8726_sdxc_softc *sc)
|
|
|
|
{
|
|
|
|
int i;
|
|
|
|
uint32_t pdmar;
|
|
|
|
uint32_t sr;
|
|
|
|
struct mmc_data *data;
|
|
|
|
|
|
|
|
data = sc->cmd->data;
|
|
|
|
|
|
|
|
if (data == NULL || data->len == 0)
|
|
|
|
return;
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Engaging the DMA hardware is recommended before writing
|
|
|
|
* to AML_SDXC_SEND_REG so that the FIFOs are ready to go.
|
|
|
|
*
|
|
|
|
* Presumably AML_SDXC_CNTRL_REG and AML_SDXC_DMA_ADDR_REG
|
|
|
|
* must be set up prior to this happening.
|
|
|
|
*/
|
|
|
|
|
|
|
|
pdmar = CSR_READ_4(sc, AML_SDXC_PDMA_REG);
|
|
|
|
|
|
|
|
pdmar &= ~AML_SDXC_PDMA_RX_FLUSH_MODE_SW;
|
|
|
|
pdmar |= AML_SDXC_PDMA_DMA_EN;
|
|
|
|
|
|
|
|
if (sc->auto_fill_flush == true) {
|
|
|
|
CSR_WRITE_4(sc, AML_SDXC_PDMA_REG, pdmar);
|
|
|
|
CSR_BARRIER(sc, AML_SDXC_PDMA_REG);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ((data->flags & MMC_DATA_READ) != 0) {
|
|
|
|
pdmar |= AML_SDXC_PDMA_RX_FLUSH_MODE_SW;
|
|
|
|
CSR_WRITE_4(sc, AML_SDXC_PDMA_REG, pdmar);
|
|
|
|
CSR_BARRIER(sc, AML_SDXC_PDMA_REG);
|
|
|
|
} else {
|
|
|
|
pdmar |= AML_SDXC_PDMA_TX_FILL;
|
|
|
|
CSR_WRITE_4(sc, AML_SDXC_PDMA_REG, pdmar);
|
|
|
|
CSR_BARRIER(sc, AML_SDXC_PDMA_REG);
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Wait up to 100us for data to show up.
|
|
|
|
*/
|
|
|
|
for (i = 0; i < 100; i++) {
|
|
|
|
sr = CSR_READ_4(sc, AML_SDXC_STATUS_REG);
|
|
|
|
if ((sr & AML_SDXC_STATUS_TX_CNT_MASK) != 0)
|
|
|
|
break;
|
|
|
|
DELAY(1);
|
|
|
|
}
|
|
|
|
if (i >= 100)
|
|
|
|
device_printf(sc->dev, "TX FIFO fill timeout\n");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
aml8726_sdxc_disengage_dma(struct aml8726_sdxc_softc *sc)
|
|
|
|
{
|
|
|
|
int i;
|
|
|
|
uint32_t pdmar;
|
|
|
|
uint32_t sr;
|
|
|
|
struct mmc_data *data;
|
|
|
|
|
|
|
|
data = sc->cmd->data;
|
|
|
|
|
|
|
|
if (data == NULL || data->len == 0)
|
|
|
|
return;
|
|
|
|
|
|
|
|
pdmar = CSR_READ_4(sc, AML_SDXC_PDMA_REG);
|
|
|
|
|
|
|
|
if (sc->auto_fill_flush == true) {
|
|
|
|
pdmar &= ~AML_SDXC_PDMA_DMA_EN;
|
|
|
|
CSR_WRITE_4(sc, AML_SDXC_PDMA_REG, pdmar);
|
|
|
|
CSR_BARRIER(sc, AML_SDXC_PDMA_REG);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ((data->flags & MMC_DATA_READ) != 0) {
|
|
|
|
pdmar |= AML_SDXC_PDMA_RX_FLUSH_NOW;
|
|
|
|
CSR_WRITE_4(sc, AML_SDXC_PDMA_REG, pdmar);
|
|
|
|
CSR_BARRIER(sc, AML_SDXC_PDMA_REG);
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Wait up to 100us for data to drain.
|
|
|
|
*/
|
|
|
|
for (i = 0; i < 100; i++) {
|
|
|
|
sr = CSR_READ_4(sc, AML_SDXC_STATUS_REG);
|
|
|
|
if ((sr & AML_SDXC_STATUS_RX_CNT_MASK) == 0)
|
|
|
|
break;
|
|
|
|
DELAY(1);
|
|
|
|
}
|
|
|
|
if (i >= 100)
|
|
|
|
device_printf(sc->dev, "RX FIFO drain timeout\n");
|
|
|
|
}
|
|
|
|
|
|
|
|
pdmar &= ~(AML_SDXC_PDMA_DMA_EN | AML_SDXC_PDMA_RX_FLUSH_MODE_SW);
|
|
|
|
|
|
|
|
CSR_WRITE_4(sc, AML_SDXC_PDMA_REG, pdmar);
|
|
|
|
CSR_BARRIER(sc, AML_SDXC_PDMA_REG);
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
aml8726_sdxc_start_command(struct aml8726_sdxc_softc *sc,
|
|
|
|
struct mmc_command *cmd)
|
|
|
|
{
|
|
|
|
bus_addr_t baddr;
|
|
|
|
uint32_t block_size;
|
|
|
|
uint32_t ctlr;
|
|
|
|
uint32_t ier;
|
|
|
|
uint32_t sndr;
|
|
|
|
uint32_t timeout;
|
|
|
|
int error;
|
|
|
|
struct mmc_data *data;
|
|
|
|
|
|
|
|
AML_SDXC_LOCK_ASSERT(sc);
|
|
|
|
|
|
|
|
if (cmd->opcode > 0x3f)
|
|
|
|
return (MMC_ERR_INVALID);
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Ensure the hardware state machine is in a known state.
|
|
|
|
*/
|
|
|
|
aml8726_sdxc_soft_reset(sc);
|
|
|
|
|
|
|
|
sndr = cmd->opcode;
|
|
|
|
|
|
|
|
if ((cmd->flags & MMC_RSP_136) != 0) {
|
|
|
|
sndr |= AML_SDXC_SEND_CMD_HAS_RESP;
|
|
|
|
sndr |= AML_SDXC_SEND_RESP_136;
|
|
|
|
/*
|
|
|
|
* According to the SD spec the 136 bit response is
|
|
|
|
* used for getting the CID or CSD in which case the
|
|
|
|
* CRC7 is embedded in the contents rather than being
|
|
|
|
* calculated over the entire response (the controller
|
|
|
|
* always checks the CRC7 over the entire response).
|
|
|
|
*/
|
|
|
|
sndr |= AML_SDXC_SEND_RESP_NO_CRC7_CHECK;
|
|
|
|
} else if ((cmd->flags & MMC_RSP_PRESENT) != 0)
|
|
|
|
sndr |= AML_SDXC_SEND_CMD_HAS_RESP;
|
|
|
|
|
|
|
|
if ((cmd->flags & MMC_RSP_CRC) == 0)
|
|
|
|
sndr |= AML_SDXC_SEND_RESP_NO_CRC7_CHECK;
|
|
|
|
|
|
|
|
if (cmd->opcode == MMC_STOP_TRANSMISSION)
|
|
|
|
sndr |= AML_SDXC_SEND_DATA_STOP;
|
|
|
|
|
|
|
|
data = cmd->data;
|
|
|
|
|
|
|
|
baddr = 0;
|
|
|
|
ctlr = CSR_READ_4(sc, AML_SDXC_CNTRL_REG);
|
|
|
|
ier = AML_SDXC_IRQ_ENABLE_STANDARD;
|
|
|
|
timeout = AML_SDXC_CMD_TIMEOUT;
|
|
|
|
|
|
|
|
ctlr &= ~AML_SDXC_CNTRL_PKG_LEN_MASK;
|
|
|
|
|
|
|
|
if (data && data->len &&
|
|
|
|
(data->flags & (MMC_DATA_READ | MMC_DATA_WRITE)) != 0) {
|
|
|
|
block_size = data->len;
|
|
|
|
|
|
|
|
if ((data->flags & MMC_DATA_MULTI) != 0) {
|
|
|
|
block_size = MMC_SECTOR_SIZE;
|
|
|
|
if ((data->len % block_size) != 0)
|
|
|
|
return (MMC_ERR_INVALID);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (block_size > 512)
|
|
|
|
return (MMC_ERR_INVALID);
|
|
|
|
|
|
|
|
sndr |= AML_SDXC_SEND_CMD_HAS_DATA;
|
|
|
|
sndr |= ((data->flags & MMC_DATA_WRITE) != 0) ?
|
|
|
|
AML_SDXC_SEND_DATA_WRITE : 0;
|
|
|
|
sndr |= (((data->len / block_size) - 1) <<
|
|
|
|
AML_SDXC_SEND_REP_PKG_CNT_SHIFT);
|
|
|
|
|
|
|
|
ctlr |= ((block_size < 512) ? block_size : 0) <<
|
|
|
|
AML_SDXC_CNTRL_PKG_LEN_SHIFT;
|
|
|
|
|
|
|
|
ier &= ~AML_SDXC_IRQ_ENABLE_RESP_OK;
|
|
|
|
ier |= (sc->auto_fill_flush == true ||
|
|
|
|
(data->flags & MMC_DATA_WRITE) != 0) ?
|
|
|
|
AML_SDXC_IRQ_ENABLE_DMA_DONE :
|
|
|
|
AML_SDXC_IRQ_ENABLE_TRANSFER_DONE_OK;
|
|
|
|
|
|
|
|
error = bus_dmamap_load(sc->dmatag, sc->dmamap,
|
|
|
|
data->data, data->len, aml8726_sdxc_mapmem, &baddr,
|
|
|
|
BUS_DMA_NOWAIT);
|
|
|
|
if (error)
|
|
|
|
return (MMC_ERR_NO_MEMORY);
|
|
|
|
|
|
|
|
if ((data->flags & MMC_DATA_READ) != 0) {
|
|
|
|
bus_dmamap_sync(sc->dmatag, sc->dmamap,
|
|
|
|
BUS_DMASYNC_PREREAD);
|
|
|
|
timeout = AML_SDXC_READ_TIMEOUT *
|
|
|
|
(data->len / block_size);
|
|
|
|
} else {
|
|
|
|
bus_dmamap_sync(sc->dmatag, sc->dmamap,
|
|
|
|
BUS_DMASYNC_PREWRITE);
|
|
|
|
timeout = AML_SDXC_WRITE_TIMEOUT *
|
|
|
|
(data->len / block_size);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
sc->cmd = cmd;
|
|
|
|
|
|
|
|
cmd->error = MMC_ERR_NONE;
|
|
|
|
|
|
|
|
sc->busy.time = 0;
|
|
|
|
sc->busy.error = MMC_ERR_NONE;
|
|
|
|
|
|
|
|
if (timeout > AML_SDXC_MAX_TIMEOUT)
|
|
|
|
timeout = AML_SDXC_MAX_TIMEOUT;
|
|
|
|
|
|
|
|
callout_reset(&sc->ch, msecs_to_ticks(timeout),
|
|
|
|
aml8726_sdxc_timeout, sc);
|
|
|
|
|
|
|
|
CSR_WRITE_4(sc, AML_SDXC_IRQ_ENABLE_REG, ier);
|
|
|
|
|
|
|
|
CSR_WRITE_4(sc, AML_SDXC_CNTRL_REG, ctlr);
|
|
|
|
CSR_WRITE_4(sc, AML_SDXC_DMA_ADDR_REG, (uint32_t)baddr);
|
|
|
|
CSR_WRITE_4(sc, AML_SDXC_CMD_ARGUMENT_REG, cmd->arg);
|
|
|
|
|
|
|
|
aml8726_sdxc_engage_dma(sc);
|
|
|
|
|
|
|
|
CSR_WRITE_4(sc, AML_SDXC_SEND_REG, sndr);
|
|
|
|
CSR_BARRIER(sc, AML_SDXC_SEND_REG);
|
|
|
|
|
|
|
|
return (MMC_ERR_NONE);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
aml8726_sdxc_finish_command(struct aml8726_sdxc_softc *sc, int mmc_error)
|
|
|
|
{
|
|
|
|
int mmc_stop_error;
|
|
|
|
struct mmc_command *cmd;
|
|
|
|
struct mmc_command *stop_cmd;
|
|
|
|
struct mmc_data *data;
|
|
|
|
|
|
|
|
AML_SDXC_LOCK_ASSERT(sc);
|
|
|
|
|
|
|
|
/* Clear all interrupts since the request is no longer in flight. */
|
|
|
|
CSR_WRITE_4(sc, AML_SDXC_IRQ_STATUS_REG, AML_SDXC_IRQ_STATUS_CLEAR);
|
|
|
|
CSR_BARRIER(sc, AML_SDXC_IRQ_STATUS_REG);
|
|
|
|
|
|
|
|
/* In some cases (e.g. finish called via timeout) this is a NOP. */
|
|
|
|
callout_stop(&sc->ch);
|
|
|
|
|
|
|
|
cmd = sc->cmd;
|
|
|
|
sc->cmd = NULL;
|
|
|
|
|
|
|
|
cmd->error = mmc_error;
|
|
|
|
|
|
|
|
data = cmd->data;
|
|
|
|
|
|
|
|
if (data && data->len
|
|
|
|
&& (data->flags & (MMC_DATA_READ | MMC_DATA_WRITE)) != 0) {
|
|
|
|
if ((data->flags & MMC_DATA_READ) != 0)
|
|
|
|
bus_dmamap_sync(sc->dmatag, sc->dmamap,
|
|
|
|
BUS_DMASYNC_POSTREAD);
|
|
|
|
else
|
|
|
|
bus_dmamap_sync(sc->dmatag, sc->dmamap,
|
|
|
|
BUS_DMASYNC_POSTWRITE);
|
|
|
|
bus_dmamap_unload(sc->dmatag, sc->dmamap);
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* If there's a linked stop command, then start the stop command.
|
|
|
|
* In order to establish a known state attempt the stop command
|
|
|
|
* even if the original request encountered an error.
|
|
|
|
*/
|
|
|
|
|
|
|
|
stop_cmd = (cmd->mrq->stop != cmd) ? cmd->mrq->stop : NULL;
|
|
|
|
|
|
|
|
if (stop_cmd != NULL) {
|
|
|
|
|
|
|
|
/*
|
2016-05-04 15:48:59 +00:00
|
|
|
* If the original command executed successfully, then
|
2015-03-31 11:50:46 +00:00
|
|
|
* the hardware will also have automatically executed
|
|
|
|
* a stop command so don't bother with the one supplied
|
|
|
|
* with the original request.
|
|
|
|
*/
|
|
|
|
|
|
|
|
if (mmc_error == MMC_ERR_NONE) {
|
|
|
|
stop_cmd->error = MMC_ERR_NONE;
|
|
|
|
stop_cmd->resp[0] = cmd->resp[0];
|
|
|
|
stop_cmd->resp[1] = cmd->resp[1];
|
|
|
|
stop_cmd->resp[2] = cmd->resp[2];
|
|
|
|
stop_cmd->resp[3] = cmd->resp[3];
|
|
|
|
} else {
|
|
|
|
mmc_stop_error = aml8726_sdxc_start_command(sc,
|
|
|
|
stop_cmd);
|
|
|
|
if (mmc_stop_error == MMC_ERR_NONE) {
|
|
|
|
AML_SDXC_UNLOCK(sc);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
stop_cmd->error = mmc_stop_error;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
AML_SDXC_UNLOCK(sc);
|
|
|
|
|
|
|
|
/* Execute the callback after dropping the lock. */
|
|
|
|
if (cmd->mrq != NULL)
|
|
|
|
cmd->mrq->done(cmd->mrq);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
aml8726_sdxc_timeout(void *arg)
|
|
|
|
{
|
|
|
|
struct aml8726_sdxc_softc *sc = (struct aml8726_sdxc_softc *)arg;
|
|
|
|
|
|
|
|
/*
|
|
|
|
* The command failed to complete in time so forcefully
|
|
|
|
* terminate it.
|
|
|
|
*/
|
|
|
|
aml8726_sdxc_soft_reset(sc);
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Ensure the command has terminated before continuing on
|
|
|
|
* to things such as bus_dmamap_sync / bus_dmamap_unload.
|
|
|
|
*/
|
|
|
|
while ((CSR_READ_4(sc, AML_SDXC_STATUS_REG) &
|
|
|
|
AML_SDXC_STATUS_BUSY) != 0)
|
|
|
|
cpu_spinwait();
|
|
|
|
|
|
|
|
aml8726_sdxc_finish_command(sc, MMC_ERR_TIMEOUT);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
aml8726_sdxc_busy_check(void *arg)
|
|
|
|
{
|
|
|
|
struct aml8726_sdxc_softc *sc = (struct aml8726_sdxc_softc *)arg;
|
|
|
|
uint32_t sr;
|
|
|
|
|
|
|
|
sc->busy.time += AML_SDXC_BUSY_POLL_INTVL;
|
|
|
|
|
|
|
|
sr = CSR_READ_4(sc, AML_SDXC_STATUS_REG);
|
|
|
|
|
|
|
|
if ((sr & AML_SDXC_STATUS_DAT0) == 0) {
|
|
|
|
if (sc->busy.time < AML_SDXC_BUSY_TIMEOUT) {
|
|
|
|
callout_reset(&sc->ch,
|
|
|
|
msecs_to_ticks(AML_SDXC_BUSY_POLL_INTVL),
|
|
|
|
aml8726_sdxc_busy_check, sc);
|
|
|
|
AML_SDXC_UNLOCK(sc);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (sc->busy.error == MMC_ERR_NONE)
|
|
|
|
sc->busy.error = MMC_ERR_TIMEOUT;
|
|
|
|
}
|
|
|
|
|
|
|
|
aml8726_sdxc_finish_command(sc, sc->busy.error);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
aml8726_sdxc_intr(void *arg)
|
|
|
|
{
|
|
|
|
struct aml8726_sdxc_softc *sc = (struct aml8726_sdxc_softc *)arg;
|
|
|
|
uint32_t isr;
|
|
|
|
uint32_t pdmar;
|
|
|
|
uint32_t sndr;
|
|
|
|
uint32_t sr;
|
|
|
|
int i;
|
|
|
|
int mmc_error;
|
|
|
|
int start;
|
|
|
|
int stop;
|
|
|
|
|
|
|
|
AML_SDXC_LOCK(sc);
|
|
|
|
|
|
|
|
isr = CSR_READ_4(sc, AML_SDXC_IRQ_STATUS_REG);
|
|
|
|
sndr = CSR_READ_4(sc, AML_SDXC_SEND_REG);
|
|
|
|
sr = CSR_READ_4(sc, AML_SDXC_STATUS_REG);
|
|
|
|
|
|
|
|
if (sc->cmd == NULL)
|
|
|
|
goto spurious;
|
|
|
|
|
|
|
|
mmc_error = MMC_ERR_NONE;
|
|
|
|
|
|
|
|
if ((isr & (AML_SDXC_IRQ_STATUS_TX_FIFO_EMPTY |
|
|
|
|
AML_SDXC_IRQ_STATUS_RX_FIFO_FULL)) != 0)
|
|
|
|
mmc_error = MMC_ERR_FIFO;
|
|
|
|
else if ((isr & (AML_SDXC_IRQ_ENABLE_A_PKG_CRC_ERR |
|
|
|
|
AML_SDXC_IRQ_ENABLE_RESP_CRC_ERR)) != 0)
|
|
|
|
mmc_error = MMC_ERR_BADCRC;
|
|
|
|
else if ((isr & (AML_SDXC_IRQ_ENABLE_A_PKG_TIMEOUT_ERR |
|
|
|
|
AML_SDXC_IRQ_ENABLE_RESP_TIMEOUT_ERR)) != 0)
|
|
|
|
mmc_error = MMC_ERR_TIMEOUT;
|
|
|
|
else if ((isr & (AML_SDXC_IRQ_STATUS_RESP_OK |
|
|
|
|
AML_SDXC_IRQ_STATUS_DMA_DONE |
|
|
|
|
AML_SDXC_IRQ_STATUS_TRANSFER_DONE_OK)) != 0) {
|
|
|
|
;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
spurious:
|
|
|
|
/*
|
|
|
|
* Clear spurious interrupts while leaving intacted any
|
|
|
|
* interrupts that may have occurred after we read the
|
|
|
|
* interrupt status register.
|
|
|
|
*/
|
|
|
|
|
|
|
|
CSR_WRITE_4(sc, AML_SDXC_IRQ_STATUS_REG,
|
|
|
|
(AML_SDXC_IRQ_STATUS_CLEAR & isr));
|
|
|
|
CSR_BARRIER(sc, AML_SDXC_IRQ_STATUS_REG);
|
|
|
|
AML_SDXC_UNLOCK(sc);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
aml8726_sdxc_disengage_dma(sc);
|
|
|
|
|
|
|
|
if ((sndr & AML_SDXC_SEND_CMD_HAS_RESP) != 0) {
|
|
|
|
start = 0;
|
|
|
|
stop = 1;
|
|
|
|
if ((sndr & AML_SDXC_SEND_RESP_136) != 0) {
|
|
|
|
start = 1;
|
2016-04-10 23:07:00 +00:00
|
|
|
stop = start + 4;
|
2015-03-31 11:50:46 +00:00
|
|
|
}
|
|
|
|
for (i = start; i < stop; i++) {
|
|
|
|
pdmar = CSR_READ_4(sc, AML_SDXC_PDMA_REG);
|
|
|
|
pdmar &= ~(AML_SDXC_PDMA_DMA_EN |
|
|
|
|
AML_SDXC_PDMA_RESP_INDEX_MASK);
|
|
|
|
pdmar |= i << AML_SDXC_PDMA_RESP_INDEX_SHIFT;
|
|
|
|
CSR_WRITE_4(sc, AML_SDXC_PDMA_REG, pdmar);
|
|
|
|
sc->cmd->resp[(stop - 1) - i] = CSR_READ_4(sc,
|
|
|
|
AML_SDXC_CMD_ARGUMENT_REG);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ((sr & AML_SDXC_STATUS_BUSY) != 0 &&
|
|
|
|
/*
|
|
|
|
* A multiblock operation may keep the hardware
|
|
|
|
* busy until stop transmission is executed.
|
|
|
|
*/
|
|
|
|
(isr & (AML_SDXC_IRQ_STATUS_DMA_DONE |
|
|
|
|
AML_SDXC_IRQ_STATUS_TRANSFER_DONE_OK)) == 0) {
|
|
|
|
if (mmc_error == MMC_ERR_NONE)
|
|
|
|
mmc_error = MMC_ERR_FAILED;
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Issue a soft reset to terminate the command.
|
|
|
|
*
|
|
|
|
* Ensure the command has terminated before continuing on
|
|
|
|
* to things such as bus_dmamap_sync / bus_dmamap_unload.
|
|
|
|
*/
|
|
|
|
|
|
|
|
aml8726_sdxc_soft_reset(sc);
|
|
|
|
|
|
|
|
while ((CSR_READ_4(sc, AML_SDXC_STATUS_REG) &
|
|
|
|
AML_SDXC_STATUS_BUSY) != 0)
|
|
|
|
cpu_spinwait();
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* The stop command can be generated either manually or
|
|
|
|
* automatically by the hardware if MISC_MANUAL_STOP_MODE
|
|
|
|
* has not been set. In either case check for busy.
|
|
|
|
*/
|
|
|
|
|
|
|
|
if (((sc->cmd->flags & MMC_RSP_BUSY) != 0 ||
|
|
|
|
(sndr & AML_SDXC_SEND_INDEX_MASK) == MMC_STOP_TRANSMISSION) &&
|
|
|
|
(sr & AML_SDXC_STATUS_DAT0) == 0) {
|
|
|
|
sc->busy.error = mmc_error;
|
|
|
|
callout_reset(&sc->ch,
|
|
|
|
msecs_to_ticks(AML_SDXC_BUSY_POLL_INTVL),
|
|
|
|
aml8726_sdxc_busy_check, sc);
|
|
|
|
CSR_WRITE_4(sc, AML_SDXC_IRQ_STATUS_REG,
|
|
|
|
(AML_SDXC_IRQ_STATUS_CLEAR & isr));
|
|
|
|
CSR_BARRIER(sc, AML_SDXC_IRQ_STATUS_REG);
|
|
|
|
AML_SDXC_UNLOCK(sc);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
aml8726_sdxc_finish_command(sc, mmc_error);
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
aml8726_sdxc_probe(device_t dev)
|
|
|
|
{
|
|
|
|
|
|
|
|
if (!ofw_bus_status_okay(dev))
|
|
|
|
return (ENXIO);
|
|
|
|
|
|
|
|
if (!ofw_bus_is_compatible(dev, "amlogic,aml8726-sdxc-m8"))
|
|
|
|
return (ENXIO);
|
|
|
|
|
|
|
|
device_set_desc(dev, "Amlogic aml8726-m8 SDXC");
|
|
|
|
|
|
|
|
return (BUS_PROBE_DEFAULT);
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
aml8726_sdxc_attach(device_t dev)
|
|
|
|
{
|
|
|
|
struct aml8726_sdxc_softc *sc = device_get_softc(dev);
|
|
|
|
char *voltages;
|
|
|
|
char *voltage;
|
|
|
|
int error;
|
|
|
|
int nvoltages;
|
|
|
|
pcell_t prop[3];
|
|
|
|
phandle_t node;
|
|
|
|
ssize_t len;
|
|
|
|
device_t child;
|
|
|
|
uint32_t ectlr;
|
|
|
|
uint32_t miscr;
|
|
|
|
uint32_t pdmar;
|
|
|
|
|
|
|
|
sc->dev = dev;
|
|
|
|
|
|
|
|
sc->auto_fill_flush = false;
|
|
|
|
|
|
|
|
pdmar = AML_SDXC_PDMA_DMA_URGENT |
|
|
|
|
(49 << AML_SDXC_PDMA_TX_THOLD_SHIFT) |
|
|
|
|
(7 << AML_SDXC_PDMA_RX_THOLD_SHIFT) |
|
|
|
|
(15 << AML_SDXC_PDMA_RD_BURST_SHIFT) |
|
|
|
|
(7 << AML_SDXC_PDMA_WR_BURST_SHIFT);
|
|
|
|
|
|
|
|
miscr = (2 << AML_SDXC_MISC_WCRC_OK_PAT_SHIFT) |
|
|
|
|
(5 << AML_SDXC_MISC_WCRC_ERR_PAT_SHIFT);
|
|
|
|
|
|
|
|
ectlr = (12 << AML_SDXC_ENH_CNTRL_SDIO_IRQ_PERIOD_SHIFT);
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Certain bitfields are dependent on the hardware revision.
|
|
|
|
*/
|
|
|
|
switch (aml8726_soc_hw_rev) {
|
|
|
|
case AML_SOC_HW_REV_M8:
|
|
|
|
switch (aml8726_soc_metal_rev) {
|
|
|
|
case AML_SOC_M8_METAL_REV_M2_A:
|
|
|
|
sc->auto_fill_flush = true;
|
|
|
|
miscr |= (6 << AML_SDXC_MISC_TXSTART_THOLD_SHIFT);
|
|
|
|
ectlr |= (64 << AML_SDXC_ENH_CNTRL_RX_FULL_THOLD_SHIFT) |
|
|
|
|
AML_SDXC_ENH_CNTRL_WR_RESP_MODE_SKIP_M8M2;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
miscr |= (7 << AML_SDXC_MISC_TXSTART_THOLD_SHIFT);
|
|
|
|
ectlr |= (63 << AML_SDXC_ENH_CNTRL_RX_FULL_THOLD_SHIFT) |
|
|
|
|
AML_SDXC_ENH_CNTRL_DMA_NO_WR_RESP_CHECK_M8 |
|
|
|
|
(255 << AML_SDXC_ENH_CNTRL_RX_TIMEOUT_SHIFT_M8);
|
|
|
|
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case AML_SOC_HW_REV_M8B:
|
|
|
|
miscr |= (7 << AML_SDXC_MISC_TXSTART_THOLD_SHIFT);
|
|
|
|
ectlr |= (63 << AML_SDXC_ENH_CNTRL_RX_FULL_THOLD_SHIFT) |
|
|
|
|
AML_SDXC_ENH_CNTRL_DMA_NO_WR_RESP_CHECK_M8 |
|
|
|
|
(255 << AML_SDXC_ENH_CNTRL_RX_TIMEOUT_SHIFT_M8);
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
device_printf(dev, "unsupported SoC\n");
|
|
|
|
return (ENXIO);
|
|
|
|
/* NOTREACHED */
|
|
|
|
}
|
|
|
|
|
|
|
|
node = ofw_bus_get_node(dev);
|
|
|
|
|
|
|
|
len = OF_getencprop(node, "clock-frequency", prop, sizeof(prop));
|
|
|
|
if ((len / sizeof(prop[0])) != 1 || prop[0] == 0) {
|
|
|
|
device_printf(dev,
|
|
|
|
"missing clock-frequency attribute in FDT\n");
|
|
|
|
return (ENXIO);
|
|
|
|
}
|
|
|
|
|
|
|
|
sc->ref_freq = prop[0];
|
|
|
|
|
|
|
|
sc->pwr_en.dev = NULL;
|
|
|
|
|
|
|
|
len = OF_getencprop(node, "mmc-pwr-en", prop, sizeof(prop));
|
|
|
|
if (len > 0) {
|
|
|
|
if ((len / sizeof(prop[0])) == 3) {
|
|
|
|
sc->pwr_en.dev = OF_device_from_xref(prop[0]);
|
|
|
|
sc->pwr_en.pin = prop[1];
|
|
|
|
sc->pwr_en.pol = prop[2];
|
|
|
|
}
|
|
|
|
|
|
|
|
if (sc->pwr_en.dev == NULL) {
|
|
|
|
device_printf(dev,
|
|
|
|
"unable to process mmc-pwr-en attribute in FDT\n");
|
|
|
|
return (ENXIO);
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Turn off power and then configure the output driver. */
|
|
|
|
if (aml8726_sdxc_power_off(sc) != 0 ||
|
|
|
|
GPIO_PIN_SETFLAGS(sc->pwr_en.dev, sc->pwr_en.pin,
|
|
|
|
GPIO_PIN_OUTPUT) != 0) {
|
|
|
|
device_printf(dev,
|
|
|
|
"could not use gpio to control power\n");
|
|
|
|
return (ENXIO);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
len = OF_getprop_alloc(node, "mmc-voltages",
|
|
|
|
sizeof(char), (void **)&voltages);
|
|
|
|
|
|
|
|
if (len < 0) {
|
|
|
|
device_printf(dev, "missing mmc-voltages attribute in FDT\n");
|
|
|
|
return (ENXIO);
|
|
|
|
}
|
|
|
|
|
|
|
|
sc->voltages[0] = 0;
|
|
|
|
sc->voltages[1] = 0;
|
|
|
|
|
|
|
|
voltage = voltages;
|
|
|
|
nvoltages = 0;
|
|
|
|
|
|
|
|
while (len && nvoltages < 2) {
|
|
|
|
if (strncmp("1.8", voltage, len) == 0)
|
|
|
|
sc->voltages[nvoltages] = MMC_OCR_LOW_VOLTAGE;
|
|
|
|
else if (strncmp("3.3", voltage, len) == 0)
|
|
|
|
sc->voltages[nvoltages] = MMC_OCR_320_330 |
|
|
|
|
MMC_OCR_330_340;
|
|
|
|
else {
|
|
|
|
device_printf(dev,
|
|
|
|
"unknown voltage attribute %.*s in FDT\n",
|
|
|
|
len, voltage);
|
2016-05-18 23:41:58 +00:00
|
|
|
OF_prop_free(voltages);
|
2015-03-31 11:50:46 +00:00
|
|
|
return (ENXIO);
|
|
|
|
}
|
|
|
|
|
|
|
|
nvoltages++;
|
|
|
|
|
|
|
|
/* queue up next string */
|
|
|
|
while (*voltage && len) {
|
|
|
|
voltage++;
|
|
|
|
len--;
|
|
|
|
}
|
|
|
|
if (len) {
|
|
|
|
voltage++;
|
|
|
|
len--;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-05-18 23:41:58 +00:00
|
|
|
OF_prop_free(voltages);
|
2015-03-31 11:50:46 +00:00
|
|
|
|
|
|
|
sc->vselect.dev = NULL;
|
|
|
|
|
|
|
|
len = OF_getencprop(node, "mmc-vselect", prop, sizeof(prop));
|
|
|
|
if (len > 0) {
|
|
|
|
if ((len / sizeof(prop[0])) == 2) {
|
|
|
|
sc->vselect.dev = OF_device_from_xref(prop[0]);
|
|
|
|
sc->vselect.pin = prop[1];
|
|
|
|
sc->vselect.pol = 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (sc->vselect.dev == NULL) {
|
|
|
|
device_printf(dev,
|
|
|
|
"unable to process mmc-vselect attribute in FDT\n");
|
|
|
|
return (ENXIO);
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* With the power off select voltage 0 and then
|
|
|
|
* configure the output driver.
|
|
|
|
*/
|
|
|
|
if (GPIO_PIN_SET(sc->vselect.dev, sc->vselect.pin, 0) != 0 ||
|
|
|
|
GPIO_PIN_SETFLAGS(sc->vselect.dev, sc->vselect.pin,
|
|
|
|
GPIO_PIN_OUTPUT) != 0) {
|
|
|
|
device_printf(dev,
|
|
|
|
"could not use gpio to set voltage\n");
|
|
|
|
return (ENXIO);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (nvoltages == 0) {
|
|
|
|
device_printf(dev, "no voltages in FDT\n");
|
|
|
|
return (ENXIO);
|
|
|
|
} else if (nvoltages == 1 && sc->vselect.dev != NULL) {
|
|
|
|
device_printf(dev, "only one voltage in FDT\n");
|
|
|
|
return (ENXIO);
|
|
|
|
} else if (nvoltages == 2 && sc->vselect.dev == NULL) {
|
|
|
|
device_printf(dev, "too many voltages in FDT\n");
|
|
|
|
return (ENXIO);
|
|
|
|
}
|
|
|
|
|
|
|
|
sc->card_rst.dev = NULL;
|
|
|
|
|
|
|
|
len = OF_getencprop(node, "mmc-rst", prop, sizeof(prop));
|
|
|
|
if (len > 0) {
|
|
|
|
if ((len / sizeof(prop[0])) == 3) {
|
|
|
|
sc->card_rst.dev = OF_device_from_xref(prop[0]);
|
|
|
|
sc->card_rst.pin = prop[1];
|
|
|
|
sc->card_rst.pol = prop[2];
|
|
|
|
}
|
|
|
|
|
|
|
|
if (sc->card_rst.dev == NULL) {
|
|
|
|
device_printf(dev,
|
|
|
|
"unable to process mmc-rst attribute in FDT\n");
|
|
|
|
return (ENXIO);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (bus_alloc_resources(dev, aml8726_sdxc_spec, sc->res)) {
|
|
|
|
device_printf(dev, "could not allocate resources for device\n");
|
|
|
|
return (ENXIO);
|
|
|
|
}
|
|
|
|
|
|
|
|
AML_SDXC_LOCK_INIT(sc);
|
|
|
|
|
|
|
|
error = bus_dma_tag_create(bus_get_dma_tag(dev), AML_SDXC_ALIGN_DMA, 0,
|
|
|
|
BUS_SPACE_MAXADDR_32BIT, BUS_SPACE_MAXADDR, NULL, NULL,
|
|
|
|
AML_SDXC_MAX_DMA, 1, AML_SDXC_MAX_DMA, 0, NULL, NULL, &sc->dmatag);
|
|
|
|
if (error)
|
|
|
|
goto fail;
|
|
|
|
|
|
|
|
error = bus_dmamap_create(sc->dmatag, 0, &sc->dmamap);
|
|
|
|
|
|
|
|
if (error)
|
|
|
|
goto fail;
|
|
|
|
|
|
|
|
error = bus_setup_intr(dev, sc->res[1], INTR_TYPE_MISC | INTR_MPSAFE,
|
|
|
|
NULL, aml8726_sdxc_intr, sc, &sc->ih_cookie);
|
|
|
|
if (error) {
|
|
|
|
device_printf(dev, "could not setup interrupt handler\n");
|
|
|
|
goto fail;
|
|
|
|
}
|
|
|
|
|
|
|
|
callout_init_mtx(&sc->ch, &sc->mtx, CALLOUT_RETURNUNLOCKED);
|
|
|
|
|
|
|
|
sc->host.f_min = 200000;
|
|
|
|
sc->host.f_max = 100000000;
|
|
|
|
sc->host.host_ocr = sc->voltages[0] | sc->voltages[1];
|
|
|
|
sc->host.caps = MMC_CAP_8_BIT_DATA | MMC_CAP_4_BIT_DATA |
|
|
|
|
MMC_CAP_HSPEED;
|
|
|
|
|
|
|
|
aml8726_sdxc_soft_reset(sc);
|
|
|
|
|
|
|
|
CSR_WRITE_4(sc, AML_SDXC_PDMA_REG, pdmar);
|
|
|
|
|
|
|
|
CSR_WRITE_4(sc, AML_SDXC_MISC_REG, miscr);
|
|
|
|
|
|
|
|
CSR_WRITE_4(sc, AML_SDXC_ENH_CNTRL_REG, ectlr);
|
|
|
|
|
|
|
|
child = device_add_child(dev, "mmc", -1);
|
|
|
|
|
|
|
|
if (!child) {
|
|
|
|
device_printf(dev, "could not add mmc\n");
|
|
|
|
error = ENXIO;
|
|
|
|
goto fail;
|
|
|
|
}
|
|
|
|
|
|
|
|
error = device_probe_and_attach(child);
|
|
|
|
|
|
|
|
if (error) {
|
|
|
|
device_printf(dev, "could not attach mmc\n");
|
|
|
|
goto fail;
|
|
|
|
}
|
|
|
|
|
|
|
|
return (0);
|
|
|
|
|
|
|
|
fail:
|
|
|
|
if (sc->ih_cookie)
|
|
|
|
bus_teardown_intr(dev, sc->res[1], sc->ih_cookie);
|
|
|
|
|
|
|
|
if (sc->dmamap)
|
|
|
|
bus_dmamap_destroy(sc->dmatag, sc->dmamap);
|
|
|
|
|
|
|
|
if (sc->dmatag)
|
|
|
|
bus_dma_tag_destroy(sc->dmatag);
|
|
|
|
|
|
|
|
AML_SDXC_LOCK_DESTROY(sc);
|
|
|
|
|
|
|
|
(void)aml8726_sdxc_power_off(sc);
|
|
|
|
|
|
|
|
bus_release_resources(dev, aml8726_sdxc_spec, sc->res);
|
|
|
|
|
|
|
|
return (error);
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
aml8726_sdxc_detach(device_t dev)
|
|
|
|
{
|
|
|
|
struct aml8726_sdxc_softc *sc = device_get_softc(dev);
|
|
|
|
|
|
|
|
AML_SDXC_LOCK(sc);
|
|
|
|
|
|
|
|
if (sc->cmd != NULL) {
|
|
|
|
AML_SDXC_UNLOCK(sc);
|
|
|
|
return (EBUSY);
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Turn off the power, reset the hardware state machine,
|
|
|
|
* and disable the interrupts.
|
|
|
|
*/
|
|
|
|
aml8726_sdxc_power_off(sc);
|
|
|
|
aml8726_sdxc_soft_reset(sc);
|
|
|
|
CSR_WRITE_4(sc, AML_SDXC_IRQ_ENABLE_REG, 0);
|
|
|
|
|
|
|
|
AML_SDXC_UNLOCK(sc);
|
|
|
|
|
|
|
|
bus_generic_detach(dev);
|
|
|
|
|
|
|
|
bus_teardown_intr(dev, sc->res[1], sc->ih_cookie);
|
|
|
|
|
|
|
|
bus_dmamap_destroy(sc->dmatag, sc->dmamap);
|
|
|
|
|
|
|
|
bus_dma_tag_destroy(sc->dmatag);
|
|
|
|
|
|
|
|
AML_SDXC_LOCK_DESTROY(sc);
|
|
|
|
|
|
|
|
bus_release_resources(dev, aml8726_sdxc_spec, sc->res);
|
|
|
|
|
|
|
|
return (0);
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
aml8726_sdxc_shutdown(device_t dev)
|
|
|
|
{
|
|
|
|
struct aml8726_sdxc_softc *sc = device_get_softc(dev);
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Turn off the power, reset the hardware state machine,
|
|
|
|
* and disable the interrupts.
|
|
|
|
*/
|
|
|
|
aml8726_sdxc_power_off(sc);
|
|
|
|
aml8726_sdxc_soft_reset(sc);
|
|
|
|
CSR_WRITE_4(sc, AML_SDXC_IRQ_ENABLE_REG, 0);
|
|
|
|
|
|
|
|
return (0);
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
aml8726_sdxc_update_ios(device_t bus, device_t child)
|
|
|
|
{
|
|
|
|
struct aml8726_sdxc_softc *sc = device_get_softc(bus);
|
|
|
|
struct mmc_ios *ios = &sc->host.ios;
|
|
|
|
unsigned int divisor;
|
|
|
|
int error;
|
|
|
|
int i;
|
|
|
|
uint32_t cctlr;
|
|
|
|
uint32_t clk2r;
|
|
|
|
uint32_t ctlr;
|
|
|
|
uint32_t freq;
|
|
|
|
|
|
|
|
ctlr = (7 << AML_SDXC_CNTRL_TX_ENDIAN_SHIFT) |
|
|
|
|
(7 << AML_SDXC_CNTRL_RX_ENDIAN_SHIFT) |
|
|
|
|
(0xf << AML_SDXC_CNTRL_RX_PERIOD_SHIFT) |
|
|
|
|
(0x7f << AML_SDXC_CNTRL_RX_TIMEOUT_SHIFT);
|
|
|
|
|
|
|
|
switch (ios->bus_width) {
|
|
|
|
case bus_width_8:
|
|
|
|
ctlr |= AML_SDXC_CNTRL_BUS_WIDTH_8;
|
|
|
|
break;
|
|
|
|
case bus_width_4:
|
|
|
|
ctlr |= AML_SDXC_CNTRL_BUS_WIDTH_4;
|
|
|
|
break;
|
|
|
|
case bus_width_1:
|
|
|
|
ctlr |= AML_SDXC_CNTRL_BUS_WIDTH_1;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
return (EINVAL);
|
|
|
|
}
|
|
|
|
|
|
|
|
CSR_WRITE_4(sc, AML_SDXC_CNTRL_REG, ctlr);
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Disable clocks and then clock module prior to setting desired values.
|
|
|
|
*/
|
|
|
|
cctlr = CSR_READ_4(sc, AML_SDXC_CLK_CNTRL_REG);
|
|
|
|
cctlr &= ~(AML_SDXC_CLK_CNTRL_TX_CLK_EN | AML_SDXC_CLK_CNTRL_RX_CLK_EN |
|
|
|
|
AML_SDXC_CLK_CNTRL_SD_CLK_EN);
|
|
|
|
CSR_WRITE_4(sc, AML_SDXC_CLK_CNTRL_REG, cctlr);
|
|
|
|
CSR_BARRIER(sc, AML_SDXC_CLK_CNTRL_REG);
|
|
|
|
cctlr &= ~AML_SDXC_CLK_CNTRL_CLK_MODULE_EN;
|
|
|
|
CSR_WRITE_4(sc, AML_SDXC_CLK_CNTRL_REG, cctlr);
|
|
|
|
CSR_BARRIER(sc, AML_SDXC_CLK_CNTRL_REG);
|
|
|
|
|
|
|
|
/*
|
|
|
|
* aml8726-m8
|
|
|
|
*
|
|
|
|
* Clock select 1 fclk_div2 (1.275 GHz)
|
|
|
|
*/
|
|
|
|
cctlr &= ~AML_SDXC_CLK_CNTRL_CLK_SEL_MASK;
|
|
|
|
cctlr |= (1 << AML_SDXC_CLK_CNTRL_CLK_SEL_SHIFT);
|
|
|
|
|
|
|
|
divisor = sc->ref_freq / ios->clock - 1;
|
|
|
|
if (divisor == 0 || divisor == -1)
|
|
|
|
divisor = 1;
|
|
|
|
if ((sc->ref_freq / (divisor + 1)) > ios->clock)
|
|
|
|
divisor += 1;
|
|
|
|
if (divisor > (AML_SDXC_CLK_CNTRL_CLK_DIV_MASK >>
|
|
|
|
AML_SDXC_CLK_CNTRL_CLK_DIV_SHIFT))
|
|
|
|
divisor = AML_SDXC_CLK_CNTRL_CLK_DIV_MASK >>
|
|
|
|
AML_SDXC_CLK_CNTRL_CLK_DIV_SHIFT;
|
|
|
|
|
|
|
|
cctlr &= ~AML_SDXC_CLK_CNTRL_CLK_DIV_MASK;
|
|
|
|
cctlr |= divisor << AML_SDXC_CLK_CNTRL_CLK_DIV_SHIFT;
|
|
|
|
|
|
|
|
cctlr &= ~AML_SDXC_CLK_CNTRL_MEM_PWR_MASK;
|
|
|
|
cctlr |= AML_SDXC_CLK_CNTRL_MEM_PWR_ON;
|
|
|
|
|
|
|
|
CSR_WRITE_4(sc, AML_SDXC_CLK_CNTRL_REG, cctlr);
|
|
|
|
CSR_BARRIER(sc, AML_SDXC_CLK_CNTRL_REG);
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Enable clock module and then clocks after setting desired values.
|
|
|
|
*/
|
|
|
|
cctlr |= AML_SDXC_CLK_CNTRL_CLK_MODULE_EN;
|
|
|
|
CSR_WRITE_4(sc, AML_SDXC_CLK_CNTRL_REG, cctlr);
|
|
|
|
CSR_BARRIER(sc, AML_SDXC_CLK_CNTRL_REG);
|
|
|
|
cctlr |= AML_SDXC_CLK_CNTRL_TX_CLK_EN | AML_SDXC_CLK_CNTRL_RX_CLK_EN |
|
|
|
|
AML_SDXC_CLK_CNTRL_SD_CLK_EN;
|
|
|
|
CSR_WRITE_4(sc, AML_SDXC_CLK_CNTRL_REG, cctlr);
|
|
|
|
CSR_BARRIER(sc, AML_SDXC_CLK_CNTRL_REG);
|
|
|
|
|
|
|
|
freq = sc->ref_freq / divisor;
|
|
|
|
|
|
|
|
for (i = 0; aml8726_sdxc_clk_phases[i].voltage; i++) {
|
|
|
|
if ((aml8726_sdxc_clk_phases[i].voltage &
|
|
|
|
(1 << ios->vdd)) != 0 &&
|
|
|
|
freq > aml8726_sdxc_clk_phases[i].freq)
|
|
|
|
break;
|
|
|
|
if (aml8726_sdxc_clk_phases[i].freq == 0)
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
clk2r = (1 << AML_SDXC_CLK2_SD_PHASE_SHIFT) |
|
|
|
|
(aml8726_sdxc_clk_phases[i].rx_phase <<
|
|
|
|
AML_SDXC_CLK2_RX_PHASE_SHIFT);
|
|
|
|
CSR_WRITE_4(sc, AML_SDXC_CLK2_REG, clk2r);
|
|
|
|
CSR_BARRIER(sc, AML_SDXC_CLK2_REG);
|
|
|
|
|
|
|
|
error = 0;
|
|
|
|
|
|
|
|
switch (ios->power_mode) {
|
|
|
|
case power_up:
|
|
|
|
/*
|
|
|
|
* Configure and power on the regulator so that the
|
|
|
|
* voltage stabilizes prior to powering on the card.
|
|
|
|
*/
|
|
|
|
if (sc->vselect.dev != NULL) {
|
|
|
|
for (i = 0; i < 2; i++)
|
|
|
|
if ((sc->voltages[i] & (1 << ios->vdd)) != 0)
|
|
|
|
break;
|
|
|
|
if (i >= 2)
|
|
|
|
return (EINVAL);
|
|
|
|
error = GPIO_PIN_SET(sc->vselect.dev,
|
|
|
|
sc->vselect.pin, i);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case power_on:
|
|
|
|
error = aml8726_sdxc_power_on(sc);
|
|
|
|
if (error)
|
|
|
|
break;
|
|
|
|
|
|
|
|
if (sc->card_rst.dev != NULL) {
|
|
|
|
if (GPIO_PIN_SET(sc->card_rst.dev, sc->card_rst.pin,
|
|
|
|
PIN_ON_FLAG(sc->card_rst.pol)) != 0 ||
|
|
|
|
GPIO_PIN_SETFLAGS(sc->card_rst.dev,
|
|
|
|
sc->card_rst.pin,
|
|
|
|
GPIO_PIN_OUTPUT) != 0)
|
|
|
|
error = ENXIO;
|
|
|
|
|
|
|
|
DELAY(5);
|
|
|
|
|
|
|
|
if (GPIO_PIN_SET(sc->card_rst.dev, sc->card_rst.pin,
|
|
|
|
PIN_OFF_FLAG(sc->card_rst.pol)) != 0)
|
|
|
|
error = ENXIO;
|
|
|
|
|
|
|
|
DELAY(5);
|
|
|
|
|
|
|
|
if (error) {
|
|
|
|
device_printf(sc->dev,
|
|
|
|
"could not use gpio to reset card\n");
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case power_off:
|
|
|
|
error = aml8726_sdxc_power_off(sc);
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
return (EINVAL);
|
|
|
|
}
|
|
|
|
|
|
|
|
return (error);
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
aml8726_sdxc_request(device_t bus, device_t child, struct mmc_request *req)
|
|
|
|
{
|
|
|
|
struct aml8726_sdxc_softc *sc = device_get_softc(bus);
|
|
|
|
int mmc_error;
|
|
|
|
|
|
|
|
AML_SDXC_LOCK(sc);
|
|
|
|
|
|
|
|
if (sc->cmd != NULL) {
|
|
|
|
AML_SDXC_UNLOCK(sc);
|
|
|
|
return (EBUSY);
|
|
|
|
}
|
|
|
|
|
|
|
|
mmc_error = aml8726_sdxc_start_command(sc, req->cmd);
|
|
|
|
|
|
|
|
AML_SDXC_UNLOCK(sc);
|
|
|
|
|
|
|
|
/* Execute the callback after dropping the lock. */
|
|
|
|
if (mmc_error != MMC_ERR_NONE) {
|
|
|
|
req->cmd->error = mmc_error;
|
|
|
|
req->done(req);
|
|
|
|
}
|
|
|
|
|
|
|
|
return (0);
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
aml8726_sdxc_read_ivar(device_t bus, device_t child,
|
|
|
|
int which, uintptr_t *result)
|
|
|
|
{
|
|
|
|
struct aml8726_sdxc_softc *sc = device_get_softc(bus);
|
|
|
|
|
|
|
|
switch (which) {
|
|
|
|
case MMCBR_IVAR_BUS_MODE:
|
|
|
|
*(int *)result = sc->host.ios.bus_mode;
|
|
|
|
break;
|
|
|
|
case MMCBR_IVAR_BUS_WIDTH:
|
|
|
|
*(int *)result = sc->host.ios.bus_width;
|
|
|
|
break;
|
|
|
|
case MMCBR_IVAR_CHIP_SELECT:
|
|
|
|
*(int *)result = sc->host.ios.chip_select;
|
|
|
|
break;
|
|
|
|
case MMCBR_IVAR_CLOCK:
|
|
|
|
*(int *)result = sc->host.ios.clock;
|
|
|
|
break;
|
|
|
|
case MMCBR_IVAR_F_MIN:
|
|
|
|
*(int *)result = sc->host.f_min;
|
|
|
|
break;
|
|
|
|
case MMCBR_IVAR_F_MAX:
|
|
|
|
*(int *)result = sc->host.f_max;
|
|
|
|
break;
|
|
|
|
case MMCBR_IVAR_HOST_OCR:
|
|
|
|
*(int *)result = sc->host.host_ocr;
|
|
|
|
break;
|
|
|
|
case MMCBR_IVAR_MODE:
|
|
|
|
*(int *)result = sc->host.mode;
|
|
|
|
break;
|
|
|
|
case MMCBR_IVAR_OCR:
|
|
|
|
*(int *)result = sc->host.ocr;
|
|
|
|
break;
|
|
|
|
case MMCBR_IVAR_POWER_MODE:
|
|
|
|
*(int *)result = sc->host.ios.power_mode;
|
|
|
|
break;
|
|
|
|
case MMCBR_IVAR_VDD:
|
|
|
|
*(int *)result = sc->host.ios.vdd;
|
|
|
|
break;
|
|
|
|
case MMCBR_IVAR_CAPS:
|
|
|
|
*(int *)result = sc->host.caps;
|
|
|
|
break;
|
|
|
|
case MMCBR_IVAR_MAX_DATA:
|
|
|
|
*(int *)result = AML_SDXC_MAX_DMA / MMC_SECTOR_SIZE;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
return (EINVAL);
|
|
|
|
}
|
|
|
|
|
|
|
|
return (0);
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
aml8726_sdxc_write_ivar(device_t bus, device_t child,
|
|
|
|
int which, uintptr_t value)
|
|
|
|
{
|
|
|
|
struct aml8726_sdxc_softc *sc = device_get_softc(bus);
|
|
|
|
|
|
|
|
switch (which) {
|
|
|
|
case MMCBR_IVAR_BUS_MODE:
|
|
|
|
sc->host.ios.bus_mode = value;
|
|
|
|
break;
|
|
|
|
case MMCBR_IVAR_BUS_WIDTH:
|
|
|
|
sc->host.ios.bus_width = value;
|
|
|
|
break;
|
|
|
|
case MMCBR_IVAR_CHIP_SELECT:
|
|
|
|
sc->host.ios.chip_select = value;
|
|
|
|
break;
|
|
|
|
case MMCBR_IVAR_CLOCK:
|
|
|
|
sc->host.ios.clock = value;
|
|
|
|
break;
|
|
|
|
case MMCBR_IVAR_MODE:
|
|
|
|
sc->host.mode = value;
|
|
|
|
break;
|
|
|
|
case MMCBR_IVAR_OCR:
|
|
|
|
sc->host.ocr = value;
|
|
|
|
break;
|
|
|
|
case MMCBR_IVAR_POWER_MODE:
|
|
|
|
sc->host.ios.power_mode = value;
|
|
|
|
break;
|
|
|
|
case MMCBR_IVAR_VDD:
|
|
|
|
sc->host.ios.vdd = value;
|
|
|
|
break;
|
|
|
|
/* These are read-only */
|
|
|
|
case MMCBR_IVAR_CAPS:
|
|
|
|
case MMCBR_IVAR_HOST_OCR:
|
|
|
|
case MMCBR_IVAR_F_MIN:
|
|
|
|
case MMCBR_IVAR_F_MAX:
|
|
|
|
case MMCBR_IVAR_MAX_DATA:
|
|
|
|
default:
|
|
|
|
return (EINVAL);
|
|
|
|
}
|
|
|
|
|
|
|
|
return (0);
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
aml8726_sdxc_get_ro(device_t bus, device_t child)
|
|
|
|
{
|
|
|
|
|
|
|
|
return (0);
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
aml8726_sdxc_acquire_host(device_t bus, device_t child)
|
|
|
|
{
|
|
|
|
struct aml8726_sdxc_softc *sc = device_get_softc(bus);
|
|
|
|
|
|
|
|
AML_SDXC_LOCK(sc);
|
|
|
|
|
|
|
|
while (sc->bus_busy)
|
|
|
|
mtx_sleep(sc, &sc->mtx, PZERO, "sdxc", hz / 5);
|
|
|
|
sc->bus_busy++;
|
|
|
|
|
|
|
|
AML_SDXC_UNLOCK(sc);
|
|
|
|
|
|
|
|
return (0);
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
aml8726_sdxc_release_host(device_t bus, device_t child)
|
|
|
|
{
|
|
|
|
struct aml8726_sdxc_softc *sc = device_get_softc(bus);
|
|
|
|
|
|
|
|
AML_SDXC_LOCK(sc);
|
|
|
|
|
|
|
|
sc->bus_busy--;
|
|
|
|
wakeup(sc);
|
|
|
|
|
|
|
|
AML_SDXC_UNLOCK(sc);
|
|
|
|
|
|
|
|
return (0);
|
|
|
|
}
|
|
|
|
|
|
|
|
static device_method_t aml8726_sdxc_methods[] = {
|
|
|
|
/* Device interface */
|
|
|
|
DEVMETHOD(device_probe, aml8726_sdxc_probe),
|
|
|
|
DEVMETHOD(device_attach, aml8726_sdxc_attach),
|
|
|
|
DEVMETHOD(device_detach, aml8726_sdxc_detach),
|
|
|
|
DEVMETHOD(device_shutdown, aml8726_sdxc_shutdown),
|
|
|
|
|
|
|
|
/* Bus interface */
|
|
|
|
DEVMETHOD(bus_read_ivar, aml8726_sdxc_read_ivar),
|
|
|
|
DEVMETHOD(bus_write_ivar, aml8726_sdxc_write_ivar),
|
|
|
|
|
|
|
|
/* MMC bridge interface */
|
|
|
|
DEVMETHOD(mmcbr_update_ios, aml8726_sdxc_update_ios),
|
|
|
|
DEVMETHOD(mmcbr_request, aml8726_sdxc_request),
|
|
|
|
DEVMETHOD(mmcbr_get_ro, aml8726_sdxc_get_ro),
|
|
|
|
DEVMETHOD(mmcbr_acquire_host, aml8726_sdxc_acquire_host),
|
|
|
|
DEVMETHOD(mmcbr_release_host, aml8726_sdxc_release_host),
|
|
|
|
|
|
|
|
DEVMETHOD_END
|
|
|
|
};
|
|
|
|
|
|
|
|
static driver_t aml8726_sdxc_driver = {
|
|
|
|
"aml8726_sdxc",
|
|
|
|
aml8726_sdxc_methods,
|
|
|
|
sizeof(struct aml8726_sdxc_softc),
|
|
|
|
};
|
|
|
|
|
|
|
|
static devclass_t aml8726_sdxc_devclass;
|
|
|
|
|
|
|
|
DRIVER_MODULE(aml8726_sdxc, simplebus, aml8726_sdxc_driver,
|
|
|
|
aml8726_sdxc_devclass, 0, 0);
|
|
|
|
MODULE_DEPEND(aml8726_sdxc, aml8726_gpio, 1, 1, 1);
|
2015-12-14 01:09:25 +00:00
|
|
|
DRIVER_MODULE(mmc, aml8726_sdxc, mmc_driver, mmc_devclass, NULL, NULL);
|
2016-03-21 00:52:24 +00:00
|
|
|
MODULE_DEPEND(aml8726_sdxc, mmc, 1, 1, 1);
|