Hi all,

This patch adds a new touchpad driver, elan(4), which supports older non
PTP I2C Elantech touchpads. I have tested this on my HP Chromebook 13
and it appears to work well, multitouch is working as well as the three
physical mouse buttons. I do not know how well this will work on other
Elantech touchpads however; I believe there are a few devices that use
the same protocol though they may have different ACPI hids. 

This driver is similar to iatp(4) and is largely based upon it although
the actual protocol used by the touchpad is quite different.

I have added a basic man page following iatp(4) as a guide. I wasn't
sure if I should add the copyright and OpenBSD headers and so for this
first patch I have omitted them.

This together with my previous sdhc(4) patch results in a mostly working
OpenBSD install on the HP Chromebook 13, unfortunately apm(4) is still
problematic and the device gets stuck in sleep.

Thanks,
Ben.


Index: sys/arch/amd64/conf/GENERIC
===================================================================
RCS file: /cvs/src/sys/arch/amd64/conf/GENERIC,v
retrieving revision 1.464
diff -u -p -r1.464 GENERIC
--- sys/arch/amd64/conf/GENERIC 26 Oct 2018 20:26:19 -0000      1.464
+++ sys/arch/amd64/conf/GENERIC 9 Nov 2018 03:25:17 -0000
@@ -177,6 +177,8 @@ imt*        at ihidev?              # HID-over-i2c multitou
 wsmouse* at imt? mux 0
 iatp* at iic?                  # Atmel maXTouch i2c touchpad/touchscreen
 wsmouse* at iatp? mux 0
+elan* at iic?                  # Elantech i2c touchpad
+wsmouse* at elan? mux 0
 
 skgpio0 at isa? port 0x680     # Soekris net6501 GPIO and LEDs
 gpio* at skgpio?
Index: sys/dev/acpi/dwiic_acpi.c
===================================================================
RCS file: /cvs/src/sys/dev/acpi/dwiic_acpi.c,v
retrieving revision 1.8
diff -u -p -r1.8 dwiic_acpi.c
--- sys/dev/acpi/dwiic_acpi.c   1 Jul 2018 11:37:11 -0000       1.8
+++ sys/dev/acpi/dwiic_acpi.c   9 Nov 2018 03:25:18 -0000
@@ -51,6 +51,8 @@ int           dwiic_acpi_found_ihidev(struct dwii
                    struct aml_node *, char *, struct dwiic_crs);
 int            dwiic_acpi_found_iatp(struct dwiic_softc *, struct aml_node *,
                    char *, struct dwiic_crs);
+int            dwiic_acpi_found_elan(struct dwiic_softc *, struct aml_node *,
+                   char *, struct dwiic_crs);
 void           dwiic_acpi_get_params(struct dwiic_softc *, char *, uint16_t *,
                    uint16_t *, uint32_t *);
 void           dwiic_acpi_power(struct dwiic_softc *, int);
@@ -87,6 +89,11 @@ const char *iatp_hids[] = {
        NULL
 };
 
