diff options
author | Lars-Peter Clausen <lars@metafoo.de> | 2009-05-18 17:55:41 +0000 |
---|---|---|
committer | Lars-Peter Clausen <lars@metafoo.de> | 2009-05-18 17:55:41 +0000 |
commit | f5affd4f36d839d706b02f1ffb69e0cf70c4140c (patch) | |
tree | 105a7093a2f5989136da5fe7128d6d95a3b9cad4 /target/linux/s3c24xx/files-2.6.30/drivers/ar6000/hif/hif2.c | |
parent | 267af10b33745d098198ad9c1d2b5ab69efb128f (diff) | |
download | master-31e0f0ae-f5affd4f36d839d706b02f1ffb69e0cf70c4140c.tar.gz master-31e0f0ae-f5affd4f36d839d706b02f1ffb69e0cf70c4140c.tar.bz2 master-31e0f0ae-f5affd4f36d839d706b02f1ffb69e0cf70c4140c.zip |
bump to 2.6.30-rc6
SVN-Revision: 15918
Diffstat (limited to 'target/linux/s3c24xx/files-2.6.30/drivers/ar6000/hif/hif2.c')
-rw-r--r-- | target/linux/s3c24xx/files-2.6.30/drivers/ar6000/hif/hif2.c | 768 |
1 files changed, 768 insertions, 0 deletions
diff --git a/target/linux/s3c24xx/files-2.6.30/drivers/ar6000/hif/hif2.c b/target/linux/s3c24xx/files-2.6.30/drivers/ar6000/hif/hif2.c new file mode 100644 index 0000000000..386d96e668 --- /dev/null +++ b/target/linux/s3c24xx/files-2.6.30/drivers/ar6000/hif/hif2.c @@ -0,0 +1,768 @@ +/* + * hif2.c - HIF layer re-implementation for the Linux SDIO stack + * + * Copyright (C) 2008, 2009 by OpenMoko, Inc. + * Written by Werner Almesberger <werner@openmoko.org> + * 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 version 2 as + * published by the Free Software Foundation; + * + * Based on: + * + * @abstract: HIF layer reference implementation for Atheros SDIO stack + * @notice: Copyright (c) 2004-2006 Atheros Communications Inc. + */ + + +#include <linux/kernel.h> +#include <linux/kthread.h> +#include <linux/list.h> +#include <linux/wait.h> +#include <linux/spinlock.h> +#include <linux/mutex.h> +#include <linux/sched.h> +#include <linux/mmc/sdio_func.h> +#include <linux/mmc/sdio.h> +#include <linux/mmc/sdio_ids.h> + +#include "athdefs.h" +#include "a_types.h" +#include "hif.h" + + +/* @@@ Hack - this wants cleaning up */ + +#ifdef CONFIG_MACH_NEO1973_GTA02 + +#include <mach/gta02-pm-wlan.h> + +#else /* CONFIG_MACH_NEO1973_GTA02 */ + +#define gta02_wlan_query_rfkill_lock() 1 +#define gta02_wlan_set_rfkill_cb(cb, hif) ((void) cb) +#define gta02_wlan_query_rfkill_unlock() +#define gta02_wlan_clear_rfkill_cb() + +#endif /* !CONFIG_MACH_NEO1973_GTA02 */ + + +/* + * KNOWN BUGS: + * + * - HIF_DEVICE_IRQ_ASYNC_SYNC doesn't work yet (gets MMC errors) + * - latency can reach hundreds of ms, probably because of scheduling delays + * - packets go through about three queues before finally hitting the network + */ + +/* + * Differences from Atheros' HIFs: + * + * - synchronous and asynchronous requests may get reordered with respect to + * each other, e.g., if HIFReadWrite returns for an asynchronous request and + * then HIFReadWrite is called for a synchronous request, the synchronous + * request may be executed before the asynchronous request. + * + * - request queue locking seems unnecessarily complex in the Atheros HIFs. + * + * - Atheros mask interrupts by calling sdio_claim_irq/sdio_release_irq, which + * can cause quite a bit of overhead. This HIF has its own light-weight + * interrupt masking. + * + * - Atheros call deviceInsertedHandler from a thread spawned off the probe or + * device insertion function. The original explanation for the Atheros SDIO + * stack said that this is done because a delay is needed to let the chip + * complete initialization. There is indeed a one second delay in the thread. + * + * The Atheros Linux SDIO HIF removes the delay and only retains the thread. + * Experimentally removing the thread didn't show any conflicts, so let's get + * rid of it for good. + * + * - The Atheros SDIO stack with Samuel's driver sets SDIO_CCCR_POWER in + * SDIO_POWER_EMPC. Atheros' Linux SDIO code apparently doesn't. We don't + * either, and this seems to work fine. + * @@@ Need to check this with Atheros. + */ + + +#define MBOXES 4 + +#define HIF_MBOX_BLOCK_SIZE 128 +#define HIF_MBOX_BASE_ADDR 0x800 +#define HIF_MBOX_WIDTH 0x800 +#define HIF_MBOX_START_ADDR(mbox) \ + (HIF_MBOX_BASE_ADDR+(mbox)*HIF_MBOX_WIDTH) + + +struct hif_device { + void *htc_handle; + struct sdio_func *func; + + /* + * @@@ our sweet little bit of bogosity - the mechanism that lets us + * use the SDIO stack from softirqs. This really wants to use skbs. + */ + struct list_head queue; + spinlock_t queue_lock; + struct task_struct *io_task; + wait_queue_head_t wait; + + /* + * activate_lock protects "active" and the activation/deactivation + * process itself. + * + * Relation to other locks: The SDIO function can be claimed while + * activate_lock is being held, but trying to acquire activate_lock + * while having ownership of the SDIO function could cause a deadlock. + */ + int active; + struct mutex activate_lock; +}; + +struct hif_request { + struct list_head list; + struct sdio_func *func; + int (*read)(struct sdio_func *func, + void *dst, unsigned int addr, int count); + int (*write)(struct sdio_func *func, + unsigned int addr, void *src, int count); + void *buf; + unsigned long addr; + int len; + A_STATUS (*completion)(void *context, A_STATUS status); + void *context; +}; + + +static HTC_CALLBACKS htcCallbacks; + +/* + * shutdown_lock prevents recursion through HIFShutDownDevice + */ +static DEFINE_MUTEX(shutdown_lock); + + +/* ----- Request processing ------------------------------------------------ */ + + +static A_STATUS process_request(struct hif_request *req) +{ + int ret; + A_STATUS status; + + dev_dbg(&req->func->dev, "process_request(req %p)\n", req); + sdio_claim_host(req->func); + if (req->read) { + ret = req->read(req->func, req->buf, req->addr, req->len); + } else { + ret = req->write(req->func, req->addr, req->buf, req->len); + } + sdio_release_host(req->func); + status = ret ? A_ERROR : A_OK; + if (req->completion) + req->completion(req->context, status); + kfree(req); + return status; +} + + +static void enqueue_request(struct hif_device *hif, struct hif_request *req) +{ + unsigned long flags; + + dev_dbg(&req->func->dev, "enqueue_request(req %p)\n", req); + spin_lock_irqsave(&hif->queue_lock, flags); + list_add_tail(&req->list, &hif->queue); + spin_unlock_irqrestore(&hif->queue_lock, flags); + wake_up(&hif->wait); +} + + +static struct hif_request *dequeue_request(struct hif_device *hif) +{ + struct hif_request *req; + unsigned long flags; + + spin_lock_irqsave(&hif->queue_lock, flags); + if (list_empty(&hif->queue)) + req = NULL; + else { + req = list_first_entry(&hif->queue, + struct hif_request, list); + list_del(&req->list); + } + spin_unlock_irqrestore(&hif->queue_lock, flags); + return req; +} + + +static void wait_queue_empty(struct hif_device *hif) +{ + unsigned long flags; + int empty; + + while (1) { + spin_lock_irqsave(&hif->queue_lock, flags); + empty = list_empty(&hif->queue); + spin_unlock_irqrestore(&hif->queue_lock, flags); + if (empty) + break; + else + yield(); + } +} + + +static int io(void *data) +{ + struct hif_device *hif = data; + struct sched_param param = { .sched_priority = 2 }; + /* one priority level slower than ksdioirqd (which is at 1) */ + DEFINE_WAIT(wait); + struct hif_request *req; + + sched_setscheduler(current, SCHED_FIFO, ¶m); + + while (1) { + while (1) { + /* + * Since we never use signals here, one might think + * that this ought to be TASK_UNINTERRUPTIBLE. However, + * such a task would increase the load average and, + * worse, it would trigger the softlockup check. + */ + prepare_to_wait(&hif->wait, &wait, TASK_INTERRUPTIBLE); + if (kthread_should_stop()) { + finish_wait(&hif->wait, &wait); + return 0; + } + req = dequeue_request(hif); + if (req) + break; + schedule(); + } + finish_wait(&hif->wait, &wait); + + (void) process_request(req); + } + return 0; +} + + +A_STATUS HIFReadWrite(HIF_DEVICE *hif, A_UINT32 address, A_UCHAR *buffer, + A_UINT32 length, A_UINT32 request, void *context) +{ + struct device *dev = HIFGetOSDevice(hif); + struct hif_request *req; + + dev_dbg(dev, "HIFReadWrite(device %p, address 0x%x, buffer %p, " + "length %d, request 0x%x, context %p)\n", + hif, address, buffer, length, request, context); + + BUG_ON(!(request & (HIF_SYNCHRONOUS | HIF_ASYNCHRONOUS))); + BUG_ON(!(request & (HIF_BYTE_BASIS | HIF_BLOCK_BASIS))); + BUG_ON(!(request & (HIF_READ | HIF_WRITE))); + BUG_ON(!(request & HIF_EXTENDED_IO)); + + if (address >= HIF_MBOX_START_ADDR(0) && + address < HIF_MBOX_START_ADDR(MBOXES+1)) { + BUG_ON(length > HIF_MBOX_WIDTH); + /* Adjust the address so that the last byte falls on the EOM + address. */ + address += HIF_MBOX_WIDTH-length; + } + + req = kzalloc(sizeof(*req), GFP_ATOMIC); + if (!req) { + if (request & HIF_ASYNCHRONOUS) + htcCallbacks.rwCompletionHandler(context, A_ERROR); + return A_ERROR; + } + + req->func = hif->func; + req->addr = address; + req->buf = buffer; + req->len = length; + + if (request & HIF_READ) { + if (request & HIF_FIXED_ADDRESS) + req->read = sdio_readsb; + else + req->read = sdio_memcpy_fromio; + } else { + if (request & HIF_FIXED_ADDRESS) + req->write = sdio_writesb; + else + req->write = sdio_memcpy_toio; + } + + if (!(request & HIF_ASYNCHRONOUS)) + return process_request(req); + + req->completion = htcCallbacks.rwCompletionHandler; + req->context = context; + enqueue_request(hif, req); + + return A_OK; +} + + +/* ----- Interrupt handling ------------------------------------------------ */ + +/* + * Volatile ought to be good enough to make gcc do the right thing on S3C24xx. + * No need to use atomic or put barriers, keeping the code more readable. + * + * Warning: this story changes if going SMP/SMT. + */ + +static volatile int masked = 1; +static volatile int pending; +static volatile int in_interrupt; + + +static void ar6000_do_irq(struct sdio_func *func) +{ + HIF_DEVICE *hif = sdio_get_drvdata(func); + struct device *dev = HIFGetOSDevice(hif); + A_STATUS status; + + dev_dbg(dev, "ar6000_do_irq -> %p\n", htcCallbacks.dsrHandler); + + status = htcCallbacks.dsrHandler(hif->htc_handle); + BUG_ON(status != A_OK); +} + + +static void sdio_ar6000_irq(struct sdio_func *func) +{ + HIF_DEVICE *hif = sdio_get_drvdata(func); + struct device *dev = HIFGetOSDevice(hif); + + dev_dbg(dev, "sdio_ar6000_irq\n"); + + in_interrupt = 1; + if (masked) { + in_interrupt = 0; + pending++; + return; + } + /* + * @@@ This is ugly. If we don't drop the lock, we'll deadlock when + * the handler tries to do SDIO. So there are four choices: + * + * 1) Break the call chain by calling the callback from a workqueue. + * Ugh. + * 2) Make process_request aware that we already have the lock. + * 3) Drop the lock. Which is ugly but should be safe as long as we're + * making sure the device doesn't go away. + * 4) Change the AR6k driver such that it only issues asynchronous + * quests when called from an interrupt. + * + * Solution 2) is probably the best for now. Will try it later. + */ + sdio_release_host(func); + ar6000_do_irq(func); + sdio_claim_host(func); + in_interrupt = 0; +} + + +void HIFAckInterrupt(HIF_DEVICE *hif) +{ + struct device *dev = HIFGetOSDevice(hif); + + dev_dbg(dev, "HIFAckInterrupt\n"); + /* do nothing */ +} + + +void HIFUnMaskInterrupt(HIF_DEVICE *hif) +{ + struct device *dev = HIFGetOSDevice(hif); + + dev_dbg(dev, "HIFUnMaskInterrupt\n"); + do { + masked = 1; + if (pending) { + pending = 0; + ar6000_do_irq(hif->func); + /* We may take an interrupt before unmasking and thus + get it pending. In this case, we just loop back. */ + } + masked = 0; + } + while (pending); +} + + +void HIFMaskInterrupt(HIF_DEVICE *hif) +{ + struct device *dev = HIFGetOSDevice(hif); + + dev_dbg(dev, "HIFMaskInterrupt\n"); + /* + * Since sdio_ar6000_irq can also be called from a process context, we + * may conceivably end up racing with it. Thus, we need to wait until + * we can be sure that no concurrent interrupt processing is going on + * before we return. + * + * Note: this may be a bit on the paranoid side - the callers may + * actually be nice enough to disable scheduling. Check later. + */ + masked = 1; + while (in_interrupt) + yield(); +} + + +/* ----- HIF API glue functions -------------------------------------------- */ + + +struct device *HIFGetOSDevice(HIF_DEVICE *hif) +{ + return &hif->func->dev; +} + + +void HIFSetHandle(void *hif_handle, void *handle) +{ + HIF_DEVICE *hif = (HIF_DEVICE *) hif_handle; + + hif->htc_handle = handle; +} + + +/* ----- Device configuration (HIF side) ----------------------------------- */ + + +A_STATUS HIFConfigureDevice(HIF_DEVICE *hif, + HIF_DEVICE_CONFIG_OPCODE opcode, void *config, A_UINT32 configLen) +{ + struct device *dev = HIFGetOSDevice(hif); + HIF_DEVICE_IRQ_PROCESSING_MODE *ipm_cfg = config; + A_UINT32 *mbs_cfg = config; + int i; + + dev_dbg(dev, "HIFConfigureDevice\n"); + + switch (opcode) { + case HIF_DEVICE_GET_MBOX_BLOCK_SIZE: + for (i = 0; i != MBOXES; i++) + mbs_cfg[i] = HIF_MBOX_BLOCK_SIZE; + break; + case HIF_DEVICE_GET_MBOX_ADDR: + for (i = 0; i != MBOXES; i++) + mbs_cfg[i] = HIF_MBOX_START_ADDR(i); + break; + case HIF_DEVICE_GET_IRQ_PROC_MODE: + *ipm_cfg = HIF_DEVICE_IRQ_SYNC_ONLY; +// *ipm_cfg = HIF_DEVICE_IRQ_ASYNC_SYNC; + break; + default: + return A_ERROR; + } + return A_OK; +} + + +/* ----- Device probe and removal (Linux side) ----------------------------- */ + + +static int ar6000_do_activate(struct hif_device *hif) +{ + struct sdio_func *func = hif->func; + struct device *dev = &func->dev; + int ret; + + dev_dbg(dev, "ar6000_do_activate\n"); + + sdio_claim_host(func); + sdio_enable_func(func); + + INIT_LIST_HEAD(&hif->queue); + init_waitqueue_head(&hif->wait); + spin_lock_init(&hif->queue_lock); + + ret = sdio_set_block_size(func, HIF_MBOX_BLOCK_SIZE); + if (ret < 0) { + dev_err(dev, "sdio_set_block_size returns %d\n", ret); + goto out_enabled; + } + ret = sdio_claim_irq(func, sdio_ar6000_irq); + if (ret) { + dev_err(dev, "sdio_claim_irq returns %d\n", ret); + goto out_enabled; + } + /* Set SDIO_BUS_CD_DISABLE in SDIO_CCCR_IF ? */ +#if 0 + sdio_f0_writeb(func, SDIO_CCCR_CAP_E4MI, SDIO_CCCR_CAPS, &ret); + if (ret) { + dev_err(dev, "sdio_f0_writeb(SDIO_CCCR_CAPS) returns %d\n", + ret); + goto out_got_irq; + } +#else + if (0) /* avoid warning */ + goto out_got_irq; +#endif + + sdio_release_host(func); + + hif->io_task = kthread_run(io, hif, "ar6000_io"); + ret = IS_ERR(hif->io_task); + if (ret) { + dev_err(dev, "kthread_run(ar6000_io): %d\n", ret); + goto out_func_ready; + } + + ret = htcCallbacks.deviceInsertedHandler(hif); + if (ret == A_OK) + return 0; + + dev_err(dev, "deviceInsertedHandler: %d\n", ret); + + ret = kthread_stop(hif->io_task); + if (ret) + dev_err(dev, "kthread_stop (ar6000_io): %d\n", ret); + +out_func_ready: + sdio_claim_host(func); + +out_got_irq: + sdio_release_irq(func); + +out_enabled: + sdio_disable_func(func); + sdio_release_host(func); + + return ret; +} + + +static void ar6000_do_deactivate(struct hif_device *hif) +{ + struct sdio_func *func = hif->func; + struct device *dev = &func->dev; + int ret; + + dev_dbg(dev, "ar6000_do_deactivate\n"); + if (!hif->active) + return; + + if (mutex_trylock(&shutdown_lock)) { + /* + * Funny, Atheros' HIF does this call, but this just puts us in + * a recursion through HTCShutDown/HIFShutDown if unloading the + * module. + * + * However, we need it for suspend/resume. See the comment at + * HIFShutDown, below. + */ + ret = htcCallbacks.deviceRemovedHandler(hif->htc_handle, A_OK); + if (ret != A_OK) + dev_err(dev, "deviceRemovedHandler: %d\n", ret); + mutex_unlock(&shutdown_lock); + } + wait_queue_empty(hif); + ret = kthread_stop(hif->io_task); + if (ret) + dev_err(dev, "kthread_stop (ar6000_io): %d\n", ret); + sdio_claim_host(func); + sdio_release_irq(func); + sdio_disable_func(func); + sdio_release_host(func); +} + + +static int ar6000_activate(struct hif_device *hif) +{ + int ret = 0; + + dev_dbg(&hif->func->dev, "ar6000_activate\n"); + mutex_lock(&hif->activate_lock); + if (!hif->active) { + ret = ar6000_do_activate(hif); + if (ret) { + printk(KERN_ERR "%s: Failed to activate %d\n", + __func__, ret); + goto out; + } + hif->active = 1; + } +out: + mutex_unlock(&hif->activate_lock); + return ret; +} + + +static void ar6000_deactivate(struct hif_device *hif) +{ + dev_dbg(&hif->func->dev, "ar6000_deactivate\n"); + mutex_lock(&hif->activate_lock); + if (hif->active) { + ar6000_do_deactivate(hif); + hif->active = 0; + } + mutex_unlock(&hif->activate_lock); +} + + +static int ar6000_rfkill_cb(void *data, int on) +{ + struct hif_device *hif = data; + struct sdio_func *func = hif->func; + struct device *dev = &func->dev; + + dev_dbg(dev, "ar6000_rfkill_cb: on %d\n", on); + if (on) + return ar6000_activate(hif); + ar6000_deactivate(hif); + return 0; +} + + +static int sdio_ar6000_probe(struct sdio_func *func, + const struct sdio_device_id *id) +{ + struct device *dev = &func->dev; + struct hif_device *hif; + int ret = 0; + + dev_dbg(dev, "sdio_ar6000_probe\n"); + BUG_ON(!htcCallbacks.deviceInsertedHandler); + + hif = kzalloc(sizeof(*hif), GFP_KERNEL); + if (!hif) + return -ENOMEM; + + sdio_set_drvdata(func, hif); + hif->func = func; + mutex_init(&hif->activate_lock); + hif->active = 0; + + if (gta02_wlan_query_rfkill_lock()) + ret = ar6000_activate(hif); + if (!ret) { + gta02_wlan_set_rfkill_cb(ar6000_rfkill_cb, hif); + return 0; + } + gta02_wlan_query_rfkill_unlock(); + sdio_set_drvdata(func, NULL); + kfree(hif); + return ret; +} + + +static void sdio_ar6000_remove(struct sdio_func *func) +{ + struct device *dev = &func->dev; + HIF_DEVICE *hif = sdio_get_drvdata(func); + + dev_dbg(dev, "sdio_ar6000_remove\n"); + gta02_wlan_clear_rfkill_cb(); + ar6000_deactivate(hif); + sdio_set_drvdata(func, NULL); + kfree(hif); +} + + +/* ----- Device registration/unregistration (called by HIF) ---------------- */ + + +#define ATHEROS_SDIO_DEVICE(id, offset) \ + SDIO_DEVICE(SDIO_VENDOR_ID_ATHEROS, SDIO_DEVICE_ID_ATHEROS_##id | (offset)) + +static const struct sdio_device_id sdio_ar6000_ids[] = { + { ATHEROS_SDIO_DEVICE(AR6002, 0) }, + { ATHEROS_SDIO_DEVICE(AR6002, 0x1) }, + { ATHEROS_SDIO_DEVICE(AR6001, 0x8) }, + { ATHEROS_SDIO_DEVICE(AR6001, 0x9) }, + { ATHEROS_SDIO_DEVICE(AR6001, 0xa) }, + { ATHEROS_SDIO_DEVICE(AR6001, 0xb) }, + { /* end: all zeroes */ }, +}; + +MODULE_DEVICE_TABLE(sdio, sdio_ar6000_ids); + + +static struct sdio_driver sdio_ar6000_driver = { + .probe = sdio_ar6000_probe, + .remove = sdio_ar6000_remove, + .name = "sdio_ar6000", + .id_table = sdio_ar6000_ids, +}; + + +int HIFInit(HTC_CALLBACKS *callbacks) +{ + int ret; + + BUG_ON(!callbacks); + + printk(KERN_DEBUG "HIFInit\n"); + htcCallbacks = *callbacks; + + ret = sdio_register_driver(&sdio_ar6000_driver); + if (ret) { + printk(KERN_ERR + "sdio_register_driver(sdio_ar6000_driver): %d\n", ret); + return A_ERROR; + } + + return 0; +} + + +/* + * We have four possible call chains here: + * + * System shutdown/reboot: + * + * kernel_restart_prepare ...> device_shutdown ... > s3cmci_shutdown -> + * mmc_remove_host ..> sdio_bus_remove -> sdio_ar6000_remove -> + * ar6000_deactivate -> ar6000_do_deactivate -> + * deviceRemovedHandler (HTCTargetRemovedHandler) -> HIFShutDownDevice + * + * This is roughly the same sequence as suspend, described below. + * + * Module removal: + * + * sys_delete_module -> ar6000_cleanup_module -> HTCShutDown -> + * HIFShutDownDevice -> sdio_unregister_driver ...> sdio_bus_remove -> + * sdio_ar6000_remove -> ar6000_deactivate -> ar6000_do_deactivate + * + * In this case, HIFShutDownDevice must call sdio_unregister_driver to + * notify the driver about its removal. ar6000_do_deactivate must not call + * deviceRemovedHandler, because that would loop back into HIFShutDownDevice. + * + * Suspend: + * + * device_suspend ...> s3cmci_suspend ...> sdio_bus_remove -> + * sdio_ar6000_remove -> ar6000_deactivate -> ar6000_do_deactivate -> + * deviceRemovedHandler (HTCTargetRemovedHandler) -> HIFShutDownDevice + * + * We must call deviceRemovedHandler to inform the ar6k stack that the device + * has been removed. Since HTCTargetRemovedHandler calls back into + * HIFShutDownDevice, we must also prevent the call to + * sdio_unregister_driver, or we'd end up recursing into the SDIO stack, + * eventually deadlocking somewhere. + * + * rfkill: + * + * rfkill_state_store -> rfkill_toggle_radio -> gta02_wlan_toggle_radio -> + * ar6000_rfkill_cb -> ar6000_deactivate -> ar6000_do_deactivate -> + * deviceRemovedHandler (HTCTargetRemovedHandler) -> HIFShutDownDevice + * + * This is similar to suspend - only the entry point changes. + */ + +void HIFShutDownDevice(HIF_DEVICE *hif) +{ + /* Beware, HTCShutDown calls us with hif == NULL ! */ + if (mutex_trylock(&shutdown_lock)) { + sdio_unregister_driver(&sdio_ar6000_driver); + mutex_unlock(&shutdown_lock); + } +} |