diff mbox

[v1] HID: bug fixes in hid-cp2112 driver

Message ID 1429143746-16967-1-git-send-email-ellen@cumulusnetworks.com (mailing list archive)
State New, archived
Delegated to: Jiri Kosina
Headers show

Commit Message

Ellen Wang April 16, 2015, 12:22 a.m. UTC
1. cp2112_xfer() byte swaps smbus words incorrectly:

While i2c is by and large big endian, smbus transactions are
little endian.  This only affects word operands.  Essentially,
all occurences of be16 in cp2112_xfer() are now le16.

2. I2C_SMBUS_BYTE write in cp2112_xfer() should pass "command"
as the single data byte, not "data->byte":

When tickled the right way, this actually segfaults the kernel
because "data" is NULL.

3. cp2112_i2c_xfer() only supports a single i2c_msg and only
reads up to 61 bytes:

This is a serious limitation.  For example, the at24 eeprom driver
generates paired write and read messages (for eeprom address and data).
And the reads can be quite large.  The fix consists of a loop
to go through all the messages, and a loop around cp2112_read()
to pick up all the returned data.  For extra credit, it now also
supports write-read pairs and implements them as
CP2112_DATA_WRITE_READ_REQUEST.

Signed-off-by: Ellen Wang <ellen@cumulusnetworks.com>
---
This is a funny driver that is a bridge from usb hid to i2c.
So while it officially belongs to hid, most of the functionality
is about i2c.  So I'm sending this patch to both linux-input
and linux-i2c.  David Barksdale <dbarksdale@uplogix.com> is
the original author/owner in the .c file.

While this patch contains three bug fixes, the first two are
quite trivial.  I'm hoping it's OK to glom them into one.

The patch was made against master in
git://git.kernel.org/pub/scm/linux/kernel/git/jikos/hid.git,
but the only file modified (drivers/hid/hid-cp2112.c) is the same
pretty much everywhere (including mainline and the i2c repository).

Testing was done on the Facebook Wedge switch, documented at
http://files.opencompute.org/oc/public.php?service=files&t=cbc56082857b154a157aa46cdcb4b9b1

Thank you for your time.
---
 drivers/hid/hid-cp2112.c |  147 +++++++++++++++++++++++++++++-----------------
 1 file changed, 92 insertions(+), 55 deletions(-)

Comments

Wolfram Sang April 16, 2015, 7:41 a.m. UTC | #1
On Wed, Apr 15, 2015 at 05:22:26PM -0700, Ellen Wang wrote:
> 1. cp2112_xfer() byte swaps smbus words incorrectly:
> 
> While i2c is by and large big endian, smbus transactions are
> little endian.  This only affects word operands.  Essentially,
> all occurences of be16 in cp2112_xfer() are now le16.
> 
> 2. I2C_SMBUS_BYTE write in cp2112_xfer() should pass "command"
> as the single data byte, not "data->byte":
> 
> When tickled the right way, this actually segfaults the kernel
> because "data" is NULL.
> 
> 3. cp2112_i2c_xfer() only supports a single i2c_msg and only
> reads up to 61 bytes:
> 
> This is a serious limitation.  For example, the at24 eeprom driver
> generates paired write and read messages (for eeprom address and data).
> And the reads can be quite large.  The fix consists of a loop

Well, at24 detects how many bytes it got and continues from there.

> to go through all the messages, and a loop around cp2112_read()
> to pick up all the returned data.  For extra credit, it now also
> supports write-read pairs and implements them as
> CP2112_DATA_WRITE_READ_REQUEST.
> 
> Signed-off-by: Ellen Wang <ellen@cumulusnetworks.com>
> ---
> This is a funny driver that is a bridge from usb hid to i2c.
> So while it officially belongs to hid, most of the functionality
> is about i2c.  So I'm sending this patch to both linux-input
> and linux-i2c.  David Barksdale <dbarksdale@uplogix.com> is
> the original author/owner in the .c file.

Probably, this would be best an MFD driver with the i2c and gpio
functionality sourced out to the relevant subsystems. Dunno how well
this works with hid drivers, though.

> While this patch contains three bug fixes, the first two are
> quite trivial.  I'm hoping it's OK to glom them into one.

Then, it should be easy to factor them out? Really, the "one patch per
issue" rule is extremly helpful when fixing regressions.