+const char *elan_hids[] = {
+       "ELAN0000",
+       NULL
+};
+
 int
 dwiic_acpi_match(struct device *parent, void *match, void *aux)
 {
@@ -388,6 +395,8 @@ dwiic_acpi_found_hid(struct aml_node *no
                return dwiic_acpi_found_ihidev(sc, node, dev, crs);
        else if (dwiic_matchhids(dev, iatp_hids))
                return dwiic_acpi_found_iatp(sc, node, dev, crs);
+       else if (dwiic_matchhids(dev, elan_hids))
+               return dwiic_acpi_found_elan(sc, node, dev, crs);
 
        memset(&ia, 0, sizeof(ia));
        ia.ia_tag = sc->sc_iba.iba_tag;
@@ -489,6 +498,34 @@ dwiic_acpi_found_iatp(struct dwiic_softc
        ia.ia_tag = sc->sc_iba.iba_tag;
        ia.ia_size = 1;
        ia.ia_name = "iatp";
+       ia.ia_addr = crs.i2c_addr;
+       ia.ia_cookie = dev;
+
+       if (crs.irq_int <= 0 && crs.gpio_int_node == NULL) {
+               printf("%s: couldn't find irq for %s\n", sc->sc_dev.dv_xname,
+                  aml_nodename(node->parent));
+               return 0;
+       }
+       ia.ia_intr = &crs;
+
+       if (config_found(sc->sc_iic, &ia, dwiic_i2c_print)) {
+               node->parent->attached = 1;
+               return 0;
+       }
+
+       return 1;
+}
+
+int
+dwiic_acpi_found_elan(struct dwiic_softc *sc, struct aml_node *node, char *dev,
+    struct dwiic_crs crs)
+{
+       struct i2c_attach_args ia;
+
+       memset(&ia, 0, sizeof(ia));
+       ia.ia_tag = sc->sc_iba.iba_tag;
+       ia.ia_size = 1;
+       ia.ia_name = "elan";
        ia.ia_addr = crs.i2c_addr;
        ia.ia_cookie = dev;
 
Index: sys/dev/i2c/elan.c
===================================================================
RCS file: sys/dev/i2c/elan.c
diff -N sys/dev/i2c/elan.c
--- /dev/null   1 Jan 1970 00:00:00 -0000
+++ sys/dev/i2c/elan.c  9 Nov 2018 03:25:18 -0000
@@ -0,0 +1,511 @@
+#include <sys/param.h>
+#include <sys/systm.h>
+#include <sys/kernel.h>
+#include <sys/device.h>
+#include <sys/malloc.h>
+#include <sys/stdint.h>
+
+#include <dev/i2c/i2cvar.h>
+
+#include <dev/wscons/wsconsio.h>
+#include <dev/wscons/wsmousevar.h>
+#include <dev/hid/hid.h>
+#include <dev/hid/hidmsvar.h>
+
+/* #define ELAN_DEBUG */
+
+#ifdef ELAN_DEBUG
+#define DPRINTF(x) printf x
+#else
+#define DPRINTF(x)
+#endif
+
+#define ELAN_INPUT             0x0003
+#define ELAN_MAX_X_AXIS                0x0106
+#define ELAN_MAX_Y_AXIS                0x0107
+#define ELAN_RESOLUTION                0x0108
+
+#define ELAN_COMMAND           0x0005
+#define ELAN_CONTROL           0x0300
+
+#define ELAN_CMD_WAKEUP                0x0800
+#define ELAN_CMD_SLEEP         0x0801
+#define ELAN_CMD_RESET         0x0100
+
+#define ELAN_CTRL_ABSOLUTE     0x0001
+#define ELAN_CTRL_STANDARD     0x0000
+
+#define ELAN_MAX_REPORT_LEN    34
+#define ELAN_MAX_FINGERS       5
+
+#define ELAN_REPORT_ABSOLUTE   0x5D
+#define ELAN_REPORT_ID         2
+#define ELAN_TOUCH_INFO                3
+#define ELAN_FINGER_DATA       4
+
+#define ELAN_TOUCH_LMB         (1 << 0)
+#define ELAN_TOUCH_RMB         (1 << 1)
+#define ELAN_TOUCH_MMB         (1 << 2)
+
+#define ELAN_FINGER_DATA_LEN   5
+#define ELAN_FINGER_XY_HIGH    0       
+#define ELAN_FINGER_X_LOW      1
+#define ELAN_FINGER_Y_LOW      2
+#define ELAN_FINGER_WIDTH      3
+#define ELAN_FINGER_PRESSURE   4
+
+struct elan_softc {
+       struct device           sc_dev;
+       i2c_tag_t               sc_tag;
+
+       i2c_addr_t              sc_addr;
+       void                    *sc_ih;
+
+       struct device           *sc_wsmousedev;
+       char                    sc_hid[16];
+       int                     sc_enabled;
+       int                     sc_busy;
+       struct tsscale          sc_tsscale;
+
+       uint16_t                max_x;
+       uint16_t                max_y;
+       uint8_t                 res_x;
+       uint8_t                 res_y;
+};
+
+int    elan_match(struct device *, void *, void *);
+void   elan_attach(struct device *, struct device *, void *);
+int    elan_detach(struct device *, int);
+int    elan_activate(struct device *, int);
+
+int    elan_ioctl(void *, u_long, caddr_t, int, struct proc *);
+int    elan_enable(void *);
+void   elan_disable(void *);
+
+int    elan_read_reg(struct elan_softc *, uint16_t, size_t, void *);
+int    elan_write_reg(struct elan_softc *, uint16_t, uint16_t);
+void   elan_proc_report(struct elan_softc *);
+int    elan_init(struct elan_softc *);
+int    elan_reset(struct elan_softc *);
+int    elan_intr(void *);
+void   elan_sleep(struct elan_softc *, int);
+
+const struct wsmouse_accessops elan_accessops = {
+       elan_enable,
+       elan_ioctl,
+       elan_disable,
+};
+
+struct cfattach elan_ca = {
+       sizeof(struct elan_softc),
+       elan_match,
+       elan_attach,
+       elan_detach,
+       elan_activate,
+};
+
+struct cfdriver elan_cd = {
+       NULL, "elan", DV_DULL
+};
+
+int
+elan_match(struct device *parent, void *match, void *aux)
+{
+       struct i2c_attach_args *ia = aux;
+
+       if (strcmp(ia->ia_name, "elan") == 0)
+               return 1;
+
+       return 0;
+}
+
+void
+elan_attach(struct device *parent, struct device *self, void *aux)
+{
+       struct elan_softc *sc = (struct elan_softc *)self;
+       struct i2c_attach_args *ia = aux;
+       struct wsmousedev_attach_args wsmaa;
+
+       sc->sc_tag = ia->ia_tag;
+       sc->sc_addr = ia->ia_addr;
+
+       if (ia->ia_cookie != NULL)
+               memcpy(&sc->sc_hid, ia->ia_cookie, sizeof(sc->sc_hid));
+
+       if (!elan_init(sc))
+               return;
+
+       if (ia->ia_intr) {
+               printf(" %s", iic_intr_string(sc->sc_tag, ia->ia_intr));
+
+               sc->sc_ih = iic_intr_establish(sc->sc_tag, ia->ia_intr,
+                   IPL_TTY, elan_intr, sc, sc->sc_dev.dv_xname);
+               if (sc->sc_ih == NULL) {
+                       printf(", can't establish interrupt\n");
+                       return;
+               }
+       }
+
+       printf(": Elantech Touchpad (%dx%d)\n", sc->max_x, sc->max_y);
+
+       wsmaa.accessops = &elan_accessops;
+       wsmaa.accesscookie = sc;
+       sc->sc_wsmousedev = config_found(self, &wsmaa, wsmousedevprint);
+}
+
+int
+elan_detach(struct device *self, int flags)
+{
+       struct elan_softc *sc = (struct elan_softc *)self;
+
+       if (sc->sc_ih != NULL) {
+               intr_disestablish(sc->sc_ih);
+               sc->sc_ih = NULL;
+       }
+
+       sc->sc_enabled = 0;
+
+       return 0;
+}
+
+int
+elan_activate(struct device *self, int act)
+{
+       struct elan_softc *sc = (struct elan_softc *)self;
+
+       switch(act) {
+       case DVACT_WAKEUP:
+               sc->sc_busy = 1;
+               elan_init(sc);
+               sc->sc_busy = 0;
+               break;
+       }
+
+       config_activate_children(self, act);
+
+       return 0;
+}
+
+int
+elan_configure(struct elan_softc *sc)
+{
+       struct wsmousehw *hw;
+
+       hw = wsmouse_get_hw(sc->sc_wsmousedev);
+       hw->type = WSMOUSE_TYPE_ELANTECH;
+       hw->hw_type = WSMOUSEHW_CLICKPAD;
+       hw->x_min = sc->sc_tsscale.minx;
+       hw->x_max = sc->sc_tsscale.maxx;
+       hw->y_min = sc->sc_tsscale.miny;
+       hw->y_max = sc->sc_tsscale.maxy;
+       hw->h_res = sc->sc_tsscale.resx;
+       hw->v_res = sc->sc_tsscale.resy;
+       hw->mt_slots = ELAN_MAX_FINGERS;
+
+       return (wsmouse_configure(sc->sc_wsmousedev, NULL, 0));
+}
+
+int
+elan_enable(void *v)
+{
+       struct elan_softc *sc = v;
+
+       if (sc->sc_busy && tsleep(&sc->sc_busy, PRIBIO, "elan", hz) != 0) {
+               printf("%s: trying to enable but we're busy\n",
+                   sc->sc_dev.dv_xname);
+               return 1;
+       }
+
+       sc->sc_busy = 1;
+
+       DPRINTF(("%s: enabling\n", sc->sc_dev.dv_xname));
+
+       if (elan_configure(sc)) {
+               printf("%s: failed wsmouse_configure\n", sc->sc_dev.dv_xname);
+               return 1;
+       }
+
+       sc->sc_enabled = 1;
+       sc->sc_busy = 0;
+
+       return 0;
+}
+
+void
+elan_disable(void *v)
+{
+       struct elan_softc *sc = v;
+
+       DPRINTF(("%s: disabling\n", sc->sc_dev.dv_xname));
+
+       wsmouse_set_mode(sc->sc_wsmousedev, WSMOUSE_COMPAT);
+
+       sc->sc_enabled = 0;
+}
+
+int
+elan_ioctl(void *v, u_long cmd, caddr_t data, int flag, struct proc *p)
+{
+        struct elan_softc *sc = v;
+        struct wsmouse_calibcoords *wsmc = (struct wsmouse_calibcoords *)data;
+        int wsmode;
+
+        DPRINTF(("%s: %s: cmd %ld\n", sc->sc_dev.dv_xname, __func__, cmd));
+
+        switch (cmd) {
+        case WSMOUSEIO_SCALIBCOORDS:
+                sc->sc_tsscale.minx = wsmc->minx;
+                sc->sc_tsscale.maxx = wsmc->maxx;
+                sc->sc_tsscale.miny = wsmc->miny;
+                sc->sc_tsscale.maxy = wsmc->maxy;
+                sc->sc_tsscale.swapxy = wsmc->swapxy;
+                sc->sc_tsscale.resx = wsmc->resx;
+                sc->sc_tsscale.resy = wsmc->resy;
+                break;
+
+        case WSMOUSEIO_GCALIBCOORDS:
+                wsmc->minx = sc->sc_tsscale.minx;
+                wsmc->maxx = sc->sc_tsscale.maxx;
+                wsmc->miny = sc->sc_tsscale.miny;
+                wsmc->maxy = sc->sc_tsscale.maxy;
+                wsmc->swapxy = sc->sc_tsscale.swapxy;
+                wsmc->resx = sc->sc_tsscale.resx;
+                wsmc->resy = sc->sc_tsscale.resy;
+                break;
+
+        case WSMOUSEIO_GTYPE: {
+                struct wsmousehw *hw = wsmouse_get_hw(sc->sc_wsmousedev);
+                *(u_int *)data = hw->type;
+                break;
+        }
+
+        case WSMOUSEIO_SETMODE:
+                wsmode = *(u_int *)data;
+                if (wsmode != WSMOUSE_COMPAT && wsmode != WSMOUSE_NATIVE) {
+                        printf("%s: invalid mode %d\n", sc->sc_dev.dv_xname,
+                            wsmode);
+                        return EINVAL;
+                }
+                wsmouse_set_mode(sc->sc_wsmousedev, wsmode);
+                break;
+
+        default:
+                return -1;
+        }
+
+        return 0;
+}
+
+int
+elan_reset(struct elan_softc *sc)
+{
+       uint16_t buf;
+
+       if (elan_write_reg(sc, ELAN_COMMAND, ELAN_CMD_RESET)) {
+               printf("%s: failed writing reset command\n",
+                   sc->sc_dev.dv_xname);
+               return 0;
+       }
+
+       if (elan_read_reg(sc, 0x0000, sizeof(buf), &buf)) {
+               printf("%s: failed reading reset ack\n",
+                   sc->sc_dev.dv_xname);
+               return 0;
+       }
+
+       if (elan_write_reg(sc, ELAN_CONTROL, ELAN_CTRL_ABSOLUTE)) {
+               printf("%s: failed setting absolute mode\n",
+                   sc->sc_dev.dv_xname);
+               return 0;
+       }
+
+       if (elan_write_reg(sc, ELAN_COMMAND, ELAN_CMD_WAKEUP)) {
+               printf("%s: failed writing wakeup command\n",
+                   sc->sc_dev.dv_xname);
+               return 0;
+       }
+
+       return 1;
+}
+
+void
+elan_sleep(struct elan_softc *sc, int ms)
+{
+        int to = ms * hz / 1000;
+
+        if (cold)
+                delay(ms * 1000);
+        else {
+                if (to <= 0)
+                        to = 1;
+                tsleep(&sc, PWAIT, "elan", to);
+        }
+}
+
+
+int
+elan_init(struct elan_softc *sc)
+{
+       uint16_t buf;
+
+       sc->sc_enabled = 0;
+
+       if (!elan_reset(sc)) {
+               printf("%s: failed to reset\n", sc->sc_dev.dv_xname);
+               return 0;
+       }
+
+       if (elan_read_reg(sc, ELAN_MAX_X_AXIS, sizeof(buf), &buf)) {
+               printf("%s: failed reading max x\n",
+                   sc->sc_dev.dv_xname);
+               return 0;
+       }
+
+       sc->max_x = le16toh(buf) & 0xFFF;
+
+       if (elan_read_reg(sc, ELAN_MAX_Y_AXIS, sizeof(buf), &buf)) {
+               printf("%s: failed reading max y\n",
+                   sc->sc_dev.dv_xname);
+               return 0;
+       }
+
+       sc->max_y = le16toh(buf) & 0xFFF;
+
+       if (elan_read_reg(sc, ELAN_RESOLUTION, sizeof(buf), &buf)) {
+               printf("%s: failed reading resolution\n",
+                   sc->sc_dev.dv_xname);
+               return 0;
+       }
+
+       sc->res_x = le16toh(buf) & 0xFF;
+       sc->res_y = (le16toh(buf) >> 8) & 0xFF;
+
+       /* Conversion from internal format to DPI */
+       sc->res_x = 790 + sc->res_x * 10;
+       sc->res_y = 790 + sc->res_y * 10;
+
+       sc->sc_tsscale.minx = 0;
+       sc->sc_tsscale.maxx = sc->max_x;
+       sc->sc_tsscale.miny = 0;
+       sc->sc_tsscale.maxy = sc->max_y;
+       sc->sc_tsscale.swapxy = 0;
+       sc->sc_tsscale.resx = sc->res_x;
+       sc->sc_tsscale.resy = sc->res_y;
+
+       return 1;
+}
+
+int
+elan_read_reg(struct elan_softc *sc, uint16_t reg, size_t len, void *val)
+{
+       uint8_t cmd[2] = { reg & 0xff, (reg >> 8) & 0xff };
+       int ret;
+
+       iic_acquire_bus(sc->sc_tag, I2C_F_POLL);
+
+       ret = iic_exec(sc->sc_tag, I2C_OP_READ_WITH_STOP, sc->sc_addr, &cmd,
+           sizeof(cmd), val, len, I2C_F_POLL);
+
+       iic_release_bus(sc->sc_tag, I2C_F_POLL);
+
+       return ret;
+}
+
+int
+elan_write_reg(struct elan_softc *sc, uint16_t reg, uint16_t val)
+{
+       uint8_t cmd[4] = { reg & 0xff, (reg >> 8) & 0xff,
+                          val & 0xff, (val >> 8) & 0xff };
+       int ret;
+
+       iic_acquire_bus(sc->sc_tag, 0);
+
+       ret = iic_exec(sc->sc_tag, I2C_OP_WRITE_WITH_STOP, sc->sc_addr, &cmd,
+           sizeof(cmd), NULL, 0, I2C_F_POLL);
+
+       iic_release_bus(sc->sc_tag, 0);
+
+       return ret;
+}
+
+void
+elan_proc_report(struct elan_softc *sc)
+{
+       uint8_t report[ELAN_MAX_REPORT_LEN];
+       uint8_t *finger_data;
+       uint8_t report_id;
+       uint16_t len;
+       int i, s, x, y, valid_contact, pressure;
+       u_int buttons;
+
+       if (elan_read_reg(sc, 0x00, sizeof(report), report)) {
+               printf("%s: failed reading report\n",
+                   sc->sc_dev.dv_xname);
+               return;
+       }
+
+       report_id = report[ELAN_REPORT_ID];
+       len = le16toh(report[0] | (report[1] << 8));
+
+       /* we seem to get 0 length reports sometimes, ignore them */
+       if (report_id != ELAN_REPORT_ABSOLUTE ||
+           len < ELAN_MAX_REPORT_LEN) {
+               return;
+       }
+
+       finger_data = &report[ELAN_FINGER_DATA];
+
+       for (i = 0; i < ELAN_MAX_FINGERS; i++) {
+               valid_contact = report[ELAN_TOUCH_INFO] & (1 << (i + 3));
+
+               x = 0;
+               y = 0;
+               pressure = 0;
+
+               if (valid_contact) {
+                       x = (finger_data[ELAN_FINGER_XY_HIGH] & 0xF0) << 4;
+                       y = (finger_data[ELAN_FINGER_XY_HIGH] & 0x0F) << 8;
+
+                       x |= finger_data[ELAN_FINGER_X_LOW];
+                       y |= finger_data[ELAN_FINGER_Y_LOW];
+
+                       pressure = finger_data[ELAN_FINGER_PRESSURE];
+               }
+
+               wsmouse_mtstate(sc->sc_wsmousedev, i, x, y, pressure);
+               finger_data += ELAN_FINGER_DATA_LEN;
+       }
+
+       buttons = 0;
+       if (report[ELAN_TOUCH_INFO] & ELAN_TOUCH_LMB)
+               buttons |= (1 << 0);
+       if (report[ELAN_TOUCH_INFO] & ELAN_TOUCH_MMB)
+               buttons |= (1 << 1);
+       if (report[ELAN_TOUCH_INFO] & ELAN_TOUCH_RMB)
+               buttons |= (1 << 2);
+
+       s = spltty();
+
+       wsmouse_buttons(sc->sc_wsmousedev, buttons);
+       wsmouse_input_sync(sc->sc_wsmousedev);
+
+       splx(s);
+}
+
+int
+elan_intr(void *arg)
+{
+       struct elan_softc *sc = arg;
+
+       if (sc->sc_busy)
+               return 1;
+
+       sc->sc_busy = 1;
+
+       elan_proc_report(sc);
+
+       sc->sc_busy = 0;
+       wakeup(&sc->sc_busy);
+
+       return 1;
+}
Index: sys/dev/i2c/files.i2c
===================================================================
RCS file: /cvs/src/sys/dev/i2c/files.i2c,v
retrieving revision 1.61
diff -u -p -r1.61 files.i2c
--- sys/dev/i2c/files.i2c       9 Jul 2018 18:48:52 -0000       1.61
+++ sys/dev/i2c/files.i2c       9 Nov 2018 03:25:18 -0000
@@ -215,6 +215,11 @@ device     iatp: wsmousedev
 attach iatp at i2c
 file   dev/i2c/iatp.c                          iatp
 
+# Elantech trackpad
+device elan: wsmousedev
+attach elan at i2c
+file   dev/i2c/elan.c                          elan
+
 # Bosch BMC150 6-axis eCompass
 device bgw
 attach bgw at i2c
Index: share/man/man4/elan.4
===================================================================
RCS file: share/man/man4/elan.4
diff -N share/man/man4/elan.4
--- /dev/null   1 Jan 1970 00:00:00 -0000
+++ share/man/man4/elan.4       9 Nov 2018 03:25:20 -0000
@@ -0,0 +1,25 @@
+.Dd $Mdocdate: November 09 2018 $
+.Dt ELAN 4
+.Os
+.Sh NAME
+.Nm elan 
+.Nd Elantech touchpad 
+.Sh SYNOPSIS
+.Cd "elan* at iic?"
+.Cd "wsmouse* at elan? mux 0"
+.Sh DESCRIPTION
+The
+.Nm
+driver provides support for Elantech touchpad devices connected over
+Inter-Integrated Circuit (I2C) buses.
+Access to these devices is through the
+.Xr wscons 4
+driver.
+.Sh SEE ALSO
+.Xr iic 4 ,
+.Xr wsmouse 4
+.Sh AUTHORS
+The
+.Nm
+driver was written by
+.An Ben Pye Aq Mt [email protected] .

Reply via email to