spi: Added driver for CP2130 USB-to-SPI bridge
diff mbox series

Message ID 1557144380-19935-2-git-send-email-jh@henneberg-systemdesign.com
State New, archived
Headers show
Series
  • spi: Added driver for CP2130 USB-to-SPI bridge
Related show

Commit Message

Jochen Henneberg May 6, 2019, 12:06 p.m. UTC
This is a new driver for the Silicon Labs CP2130 USB-to-SPI bridge. It
is meant to be used for hotplug devices, e. g. the CP2130 combined with
the MCP251x CAN controllers.

Configuration happens when plugged (e. g. from udev), the CP2130 driver
provides sysfs files to control the bridge setup. From there you can
configured SPI bus settings, the interrupt pin, the attached
devices. You can also provide platform data in binary format if the
attached chip requires this.

The OTP ROM can be used to setup a specific product/vendor id to
simplify different device setup recognition.

Signed-off-by: Jochen Henneberg <jh@henneberg-systemdesign.com>
---
 drivers/spi/Kconfig      |    6 +
 drivers/spi/Makefile     |    1 +
 drivers/spi/spi-cp2130.c | 1672 ++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 1679 insertions(+)
 create mode 100644 drivers/spi/spi-cp2130.c

Comments

Mark Brown May 8, 2019, 7:18 a.m. UTC | #1
On Mon, May 06, 2019 at 02:06:20PM +0200, Jochen Henneberg wrote:

This driver has huge amounts of non-standard interfaces in it,
especially the userpace ABI it adds.  It would be a lot easier to review
if it were split up so that it's a series where the core SPI
functionality is added initially and then other things were layered on
top as additional patches.

> @@ -0,0 +1,1672 @@
> +// SPDX-License-Identifier: GPL-2.0
> +
> +/* Kernel driver for Silicon Labs CP2130 USB-to-SPI bridge.
> + *
> + * Copyright (C) 2019 Jochen Henneberg (jh@henneberg-systemdesign.com)
> + */

Please keep the entire comment a C++ comment, it makes things look more
intentional.

> +/* Prototypes */
> +static int cp2130_probe(struct usb_interface *intf,
> +			const struct usb_device_id *id);
> +static void cp2130_disconnect(struct usb_interface *intf);

These forward declarations are really weird in a Linux driver, normally
things like probe() are defined at the bottom of the driver just before
the driver structure which also usually goes at the end.

> +/* USB device functions */
> +static struct usb_driver cp2130_driver = {
> +	.name                 = "cp2130",
> +	.probe                = cp2130_probe,
> +	.disconnect           = cp2130_disconnect,
> +	.suspend              = NULL,
> +	.resume               = NULL,
> +	.reset_resume         = NULL,
> +	.id_table             = cp2130_devices,
> +	.supports_autosuspend = 0,
> +};

Static variables are initialized to 0 by default, no need to explicitly
do that.

