From 87b29460ab4f823147d6ce732e6f57552a53b2cc Mon Sep 17 00:00:00 2001
From: Dmitry Osipenko <digetx@gmail.com>
Date: Fri, 25 Oct 2013 21:06:08 +0400
Subject: [PATCH] staging: Add a50x EC compliant drivers

Signed-off-by: Dmitry Osipenko <digetx@gmail.com>
---
 drivers/staging/Kconfig                     |   2 +
 drivers/staging/Makefile                    |   2 +
 drivers/staging/a500/Kconfig                |  12 +
 drivers/staging/a500/Makefile               |   1 +
 drivers/staging/a500/ec/Kconfig             |  25 +
 drivers/staging/a500/ec/Makefile            |   4 +
 drivers/staging/a500/ec/a500_ec.c           | 217 +++++++++
 drivers/staging/a500/ec/a500_ec_battery.c   | 426 +++++++++++++++++
 drivers/staging/a500/ec/a500_ec_leds.c      | 119 +++++
 drivers/staging/a500/ec/a500_legacy_sysfs.c | 493 ++++++++++++++++++++
 drivers/staging/a500/ec/ec.h                |  32 ++
 11 files changed, 1333 insertions(+)
 create mode 100644 drivers/staging/a500/Kconfig
 create mode 100644 drivers/staging/a500/Makefile
 create mode 100644 drivers/staging/a500/ec/Kconfig
 create mode 100644 drivers/staging/a500/ec/Makefile
 create mode 100644 drivers/staging/a500/ec/a500_ec.c
 create mode 100644 drivers/staging/a500/ec/a500_ec_battery.c
 create mode 100644 drivers/staging/a500/ec/a500_ec_leds.c
 create mode 100644 drivers/staging/a500/ec/a500_legacy_sysfs.c
 create mode 100644 drivers/staging/a500/ec/ec.h

diff --git a/drivers/staging/Kconfig b/drivers/staging/Kconfig
index 7c197d1a1231..46e38e702dd8 100644
--- a/drivers/staging/Kconfig
+++ b/drivers/staging/Kconfig
@@ -24,6 +24,8 @@ menuconfig STAGING
 
 if STAGING
 
+source "drivers/staging/a500/Kconfig"
+
 source "drivers/staging/slicoss/Kconfig"
 
 source "drivers/staging/wlan-ng/Kconfig"
diff --git a/drivers/staging/Makefile b/drivers/staging/Makefile
index a470c7276142..4656175ffce2 100644
--- a/drivers/staging/Makefile
+++ b/drivers/staging/Makefile
@@ -1,5 +1,7 @@
 # Makefile for staging directory
 
+obj-$(CONFIG_A500)		+= a500/
+
 obj-y				+= media/
 obj-$(CONFIG_SLICOSS)		+= slicoss/
 obj-$(CONFIG_PRISM2_USB)	+= wlan-ng/
