mirror of
https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git
synced 2024-09-12 05:48:40 +00:00
![Michael Zaidman](/assets/img/avatar_default.png)
A random i2c read operation in EEPROM devices is implemented as a dummy write operation, followed by a current address read operation. The dummy write operation is used to load the target byte or word address (a.k.a offset) into the offset counter, from which the subsequent read operation then reads. To support longer than one HID report size random read, the ft260 driver issues multiple pairs of i2c write offset + read data transactions of HID report size so that the EEPROM device sees many i2c random read requests from different offsets. Two issues with the current implementation: - This approach suffers from extra overhead caused by writing offset requests. - Necessity to handle offset per HID report in big-endian representation as EEPROM devices expect. The current implementation does not do it and correctly handles the reads up to 60 bytes only. This patch addresses both issues by implementing more efficient approach. It issues a single i2c read request of up to the EEPROM page size and then waits for the data to arrive in multiple HID reports. For example, to read the 256 bytes from a 24LC512 chip, which has 128 bytes page size, the old method performs six ft260_i2c_write_read transactions while the new - two only. Before: $ sudo ./i2cperf -d 2 -o 2 -s 128 -r 0-0xff 13 0x51 -S Read block via i2ctransfer by chunks ------------------------------------------------------------------- data rate(bps) efficiency(%) data size(B) total IOs IO size(B) ------------------------------------------------------------------- 40803 85 256 2 128 Kernel log of a single 128 bytes read request: [ +2.376308] ft260_i2c_write_read: read_off 0x0 left_len 128 len 60 [ +0.000002] ft260_i2c_write: rep 0xd0 addr 0x51 off 0 len 2 wlen 2 flag 0x2 d[0] 0x0 [ +0.000707] ft260_xfer_status: bus_status 0x41, clock 100 [ +0.000173] ft260_xfer_status: bus_status 0x40, clock 100 [ +0.000001] ft260_i2c_read: rep 0xc2 addr 0x51 len 60 [ +0.008660] ft260_raw_event: i2c resp: rep 0xde len 60 [ +0.000156] ft260_xfer_status: bus_status 0x20, clock 100 [ +0.000001] ft260_i2c_write_read: read_off 0x3c left_len 68 len 60 [ +0.000001] ft260_i2c_write: rep 0xd0 addr 0x51 off 0 len 2 wlen 2 flag 0x2 d[0] 0x3c [ +0.001034] ft260_xfer_status: bus_status 0x41, clock 100 [ +0.000191] ft260_xfer_status: bus_status 0x40, clock 100 [ +0.000001] ft260_i2c_read: rep 0xc2 addr 0x51 len 60 [ +0.008614] ft260_raw_event: i2c resp: rep 0xde len 60 [ +0.000203] ft260_xfer_status: bus_status 0x20, clock 100 [ +0.000001] ft260_i2c_write_read: read_off 0x78 left_len 8 len 8 [ +0.000001] ft260_i2c_write: rep 0xd0 addr 0x51 off 0 len 2 wlen 2 flag 0x2 d[0] 0x78 [ +0.000987] ft260_xfer_status: bus_status 0x41, clock 100 [ +0.000192] ft260_xfer_status: bus_status 0x40, clock 100 [ +0.000001] ft260_i2c_read: rep 0xc2 addr 0x51 len 8 [ +0.002614] ft260_raw_event: i2c resp: rep 0xd1 len 8 [ +0.000200] ft260_xfer_status: bus_status 0x20, clock 100 After: $ sudo ./i2cperf -d 2 -o 2 -s 128 -r 0-0xff 13 0x51 -S Read block via i2ctransfer by chunks ------------------------------------------------------------------- data rate(bps) efficiency(%) data size(B) total IOs IO size(B) ------------------------------------------------------------------- 43990 85 256 2 128 Kernel log of a single 128 bytes read request: [ +1.464346] ft260_i2c_write_read: off 0x0 rlen 128 wlen 2 [ +0.000002] ft260_i2c_write: rep 0xd0 addr 0x51 off 0 len 2 wlen 2 flag 0x2 d[0] 0x0 [ +0.001653] ft260_xfer_status: bus_status 0x41, clock 100 [ +0.000188] ft260_xfer_status: bus_status 0x40, clock 100 [ +0.000002] ft260_i2c_read: rep 0xc2 addr 0x51 len 128 rlen 60 flag 0x3 [ +0.008609] ft260_raw_event: i2c resp: rep 0xde len 60 [ +0.000157] ft260_xfer_status: bus_status 0x40, clock 100 [ +0.000002] ft260_i2c_read: rep 0xc2 addr 0x51 len 68 rlen 60 flag 0x0 [ +0.008840] ft260_raw_event: i2c resp: rep 0xde len 60 [ +0.000203] ft260_xfer_status: bus_status 0x40, clock 100 [ +0.000002] ft260_i2c_read: rep 0xc2 addr 0x51 len 8 rlen 8 flag 0x4 [ +0.002794] ft260_raw_event: i2c resp: rep 0xd1 len 8 [ +0.000201] ft260_xfer_status: bus_status 0x20, clock 100 Signed-off-by: Michael Zaidman <michael.zaidman@gmail.com> Tested-by: Guillaume Champagne <champagne.guillaume.c@gmail.com> Signed-off-by: Jiri Kosina <jkosina@suse.cz>
1054 lines
26 KiB
C
1054 lines
26 KiB
C
// SPDX-License-Identifier: GPL-2.0-only
|
|
/*
|
|
* hid-ft260.c - FTDI FT260 USB HID to I2C host bridge
|
|
*
|
|
* Copyright (c) 2021, Michael Zaidman <michaelz@xsightlabs.com>
|
|
*
|
|
* Data Sheet:
|
|
* https://www.ftdichip.com/Support/Documents/DataSheets/ICs/DS_FT260.pdf
|
|
*/
|
|
|
|
#include "hid-ids.h"
|
|
#include <linux/hidraw.h>
|
|
#include <linux/i2c.h>
|
|
#include <linux/module.h>
|
|
#include <linux/usb.h>
|
|
|
|
#ifdef DEBUG
|
|
static int ft260_debug = 1;
|
|
#else
|
|
static int ft260_debug;
|
|
#endif
|
|
module_param_named(debug, ft260_debug, int, 0600);
|
|
MODULE_PARM_DESC(debug, "Toggle FT260 debugging messages");
|
|
|
|
#define ft260_dbg(format, arg...) \
|
|
do { \
|
|
if (ft260_debug) \
|
|
pr_info("%s: " format, __func__, ##arg); \
|
|
} while (0)
|
|
|
|
#define FT260_REPORT_MAX_LENGTH (64)
|
|
#define FT260_I2C_DATA_REPORT_ID(len) (FT260_I2C_REPORT_MIN + (len - 1) / 4)
|
|
/*
|
|
* The input report format assigns 62 bytes for the data payload, but ft260
|
|
* returns 60 and 2 in two separate transactions. To minimize transfer time
|
|
* in reading chunks mode, set the maximum read payload length to 60 bytes.
|
|
*/
|
|
#define FT260_RD_DATA_MAX (60)
|
|
#define FT260_WR_DATA_MAX (60)
|
|
|
|
/*
|
|
* Device interface configuration.
|
|
* The FT260 has 2 interfaces that are controlled by DCNF0 and DCNF1 pins.
|
|
* First implementes USB HID to I2C bridge function and
|
|
* second - USB HID to UART bridge function.
|
|
*/
|
|
enum {
|
|
FT260_MODE_ALL = 0x00,
|
|
FT260_MODE_I2C = 0x01,
|
|
FT260_MODE_UART = 0x02,
|
|
FT260_MODE_BOTH = 0x03,
|
|
};
|
|
|
|
/* Control pipe */
|
|
enum {
|
|
FT260_GET_RQST_TYPE = 0xA1,
|
|
FT260_GET_REPORT = 0x01,
|
|
FT260_SET_RQST_TYPE = 0x21,
|
|
FT260_SET_REPORT = 0x09,
|
|
FT260_FEATURE = 0x03,
|
|
};
|
|
|
|
/* Report IDs / Feature In */
|
|
enum {
|
|
FT260_CHIP_VERSION = 0xA0,
|
|
FT260_SYSTEM_SETTINGS = 0xA1,
|
|
FT260_I2C_STATUS = 0xC0,
|
|
FT260_I2C_READ_REQ = 0xC2,
|
|
FT260_I2C_REPORT_MIN = 0xD0,
|
|
FT260_I2C_REPORT_MAX = 0xDE,
|
|
FT260_GPIO = 0xB0,
|
|
FT260_UART_INTERRUPT_STATUS = 0xB1,
|
|
FT260_UART_STATUS = 0xE0,
|
|
FT260_UART_RI_DCD_STATUS = 0xE1,
|
|
FT260_UART_REPORT = 0xF0,
|
|
};
|
|
|
|
/* Feature Out */
|
|
enum {
|
|
FT260_SET_CLOCK = 0x01,
|
|
FT260_SET_I2C_MODE = 0x02,
|
|
FT260_SET_UART_MODE = 0x03,
|
|
FT260_ENABLE_INTERRUPT = 0x05,
|
|
FT260_SELECT_GPIO2_FUNC = 0x06,
|
|
FT260_ENABLE_UART_DCD_RI = 0x07,
|
|
FT260_SELECT_GPIOA_FUNC = 0x08,
|
|
FT260_SELECT_GPIOG_FUNC = 0x09,
|
|
FT260_SET_INTERRUPT_TRIGGER = 0x0A,
|
|
FT260_SET_SUSPEND_OUT_POLAR = 0x0B,
|
|
FT260_ENABLE_UART_RI_WAKEUP = 0x0C,
|
|
FT260_SET_UART_RI_WAKEUP_CFG = 0x0D,
|
|
FT260_SET_I2C_RESET = 0x20,
|
|
FT260_SET_I2C_CLOCK_SPEED = 0x22,
|
|
FT260_SET_UART_RESET = 0x40,
|
|
FT260_SET_UART_CONFIG = 0x41,
|
|
FT260_SET_UART_BAUD_RATE = 0x42,
|
|
FT260_SET_UART_DATA_BIT = 0x43,
|
|
FT260_SET_UART_PARITY = 0x44,
|
|
FT260_SET_UART_STOP_BIT = 0x45,
|
|
FT260_SET_UART_BREAKING = 0x46,
|
|
FT260_SET_UART_XON_XOFF = 0x49,
|
|
};
|
|
|
|
/* Response codes in I2C status report */
|
|
enum {
|
|
FT260_I2C_STATUS_SUCCESS = 0x00,
|
|
FT260_I2C_STATUS_CTRL_BUSY = 0x01,
|
|
FT260_I2C_STATUS_ERROR = 0x02,
|
|
FT260_I2C_STATUS_ADDR_NO_ACK = 0x04,
|
|
FT260_I2C_STATUS_DATA_NO_ACK = 0x08,
|
|
FT260_I2C_STATUS_ARBITR_LOST = 0x10,
|
|
FT260_I2C_STATUS_CTRL_IDLE = 0x20,
|
|
FT260_I2C_STATUS_BUS_BUSY = 0x40,
|
|
};
|
|
|
|
/* I2C Conditions flags */
|
|
enum {
|
|
FT260_FLAG_NONE = 0x00,
|
|
FT260_FLAG_START = 0x02,
|
|
FT260_FLAG_START_REPEATED = 0x03,
|
|
FT260_FLAG_STOP = 0x04,
|
|
FT260_FLAG_START_STOP = 0x06,
|
|
FT260_FLAG_START_STOP_REPEATED = 0x07,
|
|
};
|
|
|
|
#define FT260_SET_REQUEST_VALUE(report_id) ((FT260_FEATURE << 8) | report_id)
|
|
|
|
/* Feature In reports */
|
|
|
|
struct ft260_get_chip_version_report {
|
|
u8 report; /* FT260_CHIP_VERSION */
|
|
u8 chip_code[4]; /* FTDI chip identification code */
|
|
u8 reserved[8];
|
|
} __packed;
|
|
|
|
struct ft260_get_system_status_report {
|
|
u8 report; /* FT260_SYSTEM_SETTINGS */
|
|
u8 chip_mode; /* DCNF0 and DCNF1 status, bits 0-1 */
|
|
u8 clock_ctl; /* 0 - 12MHz, 1 - 24MHz, 2 - 48MHz */
|
|
u8 suspend_status; /* 0 - not suspended, 1 - suspended */
|
|
u8 pwren_status; /* 0 - FT260 is not ready, 1 - ready */
|
|
u8 i2c_enable; /* 0 - disabled, 1 - enabled */
|
|
u8 uart_mode; /* 0 - OFF; 1 - RTS_CTS, 2 - DTR_DSR, */
|
|
/* 3 - XON_XOFF, 4 - No flow control */
|
|
u8 hid_over_i2c_en; /* 0 - disabled, 1 - enabled */
|
|
u8 gpio2_function; /* 0 - GPIO, 1 - SUSPOUT, */
|
|
/* 2 - PWREN, 4 - TX_LED */
|
|
u8 gpioA_function; /* 0 - GPIO, 3 - TX_ACTIVE, 4 - TX_LED */
|
|
u8 gpioG_function; /* 0 - GPIO, 2 - PWREN, */
|
|
/* 5 - RX_LED, 6 - BCD_DET */
|
|
u8 suspend_out_pol; /* 0 - active-high, 1 - active-low */
|
|
u8 enable_wakeup_int; /* 0 - disabled, 1 - enabled */
|
|
u8 intr_cond; /* Interrupt trigger conditions */
|
|
u8 power_saving_en; /* 0 - disabled, 1 - enabled */
|
|
u8 reserved[10];
|
|
} __packed;
|
|
|
|
struct ft260_get_i2c_status_report {
|
|
u8 report; /* FT260_I2C_STATUS */
|
|
u8 bus_status; /* I2C bus status */
|
|
__le16 clock; /* I2C bus clock in range 60-3400 KHz */
|
|
u8 reserved;
|
|
} __packed;
|
|
|
|
/* Feature Out reports */
|
|
|
|
struct ft260_set_system_clock_report {
|
|
u8 report; /* FT260_SYSTEM_SETTINGS */
|
|
u8 request; /* FT260_SET_CLOCK */
|
|
u8 clock_ctl; /* 0 - 12MHz, 1 - 24MHz, 2 - 48MHz */
|
|
} __packed;
|
|
|
|
struct ft260_set_i2c_mode_report {
|
|
u8 report; /* FT260_SYSTEM_SETTINGS */
|
|
u8 request; /* FT260_SET_I2C_MODE */
|
|
u8 i2c_enable; /* 0 - disabled, 1 - enabled */
|
|
} __packed;
|
|
|
|
struct ft260_set_uart_mode_report {
|
|
u8 report; /* FT260_SYSTEM_SETTINGS */
|
|
u8 request; /* FT260_SET_UART_MODE */
|
|
u8 uart_mode; /* 0 - OFF; 1 - RTS_CTS, 2 - DTR_DSR, */
|
|
/* 3 - XON_XOFF, 4 - No flow control */
|
|
} __packed;
|
|
|
|
struct ft260_set_i2c_reset_report {
|
|
u8 report; /* FT260_SYSTEM_SETTINGS */
|
|
u8 request; /* FT260_SET_I2C_RESET */
|
|
} __packed;
|
|
|
|
struct ft260_set_i2c_speed_report {
|
|
u8 report; /* FT260_SYSTEM_SETTINGS */
|
|
u8 request; /* FT260_SET_I2C_CLOCK_SPEED */
|
|
__le16 clock; /* I2C bus clock in range 60-3400 KHz */
|
|
} __packed;
|
|
|
|
/* Data transfer reports */
|
|
|
|
struct ft260_i2c_write_request_report {
|
|
u8 report; /* FT260_I2C_REPORT */
|
|
u8 address; /* 7-bit I2C address */
|
|
u8 flag; /* I2C transaction condition */
|
|
u8 length; /* data payload length */
|
|
u8 data[FT260_WR_DATA_MAX]; /* data payload */
|
|
} __packed;
|
|
|
|
struct ft260_i2c_read_request_report {
|
|
u8 report; /* FT260_I2C_READ_REQ */
|
|
u8 address; /* 7-bit I2C address */
|
|
u8 flag; /* I2C transaction condition */
|
|
__le16 length; /* data payload length */
|
|
} __packed;
|
|
|
|
struct ft260_i2c_input_report {
|
|
u8 report; /* FT260_I2C_REPORT */
|
|
u8 length; /* data payload length */
|
|
u8 data[2]; /* data payload */
|
|
} __packed;
|
|
|
|
static const struct hid_device_id ft260_devices[] = {
|
|
{ HID_USB_DEVICE(USB_VENDOR_ID_FUTURE_TECHNOLOGY,
|
|
USB_DEVICE_ID_FT260) },
|
|
{ /* END OF LIST */ }
|
|
};
|
|
MODULE_DEVICE_TABLE(hid, ft260_devices);
|
|
|
|
struct ft260_device {
|
|
struct i2c_adapter adap;
|
|
struct hid_device *hdev;
|
|
struct completion wait;
|
|
struct mutex lock;
|
|
u8 write_buf[FT260_REPORT_MAX_LENGTH];
|
|
u8 *read_buf;
|
|
u16 read_idx;
|
|
u16 read_len;
|
|
u16 clock;
|
|
};
|
|
|
|
static int ft260_hid_feature_report_get(struct hid_device *hdev,
|
|
unsigned char report_id, u8 *data,
|
|
size_t len)
|
|
{
|
|
u8 *buf;
|
|
int ret;
|
|
|
|
buf = kmalloc(len, GFP_KERNEL);
|
|
if (!buf)
|
|
return -ENOMEM;
|
|
|
|
ret = hid_hw_raw_request(hdev, report_id, buf, len, HID_FEATURE_REPORT,
|
|
HID_REQ_GET_REPORT);
|
|
if (likely(ret == len))
|
|
memcpy(data, buf, len);
|
|
else if (ret >= 0)
|
|
ret = -EIO;
|
|
kfree(buf);
|
|
return ret;
|
|
}
|
|
|
|
static int ft260_hid_feature_report_set(struct hid_device *hdev, u8 *data,
|
|
size_t len)
|
|
{
|
|
u8 *buf;
|
|
int ret;
|
|
|
|
buf = kmemdup(data, len, GFP_KERNEL);
|
|
if (!buf)
|
|
return -ENOMEM;
|
|
|
|
buf[0] = FT260_SYSTEM_SETTINGS;
|
|
|
|
ret = hid_hw_raw_request(hdev, buf[0], buf, len, HID_FEATURE_REPORT,
|
|
HID_REQ_SET_REPORT);
|
|
|
|
kfree(buf);
|
|
return ret;
|
|
}
|
|
|
|
static int ft260_i2c_reset(struct hid_device *hdev)
|
|
{
|
|
struct ft260_set_i2c_reset_report report;
|
|
int ret;
|
|
|
|
report.request = FT260_SET_I2C_RESET;
|
|
|
|
ret = ft260_hid_feature_report_set(hdev, (u8 *)&report, sizeof(report));
|
|
if (ret < 0) {
|
|
hid_err(hdev, "failed to reset I2C controller: %d\n", ret);
|
|
return ret;
|
|
}
|
|
|
|
ft260_dbg("done\n");
|
|
return ret;
|
|
}
|
|
|
|
static int ft260_xfer_status(struct ft260_device *dev)
|
|
{
|
|
struct hid_device *hdev = dev->hdev;
|
|
struct ft260_get_i2c_status_report report;
|
|
int ret;
|
|
|
|
ret = ft260_hid_feature_report_get(hdev, FT260_I2C_STATUS,
|
|
(u8 *)&report, sizeof(report));
|
|
if (unlikely(ret < 0)) {
|
|
hid_err(hdev, "failed to retrieve status: %d\n", ret);
|
|
return ret;
|
|
}
|
|
|
|
dev->clock = le16_to_cpu(report.clock);
|
|
ft260_dbg("bus_status %#02x, clock %u\n", report.bus_status,
|
|
dev->clock);
|
|
|
|
if (report.bus_status & FT260_I2C_STATUS_CTRL_BUSY)
|
|
return -EAGAIN;
|
|
|
|
/*
|
|
* The error condition (bit 1) is a status bit reflecting any
|
|
* error conditions. When any of the bits 2, 3, or 4 are raised
|
|
* to 1, bit 1 is also set to 1.
|
|
*/
|
|
if (report.bus_status & FT260_I2C_STATUS_ERROR) {
|
|
hid_err(hdev, "i2c bus error: %#02x\n", report.bus_status);
|
|
return -EIO;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int ft260_hid_output_report(struct hid_device *hdev, u8 *data,
|
|
size_t len)
|
|
{
|
|
u8 *buf;
|
|
int ret;
|
|
|
|
buf = kmemdup(data, len, GFP_KERNEL);
|
|
if (!buf)
|
|
return -ENOMEM;
|
|
|
|
ret = hid_hw_output_report(hdev, buf, len);
|
|
|
|
kfree(buf);
|
|
return ret;
|
|
}
|
|
|
|
static int ft260_hid_output_report_check_status(struct ft260_device *dev,
|
|
u8 *data, int len)
|
|
{
|
|
int ret, usec, try = 100;
|
|
struct hid_device *hdev = dev->hdev;
|
|
|
|
ret = ft260_hid_output_report(hdev, data, len);
|
|
if (ret < 0) {
|
|
hid_err(hdev, "%s: failed to start transfer, ret %d\n",
|
|
__func__, ret);
|
|
ft260_i2c_reset(hdev);
|
|
return ret;
|
|
}
|
|
|
|
/* transfer time = 1 / clock(KHz) * 9 bits * bytes */
|
|
usec = len * 9000 / dev->clock;
|
|
if (usec > 2000) {
|
|
usec -= 1500;
|
|
usleep_range(usec, usec + 100);
|
|
ft260_dbg("wait %d usec, len %d\n", usec, len);
|
|
}
|
|
|
|
do {
|
|
ret = ft260_xfer_status(dev);
|
|
if (ret != -EAGAIN)
|
|
break;
|
|
} while (--try);
|
|
|
|
if (ret == 0)
|
|
return 0;
|
|
|
|
ft260_i2c_reset(hdev);
|
|
return -EIO;
|
|
}
|
|
|
|
static int ft260_i2c_write(struct ft260_device *dev, u8 addr, u8 *data,
|
|
int len, u8 flag)
|
|
{
|
|
int ret, wr_len, idx = 0;
|
|
struct hid_device *hdev = dev->hdev;
|
|
struct ft260_i2c_write_request_report *rep =
|
|
(struct ft260_i2c_write_request_report *)dev->write_buf;
|
|
|
|
rep->flag = FT260_FLAG_START;
|
|
|
|
do {
|
|
if (len <= FT260_WR_DATA_MAX) {
|
|
wr_len = len;
|
|
if (flag == FT260_FLAG_START_STOP)
|
|
rep->flag |= FT260_FLAG_STOP;
|
|
} else {
|
|
wr_len = FT260_WR_DATA_MAX;
|
|
}
|
|
|
|
rep->report = FT260_I2C_DATA_REPORT_ID(wr_len);
|
|
rep->address = addr;
|
|
rep->length = wr_len;
|
|
|
|
memcpy(rep->data, &data[idx], wr_len);
|
|
|
|
ft260_dbg("rep %#02x addr %#02x off %d len %d wlen %d flag %#x d[0] %#02x\n",
|
|
rep->report, addr, idx, len, wr_len,
|
|
rep->flag, data[0]);
|
|
|
|
ret = ft260_hid_output_report_check_status(dev, (u8 *)rep,
|
|
wr_len + 4);
|
|
if (ret < 0) {
|
|
hid_err(hdev, "%s: failed with %d\n", __func__, ret);
|
|
return ret;
|
|
}
|
|
|
|
len -= wr_len;
|
|
idx += wr_len;
|
|
rep->flag = 0;
|
|
|
|
} while (len > 0);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int ft260_smbus_write(struct ft260_device *dev, u8 addr, u8 cmd,
|
|
u8 *data, u8 data_len, u8 flag)
|
|
{
|
|
int ret = 0;
|
|
int len = 4;
|
|
|
|
struct ft260_i2c_write_request_report *rep =
|
|
(struct ft260_i2c_write_request_report *)dev->write_buf;
|
|
|
|
if (data_len >= sizeof(rep->data))
|
|
return -EINVAL;
|
|
|
|
rep->address = addr;
|
|
rep->data[0] = cmd;
|
|
rep->length = data_len + 1;
|
|
rep->flag = flag;
|
|
len += rep->length;
|
|
|
|
rep->report = FT260_I2C_DATA_REPORT_ID(len);
|
|
|
|
if (data_len > 0)
|
|
memcpy(&rep->data[1], data, data_len);
|
|
|
|
ft260_dbg("rep %#02x addr %#02x cmd %#02x datlen %d replen %d\n",
|
|
rep->report, addr, cmd, rep->length, len);
|
|
|
|
ret = ft260_hid_output_report_check_status(dev, (u8 *)rep, len);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int ft260_i2c_read(struct ft260_device *dev, u8 addr, u8 *data,
|
|
u16 len, u8 flag)
|
|
{
|
|
u16 rd_len;
|
|
int timeout, ret;
|
|
struct ft260_i2c_read_request_report rep;
|
|
struct hid_device *hdev = dev->hdev;
|
|
|
|
if ((flag & FT260_FLAG_START_REPEATED) == FT260_FLAG_START_REPEATED)
|
|
flag = FT260_FLAG_START_REPEATED;
|
|
else
|
|
flag = FT260_FLAG_START;
|
|
do {
|
|
if (len <= FT260_RD_DATA_MAX) {
|
|
rd_len = len;
|
|
flag |= FT260_FLAG_STOP;
|
|
} else {
|
|
rd_len = FT260_RD_DATA_MAX;
|
|
}
|
|
|
|
dev->read_idx = 0;
|
|
dev->read_buf = data;
|
|
dev->read_len = rd_len;
|
|
|
|
rep.report = FT260_I2C_READ_REQ;
|
|
rep.length = cpu_to_le16(rd_len);
|
|
rep.address = addr;
|
|
rep.flag = flag;
|
|
|
|
ft260_dbg("rep %#02x addr %#02x len %d rlen %d flag %#x\n",
|
|
rep.report, rep.address, len, rd_len, flag);
|
|
|
|
reinit_completion(&dev->wait);
|
|
|
|
ret = ft260_hid_output_report(hdev, (u8 *)&rep, sizeof(rep));
|
|
if (ret < 0) {
|
|
hid_err(hdev, "%s: failed with %d\n", __func__, ret);
|
|
return ret;
|
|
}
|
|
|
|
timeout = msecs_to_jiffies(5000);
|
|
if (!wait_for_completion_timeout(&dev->wait, timeout)) {
|
|
ft260_i2c_reset(hdev);
|
|
return -ETIMEDOUT;
|
|
}
|
|
|
|
ret = ft260_xfer_status(dev);
|
|
if (ret < 0) {
|
|
ft260_i2c_reset(hdev);
|
|
return -EIO;
|
|
}
|
|
|
|
len -= rd_len;
|
|
data += rd_len;
|
|
flag = 0;
|
|
|
|
} while (len > 0);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* A random read operation is implemented as a dummy write operation, followed
|
|
* by a current address read operation. The dummy write operation is used to
|
|
* load the target byte address into the current byte address counter, from
|
|
* which the subsequent current address read operation then reads.
|
|
*/
|
|
static int ft260_i2c_write_read(struct ft260_device *dev, struct i2c_msg *msgs)
|
|
{
|
|
int ret;
|
|
int wr_len = msgs[0].len;
|
|
int rd_len = msgs[1].len;
|
|
struct hid_device *hdev = dev->hdev;
|
|
u8 addr = msgs[0].addr;
|
|
u16 read_off = 0;
|
|
|
|
if (wr_len > 2) {
|
|
hid_err(hdev, "%s: invalid wr_len: %d\n", __func__, wr_len);
|
|
return -EOPNOTSUPP;
|
|
}
|
|
|
|
if (ft260_debug) {
|
|
if (wr_len == 2)
|
|
read_off = be16_to_cpu(*(u16 *)msgs[0].buf);
|
|
else
|
|
read_off = *msgs[0].buf;
|
|
|
|
pr_info("%s: off %#x rlen %d wlen %d\n", __func__,
|
|
read_off, rd_len, wr_len);
|
|
}
|
|
|
|
ret = ft260_i2c_write(dev, addr, msgs[0].buf, wr_len,
|
|
FT260_FLAG_START);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
ret = ft260_i2c_read(dev, addr, msgs[1].buf, rd_len,
|
|
FT260_FLAG_START_STOP_REPEATED);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int ft260_i2c_xfer(struct i2c_adapter *adapter, struct i2c_msg *msgs,
|
|
int num)
|
|
{
|
|
int ret;
|
|
struct ft260_device *dev = i2c_get_adapdata(adapter);
|
|
struct hid_device *hdev = dev->hdev;
|
|
|
|
mutex_lock(&dev->lock);
|
|
|
|
ret = hid_hw_power(hdev, PM_HINT_FULLON);
|
|
if (ret < 0) {
|
|
hid_err(hdev, "failed to enter FULLON power mode: %d\n", ret);
|
|
mutex_unlock(&dev->lock);
|
|
return ret;
|
|
}
|
|
|
|
if (num == 1) {
|
|
if (msgs->flags & I2C_M_RD)
|
|
ret = ft260_i2c_read(dev, msgs->addr, msgs->buf,
|
|
msgs->len, FT260_FLAG_START_STOP);
|
|
else
|
|
ret = ft260_i2c_write(dev, msgs->addr, msgs->buf,
|
|
msgs->len, FT260_FLAG_START_STOP);
|
|
if (ret < 0)
|
|
goto i2c_exit;
|
|
|
|
} else {
|
|
/* Combined write then read message */
|
|
ret = ft260_i2c_write_read(dev, msgs);
|
|
if (ret < 0)
|
|
goto i2c_exit;
|
|
}
|
|
|
|
ret = num;
|
|
i2c_exit:
|
|
hid_hw_power(hdev, PM_HINT_NORMAL);
|
|
mutex_unlock(&dev->lock);
|
|
return ret;
|
|
}
|
|
|
|
static int ft260_smbus_xfer(struct i2c_adapter *adapter, u16 addr, u16 flags,
|
|
char read_write, u8 cmd, int size,
|
|
union i2c_smbus_data *data)
|
|
{
|
|
int ret;
|
|
struct ft260_device *dev = i2c_get_adapdata(adapter);
|
|
struct hid_device *hdev = dev->hdev;
|
|
|
|
ft260_dbg("smbus size %d\n", size);
|
|
|
|
mutex_lock(&dev->lock);
|
|
|
|
ret = hid_hw_power(hdev, PM_HINT_FULLON);
|
|
if (ret < 0) {
|
|
hid_err(hdev, "power management error: %d\n", ret);
|
|
mutex_unlock(&dev->lock);
|
|
return ret;
|
|
}
|
|
|
|
switch (size) {
|
|
case I2C_SMBUS_QUICK:
|
|
if (read_write == I2C_SMBUS_READ)
|
|
ret = ft260_i2c_read(dev, addr, &data->byte, 0,
|
|
FT260_FLAG_START_STOP);
|
|
else
|
|
ret = ft260_smbus_write(dev, addr, cmd, NULL, 0,
|
|
FT260_FLAG_START_STOP);
|
|
break;
|
|
case I2C_SMBUS_BYTE:
|
|
if (read_write == I2C_SMBUS_READ)
|
|
ret = ft260_i2c_read(dev, addr, &data->byte, 1,
|
|
FT260_FLAG_START_STOP);
|
|
else
|
|
ret = ft260_smbus_write(dev, addr, cmd, NULL, 0,
|
|
FT260_FLAG_START_STOP);
|
|
break;
|
|
case I2C_SMBUS_BYTE_DATA:
|
|
if (read_write == I2C_SMBUS_READ) {
|
|
ret = ft260_smbus_write(dev, addr, cmd, NULL, 0,
|
|
FT260_FLAG_START);
|
|
if (ret)
|
|
goto smbus_exit;
|
|
|
|
ret = ft260_i2c_read(dev, addr, &data->byte, 1,
|
|
FT260_FLAG_START_STOP_REPEATED);
|
|
} else {
|
|
ret = ft260_smbus_write(dev, addr, cmd, &data->byte, 1,
|
|
FT260_FLAG_START_STOP);
|
|
}
|
|
break;
|
|
case I2C_SMBUS_WORD_DATA:
|
|
if (read_write == I2C_SMBUS_READ) {
|
|
ret = ft260_smbus_write(dev, addr, cmd, NULL, 0,
|
|
FT260_FLAG_START);
|
|
if (ret)
|
|
goto smbus_exit;
|
|
|
|
ret = ft260_i2c_read(dev, addr, (u8 *)&data->word, 2,
|
|
FT260_FLAG_START_STOP_REPEATED);
|
|
} else {
|
|
ret = ft260_smbus_write(dev, addr, cmd,
|
|
(u8 *)&data->word, 2,
|
|
FT260_FLAG_START_STOP);
|
|
}
|
|
break;
|
|
case I2C_SMBUS_BLOCK_DATA:
|
|
if (read_write == I2C_SMBUS_READ) {
|
|
ret = ft260_smbus_write(dev, addr, cmd, NULL, 0,
|
|
FT260_FLAG_START);
|
|
if (ret)
|
|
goto smbus_exit;
|
|
|
|
ret = ft260_i2c_read(dev, addr, data->block,
|
|
data->block[0] + 1,
|
|
FT260_FLAG_START_STOP_REPEATED);
|
|
} else {
|
|
ret = ft260_smbus_write(dev, addr, cmd, data->block,
|
|
data->block[0] + 1,
|
|
FT260_FLAG_START_STOP);
|
|
}
|
|
break;
|
|
case I2C_SMBUS_I2C_BLOCK_DATA:
|
|
if (read_write == I2C_SMBUS_READ) {
|
|
ret = ft260_smbus_write(dev, addr, cmd, NULL, 0,
|
|
FT260_FLAG_START);
|
|
if (ret)
|
|
goto smbus_exit;
|
|
|
|
ret = ft260_i2c_read(dev, addr, data->block + 1,
|
|
data->block[0],
|
|
FT260_FLAG_START_STOP_REPEATED);
|
|
} else {
|
|
ret = ft260_smbus_write(dev, addr, cmd, data->block + 1,
|
|
data->block[0],
|
|
FT260_FLAG_START_STOP);
|
|
}
|
|
break;
|
|
default:
|
|
hid_err(hdev, "unsupported smbus transaction size %d\n", size);
|
|
ret = -EOPNOTSUPP;
|
|
}
|
|
|
|
smbus_exit:
|
|
hid_hw_power(hdev, PM_HINT_NORMAL);
|
|
mutex_unlock(&dev->lock);
|
|
return ret;
|
|
}
|
|
|
|
static u32 ft260_functionality(struct i2c_adapter *adap)
|
|
{
|
|
return I2C_FUNC_I2C | I2C_FUNC_SMBUS_BYTE | I2C_FUNC_SMBUS_QUICK |
|
|
I2C_FUNC_SMBUS_BYTE_DATA | I2C_FUNC_SMBUS_WORD_DATA |
|
|
I2C_FUNC_SMBUS_BLOCK_DATA | I2C_FUNC_SMBUS_I2C_BLOCK;
|
|
}
|
|
|
|
static const struct i2c_adapter_quirks ft260_i2c_quirks = {
|
|
.flags = I2C_AQ_COMB_WRITE_THEN_READ,
|
|
.max_comb_1st_msg_len = 2,
|
|
};
|
|
|
|
static const struct i2c_algorithm ft260_i2c_algo = {
|
|
.master_xfer = ft260_i2c_xfer,
|
|
.smbus_xfer = ft260_smbus_xfer,
|
|
.functionality = ft260_functionality,
|
|
};
|
|
|
|
static int ft260_get_system_config(struct hid_device *hdev,
|
|
struct ft260_get_system_status_report *cfg)
|
|
{
|
|
int ret;
|
|
int len = sizeof(struct ft260_get_system_status_report);
|
|
|
|
ret = ft260_hid_feature_report_get(hdev, FT260_SYSTEM_SETTINGS,
|
|
(u8 *)cfg, len);
|
|
if (ret < 0) {
|
|
hid_err(hdev, "failed to retrieve system status\n");
|
|
return ret;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int ft260_is_interface_enabled(struct hid_device *hdev)
|
|
{
|
|
struct ft260_get_system_status_report cfg;
|
|
struct usb_interface *usbif = to_usb_interface(hdev->dev.parent);
|
|
int interface = usbif->cur_altsetting->desc.bInterfaceNumber;
|
|
int ret;
|
|
|
|
ret = ft260_get_system_config(hdev, &cfg);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
ft260_dbg("interface: 0x%02x\n", interface);
|
|
ft260_dbg("chip mode: 0x%02x\n", cfg.chip_mode);
|
|
ft260_dbg("clock_ctl: 0x%02x\n", cfg.clock_ctl);
|
|
ft260_dbg("i2c_enable: 0x%02x\n", cfg.i2c_enable);
|
|
ft260_dbg("uart_mode: 0x%02x\n", cfg.uart_mode);
|
|
|
|
switch (cfg.chip_mode) {
|
|
case FT260_MODE_ALL:
|
|
case FT260_MODE_BOTH:
|
|
if (interface == 1)
|
|
hid_info(hdev, "uart interface is not supported\n");
|
|
else
|
|
ret = 1;
|
|
break;
|
|
case FT260_MODE_UART:
|
|
hid_info(hdev, "uart interface is not supported\n");
|
|
break;
|
|
case FT260_MODE_I2C:
|
|
ret = 1;
|
|
break;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
static int ft260_byte_show(struct hid_device *hdev, int id, u8 *cfg, int len,
|
|
u8 *field, u8 *buf)
|
|
{
|
|
int ret;
|
|
|
|
ret = ft260_hid_feature_report_get(hdev, id, cfg, len);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
return scnprintf(buf, PAGE_SIZE, "%d\n", *field);
|
|
}
|
|
|
|
static int ft260_word_show(struct hid_device *hdev, int id, u8 *cfg, int len,
|
|
u16 *field, u8 *buf)
|
|
{
|
|
int ret;
|
|
|
|
ret = ft260_hid_feature_report_get(hdev, id, cfg, len);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
return scnprintf(buf, PAGE_SIZE, "%d\n", le16_to_cpu(*field));
|
|
}
|
|
|
|
#define FT260_ATTR_SHOW(name, reptype, id, type, func) \
|
|
static ssize_t name##_show(struct device *kdev, \
|
|
struct device_attribute *attr, char *buf) \
|
|
{ \
|
|
struct reptype rep; \
|
|
struct hid_device *hdev = to_hid_device(kdev); \
|
|
type *field = &rep.name; \
|
|
int len = sizeof(rep); \
|
|
\
|
|
return func(hdev, id, (u8 *)&rep, len, field, buf); \
|
|
}
|
|
|
|
#define FT260_SSTAT_ATTR_SHOW(name) \
|
|
FT260_ATTR_SHOW(name, ft260_get_system_status_report, \
|
|
FT260_SYSTEM_SETTINGS, u8, ft260_byte_show)
|
|
|
|
#define FT260_I2CST_ATTR_SHOW(name) \
|
|
FT260_ATTR_SHOW(name, ft260_get_i2c_status_report, \
|
|
FT260_I2C_STATUS, u16, ft260_word_show)
|
|
|
|
#define FT260_ATTR_STORE(name, reptype, id, req, type, func) \
|
|
static ssize_t name##_store(struct device *kdev, \
|
|
struct device_attribute *attr, \
|
|
const char *buf, size_t count) \
|
|
{ \
|
|
struct reptype rep; \
|
|
struct hid_device *hdev = to_hid_device(kdev); \
|
|
type name; \
|
|
int ret; \
|
|
\
|
|
if (!func(buf, 10, &name)) { \
|
|
rep.name = name; \
|
|
rep.report = id; \
|
|
rep.request = req; \
|
|
ret = ft260_hid_feature_report_set(hdev, (u8 *)&rep, \
|
|
sizeof(rep)); \
|
|
if (!ret) \
|
|
ret = count; \
|
|
} else { \
|
|
ret = -EINVAL; \
|
|
} \
|
|
return ret; \
|
|
}
|
|
|
|
#define FT260_BYTE_ATTR_STORE(name, reptype, req) \
|
|
FT260_ATTR_STORE(name, reptype, FT260_SYSTEM_SETTINGS, req, \
|
|
u8, kstrtou8)
|
|
|
|
#define FT260_WORD_ATTR_STORE(name, reptype, req) \
|
|
FT260_ATTR_STORE(name, reptype, FT260_SYSTEM_SETTINGS, req, \
|
|
u16, kstrtou16)
|
|
|
|
FT260_SSTAT_ATTR_SHOW(chip_mode);
|
|
static DEVICE_ATTR_RO(chip_mode);
|
|
|
|
FT260_SSTAT_ATTR_SHOW(pwren_status);
|
|
static DEVICE_ATTR_RO(pwren_status);
|
|
|
|
FT260_SSTAT_ATTR_SHOW(suspend_status);
|
|
static DEVICE_ATTR_RO(suspend_status);
|
|
|
|
FT260_SSTAT_ATTR_SHOW(hid_over_i2c_en);
|
|
static DEVICE_ATTR_RO(hid_over_i2c_en);
|
|
|
|
FT260_SSTAT_ATTR_SHOW(power_saving_en);
|
|
static DEVICE_ATTR_RO(power_saving_en);
|
|
|
|
FT260_SSTAT_ATTR_SHOW(i2c_enable);
|
|
FT260_BYTE_ATTR_STORE(i2c_enable, ft260_set_i2c_mode_report,
|
|
FT260_SET_I2C_MODE);
|
|
static DEVICE_ATTR_RW(i2c_enable);
|
|
|
|
FT260_SSTAT_ATTR_SHOW(uart_mode);
|
|
FT260_BYTE_ATTR_STORE(uart_mode, ft260_set_uart_mode_report,
|
|
FT260_SET_UART_MODE);
|
|
static DEVICE_ATTR_RW(uart_mode);
|
|
|
|
FT260_SSTAT_ATTR_SHOW(clock_ctl);
|
|
FT260_BYTE_ATTR_STORE(clock_ctl, ft260_set_system_clock_report,
|
|
FT260_SET_CLOCK);
|
|
static DEVICE_ATTR_RW(clock_ctl);
|
|
|
|
FT260_I2CST_ATTR_SHOW(clock);
|
|
FT260_WORD_ATTR_STORE(clock, ft260_set_i2c_speed_report,
|
|
FT260_SET_I2C_CLOCK_SPEED);
|
|
static DEVICE_ATTR_RW(clock);
|
|
|
|
static ssize_t i2c_reset_store(struct device *kdev,
|
|
struct device_attribute *attr, const char *buf,
|
|
size_t count)
|
|
{
|
|
struct hid_device *hdev = to_hid_device(kdev);
|
|
int ret = ft260_i2c_reset(hdev);
|
|
|
|
if (ret)
|
|
return ret;
|
|
return count;
|
|
}
|
|
static DEVICE_ATTR_WO(i2c_reset);
|
|
|
|
static const struct attribute_group ft260_attr_group = {
|
|
.attrs = (struct attribute *[]) {
|
|
&dev_attr_chip_mode.attr,
|
|
&dev_attr_pwren_status.attr,
|
|
&dev_attr_suspend_status.attr,
|
|
&dev_attr_hid_over_i2c_en.attr,
|
|
&dev_attr_power_saving_en.attr,
|
|
&dev_attr_i2c_enable.attr,
|
|
&dev_attr_uart_mode.attr,
|
|
&dev_attr_clock_ctl.attr,
|
|
&dev_attr_i2c_reset.attr,
|
|
&dev_attr_clock.attr,
|
|
NULL
|
|
}
|
|
};
|
|
|
|
static int ft260_probe(struct hid_device *hdev, const struct hid_device_id *id)
|
|
{
|
|
struct ft260_device *dev;
|
|
struct ft260_get_chip_version_report version;
|
|
int ret;
|
|
|
|
if (!hid_is_usb(hdev))
|
|
return -EINVAL;
|
|
|
|
dev = devm_kzalloc(&hdev->dev, sizeof(*dev), GFP_KERNEL);
|
|
if (!dev)
|
|
return -ENOMEM;
|
|
|
|
ret = hid_parse(hdev);
|
|
if (ret) {
|
|
hid_err(hdev, "failed to parse HID\n");
|
|
return ret;
|
|
}
|
|
|
|
ret = hid_hw_start(hdev, HID_CONNECT_HIDRAW);
|
|
if (ret) {
|
|
hid_err(hdev, "failed to start HID HW\n");
|
|
return ret;
|
|
}
|
|
|
|
ret = hid_hw_open(hdev);
|
|
if (ret) {
|
|
hid_err(hdev, "failed to open HID HW\n");
|
|
goto err_hid_stop;
|
|
}
|
|
|
|
ret = ft260_hid_feature_report_get(hdev, FT260_CHIP_VERSION,
|
|
(u8 *)&version, sizeof(version));
|
|
if (ret < 0) {
|
|
hid_err(hdev, "failed to retrieve chip version\n");
|
|
goto err_hid_close;
|
|
}
|
|
|
|
hid_info(hdev, "chip code: %02x%02x %02x%02x\n",
|
|
version.chip_code[0], version.chip_code[1],
|
|
version.chip_code[2], version.chip_code[3]);
|
|
|
|
ret = ft260_is_interface_enabled(hdev);
|
|
if (ret <= 0)
|
|
goto err_hid_close;
|
|
|
|
hid_set_drvdata(hdev, dev);
|
|
dev->hdev = hdev;
|
|
dev->adap.owner = THIS_MODULE;
|
|
dev->adap.class = I2C_CLASS_HWMON;
|
|
dev->adap.algo = &ft260_i2c_algo;
|
|
dev->adap.quirks = &ft260_i2c_quirks;
|
|
dev->adap.dev.parent = &hdev->dev;
|
|
snprintf(dev->adap.name, sizeof(dev->adap.name),
|
|
"FT260 usb-i2c bridge on hidraw%d",
|
|
((struct hidraw *)hdev->hidraw)->minor);
|
|
|
|
mutex_init(&dev->lock);
|
|
init_completion(&dev->wait);
|
|
|
|
ret = ft260_xfer_status(dev);
|
|
if (ret)
|
|
ft260_i2c_reset(hdev);
|
|
|
|
i2c_set_adapdata(&dev->adap, dev);
|
|
ret = i2c_add_adapter(&dev->adap);
|
|
if (ret) {
|
|
hid_err(hdev, "failed to add i2c adapter\n");
|
|
goto err_hid_close;
|
|
}
|
|
|
|
ret = sysfs_create_group(&hdev->dev.kobj, &ft260_attr_group);
|
|
if (ret < 0) {
|
|
hid_err(hdev, "failed to create sysfs attrs\n");
|
|
goto err_i2c_free;
|
|
}
|
|
|
|
return 0;
|
|
|
|
err_i2c_free:
|
|
i2c_del_adapter(&dev->adap);
|
|
err_hid_close:
|
|
hid_hw_close(hdev);
|
|
err_hid_stop:
|
|
hid_hw_stop(hdev);
|
|
return ret;
|
|
}
|
|
|
|
static void ft260_remove(struct hid_device *hdev)
|
|
{
|
|
struct ft260_device *dev = hid_get_drvdata(hdev);
|
|
|
|
if (!dev)
|
|
return;
|
|
|
|
sysfs_remove_group(&hdev->dev.kobj, &ft260_attr_group);
|
|
i2c_del_adapter(&dev->adap);
|
|
|
|
hid_hw_close(hdev);
|
|
hid_hw_stop(hdev);
|
|
}
|
|
|
|
static int ft260_raw_event(struct hid_device *hdev, struct hid_report *report,
|
|
u8 *data, int size)
|
|
{
|
|
struct ft260_device *dev = hid_get_drvdata(hdev);
|
|
struct ft260_i2c_input_report *xfer = (void *)data;
|
|
|
|
if (xfer->report >= FT260_I2C_REPORT_MIN &&
|
|
xfer->report <= FT260_I2C_REPORT_MAX) {
|
|
ft260_dbg("i2c resp: rep %#02x len %d\n", xfer->report,
|
|
xfer->length);
|
|
|
|
memcpy(&dev->read_buf[dev->read_idx], &xfer->data,
|
|
xfer->length);
|
|
dev->read_idx += xfer->length;
|
|
|
|
if (dev->read_idx == dev->read_len)
|
|
complete(&dev->wait);
|
|
|
|
} else {
|
|
hid_err(hdev, "unknown report: %#02x\n", xfer->report);
|
|
return 0;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
static struct hid_driver ft260_driver = {
|
|
.name = "ft260",
|
|
.id_table = ft260_devices,
|
|
.probe = ft260_probe,
|
|
.remove = ft260_remove,
|
|
.raw_event = ft260_raw_event,
|
|
};
|
|
|
|
module_hid_driver(ft260_driver);
|
|
MODULE_DESCRIPTION("FTDI FT260 USB HID to I2C host bridge");
|
|
MODULE_AUTHOR("Michael Zaidman <michael.zaidman@gmail.com>");
|
|
MODULE_LICENSE("GPL v2");
|