diff options
Diffstat (limited to 'drivers/input/misc/ibm-panel.c')
| -rw-r--r-- | drivers/input/misc/ibm-panel.c | 200 | 
1 files changed, 200 insertions, 0 deletions
diff --git a/drivers/input/misc/ibm-panel.c b/drivers/input/misc/ibm-panel.c new file mode 100644 index 000000000000..a8fba0054719 --- /dev/null +++ b/drivers/input/misc/ibm-panel.c @@ -0,0 +1,200 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) IBM Corporation 2020 + */ + +#include <linux/i2c.h> +#include <linux/init.h> +#include <linux/input.h> +#include <linux/kernel.h> +#include <linux/limits.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/spinlock.h> + +#define DEVICE_NAME		"ibm-panel" +#define PANEL_KEYCODES_COUNT	3 + +struct ibm_panel { +	u8 idx; +	u8 command[11]; +	u32 keycodes[PANEL_KEYCODES_COUNT]; +	spinlock_t lock;	/* protects writes to idx and command */ +	struct input_dev *input; +}; + +static u8 ibm_panel_calculate_checksum(struct ibm_panel *panel) +{ +	u8 chksum; +	u16 sum = 0; +	unsigned int i; + +	for (i = 0; i < sizeof(panel->command) - 1; ++i) { +		sum += panel->command[i]; +		if (sum & 0xff00) { +			sum &= 0xff; +			sum++; +		} +	} + +	chksum = sum & 0xff; +	chksum = ~chksum; +	chksum++; + +	return chksum; +} + +static void ibm_panel_process_command(struct ibm_panel *panel) +{ +	u8 button; +	u8 chksum; + +	if (panel->command[0] != 0xff && panel->command[1] != 0xf0) { +		dev_dbg(&panel->input->dev, "command invalid: %02x %02x\n", +			panel->command[0], panel->command[1]); +		return; +	} + +	chksum = ibm_panel_calculate_checksum(panel); +	if (chksum != panel->command[sizeof(panel->command) - 1]) { +		dev_dbg(&panel->input->dev, +			"command failed checksum: %u != %u\n", chksum, +			panel->command[sizeof(panel->command) - 1]); +		return; +	} + +	button = panel->command[2] & 0xf; +	if (button < PANEL_KEYCODES_COUNT) { +		input_report_key(panel->input, panel->keycodes[button], +				 !(panel->command[2] & 0x80)); +		input_sync(panel->input); +	} else { +		dev_dbg(&panel->input->dev, "unknown button %u\n", +			button); +	} +} + +static int ibm_panel_i2c_slave_cb(struct i2c_client *client, +				  enum i2c_slave_event event, u8 *val) +{ +	unsigned long flags; +	struct ibm_panel *panel = i2c_get_clientdata(client); + +	dev_dbg(&panel->input->dev, "event: %u data: %02x\n", event, *val); + +	spin_lock_irqsave(&panel->lock, flags); + +	switch (event) { +	case I2C_SLAVE_STOP: +		if (panel->idx == sizeof(panel->command)) +			ibm_panel_process_command(panel); +		else +			dev_dbg(&panel->input->dev, +				"command incorrect size %u\n", panel->idx); +		fallthrough; +	case I2C_SLAVE_WRITE_REQUESTED: +		panel->idx = 0; +		break; +	case I2C_SLAVE_WRITE_RECEIVED: +		if (panel->idx < sizeof(panel->command)) +			panel->command[panel->idx++] = *val; +		else +			/* +			 * The command is too long and therefore invalid, so set the index +			 * to it's largest possible value. When a STOP is finally received, +			 * the command will be rejected upon processing. +			 */ +			panel->idx = U8_MAX; +		break; +	case I2C_SLAVE_READ_REQUESTED: +	case I2C_SLAVE_READ_PROCESSED: +		*val = 0xff; +		break; +	default: +		break; +	} + +	spin_unlock_irqrestore(&panel->lock, flags); + +	return 0; +} + +static int ibm_panel_probe(struct i2c_client *client, +			   const struct i2c_device_id *id) +{ +	struct ibm_panel *panel; +	int i; +	int error; + +	panel = devm_kzalloc(&client->dev, sizeof(*panel), GFP_KERNEL); +	if (!panel) +		return -ENOMEM; + +	spin_lock_init(&panel->lock); + +	panel->input = devm_input_allocate_device(&client->dev); +	if (!panel->input) +		return -ENOMEM; + +	panel->input->name = client->name; +	panel->input->id.bustype = BUS_I2C; + +	error = device_property_read_u32_array(&client->dev, +					       "linux,keycodes", +					       panel->keycodes, +					       PANEL_KEYCODES_COUNT); +	if (error) { +		/* +		 * Use gamepad buttons as defaults for compatibility with +		 * existing applications. +		 */ +		panel->keycodes[0] = BTN_NORTH; +		panel->keycodes[1] = BTN_SOUTH; +		panel->keycodes[2] = BTN_SELECT; +	} + +	for (i = 0; i < PANEL_KEYCODES_COUNT; ++i) +		input_set_capability(panel->input, EV_KEY, panel->keycodes[i]); + +	error = input_register_device(panel->input); +	if (error) { +		dev_err(&client->dev, +			"Failed to register input device: %d\n", error); +		return error; +	} + +	i2c_set_clientdata(client, panel); +	error = i2c_slave_register(client, ibm_panel_i2c_slave_cb); +	if (error) { +		dev_err(&client->dev, +			"Failed to register as i2c slave: %d\n", error); +		return error; +	} + +	return 0; +} + +static void ibm_panel_remove(struct i2c_client *client) +{ +	i2c_slave_unregister(client); +} + +static const struct of_device_id ibm_panel_match[] = { +	{ .compatible = "ibm,op-panel" }, +	{ } +}; +MODULE_DEVICE_TABLE(of, ibm_panel_match); + +static struct i2c_driver ibm_panel_driver = { +	.driver = { +		.name = DEVICE_NAME, +		.of_match_table = ibm_panel_match, +	}, +	.probe = ibm_panel_probe, +	.remove = ibm_panel_remove, +}; +module_i2c_driver(ibm_panel_driver); + +MODULE_AUTHOR("Eddie James <[email protected]>"); +MODULE_DESCRIPTION("IBM Operation Panel Driver"); +MODULE_LICENSE("GPL");  |