power: isl9519q: Add pm8921 external-charger support

The isl9519q interface with the pm8921 via external-charger registration.
The pm8921 notifies the isl9519q to start/stop charging.
This way the isl9519q driver doesn't need to:
1. Monitor the charge current for end-of-charging detection.
2. Get interrupt from isl9519q HW.
3. Check n_valid_gpio for DC Inserted validity.

Change-Id: Ia0472440010fe1582a5aa8b557c9e4ab17e1a657
Signed-off-by: Amir Samuelov <amirs@codeaurora.org>
diff --git a/drivers/power/Kconfig b/drivers/power/Kconfig
index 179a4ac..cca1035 100644
--- a/drivers/power/Kconfig
+++ b/drivers/power/Kconfig
@@ -261,7 +261,8 @@
 
 config ISL9519_CHARGER
 	tristate "isl9519 charger"
-	depends on BATTERY_MSM8X60
+	depends on (BATTERY_MSM8X60 || PM8921_CHARGER)
+	depends on I2C
 	default n
 	help
 	  The isl9519q charger chip from intersil is connected to an external
diff --git a/drivers/power/isl9519q.c b/drivers/power/isl9519q.c
index a45d286..733de45 100644
--- a/drivers/power/isl9519q.c
+++ b/drivers/power/isl9519q.c
@@ -20,9 +20,11 @@
 #include <linux/workqueue.h>
 #include <linux/interrupt.h>
 #include <linux/msm-charger.h>
+#include <linux/mfd/pm8xxx/pm8921-charger.h>
 #include <linux/slab.h>
 #include <linux/i2c/isl9519.h>
 #include <linux/msm_adc.h>
+#include <linux/spinlock.h>
 
 #define CHG_CURRENT_REG		0x14
 #define MAX_SYS_VOLTAGE_REG	0x15
@@ -34,7 +36,7 @@
 
 #define TRCKL_CHG_STATUS_BIT	0x80
 
-#define ISL9519_CHG_PERIOD	((HZ) * 150)
+#define ISL9519_CHG_PERIOD_SEC	150
 
 struct isl9519q_struct {
 	struct i2c_client		*client;
@@ -52,6 +54,10 @@
 	struct msm_hardware_charger	adapter_hw_chg;
 	int				suspended;
 	int				charge_at_resume;
+	struct ext_chg_pm8921		ext_chg;
+	spinlock_t			lock;
+	bool				notify_by_pmic;
+	bool				trickle;
 };
 
 static int isl9519q_read_reg(struct i2c_client *client, int reg,
@@ -70,6 +76,8 @@
 	} else
 		*val = ret;
 
+	pr_debug("%s.reg=0x%x.val=0x%x.\n", __func__, reg, *val);
+
 	return 0;
 }
 
@@ -79,6 +87,8 @@
 	int ret;
 	struct isl9519q_struct *isl_chg;
 
+	pr_debug("%s.reg=0x%x.val=0x%x.\n", __func__, reg, val);
+
 	isl_chg = i2c_get_clientdata(client);
 	ret = i2c_smbus_write_word_data(isl_chg->client, reg, val);
 
@@ -91,6 +101,14 @@
 	return 0;
 }
 
+/**
+ * Read charge-current via ADC.
+ *
+ * The ISL CCMON (charge-current-monitor) pin is connected to
+ * the PMIC MPP#X pin.
+ * This not required when notify_by_pmic is used where the PMIC
+ * uses BMS to notify the ISL on charging-done / charge-resume.
+ */
 static int isl_read_adc(int channel, int *mv_reading)
 {
 	int ret;
@@ -136,98 +154,129 @@
 out:
 	*mv_reading = 0;
 	pr_debug("%s: done with error for %d\n", __func__, channel);
-	return -EINVAL;
 
+	return -EINVAL;
 }
 
