123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269 |
- // SPDX-License-Identifier: GPL-2.0-or-later
- /*
- * The Netronix embedded controller is a microcontroller found in some
- * e-book readers designed by the original design manufacturer Netronix, Inc.
- * It contains RTC, battery monitoring, system power management, and PWM
- * functionality.
- *
- * This driver implements register access, version detection, and system
- * power-off/reset.
- *
- * Copyright 2020 Jonathan Neuschäfer <[email protected]>
- */
- #include <linux/delay.h>
- #include <linux/errno.h>
- #include <linux/i2c.h>
- #include <linux/mfd/core.h>
- #include <linux/mfd/ntxec.h>
- #include <linux/module.h>
- #include <linux/pm.h>
- #include <linux/reboot.h>
- #include <linux/regmap.h>
- #include <linux/types.h>
- #include <asm/unaligned.h>
- #define NTXEC_REG_VERSION 0x00
- #define NTXEC_REG_POWEROFF 0x50
- #define NTXEC_REG_POWERKEEP 0x70
- #define NTXEC_REG_RESET 0x90
- #define NTXEC_POWEROFF_VALUE 0x0100
- #define NTXEC_POWERKEEP_VALUE 0x0800
- #define NTXEC_RESET_VALUE 0xff00
- static struct i2c_client *poweroff_restart_client;
- static void ntxec_poweroff(void)
- {
- int res;
- u8 buf[3] = { NTXEC_REG_POWEROFF };
- struct i2c_msg msgs[] = {
- {
- .addr = poweroff_restart_client->addr,
- .flags = 0,
- .len = sizeof(buf),
- .buf = buf,
- },
- };
- put_unaligned_be16(NTXEC_POWEROFF_VALUE, buf + 1);
- res = i2c_transfer(poweroff_restart_client->adapter, msgs, ARRAY_SIZE(msgs));
- if (res < 0)
- dev_warn(&poweroff_restart_client->dev,
- "Failed to power off (err = %d)\n", res);
- /*
- * The time from the register write until the host CPU is powered off
- * has been observed to be about 2.5 to 3 seconds. Sleep long enough to
- * safely avoid returning from the poweroff handler.
- */
- msleep(5000);
- }
- static int ntxec_restart(struct notifier_block *nb,
- unsigned long action, void *data)
- {
- int res;
- u8 buf[3] = { NTXEC_REG_RESET };
- /*
- * NOTE: The lower half of the reset value is not sent, because sending
- * it causes an I2C error. (The reset handler in the downstream driver
- * does send the full two-byte value, but doesn't check the result).
- */
- struct i2c_msg msgs[] = {
- {
- .addr = poweroff_restart_client->addr,
- .flags = 0,
- .len = sizeof(buf) - 1,
- .buf = buf,
- },
- };
- put_unaligned_be16(NTXEC_RESET_VALUE, buf + 1);
- res = i2c_transfer(poweroff_restart_client->adapter, msgs, ARRAY_SIZE(msgs));
- if (res < 0)
- dev_warn(&poweroff_restart_client->dev,
- "Failed to restart (err = %d)\n", res);
- return NOTIFY_DONE;
- }
- static struct notifier_block ntxec_restart_handler = {
- .notifier_call = ntxec_restart,
- .priority = 128,
- };
- static int regmap_ignore_write(void *context,
- unsigned int reg, unsigned int val)
- {
- struct regmap *regmap = context;
- regmap_write(regmap, reg, val);
- return 0;
- }
- static int regmap_wrap_read(void *context, unsigned int reg,
- unsigned int *val)
- {
- struct regmap *regmap = context;
- return regmap_read(regmap, reg, val);
- }
- /*
- * Some firmware versions do not ack written data, add a wrapper. It
- * is used to stack another regmap on top.
- */
- static const struct regmap_config regmap_config_noack = {
- .name = "ntxec_noack",
- .reg_bits = 8,
- .val_bits = 16,
- .cache_type = REGCACHE_NONE,
- .reg_write = regmap_ignore_write,
- .reg_read = regmap_wrap_read
- };
- static const struct regmap_config regmap_config = {
- .name = "ntxec",
- .reg_bits = 8,
- .val_bits = 16,
- .cache_type = REGCACHE_NONE,
- .val_format_endian = REGMAP_ENDIAN_BIG,
- };
- static const struct mfd_cell ntxec_subdev[] = {
- { .name = "ntxec-rtc" },
- { .name = "ntxec-pwm" },
- };
- static const struct mfd_cell ntxec_subdev_pwm[] = {
- { .name = "ntxec-pwm" },
- };
- static int ntxec_probe(struct i2c_client *client)
- {
- struct ntxec *ec;
- unsigned int version;
- int res;
- const struct mfd_cell *subdevs;
- size_t n_subdevs;
- ec = devm_kmalloc(&client->dev, sizeof(*ec), GFP_KERNEL);
- if (!ec)
- return -ENOMEM;
- ec->dev = &client->dev;
- ec->regmap = devm_regmap_init_i2c(client, ®map_config);
- if (IS_ERR(ec->regmap)) {
- dev_err(ec->dev, "Failed to set up regmap for device\n");
- return PTR_ERR(ec->regmap);
- }
- /* Determine the firmware version */
- res = regmap_read(ec->regmap, NTXEC_REG_VERSION, &version);
- if (res < 0) {
- dev_err(ec->dev, "Failed to read firmware version number\n");
- return res;
- }
- /* Bail out if we encounter an unknown firmware version */
- switch (version) {
- case NTXEC_VERSION_KOBO_AURA:
- subdevs = ntxec_subdev;
- n_subdevs = ARRAY_SIZE(ntxec_subdev);
- break;
- case NTXEC_VERSION_TOLINO_SHINE2:
- subdevs = ntxec_subdev_pwm;
- n_subdevs = ARRAY_SIZE(ntxec_subdev_pwm);
- /* Another regmap stacked on top of the other */
- ec->regmap = devm_regmap_init(ec->dev, NULL,
- ec->regmap,
- ®map_config_noack);
- if (IS_ERR(ec->regmap))
- return PTR_ERR(ec->regmap);
- break;
- default:
- dev_err(ec->dev,
- "Netronix embedded controller version %04x is not supported.\n",
- version);
- return -ENODEV;
- }
- dev_info(ec->dev,
- "Netronix embedded controller version %04x detected.\n", version);
- if (of_device_is_system_power_controller(ec->dev->of_node)) {
- /*
- * Set the 'powerkeep' bit. This is necessary on some boards
- * in order to keep the system running.
- */
- res = regmap_write(ec->regmap, NTXEC_REG_POWERKEEP,
- NTXEC_POWERKEEP_VALUE);
- if (res < 0)
- return res;
- if (poweroff_restart_client)
- /*
- * Another instance of the driver already took
- * poweroff/restart duties.
- */
- dev_err(ec->dev, "poweroff_restart_client already assigned\n");
- else
- poweroff_restart_client = client;
- if (pm_power_off)
- /* Another driver already registered a poweroff handler. */
- dev_err(ec->dev, "pm_power_off already assigned\n");
- else
- pm_power_off = ntxec_poweroff;
- res = register_restart_handler(&ntxec_restart_handler);
- if (res)
- dev_err(ec->dev,
- "Failed to register restart handler: %d\n", res);
- }
- i2c_set_clientdata(client, ec);
- res = devm_mfd_add_devices(ec->dev, PLATFORM_DEVID_NONE,
- subdevs, n_subdevs, NULL, 0, NULL);
- if (res)
- dev_err(ec->dev, "Failed to add subdevices: %d\n", res);
- return res;
- }
- static void ntxec_remove(struct i2c_client *client)
- {
- if (client == poweroff_restart_client) {
- poweroff_restart_client = NULL;
- pm_power_off = NULL;
- unregister_restart_handler(&ntxec_restart_handler);
- }
- }
- static const struct of_device_id of_ntxec_match_table[] = {
- { .compatible = "netronix,ntxec", },
- {}
- };
- MODULE_DEVICE_TABLE(of, of_ntxec_match_table);
- static struct i2c_driver ntxec_driver = {
- .driver = {
- .name = "ntxec",
- .of_match_table = of_ntxec_match_table,
- },
- .probe_new = ntxec_probe,
- .remove = ntxec_remove,
- };
- module_i2c_driver(ntxec_driver);
- MODULE_AUTHOR("Jonathan Neuschäfer <[email protected]>");
- MODULE_DESCRIPTION("Core driver for Netronix EC");
- MODULE_LICENSE("GPL");
|