Dear maintainers.As a linux-patch newbie i made some mistakes in my first attempt to send this patch. This patch contains a new driver for i2c connected chip, Infineon TLV493D-A1B6.
The chip is 3D hall-effect sensor with thermometer.
This particular driver senses magnetic field rotation in X/Y plane with 1 degree resolution and +/- 1 degree error.
Input device is created for the angle sensing part. Hwmon device is created for the thermometer part.Input device axis must be configured by device-tree. There are also optional parameters regarding absolute/relative mode switching, minimum step in relative mode, filtering and thermometer calibration.
We are using that device as high reliability rotary encoder. Signed-off-by: Jakub Ladman <[email protected]> --- .../bindings/input/tlv493d-a1b6_rotenc.txt | 70 ++ drivers/input/misc/Kconfig | 9 + drivers/input/misc/Makefile | 2 +- drivers/input/misc/tlv493d-a1b6_rotenc.c | 675 ++++++++++++++++++ 4 files changed, 755 insertions(+), 1 deletion(-)create mode 100644 Documentation/devicetree/bindings/input/tlv493d-a1b6_rotenc.txt
create mode 100644 drivers/input/misc/tlv493d-a1b6_rotenc.cdiff --git a/Documentation/devicetree/bindings/input/tlv493d-a1b6_rotenc.txt b/Documentation/devicetree/bindings/input/tlv493d-a1b6_rotenc.txt
new file mode 100644
index 000000000000..0f7dac615dc1
--- /dev/null
+++ b/Documentation/devicetree/bindings/input/tlv493d-a1b6_rotenc.txt
@@ -0,0 +1,70 @@
+This is driver of Infineon TLV493D-1AB6 chip, acting like rotary encoder.
+TLV493-A1B6 is I2C slave.
+
+Example of usage (for raspberry-pi):
+
+/dts-v1/;
+/plugin/;
+
+/ {
+ compatible = "brcm,bcm2708";
+
+ fragment@0 {
+ target = <&i2c0>;
+ __overlay__ {
+ status = "okay";
+
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ tlv493da1b6@1f {
+ compatible = "infineon,tlv493da1b6";
+ reg = <0x1f>;
+ relative_step = <12>;
+ filter = <1>;
+ temp_filter = <6>;
+ temp_base = <340>;
+ refresh_rate_ms = <15>;
+ linux,axis = <8>; /* REL_WHEEL */
+ };
+
+ };
+ };
+};
+
+reg is the only mandatory parameter, for the others defaults will be
used if not specified.
+ +reg = <0x1f> - set to alternate address+Can be switched to <0x5f>, the primary address, but this is unavailable at for some +i2c controllers.
+For example 0x1f must be used for NXP IMx6 SOC. +This may change in future with its i2c-controller driver updates. ++0x5f is the device's default address, but we need to preserve the address after device +periodical reset and it's impossible to reset it to primary address on some i2c controllers.
+Raspberry Pi 3B+ is proven to work with both addresses. + +See driver source code for more details. + +relative_step can be: + zero -> absolute output 0-359 deg + non-zero -> relative tick if change > step + + +filter: + coefficient of first order IIR filter + - set to balance speed and noise of magnetic vector components + +temp_filter: + coefficient of first order IIR filter + - set to transform temperature sensor noise to additional resolution + +temp_base:+ raw binary value for 25 deg C, typical value is 340, but it differs device to device. See datasheet.
+ +refresh_rate: + hardware starts new measurement 12 ms after last readout + set the refresh rate to T > 12 ms + +linux,axis: + set to appropriate input event diff --git a/drivers/input/misc/Kconfig b/drivers/input/misc/Kconfig index 7d9ae394e597..91a36a632f62 100644 --- a/drivers/input/misc/Kconfig +++ b/drivers/input/misc/Kconfig @@ -894,4 +894,13 @@ config INPUT_STPMIC1_ONKEY To compile this driver as a module, choose M here: the module will be called stpmic1_onkey. +config INPUT_TLV493D_A1B6_ROTENC + tristate "Infineon TLV493D-A1B6 Rotary Encoder" + depends on I2C + help + Say Y to enable support of TLV493D-A1B6 as rotary encoder. + + To compile this driver as a module, choose M here. The + module will be called tlv493d-a1b6_rotenc. + endif diff --git a/drivers/input/misc/Makefile b/drivers/input/misc/Makefile index 8fd187f314bd..071fb506475f 100644 --- a/drivers/input/misc/Makefile +++ b/drivers/input/misc/Makefile @@ -85,4 +85,4 @@ obj-$(CONFIG_INPUT_WM831X_ON) += wm831x-on.o obj-$(CONFIG_INPUT_XEN_KBDDEV_FRONTEND) += xen-kbdfront.o obj-$(CONFIG_INPUT_YEALINK) += yealink.o obj-$(CONFIG_INPUT_IDEAPAD_SLIDEBAR) += ideapad_slidebar.o - +obj-$(CONFIG_INPUT_TLV493D_A1B6_ROTENC) += tlv493d-a1b6_rotenc.odiff --git a/drivers/input/misc/tlv493d-a1b6_rotenc.c b/drivers/input/misc/tlv493d-a1b6_rotenc.c
new file mode 100644 index 000000000000..33e53fd30fb0 --- /dev/null +++ b/drivers/input/misc/tlv493d-a1b6_rotenc.c @@ -0,0 +1,675 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * TLV493D-A1B6 three axis magnetic sensor acting as rotary encoder. + * Rotation in X/Y plane is reported, Z-axis is ignored. + * Temperature is reported. + * + * Copyright (C) 2019 Jakub Ladman <[email protected]> + * + * This software is licensed under the terms of the GNU General Public + * License version 2, as published by the Free Software Foundation, and + * may be copied, distributed, and modified under those terms. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#include <linux/slab.h> +#include <linux/module.h> +#include <linux/kernel.h> +#include <linux/timer.h> +#include <linux/workqueue.h> + +#include <linux/input.h> +#include <linux/i2c.h> +#include <linux/delay.h> + +#include <linux/of.h> +#include <linux/types.h> + +#include <linux/hwmon.h> +#include <linux/hwmon-sysfs.h> + +#define TLV493DA1B6_ROTENC_NAME "tlv493da1b6_rotenc" + +#define TLV_BITS_PAR (1<<7) +#define TLV_BITS_ADR (1<<5) +#define TLV_BITS_INT (1<<2) +#define TLV_BITS_FST (1<<1) +#define TLV_BITS_LOW (1<<0) +#define TLV_BITS_TEM (1<<7) +#define TLV_BITS_LPP (1<<6) +#define TLV_BITS_PTE (1<<5) + +#define TLV_BITS_FRM(a) (((a) >> 2) & 0b11) /* frame counter */ +#define TLV_BITS_CHA(a) (((a) >> 0) & 0b11) /* ongoing conversion */ + +struct tlv493da1b6_rotenc_data { + struct i2c_client *client; + struct input_dev *input_dev; + struct timer_list i2c_timer; + struct work_struct offload; + + struct device *hwmon_dev; + struct attribute_group attr_group; + const struct attribute_group *groups[2]; + struct attribute **attrs; + struct sensor_device_attribute *attr; + + u32 refresh_rate_ms; + u32 filter_bits; + u32 temp_filter_bits; + u32 temp_base; + u32 axis; + u32 relative_step; + +/* private */ + s32 x_filter; + s32 y_filter; + s32 t_filter; + u32 frame_cnt; + int lastabs; + int temperature; +}; + +static uint8_t parity(uint32_t x) +{ + x ^= x >> 16; + x ^= x >> 8; + x ^= x >> 4; + x ^= x >> 2; + x ^= x >> 1; + return (~x << 7); /* returns parity at seventh bit possition! */ +} + +/********************************************************************** + * input values filter (https://electronics.stackexchange.com/a/34426) + **********************************************************************/ + +/* + * @details Implement a first order IIR filter to approximate a K sample + * moving average. This function implements the equation: + * + * y[n] = alpha * x[n] + (1 - alpha) * y[n-1] + * + * @param *filter - a Signed 15.16 fixed-point value. + * @param sample - the 16-bit value of the current sample. + * @param bits - this is roughly = log2( 1 / alpha ). + */ + +int16_t IIR_Filter(int32_t *filter, int32_t sample, uint32_t bits) +{ + int32_t local_sample = (int32_t) sample << 16; + *filter += (local_sample - *filter) >> bits; + return (int16_t) ((*filter + 0x8000) >> 16); +} + +/********************************************************************** + * angle computation + **********************************************************************/ + +/* + * Original copyright notice: + * This algorithm and code examples on this page are open-source, + * use them as you like but please mention www.RomanBlack.com. + * + * Fast XY vector to integer degree algorithm - Jan 2011 www.RomanBlack.com + * Converts any XY values including 0 to a degree value that should be + * within +/- 1 degree of the accurate value without needing + * large slow trig functions like ArcTan() or ArcCos(). + * NOTE! at least one of the X or Y values must be non-zero! + * This is the full version, for all 4 quadrants and will generate + * the angle in integer degrees from 0-359. + * Any values of X and Y are usable including negative values provided + * they are between -95443717 and 95443717 + * so the 32bit multiply does not overflow. + * + * modified to 32bit and corrected to 0-359 degrees output by Jakub Ladman + */ + +int angle(int32_t y, int32_t x) +{ + + uint32_t negflag; + uint32_t comp; + int32_t degree; /* this will hold the result */ + int32_t ux; + int32_t uy; + + /* preventing div by zero, in hardware fault condition */ + if (!(x && y)) + return -1; + + /* Save the sign flags then remove signs and get XY as unsigned ints */ + negflag = 0; + if (x < 0) { + negflag |= 0x01; /* x flag bit */ + x = (0 - x); /* is now + */ + } + ux = x; /* copy to unsigned var before multiply */ + if (y < 0) { + negflag |= 0x02; /* y flag bit */ + y = (0 - y); /* is now + */ + } + uy = y; /* copy to unsigned var before multiply */ + + /* 1. Calc the scaled "degrees" */ + if (ux > uy) { + degree = (uy * 45) / ux; /* degree result will be 0-45 range */ + negflag |= 0x10; /* octant flag bit */ + } else { + degree = (ux * 45) / uy; /* degree result will be 0-45 range */ + } + + /* 2. Compensate for the 4 degree error curve */ + comp = 0; + if (degree > 22) { /* if top half of range */ + if (degree <= 44) + comp++; + if (degree <= 41) + comp++; + if (degree <= 37) + comp++; + if (degree <= 32) + comp++; /* max is 4 degrees compensated */ + } else { /* else is lower half of range */ + if (degree >= 2) + comp++; + if (degree >= 6) + comp++; + if (degree >= 10) + comp++; + if (degree >= 15) + comp++; /* max is 4 degrees compensated */ + } + degree += comp; /* degree is now accurate to +/- 1 degree! */ + + /* Invert degree if it was X>Y octant, makes 0-45 into 90-45 */ + if (negflag & 0x10) + degree = (90 - degree); + + /* 3. Degree is now 0-90 range for this quadrant, + * need to invert it for whichever quadrant it was in + */ + if (negflag & 0x02) { /* if -Y */ + if (negflag & 0x01) /* if -Y -X */ + degree = (180 + degree); + else /* else is -Y +X */ + degree = (180 - degree); + } else { /* else is +Y */ + if (negflag & 0x01) { /* if +Y -X */ + degree = (359 - degree); + if (negflag & 0x10) + degree += 1; + } + } + return degree; +} + +/* + * TLV493D-A1B6 device reset + * Reset is needed to set up secondary address and to unlock the device + * if the measurement stalls. + * From time to time the device freezes, returning the same old data again + * and again. + * This behavior is documented in official documentation from the + * manufacturer. + * The stalled state is detectable observing the frame counter, + * if it stops incrementing and overflowing the device must be reset. + * See the detection part in work handler function. + * + * The device is reset if i2c address 0x00 is called in write mode. + * The new device address is set by the state (voltage) of the SDA line + * from 4 us to 14 us after address ACK is sent. + * This is done in this driver by sending 0x00 or 0xff byte according + * to requested address. This behavior was tested on bare metal MCU, + * before implemented here. + * + * Unfortunately at least in case of IMx6 SOC, there is impossible + * to send the 0xff byte in that time limit. + * This leads to practical limitation of single TLV493D-A1B6 chip at 0x1f + * on single i2c bus. + * + * On Raspberry PI 3B+ is possible to use both addresses. + * + * Another limitation is that if there were two (or more) these chips on + * the same bus, both will respond to reset command in the same way and + * thus it will lead to address confilct. + * + * This has to be solved by additional hardware and driver modification. + * I.e. disconnect clock and/or data signal from the unrelated chip + * during reset. + * + * In the datasheet and user manual is also described a possibility + * to select up to 8 different addresses and then share a single bus + * by these 8 devices. + * Without help of additional hardware and driver modification, + * unable to maintain these further addresses which are to be set + * by power sequencing. This driver doesn't support power sequencing. + * + * After the reset, the configuration is sent to the device. + */ + +static int i2c_device_reset(struct i2c_client *client) +{ + struct i2c_msg tlv493_reset_msg[1]; + int result = 0; + u8 buf[10]; + + buf[0] = 0x00; /* Reset the device to the alternate address 0x1f. */ + + tlv493_reset_msg[0].addr = 0; + tlv493_reset_msg[0].flags = 0; + tlv493_reset_msg[0].len = 1; + tlv493_reset_msg[0].buf = buf; + + /* If MSB of the address is high, default address 0x5e is used. */ + /* It does not work if the controller is pulling SDA line low */ + /* after ACK. Only the alternate address is then usable. */ + if (client->addr & 0x40) + buf[0] = 0xff; + + + result = i2c_transfer(client->adapter, tlv493_reset_msg, 1); + + udelay(10); + + /* + * The initialization part: + * There is a strange request in the datasheet to not change some bits. + * Original values has to be read from registers 6..9, then modified + * and written back to registers 0..3. + * + * There is also need to set the parity so the parity of + * all the 32 bits of configuration will be odd. + * + * The parity checking can be disabled, but the first configuration + * has to be "secured" anyway. + * + * This driver exploits master controlled low power mode, in which + * the measurement is started 12 ms after the last readout. + * If faster refresh rate needed, the low power bit must be + * switched off. Then the refresh rate can be as low as 300 us. + * + * This version of the driver doesn't support these refresh rates, + * because it is assumed to be unpractical on the non-realtime os. + */ + + /* Read all registers to get the default values for write registers */ + result = i2c_master_recv(client, buf, 10); + if (result < 0) { + dev_info(&client->dev, "FAIL: i2c can't read data: %d", result); + return result; + } + + udelay(20); + + /* Prepare config data, use the read values in buffer[6..9] */ + buf[6] = 0x00; + + buf[7] |= TLV_BITS_FST | TLV_BITS_LOW; + /* Other bits must correspond to bits from read register 7. */ + + /* All bits must correspond to bits 7:0 from read register 8. */ + + buf[9] |= TLV_BITS_LPP | TLV_BITS_PTE; + /* Other bits must correspond to bits from read register 9. */ + + /* Update parity bit according to actual content of buf[6..9] */ + buf[7] ^= parity(*((uint32_t *) (buf + 6))); + + /* Send configuration data */ + result = i2c_master_send(client, buf + 6, 4); + if (result < 0) + dev_info(&client->dev, + "FAIL: i2c can't write data: %d", result); + + return result; +} + +static void i2c_work_handler(struct work_struct *work) +{ + struct tlv493da1b6_rotenc_data *rotenc = + container_of(work, struct tlv493da1b6_rotenc_data, offload); + struct input_dev *input_dev = rotenc->input_dev; + struct { + signed int val:12; + } tmp; + + u8 buf[10]; + s16 x, y; + u8 attempt = 1; + int curabs; + + do { + if (attempt < 10) { /* try it ten times */ + i2c_master_recv(rotenc->client, buf, 7); + attempt++; + } else { /* give up */ + dev_info(&rotenc->client->dev, + "FAIL: tlv493 read failed, attempt %d\n", + attempt); + return; + } + + /* zero if all three axes consistent */ + if (TLV_BITS_CHA(buf[3])) { + dev_info(&rotenc->client->dev, + "tlv493 read inconsistent, attempt %d\n", + attempt); + continue; /* read again if not */ + } + + /* if the frame was already received */ + if (rotenc->frame_cnt == TLV_BITS_FRM(buf[3])) { + dev_info(&rotenc->client->dev, + "FAIL: tlv493 stalled %d, resetting\n", + TLV_BITS_FRM(buf[3])); + if (i2c_device_reset(rotenc->client) < 0) { + dev_err(&rotenc->client->dev, + "FAIL: i2c device doesn't respond to reset."); + /* Device permanent error, the hw dissappeared from bus or so. */ + /* I would like to stop and exit module here, but don't know how to. */ + } + /* reinit with unseen frame number again */ + rotenc->frame_cnt = 0xff; + /* discard data and wait for next timer event */ + return; + } + rotenc->frame_cnt = TLV_BITS_FRM(buf[3]); + + } while (TLV_BITS_CHA(buf[3])); + + /* Magnetic axis X data */ + tmp.val = buf[0] << 4; + tmp.val |= buf[4] >> 4; + + x = IIR_Filter(&rotenc->x_filter, tmp.val, rotenc->filter_bits); + + /* Magnetic axis Y data */ + tmp.val = buf[1] << 4; + tmp.val |= buf[4] & 0xf; + + y = IIR_Filter(&rotenc->y_filter, tmp.val, rotenc->filter_bits); + + /* Temperature data */ + tmp.val = buf[6]; + tmp.val |= (buf[3] << 4) & 0xf00; + + /* + * subtracting typical offset of 340 LSB's + * converting to degC by 1.1C/LSB + * adding reference temperature of 25C + */ + + rotenc->temperature = 25000 + + IIR_Filter(&rotenc->t_filter, + 1100 * (tmp.val - rotenc->temp_base), + rotenc->temp_filter_bits); /* TEMP: 0.001C/bit */ + + /* compute current angle */ + curabs = angle(y, x); + + if (curabs < 0) + return; /* invalid values aren't reported */ + + /* non-zero -> relative, zero -> absolute */ + if (rotenc->relative_step) { + + int diff = (curabs - rotenc->lastabs); + + if (diff > 180) + diff -= 360; + else if (diff < -180) + diff += 360; + + if (abs(diff) > rotenc->relative_step) { + diff /= abs(diff); + input_report_rel(input_dev, rotenc->axis, diff); + rotenc->lastabs = curabs; + } + } else { + input_report_abs(input_dev, rotenc->axis, curabs); + } + + input_sync(input_dev); +} + +static ssize_t show_temperature(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct tlv493da1b6_rotenc_data *rotenc = dev_get_drvdata(dev); + + return sprintf(buf, "%d\n", rotenc->temperature); +} + +static void i2c_timer_callback(struct timer_list *t) +{ + + struct tlv493da1b6_rotenc_data *rotenc = + from_timer(rotenc, t, i2c_timer); + schedule_work(&rotenc->offload); + + mod_timer(&rotenc->i2c_timer, + jiffies + msecs_to_jiffies(rotenc->refresh_rate_ms)); +} ++static void tlv493da1b6_rotenc_power(struct tlv493da1b6_rotenc_data *rotenc,
+ bool poweron)
+{
+}
+
+static int tlv493da1b6_rotenc_probe(struct i2c_client *client,
+ const struct i2c_device_id *id)
+{
+ struct tlv493da1b6_rotenc_data *rotenc;
+ struct device_node *np = client->dev.of_node;
+ struct input_dev *input_dev;
+ int error;
+ char *sname;
+
+ if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C)) {
+ dev_err(&client->dev, "need I2C_FUNC_I2C\n");
+ return -EIO;
+ }
+
+ rotenc = devm_kzalloc(&client->dev, sizeof(*rotenc), GFP_KERNEL);
+ if (!rotenc)
+ return -ENOMEM;
+
+ input_dev = devm_input_allocate_device(&client->dev);
+ if (!input_dev)
+ return -ENOMEM;
+
+ rotenc->client = client;
+ rotenc->input_dev = input_dev;
+
+ tlv493da1b6_rotenc_power(rotenc, true);
+
+ input_dev->name = TLV493DA1B6_ROTENC_NAME;
+ input_dev->id.bustype = BUS_I2C;
+ input_dev->dev.parent = &client->dev;
+
+ i2c_set_clientdata(client, rotenc);
+ device_init_wakeup(&client->dev, 1);
+
+ error = i2c_device_reset(rotenc->client);
+ if (error < 0) {
+ dev_err(&client->dev, "Unable to reset %s\n", input_dev->name);
+ return error;
+ }
+
+ if (of_property_read_u32(np, "relative_step", &rotenc->relative_step))
+ rotenc->relative_step = 0;
+
+ dev_info(&client->dev, "relative_step %d\n", rotenc->relative_step);
+
+ /* if zero, absolute value reported */
+ if (of_property_read_u32(np, "linux,axis", &rotenc->axis)) {
+ if (rotenc->relative_step)
+ rotenc->axis = REL_WHEEL;
+ else
+ rotenc->axis = ABS_WHEEL;
+ }
+ dev_info(&client->dev, "axis %d\n", rotenc->axis);
+
+ if (rotenc->relative_step) {
+ __set_bit(EV_REL, input_dev->evbit);
+ input_set_capability(input_dev, EV_REL, rotenc->axis);
+ } else {
+ __set_bit(EV_ABS, input_dev->evbit);
+ input_set_abs_params(input_dev, rotenc->axis, 0, 359, 0, 0);
+ }
+
+ error = input_register_device(rotenc->input_dev);
+ if (error) {
+ dev_err(&client->dev, "Unable to register %s input device\n",
+ input_dev->name);
+ return error;
+ }
+
+ rotenc->attrs =
+ devm_kzalloc(&client->dev, sizeof(*rotenc->attrs) * 2, GFP_KERNEL);
+ if (rotenc->attrs == NULL) {
+ error = -ENOMEM;
+ goto err2;
+ }
+
+ rotenc->attr =
+ devm_kzalloc(&client->dev, sizeof(*rotenc->attr), GFP_KERNEL);
+ if (rotenc->attr == NULL) {
+ error = -ENOMEM;
+ goto err2;
+ }
+
+ sysfs_attr_init(&rotenc->attr->dev_attr.attr);
+
+ rotenc->attr->dev_attr.attr.name =
+ devm_kasprintf(&client->dev, GFP_KERNEL, "temp1_input");
+ if (rotenc->attr->dev_attr.attr.name == NULL) {
+ error = -ENOMEM;
+ goto err2;
+ }
+
+ rotenc->attr->dev_attr.show = show_temperature;
+ rotenc->attr->dev_attr.attr.mode = 0444;
+ rotenc->attr->index = 0;
+
+ rotenc->attrs[0] = &rotenc->attr->dev_attr.attr;
+
+ rotenc->attr_group.attrs = rotenc->attrs;
+ rotenc->groups[0] = &rotenc->attr_group;
+
+ sname = devm_kstrdup(&client->dev, input_dev->name, GFP_KERNEL);
+ if (!sname) {
+ error = -ENOMEM;
+ goto err2;
+ }
+
+ strreplace(sname, '-', '_');
+ rotenc->hwmon_dev =
+ hwmon_device_register_with_groups(&client->dev, sname, rotenc,
+ rotenc->groups);
+ if (IS_ERR(rotenc->hwmon_dev)) {
+ error = PTR_ERR(rotenc->hwmon_dev);
+ goto err2;
+ }
+
+ rotenc->x_filter = 0;
+ rotenc->y_filter = 0;
+ rotenc->t_filter = 0;
+ rotenc->frame_cnt = 0xff; /* received frame nr. will differ for sure */
+
+ if (of_property_read_u32(np, "filter", &rotenc->filter_bits))
+ rotenc->filter_bits = 3;
+
+ dev_info(&client->dev, "filter %d\n", rotenc->filter_bits);
+
+ if (of_property_read_u32(np, "temp_filter", &rotenc->temp_filter_bits))
+ rotenc->temp_filter_bits = 8;
+
+ dev_info(&client->dev, "temp_filter %d\n", rotenc->temp_filter_bits);
+
+ if (of_property_read_u32(np, "temp_base", &rotenc->temp_base))
+ rotenc->temp_base = 340;
+
+ dev_info(&client->dev, "temp_base %d\n", rotenc->temp_base);
+
+ if (of_property_read_u32
+ (np, "refresh_rate_ms", &rotenc->refresh_rate_ms)) {
+ rotenc->refresh_rate_ms = 20;
+ }
+/* measurement is started by hw 12ms after last readout */
+ if (rotenc->refresh_rate_ms < 12)
+ rotenc->refresh_rate_ms = 20;
+ dev_info(&client->dev, "refresh_rate_ms %d\n", rotenc->refresh_rate_ms);
+
+ INIT_WORK(&rotenc->offload, i2c_work_handler);
+ timer_setup(&rotenc->i2c_timer, i2c_timer_callback, 0);
+ error =
+ mod_timer(&rotenc->i2c_timer,
+ jiffies + msecs_to_jiffies(rotenc->refresh_rate_ms));
+
+ if (error) {
+ dev_err(&client->dev, "Unable to register %s timer\n",
+ input_dev->name);
+ goto err1;
+ }
+
+ return 0;
+
+err1: del_timer(&rotenc->i2c_timer);
+ hwmon_device_unregister(rotenc->hwmon_dev);
+err2: input_unregister_device(rotenc->input_dev);
+
+ return error;
+}
+
+static int tlv493da1b6_rotenc_remove(struct i2c_client *client)
+{
+ struct tlv493da1b6_rotenc_data *rotenc = i2c_get_clientdata(client);
+
+ del_timer(&rotenc->i2c_timer);
+ hwmon_device_unregister(rotenc->hwmon_dev);
+ input_unregister_device(rotenc->input_dev);
+ tlv493da1b6_rotenc_power(rotenc, false);
+
+ return 0;
+}
+
+
+static const struct i2c_device_id tlv493da1b6_rotenc_id[] = {
+ { TLV493DA1B6_ROTENC_NAME, 0 },
+ { }
+};
+
+MODULE_DEVICE_TABLE(i2c, tlv493da1b6_rotenc_id);
+
+#ifdef CONFIG_OF
+static const struct of_device_id tlv493da1b6_rotenc_dt_ids[] = {
+ {.compatible = "infineon,tlv493da1b6", },
+ { }
+};
+
+MODULE_DEVICE_TABLE(of, tlv493da1b6_rotenc_dt_ids);
+#endif
+
+static struct i2c_driver tlv493da1b6_rotenc_driver = {
+ .probe = tlv493da1b6_rotenc_probe,
+ .remove = tlv493da1b6_rotenc_remove,
+ .id_table = tlv493da1b6_rotenc_id,
+ .driver = {
+ .name = TLV493DA1B6_ROTENC_NAME,
+ .of_match_table = of_match_ptr(tlv493da1b6_rotenc_dt_ids),
+ },
+};
+
+module_i2c_driver(tlv493da1b6_rotenc_driver);
+
+MODULE_ALIAS(TLV493DA1B6_ROTENC_NAME);
+MODULE_DESCRIPTION("Infineon TLV493D-A1B6 magnetic sensor as rotary
encoder");
+MODULE_AUTHOR("Jakub Ladman <[email protected]>");
+MODULE_LICENSE("GPL v2");
+MODULE_VERSION("0.0.3");
--
