[PATCH] USB: serial: cp210x: Implement GPIO support for CP2102N

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



Pretty much what the title says. No other functions/devices
touched. Tested on CP2102N-QFN28.

Limitation: Even though the QFN28 package has 7 GPIOs,
this patch allows to control only the first 4.
What I've found using reverse engineering regarding the
other 3 pins collides both with reality and with other
documented functions, so I did not dare to just use my
findings because I cannot test on other packages.
Instead, I decided to play it safe and only support 4 GPIOs.

Signed-off-by: Karoly Pados <pados@xxxxxxxx>
---
 drivers/usb/serial/cp210x.c | 259 +++++++++++++++++++++++++++++++++++-
 1 file changed, 257 insertions(+), 2 deletions(-)

diff --git a/drivers/usb/serial/cp210x.c b/drivers/usb/serial/cp210x.c
index 793b86252c46..adb450185ce8 100644
--- a/drivers/usb/serial/cp210x.c
+++ b/drivers/usb/serial/cp210x.c
@@ -6,7 +6,8 @@
  *
  * Support to set flow control line levels using TIOCMGET and TIOCMSET
  * thanks to Karl Hiramoto karl@xxxxxxxxxxxx. RTSCTS hardware flow
- * control thanks to Munir Nassar nassarmu@xxxxxxxxxxxxx
+ * control thanks to Munir Nassar nassarmu@xxxxxxxxxxxxx.
+ * GPIO support for CP2102N thanks to Karoly Pados.
  *
  */
 
@@ -229,11 +230,16 @@ struct cp210x_serial_private {
 	struct gpio_chip	gc;
 	u8			config;
 	u8			gpio_mode;
+	u8			gpio_control;
+	u8			gpio_dir;
 	bool			gpio_registered;
 #endif
 	u8			partnum;
 };
 
