/* SPDX-License-Identifier: GPL-2.0-only * * Verifone Gsensor driver for screen orientation */ #include #include #include #include #include #include #include #include #include extern struct kobject *vfiec_kobj; /* Verifone API Definitions */ enum gsensor_orientation_hex { GSENSOR_ORIENT_UNKNOWN_HEX = 0x00, GSENSOR_ORIENT_PORTRAIT_HEX = 0x14, GSENSOR_ORIENT_LANDSCAPE_HEX = 0x15, GSENSOR_ORIENT_PORTRAIT_FLIP_HEX = 0x16, GSENSOR_ORIENT_LANDSCAPE_FLIP_HEX = 0x17, }; #define GSENSOR_ORIENT_PORTRAIT_STR "portrait" #define GSENSOR_ORIENT_LANDSCAPE_STR "landscape" #define GSENSOR_ORIENT_PORTRAIT_FLIP_STR "portrait_flip" #define GSENSOR_ORIENT_LANDSCAPE_FLIP_STR "landscape_flip" #define GSENSOR_MODE_SOFT_RESET BIT(2) #define GSENSOR_MODE_INITIALIZED BIT(1) /* Register Definitions */ #define ST_ACCEL_WHO_AM_I_ADDR 0x0F #define ST_ACCEL_CTRL_REG1_ADDR 0x20 #define ST_ACCEL_CTRL_REG4_ADDR 0x23 #define ST_ACCEL_OUT_X_L_ADDR 0x28 #define SC7A20_WHO_AM_I_VALUE 0x11 /* Configuration */ #define GSENSOR_DEBOUNCE_MS 300 #define GSENSOR_POLL_INTERVAL_MS 200 struct gsensor_data { struct i2c_client *client; struct mutex lock; struct delayed_work poll_work; struct kobject *gsensor_kobj; bool enabled; bool initialized; enum gsensor_orientation_hex orientation_hex; enum gsensor_orientation_hex pending_orientation; char orientation_str[32]; unsigned long last_change_jiffies; u8 who_am_i; }; static struct gsensor_data *g_data = NULL; /* I2C Helper Functions */ static int gsensor_read_reg(struct i2c_client *client, u8 reg, u8 *data) { int ret = i2c_smbus_read_byte_data(client, reg); if (ret < 0) return ret; *data = ret; return 0; } static int gsensor_write_reg(struct i2c_client *client, u8 reg, u8 value) { return i2c_smbus_write_byte_data(client, reg, value); } /* Read the raw data */ static int gsensor_read_data(struct gsensor_data *data, int *x, int *y, int *z) { u8 xl, xh, yl, yh, zl, zh; s16 raw_x, raw_y, raw_z; int ret; /* Read the X-axis byte by byte */ ret = gsensor_read_reg(data->client, 0x28, &xl); if (ret < 0) return ret; ret = gsensor_read_reg(data->client, 0x29, &xh); if (ret < 0) return ret; /* Read the Y-axis byte by byte */ ret = gsensor_read_reg(data->client, 0x2a, &yl); if (ret < 0) return ret; ret = gsensor_read_reg(data->client, 0x2b, &yh); if (ret < 0) return ret; /* Z-axis */ ret = gsensor_read_reg(data->client, 0x2c, &zl); if (ret < 0) return ret; ret = gsensor_read_reg(data->client, 0x2d, &zh); if (ret < 0) return ret; raw_x = (s16)((xh << 8) | xl); raw_y = (s16)((yh << 8) | yl); raw_z = (s16)((zh << 8) | zl); /* ±2g Mode: 1 LSB = 0.06103515625 mg */ *x = (raw_x * 61) / 1000; *y = (raw_y * 61) / 1000; *z = (raw_z * 61) / 1000; return 0; } /* Enable/Disable sensor */ static int gsensor_set_enable(struct gsensor_data *data, bool enable) { int ret; if (enable) { /* CTRL_REG1: 0x87 = 100Hz (0x80) + Enable all axes (0x07) */ ret = gsensor_write_reg(data->client, ST_ACCEL_CTRL_REG1_ADDR, 0x87); if (ret < 0) return ret; msleep(20); } else { /* CTRL_REG1: 0x00 = Standby mode */ ret = gsensor_write_reg(data->client, ST_ACCEL_CTRL_REG1_ADDR, 0x00); if (ret < 0) return ret; } return 0; } /* Initialize sensor */ static int gsensor_init(struct gsensor_data *data) { u8 who_am_i; int ret; /* 1. Verify device */ ret = gsensor_read_reg(data->client, ST_ACCEL_WHO_AM_I_ADDR, &who_am_i); if (ret < 0) { dev_err(&data->client->dev, "Failed to read WHO_AM_I: %d\n", ret); return ret; } data->who_am_i = who_am_i; dev_info(&data->client->dev, "WHO_AM_I = 0x%02x\n", who_am_i); if (who_am_i != SC7A20_WHO_AM_I_VALUE) { dev_err(&data->client->dev, "Invalid WHO_AM_I: 0x%02x\n", who_am_i); return -ENODEV; } /* 2. Setting CTRL_REG4: BDU enable , ±2g */ ret = gsensor_write_reg(data->client, ST_ACCEL_CTRL_REG4_ADDR, 0x80); if (ret < 0) return ret; /* 3. Disable the sensor at startup */ ret = gsensor_write_reg(data->client, ST_ACCEL_CTRL_REG1_ADDR, 0x00); if (ret < 0) return ret; data->initialized = true; dev_info(&data->client->dev, "Gsensor initialized successfully\n"); return 0; } /* Orientation Calculation */ static enum gsensor_orientation_hex gsensor_calc_orientation(int x, int y, int z) { int abs_x = abs(x); int abs_y = abs(y); int abs_z = abs(z); /* Device flat - Z axis maximum */ if (abs_z > 800 && abs_z > abs_x && abs_z > abs_y) { return GSENSOR_ORIENT_UNKNOWN_HEX; } /* Landscape mode: X axis maximum */ if (abs_x > 800 && abs_x > abs_y) { if (x > 0) return GSENSOR_ORIENT_LANDSCAPE_FLIP_HEX; else return GSENSOR_ORIENT_LANDSCAPE_HEX; } /* Portrait mode: Y axis maximum. */ if (abs_y > 800 && abs_y > abs_x) { if (y > 0) return GSENSOR_ORIENT_PORTRAIT_FLIP_HEX; else return GSENSOR_ORIENT_PORTRAIT_HEX; } return GSENSOR_ORIENT_UNKNOWN_HEX; } static void gsensor_update_orientation(struct gsensor_data *data) { int x, y, z; enum gsensor_orientation_hex new_orient; unsigned long now = jiffies; if (!data->enabled) return; if (gsensor_read_data(data, &x, &y, &z) < 0) return; new_orient = gsensor_calc_orientation(x, y, z); if (new_orient != GSENSOR_ORIENT_UNKNOWN_HEX && new_orient != data->orientation_hex) { if (new_orient == data->pending_orientation) { if (time_after(now, data->last_change_jiffies + msecs_to_jiffies(GSENSOR_DEBOUNCE_MS))) { data->orientation_hex = new_orient; data->pending_orientation = GSENSOR_ORIENT_UNKNOWN_HEX; switch (new_orient) { case GSENSOR_ORIENT_PORTRAIT_HEX: strcpy(data->orientation_str, GSENSOR_ORIENT_PORTRAIT_STR); break; case GSENSOR_ORIENT_LANDSCAPE_HEX: strcpy(data->orientation_str, GSENSOR_ORIENT_LANDSCAPE_STR); break; case GSENSOR_ORIENT_PORTRAIT_FLIP_HEX: strcpy(data->orientation_str, GSENSOR_ORIENT_PORTRAIT_FLIP_STR); break; case GSENSOR_ORIENT_LANDSCAPE_FLIP_HEX: strcpy(data->orientation_str, GSENSOR_ORIENT_LANDSCAPE_FLIP_STR); break; default: break; } dev_info(&data->client->dev, "Orientation: %s [X=%d, Y=%d, Z=%d]\n", data->orientation_str, x, y, z); } } else { data->pending_orientation = new_orient; data->last_change_jiffies = now; } } else if (new_orient == data->orientation_hex) { data->pending_orientation = GSENSOR_ORIENT_UNKNOWN_HEX; } } static void gsensor_poll_work(struct work_struct *work) { struct gsensor_data *data = container_of(work, struct gsensor_data, poll_work.work); mutex_lock(&data->lock); gsensor_update_orientation(data); mutex_unlock(&data->lock); if (data->enabled) schedule_delayed_work(&data->poll_work, msecs_to_jiffies(GSENSOR_POLL_INTERVAL_MS)); } /* Sysfs Interface */ static ssize_t enable_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) { return sprintf(buf, "%d\n", g_data ? g_data->enabled : 0); } static ssize_t enable_store(struct kobject *kobj, struct kobj_attribute *attr, const char *buf, size_t count) { struct gsensor_data *data = g_data; unsigned long val; int ret; if (!data) return -ENODEV; ret = kstrtoul(buf, 0, &val); if (ret) return ret; mutex_lock(&data->lock); if (val && !data->enabled) { ret = gsensor_set_enable(data, true); if (ret == 0) { data->enabled = true; schedule_delayed_work(&data->poll_work, 0); gsensor_update_orientation(data); } } else if (!val && data->enabled) { cancel_delayed_work_sync(&data->poll_work); ret = gsensor_set_enable(data, false); if (ret == 0) data->enabled = false; } mutex_unlock(&data->lock); return ret < 0 ? ret : count; } static struct kobj_attribute enable_attr = __ATTR_RW(enable); static ssize_t screen_orientation_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) { return sprintf(buf, "%s\n", g_data ? g_data->orientation_str : "unknown"); } static struct kobj_attribute screen_orientation_attr = __ATTR_RO(screen_orientation); static ssize_t instantaneous_orientation_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) { return sprintf(buf, "0x%02x\n", g_data ? g_data->orientation_hex : 0); } static struct kobj_attribute instantaneous_orientation_attr = __ATTR_RO(instantaneous_orientation); static ssize_t mode_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) { u8 mode = 0; if (g_data && g_data->initialized) mode |= GSENSOR_MODE_INITIALIZED; return sprintf(buf, "0x%02x\n", mode); } static ssize_t mode_store(struct kobject *kobj, struct kobj_attribute *attr, const char *buf, size_t count) { struct gsensor_data *data = g_data; unsigned long val; if (!data) return -ENODEV; if (kstrtoul(buf, 0, &val)) return -EINVAL; if (val & GSENSOR_MODE_SOFT_RESET) { mutex_lock(&data->lock); gsensor_init(data); data->orientation_hex = GSENSOR_ORIENT_LANDSCAPE_HEX; strcpy(data->orientation_str, GSENSOR_ORIENT_LANDSCAPE_STR); mutex_unlock(&data->lock); } return count; } static struct kobj_attribute mode_attr = __ATTR_RW(mode); static ssize_t state_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) { u8 state = 0; if (g_data && g_data->enabled) state |= 0x01; return sprintf(buf, "0x%02x\n", state); } static struct kobj_attribute state_attr = __ATTR_RO(state); static ssize_t raw_data_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) { int x, y, z; if (!g_data) return -ENODEV; if (gsensor_read_data(g_data, &x, &y, &z) < 0) return -EIO; return sprintf(buf, "%d %d %d\n", x, y, z); } static struct kobj_attribute raw_data_attr = __ATTR_RO(raw_data); static struct attribute *gsensor_attrs[] = { &enable_attr.attr, &screen_orientation_attr.attr, &instantaneous_orientation_attr.attr, &mode_attr.attr, &state_attr.attr, &raw_data_attr.attr, NULL, }; static const struct attribute_group gsensor_attr_group = { .attrs = gsensor_attrs, }; /* I2C Probe/Remove */ static int gsensor_probe(struct i2c_client *client) { struct gsensor_data *data; int ret; dev_info(&client->dev, "Gsensor probe\n"); data = kzalloc(sizeof(*data), GFP_KERNEL); if (!data) return -ENOMEM; data->client = client; mutex_init(&data->lock); INIT_DELAYED_WORK(&data->poll_work, gsensor_poll_work); data->orientation_hex = GSENSOR_ORIENT_LANDSCAPE_HEX; strcpy(data->orientation_str, GSENSOR_ORIENT_LANDSCAPE_STR); data->pending_orientation = GSENSOR_ORIENT_UNKNOWN_HEX; data->last_change_jiffies = jiffies; i2c_set_clientdata(client, data); ret = gsensor_init(data); if (ret < 0) { dev_err(&client->dev, "Init failed: %d\n", ret); goto err_free; } g_data = data; if (vfiec_kobj) { data->gsensor_kobj = kobject_create_and_add("gsensor", vfiec_kobj); if (data->gsensor_kobj) { ret = sysfs_create_group(data->gsensor_kobj, &gsensor_attr_group); if (ret < 0) { dev_err(&client->dev, "Failed to create sysfs group\n"); kobject_put(data->gsensor_kobj); goto err_free; } } else { dev_err(&client->dev, "Failed to create gsensor kobject\n"); goto err_free; } } else { dev_err(&client->dev, "vfiec_kobj not available\n"); goto err_free; } dev_info(&client->dev, "Gsensor driver loaded\n"); return 0; err_free: kfree(data); g_data = NULL; return ret; } static int gsensor_remove(struct i2c_client *client) { struct gsensor_data *data = i2c_get_clientdata(client); if (data) { cancel_delayed_work_sync(&data->poll_work); if (data->enabled) gsensor_set_enable(data, false); if (data->gsensor_kobj) { sysfs_remove_group(data->gsensor_kobj, &gsensor_attr_group); kobject_put(data->gsensor_kobj); } mutex_destroy(&data->lock); kfree(data); g_data = NULL; } return 0; } /* I2C Driver */ static const struct i2c_device_id gsensor_id_table[] = { { "sc7a20", 0 }, { "gsensor", 0 }, {} }; MODULE_DEVICE_TABLE(i2c, gsensor_id_table); static struct i2c_driver gsensor_driver = { .driver = { .name = "verifone-gsensor" }, .probe_new = gsensor_probe, .remove = gsensor_remove, .id_table = gsensor_id_table, }; /* Init/Exit */ int gsensor_init_main(void) { printk(KERN_INFO "gsensor: Registering driver\n"); return i2c_add_driver(&gsensor_driver); } EXPORT_SYMBOL(gsensor_init_main); void gsensor_exit_main(void) { printk(KERN_INFO "gsensor: Unregistering driver\n"); i2c_del_driver(&gsensor_driver); } EXPORT_SYMBOL(gsensor_exit_main); MODULE_LICENSE("GPL v2"); MODULE_AUTHOR("Verifone, Inc."); MODULE_DESCRIPTION("Verifone Gsensor driver for screen orientation");