hif-linux-sdio.patch
authorWerner Almesberger <werner@openmoko.org>
Fri, 21 Nov 2008 11:23:02 +0000 (11:23 +0000)
committerAndy Green <agreen@pads.home.warmcat.com>
Fri, 21 Nov 2008 11:23:02 +0000 (11:23 +0000)
This is a replacement for Atheros' HIF layer that uses the Linux SDIO
stack.

Using GPLv2, like Atheros' code this is based on.

Work in progress.

Not-Yet-Signed-off-by: Werner Almesberger <werner@openmoko.org>

drivers/ar6000/Makefile
drivers/ar6000/hif/hif2.c [new file with mode: 0644]

index 7551dbf..f8f4431 100644 (file)
@@ -21,7 +21,7 @@ ar6000-objs += htc/ar6k.o                        \
                htc/htc_recv.o             \
                htc/htc_services.o          \
                htc/htc.o                  \
-               hif/hif.o                  \
+               hif/hif2.o                 \
                bmi/bmi.o                   \
                ar6000/ar6000_drv.o         \
                ar6000/ar6000_raw_if.o     \
diff --git a/drivers/ar6000/hif/hif2.c b/drivers/ar6000/hif/hif2.c
new file mode 100644 (file)
index 0000000..6f20307
--- /dev/null
@@ -0,0 +1,598 @@
+/*
+ * hif2.c - HIF layer re-implementation for the Linux SDIO stack
+ *
+ * Copyright (C) 2008 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/sched.h>
+#include <linux/mmc/sdio_func.h>
+#include <linux/mmc/sdio.h>
+#include <linux/mmc/sdio_ids.h>
+#include <asm/gpio.h>
+
+#include "athdefs.h"
+#include "a_types.h"
+#include "hif.h"
+
+
+/*
+ * KNOWN BUGS:
+ *
+ * - HIF_DEVICE_IRQ_ASYNC_SYNC doesn't work yet (gets MMC errors)
+ * - driver doesn't remove cleanly yet
+ * - 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;
+};
+
+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;
+
+
+/* ----- 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, &param);
+
+       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 sdio_ar6000_probe(struct sdio_func *func,
+    const struct sdio_device_id *id)
+{
+       struct device *dev = &func->dev;
+       struct hif_device *hif;
+       int ret;
+
+       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);
+       sdio_claim_host(func);
+       sdio_enable_func(func);
+
+       hif->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;
+       }
+#endif
+
+       sdio_release_host(func);
+
+       hif->io_task = kthread_run(io, hif, "ar6000_io");
+       if (IS_ERR(hif->io_task)) {
+               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);
+
+/* generates a warning */
+out_got_irq:
+       sdio_release_irq(func);
+
+out_enabled:
+       sdio_set_drvdata(func, NULL);
+       sdio_disable_func(func);
+       sdio_release_host(func);
+
+       return ret;
+}
+
+
+static void sdio_ar6000_remove(struct sdio_func *func)
+{
+       struct device *dev = &func->dev;
+       HIF_DEVICE *hif = sdio_get_drvdata(func);
+       int ret;
+
+#if 0
+       /*
+        * Funny, Atheros' HIF does this call, but this just puts us in a
+        * recursion through HTCShutDown/HIFShutDown if unloading the
+        * module.
+        */
+       ret = htcCallbacks.deviceRemovedHandler(hif->htc_handle, A_OK);
+       if (ret != A_OK)
+               dev_err(dev, "deviceRemovedHandler: %d\n", ret);
+#endif
+       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_set_drvdata(func, NULL);
+       sdio_disable_func(func);
+       sdio_release_host(func);
+       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(AR6000, 0)        },
+       { ATHEROS_SDIO_DEVICE(AR6000, 0x1)      },
+       { ATHEROS_SDIO_DEVICE(AR6000, 0x8)      },
+       { ATHEROS_SDIO_DEVICE(AR6000, 0x9)      },
+       { ATHEROS_SDIO_DEVICE(AR6000, 0xa)      },
+       { ATHEROS_SDIO_DEVICE(AR6000, 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;
+}
+
+
+void HIFShutDownDevice(HIF_DEVICE *hif)
+{
+       /* Beware, HTCShutDown calls us with hif == NULL ! */
+       sdio_unregister_driver(&sdio_ar6000_driver);
+}