+#define CP2102N_PIN_OPENDRAIN(var, pin)	(!((var) & BIT(pin)))
+#define CP2102N_PIN_GPIO(var, pin)	(!((var) & BIT(pin)))
+
 struct cp210x_port_private {
 	__u8			bInterfaceNumber;
 	bool			has_swapped_line_ctl;
@@ -349,6 +355,7 @@ static struct usb_serial_driver * const serial_drivers[] = {
 #define CP210X_GET_PORTCONFIG	0x370C
 #define CP210X_GET_DEVICEMODE	0x3711
 #define CP210X_WRITE_LATCH	0x37E1
+#define CP210X_READ_2NCONFIG	0x000E
 
 /* Part number definitions */
 #define CP210X_PARTNUM_CP2101	0x01
@@ -362,6 +369,14 @@ static struct usb_serial_driver * const serial_drivers[] = {
 #define CP210X_PARTNUM_CP2102N_QFN20	0x22
 #define CP210X_PARTNUM_UNKNOWN	0xFF
 
+#define	IS_CP2102N(partnum)   (((partnum) == CP210X_PARTNUM_CP2102N_QFN28) || \
+			       ((partnum) == CP210X_PARTNUM_CP2102N_QFN24) || \
+			       ((partnum) == CP210X_PARTNUM_CP2102N_QFN20))
+
+#define CP210X_2NCONFIG_GPIO_CONTROL_IDX	600
+#define CP210X_2NCONFIG_GPIO_MODE_IDX		581
+#define CP210X_2NCONFIG_GPIO_RSTLATCH_IDX	587
+
 /* CP210X_GET_COMM_STATUS returns these 0x13 bytes */
 struct cp210x_comm_status {
 	__le32   ulErrors;
@@ -1455,6 +1470,235 @@ static void cp210x_gpio_remove(struct usb_serial *serial)
 	}
 }
 
+static int cp2102n_gpio_get(struct gpio_chip *gc, unsigned int gpio)
+{
+	struct usb_serial *serial = gpiochip_get_data(gc);
+	int result;
+	u8 buf;
+
+	result = cp210x_read_vendor_block(serial, REQTYPE_DEVICE_TO_HOST,
+					  CP210X_READ_LATCH, &buf, sizeof(buf));
+	if (result < 0)
+		return result;
+
+	return !!(buf & BIT(gpio));
+}
+
+static void cp2102n_gpio_set(struct gpio_chip *gc, unsigned int gpio, int value)
+{
+	int result;
+	struct cp210x_gpio_write buf;
+	struct usb_serial *serial = gpiochip_get_data(gc);
+	struct cp210x_serial_private *priv = usb_get_serial_data(serial);
+
+	/* Ignore request if pin is not in our control */
+	if (!CP2102N_PIN_GPIO(priv->gpio_control, gpio)) {
+		dev_warn(&serial->interface->dev,
+			 "Cannot control GPIO with active alternate function.\n");
+		return;
+	}
+
+	buf.state = (value == 1) ? BIT(gpio) : 0;
+	buf.mask = BIT(gpio);
+
+	result = usb_control_msg(serial->dev,
+				 usb_sndctrlpipe(serial->dev, 0),
+				 CP210X_VENDOR_SPECIFIC,
+				 REQTYPE_HOST_TO_DEVICE,
+				 CP210X_WRITE_LATCH,
+				 *(u16 *)&buf,
+				 NULL, 0, USB_CTRL_SET_TIMEOUT);
+
+	if (result < 0) {
+		dev_err(&serial->interface->dev,
+			"Failed to set GPIO value.\n");
+	}
+}
+
+static int cp2102n_gpio_direction_get(struct gpio_chip *gc, unsigned int gpio)
+{
+	struct usb_serial *serial = gpiochip_get_data(gc);
+	struct cp210x_serial_private *priv = usb_get_serial_data(serial);
+
+	return priv->gpio_dir & BIT(gpio);
+}
+
+static int cp2102n_gpio_direction_input(struct gpio_chip *gc, unsigned int gpio)
+{
+	struct usb_serial *serial = gpiochip_get_data(gc);
+	struct cp210x_serial_private *priv = usb_get_serial_data(serial);
+
+	/* Return an error if pin is not in our control */
+	if (!CP2102N_PIN_GPIO(priv->gpio_control, gpio)) {
+		dev_warn(&serial->interface->dev,
+			 "Cannot control GPIO with active alternate function.\n");
+		return -EPERM;
+	}
+
+	/* Push-pull pins cannot be changed to be inputs */
+	if (!CP2102N_PIN_OPENDRAIN(priv->gpio_mode, gpio)) {
+		dev_warn(&serial->interface->dev,
+			 "Cannot change direction of a push-pull GPIO to input.\n");
+		return -EPERM;
+	}
+
+	/* Make sure to release pin if it is being driven low */
+	cp2102n_gpio_set(gc, gpio, 1);
+
+	/* Note pin direction to ourselves */
+	priv->gpio_dir |= BIT(gpio);
+
+	return 0;
+}
+
+static int cp2102n_gpio_direction_output(struct gpio_chip *gc,
+					 unsigned int gpio,
+					 int value)
+{
+	struct usb_serial *serial = gpiochip_get_data(gc);
+	struct cp210x_serial_private *priv = usb_get_serial_data(serial);
+
+	/* Return an error if pin is not in our control */
+	if (!CP2102N_PIN_GPIO(priv->gpio_control, gpio)) {
+		dev_warn(&serial->interface->dev,
+			 "Cannot control GPIO with active alternate function.\n");
+		return -EPERM;
+	}
+
+	/* Note pin direction to ourselves */
+	priv->gpio_dir &= ~BIT(gpio);
+
+	/* Set requested initial output value */
+	cp2102n_gpio_set(gc, gpio, value);
+
+	return 0;
+}
+
+static u16 fletcher16(u8 *data, u16 bytes)
+{
+	u16 sum1 = 0xff, sum2 = 0xff;
+	u16 tlen;
+
+	while (bytes) {
+		tlen = bytes >= 20 ? 20 : bytes;
+		bytes -= tlen;
+		do {
+			sum2 += sum1 += *data++;
+		} while (--tlen);
+		sum1 = (sum1 & 0xff) + (sum1 >> 8);
+		sum2 = (sum2 & 0xff) + (sum2 >> 8);
+	}
+	/* Second reduction step to reduce sums to 8 bits */
+	sum1 = (sum1 & 0xff) + (sum1 >> 8);
+	sum2 = (sum2 & 0xff) + (sum2 >> 8);
+	return sum2 << 8 | sum1;
+}
+
+static int cp2102n_gpio_init(struct usb_serial *serial)
+{
+	const u16 CONFIG_SIZE = 0x02A6;
+	int result;
+	u16 config_csum;
+	u8 *config_buf;
+	u8 gpio_ctrl;
+	u8 gpio_mode;
+	u8 gpio_latch;
+	u8 gpio_rst_latch;
+	u8 i;
+	bool config_valid = true;
+	struct cp210x_serial_private *priv = usb_get_serial_data(serial);
+
+	/* Retrieve device configuration from the device.
+	 * The array received contains all customization settings
+	 * done at the factory/manufacturer.
+	 * Format of the array is documented at the time of writing at
+	 * https://www.silabs.com/community/interface/knowledge-base.entry.html/2017/03/31/cp2102n_setconfig-xsfa
+	 */
+	config_buf = kmalloc(CONFIG_SIZE, GFP_KERNEL);
+	if (!config_buf)
+		return -ENOMEM;
+
+	result = cp210x_read_vendor_block(serial,
+					  REQTYPE_DEVICE_TO_HOST,
+					  CP210X_READ_2NCONFIG,
+					  config_buf,
+					  CONFIG_SIZE);
+	if (result < 0)
+		return result;
+
+	/* Check configuration for validity.
+	 * The last two bytes of the array contain a fletcher16
+	 * checksum of all the bytes before, which we can validate.
+	 */
+	config_csum = fletcher16(config_buf, CONFIG_SIZE - 2);
+	if (((config_csum & 0xFF) != config_buf[CONFIG_SIZE - 1]) ||
+	    ((config_csum >> 8)   != config_buf[CONFIG_SIZE - 2])) {
+		config_valid = false;
+		dev_err(&serial->interface->dev,
+			"Corrupted device configuration received\n");
+	}
+
+	gpio_mode = config_buf[CP210X_2NCONFIG_GPIO_MODE_IDX];
+	gpio_ctrl = config_buf[CP210X_2NCONFIG_GPIO_CONTROL_IDX];
+	gpio_rst_latch = config_buf[CP210X_2NCONFIG_GPIO_RSTLATCH_IDX];
+
+	kfree(config_buf);
+
+	if (!config_valid)
+		return -EIO;
+
+	/* We only support 4 GPIOs even on the QFN28 package because
+	 * documentation about the CP2102N GetConfig Array
+	 * does not seem to correspond to reality on this device.
+	 */
+	priv->gc.ngpio = 4;
+
+	/* Get default pin states after reset. Needed so we can determine
+	 * the direction of an open-drain pin.
+	 */
+	gpio_latch = (gpio_rst_latch >> 3) & 0x0F;
+
+	/* 0 indicates open-drain mode, 1 is push-pull */
+	priv->gpio_mode = (gpio_mode >> 3) & 0x0F;
+
+	/* 0 indicates GPIO mode, 1 is alternate function */
+	priv->gpio_control = (gpio_ctrl >> 2) & 0x0F;
+
+	/* The CP2102N does not strictly has input and output pin modes,
+	 * it only knows open-drain and push-pull modes which is set at
+	 * factory. An open-drain pin can function both as an
+	 * input or an output. We emulate input mode for open-drain pins
+	 * by making sure they are not driven low, and we do not allow
+	 * push-pull pins to be set as an input.
+	 */
+	for (i = 0; i < priv->gc.ngpio; ++i) {
+		/* Set direction to "input" iff
+		 * pin is open-drain and reset value is 1
+		 */
+		if (CP2102N_PIN_OPENDRAIN(priv->gpio_mode, i) &&
+		    (gpio_latch & BIT(i))) {
+			priv->gpio_dir |= BIT(i);
+		}
+	}
+
+	priv->gc.label = "cp2102n";
+	priv->gc.get_direction = cp2102n_gpio_direction_get;
+	priv->gc.direction_input = cp2102n_gpio_direction_input;
+	priv->gc.direction_output = cp2102n_gpio_direction_output;
+	priv->gc.get = cp2102n_gpio_get;
+	priv->gc.set = cp2102n_gpio_set;
+	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, serial);
+	if (!result)
+		priv->gpio_registered = true;
+
+	return result;
+}
+
 #else
 
 static int cp2105_shared_gpio_init(struct usb_serial *serial)
