--- /dev/null +++ b/arch/arm/plat-omap/bootreason.c @@ -0,0 +1,79 @@ +/* + * linux/arch/arm/plat-omap/bootreason.c + * + * OMAP Bootreason passing + * + * Copyright (c) 2004 Nokia + * + * Written by David Weinehall + * + * 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. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + * NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 675 Mass Ave, Cambridge, MA 02139, USA. + */ +#include +#include +#include + +static char boot_reason[16]; + +static int omap_bootreason_read_proc(char *page, char **start, off_t off, + int count, int *eof, void *data) +{ + int len = 0; + + len += sprintf(page + len, "%s\n", boot_reason); + + *start = page + off; + + if (len > off) + len -= off; + else + len = 0; + + return len < count ? len : count; +} + +static int __init bootreason_init(void) +{ + const struct omap_boot_reason_config *cfg; + int reason_valid = 0; + + cfg = omap_get_config(OMAP_TAG_BOOT_REASON, struct omap_boot_reason_config); + if (cfg != NULL) { + strncpy(boot_reason, cfg->reason_str, sizeof(cfg->reason_str)); + boot_reason[sizeof(cfg->reason_str)] = 0; + reason_valid = 1; + } else { + /* Read the boot reason from the OMAP registers */ + } + + if (!reason_valid) + return -ENOENT; + + printk(KERN_INFO "Bootup reason: %s\n", boot_reason); + + if (!create_proc_read_entry("bootreason", S_IRUGO, NULL, + omap_bootreason_read_proc, NULL)) + return -ENOMEM; + + return 0; +} + +late_initcall(bootreason_init); --- a/arch/arm/plat-omap/common.c +++ b/arch/arm/plat-omap/common.c @@ -24,18 +24,90 @@ #include +#include + #define NO_LENGTH_CHECK 0xffffffff struct omap_board_config_kernel *omap_board_config __initdata; int omap_board_config_size; +unsigned char omap_bootloader_tag[1024]; +int omap_bootloader_tag_len; + +/* used by omap-smp.c and board-4430sdp.c */ +void __iomem *gic_cpu_base_addr; + +#ifdef CONFIG_OMAP_BOOT_TAG + +static int __init parse_tag_omap(const struct tag *tag) +{ + u32 size = tag->hdr.size - (sizeof(tag->hdr) >> 2); + + size <<= 2; + if (size > sizeof(omap_bootloader_tag)) + return -1; + + memcpy(omap_bootloader_tag, tag->u.omap.data, size); + omap_bootloader_tag_len = size; + + return 0; +} + +__tagtable(ATAG_BOARD, parse_tag_omap); + +#endif + static const void *__init get_config(u16 tag, size_t len, int skip, size_t *len_out) { struct omap_board_config_kernel *kinfo = NULL; int i; +#ifdef CONFIG_OMAP_BOOT_TAG + struct omap_board_config_entry *info = NULL; + + if (omap_bootloader_tag_len > 4) + info = (struct omap_board_config_entry *) omap_bootloader_tag; + while (info != NULL) { + u8 *next; + + if (info->tag == tag) { + if (skip == 0) + break; + skip--; + } + + if ((info->len & 0x03) != 0) { + /* We bail out to avoid an alignment fault */ + printk(KERN_ERR "OMAP peripheral config: Length (%d) not word-aligned (tag %04x)\n", + info->len, info->tag); + return NULL; + } + next = (u8 *) info + sizeof(*info) + info->len; + if (next >= omap_bootloader_tag + omap_bootloader_tag_len) + info = NULL; + else + info = (struct omap_board_config_entry *) next; + } + if (info != NULL) { + /* Check the length as a lame attempt to check for + * binary inconsistency. */ + if (len != NO_LENGTH_CHECK) { + /* Word-align len */ + if (len & 0x03) + len = (len + 3) & ~0x03; + if (info->len != len) { + printk(KERN_ERR "OMAP peripheral config: Length mismatch with tag %x (want %d, got %d)\n", + tag, len, info->len); + return NULL; + } + } + if (len_out != NULL) + *len_out = info->len; + return info->data; + } +#endif /* Try to find the config from the board-specific structures * in the kernel. */ for (i = 0; i < omap_board_config_size; i++) { --- /dev/null +++ b/arch/arm/plat-omap/component-version.c @@ -0,0 +1,64 @@ +/* + * linux/arch/arm/plat-omap/component-version.c + * + * Copyright (C) 2005 Nokia Corporation + * Written by Juha Yrjölä + * + * 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 +#include +#include +#include +#include + +static int component_version_read_proc(char *page, char **start, off_t off, + int count, int *eof, void *data) +{ + int len, i; + const struct omap_version_config *ver; + char *p; + + i = 0; + p = page; + while ((ver = omap_get_nr_config(OMAP_TAG_VERSION_STR, + struct omap_version_config, i)) != NULL) { + p += sprintf(p, "%-12s%s\n", ver->component, ver->version); + i++; + } + + len = (p - page) - off; + if (len < 0) + len = 0; + + *eof = (len <= count) ? 1 : 0; + *start = page + off; + + return len; +} + +static int __init component_version_init(void) +{ + if (omap_get_config(OMAP_TAG_VERSION_STR, struct omap_version_config) == NULL) + return -ENODEV; + if (!create_proc_read_entry("component_version", S_IRUGO, NULL, + component_version_read_proc, NULL)) + return -ENOMEM; + + return 0; +} + +static void __exit component_version_exit(void) +{ + remove_proc_entry("component_version", NULL); +} + +late_initcall(component_version_init); +module_exit(component_version_exit); + +MODULE_AUTHOR("Juha Yrjölä "); +MODULE_DESCRIPTION("Component version driver"); +MODULE_LICENSE("GPL"); --- a/arch/arm/plat-omap/Kconfig +++ b/arch/arm/plat-omap/Kconfig @@ -84,6 +84,38 @@ config OMAP_RESET_CLOCKS probably do not want this option enabled until your device drivers work properly. +config OMAP_BOOT_TAG + bool "OMAP bootloader information passing" + depends on ARCH_OMAP + default n + help + Say Y, if you have a bootloader which passes information + about your board and its peripheral configuration. + +config OMAP_BOOT_REASON + bool "Support for boot reason" + depends on OMAP_BOOT_TAG + default n + help + Say Y, if you want to have a procfs entry for reading the boot + reason in user-space. + +config OMAP_COMPONENT_VERSION + bool "Support for component version display" + depends on OMAP_BOOT_TAG && PROC_FS + default n + help + Say Y, if you want to have a procfs entry for reading component + versions (supplied by the bootloader) in user-space. + +config OMAP_GPIO_SWITCH + bool "GPIO switch support" + help + Say Y, if you want to have support for reporting of GPIO + switches (e.g. cover switches) via sysfs. Your bootloader has + to provide information about the switches to the kernel via the + ATAG_BOARD mechanism if they're not defined by the board config. + config OMAP_MUX bool "OMAP multiplexing support" depends on ARCH_OMAP --- a/arch/arm/plat-omap/Makefile +++ b/arch/arm/plat-omap/Makefile @@ -20,6 +20,9 @@ obj-$(CONFIG_ARCH_OMAP4) += omap_device. obj-$(CONFIG_OMAP_MCBSP) += mcbsp.o obj-$(CONFIG_OMAP_DM_TIMER) += dmtimer.o +obj-$(CONFIG_OMAP_BOOT_REASON) += bootreason.o +obj-$(CONFIG_OMAP_COMPONENT_VERSION) += component-version.o +obj-$(CONFIG_OMAP_GPIO_SWITCH) += gpio-switch.o obj-$(CONFIG_OMAP_DEBUG_DEVICES) += debug-devices.o obj-$(CONFIG_OMAP_DEBUG_LEDS) += debug-leds.o i2c-omap-$(CONFIG_I2C_OMAP) := i2c.o --- a/arch/arm/include/asm/setup.h +++ b/arch/arm/include/asm/setup.h @@ -136,6 +136,13 @@ struct tag_acorn { __u8 adfsdrives; }; +/* TI OMAP specific information */ +#define ATAG_BOARD 0x414f4d50 + +struct tag_omap { + u8 data[0]; +}; + /* footbridge memory clock, see arch/arm/mach-footbridge/arch.c */ #define ATAG_MEMCLK 0x41000402 @@ -162,6 +169,11 @@ struct tag { struct tag_acorn acorn; /* + * OMAP specific + */ + struct tag_omap omap; + + /* * DC21285 specific */ struct tag_memclk memclk; --- /dev/null +++ b/arch/arm/plat-omap/gpio-switch.c @@ -0,0 +1,554 @@ +/* + * linux/arch/arm/plat-omap/gpio-switch.c + * + * Copyright (C) 2004-2006 Nokia Corporation + * Written by Juha Yrjölä + * and Paul Mundt + * + * 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct gpio_switch { + char name[14]; + u16 gpio; + unsigned flags:4; + unsigned type:4; + unsigned state:1; + unsigned both_edges:1; + + u16 debounce_rising; + u16 debounce_falling; + + void (* notify)(void *data, int state); + void *notify_data; + + struct work_struct work; + struct timer_list timer; + struct platform_device pdev; + + struct list_head node; +}; + +static LIST_HEAD(gpio_switches); +static struct platform_device *gpio_sw_platform_dev; +static struct platform_driver gpio_sw_driver; + +static const struct omap_gpio_switch *board_gpio_sw_table; +static int board_gpio_sw_count; + +static const char *cover_str[2] = { "open", "closed" }; +static const char *connection_str[2] = { "disconnected", "connected" }; +static const char *activity_str[2] = { "inactive", "active" }; + +/* + * GPIO switch state default debounce delay in ms + */ +#define OMAP_GPIO_SW_DEFAULT_DEBOUNCE 10 + +static const char **get_sw_str(struct gpio_switch *sw) +{ + switch (sw->type) { + case OMAP_GPIO_SWITCH_TYPE_COVER: + return cover_str; + case OMAP_GPIO_SWITCH_TYPE_CONNECTION: + return connection_str; + case OMAP_GPIO_SWITCH_TYPE_ACTIVITY: + return activity_str; + default: + BUG(); + return NULL; + } +} + +static const char *get_sw_type(struct gpio_switch *sw) +{ + switch (sw->type) { + case OMAP_GPIO_SWITCH_TYPE_COVER: + return "cover"; + case OMAP_GPIO_SWITCH_TYPE_CONNECTION: + return "connection"; + case OMAP_GPIO_SWITCH_TYPE_ACTIVITY: + return "activity"; + default: + BUG(); + return NULL; + } +} + +static void print_sw_state(struct gpio_switch *sw, int state) +{ + const char **str; + + str = get_sw_str(sw); + if (str != NULL) + printk(KERN_INFO "%s (GPIO %d) is now %s\n", sw->name, sw->gpio, str[state]); +} + +static int gpio_sw_get_state(struct gpio_switch *sw) +{ + int state; + + state = gpio_get_value(sw->gpio); + if (sw->flags & OMAP_GPIO_SWITCH_FLAG_INVERTED) + state = !state; + + return state; +} + +static ssize_t gpio_sw_state_store(struct device *dev, + struct device_attribute *attr, + const char *buf, + size_t count) +{ + struct gpio_switch *sw = dev_get_drvdata(dev); + const char **str; + char state[16]; + int enable; + + if (!(sw->flags & OMAP_GPIO_SWITCH_FLAG_OUTPUT)) + return -EPERM; + + if (sscanf(buf, "%15s", state) != 1) + return -EINVAL; + + str = get_sw_str(sw); + if (strcmp(state, str[0]) == 0) + sw->state = enable = 0; + else if (strcmp(state, str[1]) == 0) + sw->state = enable = 1; + else + return -EINVAL; + + if (sw->flags & OMAP_GPIO_SWITCH_FLAG_INVERTED) + enable = !enable; + gpio_set_value(sw->gpio, enable); + + return count; +} + +static ssize_t gpio_sw_state_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct gpio_switch *sw = dev_get_drvdata(dev); + const char **str; + + str = get_sw_str(sw); + return sprintf(buf, "%s\n", str[sw->state]); +} + +static DEVICE_ATTR(state, S_IRUGO | S_IWUSR, gpio_sw_state_show, + gpio_sw_state_store); + +static ssize_t gpio_sw_type_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct gpio_switch *sw = dev_get_drvdata(dev); + + return sprintf(buf, "%s\n", get_sw_type(sw)); +} + +static DEVICE_ATTR(type, S_IRUGO, gpio_sw_type_show, NULL); + +static ssize_t gpio_sw_direction_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct gpio_switch *sw = dev_get_drvdata(dev); + int is_output; + + is_output = sw->flags & OMAP_GPIO_SWITCH_FLAG_OUTPUT; + return sprintf(buf, "%s\n", is_output ? "output" : "input"); +} + +static DEVICE_ATTR(direction, S_IRUGO, gpio_sw_direction_show, NULL); + + +static irqreturn_t gpio_sw_irq_handler(int irq, void *arg) +{ + struct gpio_switch *sw = arg; + unsigned long timeout; + int state; + + if (!sw->both_edges) { + if (gpio_get_value(sw->gpio)) + irq_set_irq_type(OMAP_GPIO_IRQ(sw->gpio), IRQ_TYPE_EDGE_FALLING); + else + irq_set_irq_type(OMAP_GPIO_IRQ(sw->gpio), IRQ_TYPE_EDGE_RISING); + } + + state = gpio_sw_get_state(sw); + if (sw->state == state) + return IRQ_HANDLED; + + if (state) + timeout = sw->debounce_rising; + else + timeout = sw->debounce_falling; + if (!timeout) + schedule_work(&sw->work); + else + mod_timer(&sw->timer, jiffies + msecs_to_jiffies(timeout)); + + return IRQ_HANDLED; +} + +static void gpio_sw_timer(unsigned long arg) +{ + struct gpio_switch *sw = (struct gpio_switch *) arg; + + schedule_work(&sw->work); +} + +static void gpio_sw_handler(struct work_struct *work) +{ + struct gpio_switch *sw = container_of(work, struct gpio_switch, work); + int state; + + state = gpio_sw_get_state(sw); + if (sw->state == state) + return; + + sw->state = state; + if (sw->notify != NULL) + sw->notify(sw->notify_data, state); + sysfs_notify(&sw->pdev.dev.kobj, NULL, "state"); + print_sw_state(sw, state); +} + +static int __init can_do_both_edges(struct gpio_switch *sw) +{ + if (!cpu_class_is_omap1()) + return 1; + if (OMAP_GPIO_IS_MPUIO(sw->gpio)) + return 0; + else + return 1; +} + +static void gpio_sw_release(struct device *dev) +{ +} + +static int __init new_switch(struct gpio_switch *sw) +{ + int r, direction, trigger; + + switch (sw->type) { + case OMAP_GPIO_SWITCH_TYPE_COVER: + case OMAP_GPIO_SWITCH_TYPE_CONNECTION: + case OMAP_GPIO_SWITCH_TYPE_ACTIVITY: + break; + default: + printk(KERN_ERR "invalid GPIO switch type: %d\n", sw->type); + return -EINVAL; + } + + sw->pdev.name = sw->name; + sw->pdev.id = -1; + + sw->pdev.dev.parent = &gpio_sw_platform_dev->dev; + sw->pdev.dev.driver = &gpio_sw_driver.driver; + sw->pdev.dev.release = gpio_sw_release; + + r = platform_device_register(&sw->pdev); + if (r) { + printk(KERN_ERR "gpio-switch: platform device registration " + "failed for %s", sw->name); + return r; + } + dev_set_drvdata(&sw->pdev.dev, sw); + + r = gpio_request(sw->gpio, "gpio-switch"); + if (r < 0) { + platform_device_unregister(&sw->pdev); + return r; + } + + /* input: 1, output: 0 */ + direction = !(sw->flags & OMAP_GPIO_SWITCH_FLAG_OUTPUT); + if (direction) + gpio_direction_input(sw->gpio); + else + gpio_direction_output(sw->gpio, 0); + + sw->state = gpio_sw_get_state(sw); + + r = 0; + r |= device_create_file(&sw->pdev.dev, &dev_attr_state); + r |= device_create_file(&sw->pdev.dev, &dev_attr_type); + r |= device_create_file(&sw->pdev.dev, &dev_attr_direction); + if (r) + printk(KERN_ERR "gpio-switch: attribute file creation " + "failed for %s\n", sw->name); + + if (!direction) + return 0; + + if (can_do_both_edges(sw)) { + trigger = IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING; + sw->both_edges = 1; + } else { + if (gpio_get_value(sw->gpio)) + trigger = IRQF_TRIGGER_FALLING; + else + trigger = IRQF_TRIGGER_RISING; + } + r = request_irq(OMAP_GPIO_IRQ(sw->gpio), gpio_sw_irq_handler, + IRQF_SHARED | trigger, sw->name, sw); + if (r < 0) { + printk(KERN_ERR "gpio-switch: request_irq() failed " + "for GPIO %d\n", sw->gpio); + platform_device_unregister(&sw->pdev); + gpio_free(sw->gpio); + return r; + } + + INIT_WORK(&sw->work, gpio_sw_handler); + init_timer(&sw->timer); + + sw->timer.function = gpio_sw_timer; + sw->timer.data = (unsigned long)sw; + + list_add(&sw->node, &gpio_switches); + + return 0; +} + +static int __init add_atag_switches(void) +{ + const struct omap_gpio_switch_config *cfg; + struct gpio_switch *sw; + int i, r; + + for (i = 0; ; i++) { + cfg = omap_get_nr_config(OMAP_TAG_GPIO_SWITCH, + struct omap_gpio_switch_config, i); + if (cfg == NULL) + break; + sw = kzalloc(sizeof(*sw), GFP_KERNEL); + if (sw == NULL) { + printk(KERN_ERR "gpio-switch: kmalloc failed\n"); + return -ENOMEM; + } + strncpy(sw->name, cfg->name, sizeof(cfg->name)); + sw->gpio = cfg->gpio; + sw->flags = cfg->flags; + sw->type = cfg->type; + sw->debounce_rising = OMAP_GPIO_SW_DEFAULT_DEBOUNCE; + sw->debounce_falling = OMAP_GPIO_SW_DEFAULT_DEBOUNCE; + if ((r = new_switch(sw)) < 0) { + kfree(sw); + return r; + } + } + return 0; +} + +static struct gpio_switch * __init find_switch(int gpio, const char *name) +{ + struct gpio_switch *sw; + + list_for_each_entry(sw, &gpio_switches, node) { + if ((gpio < 0 || sw->gpio != gpio) && + (name == NULL || strcmp(sw->name, name) != 0)) + continue; + + if (gpio < 0 || name == NULL) + goto no_check; + + if (strcmp(sw->name, name) != 0) + printk("gpio-switch: name mismatch for %d (%s, %s)\n", + gpio, name, sw->name); + else if (sw->gpio != gpio) + printk("gpio-switch: GPIO mismatch for %s (%d, %d)\n", + name, gpio, sw->gpio); +no_check: + return sw; + } + return NULL; +} + +static int __init add_board_switches(void) +{ + int i; + + for (i = 0; i < board_gpio_sw_count; i++) { + const struct omap_gpio_switch *cfg; + struct gpio_switch *sw; + int r; + + cfg = board_gpio_sw_table + i; + if (strlen(cfg->name) > sizeof(sw->name) - 1) + return -EINVAL; + /* Check whether we only update an existing switch + * or add a new switch. */ + sw = find_switch(cfg->gpio, cfg->name); + if (sw != NULL) { + sw->debounce_rising = cfg->debounce_rising; + sw->debounce_falling = cfg->debounce_falling; + sw->notify = cfg->notify; + sw->notify_data = cfg->notify_data; + continue; + } else { + if (cfg->gpio < 0 || cfg->name == NULL) { + printk("gpio-switch: required switch not " + "found (%d, %s)\n", cfg->gpio, + cfg->name); + continue; + } + } + sw = kzalloc(sizeof(*sw), GFP_KERNEL); + if (sw == NULL) { + printk(KERN_ERR "gpio-switch: kmalloc failed\n"); + return -ENOMEM; + } + strlcpy(sw->name, cfg->name, sizeof(sw->name)); + sw->gpio = cfg->gpio; + sw->flags = cfg->flags; + sw->type = cfg->type; + sw->debounce_rising = cfg->debounce_rising; + sw->debounce_falling = cfg->debounce_falling; + sw->notify = cfg->notify; + sw->notify_data = cfg->notify_data; + if ((r = new_switch(sw)) < 0) { + kfree(sw); + return r; + } + } + return 0; +} + +static void gpio_sw_cleanup(void) +{ + struct gpio_switch *sw = NULL, *old = NULL; + + list_for_each_entry(sw, &gpio_switches, node) { + if (old != NULL) + kfree(old); + flush_scheduled_work(); + del_timer_sync(&sw->timer); + + free_irq(OMAP_GPIO_IRQ(sw->gpio), sw); + + device_remove_file(&sw->pdev.dev, &dev_attr_state); + device_remove_file(&sw->pdev.dev, &dev_attr_type); + device_remove_file(&sw->pdev.dev, &dev_attr_direction); + + platform_device_unregister(&sw->pdev); + gpio_free(sw->gpio); + old = sw; + } + kfree(old); +} + +static void __init report_initial_state(void) +{ + struct gpio_switch *sw; + + list_for_each_entry(sw, &gpio_switches, node) { + int state; + + state = gpio_get_value(sw->gpio); + if (sw->flags & OMAP_GPIO_SWITCH_FLAG_INVERTED) + state = !state; + if (sw->notify != NULL) + sw->notify(sw->notify_data, state); + print_sw_state(sw, state); + } +} + +static int gpio_sw_remove(struct platform_device *dev) +{ + return 0; +} + +static struct platform_driver gpio_sw_driver = { + .remove = gpio_sw_remove, + .driver = { + .name = "gpio-switch", + }, +}; + +void __init omap_register_gpio_switches(const struct omap_gpio_switch *tbl, + int count) +{ + BUG_ON(board_gpio_sw_table != NULL); + + board_gpio_sw_table = tbl; + board_gpio_sw_count = count; +} + +static int __init gpio_sw_init(void) +{ + int r; + + printk(KERN_INFO "OMAP GPIO switch handler initializing\n"); + + r = platform_driver_register(&gpio_sw_driver); + if (r) + return r; + + gpio_sw_platform_dev = platform_device_register_simple("gpio-switch", + -1, NULL, 0); + if (IS_ERR(gpio_sw_platform_dev)) { + r = PTR_ERR(gpio_sw_platform_dev); + goto err1; + } + + r = add_atag_switches(); + if (r < 0) + goto err2; + + r = add_board_switches(); + if (r < 0) + goto err2; + + report_initial_state(); + + return 0; +err2: + gpio_sw_cleanup(); + platform_device_unregister(gpio_sw_platform_dev); +err1: + platform_driver_unregister(&gpio_sw_driver); + return r; +} + +static void __exit gpio_sw_exit(void) +{ + gpio_sw_cleanup(); + platform_device_unregister(gpio_sw_platform_dev); + platform_driver_unregister(&gpio_sw_driver); +} + +#ifndef MODULE +late_initcall(gpio_sw_init); +#else +module_init(gpio_sw_init); +#endif +module_exit(gpio_sw_exit); + +MODULE_AUTHOR("Juha Yrjölä , Paul Mundt