usb: typec: ucsi: add Huawei Matebook E Go ucsi driver

The Huawei Matebook E Go tablet implements the UCSI interface in the
onboard EC. Add the glue driver to interface with the platform's UCSI
implementation.

This driver is inspired by the following drivers:
	drivers/usb/typec/ucsi/ucsi_yoga_c630.c
	drivers/usb/typec/ucsi/ucsi_glink.c
	drivers/soc/qcom/pmic_glink_altmode.c

Signed-off-by: Pengyu Luo <mitltlatltl@gmail.com>
Reviewed-by: Heikki Krogerus <heikki.krogerus@linux.intel.com>
Link: https://lore.kernel.org/r/20250316094357.462022-1-mitltlatltl@gmail.com
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
This commit is contained in:
Pengyu Luo
2025-03-16 17:43:55 +08:00
committed by Greg Kroah-Hartman
parent 9bf4294d0c
commit 00327d7f2c
4 changed files with 535 additions and 0 deletions

View File

@@ -10950,6 +10950,7 @@ M: Pengyu Luo <mitltlatltl@gmail.com>
S: Maintained
F: Documentation/devicetree/bindings/platform/huawei,gaokun-ec.yaml
F: drivers/platform/arm64/huawei-gaokun-ec.c
F: drivers/usb/typec/ucsi/ucsi_huawei_gaokun.c
F: include/linux/platform_data/huawei-gaokun-ec.h
HUGETLB SUBSYSTEM

View File

@@ -91,4 +91,15 @@ config UCSI_LENOVO_YOGA_C630
To compile the driver as a module, choose M here: the module will be
called ucsi_yoga_c630.
config UCSI_HUAWEI_GAOKUN
tristate "UCSI Interface Driver for Huawei Matebook E Go"
depends on EC_HUAWEI_GAOKUN
select DRM_AUX_HPD_BRIDGE
help
This driver enables UCSI support on the Huawei Matebook E Go tablet,
which is a sc8280xp-based 2-in-1 tablet.
To compile the driver as a module, choose M here: the module will be
called ucsi_huawei_gaokun.
endif

View File

@@ -23,3 +23,4 @@ obj-$(CONFIG_UCSI_STM32G0) += ucsi_stm32g0.o
obj-$(CONFIG_UCSI_PMIC_GLINK) += ucsi_glink.o
obj-$(CONFIG_CROS_EC_UCSI) += cros_ec_ucsi.o
obj-$(CONFIG_UCSI_LENOVO_YOGA_C630) += ucsi_yoga_c630.o
obj-$(CONFIG_UCSI_HUAWEI_GAOKUN) += ucsi_huawei_gaokun.o

View File