@@ -1462,6 +1706,11 @@ static int cp2105_shared_gpio_init(struct usb_serial *serial)
 	return 0;
 }
 
+static int cp2102n_gpio_init(struct usb_serial *serial)
+{
+	return 0;
+}
+
 static void cp210x_gpio_remove(struct usb_serial *serial)
 {
 	/* Nothing to do */
@@ -1522,7 +1771,13 @@ static int cp210x_attach(struct usb_serial *serial)
 
 	usb_set_serial_data(serial, priv);
 
-	if (priv->partnum == CP210X_PARTNUM_CP2105) {
+	if (IS_CP2102N(priv->partnum)) {
+		result = cp2102n_gpio_init(serial);
+		if (result < 0) {
+			dev_err(&serial->interface->dev,
+				"GPIO initialisation failed, continuing without GPIO support\n");
+		}
+	} else if (priv->partnum == CP210X_PARTNUM_CP2105) {
 		result = cp2105_shared_gpio_init(serial);
 		if (result < 0) {
 			dev_err(&serial->interface->dev,
-- 
2.17.1

--
To unsubscribe from this list: send the line "unsubscribe linux-usb" in
the body of a message to majordomo@xxxxxxxxxxxxxxx
More majordomo info at  http://vger.kernel.org/majordomo-info.html



[Index of Archives]     [Linux Media]     [Linux Input]     [Linux Audio Users]     [Yosemite News]     [Linux Kernel]     [Linux SCSI]     [Old Linux USB Devel Archive]

  Powered by Linux