7d12e780e0
Maintain a per-CPU global "struct pt_regs *" variable which can be used instead of passing regs around manually through all ~1800 interrupt handlers in the Linux kernel. The regs pointer is used in few places, but it potentially costs both stack space and code to pass it around. On the FRV arch, removing the regs parameter from all the genirq function results in a 20% speed up of the IRQ exit path (ie: from leaving timer_interrupt() to leaving do_IRQ()). Where appropriate, an arch may override the generic storage facility and do something different with the variable. On FRV, for instance, the address is maintained in GR28 at all times inside the kernel as part of general exception handling. Having looked over the code, it appears that the parameter may be handed down through up to twenty or so layers of functions. Consider a USB character device attached to a USB hub, attached to a USB controller that posts its interrupts through a cascaded auxiliary interrupt controller. A character device driver may want to pass regs to the sysrq handler through the input layer which adds another few layers of parameter passing. I've build this code with allyesconfig for x86_64 and i386. I've runtested the main part of the code on FRV and i386, though I can't test most of the drivers. I've also done partial conversion for powerpc and MIPS - these at least compile with minimal configurations. This will affect all archs. Mostly the changes should be relatively easy. Take do_IRQ(), store the regs pointer at the beginning, saving the old one: struct pt_regs *old_regs = set_irq_regs(regs); And put the old one back at the end: set_irq_regs(old_regs); Don't pass regs through to generic_handle_irq() or __do_IRQ(). In timer_interrupt(), this sort of change will be necessary: - update_process_times(user_mode(regs)); - profile_tick(CPU_PROFILING, regs); + update_process_times(user_mode(get_irq_regs())); + profile_tick(CPU_PROFILING); I'd like to move update_process_times()'s use of get_irq_regs() into itself, except that i386, alone of the archs, uses something other than user_mode(). Some notes on the interrupt handling in the drivers: (*) input_dev() is now gone entirely. The regs pointer is no longer stored in the input_dev struct. (*) finish_unlinks() in drivers/usb/host/ohci-q.c needs checking. It does something different depending on whether it's been supplied with a regs pointer or not. (*) Various IRQ handler function pointers have been moved to type irq_handler_t. Signed-Off-By: David Howells <dhowells@redhat.com> (cherry picked from 1b16e7ac850969f38b375e511e3fa2f474a33867 commit)
2146 lines
42 KiB
C
2146 lines
42 KiB
C
/*
|
|
* linux/drivers/mmc/wbsd.c - Winbond W83L51xD SD/MMC driver
|
|
*
|
|
* Copyright (C) 2004-2005 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/mmc/protocol.h>
|
|
|
|
#include <asm/io.h>
|
|
#include <asm/dma.h>
|
|
#include <asm/scatterlist.h>
|
|
|
|
#include "wbsd.h"
|
|
|
|
#define DRIVER_NAME "wbsd"
|
|
#define DRIVER_VERSION "1.6"
|
|
|
|
#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 nopnp = 0;
|
|
#else
|
|
static const unsigned int nopnp = 1;
|
|
#endif
|
|
static unsigned int io = 0x248;
|
|
static unsigned int irq = 6;
|
|
static int 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_CCRC;
|
|
ier |= WBSD_EINT_TIMEOUT;
|
|
ier |= WBSD_EINT_CRC;
|
|
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;
|
|
|
|
printk(KERN_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;
|
|
|
|
DBGF("Ending request, cmd (%x)\n", mrq->cmd->opcode);
|
|
|
|
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_kmap_sg(struct wbsd_host *host)
|
|
{
|
|
host->mapped_sg = kmap_atomic(host->cur_sg->page, KM_BIO_SRC_IRQ) +
|
|
host->cur_sg->offset;
|
|
return host->mapped_sg;
|
|
}
|
|
|
|
static inline void wbsd_kunmap_sg(struct wbsd_host *host)
|
|
{
|
|
kunmap_atomic(host->mapped_sg, KM_BIO_SRC_IRQ);
|
|
}
|
|
|
|
static inline void wbsd_sg_to_dma(struct wbsd_host *host, struct mmc_data *data)
|
|
{
|
|
unsigned int len, i, size;
|
|
struct scatterlist *sg;
|
|
char *dmabuf = host->dma_buffer;
|
|
char *sgbuf;
|
|
|
|
size = host->size;
|
|
|
|
sg = data->sg;
|
|
len = data->sg_len;
|
|
|
|
/*
|
|
* Just loop through all entries. Size might not
|
|
* be the entire list though so make sure that
|
|
* we do not transfer too much.
|
|
*/
|
|
for (i = 0; i < len; i++) {
|
|
sgbuf = kmap_atomic(sg[i].page, KM_BIO_SRC_IRQ) + sg[i].offset;
|
|
if (size < sg[i].length)
|
|
memcpy(dmabuf, sgbuf, size);
|
|
else
|
|
memcpy(dmabuf, sgbuf, sg[i].length);
|
|
kunmap_atomic(sgbuf, KM_BIO_SRC_IRQ);
|
|
dmabuf += sg[i].length;
|
|
|
|
if (size < sg[i].length)
|
|
size = 0;
|
|
else
|
|
size -= sg[i].length;
|
|
|
|
if (size == 0)
|
|
break;
|
|
}
|
|
|
|
/*
|
|
* Check that we didn't get a request to transfer
|
|
* more data than can fit into the SG list.
|
|
*/
|
|
|
|
BUG_ON(size != 0);
|
|
|
|
host->size -= size;
|
|
}
|
|
|
|
static inline void wbsd_dma_to_sg(struct wbsd_host *host, struct mmc_data *data)
|
|
{
|
|
unsigned int len, i, size;
|
|
struct scatterlist *sg;
|
|
char *dmabuf = host->dma_buffer;
|
|
char *sgbuf;
|
|
|
|
size = host->size;
|
|
|
|
sg = data->sg;
|
|
len = data->sg_len;
|
|
|
|
/*
|
|
* Just loop through all entries. Size might not
|
|
* be the entire list though so make sure that
|
|
* we do not transfer too much.
|
|
*/
|
|
for (i = 0; i < len; i++) {
|
|
sgbuf = kmap_atomic(sg[i].page, KM_BIO_SRC_IRQ) + sg[i].offset;
|
|
if (size < sg[i].length)
|
|
memcpy(sgbuf, dmabuf, size);
|
|
else
|
|
memcpy(sgbuf, dmabuf, sg[i].length);
|
|
kunmap_atomic(sgbuf, KM_BIO_SRC_IRQ);
|
|
dmabuf += sg[i].length;
|
|
|
|
if (size < sg[i].length)
|
|
size = 0;
|
|
else
|
|
size -= sg[i].length;
|
|
|
|
if (size == 0)
|
|
break;
|
|
}
|
|
|
|
/*
|
|
* Check that we didn't get a request to transfer
|
|
* more data than can fit into the SG list.
|
|
*/
|
|
|
|
BUG_ON(size != 0);
|
|
|
|
host->size -= size;
|
|
}
|
|
|
|
/*
|
|
* 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 = MMC_ERR_INVALID;
|
|
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 = MMC_ERR_INVALID;
|
|
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;
|
|
|
|
DBGF("Sending cmd (%x)\n", cmd->opcode);
|
|
|
|
/*
|
|
* 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 = MMC_ERR_NONE;
|
|
|
|
/*
|
|
* 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 = MMC_ERR_TIMEOUT;
|
|
/* Timeout? */
|
|
else if (isr & WBSD_INT_TIMEOUT)
|
|
cmd->error = MMC_ERR_TIMEOUT;
|
|
/* CRC? */
|
|
else if ((cmd->flags & MMC_RSP_CRC) && (isr & WBSD_INT_CRC))
|
|
cmd->error = MMC_ERR_BADCRC;
|
|
/* All ok */
|
|
else {
|
|
if (cmd->flags & MMC_RSP_136)
|
|
wbsd_get_long_reply(host, cmd);
|
|
else
|
|
wbsd_get_short_reply(host, cmd);
|
|
}
|
|
}
|
|
|
|
DBGF("Sent cmd (%x), res %d\n", cmd->opcode, cmd->error);
|
|
}
|
|
|
|
/*
|
|
* 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 (data->bytes_xfered == host->size)
|
|
return;
|
|
|
|
buffer = wbsd_kmap_sg(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++;
|
|
|
|
/*
|
|
* Transfer done?
|
|
*/
|
|
if (data->bytes_xfered == host->size) {
|
|
wbsd_kunmap_sg(host);
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* End of scatter list entry?
|
|
*/
|
|
if (host->remain == 0) {
|
|
wbsd_kunmap_sg(host);
|
|
|
|
/*
|
|
* Get next entry. Check if last.
|
|
*/
|
|
if (!wbsd_next_sg(host)) {
|
|
/*
|
|
* We should never reach this point.
|
|
* It means that we're trying to
|
|
* transfer more blocks than can fit
|
|
* into the scatter list.
|
|
*/
|
|
BUG_ON(1);
|
|
|
|
host->size = data->bytes_xfered;
|
|
|
|
return;
|
|
}
|
|
|
|
buffer = wbsd_kmap_sg(host);
|
|
}
|
|
}
|
|
}
|
|
|
|
wbsd_kunmap_sg(host);
|
|
|
|
/*
|
|
* This is a very dirty hack to solve a
|
|
* hardware problem. The chip doesn't trigger
|
|
* FIFO threshold interrupts properly.
|
|
*/
|
|
if ((host->size - 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 transfered.
|
|
*/
|
|
if (data->bytes_xfered == host->size)
|
|
return;
|
|
|
|
buffer = wbsd_kmap_sg(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++;
|
|
|
|
/*
|
|
* Transfer done?
|
|
*/
|
|
if (data->bytes_xfered == host->size) {
|
|
wbsd_kunmap_sg(host);
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* End of scatter list entry?
|
|
*/
|
|
if (host->remain == 0) {
|
|
wbsd_kunmap_sg(host);
|
|
|
|
/*
|
|
* Get next entry. Check if last.
|
|
*/
|
|
if (!wbsd_next_sg(host)) {
|
|
/*
|
|
* We should never reach this point.
|
|
* It means that we're trying to
|
|
* transfer more blocks than can fit
|
|
* into the scatter list.
|
|
*/
|
|
BUG_ON(1);
|
|
|
|
host->size = data->bytes_xfered;
|
|
|
|
return;
|
|
}
|
|
|
|
buffer = wbsd_kmap_sg(host);
|
|
}
|
|
}
|
|
}
|
|
|
|
wbsd_kunmap_sg(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;
|
|
|
|
DBGF("blksz %04x blks %04x flags %08x\n",
|
|
data->blksz, data->blocks, data->flags);
|
|
DBGF("tsac %d ms nsac %d clk\n",
|
|
data->timeout_ns / 1000000, data->timeout_clks);
|
|
|
|
/*
|
|
* Calculate size.
|
|
*/
|
|
host->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 = MMC_ERR_INVALID;
|
|
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(host->size > 0x10000);
|
|
if (host->size > 0x10000) {
|
|
data->error = MMC_ERR_INVALID;
|
|
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, host->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 = MMC_ERR_NONE;
|
|
}
|
|
|
|
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);
|
|
|
|
/*
|
|
* Any leftover data?
|
|
*/
|
|
if (count) {
|
|
printk(KERN_ERR "%s: Incomplete DMA transfer. "
|
|
"%d bytes left.\n",
|
|
mmc_hostname(host->mmc), count);
|
|
|
|
data->error = MMC_ERR_FAILED;
|
|
} else {
|
|
/*
|
|
* Transfer data from DMA buffer to
|
|
* SG list.
|
|
*/
|
|
if (data->flags & MMC_DATA_READ)
|
|
wbsd_dma_to_sg(host, data);
|
|
|
|
data->bytes_xfered = host->size;
|
|
}
|
|
}
|
|
|
|
DBGF("Ending data transfer (%d bytes)\n", data->bytes_xfered);
|
|
|
|
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;
|
|
|
|
/*
|
|
* If there is no card in the slot then
|
|
* timeout immediatly.
|
|
*/
|
|
if (!(host->flags & WBSD_FCARD_PRESENT)) {
|
|
cmd->error = MMC_ERR_TIMEOUT;
|
|
goto done;
|
|
}
|
|
|
|
/*
|
|
* Does the request include data?
|
|
*/
|
|
if (cmd->data) {
|
|
wbsd_prepare_data(host, cmd->data);
|
|
|
|
if (cmd->data->error != MMC_ERR_NONE)
|
|
goto done;
|
|
}
|
|
|
|
wbsd_send_command(host, cmd);
|
|
|
|
/*
|
|
* If this is a data transfer the request
|
|
* will be finished after the data has
|
|
* transfered.
|
|
*/
|
|
if (cmd->data && (cmd->error == MMC_ERR_NONE)) {
|
|
/*
|
|
* 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 immediatly
|
|
* 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 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) {
|
|
printk(KERN_ERR "%s: Card removed during transfer!\n",
|
|
mmc_hostname(host->mmc));
|
|
wbsd_reset(host);
|
|
|
|
host->mrq->cmd->error = MMC_ERR_FAILED;
|
|
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->size == data->bytes_xfered) {
|
|
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 = MMC_ERR_BADCRC;
|
|
|
|
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 = MMC_ERR_TIMEOUT;
|
|
|
|
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);
|
|
}
|
|
|
|
static void wbsd_tasklet_block(unsigned long param)
|
|
{
|
|
struct wbsd_host *host = (struct wbsd_host *)param;
|
|
struct mmc_data *data;
|
|
|
|
spin_lock(&host->lock);
|
|
|
|
if ((wbsd_read_index(host, WBSD_IDX_CRCSTATUS) & WBSD_CRC_MASK) !=
|
|
WBSD_CRC_OK) {
|
|
data = wbsd_get_data(host);
|
|
if (!data)
|
|
goto end;
|
|
|
|
DBGF("CRC error\n");
|
|
|
|
data->error = MMC_ERR_BADCRC;
|
|
|
|
tasklet_schedule(&host->finish_tasklet);
|
|
}
|
|
|
|
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_BUSYEND)
|
|
tasklet_hi_schedule(&host->block_tasklet);
|
|
if (isr & WBSD_INT_TC)
|
|
tasklet_schedule(&host->finish_tasklet);
|
|
|
|
return IRQ_HANDLED;
|
|
}
|
|
|
|
/*****************************************************************************\
|
|
* *
|
|
* Device initialisation and shutdown *
|
|
* *
|
|
\*****************************************************************************/
|
|
|
|
/*
|
|
* Allocate/free MMC structure.
|
|
*/
|
|
|
|
static int __devinit 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 | MMC_CAP_MULTIWRITE | MMC_CAP_BYTEBLOCK;
|
|
|
|
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_hw_segs = 128;
|
|
mmc->max_phys_segs = 128;
|
|
|
|
/*
|
|
* Maximum number of sectors in one transfer. Also limited by 64kB
|
|
* buffer.
|
|
*/
|
|
mmc->max_sectors = 128;
|
|
|
|
/*
|
|
* Maximum segment size. Could be one segment with the maximum number
|
|
* of segments.
|
|
*/
|
|
mmc->max_seg_size = mmc->max_sectors * 512;
|
|
|
|
dev_set_drvdata(dev, mmc);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void __devexit 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 __devinit 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 __devinit 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 __devexit 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 __devinit 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(host->mmc->dev, host->dma_buffer,
|
|
WBSD_DMA_SIZE, DMA_BIDIRECTIONAL);
|
|
|
|
/*
|
|
* ISA DMA must be aligned on a 64k basis.
|
|
*/
|
|
if ((host->dma_addr & 0xffff) != 0)
|
|
goto kfree;
|
|
/*
|
|
* ISA cannot access memory above 16 MB.
|
|
*/
|
|
else if (host->dma_addr >= 0x1000000)
|
|
goto kfree;
|
|
|
|
host->dma = dma;
|
|
|
|
return;
|
|
|
|
kfree:
|
|
/*
|
|
* If we've gotten here then there is some kind of alignment bug
|
|
*/
|
|
BUG_ON(1);
|
|
|
|
dma_unmap_single(host->mmc->dev, host->dma_addr,
|
|
WBSD_DMA_SIZE, DMA_BIDIRECTIONAL);
|
|
host->dma_addr = (dma_addr_t)NULL;
|
|
|
|
kfree(host->dma_buffer);
|
|
host->dma_buffer = NULL;
|
|
|
|
free:
|
|
free_dma(dma);
|
|
|
|
err:
|
|
printk(KERN_WARNING DRIVER_NAME ": Unable to allocate DMA %d. "
|
|
"Falling back on FIFO.\n", dma);
|
|
}
|
|
|
|
static void __devexit wbsd_release_dma(struct wbsd_host *host)
|
|
{
|
|
if (host->dma_addr) {
|
|
dma_unmap_single(host->mmc->dev, 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 = (dma_addr_t)NULL;
|
|
}
|
|
|
|
/*
|
|
* Allocate/free IRQ.
|
|
*/
|
|
|
|
static int __devinit wbsd_request_irq(struct wbsd_host *host, int irq)
|
|
{
|
|
int ret;
|
|
|
|
/*
|
|
* Allocate interrupt.
|
|
*/
|
|
|
|
ret = request_irq(irq, wbsd_irq, IRQF_SHARED, DRIVER_NAME, host);
|
|
if (ret)
|
|
return ret;
|
|
|
|
host->irq = irq;
|
|
|
|
/*
|
|
* Set up tasklets.
|
|
*/
|
|
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);
|
|
tasklet_init(&host->block_tasklet, wbsd_tasklet_block,
|
|
(unsigned long)host);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void __devexit 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);
|
|
tasklet_kill(&host->block_tasklet);
|
|
}
|
|
|
|
/*
|
|
* Allocate all resources for the host.
|
|
*/
|
|
|
|
static int __devinit 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 __devexit 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 __devinit 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)) {
|
|
printk(KERN_WARNING 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)) {
|
|
printk(KERN_WARNING 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);
|
|
|
|
printk(KERN_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 __devexit 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 __devinit wbsd_probe(struct platform_device *dev)
|
|
{
|
|
/* Use the module parameters for resources */
|
|
return wbsd_init(&dev->dev, io, irq, dma, 0);
|
|
}
|
|
|
|
static int __devexit wbsd_remove(struct platform_device *dev)
|
|
{
|
|
wbsd_shutdown(&dev->dev, 0);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* PnP
|
|
*/
|
|
|
|
#ifdef CONFIG_PNP
|
|
|
|
static int __devinit
|
|
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 __devexit wbsd_pnp_remove(struct pnp_dev *dev)
|
|
{
|
|
wbsd_shutdown(&dev->dev, 1);
|
|
}
|
|
|
|
#endif /* CONFIG_PNP */
|
|
|
|
/*
|
|
* Power management
|
|
*/
|
|
|
|
#ifdef CONFIG_PM
|
|
|
|
static int wbsd_suspend(struct wbsd_host *host, pm_message_t state)
|
|
{
|
|
BUG_ON(host == NULL);
|
|
|
|
return mmc_suspend_host(host->mmc, state);
|
|
}
|
|
|
|
static int wbsd_resume(struct wbsd_host *host)
|
|
{
|
|
BUG_ON(host == NULL);
|
|
|
|
wbsd_init_device(host);
|
|
|
|
return mmc_resume_host(host->mmc);
|
|
}
|
|
|
|
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;
|
|
int ret;
|
|
|
|
if (mmc == NULL)
|
|
return 0;
|
|
|
|
DBGF("Suspending...\n");
|
|
|
|
host = mmc_priv(mmc);
|
|
|
|
ret = wbsd_suspend(host, state);
|
|
if (ret)
|
|
return ret;
|
|
|
|
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);
|
|
|
|
return wbsd_resume(host);
|
|
}
|
|
|
|
#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);
|
|
struct wbsd_host *host;
|
|
|
|
if (mmc == NULL)
|
|
return 0;
|
|
|
|
DBGF("Suspending...\n");
|
|
|
|
host = mmc_priv(mmc);
|
|
|
|
return wbsd_suspend(host, state);
|
|
}
|
|
|
|
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)) {
|
|
printk(KERN_WARNING 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);
|
|
|
|
return wbsd_resume(host);
|
|
}
|
|
|
|
#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 = __devexit_p(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 = __devexit_p(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;
|
|
|
|
printk(KERN_INFO DRIVER_NAME
|
|
": Winbond W83L51xD SD/MMC card interface driver, "
|
|
DRIVER_VERSION "\n");
|
|
printk(KERN_INFO DRIVER_NAME ": Copyright(c) Pierre Ossman\n");
|
|
|
|
#ifdef CONFIG_PNP
|
|
|
|
if (!nopnp) {
|
|
result = pnp_register_driver(&wbsd_pnp_driver);
|
|
if (result < 0)
|
|
return result;
|
|
}
|
|
#endif /* CONFIG_PNP */
|
|
|
|
if (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 (!nopnp)
|
|
pnp_unregister_driver(&wbsd_pnp_driver);
|
|
|
|
#endif /* CONFIG_PNP */
|
|
|
|
if (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(nopnp, uint, 0444);
|
|
#endif
|
|
module_param(io, uint, 0444);
|
|
module_param(irq, uint, 0444);
|
|
module_param(dma, int, 0444);
|
|
|
|
MODULE_LICENSE("GPL");
|
|
MODULE_AUTHOR("Pierre Ossman <drzeus@drzeus.cx>");
|
|
MODULE_DESCRIPTION("Winbond W83L51xD SD/MMC card interface driver");
|
|
MODULE_VERSION(DRIVER_VERSION);
|
|
|
|
#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)");
|