blob: ecae7165591429b04ad8004531ceb80d69973885 [file]
// SPDX-License-Identifier: GPL-2.0
/*
* Copyright (C) 2020 MediaTek Inc.
*/
#include <linux/extcon-provider.h>
#include <linux/gpio.h>
#include <linux/gpio/consumer.h>
#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/of_gpio.h>
#include <linux/platform_device.h>
#include <linux/slab.h>
#include <linux/workqueue.h>
#include <linux/pinctrl/consumer.h>
#include <linux/debugfs.h>
#include <linux/seq_file.h>
#include <linux/uaccess.h>
#include "extcon-kirkwood-usb.h"
#define USB_GPIO_DEBOUNCE_MS 20 /* ms */
static const unsigned int usb_cables[] = {
EXTCON_USB,
EXTCON_USB_HOST,
EXTCON_CHG_USB_SDP,
EXTCON_CHG_USB_DCP,
EXTCON_CHG_USB_CDP,
/* must end with EXTCON_NONE */
EXTCON_NONE,
};
#define SET_EXTCON_STATE(info, cable, val) {\
(info)->usb_cable = (((cable) & 0x03) << 16) | ((val) & 0x01);\
}
#define GET_EXTCON_CABLE(info) ((info)->usb_cable >> 16)
#define GET_EXTCON_STATUS(info) ((info)->usb_cable & 0x01)
#define SET_EXTCON_BC12_STATE(info, cable, val) {\
(info)->bc12_cable = (((cable) & 0x0F) << 16) | ((val) & 0x01);\
}
#define GET_EXTCON_BC12_CABLE(info) ((info)->bc12_cable >> 16)
#define GET_EXTCON_BC12_STATUS(info) ((info)->bc12_cable & 0x01)
static enum power_supply_property kirkwood_psy_props[] = {
POWER_SUPPLY_PROP_USB_TYPE,
POWER_SUPPLY_PROP_VOLTAGE_MAX,
POWER_SUPPLY_PROP_CURRENT_MAX,
};
static enum power_supply_usb_type kirkwood_psy_usb_types[] = {
POWER_SUPPLY_USB_TYPE_UNKNOWN,
POWER_SUPPLY_USB_TYPE_SDP,
POWER_SUPPLY_USB_TYPE_DCP,
POWER_SUPPLY_USB_TYPE_CDP,
};
static enum power_supply_usb_type kirkwood_bc12_role_to_psy_usb_type(int bc12_role) {
switch (bc12_role) {
case BC12_DETECTED_DCP:
return POWER_SUPPLY_USB_TYPE_DCP;
case BC12_DETECTED_CDP:
case BC12_DETECTED_CDP_B:
return POWER_SUPPLY_USB_TYPE_CDP;
case BC12_DETECTED_SDP:
case BC12_DETECTED_SDP_B:
return POWER_SUPPLY_USB_TYPE_SDP;
default:
return POWER_SUPPLY_USB_TYPE_UNKNOWN;
}
}
static int kirkwood_psy_get_prop(struct power_supply *psy,
enum power_supply_property psp,
union power_supply_propval *val)
{
struct extcon_kirkwood *info = power_supply_get_drvdata(psy);
int ret = 0;
switch (psp) {
case POWER_SUPPLY_PROP_USB_TYPE:
val->intval = info->bc12_usb_type;
break;
case POWER_SUPPLY_PROP_VOLTAGE_MAX:
val->intval = 5000000;
break;
case POWER_SUPPLY_PROP_CURRENT_MAX:
mutex_lock(&info->ekw_mutex);
if (info->bc12_usb_type == POWER_SUPPLY_USB_TYPE_DCP ||
info->bc12_usb_type == POWER_SUPPLY_USB_TYPE_CDP) {
val->intval = 1500000;
} else {
val->intval = 500000;
}
mutex_unlock(&info->ekw_mutex);
break;
default:
ret = -EINVAL;
break;
}
return ret;
}
static int kirkwood_psy_prop_writeable(struct power_supply *psy,
enum power_supply_property psp)
{
return 0;
}
static const char *kirkwood_psy_name_prefix = "extcon-kirkwood-bc12-psy-";
static int devm_kirkwood_psy_register(struct extcon_kirkwood *info)
{
struct power_supply_config psy_cfg = {};
const char *info_dev_name = dev_name(info->dev);
size_t psy_name_len = strlen(kirkwood_psy_name_prefix) +
strlen(info_dev_name) + 1;
char *psy_name;
psy_cfg.drv_data = info;
psy_cfg.fwnode = dev_fwnode(info->dev);
psy_name = devm_kzalloc(info->dev, psy_name_len, GFP_KERNEL);
if (!psy_name)
return -ENOMEM;
snprintf(psy_name, psy_name_len, "%s%s", kirkwood_psy_name_prefix,
info_dev_name);
info->psy_desc.name = psy_name;
info->psy_desc.type = POWER_SUPPLY_TYPE_USB;
info->psy_desc.usb_types = kirkwood_psy_usb_types;
info->psy_desc.num_usb_types = ARRAY_SIZE(kirkwood_psy_usb_types);
info->psy_desc.properties = kirkwood_psy_props;
info->psy_desc.num_properties = ARRAY_SIZE(kirkwood_psy_props);
info->psy_desc.get_property = kirkwood_psy_get_prop;
info->psy_desc.set_property = NULL;
info->psy_desc.property_is_writeable = kirkwood_psy_prop_writeable;
info->bc12_usb_type = POWER_SUPPLY_USB_TYPE_UNKNOWN;
info->psy = devm_power_supply_register(info->dev, &info->psy_desc,
&psy_cfg);
return PTR_ERR_OR_ZERO(info->psy);
}
static void kirkwood_switch_role(struct extcon_kirkwood *info, enum usb_role role)
{
struct extcon_dev *edev = info->edev;
switch (role) {
case USB_ROLE_HOST:
if (info->bc12_role == BC12_DETECTED_CDP ||
info->bc12_role == BC12_DETECTED_SDP) {
SET_EXTCON_STATE(info, EXTCON_USB_HOST, true);
extcon_set_state_sync(edev, EXTCON_USB_HOST, true);
} else {
dev_info(info->dev,
"BC12 is disconnections since %i\n",
info->bc12_role);
}
break;
case USB_ROLE_DEVICE:
if (info->bc12_role == BC12_DETECTED_CDP ||
info->bc12_role == BC12_DETECTED_SDP) {
SET_EXTCON_STATE(info, EXTCON_USB_HOST, false);
extcon_set_state_sync(edev, EXTCON_USB_HOST, false);
SET_EXTCON_STATE(info, EXTCON_USB, true);
extcon_set_state_sync(edev, EXTCON_USB, true);
} else {
dev_info(info->dev,
"BC12 is disconnections since %i\n",
info->bc12_role);
}
break;
case USB_ROLE_NONE:
default:
SET_EXTCON_STATE(info, EXTCON_USB_HOST, false);
extcon_set_state_sync(edev, EXTCON_USB_HOST, false);
SET_EXTCON_STATE(info, EXTCON_USB, false);
extcon_set_state_sync(edev, EXTCON_USB, false);
dev_info(info->dev,
"BC12 connections status: %i\n",
info->bc12_role);
break;
}
}
static int kirkwood_usb_set_role(struct usb_role_switch *sw,
enum usb_role role)
{
struct extcon_kirkwood *info = usb_role_switch_get_drvdata(sw);
if (info == NULL)
return -ENODEV;
mutex_lock(&info->ekw_mutex);
if (info->usb_hal_ver_en) {
if (info->usb_hal_ver != 0x13) {
dev_info(info->dev,
"ban is ignored since usb_hal_ver: 0x%02X\n",
info->usb_hal_ver);
goto skip_ban;
}
if (!info->forced_role_none) {
info->ban_usb_data_role = USB_ROLE_NONE;
info->usb_data_role = role;
kirkwood_switch_role(info, role);
} else {
/* forced to NONE from sysfs, can't be changed from role_switch
* interface
*/
info->ban_usb_data_role = role;
dev_info(info->dev,
"%s is banned since usb role NONE\n",
role == USB_ROLE_HOST ? "HOST" :
role == USB_ROLE_DEVICE ? "DEVICE" :
"NONE");
}
} else {
skip_ban:
info->usb_data_role = role;
kirkwood_switch_role(info, role);
}
mutex_unlock(&info->ekw_mutex);
return 0;
}
static enum usb_role kirkwood_usb_get_role(struct usb_role_switch *sw)
{
struct extcon_kirkwood *info = usb_role_switch_get_drvdata(sw);
enum usb_role role;
if (info == NULL)
return USB_ROLE_NONE;
mutex_lock(&info->ekw_mutex);
role = info->usb_data_role;
mutex_unlock(&info->ekw_mutex);
return role;
}
/*
*
* State | GOOD_BAT | SW_OPEN | CHG_AL_N|CHG_DET
* ----------------------------------------------------
* [1] DCP | X | Hi-Z | L | H
* [2] CDP | H | L | L | H
* [3] CDP!| L | Hi-Z | L | H
* [4] SDP | H | L | L | L
* [5] SDP!| L | Hi-Z | L | L
* [6] BRK | L | Hi-Z | Hi-Z | L
* [7] UKN | X | Hi-Z | Hi-Z | Hi-Z to Low
*
* more details, please watch FSA831AL10 datasheet.
*/
static void kirkwood_bc12_detect_cable(struct work_struct *work)
{
u32 good_bat;
u32 sw_open;
u32 chg_aln;
u32 chg_det;
struct extcon_dev *edev;
struct extcon_kirkwood *info = container_of(to_delayed_work(work),
struct extcon_kirkwood,
wq_detcable);
int old_bc12_role = info->bc12_role;
/*
* check good_bat, sw_open, chg_aln and chg_det
* and update cable state
*/
edev = info->edev;
good_bat = info->good_bat_gpiod ?
gpiod_get_value_cansleep(info->good_bat_gpiod) :
info->good_bat_polarity;
sw_open = gpiod_get_value_cansleep(info->sw_open_gpiod);
chg_aln = gpiod_get_value_cansleep(info->chg_aln_gpiod);
chg_det = gpiod_get_value_cansleep(info->chg_det_gpiod);
mutex_lock(&info->ekw_mutex);
/* at first we clean states which are no longer active */
if (chg_aln != info->chg_aln_polarity) {
if (sw_open != info->sw_open_polarity) {
if (chg_det != info->chg_det_polarity) {
if (good_bat == info->good_bat_polarity)
info->bc12_role = BC12_DETECTED_SDP;
/* low undefined */
} else {
if (good_bat == info->good_bat_polarity)
info->bc12_role = BC12_DETECTED_CDP;
/* low undefined */
}
} else {
if (chg_det != info->chg_det_polarity) {
if (good_bat != info->good_bat_polarity)
info->bc12_role = BC12_DETECTED_SDP_B;
/* high undefined */
} else {
info->bc12_role = BC12_DETECTED_DCP;
if (good_bat != info->good_bat_polarity)
info->bc12_role = BC12_DETECTED_CDP_B;
}
}
} else {
if (chg_det == info->chg_det_polarity) {
if (chg_det != info->chg_det_polarity) {
if (good_bat != info->good_bat_polarity)
info->bc12_role = BC12_DETECTED_BRK;
/* others undefined */
} else {
info->bc12_role = BC12_DETECTED_UNK;
}
} else {
info->bc12_role = BC12_DETECTED_NON;
}
}
switch (info->bc12_role) {
case BC12_DETECTED_SDP:
case BC12_DETECTED_SDP_B:
SET_EXTCON_BC12_STATE(info, EXTCON_CHG_USB_SDP, true);
extcon_set_state_sync(edev, EXTCON_CHG_USB_SDP, true);
extcon_set_state_sync(edev, EXTCON_CHG_USB_DCP, false);
extcon_set_state_sync(edev, EXTCON_CHG_USB_CDP, false);
break;
case BC12_DETECTED_DCP:
extcon_set_state_sync(edev, EXTCON_CHG_USB_SDP, false);
SET_EXTCON_BC12_STATE(info, EXTCON_CHG_USB_DCP, true);
extcon_set_state_sync(edev, EXTCON_CHG_USB_DCP, true);
extcon_set_state_sync(edev, EXTCON_CHG_USB_CDP, false);
break;
case BC12_DETECTED_CDP:
case BC12_DETECTED_CDP_B:
extcon_set_state_sync(edev, EXTCON_CHG_USB_SDP, false);
extcon_set_state_sync(edev, EXTCON_CHG_USB_DCP, false);
SET_EXTCON_BC12_STATE(info, EXTCON_CHG_USB_CDP, true);
extcon_set_state_sync(edev, EXTCON_CHG_USB_CDP, true);
break;
default:
SET_EXTCON_BC12_STATE(info, EXTCON_NONE, false);
extcon_set_state_sync(edev, EXTCON_CHG_USB_SDP, false);
extcon_set_state_sync(edev, EXTCON_CHG_USB_DCP, false);
extcon_set_state_sync(edev, EXTCON_CHG_USB_CDP, false);
break;
}
if (!(info->bc12_role == BC12_DETECTED_CDP ||
info->bc12_role == BC12_DETECTED_SDP)) {
info->usb_data_role = USB_ROLE_NONE;
kirkwood_switch_role(info, USB_ROLE_NONE);
}
if (info->bc12_role != old_bc12_role) {
info->bc12_usb_type = kirkwood_bc12_role_to_psy_usb_type(info->bc12_role);
power_supply_changed(info->psy);
}
mutex_unlock(&info->ekw_mutex);
}
static irqreturn_t kirkwood_bc12_irq_handler(int irq, void *dev_id)
{
struct extcon_kirkwood *info = dev_id;
queue_delayed_work(system_power_efficient_wq, &info->wq_detcable,
info->debounce_jiffies);
return IRQ_HANDLED;
}
#if IS_ENABLED(CONFIG_DEBUG_FS)
static int kirkwood_show(struct seq_file *s, void *v)
{
struct extcon_kirkwood *info = s->private;
unsigned int good_bat = 0;
unsigned int sw_open = 0;
unsigned int chg_aln = 0;
unsigned int chg_det = 0;
unsigned int cable;
unsigned int value;
char * const szusbcable[] = {
"EXTCON_NONE",
"EXTCON_USB",
"EXTCON_USB_HOST"
};
char * const szusbval[] = {
"DETACH",
"ATTACH"
};
char * const szbc12cable[] = {
"BC12_NON",
"BC12_DCP",
"BC12_CDP",
"BC12_CDP_B",
"BC12_SDP",
"BC12_SDP_B",
"BC12_BRK",
"BC12_UNK"
};
char * const szusbrole[] = {
"TYPEC_NONE",
"TYPEC_HOST",
"TYPEC_DEVICE"
};
seq_puts(s,
"\n======Mediatek Extcon kirkwood Debug Information======\n");
seq_puts(s,
" | BC12 | State | DP_HOST | DM_HOST | DP_CON | DM_CON\n");
seq_puts(s,
"--+-------+-------+---------+---------+---------+-------\n");
seq_puts(s,
" B| DCP | | Hi-Z | Hi-Z | Vdp_src | Hi-Z\n");
seq_puts(s,
" C| CDP | USB | DP_CON | DM_CON | DP_HOST | DM_HOST\n");
seq_puts(s,
" 1| CDP_B | | Hi-Z | Hi-Z | Vdp_src | Hi-Z\n");
seq_puts(s,
" 2| SDP | USB | DP_CON | DM_CON | DP_HOST | DM_HOST\n");
seq_puts(s,
" 2| SDP_B | | Hi-Z | Hi-Z | Vdp_src | Hi-Z\n");
seq_puts(s,
" 2| BRK | | Hi-Z | Hi-Z | Hi-Z | Hi-Z\n");
seq_puts(s,
" 2| UNK | | Hi-Z | Hi-Z | Hi-Z | Hi-Z\n");
seq_puts(s,
"--+-------+-------+---------+---------+---------+--------\n");
mutex_lock(&info->ekw_mutex);
/* parameters */
if (!info->good_bat_fixed && info->good_bat_gpiod) {
seq_printf(s, "[CFG] good_bat with GPIO%i property\n",
info->good_bat_gpio_dts);
seq_printf(s, " good_bat_gpiod : %p\n",
info->good_bat_gpiod);
seq_printf(s, " good_bat_irq : %i\n",
info->good_bat_irq);
seq_printf(s, " good_bat_polarity : %i\n",
info->good_bat_polarity);
} else {
seq_printf(s, " good_bat is fixed without battery: %i\n",
info->good_bat_polarity);
}
if (info->sw_open_gpiod) {
seq_printf(s, "[CFG] sw_open with GPIO%i property\n",
info->sw_open_gpio_dts);
seq_printf(s, " sw_open_gpiod : %p\n",
info->sw_open_gpiod);
seq_printf(s, " sw_open_irq : %i\n",
info->sw_open_irq);
seq_printf(s, " sw_open_polarity : %i\n",
info->sw_open_polarity);
}
if (info->chg_aln_gpiod) {
seq_printf(s, "[CFG] chg_aln with GPIO%i property\n",
info->chg_aln_gpio_dts);
seq_printf(s, " chg_aln_gpiod : %p\n",
info->chg_aln_gpiod);
seq_printf(s, " chg_aln_irq : %i\n",
info->chg_aln_irq);
seq_printf(s, " chg_aln_polarity : %i\n",
info->chg_aln_polarity);
}
if (info->chg_det_gpiod) {
seq_printf(s, "[CFG] chg_aln with GPIO%i property\n",
info->chg_det_gpio_dts);
seq_printf(s, " chg_det_gpiod : %p\n",
info->chg_det_gpiod);
seq_printf(s, " chg_det_irq : %i\n",
info->chg_det_irq);
seq_printf(s, " chg_det_polarity : %i\n",
info->chg_det_polarity);
}
if (info->debounce_jiffies)
seq_printf(s, "[CFG][SW]debounce_jiffies: %u ms)\n",
jiffies_to_msecs(info->debounce_jiffies));
else
seq_printf(s, "[CFG][HW]debounce_jiffies: %u ms)\n",
USB_GPIO_DEBOUNCE_MS);
if (!info->good_bat_fixed && info->good_bat_gpiod) {
good_bat = gpiod_get_value_cansleep(info->good_bat_gpiod);
seq_printf(s, "gpio%u good_bat pin : %i\n",
info->good_bat_gpio_dts, good_bat);
}
if (info->sw_open_gpiod) {
sw_open = gpiod_get_value_cansleep(info->sw_open_gpiod);
seq_printf(s, "gpio%u sw_open pin : %i\n",
info->sw_open_gpio_dts, sw_open);
}
if (info->chg_aln_gpiod) {
chg_aln = gpiod_get_value_cansleep(info->chg_aln_gpiod);
seq_printf(s, "gpio%u chg_aln pin : %i\n",
info->chg_aln_gpio_dts, chg_aln);
}
if (info->chg_det_gpiod) {
chg_det = gpiod_get_value_cansleep(info->chg_det_gpiod);
seq_printf(s, "gpio%u chg_det pin : %i\n",
info->chg_det_gpio_dts, chg_det);
}
if (!info->good_bat_fixed && info->good_bat_gpiod)
good_bat = gpiod_get_value_cansleep(info->good_bat_gpiod);
else
good_bat = info->good_bat_polarity;
seq_printf(s, "bc12_cable : 0x%08X\n", info->bc12_cable);
seq_printf(s, "usb_cable : 0x%08X\n", info->usb_cable);
seq_printf(s, "usb_data_role : %i\n", info->usb_data_role);
seq_printf(s, "port_data_role : %i\n", info->port_data_role);
seq_printf(s, "usb_hal_ver_en : %i\n", info->usb_hal_ver_en);
seq_printf(s, "usb_hal_ver : 0x%02X\n", info->usb_hal_ver);
seq_printf(s, "ban_usb_data_role : %i\n", info->ban_usb_data_role);
seq_printf(s, "forced_role_none : %i\n", info->forced_role_none);
/* current result */
seq_printf(s, "[FINAL ] FSA831A chg_aln: %s, sw_open: %s,",
(chg_aln == info->chg_aln_polarity) ? "H" : "L",
(sw_open == info->sw_open_polarity) ? "H" : "L");
seq_printf(s, " chg_det: %s, good_bat: %s\n",
(chg_det == info->chg_det_polarity) ? "H" : "L",
(good_bat == info->good_bat_polarity) ? "H" : "L");
cable = GET_EXTCON_BC12_CABLE(info);
value = GET_EXTCON_BC12_STATUS(info);
seq_printf(s, "[FINAL ] BC12: %s : %s (%i)\n",
(cable == EXTCON_CHG_USB_SDP) ? "SDP" :
(cable == EXTCON_CHG_USB_DCP) ? "DCP" :
(cable == EXTCON_CHG_USB_CDP) ? "CDP" : "UNDEFINED",
szbc12cable[info->bc12_role], info->bc12_role);
if (info->usb_hal_ver_en) {
seq_printf(s,
"[FINAL ] Type-C: usb hal version: 0x%02X\n",
info->usb_hal_ver);
seq_printf(s,
"[FINAL ] Type-C: usb role: %s\n",
szusbrole[info->usb_data_role]);
seq_printf(s,
"[FINAL ] Type-C: ban usb role: %s\n",
szusbrole[info->ban_usb_data_role]);
} else {
seq_printf(s,
"[FINAL ] Type-C: usb role: %s\n",
szusbrole[info->usb_data_role]);
}
seq_printf(s,
"[FINAL ] Type-C: data role: %s\n",
info->port_data_role == TYPEC_PORT_UFP ? "UFP" :
info->port_data_role == TYPEC_PORT_DFP ? "DFP" : "DRD");
seq_printf(s, "[CURRENT] %s Port: %s(%i), %s(%i)\n",
(GET_EXTCON_CABLE(info) == EXTCON_USB) ?
(GET_EXTCON_STATUS(info) ? "USB" : "USB/NONE")
: (GET_EXTCON_CABLE(info) == EXTCON_USB_HOST) ? "USB-HOST"
: "UNKNOWN",
szusbcable[GET_EXTCON_CABLE(info)],
GET_EXTCON_CABLE(info),
szusbval[GET_EXTCON_STATUS(info)],
GET_EXTCON_STATUS(info));
mutex_unlock(&info->ekw_mutex);
return 0;
}
static int kirkwood_open(struct inode *inode, struct file *file)
{
return single_open(file, kirkwood_show, inode->i_private);
}
static const struct file_operations kirkwood_proc_fops = {
.open = kirkwood_open,
.read = seq_read,
.llseek = seq_lseek,
.release = single_release,
};
static int kirkwood_role_show(struct seq_file *sf, void *unused)
{
struct extcon_kirkwood *info = sf->private;
mutex_lock(&info->ekw_mutex);
seq_printf(sf, "current ssusb: %s, typec role: %s, data role: %s\n",
(GET_EXTCON_CABLE(info) == EXTCON_USB) ?
(GET_EXTCON_STATUS(info) ? "device" : "none")
: "host",
(info->usb_data_role == USB_ROLE_HOST) ? "host" :
(info->usb_data_role == USB_ROLE_DEVICE) ? "device" :
"none",
info->port_data_role == TYPEC_PORT_DFP ? "dfp" :
info->port_data_role == TYPEC_PORT_UFP ? "ufp" : "drd");
seq_puts(sf, "--online help:\n");
seq_puts(sf, " echo -n <device/host/none,ufp/dfp/drd> > role\n");
seq_puts(sf, " e.x 1: echo -n device > role\n");
seq_puts(sf, " e.x 2: echo -n ufp > role\n");
mutex_unlock(&info->ekw_mutex);
return 0;
}
static int kirkwood_role_open(struct inode *inode, struct file *file)
{
return single_open(file, kirkwood_role_show, inode->i_private);
}
static ssize_t kirkwood_role_write(struct file *file, const char __user *ubuf,
size_t count, loff_t *ppos)
{
struct seq_file *sf = file->private_data;
struct extcon_kirkwood *info = sf->private;
char buf[16] = { 0 };
if (copy_from_user(&buf, ubuf, min_t(size_t, sizeof(buf) - 1, count)))
return -EFAULT;
mutex_lock(&info->ekw_mutex);
if (!strncmp(buf, "host", 4)) {
if (info->usb_data_role != USB_ROLE_HOST) {
info->usb_data_role = USB_ROLE_HOST;
kirkwood_switch_role(info, USB_ROLE_HOST);
info->forced_role_none = false;
} else {
dev_info(info->dev, "HOST role already.\n");
}
} else if (!strncmp(buf, "device", 6)) {
if (info->usb_data_role != USB_ROLE_DEVICE) {
info->usb_data_role = USB_ROLE_DEVICE;
kirkwood_switch_role(info, USB_ROLE_DEVICE);
info->forced_role_none = false;
} else {
dev_info(info->dev, "DEVICE role already.\n");
}
} else if (!strncmp(buf, "none", 4)) {
if (info->usb_data_role != USB_ROLE_NONE) {
info->usb_data_role = USB_ROLE_NONE;
kirkwood_switch_role(info, USB_ROLE_NONE);
info->forced_role_none = true;
} else {
dev_info(info->dev, "NONE role already.\n");
}
} else if (!strncmp(buf, "ufp", 3)) {
if (info->port_data_role != TYPEC_PORT_UFP) {
info->port_data_role = TYPEC_PORT_UFP;
if (info->usb_data_role != USB_ROLE_DEVICE) {
kirkwood_switch_role(info,
USB_ROLE_DEVICE);
info->forced_role_none = false;
}
} else {
dev_info(info->dev, "UFP role already.\n");
}
} else if (!strncmp(buf, "dfp", 3)) {
if (info->port_data_role != TYPEC_PORT_DFP) {
info->port_data_role = TYPEC_PORT_DFP;
if (info->usb_data_role != USB_ROLE_HOST) {
kirkwood_switch_role(info,
USB_ROLE_HOST);
info->forced_role_none = false;
}
} else {
dev_info(info->dev, "DFP role already.\n");
}
} else if (!strncmp(buf, "drd", 3)) {
if (info->port_data_role != TYPEC_PORT_DRD) {
info->port_data_role = TYPEC_PORT_DRD;
if (info->usb_data_role != USB_ROLE_NONE) {
kirkwood_switch_role(info,
USB_ROLE_NONE);
info->forced_role_none = true;
}
} else {
dev_info(info->dev, "DRD role already.\n");
}
} else {
dev_info(info->dev, "Wrong parameter: (%s)\n", buf);
mutex_unlock(&info->ekw_mutex);
return -EINVAL;
}
mutex_unlock(&info->ekw_mutex);
return count;
}
static const struct file_operations kirkwood_role_fops = {
.open = kirkwood_role_open,
.write = kirkwood_role_write,
.read = seq_read,
.llseek = seq_lseek,
.release = single_release,
};
int kirkwood_init_debugfs(struct extcon_kirkwood *info)
{
int ret;
struct dentry *file;
struct dentry *root;
root = debugfs_create_dir(dev_driver_string(info->dev), NULL);
if (!root) {
ret = -ENOMEM;
goto err0;
}
file = debugfs_create_file("kirkwood", 0444, root,
info, &kirkwood_proc_fops);
if (!file) {
ret = -ENOMEM;
goto err1;
}
file = debugfs_create_file("role", 0644, root,
info, &kirkwood_role_fops);
if (!file) {
ret = -ENOMEM;
goto err1;
}
info->debugfs_root = root;
return 0;
err1:
debugfs_remove_recursive(root);
err0:
return ret;
}
void kirkwood_exit_debugfs(struct extcon_kirkwood *info)
{
debugfs_remove_recursive(info->debugfs_root);
}
#else
#define kirkwood_init_debugfs(info)
#define kirkwood_exit_debugfs(info)
#endif
static ssize_t role_show(struct device *dev,
struct device_attribute *attr,
char *buf)
{
ssize_t ret;
struct extcon_kirkwood *info = dev_get_drvdata(dev);
mutex_lock(&info->ekw_mutex);
ret = sprintf(buf, "%s\n",
(GET_EXTCON_CABLE(info) == EXTCON_USB) ?
(GET_EXTCON_STATUS(info) ? "device" : "none")
: "host");
mutex_unlock(&info->ekw_mutex);
return ret;
}
static ssize_t role_store(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t count)
{
struct extcon_kirkwood *info = dev_get_drvdata(dev);
mutex_lock(&info->ekw_mutex);
if (!strncmp(buf, "host", 4)) {
if (info->usb_data_role != USB_ROLE_HOST) {
info->usb_data_role = USB_ROLE_HOST;
kirkwood_switch_role(info, USB_ROLE_HOST);
info->forced_role_none = false;
} else {
dev_info(info->dev, "HOST role already.\n");
}
} else if (!strncmp(buf, "device", 6)) {
if (info->usb_data_role != USB_ROLE_DEVICE) {
info->usb_data_role = USB_ROLE_DEVICE;
kirkwood_switch_role(info, USB_ROLE_DEVICE);
info->forced_role_none = false;
} else {
dev_info(info->dev, "DEVICE role already.\n");
}
} else if (!strncmp(buf, "none", 4)) {
if (info->usb_data_role != USB_ROLE_NONE) {
info->usb_data_role = USB_ROLE_NONE;
kirkwood_switch_role(info, USB_ROLE_NONE);
/* forced NONE from sysfs */
info->forced_role_none = true;
} else {
dev_info(info->dev, "NONE role already.\n");
}
} else {
dev_info(info->dev, "Wrong parameter: (%s)\n", buf);
return -EINVAL;
}
mutex_unlock(&info->ekw_mutex);
return count;
}
static DEVICE_ATTR_RW(role);
static struct attribute *usb_role_attrs[] = {
&dev_attr_role.attr,
NULL
};
static const struct attribute_group kirkwood_attr_group = {
.attrs = usb_role_attrs,
};
static int kirkwood_extcon_probe(struct platform_device *pdev)
{
struct usb_role_switch_desc sw_desc = { 0 };
struct device *dev = &pdev->dev;
struct device_node *np = dev->of_node;
struct extcon_kirkwood *info;
unsigned int val;
int ret;
if (!np)
return -EINVAL;
info = devm_kzalloc(&pdev->dev, sizeof(*info), GFP_KERNEL);
if (!info)
return -ENOMEM;
ret = sysfs_create_group(&dev->kobj, &kirkwood_attr_group);
if (ret)
return ret;
mutex_init(&info->ekw_mutex);
platform_set_drvdata(pdev, info);
info->dev = dev;
info->usb_data_role = USB_ROLE_NONE;
info->port_data_role = TYPEC_PORT_DRD;
info->ban_usb_data_role = USB_ROLE_NONE;
info->usb_hal_ver_en =
device_property_read_bool(dev, "android,usb-hal-ver");
if (info->usb_hal_ver_en) {
ret = device_property_read_u32(dev,
"android,usb-hal-ver",
&info->usb_hal_ver);
if (ret)
return ret;
}
info->forced_role_none = false;
dev_info(info->dev,
"usb role ban algorithm: %sable and usb_hal_ver: 0x%02X\n",
info->usb_hal_ver_en ? " En" : "Dis",
info->usb_hal_ver);
info->edev = devm_extcon_dev_allocate(dev, usb_cables);
if (IS_ERR(info->edev)) {
dev_info(dev, "failed to allocate extcon device\n");
return -ENOMEM;
}
ret = devm_extcon_dev_register(dev, info->edev);
if (ret < 0) {
dev_info(dev, "failed to register extcon device\n");
return ret;
}
info->good_bat_polarity = 1;
info->sw_open_polarity = 1;
info->chg_aln_polarity = 1;
info->chg_det_polarity = 1;
info->good_bat_fixed =
device_property_read_bool(dev, "fsa831a,good-bat-fixed");
info->good_bat_gpiod =
devm_gpiod_get_optional(&pdev->dev, "good-bat", GPIOD_IN);
info->sw_open_gpiod =
devm_gpiod_get_optional(&pdev->dev, "sw-open", GPIOD_IN);
info->chg_aln_gpiod =
devm_gpiod_get_optional(&pdev->dev, "chg-aln", GPIOD_IN);
info->chg_det_gpiod =
devm_gpiod_get_optional(&pdev->dev, "chg-det", GPIOD_IN);
if (!info->good_bat_fixed && info->good_bat_gpiod) {
ret = of_property_read_u32(np, "goodbat_gpio_pin", &val);
if (ret == 0) {
info->good_bat_gpio_dts = val;
dev_info(dev,
"goodbat_gpio_pin gpio pin: %u\n", val);
}
ret = of_property_read_u32(np, "goodbat_polarity", &val);
if (ret == 0) {
info->good_bat_polarity = val ? 1 : 0;
dev_info(dev,
"goodbat_polarity gpio pin active: %u\n", val);
}
info->good_bat_irq = gpiod_to_irq(info->good_bat_gpiod);
if (info->good_bat_irq < 0) {
dev_info(dev, "failed to get goodbat IRQ\n");
return info->good_bat_irq;
}
}
if (info->sw_open_gpiod) {
ret = of_property_read_u32(np, "swopen-gpio-pin", &val);
if (ret == 0) {
info->sw_open_gpio_dts = val;
dev_info(dev, "swopen-gpio-pin pin: %u\n", val);
}
ret = of_property_read_u32(np, "swopen-polarity", &val);
if (ret == 0) {
info->sw_open_polarity = val ? 1 : 0;
dev_info(dev,
"swopen-polarity gpio pin active: %u\n", val);
}
info->sw_open_irq = gpiod_to_irq(info->sw_open_gpiod);
if (info->sw_open_irq < 0) {
dev_info(dev, "failed to get swopen IRQ\n");
return info->sw_open_irq;
}
}
if (info->chg_aln_gpiod) {
ret = of_property_read_u32(np, "chgaln-gpio-pin", &val);
if (ret == 0) {
info->chg_aln_gpio_dts = val;
dev_info(dev, "chgaln-gpio-pin gpio pin: %u\n", val);
}
ret = of_property_read_u32(np, "chgaln-polarity", &val);
if (ret == 0) {
info->chg_aln_polarity = val ? 1 : 0;
dev_info(dev,
"chgaln-polarity gpio pin active: %u\n", val);
}
info->chg_aln_irq = gpiod_to_irq(info->chg_aln_gpiod);
if (info->chg_aln_irq < 0) {
dev_info(dev, "failed to get chgaln IRQ\n");
return info->chg_aln_irq;
}
}
if (info->chg_det_gpiod) {
ret = of_property_read_u32(np, "chgdet-gpio-pin", &val);
if (ret == 0) {
info->chg_det_gpio_dts = val;
dev_info(dev, "chgdet-gpio-pin pin: %u!\n", val);
}
ret = of_property_read_u32(np, "chgdet-polarity", &val);
if (ret == 0) {
info->chg_det_polarity = val ? 1 : 0;
dev_info(dev,
"chgdet-polarity gpio pin active: %u\n", val);
}
info->chg_det_irq = gpiod_to_irq(info->chg_det_gpiod);
if (info->sw_open_irq < 0) {
dev_info(dev, "failed to get chgdet IRQ\n");
return info->sw_open_irq;
}
}
if (info->good_bat_fixed) {
if (!info->sw_open_gpiod &&
!info->chg_aln_gpiod &&
!info->chg_det_gpiod) {
dev_info(dev, "failed to get gpios\n");
return -ENODEV;
}
} else {
if (!info->good_bat_gpiod &&
!info->sw_open_gpiod &&
!info->chg_aln_gpiod &&
!info->chg_det_gpiod) {
dev_info(dev, "failed to get gpios\n");
return -ENODEV;
}
}
if (!info->good_bat_fixed && IS_ERR(info->good_bat_gpiod))
return PTR_ERR(info->good_bat_gpiod);
if (IS_ERR(info->sw_open_gpiod))
return PTR_ERR(info->sw_open_gpiod);
if (IS_ERR(info->chg_aln_gpiod))
return PTR_ERR(info->chg_aln_gpiod);
if (IS_ERR(info->chg_det_gpiod))
return PTR_ERR(info->chg_det_gpiod);
if (!info->good_bat_fixed && info->good_bat_gpiod)
ret = gpiod_set_debounce(info->good_bat_gpiod,
USB_GPIO_DEBOUNCE_MS * 1000);
if (!ret && info->sw_open_gpiod)
ret = gpiod_set_debounce(info->sw_open_gpiod,
USB_GPIO_DEBOUNCE_MS * 1000);
if (!ret && info->chg_aln_gpiod)
ret = gpiod_set_debounce(info->chg_aln_gpiod,
USB_GPIO_DEBOUNCE_MS * 1000);
if (!ret && info->chg_det_gpiod)
ret = gpiod_set_debounce(info->chg_det_gpiod,
USB_GPIO_DEBOUNCE_MS * 1000);
if (ret < 0)
info->debounce_jiffies =
msecs_to_jiffies(USB_GPIO_DEBOUNCE_MS);
INIT_DELAYED_WORK(&info->wq_detcable, kirkwood_bc12_detect_cable);
if (!info->good_bat_fixed && info->good_bat_gpiod) {
ret = devm_request_threaded_irq(dev, info->good_bat_irq, NULL,
kirkwood_bc12_irq_handler,
IRQF_TRIGGER_RISING |
IRQF_TRIGGER_FALLING |
IRQF_ONESHOT,
pdev->name, info);
if (ret < 0) {
dev_info(dev,
"failed to request handler for goodbat IRQ\n");
return ret;
}
}
if (info->sw_open_gpiod) {
ret = devm_request_threaded_irq(dev, info->sw_open_irq, NULL,
kirkwood_bc12_irq_handler,
IRQF_TRIGGER_RISING |
IRQF_TRIGGER_FALLING |
IRQF_ONESHOT,
pdev->name, info);
if (ret < 0) {
dev_info(dev,
"failed to request handler for swopen IRQ\n");
return ret;
}
}
if (info->chg_aln_gpiod) {
ret = devm_request_threaded_irq(dev, info->chg_aln_irq, NULL,
kirkwood_bc12_irq_handler,
IRQF_TRIGGER_RISING |
IRQF_TRIGGER_FALLING |
IRQF_ONESHOT,
pdev->name, info);
if (ret < 0) {
dev_info(dev,
"failed to request handler for chgaln IRQ\n");
return ret;
}
}
if (info->chg_det_gpiod) {
ret = devm_request_threaded_irq(dev, info->chg_det_irq, NULL,
kirkwood_bc12_irq_handler,
IRQF_TRIGGER_RISING |
IRQF_TRIGGER_FALLING |
IRQF_ONESHOT,
pdev->name, info);
if (ret < 0) {
dev_info(dev,
"failed to request handler for chgdet IRQ\n");
return ret;
}
}
sw_desc.driver_data = info;
sw_desc.fwnode = dev_fwnode(dev);
sw_desc.allow_userspace_control = true,
sw_desc.set = kirkwood_usb_set_role,
sw_desc.get = kirkwood_usb_get_role,
info->usb_role_sw = usb_role_switch_register(dev, &sw_desc);
if (IS_ERR(info->usb_role_sw)) {
dev_info(dev, "failed to register role switch device\n");
return PTR_ERR(info->usb_role_sw);
}
ret = devm_kirkwood_psy_register(info);
if (ret < 0) {
dev_info(dev, "failed to register psy\n");
return ret;
}
/* Perform initial detection */
kirkwood_bc12_detect_cable(&info->wq_detcable.work);
device_set_wakeup_capable(&pdev->dev, true);
kirkwood_init_debugfs(info);
/* set to DEVICE by default as factory fixture doesn't connect
* CC1/CC2 for a valid TypeC connection and role config.
*/
mutex_lock(&info->ekw_mutex);
info->usb_data_role = USB_ROLE_DEVICE;
kirkwood_switch_role(info, USB_ROLE_DEVICE);
mutex_unlock(&info->ekw_mutex);
dev_info(dev, " successfully!\n");
return 0;
}
static int kirkwood_extcon_remove(struct platform_device *pdev)
{
struct extcon_kirkwood *info = platform_get_drvdata(pdev);
kirkwood_exit_debugfs(info);
cancel_delayed_work_sync(&info->wq_detcable);
device_init_wakeup(&pdev->dev, false);
mutex_destroy(&info->ekw_mutex);
return 0;
}
#if IS_ENABLED(CONFIG_PM_SLEEP)
static int kirkwood_extcon_suspend(struct device *dev)
{
struct extcon_kirkwood *info = dev_get_drvdata(dev);
int ret = 0;
if (device_may_wakeup(dev)) {
if (!info->good_bat_fixed && info->good_bat_gpiod) {
ret = enable_irq_wake(info->good_bat_irq);
if (ret)
return ret;
}
if (info->sw_open_gpiod) {
ret = enable_irq_wake(info->sw_open_irq);
if (ret) {
if (!info->good_bat_fixed &&
info->good_bat_gpiod)
disable_irq_wake(info->good_bat_irq);
return ret;
}
}
if (info->chg_aln_gpiod) {
ret = enable_irq_wake(info->chg_aln_irq);
if (ret) {
if (!info->good_bat_fixed &&
info->good_bat_gpiod)
disable_irq_wake(info->good_bat_irq);
if (info->sw_open_gpiod)
disable_irq_wake(info->sw_open_irq);
return ret;
}
}
if (info->chg_det_gpiod) {
ret = enable_irq_wake(info->chg_det_irq);
if (ret) {
if (!info->good_bat_fixed &&
info->good_bat_gpiod)
disable_irq_wake(info->good_bat_irq);
if (info->sw_open_gpiod)
disable_irq_wake(info->sw_open_irq);
if (info->chg_aln_gpiod)
disable_irq_wake(info->chg_aln_irq);
return ret;
}
}
}
/*
* We don't want to process any IRQs after this point
* as GPIOs used behind I2C subsystem might not be
* accessible until resume completes. So disable IRQ.
*/
if (!info->good_bat_fixed && info->good_bat_gpiod)
disable_irq(info->good_bat_irq);
if (info->sw_open_gpiod)
disable_irq(info->sw_open_irq);
if (info->chg_aln_gpiod)
disable_irq(info->chg_aln_irq);
if (info->chg_det_gpiod)
disable_irq(info->chg_det_irq);
if (!device_may_wakeup(dev))
pinctrl_pm_select_sleep_state(dev);
return ret;
}
static int kirkwood_extcon_resume(struct device *dev)
{
struct extcon_kirkwood *info = dev_get_drvdata(dev);
int ret = 0;
if (!device_may_wakeup(dev))
pinctrl_pm_select_default_state(dev);
if (device_may_wakeup(dev)) {
if (!info->good_bat_fixed && info->good_bat_gpiod) {
ret = disable_irq_wake(info->good_bat_irq);
if (ret)
return ret;
}
if (info->sw_open_gpiod) {
ret = disable_irq_wake(info->sw_open_irq);
if (ret) {
if (!info->good_bat_fixed &&
info->good_bat_gpiod)
enable_irq_wake(info->good_bat_irq);
return ret;
}
}
if (info->chg_aln_gpiod) {
ret = disable_irq_wake(info->chg_aln_irq);
if (ret) {
if (!info->good_bat_fixed &&
info->good_bat_gpiod)
enable_irq_wake(info->good_bat_irq);
if (info->sw_open_gpiod)
enable_irq_wake(info->sw_open_irq);
return ret;
}
}
if (info->chg_det_gpiod) {
ret = disable_irq_wake(info->chg_det_irq);
if (ret) {
if (!info->good_bat_fixed &&
info->good_bat_gpiod)
enable_irq_wake(info->good_bat_irq);
if (info->sw_open_gpiod)
enable_irq_wake(info->sw_open_irq);
if (info->chg_aln_gpiod)
enable_irq_wake(info->chg_aln_irq);
return ret;
}
}
}
if (!info->good_bat_fixed && info->good_bat_gpiod)
enable_irq(info->good_bat_irq);
if (info->sw_open_gpiod)
enable_irq(info->sw_open_irq);
if (info->chg_aln_gpiod)
enable_irq(info->chg_aln_irq);
if (info->chg_det_gpiod)
enable_irq(info->chg_det_irq);
queue_delayed_work(system_power_efficient_wq,
&info->wq_detcable, 0);
return ret;
}
#endif
static SIMPLE_DEV_PM_OPS(kirkwood_extcon_pm_ops,
kirkwood_extcon_suspend, kirkwood_extcon_resume);
static const struct of_device_id kirkwood_extcon_dt_match[] = {
{ .compatible = "linux,extcon-usb-kirkwood", },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, kirkwood_extcon_dt_match);
static const struct platform_device_id kirkwood_extcon_platform_ids[] = {
{ .name = "extcon-usb-kirkwood", },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(platform, kirkwood_extcon_platform_ids);
static struct platform_driver kirkwood_extcon_driver = {
.probe = kirkwood_extcon_probe,
.remove = kirkwood_extcon_remove,
.driver = {
.name = "extcon-usb-kirkwood",
.pm = &kirkwood_extcon_pm_ops,
.of_match_table = kirkwood_extcon_dt_match,
},
.id_table = kirkwood_extcon_platform_ids,
};
module_platform_driver(kirkwood_extcon_driver);
MODULE_AUTHOR("Zhanyong Wang <zhanyong.wang@mediatek.com>");
MODULE_DESCRIPTION("USB kirkwood extcon driver");
MODULE_LICENSE("GPL v2");