HID: add experimental access to PicoLCD device's EEPROM and FLASH
authorBruno Prémont <bonbons@linux-vserver.org>
Tue, 30 Mar 2010 20:38:09 +0000 (22:38 +0200)
committerJiri Kosina <jkosina@suse.cz>
Wed, 31 Mar 2010 09:32:31 +0000 (11:32 +0200)
The PicoLCD device has a small amount of EEPROM and also provides access
to its FLASH where firmware and splash image are saved.
In flasher mode FLASH access is the only active feature.

Give read/write access to both via debugfs files.

NOTE: EEPROM and FLASH access should be switched to better suited API,
      until then the will reside in debugfs

Signed-off-by: Bruno Prémont <bonbons@linux-vserver.org>
Signed-off-by: Jiri Kosina <jkosina@suse.cz>
drivers/hid/Kconfig
drivers/hid/hid-picolcd.c

index 34f6593..a2ecd83 100644 (file)
@@ -281,9 +281,9 @@ config HID_PICOLCD
          - Backlight control         (needs CONFIG_BACKLIGHT_CLASS_DEVICE)
          - Contrast control          (needs CONFIG_LCD_CLASS_DEVICE)
          - General purpose outputs   (needs CONFIG_LEDS_CLASS)
+         - EEProm / Flash access     (via debugfs)
          Features that are not (yet) supported:
          - IR
-         - EEProm / Flash access
 
 config HID_QUANTA
        tristate "Quanta Optical Touch"
