| // SPDX-License-Identifier: BSD-2-Clause OR GPL-2.0-or-later | 
 | /* | 
 |  * ENE KB3930 Embedded Controller Driver | 
 |  * | 
 |  * Copyright (C) 2020 Lubomir Rintel | 
 |  */ | 
 |  | 
 | #include <linux/delay.h> | 
 | #include <linux/gpio/consumer.h> | 
 | #include <linux/i2c.h> | 
 | #include <linux/mfd/core.h> | 
 | #include <linux/module.h> | 
 | #include <linux/reboot.h> | 
 | #include <linux/regmap.h> | 
 |  | 
 | /* I2C registers that are multiplexing access to the EC RAM. */ | 
 | enum { | 
 | 	EC_DATA_IN	= 0x00, | 
 | 	EC_RAM_OUT	= 0x80, | 
 | 	EC_RAM_IN	= 0x81, | 
 | }; | 
 |  | 
 | /* EC RAM registers. */ | 
 | enum { | 
 | 	EC_MODEL	= 0x30, | 
 | 	EC_VERSION_MAJ	= 0x31, | 
 | 	EC_VERSION_MIN	= 0x32, | 
 | }; | 
 |  | 
 | struct kb3930 { | 
 | 	struct i2c_client *client; | 
 | 	struct regmap *ram_regmap; | 
 | 	struct gpio_descs *off_gpios; | 
 | }; | 
 |  | 
 | struct kb3930 *kb3930_power_off; | 
 |  | 
 | #define EC_GPIO_WAVE		0 | 
 | #define EC_GPIO_OFF_MODE	1 | 
 |  | 
 | #define EC_OFF_MODE_REBOOT	0 | 
 | #define EC_OFF_MODE_POWER	1 | 
 |  | 
 | static void kb3930_off(struct kb3930 *ddata, int off_mode) | 
 | { | 
 | 	gpiod_direction_output(ddata->off_gpios->desc[EC_GPIO_OFF_MODE], | 
 | 			       off_mode); | 
 |  | 
 | 	/* | 
 | 	 * This creates a 10 Hz wave on EC_GPIO_WAVE that signals a | 
 | 	 * shutdown request to the EC. Once the EC detects it, it will | 
 | 	 * proceed to turn the power off or reset the board depending on | 
 | 	 * the value of EC_GPIO_OFF_MODE. | 
 | 	 */ | 
 | 	while (1) { | 
 | 		mdelay(50); | 
 | 		gpiod_direction_output(ddata->off_gpios->desc[EC_GPIO_WAVE], 0); | 
 | 		mdelay(50); | 
 | 		gpiod_direction_output(ddata->off_gpios->desc[EC_GPIO_WAVE], 1); | 
 | 	} | 
 | } | 
 |  | 
 | static int kb3930_restart(struct notifier_block *this, | 
 | 			  unsigned long mode, void *cmd) | 
 | { | 
 | 	kb3930_off(kb3930_power_off, EC_OFF_MODE_REBOOT); | 
 | 	return NOTIFY_DONE; | 
 | } | 
 |  | 
 | static void kb3930_pm_power_off(void) | 
 | { | 
 | 	kb3930_off(kb3930_power_off, EC_OFF_MODE_POWER); | 
 | } | 
 |  | 
 | static struct notifier_block kb3930_restart_nb = { | 
 | 	.notifier_call = kb3930_restart, | 
 | }; | 
 |  | 
 | static const struct mfd_cell ariel_ec_cells[] = { | 
 | 	{ .name = "dell-wyse-ariel-led", }, | 
 | 	{ .name = "dell-wyse-ariel-power", }, | 
 | }; | 
 |  | 
 | static int kb3930_ec_ram_reg_write(void *context, unsigned int reg, | 
 | 				   unsigned int val) | 
 | { | 
 | 	struct kb3930 *ddata = context; | 
 |  | 
 | 	return i2c_smbus_write_word_data(ddata->client, EC_RAM_OUT, | 
 | 					 (val << 8) | reg); | 
 | } | 
 |  | 
 | static int kb3930_ec_ram_reg_read(void *context, unsigned int reg, | 
 | 				  unsigned int *val) | 
 | { | 
 | 	struct kb3930 *ddata = context; | 
 | 	int ret; | 
 |  | 
 | 	ret = i2c_smbus_write_word_data(ddata->client, EC_RAM_IN, reg); | 
 | 	if (ret < 0) | 
 | 		return ret; | 
 |  | 
 | 	ret = i2c_smbus_read_word_data(ddata->client, EC_DATA_IN); | 
 | 	if (ret < 0) | 
 | 		return ret; | 
 |  | 
 | 	*val = ret >> 8; | 
 | 	return 0; | 
 | } | 
 |  | 
 | static const struct regmap_config kb3930_ram_regmap_config = { | 
 | 	.name = "ec_ram", | 
 | 	.reg_bits = 8, | 
 | 	.val_bits = 8, | 
 | 	.reg_stride = 1, | 
 | 	.max_register = 0xff, | 
 | 	.reg_write = kb3930_ec_ram_reg_write, | 
 | 	.reg_read = kb3930_ec_ram_reg_read, | 
 | 	.fast_io = false, | 
 | }; | 
 |  | 
 | static int kb3930_probe(struct i2c_client *client) | 
 | { | 
 | 	struct device *dev = &client->dev; | 
 | 	struct device_node *np = dev->of_node; | 
 | 	struct kb3930 *ddata; | 
 | 	unsigned int model; | 
 | 	int ret; | 
 |  | 
 | 	ddata = devm_kzalloc(dev, sizeof(*ddata), GFP_KERNEL); | 
 | 	if (!ddata) | 
 | 		return -ENOMEM; | 
 |  | 
 | 	kb3930_power_off = ddata; | 
 | 	ddata->client = client; | 
 | 	i2c_set_clientdata(client, ddata); | 
 |  | 
 | 	ddata->ram_regmap = devm_regmap_init(dev, NULL, ddata, | 
 | 					     &kb3930_ram_regmap_config); | 
 | 	if (IS_ERR(ddata->ram_regmap)) | 
 | 		return PTR_ERR(ddata->ram_regmap); | 
 |  | 
 | 	ret = regmap_read(ddata->ram_regmap, EC_MODEL, &model); | 
 | 	if (ret < 0) | 
 | 		return ret; | 
 |  | 
 | 	/* Currently we only support the cells present on Dell Ariel model. */ | 
 | 	if (model != 'J') { | 
 | 		dev_err(dev, "unknown board model: %02x\n", model); | 
 | 		return -ENODEV; | 
 | 	} | 
 |  | 
 | 	ret = devm_mfd_add_devices(dev, PLATFORM_DEVID_AUTO, | 
 | 				   ariel_ec_cells, | 
 | 				   ARRAY_SIZE(ariel_ec_cells), | 
 | 				   NULL, 0, NULL); | 
 | 	if (ret) | 
 | 		return ret; | 
 |  | 
 | 	if (of_property_read_bool(np, "system-power-controller")) { | 
 | 		ddata->off_gpios = | 
 | 			devm_gpiod_get_array_optional(dev, "off", GPIOD_IN); | 
 | 		if (IS_ERR(ddata->off_gpios)) | 
 | 			return PTR_ERR(ddata->off_gpios); | 
 | 		if (ddata->off_gpios->ndescs < 2) { | 
 | 			dev_err(dev, "invalid off-gpios property\n"); | 
 | 			return -EINVAL; | 
 | 		} | 
 | 	} | 
 |  | 
 | 	if (ddata->off_gpios) { | 
 | 		register_restart_handler(&kb3930_restart_nb); | 
 | 		if (!pm_power_off) | 
 | 			pm_power_off = kb3930_pm_power_off; | 
 | 	} | 
 |  | 
 | 	return 0; | 
 | } | 
 |  | 
 | static int kb3930_remove(struct i2c_client *client) | 
 | { | 
 | 	struct kb3930 *ddata = i2c_get_clientdata(client); | 
 |  | 
 | 	if (ddata->off_gpios) { | 
 | 		if (pm_power_off == kb3930_pm_power_off) | 
 | 			pm_power_off = NULL; | 
 | 		unregister_restart_handler(&kb3930_restart_nb); | 
 | 	} | 
 | 	kb3930_power_off = NULL; | 
 |  | 
 | 	return 0; | 
 | } | 
 |  | 
 | static const struct of_device_id kb3930_dt_ids[] = { | 
 | 	{ .compatible = "ene,kb3930" }, | 
 | 	{ } | 
 | }; | 
 | MODULE_DEVICE_TABLE(of, kb3930_dt_ids); | 
 |  | 
 | static struct i2c_driver kb3930_driver = { | 
 | 	.probe_new = kb3930_probe, | 
 | 	.remove = kb3930_remove, | 
 | 	.driver = { | 
 | 		.name = "ene-kb3930", | 
 | 		.of_match_table = of_match_ptr(kb3930_dt_ids), | 
 | 	}, | 
 | }; | 
 | module_i2c_driver(kb3930_driver); | 
 |  | 
 | MODULE_AUTHOR("Lubomir Rintel <[email protected]>"); | 
 | MODULE_DESCRIPTION("ENE KB3930 Embedded Controller Driver"); | 
 | MODULE_LICENSE("Dual BSD/GPL"); |