diff --git a/drivers/staging/a500/Kconfig b/drivers/staging/a500/Kconfig
new file mode 100644
index 000000000000..859ee0df47a9
--- /dev/null
+++ b/drivers/staging/a500/Kconfig
@@ -0,0 +1,12 @@
+config A500
+	bool "Acer A500 drivers"
+	depends on ARCH_TEGRA
+	default n
+	---help---
+	  Say Y here to build Acer A500 tablet device drivers.
+
+if A500
+
+source "drivers/staging/a500/ec/Kconfig"
+
+endif # A500
diff --git a/drivers/staging/a500/Makefile b/drivers/staging/a500/Makefile
new file mode 100644
index 000000000000..e4dfc34989a6
--- /dev/null
+++ b/drivers/staging/a500/Makefile
@@ -0,0 +1 @@
+obj-$(CONFIG_A500)	+= ec/
diff --git a/drivers/staging/a500/ec/Kconfig b/drivers/staging/a500/ec/Kconfig
new file mode 100644
index 000000000000..e20477516130
--- /dev/null
+++ b/drivers/staging/a500/ec/Kconfig
@@ -0,0 +1,25 @@
+config MFD_EC_A500
+	bool "Embedded Controller driver for Acer A50x tablets"
+	depends on I2C && ARCH_TEGRA
+	select MFD_CORE
+	help
+	  Say Y to include support for Acer A50x compliant embedded
+	  controller.
+
+config EC_A500_LEGACY_SYSFS
+	tristate "Legacy A50x EC sysfs for Android compatibility"
+	depends on MFD_EC_A500
+	help
+	  Say Y to include support for Acer A50x EC legacy sysfs.
+
+config EC_A500_BATTERY
+	tristate "Acer A50x battery driver"
+	depends on MFD_EC_A500 && POWER_SUPPLY
+	help
+	  Say Y to include support for battery behind A500 EC.
+
+config EC_A500_LEDS
+	tristate "LEDs support for Acer A50x"
+	depends on MFD_EC_A500 && LEDS_CLASS
+	help
+	  Say Y to enable control of power button leds.
diff --git a/drivers/staging/a500/ec/Makefile b/drivers/staging/a500/ec/Makefile
new file mode 100644
index 000000000000..77a86477592f
--- /dev/null
+++ b/drivers/staging/a500/ec/Makefile
@@ -0,0 +1,4 @@
+obj-$(CONFIG_EC_A500_BATTERY)		+= a500_ec_battery.o
+obj-$(CONFIG_EC_A500_LEDS)		+= a500_ec_leds.o
+obj-$(CONFIG_EC_A500_LEGACY_SYSFS)	+= a500_legacy_sysfs.o
+obj-$(CONFIG_MFD_EC_A500)		+= a500_ec.o
diff --git a/drivers/staging/a500/ec/a500_ec.c b/drivers/staging/a500/ec/a500_ec.c
new file mode 100644
index 000000000000..ccaadc99d7c5
--- /dev/null
+++ b/drivers/staging/a500/ec/a500_ec.c
@@ -0,0 +1,217 @@
+/*
+ * MFD driver for Acer A50x embedded controller
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ */
+
+#include <linux/module.h>
+#include <linux/delay.h>
+#include <linux/i2c.h>
+#include <linux/mfd/core.h>
+#include <linux/of_device.h>
+
+#include <asm/system_misc.h>
+
+#include "ec.h"
+
+/*				addr	timeout */
+EC_REG_DATA(SHUTDOWN,		0x52,	0);
+EC_REG_DATA(WARM_REBOOT,	0x54,	0);
+EC_REG_DATA(COLD_REBOOT,	0x55,	1000);
+
+static DEFINE_MUTEX(ec_mutex);
+
+static struct ec_info {
+	struct i2c_client	*client;
+	struct notifier_block	panic_notifier;
+	int			i2c_retry_count;
+} *ec_chip;
+
+inline void ec_lock(void)
+{
+	mutex_lock(&ec_mutex);
+}
+EXPORT_SYMBOL_GPL(ec_lock);
+
+inline void ec_unlock(void)
+{
+	mutex_unlock(&ec_mutex);
+}
+EXPORT_SYMBOL_GPL(ec_unlock);
+
+#define I2C_ERR_TIMEOUT	500
+int ec_read_word_data_locked(struct ec_reg_data *reg_data)
+{
+	struct i2c_client *client = ec_chip->client;
+	int retries = ec_chip->i2c_retry_count;
+	s32 ret = 0;
+
+	while (retries > 0) {
+		ret = i2c_smbus_read_word_data(client, reg_data->addr);
+		if (ret >= 0)
+			break;
+		msleep(I2C_ERR_TIMEOUT);
+		retries--;
+	}
+
+	if (ret < 0) {
+		dev_err(&client->dev, "%s: i2c read at address 0x%x failed\n",
+			__func__, reg_data->addr);
+		return ret;
+	}
+
+	msleep(reg_data->timeout);
+
+	return le16_to_cpu(ret);
+}
+EXPORT_SYMBOL_GPL(ec_read_word_data_locked);
+
+int ec_read_word_data(struct ec_reg_data *reg_data)
+{
+	s32 ret;
+
+	ec_lock();
+	ret = ec_read_word_data_locked(reg_data);
+	ec_unlock();
+
+	return ret;
+}
+EXPORT_SYMBOL_GPL(ec_read_word_data);
+
+int ec_write_word_data_locked(struct ec_reg_data *reg_data, u16 value)
+{
+	struct i2c_client *client = ec_chip->client;
+	int retries = ec_chip->i2c_retry_count;
+	s32 ret = 0;
+
+	while (retries > 0) {
+		ret = i2c_smbus_write_word_data(client, reg_data->addr,
+						le16_to_cpu(value));
+		if (ret >= 0)
+			break;
+		msleep(I2C_ERR_TIMEOUT);
+		retries--;
+	}
+
+	if (ret < 0) {
+		dev_err(&client->dev, "%s: i2c write to address 0x%x failed\n",
+			__func__, reg_data->addr);
+		return ret;
+	}
+
+	msleep(reg_data->timeout);
+
+	return 0;
+}
+EXPORT_SYMBOL_GPL(ec_write_word_data_locked);
+
+int ec_write_word_data(struct ec_reg_data *reg_data, u16 value)
+{
+	s32 ret;
+
+	ec_lock();
+	ret = ec_write_word_data_locked(reg_data, value);
+	ec_unlock();
+
+	return ret;
+}
+EXPORT_SYMBOL_GPL(ec_write_word_data);
+
+static void ec_poweroff(void)
+{
+	dev_info(&ec_chip->client->dev, "poweroff ...\n");
+
+	ec_write_word_data(SHUTDOWN, 0);
+}
+
+static void ec_reboot(enum reboot_mode mode, const char *cmd)
+{
+	if (oops_in_progress)
+		ec_write_word_data_locked(WARM_REBOOT, 0);
+
+	dev_info(&ec_chip->client->dev, "reboot ...\n");
+
+	ec_write_word_data(COLD_REBOOT, 1);
+}
+
+static struct mfd_cell ec_cell[] = {
+	{
+		.name = "a50x-battery",
+		.of_compatible = "acer,a50x-battery",
+		.id = 1,
+	},
+	{
+		.name = "a50x-leds",
+		.of_compatible = "acer,a50x-leds",
+		.id = 1,
+	},
+};
+
+static int ec_probe(struct i2c_client *client,
+				const struct i2c_device_id *id)
+{
+	struct device_node *np = client->dev.of_node;
+	int ret;
+
+	ec_chip = devm_kzalloc(&client->dev, sizeof(*ec_chip), GFP_KERNEL);
+	if (!ec_chip)
+		return -ENOMEM;
+
+	if (of_property_read_u32(np, "ec,i2c-retry-count",
+				 &ec_chip->i2c_retry_count))
+		ec_chip->i2c_retry_count = 5;
+
+	ec_chip->client = client;
+
+	/* register battery and leds */
+	ret = mfd_add_devices(&client->dev, -1,
+			      ec_cell, ARRAY_SIZE(ec_cell),
+			      NULL, 0, NULL);
+	if (ret) {
+		dev_err(&client->dev, "Failed to add subdevices\n");
+		return ret;
+	}
+
+	/* set pm functions */
+	if (of_property_read_bool(np, "system-power-controller")) {
+		arm_pm_restart = ec_reboot;
+
+		if (!pm_power_off)
+			pm_power_off = ec_poweroff;
+	}
+
+	dev_dbg(&client->dev, "device registered\n");
+
+	return 0;
+}
+
+static const struct of_device_id ec_match[] = {
+	{ .compatible = "acer,a50x-ec" },
+	{ }
+};
+MODULE_DEVICE_TABLE(of, ec_match);
+
+static const struct i2c_device_id ec_id[] = {
+	{ "a50x_EC", 0 },
+	{}
+};
+MODULE_DEVICE_TABLE(i2c, ec_id);
+
+static struct i2c_driver a50x_ec_driver = {
+	.driver = {
+		.name = "a50x-ec",
+		.of_match_table = ec_match,
+		.owner = THIS_MODULE,
+	},
+	.id_table = ec_id,
+	.probe = ec_probe,
+};
+module_i2c_driver(a50x_ec_driver);
+
+MODULE_ALIAS("i2c:a50x-ec");
+MODULE_DESCRIPTION("Acer A50x EC MFD driver");
+MODULE_AUTHOR("Dmitry Osipenko <digetx@gmail.com>");
+MODULE_LICENSE("GPL");
diff --git a/drivers/staging/a500/ec/a500_ec_battery.c b/drivers/staging/a500/ec/a500_ec_battery.c
new file mode 100644
index 000000000000..f47087553c32
--- /dev/null
+++ b/drivers/staging/a500/ec/a500_ec_battery.c
@@ -0,0 +1,426 @@
+/*
+ * Battery driver for Acer A50x tablets
+ *
+ * based on nVidia's SBS and Acer's EC battery drivers
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ */
+
+#include <linux/module.h>
+#include <linux/platform_device.h>
+#include <linux/power_supply.h>
+
+#include "ec.h"
+
+#define BATTERY_NAME		"ec-battery"
+#define POLL_TIME_DEFAULT	60
+
+enum {
+	REG_VOLTAGE,
+	REG_CAPACITY,
+	REG_HEALTH,
+	REG_DESIGN_CAPACITY,
+	REG_SERIAL_NUMBER,
+	REG_TEMPERATURE,
+	REG_CURRENT,
+};
+
+#define EC_DATA(_psp, _addr, _timeout) {	\
+	.psp = _psp,				\
+	.reg_data = {				\
+		.addr = _addr,			\
+		.timeout = _timeout		\
+	},					\
+}
+
+static struct chip_data {
+	enum power_supply_property psp;
+	struct ec_reg_data reg_data;
+} ec_data[] = {
+	[REG_VOLTAGE] =
+		EC_DATA(POWER_SUPPLY_PROP_VOLTAGE_NOW, 0x01, 0),
+	[REG_CAPACITY] =
+		EC_DATA(POWER_SUPPLY_PROP_CAPACITY, 0x00, 0),
+	[REG_CURRENT] =
+		EC_DATA(POWER_SUPPLY_PROP_CURRENT_NOW, 0x03, 10),
+	[REG_DESIGN_CAPACITY] =
+		EC_DATA(POWER_SUPPLY_PROP_ENERGY_FULL_DESIGN, 0x08, 0),
+	[REG_HEALTH] =
+		EC_DATA(POWER_SUPPLY_PROP_HEALTH, 0x09, 10),
+	[REG_SERIAL_NUMBER] =
+		EC_DATA(POWER_SUPPLY_PROP_SERIAL_NUMBER, 0x6a, 0),
+	[REG_TEMPERATURE] =
+		EC_DATA(POWER_SUPPLY_PROP_TEMP, 0x0A, 0),
+};
+
+static enum power_supply_property ec_properties[] = {
+	POWER_SUPPLY_PROP_STATUS,
+	POWER_SUPPLY_PROP_HEALTH,
+	POWER_SUPPLY_PROP_ENERGY_FULL_DESIGN,
+	POWER_SUPPLY_PROP_PRESENT,
+	POWER_SUPPLY_PROP_TECHNOLOGY,
+	POWER_SUPPLY_PROP_VOLTAGE_NOW,
+	POWER_SUPPLY_PROP_CAPACITY,
+	POWER_SUPPLY_PROP_TEMP,
+	POWER_SUPPLY_PROP_SERIAL_NUMBER,
+	POWER_SUPPLY_PROP_CURRENT_NOW,
+};
+
+struct ec_battery_info {
+	struct device 			*dev;
+	struct delayed_work		poll_work;
+	struct notifier_block		panic_notifier;
+	struct power_supply		*bat;
+	struct power_supply_desc	bat_desc;
+	bool				poll_disabled;
+	bool				is_supplied;
+	bool				is_panic;
+	int				capacity;
+	int				poll_interval;
+};
+
+static int ec_get_battery_presence(union power_supply_propval *val)
+{
+	s32 ret;
+
+	ret = ec_read_word_data(&ec_data[REG_DESIGN_CAPACITY].reg_data);
+	if (ret <= 0)
+		val->intval = 0;
+	else
+		val->intval = 1;
+
+	return 0;
+}
+
+static int ec_get_battery_health(union power_supply_propval *val)
+{
+	s32 ret;
+
+	ret = ec_read_word_data(&ec_data[REG_HEALTH].reg_data);
+	if (ret < 0)
+		return ret;
+
+	if (ret > 50)
+		val->intval = POWER_SUPPLY_HEALTH_GOOD;
+	else
+		val->intval = POWER_SUPPLY_HEALTH_DEAD;
+
+	return 0;
+}
+
+static bool ec_get_battery_capacity(struct ec_battery_info *chip)
+{
+	int capacity;
+	s32 ret;
+
+	ret = ec_read_word_data(&ec_data[REG_CAPACITY].reg_data);
+	if (ret < 0)
+		return false;
+
+	/* sbs spec says that this can be >100 %
+	* even if max value is 100 % */
+	capacity = min(ret, 100);
+
+	if (chip->capacity != capacity) {
+		chip->capacity = capacity;
+		return true;
+	}
+
+	return false;
+}
+
+static void ec_get_battery_status(struct ec_battery_info *chip,
+				  union power_supply_propval *val)
+{
+	if (chip->capacity < 100) {
+		if (chip->is_supplied)
+			val->intval = POWER_SUPPLY_STATUS_CHARGING;
+		else
+			val->intval = POWER_SUPPLY_STATUS_DISCHARGING;
+	} else
+		val->intval = POWER_SUPPLY_STATUS_FULL;
+}
+
+static int ec_get_battery_property(int reg_offset,
+				   union power_supply_propval *val)
+{
+	s32 ret;
+
+	ret = ec_read_word_data(&ec_data[reg_offset].reg_data);
+	if (ret < 0)
+		return ret;
+
+	val->intval = ret;
+
+	return 0;
+}
+
+static void ec_unit_adjustment(struct device *dev,
+				enum power_supply_property psp,
+				union power_supply_propval *val)
+{
+#define BASE_UNIT_CONVERSION		1000
+#define TEMP_KELVIN_TO_CELSIUS		2731
+	switch (psp) {
+	case POWER_SUPPLY_PROP_CURRENT_NOW:
+		val->intval = (s16) val->intval;
+		break;
+
+	case POWER_SUPPLY_PROP_ENERGY_FULL_DESIGN:
+	case POWER_SUPPLY_PROP_VOLTAGE_NOW:
+		val->intval *= BASE_UNIT_CONVERSION;
+		break;
+
+	case POWER_SUPPLY_PROP_TEMP:
+		/* sbs provides battery temperature in 0.1K
+		 * so convert it to 0.1°C
+		 */
+		val->intval -= TEMP_KELVIN_TO_CELSIUS;
+		break;
+
+	default:
+		dev_dbg(dev,
+			"%s: no need for unit conversion %d\n", __func__, psp);
+	}
+}
+
+#define SERIAL_PARTS_NB	11
+#define SERIAL_STRLEN	(SERIAL_PARTS_NB * 2 + 1)
+static char ec_serial[SERIAL_STRLEN] = "";
+
+static int ec_get_battery_serial_number(union power_supply_propval *val)
+{
+	s32 ret;
+	int i;
+
+	if (strlen(ec_serial) == 0) {
+		ec_lock();
+		for (i = 0; i < SERIAL_PARTS_NB; i++) {
+			ret = ec_read_word_data_locked(
+					&ec_data[REG_SERIAL_NUMBER].reg_data);
+
+			snprintf(ec_serial, SERIAL_STRLEN,
+				"%s%s", ec_serial, (char *)&ret);
+		}
+		ec_unlock();
+	}
+
+	val->strval = ec_serial;
+
+	return 0;
+}
+
+static int ec_get_property_index(struct device *dev,
+				 enum power_supply_property psp)
+{
+	int count;
+	for (count = 0; count < ARRAY_SIZE(ec_data); count++)
+		if (psp == ec_data[count].psp)
+			return count;
+
+	dev_warn(dev, "%s: Invalid Property - %d\n", __func__, psp);
+
+	return -EINVAL;
+}
+
+static int ec_get_property(struct power_supply *psy,
+			   enum power_supply_property psp,
+			   union power_supply_propval *val)
+{
+	struct ec_battery_info *chip = power_supply_get_drvdata(psy);
+	int ret = 0;
+
+	switch (psp) {
+	case POWER_SUPPLY_PROP_SERIAL_NUMBER:
+		ret = ec_get_battery_serial_number(val);
+		break;
+
+	case POWER_SUPPLY_PROP_HEALTH:
+		ret = ec_get_battery_health(val);
+		break;
+
+	case POWER_SUPPLY_PROP_PRESENT:
+		ret = ec_get_battery_presence(val);
+		break;
+
+	case POWER_SUPPLY_PROP_STATUS:
+		ec_get_battery_status(chip, val);
+		break;
+
+	case POWER_SUPPLY_PROP_TECHNOLOGY:
+		val->intval = POWER_SUPPLY_TECHNOLOGY_LION;
+		break;
+
+	case POWER_SUPPLY_PROP_CAPACITY:
+		ec_get_battery_capacity(chip);
+		val->intval = chip->capacity;
+		break;
+
+	case POWER_SUPPLY_PROP_ENERGY_FULL_DESIGN:
+	case POWER_SUPPLY_PROP_CURRENT_NOW:
+	case POWER_SUPPLY_PROP_VOLTAGE_NOW:
+	case POWER_SUPPLY_PROP_TEMP:
+		ret = ec_get_property_index(chip->dev, psp);
+		if (ret < 0)
+			break;
+
+		ret = ec_get_battery_property(ret, val);
+		break;
+
+	default:
+		dev_err(chip->dev, "%s: INVALID property\n", __func__);
+		return -EINVAL;
+	}
+
+	if (!ret) {
+		/* Convert units to match requirements for power supply class */
+		ec_unit_adjustment(chip->dev, psp, val);
+	}
+
+	dev_dbg(chip->dev,
+		"%s: property = %d, value = %x\n", __func__, psp, val->intval);
+
+	/* battery not present, so return NODATA for properties */
+	if (ret)
+		return -ENODATA;
+
+	return 0;
+}
+
+static void ec_external_power_changed(struct power_supply *psy)
+{
+	struct ec_battery_info *chip = power_supply_get_drvdata(psy);
+	bool supplied_state;
+
+	supplied_state = power_supply_am_i_supplied(psy);
+
+	/* suppress bogus notifications */
+	if (chip->is_supplied != supplied_state) {
+		chip->is_supplied = supplied_state;
+		/* notify OS immediately */
+		flush_delayed_work(&chip->poll_work);
+	}
+}
+
+static void ec_delayed_work(struct work_struct *work)
+{
+	struct ec_battery_info *chip;
+	bool capacity_changed;
+
+	chip = container_of(work, struct ec_battery_info, poll_work.work);
+
+	if (chip->poll_disabled)
+		return;
+
+	capacity_changed = ec_get_battery_capacity(chip);
+
+	if (capacity_changed)
+		power_supply_changed(chip->bat);
+
+	/* send continuous uevent notify */
+	set_timer_slack(&chip->poll_work.timer, chip->poll_interval * HZ / 4);
+	schedule_delayed_work(&chip->poll_work, chip->poll_interval * HZ);
+}
+
+static int ec_probe(struct platform_device *pdev)
+{
+	struct power_supply_config psy_cfg = {};
+	struct ec_battery_info *chip;
+
+	chip = devm_kzalloc(&pdev->dev, sizeof(*chip), GFP_KERNEL);
+	if (!chip)
+		return -ENOMEM;
+
+	psy_cfg.of_node = pdev->dev.of_node;
+	psy_cfg.drv_data = chip;
+
+	chip->dev = &pdev->dev;
+	chip->poll_interval = POLL_TIME_DEFAULT;
+	chip->capacity = -1;
+
+	chip->bat_desc.name = BATTERY_NAME;
+	chip->bat_desc.type = POWER_SUPPLY_TYPE_BATTERY;
+	chip->bat_desc.properties = ec_properties;
+	chip->bat_desc.num_properties = ARRAY_SIZE(ec_properties);
+	chip->bat_desc.get_property = ec_get_property;
+	chip->bat_desc.external_power_changed = ec_external_power_changed;
+
+	chip->bat = power_supply_register_no_ws(&pdev->dev,
+						&chip->bat_desc, &psy_cfg);
+	if (IS_ERR(chip->bat)) {
+		dev_err(&pdev->dev,
+			"%s: Failed to register power supply\n", __func__);
+		return PTR_ERR(chip->bat);
+	}
+
+	if (power_supply_am_i_supplied(chip->bat))
+		chip->is_supplied = true;
+
+	INIT_DELAYED_WORK(&chip->poll_work, ec_delayed_work);
+	schedule_work(&chip->poll_work.work);
+
+	platform_set_drvdata(pdev, chip);
+
+	return 0;
+}
+
+static int ec_remove(struct platform_device *pdev)
+{
+	struct ec_battery_info *chip = dev_get_drvdata(&pdev->dev);
+
+	chip->poll_disabled = true;
+	cancel_delayed_work_sync(&chip->poll_work);
+
+	power_supply_unregister(chip->bat);
+
+	return 0;
+}
+
+#ifdef CONFIG_PM_SLEEP
+static int ec_suspend(struct device *dev)
+{
+	struct ec_battery_info *chip = dev_get_drvdata(dev);
+
+	chip->poll_disabled = true;
+	cancel_delayed_work_sync(&chip->poll_work);
+
+	return 0;
+}
+
+static int ec_resume(struct device *dev)
+{
+	struct ec_battery_info *chip = dev_get_drvdata(dev);
+
+	chip->poll_disabled = false;
+	schedule_delayed_work(&chip->poll_work, HZ);
+
+	return 0;
+}
+#endif
+
+static SIMPLE_DEV_PM_OPS(ec_battery_pm_ops, ec_suspend, ec_resume);
+
+static const struct of_device_id ec_battery_match[] = {
+	{ .compatible = "acer,a50x-battery" },
+	{ }
+};
+MODULE_DEVICE_TABLE(of, ec_battery_match);
+
+static struct platform_driver ec_battery_driver = {
+	.driver = {
+		.name = "a50x-battery",
+		.of_match_table = ec_battery_match,
+		.owner = THIS_MODULE,
+		.pm = &ec_battery_pm_ops,
+	},
+	.probe = ec_probe,
+	.remove = ec_remove,
+};
+module_platform_driver(ec_battery_driver);
+
+MODULE_DESCRIPTION("Acer A50x EC battery driver");
+MODULE_AUTHOR("Dmitry Osipenko <digetx@gmail.com>");
+MODULE_LICENSE("GPL");
diff --git a/drivers/staging/a500/ec/a500_ec_leds.c b/drivers/staging/a500/ec/a500_ec_leds.c
new file mode 100644
index 000000000000..8e2b60de1834
--- /dev/null
+++ b/drivers/staging/a500/ec/a500_ec_leds.c
@@ -0,0 +1,119 @@
+/*
+ * LEDs driver for Acer A50x tablets
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ */
+
+#include <linux/leds.h>
+#include <linux/module.h>
+#include <linux/of_device.h>
+
+#include "ec.h"
+
+struct ec_led {
+	struct led_classdev	cdev;
+	struct work_struct	work;
+	u8			new_state;
+};
+
+EC_REG_DATA(RESET_LEDS,		0x40,	100);
+EC_REG_DATA(POWER_LED_ON,	0x42,	100);
+EC_REG_DATA(CHARGE_LED_ON,	0x43,	100);
+EC_REG_DATA(ANDROID_LEDS_OFF,	0x5A,	100);
+
+static void ec_led_set(struct led_classdev *led_cdev,
+		       enum led_brightness value)
+{
+	struct ec_led *led = container_of(led_cdev, struct ec_led, cdev);
+
+	led->new_state = value;
+	schedule_work(&led->work);
+}
+
+#define EC_LED(_color, _addr)						\
+static struct ec_led ec_##_color##_led = {			\
+	.cdev = {							\
+		.name			= "power-button-" #_color,	\
+		.brightness_set		= ec_led_set,			\
+		.max_brightness		= 1,				\
+		.flags			= LED_CORE_SUSPENDRESUME,	\
+	},								\
+};									\
+static void ec_##_color##_led_work(struct work_struct *work)		\
+{									\
+	struct ec_led *led = container_of(work, struct ec_led, work);	\
+									\
+	if (led->new_state)						\
+		ec_write_word_data(_addr, 0);				\
+	else								\
+		ec_write_word_data(RESET_LEDS, 0);			\
+}
+
+EC_LED(white, POWER_LED_ON);
+EC_LED(orange, CHARGE_LED_ON);
+
+static int ec_leds_probe(struct platform_device *pdev)
+{
+	struct device_node *np = pdev->dev.of_node;
+	int ret;
+
+	INIT_WORK(&ec_white_led.work, ec_white_led_work);
+	INIT_WORK(&ec_orange_led.work, ec_orange_led_work);
+
+	ret = led_classdev_register(&pdev->dev, &ec_white_led.cdev);
+	if (ret) {
+		dev_err(&pdev->dev,
+			"%s: Failed to register white led\n", __func__);
+		return ret;
+	}
+
+	ret = led_classdev_register(&pdev->dev, &ec_orange_led.cdev);
+	if (ret) {
+		dev_err(&pdev->dev,
+			"%s: Failed to register orange led\n", __func__);
+		led_classdev_unregister(&ec_white_led.cdev);
+		return ret;
+	}
+
+	if (of_property_read_bool(np, "leds-reset")) {
+		ec_write_word_data(RESET_LEDS, 0);
+		ec_write_word_data(ANDROID_LEDS_OFF, 0);
+	}
+
+	return 0;
+}
+
+static int ec_leds_remove(struct platform_device *pdev)
+{
+	led_classdev_unregister(&ec_white_led.cdev);
+	cancel_work_sync(&ec_white_led.work);
+
+	led_classdev_unregister(&ec_orange_led.cdev);
+	cancel_work_sync(&ec_orange_led.work);
+
+	return 0;
+}
+
+static const struct of_device_id ec_leds_match[] = {
+	{ .compatible = "acer,a50x-leds" },
+	{ }
+};
+MODULE_DEVICE_TABLE(of, ec_leds_match);
+
+static struct platform_driver ec_leds_driver = {
+	.driver = {
+		.name = "a50x-leds",
+		.of_match_table = ec_leds_match,
+		.owner = THIS_MODULE,
+	},
+	.probe = ec_leds_probe,
+	.remove = ec_leds_remove,
+};
+module_platform_driver(ec_leds_driver);
+
+MODULE_DESCRIPTION("Acer A50x EC LEDs driver");
+MODULE_AUTHOR("Dmitry Osipenko <digetx@gmail.com>");
+MODULE_LICENSE("GPL");
diff --git a/drivers/staging/a500/ec/a500_legacy_sysfs.c b/drivers/staging/a500/ec/a500_legacy_sysfs.c
new file mode 100644
index 000000000000..51f710db4feb
--- /dev/null
+++ b/drivers/staging/a500/ec/a500_legacy_sysfs.c
@@ -0,0 +1,493 @@
+/*
+ * Legacy sysfs for Android compatibility
+ *
+ * based on Acer EC battery driver
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ */
+
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+
+#include <linux/module.h>
+
+#include "ec.h"
+
+#define BTMAC_PARTS_NB		3
+#define CABC_MASK		BIT(8)
+#define GYRO_GAIN_PARTS_NB	18
+#define PART_SZ			4
+#define SYSCONF_MASK		0x0000FFFF
+#define WIFIMAC_PARTS_NB	3
+
+#define EC_ATTR(_name, _mode, _show, _store) \
+struct kobj_attribute _name##_attr = __ATTR(_name, _mode, _show, _store)
+
+/*				addr	timeout */
+EC_REG_DATA(RESET_LED,		0x40,	100);
+EC_REG_DATA(LEDS_OFF,		0x41,	100);
+EC_REG_DATA(POWER_LED_ON,	0x42,	100);
+EC_REG_DATA(CHARGE_LED_ON,	0x43,	100);
+EC_REG_DATA(AUDIO_CTRL,		0x44,	0);
+EC_REG_DATA(POWER_CTRL_3G,	0x45,	100);
+EC_REG_DATA(GPS_POWER_OFF,	0x47,	0);
+EC_REG_DATA(GPS_3G_STATUS_RD,	0x48,	0);
+EC_REG_DATA(GPS_3G_STATUS_WR,	0x49,	0);
+EC_REG_DATA(GPS_POWER_ON,	0x4A,	0);
+EC_REG_DATA(MISC_CTRL_RD,	0x4C,	10);
+EC_REG_DATA(MISC_CTRL_WR,	0x4D,	10);
+EC_REG_DATA(ANDROID_LEDS_OFF,	0x5A,	100);
+EC_REG_DATA(BTMAC_RD,		0x62,	10);
+EC_REG_DATA(BTMAC_WR,		0x63,	10);
+EC_REG_DATA(WIFIMAC_RD,		0x64,	10);
+EC_REG_DATA(WIFIMAC_WR,		0x65,	10);
+EC_REG_DATA(LS_GAIN_RD,		0x71,	10);
+EC_REG_DATA(LS_GAIN_WR,		0x72,	10);
+EC_REG_DATA(GYRO_GAIN_RD,	0x73,	10);
+EC_REG_DATA(GYRO_GAIN_WR,	0x74,	10);
+
+static int power_state_3g;
+static int power_state_gps;
+
+static void ec_read_multipart(char *buf, struct ec_reg_data *reg_data,
+			      int parts_nb)
+{
+	char *write_offset;
+	int length, i;
+	s32 ret;
+
+	length = parts_nb * PART_SZ;
+	write_offset = buf + length;
+
+	ec_lock();
+	for (i = 0; i < parts_nb; i++) {
+		ret = ec_read_word_data_locked(reg_data);
+
+		write_offset -= PART_SZ;
+
+		snprintf(write_offset, length + 1, "%04x%s",
+			 ret, (i == 0) ? "" : write_offset + PART_SZ);
+	}
+	ec_unlock();
+}
+
+static void ec_write_multipart(const char *buf, struct ec_reg_data *reg_data,
+			       int parts_nb)
+{
+	char part_buf[PART_SZ + 1];
+	int read_offset, val, i;
+	s32 ret;
+
+	/* don't count trailing "\n" */
+	ret = strlen(buf) - 1;
+
+	if (ret != parts_nb * PART_SZ) {
+		pr_err("%s: length %d is not equal to required %d\n",
+			  __func__, ret, parts_nb * PART_SZ);
+		return;
+	}
+
+	ec_lock();
+	for (i = 0; i < parts_nb; i++) {
+		read_offset = (parts_nb - i + 1) * PART_SZ;
+
+		snprintf(part_buf, ARRAY_SIZE(part_buf),
+			 "%s", buf + read_offset);
+
+		ret = kstrtoint(part_buf, 16, &val);
+		if (ret < 0)
+			pr_err("%s: failed to convert hex str: %s\n",
+				__func__, part_buf);
+
+		ec_write_word_data_locked(reg_data, val);
+	}
+	ec_unlock();
+}
+
+static ssize_t gyro_show(struct kobject *kobj,
+			 struct kobj_attribute *attr,
+			 char *buf)
+{
+	ec_read_multipart(buf, GYRO_GAIN_RD, GYRO_GAIN_PARTS_NB);
+
+	return sprintf(buf, "%s\n", buf);
+}
+
+static ssize_t gyro_store(struct kobject *kobj,
+			  struct kobj_attribute *attr,
+			  const char *buf, size_t n)
+{
+	ec_write_multipart(buf, GYRO_GAIN_WR, GYRO_GAIN_PARTS_NB);
+
+	return n;
+}
+
+static ssize_t pwr_led_on_store(struct kobject *kobj,
+				struct kobj_attribute *attr,
+				const char *buf, size_t n)
+{
+	ec_write_word_data(POWER_LED_ON, 0);
+
+	return n;
+}
+
+static ssize_t chrg_led_on_store(struct kobject *kobj,
+				 struct kobj_attribute *attr,
+				 const char *buf, size_t n)
+{
+	ec_write_word_data(CHARGE_LED_ON, 0);
+
+	return n;
+}
+
+static ssize_t reset_led_store(struct kobject *kobj,
+			       struct kobj_attribute *attr,
+			       const char *buf, size_t n)
+{
+	ec_write_word_data(RESET_LED, 0);
+
+	return n;
+}
+
+static ssize_t leds_off_store(struct kobject *kobj,
+			      struct kobj_attribute *attr,
+			      const char *buf, size_t n)
+{
+	ec_write_word_data(LEDS_OFF, 0);
+
+	return n;
+}
+
+static ssize_t android_off_store(struct kobject *kobj,
+				 struct kobj_attribute *attr,
+				 const char *buf, size_t n)
+{
+	ec_write_word_data(ANDROID_LEDS_OFF, 0);
+
+	return n;
+}
+
+static ssize_t ls_gain_show(struct kobject *kobj,
+			    struct kobj_attribute *attr, char *buf)
+{
+	s32 ret = ec_read_word_data(LS_GAIN_RD);
+
+	return sprintf(buf, "%04x\n", ret);
+}
+
+static ssize_t ls_gain_store(struct kobject *kobj,
+			     struct kobj_attribute *attr,
+			     const char *buf, size_t n)
+{
+	int val;
+	s32 ret;
+
+	ret = kstrtoint(buf, 16, &val);
+	if (ret < 0) {
+		pr_err("%s: failed to convert hex str: %s\n", __func__, buf);
+		return ret;
+	}
+
+	ec_write_word_data(LS_GAIN_WR, val);
+
+	return n;
+}
+
+static ssize_t btmac_show(struct kobject *kobj,
+			  struct kobj_attribute *attr,
+			  char *buf)
+{
+	ec_read_multipart(buf, BTMAC_RD, BTMAC_PARTS_NB);
+
+	return sprintf(buf, "%c%c:%c%c:%c%c:%c%c:%c%c:%c%c\n",
+			buf[0], buf[1], buf[2], buf[3], buf[4],
+			buf[5], buf[6], buf[7], buf[8], buf[9],
+			buf[10], buf[11]);
+}
+
+static ssize_t btmac_store(struct kobject *kobj,
+			   struct kobj_attribute *attr,
+			   const char *buf, size_t n)
+{
+	ec_write_multipart(buf, BTMAC_WR, BTMAC_PARTS_NB);
+
+	return n;
+}
+
+static ssize_t wifimac_show(struct kobject *kobj,
+			    struct kobj_attribute *attr, char *buf)
+{
+	ec_read_multipart(buf, WIFIMAC_RD, WIFIMAC_PARTS_NB);
+
+	return sprintf(buf, "%s\n", buf);
+}
+
+static ssize_t wifimac_store(struct kobject *kobj,
+			     struct kobj_attribute *attr,
+			     const char *buf, size_t n)
+{
+	ec_write_multipart(buf, WIFIMAC_WR, WIFIMAC_PARTS_NB);
+
+	return n;
+}
+
+static ssize_t device_status_show(struct kobject *kobj,
+				  struct kobj_attribute *attr,  char *buf)
+{
+	s32 ret;
+	int i;
+
+	ret = ec_read_word_data(GPS_3G_STATUS_RD);
+
+	for (i = 15; i >= 0; i--)
+		buf[i] = ret >> (15 - i) & 0x1 ? '1' : '0';
+
+	return sprintf(buf, "%s\n", buf);
+}
+
+static ssize_t device_status_store(struct kobject *kobj,
+				   struct kobj_attribute *attr,
+				   const char *buf, size_t n)
+{
+	s32 ret;
+	int val;
+
+	ret = kstrtoint(buf, 10, &val);
+	if (ret < 0) {
+		pr_err("%s: failed to convert str: %s\n", __func__, buf);
+		return ret;
+	}
+
+	ec_write_word_data(GPS_3G_STATUS_WR, val);
+
+	return n;
+}
+
+static ssize_t status_3g_show(struct kobject *kobj,
+			      struct kobj_attribute *attr, char *buf)
+{
+	return sprintf(buf, "%d\n", power_state_3g);
+}
+
+static ssize_t status_3g_store(struct kobject *kobj,
+			       struct kobj_attribute *attr,
+			       const char *buf, size_t n)
+{
+	s32 ret;
+	int val;
+
+	ret = kstrtoint(buf, 10, &val);
+	if (ret < 0) {
+		pr_err("%s: failed to convert str: %s\n", __func__, buf);
+		return ret;
+	}
+
+	power_state_3g = val;
+	ec_write_word_data(POWER_CTRL_3G, val);
+
+	return n;
+}
+
+static ssize_t status_gps_show(struct kobject *kobj,
+			       struct kobj_attribute *attr, char *buf)
+{
+	return sprintf(buf, "%d\n", power_state_gps);
+}
+
+static ssize_t status_gps_store(struct kobject *kobj,
+				struct kobj_attribute *attr,
+				const char *buf, size_t n)
+{
+	s32 ret;
+	int val;
+
+	ret = kstrtoint(buf, 10, &val);
+	if (ret < 0) {
+		pr_err("%s: failed to convert str: %s\n", __func__, buf);
+		return ret;
+	}
+
+	power_state_gps = !!val;
+
+	if (power_state_gps)
+		ec_write_word_data(GPS_POWER_ON, 0);
+	else
+		ec_write_word_data(GPS_POWER_OFF, 0);
+
+	return n;
+}
+
+static ssize_t cabc_show(struct kobject *kobj,
+			 struct kobj_attribute *attr, char *buf)
+{
+	bool enabled = ec_read_word_data(MISC_CTRL_RD) & CABC_MASK;
+
+	return sprintf(buf, "%d\n", enabled);
+}
+
+static ssize_t cabc_store(struct kobject *kobj,
+			  struct kobj_attribute *attr,
+			  const char *buf, size_t n)
+{
+	s32 ret;
+	int val;
+
+	ret = kstrtoint(buf, 10, &val);
+	if (ret < 0) {
+		pr_err("%s: failed to convert str: %s\n", __func__, buf);
+		return ret;
+	}
+
+	ret = ec_read_word_data(MISC_CTRL_RD);
+	if (ret < 0)
+		return ret;
+
+	if (val)
+		ret |= CABC_MASK;
+	else
+		ret &= (~CABC_MASK);
+
+	ec_write_word_data(MISC_CTRL_WR, ret);
+
+	return n;
+}
+
+static ssize_t sysconf_show(struct kobject *kobj,
+			    struct kobj_attribute *attr, char *buf)
+{
+	s32 ret = ec_read_word_data(MISC_CTRL_RD) & SYSCONF_MASK;
+
+	return sprintf(buf, "%d\n", ret);
+}
+
+static ssize_t sysconf_store(struct kobject *kobj,
+			     struct kobj_attribute *attr,
+			     const char *buf, size_t n)
+{
+	s32 ret;
+	int val;
+
+	ret = kstrtoint(buf, 10, &val);
+	if (ret < 0) {
+		pr_err("%s: failed to convert str: %s\n", __func__, buf);
+		return ret;
+	}
+	val &= SYSCONF_MASK;
+
+	ec_write_word_data(MISC_CTRL_WR, val);
+
+	return n;
+}
+
+static ssize_t audioconf_show(struct kobject *kobj,
+			      struct kobj_attribute *attr, char *buf)
+{
+	s32 ret = ec_read_word_data(AUDIO_CTRL) & SYSCONF_MASK;
+
+	return sprintf(buf, "%d\n", ret);
+}
+
+static ssize_t audioconf_store(struct kobject *kobj,
+			       struct kobj_attribute *attr,
+			       const char *buf, size_t n)
+{
+	s32 ret;
+	int val;
+
+	ret = kstrtoint(buf, 10, &val);
+	if (ret < 0) {
+		pr_err("%s: failed to convert str: %s\n", __func__, buf);
+		return ret;
+	}
+	val &= SYSCONF_MASK;
+
+	ec_write_word_data(AUDIO_CTRL, val);
+
+	return n;
+}
+
+static EC_ATTR(GyroGain,
+	       S_IWUSR|S_IRUGO, gyro_show, gyro_store);
+static EC_ATTR(PowerLED,
+	       S_IRUGO, NULL, pwr_led_on_store);
+static EC_ATTR(ChargeLED,
+	       S_IRUGO, NULL, chrg_led_on_store);
+static EC_ATTR(OriSts,
+	       S_IRUGO, NULL, reset_led_store);
+static EC_ATTR(OffLED,
+	       S_IRUGO, NULL, leds_off_store);
+static EC_ATTR(LEDAndroidOff,
+	       S_IRUGO, NULL, android_off_store);
+static EC_ATTR(AutoLSGain,
+	       S_IWUSR|S_IRUGO, ls_gain_show, ls_gain_store);
+static EC_ATTR(BTMAC,
+	       S_IWUSR|S_IRUGO, btmac_show, btmac_store);
+static EC_ATTR(WIFIMAC,
+	       S_IWUSR|S_IRUGO, wifimac_show, wifimac_store);
+static EC_ATTR(DeviceStatus,
+	       S_IWUSR|S_IRUGO, device_status_show, device_status_store);
+static EC_ATTR(ThreeGPower,
+	       S_IWUSR|S_IRUGO, status_3g_show, status_3g_store);
+static EC_ATTR(GPSPower,
+	       S_IWUSR|S_IRUGO, status_gps_show, status_gps_store);
+static EC_ATTR(Cabc,
+	       S_IWUSR|S_IRUGO, cabc_show, cabc_store);
+static EC_ATTR(SystemConfig,
+	       S_IWUSR|S_IRUGO, sysconf_show, sysconf_store);
+static EC_ATTR(MicSwitch,
+	       S_IWUSR|S_IRUGO, audioconf_show, audioconf_store);
+
+static struct attribute *ec_attrs[] = {
+	&GyroGain_attr.attr,
+	&PowerLED_attr.attr,
+	&ChargeLED_attr.attr,
+	&OriSts_attr.attr,
+	&OffLED_attr.attr,
+	&LEDAndroidOff_attr.attr,
+	&AutoLSGain_attr.attr,
+	&BTMAC_attr.attr,
+	&WIFIMAC_attr.attr,
+	&DeviceStatus_attr.attr,
+	&ThreeGPower_attr.attr,
+	&GPSPower_attr.attr,
+	&Cabc_attr.attr,
+	&SystemConfig_attr.attr,
+	&MicSwitch_attr.attr,
+	NULL,
+};
+
+static struct attribute_group ec_attr_group = {
+	.attrs = ec_attrs,
+};
+
+static struct kobject *ec_legacy_kobj;
+
+int ec_create_legacy_sysfs(void)
+{
+	int ret;
+
+	ec_legacy_kobj = kobject_create_and_add("EcControl", NULL);
+	if (!ec_legacy_kobj)
+		return -ENOMEM;
+
+	ret = sysfs_create_group(ec_legacy_kobj, &ec_attr_group);
+	if (ret)
+		kobject_put(ec_legacy_kobj);
+
+	return ret;
+}
+
+void ec_release_legacy_sysfs(void)
+{
+	sysfs_remove_group(ec_legacy_kobj, &ec_attr_group);
+	kobject_put(ec_legacy_kobj);
+}
+
+module_init(ec_create_legacy_sysfs);
+module_exit(ec_release_legacy_sysfs);
+
+MODULE_DESCRIPTION("Acer A50x EC legacy sysfs module");
+MODULE_AUTHOR("Dmitry Osipenko <digetx@gmail.com>");
+MODULE_LICENSE("GPL");
diff --git a/drivers/staging/a500/ec/ec.h b/drivers/staging/a500/ec/ec.h
new file mode 100644
index 000000000000..ee7617dc6a89
--- /dev/null
+++ b/drivers/staging/a500/ec/ec.h
@@ -0,0 +1,32 @@
+/*
+ * MFD driver for Acer A50x embedded controller
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ */
+
+#ifndef __MFD_A500_EC_H
+#define __MFD_A500_EC_H
+
+struct ec_reg_data {
+	u8  addr;
+	u16 timeout;
+};
+
+#define EC_REG_DATA(_name, _addr, _timeout)		\
+static struct ec_reg_data ec_##_name##_ = {		\
+	.addr = _addr,					\
+	.timeout = _timeout,				\
+};							\
+static struct ec_reg_data *_name = &ec_##_name##_;
+
+extern int ec_read_word_data_locked(struct ec_reg_data *reg_data);
+extern int ec_read_word_data(struct ec_reg_data *reg_data);
+extern int ec_write_word_data_locked(struct ec_reg_data *reg_data, u16 value);
+extern int ec_write_word_data(struct ec_reg_data *reg_data, u16 value);
+extern void ec_lock(void);
+extern void ec_unlock(void);
+
+#endif	/* __MFD_A500_EC_H */