-static void isl9519q_charge(struct work_struct *isl9519_work)
+static bool is_trickle_charging(struct isl9519q_struct *isl_chg)
 {
-	u16 temp;
+	u16 ctrl = 0;
 	int ret;
+
+	ret = isl9519q_read_reg(isl_chg->client, CONTROL_REG, &ctrl);
+
+	if (!ret) {
+		pr_debug("%s.control_reg=0x%x.\n", __func__, ctrl);
+	} else {
+		dev_err(&isl_chg->client->dev,
+			"%s couldnt read cntrl reg\n", __func__);
+	}
+
+	if (ctrl & TRCKL_CHG_STATUS_BIT)
+		return true;
+
+	return false;
+}
+
+static void isl_adapter_check_ichg(struct isl9519q_struct *isl_chg)
+{
+	int ichg; /* isl charger current */
+	int mv_reading = 0;
+
+	ichg = isl_read_adc(CHANNEL_ADC_BATT_AMON, &mv_reading);
+
+	dev_dbg(&isl_chg->client->dev, "%s mv_reading=%d\n",
+		__func__, mv_reading);
+	dev_dbg(&isl_chg->client->dev, "%s isl_charger_current=%d\n",
+		__func__, ichg);
+
+	if (ichg >= 0 && ichg <= isl_chg->term_current)
+		msm_charger_notify_event(&isl_chg->adapter_hw_chg,
+					 CHG_DONE_EVENT);
+
+	isl_chg->trickle = is_trickle_charging(isl_chg);
+	if (isl_chg->trickle)
+		msm_charger_notify_event(&isl_chg->adapter_hw_chg,
+					 CHG_BATT_BEGIN_FAST_CHARGING);
+}
+
+/**
+ * isl9519q_worker
+ *
+ * Periodic task required to kick the ISL HW watchdog to keep
+ * charging.
+ *
+ * @isl9519_work: work context.
+ */
+static void isl9519q_worker(struct work_struct *isl9519_work)
+{
 	struct isl9519q_struct *isl_chg;
-	int isl_charger_current;
-	int mv_reading;
 
 	isl_chg = container_of(isl9519_work, struct isl9519q_struct,
 			charge_work.work);
 
 	dev_dbg(&isl_chg->client->dev, "%s\n", __func__);
 
-	if (isl_chg->charging) {
-		isl_charger_current = isl_read_adc(CHANNEL_ADC_BATT_AMON,
-								&mv_reading);
-		dev_dbg(&isl_chg->client->dev, "%s mv_reading=%d\n",
-				__func__, mv_reading);
-		dev_dbg(&isl_chg->client->dev, "%s isl_charger_current=%d\n",
-				__func__, isl_charger_current);
-		if (isl_charger_current >= 0
-			&& isl_charger_current <= isl_chg->term_current) {
-			msm_charger_notify_event(
-					&isl_chg->adapter_hw_chg,
-					CHG_DONE_EVENT);
-		}
-		isl9519q_write_reg(isl_chg->client, CHG_CURRENT_REG,
-				isl_chg->chgcurrent);
-		ret = isl9519q_read_reg(isl_chg->client, CONTROL_REG, &temp);
-		if (!ret) {
-			if (!(temp & TRCKL_CHG_STATUS_BIT))
-				msm_charger_notify_event(
-						&isl_chg->adapter_hw_chg,
-						CHG_BATT_BEGIN_FAST_CHARGING);
-		} else {
-			dev_err(&isl_chg->client->dev,
-				"%s couldnt read cntrl reg\n", __func__);
-		}
-		schedule_delayed_work(&isl_chg->charge_work,
-						ISL9519_CHG_PERIOD);
+	if (!isl_chg->charging) {
+		pr_info("%s.stop charging.\n", __func__);
+		isl9519q_write_reg(isl_chg->client, CHG_CURRENT_REG, 0);
+		return; /* Stop periodic worker */
 	}
+
+	/* Kick the dog by writting to CHG_CURRENT_REG */
+	isl9519q_write_reg(isl_chg->client, CHG_CURRENT_REG,
+			   isl_chg->chgcurrent);
+
+	if (isl_chg->notify_by_pmic)
+		isl_chg->trickle = is_trickle_charging(isl_chg);
+	else
+		isl_adapter_check_ichg(isl_chg);
+
+	schedule_delayed_work(&isl_chg->charge_work,
+			      (ISL9519_CHG_PERIOD_SEC * HZ));
 }
 
-static int isl9519q_start_charging(struct msm_hardware_charger *hw_chg,
-		int chg_voltage, int chg_current)
+static int isl9519q_start_charging(struct isl9519q_struct *isl_chg,
+				   int chg_voltage, int chg_current)
 {
-	struct isl9519q_struct *isl_chg;
 	int ret = 0;
 
-	isl_chg = container_of(hw_chg, struct isl9519q_struct, adapter_hw_chg);
-	if (isl_chg->charging)
-		/* we are already charging */
+	pr_info("%s.\n", __func__);
+
+	if (isl_chg->charging) {
+		pr_warn("%s.already charging.\n", __func__);
 		return 0;
+	}
 
 	if (isl_chg->suspended) {
+		pr_warn("%s.suspended - can't start charging.\n", __func__);
 		isl_chg->charge_at_resume = 1;
 		return 0;
 	}
 
-	dev_dbg(&isl_chg->client->dev, "%s\n", __func__);
+	dev_dbg(&isl_chg->client->dev,
+		"%s starting timed work.period=%d seconds.\n",
+		__func__, (int) ISL9519_CHG_PERIOD_SEC);
 
-	ret = isl9519q_write_reg(isl_chg->client, CHG_CURRENT_REG,
-						isl_chg->chgcurrent);
-	if (ret) {
-		dev_err(&isl_chg->client->dev,
-			"%s coulnt write to current_reg\n", __func__);
-		goto out;
-	}
+	/*
+	 * The ISL will start charging from the worker context.
+	 * This API might be called from interrupt context.
+	 */
+	schedule_delayed_work(&isl_chg->charge_work, 1);
 
-	dev_dbg(&isl_chg->client->dev, "%s starting timed work\n",
-							__func__);
-	schedule_delayed_work(&isl_chg->charge_work,
-						ISL9519_CHG_PERIOD);
 	isl_chg->charging = true;
 
-out:
 	return ret;
 }
 
-static int isl9519q_stop_charging(struct msm_hardware_charger *hw_chg)
+static int isl9519q_stop_charging(struct isl9519q_struct *isl_chg)
 {
-	struct isl9519q_struct *isl_chg;
-	int ret = 0;
+	pr_info("%s.\n", __func__);
 
-	isl_chg = container_of(hw_chg, struct isl9519q_struct, adapter_hw_chg);
-	if (!(isl_chg->charging))
-		/* we arent charging */
+	if (!(isl_chg->charging)) {
+		pr_warn("%s.already not charging.\n", __func__);
 		return 0;
+	}
 
 	if (isl_chg->suspended) {
 		isl_chg->charge_at_resume = 0;
@@ -236,17 +285,71 @@
 
 	dev_dbg(&isl_chg->client->dev, "%s\n", __func__);
 
-	ret = isl9519q_write_reg(isl_chg->client, CHG_CURRENT_REG, 0);
-	if (ret) {
-		dev_err(&isl_chg->client->dev,
-			"%s coulnt write to current_reg\n", __func__);
-		goto out;
-	}
-
 	isl_chg->charging = false;
-	cancel_delayed_work(&isl_chg->charge_work);
-out:
-	return ret;
+	isl_chg->trickle = false;
+	/*
+	 * The ISL will stop charging from the worker context.
+	 * This API might be called from interrupt context.
+	 */
+	schedule_delayed_work(&isl_chg->charge_work, 1);
+
+	return 0;
+}
+
+static int isl_ext_start_charging(void *ctx)
+{
+	int rc;
+	struct isl9519q_struct *isl_chg = ctx;
+	unsigned long flags;
+
+	spin_lock_irqsave(&isl_chg->lock, flags);
+	rc = isl9519q_start_charging(isl_chg, 0, isl_chg->chgcurrent);
+	spin_unlock_irqrestore(&isl_chg->lock, flags);
+
+	return rc;
+}
+
+static int isl_ext_stop_charging(void *ctx)
+{
+	int rc;
+	struct isl9519q_struct *isl_chg = ctx;
+	unsigned long flags;
+
+	spin_lock_irqsave(&isl_chg->lock, flags);
+	rc = isl9519q_stop_charging(isl_chg);
+	spin_unlock_irqrestore(&isl_chg->lock, flags);
+
+	return rc;
+}
+
+static bool isl_ext_is_trickle(void *ctx)
+{
+	struct isl9519q_struct *isl_chg = ctx;
+
+	return isl_chg->trickle;
+}
+
+static int isl_adapter_start_charging(struct msm_hardware_charger *hw_chg,
+				      int chg_voltage, int chg_current)
+{
+	int rc;
+	struct isl9519q_struct *isl_chg;
+
+	isl_chg = container_of(hw_chg, struct isl9519q_struct, adapter_hw_chg);
+	rc = isl9519q_start_charging(isl_chg, chg_voltage, chg_current);
+
+	return rc;
+}
+
+static int isl_adapter_stop_charging(struct msm_hardware_charger *hw_chg)
+{
+	int rc;
+	struct isl9519q_struct *isl_chg;
+
+	isl_chg = container_of(hw_chg, struct isl9519q_struct, adapter_hw_chg);
+	rc = isl9519q_stop_charging(isl_chg);
+
+	return rc;
 }
 
 static int isl9519q_charging_switched(struct msm_hardware_charger *hw_chg)
@@ -296,6 +399,93 @@
 #define DEFAULT_MAX_VOLTAGE_REG_VALUE	0x1070
 #define DEFAULT_MIN_VOLTAGE_REG_VALUE	0x0D00
 
+static int __devinit isl9519q_init_adapter(struct isl9519q_struct *isl_chg)
+{
+	int ret;
+	struct isl_platform_data *pdata = isl_chg->client->dev.platform_data;
+	struct i2c_client *client = isl_chg->client;
+
+	isl_chg->adapter_hw_chg.type = CHG_TYPE_AC;
+	isl_chg->adapter_hw_chg.rating = 2;
+	isl_chg->adapter_hw_chg.name = "isl-adapter";
+	isl_chg->adapter_hw_chg.start_charging = isl_adapter_start_charging;
+	isl_chg->adapter_hw_chg.stop_charging = isl_adapter_stop_charging;
+	isl_chg->adapter_hw_chg.charging_switched = isl9519q_charging_switched;
+
+	ret = gpio_request(pdata->valid_n_gpio, "isl_charger_valid");
+	if (ret) {
+		dev_err(&client->dev, "%s gpio_request failed "
+				      "for %d ret=%d\n",
+			__func__, pdata->valid_n_gpio, ret);
+		goto out;
+	}
+
+	ret = msm_charger_register(&isl_chg->adapter_hw_chg);
+	if (ret) {
+		dev_err(&client->dev,
+			"%s msm_charger_register failed for ret =%d\n",
+			__func__, ret);
+		goto free_gpio;
+	}
+
+	ret = request_threaded_irq(client->irq, NULL,
+				   isl_valid_handler,
+				   IRQF_TRIGGER_FALLING |
+				   IRQF_TRIGGER_RISING,
+				   "isl_charger_valid", client);
+	if (ret) {
+		dev_err(&client->dev,
+			"%s request_threaded_irq failed "
+			"for %d ret =%d\n",
+			__func__, client->irq, ret);
+		goto unregister;
+	}
+	irq_set_irq_wake(client->irq, 1);
+
+	ret = gpio_get_value_cansleep(isl_chg->valid_n_gpio);
+	if (ret < 0) {
+		dev_err(&client->dev,
+			"%s gpio_get_value failed for %d ret=%d\n",
+			__func__, pdata->valid_n_gpio, ret);
+		/* assume absent */
+		ret = 1;
+	}
+	if (!ret) {
+		msm_charger_notify_event(&isl_chg->adapter_hw_chg,
+				CHG_INSERTED_EVENT);
+		isl_chg->present = 1;
+	}
+
+	return 0;
+
+unregister:
+	msm_charger_unregister(&isl_chg->adapter_hw_chg);
+free_gpio:
+	gpio_free(pdata->valid_n_gpio);
+out:
+	return ret;
+
+}
+
+static int __devinit isl9519q_init_ext_chg(struct isl9519q_struct *isl_chg)
+{
+	int ret;
+
+	isl_chg->ext_chg.name = "isl9519q";
+	isl_chg->ext_chg.ctx = isl_chg;
+	isl_chg->ext_chg.start_charging = isl_ext_start_charging;
+	isl_chg->ext_chg.stop_charging = isl_ext_stop_charging;
+	isl_chg->ext_chg.is_trickle = isl_ext_is_trickle;
+	ret = register_external_dc_charger(&isl_chg->ext_chg);
+	if (ret) {
+		pr_err("%s.failed to register external dc charger.ret=%d.\n",
+		       __func__, ret);
+		return ret;
+	}
+
+	return 0;
+}
+
 static int __devinit isl9519q_probe(struct i2c_client *client,
 				    const struct i2c_device_id *id)
 {
@@ -306,6 +496,8 @@
 	ret = 0;
 	pdata = client->dev.platform_data;
 
+	pr_info("%s.\n", __func__);
+
 	if (pdata == NULL) {
 		dev_err(&client->dev, "%s no platform data\n", __func__);
 		ret = -EINVAL;
@@ -324,7 +516,9 @@
 		goto out;
 	}
 
-	INIT_DELAYED_WORK(&isl_chg->charge_work, isl9519q_charge);
+	spin_lock_init(&isl_chg->lock);
+
+	INIT_DELAYED_WORK(&isl_chg->charge_work, isl9519q_worker);
 	isl_chg->client = client;
 	isl_chg->chgcurrent = pdata->chgcurrent;
 	isl_chg->term_current = pdata->term_current;
@@ -337,12 +531,14 @@
 	isl_chg->chgcurrent &= ~0x7F;
 	isl_chg->input_current &= ~0x7F;
 
-	isl_chg->adapter_hw_chg.type = CHG_TYPE_AC;
-	isl_chg->adapter_hw_chg.rating = 2;
-	isl_chg->adapter_hw_chg.name = "isl-adapter";
-	isl_chg->adapter_hw_chg.start_charging = isl9519q_start_charging;
-	isl_chg->adapter_hw_chg.stop_charging = isl9519q_stop_charging;
-	isl_chg->adapter_hw_chg.charging_switched = isl9519q_charging_switched;
+	/**
+	 * ISL is Notified by PMIC to start/stop charging, rather than
+	 * handling interrupt from ISL for End-Of-Chargring, and
+	 * monitoring the charge-current periodically. The valid_n_gpio
+	 * is also not used, dc-present is detected by PMIC.
+	 */
+	isl_chg->notify_by_pmic = (client->irq == 0);
+	i2c_set_clientdata(client, isl_chg);
 
 	if (pdata->chg_detection_config) {
 		ret = pdata->chg_detection_config();
@@ -353,35 +549,6 @@
 		}
 	}
 
-	ret = gpio_request(pdata->valid_n_gpio, "isl_charger_valid");
-	if (ret) {
-		dev_err(&client->dev, "%s gpio_request failed for %d ret=%d\n",
-			__func__, pdata->valid_n_gpio, ret);
-		goto free_isl_chg;
-	}
-
-	i2c_set_clientdata(client, isl_chg);
-
-	ret = msm_charger_register(&isl_chg->adapter_hw_chg);
-	if (ret) {
-		dev_err(&client->dev,
-			"%s msm_charger_register failed for ret =%d\n",
-			__func__, ret);
-		goto free_gpio;
-	}
-
-	ret = request_threaded_irq(client->irq, NULL,
-				   isl_valid_handler,
-				   IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,
-				   "isl_charger_valid", client);
-	if (ret) {
-		dev_err(&client->dev,
-			"%s request_threaded_irq failed for %d ret =%d\n",
-			__func__, client->irq, ret);
-		goto unregister;
-	}
-	irq_set_irq_wake(client->irq, 1);
-
 	isl_chg->max_system_voltage &= MAX_VOLTAGE_REG_MASK;
 	isl_chg->min_system_voltage &= MIN_VOLTAGE_REG_MASK;
 	if (isl_chg->max_system_voltage == 0)
@@ -391,57 +558,34 @@
 
 	ret = isl9519q_write_reg(isl_chg->client, MAX_SYS_VOLTAGE_REG,
 			isl_chg->max_system_voltage);
-	if (ret) {
-		dev_err(&client->dev,
-			"%s couldnt write to MAX_SYS_VOLTAGE_REG ret=%d\n",
-			__func__, ret);
-		goto free_irq;
-	}
+	if (ret)
+		goto free_isl_chg;
 
 	ret = isl9519q_write_reg(isl_chg->client, MIN_SYS_VOLTAGE_REG,
 			isl_chg->min_system_voltage);
-	if (ret) {
-		dev_err(&client->dev,
-			"%s couldnt write to MIN_SYS_VOLTAGE_REG ret=%d\n",
-			__func__, ret);
-		goto free_irq;
-	}
+	if (ret)
+		goto free_isl_chg;
 
 	if (isl_chg->input_current) {
 		ret = isl9519q_write_reg(isl_chg->client,
 				INPUT_CURRENT_REG,
 				isl_chg->input_current);
-		if (ret) {
-			dev_err(&client->dev,
-				"%s couldnt write INPUT_CURRENT_REG ret=%d\n",
-				__func__, ret);
-			goto free_irq;
-		}
+		if (ret)
+			goto free_isl_chg;
 	}
 
-	ret = gpio_get_value_cansleep(isl_chg->valid_n_gpio);
-	if (ret < 0) {
-		dev_err(&client->dev,
-			"%s gpio_get_value failed for %d ret=%d\n", __func__,
-			pdata->valid_n_gpio, ret);
-		/* assume absent */
-		ret = 1;
-	}
-	if (!ret) {
-		msm_charger_notify_event(&isl_chg->adapter_hw_chg,
-				CHG_INSERTED_EVENT);
-		isl_chg->present = 1;
-	}
+	if (isl_chg->notify_by_pmic)
+		ret = isl9519q_init_ext_chg(isl_chg);
+	else
+		ret = isl9519q_init_adapter(isl_chg);
 
-	pr_debug("%s OK chg_present=%d\n", __func__, isl_chg->present);
+	if (ret)
+		goto free_isl_chg;
+
+	pr_info("%s OK.\n", __func__);
+
 	return 0;
 
-free_irq:
-	free_irq(client->irq, NULL);
-unregister:
-	msm_charger_unregister(&isl_chg->adapter_hw_chg);
-free_gpio:
-	gpio_free(pdata->valid_n_gpio);
 free_isl_chg:
 	kfree(isl_chg);
 out:
@@ -493,7 +637,7 @@
 	isl_chg->suspended  = 0;
 	if (isl_chg->charge_at_resume) {
 		isl_chg->charge_at_resume = 0;
-		isl9519q_start_charging(&isl_chg->adapter_hw_chg, 0, 0);
+		isl9519q_start_charging(isl_chg, 0, 0);
 	}
 	return 0;
 }
@@ -519,10 +663,12 @@
 
 static int __init isl9519q_init(void)
 {
+	pr_info("%s. isl9519q SW rev 1.01\n", __func__);
+
 	return i2c_add_driver(&isl9519q_driver);
 }
 
-module_init(isl9519q_init);
+late_initcall_sync(isl9519q_init);
 
 static void __exit isl9519q_exit(void)
 {