> +static int __init cp2130_init(void)
> +{

module_usb_driver().

> +static int cp2130_spi_setup(struct spi_device *spi)
> +{
> +	return 0;
> +}
> +
> +static void cp2130_spi_cleanup(struct spi_device *spi)
> +{
> +}

Omit empty functions.  If the framework won't let you omit empty
functions they probably can't safely be empty.

> +	ret = sprintf(out, "channel\tcs_mode\tirq_pin\tclock_phase\tpolarity"
> +		"\tcs_pin_mode\tclock_freq\tdelay_mask"
> +		"\tinter_byte_delay\tpre_delay\tpost_delay"
> +		"\tmod_alias\n");
> +	strcat(buf, out);
> +	mutex_lock(&chip->chn_config_lock);
> +	for (i = 0; i < CP2130_NUM_GPIOS; i++) {
> +		chn = &chip->chn_configs[i];
> +		ret += sprintf(out, "%d\t%d\t%d\t%d\t\t%d\t\t%d\t\t%s\t%d"
> +			"\t\t%d\t\t\t%d\t\t%d\t\t'%s'\n",
> +			i, chn->cs_en, chn->irq_pin, chn->clock_phase,
> +			chn->polarity, chn->cs_pin_mode,
> +			cp2130_spi_speed_to_string(chn->clock_freq),
> +			chn->delay_mask, chn->inter_byte_delay,
> +			chn->pre_deassert_delay, chn->post_assert_delay,
> +			chn->modalias);
> +		strcat(buf, out);
> +	}

This looks like a bunch of mostly very generic diagnostic data, if it's
useful to have it should be added in the framework so it's available for
all drivers.

> +static ssize_t channel_config_store(struct device *dev,
> +				struct device_attribute *attr,
> +				const char *buf, size_t count)
> +{

This is adding a completely non-standard ABI for configuring things -
why not use standard interfaces?

> +static DEVICE_ATTR_RW(channel_config);

Device attributes in sysfs should follow sysfs rules, including having
just a single value per file to ease machine parsing.

> +out:
> +	return (!ret ? len : ret);

Please write normal conditional statements to make things easier for
people reading the driver.

> +	mutex_lock(&dev->usb_bus_lock);

What is this protecting?

> +	/* iterate through all transfers */
> +	list_for_each_entry(xfer, &mesg->transfers, transfer_list) {
> +		dev_dbg(&master->dev, "spi transfer stats: %p, %p, %d",
> +			xfer->tx_buf, xfer->rx_buf, xfer->len);

It's not clear to me why the driver can't use transfer_one() instead of
transfer_one_message().
Jochen Henneberg May 9, 2019, 6:32 a.m. UTC | #2
Mark Brown <broonie@kernel.org> writes:
> On Mon, May 06, 2019 at 02:06:20PM +0200, Jochen Henneberg wrote:
>
> This driver has huge amounts of non-standard interfaces in it,
> especially the userpace ABI it adds.  It would be a lot easier to review
> if it were split up so that it's a series where the core SPI
> functionality is added initially and then other things were layered on
> top as additional patches.

Will do that once the open issues have been clarified (see comments below).

>
>> @@ -0,0 +1,1672 @@
>> +// SPDX-License-Identifier: GPL-2.0
>> +
>> +/* Kernel driver for Silicon Labs CP2130 USB-to-SPI bridge.
>> + *
>> + * Copyright (C) 2019 Jochen Henneberg (jh@henneberg-systemdesign.com)
>> + */
>
> Please keep the entire comment a C++ comment, it makes things look more
> intentional.

I fixed that.

>
>> +/* Prototypes */
>> +static int cp2130_probe(struct usb_interface *intf,
>> +			const struct usb_device_id *id);
>> +static void cp2130_disconnect(struct usb_interface *intf);
>
> These forward declarations are really weird in a Linux driver, normally
> things like probe() are defined at the bottom of the driver just before
> the driver structure which also usually goes at the end.
>
>> +/* USB device functions */
>> +static struct usb_driver cp2130_driver = {
>> +	.name                 = "cp2130",
>> +	.probe                = cp2130_probe,
>> +	.disconnect           = cp2130_disconnect,
>> +	.suspend              = NULL,
>> +	.resume               = NULL,
>> +	.reset_resume         = NULL,
>> +	.id_table             = cp2130_devices,
>> +	.supports_autosuspend = 0,
>> +};
>
> Static variables are initialized to 0 by default, no need to explicitly
> do that.

I fixed that.

>
>> +static int __init cp2130_init(void)
>> +{
>
> module_usb_driver().

Fixed that.

>
>> +static int cp2130_spi_setup(struct spi_device *spi)
>> +{
>> +	return 0;
>> +}
>> +
>> +static void cp2130_spi_cleanup(struct spi_device *spi)
>> +{
>> +}
>
> Omit empty functions.  If the framework won't let you omit empty
> functions they probably can't safely be empty.

I removed the empty functions. Most of the things that happen there need
information from platform data or DT which would not be available for
CP2130.

>
>> +	ret = sprintf(out, "channel\tcs_mode\tirq_pin\tclock_phase\tpolarity"
>> +		"\tcs_pin_mode\tclock_freq\tdelay_mask"
>> +		"\tinter_byte_delay\tpre_delay\tpost_delay"
>> +		"\tmod_alias\n");
>> +	strcat(buf, out);
>> +	mutex_lock(&chip->chn_config_lock);
>> +	for (i = 0; i < CP2130_NUM_GPIOS; i++) {
>> +		chn = &chip->chn_configs[i];
>> +		ret += sprintf(out, "%d\t%d\t%d\t%d\t\t%d\t\t%d\t\t%s\t%d"
>> +			"\t\t%d\t\t\t%d\t\t%d\t\t'%s'\n",
>> +			i, chn->cs_en, chn->irq_pin, chn->clock_phase,
>> +			chn->polarity, chn->cs_pin_mode,
>> +			cp2130_spi_speed_to_string(chn->clock_freq),
>> +			chn->delay_mask, chn->inter_byte_delay,
>> +			chn->pre_deassert_delay, chn->post_assert_delay,
>> +			chn->modalias);
>> +		strcat(buf, out);
>> +	}
>
> This looks like a bunch of mostly very generic diagnostic data, if it's
> useful to have it should be added in the framework so it's available for
> all drivers.

The information is quite specific for the CP2130 so I cannot see how
this could fit into the SPI framework.

>
>> +static ssize_t channel_config_store(struct device *dev,
>> +				struct device_attribute *attr,
>> +				const char *buf, size_t count)
>> +{
>
> This is adding a completely non-standard ABI for configuring things -
> why not use standard interfaces?
>
>> +static DEVICE_ATTR_RW(channel_config);

I think this needs some more explanation about the nature of the
chip. As it is a USB device and the SPI performance is not as good as it
would be with a SOC SPI port the chip is typically used for prototyping
or for bus testers, e. g. CAN or ARINC429 chips that need an SPI bus.

We could use the timing information that comes with each SPI transfer to
setup the transport parameters of the chip, however, there are several
settings that may be incomplete. E. g. the IRQ pin. If the SPI slave
chip IRQ is connected to one of the GPIOs of CP2130 nobody knows upfront
which IRQ to configure for the slave chip driver. Same issue applies for
the CS pin, there is pre-numbered GPIO available for CS before the
CP2130 is plugged so you cannot setup other driver in advance.

If the chip is permanently connected (e. g. in an embedded board, which
is unlikely because those often have host SPI ports anyway) we may have
an advantage from DT pre-configuration but I think this use-case is
quite unlikely and then there would still be the problem to know which
data is valid, the one that comes with the transfer message or the one
configured from sysfs.

I have added a mechanism to provide driver platform data for the slave
chip via sysfs which is a bit of a hack but some slave drivers need the
platform data to work without modifications.

What happens is that if the CP2130 is plugged you need to run some udev
actions to setup the communication parameters, the interrupt GPIOs, the
SPI slave chip platform data (maybe) and modalias along with some pin
configurations. If somebody builds a test adapter with the CP2130 it
should be shipped with udev rules and adapter configuration scripts.

The only reason why I put all this into the driver and not into some
userspace implementation (libusb) is that this drivers allows to use
and/or develop/debug SPI slave chip drivers, e. g. on a laptop where no
host SPI port is available.

>
> Device attributes in sysfs should follow sysfs rules, including having
> just a single value per file to ease machine parsing.
>
>> +out:
>> +	return (!ret ? len : ret);
>
> Please write normal conditional statements to make things easier for
> people reading the driver.

Fixed that.

>
>> +	mutex_lock(&dev->usb_bus_lock);
>
> What is this protecting?

To be perfectly honest this is just a precaution. Polling GPIO states or
writing the OTP ROM may interfere with SPI communication (I did not
really test that nor does the datasheet provide sufficient information
if that matters or not) so I wanted to be save that every action that
needs multiple USB transfers is protected against other multi USB
transfers. I will try if this really is an issue and remove the lock if
possible.

>
>> +	/* iterate through all transfers */
>> +	list_for_each_entry(xfer, &mesg->transfers, transfer_list) {
>> +		dev_dbg(&master->dev, "spi transfer stats: %p, %p, %d",
>> +			xfer->tx_buf, xfer->rx_buf, xfer->len);
>
> It's not clear to me why the driver can't use transfer_one() instead of
> transfer_one_message().

The documentation says that if both callbacks are provided the framework
will always use transfer_one_message() which I think is the superior
callback because we can keep the SPI configuration as it is if the same
channel is used with subsequent transfers (performance) which we cannot
do for transfer_one(), at least if the driver should remain stateless
for the transfers.

Regards
-Jochen
Mark Brown May 12, 2019, 8:26 a.m. UTC | #3
On Thu, May 09, 2019 at 08:32:20AM +0200, Jochen Henneberg wrote:
> Mark Brown <broonie@kernel.org> writes:
> > On Mon, May 06, 2019 at 02:06:20PM +0200, Jochen Henneberg wrote:

> >> +	mutex_lock(&chip->chn_config_lock);
> >> +	for (i = 0; i < CP2130_NUM_GPIOS; i++) {
> >> +		chn = &chip->chn_configs[i];
> >> +		ret += sprintf(out, "%d\t%d\t%d\t%d\t\t%d\t\t%d\t\t%s\t%d"
> >> +			"\t\t%d\t\t\t%d\t\t%d\t\t'%s'\n",
> >> +			i, chn->cs_en, chn->irq_pin, chn->clock_phase,
> >> +			chn->polarity, chn->cs_pin_mode,
> >> +			cp2130_spi_speed_to_string(chn->clock_freq),
> >> +			chn->delay_mask, chn->inter_byte_delay,
> >> +			chn->pre_deassert_delay, chn->post_assert_delay,
> >> +			chn->modalias);
> >> +		strcat(buf, out);
> >> +	}

> > This looks like a bunch of mostly very generic diagnostic data, if it's
> > useful to have it should be added in the framework so it's available for
> > all drivers.

> The information is quite specific for the CP2130 so I cannot see how
> this could fit into the SPI framework.

All those delays, polarities and speeds look very generic.

> We could use the timing information that comes with each SPI transfer to
> setup the transport parameters of the chip, however, there are several
> settings that may be incomplete. E. g. the IRQ pin. If the SPI slave
> chip IRQ is connected to one of the GPIOs of CP2130 nobody knows upfront
> which IRQ to configure for the slave chip driver. Same issue applies for
> the CS pin, there is pre-numbered GPIO available for CS before the
> CP2130 is plugged so you cannot setup other driver in advance.

This is the same problem as all the plugin modules for non-enumerable
buses like the Raspberry Pi have, they currently use things like DT or
ACPI overlays to enumerate - there are some efforts at improving things
as it's not ideal at the minute.  I'd expect you to be trying to use
similar interfaces to them rather than inventing something completely
driver specific, users shouldn't have to figure out some random driver
specific interface for this.

> If the chip is permanently connected (e. g. in an embedded board, which
> is unlikely because those often have host SPI ports anyway) we may have
> an advantage from DT pre-configuration but I think this use-case is
> quite unlikely and then there would still be the problem to know which
> data is valid, the one that comes with the transfer message or the one
> configured from sysfs.

This is one reason why you shouldn't have a random sysfs interface.

> >> +	/* iterate through all transfers */
> >> +	list_for_each_entry(xfer, &mesg->transfers, transfer_list) {
> >> +		dev_dbg(&master->dev, "spi transfer stats: %p, %p, %d",
> >> +			xfer->tx_buf, xfer->rx_buf, xfer->len);

> > It's not clear to me why the driver can't use transfer_one() instead of
> > transfer_one_message().

> The documentation says that if both callbacks are provided the framework
> will always use transfer_one_message() which I think is the superior
> callback because we can keep the SPI configuration as it is if the same

No, it's better to use transfer_one() if you can as it means there is
less open coding of standard features in the driver.

> channel is used with subsequent transfers (performance) which we cannot
> do for transfer_one(), at least if the driver should remain stateless
> for the transfers.

It's perfectly OK to cache the last settings that were sent to the
hardware and only reconfigure if there's a change, several drivers do
that already.
Jochen Henneberg May 12, 2019, 11:13 p.m. UTC | #4
Mark Brown <broonie@kernel.org> writes:

> On Thu, May 09, 2019 at 08:32:20AM +0200, Jochen Henneberg wrote:
>> Mark Brown <broonie@kernel.org> writes:
>> > On Mon, May 06, 2019 at 02:06:20PM +0200, Jochen Henneberg wrote:
>
>> >> +	mutex_lock(&chip->chn_config_lock);
>> >> +	for (i = 0; i < CP2130_NUM_GPIOS; i++) {
>> >> +		chn = &chip->chn_configs[i];
>> >> +		ret += sprintf(out, "%d\t%d\t%d\t%d\t\t%d\t\t%d\t\t%s\t%d"
>> >> +			"\t\t%d\t\t\t%d\t\t%d\t\t'%s'\n",
>> >> +			i, chn->cs_en, chn->irq_pin, chn->clock_phase,
>> >> +			chn->polarity, chn->cs_pin_mode,
>> >> +			cp2130_spi_speed_to_string(chn->clock_freq),
>> >> +			chn->delay_mask, chn->inter_byte_delay,
>> >> +			chn->pre_deassert_delay, chn->post_assert_delay,
>> >> +			chn->modalias);
>> >> +		strcat(buf, out);
>> >> +	}
>
>> > This looks like a bunch of mostly very generic diagnostic data, if it's
>> > useful to have it should be added in the framework so it's available for
>> > all drivers.
>
>> The information is quite specific for the CP2130 so I cannot see how
>> this could fit into the SPI framework.
>
> All those delays, polarities and speeds look very generic.
>
>> We could use the timing information that comes with each SPI transfer to
>> setup the transport parameters of the chip, however, there are several
>> settings that may be incomplete. E. g. the IRQ pin. If the SPI slave
>> chip IRQ is connected to one of the GPIOs of CP2130 nobody knows upfront
>> which IRQ to configure for the slave chip driver. Same issue applies for
>> the CS pin, there is pre-numbered GPIO available for CS before the
>> CP2130 is plugged so you cannot setup other driver in advance.
>
> This is the same problem as all the plugin modules for non-enumerable
> buses like the Raspberry Pi have, they currently use things like DT or
> ACPI overlays to enumerate - there are some efforts at improving things
> as it's not ideal at the minute.  I'd expect you to be trying to use
> similar interfaces to them rather than inventing something completely
> driver specific, users shouldn't have to figure out some random driver
> specific interface for this.

Sorry, I was not aware of the APIs. This would mean I have to re-write a
relevant part of the driver and I can remove all the modalias loading
code as well. I will do the changes and try to support both, DT and ACPI
overlays. Thanks for your advice.

>
>> If the chip is permanently connected (e. g. in an embedded board, which
>> is unlikely because those often have host SPI ports anyway) we may have
>> an advantage from DT pre-configuration but I think this use-case is
>> quite unlikely and then there would still be the problem to know which
>> data is valid, the one that comes with the transfer message or the one
>> configured from sysfs.
>
> This is one reason why you shouldn't have a random sysfs interface.
>
>> >> +	/* iterate through all transfers */
>> >> +	list_for_each_entry(xfer, &mesg->transfers, transfer_list) {
>> >> +		dev_dbg(&master->dev, "spi transfer stats: %p, %p, %d",
>> >> +			xfer->tx_buf, xfer->rx_buf, xfer->len);
>
>> > It's not clear to me why the driver can't use transfer_one() instead of
>> > transfer_one_message().
>
>> The documentation says that if both callbacks are provided the framework
>> will always use transfer_one_message() which I think is the superior
>> callback because we can keep the SPI configuration as it is if the same
>
> No, it's better to use transfer_one() if you can as it means there is
> less open coding of standard features in the driver.

Ok, understood. I will change this and keep the state of the last
transfer.

The changes, especially for DT and ACPI overlays will take me some time,
I will come back to you once this is done and well tested.

Regards
-Jochen

>
>> channel is used with subsequent transfers (performance) which we cannot
>> do for transfer_one(), at least if the driver should remain stateless
>> for the transfers.
>
> It's perfectly OK to cache the last settings that were sent to the
> hardware and only reconfigure if there's a change, several drivers do
> that already.

Patch
diff mbox series

diff --git a/drivers/spi/Kconfig b/drivers/spi/Kconfig
index f761655..a58684c 100644
--- a/drivers/spi/Kconfig
+++ b/drivers/spi/Kconfig
@@ -205,6 +205,12 @@  config SPI_COLDFIRE_QSPI
 	  This enables support for the Coldfire QSPI controller in master
 	  mode.
 
+config SPI_CP2130
+	tristate "CP2130 USB-SPI bridge"
+	depends on USB
+	help
+	  This enables support for Silicon Labs CP2130 USB-to-SPI bridge.
+
 config SPI_DAVINCI
 	tristate "Texas Instruments DaVinci/DA8x/OMAP-L/AM1x SoC SPI controller"
 	depends on ARCH_DAVINCI || ARCH_KEYSTONE
diff --git a/drivers/spi/Makefile b/drivers/spi/Makefile
index d8fc03c..7ec9204 100644
--- a/drivers/spi/Makefile
+++ b/drivers/spi/Makefile
@@ -31,6 +31,7 @@  obj-$(CONFIG_SPI_BUTTERFLY)		+= spi-butterfly.o
 obj-$(CONFIG_SPI_CADENCE)		+= spi-cadence.o
 obj-$(CONFIG_SPI_CLPS711X)		+= spi-clps711x.o
 obj-$(CONFIG_SPI_COLDFIRE_QSPI)		+= spi-coldfire-qspi.o
+obj-$(CONFIG_SPI_CP2130)		+= spi-cp2130.o
 obj-$(CONFIG_SPI_DAVINCI)		+= spi-davinci.o
 obj-$(CONFIG_SPI_DLN2)			+= spi-dln2.o
 obj-$(CONFIG_SPI_DESIGNWARE)		+= spi-dw.o
diff --git a/drivers/spi/spi-cp2130.c b/drivers/spi/spi-cp2130.c
new file mode 100644
index 0000000..99a0254
--- /dev/null
+++ b/drivers/spi/spi-cp2130.c
@@ -0,0 +1,1672 @@ 
+// SPDX-License-Identifier: GPL-2.0
+
+/* Kernel driver for Silicon Labs CP2130 USB-to-SPI bridge.
+ *
+ * Copyright (C) 2019 Jochen Henneberg (jh@henneberg-systemdesign.com)
+ */
+
+#include <linux/slab.h>
+#include <linux/module.h>
+#include <linux/usb.h>
+#include <linux/spi/spi.h>
+#include <linux/workqueue.h>
+#include <linux/string.h>
+#include <asm/byteorder.h>
+#include <linux/interrupt.h>
+#include <linux/irqdomain.h>
+#include <linux/irq.h>
+#include <linux/gpio.h>
+#include <linux/version.h>
+
+#define USB_DEVICE_ID_CP2130         0x87a0
+#define USB_VENDOR_ID_CYGNAL         0x10c4
+
+#define CP2130_NUM_GPIOS             11
+#define CP2130_IRQ_POLL_INTERVAL     (1 * 1000 * 1000) /* us */
+
+#define CP2130_MAX_USB_PACKET_SIZE   64
+
+#define CP2130_CMD_READ              0x00
+#define CP2130_CMD_WRITE             0x01
+#define CP2130_CMD_WRITEREAD         0x02
+#define CP2130_BULK_OFFSET_CMD       2
+#define CP2130_BULK_OFFSET_LENGTH    4
+#define CP2130_BULK_OFFSET_DATA      8
+#define CP2130_BULK_HEADER_SIZE      (CP2130_BULK_OFFSET_DATA)
+
+#define CP2130_BREQ_GET_GPIO_VALUES  0x20
+#define CP2130_BREQ_SET_GPIO_MODE    0x23
+#define CP2130_BREQ_GET_GPIO_CS      0x24
+#define CP2130_BREQ_SET_GPIO_CS      0x25
+#define CP2130_BREQ_GET_SPI_WORD     0x30
+#define CP2130_BREQ_SET_SPI_WORD     0x31
+#define CP2130_BREQ_GET_SPI_DELAY    0x32
+#define CP2130_BREQ_SET_SPI_DELAY    0x33
+#define CP2130_BREQ_GET_LOCK_BYTE    0x6E
+#define CP2130_BREQ_GET_PIN_CONFIG   0x6C
+#define CP2130_BREQ_SET_PIN_CONFIG   0x6D
+
+#define CP2130_BREQ_MEMORY_KEY       0xA5F1
+
+/* cp2130 attached chip */
+struct cp2130_channel {
+	int updated;
+	int cs_en; /* chip-select enable mode */
+	int irq_pin;
+	int clock_phase;
+	int polarity;
+	int cs_pin_mode;
+	int clock_freq; /* spi clock frequency */
+	int delay_mask; /* cs enable, pre-deassert,
+			 * post assert and inter-byte delay enable
+			 */
+	int inter_byte_delay;
+	int pre_deassert_delay;
+	int post_assert_delay;
+	char *modalias;
+	void *pdata;
+	struct spi_device *chip;
+};
+
+/* cp2130 OTP ROM */
+struct cp2130_otprom {
+	int lock_byte;
+	int pin_config[CP2130_NUM_GPIOS];
+	int suspend_level;
+	int suspend_mode;
+	int wakeup_mask;
+	int wakeup_match;
+	int divider;
+};
+
+struct cp2130_gpio_irq {
+	struct irq_domain *irq_domain;
+	struct mutex      irq_lock;
+	int               virq[CP2130_NUM_GPIOS];
+	u16               irq_mask;
+};
+
+/* cp2130 device structure */
+struct cp2130_device {
+	struct usb_device *udev;
+	struct usb_interface *intf;
+	struct spi_master *spi_master;
+
+	struct mutex chn_config_lock;
+	struct mutex otprom_lock;
+	struct mutex usb_bus_lock;
+
+	struct work_struct update_chn_config;
+	struct work_struct read_chn_config;
+	struct work_struct update_otprom;
+	struct work_struct read_otprom;
+	struct cp2130_channel chn_configs[CP2130_NUM_GPIOS];
+	struct cp2130_otprom otprom_config;
+
+	struct cp2130_gpio_irq irq_chip;
+	struct work_struct irq_work;
+
+	struct gpio_chip gpio_chip;
+	char *gpio_names[CP2130_NUM_GPIOS];
+	u8 gpio_states[2];
+
+	int current_channel;
+
+	int irq_poll_interval;
+
+	u8 *usb_xfer;
+};
+
+/* Prototypes */
+static int cp2130_probe(struct usb_interface *intf,
+			const struct usb_device_id *id);
+static void cp2130_disconnect(struct usb_interface *intf);
+
+static const struct usb_device_id cp2130_devices[] = {
+	{ USB_DEVICE(USB_VENDOR_ID_CYGNAL, USB_DEVICE_ID_CP2130) },
+	{ }
+};
+
+/* USB device functions */
+static struct usb_driver cp2130_driver = {
+	.name                 = "cp2130",
+	.probe                = cp2130_probe,
+	.disconnect           = cp2130_disconnect,
+	.suspend              = NULL,
+	.resume               = NULL,
+	.reset_resume         = NULL,
+	.id_table             = cp2130_devices,
+	.supports_autosuspend = 0,
+};
+
+static int __init cp2130_init(void)
+{
+	int ret;
+
+	pr_debug("%s", __func__);
+	ret = usb_register_driver(&cp2130_driver, THIS_MODULE, "cp2130");
+	if (ret)
+		pr_err("can't register cp2130 driver");
+
+	return ret;
+}
+
+static void __exit cp2130_exit(void)
+{
+	pr_debug("%s", __func__);
+	usb_deregister(&cp2130_driver);
+}
+
+static int cp2130_spi_setup(struct spi_device *spi)
+{
+	return 0;
+}
+
+static void cp2130_spi_cleanup(struct spi_device *spi)
+{
+}
+
+static char *cp2130_spi_speed_to_string(int reg_val)
+{
+	switch (reg_val) {
+	case 0: return "12 MHz   ";
+	case 1: return "6 MHz    ";
+	case 2: return "3 MHz    ";
+	case 3: return "1.5 MHz  ";
+	case 4: return "750 kHz  ";
+	case 5: return "375 kHz  ";
+	case 6: return "187.5 kHz";
+	case 7: return "93.8 kHz ";
+	}
+	return "";
+}
+
+static unsigned int cp2130_spi_speed_to_nsec(int reg_val)
+{
+#define USEC (1 * 1000 * 1000)
+#define FREQ_MHZ(n) (n * 1000)
+#define FREQ_KHZ(n) (n)
+
+	switch (reg_val) {
+	case 0: return USEC / FREQ_MHZ(12);
+	case 1: return USEC / FREQ_MHZ(6);
+	case 2: return USEC / FREQ_MHZ(3);
+	case 3: return USEC / FREQ_KHZ(1500);
+	case 4: return USEC / FREQ_KHZ(750);
+	case 5: return USEC / FREQ_KHZ(375);
+	case 6: return USEC / FREQ_KHZ(187);
+	case 7: return USEC / FREQ_KHZ(93);
+	}
+	return USEC / FREQ_KHZ(93);
+}
+
+static ssize_t channel_config_show(struct device *dev,
+				struct device_attribute *attr, char *buf)
+{
+	struct usb_interface *intf;
+	struct usb_device *udev;
+	struct cp2130_device *chip;
+	struct cp2130_channel *chn;
+	int i = 0;
+	char out[256];
+	ssize_t ret;
+
+	intf = to_usb_interface(dev);
+	if (!intf)
+		return -EFAULT;
+
+	chip = usb_get_intfdata(intf);
+	if (!chip)
+		return -EFAULT;
+
+	udev = usb_get_dev(interface_to_usbdev(intf));
+	if (!udev)
+		return -EFAULT;
+
+	ret = sprintf(out, "channel\tcs_mode\tirq_pin\tclock_phase\tpolarity"
+		"\tcs_pin_mode\tclock_freq\tdelay_mask"
+		"\tinter_byte_delay\tpre_delay\tpost_delay"
+		"\tmod_alias\n");
+	strcat(buf, out);
+	mutex_lock(&chip->chn_config_lock);
+	for (i = 0; i < CP2130_NUM_GPIOS; i++) {
+		chn = &chip->chn_configs[i];
+		ret += sprintf(out, "%d\t%d\t%d\t%d\t\t%d\t\t%d\t\t%s\t%d"
+			"\t\t%d\t\t\t%d\t\t%d\t\t'%s'\n",
+			i, chn->cs_en, chn->irq_pin, chn->clock_phase,
+			chn->polarity, chn->cs_pin_mode,
+			cp2130_spi_speed_to_string(chn->clock_freq),
+			chn->delay_mask, chn->inter_byte_delay,
+			chn->pre_deassert_delay, chn->post_assert_delay,
+			chn->modalias);
+		strcat(buf, out);
+	}
+	mutex_unlock(&chip->chn_config_lock);
+
+	return ret;
+}
+
+static ssize_t channel_config_store(struct device *dev,
+				struct device_attribute *attr,
+				const char *buf, size_t count)
+{
+	struct cp2130_channel chn;
+	struct usb_interface *intf;
+	struct usb_device *udev;
+	struct cp2130_device *chip;
+	char *lbuf;
+	char *del; /* delimiter ',' */
+	char *pos; /* current item position in buffer */
+	int i, ret, eos, chn_id;
+
+	intf = to_usb_interface(dev);
+	if (!intf)
+		return -EFAULT;
+
+	chip = usb_get_intfdata(intf);
+	if (!chip)
+		return -EFAULT;
+
+	udev = usb_get_dev(interface_to_usbdev(intf));
+	if (!udev)
+		return -EFAULT;
+
+	dev_dbg(&udev->dev, "received '%s' from 'channel_config'", buf);
+
+	if (!count)
+		return -EINVAL;
+
+	lbuf = kstrdup(buf, GFP_KERNEL);
+	if (!lbuf)
+		return -ENOMEM;
+
+	pos = lbuf;
+	eos = i = 0;
+	do {
+		/* search for next separator or end-of-string */
+		del = strchr(pos, ',');
+		if (!del)
+			del = strchr(pos, '\0');
+		if (!del) {
+			ret = -EINVAL;
+			goto out;
+		}
+
+		if (*del == '\0')
+			eos = 1;
+		*del = '\0';
+
+		dev_dbg(&udev->dev, "parsing: %s(%d)", pos, i);
+
+		/* parse current item */
+		switch (i) {
+		case 0: /* channel id */
+			ret = kstrtoint(pos, 10, &chn_id);
+			ret |= (chn_id < 0 || chn_id > 10) ? -EINVAL : 0;
+			if (ret)
+				goto out;
+			break;
+		case 1: /* chip-select enable */
+			ret = kstrtoint(pos, 10, &chn.cs_en);
+			ret |= (chn.cs_en < 0 || chn.cs_en > 2) ? -EINVAL : 0;
+			if (ret)
+				goto out;
+			break;
+		case 2: /* irq pin */
+			ret = kstrtoint(pos, 10, &chn.irq_pin);
+			ret |= (chn.irq_pin > 10) ? -EINVAL : 0;
+			if (ret)
+				goto out;
+			break;
+		case 3: /* clock phase */
+			ret = kstrtoint(pos, 10, &chn.clock_phase);
+			if (ret)
+				goto out;
+			chn.clock_phase = !!chn.clock_phase;
+			break;
+		case 4: /* polarity */
+			ret = kstrtoint(pos, 10, &chn.polarity);
+			if (ret)
+				goto out;
+			chn.polarity = !!chn.polarity;
+			break;
+		case 5: /* chip-select pin mode */
+			ret = kstrtoint(pos, 10, &chn.cs_pin_mode);
+			if (ret)
+				goto out;
+			chn.cs_pin_mode = !!chn.cs_pin_mode;
+			break;
+		case 6: /* spi clock frequency */
+			ret = kstrtoint(pos, 10, &chn.clock_freq);
+			ret |= (chn.clock_freq < 0 || chn.clock_freq > 7) ?
+				-EINVAL : 0;
+			if (ret)
+				goto out;
+			break;
+		case 7: /* delay mask */
+			ret = kstrtoint(pos, 10, &chn.delay_mask);
+			ret |= (chn.delay_mask < 0 || chn.delay_mask > 15) ?
+				-EINVAL : 0;
+			if (ret)
+				goto out;
+			break;
+		case 8: /* inter-byte delay */
+			ret = kstrtoint(pos, 10, &chn.inter_byte_delay);
+			ret |= (chn.inter_byte_delay < 0 ||
+				chn.inter_byte_delay > 0xffff) ?
+				-EINVAL : 0;
+			if (ret)
+				goto out;
+			break;
+		case 9: /* pre-deassert delay */
+			ret = kstrtoint(pos, 10, &chn.pre_deassert_delay);
+			ret |= (chn.pre_deassert_delay < 0 ||
+				chn.pre_deassert_delay > 0xffff) ?
+				-EINVAL : 0;
+			if (ret)
+				goto out;
+			break;
+		case 10: /* post-assert delay */
+			ret = kstrtoint(pos, 10, &chn.post_assert_delay);
+			ret |= (chn.post_assert_delay < 0 ||
+				chn.post_assert_delay > 0xffff) ?
+				-EINVAL : 0;
+			if (ret)
+				goto out;
+			break;
+		case 11: /* modalias */
+			chn.modalias = kstrdup(pos, GFP_KERNEL);
+			if (!chn.modalias) {
+				ret = -EINVAL;
+				goto out;
+			}
+			break;
+		default:
+			ret = -EINVAL;
+			goto out;
+		}
+
+		/* move the parser position forward */
+		pos = ++del;
+		i++;
+	} while (!eos);
+
+	if (i < 11) {
+		ret = -EINVAL;
+		goto out;
+	}
+
+	chn.updated = 1;
+	mutex_lock(&chip->chn_config_lock);
+	if (chip->chn_configs[chn_id].updated) {
+		ret = -EINVAL;
+		goto unlock;
+	}
+	/* preserve pdata */
+	chn.pdata = chip->chn_configs[chn_id].pdata;
+
+	memcpy(&chip->chn_configs[chn_id], &chn, sizeof(chn));
+	schedule_work(&chip->update_chn_config);
+	ret = count;
+
+unlock:
+	mutex_unlock(&chip->chn_config_lock);
+
+out:
+	kfree(lbuf);
+	return ret;
+}
+static DEVICE_ATTR_RW(channel_config);
+
+
+static ssize_t channel_pdata_store(struct device *dev,
+				struct device_attribute *attr,
+				const char *buf, size_t count)
+{
+	struct usb_interface *intf;
+	struct usb_device *udev;
+	struct cp2130_device *chip;
+	struct cp2130_channel *chn;
+	int chn_id;
+	int ret;
+
+	intf = to_usb_interface(dev);
+	if (!intf)
+		return -EFAULT;
+
+	chip = usb_get_intfdata(intf);
+	if (!chip)
+		return -EFAULT;
+
+	udev = usb_get_dev(interface_to_usbdev(intf));
+	if (!udev)
+		return -EFAULT;
+
+	/* first byte is channel id,
+	 * then follows binary platform data
+	 */
+	if (count < 2)
+		return -EINVAL;
+
+	chn_id = buf[0];
+	dev_dbg(&udev->dev, "received pdata for channel %u", chn_id);
+
+	if (chn_id < 0 || chn_id >= CP2130_NUM_GPIOS)
+		return -EINVAL;
+
+	chn = &chip->chn_configs[chn_id];
+
+	/* pdata can be set only once */
+	if (chn->pdata) {
+		ret = -EINVAL;
+		goto out;
+	}
+
+	dev_dbg(&udev->dev, "set pdata for channel %u", chn_id);
+
+	mutex_lock(&chip->chn_config_lock);
+	chn->pdata = kzalloc(count - 1, GFP_KERNEL);
+	if (!chn->pdata) {
+		ret = -ENOMEM;
+		goto out;
+	}
+
+	if (!memcpy(chn->pdata, buf + 1, count - 1)) {
+		kfree(chn->pdata);
+		chn->pdata = NULL;
+		ret = -EIO;
+		goto out;
+	}
+
+	ret = count;
+out:
+	mutex_unlock(&chip->chn_config_lock);
+	return ret;
+}
+static DEVICE_ATTR_WO(channel_pdata);
+
+static char *cp2130_pin_mode_to_string(int val)
+{
+	switch (val) {
+	case 0:  return "in        ";
+	case 1:  return "open-drain";
+	case 2:  return "push-pull ";
+	case 3:  return "nCS       ";
+	default: return "special   ";
+	}
+	return "";
+}
+
+static ssize_t otp_rom_show(struct device *dev,
+			struct device_attribute *attr, char *buf)
+{
+	struct usb_interface *intf;
+	struct usb_device *udev;
+	struct cp2130_device *chip;
+	int pin;
+	int i = 0;
+	char out[256];
+	ssize_t ret;
+
+	intf = to_usb_interface(dev);
+	if (!intf)
+		return -EFAULT;
+
+	chip = usb_get_intfdata(intf);
+	if (!chip)
+		return -EFAULT;
+
+	udev = usb_get_dev(interface_to_usbdev(intf));
+	if (!udev)
+		return -EFAULT;
+
+	mutex_lock(&chip->otprom_lock);
+
+	ret = sprintf(out, "OTP lock status (ro):"
+		"\nvid\t\tpid\t\tmax_power\tpower_mode"
+		"\tversion\t\tmanu_2\t\tmanu_1"
+		"\t\tpriority\tproduct_1\tproduct_2\tserial"
+		"\t\tpin_config\n");
+	strcat(buf, out);
+	for (i = 0; i < 12; i++) {
+		if (!!((chip->otprom_config.lock_byte >> i) & 1))
+			ret += sprintf(out, "unlocked\t");
+		else
+			ret += sprintf(out, "locked\t\t");
+		strcat(buf, out);
+	}
+	ret += sprintf(out, "\n\nOTP pin configuration (rw):\n");
+	strcat(buf, out);
+
+	for (i = 0; i < CP2130_NUM_GPIOS; i++) {
+		ret += sprintf(out, "pin %d\t\t", i);
+		strcat(buf, out);
+	}
+	strcat(buf, "\n");
+	ret++;
+	for (i = 0; i < CP2130_NUM_GPIOS; i++) {
+		pin = chip->otprom_config.pin_config[i];
+		ret += sprintf(out, "%s\t", cp2130_pin_mode_to_string(pin));
+		strcat(buf, out);
+	}
+
+	ret += sprintf(out, "\n\nOTP pin configuration extra data(ro):"
+		"\nsuspend_level\tsuspend_mode\twakeup_mask\twakeup_match"
+		"\tdivider\n");
+	strcat(buf, out);
+	ret += sprintf(out, "%d\t\t%d\t\t%d\t\t%d\t\t%d\n",
+		chip->otprom_config.suspend_level,
+		chip->otprom_config.suspend_mode,
+		chip->otprom_config.wakeup_mask,
+		chip->otprom_config.wakeup_match,
+		chip->otprom_config.divider);
+	strcat(buf, out);
+
+	mutex_unlock(&chip->otprom_lock);
+
+	return ret;
+}
+
+static ssize_t otp_rom_store(struct device *dev,
+			struct device_attribute *attr,
+			const char *buf, size_t count)
+{
+	int pin_config[CP2130_NUM_GPIOS];
+	struct usb_interface *intf;
+	struct usb_device *udev;
+	struct cp2130_device *chip;
+	char *lbuf;
+	char *del; /* delimiter ',' */
+	char *pos; /* current item position in buffer */
+	int i, ret, eos;
+
+	intf = to_usb_interface(dev);
+	if (!intf)
+		return -EFAULT;
+
+	chip = usb_get_intfdata(intf);
+	if (!chip)
+		return -EFAULT;
+
+	udev = usb_get_dev(interface_to_usbdev(intf));
+	if (!udev)
+		return -EFAULT;
+
+	dev_dbg(&udev->dev, "received '%s' from 'otp_rom'", buf);
+
+	if (!count)
+		return -EINVAL;
+
+	lbuf = kstrdup(buf, GFP_KERNEL);
+	if (!lbuf)
+		return -ENOMEM;
+
+	pos = lbuf;
+	eos = i = 0;
+	do {
+		/* search for next separator or end-of-string */
+		del = strchr(pos, ',');
+		if (!del)
+			del = strchr(pos, '\0');
+		if (!del) {
+			ret = -EINVAL;
+			goto out;
+		}
+
+		if (*del == '\0')
+			eos = 1;
+		*del = '\0';
+
+		dev_dbg(&udev->dev, "parsing: %s(%d)", pos, i);
+
+		if (i == CP2130_NUM_GPIOS) {
+			ret = -EINVAL;
+			goto out;
+		}
+
+		/* parse current item */
+		if (*pos == 'x') {
+			pin_config[i] = -1;
+		} else {
+			ret = kstrtoint(pos, 10, &(pin_config[i]));
+			ret |= (pin_config[i] < 0 ||
+				pin_config[i] > 3) ?
+				-EINVAL : 0;
+			if (ret)
+				goto out;
+		}
+
+		/* move the parser position forward */
+		pos = ++del;
+		i++;
+	} while (!eos);
+
+	if (i < 11) {
+		ret = -EINVAL;
+		goto out;
+	}
+
+	mutex_lock(&chip->otprom_lock);
+
+	for (i = 0; i < CP2130_NUM_GPIOS; i++) {
+		if (pin_config[i] < 0)
+			continue;
+		pr_info("cp2130 read OTP: %d", pin_config[i]);
+		chip->otprom_config.pin_config[i] = pin_config[i];
+	}
+	schedule_work(&chip->update_otprom);
+	ret = count;
+
+	mutex_unlock(&chip->otprom_lock);
+
+out:
+	kfree(lbuf);
+	return ret;
+}
+static DEVICE_ATTR_RW(otp_rom);
+
+static ssize_t irq_poll_interval_show(struct device *dev,
+				struct device_attribute *attr, char *buf)
+{
+	struct usb_interface *intf;
+	struct usb_device *udev;
+	struct cp2130_device *chip;
+	ssize_t ret;
+
+	intf = to_usb_interface(dev);
+	if (!intf)
+		return -EFAULT;
+
+	chip = usb_get_intfdata(intf);
+	if (!chip)
+		return -EFAULT;
+
+	udev = usb_get_dev(interface_to_usbdev(intf));
+	if (!udev)
+		return -EFAULT;
+
+	ret = sprintf(buf, "%d us\n", chip->irq_poll_interval);
+
+	return ret;
+}
+
+static ssize_t irq_poll_interval_store(struct device *dev,
+				struct device_attribute *attr,
+				const char *buf, size_t count)
+{
+	struct usb_interface *intf;
+	struct usb_device *udev;
+	struct cp2130_device *chip;
+	int ret, interval;
+
+	intf = to_usb_interface(dev);
+	if (!intf)
+		return -EFAULT;
+
+	chip = usb_get_intfdata(intf);
+	if (!chip)
+		return -EFAULT;
+
+	udev = usb_get_dev(interface_to_usbdev(intf));
+	if (!udev)
+		return -EFAULT;
+
+	ret = kstrtoint(buf, 10, &interval);
+
+	/* we only allow numbers and nothing below 10us */
+	if (ret || interval < 10)
+		return -EINVAL;
+
+	chip->irq_poll_interval = interval;
+
+	return count;
+}
+static DEVICE_ATTR_RW(irq_poll_interval);
+
+static int cp2130_transfer_bulk_message(struct spi_master *master,
+					struct spi_transfer *xfer,
+					unsigned int pos)
+{
+	int header_size, data_size, len, tx_len;
+	int ret = -EINVAL;
+	unsigned int limit;
+	unsigned int recv_pipe, xmit_pipe;
+	struct cp2130_device *dev =
+		(struct cp2130_device *) spi_master_get_devdata(master);
+	bool first_frame = !pos;
+
+	xmit_pipe = usb_sndbulkpipe(dev->udev, 0x01);
+	recv_pipe = usb_rcvbulkpipe(dev->udev, 0x82);
+
+	dev_dbg(&master->dev, "recv/xmit pipes: %u / %u",
+		recv_pipe, xmit_pipe);
+
+	/* if this is the first packet we need to prepare the header,
+	 * if not we can use all available space in the packet
+	 */
+	if (first_frame) {
+		/* zero the transfer buffer header, this implicitely
+		 * initializes the reserved fields
+		 */
+		memset(dev->usb_xfer, 0, CP2130_BULK_HEADER_SIZE);
+
+		/* init length field */
+		*((u32 *) (dev->usb_xfer + CP2130_BULK_OFFSET_LENGTH)) =
+			__cpu_to_le32(xfer->len);
+
+		header_size = CP2130_BULK_HEADER_SIZE;
+
+		/* now set the command type according to the SPI
+		 * message setup
+		 */
+		if (xfer->tx_buf && xfer->rx_buf) {
+			/* Simultaneous SPI write and read */
+			limit = CP2130_MAX_USB_PACKET_SIZE -
+				CP2130_BULK_HEADER_SIZE;
+			data_size = min(limit, xfer->len - pos);
+			tx_len = header_size + data_size;
+			dev->usb_xfer[CP2130_BULK_OFFSET_CMD] =
+				CP2130_CMD_WRITEREAD;
+		} else if (xfer->tx_buf) {
+			/* SPI write only */
+			limit = CP2130_MAX_USB_PACKET_SIZE -
+				CP2130_BULK_HEADER_SIZE;
+			data_size = min(limit, xfer->len - pos);
+			tx_len = header_size + data_size;
+			dev->usb_xfer[CP2130_BULK_OFFSET_CMD] =
+				CP2130_CMD_WRITE;
+		} else if (xfer->rx_buf) {
+			/* SPI read only */
+			limit = CP2130_MAX_USB_PACKET_SIZE;
+			data_size = min(limit, xfer->len - pos);
+			tx_len = CP2130_BULK_HEADER_SIZE;
+			dev->usb_xfer[CP2130_BULK_OFFSET_CMD] =
+				CP2130_CMD_READ;
+		}
+	} else {
+		header_size = 0;
+		limit = CP2130_MAX_USB_PACKET_SIZE;
+		data_size = min(limit, xfer->len - pos);
+		tx_len = header_size + data_size;
+	}
+
+	/* copy chunk of SPI tx data */
+	if (xfer->tx_buf)
+		memcpy(dev->usb_xfer + header_size, xfer->tx_buf + pos,
+			data_size);
+
+	/* prepare URB and submit sync CP2130 / AN792 p.7: 'any
+	 * previous data transfer command must complete before another
+	 * data transfer command is issued', so there is no advantage
+	 * from using the async USB API
+	 */
+	if (xfer->tx_buf && xfer->rx_buf) {
+		/* simultaneous SPI write and read */
+		ret = usb_bulk_msg(dev->udev, xmit_pipe, dev->usb_xfer,
+				tx_len, &len, 200);
+		dev_dbg(&master->dev,
+			"write-read - usb tx phase: ret=%d, wrote %d/%d",
+			ret, len, tx_len);
+		if (ret)
+			goto out;
+		ret = usb_bulk_msg(dev->udev, recv_pipe, xfer->rx_buf + pos,
+				data_size, &len, 200);
+		dev_dbg(&master->dev,
+			"write-read - usb rx phase: ret=%d, read %d/%d",
+			ret, len, data_size);
+	} else if (xfer->tx_buf) {
+		/* SPI write only */
+		ret = usb_bulk_msg(dev->udev, xmit_pipe, dev->usb_xfer,
+				tx_len, &len, 200);
+		dev_dbg(&master->dev,
+			"write - usb tx phase: ret=%d, wrote %d/%d",
+			ret, len, tx_len);
+		len -= header_size;
+	} else if (xfer->rx_buf) {
+		/* SPI read only */
+		if (first_frame) { /* write read request only for first frame */
+			ret = usb_bulk_msg(dev->udev, xmit_pipe, dev->usb_xfer,
+					tx_len, &len, 200);
+			dev_dbg(&master->dev,
+				"read - usb tx phase: ret=%d, wrote %d/%d",
+				ret, len, tx_len);
+			if (ret)
+				goto out;
+		}
+		ret = usb_bulk_msg(dev->udev, recv_pipe, xfer->rx_buf + pos,
+				data_size, &len, 200);
+		dev_dbg(&master->dev,
+			"read - usb rx phase: ret=%d, read %d/%d",
+			ret, len, data_size);
+	}
+
+out:
+	return (!ret ? len : ret);
+}
+
+static int cp2130_spi_transfer_one_message(struct spi_master *master,
+					struct spi_message *mesg)
+{
+	struct spi_transfer *xfer;
+	struct cp2130_device *dev =
+		(struct cp2130_device *) spi_master_get_devdata(master);
+	int ret = 0, chn_id;
+	struct cp2130_channel *chn;
+	unsigned int xmit_ctrl_pipe;
+	unsigned int pos, transfer_delay;
+
+	dev_dbg(&master->dev, "spi transfer one message");
+
+	/* search for spi setup of this device */
+	for (chn_id = 0; chn_id < CP2130_NUM_GPIOS; chn_id++) {
+		chn = &dev->chn_configs[chn_id];
+		if (chn->chip == mesg->spi)
+			break;
+	}
+
+	if (chn_id == CP2130_NUM_GPIOS) {
+		ret = -ENODEV;
+		goto out_notfound;
+	}
+
+	mutex_lock(&dev->usb_bus_lock);
+
+	xmit_ctrl_pipe = usb_sndctrlpipe(dev->udev, 0);
+	if (chn_id != dev->current_channel) {
+		dev_dbg(&dev->udev->dev, "load setup for channel %d", chn_id);
+		dev->usb_xfer[0] = chn_id;
+		dev->usb_xfer[1] = chn->cs_en;
+		ret = usb_control_msg(
+			dev->udev, xmit_ctrl_pipe,
+			CP2130_BREQ_SET_GPIO_CS,
+			USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_DIR_OUT,
+			0, 0,
+			dev->usb_xfer, 2, 200);
+		if (ret != 2)
+			goto out;
+
+		dev->current_channel = chn_id;
+	}
+
+	/* iterate through all transfers */
+	list_for_each_entry(xfer, &mesg->transfers, transfer_list) {
+		dev_dbg(&master->dev, "spi transfer stats: %p, %p, %d",
+			xfer->tx_buf, xfer->rx_buf, xfer->len);
+
+		/* empty transfer */
+		if (!xfer->tx_buf && !xfer->rx_buf) {
+			udelay(xfer->delay_usecs);
+			continue;
+		}
+
+		/* be prepared for messages > 64 byte */
+		pos = 0;
+
+		/* split the xfer data into chunks for 64 bytes and submit */
+		while (pos < xfer->len) {
+			ret = cp2130_transfer_bulk_message(master, xfer, pos);
+
+			/* wait until all bytes are sent on the SPI bus */
+			transfer_delay = cp2130_spi_speed_to_nsec(
+				chn->clock_freq) /* nsec per bit */
+				* ret * 8 /* number of bits */
+				* 2 /* headroom */
+				/ 1000 /* to usec */;
+			udelay(transfer_delay);
+
+			if (ret < 0)
+				break;
+			pos += ret;
+		}
+		udelay(xfer->delay_usecs);
+		mesg->actual_length += xfer->len;
+	}
+
+out:
+	mutex_unlock(&dev->usb_bus_lock);
+out_notfound:
+	mesg->status = (ret < 0) ? ret : 0;
+	if (mesg->status)
+		dev_err(&master->dev, "USB transfer failed with %d", ret);
+	spi_finalize_current_message(master); /* signal done to queue */
+	return mesg->status;
+}
+
+static int cp2130_irq_from_pin(struct cp2130_device *dev, int pin)
+{
+	return dev->irq_chip.virq[pin];
+}
+
+static void cp2130_update_channel_config(struct work_struct *work)
+{
+	int i;
+	int ret;
+	unsigned int xmit_pipe;
+	struct cp2130_device *dev = container_of(work,
+						struct cp2130_device,
+						update_chn_config);
+	struct cp2130_channel *chn;
+
+	dev_dbg(&dev->udev->dev, "control pipes: %u, %u",
+		usb_sndctrlpipe(dev->udev, 0),
+		usb_rcvctrlpipe(dev->udev, 0));
+
+	xmit_pipe = usb_sndctrlpipe(dev->udev, 0);
+
+	mutex_lock(&dev->chn_config_lock);
+	for (i = 0; i < CP2130_NUM_GPIOS; i++) {
+		chn = &dev->chn_configs[i];
+		if (!chn->updated)
+			continue;
+
+		if (chn->updated > 1)
+			continue;
+
+		chn->updated++;
+
+		dev_dbg(&dev->udev->dev, "update config of channel %d", i);
+		dev->usb_xfer[0] = i;
+
+		mutex_lock(&dev->usb_bus_lock);
+
+		dev_dbg(&dev->udev->dev, "set spi word");
+		dev->usb_xfer[1] = (chn->clock_freq  << 0) |
+			(chn->cs_pin_mode << 3) |
+			(chn->polarity    << 4) |
+			(chn->clock_phase << 5);
+
+		ret = usb_control_msg(
+			dev->udev, xmit_pipe,
+			CP2130_BREQ_SET_SPI_WORD,
+			USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_DIR_OUT,
+			0, 0,
+			dev->usb_xfer, 2, 200);
+		if (ret < 2)
+			goto error_unlock;
+
+		dev_dbg(&dev->udev->dev, "set spi delay");
+		dev->usb_xfer[1] = chn->delay_mask;
+
+		dev->usb_xfer[2] = (chn->inter_byte_delay & 0xff00) >> 8;
+		dev->usb_xfer[3] = (chn->inter_byte_delay & 0x00ff) >> 0;
+
+		dev->usb_xfer[4] = (chn->post_assert_delay & 0xff00) >> 8;
+		dev->usb_xfer[5] = (chn->post_assert_delay & 0x00ff) >> 0;
+
+		dev->usb_xfer[6] = (chn->pre_deassert_delay & 0xff00) >> 8;
+		dev->usb_xfer[7] = (chn->pre_deassert_delay & 0x00ff) >> 0;
+		ret = usb_control_msg(
+			dev->udev, xmit_pipe,
+			CP2130_BREQ_SET_SPI_DELAY,
+			USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_DIR_OUT,
+			0, 0,
+			dev->usb_xfer, 8, 200);
+		if (ret < 8)
+			goto error_unlock;
+
+		/* configure irq pin as input if required */
+		if (chn->irq_pin >= 0) {
+			dev->usb_xfer[0] = chn->irq_pin;
+			dev->usb_xfer[1] = 0; /* input */
+			dev->usb_xfer[2] = 0; /* value, ignored for input */
+			ret = usb_control_msg(
+				dev->udev, xmit_pipe,
+				CP2130_BREQ_SET_GPIO_MODE,
+				USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_DIR_OUT,
+				0, 0,
+				dev->usb_xfer, 3, 200);
+			if (ret < 3)
+				goto error_unlock;
+		}
+
+		mutex_unlock(&dev->usb_bus_lock);
+
+		dev_dbg(&dev->udev->dev, "try register %s", chn->modalias);
+
+		chn->chip = spi_alloc_device(dev->spi_master);
+		if (!chn->chip)
+			goto error;
+
+		chn->chip->max_speed_hz = dev->spi_master->max_speed_hz;
+		chn->chip->chip_select = i;
+		chn->chip->mode = (chn->polarity << 1) | chn->clock_phase;
+		chn->chip->bits_per_word = 8; /* we can only do this */
+		chn->chip->irq = cp2130_irq_from_pin(dev, chn->irq_pin);
+
+		chn->chip->dev.platform_data = chn->pdata;
+
+		dev_dbg(&dev->udev->dev, "initialized %s chip", chn->modalias);
+		strncpy(chn->chip->modalias, chn->modalias,
+			sizeof(chn->chip->modalias));
+
+		ret = spi_add_device(chn->chip);
+		if (ret)
+			goto error;
+
+		dev_dbg(&dev->udev->dev, "%s probe complete", chn->modalias);
+	}
+	mutex_unlock(&dev->chn_config_lock);
+
+	schedule_work(&dev->read_chn_config);
+	return;
+
+error_unlock:
+	mutex_unlock(&dev->usb_bus_lock);
+
+error:
+	mutex_unlock(&dev->chn_config_lock);
+	dev_err(&dev->udev->dev, "failed to configure channel %d", i);
+}
+
+static void cp2130_read_channel_config(struct work_struct *work)
+{
+	int i;
+	int ret;
+	unsigned int recv_pipe;
+	struct cp2130_device *dev = container_of(work,
+						struct cp2130_device,
+						read_chn_config);
+	struct cp2130_channel *chn;
+
+	dev_dbg(&dev->udev->dev, "control pipes: %u, %u",
+		usb_sndctrlpipe(dev->udev, 0),
+		usb_rcvctrlpipe(dev->udev, 0));
+
+	recv_pipe = usb_rcvctrlpipe(dev->udev, 0);
+
+	mutex_lock(&dev->chn_config_lock);
+	mutex_lock(&dev->usb_bus_lock);
+	dev_dbg(&dev->udev->dev, "read channel configs");
+
+	dev_dbg(&dev->udev->dev, "get spi word");
+	ret = usb_control_msg(
+		dev->udev, recv_pipe,
+		CP2130_BREQ_GET_SPI_WORD,
+		USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_DIR_IN,
+		0, 0,
+		dev->usb_xfer, 11, 200);
+
+	for (i = 0; i < CP2130_NUM_GPIOS; i++) {
+		chn = &dev->chn_configs[i];
+
+		chn->clock_freq = dev->usb_xfer[i] & 7;
+		chn->cs_pin_mode = !!(dev->usb_xfer[i] & 8);
+		chn->polarity = !!(dev->usb_xfer[i] & 16);
+		chn->clock_phase = !!(dev->usb_xfer[i] & 32);
+	}
+
+	dev_dbg(&dev->udev->dev, "get delays");
+	for (i = 0; i < CP2130_NUM_GPIOS; i++) {
+		ret = usb_control_msg(
+			dev->udev, recv_pipe,
+			CP2130_BREQ_GET_SPI_DELAY,
+			USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_DIR_IN,
+			0, i,
+			dev->usb_xfer, 8, 200);
+
+		chn = &dev->chn_configs[i];
+
+		chn->delay_mask = dev->usb_xfer[1];
+		chn->inter_byte_delay = dev->usb_xfer[2] << 8;
+		chn->inter_byte_delay |= dev->usb_xfer[3] & 0xff;
+		chn->post_assert_delay = dev->usb_xfer[4] << 8;
+		chn->post_assert_delay |= dev->usb_xfer[5] & 0xff;
+		chn->pre_deassert_delay = dev->usb_xfer[6] << 8;
+		chn->pre_deassert_delay |= dev->usb_xfer[7] & 0xff;
+	}
+
+	mutex_unlock(&dev->usb_bus_lock);
+	mutex_unlock(&dev->chn_config_lock);
+}
+
+static void cp2130_update_otprom(struct work_struct *work)
+{
+	int i;
+	struct cp2130_device *dev = container_of(work,
+						struct cp2130_device,
+						update_otprom);
+	int ret;
+	unsigned int xmit_pipe;
+
+	xmit_pipe = usb_sndctrlpipe(dev->udev, 0);
+
+	mutex_lock(&dev->otprom_lock);
+
+	for (i = 0; i < CP2130_NUM_GPIOS; i++)
+		dev->usb_xfer[i] = dev->otprom_config.pin_config[i];
+
+	mutex_lock(&dev->usb_bus_lock);
+
+	ret = usb_control_msg(
+		dev->udev, xmit_pipe,
+		CP2130_BREQ_SET_PIN_CONFIG,
+		USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_DIR_OUT,
+		CP2130_BREQ_MEMORY_KEY, 0,
+		dev->usb_xfer, 20, 200);
+
+	if (ret)
+		dev_err(&dev->udev->dev, "error writing OTP ROM pin config");
+
+	mutex_unlock(&dev->usb_bus_lock);
+	mutex_unlock(&dev->otprom_lock);
+
+	schedule_work(&dev->read_otprom);
+}
+
+static void cp2130_read_otprom(struct work_struct *work)
+{
+	int i;
+	int ret;
+	unsigned int recv_pipe;
+	struct cp2130_device *dev = container_of(work,
+						struct cp2130_device,
+						read_otprom);
+
+	dev_dbg(&dev->udev->dev, "control pipes: %u, %u",
+		usb_sndctrlpipe(dev->udev, 0),
+		usb_rcvctrlpipe(dev->udev, 0));
+
+	recv_pipe = usb_rcvctrlpipe(dev->udev, 0);
+
+	mutex_lock(&dev->otprom_lock);
+	mutex_lock(&dev->usb_bus_lock);
+	dev_dbg(&dev->udev->dev, "read OTP ROM");
+
+	dev_dbg(&dev->udev->dev, "get lock byte");
+	ret = usb_control_msg(
+		dev->udev, recv_pipe,
+		CP2130_BREQ_GET_LOCK_BYTE,
+		USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_DIR_IN,
+		0, 0,
+		dev->usb_xfer, 2, 200);
+	dev_info(&dev->udev->dev, "lock byte %02X %02X",
+		dev->usb_xfer[0] & 0xff, dev->usb_xfer[1] & 0x0f);
+	dev->otprom_config.lock_byte = dev->usb_xfer[0] & 0xff;
+	dev->otprom_config.lock_byte |= (dev->usb_xfer[1] & 0x0f) << 8;
+
+	dev_dbg(&dev->udev->dev, "get pin config");
+	ret = usb_control_msg(
+		dev->udev, recv_pipe,
+		CP2130_BREQ_GET_PIN_CONFIG,
+		USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_DIR_IN,
+		0, 0,
+		dev->usb_xfer, 20, 200);
+
+	for (i = 0; i < CP2130_NUM_GPIOS; ++i) {
+		dev_info(&dev->udev->dev, "pin %d: %02X",
+			i, dev->usb_xfer[i]);
+		dev->otprom_config.pin_config[i] = dev->usb_xfer[i];
+	}
+	dev->otprom_config.suspend_level =
+		(dev->usb_xfer[i] << 8) | dev->usb_xfer[i + 1];
+	i += 2;
+	dev->otprom_config.suspend_mode =
+		(dev->usb_xfer[i] << 8) | dev->usb_xfer[i + 1];
+	i += 2;
+	dev->otprom_config.wakeup_mask =
+		(dev->usb_xfer[i] << 8) | dev->usb_xfer[i + 1];
+	i += 2;
+	dev->otprom_config.wakeup_match =
+		(dev->usb_xfer[i] << 8) | dev->usb_xfer[i + 1];
+	i += 2;
+	dev->otprom_config.divider = dev->usb_xfer[i];
+
+	mutex_unlock(&dev->usb_bus_lock);
+	mutex_unlock(&dev->otprom_lock);
+}
+
+static void cp2130_read_gpios(struct work_struct *work)
+{
+	unsigned int recv_pipe;
+	struct cp2130_device *dev = container_of(work,
+						struct cp2130_device,
+						irq_work);
+	int i, set;
+	unsigned long flags;
+
+loop:
+	recv_pipe = usb_rcvctrlpipe(dev->udev, 0);
+
+	dev_dbg(&dev->udev->dev, "start read gpios");
+
+	mutex_lock(&dev->usb_bus_lock);
+
+	i = usb_control_msg(
+		dev->udev, recv_pipe,
+		CP2130_BREQ_GET_GPIO_VALUES,
+		USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_DIR_IN,
+		0, 0,
+		dev->usb_xfer, 2, 200);
+
+	dev->gpio_states[0] = dev->usb_xfer[0];
+	dev->gpio_states[1] = dev->usb_xfer[1];
+
+	mutex_unlock(&dev->usb_bus_lock);
+
+	if (i < 2) {
+		dev_err(&dev->udev->dev, "failed to read gpios");
+		goto next;
+	}
+
+	dev_dbg(&dev->udev->dev, "read gpios 1: %02X",
+		dev->gpio_states[1] & 0xff & ~(1 | 2 | 4));
+	dev_dbg(&dev->udev->dev, "read gpios 2: %02X",
+		dev->gpio_states[0] & 0xff & ~(2 | 128));
+
+	for (i = 0; i < CP2130_NUM_GPIOS; ++i) {
+		if (!(dev->irq_chip.irq_mask & (1 << i)))
+			continue;
+
+		switch (i) {
+		case 0:
+		case 1:
+		case 2:
+		case 3:
+		case 4:
+			set = dev->gpio_states[1] & (8 << i);
+			break;
+		case 5:
+			set = dev->gpio_states[0] & (1 << 0);
+			break;
+		case 6:
+		case 7:
+		case 8:
+		case 9:
+		case 10:
+			set = dev->gpio_states[0] & (4 << (i - 6));
+			break;
+		}
+
+		if (!set) {
+			dev_dbg(&dev->udev->dev, "issue irq on %d", i);
+			local_irq_save(flags);
+			generic_handle_irq(dev->irq_chip.virq[i]);
+			local_irq_restore(flags);
+		}
+	}
+
+next:
+	if (dev->irq_poll_interval < 0)
+		return;
+
+	usleep_range(dev->irq_poll_interval, dev->irq_poll_interval + 20);
+	goto loop;
+}
+
+static void cp2130_gpio_irq_mask(struct irq_data *data)
+{
+	struct cp2130_gpio_irq *irq_dev = irq_data_get_irq_chip_data(data);
+	struct cp2130_device *dev
+		= container_of(irq_dev, struct cp2130_device, irq_chip);
+
+	dev_dbg(&dev->udev->dev, "irq mask %lu", data->hwirq);
+	irq_dev->irq_mask &= ~(1 << data->hwirq);
+}
+
+static void cp2130_gpio_irq_unmask(struct irq_data *data)
+{
+	struct cp2130_gpio_irq *irq_dev = irq_data_get_irq_chip_data(data);
+	struct cp2130_device *dev
+		= container_of(irq_dev, struct cp2130_device, irq_chip);
+
+	dev_dbg(&dev->udev->dev, "irq unmask %lu", data->hwirq);
+	irq_dev->irq_mask |= 1 << data->hwirq;
+}
+
+static int cp2130_gpio_irq_set_type(struct irq_data *data, unsigned int type)
+{
+	return 0;
+}
+
+static void cp2130_gpio_irq_bus_lock(struct irq_data *data)
+{
+	struct cp2130_gpio_irq *irq_dev = irq_data_get_irq_chip_data(data);
+
+	mutex_lock(&irq_dev->irq_lock);
+}
+
+static void cp2130_gpio_irq_bus_unlock(struct irq_data *data)
+{
+	struct cp2130_gpio_irq *irq_dev = irq_data_get_irq_chip_data(data);
+
+	mutex_unlock(&irq_dev->irq_lock);
+}
+
+static struct irq_chip cp2130_gpio_irq_chip = {
+	.name                = "gpio-cp2130",
+	.irq_mask            = cp2130_gpio_irq_mask,
+	.irq_unmask          = cp2130_gpio_irq_unmask,
+	.irq_set_type        = cp2130_gpio_irq_set_type,
+	.irq_bus_lock        = cp2130_gpio_irq_bus_lock,
+	.irq_bus_sync_unlock = cp2130_gpio_irq_bus_unlock,
+};
+
+static int cp2130_gpio_irq_map(struct irq_domain *domain, unsigned int irq,
+			irq_hw_number_t hwirq)
+{
+	irq_set_chip_data(irq, domain->host_data);
+	irq_set_chip(irq, &cp2130_gpio_irq_chip);
+	irq_set_chip_and_handler(irq, &cp2130_gpio_irq_chip, handle_simple_irq);
+	irq_set_noprobe(irq);
+	return 0;
+}
+
+static const struct irq_domain_ops cp2130_gpio_irq_domain_ops = {
+	.map = cp2130_gpio_irq_map,
+};
+
+static void cp2130_gpio_irq_remove(struct cp2130_device *dev);
+
+static int cp2130_gpio_irq_probe(struct cp2130_device *dev)
+{
+	struct cp2130_gpio_irq *irq_dev = &dev->irq_chip;
+	int i;
+
+	mutex_init(&irq_dev->irq_lock);
+
+	irq_dev->irq_domain = irq_domain_add_linear(
+		dev->udev->dev.of_node, CP2130_NUM_GPIOS,
+		&cp2130_gpio_irq_domain_ops, irq_dev);
+
+	if (!irq_dev->irq_domain) {
+		dev_err(&dev->udev->dev, "failed to register IRQ domain");
+		return -ENOMEM;
+	}
+
+	irq_dev->irq_mask = 0;
+	for (i = 0; i < CP2130_NUM_GPIOS; ++i) {
+		irq_dev->virq[i] =
+			irq_create_mapping(irq_dev->irq_domain, i);
+		dev_dbg(&dev->udev->dev, "created virtual irq %d",
+			irq_dev->virq[i]);
+	}
+
+	INIT_WORK(&dev->irq_work, cp2130_read_gpios);
+	schedule_work(&dev->irq_work);
+
+	return 0;
+}
+
+static void cp2130_gpio_irq_remove(struct cp2130_device *dev)
+{
+	int i = 0;
+
+	if (!dev->irq_chip.irq_domain)
+		return;
+
+	for (i = 0; i < CP2130_NUM_GPIOS; ++i)
+		irq_dispose_mapping(dev->irq_chip.virq[i]);
+
+	irq_domain_remove(dev->irq_chip.irq_domain);
+}
+
+static int cp2130_gpio_direction_input(struct gpio_chip *gc, unsigned int off)
+{
+	struct cp2130_device *dev = gpiochip_get_data(gc);
+	int ret;
+	unsigned int xmit_pipe;
+
+	xmit_pipe = usb_sndctrlpipe(dev->udev, 0);
+
+	mutex_lock(&dev->usb_bus_lock);
+
+	dev->usb_xfer[0] = off;
+	dev->usb_xfer[1] = 0; /* input */
+	dev->usb_xfer[2] = 0; /* value, ignored for input */
+	ret = usb_control_msg(
+		dev->udev, xmit_pipe,
+		CP2130_BREQ_SET_GPIO_MODE,
+		USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_DIR_OUT,
+		0, 0,
+		dev->usb_xfer, 3, 200);
+
+	mutex_unlock(&dev->usb_bus_lock);
+
+	return (ret == 3) ? 0 : -EIO;
+}
+
+static int cp2130_gpio_direction_output(struct gpio_chip *gc, unsigned int off,
+					int val)
+{
+	struct cp2130_device *dev = gpiochip_get_data(gc);
+	int ret;
+	unsigned int xmit_pipe;
+
+	xmit_pipe = usb_sndctrlpipe(dev->udev, 0);
+
+	mutex_lock(&dev->usb_bus_lock);
+
+	dev->usb_xfer[0] = off;
+	dev->usb_xfer[1] = 2; /* push-pull output */
+	dev->usb_xfer[2] = val; /* state */
+	ret = usb_control_msg(
+		dev->udev, xmit_pipe,
+		CP2130_BREQ_SET_GPIO_MODE,
+		USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_DIR_OUT,
+		0, 0,
+		dev->usb_xfer, 3, 200);
+
+	mutex_unlock(&dev->usb_bus_lock);
+
+	return (ret == 3) ? 0 : -EIO;
+}
+
+static int cp2130_gpio_get_value(struct gpio_chip *gc, unsigned int off)
+{
+	struct cp2130_device *dev = gpiochip_get_data(gc);
+	int set;
+
+	switch (off) {
+	case 0:
+	case 1:
+	case 2:
+	case 3:
+	case 4:
+		set = !!(dev->gpio_states[1] & (8 << off));
+		break;
+	case 5:
+		set = !!(dev->gpio_states[0] & (1 << 0));
+		break;
+	case 6:
+	case 7:
+	case 8:
+	case 9:
+	case 10:
+		set = !!(dev->gpio_states[0] & (4 << (off - 6)));
+		break;
+	default:
+		set = -EINVAL;
+		break;
+	}
+	return set;
+}
+
+static void cp2130_gpio_set_value(struct gpio_chip *gc, unsigned int off,
+				int val)
+{
+	cp2130_gpio_direction_output(gc, off, val);
+}
+
+static const char *const cp2130_gpio_names[] = {
+	"........-_cs0",
+	"........-_cs1",
+	"........-_cs2",
+	"........-_rtr",
+	"........-event_counter",
+	"........-clk_out",
+	"........-gpi",
+	"........-gpo",
+	"........-activity",
+	"........-suspend",
+	"........-_suspend",
+};
+
+static struct attribute *cp2130_sysfs_attributes[] = {
+	&dev_attr_channel_config.attr,
+	&dev_attr_channel_pdata.attr,
+	&dev_attr_otp_rom.attr,
+	&dev_attr_irq_poll_interval.attr,
+	NULL
+};
+
+static const struct attribute_group cp2130_sysfs_atributes_group = {
+	.name = "cp2130",
+	.attrs = cp2130_sysfs_attributes,
+};
+
+static int cp2130_probe(struct usb_interface *intf,
+			const struct usb_device_id *id)
+{
+	struct usb_device *udev = usb_get_dev(interface_to_usbdev(intf));
+	struct cp2130_device *dev;
+	struct spi_master *spi_master = NULL;
+	struct gpio_chip *gc;
+	struct usb_host_interface *iface_desc = intf->cur_altsetting;
+	struct usb_endpoint_descriptor *endpoint;
+	int i, ret;
+
+	ret = -ENOMEM;
+
+	pr_debug("%s", __func__);
+
+	if (!udev)
+		return -ENODEV;
+
+	dev = devm_kzalloc(&udev->dev, sizeof(struct cp2130_device),
+			GFP_KERNEL);
+	if (!dev)
+		return -ENOMEM;
+
+	dev->usb_xfer = devm_kmalloc(&udev->dev, CP2130_MAX_USB_PACKET_SIZE,
+				GFP_KERNEL);
+	if (!dev->usb_xfer)
+		return -ENOMEM;
+
+	dev->udev = udev;
+	dev->intf = intf;
+
+	mutex_init(&dev->usb_bus_lock);
+	mutex_init(&dev->chn_config_lock);
+	mutex_init(&dev->otprom_lock);
+
+	dev->current_channel = -1;
+
+	usb_set_intfdata(intf, dev);
+
+	for (i = 0; i < iface_desc->desc.bNumEndpoints; ++i) {
+		endpoint = &iface_desc->endpoint[i].desc;
+		dev_dbg(&udev->dev, "ep addr: 0x%02x",
+			endpoint->bEndpointAddress);
+		if (usb_endpoint_is_bulk_in(endpoint))
+			dev_dbg(&udev->dev, "bulk in ep addr: 0x%02x",
+				endpoint->bEndpointAddress);
+		if (usb_endpoint_is_bulk_out(endpoint))
+			dev_dbg(&udev->dev, "bulk out ep addr: 0x%02x",
+				endpoint->bEndpointAddress);
+	}
+
+	spi_master = spi_alloc_master(&udev->dev, sizeof(void *));
+	if (!spi_master)
+		return -ENOMEM;
+
+	spi_master_set_devdata(spi_master, (void *) dev);
+
+	spi_master->min_speed_hz = 93 * 1000 + 800; /* 93.8kHz */
+	spi_master->max_speed_hz = 12 * 1000 * 1000; /* 12 MHz */
+
+	spi_master->bus_num = -1; /* dynamically assigned */
+	spi_master->num_chipselect = CP2130_NUM_GPIOS;
+	spi_master->mode_bits =
+		SPI_MODE_0 | SPI_MODE_1 | SPI_MODE_2 | SPI_MODE_3;
+
+	spi_master->flags = 0;
+	spi_master->setup = cp2130_spi_setup;
+	spi_master->cleanup = cp2130_spi_cleanup;
+	spi_master->transfer_one_message = cp2130_spi_transfer_one_message;
+
+	ret = devm_spi_register_master(&udev->dev, spi_master);
+
+	if (ret) {
+		dev_err(&udev->dev, "failed to register SPI master");
+		return -ENOMEM;
+	}
+
+	dev->spi_master = spi_master;
+	dev_dbg(&udev->dev, "registered SPI master");
+
+	/* now enable the gpio chip */
+	gc = &dev->gpio_chip;
+	gc->direction_input = cp2130_gpio_direction_input;
+	gc->direction_output = cp2130_gpio_direction_output;
+	gc->get = cp2130_gpio_get_value;
+	gc->set = cp2130_gpio_set_value;
+	gc->can_sleep = true;
+
+	gc->base = -1; /* auto */
+	gc->ngpio = CP2130_NUM_GPIOS;
+	for (i = 0; i < CP2130_NUM_GPIOS; i++) {
+		dev->gpio_names[i] = kstrdup(cp2130_gpio_names[i], GFP_KERNEL);
+		memcpy(dev->gpio_names[i],
+			dev_name(&spi_master->dev), 3 + 5 /* spixxxxx */);
+	}
+	gc->names = (const char **) dev->gpio_names;
+	gc->label = dev_name(&spi_master->dev);
+	gc->owner = THIS_MODULE;
+
+	ret = devm_gpiochip_add_data(&udev->dev, &dev->gpio_chip, dev);
+	if (ret)
+		dev_err(&udev->dev, "failed to register gpio chip");
+
+	/* start irq polling */
+	dev->irq_poll_interval = CP2130_IRQ_POLL_INTERVAL;
+	cp2130_gpio_irq_probe(dev);
+	dev_dbg(&udev->dev, "registered irq chip");
+
+	/* create sysfs files */
+	ret = sysfs_create_group(&intf->dev.kobj,
+				&cp2130_sysfs_atributes_group);
+	if (ret)
+		dev_err(&udev->dev, "device_create_file failed");
+
+	INIT_WORK(&dev->update_chn_config, cp2130_update_channel_config);
+	INIT_WORK(&dev->read_chn_config, cp2130_read_channel_config);
+	INIT_WORK(&dev->update_otprom, cp2130_update_otprom);
+	INIT_WORK(&dev->read_otprom, cp2130_read_otprom);
+	schedule_work(&dev->read_chn_config);
+	schedule_work(&dev->read_otprom);
+
+	return 0;
+}
+
+static void cp2130_disconnect(struct usb_interface *intf)
+{
+	struct cp2130_device *dev = usb_get_intfdata(intf);
+	int i = dev->irq_poll_interval;
+
+	dev->irq_poll_interval = -1;
+	usleep_range(i, i + 100); /* wait for worker to complete */
+
+	/* Remove all devices (if any) attached to the SPI bus */
+	for (i = 0; i < CP2130_NUM_GPIOS; i++)
+		if (dev->chn_configs[i].chip)
+			spi_unregister_device(dev->chn_configs[i].chip);
+
+	cp2130_gpio_irq_remove(dev);
+	for (i = 0; i < CP2130_NUM_GPIOS; i++) {
+		struct cp2130_channel *chn_cfg = &dev->chn_configs[i];
+
+		kfree(dev->gpio_names[i]);
+		kfree(chn_cfg->pdata);
+	}
+
+	/* remove sysfs files */
+	sysfs_remove_group(&intf->dev.kobj, &cp2130_sysfs_atributes_group);
+}
+
+module_init(cp2130_init);
+module_exit(cp2130_exit);
+MODULE_AUTHOR("Jochen Henneberg <jh@henneberg-systemdesign.com>");
+MODULE_DESCRIPTION("Silicon Labs CP2130 USB-to-SPI bridge");
+MODULE_LICENSE("GPL");
+
+MODULE_DEVICE_TABLE(usb, cp2130_devices);