Add a driver for the SMB2 charger block found in the Qualcomm PMI8998
and PM660.
Signed-off-by: Caleb Connolly <caleb.connolly@xxxxxxxxxx>
---
Changes since v1:
* Renamed from qcom_smb2 to qcom_pmi8998_charger
---
drivers/power/supply/Kconfig | 9 +
drivers/power/supply/Makefile | 1 +
drivers/power/supply/qcom_pmi8998_charger.c | 1044 +++++++++++++++++++
3 files changed, 1054 insertions(+)
create mode 100644 drivers/power/supply/qcom_pmi8998_charger.c
diff --git a/drivers/power/supply/Makefile b/drivers/power/supply/Makefile
index 7f02f36aea55..a39e3eda6dbd 100644
+
+/* Init sequence derived from vendor downstream driver */
+static const struct smb2_register smb2_init_seq[] = {
+ { .addr = AICL_RERUN_TIME_CFG, .mask = AICL_RERUN_TIME_MASK, .val = 0 },
+ /*
+ * By default configure us as an upstream facing port
+ * FIXME: for OTG we should set UFP_EN_CMD_BIT and DFP_EN_CMD_BIT both to 0
+ */
+ { .addr = TYPE_C_INTRPT_ENB_SOFTWARE_CTRL,
+ .mask = TYPEC_POWER_ROLE_CMD_MASK | VCONN_EN_SRC_BIT |
+ VCONN_EN_VALUE_BIT,
+ .val = VCONN_EN_SRC_BIT | UFP_EN_CMD_BIT },
+ /*
+ * disable Type-C factory mode and stay in Attached.SRC state when VCONN
+ * over-current happens
+ */
+ { .addr = TYPE_C_CFG,
+ .mask = FACTORY_MODE_DETECTION_EN_BIT | VCONN_OC_CFG_BIT,
+ .val = 0 },
+ /* Configure VBUS for software control */
+ { .addr = OTG_CFG, .mask = OTG_EN_SRC_CFG_BIT, .val = 0 },
+ { .addr = FG_UPDATE_CFG_2_SEL,
+ .mask = SOC_LT_CHG_RECHARGE_THRESH_SEL_BIT |
+ VBT_LT_CHG_RECHARGE_THRESH_SEL_BIT,
+ .val = VBT_LT_CHG_RECHARGE_THRESH_SEL_BIT },
+ /* Enable charging */
+ { .addr = USBIN_OPTIONS_1_CFG, .mask = HVDCP_EN_BIT, .val = 0 },
+ { .addr = CHARGING_ENABLE_CMD,
+ .mask = CHARGING_ENABLE_CMD_BIT,
+ .val = CHARGING_ENABLE_CMD_BIT },
+ /* Allow overriding the current limit */
+ // { .addr = USBIN_LOAD_CFG,
+ // .mask = ICL_OVERRIDE_AFTER_APSD_BIT,
+ // .val = ICL_OVERRIDE_AFTER_APSD_BIT },
+ { .addr = CHGR_CFG2,
+ .mask = CHG_EN_SRC_BIT | CHG_EN_POLARITY_BIT |
+ PRETOFAST_TRANSITION_CFG_BIT | BAT_OV_ECC_BIT | I_TERM_BIT |
+ AUTO_RECHG_BIT | EN_ANALOG_DROP_IN_VBATT_BIT |
+ CHARGER_INHIBIT_BIT,
+ .val = 0 },
+ /*
+ * No clue what this does
+ */
+ { .addr = STAT_CFG,
+ .mask = STAT_SW_OVERRIDE_CFG_BIT,
+ .val = STAT_SW_OVERRIDE_CFG_BIT },
+ /*
+ * Set the default SDP charger type to a 500ma USB 2.0 port
+ */
+ { .addr = USBIN_ICL_OPTIONS,
+ .mask = USB51_MODE_BIT | USBIN_MODE_CHG_BIT,
+ .val = USB51_MODE_BIT },
+ /*
+ * Disable watchdog
+ */
+ { .addr = SNARL_BARK_BITE_WD_CFG, .mask = 0xff, .val = 0 },
+ { .addr = WD_CFG,
+ .mask = WATCHDOG_TRIGGER_AFP_EN_BIT | WDOG_TIMER_EN_ON_PLUGIN_BIT |
+ BARK_WDOG_INT_EN_BIT,
+ .val = 0 },
+ /* OnePlus init stuff from "op_set_collapse_fet" */
+ { .addr = USBIN_5V_AICL_THRESHOLD_CFG,
+ .mask = USBIN_5V_AICL_THRESHOLD_CFG_MASK,
+ .val = 0x3 },
+ { .addr = USBIN_CONT_AICL_THRESHOLD_CFG,
+ .mask = USBIN_CONT_AICL_THRESHOLD_CFG_MASK,
+ .val = 0x3 },
+ /* Yay undocumented register values! */
+ { .addr = USBIN_LOAD_CFG, .mask = BIT(0) | BIT(1), .val = 0x3 },
+ /* Enable Automatic Input Current Limit, this will slowly ramp up the current
+ * When connected to a wall charger, and automatically stop when it detects
+ * the charger current limit (voltage drop?) or it reaches the programmed limit.
+ */
+ { .addr = USBIN_AICL_OPTIONS_CFG,
+ .mask = USBIN_AICL_START_AT_MAX_BIT | USBIN_AICL_ADC_EN_BIT |
+ USBIN_AICL_EN_BIT | SUSPEND_ON_COLLAPSE_USBIN_BIT |
+ USBIN_HV_COLLAPSE_RESPONSE_BIT |
+ USBIN_LV_COLLAPSE_RESPONSE_BIT,
+ .val = USBIN_HV_COLLAPSE_RESPONSE_BIT |
+ USBIN_LV_COLLAPSE_RESPONSE_BIT | USBIN_AICL_EN_BIT },
+ /*
+ * Set pre charge current to default, the OnePlus 6 bootloader
+ * sets this very conservatively.
+ * NOTE: seems to be reset to zero again anyway after boot
+ */
+ { .addr = PRE_CHARGE_CURRENT_CFG,
+ .mask = PRE_CHARGE_CURRENT_SETTING_MASK,
+ .val = 500000 / 25000 },
+ /*
+ * Set "fast charge current" to the default 2A, the OnePlus 6 also
+ * sets this very conservatively.
+ * NOTE: seems to be reset to zero again anyway after boot
+ */
+ { .addr = FAST_CHARGE_CURRENT_CFG,
+ .mask = FAST_CHARGE_CURRENT_SETTING_MASK,
+ .addr = 1950000 / 25000 },
+};
+
+static int smb2_init_hw(struct smb2_chip *chip)
+{
+ int rc, i;
+
+ for (i = 0; i < ARRAY_SIZE(smb2_init_seq); i++) {
+ dev_dbg(chip->dev, "%d: Writing 0x%02x to 0x%02x\n", i,
+ smb2_init_seq[i].val, smb2_init_seq[i].addr);
+ rc = regmap_update_bits(chip->regmap,
+ chip->base + smb2_init_seq[i].addr,
+ smb2_init_seq[i].mask,
+ smb2_init_seq[i].val);
+ if (rc < 0)
+ return dev_err_probe(
+ chip->dev, rc,
+ "%s: Failed to write 0x%02x to 0x%02x\n",
+ __func__, smb2_init_seq[i].val,
+ smb2_init_seq[i].addr);
+ }
+
+ return 0;
+}
+
+struct smb2_irqs {
+ const char *name;
+ irqreturn_t (*handler)(int irq, void *data);
+};
+
+static const struct smb2_irqs irqs[] = {
+ { .name = "bat-ov", .handler = smb2_handle_batt_overvoltage },
+ { .name = "usb-plugin", .handler = smb2_handle_usb_plugin },
+ { .name = "usbin-icl-change", .handler = smb2_handle_usb_icl_change },
+ { .name = "wdog-bark", .handler = smb2_handle_wdog_bark },
+};
+
+static int smb2_probe(struct platform_device *pdev)
+{
+ struct power_supply_config supply_config = {};
+ struct smb2_chip *chip;
+ int rc, i, irq;
+
+ chip = devm_kzalloc(&pdev->dev, sizeof(*chip), GFP_KERNEL);
+ if (!chip)
+ return -ENOMEM;
+
+ chip->dev = &pdev->dev;
+ chip->name = pdev->name;
+
+ chip->regmap = dev_get_regmap(pdev->dev.parent, NULL);
+ if (!chip->regmap) {
+ return dev_err_probe(chip->dev, -ENODEV,
+ "failed to locate the regmap\n");
+ }
+
+ rc = device_property_read_u32(chip->dev, "reg", &chip->base);
+ if (rc < 0) {
+ return dev_err_probe(chip->dev, rc,
+ "Couldn't read base address\n");
+ }
+
+ chip->usb_in_v_chan = devm_iio_channel_get(chip->dev, "usbin_v");
+ if (IS_ERR(chip->usb_in_v_chan))
+ return dev_err_probe(
+ chip->dev, PTR_ERR(chip->usb_in_v_chan),
+ "Couldn't get usbin_v IIO channel from RRADC\n");
+
+ chip->usb_in_i_chan = devm_iio_channel_get(chip->dev, "usbin_i");
+ if (IS_ERR(chip->usb_in_i_chan)) {
+ return dev_err_probe(
+ chip->dev, PTR_ERR(chip->usb_in_i_chan),
+ "Couldn't get usbin_i IIO channel from RRADC\n");
+ }
+
+ rc = smb2_init_hw(chip);
+ if (rc < 0)
+ return rc;
+
+ supply_config.drv_data = chip;
+ supply_config.of_node = pdev->dev.of_node;
+
+ chip->chg_psy = devm_power_supply_register(chip->dev, &smb2_psy_desc,
+ &supply_config);
+ if (IS_ERR(chip->chg_psy))
+ return dev_err_probe(chip->dev, PTR_ERR(chip->chg_psy),
+ "failed to register power supply\n");
+
+ rc = power_supply_get_battery_info(chip->chg_psy, &chip->batt_info);
+ if (rc)
+ return dev_err_probe(chip->dev, rc,
+ "Failed to get battery info\n");
+
+ rc = devm_delayed_work_autocancel(chip->dev, &chip->status_change_work,
+ smb2_status_change_work);
+ if (rc)
+ return dev_err_probe(
+ chip->dev, rc,
+ "Failed to initialise status change work\n");
+
+ rc = (chip->batt_info->voltage_max_design_uv - 3487500) / 7500 + 1;
+ rc = regmap_update_bits(chip->regmap, chip->base + FLOAT_VOLTAGE_CFG,
+ FLOAT_VOLTAGE_SETTING_MASK, rc);
+ if (rc < 0)
+ return dev_err_probe(chip->dev, rc, "Couldn't set vbat max\n");
+
+ for (i = 0; i < ARRAY_SIZE(irqs); i++) {
+ irq = of_irq_get_byname(pdev->dev.of_node, irqs[i].name);
+ if (irq < 0)
+ return dev_err_probe(chip->dev, irq,
+ "Couldn't get irq %s byname\n",
+ irqs[i].name);
+ rc = devm_request_threaded_irq(chip->dev, irq, NULL,
+ irqs[i].handler, IRQF_ONESHOT,
+ irqs[i].name, chip);
+ if (rc < 0)
+ return dev_err_probe(chip->dev, irq,
+ "Couldn't request irq %s\n",
+ irqs[i].name);
+ }
+
+ platform_set_drvdata(pdev, chip);
+
+ /* Initialise charger state */
+ schedule_delayed_work(&chip->status_change_work, 0);
+
+ return 0;
+}
+
+static const struct of_device_id smb2_match_id_table[] = {
+ { .compatible = "qcom,pmi8998-charger" },
+ { .compatible = "qcom,pm660-charger" },
+ { /* sentinal */ }
+};
+MODULE_DEVICE_TABLE(of, smb2_match_id_table);
+
+static struct platform_driver qcom_spmi_smb2 = {
+ .probe = smb2_probe,
+ .driver = {
+ .name = "qcom-pmi8998-charger",
+ .of_match_table = smb2_match_id_table,
+ },
+};
+
+module_platform_driver(qcom_spmi_smb2);
+
+MODULE_AUTHOR("Caleb Connolly <caleb.connolly@xxxxxxxxxx>");
+MODULE_DESCRIPTION("Qualcomm SMB2 Charger Driver");
+MODULE_LICENSE("GPL");