index 5176773..66f9cfd 100644 (file)
@@ -169,6 +169,10 @@ struct picolcd_pending {
 struct picolcd_data {
        struct hid_device *hdev;
 #ifdef CONFIG_DEBUG_FS
+       struct dentry *debug_reset;
+       struct dentry *debug_eeprom;
+       struct dentry *debug_flash;
+       struct mutex mutex_flash;
        int addr_sz;
 #endif
        u8 version[2];
@@ -1313,6 +1317,357 @@ static DEVICE_ATTR(operation_mode, 0644, picolcd_operation_mode_show,
 
 
 #ifdef CONFIG_DEBUG_FS
+/*
+ * The "reset" file
+ */
+static int picolcd_debug_reset_show(struct seq_file *f, void *p)
+{
+       if (picolcd_fbinfo((struct picolcd_data *)f->private))
+               seq_printf(f, "all fb\n");
+       else
+               seq_printf(f, "all\n");
+       return 0;
+}
+
+static int picolcd_debug_reset_open(struct inode *inode, struct file *f)
+{
+       return single_open(f, picolcd_debug_reset_show, inode->i_private);
+}
+
+static ssize_t picolcd_debug_reset_write(struct file *f, const char __user *user_buf,
+               size_t count, loff_t *ppos)
+{
+       struct picolcd_data *data = ((struct seq_file *)f->private_data)->private;
+       char buf[32];
+       size_t cnt = min(count, sizeof(buf)-1);
+       if (copy_from_user(buf, user_buf, cnt))
+               return -EFAULT;
+
+       while (cnt > 0 && (buf[cnt-1] == ' ' || buf[cnt-1] == '\n'))
+               cnt--;
+       buf[cnt] = '\0';
+       if (strcmp(buf, "all") == 0) {
+               picolcd_reset(data->hdev);
+               picolcd_fb_reset(data, 1);
+       } else if (strcmp(buf, "fb") == 0) {
+               picolcd_fb_reset(data, 1);
+       } else {
+               return -EINVAL;
+       }
+       return count;
+}
+
+static const struct file_operations picolcd_debug_reset_fops = {
+       .owner    = THIS_MODULE,
+       .open     = picolcd_debug_reset_open,
+       .read     = seq_read,
+       .llseek   = seq_lseek,
+       .write    = picolcd_debug_reset_write,
+       .release  = single_release,
+};
+
+/*
+ * The "eeprom" file
+ */
+static int picolcd_debug_eeprom_open(struct inode *i, struct file *f)
+{
+       f->private_data = i->i_private;
+       return 0;
+}
+
+static ssize_t picolcd_debug_eeprom_read(struct file *f, char __user *u,
+               size_t s, loff_t *off)
+{
+       struct picolcd_data *data = f->private_data;
+       struct picolcd_pending *resp;
+       u8 raw_data[3];
+       ssize_t ret = -EIO;
+
+       if (s == 0)
+               return -EINVAL;
+       if (*off > 0x0ff)
+               return 0;
+
+       /* prepare buffer with info about what we want to read (addr & len) */
+       raw_data[0] = *off & 0xff;
+       raw_data[1] = (*off >> 8) && 0xff;
+       raw_data[2] = s < 20 ? s : 20;
+       if (*off + raw_data[2] > 0xff)
+               raw_data[2] = 0x100 - *off;
+       resp = picolcd_send_and_wait(data->hdev, REPORT_EE_READ, raw_data,
+                       sizeof(raw_data));
+       if (!resp)
+               return -EIO;
+
+       if (resp->in_report && resp->in_report->id == REPORT_EE_DATA) {
+               /* successful read :) */
+               ret = resp->raw_data[2];
+               if (ret > s)
+                       ret = s;
+               if (copy_to_user(u, resp->raw_data+3, ret))
+                       ret = -EFAULT;
+               else
+                       *off += ret;
+       } /* anything else is some kind of IO error */
+
+       kfree(resp);
+       return ret;
+}
+
+static ssize_t picolcd_debug_eeprom_write(struct file *f, const char __user *u,
+               size_t s, loff_t *off)
+{
+       struct picolcd_data *data = f->private_data;
+       struct picolcd_pending *resp;
+       ssize_t ret = -EIO;
+       u8 raw_data[23];
+
+       if (s == 0)
+               return -EINVAL;
+       if (*off > 0x0ff)
+               return -ENOSPC;
+
+       memset(raw_data, 0, sizeof(raw_data));
+       raw_data[0] = *off & 0xff;
+       raw_data[1] = (*off >> 8) && 0xff;
+       raw_data[2] = s < 20 ? s : 20;
+       if (*off + raw_data[2] > 0xff)
+               raw_data[2] = 0x100 - *off;
+
+       if (copy_from_user(raw_data+3, u, raw_data[2]))
+               return -EFAULT;
+       resp = picolcd_send_and_wait(data->hdev, REPORT_EE_WRITE, raw_data,
+                       sizeof(raw_data));
+
+       if (!resp)
+               return -EIO;
+
+       if (resp->in_report && resp->in_report->id == REPORT_EE_DATA) {
+               /* check if written data matches */
+               if (memcmp(raw_data, resp->raw_data, 3+raw_data[2]) == 0) {
+                       *off += raw_data[2];
+                       ret = raw_data[2];
+               }
+       }
+       kfree(resp);
+       return ret;
+}
+
+/*
+ * Notes:
+ * - read/write happens in chunks of at most 20 bytes, it's up to userspace
+ *   to loop in order to get more data.
+ * - on write errors on otherwise correct write request the bytes
+ *   that should have been written are in undefined state.
+ */
+static const struct file_operations picolcd_debug_eeprom_fops = {
+       .owner    = THIS_MODULE,
+       .open     = picolcd_debug_eeprom_open,
+       .read     = picolcd_debug_eeprom_read,
+       .write    = picolcd_debug_eeprom_write,
+       .llseek   = generic_file_llseek,
+};
+
+/*
+ * The "flash" file
+ */
+static int picolcd_debug_flash_open(struct inode *i, struct file *f)
+{
+       f->private_data = i->i_private;
+       return 0;
+}
+
+/* record a flash address to buf (bounds check to be done by caller) */
+static int _picolcd_flash_setaddr(struct picolcd_data *data, u8 *buf, long off)
+{
+       buf[0] = off & 0xff;
+       buf[1] = (off >> 8) & 0xff;
+       if (data->addr_sz == 3)
+               buf[2] = (off >> 16) & 0xff;
+       return data->addr_sz == 2 ? 2 : 3;
+}
+
+/* read a given size of data (bounds check to be done by caller) */
+static ssize_t _picolcd_flash_read(struct picolcd_data *data, int report_id,
+               char __user *u, size_t s, loff_t *off)
+{
+       struct picolcd_pending *resp;
+       u8 raw_data[4];
+       ssize_t ret = 0;
+       int len_off, err = -EIO;
+
+       while (s > 0) {
+               err = -EIO;
+               len_off = _picolcd_flash_setaddr(data, raw_data, *off);
+               raw_data[len_off] = s > 32 ? 32 : s;
+               resp = picolcd_send_and_wait(data->hdev, report_id, raw_data, len_off+1);
+               if (!resp || !resp->in_report)
+                       goto skip;
+               if (resp->in_report->id == REPORT_MEMORY ||
+                       resp->in_report->id == REPORT_BL_READ_MEMORY) {
+                       if (memcmp(raw_data, resp->raw_data, len_off+1) != 0)
+                               goto skip;
+                       if (copy_to_user(u+ret, resp->raw_data+len_off+1, raw_data[len_off])) {
+                               err = -EFAULT;
+                               goto skip;
+                       }
+                       *off += raw_data[len_off];
+                       s    -= raw_data[len_off];
+                       ret  += raw_data[len_off];
+                       err   = 0;
+               }
+skip:
+               kfree(resp);
+               if (err)
+                       return ret > 0 ? ret : err;
+       }
+       return ret;
+}
+
+static ssize_t picolcd_debug_flash_read(struct file *f, char __user *u,
+               size_t s, loff_t *off)
+{
+       struct picolcd_data *data = f->private_data;
+
+       if (s == 0)
+               return -EINVAL;
+       if (*off > 0x05fff)
+               return 0;
+       if (*off + s > 0x05fff)
+               s = 0x06000 - *off;
+
+       if (data->status & PICOLCD_BOOTLOADER)
+               return _picolcd_flash_read(data, REPORT_BL_READ_MEMORY, u, s, off);
+       else
+               return _picolcd_flash_read(data, REPORT_READ_MEMORY, u, s, off);
+}
+
+/* erase block aligned to 64bytes boundary */
+static ssize_t _picolcd_flash_erase64(struct picolcd_data *data, int report_id,
+               loff_t *off)
+{
+       struct picolcd_pending *resp;
+       u8 raw_data[3];
+       int len_off;
+       ssize_t ret = -EIO;
+
+       if (*off & 0x3f)
+               return -EINVAL;
+
+       len_off = _picolcd_flash_setaddr(data, raw_data, *off);
+       resp = picolcd_send_and_wait(data->hdev, report_id, raw_data, len_off);
+       if (!resp || !resp->in_report)
+               goto skip;
+       if (resp->in_report->id == REPORT_MEMORY ||
+               resp->in_report->id == REPORT_BL_ERASE_MEMORY) {
+               if (memcmp(raw_data, resp->raw_data, len_off) != 0)
+                       goto skip;
+               ret = 0;
+       }
+skip:
+       kfree(resp);
+       return ret;
+}
+
+/* write a given size of data (bounds check to be done by caller) */
+static ssize_t _picolcd_flash_write(struct picolcd_data *data, int report_id,
+               const char __user *u, size_t s, loff_t *off)
+{
+       struct picolcd_pending *resp;
+       u8 raw_data[36];
+       ssize_t ret = 0;
+       int len_off, err = -EIO;
+
+       while (s > 0) {
+               err = -EIO;
+               len_off = _picolcd_flash_setaddr(data, raw_data, *off);
+               raw_data[len_off] = s > 32 ? 32 : s;
+               if (copy_from_user(raw_data+len_off+1, u, raw_data[len_off])) {
+                       err = -EFAULT;
+                       goto skip;
+               }
+               resp = picolcd_send_and_wait(data->hdev, report_id, raw_data,
+                               len_off+1+raw_data[len_off]);
+               if (!resp || !resp->in_report)
+                       goto skip;
+               if (resp->in_report->id == REPORT_MEMORY ||
+                       resp->in_report->id == REPORT_BL_WRITE_MEMORY) {
+                       if (memcmp(raw_data, resp->raw_data, len_off+1+raw_data[len_off]) != 0)
+                               goto skip;
+                       *off += raw_data[len_off];
+                       s    -= raw_data[len_off];
+                       ret  += raw_data[len_off];
+                       err   = 0;
+               }
+skip:
+               kfree(resp);
+               if (err)
+                       break;
+       }
+       return ret > 0 ? ret : err;
+}
+
+static ssize_t picolcd_debug_flash_write(struct file *f, const char __user *u,
+               size_t s, loff_t *off)
+{
+       struct picolcd_data *data = f->private_data;
+       ssize_t err, ret = 0;
+       int report_erase, report_write;
+
+       if (s == 0)
+               return -EINVAL;
+       if (*off > 0x5fff)
+               return -ENOSPC;
+       if (s & 0x3f)
+               return -EINVAL;
+       if (*off & 0x3f)
+               return -EINVAL;
+
+       if (data->status & PICOLCD_BOOTLOADER) {
+               report_erase = REPORT_BL_ERASE_MEMORY;
+               report_write = REPORT_BL_WRITE_MEMORY;
+       } else {
+               report_erase = REPORT_ERASE_MEMORY;
+               report_write = REPORT_WRITE_MEMORY;
+       }
+       mutex_lock(&data->mutex_flash);
+       while (s > 0) {
+               err = _picolcd_flash_erase64(data, report_erase, off);
+               if (err)
+                       break;
+               err = _picolcd_flash_write(data, report_write, u, 64, off);
+               if (err < 0)
+                       break;
+               ret += err;
+               *off += err;
+               s -= err;
+               if (err != 64)
+                       break;
+       }
+       mutex_unlock(&data->mutex_flash);
+       return ret > 0 ? ret : err;
+}
+
+/*
+ * Notes:
+ * - concurrent writing is prevented by mutex and all writes must be
+ *   n*64 bytes and 64-byte aligned, each write being preceeded by an
+ *   ERASE which erases a 64byte block.
+ *   If less than requested was written or an error is returned for an
+ *   otherwise correct write request the next 64-byte block which should
+ *   have been written is in undefined state (mostly: original, erased,
+ *   (half-)written with write error)
+ * - reading can happend without special restriction
+ */
+static const struct file_operations picolcd_debug_flash_fops = {
+       .owner    = THIS_MODULE,
+       .open     = picolcd_debug_flash_open,
+       .read     = picolcd_debug_flash_read,
+       .write    = picolcd_debug_flash_write,
+       .llseek   = generic_file_llseek,
+};
+
+
 /*
  * Helper code for HID report level dumping/debugging
  */
@@ -1788,9 +2143,66 @@ static void picolcd_debug_raw_event(struct picolcd_data *data,
        wake_up_interruptible(&hdev->debug_wait);
        kfree(buff);
 }
+
+static void picolcd_init_devfs(struct picolcd_data *data,
+               struct hid_report *eeprom_r, struct hid_report *eeprom_w,
+               struct hid_report *flash_r, struct hid_report *flash_w,
+               struct hid_report *reset)
+{
+       struct hid_device *hdev = data->hdev;
+
+       mutex_init(&data->mutex_flash);
+
+       /* reset */
+       if (reset)
+               data->debug_reset = debugfs_create_file("reset", 0600,
+                               hdev->debug_dir, data, &picolcd_debug_reset_fops);
+
+       /* eeprom */
+       if (eeprom_r || eeprom_w)
+               data->debug_eeprom = debugfs_create_file("eeprom",
+                       (eeprom_w ? S_IWUSR : 0) | (eeprom_r ? S_IRUSR : 0),
+                       hdev->debug_dir, data, &picolcd_debug_eeprom_fops);
+
+       /* flash */
+       if (flash_r && flash_r->maxfield == 1 && flash_r->field[0]->report_size == 8)
+               data->addr_sz = flash_r->field[0]->report_count - 1;
+       else
+               data->addr_sz = -1;
+       if (data->addr_sz == 2 || data->addr_sz == 3) {
+               data->debug_flash = debugfs_create_file("flash",
+                       (flash_w ? S_IWUSR : 0) | (flash_r ? S_IRUSR : 0),
+                       hdev->debug_dir, data, &picolcd_debug_flash_fops);
+       } else if (flash_r || flash_w)
+               dev_warn(&hdev->dev, "Unexpected FLASH access reports, "
+                               "please submit rdesc for review\n");
+}
+
+static void picolcd_exit_devfs(struct picolcd_data *data)
+{
+       struct dentry *dent;
+
+       dent = data->debug_reset;
+       data->debug_reset = NULL;
+       if (dent)
+               debugfs_remove(dent);
+       dent = data->debug_eeprom;
+       data->debug_eeprom = NULL;
+       if (dent)
+               debugfs_remove(dent);
+       dent = data->debug_flash;
+       data->debug_flash = NULL;
+       if (dent)
+               debugfs_remove(dent);
+       mutex_destroy(&data->mutex_flash);
+}
 #else
 #define picolcd_debug_raw_event(data, hdev, report, raw_data, size)
-#endif
+#define picolcd_init_devfs(data, eeprom_r, eeprom_w, flash_r, flash_w, reset)
+static void picolcd_exit_devfs(struct picolcd_data *data)
+{
+}
+#endif /* CONFIG_DEBUG_FS */
 
 /*
  * Handle raw report as sent by device
@@ -1900,7 +2312,6 @@ static inline void picolcd_exit_cir(struct picolcd_data *data)
 
 static int picolcd_probe_lcd(struct hid_device *hdev, struct picolcd_data *data)
 {
-       struct hid_report *report;
        int error;
 
        error = picolcd_check_version(hdev);
@@ -1942,13 +2353,11 @@ static int picolcd_probe_lcd(struct hid_device *hdev, struct picolcd_data *data)
        if (error)
                goto err;
 
-#ifdef CONFIG_DEBUG_FS
-       report = picolcd_out_report(REPORT_READ_MEMORY, hdev);
-       if (report && report->maxfield == 1 && report->field[0]->report_size == 8)
-               data->addr_sz = report->field[0]->report_count - 1;
-       else
-               data->addr_sz = -1;
-#endif
+       picolcd_init_devfs(data, picolcd_out_report(REPORT_EE_READ, hdev),
+                       picolcd_out_report(REPORT_EE_WRITE, hdev),
+                       picolcd_out_report(REPORT_READ_MEMORY, hdev),
+                       picolcd_out_report(REPORT_WRITE_MEMORY, hdev),
+                       picolcd_out_report(REPORT_RESET, hdev));
        return 0;
 err:
        picolcd_exit_leds(data);
@@ -1962,7 +2371,6 @@ err:
 
 static int picolcd_probe_bootloader(struct hid_device *hdev, struct picolcd_data *data)
 {
-       struct hid_report *report;
        int error;
 
        error = picolcd_check_version(hdev);
@@ -1974,13 +2382,9 @@ static int picolcd_probe_bootloader(struct hid_device *hdev, struct picolcd_data
                                "please submit /sys/kernel/debug/hid/%s/rdesc for this device.\n",
                                dev_name(&hdev->dev));
 
-#ifdef CONFIG_DEBUG_FS
-       report = picolcd_out_report(REPORT_BL_READ_MEMORY, hdev);
-       if (report && report->maxfield == 1 && report->field[0]->report_size == 8)
-               data->addr_sz = report->field[0]->report_count - 1;
-       else
-               data->addr_sz = -1;
-#endif
+       picolcd_init_devfs(data, NULL, NULL,
+                       picolcd_out_report(REPORT_BL_READ_MEMORY, hdev),
+                       picolcd_out_report(REPORT_BL_WRITE_MEMORY, hdev), NULL);
        return 0;
 }
 
@@ -2073,6 +2477,7 @@ static void picolcd_remove(struct hid_device *hdev)
        data->status |= PICOLCD_FAILED;
        spin_unlock_irqrestore(&data->lock, flags);
 
+       picolcd_exit_devfs(data);
        device_remove_file(&hdev->dev, &dev_attr_operation_mode);
        hdev->ll_driver->close(hdev);
        hid_hw_stop(hdev);