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] .