rt-thread/bsp/fh8620/drivers/mmc.c

711 lines
20 KiB
C

/*
* This file is part of FH8620 BSP for RT-Thread distribution.
*
* Copyright (c) 2016 Shanghai Fullhan Microelectronics Co., Ltd.
* 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 as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* 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.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Visit http://www.fullhan.com to get contact with Fullhan.
*
* Change Logs:
* Date Author Notes
*/
#include "board_info.h"
#include <rtdef.h>
#include <rtdevice.h>
#include <drivers/mmcsd_core.h>
#include "mmc.h"
//#define FH_MMC_DEBUG
#define MMC_USE_INTERNAL_BUF
#ifdef FH_MMC_DEBUG
#define PRINT_MMC_DBG(fmt, args...) \
do \
{ \
rt_kprintf("FH_MMC_DEBUG: tick-%d, ", rt_tick_get()); \
rt_kprintf(fmt, ## args); \
} \
while(0)
#else
#define PRINT_MMC_DBG(fmt, args...) do { } while (0)
#endif
#define PRINT_MMC_REGS(base) \
do \
{ \
int i_for_marco; \
rt_uint32_t addr; \
for(i_for_marco=0; i_for_marco<20; i_for_marco++) \
{ \
addr = base + i_for_marco*4*4; \
rt_kprintf("0x%x: 0x%x, 0x%x, 0x%x, 0x%x\n", addr, \
GET_REG(addr+0x0), \
GET_REG(addr+0x4), \
GET_REG(addr+0x8), \
GET_REG(addr+0xc)); \
} \
} \
while(0)
#define MMC_INTERNAL_DMA_BUF_SIZE (32*1024)
static rt_uint32_t *g_mmc_dma_buf;
static int fh_mmc_write_pio(struct mmc_driver *mmc_drv)
{
struct fh_mmc_obj *mmc_obj = (struct fh_mmc_obj *)mmc_drv->priv;
struct rt_mmcsd_cmd *cmd = mmc_drv->cmd;
struct rt_mmcsd_data *data = RT_NULL;
rt_uint32_t size;
if(cmd)
data = cmd->data;
if(!data)
{
rt_kprintf("ERROR: %s, data is NULL\n", __func__);
return -RT_EIO;
}
size = data->blks * data->blksize;
PRINT_MMC_DBG("%s, Send %d bytes\n", __func__, size);
MMC_WriteData(mmc_obj, data->buf, size);
MMC_ResetFifo(mmc_obj);
return 0;
}
static int fh_mmc_read_pio(struct mmc_driver *mmc_drv)
{
struct fh_mmc_obj *mmc_obj = (struct fh_mmc_obj *)mmc_drv->priv;
struct rt_mmcsd_cmd *cmd = mmc_drv->cmd;
struct rt_mmcsd_data *data = RT_NULL;
rt_uint32_t size;
int ret;
if(cmd)
data = cmd->data;
if(!data)
{
rt_kprintf("ERROR: %s, data is NULL\n", __func__);
return -RT_EIO;
}
size = data->blks * data->blksize;
PRINT_MMC_DBG("%s, read %d bytes\n", __func__, size);
ret = MMC_ReadData(mmc_obj, data->buf, size);
if(ret)
{
rt_kprintf("ERROR: %s, fifo IO error, ret: %d\n", __func__, ret);
return -RT_EIO;
}
MMC_ResetFifo(mmc_obj);
return 0;
}
static void fh_mmc_set_iocfg(struct rt_mmcsd_host *host, struct rt_mmcsd_io_cfg *io_cfg)
{
rt_uint32_t clkdiv;
struct mmc_driver *mmc_drv = host->private_data;
struct fh_mmc_obj *mmc_obj = (struct fh_mmc_obj *)mmc_drv->priv;
PRINT_MMC_DBG("%s start\n", __func__);
//fixme: read from PMU
//why io_cfg->clock == 0 ?
if(io_cfg->clock)
{
clkdiv = MMC_CLOCK_IN / io_cfg->clock / 2;
MMC_UpdateClockRegister(mmc_obj, clkdiv);
PRINT_MMC_DBG("io_cfg->clock: %lu, clock in: %lu, clkdiv: %d\n", io_cfg->clock, MMC_CLOCK_IN, clkdiv);
}
if (io_cfg->bus_width == MMCSD_BUS_WIDTH_4)
{
MMC_SetCardWidth(mmc_obj, MMC_CARD_WIDTH_4BIT);
PRINT_MMC_DBG("set to 4-bit mode\n", MMC_CLOCK_IN, clkdiv);
}
else
{
MMC_SetCardWidth(mmc_obj, MMC_CARD_WIDTH_1BIT);
PRINT_MMC_DBG("set to 1-bit mode\n", MMC_CLOCK_IN, clkdiv);
}
/* maybe switch power to the card */
switch (io_cfg->power_mode)
{
case MMCSD_POWER_OFF:
break;
case MMCSD_POWER_UP:
break;
case MMCSD_POWER_ON:
break;
default:
rt_kprintf("ERROR: %s, unknown power_mode %d\n", __func__, io_cfg->power_mode);
break;
}
PRINT_MMC_DBG("%s end\n", __func__);
}
static void fh_mmc_enable_sdio_irq(struct rt_mmcsd_host *host, rt_int32_t enable)
{
struct mmc_driver *mmc_drv = host->private_data;
struct fh_mmc_obj *mmc_obj = (struct fh_mmc_obj *)mmc_drv->priv;
rt_uint32_t reg;
PRINT_MMC_DBG("%s start\n", __func__);
if (enable)
{
MMC_ClearRawInterrupt(mmc_obj, MMC_INT_STATUS_SDIO);
reg = MMC_GetInterruptMask(mmc_obj);
reg |= MMC_INT_STATUS_SDIO;
MMC_SetInterruptMask(mmc_obj, reg);
}
else
{
reg = MMC_GetInterruptMask(mmc_obj);
reg &= ~MMC_INT_STATUS_SDIO;
MMC_SetInterruptMask(mmc_obj, reg);
}
}
static rt_int32_t fh_mmc_get_card_status(struct rt_mmcsd_host *host)
{
PRINT_MMC_DBG("%s, start\n", __func__);
PRINT_MMC_DBG("%s, end\n", __func__);
return 0;
}
static void fh_mmc_send_command(struct mmc_driver *mmc_drv, struct rt_mmcsd_cmd *cmd)
{
struct fh_mmc_obj *mmc_obj = (struct fh_mmc_obj *)mmc_drv->priv;
struct rt_mmcsd_host *host = mmc_drv->host;
struct rt_mmcsd_req *req = mmc_drv->req;
//fixme: cmd->data or req->data
struct rt_mmcsd_data *data = cmd->data;
int ret;
rt_uint32_t retries = 0;
rt_uint32_t cmd_flags = 0;
PRINT_MMC_DBG("%s, start\n", __func__);
if (!cmd)
{
//fixme: stop dma
rt_kprintf("ERROR: %s, cmd is NULL\n", __func__);
return;
}
if (data)
{
cmd_flags |= MMC_CMD_FLAG_DATA_EXPECTED;
/* always set data start - also set direction flag for read */
if (data->flags & DATA_DIR_WRITE)
cmd_flags |= MMC_CMD_FLAG_WRITE_TO_CARD;
if (data->flags & DATA_STREAM)
cmd_flags |= MMC_CMD_FLAG_DATA_STREAM;
}
if (cmd == req->stop)
cmd_flags |= MMC_CMD_FLAG_STOP_TRANSFER;
else
cmd_flags |= MMC_CMD_FLAG_WAIT_PREV_DATA;
switch (resp_type(cmd))
{
case RESP_NONE:
break;
case RESP_R1:
case RESP_R5:
case RESP_R6:
case RESP_R7:
case RESP_R1B:
cmd_flags |= MMC_CMD_FLAG_RESPONSE_EXPECTED;
cmd_flags |= MMC_CMD_FLAG_CHECK_RESP_CRC;
break;
case RESP_R2:
cmd_flags |= MMC_CMD_FLAG_RESPONSE_EXPECTED;
cmd_flags |= MMC_CMD_FLAG_CHECK_RESP_CRC;
cmd_flags |= MMC_CMD_FLAG_LONG_RESPONSE;
break;
case RESP_R3:
case RESP_R4:
cmd_flags |= MMC_CMD_FLAG_RESPONSE_EXPECTED;
break;
default:
rt_kprintf("ERROR: %s, unknown cmd type %x\n", __func__, resp_type(cmd));
return;
}
if (cmd->cmd_code == GO_IDLE_STATE)
cmd_flags |= MMC_CMD_FLAG_SEND_INIT;
/* CMD 11 check switch voltage */
if (cmd->cmd_code == READ_DAT_UNTIL_STOP)
cmd_flags |= MMC_CMD_FLAG_SWITCH_VOLTAGE;
PRINT_MMC_DBG("cmd code: %d, args: 0x%x, resp type: 0x%x, flag: 0x%x\n", cmd->cmd_code, cmd->arg, resp_type(cmd), cmd_flags);
ret = MMC_SendCommand(mmc_obj, cmd->cmd_code, cmd->arg, cmd_flags);
if(ret)
{
rt_kprintf("ERROR: %s, Send command timeout, cmd: %d, status: 0x%x\n", __func__, cmd->cmd_code, MMC_GetStatus(mmc_obj));
}
}
static void fh_mmc_perpare_data(struct mmc_driver *mmc_drv)
{
struct rt_mmcsd_cmd *cmd = mmc_drv->cmd;
struct rt_mmcsd_data *data = cmd->data;
struct fh_mmc_obj *mmc_obj = (struct fh_mmc_obj *)mmc_drv->priv;
rt_uint32_t data_size;
int i;
if(!data)
{
MMC_SetBlockSize(mmc_obj, 0);
MMC_SetByteCount(mmc_obj, 0);
return;
}
PRINT_MMC_DBG("%s, start\n", __func__);
if(MMC_ResetFifo(mmc_obj))
{
return;
}
data_size = data->blks * data->blksize;
MMC_SetBlockSize(mmc_obj, data->blksize);
if(data_size % 4)
{
rt_kprintf("ERROR: data_size should be a multiple of 4, but now is %d\n", data_size);
}
MMC_SetByteCount(mmc_obj, data_size);
PRINT_MMC_DBG("%s, set blk size: 0x%x, byte count: 0x%x\n", __func__, data->blksize, data_size);
if(data_size > MMC_DMA_DESC_BUFF_SIZE * mmc_drv->max_desc)
{
rt_kprintf("ERROR: %s, given buffer is too big, size: 0x%x, max: 0x%x\n", __func__, data_size, MMC_DMA_DESC_BUFF_SIZE * mmc_drv->max_desc);
return;
}
if (data_size > MMC_INTERNAL_DMA_BUF_SIZE)
{
rt_kprintf("ERROR: please increase MMC_INTERNAL_DMA_BUF_SIZE.\n");
return;
}
#ifdef MMC_USE_DMA
#ifdef MMC_USE_INTERNAL_BUF
if (data->flags & DATA_DIR_WRITE)
{
rt_memcpy(g_mmc_dma_buf, data->buf, data_size);
mmu_clean_invalidated_dcache(g_mmc_dma_buf, data_size);
}
else
{
mmu_invalidate_dcache(g_mmc_dma_buf, data_size);
}
MMC_InitDescriptors(mmc_obj, (rt_uint32_t*)g_mmc_dma_buf, data_size);
mmu_clean_invalidated_dcache(mmc_obj->descriptors, sizeof(MMC_DMA_Descriptors) * mmc_drv->max_desc);
MMC_StartDma(mmc_obj);
#else
MMC_InitDescriptors(mmc_obj, data->buf, data_size);
mmu_clean_invalidated_dcache(mmc_obj->descriptors, sizeof(MMC_DMA_Descriptors) * mmc_drv->max_desc);
mmu_clean_invalidated_dcache(data->buf, data_size);
MMC_StartDma(mmc_obj);
#endif
#endif
PRINT_MMC_DBG("%s, end\n", __func__);
}
int fh_mmc_wait_card_idle(struct fh_mmc_obj *mmc_obj)
{
rt_uint32_t tick, timeout;
tick = rt_tick_get();
timeout = tick + RT_TICK_PER_SECOND / 2; //500ms
while(MMC_GetStatus(mmc_obj) & MMC_STATUS_DATA_BUSY)
{
tick = rt_tick_get();
if(tick > timeout)
{
return -RT_ETIMEOUT;
}
}
return 0;
}
static int fh_mmc_get_response(struct mmc_driver *mmc_drv, struct rt_mmcsd_cmd *cmd)
{
int i;
rt_uint32_t tick, timeout, status;
struct fh_mmc_obj *mmc_obj = (struct fh_mmc_obj *)mmc_drv->priv;
cmd->resp[0] = 0;
cmd->resp[1] = 0;
cmd->resp[2] = 0;
cmd->resp[3] = 0;
tick = rt_tick_get();
timeout = tick + RT_TICK_PER_SECOND / 2; //500ms
//fixme: spin_lock_irqsave?
do
{
status = MMC_GetRawInterrupt(mmc_obj);
tick = rt_tick_get();
if(tick > timeout)
{
PRINT_MMC_DBG("ERROR: %s, get response timeout(cmd is not received by card), RINTSTS: 0x%x, cmd: %d\n", __func__, status, cmd->cmd_code);
return -RT_ETIMEOUT;
}
}
while(!(status & MMC_INT_STATUS_CMD_DONE));
MMC_ClearRawInterrupt(mmc_obj, MMC_INT_STATUS_CMD_DONE);
for (i = 0; i < 4; i++)
{
if (resp_type(cmd) == RESP_R2)
{
cmd->resp[i] = MMC_GetResponse(mmc_obj, 3 - i);
//fixme : R2 must delay some time here ,when use UHI card, need check why
//1ms
//rt_thread_sleep(RT_TICK_PER_SECOND / 100);
}
else
{
cmd->resp[i] = MMC_GetResponse(mmc_obj, i);
}
}
PRINT_MMC_DBG("resp: 0x%x, 0x%x, 0x%x, 0x%x\n", cmd->resp[0], cmd->resp[1], cmd->resp[2], cmd->resp[3]);
if (status & MMC_INT_STATUS_RESPONSE_TIMEOUT)
{
MMC_ClearRawInterrupt(mmc_obj, MMC_INT_STATUS_RESPONSE_TIMEOUT);
PRINT_MMC_DBG("ERROR: %s, get response timeout, RINTSTS: 0x%x\n", __func__, status);
return -RT_ETIMEOUT;
}
else if (status & (MMC_INT_STATUS_RESP_CRC_ERROR | MMC_INT_STATUS_RESPONSE_ERROR))
{
MMC_ClearRawInterrupt(mmc_obj, MMC_INT_STATUS_RESP_CRC_ERROR | MMC_INT_STATUS_RESPONSE_ERROR);
rt_kprintf("ERROR: %s, response error or response crc error, RINTSTS: 0x%x\n", __func__, status);
//return -RT_ERROR;
}
return 0;
}
static int fh_mmc_start_transfer(struct mmc_driver *mmc_drv)
{
struct fh_mmc_obj *mmc_obj = (struct fh_mmc_obj *)mmc_drv->priv;
struct rt_mmcsd_host *host = mmc_drv->host;
struct rt_mmcsd_req *req = mmc_drv->req;
struct rt_mmcsd_cmd *cmd = mmc_drv->cmd;
struct rt_mmcsd_data *data = RT_NULL;
int ret;
rt_uint32_t interrupt, status, reg;
if(cmd)
data = cmd->data;
if(!data)
{
return 0;
}
PRINT_MMC_DBG("%s, start\n", __func__);
//fixme: spin_lock_irqsave(&host->lock, flags);
//open data interrupts
reg = MMC_GetInterruptMask(mmc_obj);
reg |= MMC_INT_STATUS_DATA;
MMC_SetInterruptMask(mmc_obj, reg);
//fixme: spin_unlock_irqrestore(&host->lock, flags);
ret = rt_completion_wait(&mmc_drv->transfer_completion, RT_TICK_PER_SECOND * 5);
reg = MMC_GetInterruptMask(mmc_obj);
reg &= ~MMC_INT_STATUS_DATA;
MMC_SetInterruptMask(mmc_obj, reg);
if(ret)
{
//fixme: error handle
cmd->err = ret;
interrupt = MMC_GetRawInterrupt(mmc_obj);
status = MMC_GetStatus(mmc_obj);
rt_kprintf("ERROR: %s, transfer timeout, ret: %d, RINTSTS: 0x%x, STATUS: 0x%x\n", __func__, ret, interrupt, status);
//PRINT_MMC_REGS(mmc_obj->base);
return -RT_ETIMEOUT;
}
data->bytes_xfered = data->blks * data->blksize;
#ifdef MMC_USE_INTERNAL_BUF
if (!(data->flags & DATA_DIR_WRITE))
{
rt_memcpy(data->buf, g_mmc_dma_buf, data->bytes_xfered);
mmu_invalidate_dcache(g_mmc_dma_buf, data->bytes_xfered);
}
#endif
return 0;
}
static void fh_mmc_complete_request(struct mmc_driver *mmc_drv)
{
struct fh_mmc_obj *mmc_obj = (struct fh_mmc_obj *)mmc_drv->priv;
#ifdef MMC_USE_DMA
MMC_StopDma(mmc_obj);
#endif
mmc_drv->cmd = RT_NULL;
mmc_drv->req = RT_NULL;
mmc_drv->data = RT_NULL;
rt_memset(mmc_obj->descriptors, 0, 4096);
MMC_SetBlockSize(mmc_obj, 0);
MMC_SetByteCount(mmc_obj, 0);
mmcsd_req_complete(mmc_drv->host);
}
static void fh_mmc_request(struct rt_mmcsd_host *host, struct rt_mmcsd_req *req)
{
int ret;
struct mmc_driver *mmc_drv = host->private_data;
struct rt_mmcsd_cmd *cmd = req->cmd;
struct fh_mmc_obj *mmc_obj = (struct fh_mmc_obj *)mmc_drv->priv;
PRINT_MMC_DBG("%s start\n", __func__);
mmc_drv->req = req;
mmc_drv->cmd = cmd;
rt_completion_init(&mmc_drv->transfer_completion);
ret = fh_mmc_wait_card_idle(mmc_obj);
if (ret)
{
rt_kprintf("ERROR: %s, data transfer timeout, status: 0x%x\n", __func__, MMC_GetStatus(mmc_obj));
return;
}
fh_mmc_perpare_data(mmc_drv);
fh_mmc_send_command(mmc_drv, cmd);
ret = fh_mmc_get_response(mmc_drv, cmd);
if(ret)
{
cmd->err = ret;
rt_kprintf("%s,get response returns %d, cmd: %d\n", __func__, ret, cmd->cmd_code);
goto out;
}
fh_mmc_start_transfer(mmc_drv);
if(req->stop)
{
/* send stop command */
PRINT_MMC_DBG("%s send stop\n", __func__);
fh_mmc_send_command(mmc_drv, req->stop);
}
out:
fh_mmc_complete_request(mmc_drv);
PRINT_MMC_DBG("%s end\n", __func__);
}
static const struct rt_mmcsd_host_ops fh_mmc_ops =
{
.request = fh_mmc_request,
.set_iocfg = fh_mmc_set_iocfg,
.enable_sdio_irq = fh_mmc_enable_sdio_irq,
.get_card_status = fh_mmc_get_card_status,
};
static void fh_mmc_interrupt(int irq, void *param)
{
struct mmc_driver *mmc_drv = (struct mmc_driver *)param;
struct fh_mmc_obj *mmc_obj = (struct fh_mmc_obj *)mmc_drv->priv;
struct rt_mmcsd_req *req = mmc_drv->req;
struct rt_mmcsd_cmd *cmd = mmc_drv->cmd;
struct rt_mmcsd_data *data;
rt_uint32_t status;
if (cmd && cmd->data)
{
data = cmd->data;
}
status = MMC_GetUnmaskedInterrupt(mmc_obj);
PRINT_MMC_DBG("unmasked interrupts: 0x%x\n", status);
if(status & MMC_INT_STATUS_CARD_DETECT)
{
rt_uint32_t card_status = MMC_GetCardStatus(mmc_obj);
if (card_status == CARD_UNPLUGED)
{
rt_kprintf("card disconnected\n");
}
else
{
rt_kprintf("card connected\n");
}
mmcsd_change(mmc_drv->host);
}
if (status & MMC_INT_STATUS_SDIO)
{
//fixme: handle sdio
//mmc_signal_sdio_irq ?
}
if(status & MMC_INIT_STATUS_DATA_ERROR)
{
rt_kprintf("ERROR: %s, data error, status: 0x%x\n", __func__, status);
}
if (status & MMC_INT_STATUS_TRANSFER_OVER)
{
//MMC_ResetFifo(mmc_obj);
//rt_completion_done(&mmc_drv->transfer_completion);
}
if (status & MMC_INT_STATUS_TX_REQUEST)
{
fh_mmc_write_pio(mmc_drv);
}
if (status & MMC_INT_STATUS_RX_REQUEST)
{
fh_mmc_read_pio(mmc_drv);
}
MMC_ClearRawInterrupt(mmc_obj, MMC_INT_STATUS_ALL);
rt_completion_done(&mmc_drv->transfer_completion);
}
int fh_mmc_probe(void *priv_data)
{
struct mmc_driver *mmc_drv;
struct rt_mmcsd_host *host;
struct fh_mmc_obj *mmc_obj = (struct fh_mmc_obj *)priv_data;
PRINT_MMC_DBG("%s start\n", __func__);
mmc_drv = (struct mmc_driver*)rt_malloc(sizeof(struct mmc_driver));
rt_memset(mmc_drv, 0, sizeof(struct mmc_driver));
mmc_drv->priv = mmc_obj;
host = mmcsd_alloc_host();
if (!host)
{
rt_kprintf("ERROR: %s, failed to malloc host\n", __func__);
return -RT_ENOMEM;
}
mmc_obj->descriptors = (MMC_DMA_Descriptors*)rt_malloc(4096+64);
mmc_obj->descriptors = (MMC_DMA_Descriptors*)(((UINT32)(mmc_obj->descriptors)+31)&(~31)); //cache-line aligned...
g_mmc_dma_buf = rt_malloc(MMC_INTERNAL_DMA_BUF_SIZE+64);
g_mmc_dma_buf = (rt_uint32_t*)(((rt_uint32_t)g_mmc_dma_buf+31) & (~31));
if(!mmc_obj->descriptors)
{
rt_kprintf("ERROR: %s, failed to malloc dma descriptors\n", __func__);
return -RT_ENOMEM;
}
rt_memset(mmc_obj->descriptors, 0, 4096);
mmc_drv->max_desc = 4096 / (sizeof(MMC_DMA_Descriptors));
host->ops = &fh_mmc_ops;
host->freq_min = MMC_FEQ_MIN;
host->freq_max = MMC_FEQ_MAX;
host->valid_ocr = VDD_32_33 | VDD_33_34;
host->flags = MMCSD_MUTBLKWRITE | \
MMCSD_SUP_HIGHSPEED | MMCSD_SUP_SDIO_IRQ;
host->max_seg_size = MMC_DMA_DESC_BUFF_SIZE;
host->max_dma_segs = mmc_drv->max_desc;
host->max_blk_size = 512;
//fixme: max_blk_count?
host->max_blk_count = 2048;
host->private_data = mmc_drv;
mmc_drv->host = host;
gpio_request(mmc_obj->power_pin_gpio);
gpio_direction_output(mmc_obj->power_pin_gpio, 0);
MMC_Init(mmc_obj);
if(mmc_obj->id == 0){
rt_hw_interrupt_install(mmc_obj->irq, fh_mmc_interrupt, (void *)mmc_drv, "mmc_isr_0");
}
else if(mmc_obj->id == 1){
rt_hw_interrupt_install(mmc_obj->irq, fh_mmc_interrupt, (void *)mmc_drv, "mmc_isr_1");
}
rt_hw_interrupt_umask(mmc_obj->irq);
mmcsd_change(host);
MMC_SetInterruptMask(mmc_obj, MMC_INT_STATUS_CARD_DETECT);
PRINT_MMC_DBG("%s end\n", __func__);
return 0;
}
int fh_mmc_exit(void *priv_data)
{
return 0;
}
struct fh_board_ops mmc_driver_ops =
{
.probe = fh_mmc_probe,
.exit = fh_mmc_exit,
};
void rt_hw_mmc_init(void)
{
PRINT_MMC_DBG("%s start\n", __func__);
fh_board_driver_register("mmc", &mmc_driver_ops);
PRINT_MMC_DBG("%s end\n", __func__);
}