/* * Copyright 2016-2017 Google, Inc * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../typec/pd.h" #include "../typec/tcpm.h" #include "usbpd.h" #define LOG_BUFFER_ENTRIES 1024 #define LOG_BUFFER_ENTRY_SIZE 256 struct usbpd { struct device dev; struct tcpm_port *tcpm_port; struct tcpc_dev tcpc_dev; struct pd_phy_params pdphy_params; bool pdphy_open; struct extcon_dev *extcon; struct power_supply *usb_psy; struct notifier_block psy_nb; struct regulator *vbus; struct regulator *vconn; bool vbus_output; bool vconn_output; bool vbus_present; enum power_supply_type psy_type; enum typec_cc_status cc1; enum typec_cc_status cc2; bool is_cable_flipped; bool pending_update_usb_data; bool extcon_usb; bool extcon_usb_host; bool extcon_usb_cc; bool extcon_usb_ss; struct mutex lock; /* struct usbpd access lock */ struct workqueue_struct *wq; /* debugfs logging */ struct dentry *dentry; struct mutex logbuffer_lock; /* log buffer access lock */ int logbuffer_head; int logbuffer_tail; u8 *logbuffer[LOG_BUFFER_ENTRIES]; bool in_pr_swap; bool suspend_supported; }; /* * Logging */ static bool pd_engine_log_full(struct usbpd *pd) { return pd->logbuffer_tail == (pd->logbuffer_head + 1) % LOG_BUFFER_ENTRIES; } static void _pd_engine_log(struct usbpd *pd, const char *fmt, va_list args) { char tmpbuffer[LOG_BUFFER_ENTRY_SIZE]; u64 ts_nsec = local_clock(); unsigned long rem_nsec; if (!pd->logbuffer[pd->logbuffer_head]) { pd->logbuffer[pd->logbuffer_head] = kzalloc(LOG_BUFFER_ENTRY_SIZE, GFP_KERNEL); if (!pd->logbuffer[pd->logbuffer_head]) return; } vsnprintf(tmpbuffer, sizeof(tmpbuffer), fmt, args); mutex_lock(&pd->logbuffer_lock); if (pd_engine_log_full(pd)) { pd->logbuffer_head = max(pd->logbuffer_head - 1, 0); strlcpy(tmpbuffer, "overflow", LOG_BUFFER_ENTRY_SIZE); } if (pd->logbuffer_head < 0 || pd->logbuffer_head >= LOG_BUFFER_ENTRIES) { dev_warn(&pd->dev, "Bad log buffer index %d\n", pd->logbuffer_head); goto abort; } if (!pd->logbuffer[pd->logbuffer_head]) { dev_warn(&pd->dev, "Log buffer index %d is NULL\n", pd->logbuffer_head); goto abort; } rem_nsec = do_div(ts_nsec, 1000000000); scnprintf(pd->logbuffer[pd->logbuffer_head], LOG_BUFFER_ENTRY_SIZE, "[%5lu.%06lu] %s", (unsigned long)ts_nsec, rem_nsec / 1000, tmpbuffer); pd->logbuffer_head = (pd->logbuffer_head + 1) % LOG_BUFFER_ENTRIES; abort: mutex_unlock(&pd->logbuffer_lock); } static void pd_engine_log(struct usbpd *pd, const char *fmt, ...) { va_list args; va_start(args, fmt); _pd_engine_log(pd, fmt, args); va_end(args); } static int pd_engine_seq_show(struct seq_file *s, void *v) { struct usbpd *pd = (struct usbpd *)s->private; int tail; mutex_lock(&pd->logbuffer_lock); tail = pd->logbuffer_tail; while (tail != pd->logbuffer_head) { seq_printf(s, "%s\n", pd->logbuffer[tail]); tail = (tail + 1) % LOG_BUFFER_ENTRIES; } if (!seq_has_overflowed(s)) pd->logbuffer_tail = tail; mutex_unlock(&pd->logbuffer_lock); return 0; } static int pd_engine_debug_open(struct inode *inode, struct file *file) { return single_open(file, pd_engine_seq_show, inode->i_private); } static const struct file_operations pd_engine_debug_operations = { .open = pd_engine_debug_open, .llseek = seq_lseek, .read = seq_read, .release = single_release, }; static struct dentry *rootdir; static int pd_engine_debugfs_init(struct usbpd *pd) { mutex_init(&pd->logbuffer_lock); /* /sys/kernel/debug/pd_engine/usbpdX */ if (!rootdir) { rootdir = debugfs_create_dir("pd_engine", NULL); if (!rootdir) return -ENOMEM; } pd->dentry = debugfs_create_file(dev_name(&pd->dev), S_IFREG | S_IRUGO, rootdir, pd, &pd_engine_debug_operations); return 0; } static void pd_engine_debugfs_exit(struct usbpd *pd) { debugfs_remove(pd->dentry); } static const char * const get_typec_mode_name( enum power_supply_typec_mode typec_mode) { switch (typec_mode) { case POWER_SUPPLY_TYPEC_NONE: return "NONE"; case POWER_SUPPLY_TYPEC_SOURCE_DEFAULT: return "SOURCE_DEFAULT"; case POWER_SUPPLY_TYPEC_SOURCE_MEDIUM: return "SOURCE_MEDIUM"; case POWER_SUPPLY_TYPEC_SOURCE_HIGH: return "SOURCE_HIGH"; case POWER_SUPPLY_TYPEC_NON_COMPLIANT: return "NON_COMPLIANT"; case POWER_SUPPLY_TYPEC_SINK: return "SINK"; case POWER_SUPPLY_TYPEC_SINK_POWERED_CABLE: return "SINK_POWERED_CABLE"; case POWER_SUPPLY_TYPEC_SINK_DEBUG_ACCESSORY: return "SINK_DEBUG_ACCESSORY"; case POWER_SUPPLY_TYPEC_SINK_AUDIO_ADAPTER: return "SINK_AUDIO_ADAPTER"; case POWER_SUPPLY_TYPEC_POWERED_CABLE_ONLY: return "POWERED_CABLE_ONLY"; default: return "UNDEFINED"; } } static const char * const get_psy_type_name(enum power_supply_type psy_type) { switch (psy_type) { case POWER_SUPPLY_TYPE_UNKNOWN: return "UNKNOWN"; case POWER_SUPPLY_TYPE_USB: return "SDP"; case POWER_SUPPLY_TYPE_USB_CDP: return "CDP"; case POWER_SUPPLY_TYPE_USB_DCP: return "DCP"; case POWER_SUPPLY_TYPE_USB_HVDCP: return "HVDCP2"; case POWER_SUPPLY_TYPE_USB_HVDCP_3: return "HVDCP3"; default: return "UNDEFINED"; } } /* must align with smblib's integer representation of cc orientation. */ enum typec_cc_orientation { TYPEC_CC_ORIENTATION_NONE = 0, TYPEC_CC_ORIENTATION_CC1 = 1, TYPEC_CC_ORIENTATION_CC2 = 2, }; static const char * const get_typec_cc_orientation_name( enum typec_cc_orientation typec_cc_orientation) { switch (typec_cc_orientation) { case TYPEC_CC_ORIENTATION_NONE: return "NONE"; case TYPEC_CC_ORIENTATION_CC1: return "CC1"; case TYPEC_CC_ORIENTATION_CC2: return "CC2"; default: return "UNDEFINED"; } } static const char * const get_typec_cc_status_name( enum typec_cc_status cc_status) { switch (cc_status) { case TYPEC_CC_OPEN: return "OPEN"; case TYPEC_CC_RA: return "Ra"; case TYPEC_CC_RD: return "Rd"; case TYPEC_CC_RP_DEF: return "Rd-def"; case TYPEC_CC_RP_1_5: return "Rd-1.5"; case TYPEC_CC_RP_3_0: return "Rd-3.0"; default: return "UNDEFINED"; } } /* * parses the type-c mode to cc pin status in the orientation_none special case. */ static void parse_cc_status_none_orientation( enum power_supply_typec_mode typec_mode, enum typec_cc_status *cc1, enum typec_cc_status *cc2) { switch (typec_mode) { case POWER_SUPPLY_TYPEC_SINK_DEBUG_ACCESSORY: *cc1 = *cc2 = TYPEC_CC_RD; break; case POWER_SUPPLY_TYPEC_SINK_AUDIO_ADAPTER: *cc1 = *cc2 = TYPEC_CC_RA; break; default: *cc1 = *cc2 = TYPEC_CC_OPEN; break; } } /* * parses the type-c mode to the status of the active and inactive cc pin. */ static void parse_cc_status_active_inactive( enum power_supply_typec_mode typec_mode, enum typec_cc_status *cc_active, enum typec_cc_status *cc_inactive) { switch (typec_mode) { case POWER_SUPPLY_TYPEC_NONE: *cc_active = TYPEC_CC_OPEN; *cc_inactive = TYPEC_CC_OPEN; break; case POWER_SUPPLY_TYPEC_SOURCE_DEFAULT: *cc_active = TYPEC_CC_RP_DEF; *cc_inactive = TYPEC_CC_OPEN; break; case POWER_SUPPLY_TYPEC_SOURCE_MEDIUM: *cc_active = TYPEC_CC_RP_1_5; *cc_inactive = TYPEC_CC_OPEN; break; case POWER_SUPPLY_TYPEC_SOURCE_HIGH: *cc_active = TYPEC_CC_RP_3_0; *cc_inactive = TYPEC_CC_OPEN; break; case POWER_SUPPLY_TYPEC_NON_COMPLIANT: *cc_active = TYPEC_CC_OPEN; *cc_inactive = TYPEC_CC_OPEN; break; case POWER_SUPPLY_TYPEC_SINK: *cc_active = TYPEC_CC_RD; *cc_inactive = TYPEC_CC_OPEN; break; case POWER_SUPPLY_TYPEC_SINK_POWERED_CABLE: *cc_active = TYPEC_CC_RD; *cc_inactive = TYPEC_CC_RA; break; case POWER_SUPPLY_TYPEC_SINK_DEBUG_ACCESSORY: *cc_active = TYPEC_CC_RD; *cc_inactive = TYPEC_CC_RD; break; case POWER_SUPPLY_TYPEC_SINK_AUDIO_ADAPTER: *cc_active = TYPEC_CC_RA; *cc_inactive = TYPEC_CC_RA; break; case POWER_SUPPLY_TYPEC_POWERED_CABLE_ONLY: *cc_active = TYPEC_CC_RA; *cc_inactive = TYPEC_CC_OPEN; break; default: *cc_active = TYPEC_CC_OPEN; *cc_inactive = TYPEC_CC_OPEN; break; } } static void parse_cc_status(enum power_supply_typec_mode typec_mode, enum typec_cc_orientation typec_cc_orientation, enum typec_cc_status *cc1, enum typec_cc_status *cc2) { enum typec_cc_status cc_active, cc_inactive; /* handle orientation_none special cases */ if (typec_cc_orientation == TYPEC_CC_ORIENTATION_NONE) { parse_cc_status_none_orientation(typec_mode, cc1, cc2); return; } parse_cc_status_active_inactive(typec_mode, &cc_active, &cc_inactive); /* assign cc1, cc2 status based on orientation */ if (typec_cc_orientation == TYPEC_CC_ORIENTATION_CC1) { *cc1 = cc_active; *cc2 = cc_inactive; } else if (typec_cc_orientation == TYPEC_CC_ORIENTATION_CC2) { *cc1 = cc_inactive; *cc2 = cc_active; } else { *cc1 = *cc2 = TYPEC_CC_OPEN; } } static inline bool psy_support_usb_data(enum power_supply_type psy_type) { return (psy_type == POWER_SUPPLY_TYPE_USB) || (psy_type == POWER_SUPPLY_TYPE_USB_CDP); } static int update_usb_data_role(struct usbpd *pd) { int ret; bool extcon_usb; ret = extcon_set_cable_state_(pd->extcon, EXTCON_USB_CC, pd->extcon_usb_cc); if (ret < 0) { pd_engine_log(pd, "unable to set extcon usb cc [%s], ret=%d", pd->extcon_usb_cc ? "Y" : "N", ret); return ret; } ret = extcon_set_cable_state_(pd->extcon, EXTCON_USB_SPEED, pd->extcon_usb_ss); if (ret < 0) { pd_engine_log(pd, "unable to set extcon usb ss [%s], ret=%d", pd->extcon_usb_ss ? "Y" : "N", ret); return ret; } /* Turn on USB data (device role) only when a valid power supply that * supports USB data connection is connected, i.e. SDP or CDP * * update_usb_data_role() is called when APSD is done, thus here * pd->psy_type is a valid detection result. */ extcon_usb = pd->extcon_usb && psy_support_usb_data(pd->psy_type); /* * EXTCON_USB_HOST and EXTCON_USB is mutually exlusive. * Therefore while responding to commands such as DR_SWAP, * update the cable state of the one being turned off before * turning on the other one. */ if (!pd->extcon_usb_host) { ret = extcon_set_cable_state_(pd->extcon, EXTCON_USB_HOST, pd->extcon_usb_host); if (ret < 0) { pd_engine_log(pd, "unable to set extcon usb host [%s], ret=%d", pd->extcon_usb_host ? "Y" : "N", ret); return ret; } ret = extcon_set_cable_state_(pd->extcon, EXTCON_USB, extcon_usb); if (ret < 0) { pd_engine_log(pd, "unable to set extcon usb [%s], ret=%d", extcon_usb ? "Y" : "N", ret); return ret; } } else { ret = extcon_set_cable_state_(pd->extcon, EXTCON_USB, extcon_usb); if (ret < 0) { pd_engine_log(pd, "unable to set extcon usb [%s], ret=%d", extcon_usb ? "Y" : "N", ret); return ret; } ret = extcon_set_cable_state_(pd->extcon, EXTCON_USB_HOST, pd->extcon_usb_host); if (ret < 0) { pd_engine_log(pd, "unable to set extcon usb host [%s], ret=%d", pd->extcon_usb_host ? "Y" : "N", ret); return ret; } } pd_engine_log(pd, "usb extcon: cc [%s], speed [%s], host [%s], usb [%s]", pd->extcon_usb_cc ? "Y" : "N", pd->extcon_usb_ss ? "Y" : "N", pd->extcon_usb_host ? "Y" : "N", extcon_usb ? "Y" : "N"); return ret; } struct psy_changed_event { struct work_struct work; struct usbpd *pd; }; static void psy_changed_handler(struct work_struct *work) { struct psy_changed_event *event = container_of(work, struct psy_changed_event, work); struct usbpd *pd = event->pd; bool vbus_present; enum typec_cc_status cc1; enum typec_cc_status cc2; enum power_supply_type psy_type; enum power_supply_typec_mode typec_mode; enum typec_cc_orientation typec_cc_orientation; bool apsd_done; union power_supply_propval val; int ret = 0; ret = power_supply_get_property(pd->usb_psy, POWER_SUPPLY_PROP_TYPE, &val); if (ret < 0) { pd_engine_log(pd, "Unable to read TYPE, ret=%d", ret); return; } psy_type = val.intval; ret = power_supply_get_property(pd->usb_psy, POWER_SUPPLY_PROP_PD_APSD_DONE, &val); if (ret < 0) { pd_engine_log(pd, "Unable to read APSD_DONE, ret=%d", ret); return; } apsd_done = !!val.intval; ret = power_supply_get_property(pd->usb_psy, POWER_SUPPLY_PROP_PRESENT, &val); if (ret < 0) { pd_engine_log(pd, "Unable to read PRESENT, ret=%d", ret); return; } vbus_present = val.intval; ret = power_supply_get_property(pd->usb_psy, POWER_SUPPLY_PROP_TYPEC_MODE, &val); if (ret < 0) { pd_engine_log(pd, "Unable to read TYPEC_MODE, ret=%d", ret); return; } typec_mode = val.intval; ret = power_supply_get_property(pd->usb_psy, POWER_SUPPLY_PROP_TYPEC_CC_ORIENTATION, &val); if (ret < 0) { pd_engine_log(pd, "Unable to read TYPEC_CC_ORIENTATION, ret=%d", ret); return; } typec_cc_orientation = val.intval; parse_cc_status(typec_mode, typec_cc_orientation, &cc1, &cc2); pd_engine_log(pd, "type [%s], apsd done [%s], vbus present [%s], typec_mode [%s], typec_orientation [%s], cc1 [%s], cc2 [%s]", get_psy_type_name(psy_type), apsd_done ? "Y" : "N", vbus_present ? "Y" : "N", get_typec_mode_name(typec_mode), get_typec_cc_orientation_name(typec_cc_orientation), get_typec_cc_status_name(cc1), get_typec_cc_status_name(cc2)); mutex_lock(&pd->lock); pd->psy_type = psy_type; pd->is_cable_flipped = typec_cc_orientation == TYPEC_CC_ORIENTATION_CC2; pd->extcon_usb_cc = pd->is_cable_flipped; if (vbus_present != pd->vbus_present) { pd_engine_log(pd, "vbus present: %s -> %s", pd->vbus_present ? "True" : "False", vbus_present ? "True" : "False"); pd->vbus_present = vbus_present; tcpm_vbus_change(pd->tcpm_port); } if (cc1 != pd->cc1 || cc2 != pd->cc2) { pd_engine_log(pd, "cc1: %s -> %s, cc2: %s -> %s", get_typec_cc_status_name(pd->cc1), get_typec_cc_status_name(cc1), get_typec_cc_status_name(pd->cc2), get_typec_cc_status_name(cc2)); pd->cc1 = cc1; pd->cc2 = cc2; tcpm_cc_change(pd->tcpm_port); } if (apsd_done && pd->pending_update_usb_data) { pd_engine_log(pd, "APSD is done now, update pending usb data role"); ret = update_usb_data_role(pd); if (ret < 0) goto unlock; pd->pending_update_usb_data = false; } unlock: kfree(event); mutex_unlock(&pd->lock); } static int psy_changed(struct notifier_block *nb, unsigned long evt, void *ptr) { struct usbpd *pd; struct psy_changed_event *event; pd = container_of(nb, struct usbpd, psy_nb); if (ptr != pd->usb_psy || evt != PSY_EVENT_PROP_CHANGED) return 0; event = kzalloc(sizeof(*event), GFP_ATOMIC); if (!event) return -ENOMEM; INIT_WORK(&event->work, psy_changed_handler); event->pd = pd; queue_work(pd->wq, &event->work); return 0; } static int tcpm_init(struct tcpc_dev *dev) { return 0; } static int tcpm_get_vbus(struct tcpc_dev *dev) { struct usbpd *pd = container_of(dev, struct usbpd, tcpc_dev); bool vbus_on; int ret; mutex_lock(&pd->lock); vbus_on = pd->vbus_present || pd->vbus_output; pd_engine_log(pd, "tcpm_get_vbus: %s", vbus_on ? "True" : "False"); ret = vbus_on ? 1 : 0; mutex_unlock(&pd->lock); return ret; } static int tcpm_set_cc(struct tcpc_dev *dev, enum typec_cc_status cc) { union power_supply_propval val = {0}; struct usbpd *pd = container_of(dev, struct usbpd, tcpc_dev); int ret = 0; mutex_lock(&pd->lock); switch (cc) { case TYPEC_CC_OPEN: val.intval = POWER_SUPPLY_TYPEC_PR_NONE; break; case TYPEC_CC_RD: val.intval = POWER_SUPPLY_TYPEC_PR_SINK; break; case TYPEC_CC_RP_DEF: val.intval = POWER_SUPPLY_TYPEC_PR_SOURCE; break; default: pd_engine_log(pd, "tcpm_set_cc: invalid cc %s", get_typec_cc_status_name(cc)); ret = -EINVAL; goto unlock; } ret = power_supply_set_property(pd->usb_psy, POWER_SUPPLY_PROP_TYPEC_POWER_ROLE, &val); if (ret < 0) pd_engine_log(pd, "unable to set POWER_ROLE, ret=%d", ret); unlock: mutex_unlock(&pd->lock); return ret; } static int tcpm_get_cc(struct tcpc_dev *dev, enum typec_cc_status *cc1, enum typec_cc_status *cc2) { struct usbpd *pd = container_of(dev, struct usbpd, tcpc_dev); mutex_lock(&pd->lock); pd_engine_log(pd, "tcpm_get_cc: cc1=%s, cc2=%s", get_typec_cc_status_name(pd->cc1), get_typec_cc_status_name(pd->cc2)); *cc1 = pd->cc1; *cc2 = pd->cc2; mutex_unlock(&pd->lock); return 0; } static int tcpm_set_polarity(struct tcpc_dev *dev, enum typec_cc_polarity polarity) { /* Hardware handles polarity, no op here. */ return 0; } static int tcpm_set_vconn(struct tcpc_dev *dev, bool on) { struct usbpd *pd = container_of(dev, struct usbpd, tcpc_dev); int ret = 0; mutex_lock(&pd->lock); if (on != pd->vconn_output) { if (on) ret = regulator_enable(pd->vconn); else ret = regulator_disable(pd->vconn); if (ret < 0) { pd_engine_log(pd, "unable to turn %s vconn, ret=%d", on ? "on" : "off", ret); goto unlock; } pd->vconn_output = on; } pd_engine_log(pd, "set vconn: %s", on ? "on" : "off"); unlock: mutex_unlock(&pd->lock); return ret; } static int tcpm_set_vbus(struct tcpc_dev *dev, bool on, bool charge) { struct usbpd *pd = container_of(dev, struct usbpd, tcpc_dev); int ret = 0; mutex_lock(&pd->lock); if (on != pd->vbus_output) { if (on) ret = regulator_enable(pd->vbus); else ret = regulator_disable(pd->vbus); if (ret < 0) { pd_engine_log(pd, "unable to turn %s vbus, ret=%d", on ? "on" : "off", ret); goto unlock; } pd->vbus_output = on; /* * No interrupt will be trigerred for the vbus change. * Manually inform tcpm to check the vbus change. */ tcpm_vbus_change(pd->tcpm_port); } pd_engine_log(pd, "set vbus: %s", on ? "on" : "off"); unlock: mutex_unlock(&pd->lock); return ret; } static int tcpm_set_current_limit(struct tcpc_dev *dev, u32 max_ma, u32 mv) { union power_supply_propval val = {0}; struct usbpd *pd = container_of(dev, struct usbpd, tcpc_dev); int ret = 0; val.intval = mv * 1000; ret = power_supply_set_property(pd->usb_psy, POWER_SUPPLY_PROP_VOLTAGE_MAX, &val); if (ret < 0) { pd_engine_log(pd, "unable to set voltage to %d, ret=%d", mv, ret); return ret; } val.intval = max_ma * 1000; ret = power_supply_set_property(pd->usb_psy, POWER_SUPPLY_PROP_PD_CURRENT_MAX, &val); if (ret < 0) { pd_engine_log(pd, "unable to set pd current max to %d, ret=%d", max_ma, ret); return ret; } pd_engine_log(pd, "max_ma := %d, mv := %d", max_ma, mv); return ret; } static int tcpm_set_pd_rx(struct tcpc_dev *dev, bool on) { union power_supply_propval val = {0}; struct usbpd *pd = container_of(dev, struct usbpd, tcpc_dev); int ret = 0; if (pd->pdphy_open == on) { pd_engine_log(pd, "pd_phy already %s", on ? "open" : "closed"); return 0; } if (on) { ret = pd_phy_open(&pd->pdphy_params); if (ret < 0) { pd_engine_log(pd, "unable to %s pd_phy, ret=%d", on ? "open" : "close", ret); return ret; } val.intval = 1; } else { /* pd_phy_close() has no return value. */ pd_phy_close(); val.intval = 0; } pd->pdphy_open = on; pd_engine_log(pd, "%s pd_phy", on ? "open" : "close"); ret = power_supply_set_property(pd->usb_psy, POWER_SUPPLY_PROP_PD_CC_OVERRIDE, &val); if (ret < 0) { pd_engine_log(pd, "unable to set CC_OVERRIDE to %s, ret=%d", on ? "true" : "false", ret); return ret; } return 0; } static int get_data_len(__le16 header) { int ret; ret = pd_header_cnt(__le16_to_cpu(header)); ret *= 4; return ret; } static const char * const get_tcpm_transmit_type_name( enum tcpm_transmit_type type) { switch (type) { case TCPC_TX_SOP: return "SOP"; case TCPC_TX_SOP_PRIME: return "SOP_PRIME"; case TCPC_TX_SOP_PRIME_PRIME: return "SOP_PRIME_PRIME"; case TCPC_TX_SOP_DEBUG_PRIME: return "SOP_DEBUG_PRIME"; case TCPC_TX_SOP_DEBUG_PRIME_PRIME: return "SOP_DEBUG_PRIME_PRIME"; case TCPC_TX_HARD_RESET: return "HARD_RESET"; case TCPC_TX_CABLE_RESET: return "CABLE_RESET"; case TCPC_TX_BIST_MODE_2: return "BIST_MODE_2"; default: return "UNDEFINED"; } } struct pd_transmit_work { struct work_struct work; struct usbpd *pd; enum tcpm_transmit_type type; struct pd_message msg; }; static const char * const get_tcpm_transmit_status_name( enum tcpm_transmit_status status) { switch (status) { case TCPC_TX_SUCCESS: return "SUCCESS"; case TCPC_TX_DISCARDED: return "DISCARDED"; case TCPC_TX_FAILED: return "FAILED"; default: return "UNDEFINED"; } } #define PD_TX_TIMEOUT_MS (PD_T_TCPC_TX_TIMEOUT - 10) static void pd_transmit_handler(struct work_struct *work) { struct pd_transmit_work *pd_tx_work = container_of(work, struct pd_transmit_work, work); struct usbpd *pd = pd_tx_work->pd; enum tcpm_transmit_type type = pd_tx_work->type; struct pd_message *msg = &pd_tx_work->msg; enum tcpm_transmit_status status; bool signal; int ret = 0; switch (type) { case TCPC_TX_HARD_RESET: ret = pd_phy_signal(HARD_RESET_SIG, PD_TX_TIMEOUT_MS); signal = true; break; case TCPC_TX_CABLE_RESET: ret = pd_phy_signal(CABLE_RESET_SIG, PD_TX_TIMEOUT_MS); signal = true; break; case TCPC_TX_SOP: ret = pd_phy_write(msg->header, (u8 *)msg->payload, get_data_len(msg->header), SOP_MSG, PD_TX_TIMEOUT_MS); signal = false; break; case TCPC_TX_SOP_PRIME: ret = pd_phy_write(msg->header, (u8 *)msg->payload, get_data_len(msg->header), SOPI_MSG, PD_TX_TIMEOUT_MS); signal = false; break; case TCPC_TX_SOP_PRIME_PRIME: ret = pd_phy_write(msg->header, (u8 *)msg->payload, get_data_len(msg->header), SOPII_MSG, PD_TX_TIMEOUT_MS); signal = false; break; default: pd_engine_log(pd, "unknown pd tx type"); kfree(pd_tx_work); return; } /* * pd_phy_write()/pd_phy_signal() return value for hw irq event: * - tx succeeded hw irq: 0 for signal or actual tx len for write * - tx discarded hw irq: -EBUSY * - tx failed hw irq: -EFAULT */ if (ret >= 0) status = TCPC_TX_SUCCESS; else if (ret == -EBUSY) status = TCPC_TX_DISCARDED; else status = TCPC_TX_FAILED; tcpm_pd_transmit_complete(pd->tcpm_port, status); if (signal) pd_engine_log(pd, "pd tx type [%s], pdphy ret [%d], status [%s]", get_tcpm_transmit_type_name(type), ret, get_tcpm_transmit_status_name(status)); else pd_engine_log(pd, "pd tx header [%#x], type [%s], pdphy ret [%d], status [%s]", msg->header, get_tcpm_transmit_type_name(type), ret, get_tcpm_transmit_status_name(status)); kfree(pd_tx_work); } static int tcpm_pd_transmit(struct tcpc_dev *dev, enum tcpm_transmit_type type, const struct pd_message *msg) { struct usbpd *pd = container_of(dev, struct usbpd, tcpc_dev); struct pd_transmit_work *pd_tx_work; switch (type) { case TCPC_TX_HARD_RESET: case TCPC_TX_CABLE_RESET: case TCPC_TX_SOP: case TCPC_TX_SOP_PRIME: case TCPC_TX_SOP_PRIME_PRIME: break; default: pd_engine_log(pd, "unsupported pd type: %s", get_tcpm_transmit_type_name(type)); return -EINVAL; } pd_tx_work = kzalloc(sizeof(*pd_tx_work), GFP_ATOMIC); if (!pd_tx_work) return -ENOMEM; INIT_WORK(&pd_tx_work->work, pd_transmit_handler); pd_tx_work->pd = pd; pd_tx_work->type = type; if (msg) memcpy(&pd_tx_work->msg, msg, sizeof(*msg)); queue_work(pd->wq, &pd_tx_work->work); if (msg) pd_engine_log(pd, "queue pd tx header [%#x], type [%s]", msg->header, get_tcpm_transmit_type_name(type)); else pd_engine_log(pd, "queue pd tx type [%s]", get_tcpm_transmit_type_name(type)); return 0; } static int tcpm_start_drp_toggling(struct tcpc_dev *dev, enum typec_cc_status cc) { /* * Ignore the typec_cc_status for now. As current no * src_pdo is configured, the default is Rp_def. */ union power_supply_propval val = {0}; struct usbpd *pd = container_of(dev, struct usbpd, tcpc_dev); int ret; mutex_lock(&pd->lock); val.intval = POWER_SUPPLY_TYPEC_PR_DUAL; ret = power_supply_set_property(pd->usb_psy, POWER_SUPPLY_PROP_TYPEC_POWER_ROLE, &val); if (ret < 0) { pd_engine_log(pd, "unable to set POWER_ROLE to PR_DUAL, ret=%d", ret); goto unlock; } pd->cc1 = TYPEC_CC_OPEN; pd->cc2 = TYPEC_CC_OPEN; pd_engine_log(pd, "start toggling"); unlock: mutex_unlock(&pd->lock); return ret; } static int tcpm_set_in_pr_swap(struct tcpc_dev *dev, bool pr_swap) { union power_supply_propval val = {0}; struct usbpd *pd = container_of(dev, struct usbpd, tcpc_dev); int ret; mutex_lock(&pd->lock); if (pd->in_pr_swap != pr_swap) { if (!pr_swap) { val.intval = POWER_SUPPLY_TYPEC_PR_DUAL; ret = power_supply_set_property(pd->usb_psy, POWER_SUPPLY_PROP_TYPEC_POWER_ROLE, &val); if (ret < 0) { pd_engine_log(pd, "unable to set POWER_ROLE to PR_DUAL, ret=%d", ret); goto unlock; } /* Required for the PMIC to recover */ pd_engine_log(pd, "sleeping for 20mec"); msleep(20); } val.intval = pr_swap ? 1 : 0; ret = power_supply_set_property(pd->usb_psy, POWER_SUPPLY_PROP_PR_SWAP, &val); if (ret < 0) { pd_engine_log(pd, "unable to set PR_SWAP to %d, ret=%d", pr_swap ? 1 : 0, ret); goto unlock; } pd->in_pr_swap = pr_swap; pd_engine_log(pd, "PR_SWAP = %d", pr_swap ? 1 : 0); } unlock: mutex_unlock(&pd->lock); return ret; } static int tcpm_set_suspend_supported(struct tcpc_dev *dev, bool suspend_supported) { union power_supply_propval val = {0}; struct usbpd *pd = container_of(dev, struct usbpd, tcpc_dev); int ret = 0; mutex_lock(&pd->lock); if (suspend_supported == pd->suspend_supported) goto unlock; /* Attempt once */ pd->suspend_supported = suspend_supported; val.intval = suspend_supported ? 1 : 0; pd_engine_log(pd, "usb suspend %d", suspend_supported ? 1 : 0); ret = power_supply_set_property(pd->usb_psy, POWER_SUPPLY_PROP_PD_USB_SUSPEND_SUPPORTED, &val); if (ret < 0) { pd_engine_log(pd, "unable to set suspend flag to %d, ret=%d", suspend_supported ? 1 : 0, ret); } unlock: mutex_unlock(&pd->lock); return ret; } enum power_role get_pdphy_power_role(enum typec_role role) { switch (role) { case TYPEC_SINK: return PR_SINK; case TYPEC_SOURCE: return PR_SRC; default: return PR_NONE; } } static const char * const get_typec_role_name(enum typec_role role) { switch (role) { case TYPEC_SINK: return "SINK"; case TYPEC_SOURCE: return "SOURCE"; default: return "UNDEFINED"; } } static const char * const get_pdphy_pr_name(enum power_role pdphy_pr) { switch (pdphy_pr) { case PR_SINK: return "PR_SINK"; case PR_SRC: return "PR_SRC"; case PR_NONE: return "PR_NONE"; default: return "UNDEFINED"; } } enum data_role get_pdphy_data_role(enum typec_data_role data) { switch (data) { case TYPEC_DEVICE: return DR_UFP; case TYPEC_HOST: return DR_DFP; default: return DR_NONE; } } static const char * const get_typec_data_role_name(enum typec_data_role data) { switch (data) { case TYPEC_DEVICE: return "DEVICE"; case TYPEC_HOST: return "HOST"; default: return "UNDEFINED"; } } static const char * const get_pdphy_dr_name(enum data_role pdphy_dr) { switch (pdphy_dr) { case DR_UFP: return "DR_UFP"; case DR_DFP: return "DR_DFP"; case DR_NONE: return "DR_NONE"; default: return "UNDEFINED"; } } static int set_pd_header(struct usbpd *pd, enum typec_role role, enum typec_data_role data) { enum power_role pdphy_pr; enum data_role pdphy_dr; int ret = 0; pdphy_pr = get_pdphy_power_role(role); pdphy_dr = get_pdphy_data_role(data); ret = pd_phy_update_roles(pdphy_dr, pdphy_pr); if (ret < 0) { pd_engine_log(pd, "unable to set pd_phy_header: %s, %s, ret=%d", get_pdphy_pr_name(pdphy_pr), get_pdphy_dr_name(pdphy_dr), ret); return ret; } pd_engine_log(pd, "set pd_phy_header: %s, %s", get_pdphy_pr_name(pdphy_pr), get_pdphy_dr_name(pdphy_dr)); return 0; } #define EXTCON_USB_SUPER_SPEED true static int set_usb_data_role(struct usbpd *pd, bool attached, enum typec_role role, enum typec_data_role data) { int ret; union power_supply_propval val = {0}; bool apsd_done; pd->extcon_usb_cc = pd->is_cable_flipped; pd->extcon_usb_ss = EXTCON_USB_SUPER_SPEED; if (!attached) { pd->extcon_usb = false; pd->extcon_usb_host = false; } else if (data == TYPEC_HOST) { pd->extcon_usb = false; pd->extcon_usb_host = true; } else { pd->extcon_usb = true; pd->extcon_usb_host = false; } ret = power_supply_get_property(pd->usb_psy, POWER_SUPPLY_PROP_PD_APSD_DONE, &val); if (ret < 0) { pd_engine_log(pd, "Unable to read APSD_DONE, ret=%d", ret); return ret; } if (val.intval == 0) apsd_done = false; else apsd_done = true; pd_engine_log(pd, "set usb_data_role: power [%s], data [%s], apsd_done [%s], attached [%s]", get_typec_role_name(role), get_typec_data_role_name(data), apsd_done ? "Y" : "N", attached ? "Y" : "N"); if (attached && role == TYPEC_SINK && !apsd_done) { /* wait for APSD done */ pd_engine_log(pd, "APSD is not done, delay update usb data role"); pd->pending_update_usb_data = true; } else { ret = update_usb_data_role(pd); if (ret < 0) return ret; pd->pending_update_usb_data = false; } return ret; } static int tcpm_set_roles(struct tcpc_dev *dev, bool attached, enum typec_role role, enum typec_data_role data) { struct usbpd *pd = container_of(dev, struct usbpd, tcpc_dev); int ret; mutex_lock(&pd->lock); ret = set_pd_header(pd, role, data); if (ret < 0) goto unlock; ret = set_usb_data_role(pd, attached, role, data); unlock: mutex_unlock(&pd->lock); return ret; } static void pd_phy_signal_rx(struct usbpd *pd, enum pd_sig_type type) { switch (type) { case HARD_RESET_SIG: tcpm_pd_hard_reset(pd->tcpm_port); pd_engine_log(pd, "received pd hard reset"); break; default: pd_engine_log(pd, "received unsupported signal: %d", type); return; } } static void pd_phy_message_rx(struct usbpd *pd, enum pd_msg_type type, u8 *buf, size_t len) { struct pd_message msg; if (len < 2) { pd_engine_log(pd, "invalid message received, len=%ld", len); return; } msg.header = cpu_to_le16(*((u16 *)buf)); buf += sizeof(u16); len -= sizeof(u16); if (get_data_len(msg.header) != len) { pd_engine_log(pd, "header len %d != len %ld", get_data_len(msg.header), len); return; } if (len > PD_MAX_PAYLOAD * 4) { pd_engine_log(pd, "len %ld > PD_MAX_PAYLOAD", len); return; } memcpy(msg.payload, buf, len); pd_engine_log(pd, "pd rx header [%#x]", msg.header); tcpm_pd_receive(pd->tcpm_port, &msg); } static void pd_phy_shutdown(struct usbpd *pd) { pd_engine_log(pd, "pd phy shutdown"); } enum pdo_role { SNK_PDO, SRC_PDO, }; static const char * const pdo_prop_name[] = { [SNK_PDO] = "snk-pdo", [SRC_PDO] = "src-pdo", }; #define PDO_FIXED_FLAGS \ (PDO_FIXED_DUAL_ROLE | PDO_FIXED_DATA_SWAP | PDO_FIXED_USB_COMM) /* static const u32 src_pdo[] = { PDO_FIXED(5000, 900, PDO_FIXED_FLAGS), }; static const u32 snk_pdo[] = { PDO_FIXED(5000, 3000, PDO_FIXED_FLAGS), PDO_FIXED(9000, 3000, PDO_FIXED_FLAGS), }; static const struct tcpc_config pd_tcpc_config = { .src_pdo = src_pdo, .nr_src_pdo = ARRAY_SIZE(src_pdo), .snk_pdo = snk_pdo, .nr_snk_pdo = ARRAY_SIZE(snk_pdo), .max_snk_mv = 9000, .max_snk_ma = 3000, .max_snk_mw = 27000, .operating_snk_mw = 7600, .type = TYPEC_PORT_DRP, .default_role = TYPEC_SINK, .try_role_hw = true, .alt_modes = NULL, }; */ static u32 *parse_pdo(struct usbpd *pd, enum pdo_role role, unsigned int *nr_pdo) { struct device *dev = &pd->dev; u32 *dt_array; u32 *pdo; int i, count, rc; count = device_property_read_u32_array(dev->parent, pdo_prop_name[role], NULL, 0); if (count > 0) { if (count % 4) return ERR_PTR(-EINVAL); *nr_pdo = count / 4; dt_array = devm_kcalloc(dev, count, sizeof(*dt_array), GFP_KERNEL); if (!dt_array) return ERR_PTR(-ENOMEM); rc = device_property_read_u32_array(dev->parent, pdo_prop_name[role], dt_array, count); if (rc) return ERR_PTR(rc); pdo = devm_kcalloc(dev, *nr_pdo, sizeof(*pdo), GFP_KERNEL); if (!pdo) return ERR_PTR(-ENOMEM); for (i = 0; i < *nr_pdo; i++) { switch (dt_array[i * 4]) { case PDO_TYPE_FIXED: pdo[i] = PDO_FIXED(dt_array[i * 4 + 1], dt_array[i * 4 + 2], PDO_FIXED_FLAGS); break; case PDO_TYPE_BATT: pdo[i] = PDO_BATT(dt_array[i * 4 + 1], dt_array[i * 4 + 2], dt_array[i * 4 + 3]); break; case PDO_TYPE_VAR: pdo[i] = PDO_VAR(dt_array[i * 4 + 1], dt_array[i * 4 + 2], dt_array[i * 4 + 3]); break; /*case PDO_TYPE_AUG:*/ default: return ERR_PTR(-EINVAL); } } return pdo; } return ERR_PTR(-EINVAL); } static int init_tcpc_config(struct tcpc_dev *pd_tcpc_dev) { struct usbpd *pd = container_of(pd_tcpc_dev, struct usbpd, tcpc_dev); struct device *dev = &pd->dev; struct tcpc_config *config; int ret; pd_tcpc_dev->config = devm_kzalloc(dev, sizeof(*config), GFP_KERNEL); if (!pd_tcpc_dev->config) return -ENOMEM; config = pd_tcpc_dev->config; ret = device_property_read_u32(dev->parent, "port-type", &config->type); if (ret < 0) return ret; switch (config->type) { case TYPEC_PORT_UFP: config->snk_pdo = parse_pdo(pd, SNK_PDO, &config->nr_snk_pdo); if (IS_ERR(config->snk_pdo)) return PTR_ERR(config->snk_pdo); break; case TYPEC_PORT_DFP: config->src_pdo = parse_pdo(pd, SRC_PDO, &config->nr_src_pdo); if (IS_ERR(config->src_pdo)) return PTR_ERR(config->src_pdo); break; case TYPEC_PORT_DRP: config->snk_pdo = parse_pdo(pd, SNK_PDO, &config->nr_snk_pdo); if (IS_ERR(config->snk_pdo)) return PTR_ERR(config->snk_pdo); config->src_pdo = parse_pdo(pd, SRC_PDO, &config->nr_src_pdo); if (IS_ERR(config->src_pdo)) return PTR_ERR(config->src_pdo); ret = device_property_read_u32(dev->parent, "default-role", &config->default_role); if (ret < 0) return ret; config->try_role_hw = device_property_read_bool(dev->parent, "try-role-hw"); break; default: return -EINVAL; } if (config->type == TYPEC_PORT_UFP || config->type == TYPEC_PORT_DRP) { ret = device_property_read_u32(dev->parent, "max-snk-mv", &config->max_snk_mv); ret = device_property_read_u32(dev->parent, "max-snk-ma", &config->max_snk_ma); ret = device_property_read_u32(dev->parent, "max-snk-mw", &config->max_snk_mw); ret = device_property_read_u32(dev->parent, "op-snk-mw", &config->operating_snk_mw); if (ret < 0) return ret; } /* TODO: parse alt mode from DT */ config->alt_modes = NULL; return 0; } static int init_tcpc_dev(struct tcpc_dev *pd_tcpc_dev) { int ret; ret = init_tcpc_config(pd_tcpc_dev); if (ret < 0) return ret; pd_tcpc_dev->init = tcpm_init; pd_tcpc_dev->get_vbus = tcpm_get_vbus; pd_tcpc_dev->set_cc = tcpm_set_cc; pd_tcpc_dev->get_cc = tcpm_get_cc; pd_tcpc_dev->set_polarity = tcpm_set_polarity; pd_tcpc_dev->set_vconn = tcpm_set_vconn; pd_tcpc_dev->set_vbus = tcpm_set_vbus; pd_tcpc_dev->set_current_limit = tcpm_set_current_limit; pd_tcpc_dev->set_pd_rx = tcpm_set_pd_rx; pd_tcpc_dev->set_roles = tcpm_set_roles; pd_tcpc_dev->try_role = NULL; pd_tcpc_dev->pd_transmit = tcpm_pd_transmit; pd_tcpc_dev->start_drp_toggling = tcpm_start_drp_toggling; pd_tcpc_dev->set_in_pr_swap = tcpm_set_in_pr_swap; pd_tcpc_dev->set_suspend_supported = tcpm_set_suspend_supported; pd_tcpc_dev->mux = NULL; return 0; } static void init_pd_phy_params(struct pd_phy_params *pdphy_params) { pdphy_params->signal_cb = pd_phy_signal_rx; pdphy_params->msg_rx_cb = pd_phy_message_rx; pdphy_params->shutdown_cb = pd_phy_shutdown; pdphy_params->data_role = DR_UFP; pdphy_params->power_role = PR_SINK; pdphy_params->frame_filter_val = FRAME_FILTER_EN_SOP | FRAME_FILTER_EN_HARD_RESET; } static const unsigned int usbpd_extcon_cable[] = { EXTCON_USB, EXTCON_USB_HOST, EXTCON_USB_CC, EXTCON_USB_SPEED, EXTCON_NONE, }; /* EXTCON_USB and EXTCON_USB_HOST are mutually exclusive */ static const u32 usbpd_extcon_exclusive[] = {0x3, 0}; static int num_pd_instances; /** * usbpd_create - Create a new instance of USB PD protocol/policy engine * @parent - parent device to associate with * * This creates a new usbpd class device which manages the state of a * USB PD-capable port. The parent device that is passed in should be * associated with the physical device port, e.g. a PD PHY. * * Derived from policy_engine.c. * * Return: struct usbpd pointer, or an ERR_PTR value */ struct usbpd *usbpd_create(struct device *parent) { int ret; struct usbpd *pd; pd = kzalloc(sizeof(*pd), GFP_KERNEL); if (!pd) return ERR_PTR(-ENOMEM); mutex_init(&pd->lock); device_initialize(&pd->dev); pd->dev.parent = parent; dev_set_drvdata(&pd->dev, pd); ret = dev_set_name(&pd->dev, "usbpd%d", num_pd_instances++); if (ret < 0) goto free_pd; ret = device_add(&pd->dev); if (ret < 0) goto free_pd; ret = pd_engine_debugfs_init(pd); if (ret < 0) goto del_pd; pd->vbus = devm_regulator_get(parent, "vbus"); if (IS_ERR(pd->vbus)) { ret = PTR_ERR(pd->vbus); goto exit_debugfs; } pd->vconn = devm_regulator_get(parent, "vconn"); if (IS_ERR(pd->vconn)) { ret = PTR_ERR(pd->vconn); goto exit_debugfs; } pd->extcon = devm_extcon_dev_allocate(parent, usbpd_extcon_cable); if (IS_ERR(pd->extcon)) { pd_engine_log(pd, "failed to allocate extcon device"); ret = PTR_ERR(pd->extcon); goto exit_debugfs; } pd->extcon->mutually_exclusive = usbpd_extcon_exclusive; ret = devm_extcon_dev_register(parent, pd->extcon); if (ret < 0) { pd_engine_log(pd, "failed to register extcon device"); goto exit_debugfs; } pd->wq = create_singlethread_workqueue(dev_name(&pd->dev)); if (!pd->wq) { ret = -ENOMEM; goto exit_debugfs; } pd->usb_psy = power_supply_get_by_name("usb"); if (!pd->usb_psy) { pd_engine_log(pd, "Could not get USB power_supply, deferring probe"); ret = -EPROBE_DEFER; goto del_wq; } /* * TCPM callbacks may access pd->usb_psy. Therefore, tcpm_register_port * must be called after pd->usb_psy is initialized. */ ret = init_tcpc_dev(&pd->tcpc_dev); if (ret < 0) goto put_psy; pd->tcpm_port = tcpm_register_port(&pd->dev, &pd->tcpc_dev); if (IS_ERR(pd->tcpm_port)) { ret = PTR_ERR(pd->tcpm_port); goto put_psy; } psy_changed(&pd->psy_nb, PSY_EVENT_PROP_CHANGED, pd->usb_psy); pd->psy_nb.notifier_call = psy_changed; ret = power_supply_reg_notifier(&pd->psy_nb); if (ret < 0) goto unreg_tcpm; init_pd_phy_params(&pd->pdphy_params); pd->suspend_supported = true; return pd; unreg_tcpm: tcpm_unregister_port(pd->tcpm_port); put_psy: power_supply_put(pd->usb_psy); del_wq: destroy_workqueue(pd->wq); exit_debugfs: pd_engine_debugfs_exit(pd); del_pd: device_del(&pd->dev); free_pd: num_pd_instances--; kfree(pd); return ERR_PTR(ret); } EXPORT_SYMBOL(usbpd_create); /** * usbpd_destroy - Removes and frees a usbpd instance * @pd: the instance to destroy * * Derived from policy_engine.c. */ void usbpd_destroy(struct usbpd *pd) { if (!pd) return; power_supply_unreg_notifier(&pd->psy_nb); tcpm_unregister_port(pd->tcpm_port); power_supply_put(pd->usb_psy); destroy_workqueue(pd->wq); pd_engine_debugfs_exit(pd); device_del(&pd->dev); num_pd_instances--; kfree(pd); } EXPORT_SYMBOL(usbpd_destroy); static int __init pd_engine_init(void) { return 0; } module_init(pd_engine_init); static void __exit pd_engine_exit(void) {} module_exit(pd_engine_exit); MODULE_DESCRIPTION("USB PD Engine based on Type-C Port Manager"); MODULE_LICENSE("GPL v2");