// 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 0 x00
#define REG00_INIT_VALUE 0 x01
#define REG_STATUS 0 x01
#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 0 x80
#define STATUS_USB_ID_ACA 0 xf0
#define STATUS_USB_ID_FLOAT 0 xf8
/*
* This controls the DP / DM muxes + other switches ,
* meaning of individual bits is unknown .
*/
#define REG_SWITCH_CONTROL 0 x02
#define SWITCH_STEREO_MIC 0 xc8
#define SWITCH_USB_HOST 0 xec
#define SWITCH_DISCONNECTED 0 xf8
#define SWITCH_USB_DEVICE 0 xfc
/* 5 bits? ADC 0x10 GND, 0x1a-0x1f ACA, 0x1f float */
#define REG_ID_PIN_ADC_VALUE 0 x03
/* 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 0 x04
/*
* 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 0 x05
/* Interrupts which bit is set to 1 here will not raise the HW IRQ */
#define REG_INTR_MASK 0 x06
/* ID pin ADC control, meaning of individual bits is unknown */
#define REG_ID_PIN_ADC_CTRL 0 x07
#define ID_PIN_ADC_AUTO 0 x40
#define ID_PIN_ADC_CONTINUOUS 0 x44
#define REG_CHARGER_DET 0 x08
#define CHARGER_DET_ON BIT(0 )
#define CHARGER_DET_CDP_ON BIT(1 )
#define CHARGER_DET_CDP_VAL BIT(2 )
#define REG_CHARGER_TYPE 0 x09
#define CHARGER_TYPE_UNKNOWN 0 x00
#define CHARGER_TYPE_DCP 0 x01
#define CHARGER_TYPE_SDP_OR_CDP 0 x04
#define CHARGER_TYPE_QC 0 x06
#define REG10 0 x10
#define REG10_INIT_VALUE 0 x00
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 0 x10:
status = STATUS_USB_ID_GND;
break ;
case 0 x18 ... 0 x1e:
status = STATUS_USB_ID_ACA;
break ;
case 0 x1f:
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" );
Messung V0.5 in Prozent C=95 H=100 G=97
¤ Dauer der Verarbeitung: 0.12 Sekunden
(vorverarbeitet am 2026-09-29)
¤
*© Formatika GbR, Deutschland