USB: serial: ftdi_sio: implement GPIO support for FT-X devices
authorKaroly Pados <pados@pados.hu>
Tue, 25 Sep 2018 13:59:11 +0000 (15:59 +0200)
committerJohan Hovold <johan@kernel.org>
Wed, 26 Sep 2018 09:59:56 +0000 (11:59 +0200)
This patch allows using the CBUS pins of FT-X devices as GPIO in CBUS
bitbanging mode. There is no conflict between the GPIO and VCP
functionality in this mode. Tested on FT230X and FT231X.

As there is no way to request the current CBUS register configuration
from the device, all CBUS pins are set to a known state when the first
GPIO is requested. This allows using libftdi to set the GPIO pins
before loading this module for UART functionality, a behavior that
existing applications might be relying upon (though no specific case
is known to the authors of this patch).

Signed-off-by: Karoly Pados <pados@pados.hu>
[ johan: minor style changes ]
Signed-off-by: Johan Hovold <johan@kernel.org>
drivers/usb/serial/ftdi_sio.c
drivers/usb/serial/ftdi_sio.h

index b5cef32..6b727ad 100644 (file)
@@ -39,6 +39,7 @@
 #include <linux/uaccess.h>
 #include <linux/usb.h>
 #include <linux/serial.h>
+#include <linux/gpio/driver.h>
 #include <linux/usb/serial.h>
 #include "ftdi_sio.h"
 #include "ftdi_sio_ids.h"
@@ -72,6 +73,15 @@ struct ftdi_private {
        unsigned int latency;           /* latency setting in use */
        unsigned short max_packet_size;
        struct mutex cfg_lock; /* Avoid mess by parallel calls of config ioctl() and change_speed() */
+#ifdef CONFIG_GPIOLIB
+       struct gpio_chip gc;
+       struct mutex gpio_lock; /* protects GPIO state */
+       bool gpio_registered;   /* is the gpiochip in kernel registered */
+       bool gpio_used;         /* true if the user requested a gpio */
+       u8 gpio_altfunc;        /* which pins are in gpio mode */
+       u8 gpio_output;         /* pin directions cache */
+       u8 gpio_value;          /* pin value for outputs */
+#endif
 };
 
 /* struct ftdi_sio_quirk is used by devices requiring special attention. */
@@ -1766,6 +1776,344 @@ static void remove_sysfs_attrs(struct usb_serial_port *port)
 
 }
 
