forked from torvalds/linux
Permalink
Show file tree
Hide file tree
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
mfd: Add base driver for Netronix embedded controller
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 and version detection. Third-party hardware documentation is available at: https://github.com/neuschaefer/linux/wiki/Netronix-MSP430-embedded-controller The EC supports interrupts, but the driver doesn't make use of them so far. Signed-off-by: Jonathan Neuschäfer <j.neuschaefer@gmx.net> Acked-for-MFD-by: Lee Jones <lee.jones@linaro.org>
- Loading branch information
1 parent
3284cf1
commit eee80e6b3b7cc2c733bd3f10d8e2ec23dda2fe26
Showing
4 changed files
with
270 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,221 @@ | ||
| // 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 <j.neuschaefer@gmx.net> | ||
| */ | ||
|
|
||
| #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 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_subdevices[] = { | ||
| { .name = "ntxec-rtc" }, | ||
| { .name = "ntxec-pwm" }, | ||
| }; | ||
|
|
||
| static int ntxec_probe(struct i2c_client *client) | ||
| { | ||
| struct ntxec *ec; | ||
| unsigned int version; | ||
| int res; | ||
|
|
||
| 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 res; | ||
| } | ||
|
|
||
| /* 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: | ||
| 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, ntxec_subdevices, | ||
| ARRAY_SIZE(ntxec_subdevices), NULL, 0, NULL); | ||
| if (res) | ||
| dev_err(ec->dev, "Failed to add subdevices: %d\n", res); | ||
|
|
||
| return res; | ||
| } | ||
|
|
||
| static int 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); | ||
| } | ||
|
|
||
| return 0; | ||
| } | ||
|
|
||
| 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 <j.neuschaefer@gmx.net>"); | ||
| MODULE_DESCRIPTION("Core driver for Netronix EC"); | ||
| MODULE_LICENSE("GPL"); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| /* SPDX-License-Identifier: GPL-2.0-only */ | ||
| /* | ||
| * Copyright 2020 Jonathan Neuschäfer | ||
| * | ||
| * Register access and version information for the Netronix embedded | ||
| * controller. | ||
| */ | ||
|
|
||
| #ifndef NTXEC_H | ||
| #define NTXEC_H | ||
|
|
||
| #include <linux/types.h> | ||
|
|
||
| struct device; | ||
| struct regmap; | ||
|
|
||
| struct ntxec { | ||
| struct device *dev; | ||
| struct regmap *regmap; | ||
| }; | ||
|
|
||
| /* | ||
| * Some registers, such as the battery status register (0x41), are in | ||
| * big-endian, but others only have eight significant bits, which are in the | ||
| * first byte transmitted over I2C (the MSB of the big-endian value). | ||
| * This convenience function converts an 8-bit value to 16-bit for use in the | ||
| * second kind of register. | ||
| */ | ||
| static inline __be16 ntxec_reg8(u8 value) | ||
| { | ||
| return value << 8; | ||
| } | ||
|
|
||
| /* Known firmware versions */ | ||
| #define NTXEC_VERSION_KOBO_AURA 0xd726 /* found in Kobo Aura */ | ||
|
|
||
| #endif |