> ---
>  drivers/hid/hid-cp2112.c |  147 +++++++++++++++++++++++++++++-----------------
>  1 file changed, 92 insertions(+), 55 deletions(-)
> 
> diff --git a/drivers/hid/hid-cp2112.c b/drivers/hid/hid-cp2112.c
> index 3318de6..3999af3 100644
> --- a/drivers/hid/hid-cp2112.c
> +++ b/drivers/hid/hid-cp2112.c
> @@ -444,11 +444,30 @@ static int cp2112_i2c_write_req(void *buf, u8 slave_address, u8 *data,
>  	return data_length + 3;
>  }
>  
> +static int cp2112_i2c_write_read_req(void *buf, u8 slave_address,
> +				     u8 *addr, int addr_length,
> +				     int read_length)
> +{
> +	struct cp2112_write_read_req_report *report = buf;
> +
> +	if (read_length < 1 || read_length > 512 ||
> +	    addr_length > sizeof(report->target_address))
> +		return -EINVAL;

This shows the drawback of having I2C master drivers not in the
i2c-directory: It easily misses updates to the i2c-core.
We now have the i2c-quirk infrastructure (2187f03a9576c4) which this
driver should make use of. It can describe this...

> +	for (m = msgs; m < msgs + num; m++) {
> +		/*
> +		 * If the top two messages are a write followed by a read,
> +		 * then we do them together as CP2112_DATA_WRITE_READ_REQUEST.
> +		 * Otherwise, process one message.
> +		 */
> +

and this and the core will check the messages for you. It should
simplify your code, too.
Ellen Wang April 16, 2015, 8:32 a.m. UTC | #2
[Sorry.  Thunderbird did some weird stuff to my message.  Here it is again.]

Thanks for the quick reply.

On 04/16/2015 12:41 AM, Wolfram Sang wrote:
> On Wed, Apr 15, 2015 at 05:22:26PM -0700, Ellen Wang wrote:
>> 1. cp2112_xfer() byte swaps smbus words incorrectly:
>>
>> While i2c is by and large big endian, smbus transactions are
>> little endian.  This only affects word operands.  Essentially,
>> all occurences of be16 in cp2112_xfer() are now le16.
>>
>> 2. I2C_SMBUS_BYTE write in cp2112_xfer() should pass "command"
>> as the single data byte, not "data->byte":
>>
>> When tickled the right way, this actually segfaults the kernel
>> because "data" is NULL.
>>
>> 3. cp2112_i2c_xfer() only supports a single i2c_msg and only
>> reads up to 61 bytes:
>>
>> This is a serious limitation.  For example, the at24 eeprom driver
>> generates paired write and read messages (for eeprom address and data).
>> And the reads can be quite large.  The fix consists of a loop
>
> Well, at24 detects how many bytes it got and continues from there.

That's true, but instead of returning short, the old
cp2112_i2c_xfer() fails out (with EIO) when the first USB operation
doesn't return all the bytes.  Look for "short read: %d < %d" in
the original version.  That's just broken.

Also, even if we fix that by returning the correct byte count,
the semantics are still wrong.  The request that initiated
the operation has the desired byte count.  So the controller
may have already read all the bytes from the device.  We
should return all of them.  Every operation at the driver level
should correspond to exactly the same operation on the i2c bus.


>...
>> This is a funny driver that is a bridge from usb hid to i2c.
>> So while it officially belongs to hid, most of the functionality
>> is about i2c.  So I'm sending this patch to both linux-input
>> and linux-i2c.  David Barksdale <dbarksdale@uplogix.com> is
>> the original author/owner in the .c file.
>
> Probably, this would be best an MFD driver with the i2c and gpio
> functionality sourced out to the relevant subsystems. Dunno how well
> this works with hid drivers, though.

I agree.  This is probably something that should be taken up with
the maintainer.


>> While this patch contains three bug fixes, the first two are
>> quite trivial.  I'm hoping it's OK to glom them into one.
>
> Then, it should be easy to factor them out? Really, the "one patch per
> issue" rule is extremly helpful when fixing regressions.

I'll do that.


>>...
>> +static int cp2112_i2c_write_read_req(void *buf, u8 slave_address,
>> +                     u8 *addr, int addr_length,
>> +                     int read_length)
>> +{
>> +    struct cp2112_write_read_req_report *report = buf;
>> +
>> +    if (read_length < 1 || read_length > 512 ||
>> +        addr_length > sizeof(report->target_address))
>> +        return -EINVAL;
>
> This shows the drawback of having I2C master drivers not in the
> i2c-directory: It easily misses updates to the i2c-core.
> We now have the i2c-quirk infrastructure (2187f03a9576c4) which this
> driver should make use of. It can describe this...
>
>> +    for (m = msgs; m < msgs + num; m++) {
>> +        /*
>> +         * If the top two messages are a write followed by a read,
>> +         * then we do them together as CP2112_DATA_WRITE_READ_REQUEST.
>> +         * Otherwise, process one message.
>> +         */
>> +
>
> and this and the core will check the messages for you. It should
> simplify your code, too.

I didn't know about that.  Cumulus Linux is based on 3.2.something
(debian wheezy) and i2c-quirk came in after that.

I can update the driver to use the quirk mechanism, but I would
prefer to do that as a separate checkin, so Cumulus can use
a version of hid-cp2112.c that exists somewhere in mainline
even if it's not the latest.
--
To unsubscribe from this list: send the line "unsubscribe linux-input" in
the body of a message to majordomo@vger.kernel.org
More majordomo info at  http://vger.kernel.org/majordomo-info.html
Wolfram Sang April 22, 2015, 8:03 a.m. UTC | #3
> >Well, at24 detects how many bytes it got and continues from there.
> 
> That's true, but instead of returning short, the old
> cp2112_i2c_xfer() fails out (with EIO) when the first USB operation
> doesn't return all the bytes.  Look for "short read: %d < %d" in
> the original version.  That's just broken.

Yes.

> >This shows the drawback of having I2C master drivers not in the
> >i2c-directory: It easily misses updates to the i2c-core.
> >We now have the i2c-quirk infrastructure (2187f03a9576c4) which this
> >driver should make use of. It can describe this...
> >
> >>+    for (m = msgs; m < msgs + num; m++) {
> >>+        /*
> >>+         * If the top two messages are a write followed by a read,
> >>+         * then we do them together as CP2112_DATA_WRITE_READ_REQUEST.
> >>+         * Otherwise, process one message.
> >>+         */
> >>+
> >
> >and this and the core will check the messages for you. It should
> >simplify your code, too.
> 
> I didn't know about that.  Cumulus Linux is based on 3.2.something
> (debian wheezy) and i2c-quirk came in after that.

Well, actually, it came with this merge window :)

> I can update the driver to use the quirk mechanism, but I would
> prefer to do that as a separate checkin, so Cumulus can use
> a version of hid-cp2112.c that exists somewhere in mainline
> even if it's not the latest.

Fine with me if you do the i2c-quirk update as a seperate patch.
Ellen Wang April 23, 2015, 9:46 p.m. UTC | #4
I've separated out the cp2112_i2c_xfer() bug fix from the patch.  Should 
I submit it as v2 or a new patch altogether?

Thanks.

On 4/22/2015 1:03 AM, Wolfram Sang wrote:
>
>>> Well, at24 detects how many bytes it got and continues from there.
>>
>> That's true, but instead of returning short, the old
>> cp2112_i2c_xfer() fails out (with EIO) when the first USB operation
>> doesn't return all the bytes.  Look for "short read: %d < %d" in
>> the original version.  That's just broken.
>
> Yes.
>
>>> This shows the drawback of having I2C master drivers not in the
>>> i2c-directory: It easily misses updates to the i2c-core.
>>> We now have the i2c-quirk infrastructure (2187f03a9576c4) which this
>>> driver should make use of. It can describe this...
>>>
>>>> +    for (m = msgs; m < msgs + num; m++) {
>>>> +        /*
>>>> +         * If the top two messages are a write followed by a read,
>>>> +         * then we do them together as CP2112_DATA_WRITE_READ_REQUEST.
>>>> +         * Otherwise, process one message.
>>>> +         */
>>>> +
>>>
>>> and this and the core will check the messages for you. It should
>>> simplify your code, too.
>>
>> I didn't know about that.  Cumulus Linux is based on 3.2.something
>> (debian wheezy) and i2c-quirk came in after that.
>
> Well, actually, it came with this merge window :)
>
>> I can update the driver to use the quirk mechanism, but I would
>> prefer to do that as a separate checkin, so Cumulus can use
>> a version of hid-cp2112.c that exists somewhere in mainline
>> even if it's not the latest.
>
> Fine with me if you do the i2c-quirk update as a seperate patch.
>
--
To unsubscribe from this list: send the line "unsubscribe linux-input" in
the body of a message to majordomo@vger.kernel.org
More majordomo info at  http://vger.kernel.org/majordomo-info.html
Jiri Kosina April 23, 2015, 9:47 p.m. UTC | #5
On Thu, 23 Apr 2015, Ellen Wang wrote:

> I've separated out the cp2112_i2c_xfer() bug fix from the patch.  Should I
> submit it as v2 or a new patch altogether?

Please submit this as a completely new series, obsoleting the previous 
patch.

Thanks,
diff mbox

Patch

diff --git a/drivers/hid/hid-cp2112.c b/drivers/hid/hid-cp2112.c
index 3318de6..3999af3 100644
--- a/drivers/hid/hid-cp2112.c
+++ b/drivers/hid/hid-cp2112.c
@@ -444,11 +444,30 @@  static int cp2112_i2c_write_req(void *buf, u8 slave_address, u8 *data,
 	return data_length + 3;
 }
 
+static int cp2112_i2c_write_read_req(void *buf, u8 slave_address,
+				     u8 *addr, int addr_length,
+				     int read_length)
+{
+	struct cp2112_write_read_req_report *report = buf;
+
+	if (read_length < 1 || read_length > 512 ||
+	    addr_length > sizeof(report->target_address))
+		return -EINVAL;
+
+	report->report = CP2112_DATA_WRITE_READ_REQUEST;
+	report->slave_address = slave_address << 1;
+	report->length = cpu_to_be16(read_length);
+	report->target_address_length = addr_length;
+	memcpy(report->target_address, addr, addr_length);
+	return addr_length + 5;
+}
+
 static int cp2112_i2c_xfer(struct i2c_adapter *adap, struct i2c_msg *msgs,
 			   int num)
 {
 	struct cp2112_device *dev = (struct cp2112_device *)adap->algo_data;
 	struct hid_device *hdev = dev->hdev;
+	struct i2c_msg *m;
 	u8 buf[64];
 	ssize_t count;
 	unsigned int retries;
@@ -456,71 +475,90 @@  static int cp2112_i2c_xfer(struct i2c_adapter *adap, struct i2c_msg *msgs,
 
 	hid_dbg(hdev, "I2C %d messages\n", num);
 
-	if (num != 1) {
-		hid_err(hdev,
-			"Multi-message I2C transactions not supported\n");
-		return -EOPNOTSUPP;
-	}
-
-	if (msgs->flags & I2C_M_RD)
-		count = cp2112_read_req(buf, msgs->addr, msgs->len);
-	else
-		count = cp2112_i2c_write_req(buf, msgs->addr, msgs->buf,
-					     msgs->len);
-
-	if (count < 0)
-		return count;
-
 	ret = hid_hw_power(hdev, PM_HINT_FULLON);
 	if (ret < 0) {
 		hid_err(hdev, "power management error: %d\n", ret);
 		return ret;
 	}
 
-	ret = cp2112_hid_output(hdev, buf, count, HID_OUTPUT_REPORT);
-	if (ret < 0) {
-		hid_warn(hdev, "Error starting transaction: %d\n", ret);
-		goto power_normal;
-	}
+	for (m = msgs; m < msgs + num; m++) {
+		/*
+		 * If the top two messages are a write followed by a read,
+		 * then we do them together as CP2112_DATA_WRITE_READ_REQUEST.
+		 * Otherwise, process one message.
+		 */
+
+		if (m + 1 < msgs + num && m[0].addr == m[1].addr &&
+		    !(m[0].flags & I2C_M_RD) && (m[1].flags & I2C_M_RD)) {
+			hid_dbg(hdev, "I2C msg %zd write-read %#04x wlen %d rlen %d\n",
+				m - msgs, m[0].addr, m[0].len, m[1].len);
+			count = cp2112_i2c_write_read_req(buf, m[0].addr,
+					m[0].buf, m[0].len, m[1].len);
+			m++;
+		} else if (m->flags & I2C_M_RD) {
+			hid_dbg(hdev, "I2C msg %zd read %#04x len %d\n",
+				m - msgs, m->addr, m->len);
+			count = cp2112_read_req(buf, m->addr, m->len);
+		} else {
+			hid_dbg(hdev, "I2C msg %zd write %#04x len %d\n",
+				m - msgs, m->addr, m->len);
+			count = cp2112_i2c_write_req(buf, m->addr, m->buf,
+						     m->len);
+		}
 
-	for (retries = 0; retries < XFER_STATUS_RETRIES; ++retries) {
-		ret = cp2112_xfer_status(dev);
-		if (-EBUSY == ret)
-			continue;
-		if (ret < 0)
+		if (count < 0) {
+			ret = count;
 			goto power_normal;
-		break;
-	}
+		}
 
-	if (XFER_STATUS_RETRIES <= retries) {
-		hid_warn(hdev, "Transfer timed out, cancelling.\n");
-		buf[0] = CP2112_CANCEL_TRANSFER;
-		buf[1] = 0x01;
+		ret = cp2112_hid_output(hdev, buf, count, HID_OUTPUT_REPORT);
+		if (ret < 0) {
+			hid_warn(hdev, "Error starting transaction: %d\n", ret);
+			goto power_normal;
+		}
 
-		ret = cp2112_hid_output(hdev, buf, 2, HID_OUTPUT_REPORT);
-		if (ret < 0)
-			hid_warn(hdev, "Error cancelling transaction: %d\n",
-				 ret);
+		for (retries = 0; retries < XFER_STATUS_RETRIES; ++retries) {
+			ret = cp2112_xfer_status(dev);
+			if (-EBUSY == ret)
+				continue;
+			if (ret < 0)
+				goto power_normal;
+			break;
+		}
 
-		ret = -ETIMEDOUT;
-		goto power_normal;
-	}
+		if (XFER_STATUS_RETRIES <= retries) {
+			hid_warn(hdev, "Transfer timed out, cancelling.\n");
+			buf[0] = CP2112_CANCEL_TRANSFER;
+			buf[1] = 0x01;
 
-	if (!(msgs->flags & I2C_M_RD))
-		goto finish;
+			ret = cp2112_hid_output(hdev, buf, 2,
+						HID_OUTPUT_REPORT);
+			if (ret < 0)
+				hid_warn(hdev,
+					 "Error cancelling transaction: %d\n",
+					 ret);
 
-	ret = cp2112_read(dev, msgs->buf, msgs->len);
-	if (ret < 0)
-		goto power_normal;
-	if (ret != msgs->len) {
-		hid_warn(hdev, "short read: %d < %d\n", ret, msgs->len);
-		ret = -EIO;
-		goto power_normal;
+			ret = -ETIMEDOUT;
+			goto power_normal;
+		}
+
+		if (!(m->flags & I2C_M_RD))
+			continue;
+
+		for (count = 0; count < m->len;) {
+			ret = cp2112_read(dev, m->buf + count, m->len - count);
+			if (ret < 0)
+				goto power_normal;
+			count += ret;
+			if (count > m->len) {
+				hid_warn(hdev, "long read: %d > %zd\n",
+					 ret, m->len - count + ret);
+			}
+		}
 	}
 
-finish:
 	/* return the number of transferred messages */
-	ret = 1;
+	ret = num;
 
 power_normal:
 	hid_hw_power(hdev, PM_HINT_NORMAL);
@@ -535,7 +573,7 @@  static int cp2112_xfer(struct i2c_adapter *adap, u16 addr,
 	struct cp2112_device *dev = (struct cp2112_device *)adap->algo_data;
 	struct hid_device *hdev = dev->hdev;
 	u8 buf[64];
-	__be16 word;
+	__le16 word;
 	ssize_t count;
 	size_t read_length = 0;
 	unsigned int retries;
@@ -552,8 +590,7 @@  static int cp2112_xfer(struct i2c_adapter *adap, u16 addr,
 		if (I2C_SMBUS_READ == read_write)
 			count = cp2112_read_req(buf, addr, read_length);
 		else
-			count = cp2112_write_req(buf, addr, data->byte, NULL,
-						 0);
+			count = cp2112_write_req(buf, addr, command, NULL, 0);
 		break;
 	case I2C_SMBUS_BYTE_DATA:
 		read_length = 1;
@@ -567,7 +604,7 @@  static int cp2112_xfer(struct i2c_adapter *adap, u16 addr,
 		break;
 	case I2C_SMBUS_WORD_DATA:
 		read_length = 2;
-		word = cpu_to_be16(data->word);
+		word = cpu_to_le16(data->word);
 
 		if (I2C_SMBUS_READ == read_write)
 			count = cp2112_write_read_req(buf, addr, read_length,
@@ -580,7 +617,7 @@  static int cp2112_xfer(struct i2c_adapter *adap, u16 addr,
 		size = I2C_SMBUS_WORD_DATA;
 		read_write = I2C_SMBUS_READ;
 		read_length = 2;
-		word = cpu_to_be16(data->word);
+		word = cpu_to_le16(data->word);
 
 		count = cp2112_write_read_req(buf, addr, read_length, command,
 					      (u8 *)&word, 2);
@@ -673,7 +710,7 @@  static int cp2112_xfer(struct i2c_adapter *adap, u16 addr,
 		data->byte = buf[0];
 		break;
 	case I2C_SMBUS_WORD_DATA:
-		data->word = be16_to_cpup((__be16 *)buf);
+		data->word = le16_to_cpup((__le16 *)buf);
 		break;
 	case I2C_SMBUS_BLOCK_DATA:
 		if (read_length > I2C_SMBUS_BLOCK_MAX) {