mirror of
https://mirrors.bfsu.edu.cn/git/linux.git
synced 2024-11-16 16:54:20 +08:00
dac562fc5f
When the kernel is running in secure boot mode, we lock down the kernel to prevent userspace from modifying the running kernel image. Whilst this includes prohibiting access to things like /dev/mem, it must also prevent access by means of configuring driver modules in such a way as to cause a device to access or modify the kernel image. To this end, annotate module_param* statements that refer to hardware configuration and indicate for future reference what type of parameter they specify. The parameter parser in the core sees this information and can skip such parameters with an error message if the kernel is locked down. The module initialisation then runs as normal, but just sees whatever the default values for those parameters is. Note that we do still need to do the module initialisation because some drivers have viable defaults set in case parameters aren't specified and some drivers support automatic configuration (e.g. PNP or PCI) in addition to manually coded parameters. This patch annotates drivers in drivers/mmc/host/. Suggested-by: Alan Cox <gnomes@lxorguk.ukuu.org.uk> Signed-off-by: David Howells <dhowells@redhat.com> cc: Pierre Ossman <pierre@ossman.eu> cc: Ulf Hansson <ulf.hansson@linaro.org> cc: linux-mmc@vger.kernel.org
2020 lines
39 KiB
C
2020 lines
39 KiB
C
/*
|
|
* linux/drivers/mmc/host/wbsd.c - Winbond W83L51xD SD/MMC driver
|
|
*
|
|
* Copyright (C) 2004-2007 Pierre Ossman, 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.
|
|
*
|
|
*
|
|
* Warning!
|
|
*
|
|
* Changes to the FIFO system should be done with extreme care since
|
|
* the hardware is full of bugs related to the FIFO. Known issues are:
|
|
*
|
|
* - FIFO size field in FSR is always zero.
|
|
*
|
|
* - FIFO interrupts tend not to work as they should. Interrupts are
|
|
* triggered only for full/empty events, not for threshold values.
|
|
*
|
|
* - On APIC systems the FIFO empty interrupt is sometimes lost.
|
|
*/
|
|
|
|
#include <linux/module.h>
|
|
#include <linux/moduleparam.h>
|
|
#include <linux/init.h>
|
|
#include <linux/ioport.h>
|
|
#include <linux/platform_device.h>
|
|
#include <linux/interrupt.h>
|
|
#include <linux/dma-mapping.h>
|
|
#include <linux/delay.h>
|
|
#include <linux/pnp.h>
|
|
#include <linux/highmem.h>
|
|
#include <linux/mmc/host.h>
|
|
#include <linux/scatterlist.h>
|
|
#include <linux/slab.h>
|
|
|
|
#include <asm/io.h>
|
|
#include <asm/dma.h>
|
|
|
|
#include "wbsd.h"
|
|
|
|
#define DRIVER_NAME "wbsd"
|
|
|
|
#define DBG(x...) \
|
|
pr_debug(DRIVER_NAME ": " x)
|
|
#define DBGF(f, x...) \
|
|
pr_debug(DRIVER_NAME " [%s()]: " f, __func__ , ##x)
|
|
|
|
/*
|
|
* Device resources
|
|
*/
|
|
|
|
#ifdef CONFIG_PNP
|
|
|
|
static const struct pnp_device_id pnp_dev_table[] = {
|
|
{ "WEC0517", 0 },
|
|
{ "WEC0518", 0 },
|
|
{ "", 0 },
|
|
};
|
|
|
|
MODULE_DEVICE_TABLE(pnp, pnp_dev_table);
|
|
|
|
#endif /* CONFIG_PNP */
|
|
|
|
static const int config_ports[] = { 0x2E, 0x4E };
|
|
static const int unlock_codes[] = { 0x83, 0x87 };
|
|
|
|
static const int valid_ids[] = {
|
|
0x7112,
|
|
};
|
|
|
|
#ifdef CONFIG_PNP
|
|
static unsigned int param_nopnp = 0;
|
|
#else
|
|
static const unsigned int param_nopnp = 1;
|
|
#endif
|
|
static unsigned int param_io = 0x248;
|
|
static unsigned int param_irq = 6;
|
|
static int param_dma = 2;
|
|
|
|
/*
|
|
* Basic functions
|
|
*/
|
|
|
|
static inline void wbsd_unlock_config(struct wbsd_host *host)
|
|
{
|
|
BUG_ON(host->config == 0);
|
|
|
|
outb(host->unlock_code, host->config);
|
|
outb(host->unlock_code, host->config);
|
|
}
|
|
|
|
static inline void wbsd_lock_config(struct wbsd_host *host)
|
|
{
|
|
BUG_ON(host->config == 0);
|
|
|
|
outb(LOCK_CODE, host->config);
|
|
}
|
|
|
|
static inline void wbsd_write_config(struct wbsd_host *host, u8 reg, u8 value)
|
|
{
|
|
BUG_ON(host->config == 0);
|
|
|
|
outb(reg, host->config);
|
|
outb(value, host->config + 1);
|
|
}
|
|
|
|
static inline u8 wbsd_read_config(struct wbsd_host *host, u8 reg)
|
|
{
|
|
BUG_ON(host->config == 0);
|
|
|
|
outb(reg, host->config);
|
|
return inb(host->config + 1);
|
|
}
|
|
|
|
static inline void wbsd_write_index(struct wbsd_host *host, u8 index, u8 value)
|
|
{
|
|
outb(index, host->base + WBSD_IDXR);
|
|
outb(value, host->base + WBSD_DATAR);
|
|
}
|
|
|
|
static inline u8 wbsd_read_index(struct wbsd_host *host, u8 index)
|
|
{
|
|
outb(index, host->base + WBSD_IDXR);
|
|
return inb(host->base + WBSD_DATAR);
|
|
}
|
|
|
|
/*
|
|
* Common routines
|
|
*/
|
|
|
|
static void wbsd_init_device(struct wbsd_host *host)
|
|
{
|
|
u8 setup, ier;
|
|
|
|
/*
|
|
* Reset chip (SD/MMC part) and fifo.
|
|
*/
|
|
setup = wbsd_read_index(host, WBSD_IDX_SETUP);
|
|
setup |= WBSD_FIFO_RESET | WBSD_SOFT_RESET;
|
|
wbsd_write_index(host, WBSD_IDX_SETUP, setup);
|
|
|
|
/*
|
|
* Set DAT3 to input
|
|
*/
|
|
setup &= ~WBSD_DAT3_H;
|
|
wbsd_write_index(host, WBSD_IDX_SETUP, setup);
|
|
host->flags &= ~WBSD_FIGNORE_DETECT;
|
|
|
|
/*
|
|
* Read back default clock.
|
|
*/
|
|
host->clk = wbsd_read_index(host, WBSD_IDX_CLK);
|
|
|
|
/*
|
|
* Power down port.
|
|
*/
|
|
outb(WBSD_POWER_N, host->base + WBSD_CSR);
|
|
|
|
/*
|
|
* Set maximum timeout.
|
|
*/
|
|
wbsd_write_index(host, WBSD_IDX_TAAC, 0x7F);
|
|
|
|
/*
|
|
* Test for card presence
|
|
*/
|
|
if (inb(host->base + WBSD_CSR) & WBSD_CARDPRESENT)
|
|
host->flags |= WBSD_FCARD_PRESENT;
|
|
else
|
|
host->flags &= ~WBSD_FCARD_PRESENT;
|
|
|
|
/*
|
|
* Enable interesting interrupts.
|
|
*/
|
|
ier = 0;
|
|
ier |= WBSD_EINT_CARD;
|
|
ier |= WBSD_EINT_FIFO_THRE;
|
|
ier |= WBSD_EINT_CRC;
|
|
ier |= WBSD_EINT_TIMEOUT;
|
|
ier |= WBSD_EINT_TC;
|
|
|
|
outb(ier, host->base + WBSD_EIR);
|
|
|
|
/*
|
|
* Clear interrupts.
|
|
*/
|
|
inb(host->base + WBSD_ISR);
|
|
}
|
|
|
|
static void wbsd_reset(struct wbsd_host *host)
|
|
{
|
|
u8 setup;
|
|
|
|
pr_err("%s: Resetting chip\n", mmc_hostname(host->mmc));
|
|
|
|
/*
|
|
* Soft reset of chip (SD/MMC part).
|
|
*/
|
|
setup = wbsd_read_index(host, WBSD_IDX_SETUP);
|
|
setup |= WBSD_SOFT_RESET;
|
|
wbsd_write_index(host, WBSD_IDX_SETUP, setup);
|
|
}
|
|
|
|
static void wbsd_request_end(struct wbsd_host *host, struct mmc_request *mrq)
|
|
{
|
|
unsigned long dmaflags;
|
|
|
|
if (host->dma >= 0) {
|
|
/*
|
|
* Release ISA DMA controller.
|
|
*/
|
|
dmaflags = claim_dma_lock();
|
|
disable_dma(host->dma);
|
|
clear_dma_ff(host->dma);
|
|
release_dma_lock(dmaflags);
|
|
|
|
/*
|
|
* Disable DMA on host.
|
|
*/
|
|
wbsd_write_index(host, WBSD_IDX_DMA, 0);
|
|
}
|
|
|
|
host->mrq = NULL;
|
|
|
|
/*
|
|
* MMC layer might call back into the driver so first unlock.
|
|
*/
|
|
spin_unlock(&host->lock);
|
|
mmc_request_done(host->mmc, mrq);
|
|
spin_lock(&host->lock);
|
|
}
|
|
|
|
/*
|
|
* Scatter/gather functions
|
|
*/
|
|
|
|
static inline void wbsd_init_sg(struct wbsd_host *host, struct mmc_data *data)
|
|
{
|
|
/*
|
|
* Get info. about SG list from data structure.
|
|
*/
|
|
host->cur_sg = data->sg;
|
|
host->num_sg = data->sg_len;
|
|
|
|
host->offset = 0;
|
|
host->remain = host->cur_sg->length;
|
|
}
|
|
|
|
static inline int wbsd_next_sg(struct wbsd_host *host)
|
|
{
|
|
/*
|
|
* Skip to next SG entry.
|
|
*/
|
|
host->cur_sg++;
|
|
host->num_sg--;
|
|
|
|
/*
|
|
* Any entries left?
|
|
*/
|
|
if (host->num_sg > 0) {
|
|
host->offset = 0;
|
|
host->remain = host->cur_sg->length;
|
|
}
|
|
|
|
return host->num_sg;
|
|
}
|
|
|
|
static inline char *wbsd_sg_to_buffer(struct wbsd_host *host)
|
|
{
|
|
return sg_virt(host->cur_sg);
|
|
}
|
|
|
|
static inline void wbsd_sg_to_dma(struct wbsd_host *host, struct mmc_data *data)
|
|
{
|
|
unsigned int len, i;
|
|
struct scatterlist *sg;
|
|
char *dmabuf = host->dma_buffer;
|
|
char *sgbuf;
|
|
|
|
sg = data->sg;
|
|
len = data->sg_len;
|
|
|
|
for (i = 0; i < len; i++) {
|
|
sgbuf = sg_virt(&sg[i]);
|
|
memcpy(dmabuf, sgbuf, sg[i].length);
|
|
dmabuf += sg[i].length;
|
|
}
|
|
}
|
|
|
|
static inline void wbsd_dma_to_sg(struct wbsd_host *host, struct mmc_data *data)
|
|
{
|
|
unsigned int len, i;
|
|
struct scatterlist *sg;
|
|
char *dmabuf = host->dma_buffer;
|
|
char *sgbuf;
|
|
|
|
sg = data->sg;
|
|
len = data->sg_len;
|
|
|
|
for (i = 0; i < len; i++) {
|
|
sgbuf = sg_virt(&sg[i]);
|
|
memcpy(sgbuf, dmabuf, sg[i].length);
|
|
dmabuf += sg[i].length;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Command handling
|
|
*/
|
|
|
|
static inline void wbsd_get_short_reply(struct wbsd_host *host,
|
|
struct mmc_command *cmd)
|
|
{
|
|
/*
|
|
* Correct response type?
|
|
*/
|
|
if (wbsd_read_index(host, WBSD_IDX_RSPLEN) != WBSD_RSP_SHORT) {
|
|
cmd->error = -EILSEQ;
|
|
return;
|
|
}
|
|
|
|
cmd->resp[0] = wbsd_read_index(host, WBSD_IDX_RESP12) << 24;
|
|
cmd->resp[0] |= wbsd_read_index(host, WBSD_IDX_RESP13) << 16;
|
|
cmd->resp[0] |= wbsd_read_index(host, WBSD_IDX_RESP14) << 8;
|
|
cmd->resp[0] |= wbsd_read_index(host, WBSD_IDX_RESP15) << 0;
|
|
cmd->resp[1] = wbsd_read_index(host, WBSD_IDX_RESP16) << 24;
|
|
}
|
|
|
|
static inline void wbsd_get_long_reply(struct wbsd_host *host,
|
|
struct mmc_command *cmd)
|
|
{
|
|
int i;
|
|
|
|
/*
|
|
* Correct response type?
|
|
*/
|
|
if (wbsd_read_index(host, WBSD_IDX_RSPLEN) != WBSD_RSP_LONG) {
|
|
cmd->error = -EILSEQ;
|
|
return;
|
|
}
|
|
|
|
for (i = 0; i < 4; i++) {
|
|
cmd->resp[i] =
|
|
wbsd_read_index(host, WBSD_IDX_RESP1 + i * 4) << 24;
|
|
cmd->resp[i] |=
|
|
wbsd_read_index(host, WBSD_IDX_RESP2 + i * 4) << 16;
|
|
cmd->resp[i] |=
|
|
wbsd_read_index(host, WBSD_IDX_RESP3 + i * 4) << 8;
|
|
cmd->resp[i] |=
|
|
wbsd_read_index(host, WBSD_IDX_RESP4 + i * 4) << 0;
|
|
}
|
|
}
|
|
|
|
static void wbsd_send_command(struct wbsd_host *host, struct mmc_command *cmd)
|
|
{
|
|
int i;
|
|
u8 status, isr;
|
|
|
|
/*
|
|
* Clear accumulated ISR. The interrupt routine
|
|
* will fill this one with events that occur during
|
|
* transfer.
|
|
*/
|
|
host->isr = 0;
|
|
|
|
/*
|
|
* Send the command (CRC calculated by host).
|
|
*/
|
|
outb(cmd->opcode, host->base + WBSD_CMDR);
|
|
for (i = 3; i >= 0; i--)
|
|
outb((cmd->arg >> (i * 8)) & 0xff, host->base + WBSD_CMDR);
|
|
|
|
cmd->error = 0;
|
|
|
|
/*
|
|
* Wait for the request to complete.
|
|
*/
|
|
do {
|
|
status = wbsd_read_index(host, WBSD_IDX_STATUS);
|
|
} while (status & WBSD_CARDTRAFFIC);
|
|
|
|
/*
|
|
* Do we expect a reply?
|
|
*/
|
|
if (cmd->flags & MMC_RSP_PRESENT) {
|
|
/*
|
|
* Read back status.
|
|
*/
|
|
isr = host->isr;
|
|
|
|
/* Card removed? */
|
|
if (isr & WBSD_INT_CARD)
|
|
cmd->error = -ENOMEDIUM;
|
|
/* Timeout? */
|
|
else if (isr & WBSD_INT_TIMEOUT)
|
|
cmd->error = -ETIMEDOUT;
|
|
/* CRC? */
|
|
else if ((cmd->flags & MMC_RSP_CRC) && (isr & WBSD_INT_CRC))
|
|
cmd->error = -EILSEQ;
|
|
/* All ok */
|
|
else {
|
|
if (cmd->flags & MMC_RSP_136)
|
|
wbsd_get_long_reply(host, cmd);
|
|
else
|
|
wbsd_get_short_reply(host, cmd);
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Data functions
|
|
*/
|
|
|
|
static void wbsd_empty_fifo(struct wbsd_host *host)
|
|
{
|
|
struct mmc_data *data = host->mrq->cmd->data;
|
|
char *buffer;
|
|
int i, fsr, fifo;
|
|
|
|
/*
|
|
* Handle excessive data.
|
|
*/
|
|
if (host->num_sg == 0)
|
|
return;
|
|
|
|
buffer = wbsd_sg_to_buffer(host) + host->offset;
|
|
|
|
/*
|
|
* Drain the fifo. This has a tendency to loop longer
|
|
* than the FIFO length (usually one block).
|
|
*/
|
|
while (!((fsr = inb(host->base + WBSD_FSR)) & WBSD_FIFO_EMPTY)) {
|
|
/*
|
|
* The size field in the FSR is broken so we have to
|
|
* do some guessing.
|
|
*/
|
|
if (fsr & WBSD_FIFO_FULL)
|
|
fifo = 16;
|
|
else if (fsr & WBSD_FIFO_FUTHRE)
|
|
fifo = 8;
|
|
else
|
|
fifo = 1;
|
|
|
|
for (i = 0; i < fifo; i++) {
|
|
*buffer = inb(host->base + WBSD_DFR);
|
|
buffer++;
|
|
host->offset++;
|
|
host->remain--;
|
|
|
|
data->bytes_xfered++;
|
|
|
|
/*
|
|
* End of scatter list entry?
|
|
*/
|
|
if (host->remain == 0) {
|
|
/*
|
|
* Get next entry. Check if last.
|
|
*/
|
|
if (!wbsd_next_sg(host))
|
|
return;
|
|
|
|
buffer = wbsd_sg_to_buffer(host);
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* This is a very dirty hack to solve a
|
|
* hardware problem. The chip doesn't trigger
|
|
* FIFO threshold interrupts properly.
|
|
*/
|
|
if ((data->blocks * data->blksz - data->bytes_xfered) < 16)
|
|
tasklet_schedule(&host->fifo_tasklet);
|
|
}
|
|
|
|
static void wbsd_fill_fifo(struct wbsd_host *host)
|
|
{
|
|
struct mmc_data *data = host->mrq->cmd->data;
|
|
char *buffer;
|
|
int i, fsr, fifo;
|
|
|
|
/*
|
|
* Check that we aren't being called after the
|
|
* entire buffer has been transferred.
|
|
*/
|
|
if (host->num_sg == 0)
|
|
return;
|
|
|
|
buffer = wbsd_sg_to_buffer(host) + host->offset;
|
|
|
|
/*
|
|
* Fill the fifo. This has a tendency to loop longer
|
|
* than the FIFO length (usually one block).
|
|
*/
|
|
while (!((fsr = inb(host->base + WBSD_FSR)) & WBSD_FIFO_FULL)) {
|
|
/*
|
|
* The size field in the FSR is broken so we have to
|
|
* do some guessing.
|
|
*/
|
|
if (fsr & WBSD_FIFO_EMPTY)
|
|
fifo = 0;
|
|
else if (fsr & WBSD_FIFO_EMTHRE)
|
|
fifo = 8;
|
|
else
|
|
fifo = 15;
|
|
|
|
for (i = 16; i > fifo; i--) {
|
|
outb(*buffer, host->base + WBSD_DFR);
|
|
buffer++;
|
|
host->offset++;
|
|
host->remain--;
|
|
|
|
data->bytes_xfered++;
|
|
|
|
/*
|
|
* End of scatter list entry?
|
|
*/
|
|
if (host->remain == 0) {
|
|
/*
|
|
* Get next entry. Check if last.
|
|
*/
|
|
if (!wbsd_next_sg(host))
|
|
return;
|
|
|
|
buffer = wbsd_sg_to_buffer(host);
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* The controller stops sending interrupts for
|
|
* 'FIFO empty' under certain conditions. So we
|
|
* need to be a bit more pro-active.
|
|
*/
|
|
tasklet_schedule(&host->fifo_tasklet);
|
|
}
|
|
|
|
static void wbsd_prepare_data(struct wbsd_host *host, struct mmc_data *data)
|
|
{
|
|
u16 blksize;
|
|
u8 setup;
|
|
unsigned long dmaflags;
|
|
unsigned int size;
|
|
|
|
/*
|
|
* Calculate size.
|
|
*/
|
|
size = data->blocks * data->blksz;
|
|
|
|
/*
|
|
* Check timeout values for overflow.
|
|
* (Yes, some cards cause this value to overflow).
|
|
*/
|
|
if (data->timeout_ns > 127000000)
|
|
wbsd_write_index(host, WBSD_IDX_TAAC, 127);
|
|
else {
|
|
wbsd_write_index(host, WBSD_IDX_TAAC,
|
|
data->timeout_ns / 1000000);
|
|
}
|
|
|
|
if (data->timeout_clks > 255)
|
|
wbsd_write_index(host, WBSD_IDX_NSAC, 255);
|
|
else
|
|
wbsd_write_index(host, WBSD_IDX_NSAC, data->timeout_clks);
|
|
|
|
/*
|
|
* Inform the chip of how large blocks will be
|
|
* sent. It needs this to determine when to
|
|
* calculate CRC.
|
|
*
|
|
* Space for CRC must be included in the size.
|
|
* Two bytes are needed for each data line.
|
|
*/
|
|
if (host->bus_width == MMC_BUS_WIDTH_1) {
|
|
blksize = data->blksz + 2;
|
|
|
|
wbsd_write_index(host, WBSD_IDX_PBSMSB, (blksize >> 4) & 0xF0);
|
|
wbsd_write_index(host, WBSD_IDX_PBSLSB, blksize & 0xFF);
|
|
} else if (host->bus_width == MMC_BUS_WIDTH_4) {
|
|
blksize = data->blksz + 2 * 4;
|
|
|
|
wbsd_write_index(host, WBSD_IDX_PBSMSB,
|
|
((blksize >> 4) & 0xF0) | WBSD_DATA_WIDTH);
|
|
wbsd_write_index(host, WBSD_IDX_PBSLSB, blksize & 0xFF);
|
|
} else {
|
|
data->error = -EINVAL;
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* Clear the FIFO. This is needed even for DMA
|
|
* transfers since the chip still uses the FIFO
|
|
* internally.
|
|
*/
|
|
setup = wbsd_read_index(host, WBSD_IDX_SETUP);
|
|
setup |= WBSD_FIFO_RESET;
|
|
wbsd_write_index(host, WBSD_IDX_SETUP, setup);
|
|
|
|
/*
|
|
* DMA transfer?
|
|
*/
|
|
if (host->dma >= 0) {
|
|
/*
|
|
* The buffer for DMA is only 64 kB.
|
|
*/
|
|
BUG_ON(size > 0x10000);
|
|
if (size > 0x10000) {
|
|
data->error = -EINVAL;
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* Transfer data from the SG list to
|
|
* the DMA buffer.
|
|
*/
|
|
if (data->flags & MMC_DATA_WRITE)
|
|
wbsd_sg_to_dma(host, data);
|
|
|
|
/*
|
|
* Initialise the ISA DMA controller.
|
|
*/
|
|
dmaflags = claim_dma_lock();
|
|
disable_dma(host->dma);
|
|
clear_dma_ff(host->dma);
|
|
if (data->flags & MMC_DATA_READ)
|
|
set_dma_mode(host->dma, DMA_MODE_READ & ~0x40);
|
|
else
|
|
set_dma_mode(host->dma, DMA_MODE_WRITE & ~0x40);
|
|
set_dma_addr(host->dma, host->dma_addr);
|
|
set_dma_count(host->dma, size);
|
|
|
|
enable_dma(host->dma);
|
|
release_dma_lock(dmaflags);
|
|
|
|
/*
|
|
* Enable DMA on the host.
|
|
*/
|
|
wbsd_write_index(host, WBSD_IDX_DMA, WBSD_DMA_ENABLE);
|
|
} else {
|
|
/*
|
|
* This flag is used to keep printk
|
|
* output to a minimum.
|
|
*/
|
|
host->firsterr = 1;
|
|
|
|
/*
|
|
* Initialise the SG list.
|
|
*/
|
|
wbsd_init_sg(host, data);
|
|
|
|
/*
|
|
* Turn off DMA.
|
|
*/
|
|
wbsd_write_index(host, WBSD_IDX_DMA, 0);
|
|
|
|
/*
|
|
* Set up FIFO threshold levels (and fill
|
|
* buffer if doing a write).
|
|
*/
|
|
if (data->flags & MMC_DATA_READ) {
|
|
wbsd_write_index(host, WBSD_IDX_FIFOEN,
|
|
WBSD_FIFOEN_FULL | 8);
|
|
} else {
|
|
wbsd_write_index(host, WBSD_IDX_FIFOEN,
|
|
WBSD_FIFOEN_EMPTY | 8);
|
|
wbsd_fill_fifo(host);
|
|
}
|
|
}
|
|
|
|
data->error = 0;
|
|
}
|
|
|
|
static void wbsd_finish_data(struct wbsd_host *host, struct mmc_data *data)
|
|
{
|
|
unsigned long dmaflags;
|
|
int count;
|
|
u8 status;
|
|
|
|
WARN_ON(host->mrq == NULL);
|
|
|
|
/*
|
|
* Send a stop command if needed.
|
|
*/
|
|
if (data->stop)
|
|
wbsd_send_command(host, data->stop);
|
|
|
|
/*
|
|
* Wait for the controller to leave data
|
|
* transfer state.
|
|
*/
|
|
do {
|
|
status = wbsd_read_index(host, WBSD_IDX_STATUS);
|
|
} while (status & (WBSD_BLOCK_READ | WBSD_BLOCK_WRITE));
|
|
|
|
/*
|
|
* DMA transfer?
|
|
*/
|
|
if (host->dma >= 0) {
|
|
/*
|
|
* Disable DMA on the host.
|
|
*/
|
|
wbsd_write_index(host, WBSD_IDX_DMA, 0);
|
|
|
|
/*
|
|
* Turn of ISA DMA controller.
|
|
*/
|
|
dmaflags = claim_dma_lock();
|
|
disable_dma(host->dma);
|
|
clear_dma_ff(host->dma);
|
|
count = get_dma_residue(host->dma);
|
|
release_dma_lock(dmaflags);
|
|
|
|
data->bytes_xfered = host->mrq->data->blocks *
|
|
host->mrq->data->blksz - count;
|
|
data->bytes_xfered -= data->bytes_xfered % data->blksz;
|
|
|
|
/*
|
|
* Any leftover data?
|
|
*/
|
|
if (count) {
|
|
pr_err("%s: Incomplete DMA transfer. "
|
|
"%d bytes left.\n",
|
|
mmc_hostname(host->mmc), count);
|
|
|
|
if (!data->error)
|
|
data->error = -EIO;
|
|
} else {
|
|
/*
|
|
* Transfer data from DMA buffer to
|
|
* SG list.
|
|
*/
|
|
if (data->flags & MMC_DATA_READ)
|
|
wbsd_dma_to_sg(host, data);
|
|
}
|
|
|
|
if (data->error) {
|
|
if (data->bytes_xfered)
|
|
data->bytes_xfered -= data->blksz;
|
|
}
|
|
}
|
|
|
|
wbsd_request_end(host, host->mrq);
|
|
}
|
|
|
|
/*****************************************************************************\
|
|
* *
|
|
* MMC layer callbacks *
|
|
* *
|
|
\*****************************************************************************/
|
|
|
|
static void wbsd_request(struct mmc_host *mmc, struct mmc_request *mrq)
|
|
{
|
|
struct wbsd_host *host = mmc_priv(mmc);
|
|
struct mmc_command *cmd;
|
|
|
|
/*
|
|
* Disable tasklets to avoid a deadlock.
|
|
*/
|
|
spin_lock_bh(&host->lock);
|
|
|
|
BUG_ON(host->mrq != NULL);
|
|
|
|
cmd = mrq->cmd;
|
|
|
|
host->mrq = mrq;
|
|
|
|
/*
|
|
* Check that there is actually a card in the slot.
|
|
*/
|
|
if (!(host->flags & WBSD_FCARD_PRESENT)) {
|
|
cmd->error = -ENOMEDIUM;
|
|
goto done;
|
|
}
|
|
|
|
if (cmd->data) {
|
|
/*
|
|
* The hardware is so delightfully stupid that it has a list
|
|
* of "data" commands. If a command isn't on this list, it'll
|
|
* just go back to the idle state and won't send any data
|
|
* interrupts.
|
|
*/
|
|
switch (cmd->opcode) {
|
|
case 11:
|
|
case 17:
|
|
case 18:
|
|
case 20:
|
|
case 24:
|
|
case 25:
|
|
case 26:
|
|
case 27:
|
|
case 30:
|
|
case 42:
|
|
case 56:
|
|
break;
|
|
|
|
/* ACMDs. We don't keep track of state, so we just treat them
|
|
* like any other command. */
|
|
case 51:
|
|
break;
|
|
|
|
default:
|
|
#ifdef CONFIG_MMC_DEBUG
|
|
pr_warn("%s: Data command %d is not supported by this controller\n",
|
|
mmc_hostname(host->mmc), cmd->opcode);
|
|
#endif
|
|
cmd->error = -EINVAL;
|
|
|
|
goto done;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Does the request include data?
|
|
*/
|
|
if (cmd->data) {
|
|
wbsd_prepare_data(host, cmd->data);
|
|
|
|
if (cmd->data->error)
|
|
goto done;
|
|
}
|
|
|
|
wbsd_send_command(host, cmd);
|
|
|
|
/*
|
|
* If this is a data transfer the request
|
|
* will be finished after the data has
|
|
* transferred.
|
|
*/
|
|
if (cmd->data && !cmd->error) {
|
|
/*
|
|
* Dirty fix for hardware bug.
|
|
*/
|
|
if (host->dma == -1)
|
|
tasklet_schedule(&host->fifo_tasklet);
|
|
|
|
spin_unlock_bh(&host->lock);
|
|
|
|
return;
|
|
}
|
|
|
|
done:
|
|
wbsd_request_end(host, mrq);
|
|
|
|
spin_unlock_bh(&host->lock);
|
|
}
|
|
|
|
static void wbsd_set_ios(struct mmc_host *mmc, struct mmc_ios *ios)
|
|
{
|
|
struct wbsd_host *host = mmc_priv(mmc);
|
|
u8 clk, setup, pwr;
|
|
|
|
spin_lock_bh(&host->lock);
|
|
|
|
/*
|
|
* Reset the chip on each power off.
|
|
* Should clear out any weird states.
|
|
*/
|
|
if (ios->power_mode == MMC_POWER_OFF)
|
|
wbsd_init_device(host);
|
|
|
|
if (ios->clock >= 24000000)
|
|
clk = WBSD_CLK_24M;
|
|
else if (ios->clock >= 16000000)
|
|
clk = WBSD_CLK_16M;
|
|
else if (ios->clock >= 12000000)
|
|
clk = WBSD_CLK_12M;
|
|
else
|
|
clk = WBSD_CLK_375K;
|
|
|
|
/*
|
|
* Only write to the clock register when
|
|
* there is an actual change.
|
|
*/
|
|
if (clk != host->clk) {
|
|
wbsd_write_index(host, WBSD_IDX_CLK, clk);
|
|
host->clk = clk;
|
|
}
|
|
|
|
/*
|
|
* Power up card.
|
|
*/
|
|
if (ios->power_mode != MMC_POWER_OFF) {
|
|
pwr = inb(host->base + WBSD_CSR);
|
|
pwr &= ~WBSD_POWER_N;
|
|
outb(pwr, host->base + WBSD_CSR);
|
|
}
|
|
|
|
/*
|
|
* MMC cards need to have pin 1 high during init.
|
|
* It wreaks havoc with the card detection though so
|
|
* that needs to be disabled.
|
|
*/
|
|
setup = wbsd_read_index(host, WBSD_IDX_SETUP);
|
|
if (ios->chip_select == MMC_CS_HIGH) {
|
|
BUG_ON(ios->bus_width != MMC_BUS_WIDTH_1);
|
|
setup |= WBSD_DAT3_H;
|
|
host->flags |= WBSD_FIGNORE_DETECT;
|
|
} else {
|
|
if (setup & WBSD_DAT3_H) {
|
|
setup &= ~WBSD_DAT3_H;
|
|
|
|
/*
|
|
* We cannot resume card detection immediately
|
|
* because of capacitance and delays in the chip.
|
|
*/
|
|
mod_timer(&host->ignore_timer, jiffies + HZ / 100);
|
|
}
|
|
}
|
|
wbsd_write_index(host, WBSD_IDX_SETUP, setup);
|
|
|
|
/*
|
|
* Store bus width for later. Will be used when
|
|
* setting up the data transfer.
|
|
*/
|
|
host->bus_width = ios->bus_width;
|
|
|
|
spin_unlock_bh(&host->lock);
|
|
}
|
|
|
|
static int wbsd_get_ro(struct mmc_host *mmc)
|
|
{
|
|
struct wbsd_host *host = mmc_priv(mmc);
|
|
u8 csr;
|
|
|
|
spin_lock_bh(&host->lock);
|
|
|
|
csr = inb(host->base + WBSD_CSR);
|
|
csr |= WBSD_MSLED;
|
|
outb(csr, host->base + WBSD_CSR);
|
|
|
|
mdelay(1);
|
|
|
|
csr = inb(host->base + WBSD_CSR);
|
|
csr &= ~WBSD_MSLED;
|
|
outb(csr, host->base + WBSD_CSR);
|
|
|
|
spin_unlock_bh(&host->lock);
|
|
|
|
return !!(csr & WBSD_WRPT);
|
|
}
|
|
|
|
static const struct mmc_host_ops wbsd_ops = {
|
|
.request = wbsd_request,
|
|
.set_ios = wbsd_set_ios,
|
|
.get_ro = wbsd_get_ro,
|
|
};
|
|
|
|
/*****************************************************************************\
|
|
* *
|
|
* Interrupt handling *
|
|
* *
|
|
\*****************************************************************************/
|
|
|
|
/*
|
|
* Helper function to reset detection ignore
|
|
*/
|
|
|
|
static void wbsd_reset_ignore(unsigned long data)
|
|
{
|
|
struct wbsd_host *host = (struct wbsd_host *)data;
|
|
|
|
BUG_ON(host == NULL);
|
|
|
|
DBG("Resetting card detection ignore\n");
|
|
|
|
spin_lock_bh(&host->lock);
|
|
|
|
host->flags &= ~WBSD_FIGNORE_DETECT;
|
|
|
|
/*
|
|
* Card status might have changed during the
|
|
* blackout.
|
|
*/
|
|
tasklet_schedule(&host->card_tasklet);
|
|
|
|
spin_unlock_bh(&host->lock);
|
|
}
|
|
|
|
/*
|
|
* Tasklets
|
|
*/
|
|
|
|
static inline struct mmc_data *wbsd_get_data(struct wbsd_host *host)
|
|
{
|
|
WARN_ON(!host->mrq);
|
|
if (!host->mrq)
|
|
return NULL;
|
|
|
|
WARN_ON(!host->mrq->cmd);
|
|
if (!host->mrq->cmd)
|
|
return NULL;
|
|
|
|
WARN_ON(!host->mrq->cmd->data);
|
|
if (!host->mrq->cmd->data)
|
|
return NULL;
|
|
|
|
return host->mrq->cmd->data;
|
|
}
|
|
|
|
static void wbsd_tasklet_card(unsigned long param)
|
|
{
|
|
struct wbsd_host *host = (struct wbsd_host *)param;
|
|
u8 csr;
|
|
int delay = -1;
|
|
|
|
spin_lock(&host->lock);
|
|
|
|
if (host->flags & WBSD_FIGNORE_DETECT) {
|
|
spin_unlock(&host->lock);
|
|
return;
|
|
}
|
|
|
|
csr = inb(host->base + WBSD_CSR);
|
|
WARN_ON(csr == 0xff);
|
|
|
|
if (csr & WBSD_CARDPRESENT) {
|
|
if (!(host->flags & WBSD_FCARD_PRESENT)) {
|
|
DBG("Card inserted\n");
|
|
host->flags |= WBSD_FCARD_PRESENT;
|
|
|
|
delay = 500;
|
|
}
|
|
} else if (host->flags & WBSD_FCARD_PRESENT) {
|
|
DBG("Card removed\n");
|
|
host->flags &= ~WBSD_FCARD_PRESENT;
|
|
|
|
if (host->mrq) {
|
|
pr_err("%s: Card removed during transfer!\n",
|
|
mmc_hostname(host->mmc));
|
|
wbsd_reset(host);
|
|
|
|
host->mrq->cmd->error = -ENOMEDIUM;
|
|
tasklet_schedule(&host->finish_tasklet);
|
|
}
|
|
|
|
delay = 0;
|
|
}
|
|
|
|
/*
|
|
* Unlock first since we might get a call back.
|
|
*/
|
|
|
|
spin_unlock(&host->lock);
|
|
|
|
if (delay != -1)
|
|
mmc_detect_change(host->mmc, msecs_to_jiffies(delay));
|
|
}
|
|
|
|
static void wbsd_tasklet_fifo(unsigned long param)
|
|
{
|
|
struct wbsd_host *host = (struct wbsd_host *)param;
|
|
struct mmc_data *data;
|
|
|
|
spin_lock(&host->lock);
|
|
|
|
if (!host->mrq)
|
|
goto end;
|
|
|
|
data = wbsd_get_data(host);
|
|
if (!data)
|
|
goto end;
|
|
|
|
if (data->flags & MMC_DATA_WRITE)
|
|
wbsd_fill_fifo(host);
|
|
else
|
|
wbsd_empty_fifo(host);
|
|
|
|
/*
|
|
* Done?
|
|
*/
|
|
if (host->num_sg == 0) {
|
|
wbsd_write_index(host, WBSD_IDX_FIFOEN, 0);
|
|
tasklet_schedule(&host->finish_tasklet);
|
|
}
|
|
|
|
end:
|
|
spin_unlock(&host->lock);
|
|
}
|
|
|
|
static void wbsd_tasklet_crc(unsigned long param)
|
|
{
|
|
struct wbsd_host *host = (struct wbsd_host *)param;
|
|
struct mmc_data *data;
|
|
|
|
spin_lock(&host->lock);
|
|
|
|
if (!host->mrq)
|
|
goto end;
|
|
|
|
data = wbsd_get_data(host);
|
|
if (!data)
|
|
goto end;
|
|
|
|
DBGF("CRC error\n");
|
|
|
|
data->error = -EILSEQ;
|
|
|
|
tasklet_schedule(&host->finish_tasklet);
|
|
|
|
end:
|
|
spin_unlock(&host->lock);
|
|
}
|
|
|
|
static void wbsd_tasklet_timeout(unsigned long param)
|
|
{
|
|
struct wbsd_host *host = (struct wbsd_host *)param;
|
|
struct mmc_data *data;
|
|
|
|
spin_lock(&host->lock);
|
|
|
|
if (!host->mrq)
|
|
goto end;
|
|
|
|
data = wbsd_get_data(host);
|
|
if (!data)
|
|
goto end;
|
|
|
|
DBGF("Timeout\n");
|
|
|
|
data->error = -ETIMEDOUT;
|
|
|
|
tasklet_schedule(&host->finish_tasklet);
|
|
|
|
end:
|
|
spin_unlock(&host->lock);
|
|
}
|
|
|
|
static void wbsd_tasklet_finish(unsigned long param)
|
|
{
|
|
struct wbsd_host *host = (struct wbsd_host *)param;
|
|
struct mmc_data *data;
|
|
|
|
spin_lock(&host->lock);
|
|
|
|
WARN_ON(!host->mrq);
|
|
if (!host->mrq)
|
|
goto end;
|
|
|
|
data = wbsd_get_data(host);
|
|
if (!data)
|
|
goto end;
|
|
|
|
wbsd_finish_data(host, data);
|
|
|
|
end:
|
|
spin_unlock(&host->lock);
|
|
}
|
|
|
|
/*
|
|
* Interrupt handling
|
|
*/
|
|
|
|
static irqreturn_t wbsd_irq(int irq, void *dev_id)
|
|
{
|
|
struct wbsd_host *host = dev_id;
|
|
int isr;
|
|
|
|
isr = inb(host->base + WBSD_ISR);
|
|
|
|
/*
|
|
* Was it actually our hardware that caused the interrupt?
|
|
*/
|
|
if (isr == 0xff || isr == 0x00)
|
|
return IRQ_NONE;
|
|
|
|
host->isr |= isr;
|
|
|
|
/*
|
|
* Schedule tasklets as needed.
|
|
*/
|
|
if (isr & WBSD_INT_CARD)
|
|
tasklet_schedule(&host->card_tasklet);
|
|
if (isr & WBSD_INT_FIFO_THRE)
|
|
tasklet_schedule(&host->fifo_tasklet);
|
|
if (isr & WBSD_INT_CRC)
|
|
tasklet_hi_schedule(&host->crc_tasklet);
|
|
if (isr & WBSD_INT_TIMEOUT)
|
|
tasklet_hi_schedule(&host->timeout_tasklet);
|
|
if (isr & WBSD_INT_TC)
|
|
tasklet_schedule(&host->finish_tasklet);
|
|
|
|
return IRQ_HANDLED;
|
|
}
|
|
|
|
/*****************************************************************************\
|
|
* *
|
|
* Device initialisation and shutdown *
|
|
* *
|
|
\*****************************************************************************/
|
|
|
|
/*
|
|
* Allocate/free MMC structure.
|
|
*/
|
|
|
|
static int wbsd_alloc_mmc(struct device *dev)
|
|
{
|
|
struct mmc_host *mmc;
|
|
struct wbsd_host *host;
|
|
|
|
/*
|
|
* Allocate MMC structure.
|
|
*/
|
|
mmc = mmc_alloc_host(sizeof(struct wbsd_host), dev);
|
|
if (!mmc)
|
|
return -ENOMEM;
|
|
|
|
host = mmc_priv(mmc);
|
|
host->mmc = mmc;
|
|
|
|
host->dma = -1;
|
|
|
|
/*
|
|
* Set host parameters.
|
|
*/
|
|
mmc->ops = &wbsd_ops;
|
|
mmc->f_min = 375000;
|
|
mmc->f_max = 24000000;
|
|
mmc->ocr_avail = MMC_VDD_32_33 | MMC_VDD_33_34;
|
|
mmc->caps = MMC_CAP_4_BIT_DATA;
|
|
|
|
spin_lock_init(&host->lock);
|
|
|
|
/*
|
|
* Set up timers
|
|
*/
|
|
init_timer(&host->ignore_timer);
|
|
host->ignore_timer.data = (unsigned long)host;
|
|
host->ignore_timer.function = wbsd_reset_ignore;
|
|
|
|
/*
|
|
* Maximum number of segments. Worst case is one sector per segment
|
|
* so this will be 64kB/512.
|
|
*/
|
|
mmc->max_segs = 128;
|
|
|
|
/*
|
|
* Maximum request size. Also limited by 64KiB buffer.
|
|
*/
|
|
mmc->max_req_size = 65536;
|
|
|
|
/*
|
|
* Maximum segment size. Could be one segment with the maximum number
|
|
* of bytes.
|
|
*/
|
|
mmc->max_seg_size = mmc->max_req_size;
|
|
|
|
/*
|
|
* Maximum block size. We have 12 bits (= 4095) but have to subtract
|
|
* space for CRC. So the maximum is 4095 - 4*2 = 4087.
|
|
*/
|
|
mmc->max_blk_size = 4087;
|
|
|
|
/*
|
|
* Maximum block count. There is no real limit so the maximum
|
|
* request size will be the only restriction.
|
|
*/
|
|
mmc->max_blk_count = mmc->max_req_size;
|
|
|
|
dev_set_drvdata(dev, mmc);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void wbsd_free_mmc(struct device *dev)
|
|
{
|
|
struct mmc_host *mmc;
|
|
struct wbsd_host *host;
|
|
|
|
mmc = dev_get_drvdata(dev);
|
|
if (!mmc)
|
|
return;
|
|
|
|
host = mmc_priv(mmc);
|
|
BUG_ON(host == NULL);
|
|
|
|
del_timer_sync(&host->ignore_timer);
|
|
|
|
mmc_free_host(mmc);
|
|
|
|
dev_set_drvdata(dev, NULL);
|
|
}
|
|
|
|
/*
|
|
* Scan for known chip id:s
|
|
*/
|
|
|
|
static int wbsd_scan(struct wbsd_host *host)
|
|
{
|
|
int i, j, k;
|
|
int id;
|
|
|
|
/*
|
|
* Iterate through all ports, all codes to
|
|
* find hardware that is in our known list.
|
|
*/
|
|
for (i = 0; i < ARRAY_SIZE(config_ports); i++) {
|
|
if (!request_region(config_ports[i], 2, DRIVER_NAME))
|
|
continue;
|
|
|
|
for (j = 0; j < ARRAY_SIZE(unlock_codes); j++) {
|
|
id = 0xFFFF;
|
|
|
|
host->config = config_ports[i];
|
|
host->unlock_code = unlock_codes[j];
|
|
|
|
wbsd_unlock_config(host);
|
|
|
|
outb(WBSD_CONF_ID_HI, config_ports[i]);
|
|
id = inb(config_ports[i] + 1) << 8;
|
|
|
|
outb(WBSD_CONF_ID_LO, config_ports[i]);
|
|
id |= inb(config_ports[i] + 1);
|
|
|
|
wbsd_lock_config(host);
|
|
|
|
for (k = 0; k < ARRAY_SIZE(valid_ids); k++) {
|
|
if (id == valid_ids[k]) {
|
|
host->chip_id = id;
|
|
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
if (id != 0xFFFF) {
|
|
DBG("Unknown hardware (id %x) found at %x\n",
|
|
id, config_ports[i]);
|
|
}
|
|
}
|
|
|
|
release_region(config_ports[i], 2);
|
|
}
|
|
|
|
host->config = 0;
|
|
host->unlock_code = 0;
|
|
|
|
return -ENODEV;
|
|
}
|
|
|
|
/*
|
|
* Allocate/free io port ranges
|
|
*/
|
|
|
|
static int wbsd_request_region(struct wbsd_host *host, int base)
|
|
{
|
|
if (base & 0x7)
|
|
return -EINVAL;
|
|
|
|
if (!request_region(base, 8, DRIVER_NAME))
|
|
return -EIO;
|
|
|
|
host->base = base;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void wbsd_release_regions(struct wbsd_host *host)
|
|
{
|
|
if (host->base)
|
|
release_region(host->base, 8);
|
|
|
|
host->base = 0;
|
|
|
|
if (host->config)
|
|
release_region(host->config, 2);
|
|
|
|
host->config = 0;
|
|
}
|
|
|
|
/*
|
|
* Allocate/free DMA port and buffer
|
|
*/
|
|
|
|
static void wbsd_request_dma(struct wbsd_host *host, int dma)
|
|
{
|
|
if (dma < 0)
|
|
return;
|
|
|
|
if (request_dma(dma, DRIVER_NAME))
|
|
goto err;
|
|
|
|
/*
|
|
* We need to allocate a special buffer in
|
|
* order for ISA to be able to DMA to it.
|
|
*/
|
|
host->dma_buffer = kmalloc(WBSD_DMA_SIZE,
|
|
GFP_NOIO | GFP_DMA | __GFP_REPEAT | __GFP_NOWARN);
|
|
if (!host->dma_buffer)
|
|
goto free;
|
|
|
|
/*
|
|
* Translate the address to a physical address.
|
|
*/
|
|
host->dma_addr = dma_map_single(mmc_dev(host->mmc), host->dma_buffer,
|
|
WBSD_DMA_SIZE, DMA_BIDIRECTIONAL);
|
|
if (dma_mapping_error(mmc_dev(host->mmc), host->dma_addr))
|
|
goto kfree;
|
|
|
|
/*
|
|
* ISA DMA must be aligned on a 64k basis.
|
|
*/
|
|
if ((host->dma_addr & 0xffff) != 0)
|
|
goto unmap;
|
|
/*
|
|
* ISA cannot access memory above 16 MB.
|
|
*/
|
|
else if (host->dma_addr >= 0x1000000)
|
|
goto unmap;
|
|
|
|
host->dma = dma;
|
|
|
|
return;
|
|
|
|
unmap:
|
|
/*
|
|
* If we've gotten here then there is some kind of alignment bug
|
|
*/
|
|
BUG_ON(1);
|
|
|
|
dma_unmap_single(mmc_dev(host->mmc), host->dma_addr,
|
|
WBSD_DMA_SIZE, DMA_BIDIRECTIONAL);
|
|
host->dma_addr = 0;
|
|
|
|
kfree:
|
|
kfree(host->dma_buffer);
|
|
host->dma_buffer = NULL;
|
|
|
|
free:
|
|
free_dma(dma);
|
|
|
|
err:
|
|
pr_warn(DRIVER_NAME ": Unable to allocate DMA %d - falling back on FIFO\n",
|
|
dma);
|
|
}
|
|
|
|
static void wbsd_release_dma(struct wbsd_host *host)
|
|
{
|
|
/*
|
|
* host->dma_addr is valid here iff host->dma_buffer is not NULL.
|
|
*/
|
|
if (host->dma_buffer) {
|
|
dma_unmap_single(mmc_dev(host->mmc), host->dma_addr,
|
|
WBSD_DMA_SIZE, DMA_BIDIRECTIONAL);
|
|
kfree(host->dma_buffer);
|
|
}
|
|
if (host->dma >= 0)
|
|
free_dma(host->dma);
|
|
|
|
host->dma = -1;
|
|
host->dma_buffer = NULL;
|
|
host->dma_addr = 0;
|
|
}
|
|
|
|
/*
|
|
* Allocate/free IRQ.
|
|
*/
|
|
|
|
static int wbsd_request_irq(struct wbsd_host *host, int irq)
|
|
{
|
|
int ret;
|
|
|
|
/*
|
|
* Set up tasklets. Must be done before requesting interrupt.
|
|
*/
|
|
tasklet_init(&host->card_tasklet, wbsd_tasklet_card,
|
|
(unsigned long)host);
|
|
tasklet_init(&host->fifo_tasklet, wbsd_tasklet_fifo,
|
|
(unsigned long)host);
|
|
tasklet_init(&host->crc_tasklet, wbsd_tasklet_crc,
|
|
(unsigned long)host);
|
|
tasklet_init(&host->timeout_tasklet, wbsd_tasklet_timeout,
|
|
(unsigned long)host);
|
|
tasklet_init(&host->finish_tasklet, wbsd_tasklet_finish,
|
|
(unsigned long)host);
|
|
|
|
/*
|
|
* Allocate interrupt.
|
|
*/
|
|
ret = request_irq(irq, wbsd_irq, IRQF_SHARED, DRIVER_NAME, host);
|
|
if (ret)
|
|
return ret;
|
|
|
|
host->irq = irq;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void wbsd_release_irq(struct wbsd_host *host)
|
|
{
|
|
if (!host->irq)
|
|
return;
|
|
|
|
free_irq(host->irq, host);
|
|
|
|
host->irq = 0;
|
|
|
|
tasklet_kill(&host->card_tasklet);
|
|
tasklet_kill(&host->fifo_tasklet);
|
|
tasklet_kill(&host->crc_tasklet);
|
|
tasklet_kill(&host->timeout_tasklet);
|
|
tasklet_kill(&host->finish_tasklet);
|
|
}
|
|
|
|
/*
|
|
* Allocate all resources for the host.
|
|
*/
|
|
|
|
static int wbsd_request_resources(struct wbsd_host *host,
|
|
int base, int irq, int dma)
|
|
{
|
|
int ret;
|
|
|
|
/*
|
|
* Allocate I/O ports.
|
|
*/
|
|
ret = wbsd_request_region(host, base);
|
|
if (ret)
|
|
return ret;
|
|
|
|
/*
|
|
* Allocate interrupt.
|
|
*/
|
|
ret = wbsd_request_irq(host, irq);
|
|
if (ret)
|
|
return ret;
|
|
|
|
/*
|
|
* Allocate DMA.
|
|
*/
|
|
wbsd_request_dma(host, dma);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Release all resources for the host.
|
|
*/
|
|
|
|
static void wbsd_release_resources(struct wbsd_host *host)
|
|
{
|
|
wbsd_release_dma(host);
|
|
wbsd_release_irq(host);
|
|
wbsd_release_regions(host);
|
|
}
|
|
|
|
/*
|
|
* Configure the resources the chip should use.
|
|
*/
|
|
|
|
static void wbsd_chip_config(struct wbsd_host *host)
|
|
{
|
|
wbsd_unlock_config(host);
|
|
|
|
/*
|
|
* Reset the chip.
|
|
*/
|
|
wbsd_write_config(host, WBSD_CONF_SWRST, 1);
|
|
wbsd_write_config(host, WBSD_CONF_SWRST, 0);
|
|
|
|
/*
|
|
* Select SD/MMC function.
|
|
*/
|
|
wbsd_write_config(host, WBSD_CONF_DEVICE, DEVICE_SD);
|
|
|
|
/*
|
|
* Set up card detection.
|
|
*/
|
|
wbsd_write_config(host, WBSD_CONF_PINS, WBSD_PINS_DETECT_GP11);
|
|
|
|
/*
|
|
* Configure chip
|
|
*/
|
|
wbsd_write_config(host, WBSD_CONF_PORT_HI, host->base >> 8);
|
|
wbsd_write_config(host, WBSD_CONF_PORT_LO, host->base & 0xff);
|
|
|
|
wbsd_write_config(host, WBSD_CONF_IRQ, host->irq);
|
|
|
|
if (host->dma >= 0)
|
|
wbsd_write_config(host, WBSD_CONF_DRQ, host->dma);
|
|
|
|
/*
|
|
* Enable and power up chip.
|
|
*/
|
|
wbsd_write_config(host, WBSD_CONF_ENABLE, 1);
|
|
wbsd_write_config(host, WBSD_CONF_POWER, 0x20);
|
|
|
|
wbsd_lock_config(host);
|
|
}
|
|
|
|
/*
|
|
* Check that configured resources are correct.
|
|
*/
|
|
|
|
static int wbsd_chip_validate(struct wbsd_host *host)
|
|
{
|
|
int base, irq, dma;
|
|
|
|
wbsd_unlock_config(host);
|
|
|
|
/*
|
|
* Select SD/MMC function.
|
|
*/
|
|
wbsd_write_config(host, WBSD_CONF_DEVICE, DEVICE_SD);
|
|
|
|
/*
|
|
* Read configuration.
|
|
*/
|
|
base = wbsd_read_config(host, WBSD_CONF_PORT_HI) << 8;
|
|
base |= wbsd_read_config(host, WBSD_CONF_PORT_LO);
|
|
|
|
irq = wbsd_read_config(host, WBSD_CONF_IRQ);
|
|
|
|
dma = wbsd_read_config(host, WBSD_CONF_DRQ);
|
|
|
|
wbsd_lock_config(host);
|
|
|
|
/*
|
|
* Validate against given configuration.
|
|
*/
|
|
if (base != host->base)
|
|
return 0;
|
|
if (irq != host->irq)
|
|
return 0;
|
|
if ((dma != host->dma) && (host->dma != -1))
|
|
return 0;
|
|
|
|
return 1;
|
|
}
|
|
|
|
/*
|
|
* Powers down the SD function
|
|
*/
|
|
|
|
static void wbsd_chip_poweroff(struct wbsd_host *host)
|
|
{
|
|
wbsd_unlock_config(host);
|
|
|
|
wbsd_write_config(host, WBSD_CONF_DEVICE, DEVICE_SD);
|
|
wbsd_write_config(host, WBSD_CONF_ENABLE, 0);
|
|
|
|
wbsd_lock_config(host);
|
|
}
|
|
|
|
/*****************************************************************************\
|
|
* *
|
|
* Devices setup and shutdown *
|
|
* *
|
|
\*****************************************************************************/
|
|
|
|
static int wbsd_init(struct device *dev, int base, int irq, int dma,
|
|
int pnp)
|
|
{
|
|
struct wbsd_host *host = NULL;
|
|
struct mmc_host *mmc = NULL;
|
|
int ret;
|
|
|
|
ret = wbsd_alloc_mmc(dev);
|
|
if (ret)
|
|
return ret;
|
|
|
|
mmc = dev_get_drvdata(dev);
|
|
host = mmc_priv(mmc);
|
|
|
|
/*
|
|
* Scan for hardware.
|
|
*/
|
|
ret = wbsd_scan(host);
|
|
if (ret) {
|
|
if (pnp && (ret == -ENODEV)) {
|
|
pr_warn(DRIVER_NAME ": Unable to confirm device presence - you may experience lock-ups\n");
|
|
} else {
|
|
wbsd_free_mmc(dev);
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Request resources.
|
|
*/
|
|
ret = wbsd_request_resources(host, base, irq, dma);
|
|
if (ret) {
|
|
wbsd_release_resources(host);
|
|
wbsd_free_mmc(dev);
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* See if chip needs to be configured.
|
|
*/
|
|
if (pnp) {
|
|
if ((host->config != 0) && !wbsd_chip_validate(host)) {
|
|
pr_warn(DRIVER_NAME ": PnP active but chip not configured! You probably have a buggy BIOS. Configuring chip manually.\n");
|
|
wbsd_chip_config(host);
|
|
}
|
|
} else
|
|
wbsd_chip_config(host);
|
|
|
|
/*
|
|
* Power Management stuff. No idea how this works.
|
|
* Not tested.
|
|
*/
|
|
#ifdef CONFIG_PM
|
|
if (host->config) {
|
|
wbsd_unlock_config(host);
|
|
wbsd_write_config(host, WBSD_CONF_PME, 0xA0);
|
|
wbsd_lock_config(host);
|
|
}
|
|
#endif
|
|
/*
|
|
* Allow device to initialise itself properly.
|
|
*/
|
|
mdelay(5);
|
|
|
|
/*
|
|
* Reset the chip into a known state.
|
|
*/
|
|
wbsd_init_device(host);
|
|
|
|
mmc_add_host(mmc);
|
|
|
|
pr_info("%s: W83L51xD", mmc_hostname(mmc));
|
|
if (host->chip_id != 0)
|
|
printk(" id %x", (int)host->chip_id);
|
|
printk(" at 0x%x irq %d", (int)host->base, (int)host->irq);
|
|
if (host->dma >= 0)
|
|
printk(" dma %d", (int)host->dma);
|
|
else
|
|
printk(" FIFO");
|
|
if (pnp)
|
|
printk(" PnP");
|
|
printk("\n");
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void wbsd_shutdown(struct device *dev, int pnp)
|
|
{
|
|
struct mmc_host *mmc = dev_get_drvdata(dev);
|
|
struct wbsd_host *host;
|
|
|
|
if (!mmc)
|
|
return;
|
|
|
|
host = mmc_priv(mmc);
|
|
|
|
mmc_remove_host(mmc);
|
|
|
|
/*
|
|
* Power down the SD/MMC function.
|
|
*/
|
|
if (!pnp)
|
|
wbsd_chip_poweroff(host);
|
|
|
|
wbsd_release_resources(host);
|
|
|
|
wbsd_free_mmc(dev);
|
|
}
|
|
|
|
/*
|
|
* Non-PnP
|
|
*/
|
|
|
|
static int wbsd_probe(struct platform_device *dev)
|
|
{
|
|
/* Use the module parameters for resources */
|
|
return wbsd_init(&dev->dev, param_io, param_irq, param_dma, 0);
|
|
}
|
|
|
|
static int wbsd_remove(struct platform_device *dev)
|
|
{
|
|
wbsd_shutdown(&dev->dev, 0);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* PnP
|
|
*/
|
|
|
|
#ifdef CONFIG_PNP
|
|
|
|
static int
|
|
wbsd_pnp_probe(struct pnp_dev *pnpdev, const struct pnp_device_id *dev_id)
|
|
{
|
|
int io, irq, dma;
|
|
|
|
/*
|
|
* Get resources from PnP layer.
|
|
*/
|
|
io = pnp_port_start(pnpdev, 0);
|
|
irq = pnp_irq(pnpdev, 0);
|
|
if (pnp_dma_valid(pnpdev, 0))
|
|
dma = pnp_dma(pnpdev, 0);
|
|
else
|
|
dma = -1;
|
|
|
|
DBGF("PnP resources: port %3x irq %d dma %d\n", io, irq, dma);
|
|
|
|
return wbsd_init(&pnpdev->dev, io, irq, dma, 1);
|
|
}
|
|
|
|
static void wbsd_pnp_remove(struct pnp_dev *dev)
|
|
{
|
|
wbsd_shutdown(&dev->dev, 1);
|
|
}
|
|
|
|
#endif /* CONFIG_PNP */
|
|
|
|
/*
|
|
* Power management
|
|
*/
|
|
|
|
#ifdef CONFIG_PM
|
|
|
|
static int wbsd_platform_suspend(struct platform_device *dev,
|
|
pm_message_t state)
|
|
{
|
|
struct mmc_host *mmc = platform_get_drvdata(dev);
|
|
struct wbsd_host *host;
|
|
|
|
if (mmc == NULL)
|
|
return 0;
|
|
|
|
DBGF("Suspending...\n");
|
|
|
|
host = mmc_priv(mmc);
|
|
|
|
wbsd_chip_poweroff(host);
|
|
return 0;
|
|
}
|
|
|
|
static int wbsd_platform_resume(struct platform_device *dev)
|
|
{
|
|
struct mmc_host *mmc = platform_get_drvdata(dev);
|
|
struct wbsd_host *host;
|
|
|
|
if (mmc == NULL)
|
|
return 0;
|
|
|
|
DBGF("Resuming...\n");
|
|
|
|
host = mmc_priv(mmc);
|
|
|
|
wbsd_chip_config(host);
|
|
|
|
/*
|
|
* Allow device to initialise itself properly.
|
|
*/
|
|
mdelay(5);
|
|
|
|
wbsd_init_device(host);
|
|
return 0;
|
|
}
|
|
|
|
#ifdef CONFIG_PNP
|
|
|
|
static int wbsd_pnp_suspend(struct pnp_dev *pnp_dev, pm_message_t state)
|
|
{
|
|
struct mmc_host *mmc = dev_get_drvdata(&pnp_dev->dev);
|
|
|
|
if (mmc == NULL)
|
|
return 0;
|
|
|
|
DBGF("Suspending...\n");
|
|
return 0;
|
|
}
|
|
|
|
static int wbsd_pnp_resume(struct pnp_dev *pnp_dev)
|
|
{
|
|
struct mmc_host *mmc = dev_get_drvdata(&pnp_dev->dev);
|
|
struct wbsd_host *host;
|
|
|
|
if (mmc == NULL)
|
|
return 0;
|
|
|
|
DBGF("Resuming...\n");
|
|
|
|
host = mmc_priv(mmc);
|
|
|
|
/*
|
|
* See if chip needs to be configured.
|
|
*/
|
|
if (host->config != 0) {
|
|
if (!wbsd_chip_validate(host)) {
|
|
pr_warn(DRIVER_NAME ": PnP active but chip not configured! You probably have a buggy BIOS. Configuring chip manually.\n");
|
|
wbsd_chip_config(host);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Allow device to initialise itself properly.
|
|
*/
|
|
mdelay(5);
|
|
|
|
wbsd_init_device(host);
|
|
return 0;
|
|
}
|
|
|
|
#endif /* CONFIG_PNP */
|
|
|
|
#else /* CONFIG_PM */
|
|
|
|
#define wbsd_platform_suspend NULL
|
|
#define wbsd_platform_resume NULL
|
|
|
|
#define wbsd_pnp_suspend NULL
|
|
#define wbsd_pnp_resume NULL
|
|
|
|
#endif /* CONFIG_PM */
|
|
|
|
static struct platform_device *wbsd_device;
|
|
|
|
static struct platform_driver wbsd_driver = {
|
|
.probe = wbsd_probe,
|
|
.remove = wbsd_remove,
|
|
|
|
.suspend = wbsd_platform_suspend,
|
|
.resume = wbsd_platform_resume,
|
|
.driver = {
|
|
.name = DRIVER_NAME,
|
|
},
|
|
};
|
|
|
|
#ifdef CONFIG_PNP
|
|
|
|
static struct pnp_driver wbsd_pnp_driver = {
|
|
.name = DRIVER_NAME,
|
|
.id_table = pnp_dev_table,
|
|
.probe = wbsd_pnp_probe,
|
|
.remove = wbsd_pnp_remove,
|
|
|
|
.suspend = wbsd_pnp_suspend,
|
|
.resume = wbsd_pnp_resume,
|
|
};
|
|
|
|
#endif /* CONFIG_PNP */
|
|
|
|
/*
|
|
* Module loading/unloading
|
|
*/
|
|
|
|
static int __init wbsd_drv_init(void)
|
|
{
|
|
int result;
|
|
|
|
pr_info(DRIVER_NAME
|
|
": Winbond W83L51xD SD/MMC card interface driver\n");
|
|
pr_info(DRIVER_NAME ": Copyright(c) Pierre Ossman\n");
|
|
|
|
#ifdef CONFIG_PNP
|
|
|
|
if (!param_nopnp) {
|
|
result = pnp_register_driver(&wbsd_pnp_driver);
|
|
if (result < 0)
|
|
return result;
|
|
}
|
|
#endif /* CONFIG_PNP */
|
|
|
|
if (param_nopnp) {
|
|
result = platform_driver_register(&wbsd_driver);
|
|
if (result < 0)
|
|
return result;
|
|
|
|
wbsd_device = platform_device_alloc(DRIVER_NAME, -1);
|
|
if (!wbsd_device) {
|
|
platform_driver_unregister(&wbsd_driver);
|
|
return -ENOMEM;
|
|
}
|
|
|
|
result = platform_device_add(wbsd_device);
|
|
if (result) {
|
|
platform_device_put(wbsd_device);
|
|
platform_driver_unregister(&wbsd_driver);
|
|
return result;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void __exit wbsd_drv_exit(void)
|
|
{
|
|
#ifdef CONFIG_PNP
|
|
|
|
if (!param_nopnp)
|
|
pnp_unregister_driver(&wbsd_pnp_driver);
|
|
|
|
#endif /* CONFIG_PNP */
|
|
|
|
if (param_nopnp) {
|
|
platform_device_unregister(wbsd_device);
|
|
|
|
platform_driver_unregister(&wbsd_driver);
|
|
}
|
|
|
|
DBG("unloaded\n");
|
|
}
|
|
|
|
module_init(wbsd_drv_init);
|
|
module_exit(wbsd_drv_exit);
|
|
#ifdef CONFIG_PNP
|
|
module_param_hw_named(nopnp, param_nopnp, uint, other, 0444);
|
|
#endif
|
|
module_param_hw_named(io, param_io, uint, ioport, 0444);
|
|
module_param_hw_named(irq, param_irq, uint, irq, 0444);
|
|
module_param_hw_named(dma, param_dma, int, dma, 0444);
|
|
|
|
MODULE_LICENSE("GPL");
|
|
MODULE_AUTHOR("Pierre Ossman <pierre@ossman.eu>");
|
|
MODULE_DESCRIPTION("Winbond W83L51xD SD/MMC card interface driver");
|
|
|
|
#ifdef CONFIG_PNP
|
|
MODULE_PARM_DESC(nopnp, "Scan for device instead of relying on PNP. (default 0)");
|
|
#endif
|
|
MODULE_PARM_DESC(io, "I/O base to allocate. Must be 8 byte aligned. (default 0x248)");
|
|
MODULE_PARM_DESC(irq, "IRQ to allocate. (default 6)");
|
|
MODULE_PARM_DESC(dma, "DMA channel to allocate. -1 for no DMA. (default 2)");
|