123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508 |
- /*
- * extcon-axp288.c - X-Power AXP288 PMIC extcon cable detection driver
- *
- * Copyright (c) 2017-2018 Hans de Goede <hdegoede@redhat.com>
- * Copyright (C) 2015 Intel Corporation
- * Author: Ramakrishna Pallala <ramakrishna.pallala@intel.com>
- *
- * 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.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- */
- #include <linux/acpi.h>
- #include <linux/module.h>
- #include <linux/kernel.h>
- #include <linux/io.h>
- #include <linux/slab.h>
- #include <linux/interrupt.h>
- #include <linux/platform_device.h>
- #include <linux/property.h>
- #include <linux/notifier.h>
- #include <linux/extcon-provider.h>
- #include <linux/regmap.h>
- #include <linux/mfd/axp20x.h>
- #include <linux/usb/role.h>
- #include <linux/workqueue.h>
- #include <asm/cpu_device_id.h>
- #include <asm/intel-family.h>
- /* Power source status register */
- #define PS_STAT_VBUS_TRIGGER BIT(0)
- #define PS_STAT_BAT_CHRG_DIR BIT(2)
- #define PS_STAT_VBUS_ABOVE_VHOLD BIT(3)
- #define PS_STAT_VBUS_VALID BIT(4)
- #define PS_STAT_VBUS_PRESENT BIT(5)
- /* BC module global register */
- #define BC_GLOBAL_RUN BIT(0)
- #define BC_GLOBAL_DET_STAT BIT(2)
- #define BC_GLOBAL_DBP_TOUT BIT(3)
- #define BC_GLOBAL_VLGC_COM_SEL BIT(4)
- #define BC_GLOBAL_DCD_TOUT_MASK (BIT(6)|BIT(5))
- #define BC_GLOBAL_DCD_TOUT_300MS 0
- #define BC_GLOBAL_DCD_TOUT_100MS 1
- #define BC_GLOBAL_DCD_TOUT_500MS 2
- #define BC_GLOBAL_DCD_TOUT_900MS 3
- #define BC_GLOBAL_DCD_DET_SEL BIT(7)
- /* BC module vbus control and status register */
- #define VBUS_CNTL_DPDM_PD_EN BIT(4)
- #define VBUS_CNTL_DPDM_FD_EN BIT(5)
- #define VBUS_CNTL_FIRST_PO_STAT BIT(6)
- /* BC USB status register */
- #define USB_STAT_BUS_STAT_MASK (BIT(3)|BIT(2)|BIT(1)|BIT(0))
- #define USB_STAT_BUS_STAT_SHIFT 0
- #define USB_STAT_BUS_STAT_ATHD 0
- #define USB_STAT_BUS_STAT_CONN 1
- #define USB_STAT_BUS_STAT_SUSP 2
- #define USB_STAT_BUS_STAT_CONF 3
- #define USB_STAT_USB_SS_MODE BIT(4)
- #define USB_STAT_DEAD_BAT_DET BIT(6)
- #define USB_STAT_DBP_UNCFG BIT(7)
- /* BC detect status register */
- #define DET_STAT_MASK (BIT(7)|BIT(6)|BIT(5))
- #define DET_STAT_SHIFT 5
- #define DET_STAT_SDP 1
- #define DET_STAT_CDP 2
- #define DET_STAT_DCP 3
- enum axp288_extcon_reg {
- AXP288_PS_STAT_REG = 0x00,
- AXP288_PS_BOOT_REASON_REG = 0x02,
- AXP288_BC_GLOBAL_REG = 0x2c,
- AXP288_BC_VBUS_CNTL_REG = 0x2d,
- AXP288_BC_USB_STAT_REG = 0x2e,
- AXP288_BC_DET_STAT_REG = 0x2f,
- };
- enum axp288_extcon_irq {
- VBUS_FALLING_IRQ = 0,
- VBUS_RISING_IRQ,
- MV_CHNG_IRQ,
- BC_USB_CHNG_IRQ,
- EXTCON_IRQ_END,
- };
- static const unsigned int axp288_extcon_cables[] = {
- EXTCON_CHG_USB_SDP,
- EXTCON_CHG_USB_CDP,
- EXTCON_CHG_USB_DCP,
- EXTCON_USB,
- EXTCON_NONE,
- };
- struct axp288_extcon_info {
- struct device *dev;
- struct regmap *regmap;
- struct regmap_irq_chip_data *regmap_irqc;
- struct usb_role_switch *role_sw;
- struct work_struct role_work;
- int irq[EXTCON_IRQ_END];
- struct extcon_dev *edev;
- struct extcon_dev *id_extcon;
- struct notifier_block id_nb;
- unsigned int previous_cable;
- bool vbus_attach;
- };
- static const struct x86_cpu_id cherry_trail_cpu_ids[] = {
- { X86_VENDOR_INTEL, 6, INTEL_FAM6_ATOM_AIRMONT, X86_FEATURE_ANY },
- {}
- };
- /* Power up/down reason string array */
- static const char * const axp288_pwr_up_down_info[] = {
- "Last wake caused by user pressing the power button",
- "Last wake caused by a charger insertion",
- "Last wake caused by a battery insertion",
- "Last wake caused by SOC initiated global reset",
- "Last wake caused by cold reset",
- "Last shutdown caused by PMIC UVLO threshold",
- "Last shutdown caused by SOC initiated cold off",
- "Last shutdown caused by user pressing the power button",
- NULL,
- };
- /*
- * Decode and log the given "reset source indicator" (rsi)
- * register and then clear it.
- */
- static void axp288_extcon_log_rsi(struct axp288_extcon_info *info)
- {
- const char * const *rsi;
- unsigned int val, i, clear_mask = 0;
- int ret;
- ret = regmap_read(info->regmap, AXP288_PS_BOOT_REASON_REG, &val);
- for (i = 0, rsi = axp288_pwr_up_down_info; *rsi; rsi++, i++) {
- if (val & BIT(i)) {
- dev_dbg(info->dev, "%s\n", *rsi);
- clear_mask |= BIT(i);
- }
- }
- /* Clear the register value for next reboot (write 1 to clear bit) */
- regmap_write(info->regmap, AXP288_PS_BOOT_REASON_REG, clear_mask);
- }
- /*
- * The below code to control the USB role-switch on devices with an AXP288
- * may seem out of place, but there are 2 reasons why this is the best place
- * to control the USB role-switch on such devices:
- * 1) On many devices the USB role is controlled by AML code, but the AML code
- * only switches between the host and none roles, because of Windows not
- * really using device mode. To make device mode work we need to toggle
- * between the none/device roles based on Vbus presence, and this driver
- * gets interrupts on Vbus insertion / removal.
- * 2) In order for our BC1.2 charger detection to work properly the role
- * mux must be properly set to device mode before we do the detection.
- */
- /* Returns the id-pin value, note pulled low / false == host-mode */
- static bool axp288_get_id_pin(struct axp288_extcon_info *info)
- {
- enum usb_role role;
- if (info->id_extcon)
- return extcon_get_state(info->id_extcon, EXTCON_USB_HOST) <= 0;
- /* We cannot access the id-pin, see what mode the AML code has set */
- role = usb_role_switch_get_role(info->role_sw);
- return role != USB_ROLE_HOST;
- }
- static void axp288_usb_role_work(struct work_struct *work)
- {
- struct axp288_extcon_info *info =
- container_of(work, struct axp288_extcon_info, role_work);
- enum usb_role role;
- bool id_pin;
- int ret;
- id_pin = axp288_get_id_pin(info);
- if (!id_pin)
- role = USB_ROLE_HOST;
- else if (info->vbus_attach)
- role = USB_ROLE_DEVICE;
- else
- role = USB_ROLE_NONE;
- ret = usb_role_switch_set_role(info->role_sw, role);
- if (ret)
- dev_err(info->dev, "failed to set role: %d\n", ret);
- }
- static bool axp288_get_vbus_attach(struct axp288_extcon_info *info)
- {
- int ret, pwr_stat;
- ret = regmap_read(info->regmap, AXP288_PS_STAT_REG, &pwr_stat);
- if (ret < 0) {
- dev_err(info->dev, "failed to read vbus status\n");
- return false;
- }
- return !!(pwr_stat & PS_STAT_VBUS_VALID);
- }
- static int axp288_handle_chrg_det_event(struct axp288_extcon_info *info)
- {
- int ret, stat, cfg;
- u8 chrg_type;
- unsigned int cable = info->previous_cable;
- bool vbus_attach = false;
- vbus_attach = axp288_get_vbus_attach(info);
- if (!vbus_attach)
- goto no_vbus;
- /* Check charger detection completion status */
- ret = regmap_read(info->regmap, AXP288_BC_GLOBAL_REG, &cfg);
- if (ret < 0)
- goto dev_det_ret;
- if (cfg & BC_GLOBAL_DET_STAT) {
- dev_dbg(info->dev, "can't complete the charger detection\n");
- goto dev_det_ret;
- }
- ret = regmap_read(info->regmap, AXP288_BC_DET_STAT_REG, &stat);
- if (ret < 0)
- goto dev_det_ret;
- chrg_type = (stat & DET_STAT_MASK) >> DET_STAT_SHIFT;
- switch (chrg_type) {
- case DET_STAT_SDP:
- dev_dbg(info->dev, "sdp cable is connected\n");
- cable = EXTCON_CHG_USB_SDP;
- break;
- case DET_STAT_CDP:
- dev_dbg(info->dev, "cdp cable is connected\n");
- cable = EXTCON_CHG_USB_CDP;
- break;
- case DET_STAT_DCP:
- dev_dbg(info->dev, "dcp cable is connected\n");
- cable = EXTCON_CHG_USB_DCP;
- break;
- default:
- dev_warn(info->dev, "unknown (reserved) bc detect result\n");
- cable = EXTCON_CHG_USB_SDP;
- }
- no_vbus:
- extcon_set_state_sync(info->edev, info->previous_cable, false);
- if (info->previous_cable == EXTCON_CHG_USB_SDP)
- extcon_set_state_sync(info->edev, EXTCON_USB, false);
- if (vbus_attach) {
- extcon_set_state_sync(info->edev, cable, vbus_attach);
- if (cable == EXTCON_CHG_USB_SDP)
- extcon_set_state_sync(info->edev, EXTCON_USB,
- vbus_attach);
- info->previous_cable = cable;
- }
- if (info->role_sw && info->vbus_attach != vbus_attach) {
- info->vbus_attach = vbus_attach;
- /* Setting the role can take a while */
- queue_work(system_long_wq, &info->role_work);
- }
- return 0;
- dev_det_ret:
- if (ret < 0)
- dev_err(info->dev, "failed to detect BC Mod\n");
- return ret;
- }
- static int axp288_extcon_id_evt(struct notifier_block *nb,
- unsigned long event, void *param)
- {
- struct axp288_extcon_info *info =
- container_of(nb, struct axp288_extcon_info, id_nb);
- /* We may not sleep and setting the role can take a while */
- queue_work(system_long_wq, &info->role_work);
- return NOTIFY_OK;
- }
- static irqreturn_t axp288_extcon_isr(int irq, void *data)
- {
- struct axp288_extcon_info *info = data;
- int ret;
- ret = axp288_handle_chrg_det_event(info);
- if (ret < 0)
- dev_err(info->dev, "failed to handle the interrupt\n");
- return IRQ_HANDLED;
- }
- static void axp288_extcon_enable(struct axp288_extcon_info *info)
- {
- regmap_update_bits(info->regmap, AXP288_BC_GLOBAL_REG,
- BC_GLOBAL_RUN, 0);
- /* Enable the charger detection logic */
- regmap_update_bits(info->regmap, AXP288_BC_GLOBAL_REG,
- BC_GLOBAL_RUN, BC_GLOBAL_RUN);
- }
- static void axp288_put_role_sw(void *data)
- {
- struct axp288_extcon_info *info = data;
- cancel_work_sync(&info->role_work);
- usb_role_switch_put(info->role_sw);
- }
- static int axp288_extcon_probe(struct platform_device *pdev)
- {
- struct axp288_extcon_info *info;
- struct axp20x_dev *axp20x = dev_get_drvdata(pdev->dev.parent);
- struct device *dev = &pdev->dev;
- const char *name;
- int ret, i, pirq;
- info = devm_kzalloc(&pdev->dev, sizeof(*info), GFP_KERNEL);
- if (!info)
- return -ENOMEM;
- info->dev = &pdev->dev;
- info->regmap = axp20x->regmap;
- info->regmap_irqc = axp20x->regmap_irqc;
- info->previous_cable = EXTCON_NONE;
- INIT_WORK(&info->role_work, axp288_usb_role_work);
- info->id_nb.notifier_call = axp288_extcon_id_evt;
- platform_set_drvdata(pdev, info);
- info->role_sw = usb_role_switch_get(dev);
- if (IS_ERR(info->role_sw))
- return PTR_ERR(info->role_sw);
- if (info->role_sw) {
- ret = devm_add_action_or_reset(dev, axp288_put_role_sw, info);
- if (ret)
- return ret;
- name = acpi_dev_get_first_match_name("INT3496", NULL, -1);
- if (name) {
- info->id_extcon = extcon_get_extcon_dev(name);
- if (!info->id_extcon)
- return -EPROBE_DEFER;
- dev_info(dev, "controlling USB role\n");
- } else {
- dev_info(dev, "controlling USB role based on Vbus presence\n");
- }
- }
- info->vbus_attach = axp288_get_vbus_attach(info);
- axp288_extcon_log_rsi(info);
- /* Initialize extcon device */
- info->edev = devm_extcon_dev_allocate(&pdev->dev,
- axp288_extcon_cables);
- if (IS_ERR(info->edev)) {
- dev_err(&pdev->dev, "failed to allocate memory for extcon\n");
- return PTR_ERR(info->edev);
- }
- /* Register extcon device */
- ret = devm_extcon_dev_register(&pdev->dev, info->edev);
- if (ret) {
- dev_err(&pdev->dev, "failed to register extcon device\n");
- return ret;
- }
- for (i = 0; i < EXTCON_IRQ_END; i++) {
- pirq = platform_get_irq(pdev, i);
- if (pirq < 0)
- return pirq;
- info->irq[i] = regmap_irq_get_virq(info->regmap_irqc, pirq);
- if (info->irq[i] < 0) {
- dev_err(&pdev->dev,
- "failed to get virtual interrupt=%d\n", pirq);
- ret = info->irq[i];
- return ret;
- }
- ret = devm_request_threaded_irq(&pdev->dev, info->irq[i],
- NULL, axp288_extcon_isr,
- IRQF_ONESHOT | IRQF_NO_SUSPEND,
- pdev->name, info);
- if (ret) {
- dev_err(&pdev->dev, "failed to request interrupt=%d\n",
- info->irq[i]);
- return ret;
- }
- }
- if (info->id_extcon) {
- ret = devm_extcon_register_notifier_all(dev, info->id_extcon,
- &info->id_nb);
- if (ret)
- return ret;
- }
- /* Make sure the role-sw is set correctly before doing BC detection */
- if (info->role_sw) {
- queue_work(system_long_wq, &info->role_work);
- flush_work(&info->role_work);
- }
- /* Start charger cable type detection */
- axp288_extcon_enable(info);
- device_init_wakeup(dev, true);
- platform_set_drvdata(pdev, info);
- return 0;
- }
- static int __maybe_unused axp288_extcon_suspend(struct device *dev)
- {
- struct axp288_extcon_info *info = dev_get_drvdata(dev);
- if (device_may_wakeup(dev))
- enable_irq_wake(info->irq[VBUS_RISING_IRQ]);
- return 0;
- }
- static int __maybe_unused axp288_extcon_resume(struct device *dev)
- {
- struct axp288_extcon_info *info = dev_get_drvdata(dev);
- /*
- * Wakeup when a charger is connected to do charger-type
- * connection and generate an extcon event which makes the
- * axp288 charger driver set the input current limit.
- */
- if (device_may_wakeup(dev))
- disable_irq_wake(info->irq[VBUS_RISING_IRQ]);
- return 0;
- }
- static SIMPLE_DEV_PM_OPS(axp288_extcon_pm_ops, axp288_extcon_suspend,
- axp288_extcon_resume);
- static const struct platform_device_id axp288_extcon_table[] = {
- { .name = "axp288_extcon" },
- {},
- };
- MODULE_DEVICE_TABLE(platform, axp288_extcon_table);
- static struct platform_driver axp288_extcon_driver = {
- .probe = axp288_extcon_probe,
- .id_table = axp288_extcon_table,
- .driver = {
- .name = "axp288_extcon",
- .pm = &axp288_extcon_pm_ops,
- },
- };
- static struct device_connection axp288_extcon_role_sw_conn = {
- .endpoint[0] = "axp288_extcon",
- .endpoint[1] = "intel_xhci_usb_sw-role-switch",
- .id = "usb-role-switch",
- };
- static int __init axp288_extcon_init(void)
- {
- if (x86_match_cpu(cherry_trail_cpu_ids))
- device_connection_add(&axp288_extcon_role_sw_conn);
- return platform_driver_register(&axp288_extcon_driver);
- }
- module_init(axp288_extcon_init);
- static void __exit axp288_extcon_exit(void)
- {
- if (x86_match_cpu(cherry_trail_cpu_ids))
- device_connection_remove(&axp288_extcon_role_sw_conn);
- platform_driver_unregister(&axp288_extcon_driver);
- }
- module_exit(axp288_extcon_exit);
- MODULE_AUTHOR("Ramakrishna Pallala <ramakrishna.pallala@intel.com>");
- MODULE_AUTHOR("Hans de Goede <hdegoede@redhat.com>");
- MODULE_DESCRIPTION("X-Powers AXP288 extcon driver");
- MODULE_LICENSE("GPL v2");
|