From a52522ec2020a0707f0efd0693484859026553f8 Mon Sep 17 00:00:00 2001 From: Thomas Makin Date: Wed, 7 Oct 2026 10:26:55 -0500 Subject: [PATCH] stmfts: support FTM4 chips Some FingerTipS chips use FTM4 variants like Ftm4cd60d Based loosely on https://github.com/android-linux-stable/bluecross/blob/android-msm-bluecross-4.9/drivers/input/touchscreen/stm/ftm4_ts.c and https://github.com/CTCaer/switch-l4t-kernel-4.9/blob/linux-dev/drivers/input/touchscreen/stm/ftm4_ts.c Assisted-by: Grok:grok-4.6 --- drivers/input/touchscreen/stmfts.c | 427 ++++++++++++++++++++++++----- 1 file changed, 358 insertions(+), 69 deletions(-) diff --git a/drivers/input/touchscreen/stmfts.c b/drivers/input/touchscreen/stmfts.c index 2b9faeef7e46..bb9baa5315c9 100644 --- a/drivers/input/touchscreen/stmfts.c +++ b/drivers/input/touchscreen/stmfts.c @@ -6,7 +6,6 @@ * Copyright David Heidelberg * Copyright Petr Hodina */ - #include #include #include @@ -32,6 +31,7 @@ #define STMFTS_MS_MT_SENSE_ON 0x93 #define STMFTS_SS_HOVER_SENSE_OFF 0x94 #define STMFTS_SS_HOVER_SENSE_ON 0x95 +#define STMFTS_TRIM_LOW_POWER_OSCILLATOR 0x97 #define STMFTS_MS_KEY_SENSE_OFF 0x9a #define STMFTS_MS_KEY_SENSE_ON 0x9b #define STMFTS_SYSTEM_RESET 0xa0 @@ -40,11 +40,17 @@ #define STMFTS_MS_CX_TUNING 0xa3 #define STMFTS_SS_CX_TUNING 0xa4 #define STMFTS5_SET_SCAN_MODE 0xa0 +#define STMFTS_WRITE_REG 0xB6 +#define STMFTM4_SWITCH_SENSE_MODE 0xC3 +#define STMFTM4_INT_ENABLE 0x48 /* FTS5 scan mode commands */ #define STMFTS5_SCAN_MODE_ACTIVE 0x00 #define STMFTS5_SCAN_ACTIVE_MULTI_TOUCH BIT(0) +/* FTM4 modes */ +#define STMFTM4_FINGER_MODE 0x01 + /* events */ #define STMFTS_EV_NO_EVENT 0x00 #define STMFTS_EV_MULTI_TOUCH_DETECTED 0x02 @@ -61,6 +67,10 @@ #define STMFTS_EV_STATUS 0x16 #define STMFTS_EV_DEBUG 0xdb +/* FTM4 done status */ +#define STMFTM4_STATUS_MS_CX_TUNING_DONE 0x01 +#define STMFTM4_STATUS_SS_CX_TUNING_DONE 0x02 + /* * FTS5 events: the event ID is in the high nibble of event[0], the low * nibble carries additional payload (bits 5:4 of the touch major axis for @@ -100,7 +110,6 @@ #define STMFTS_MASK_KEY_BACK 0x02 #define STMFTS_RESET_TIMEOUT_MS 300 - #define STMFTS_EVENT_SIZE 8 #define STMFTS_STACK_DEPTH 32 #define STMFTS_DATA_MAX_SIZE (STMFTS_EVENT_SIZE * STMFTS_STACK_DEPTH) @@ -114,34 +123,26 @@ static const struct regulator_bulk_data stmfts_supplies[] = { struct stmfts_data { const struct stmfts_chip_ops *ops; - struct i2c_client *client; struct input_dev *input; struct gpio_desc *reset_gpio; struct gpio_desc *mode_switch_gpio; struct led_classdev led_cdev; struct mutex mutex; - struct touchscreen_properties prop; - struct regulator_bulk_data *supplies; - /* * Presence of ledvdd will be used also to check * whether the LED is supported. */ struct regulator *ledvdd; - u16 chip_id; u8 chip_ver; u16 fw_ver; u8 config_id; u8 config_ver; - u8 data[STMFTS_DATA_MAX_SIZE]; - struct completion cmd_done; - bool use_key; bool led_status; bool hover_enabled; @@ -155,6 +156,8 @@ struct stmfts_chip_ops { void (*parse_events)(struct stmfts_data *sdata); int (*set_scan)(struct stmfts_data *sdata, bool enable); int (*set_hover)(struct stmfts_data *sdata, bool enable); + int (*power_on)(struct stmfts_data *sdata); + int (*runtime_suspend)(struct stmfts_data *sdata); int (*runtime_resume)(struct stmfts_data *sdata); }; @@ -220,6 +223,50 @@ static int stmfts_read_events(struct stmfts_data *sdata) return ret == ARRAY_SIZE(msgs) ? 0 : -EIO; } +static int ftm4_write_reg(struct stmfts_data *sdata, unsigned char *reg, + unsigned short num_com) +{ + struct i2c_msg xfer_msg[1] = { + { + .addr = sdata->client->addr, + .len = num_com, + .buf = reg, + }, + }; + int ret; + + ret = i2c_transfer(sdata->client->adapter, xfer_msg, 1); + if (ret < 0) + return ret; + + return ret == 1 ? 0 : -EIO; +} + +static int ftm4_read_reg(struct stmfts_data *sdata, unsigned char *reg, + int cnum, unsigned char *buf, int num) +{ + struct i2c_msg xfer_msg[2] = { + { + .addr = sdata->client->addr, + .len = cnum, + .buf = reg, + }, + { + .addr = sdata->client->addr, + .flags = I2C_M_RD, + .len = num, + .buf = buf, + }, + }; + int ret; + + ret = i2c_transfer(sdata->client->adapter, xfer_msg, 2); + if (ret < 0) + return ret; + + return ret == 2 ? 0 : -EIO; +} + /* FTS4 event handling functions */ static void stmfts_report_contact_event(struct stmfts_data *sdata, const u8 event[]) @@ -233,7 +280,6 @@ static void stmfts_report_contact_event(struct stmfts_data *sdata, u8 area = event[7]; input_mt_slot(sdata->input, slot_id); - input_mt_report_slot_state(sdata->input, MT_TOOL_FINGER, true); input_report_abs(sdata->input, ABS_MT_POSITION_X, x); input_report_abs(sdata->input, ABS_MT_POSITION_Y, y); @@ -252,7 +298,6 @@ static void stmfts_report_contact_release(struct stmfts_data *sdata, input_mt_slot(sdata->input, slot_id); input_mt_report_slot_inactive(sdata->input); - input_sync(sdata->input); } @@ -289,22 +334,18 @@ static void stmfts5_report_contact_event(struct stmfts_data *sdata, break; } fallthrough; /* Report as finger if stylus not enabled */ - case STMFTS_TOUCH_TYPE_FINGER: case STMFTS_TOUCH_TYPE_GLOVE: tool = MT_TOOL_FINGER; break; - case STMFTS_TOUCH_TYPE_PALM: /* Palm touch - report but can be filtered by userspace */ tool = MT_TOOL_PALM; break; - case STMFTS_TOUCH_TYPE_HOVER: tool = MT_TOOL_FINGER; distance = 255; break; - case STMFTS_TOUCH_TYPE_INVALID: default: dev_warn(&sdata->client->dev, @@ -328,7 +369,6 @@ static void stmfts5_report_contact_event(struct stmfts_data *sdata, input_mt_slot(sdata->input, touch_id); input_mt_report_slot_state(sdata->input, tool, true); - input_report_abs(sdata->input, ABS_MT_POSITION_X, x); input_report_abs(sdata->input, ABS_MT_POSITION_Y, y); input_report_abs(sdata->input, ABS_MT_TOUCH_MAJOR, maj); @@ -347,6 +387,70 @@ static void stmfts5_report_contact_release(struct stmfts_data *sdata, input_mt_report_slot_inactive(sdata->input); } +/* FTM4 event handling functions */ +static void stmftm4_report_contact_event(struct stmfts_data *sdata, + const u8 event[]) +{ + u8 slot = event[0] >> 4; + u16 x = (event[1] << 4) | (event[3] >> 4); + u16 y = (event[2] << 4) | (event[3] & 0x0f); + unsigned int gain = event[6] & 0x3f; + unsigned int z = (event[4] | (event[5] << 8)) << 6; + unsigned int orient = ((event[7] & 0x80) >> 5) | (event[6] >> 6); + + if (gain == 0 || gain == 0x3f) + gain = 0x40; + z /= gain + 0x40; + + input_mt_slot(sdata->input, slot); + input_mt_report_slot_state(sdata->input, MT_TOOL_FINGER, true); + input_report_abs(sdata->input, ABS_MT_POSITION_X, x); + input_report_abs(sdata->input, ABS_MT_POSITION_Y, y); + input_report_abs(sdata->input, ABS_MT_PRESSURE, z); + input_report_abs(sdata->input, ABS_MT_ORIENTATION, orient); +} + +static void stmftm4_parse_events(struct stmfts_data *sdata) +{ + int i; + + for (i = 0; i < STMFTS_STACK_DEPTH; i++) { + u8 *event = &sdata->data[i * STMFTS_EVENT_SIZE]; + + switch (event[0]) { + case STMFTS_EV_CONTROLLER_READY: + case STMFTS_EV_STATUS: + complete(&sdata->cmd_done); + fallthrough; + case STMFTS_EV_NO_EVENT: + case STMFTS_EV_DEBUG: + goto sync; + } + + switch (event[0] & STMFTS_MASK_EVENT_ID) { + case STMFTS_EV_MULTI_TOUCH_ENTER: + case STMFTS_EV_MULTI_TOUCH_MOTION: + stmftm4_report_contact_event(sdata, event); + break; + case STMFTS_EV_MULTI_TOUCH_LEAVE: + stmfts_report_contact_release(sdata, event); + break; + case STMFTS_EV_ERROR: + dev_warn(&sdata->client->dev, + "error code: %02x %02x %02x\n", + event[0], event[1], event[2]); + break; + default: + dev_err(&sdata->client->dev, + "unknown event %#02x\n", event[0]); + break; + } + } + +sync: + input_sync(sdata->input); +} + static void stmfts_report_hover_event(struct stmfts_data *sdata, const u8 event[]) { @@ -357,7 +461,6 @@ static void stmfts_report_hover_event(struct stmfts_data *sdata, input_report_abs(sdata->input, ABS_X, x); input_report_abs(sdata->input, ABS_Y, y); input_report_abs(sdata->input, ABS_DISTANCE, z); - input_sync(sdata->input); } @@ -368,15 +471,12 @@ static void stmfts_report_key_event(struct stmfts_data *sdata, const u8 event[]) input_report_key(sdata->input, KEY_BACK, 0); input_report_key(sdata->input, KEY_MENU, 0); break; - case STMFTS_MASK_KEY_BACK: input_report_key(sdata->input, KEY_BACK, 1); break; - case STMFTS_MASK_KEY_MENU: input_report_key(sdata->input, KEY_MENU, 1); break; - default: dev_warn(&sdata->client->dev, "unknown key event: %#02x\n", event[2]); @@ -399,7 +499,6 @@ static void stmfts_parse_events(struct stmfts_data *sdata) case STMFTS_EV_STATUS: complete(&sdata->cmd_done); fallthrough; - case STMFTS_EV_NO_EVENT: case STMFTS_EV_DEBUG: return; @@ -410,28 +509,23 @@ static void stmfts_parse_events(struct stmfts_data *sdata) case STMFTS_EV_MULTI_TOUCH_MOTION: stmfts_report_contact_event(sdata, event); break; - case STMFTS_EV_MULTI_TOUCH_LEAVE: stmfts_report_contact_release(sdata, event); break; - case STMFTS_EV_HOVER_ENTER: case STMFTS_EV_HOVER_LEAVE: case STMFTS_EV_HOVER_MOTION: stmfts_report_hover_event(sdata, event); break; - case STMFTS_EV_KEY_STATUS: stmfts_report_key_event(sdata, event); break; - case STMFTS_EV_ERROR: dev_warn(&sdata->client->dev, "error code: 0x%x%x%x%x%x%x", event[6], event[5], event[4], event[3], event[2], event[1]); break; - default: dev_err(&sdata->client->dev, "unknown event %#02x\n", event[0]); @@ -448,7 +542,6 @@ static void stmfts5_parse_events(struct stmfts_data *sdata) case STMFTS5_EV_CONTROLLER_READY: complete(&sdata->cmd_done); fallthrough; - case STMFTS_EV_NO_EVENT: goto sync; } @@ -458,23 +551,19 @@ static void stmfts5_parse_events(struct stmfts_data *sdata) case STMFTS5_EV_USER_REPORT: case STMFTS5_EV_DEBUG: goto sync; - case STMFTS5_EV_MULTI_TOUCH_ENTER: case STMFTS5_EV_MULTI_TOUCH_MOTION: stmfts5_report_contact_event(sdata, event); break; - case STMFTS5_EV_MULTI_TOUCH_LEAVE: stmfts5_report_contact_release(sdata, event); break; - case STMFTS5_EV_ERROR: dev_warn(&sdata->client->dev, "error code: 0x%x%x%x%x%x%x", event[6], event[5], event[4], event[3], event[2], event[1]); break; - default: dev_err(&sdata->client->dev, "unknown FTS5 event %#02x\n", event[0]); @@ -707,7 +796,6 @@ static ssize_t stmfts_sysfs_hover_enable_write(struct device *dev, if (err) return err; } - sdata->hover_enabled = hover; } @@ -756,6 +844,99 @@ static int stmfts_read_system_info(struct stmfts_data *sdata) return 0; } +static int stmftm4_read_system_info(struct stmfts_data *sdata) +{ + unsigned char regAdd[3] = { STMFTS_WRITE_REG, 0x00, 0x04 }; + unsigned char val[7] = { }; + int ret; + + ret = ftm4_read_reg(sdata, regAdd, 3, val, 7); + if (ret < 0) { + dev_err(&sdata->client->dev, "%s failed. ret: %d\n", + __func__, ret); + return ret; + } + + sdata->chip_id = be16_to_cpup((__be16 *)&val[1]); + sdata->chip_ver = 0; + sdata->fw_ver = be16_to_cpup((__be16 *)&val[5]); + sdata->config_id = 0; + sdata->config_ver = 0; + + dev_info(&sdata->client->dev, "FTM4: chip_id: %04x fw_ver: %04x\n", + sdata->chip_id, sdata->fw_ver); + + return 0; +} + +static int stmftm4_command(struct stmfts_data *sdata, unsigned char cmd) +{ + int ret; + + ret = ftm4_write_reg(sdata, &cmd, 1); + dev_info(&sdata->client->dev, "FTS Command (%02X), ret = %d\n", + cmd, ret); + + return ret; +} + +static int stmftm4_systemreset(struct stmfts_data *sdata) +{ + unsigned char addr[4] = { STMFTS_WRITE_REG, 0x00, 0x28, 0x80 }; + int ret; + + dev_info(&sdata->client->dev, "FTM4 SystemReset\n"); + ret = ftm4_write_reg(sdata, addr, 4); + msleep(10); + + return ret; +} + +static int stmftm4_wait_ready(struct stmfts_data *sdata) +{ + u8 cmd = STMFTS_READ_ONE_EVENT; + u8 event[STMFTS_EVENT_SIZE]; + unsigned long timeout = jiffies + msecs_to_jiffies(1000); + + do { + int ret = ftm4_read_reg(sdata, &cmd, 1, event, sizeof(event)); + + if (ret) + return ret; + if (event[0] == STMFTS_EV_CONTROLLER_READY) + return 0; + msleep(20); + } while (time_before(jiffies, timeout)); + + return -ETIMEDOUT; +} + +/* Poll 0x85 for {0x16, status, 0x00}. Tuning does not complete on the IRQ. */ +static int stmftm4_wait_status(struct stmfts_data *sdata, u8 status) +{ + u8 cmd = STMFTS_READ_ONE_EVENT; + u8 event[STMFTS_EVENT_SIZE]; + int retry = 200; + + while (retry--) { + int ret; + + msleep(10); + ret = ftm4_read_reg(sdata, &cmd, 1, event, sizeof(event)); + if (ret) + return ret; + if (event[0] == STMFTS_EV_STATUS && event[1] == status && + event[2] == 0) + return 0; + if (event[0] == STMFTS_EV_ERROR) + dev_warn(&sdata->client->dev, + "status error %02x %02x %02x\n", + event[0], event[1], event[2]); + } + + return -ETIMEDOUT; +} + static int stmfts_configure(struct stmfts_data *sdata) { int err; @@ -791,6 +972,117 @@ static int stmfts_configure(struct stmfts_data *sdata) return 0; } +static int stmftm4_configure(struct stmfts_data *sdata) +{ + u8 sense_mode[2] = { STMFTM4_SWITCH_SENSE_MODE, STMFTM4_FINGER_MODE }; + u8 int_enable[4] = { STMFTS_WRITE_REG, 0x00, 0x2C, STMFTM4_INT_ENABLE }; + int err; + + err = stmftm4_read_system_info(sdata); + if (err) + return err; + + err = stmftm4_systemreset(sdata); + if (err) + return err; + + err = stmftm4_wait_ready(sdata); + if (err) + return err; + + /* Trim low power oscillator */ + stmftm4_command(sdata, STMFTS_TRIM_LOW_POWER_OSCILLATOR); + msleep(200); + + err = stmftm4_command(sdata, STMFTS_MS_CX_TUNING); + if (!err) + err = stmftm4_wait_status(sdata, STMFTM4_STATUS_MS_CX_TUNING_DONE); + if (err) + dev_warn(&sdata->client->dev, + "failed to perform mutual auto tune: %d\n", err); + + err = stmftm4_command(sdata, STMFTS_SS_CX_TUNING); + if (!err) + err = stmftm4_wait_status(sdata, STMFTM4_STATUS_SS_CX_TUNING_DONE); + if (err) + dev_warn(&sdata->client->dev, + "failed to perform self auto tune: %d\n", err); + + err = ftm4_write_reg(sdata, sense_mode, sizeof(sense_mode)); + if (err) + return err; + msleep(10); + + err = stmftm4_command(sdata, STMFTS_MS_MT_SENSE_ON); + if (err) + return err; + + err = stmftm4_command(sdata, STMFTS_CLEAR_EVENT_STACK); + if (err) + return err; + + return ftm4_write_reg(sdata, int_enable, sizeof(int_enable)); +} + +static int stmfts_chip_power_on(struct stmfts_data *sdata) +{ + int err; + + if (sdata->reset_gpio) { + reinit_completion(&sdata->cmd_done); + enable_irq(sdata->client->irq); + if (!wait_for_completion_timeout(&sdata->cmd_done, + msecs_to_jiffies(STMFTS_RESET_TIMEOUT_MS))) { + dev_err(&sdata->client->dev, "controller not ready after reset"); + err = -ETIMEDOUT; + goto err_disable_irq; + } + } else { + /* + * We do not know the real controller state (was it powered + * off or reset). Let's hope that this is enough time to + * initialize. + */ + msleep(70); + enable_irq(sdata->client->irq); + } + + dev_info(&sdata->client->dev, "configure\n"); + err = sdata->ops->configure(sdata); + if (err) + goto err_disable_irq; + + /* + * At this point no one is using the touchscreen + * and I don't really care about the return value + */ + (void)i2c_smbus_write_byte(sdata->client, STMFTS_SLEEP_IN); + + return 0; + +err_disable_irq: + disable_irq(sdata->client->irq); + return err; +} + +static int stmftm4_power_on(struct stmfts_data *sdata) +{ + int err; + + /* + * FTM4 does not signal ready on the IRQ until 0x2C is set, and it + * has no sleep-in command. Poll inside configure, then enable the IRQ. + */ + msleep(20); + dev_info(&sdata->client->dev, "configure\n"); + err = sdata->ops->configure(sdata); + if (err) + return err; + + enable_irq(sdata->client->irq); + return 0; +} + static int stmfts_power_on(struct stmfts_data *sdata) { int err; @@ -807,8 +1099,6 @@ static int stmfts_power_on(struct stmfts_data *sdata) return err; if (sdata->reset_gpio) { - reinit_completion(&sdata->cmd_done); - /* * The datasheet does not specify the power on time, but * considering that the reset time is < 10ms, sleep for 20ms @@ -816,42 +1106,13 @@ static int stmfts_power_on(struct stmfts_data *sdata) */ msleep(20); gpiod_set_value_cansleep(sdata->reset_gpio, 0); - - enable_irq(sdata->client->irq); - - if (!wait_for_completion_timeout(&sdata->cmd_done, - msecs_to_jiffies(STMFTS_RESET_TIMEOUT_MS))) { - dev_err(&sdata->client->dev, "controller not ready after reset"); - err = -ETIMEDOUT; - goto err_disable_irq; - } - } else { - /* - * We do not know the real controller state (was it powered - * off or reset). Let's hope that this is enough time to - * initialize. - */ - msleep(70); - - enable_irq(sdata->client->irq); } - err = sdata->ops->configure(sdata); + err = sdata->ops->power_on(sdata); if (err) - goto err_disable_irq; + regulator_bulk_disable(ARRAY_SIZE(stmfts_supplies), + sdata->supplies); - /* - * At this point no one is using the touchscreen - * and I don't really care about the return value - */ - (void)i2c_smbus_write_byte(sdata->client, STMFTS_SLEEP_IN); - - return 0; - -err_disable_irq: - disable_irq(sdata->client->irq); - - regulator_bulk_disable(ARRAY_SIZE(stmfts_supplies), sdata->supplies); return err; } @@ -1061,12 +1322,22 @@ static void stmfts_remove(struct i2c_client *client) pm_runtime_disable(&client->dev); } +static int stmfts_chip_runtime_suspend(struct stmfts_data *sdata) +{ + return i2c_smbus_write_byte(sdata->client, STMFTS_SLEEP_IN); +} + +static int stmftm4_runtime_suspend(struct stmfts_data *sdata) +{ + return i2c_smbus_write_byte(sdata->client, STMFTS_MS_MT_SENSE_OFF); +} + static int stmfts_runtime_suspend(struct device *dev) { struct stmfts_data *sdata = dev_get_drvdata(dev); int ret; - ret = i2c_smbus_write_byte(sdata->client, STMFTS_SLEEP_IN); + ret = sdata->ops->runtime_suspend(sdata); if (ret) dev_warn(dev, "failed to suspend device: %d\n", ret); @@ -1082,7 +1353,7 @@ static int stmfts5_chip_runtime_resume(struct stmfts_data *sdata) { struct i2c_client *client = sdata->client; struct device *dev = &client->dev; - u8 int_enable_cmd[4] = { 0xB6, 0x00, 0x2C, 0x01 }; + u8 int_enable_cmd[4] = { STMFTS_WRITE_REG, 0x00, 0x2C, 0x01 }; int err; err = i2c_smbus_write_byte(client, STMFTS_SLEEP_OUT); @@ -1106,6 +1377,7 @@ static int stmfts5_chip_runtime_resume(struct stmfts_data *sdata) err = i2c_smbus_write_byte(client, STMFTS_FULL_FORCE_CALIBRATION); if (err) dev_warn(dev, "FORCE_CALIBRATION failed: %d\n", err); + msleep(50); /* Enable controller interrupts */ @@ -1157,6 +1429,8 @@ static const struct stmfts_chip_ops stmfts4_ops = { .parse_events = stmfts_parse_events, .set_scan = stmfts_set_scan, .set_hover = stmfts_set_hover, + .power_on = stmfts_chip_power_on, + .runtime_suspend = stmfts_chip_runtime_suspend, .runtime_resume = stmfts_chip_runtime_resume, }; @@ -1165,13 +1439,27 @@ static const struct stmfts_chip_ops stmfts5_ops = { .setup_input = stmfts5_setup_input, .parse_events = stmfts5_parse_events, .set_scan = stmfts5_set_scan, + .power_on = stmfts_chip_power_on, + .runtime_suspend = stmfts_chip_runtime_suspend, .runtime_resume = stmfts5_chip_runtime_resume, }; +static const struct stmfts_chip_ops stmftm4_ops = { + .configure = stmftm4_configure, + .setup_input = stmfts_setup_input, + .parse_events = stmftm4_parse_events, + .set_scan = stmfts_set_scan, + .set_hover = stmfts_set_hover, + .power_on = stmftm4_power_on, + .runtime_suspend = stmftm4_runtime_suspend, + .runtime_resume = stmftm4_configure, +}; + #ifdef CONFIG_OF static const struct of_device_id stmfts_of_match[] = { { .compatible = "st,stmfts", .data = &stmfts4_ops }, { .compatible = "st,stmfts5", .data = &stmfts5_ops }, + { .compatible = "st,stmftm4", .data = &stmftm4_ops }, { }, }; MODULE_DEVICE_TABLE(of, stmfts_of_match); @@ -1180,6 +1468,7 @@ MODULE_DEVICE_TABLE(of, stmfts_of_match); static const struct i2c_device_id stmfts_id[] = { { .name = "stmfts", .driver_data = (kernel_ulong_t)&stmfts4_ops }, { .name = "stmfts5", .driver_data = (kernel_ulong_t)&stmfts5_ops }, + { .name = "stmftm4", .driver_data = (kernel_ulong_t)&stmftm4_ops }, { } }; MODULE_DEVICE_TABLE(i2c, stmfts_id);