extcon驱动及其在USB驱动中的应用
一、简介
extcon 是External Connector的简称,用于抽象外部连接器,比如说Audio Jack、USB MicroB/TypeC接口等。它的原型是Android的switch-class驱动,经过修改后在kernel 3.4.0版本时被引入内核中。
高通(Qualcomm)平台的USB相关的EXTCON(External Connector)功能主要涉及到了对外部连接器状态的检测和响应机制,特别是当USB端口上有设备连接或断开时。EXTCON框架是Linux内核的一部分,用于统一管理所有类型的外部连接器,包括USB、耳机插孔、Docking接口等。在高通平台中,EXTCON常用于实现USB OTG(On-The-Go)模式的自动切换、USB设备接入的快速响应以及电源管理等功能。
二、功能介绍
extcon驱动的主要功能是识别外部连接器状态变化,并将状态变化通知到与外部连接器相关的其他驱动。

使用extcon驱动,有什么好处呢?之前的内核都没有extcon驱动,又是怎么处理这些外部连接器的?不妨以USB驱动为例,看看使用extcon驱动前后的变化。
USB常见的外部接口有TypeA/B/C三种,其中TypeA/B又有标准A/B、Mini A/B和Micro A/B三种,直接上图:

这三种不同的接口,TypeA/B只是物理信号上的连接,主控芯片内部没有针对TypeA/B的专用控制器,可通过VBUS和ID两个脚的状态来识别是否接入了USB主机或USB外设。接入主机前,VBUS脚上没有电压,接入主机后,主机端会在VBUS脚上提供5V电压;接入外设前,ID脚为高电平,接入外设后,ID脚被拉低。于是软件可以通过主动读取这两个脚的电平或者异步响应这两个脚的中断来获知状态的变化。

TypeC就有点特别,从TypeC规范可以看到,TypeC是有一个状态机的,从Unattached状态走到Attached Sink状态(做从设备)或者Attached Source状态(做主机),主控芯片内部是有相应的控制器的,控制器会通过寄存器汇报状态变化,并产生中断通知主控。TypeC控制器需要软件进行相应的编程来配置和使能它。

以上就是USB针对不同外部接口所面临的状况。在extcon驱动出现之前,同一份USB控制器驱动代码,比较常见的做法就是在设备树(dts)中指明是哪种接口,USB控制器驱动代码中会解析设备树中的定义,通过if...else...来走不同的代码逻辑。如果是MicroB接口,就注册VBUS和ID脚的中断、查询IO脚的电平状态;如果是TypeC接口,就注册TypeC的中断,查询TypeC的状态。假设后续又有新的接口出现,工作原理不同于已有的接口,那就又需要在USB控制器驱动中去增加相关代码。

在extcon驱动出现后,USB控制器驱动就能和外部接口驱动解耦。在USB控制器驱动看来,不管外部接口是什么,我只需知道外部接口状态的变化就好了,比如是否接入主机了、是否有设备接入了。使用extcon驱动提供的函数接口来注册notifier,当外部接口状态变化时,extcon驱动负责回调notifier,USB控制器驱动代码无需再针对不同的外部接口改来改去。不同的外部接口,都用extcon来抽象自己的行为。