+#ifdef CONFIG_GPIOLIB
+
+static const char * const ftdi_ftx_gpio_names[] = {
+       "CBUS0", "CBUS1", "CBUS2", "CBUS3"
+};
+
+static int ftdi_set_bitmode(struct usb_serial_port *port, u8 mode)
+{
+       struct ftdi_private *priv = usb_get_serial_port_data(port);
+       struct usb_serial *serial = port->serial;
+       int result;
+       u16 val;
+
+       val = (mode << 8) | (priv->gpio_output << 4) | priv->gpio_value;
+       result = usb_control_msg(serial->dev,
+                                usb_sndctrlpipe(serial->dev, 0),
+                                FTDI_SIO_SET_BITMODE_REQUEST,
+                                FTDI_SIO_SET_BITMODE_REQUEST_TYPE, val,
+                                priv->interface, NULL, 0, WDR_TIMEOUT);
+       if (result < 0) {
+               dev_err(&serial->interface->dev,
+                       "bitmode request failed for value 0x%04x: %d\n",
+                       val, result);
+       }
+
+       return result;
+}
+
+static int ftdi_set_cbus_pins(struct usb_serial_port *port)
+{
+       return ftdi_set_bitmode(port, FTDI_SIO_BITMODE_CBUS);
+}
+
+static int ftdi_exit_cbus_mode(struct usb_serial_port *port)
+{
+       struct ftdi_private *priv = usb_get_serial_port_data(port);
+
+       priv->gpio_output = 0;
+       priv->gpio_value = 0;
+       return ftdi_set_bitmode(port, FTDI_SIO_BITMODE_RESET);
+}
+
+static int ftdi_gpio_request(struct gpio_chip *gc, unsigned int offset)
+{
+       struct usb_serial_port *port = gpiochip_get_data(gc);
+       struct ftdi_private *priv = usb_get_serial_port_data(port);
+       int result;
+
+       if (priv->gpio_altfunc & BIT(offset))
+               return -ENODEV;
+
+       mutex_lock(&priv->gpio_lock);
+       if (!priv->gpio_used) {
+               /* Set default pin states, as we cannot get them from device */
+               priv->gpio_output = 0x00;
+               priv->gpio_value = 0x00;
+               result = ftdi_set_cbus_pins(port);
+               if (result) {
+                       mutex_unlock(&priv->gpio_lock);
+                       return result;
+               }
+
+               priv->gpio_used = true;
+       }
+       mutex_unlock(&priv->gpio_lock);
+
+       return 0;
+}
+
+static int ftdi_read_cbus_pins(struct usb_serial_port *port)
+{
+       struct ftdi_private *priv = usb_get_serial_port_data(port);
+       struct usb_serial *serial = port->serial;
+       unsigned char *buf;
+       int result;
+
+       buf = kmalloc(1, GFP_KERNEL);
+       if (!buf)
+               return -ENOMEM;
+
+       result = usb_control_msg(serial->dev,
+                                usb_rcvctrlpipe(serial->dev, 0),
+                                FTDI_SIO_READ_PINS_REQUEST,
+                                FTDI_SIO_READ_PINS_REQUEST_TYPE, 0,
+                                priv->interface, buf, 1, WDR_TIMEOUT);
+       if (result < 1) {
+               if (result >= 0)
+                       result = -EIO;
+       } else {
+               result = buf[0];
+       }
+
+       kfree(buf);
+
+       return result;
+}
+
+static int ftdi_gpio_get(struct gpio_chip *gc, unsigned int gpio)
+{
+       struct usb_serial_port *port = gpiochip_get_data(gc);
+       int result;
+
+       result = ftdi_read_cbus_pins(port);
+       if (result < 0)
+               return result;
+
+       return !!(result & BIT(gpio));
+}
+
+static void ftdi_gpio_set(struct gpio_chip *gc, unsigned int gpio, int value)
+{
+       struct usb_serial_port *port = gpiochip_get_data(gc);
+       struct ftdi_private *priv = usb_get_serial_port_data(port);
+
+       mutex_lock(&priv->gpio_lock);
+
+       if (value)
+               priv->gpio_value |= BIT(gpio);
+       else
+               priv->gpio_value &= ~BIT(gpio);
+
+       ftdi_set_cbus_pins(port);
+
+       mutex_unlock(&priv->gpio_lock);
+}
+
+static int ftdi_gpio_get_multiple(struct gpio_chip *gc, unsigned long *mask,
+                                       unsigned long *bits)
+{
+       struct usb_serial_port *port = gpiochip_get_data(gc);
+       int result;
+
+       result = ftdi_read_cbus_pins(port);
+       if (result < 0)
+               return result;
+
+       *bits = result & *mask;
+
+       return 0;
+}
+
+static void ftdi_gpio_set_multiple(struct gpio_chip *gc, unsigned long *mask,
+                                       unsigned long *bits)
+{
+       struct usb_serial_port *port = gpiochip_get_data(gc);
+       struct ftdi_private *priv = usb_get_serial_port_data(port);
+
+       mutex_lock(&priv->gpio_lock);
+
+       priv->gpio_value &= ~(*mask);
+       priv->gpio_value |= *bits & *mask;
+       ftdi_set_cbus_pins(port);
+
+       mutex_unlock(&priv->gpio_lock);
+}
+
+static int ftdi_gpio_direction_get(struct gpio_chip *gc, unsigned int gpio)
+{
+       struct usb_serial_port *port = gpiochip_get_data(gc);
+       struct ftdi_private *priv = usb_get_serial_port_data(port);
+
+       return !(priv->gpio_output & BIT(gpio));
+}
+
+static int ftdi_gpio_direction_input(struct gpio_chip *gc, unsigned int gpio)
+{
+       struct usb_serial_port *port = gpiochip_get_data(gc);
+       struct ftdi_private *priv = usb_get_serial_port_data(port);
+       int result;
+
+       mutex_lock(&priv->gpio_lock);
+
+       priv->gpio_output &= ~BIT(gpio);
+       result = ftdi_set_cbus_pins(port);
+
+       mutex_unlock(&priv->gpio_lock);
+
+       return result;
+}
+
+static int ftdi_gpio_direction_output(struct gpio_chip *gc, unsigned int gpio,
+                                       int value)
+{
+       struct usb_serial_port *port = gpiochip_get_data(gc);
+       struct ftdi_private *priv = usb_get_serial_port_data(port);
+       int result;
+
+       mutex_lock(&priv->gpio_lock);
+
+       priv->gpio_output |= BIT(gpio);
+       if (value)
+               priv->gpio_value |= BIT(gpio);
+       else
+               priv->gpio_value &= ~BIT(gpio);
+
+       result = ftdi_set_cbus_pins(port);
+
+       mutex_unlock(&priv->gpio_lock);
+
+       return result;
+}
+
+static int ftdi_read_eeprom(struct usb_serial *serial, void *dst, u16 addr,
+                               u16 nbytes)
+{
+       int read = 0;
+
+       if (addr % 2 != 0)
+               return -EINVAL;
+       if (nbytes % 2 != 0)
+               return -EINVAL;
+
+       /* Read EEPROM two bytes at a time */
+       while (read < nbytes) {
+               int rv;
+
+               rv = usb_control_msg(serial->dev,
+                                    usb_rcvctrlpipe(serial->dev, 0),
+                                    FTDI_SIO_READ_EEPROM_REQUEST,
+                                    FTDI_SIO_READ_EEPROM_REQUEST_TYPE,
+                                    0, (addr + read) / 2, dst + read, 2,
+                                    WDR_TIMEOUT);
+               if (rv < 2) {
+                       if (rv >= 0)
+                               return -EIO;
+                       else
+                               return rv;
+               }
+
+               read += rv;
+       }
+
+       return 0;
+}
+
+static int ftx_gpioconf_init(struct usb_serial_port *port)
+{
+       struct ftdi_private *priv = usb_get_serial_port_data(port);
+       struct usb_serial *serial = port->serial;
+       const u16 cbus_cfg_addr = 0x1a;
+       const u16 cbus_cfg_size = 4;
+       u8 *cbus_cfg_buf;
+       int result;
+       u8 i;
+
+       cbus_cfg_buf = kmalloc(cbus_cfg_size, GFP_KERNEL);
+       if (!cbus_cfg_buf)
+               return -ENOMEM;
+
+       result = ftdi_read_eeprom(serial, cbus_cfg_buf,
+                                 cbus_cfg_addr, cbus_cfg_size);
+       if (result < 0)
+               goto out_free;
+
+       /* FIXME: FT234XD alone has 1 GPIO, but how to recognize this IC? */
+       priv->gc.ngpio = 4;
+       priv->gc.names = ftdi_ftx_gpio_names;
+
+       /* Determine which pins are configured for CBUS bitbanging */
+       priv->gpio_altfunc = 0xff;
+       for (i = 0; i < priv->gc.ngpio; ++i) {
+               if (cbus_cfg_buf[i] == FTDI_FTX_CBUS_MUX_GPIO)
+                       priv->gpio_altfunc &= ~BIT(i);
+       }
+
+out_free:
+       kfree(cbus_cfg_buf);
+
+       return result;
+}
+
+static int ftdi_gpio_init(struct usb_serial_port *port)
+{
+       struct ftdi_private *priv = usb_get_serial_port_data(port);
+       struct usb_serial *serial = port->serial;
+       int result;
+
+       switch (priv->chip_type) {
+       case FTX:
+               result = ftx_gpioconf_init(port);
+               break;
+       default:
+               return 0;
+       }
+
+       if (result < 0)
+               return result;
+
+       mutex_init(&priv->gpio_lock);
+
+       priv->gc.label = "ftdi-cbus";
+       priv->gc.request = ftdi_gpio_request;
+       priv->gc.get_direction = ftdi_gpio_direction_get;
+       priv->gc.direction_input = ftdi_gpio_direction_input;
+       priv->gc.direction_output = ftdi_gpio_direction_output;
+       priv->gc.get = ftdi_gpio_get;
+       priv->gc.set = ftdi_gpio_set;
+       priv->gc.get_multiple = ftdi_gpio_get_multiple;
+       priv->gc.set_multiple = ftdi_gpio_set_multiple;
+       priv->gc.owner = THIS_MODULE;
+       priv->gc.parent = &serial->interface->dev;
+       priv->gc.base = -1;
+       priv->gc.can_sleep = true;
+
+       result = gpiochip_add_data(&priv->gc, port);
+       if (!result)
+               priv->gpio_registered = true;
+
+       return result;
+}
+
+static void ftdi_gpio_remove(struct usb_serial_port *port)
+{
+       struct ftdi_private *priv = usb_get_serial_port_data(port);
+
+       if (priv->gpio_registered) {
+               gpiochip_remove(&priv->gc);
+               priv->gpio_registered = false;
+       }
+
+       if (priv->gpio_used) {
+               /* Exiting CBUS-mode does not reset pin states. */
+               ftdi_exit_cbus_mode(port);
+               priv->gpio_used = false;
+       }
+}
+
+#else
+
+static int ftdi_gpio_init(struct usb_serial_port *port)
+{
+       return 0;
+}
+
+static void ftdi_gpio_remove(struct usb_serial_port *port) { }
+
+#endif /* CONFIG_GPIOLIB */
+
 /*
  * ***************************************************************************
  * FTDI driver specific functions
@@ -1794,7 +2142,7 @@ static int ftdi_sio_port_probe(struct usb_serial_port *port)
 {
        struct ftdi_private *priv;
        const struct ftdi_sio_quirk *quirk = usb_get_serial_data(port->serial);
-
+       int result;
 
        priv = kzalloc(sizeof(struct ftdi_private), GFP_KERNEL);
        if (!priv)
@@ -1813,6 +2161,14 @@ static int ftdi_sio_port_probe(struct usb_serial_port *port)
                priv->latency = 16;
        write_latency_timer(port);
        create_sysfs_attrs(port);
+
+       result = ftdi_gpio_init(port);
+       if (result < 0) {
+               dev_err(&port->serial->interface->dev,
+                       "GPIO initialisation failed: %d\n",
+                       result);
+       }
+
        return 0;
 }
 
@@ -1930,6 +2286,8 @@ static int ftdi_sio_port_remove(struct usb_serial_port *port)
 {
        struct ftdi_private *priv = usb_get_serial_port_data(port);
 
+       ftdi_gpio_remove(port);
+
        remove_sysfs_attrs(port);
 
        kfree(priv);
index dcd0b6e..6cfe682 100644 (file)
 #define FTDI_SIO_SET_EVENT_CHAR                6 /* Set the event character */
 #define FTDI_SIO_SET_ERROR_CHAR                7 /* Set the error character */
 #define FTDI_SIO_SET_LATENCY_TIMER     9 /* Set the latency timer */
