aboutsummaryrefslogtreecommitdiffstats
path: root/package/mmc_over_gpio
diff options
context:
space:
mode:
Diffstat (limited to 'package/mmc_over_gpio')
-rw-r--r--package/mmc_over_gpio/Makefile47
-rw-r--r--package/mmc_over_gpio/src/Makefile2
-rw-r--r--package/mmc_over_gpio/src/linux/spi/spi_gpio.h53
-rw-r--r--package/mmc_over_gpio/src/mmc_over_spigpio.c339
-rw-r--r--package/mmc_over_gpio/src/spi_gpio.c242
5 files changed, 683 insertions, 0 deletions
diff --git a/package/mmc_over_gpio/Makefile b/package/mmc_over_gpio/Makefile
new file mode 100644
index 0000000000..9076525b03
--- /dev/null
+++ b/package/mmc_over_gpio/Makefile
@@ -0,0 +1,47 @@
+#
+# Copyright (C) 2008 OpenWrt.org
+#
+# This is free software, licensed under the GNU General Public License v2.
+# See /LICENSE for more information.
+#
+# $Id: Makefile 10138 2008-01-06 19:28:26Z nbd $
+
+#XXX This package will go away once the stuff is merged into the kernel.
+
+include $(TOPDIR)/rules.mk
+include $(INCLUDE_DIR)/kernel.mk
+
+PKG_NAME:=mmc-over-gpio
+PKG_RELEASE:=1
+
+include $(INCLUDE_DIR)/package.mk
+
+
+define KernelPackage/mmc-over-gpio
+ SUBMENU:=Other modules
+ DEPENDS:=@LINUX_2_6 +kmod-spi +kmod-spi-bitbang +kmod-mmc +kmod-mmc-spi
+ TITLE:=MMC/SD card over GPIO support
+ FILES:=$(PKG_BUILD_DIR)/spi_gpio.$(LINUX_KMOD_SUFFIX) \
+ $(PKG_BUILD_DIR)/mmc_over_spigpio.$(LINUX_KMOD_SUFFIX)
+ AUTOLOAD:=$(call AutoLoad,90,spi_gpio mmc_over_spigpio)
+endef
+
+define KernelPackage/mmc-over-gpio/description
+ Support for driving an MMC/SD card over GPIO pins via SPI.
+endef
+
+define Build/Prepare
+ mkdir -p $(PKG_BUILD_DIR)
+ $(CP) ./src/* $(PKG_BUILD_DIR)/
+endef
+
+define Build/Compile
+ $(MAKE) -C "$(LINUX_DIR)" \
+ CROSS_COMPILE="$(TARGET_CROSS)" \
+ ARCH="$(LINUX_KARCH)" \
+ SUBDIRS="$(PKG_BUILD_DIR)" \
+ EXTRA_CFLAGS="$(BUILDFLAGS)" \
+ modules
+endef
+
+$(eval $(call KernelPackage,mmc-over-gpio))
diff --git a/package/mmc_over_gpio/src/Makefile b/package/mmc_over_gpio/src/Makefile
new file mode 100644
index 0000000000..d2258bb95f
--- /dev/null
+++ b/package/mmc_over_gpio/src/Makefile
@@ -0,0 +1,2 @@
+obj-m += spi_gpio.o
+obj-m += mmc_over_spigpio.o
diff --git a/package/mmc_over_gpio/src/linux/spi/spi_gpio.h b/package/mmc_over_gpio/src/linux/spi/spi_gpio.h
new file mode 100644
index 0000000000..8e7d4b189d
--- /dev/null
+++ b/package/mmc_over_gpio/src/linux/spi/spi_gpio.h
@@ -0,0 +1,53 @@
+/*
+ * spi_gpio interface to platform code
+ *
+ * Copyright (c) 2008 Piotr Skamruk
+ *
+ * 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.
+ */
+#ifndef _LINUX_SPI_SPI_GPIO
+#define _LINUX_SPI_SPI_GPIO
+
+#include <linux/types.h>
+#include <linux/spi/spi.h>
+
+
+/** struct spi_gpio_platform_data - Data definitions for a SPI-GPIO device.
+ * This structure holds information about a GPIO-based SPI device.
+ *
+ * @pin_clk: The GPIO pin number of the CLOCK pin.
+ *
+ * @pin_miso: The GPIO pin number of the MISO pin.
+ *
+ * @pin_mosi: The GPIO pin number of the MOSI pin.
+ *
+ * @pin_cs: The GPIO pin number of the CHIPSELECT pin.
+ *
+ * @cs_activelow: If true, the chip is selected when the CS line is low.
+ *
+ * @no_spi_delay: If true, no delay is done in the lowlevel bitbanging.
+ * Note that doing no delay is not standards compliant,
+ * but it might be needed to speed up transfers on some
+ * slow embedded machines.
+ *
+ * @boardinfo_setup: This callback is called after the
+ * SPI master device was registered, but before the
+ * device is registered.
+ * @boardinfo_setup_data: Data argument passed to boardinfo_setup().
+ */
+struct spi_gpio_platform_data {
+ unsigned int pin_clk;
+ unsigned int pin_miso;
+ unsigned int pin_mosi;
+ unsigned int pin_cs;
+ bool cs_activelow;
+ bool no_spi_delay;
+ int (*boardinfo_setup)(struct spi_board_info *bi,
+ struct spi_master *master,
+ void *data);
+ void *boardinfo_setup_data;
+};
+
+#endif /* _LINUX_SPI_SPI_GPIO */
diff --git a/package/mmc_over_gpio/src/mmc_over_spigpio.c b/package/mmc_over_gpio/src/mmc_over_spigpio.c
new file mode 100644
index 0000000000..36dbd0f6a8
--- /dev/null
+++ b/package/mmc_over_gpio/src/mmc_over_spigpio.c
@@ -0,0 +1,339 @@
+/*
+ * Driver for driving an MMC card over a bitbanging GPIO SPI bus.
+ *
+ * Copyright 2008 Michael Buesch <mb@bu3sch.de>
+ *
+ * Licensed under the GNU/GPL. See COPYING for details.
+ */
+
+#include <linux/platform_device.h>
+#include <linux/list.h>
+#include <linux/mutex.h>
+#include "linux/spi/spi_gpio.h" //XXX
+
+
+/* This is the maximum speed in Hz */
+#define GPIOMMC_MAXSPEED 5000000 /* Hz */
+
+
+#define DRIVER_NAME "spi-gpio-mmc"
+#define PFX DRIVER_NAME ": "
+
+
+#define GPIOMMC_MAX_NAMELEN 15
+#define GPIOMMC_MAX_NAMELEN_STR __stringify(GPIOMMC_MAX_NAMELEN)
+
+struct gpiommc_pins {
+ unsigned int gpio_di; /* Card DI pin */
+ unsigned int gpio_do; /* Card DO pin */
+ unsigned int gpio_clk; /* Card CLK pin */
+ unsigned int gpio_cs; /* Card CS pin */
+};
+
+struct gpiommc_device {
+ char name[GPIOMMC_MAX_NAMELEN + 1];
+ struct platform_device *pdev;
+ struct platform_device *spi_pdev;
+ struct gpiommc_pins pins;
+ u8 mode; /* SPI_MODE_X */
+ struct spi_board_info boardinfo;
+
+ struct list_head list;
+};
+
+
+static LIST_HEAD(gpiommc_devices_list);
+static DEFINE_MUTEX(gpiommc_mutex);
+
+
+MODULE_DESCRIPTION("SPI-GPIO based MMC driver");
+MODULE_AUTHOR("Michael Buesch");
+MODULE_LICENSE("GPL");
+
+
+static int gpiommc_boardinfo_setup(struct spi_board_info *bi,
+ struct spi_master *master,
+ void *data)
+{
+ struct gpiommc_device *d = data;
+
+ /* Bind the SPI master to the MMC-SPI host driver. */
+ strlcpy(bi->modalias, "mmc_spi", sizeof(bi->modalias));
+
+ bi->max_speed_hz = GPIOMMC_MAXSPEED;
+ bi->bus_num = master->bus_num;
+ bi->mode = d->mode;
+
+ return 0;
+}
+
+static int gpiommc_probe(struct platform_device *pdev)
+{
+ static int instance;
+ struct gpiommc_device *d = platform_get_drvdata(pdev);
+ struct spi_gpio_platform_data pdata;
+ int err = -ENOMEM;
+
+ d->spi_pdev = platform_device_alloc("spi-gpio", instance++);
+ if (!d->spi_pdev)
+ goto out;
+
+ memset(&pdata, 0, sizeof(pdata));
+ pdata.pin_clk = d->pins.gpio_clk;
+ pdata.pin_miso = d->pins.gpio_do;
+ pdata.pin_mosi = d->pins.gpio_di;
+ pdata.pin_cs = d->pins.gpio_cs;
+ pdata.cs_activelow = 1;
+ pdata.no_spi_delay = 1;
+ pdata.boardinfo_setup = gpiommc_boardinfo_setup;
+ pdata.boardinfo_setup_data = d;
+
+ err = platform_device_add_data(d->spi_pdev, &pdata, sizeof(pdata));
+ if (err)
+ goto err_free_pdev;
+ err = platform_device_register(d->spi_pdev);
+ if (err)
+ goto err_free_pdata;
+
+ printk(KERN_INFO PFX "MMC-Card \"%s\" "
+ "attached to GPIO pins %u,%u,%u,%u\n",
+ d->name, d->pins.gpio_di, d->pins.gpio_do,
+ d->pins.gpio_clk, d->pins.gpio_cs);
+out:
+ return err;
+
+err_free_pdata:
+ kfree(d->spi_pdev->dev.platform_data);
+ d->spi_pdev->dev.platform_data = NULL;
+err_free_pdev:
+ platform_device_put(d->spi_pdev);
+ return err;
+}
+
+static int gpiommc_remove(struct platform_device *pdev)
+{
+ struct gpiommc_device *d = platform_get_drvdata(pdev);
+
+ platform_device_unregister(d->spi_pdev);
+ printk(KERN_INFO PFX "MMC-Card \"%s\" removed\n", d->name);
+
+ return 0;
+}
+
+static void gpiommc_free(struct gpiommc_device *d)
+{
+ kfree(d);
+}
+
+static struct gpiommc_device * gpiommc_alloc(struct platform_device *pdev,
+ const char *name,
+ const struct gpiommc_pins *pins,
+ u8 mode)
+{
+ struct gpiommc_device *d;
+
+ d = kmalloc(sizeof(*d), GFP_KERNEL);
+ if (!d)
+ return NULL;
+
+ strcpy(d->name, name);
+ memcpy(&d->pins, pins, sizeof(d->pins));
+ d->mode = mode;
+ INIT_LIST_HEAD(&d->list);
+
+ return d;
+}
+
+/* List must be locked. */
+static struct gpiommc_device * gpiommc_find_device(const char *name)
+{
+ struct gpiommc_device *d;
+
+ list_for_each_entry(d, &gpiommc_devices_list, list) {
+ if (strcmp(d->name, name) == 0)
+ return d;
+ }
+
+ return NULL;
+}
+
+static void gpiommc_do_destroy_device(struct gpiommc_device *d)
+{
+ list_del(&d->list);
+ platform_device_unregister(d->pdev);
+ gpiommc_free(d);
+}
+
+static int gpiommc_destroy_device(const char *name)
+{
+ struct gpiommc_device *d;
+ int err = -ENODEV;
+
+ mutex_lock(&gpiommc_mutex);
+ d = gpiommc_find_device(name);
+ if (!d)
+ goto out_unlock;
+ gpiommc_do_destroy_device(d);
+ err = 0;
+out_unlock:
+ mutex_unlock(&gpiommc_mutex);
+
+ return err;
+}
+
+static int gpiommc_create_device(const char *name,
+ const struct gpiommc_pins *pins,
+ u8 mode)
+{
+ static int instance;
+ struct platform_device *pdev;
+ struct gpiommc_device *d;
+ int err;
+
+ mutex_lock(&gpiommc_mutex);
+ err = -EEXIST;
+ if (gpiommc_find_device(name))
+ goto out_unlock;
+ err = -ENOMEM;
+ pdev = platform_device_alloc(DRIVER_NAME, instance++);
+ if (!pdev)
+ goto out_unlock;
+ d = gpiommc_alloc(pdev, name, pins, mode);
+ if (!d)
+ goto err_free_pdev;
+ platform_set_drvdata(pdev, d);
+ d->pdev = pdev;
+ err = platform_device_register(pdev);
+ if (err)
+ goto err_free_mdev;
+ list_add(&d->list, &gpiommc_devices_list);
+
+ err = 0;
+out_unlock:
+ mutex_unlock(&gpiommc_mutex);
+
+ return err;
+
+err_free_mdev:
+ gpiommc_free(d);
+err_free_pdev:
+ platform_device_put(pdev);
+ goto out_unlock;
+}
+
+static ssize_t gpiommc_add_show(struct device_driver *drv,
+ char *buf)
+{
+ return snprintf(buf, PAGE_SIZE, "NAME DI_pin,DO_pin,CLK_pin,CS_pin [MODE]\n");
+}
+
+static ssize_t gpiommc_add_store(struct device_driver *drv,
+ const char *buf, size_t count)
+{
+ int res, err;
+ char name[GPIOMMC_MAX_NAMELEN + 1];
+ struct gpiommc_pins pins;
+ unsigned int mode;
+
+ res = sscanf(buf, "%" GPIOMMC_MAX_NAMELEN_STR "s %u,%u,%u,%u %u",
+ name, &pins.gpio_di, &pins.gpio_do,
+ &pins.gpio_clk, &pins.gpio_cs, &mode);
+ if (res == 5)
+ mode = 0;
+ else if (res != 6)
+ return -EINVAL;
+ switch (mode) {
+ case 0:
+ mode = SPI_MODE_0;
+ break;
+ case 1:
+ mode = SPI_MODE_1;
+ break;
+ case 2:
+ mode = SPI_MODE_2;
+ break;
+ case 3:
+ mode = SPI_MODE_3;
+ break;
+ default:
+ return -EINVAL;
+ }
+ err = gpiommc_create_device(name, &pins, mode);
+
+ return err ? err : count;
+}
+
+static ssize_t gpiommc_remove_show(struct device_driver *drv,
+ char *buf)
+{
+ return snprintf(buf, PAGE_SIZE, "write device-name to remove the device\n");
+}
+
+static ssize_t gpiommc_remove_store(struct device_driver *drv,
+ const char *buf, size_t count)
+{
+ int err;
+
+ err = gpiommc_destroy_device(buf);
+
+ return err ? err : count;
+}
+
+static DRIVER_ATTR(add, 0600,
+ gpiommc_add_show, gpiommc_add_store);
+static DRIVER_ATTR(remove, 0600,
+ gpiommc_remove_show, gpiommc_remove_store);
+
+static struct platform_driver gpiommc_plat_driver = {
+ .probe = gpiommc_probe,
+ .remove = gpiommc_remove,
+ .driver = {
+ .name = DRIVER_NAME,
+ .owner = THIS_MODULE,
+ },
+};
+
+static int __init gpiommc_modinit(void)
+{
+ int err;
+
+ err = platform_driver_register(&gpiommc_plat_driver);
+ if (err)
+ return err;
+ err = driver_create_file(&gpiommc_plat_driver.driver,
+ &driver_attr_add);
+ if (err)
+ goto err_drv_unreg;
+ err = driver_create_file(&gpiommc_plat_driver.driver,
+ &driver_attr_remove);
+ if (err)
+ goto err_remove_add;
+
+ return 0;
+
+err_remove_add:
+ driver_remove_file(&gpiommc_plat_driver.driver,
+ &driver_attr_add);
+err_drv_unreg:
+ platform_driver_unregister(&gpiommc_plat_driver);
+ return err;
+}
+module_init(gpiommc_modinit);
+
+static void __exit gpiommc_modexit(void)
+{
+ struct gpiommc_device *d, *tmp;
+
+ driver_remove_file(&gpiommc_plat_driver.driver,
+ &driver_attr_remove);
+ driver_remove_file(&gpiommc_plat_driver.driver,
+ &driver_attr_add);
+
+ mutex_lock(&gpiommc_mutex);
+ list_for_each_entry_safe(d, tmp, &gpiommc_devices_list, list)
+ gpiommc_do_destroy_device(d);
+ mutex_unlock(&gpiommc_mutex);
+
+ platform_driver_unregister(&gpiommc_plat_driver);
+}
+module_exit(gpiommc_modexit);
diff --git a/package/mmc_over_gpio/src/spi_gpio.c b/package/mmc_over_gpio/src/spi_gpio.c
new file mode 100644
index 0000000000..31048acc3f
--- /dev/null
+++ b/package/mmc_over_gpio/src/spi_gpio.c
@@ -0,0 +1,242 @@
+/*
+ * Bitbanging SPI bus driver using GPIO API
+ *
+ * Copyright (c) 2008 Piotr Skamruk
+ * Copyright (c) 2008 Michael Buesch
+ *
+ * based on spi_s3c2410_gpio.c
+ * Copyright (c) 2006 Ben Dooks
+ * Copyright (c) 2006 Simtec Electronics
+ * and on i2c-gpio.c
+ * Copyright (C) 2007 Atmel Corporation
+ *
+ * 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.
+ */
+
+#include <linux/kernel.h>
+#include <linux/init.h>
+#include <linux/delay.h>
+#include <linux/spinlock.h>
+#include <linux/workqueue.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+#include <linux/spi/spi.h>
+#include <linux/spi/spi_bitbang.h>
+#include "linux/spi/spi_gpio.h" //XXX
+#include <asm/gpio.h>
+
+
+struct spi_gpio {
+ struct spi_bitbang bitbang;
+ struct spi_gpio_platform_data *info;
+ struct platform_device *pdev;
+ struct spi_board_info bi;
+};
+
+
+static inline struct spi_gpio *spidev_to_sg(struct spi_device *dev)
+{
+ return dev->controller_data;
+}
+
+static inline void setsck(struct spi_device *dev, int val)
+{
+ struct spi_gpio *sp = spidev_to_sg(dev);
+ gpio_set_value(sp->info->pin_clk, val ? 1 : 0);
+}
+
+static inline void setmosi(struct spi_device *dev, int val )
+{
+ struct spi_gpio *sp = spidev_to_sg(dev);
+ gpio_set_value(sp->info->pin_mosi, val ? 1 : 0);
+}
+
+static inline u32 getmiso(struct spi_device *dev)
+{
+ struct spi_gpio *sp = spidev_to_sg(dev);
+ return gpio_get_value(sp->info->pin_miso) ? 1 : 0;
+}
+
+static inline void do_spidelay(struct spi_device *dev, unsigned nsecs)
+{
+ struct spi_gpio *sp = spidev_to_sg(dev);
+
+ if (!sp->info->no_spi_delay)
+ ndelay(nsecs);
+}
+
+#define spidelay(nsecs) do { \
+ /* Steal the spi_device pointer from our caller. \
+ * The bitbang-API should probably get fixed here... */ \
+ do_spidelay(spi, nsecs); \
+ } while (0)
+
+#define EXPAND_BITBANG_TXRX
+#include <linux/spi/spi_bitbang.h>
+
+static u32 spi_gpio_txrx_mode0(struct spi_device *spi,
+ unsigned nsecs, u32 word, u8 bits)
+{
+ return bitbang_txrx_be_cpha0(spi, nsecs, 0, word, bits);
+}
+
+static u32 spi_gpio_txrx_mode1(struct spi_device *spi,
+ unsigned nsecs, u32 word, u8 bits)
+{
+ return bitbang_txrx_be_cpha1(spi, nsecs, 0, word, bits);
+}
+
+static u32 spi_gpio_txrx_mode2(struct spi_device *spi,
+ unsigned nsecs, u32 word, u8 bits)
+{
+ return bitbang_txrx_be_cpha0(spi, nsecs, 1, word, bits);
+}
+
+static u32 spi_gpio_txrx_mode3(struct spi_device *spi,
+ unsigned nsecs, u32 word, u8 bits)
+{
+ return bitbang_txrx_be_cpha1(spi, nsecs, 1, word, bits);
+}
+
+static void spi_gpio_chipselect(struct spi_device *dev, int on)
+{
+ struct spi_gpio *sp = spidev_to_sg(dev);
+
+ if (sp->info->cs_activelow)
+ on = !on;
+ gpio_set_value(sp->info->pin_cs, on ? 1 : 0);
+}
+
+static int spi_gpio_probe(struct platform_device *pdev)
+{
+ struct spi_master *master;
+ struct spi_gpio_platform_data *pdata;
+ struct spi_gpio *sp;
+ struct spi_device *spidev;
+ int err;
+
+ pdata = pdev->dev.platform_data;
+ if (!pdata)
+ return -ENXIO;
+
+ err = -ENOMEM;
+ master = spi_alloc_master(&pdev->dev, sizeof(struct spi_gpio));
+ if (!master)
+ goto err_alloc_master;
+
+ sp = spi_master_get_devdata(master);
+ platform_set_drvdata(pdev, sp);
+ sp->info = pdata;
+
+ err = gpio_request(pdata->pin_clk, "spi_clock");
+ if (err)
+ goto err_request_clk;
+ err = gpio_request(pdata->pin_mosi, "spi_mosi");
+ if (err)
+ goto err_request_mosi;
+ err = gpio_request(pdata->pin_miso, "spi_miso");
+ if (err)
+ goto err_request_miso;
+ err = gpio_request(pdata->pin_cs, "spi_cs");
+ if (err)
+ goto err_request_cs;
+
+ sp->bitbang.master = spi_master_get(master);
+ sp->bitbang.master->bus_num = -1;
+ sp->bitbang.master->num_chipselect = 1;
+ sp->bitbang.chipselect = spi_gpio_chipselect;
+ sp->bitbang.txrx_word[SPI_MODE_0] = spi_gpio_txrx_mode0;
+ sp->bitbang.txrx_word[SPI_MODE_1] = spi_gpio_txrx_mode1;
+ sp->bitbang.txrx_word[SPI_MODE_2] = spi_gpio_txrx_mode2;
+ sp->bitbang.txrx_word[SPI_MODE_3] = spi_gpio_txrx_mode3;
+
+ gpio_direction_output(pdata->pin_clk, 0);
+ gpio_direction_output(pdata->pin_mosi, 0);
+ gpio_direction_output(pdata->pin_cs,
+ pdata->cs_activelow ? 1 : 0);
+ gpio_direction_input(pdata->pin_miso);
+
+ err = spi_bitbang_start(&sp->bitbang);
+ if (err)
+ goto err_no_bitbang;
+ err = pdata->boardinfo_setup(&sp->bi, master,
+ pdata->boardinfo_setup_data);
+ if (err)
+ goto err_bi_setup;
+ sp->bi.controller_data = sp;
+ spidev = spi_new_device(master, &sp->bi);
+ if (!spidev)
+ goto err_new_dev;
+
+ return 0;
+
+err_new_dev:
+err_bi_setup:
+ spi_bitbang_stop(&sp->bitbang);
+err_no_bitbang:
+ spi_master_put(sp->bitbang.master);
+ gpio_free(pdata->pin_cs);
+err_request_cs:
+ gpio_free(pdata->pin_miso);
+err_request_miso:
+ gpio_free(pdata->pin_mosi);
+err_request_mosi:
+ gpio_free(pdata->pin_clk);
+err_request_clk:
+ kfree(master);
+
+err_alloc_master:
+ return err;
+}
+
+static int __devexit spi_gpio_remove(struct platform_device *pdev)
+{
+ struct spi_gpio *sp;
+ struct spi_gpio_platform_data *pdata;
+
+ pdata = pdev->dev.platform_data;
+ sp = platform_get_drvdata(pdev);
+
+ gpio_free(pdata->pin_clk);
+ gpio_free(pdata->pin_mosi);
+ gpio_free(pdata->pin_miso);
+ gpio_free(pdata->pin_cs);
+ spi_bitbang_stop(&sp->bitbang);
+ spi_master_put(sp->bitbang.master);
+
+ return 0;
+}
+
+static struct platform_driver spi_gpio_driver = {
+ .driver = {
+ .name = "spi-gpio",
+ .owner = THIS_MODULE,
+ },
+ .probe = spi_gpio_probe,
+ .remove = __devexit_p(spi_gpio_remove),
+};
+
+static int __init spi_gpio_init(void)
+{
+ int err;
+
+ err = platform_driver_register(&spi_gpio_driver);
+ if (err)
+ printk(KERN_ERR "spi-gpio: register failed: %d\n", err);
+
+ return err;
+}
+module_init(spi_gpio_init);
+
+static void __exit spi_gpio_exit(void)
+{
+ platform_driver_unregister(&spi_gpio_driver);
+}
+module_exit(spi_gpio_exit);
+
+MODULE_AUTHOR("Piot Skamruk <piotr.skamruk at gmail.com>");
+MODULE_AUTHOR("Michael Buesch");
+MODULE_DESCRIPTION("Platform independent GPIO bitbangling SPI driver");
+MODULE_LICENSE("GPL v2");