power: pm8921-charger: enable external charger

The pmic 8921 charger has a limit of 2Amp of charge current. This limit
makes it inadequate to charge multi-cell batteries.

The pm8921 charger supports a configuration where an external charger
with more current capacity may be used to charge the battery while the
pm8921 itself can charge from a USB source. In such a configuration
the external charger source is connected to the DCIN pins, this makes the
pmic detect insertion and removal of the external charger source. In turn
this enables automatic switching on or off the usb charging path when an
external charger is plugged out or in respectively. The pm8921 charger
driver has to call start and stop on external charger. The internal
FSM and logic correctly indicates that it is charging from a DC source.

The resume charging of a fully charged battery when an external source
is present needs to start charging from an external source. The driver
relies on VBATDET comparator to signal a low.

Change-Id: Iab5439ac2408ab061bce477f4d41d7b6a8de9b38
Signed-off-by: Amir Samuelov <amirs@codeaurora.org>
diff --git a/drivers/power/pm8921-charger.c b/drivers/power/pm8921-charger.c
index 3e61ad2..82582c4 100644
--- a/drivers/power/pm8921-charger.c
+++ b/drivers/power/pm8921-charger.c
@@ -202,6 +202,9 @@
 	struct power_supply		batt_psy;
 	struct dentry			*dent;
 	struct bms_notify		bms_notify;
+	struct ext_chg_pm8921		*ext;
+	bool				ext_charging;
+	bool				ext_charge_done;
 	DECLARE_BITMAP(enabled_irqs, PM_CHG_MAX_INTS);
 	struct work_struct		battery_id_valid_work;
 	int64_t				batt_id_min;
@@ -757,8 +760,33 @@
 	return pm_chg_get_rt_status(chip, DCIN_VALID_IRQ);
 }
 
