| // SPDX-License-Identifier: GPL-2.0-or-later |
| /* |
| * ON Semiconductor LC824206XA Micro USB Switch driver |
| * |
| * Copyright (c) 2024 Hans de Goede <hansg@kernel.org> |
| * |
| * ON Semiconductor has an "Advance Information" datasheet available |
| * (ENA2222-D.PDF), but no full datasheet. So there is no documentation |
| * available for the registers. |
| * |
| * This driver is based on the register info from the extcon-fsa9285.c driver, |
| * from the Lollipop Android sources for the Lenovo Yoga Tablet 2 (Pro) |
| * 830 / 1050 / 1380 models. Note despite the name this is actually a driver |
| * for the LC824206XA not the FSA9285. The Android sources can be downloaded |
| * from Lenovo's support page for these tablets, filename: |
| * yoga_tab_2_osc_android_to_lollipop_201505.rar. |
| */ |
| |
| #include <linux/bits.h> |
| #include <linux/delay.h> |
| #include <linux/device.h> |
| #include <linux/extcon-provider.h> |
| #include <linux/i2c.h> |
| #include <linux/interrupt.h> |
| #include <linux/module.h> |
| #include <linux/power_supply.h> |
| #include <linux/property.h> |
| #include <linux/regulator/consumer.h> |
| #include <linux/workqueue.h> |
| |
| /* |
| * Register defines as mentioned above there is no datasheet with register |
| * info, so this may not be 100% accurate. |
| */ |
| #define REG00 0x00 |
| #define REG00_INIT_VALUE 0x01 |
| |
| #define REG_STATUS 0x01 |
| #define STATUS_OVP BIT(0) |
| #define STATUS_DATA_SHORT BIT(1) |
| #define STATUS_VBUS_PRESENT BIT(2) |
| #define STATUS_USB_ID GENMASK(7, 3) |
| #define STATUS_USB_ID_GND 0x80 |
| #define STATUS_USB_ID_ACA 0xf0 |
| #define STATUS_USB_ID_FLOAT 0xf8 |
| |
| /* |
| * This controls the DP/DM muxes + other switches, |
| * meaning of individual bits is unknown. |
| */ |
| #define REG_SWITCH_CONTROL 0x02 |
| #define SWITCH_STEREO_MIC 0xc8 |
| #define SWITCH_USB_HOST 0xec |
| #define SWITCH_DISCONNECTED 0xf8 |
| #define SWITCH_USB_DEVICE 0xfc |
| |
| /* 5 bits? ADC 0x10 GND, 0x1a-0x1f ACA, 0x1f float */ |
| #define REG_ID_PIN_ADC_VALUE 0x03 |
| |
| /* Masks for all 3 interrupt registers */ |
| #define INTR_ID_PIN_CHANGE BIT(0) |
| #define INTR_VBUS_CHANGE BIT(1) |
| /* Both of these get set after a continuous mode ADC conversion */ |
| #define INTR_ID_PIN_ADC_INT1 BIT(2) |
| #define INTR_ID_PIN_ADC_INT2 BIT(3) |
| /* Charger type available in reg 0x09 */ |
| #define INTR_CHARGER_DET_DONE BIT(4) |
| #define INTR_OVP BIT(5) |
| |
| /* There are 7 interrupt sources, bit 6 use is unknown (OCP?) */ |
| #define INTR_ALL GENMASK(6, 0) |
| |
| /* Unmask interrupts this driver cares about */ |
| #define INTR_MASK \ |
| (INTR_ALL & ~(INTR_ID_PIN_CHANGE | INTR_VBUS_CHANGE | INTR_CHARGER_DET_DONE)) |
| |
| /* Active (event happened and not cleared yet) interrupts */ |
| #define REG_INTR_STATUS 0x04 |
| |
| /* |
| * Writing a 1 to a bit here clears it in INTR_STATUS. These bits do NOT |
| * auto-reset to 0, so these must be set to 0 manually after clearing. |
| */ |
| #define REG_INTR_CLEAR 0x05 |
| |
| /* Interrupts which bit is set to 1 here will not raise the HW IRQ */ |
| #define REG_INTR_MASK 0x06 |
| |
| /* ID pin ADC control, meaning of individual bits is unknown */ |
| #define REG_ID_PIN_ADC_CTRL 0x07 |
| #define ID_PIN_ADC_AUTO 0x40 |
| #define ID_PIN_ADC_CONTINUOUS 0x44 |
| |
| #define REG_CHARGER_DET 0x08 |
| #define CHARGER_DET_ON BIT(0) |
| #define CHARGER_DET_CDP_ON BIT(1) |
| #define CHARGER_DET_CDP_VAL BIT(2) |
| |
| #define REG_CHARGER_TYPE 0x09 |
| #define CHARGER_TYPE_UNKNOWN 0x00 |
| #define CHARGER_TYPE_DCP 0x01 |
| #define CHARGER_TYPE_SDP_OR_CDP 0x04 |
| #define CHARGER_TYPE_QC 0x06 |
| |
| #define REG10 0x10 |
| #define REG10_INIT_VALUE 0x00 |
| |
| struct lc824206xa_data { |
| struct work_struct work; |
| struct i2c_client *client; |
| struct extcon_dev *edev; |
| struct power_supply *psy; |
| struct regulator *vbus_boost; |
| unsigned int usb_type; |
| unsigned int cable; |
| unsigned int previous_cable; |
| u8 switch_control; |
| u8 previous_switch_control; |
| bool vbus_ok; |
| bool vbus_boost_enabled; |
| bool fastcharge_over_miclr; |
| }; |
| |
| static const unsigned int lc824206xa_cables[] = { |
| EXTCON_USB_HOST, |
| EXTCON_CHG_USB_SDP, |
| EXTCON_CHG_USB_CDP, |
| EXTCON_CHG_USB_DCP, |
| EXTCON_CHG_USB_ACA, |
| EXTCON_CHG_USB_FAST, |
| EXTCON_NONE, |
| }; |
| |
| /* read/write reg helpers to add error logging to smbus byte functions */ |
| static int lc824206xa_read_reg(struct lc824206xa_data *data, u8 reg) |
| { |
| int ret; |
| |
| ret = i2c_smbus_read_byte_data(data->client, reg); |
| if (ret < 0) |
| dev_err(&data->client->dev, "Error %d reading reg 0x%02x\n", ret, reg); |
| |
| return ret; |
| } |
| |
| static int lc824206xa_write_reg(struct lc824206xa_data *data, u8 reg, u8 val) |
| { |
| int ret; |
| |
| ret = i2c_smbus_write_byte_data(data->client, reg, val); |
| if (ret < 0) |
| dev_err(&data->client->dev, "Error %d writing reg 0x%02x\n", ret, reg); |
| |
| return ret; |
| } |
| |
| static int lc824206xa_get_id(struct lc824206xa_data *data) |
| { |
| int ret; |
| |
| ret = lc824206xa_write_reg(data, REG_ID_PIN_ADC_CTRL, ID_PIN_ADC_CONTINUOUS); |
| if (ret) |
| return ret; |
| |
| ret = lc824206xa_read_reg(data, REG_ID_PIN_ADC_VALUE); |
| |
| lc824206xa_write_reg(data, REG_ID_PIN_ADC_CTRL, ID_PIN_ADC_AUTO); |
| |
| return ret; |
| } |
| |
| static void lc824206xa_set_vbus_boost(struct lc824206xa_data *data, bool enable) |
| { |
| int ret; |
| |
| if (data->vbus_boost_enabled == enable) |
| return; |
| |
| if (enable) |
| ret = regulator_enable(data->vbus_boost); |
| else |
| ret = regulator_disable(data->vbus_boost); |
| |
| if (ret == 0) |
| data->vbus_boost_enabled = enable; |
| else |
| dev_err(&data->client->dev, "Error updating Vbus boost regulator: %d\n", ret); |
| } |
| |
| static void lc824206xa_charger_detect(struct lc824206xa_data *data) |
| { |
| int charger_type, ret; |
| |
| charger_type = lc824206xa_read_reg(data, REG_CHARGER_TYPE); |
| if (charger_type < 0) |
| return; |
| |
| dev_dbg(&data->client->dev, "charger type 0x%02x\n", charger_type); |
| |
| switch (charger_type) { |
| case CHARGER_TYPE_UNKNOWN: |
| data->usb_type = POWER_SUPPLY_USB_TYPE_UNKNOWN; |
| /* Treat as SDP */ |
| data->cable = EXTCON_CHG_USB_SDP; |
| data->switch_control = SWITCH_USB_DEVICE; |
| break; |
| case CHARGER_TYPE_SDP_OR_CDP: |
| data->usb_type = POWER_SUPPLY_USB_TYPE_SDP; |
| data->cable = EXTCON_CHG_USB_SDP; |
| data->switch_control = SWITCH_USB_DEVICE; |
| |
| ret = lc824206xa_write_reg(data, REG_CHARGER_DET, |
| CHARGER_DET_CDP_ON | CHARGER_DET_ON); |
| if (ret < 0) |
| break; |
| |
| msleep(100); |
| ret = lc824206xa_read_reg(data, REG_CHARGER_DET); |
| if (ret >= 0 && (ret & CHARGER_DET_CDP_VAL)) { |
| data->usb_type = POWER_SUPPLY_USB_TYPE_CDP; |
| data->cable = EXTCON_CHG_USB_CDP; |
| } |
| |
| lc824206xa_write_reg(data, REG_CHARGER_DET, CHARGER_DET_ON); |
| break; |
| case CHARGER_TYPE_DCP: |
| data->usb_type = POWER_SUPPLY_USB_TYPE_DCP; |
| data->cable = EXTCON_CHG_USB_DCP; |
| if (data->fastcharge_over_miclr) |
| data->switch_control = SWITCH_STEREO_MIC; |
| else |
| data->switch_control = SWITCH_DISCONNECTED; |
| break; |
| case CHARGER_TYPE_QC: |
| data->usb_type = POWER_SUPPLY_USB_TYPE_DCP; |
| data->cable = EXTCON_CHG_USB_DCP; |
| data->switch_control = SWITCH_DISCONNECTED; |
| break; |
| default: |
| dev_warn(&data->client->dev, "Unknown charger type: 0x%02x\n", charger_type); |
| break; |
| } |
| } |
| |
| static void lc824206xa_work(struct work_struct *work) |
| { |
| struct lc824206xa_data *data = container_of(work, struct lc824206xa_data, work); |
| bool vbus_boost_enable = false; |
| int status, id; |
| |
| status = lc824206xa_read_reg(data, REG_STATUS); |
| if (status < 0) |
| return; |
| |
| dev_dbg(&data->client->dev, "status 0x%02x\n", status); |
| |
| data->vbus_ok = (status & (STATUS_VBUS_PRESENT | STATUS_OVP)) == STATUS_VBUS_PRESENT; |
| |
| /* Read id pin ADC if necessary */ |
| switch (status & STATUS_USB_ID) { |
| case STATUS_USB_ID_GND: |
| case STATUS_USB_ID_FLOAT: |
| break; |
| default: |
| /* Happens when the connector is inserted slowly, log at dbg level */ |
| dev_dbg(&data->client->dev, "Unknown status 0x%02x\n", status); |
| fallthrough; |
| case STATUS_USB_ID_ACA: |
| id = lc824206xa_get_id(data); |
| dev_dbg(&data->client->dev, "RID 0x%02x\n", id); |
| switch (id) { |
| case 0x10: |
| status = STATUS_USB_ID_GND; |
| break; |
| case 0x18 ... 0x1e: |
| status = STATUS_USB_ID_ACA; |
| break; |
| case 0x1f: |
| status = STATUS_USB_ID_FLOAT; |
| break; |
| default: |
| dev_warn(&data->client->dev, "Unknown RID 0x%02x\n", id); |
| return; |
| } |
| } |
| |
| /* Check for out of spec OTG charging hubs, treat as ACA */ |
| if ((status & STATUS_USB_ID) == STATUS_USB_ID_GND && |
| data->vbus_ok && !data->vbus_boost_enabled) { |
| dev_info(&data->client->dev, "Out of spec USB host adapter with Vbus present, not enabling 5V output\n"); |
| status = STATUS_USB_ID_ACA; |
| } |
| |
| switch (status & STATUS_USB_ID) { |
| case STATUS_USB_ID_ACA: |
| data->usb_type = POWER_SUPPLY_USB_TYPE_ACA; |
| data->cable = EXTCON_CHG_USB_ACA; |
| data->switch_control = SWITCH_USB_HOST; |
| break; |
| case STATUS_USB_ID_GND: |
| data->usb_type = POWER_SUPPLY_USB_TYPE_UNKNOWN; |
| data->cable = EXTCON_USB_HOST; |
| data->switch_control = SWITCH_USB_HOST; |
| vbus_boost_enable = true; |
| break; |
| case STATUS_USB_ID_FLOAT: |
| /* When fast charging with Vbus > 5V, OVP will be set */ |
| if (data->fastcharge_over_miclr && |
| data->switch_control == SWITCH_STEREO_MIC && |
| (status & STATUS_OVP)) { |
| data->cable = EXTCON_CHG_USB_FAST; |
| break; |
| } |
| |
| if (data->vbus_ok) { |
| lc824206xa_charger_detect(data); |
| } else { |
| data->usb_type = POWER_SUPPLY_USB_TYPE_UNKNOWN; |
| data->cable = EXTCON_NONE; |
| data->switch_control = SWITCH_DISCONNECTED; |
| } |
| break; |
| } |
| |
| lc824206xa_set_vbus_boost(data, vbus_boost_enable); |
| |
| if (data->switch_control != data->previous_switch_control) { |
| lc824206xa_write_reg(data, REG_SWITCH_CONTROL, data->switch_control); |
| data->previous_switch_control = data->switch_control; |
| } |
| |
| if (data->cable != data->previous_cable) { |
| extcon_set_state_sync(data->edev, data->previous_cable, false); |
| extcon_set_state_sync(data->edev, data->cable, true); |
| data->previous_cable = data->cable; |
| } |
| |
| power_supply_changed(data->psy); |
| } |
| |
| static irqreturn_t lc824206xa_irq(int irq, void *_data) |
| { |
| struct lc824206xa_data *data = _data; |
| int intr_status; |
| |
| intr_status = lc824206xa_read_reg(data, REG_INTR_STATUS); |
| if (intr_status < 0) |
| intr_status = INTR_ALL; /* Should never happen, clear all */ |
| |
| dev_dbg(&data->client->dev, "interrupt 0x%02x\n", intr_status); |
| |
| lc824206xa_write_reg(data, REG_INTR_CLEAR, intr_status); |
| lc824206xa_write_reg(data, REG_INTR_CLEAR, 0); |
| |
| schedule_work(&data->work); |
| return IRQ_HANDLED; |
| } |
| |
| /* |
| * Newer charger (power_supply) drivers expect the max input current to be |
| * provided by a parent power_supply device for the charger chip. |
| */ |
| static int lc824206xa_psy_get_prop(struct power_supply *psy, |
| enum power_supply_property psp, |
| union power_supply_propval *val) |
| { |
| struct lc824206xa_data *data = power_supply_get_drvdata(psy); |
| |
| switch (psp) { |
| case POWER_SUPPLY_PROP_ONLINE: |
| val->intval = data->vbus_ok && !data->vbus_boost_enabled; |
| break; |
| case POWER_SUPPLY_PROP_USB_TYPE: |
| val->intval = data->usb_type; |
| break; |
| case POWER_SUPPLY_PROP_CURRENT_MAX: |
| switch (data->usb_type) { |
| case POWER_SUPPLY_USB_TYPE_DCP: |
| case POWER_SUPPLY_USB_TYPE_ACA: |
| val->intval = 2000000; |
| break; |
| case POWER_SUPPLY_USB_TYPE_CDP: |
| val->intval = 1500000; |
| break; |
| default: |
| val->intval = 500000; |
| } |
| break; |
| default: |
| return -EINVAL; |
| } |
| |
| return 0; |
| } |
| |
| static const enum power_supply_property lc824206xa_psy_props[] = { |
| POWER_SUPPLY_PROP_ONLINE, |
| POWER_SUPPLY_PROP_USB_TYPE, |
| POWER_SUPPLY_PROP_CURRENT_MAX, |
| }; |
| |
| static const struct power_supply_desc lc824206xa_psy_desc = { |
| .name = "lc824206xa-charger-detect", |
| .type = POWER_SUPPLY_TYPE_USB, |
| .usb_types = BIT(POWER_SUPPLY_USB_TYPE_SDP) | |
| BIT(POWER_SUPPLY_USB_TYPE_CDP) | |
| BIT(POWER_SUPPLY_USB_TYPE_DCP) | |
| BIT(POWER_SUPPLY_USB_TYPE_ACA) | |
| BIT(POWER_SUPPLY_USB_TYPE_UNKNOWN), |
| .properties = lc824206xa_psy_props, |
| .num_properties = ARRAY_SIZE(lc824206xa_psy_props), |
| .get_property = lc824206xa_psy_get_prop, |
| }; |
| |
| static int lc824206xa_probe(struct i2c_client *client) |
| { |
| struct power_supply_config psy_cfg = { }; |
| struct device *dev = &client->dev; |
| struct lc824206xa_data *data; |
| int ret; |
| |
| data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL); |
| if (!data) |
| return -ENOMEM; |
| |
| data->client = client; |
| INIT_WORK(&data->work, lc824206xa_work); |
| data->cable = EXTCON_NONE; |
| data->previous_cable = EXTCON_NONE; |
| data->usb_type = POWER_SUPPLY_USB_TYPE_UNKNOWN; |
| /* Some designs use a custom fast-charge protocol over the mic L/R inputs */ |
| data->fastcharge_over_miclr = |
| device_property_read_bool(dev, "onnn,enable-miclr-for-dcp"); |
| |
| data->vbus_boost = devm_regulator_get(dev, "vbus"); |
| if (IS_ERR(data->vbus_boost)) |
| return dev_err_probe(dev, PTR_ERR(data->vbus_boost), |
| "getting regulator\n"); |
| |
| /* Init */ |
| ret = lc824206xa_write_reg(data, REG00, REG00_INIT_VALUE); |
| ret |= lc824206xa_write_reg(data, REG10, REG10_INIT_VALUE); |
| msleep(100); |
| ret |= lc824206xa_write_reg(data, REG_INTR_CLEAR, INTR_ALL); |
| ret |= lc824206xa_write_reg(data, REG_INTR_CLEAR, 0); |
| ret |= lc824206xa_write_reg(data, REG_INTR_MASK, INTR_MASK); |
| ret |= lc824206xa_write_reg(data, REG_ID_PIN_ADC_CTRL, ID_PIN_ADC_AUTO); |
| ret |= lc824206xa_write_reg(data, REG_CHARGER_DET, CHARGER_DET_ON); |
| if (ret) |
| return -EIO; |
| |
| /* Initialize extcon device */ |
| data->edev = devm_extcon_dev_allocate(dev, lc824206xa_cables); |
| if (IS_ERR(data->edev)) |
| return PTR_ERR(data->edev); |
| |
| ret = devm_extcon_dev_register(dev, data->edev); |
| if (ret) |
| return dev_err_probe(dev, ret, "registering extcon device\n"); |
| |
| psy_cfg.drv_data = data; |
| data->psy = devm_power_supply_register(dev, &lc824206xa_psy_desc, &psy_cfg); |
| if (IS_ERR(data->psy)) |
| return dev_err_probe(dev, PTR_ERR(data->psy), "registering power supply\n"); |
| |
| ret = devm_request_threaded_irq(dev, client->irq, NULL, lc824206xa_irq, |
| IRQF_TRIGGER_LOW | IRQF_ONESHOT, |
| KBUILD_MODNAME, data); |
| if (ret) |
| return dev_err_probe(dev, ret, "requesting IRQ\n"); |
| |
| /* Sync initial state */ |
| schedule_work(&data->work); |
| return 0; |
| } |
| |
| static const struct i2c_device_id lc824206xa_i2c_ids[] = { |
| { "lc824206xa" }, |
| { } |
| }; |
| MODULE_DEVICE_TABLE(i2c, lc824206xa_i2c_ids); |
| |
| static struct i2c_driver lc824206xa_driver = { |
| .driver = { |
| .name = KBUILD_MODNAME, |
| }, |
| .probe = lc824206xa_probe, |
| .id_table = lc824206xa_i2c_ids, |
| }; |
| |
| module_i2c_driver(lc824206xa_driver); |
| |
| MODULE_AUTHOR("Hans de Goede <hansg@kernel.org>"); |
| MODULE_DESCRIPTION("LC824206XA Micro USB Switch driver"); |
| MODULE_LICENSE("GPL"); |