@@ -0,0 +1,522 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* ucsi-huawei-gaokun - A UCSI driver for HUAWEI Matebook E Go
*
* Copyright (C) 2024-2025 Pengyu Luo <mitltlatltl@gmail.com>
*/
#include <drm/bridge/aux-bridge.h>
#include <linux/auxiliary_bus.h>
#include <linux/bitops.h>
#include <linux/completion.h>
#include <linux/container_of.h>
#include <linux/module.h>
#include <linux/notifier.h>
#include <linux/of.h>
#include <linux/platform_data/huawei-gaokun-ec.h>
#include <linux/string.h>
#include <linux/usb/pd_vdo.h>
#include <linux/usb/typec_altmode.h>
#include <linux/usb/typec_dp.h>
#include <linux/workqueue_types.h>
#include "ucsi.h"
#define EC_EVENT_UCSI 0x21
#define EC_EVENT_USB 0x22
#define GAOKUN_CCX_MASK GENMASK(1, 0)
#define GAOKUN_MUX_MASK GENMASK(3, 2)
#define GAOKUN_DPAM_MASK GENMASK(3, 0)
#define GAOKUN_HPD_STATE_MASK BIT(4)
#define GAOKUN_HPD_IRQ_MASK BIT(5)
#define GET_IDX(updt) (ffs(updt) - 1)
#define CCX_TO_ORI(ccx) (++(ccx) % 3) /* convert ccx to enum typec_orientation */
/* Configuration Channel Extension */
enum gaokun_ucsi_ccx {
USBC_CCX_NORMAL,
USBC_CCX_REVERSE,
USBC_CCX_NONE,
};
enum gaokun_ucsi_mux {
USBC_MUX_NONE,
USBC_MUX_USB_2L,
USBC_MUX_DP_4L,
USBC_MUX_USB_DP,
};
/* based on pmic_glink_altmode_pin_assignment */
enum gaokun_ucsi_dpam_pan { /* DP Alt Mode Pin Assignments */
USBC_DPAM_PAN_NONE,
USBC_DPAM_PAN_A, /* Not supported after USB Type-C Standard v1.0b */
USBC_DPAM_PAN_B, /* Not supported after USB Type-C Standard v1.0b */
USBC_DPAM_PAN_C, /* USBC_DPAM_PAN_C_REVERSE - 6 */
USBC_DPAM_PAN_D,
USBC_DPAM_PAN_E,
USBC_DPAM_PAN_F, /* Not supported after USB Type-C Standard v1.0b */
USBC_DPAM_PAN_A_REVERSE,/* Not supported after USB Type-C Standard v1.0b */
USBC_DPAM_PAN_B_REVERSE,/* Not supported after USB Type-C Standard v1.0b */
USBC_DPAM_PAN_C_REVERSE,
USBC_DPAM_PAN_D_REVERSE,
USBC_DPAM_PAN_E_REVERSE,
USBC_DPAM_PAN_F_REVERSE,/* Not supported after USB Type-C Standard v1.0b */
};
struct gaokun_ucsi_reg {
u8 num_ports;
u8 port_updt;
u8 port_data[4];
u8 checksum;
u8 reserved;
} __packed;
struct gaokun_ucsi_port {
struct completion usb_ack;
spinlock_t lock; /* serializing port resource access */
struct gaokun_ucsi *ucsi;
struct auxiliary_device *bridge;
int idx;
enum gaokun_ucsi_ccx ccx;
enum gaokun_ucsi_mux mux;
u8 mode;
u16 svid;
u8 hpd_state;
u8 hpd_irq;
};
struct gaokun_ucsi {
struct gaokun_ec *ec;
struct ucsi *ucsi;
struct gaokun_ucsi_port *ports;
struct device *dev;
struct delayed_work work;
struct notifier_block nb;
u16 version;
u8 num_ports;
};
/* -------------------------------------------------------------------------- */
/* For UCSI */
static int gaokun_ucsi_read_version(struct ucsi *ucsi, u16 *version)
{
struct gaokun_ucsi *uec = ucsi_get_drvdata(ucsi);
*version = uec->version;
return 0;
}
static int gaokun_ucsi_read_cci(struct ucsi *ucsi, u32 *cci)
{
struct gaokun_ucsi *uec = ucsi_get_drvdata(ucsi);
u8 buf[GAOKUN_UCSI_READ_SIZE];
int ret;
ret = gaokun_ec_ucsi_read(uec->ec, buf);
if (ret)
return ret;
memcpy(cci, buf, sizeof(*cci));
return 0;
}
static int gaokun_ucsi_read_message_in(struct ucsi *ucsi,
void *val, size_t val_len)
{
struct gaokun_ucsi *uec = ucsi_get_drvdata(ucsi);
u8 buf[GAOKUN_UCSI_READ_SIZE];
int ret;
ret = gaokun_ec_ucsi_read(uec->ec, buf);
if (ret)
return ret;
memcpy(val, buf + GAOKUN_UCSI_CCI_SIZE,
min(val_len, GAOKUN_UCSI_MSGI_SIZE));
return 0;
}
static int gaokun_ucsi_async_control(struct ucsi *ucsi, u64 command)
{
struct gaokun_ucsi *uec = ucsi_get_drvdata(ucsi);
u8 buf[GAOKUN_UCSI_WRITE_SIZE] = {};
memcpy(buf, &command, sizeof(command));
return gaokun_ec_ucsi_write(uec->ec, buf);
}
static void gaokun_ucsi_update_connector(struct ucsi_connector *con)
{
struct gaokun_ucsi *uec = ucsi_get_drvdata(con->ucsi);
if (con->num > uec->num_ports)
return;
con->typec_cap.orientation_aware = true;
}
static void gaokun_set_orientation(struct ucsi_connector *con,
struct gaokun_ucsi_port *port)
{
enum gaokun_ucsi_ccx ccx;
unsigned long flags;
spin_lock_irqsave(&port->lock, flags);
ccx = port->ccx;
spin_unlock_irqrestore(&port->lock, flags);
typec_set_orientation(con->port, CCX_TO_ORI(ccx));
}
static void gaokun_ucsi_connector_status(struct ucsi_connector *con)
{
struct gaokun_ucsi *uec = ucsi_get_drvdata(con->ucsi);
int idx;
idx = con->num - 1;
if (con->num > uec->num_ports) {
dev_warn(uec->dev, "set orientation out of range: con%d\n", idx);
return;
}
gaokun_set_orientation(con, &uec->ports[idx]);
}
const struct ucsi_operations gaokun_ucsi_ops = {
.read_version = gaokun_ucsi_read_version,
.read_cci = gaokun_ucsi_read_cci,
.read_message_in = gaokun_ucsi_read_message_in,
.sync_control = ucsi_sync_control_common,
.async_control = gaokun_ucsi_async_control,
.update_connector = gaokun_ucsi_update_connector,
.connector_status = gaokun_ucsi_connector_status,
};
/* -------------------------------------------------------------------------- */
/* For Altmode */
static void gaokun_ucsi_port_update(struct gaokun_ucsi_port *port,
const u8 *port_data)
{
struct gaokun_ucsi *uec = port->ucsi;
int offset = port->idx * 2; /* every port has 2 Bytes data */
unsigned long flags;
u8 dcc, ddi;
dcc = port_data[offset];
ddi = port_data[offset + 1];
spin_lock_irqsave(&port->lock, flags);
port->ccx = FIELD_GET(GAOKUN_CCX_MASK, dcc);
port->mux = FIELD_GET(GAOKUN_MUX_MASK, dcc);
port->mode = FIELD_GET(GAOKUN_DPAM_MASK, ddi);
port->hpd_state = FIELD_GET(GAOKUN_HPD_STATE_MASK, ddi);
port->hpd_irq = FIELD_GET(GAOKUN_HPD_IRQ_MASK, ddi);
/* Mode and SVID are unused; keeping them to make things clearer */
switch (port->mode) {
case USBC_DPAM_PAN_C:
case USBC_DPAM_PAN_C_REVERSE:
port->mode = DP_PIN_ASSIGN_C; /* correct it for usb later */
break;
case USBC_DPAM_PAN_D:
case USBC_DPAM_PAN_D_REVERSE:
port->mode = DP_PIN_ASSIGN_D;
break;
case USBC_DPAM_PAN_E:
case USBC_DPAM_PAN_E_REVERSE:
port->mode = DP_PIN_ASSIGN_E;
break;
case USBC_DPAM_PAN_NONE:
port->mode = TYPEC_STATE_SAFE;
break;
default:
dev_warn(uec->dev, "unknown mode %d\n", port->mode);
break;
}
switch (port->mux) {
case USBC_MUX_NONE:
port->svid = 0;
break;
case USBC_MUX_USB_2L:
port->svid = USB_SID_PD;
port->mode = TYPEC_STATE_USB; /* same as PAN_C, correct it */
break;
case USBC_MUX_DP_4L:
case USBC_MUX_USB_DP:
port->svid = USB_SID_DISPLAYPORT;
break;
default:
dev_warn(uec->dev, "unknown mux state %d\n", port->mux);
break;
}
spin_unlock_irqrestore(&port->lock, flags);
}
static int gaokun_ucsi_refresh(struct gaokun_ucsi *uec)
{
struct gaokun_ucsi_reg ureg;
int ret, idx;
ret = gaokun_ec_ucsi_get_reg(uec->ec, &ureg);
if (ret)
return GAOKUN_UCSI_NO_PORT_UPDATE;
uec->num_ports = ureg.num_ports;
idx = GET_IDX(ureg.port_updt);
if (idx < 0 || idx >= ureg.num_ports)
return GAOKUN_UCSI_NO_PORT_UPDATE;
gaokun_ucsi_port_update(&uec->ports[idx], ureg.port_data);
return idx;
}
static void gaokun_ucsi_handle_altmode(struct gaokun_ucsi_port *port)
{
struct gaokun_ucsi *uec = port->ucsi;
int idx = port->idx;
if (idx >= uec->ucsi->cap.num_connectors) {
dev_warn(uec->dev, "altmode port out of range: %d\n", idx);
return;
}
/* UCSI callback .connector_status() have set orientation */
if (port->bridge)
drm_aux_hpd_bridge_notify(&port->bridge->dev,
port->hpd_state ?
connector_status_connected :
connector_status_disconnected);
gaokun_ec_ucsi_pan_ack(uec->ec, port->idx);
}
static void gaokun_ucsi_altmode_notify_ind(struct gaokun_ucsi *uec)
{
int idx;
if (!uec->ucsi->connector) { /* slow to register */
dev_err_ratelimited(uec->dev, "ucsi connector is not initialized yet\n");
return;
}
idx = gaokun_ucsi_refresh(uec);
if (idx == GAOKUN_UCSI_NO_PORT_UPDATE)
gaokun_ec_ucsi_pan_ack(uec->ec, idx); /* ack directly if no update */
else
gaokun_ucsi_handle_altmode(&uec->ports[idx]);
}
/*
* When inserting, 2 UCSI events(connector change) are followed by USB events.
* If we received one USB event, that means USB events are not blocked, so we
* can complelte for all ports, and we should signal all events.
*/
static void gaokun_ucsi_complete_usb_ack(struct gaokun_ucsi *uec)
{
struct gaokun_ucsi_port *port;
int idx = 0;
while (idx < uec->num_ports) {
port = &uec->ports[idx++];
if (!completion_done(&port->usb_ack))
complete_all(&port->usb_ack);
}
}
/*
* USB event is necessary for enabling altmode, the event should follow
* UCSI event, if not after timeout(this notify may be disabled somehow),
* then force to enable altmode.
*/
static void gaokun_ucsi_handle_no_usb_event(struct gaokun_ucsi *uec, int idx)
{
struct gaokun_ucsi_port *port;
port = &uec->ports[idx];
if (!wait_for_completion_timeout(&port->usb_ack, 2 * HZ)) {
dev_warn(uec->dev, "No USB EVENT, triggered by UCSI EVENT");
gaokun_ucsi_altmode_notify_ind(uec);
}
}
static int gaokun_ucsi_notify(struct notifier_block *nb,
unsigned long action, void *data)
{
u32 cci;
struct gaokun_ucsi *uec = container_of(nb, struct gaokun_ucsi, nb);
switch (action) {
case EC_EVENT_USB:
gaokun_ucsi_complete_usb_ack(uec);
gaokun_ucsi_altmode_notify_ind(uec);
return NOTIFY_OK;
case EC_EVENT_UCSI:
gaokun_ucsi_read_cci(uec->ucsi, &cci);
ucsi_notify_common(uec->ucsi, cci);
if (UCSI_CCI_CONNECTOR(cci))
gaokun_ucsi_handle_no_usb_event(uec, UCSI_CCI_CONNECTOR(cci) - 1);
return NOTIFY_OK;
default:
return NOTIFY_DONE;
}
}
static int gaokun_ucsi_ports_init(struct gaokun_ucsi *uec)
{
struct gaokun_ucsi_port *ucsi_port;
struct gaokun_ucsi_reg ureg = {};
struct device *dev = uec->dev;
struct fwnode_handle *fwnode;
int i, ret, num_ports;
u32 port;
gaokun_ec_ucsi_get_reg(uec->ec, &ureg);
num_ports = ureg.num_ports;
uec->ports = devm_kcalloc(dev, num_ports, sizeof(*uec->ports),
GFP_KERNEL);
if (!uec->ports)
return -ENOMEM;
for (i = 0; i < num_ports; ++i) {
ucsi_port = &uec->ports[i];
ucsi_port->ccx = USBC_CCX_NONE;
ucsi_port->idx = i;
ucsi_port->ucsi = uec;
init_completion(&ucsi_port->usb_ack);
spin_lock_init(&ucsi_port->lock);
}
device_for_each_child_node(dev, fwnode) {
ret = fwnode_property_read_u32(fwnode, "reg", &port);
if (ret < 0) {
dev_err(dev, "missing reg property of %pOFn\n", fwnode);
fwnode_handle_put(fwnode);
return ret;
}
if (port >= num_ports) {
dev_warn(dev, "invalid connector number %d, ignoring\n", port);
continue;
}
ucsi_port = &uec->ports[port];
ucsi_port->bridge = devm_drm_dp_hpd_bridge_alloc(dev, to_of_node(fwnode));
if (IS_ERR(ucsi_port->bridge)) {
fwnode_handle_put(fwnode);
return PTR_ERR(ucsi_port->bridge);
}
}
for (i = 0; i < num_ports; i++) {
if (!uec->ports[i].bridge)
continue;
ret = devm_drm_dp_hpd_bridge_add(dev, uec->ports[i].bridge);
if (ret)
return ret;
}
return 0;
}
static void gaokun_ucsi_register_worker(struct work_struct *work)
{
struct gaokun_ucsi *uec;
struct ucsi *ucsi;
int ret;
uec = container_of(work, struct gaokun_ucsi, work.work);
ucsi = uec->ucsi;
ret = gaokun_ec_register_notify(uec->ec, &uec->nb);
if (ret) {
dev_err_probe(ucsi->dev, ret, "notifier register failed\n");
return;
}
ret = ucsi_register(ucsi);
if (ret)
dev_err_probe(ucsi->dev, ret, "ucsi register failed\n");
}
static int gaokun_ucsi_probe(struct auxiliary_device *adev,
const struct auxiliary_device_id *id)
{
struct gaokun_ec *ec = adev->dev.platform_data;
struct device *dev = &adev->dev;
struct gaokun_ucsi *uec;
int ret;
uec = devm_kzalloc(dev, sizeof(*uec), GFP_KERNEL);
if (!uec)
return -ENOMEM;
uec->ec = ec;
uec->dev = dev;
uec->version = UCSI_VERSION_1_0;
uec->nb.notifier_call = gaokun_ucsi_notify;
INIT_DELAYED_WORK(&uec->work, gaokun_ucsi_register_worker);
ret = gaokun_ucsi_ports_init(uec);
if (ret)
return ret;
uec->ucsi = ucsi_create(dev, &gaokun_ucsi_ops);
if (IS_ERR(uec->ucsi))
return PTR_ERR(uec->ucsi);
ucsi_set_drvdata(uec->ucsi, uec);
auxiliary_set_drvdata(adev, uec);
/* EC can't handle UCSI properly in the early stage */
schedule_delayed_work(&uec->work, 3 * HZ);
return 0;
}
static void gaokun_ucsi_remove(struct auxiliary_device *adev)
{
struct gaokun_ucsi *uec = auxiliary_get_drvdata(adev);
gaokun_ec_unregister_notify(uec->ec, &uec->nb);
ucsi_unregister(uec->ucsi);
ucsi_destroy(uec->ucsi);
}
static const struct auxiliary_device_id gaokun_ucsi_id_table[] = {
{ .name = GAOKUN_MOD_NAME "." GAOKUN_DEV_UCSI, },
{}
};
MODULE_DEVICE_TABLE(auxiliary, gaokun_ucsi_id_table);
static struct auxiliary_driver gaokun_ucsi_driver = {
.name = GAOKUN_DEV_UCSI,
.id_table = gaokun_ucsi_id_table,
.probe = gaokun_ucsi_probe,
.remove = gaokun_ucsi_remove,
};
module_auxiliary_driver(gaokun_ucsi_driver);
MODULE_DESCRIPTION("HUAWEI Matebook E Go UCSI driver");
MODULE_LICENSE("GPL");