+static bool is_ext_charging(struct pm8921_chg_chip *chip)
+{
+	if (chip->ext == NULL)
+		return false;
+
+	if (chip->ext_charging)
+		return true;
+
+	return false;
+}
+
+static bool is_ext_trickle_charging(struct pm8921_chg_chip *chip)
+{
+	if (chip->ext == NULL)
+		return false;
+
+	if (chip->ext->is_trickle(chip->ext->ctx))
+		return true;
+
+	return false;
+}
+
 static int is_battery_charging(int fsm_state)
 {
+	if (is_ext_charging(the_chip))
+		return 1;
+
 	switch (fsm_state) {
 	case FSM_STATE_ATC_2A:
 	case FSM_STATE_ATC_2B:
@@ -918,6 +946,15 @@
 {
 	int temp;
 
+	if (!get_prop_batt_present(chip))
+		return POWER_SUPPLY_CHARGE_TYPE_NONE;
+
+	if (is_ext_trickle_charging(chip))
+		return POWER_SUPPLY_CHARGE_TYPE_TRICKLE;
+
+	if (is_ext_charging(chip))
+		return POWER_SUPPLY_CHARGE_TYPE_FAST;
+
 	temp = pm_chg_get_rt_status(chip, TRKLCHG_IRQ);
 	if (temp)
 		return POWER_SUPPLY_CHARGE_TYPE_TRICKLE;
@@ -933,6 +970,17 @@
 {
 	int temp = 0;
 
+	if (!get_prop_batt_present(chip))
+		return POWER_SUPPLY_STATUS_UNKNOWN;
+
+	if (chip->ext) {
+		if (chip->ext_charge_done)
+			return POWER_SUPPLY_STATUS_FULL;
+
+		if (chip->ext_charging)
+			return POWER_SUPPLY_STATUS_CHARGING;
+	}
+
 	/* TODO reading the FSM state is more reliable */
 	temp = pm_chg_get_rt_status(chip, TRKLCHG_IRQ);
 
@@ -1259,6 +1307,77 @@
 	bms_notify_check(chip);
 }
 
+static void handle_stop_ext_chg(struct pm8921_chg_chip *chip)
+{
+	if (chip->ext == NULL) {
+		pr_debug("external charger not registered.\n");
+		return;
+	}
+
+	if (!chip->ext_charging) {
+		pr_debug("already not charging.\n");
+		return;
+	}
+
+	chip->ext->stop_charging(chip->ext->ctx);
+	chip->ext_charging = false;
+}
+
+static void handle_start_ext_chg(struct pm8921_chg_chip *chip)
+{
+	int dc_present;
+	int batt_present;
+	int batt_temp_ok;
+	int vbat_ov;
+	int batfet;
+	unsigned long delay =
+		round_jiffies_relative(msecs_to_jiffies(EOC_CHECK_PERIOD_MS));
+
+	if (chip->ext == NULL) {
+		pr_debug("external charger not registered.\n");
+		return;
+	}
+
+	if (chip->ext_charging) {
+		pr_debug("already charging.\n");
+		return;
+	}
+
+	dc_present = is_dc_chg_plugged_in(chip);
+	batt_present = pm_chg_get_rt_status(chip, BATT_INSERTED_IRQ);
+	batt_temp_ok = pm_chg_get_rt_status(chip, BAT_TEMP_OK_IRQ);
+	vbat_ov = pm_chg_get_rt_status(chip, VBAT_OV_IRQ);
+	batfet = pm_chg_get_rt_status(chip, BATFET_IRQ);
+
+	if (!dc_present) {
+		pr_warn("%s. dc not present.\n", __func__);
+		return;
+	}
+	if (!batt_present) {
+		pr_warn("%s. battery not present.\n", __func__);
+		return;
+	}
+	if (!batt_temp_ok) {
+		pr_warn("%s. battery temperature not ok.\n", __func__);
+		return;
+	}
+	if (vbat_ov) {
+		pr_warn("%s. battery over voltage.\n", __func__);
+		return;
+	}
+	if (!batfet) {
+		pr_warn("%s. battery FET not closed.\n", __func__);
+		return;
+	}
+
+	chip->ext->start_charging(chip->ext->ctx);
+	chip->ext_charging = true;
+	chip->ext_charge_done = false;
+	/* Start BMS */
+	schedule_delayed_work(&chip->eoc_work, delay);
+	wake_lock(&chip->eoc_wake_lock);
+}
+
 static void handle_dc_removal_insertion(struct pm8921_chg_chip *chip)
 {
 	int dc_present;
@@ -1290,11 +1409,21 @@
 
 	status = pm_chg_get_rt_status(chip, BATT_INSERTED_IRQ);
 	schedule_work(&chip->battery_id_valid_work);
+	handle_start_ext_chg(chip);
 	pr_debug("battery present=%d", status);
 	power_supply_changed(&chip->batt_psy);
 	return IRQ_HANDLED;
 }
-/* this interrupt used to restart charging a battery */
+
+/*
+ * this interrupt used to restart charging a battery.
+ *
+ * Note: When DC-inserted the VBAT can't go low.
+ * VPH_PWR is provided by the ext-charger.
+ * After End-Of-Charging from DC, charging can be resumed only
+ * if DC is removed and then inserted after the battery was in use.
+ * Therefore the handle_start_ext_chg() is not called.
+ */
 static irqreturn_t vbatdet_low_irq_handler(int irq, void *data)
 {
 	struct pm8921_chg_chip *chip = data;
@@ -1355,6 +1484,9 @@
 	struct pm8921_chg_chip *chip = data;
 
 	pr_debug("state_changed_to=%d\n", pm_chg_get_fsm_state(data));
+
+	handle_stop_ext_chg(chip);
+
 	power_supply_changed(&chip->batt_psy);
 	power_supply_changed(&chip->usb_psy);
 	power_supply_changed(&chip->dc_psy);
@@ -1432,6 +1564,7 @@
 	status = pm_chg_get_rt_status(chip, BATT_REMOVED_IRQ);
 	pr_debug("battery present=%d state=%d", !status,
 					 pm_chg_get_fsm_state(data));
+	handle_stop_ext_chg(chip);
 	power_supply_changed(&chip->batt_psy);
 	return IRQ_HANDLED;
 }
@@ -1440,6 +1573,7 @@
 {
 	struct pm8921_chg_chip *chip = data;
 
+	handle_stop_ext_chg(chip);
 	power_supply_changed(&chip->batt_psy);
 	return IRQ_HANDLED;
 }
@@ -1460,6 +1594,8 @@
 	struct pm8921_chg_chip *chip = data;
 
 	pr_debug("Batt cold fsm_state=%d\n", pm_chg_get_fsm_state(data));
+	handle_stop_ext_chg(chip);
+
 	power_supply_changed(&chip->batt_psy);
 	power_supply_changed(&chip->usb_psy);
 	power_supply_changed(&chip->dc_psy);
@@ -1482,6 +1618,8 @@
 	struct pm8921_chg_chip *chip = data;
 
 	pr_debug("batt temp ok fsm_state=%d\n", pm_chg_get_fsm_state(data));
+	handle_start_ext_chg(chip);
+
 	power_supply_changed(&chip->batt_psy);
 	power_supply_changed(&chip->usb_psy);
 	power_supply_changed(&chip->dc_psy);
@@ -1524,17 +1662,33 @@
 
 static irqreturn_t dcin_valid_irq_handler(int irq, void *data)
 {
-	handle_dc_removal_insertion(data);
+	struct pm8921_chg_chip *chip = data;
+
+	pm8921_disable_source_current(true); /* Force BATFET=ON */
+
+	handle_dc_removal_insertion(chip);
+	handle_start_ext_chg(chip);
 	return IRQ_HANDLED;
 }
 
 static irqreturn_t dcin_ov_irq_handler(int irq, void *data)
 {
+	struct pm8921_chg_chip *chip = data;
+
+	pm8921_disable_source_current(false); /* release BATFET */
+
+	handle_dc_removal_insertion(chip);
+	handle_stop_ext_chg(chip);
 	return IRQ_HANDLED;
 }
 
 static irqreturn_t dcin_uv_irq_handler(int irq, void *data)
 {
+	struct pm8921_chg_chip *chip = data;
+
+	pm8921_disable_source_current(false); /* release BATFET */
+	handle_stop_ext_chg(chip);
+
 	return IRQ_HANDLED;
 }
 
@@ -1556,7 +1710,7 @@
 }
 
 /**
- * eoc_work - internal function to check if battery EOC
+ * eoc_worker - internal function to check if battery EOC
  *		has happened
  *
  * If all conditions favouring, if the charge current is
@@ -1569,7 +1723,7 @@
 #define CONSECUTIVE_COUNT	3
 #define VBAT_TOLERANCE_MV	70
 #define CHG_DISABLE_MSLEEP	100
-static void eoc_work(struct work_struct *work)
+static void eoc_worker(struct work_struct *work)
 {
 	struct delayed_work *dwork = to_delayed_work(work);
 	struct pm8921_chg_chip *chip = container_of(dwork,
@@ -1580,41 +1734,44 @@
 	int rc;
 	static int count;
 
-	/* return if the battery is not being fastcharged */
-	fast_chg = pm_chg_get_rt_status(chip, FASTCHG_IRQ);
-	pr_debug("fast_chg = %d\n", fast_chg);
-	if (fast_chg == 0) {
-		/* enable fastchg irq */
-		pm8921_chg_enable_irq(chip, FASTCHG_IRQ);
-		count = 0;
-		wake_unlock(&chip->eoc_wake_lock);
-		return;
-	}
+	if (!is_ext_charging(chip)) {
+		/* return if the battery is not being fastcharged */
+		fast_chg = pm_chg_get_rt_status(chip, FASTCHG_IRQ);
+		pr_debug("fast_chg = %d\n", fast_chg);
+		if (fast_chg == 0) {
+			/* enable fastchg irq */
+			pm8921_chg_enable_irq(chip, FASTCHG_IRQ);
+			count = 0;
+			wake_unlock(&chip->eoc_wake_lock);
+			return;
+		}
 
-	vcp = pm_chg_get_rt_status(chip, VCP_IRQ);
-	pr_debug("vcp = %d\n", vcp);
-	if (vcp == 1)
-		goto reset_and_reschedule;
+		vcp = pm_chg_get_rt_status(chip, VCP_IRQ);
+		pr_debug("vcp = %d\n", vcp);
+		if (vcp == 1)
+			goto reset_and_reschedule;
 
-	/* reset count if battery is hot/cold */
-	rc = pm_chg_get_rt_status(chip, BAT_TEMP_OK_IRQ);
-	pr_debug("batt_temp_ok = %d\n", rc);
-	if (rc == 0)
-		goto reset_and_reschedule;
+		/* reset count if battery is hot/cold */
+		rc = pm_chg_get_rt_status(chip, BAT_TEMP_OK_IRQ);
+		pr_debug("batt_temp_ok = %d\n", rc);
+		if (rc == 0)
+			goto reset_and_reschedule;
 
-	/* reset count if battery voltage is less than vddmax */
-	vbat_meas = get_prop_battery_mvolts(chip);
-	if (vbat_meas < 0)
-		goto reset_and_reschedule;
+		/* reset count if battery voltage is less than vddmax */
+		vbat_meas = get_prop_battery_mvolts(chip);
+		if (vbat_meas < 0)
+			goto reset_and_reschedule;
 
-	rc = pm_chg_vddmax_get(chip, &vbat_programmed);
-	if (rc) {
-		pr_err("couldnt read vddmax rc = %d\n", rc);
-		goto reset_and_reschedule;
-	}
-	pr_debug("vddmax = %d vbat_meas=%d\n", vbat_programmed, vbat_meas);
-	if (vbat_meas < vbat_programmed - VBAT_TOLERANCE_MV)
-		goto reset_and_reschedule;
+		rc = pm_chg_vddmax_get(chip, &vbat_programmed);
+		if (rc) {
+			pr_err("couldnt read vddmax rc = %d\n", rc);
+			goto reset_and_reschedule;
+		}
+		pr_debug("vddmax = %d vbat_meas=%d\n",
+			 vbat_programmed, vbat_meas);
+		if (vbat_meas < vbat_programmed - VBAT_TOLERANCE_MV)
+			goto reset_and_reschedule;
+	} /* !is_ext_charging */
 
 	/* reset count if battery chg current is more than iterm */
 	rc = pm_chg_iterm_get(chip, &iterm_programmed);
@@ -1636,20 +1793,22 @@
 	if (ichg_meas * -1 > iterm_programmed)
 		goto reset_and_reschedule;
 
-	/*
-	 * TODO if charging from an external charger check SOC instead of
-	 * regulation loop
-	 */
-	regulation_loop = pm_chg_get_regulation_loop(chip);
-	if (regulation_loop < 0) {
-		pr_err("couldnt read the regulation loop err=%d\n",
-			regulation_loop);
-		goto reset_and_reschedule;
-	}
-	pr_debug("regulation_loop=%d\n", regulation_loop);
+	if (!is_ext_charging(chip)) {
+		/*
+		 * TODO if charging from an external charger
+		 * check SOC instead of regulation loop
+		 */
+		regulation_loop = pm_chg_get_regulation_loop(chip);
+		if (regulation_loop < 0) {
+			pr_err("couldnt read the regulation loop err=%d\n",
+				regulation_loop);
+			goto reset_and_reschedule;
+		}
+		pr_debug("regulation_loop=%d\n", regulation_loop);
 
-	if (regulation_loop != 0 && regulation_loop != VDD_LOOP)
-		goto reset_and_reschedule;
+		if (regulation_loop != 0 && regulation_loop != VDD_LOOP)
+			goto reset_and_reschedule;
+	} /* !is_ext_charging */
 
 	count++;
 	if (count == CONSECUTIVE_COUNT) {
@@ -1658,6 +1817,9 @@
 
 		pm_chg_auto_enable(chip, 0);
 
+		if (is_ext_charging(chip))
+			chip->ext_charge_done = true;
+
 		/* declare end of charging by invoking chgdone interrupt */
 		chgdone_irq_handler(chip->pmic_chg_irq[CHGDONE_IRQ], chip);
 		wake_unlock(&chip->eoc_wake_lock);
@@ -1738,6 +1900,36 @@
 	return rc;
 }
 
+int register_external_dc_charger(struct ext_chg_pm8921 *ext)
+{
+	if (the_chip == NULL) {
+		pr_err("called too early\n");
+		return -EINVAL;
+	}
+	/* TODO check function pointers */
+	the_chip->ext = ext;
+	the_chip->ext_charging = false;
+
+	if (is_dc_chg_plugged_in(the_chip))
+		pm8921_disable_source_current(true); /* Force BATFET=ON */
+
+	handle_start_ext_chg(the_chip);
+
+	return 0;
+}
+EXPORT_SYMBOL(register_external_dc_charger);
+
+void unregister_external_dc_charger(struct ext_chg_pm8921 *ext)
+{
+	if (the_chip == NULL) {
+		pr_err("called too early\n");
+		return;
+	}
+	handle_stop_ext_chg(the_chip);
+	the_chip->ext = NULL;
+}
+EXPORT_SYMBOL(unregister_external_dc_charger);
+
 /**
  * set_disable_status_param -
  *
@@ -2397,7 +2589,7 @@
 	the_chip = chip;
 
 	wake_lock_init(&chip->eoc_wake_lock, WAKE_LOCK_SUSPEND, "pm8921_eoc");
-	INIT_DELAYED_WORK(&chip->eoc_work, eoc_work);
+	INIT_DELAYED_WORK(&chip->eoc_work, eoc_worker);
 
 	rc = request_irqs(chip, pdev);
 	if (rc) {