49246fcdd5
Change-Id: I0d460d63070855df9b11eaf3d33eb7bb89c570f1
655 lines
18 KiB
C
655 lines
18 KiB
C
/*
|
|
* Block driver for Multi Media Cards (MMC).
|
|
*/
|
|
/* kernel headers */
|
|
#include <minix/syslib.h>
|
|
#include <minix/driver.h>
|
|
#include <minix/blockdriver.h>
|
|
#include <minix/drvlib.h>
|
|
|
|
/* system headers */
|
|
#include <sys/ioc_disk.h> /* disk IOCTL's */
|
|
|
|
/* usr headers */
|
|
#include <assert.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <stdarg.h>
|
|
#include <signal.h>
|
|
|
|
/* local headers */
|
|
#include "mmchost.h"
|
|
#include "mmclog.h"
|
|
|
|
/* used for logging */
|
|
static struct mmclog log = {
|
|
.name = "mmc_block",
|
|
.log_level = LEVEL_DEBUG,
|
|
.log_func = default_log
|
|
};
|
|
|
|
/* holding the current host controller */
|
|
static struct mmc_host host;
|
|
|
|
/*@TODO REMOVE THIS */
|
|
void
|
|
read_tsc_64(u64_t * t)
|
|
{
|
|
}
|
|
|
|
u32_t
|
|
tsc_64_to_micros(u64_t tsc)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
#define SUB_PER_DRIVE (NR_PARTITIONS * NR_PARTITIONS)
|
|
#define NR_SUBDEVS (MAX_DRIVES * SUB_PER_DRIVE)
|
|
|
|
/* When passing data over a grant one needs to pass
|
|
* a buffer to sys_safecopy copybuff is used for that*/
|
|
#define COPYBUFF_SIZE 0x1000 /* 4k buff */
|
|
static unsigned char copybuff[COPYBUFF_SIZE];
|
|
|
|
static struct sd_slot *get_slot(dev_t minor);
|
|
|
|
/* Prototypes for the block device */
|
|
static int block_open(dev_t minor, int access);
|
|
static int block_close(dev_t minor);
|
|
static int block_transfer(dev_t minor,
|
|
int do_write,
|
|
u64_t position,
|
|
endpoint_t endpt, iovec_t * iov, unsigned int nr_req, int flags);
|
|
|
|
static int block_ioctl(dev_t minor,
|
|
unsigned int request, endpoint_t endpt, cp_grant_id_t grant);
|
|
static struct device *block_part(dev_t minor);
|
|
|
|
/* System even handling */
|
|
static void sef_local_startup();
|
|
static int block_system_event_cb(int type, sef_init_info_t * info);
|
|
static void block_signal_handler_cb(int signo);
|
|
|
|
static int apply_env();
|
|
|
|
#if 0
|
|
/* set the global logging level */
|
|
static void set_log_level(int level);
|
|
#endif
|
|
|
|
/* Entry points for the BLOCK driver. */
|
|
static struct blockdriver mmc_driver = {
|
|
BLOCKDRIVER_TYPE_DISK, /* handle partition requests */
|
|
block_open, /* open or mount */
|
|
block_close, /* on a close */
|
|
block_transfer, /* does the I/O */
|
|
block_ioctl, /* ioclt's */
|
|
NULL, /* no need to clean up (yet) */
|
|
block_part, /* return partition information */
|
|
NULL, /* no geometry */
|
|
NULL, /* no interrupt processing */
|
|
NULL, /* no alarm processing */
|
|
NULL, /* no processing of other messages */
|
|
NULL /* no threading support */
|
|
};
|
|
|
|
static int
|
|
apply_env()
|
|
{
|
|
#if 0
|
|
/* @TODO: re-enable this function when __aeabi_idiv will be present
|
|
* The following code(env_parse) uses strtol.c and needs __aeabi_idiv */
|
|
/* apply the env setting passed to this driver parameters accepted
|
|
* log_level=[0-4] (NONE,WARNING,INFO,DEBUG,TRACE) instance=[0-3]
|
|
* instance/bus number to use for this driver Passing these arguments
|
|
* is done when starting the driver using the service command in the
|
|
* following way service up /sbin/mmcblk -args "log_level=2
|
|
* instance=1" */
|
|
long v;
|
|
|
|
/* Initialize the verbosity level. */
|
|
v = 0;
|
|
if (env_parse("log_level", "d", 0, &v, LEVEL_NONE,
|
|
LEVEL_TRACE) == EP_SET) {
|
|
set_log_level(v);
|
|
}
|
|
|
|
/* Find out which driver instance we are. */
|
|
v = 0;
|
|
env_parse("instance", "d", 0, &v, 0, 3);
|
|
if (host.host_set_instance(&host, v)) {
|
|
mmc_log_warn(&log, "Failed to set mmc instance to %d\n", v);
|
|
return -1; /* NOT OK */
|
|
}
|
|
#endif
|
|
return OK;
|
|
}
|
|
|
|
;
|
|
|
|
/*===========================================================================*
|
|
* block_open *
|
|
*===========================================================================*/
|
|
static int
|
|
block_open(dev_t minor, int access)
|
|
{
|
|
struct sd_slot *slot;
|
|
slot = get_slot(minor);
|
|
int i, j;
|
|
int part_count, sub_part_count;
|
|
|
|
i = j = part_count = sub_part_count = 0;
|
|
|
|
if (!slot) {
|
|
mmc_log_debug(&log,
|
|
"Not handling open on non existing slot\n");
|
|
return EIO;
|
|
}
|
|
|
|
assert(slot->host != NULL);
|
|
|
|
if (!slot->host->card_detect(slot)) {
|
|
mmc_log_debug(&log, "No card inserted in the SD slot\n");
|
|
return EIO;
|
|
}
|
|
|
|
/* If we are already open just increase the open count and return */
|
|
if (slot->card.state == SD_MODE_DATA_TRANSFER_MODE) {
|
|
assert(slot->card.open_ct >= 0);
|
|
slot->card.open_ct++;
|
|
mmc_log_trace(&log, "increased open count to %d\n",
|
|
slot->card.open_ct);
|
|
return OK;
|
|
}
|
|
|
|
/* We did not have an sd-card inserted so we are going to probe for it
|
|
*/
|
|
mmc_log_debug(&log, "First open on (%d)\n", minor);
|
|
if (!host.card_initialize(slot)) {
|
|
// * TODO: set card state to INVALID until removed? */
|
|
return EIO;
|
|
}
|
|
|
|
partition(&mmc_driver, 0 /* first card on bus */ , P_PRIMARY,
|
|
0 /* atapi device?? */ );
|
|
|
|
mmc_log_debug(&log, "descr \toffset(bytes) size(bytes)\n", minor);
|
|
|
|
mmc_log_debug(&log, "disk %d\t0x%016llx 0x%016llx\n", i,
|
|
slot->card.part[0].dv_base, slot->card.part[0].dv_size);
|
|
for (i = 1; i < 5; i++) {
|
|
if (slot->card.part[i].dv_size == 0)
|
|
continue;
|
|
part_count++;
|
|
mmc_log_debug(&log, "part %d\t0x%016llx 0x%016llx\n", i,
|
|
slot->card.part[i].dv_base, slot->card.part[i].dv_size);
|
|
for (j = 0; j < 4; j++) {
|
|
if (slot->card.part[i * 4 + j].dv_size == 0)
|
|
continue;
|
|
sub_part_count++;
|
|
mmc_log_debug(&log,
|
|
" sub %d/%d\t0x%016llx 0x%016llx\n", i, j,
|
|
slot->card.part[i * 4 + j].dv_base,
|
|
slot->card.part[i * 4 + j].dv_size);
|
|
}
|
|
}
|
|
mmc_log_info(&log, "Found %d partitions and %d sub partitions\n",
|
|
part_count, sub_part_count);
|
|
slot->card.open_ct++;
|
|
assert(slot->card.open_ct == 1);
|
|
return OK;
|
|
}
|
|
|
|
/*===========================================================================*
|
|
* block_close *
|
|
*===========================================================================*/
|
|
static int
|
|
block_close(dev_t minor)
|
|
{
|
|
struct sd_slot *slot;
|
|
|
|
slot = get_slot(minor);
|
|
if (!slot) {
|
|
mmc_log_debug(&log,
|
|
"Not handling open on non existing slot\n");
|
|
return EIO;
|
|
}
|
|
|
|
/* if we arrived here we expect a card to be present, we will need do
|
|
* deal with removal later */
|
|
assert(slot->host != NULL);
|
|
assert(slot->card.open_ct >= 1);
|
|
|
|
/* If this is not the last open count simply decrease the counter and
|
|
* return */
|
|
if (slot->card.open_ct > 1) {
|
|
slot->card.open_ct--;
|
|
mmc_log_trace(&log, "decreased open count to %d\n",
|
|
slot->card.open_ct);
|
|
return OK;
|
|
}
|
|
|
|
assert(slot->card.open_ct == 1);
|
|
mmc_log_debug(&log,
|
|
"freeing the block device as it is no longer used\n");
|
|
|
|
/* release the card as check the open_ct should be 0 */
|
|
slot->host->card_release(&slot->card);
|
|
assert(slot->card.open_ct == 0);
|
|
return OK;
|
|
}
|
|
|
|
static int
|
|
copyto(endpoint_t dst_e,
|
|
cp_grant_id_t gr_id, vir_bytes offset, vir_bytes address, size_t bytes)
|
|
{
|
|
/* Helper function that used memcpy to copy data when the endpoint ==
|
|
* SELF */
|
|
if (dst_e == SELF) {
|
|
memcpy((char *) gr_id + offset, (char *) address, bytes);
|
|
return OK;
|
|
} else {
|
|
/* Read io_size bytes from our data at the correct * offset
|
|
* and write it to the output buffer at 0 */
|
|
return sys_safecopyto(dst_e, gr_id, offset, address, bytes);
|
|
}
|
|
}
|
|
|
|
static int
|
|
copyfrom(endpoint_t src_e,
|
|
cp_grant_id_t gr_id, vir_bytes offset, vir_bytes address, size_t bytes)
|
|
{
|
|
/* Helper function that used memcpy to copy data when the endpoint ==
|
|
* SELF */
|
|
if (src_e == SELF) {
|
|
memcpy((char *) address, (char *) gr_id + offset, bytes);
|
|
return OK;
|
|
} else {
|
|
return sys_safecopyfrom(src_e, gr_id, offset, address, bytes);
|
|
}
|
|
}
|
|
|
|
/*===========================================================================*
|
|
* block_transfer *
|
|
*===========================================================================*/
|
|
static int
|
|
block_transfer(dev_t minor, /* minor device number */
|
|
int do_write, /* read or write? */
|
|
u64_t position, /* offset on device to read or write */
|
|
endpoint_t endpt, /* process doing the request */
|
|
iovec_t * iov, /* pointer to read or write request vector */
|
|
unsigned int nr_req, /* length of request vector */
|
|
int flags /* transfer flags */
|
|
)
|
|
{
|
|
unsigned long counter;
|
|
iovec_t *ciov; /* Current IO Vector */
|
|
struct device *dev; /* The device used */
|
|
struct sd_slot *slot; /* The sd slot the requests is pointed to */
|
|
vir_bytes io_size; /* Size to read/write to/from the iov */
|
|
vir_bytes io_offset; /* Size to read/write to/from the iov */
|
|
vir_bytes bytes_written;
|
|
|
|
int r, blk_size, i;
|
|
|
|
/* Get the current "device" geometry */
|
|
dev = block_part(minor);
|
|
if (dev == NULL) {
|
|
mmc_log_warn(&log,
|
|
"Transfer requested on unknown device minor(%d)\n", minor);
|
|
/* Unknown device */
|
|
return ENXIO;
|
|
}
|
|
mmc_log_trace(&log, "I/O %d %s 0x%llx\n", minor,
|
|
(do_write) ? "Write" : "Read", position);
|
|
|
|
slot = get_slot(minor);
|
|
assert(slot);
|
|
|
|
if (slot->card.blk_size == 0) {
|
|
mmc_log_warn(&log, "Request on a card with block size of 0\n");
|
|
return EINVAL;
|
|
}
|
|
if (slot->card.blk_size > COPYBUFF_SIZE) {
|
|
mmc_log_warn(&log,
|
|
"Card block size (%d) exceeds internal buffer size %d\n",
|
|
slot->card.blk_size, COPYBUFF_SIZE);
|
|
return EINVAL;
|
|
}
|
|
|
|
/* It is fully up to the driver to decide on restrictions for the
|
|
* parameters of transfers, in those cases we return EINVAL */
|
|
if (position % slot->card.blk_size != 0) {
|
|
/* Starting at a block boundary */
|
|
mmc_log_warn(&log,
|
|
"Requests must start at a block boundary"
|
|
"(start,block size)=(%016llx,%08x)\n", position,
|
|
slot->card.blk_size);
|
|
return EINVAL;
|
|
}
|
|
|
|
blk_size = slot->card.blk_size;
|
|
|
|
bytes_written = 0;
|
|
|
|
/* Are we trying to start reading past the end */
|
|
if (position >= dev->dv_size) {
|
|
mmc_log_warn(&log, "start reading past drive size\n");
|
|
return 0;
|
|
};
|
|
|
|
ciov = iov;
|
|
/* do some more validation */
|
|
for (counter = 0; counter < nr_req; counter++) {
|
|
assert(ciov != NULL);
|
|
if (ciov->iov_size % blk_size != 0) {
|
|
/* transfer a multiple of blk_size */
|
|
mmc_log_warn(&log,
|
|
"Requests must start at a block boundary "
|
|
"(start,block size)=(%016llx,%08x)\n", position,
|
|
slot->card.blk_size);
|
|
return EINVAL;
|
|
}
|
|
|
|
if (ciov->iov_size == 0 || ciov->iov_size < 0) {
|
|
mmc_log_warn(&log,
|
|
"Invalid iov size for iov %d of %d size\n",
|
|
counter, nr_req, ciov->iov_size);
|
|
return EINVAL;
|
|
}
|
|
ciov++;
|
|
}
|
|
|
|
ciov = iov;
|
|
for (counter = 0; counter < nr_req; counter++) {
|
|
/* Assume we are to transfer the amount of data given in the
|
|
* input/output vector but ensure we are not doing i/o past
|
|
* our own boundaries */
|
|
io_size = ciov->iov_size;
|
|
io_offset = position + bytes_written;
|
|
|
|
/* Check we are not reading/writing past the end */
|
|
if (position + bytes_written + io_size > dev->dv_size) {
|
|
io_size = dev->dv_size - (position + bytes_written);
|
|
};
|
|
|
|
mmc_log_trace(&log,
|
|
"I/O %s request(%d/%d) iov(grant,size,iosize,"
|
|
"offset)=(%d,%d,%d,%d)\n",
|
|
(do_write) ? "write" : "read", counter + 1, nr_req,
|
|
ciov->iov_addr, ciov->iov_size, io_size, io_offset);
|
|
/* transfer max one block at the time */
|
|
for (i = 0; i < io_size / blk_size; i++) {
|
|
if (do_write) {
|
|
/* Read io_size bytes from i/o vector starting
|
|
* at 0 and write it to out buffer at the
|
|
* correct offset */
|
|
r = copyfrom(endpt, ciov->iov_addr,
|
|
i * blk_size, (vir_bytes) copybuff,
|
|
blk_size);
|
|
if (r != OK) {
|
|
mmc_log_warn(&log,
|
|
"I/O write error: %s iov(base,size)=(%d,%d)"
|
|
" at offset=%d\n",
|
|
strerror(_SIGN r), ciov->iov_addr,
|
|
ciov->iov_size, io_offset);
|
|
return EINVAL;
|
|
}
|
|
|
|
/* write a single block */
|
|
slot->host->write(&slot->card,
|
|
(dev->dv_base / blk_size) +
|
|
(io_offset / blk_size) + i, 1, copybuff);
|
|
bytes_written += blk_size;
|
|
} else {
|
|
/* read a single block info copybuff */
|
|
slot->host->read(&slot->card,
|
|
(dev->dv_base / blk_size) +
|
|
(io_offset / blk_size) + i, 1, copybuff);
|
|
/* Read io_size bytes from our data at the
|
|
* correct offset and write it to the output
|
|
* buffer at 0 */
|
|
r = copyto(endpt, ciov->iov_addr, i * blk_size,
|
|
(vir_bytes) copybuff, blk_size);
|
|
if (r != OK) {
|
|
mmc_log_warn(&log,
|
|
"I/O read error: %s iov(base,size)=(%d,%d)"
|
|
" at offset=%d\n",
|
|
strerror(_SIGN r), ciov->iov_addr,
|
|
ciov->iov_size, io_offset);
|
|
return EINVAL;
|
|
}
|
|
bytes_written += blk_size;
|
|
}
|
|
}
|
|
ciov++;
|
|
}
|
|
return bytes_written;
|
|
}
|
|
|
|
/*===========================================================================*
|
|
* block_ioctl *
|
|
*===========================================================================*/
|
|
static int
|
|
block_ioctl(dev_t minor,
|
|
unsigned int request, endpoint_t endpt, cp_grant_id_t grant)
|
|
{
|
|
/* IOCTL handling */
|
|
struct sd_slot *slot;
|
|
mmc_log_trace(&log,
|
|
"enter (minor,request,endpoint,grant)=(%d,%lu,%d)\n", minor,
|
|
request, endpt, grant);
|
|
|
|
slot = get_slot(minor);
|
|
if (!slot) {
|
|
mmc_log_warn(&log,
|
|
"Doing ioctl on non existing block device(%d)\n", minor);
|
|
return EINVAL;
|
|
}
|
|
|
|
switch (request) {
|
|
case DIOCOPENCT:
|
|
// TODO: add a check for card validity */
|
|
mmc_log_trace(&log, "returning open count %d\n",
|
|
slot->card.open_ct);
|
|
/* return the current open count */
|
|
return sys_safecopyto(endpt, grant, 0,
|
|
(vir_bytes) & slot->card.open_ct,
|
|
sizeof(slot->card.open_ct));
|
|
case DIOCFLUSH:
|
|
/* No need to flush but some devices like movinands require
|
|
* 500 ms inactivity */
|
|
return OK;
|
|
}
|
|
|
|
return EINVAL;
|
|
}
|
|
|
|
/*===========================================================================*
|
|
* block_part *
|
|
*===========================================================================*/
|
|
static struct device *
|
|
block_part(dev_t minor)
|
|
{
|
|
/*
|
|
* Reuse the existing MINIX major/minor partitioning scheme.
|
|
* - 8 drives
|
|
* - 5 devices per drive allowing direct access to the disk and up to 4
|
|
* partitions (IBM style partitioning without extended partitions)
|
|
* - 4 Minix style sub partitions per partitions
|
|
*/
|
|
struct device *dev;
|
|
struct sd_slot *slot;
|
|
|
|
dev = NULL;
|
|
slot = get_slot(minor);
|
|
if (!slot) {
|
|
mmc_log_warn(&log,
|
|
"Device information requested for non existing partition "
|
|
"minor(%d)\n", minor);
|
|
return NULL;
|
|
}
|
|
|
|
if (!slot->host->card_detect(slot)) {
|
|
mmc_log_warn(&log,
|
|
"Device information requested from empty slot(%d)\n",
|
|
minor);
|
|
return NULL;
|
|
}
|
|
|
|
if (minor < 5) {
|
|
/* we are talking about the first disk */
|
|
dev = &slot->card.part[minor];
|
|
mmc_log_trace(&log,
|
|
"returning partition(%d) (base,size)=(0x%016llx,0x%016llx)\n",
|
|
minor, dev->dv_base, dev->dv_size);
|
|
} else if (minor >= 128 && minor <= 128 + 16) {
|
|
/* sub partitions of the first disk we don't care about the
|
|
* rest */
|
|
dev = &slot->card.subpart[minor - 128];
|
|
mmc_log_trace(&log,
|
|
"returning sub partition(%d) (base,size)=(0x%016llx,0x%016llx)\n",
|
|
minor - 128, dev->dv_base, dev->dv_size);
|
|
|
|
} else {
|
|
mmc_log_warn(&log,
|
|
"Device information requested for non existing "
|
|
"partition minor(%d)\n", minor);
|
|
}
|
|
return dev;
|
|
}
|
|
|
|
/*===========================================================================*
|
|
* sef_local_startup *
|
|
*===========================================================================*/
|
|
static void
|
|
sef_local_startup()
|
|
{
|
|
mmc_log_info(&log, "Initializing the MMC block device\n");
|
|
if (apply_env()) {
|
|
mmc_log_warn(&log,
|
|
"Failed while applying environment settings\n");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
if (host.host_init(&host)) {
|
|
mmc_log_warn(&log,
|
|
"Failed to initialize the host controller\n");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
/*
|
|
* Register callbacks for fresh start, live update and restart.
|
|
* Use the same function for all event types
|
|
*/
|
|
sef_setcb_init_fresh(block_system_event_cb);
|
|
sef_setcb_init_lu(block_system_event_cb);
|
|
sef_setcb_init_restart(block_system_event_cb);
|
|
|
|
/* Register a signal handler */
|
|
sef_setcb_signal_handler(block_signal_handler_cb);
|
|
|
|
/* SEF startup */
|
|
sef_startup();
|
|
}
|
|
|
|
/*===========================================================================*
|
|
* block_system_event_cb *
|
|
*===========================================================================*/
|
|
static int
|
|
block_system_event_cb(int type, sef_init_info_t * info)
|
|
{
|
|
/*
|
|
* Callbacks for the System event framework as registered in
|
|
* sef_local_startup */
|
|
switch (type) {
|
|
case SEF_INIT_FRESH:
|
|
mmc_log_info(&log, "System event framework fresh start\n");
|
|
break;
|
|
|
|
case SEF_INIT_LU:
|
|
/* Restore the state. post update */
|
|
mmc_log_info(&log, "System event framework live update\n");
|
|
break;
|
|
|
|
case SEF_INIT_RESTART:
|
|
mmc_log_info(&log, "System event framework post restart\n");
|
|
break;
|
|
}
|
|
return OK;
|
|
}
|
|
|
|
/*===========================================================================*
|
|
* block_signal_handler_cb *
|
|
*===========================================================================*/
|
|
static void
|
|
block_signal_handler_cb(int signo)
|
|
{
|
|
mmc_log_debug(&log, "System event framework signal handler sig(%d)\n",
|
|
signo);
|
|
/* Only check for termination signal, ignore anything else. */
|
|
if (signo != SIGTERM)
|
|
return;
|
|
// FIXME shutdown
|
|
exit(0);
|
|
}
|
|
|
|
#define IS_MINIX_SUB_PARTITION_MINOR(minor) (minor >= MINOR_d0p0s0 )
|
|
|
|
static struct sd_slot *
|
|
get_slot(dev_t minor)
|
|
{
|
|
/*
|
|
* Get an sd_slot based on the minor number.
|
|
*
|
|
* This driver only supports a single card at at time. Also as
|
|
* we are following the major/minor scheme of other driver we
|
|
* must return a slot for all minors on disk 0 these are 0-5
|
|
* for the disk and 4 main partitions and
|
|
* number 128 till 144 for sub partitions.
|
|
*/
|
|
/* If this is a minor for the first disk (e.g. minor 0 till 5) */
|
|
if (minor / DEV_PER_DRIVE == 0) {
|
|
/* we are talking about the first disk and that is all we
|
|
* support */
|
|
return &host.slot[0];
|
|
} else if ( IS_MINIX_SUB_PARTITION_MINOR(minor)
|
|
&& (((minor - MINOR_d0p0s0) / SUB_PER_DRIVE) == 0)) {
|
|
/* a minor from the first disk */
|
|
return &host.slot[0];
|
|
} else {
|
|
mmc_log_trace(&log,
|
|
"Device information requested for non existing partition "
|
|
"minor(%d)\n", minor);
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
#if 0
|
|
static void
|
|
set_log_level(int level)
|
|
{
|
|
if (level < 0 || level >= 4) {
|
|
return;
|
|
}
|
|
mmc_log_debug(&log, "Setting verbosity level to %d\n", level);
|
|
log.log_level = level;
|
|
if (host.set_log_level) {
|
|
host.set_log_level(level);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
int
|
|
main(int argc, char **argv)
|
|
{
|
|
/* early init of host mmc host controller. This code should depend on
|
|
* knowing the hardware that is running bellow. */
|
|
host_initialize_host_structure(&host);
|
|
|
|
/* Set and apply the environment */
|
|
env_setargs(argc, argv);
|
|
|
|
sef_local_startup();
|
|
blockdriver_task(&mmc_driver);
|
|
return EXIT_SUCCESS;
|
|
}
|