-#define FTDI_SIO_GET_LATENCY_TIMER     10 /* Get the latency timer */
+#define FTDI_SIO_GET_LATENCY_TIMER     0x0a /* Get the latency timer */
+#define FTDI_SIO_SET_BITMODE           0x0b /* Set bitbang mode */
+#define FTDI_SIO_READ_PINS             0x0c /* Read immediate value of pins */
+#define FTDI_SIO_READ_EEPROM           0x90 /* Read EEPROM */
 
 /* Interface indices for FT2232, FT2232H and FT4232H devices */
 #define INTERFACE_A            1
@@ -433,6 +436,28 @@ enum ftdi_sio_baudrate {
  *         1 = active
  */
 
+/* FTDI_SIO_SET_BITMODE */
+#define FTDI_SIO_SET_BITMODE_REQUEST_TYPE 0x40
+#define FTDI_SIO_SET_BITMODE_REQUEST FTDI_SIO_SET_BITMODE
+
+/* Possible bitmodes for FTDI_SIO_SET_BITMODE_REQUEST */
+#define FTDI_SIO_BITMODE_RESET         0x00
+#define FTDI_SIO_BITMODE_CBUS          0x20
+
+/* FTDI_SIO_READ_PINS */
+#define FTDI_SIO_READ_PINS_REQUEST_TYPE 0xc0
+#define FTDI_SIO_READ_PINS_REQUEST FTDI_SIO_READ_PINS
+
+/*
+ * FTDI_SIO_READ_EEPROM
+ *
+ * EEPROM format found in FTDI AN_201, "FT-X MTP memory Configuration",
+ * http://www.ftdichip.com/Support/Documents/AppNotes/AN_201_FT-X%20MTP%20Memory%20Configuration.pdf
+ */
+#define FTDI_SIO_READ_EEPROM_REQUEST_TYPE 0xc0
+#define FTDI_SIO_READ_EEPROM_REQUEST FTDI_SIO_READ_EEPROM
+
+#define FTDI_FTX_CBUS_MUX_GPIO         8
 
 
 /* Descriptors returned by the device