This patch adds support for ch341a USB to I2C bridge, it's based on a reverse engineering of the official gui client and several code examples. Tested with several eeprom models (24c32, 24c08), rtc (ds1307), adc (mcp3422). Signed-off-by: Angelo Compagnucci <angelo.compagnucci@xxxxxxxxx> --- Changes v1->v2: * Better delay in writing function * Corrected i2c supported speeds (verified also with an oscilloscope) drivers/i2c/busses/Kconfig | 10 ++ drivers/i2c/busses/Makefile | 1 + drivers/i2c/busses/i2c-ch341a.c | 388 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 399 insertions(+) create mode 100644 drivers/i2c/busses/i2c-ch341a.c diff --git a/drivers/i2c/busses/Kconfig b/drivers/i2c/busses/Kconfig index f167021..2a168c2 100644 --- a/drivers/i2c/busses/Kconfig +++ b/drivers/i2c/busses/Kconfig @@ -997,6 +997,16 @@ config I2C_RCAR comment "External I2C/SMBus adapter drivers" +config I2C_CH341A + tristate "Winchiphead CH341A USB adapter" + depends on USB + help + If you say yes to this option, support will be included for + Winchiphead CH341A, a USB to I2C interface. + + This driver can also be built as a module. If so, the module + will be called i2c-ch341a. + config I2C_DIOLAN_U2C tristate "Diolan U2C-12 USB adapter" depends on USB diff --git a/drivers/i2c/busses/Makefile b/drivers/i2c/busses/Makefile index 37f2819..16805b7 100644 --- a/drivers/i2c/busses/Makefile +++ b/drivers/i2c/busses/Makefile @@ -98,6 +98,7 @@ obj-$(CONFIG_I2C_XLP9XX) += i2c-xlp9xx.o obj-$(CONFIG_I2C_RCAR) += i2c-rcar.o # External I2C/SMBus adapter drivers +obj-$(CONFIG_I2C_CH341A) += i2c-ch341a.o obj-$(CONFIG_I2C_DIOLAN_U2C) += i2c-diolan-u2c.o obj-$(CONFIG_I2C_DLN2) += i2c-dln2.o obj-$(CONFIG_I2C_PARPORT) += i2c-parport.o diff --git a/drivers/i2c/busses/i2c-ch341a.c b/drivers/i2c/busses/i2c-ch341a.c new file mode 100644 index 0000000..ccd2a01 --- /dev/null +++ b/drivers/i2c/busses/i2c-ch341a.c @@ -0,0 +1,388 @@ +/* + * Driver for the Winchiphead CH341A USB to I2C chip + * + * Datasheet: http://www.winchiphead.com/download/CH341/CH341DS1.PDF + * + * Copyright (C) 2016 Angelo Compagnucci <angelo.compagnucci@xxxxxxxxx> + * + * 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, version 2. + * + */ + +#include <linux/device.h> +#include <linux/kernel.h> +#include <linux/errno.h> +#include <linux/module.h> +#include <linux/slab.h> +#include <linux/types.h> +#include <linux/usb.h> +#include <linux/i2c.h> +#include <linux/string.h> +#include <linux/platform_device.h> + +#define TIMEOUT 1000 + +#define CH341A_CONTROL_I2C 0xAA + +#define CH341A_I2C_CMD_STA 0x74 +#define CH341A_I2C_CMD_STO 0x75 +#define CH341A_I2C_CMD_OUT 0x80 +#define CH341A_I2C_CMD_IN 0xC0 +#define CH341A_I2C_CMD_MAX_LENGTH 32 +#define CH341A_I2C_CMD_SET 0x60 +#define CH341A_I2C_CMD_US 0x40 +#define CH341A_I2C_CMD_MS 0x50 +#define CH341A_I2C_CMD_DLY 0x0f +#define CH341A_I2C_CMD_END 0x00 + +#define SEND_PAYLOAD_LENGTH 29 +#define RECV_PAYLOAD_LENGTH 32 + +struct i2c_ch341a { + u8 in_buf[CH341A_I2C_CMD_MAX_LENGTH]; + u8 out_buf[CH341A_I2C_CMD_MAX_LENGTH]; + struct usb_device *usb_dev; + struct usb_interface *interface; + struct i2c_adapter adapter; + unsigned int clock_speed; + int ep_in, ep_out; +}; + +static int ch341a_usb_i2c_command(struct i2c_adapter *adapter, u8 *cmd, u8 len) +{ + struct i2c_ch341a *dev = (struct i2c_ch341a *)adapter->algo_data; + int sent, ret; + + ret = usb_bulk_msg(dev->usb_dev, + usb_sndbulkpipe(dev->usb_dev, dev->ep_out), + cmd, len, &sent, TIMEOUT); + if (ret != 0) return ret; + + return 0; +} + +static int ch341a_usb_i2c_start(struct i2c_adapter *adapter) +{ + u8 I2C_CMD_START[] = {CH341A_CONTROL_I2C, CH341A_I2C_CMD_STA, CH341A_I2C_CMD_END}; + print_hex_dump_bytes(__func__, DUMP_PREFIX_OFFSET, I2C_CMD_START, sizeof(I2C_CMD_START)); + return ch341a_usb_i2c_command(adapter, I2C_CMD_START, sizeof(I2C_CMD_START)); +} + +static int ch341a_usb_i2c_stop(struct i2c_adapter *adapter) +{ + u8 I2C_CMD_STOP[] = {CH341A_CONTROL_I2C, CH341A_I2C_CMD_STO, CH341A_I2C_CMD_END}; + print_hex_dump_bytes(__func__, DUMP_PREFIX_OFFSET, I2C_CMD_STOP, sizeof(I2C_CMD_STOP)); + return ch341a_usb_i2c_command(adapter, I2C_CMD_STOP, sizeof(I2C_CMD_STOP)); +} + +static int ch341a_usb_i2c_set_speed(struct i2c_adapter *adapter, unsigned int data) +{ + struct i2c_ch341a *ch341a_data = (struct i2c_ch341a *)adapter->algo_data; + u8 I2C_CMD_SET[] = {CH341A_CONTROL_I2C, CH341A_I2C_CMD_SET , CH341A_I2C_CMD_END}; + u8 speed; + + switch (data) { + case 20: + speed = 0; + break; + case 100: + speed = 1; + break; + case 400: + speed = 2; + break; + case 750: + speed = 3; + break; + default: + return -EINVAL; + } + + ch341a_data->clock_speed = data; + + I2C_CMD_SET[1] |= speed; + + print_hex_dump_bytes(__func__, DUMP_PREFIX_OFFSET, I2C_CMD_SET, sizeof(I2C_CMD_SET)); + return ch341a_usb_i2c_command(adapter, I2C_CMD_SET, sizeof(I2C_CMD_SET)); +} + +static int ch341a_usb_i2c_write(struct i2c_adapter *adapter, u8 len, u8 *data) +{ + struct i2c_ch341a *dev = (struct i2c_ch341a *)adapter->algo_data; + int ret, actual; + + dev->out_buf[0] = CH341A_CONTROL_I2C; + dev->out_buf[1] = CH341A_I2C_CMD_OUT | len; + memcpy(&dev->out_buf[2], data, len); + dev->out_buf[2+len] = CH341A_I2C_CMD_END; + + print_hex_dump_bytes(__func__, DUMP_PREFIX_OFFSET, dev->out_buf, len+3); + + ret = usb_bulk_msg(dev->usb_dev, + usb_sndbulkpipe(dev->usb_dev, dev->ep_out), + dev->out_buf, len+3, &actual, TIMEOUT); + if (ret != 0) return ret; + + return 0; +} + +static int ch341a_usb_write_bytes(struct i2c_adapter *adapter, u8 addr, u16 len, u8 *data) +{ + int i, ret; + + ret = ch341a_usb_i2c_start(adapter); + if (ret != 0) return ret; + + ret = ch341a_usb_i2c_write(adapter, 1, &addr); + if (ret != 0) return ret; + + for (i=0; i < len/SEND_PAYLOAD_LENGTH; i++) { + ret = ch341a_usb_i2c_write(adapter, + SEND_PAYLOAD_LENGTH, + &data[i*SEND_PAYLOAD_LENGTH]); + if (ret != 0) return ret; + } + if ( len % SEND_PAYLOAD_LENGTH ) { + ret = ch341a_usb_i2c_write(adapter, + len-(SEND_PAYLOAD_LENGTH*i), + &data[i*SEND_PAYLOAD_LENGTH]); + if (ret != 0) return ret; + } + + ret = ch341a_usb_i2c_stop(adapter); + usleep_range(2500, 2500); + + return ret; +} + +static int ch341a_usb_i2c_read(struct i2c_adapter *adapter, u8 len, u8 *data) +{ + struct i2c_ch341a *dev = (struct i2c_ch341a *)adapter->algo_data; + u8 I2C_CMD_READ_BYTE[] = {CH341A_CONTROL_I2C, 0, CH341A_I2C_CMD_END}; + int ret, actual; + + I2C_CMD_READ_BYTE[1] = CH341A_I2C_CMD_IN | (u8) len; + + ret = usb_bulk_msg(dev->usb_dev, + usb_sndbulkpipe(dev->usb_dev, dev->ep_out), + I2C_CMD_READ_BYTE, sizeof(I2C_CMD_READ_BYTE), &actual, TIMEOUT); + if (ret != 0) return ret; + + ret = usb_bulk_msg(dev->usb_dev, + usb_rcvbulkpipe(dev->usb_dev, dev->ep_in), + dev->in_buf, RECV_PAYLOAD_LENGTH, &actual, TIMEOUT); + if (ret != 0) return ret; + + memcpy(data, dev->in_buf, len); + + return 0; +} + +static int ch341a_usb_i2c_read_bytes(struct i2c_adapter *adapter, u8 addr, u16 len, u8 *data) +{ + int ret, i; + + ret = ch341a_usb_i2c_start(adapter); + if (ret != 0) return ret; + ret = ch341a_usb_i2c_write(adapter, 1, &addr); + if (ret != 0) return ret; + + for (i=0; i < len/RECV_PAYLOAD_LENGTH; i++) { + ret = ch341a_usb_i2c_read(adapter, + RECV_PAYLOAD_LENGTH, + &data[i*RECV_PAYLOAD_LENGTH]); + if (ret != 0) return ret; + } + if ( len % RECV_PAYLOAD_LENGTH ) { + ret = ch341a_usb_i2c_read(adapter, + len-(RECV_PAYLOAD_LENGTH*i), + &data[i*RECV_PAYLOAD_LENGTH]); + if (ret != 0) return ret; + } + + print_hex_dump_bytes(__func__, DUMP_PREFIX_OFFSET, data, len); + + ret = ch341a_usb_i2c_stop(adapter); + if (ret != 0) return ret; + ch341a_usb_i2c_write(adapter, 1, &addr); //should give error so don't check here + + return 0; +} + +static int ch341a_usb_xfer(struct i2c_adapter *adapter, struct i2c_msg *msgs, int num) +{ + struct i2c_msg *pmsg; + int i; + + for (i = 0; i < num; i++) { + pmsg = &msgs[i]; + dev_dbg(&adapter->dev, "%s: (addr=%x, flags=%x, len=%d)", __func__, + pmsg->addr, pmsg->flags, pmsg->len); + if (pmsg->flags & I2C_M_RD) { + if (ch341a_usb_i2c_read_bytes(adapter, + (pmsg->addr<<1) + 1, + pmsg->len, pmsg->buf) != 0) + return -EREMOTEIO; + } else { + if (ch341a_usb_write_bytes(adapter, + (pmsg->addr<<1), pmsg->len, pmsg->buf) != 0) + return -EREMOTEIO; + } + } + return i; +} + +static u32 ch341a_usb_func(struct i2c_adapter *adapter) +{ + return I2C_FUNC_I2C | I2C_FUNC_SMBUS_EMUL; +} + +static ssize_t ch341a_attr_clock_store_value(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) { + + struct i2c_adapter *adapter = to_i2c_adapter(dev); + unsigned int value; + int ret; + + ret = kstrtouint(buf, 10, &value); + + ret = ch341a_usb_i2c_set_speed(adapter, value); + if (ret != 0) return -EINVAL; + + return count; +} + +static ssize_t ch341a_attr_clock_show_value(struct device *dev, + struct device_attribute *attr, + char *buf) { + struct i2c_adapter *adapter = to_i2c_adapter(dev); + struct i2c_ch341a *ch341a_data = (struct i2c_ch341a *)adapter->algo_data; + return sprintf(buf, "%d\n", (ch341a_data->clock_speed)); +} + +static ssize_t ch341a_attr_clock_show_freqs(struct device *dev, + struct device_attribute *attr, + char *buf) { + return sprintf(buf, "20 100 400 750\n"); +} + +static DEVICE_ATTR(clock_frequency, S_IWUSR | S_IRUGO, + ch341a_attr_clock_show_value, + ch341a_attr_clock_store_value); +static DEVICE_ATTR(clock_frequency_available, S_IRUGO, + ch341a_attr_clock_show_freqs, NULL); + +static struct attribute *ch341a_attrs[] = { + &dev_attr_clock_frequency.attr, + &dev_attr_clock_frequency_available.attr, + NULL +}; +ATTRIBUTE_GROUPS(ch341a); + +static const struct i2c_algorithm usb_algorithm = { + .master_xfer = ch341a_usb_xfer, + .functionality = ch341a_usb_func, +}; + +static const struct usb_device_id ch341a_table[] = { + { USB_DEVICE(0x1a86, 0x5512) }, + { } +}; + +MODULE_DEVICE_TABLE(usb, ch341a_table); + +static void ch341a_free(struct i2c_ch341a *dev) +{ + usb_put_dev(dev->usb_dev); + kfree(dev); +} + +static int ch341a_probe(struct usb_interface *interface, + const struct usb_device_id *id) +{ + struct usb_host_interface *hostif = interface->cur_altsetting; + struct i2c_ch341a *dev; + int ret = -ENOMEM; + + dev_dbg(&interface->dev, "probing usb device\n"); + + dev = kzalloc(sizeof(*dev), GFP_KERNEL); + if (dev == NULL) { + dev_err(&interface->dev, "Out of memory\n"); + goto error; + } + + dev->usb_dev = usb_get_dev(interface_to_usbdev(interface)); + dev->interface = interface; + + dev->ep_out = hostif->endpoint[1].desc.bEndpointAddress; + dev->ep_in = hostif->endpoint[0].desc.bEndpointAddress; + + usb_set_intfdata(interface, dev); + + dev->adapter.owner = THIS_MODULE; + dev->adapter.class = I2C_CLASS_HWMON; + dev->adapter.algo = &usb_algorithm; + dev->adapter.algo_data = dev; + dev->adapter.dev.groups = ch341a_groups; + i2c_set_adapdata(&dev->adapter, dev); + snprintf(dev->adapter.name, sizeof(dev->adapter.name), + "i2c-ch341a at bus %03d device %03d", + dev->usb_dev->bus->busnum, dev->usb_dev->devnum); + + dev->adapter.dev.parent = &dev->interface->dev; + + ret = i2c_add_adapter(&dev->adapter); + if (ret < 0) { + goto error; + } + + ret = ch341a_usb_i2c_set_speed(&dev->adapter, 100); + if (ret != 0) return ret; + + dev_info(&dev->adapter.dev, "connected i2c-ch341a device\n"); + + return 0; + + error: + if (dev) + usb_set_intfdata(interface, NULL); + ch341a_free(dev); + + return ret; +} + +static void ch341a_disconnect(struct usb_interface *interface) +{ + struct i2c_ch341a *dev = usb_get_intfdata(interface); + + i2c_del_adapter(&dev->adapter); + usb_set_intfdata(interface, NULL); + ch341a_free(dev); + + dev_dbg(&interface->dev, "disconnected\n"); +} + +#ifdef CONFIG_OF +static const struct of_device_id ch341a_of_match[] = { + { .compatible = "ch341a" }, + { } +}; +MODULE_DEVICE_TABLE(of, ch341a_of_match); +#endif + +static struct usb_driver ch341a_driver = { + .name = "i2c-ch341a", + .probe = ch341a_probe, + .disconnect = ch341a_disconnect, + .id_table = ch341a_table, +}; + +module_usb_driver(ch341a_driver); + +MODULE_AUTHOR("Angelo Compagnucci <angelo.compagnucci@xxxxxxxxx>"); +MODULE_DESCRIPTION("i2c-ch341a driver"); +MODULE_LICENSE("GPL v2"); -- 1.9.1 -- To unsubscribe from this list: send the line "unsubscribe linux-i2c" in the body of a message to majordomo@xxxxxxxxxxxxxxx More majordomo info at http://vger.kernel.org/majordomo-info.html