三、高通QCM6490平台extcon通知机制实例解析
1、dts说明
qcom,pmic_glink {
compatible = "qcom,pmic-glink";
qcom,pmic-glink-channel = "PMIC_RTR_ADSP_APPS";
qcom,subsys-name = "adsp";
qcom,protection-domain = "tms/servreg", "msm/adsp/charger_pd";
battery_charger: qcom,battery_charger {
compatible = "qcom,battery-charger";
};
qcom,ucsi {
compatible = "qcom,ucsi-glink";
port {
usb_port0_connector: endpoint {
remote-endpoint = <&usb_port0>;
};
};
};
altmode: qcom,altmode {
compatible = "qcom,altmode-glink";
#altmode-cells = <1>;
};
};
//usb0的外部连接器为battery charger
&usb0 {
fibo,ss_switch_power = <&tlmm 106 GPIO_ACTIVE_HIGH>;
extcon = <&battery_charger>;
};
2、驱动的部分主要涉及两个部分
dwc3-msm.c和qti_battery_charger.c
(1)extcon设备的注册
- dwc3-msm.c中extcon注册的过程:
if (of_property_read_bool(node, "extcon") ) { //从dts中读取extcon,查看是否有extcon设备,如果有会进行注册extcon设备
ret = dwc3_msm_extcon_register(mdwc);
.............
}
- dwc3_msm_extcon_register注册设备:
static int dwc3_msm_extcon_register(struct dwc3_msm *mdwc)
{
struct device_node *node = mdwc->dev->of_node;
struct extcon_dev *edev;
int idx, extcon_cnt, ret = 0;
bool check_vbus_state, check_id_state, phandle_found = false;
extcon_cnt = of_count_phandle_with_args(node, "extcon", NULL);
if (extcon_cnt < 0) {
dev_err(mdwc->dev, "of_count_phandle_with_args failed\n");
return -ENODEV;
}
mdwc->extcon = devm_kcalloc(mdwc->dev, extcon_cnt,
sizeof(*mdwc->extcon), GFP_KERNEL);
if (!mdwc->extcon)
return -ENOMEM;
for (idx = 0; idx < extcon_cnt; idx++) {
edev = extcon_get_edev_by_phandle(mdwc->dev, idx); //读取具体的extcon设备,目前我们dts中的extcon设备为battery_charger
if (IS_ERR(edev) && PTR_ERR(edev) != -ENODEV)
return PTR_ERR(edev);
if (IS_ERR_OR_NULL(edev))
continue;
check_vbus_state = check_id_state = true;
phandle_found = true;
mdwc->extcon[idx].mdwc = mdwc;
mdwc->extcon[idx].edev = edev;
mdwc->extcon[idx].idx = idx;
//注册id和vbus的通知
mdwc->extcon[idx].vbus_nb.notifier_call =
dwc3_msm_vbus_notifier;
ret = extcon_register_notifier(edev, EXTCON_USB,
&mdwc->extcon[idx].vbus_nb);
if (ret < 0)
check_vbus_state = false;
mdwc->extcon[idx].id_nb.notifier_call = dwc3_msm_id_notifier;
ret = extcon_register_notifier(edev, EXTCON_USB_HOST,
&mdwc->extcon[idx].id_nb);
if (ret < 0)
check_id_state = false;
/* Update initial VBUS/ID state */
if (check_vbus_state && extcon_get_state(edev, EXTCON_USB))
- dwc3_msm_vbus_notifier通知函数:
static int dwc3_msm_vbus_notifier(struct notifier_block *nb,
unsigned long event, void *ptr)
{
struct dwc3 *dwc;
struct extcon_dev *edev = ptr;
struct extcon_nb *enb = container_of(nb, struct extcon_nb, vbus_nb);
struct dwc3_msm *mdwc = enb->mdwc;
char *eud_str;
const char *edev_name;
bool is_cdp;
printk(KERN_ERR"***dwc3_msm_vbus_notifier*******");
if (!edev || !mdwc)
return NOTIFY_DONE;
dwc = platform_get_drvdata(mdwc->dwc3);
printk(KERN_ERR"***dwc3_msm_vbus_notifier****extcon idx=%d********", enb->idx);
dev_err(mdwc->dev, "vbus:%ld event received\n", event);
edev_name = extcon_get_edev_name(edev);
dbg_log_string("edev:%s\n", edev_name);
/* detect USB spoof disconnect/connect notification with EUD device */
eud_str = strnstr(edev_name, "eud", strlen(edev_name));
printk(KERN_ERR"***dwc3_msm_vbus_notifier****eud_str=%s********",eud_str);
if (eud_str) {
if (mdwc->eud_active == event)
return NOTIFY_DONE;
mdwc->eud_active = event;
mdwc->check_eud_state = true;
} else {
if (mdwc->vbus_active == event)
return NOTIFY_DONE;
mdwc->vbus_active = event;
}
/*
* In case of ADSP based charger detection driving a pulse on
* DP to ensure proper CDP detection will be taken care by
* ADSP.
*/
is_cdp = ((mdwc->apsd_source == IIO) &&
(get_chg_type(mdwc) == POWER_SUPPLY_TYPE_USB_CDP)) ||
((mdwc->apsd_source == PSY) &&
(get_chg_type(mdwc) == POWER_SUPPLY_USB_TYPE_CDP));
/*
* Drive a pulse on DP to ensure proper CDP detection
* and only when the vbus connect event is a valid one.
*/
if (is_cdp && mdwc->vbus_active && !mdwc->check_eud_state) {
dev_err(mdwc->dev, "Connected to CDP, pull DP up\n");
mdwc->hs_phy->charger_detect(mdwc->hs_phy);
}
mdwc->ext_idx = enb->idx;
if (dwc->dr_mode == USB_DR_MODE_OTG && !mdwc->in_restart)
queue_work(mdwc->dwc3_wq, &mdwc->resume_work);
return NOTIFY_DONE;
}
- dwc3_msm_id_notifier通知函数
static int dwc3_msm_id_notifier(struct notifier_block *nb,
unsigned long event, void *ptr)
{
struct dwc3 *dwc;
struct extcon_dev *edev = ptr;
struct extcon_nb *enb = container_of(nb, struct extcon_nb, id_nb);
struct dwc3_msm *mdwc = enb->mdwc;
enum dwc3_id_state id;
printk(KERN_ERR"\n*****dwc3_msm_id_notifier****\n");
if (!edev || !mdwc)
return NOTIFY_DONE;
dwc = platform_get_drvdata(mdwc->dwc3);
dbg_event(0xFF, "extcon idx", enb->idx);
id = event ? DWC3_ID_GROUND : DWC3_ID_FLOAT;
if (mdwc->id_state == id)
return NOTIFY_DONE;
mdwc->ext_idx = enb->idx;
dev_err(mdwc->dev, "host:%ld (id:%d) event received\n", event, id);
mdwc->id_state = id;
dbg_event(0xFF, "id_state", mdwc->id_state);
queue_work(mdwc->dwc3_wq, &mdwc->resume_work);
return NOTIFY_DONE;
}
static void check_for_sdp_connection(struct work_struct *w)
{
struct dwc3_msm *mdwc =
container_of(w, struct dwc3_msm, sdp_check.work);
struct dwc3 *dwc = platform_get_drvdata(mdwc->dwc3);
if (!mdwc->vbus_active)
return;
/* floating D+/D- lines detected */
if (dwc->gadget.state < USB_STATE_DEFAULT &&
dwc3_gadget_get_link_state(dwc) != DWC3_LINK_STATE_CMPLY) {
mdwc->vbus_active = false;
dbg_event(0xFF, "Q RW SPD CHK", mdwc->vbus_active);
queue_work(mdwc->dwc3_wq, &mdwc->resume_work);
}
}
(2)qti_battery_charger.c中通过extcon_set_state_sync接口去发通知触发dwc3_msm_vbus_notifier和dwc3_msm_id_notifier
- typec线插入(device模式)是触发vbus的通知流程(ADP上的switch开关在B口侧):
client_data.id = MSG_OWNER_BC;
client_data.name = "battery_charger";
client_data.msg_cb = battery_chg_callback;
client_data.priv = bcdev;
client_data.state_cb = battery_chg_state_cb;
bcdev->client = pmic_glink_register_client(dev, &client_data);
pmic_glink会注册一个客户端,adsp检测到typec后通过glink设备发通知给client端,调回调函数battery_chg_callback
battery_chg_callback---->handle_notification-->battery_chg_update_usb_type_work-->battery_chg_update_uusb_type-->extcon_set_state_sync发extcon通知
static void battery_chg_update_uusb_type(struct battery_chg_dev *bcdev,
u32 adap_type)
{
struct psy_state *pst = &bcdev->psy_list[PSY_TYPE_USB];
int rc;
/* JIRA[MC0226-14] modified by hejiaxuan for usb_cc FT test 2023.03.14 start */
/* Handle the extcon notification for uUSB case only */
if (bcdev->connector_type != USB_CONNECTOR_TYPE_MICRO_USB) {
if (gpio_is_valid(bcdev->usb_id_gpio))
return;
}
/* JIRA[MC0226-14] modified by hejiaxuan for usb_cc FT test 2023.03.14 end */
rc = read_property_id(bcdev, pst, USB_SCOPE);
if (rc < 0) {
pr_err("Failed to read USB_SCOPE rc=%d\n", rc);
return;
}
printk(KERN_ERR"\n********battery_chg_update_uusb_type*pst->prop[USB_SCOPE]=%d****\n",pst->prop[USB_SCOPE]);
switch (pst->prop[USB_SCOPE]) {
case POWER_SUPPLY_SCOPE_DEVICE:
if (adap_type == POWER_SUPPLY_USB_TYPE_SDP ||
adap_type == POWER_SUPPLY_USB_TYPE_CDP) {
/* Device mode connect notification */
extcon_set_state_sync(bcdev->extcon, EXTCON_USB, 1);
bcdev->usb_prev_mode = EXTCON_USB;
rc = qti_typec_partner_register(bcdev->typec_class,
TYPEC_DEVICE);
if (rc < 0)
pr_err("Failed to register typec partner rc=%d\n",
rc);
}
break;
case POWER_SUPPLY_SCOPE_SYSTEM:
/* Host mode connect notification */
extcon_set_state_sync(bcdev->extcon, EXTCON_USB_HOST, 1);
bcdev->usb_prev_mode = EXTCON_USB_HOST;
rc = qti_typec_partner_register(bcdev->typec_class, TYPEC_HOST);
if (rc < 0)
pr_err("Failed to register typec partner rc=%d\n",
rc);
break;
default:
if (bcdev->usb_prev_mode == EXTCON_USB ||
bcdev->usb_prev_mode == EXTCON_USB_HOST) {
/* Disconnect notification */
extcon_set_state_sync(bcdev->extcon,
bcdev->usb_prev_mode, 0);
bcdev->usb_prev_mode = EXTCON_NONE;
qti_typec_partner_unregister(bcdev->typec_class);
}
break;
}
}
- typec线插入(device模式)是触发的通知流程(ADP上的switch开关在C口侧):
pmic_glink_register_client注册一个client,adsp通过glink发消息给client端 ,调ucsi_callback回调函数进行检测流程:
client_data.id = MSG_OWNER_UC;
client_data.name = "ucsi";
client_data.msg_cb = ucsi_callback;
client_data.priv = udev;
client_data.state_cb = ucsi_qti_state_cb;
udev->client = pmic_glink_register_client(dev, &client_data);
static void ucsi_handle_connector_change(struct work_struct *work)
{
struct ucsi_connector *con = container_of(work, struct ucsi_connector,
work);
struct ucsi *ucsi = con->ucsi;
enum typec_role role;
u64 command;
int ret;
enum usb_role u_role = USB_ROLE_NONE;
mutex_lock(&con->lock);
command = UCSI_GET_CONNECTOR_STATUS | UCSI_CONNECTOR_NUMBER(con->num);
ret = ucsi_send_command(ucsi, command, &con->status,
sizeof(con->status));
if (ret < 0) {
dev_err(ucsi->dev, "%s: GET_CONNECTOR_STATUS failed (%d)\n",
__func__, ret);
goto out_unlock;
}
role = !!(con->status.flags & UCSI_CONSTAT_PWR_DIR);
if (con->status.change & UCSI_CONSTAT_POWER_OPMODE_CHANGE)
ucsi_pwr_opmode_change(con);
if (con->status.change & UCSI_CONSTAT_POWER_DIR_CHANGE) {
typec_set_pwr_role(con->port, role);
/* Complete pending power role swap */
if (!completion_done(&con->complete))
complete(&con->complete);
}
if (con->status.change & UCSI_CONSTAT_CONNECT_CHANGE) {
typec_set_pwr_role(con->port, role);
switch (UCSI_CONSTAT_PARTNER_TYPE(con->status.flags)) {
case UCSI_CONSTAT_PARTNER_TYPE_UFP:
case UCSI_CONSTAT_PARTNER_TYPE_CABLE:
case UCSI_CONSTAT_PARTNER_TYPE_CABLE_AND_UFP:
u_role = USB_ROLE_HOST;
typec_set_data_role(con->port, TYPEC_HOST); //会调用到dwc3_msm_usb_set_role去切换role
break;
case UCSI_CONSTAT_PARTNER_TYPE_DFP:
u_role = USB_ROLE_DEVICE;
typec_set_data_role(con->port, TYPEC_DEVICE); //
break;
default:
break;
}
if (con->status.flags & UCSI_CONSTAT_CONNECTED)
ucsi_register_partner(con);
else
ucsi_unregister_partner(con);
/* Only notify USB controller if partner supports USB data */
if (!(UCSI_CONSTAT_PARTNER_FLAGS(con->status.flags) &
UCSI_CONSTAT_PARTNER_FLAG_USB))
u_role = USB_ROLE_NONE;
ret = usb_role_switch_set_role(ucsi->usb_role_sw, u_role);
if (ret)
dev_err(ucsi->dev, "%s(): failed to set role(%d):%d\n",
__func__, u_role, ret);
}
if (con->status.change & UCSI_CONSTAT_CAM_CHANGE) {
/*
* We don't need to know the currently supported alt modes here.
* Running GET_CAM_SUPPORTED command just to make sure the PPM
* does not get stuck in case it assumes we do so.
*/
command = UCSI_GET_CAM_SUPPORTED;
command |= UCSI_CONNECTOR_NUMBER(con->num);
ucsi_run_command(con->ucsi, command, NULL, 0);
}
if (con->status.change & UCSI_CONSTAT_PARTNER_CHANGE)
ucsi_partner_change(con);
ret = ucsi_acknowledge_connector_change(ucsi);
if (ret)
dev_err(ucsi->dev, "%s: ACK failed (%d)", __func__, ret);
trace_ucsi_connector_change(con->num, &con->status);
out_unlock:
clear_bit(EVENT_PENDING, &ucsi->flags);
mutex_unlock(&con->lock);
}

浙公网安备 33010602011771号