提交 c1b0bc2d 编写于 作者: H Heikki Krogerus 提交者: Greg Kroah-Hartman

usb: typec: Add support for UCSI interface

UCSI - USB Type-C Connector System Software Interface - is a
specification that defines set of registers and data
structures for controlling the USB Type-C ports. It's
designed for systems where an embedded controller (EC) is in
charge of the USB Type-C PHY or USB Power Delivery
controller. It is designed for systems with EC, but it is
not limited to them, and for example some USB Power Delivery
controllers will use it as their direct control interface.

With UCSI the EC (or USB PD controller) acts as the port
manager, implementing all USB Type-C and Power Delivery state
machines. The OS can use the interfaces for reading the
status of the ports and controlling basic operations like
role swapping.

The UCSI specification highlights the fact that it does not
define the interface method (PCI/I2C/ACPI/etc.).
Therefore the driver is implemented as library and every
supported interface method needs its own driver. Driver for
ACPI is provided in separate patch following this one.

The initial driver includes support for all required
features from UCSI specification version 1.0 (getting
connector capabilities and status, and support for power and
data role swapping), but none of the optional UCSI features
(alternate modes, power source capabilities, and cable
capabilities).
Signed-off-by: NHeikki Krogerus <heikki.krogerus@linux.intel.com>
Reviewed-by: NGuenter Roeck <linux@roeck-us.net>
Signed-off-by: NGreg Kroah-Hartman <gregkh@linuxfoundation.org>
上级 c68bb0ef
...@@ -19,4 +19,6 @@ config TYPEC_WCOVE ...@@ -19,4 +19,6 @@ config TYPEC_WCOVE
To compile this driver as module, choose M here: the module will be To compile this driver as module, choose M here: the module will be
called typec_wcove called typec_wcove
source "drivers/usb/typec/ucsi/Kconfig"
endmenu endmenu
obj-$(CONFIG_TYPEC) += typec.o obj-$(CONFIG_TYPEC) += typec.o
obj-$(CONFIG_TYPEC_WCOVE) += typec_wcove.o obj-$(CONFIG_TYPEC_WCOVE) += typec_wcove.o
obj-$(CONFIG_TYPEC_UCSI) += ucsi/
config TYPEC_UCSI
tristate "USB Type-C Connector System Software Interface driver"
depends on !CPU_BIG_ENDIAN
select TYPEC
help
USB Type-C Connector System Software Interface (UCSI) is a
specification for an interface that allows the operating system to
control the USB Type-C ports. On UCSI system the USB Type-C ports
function autonomously by default, but in order to get the status of
the ports and support basic operations like role swapping, the driver
is required. UCSI is available on most of the new Intel based systems
that are equipped with Embedded Controller and USB Type-C ports.
UCSI specification does not define the interface method, so depending
on the platform, ACPI, PCI, I2C, etc. may be used. Therefore this
driver only provides the core part, and separate drivers are needed
for every supported interface method.
The UCSI specification can be downloaded from:
http://www.intel.com/content/www/us/en/io/universal-serial-bus/usb-type-c-ucsi-spec.html
To compile the driver as a module, choose M here: the module will be
called typec_ucsi.
CFLAGS_trace.o := -I$(src)
obj-$(CONFIG_TYPEC_UCSI) += typec_ucsi.o
typec_ucsi-y := ucsi.o
typec_ucsi-$(CONFIG_FTRACE) += trace.o
#ifndef __UCSI_DEBUG_H
#define __UCSI_DEBUG_H
#include "ucsi.h"
static const char * const ucsi_cmd_strs[] = {
[0] = "Unknown command",
[UCSI_PPM_RESET] = "PPM_RESET",
[UCSI_CANCEL] = "CANCEL",
[UCSI_CONNECTOR_RESET] = "CONNECTOR_RESET",
[UCSI_ACK_CC_CI] = "ACK_CC_CI",
[UCSI_SET_NOTIFICATION_ENABLE] = "SET_NOTIFICATION_ENABLE",
[UCSI_GET_CAPABILITY] = "GET_CAPABILITY",
[UCSI_GET_CONNECTOR_CAPABILITY] = "GET_CONNECTOR_CAPABILITY",
[UCSI_SET_UOM] = "SET_UOM",
[UCSI_SET_UOR] = "SET_UOR",
[UCSI_SET_PDM] = "SET_PDM",
[UCSI_SET_PDR] = "SET_PDR",
[UCSI_GET_ALTERNATE_MODES] = "GET_ALTERNATE_MODES",
[UCSI_GET_CAM_SUPPORTED] = "GET_CAM_SUPPORTED",
[UCSI_GET_CURRENT_CAM] = "GET_CURRENT_CAM",
[UCSI_SET_NEW_CAM] = "SET_NEW_CAM",
[UCSI_GET_PDOS] = "GET_PDOS",
[UCSI_GET_CABLE_PROPERTY] = "GET_CABLE_PROPERTY",
[UCSI_GET_CONNECTOR_STATUS] = "GET_CONNECTOR_STATUS",
[UCSI_GET_ERROR_STATUS] = "GET_ERROR_STATUS",
};
static inline const char *ucsi_cmd_str(u64 raw_cmd)
{
u8 cmd = raw_cmd & GENMASK(7, 0);
return ucsi_cmd_strs[(cmd >= ARRAY_SIZE(ucsi_cmd_strs)) ? 0 : cmd];
}
static const char * const ucsi_ack_strs[] = {
[0] = "",
[UCSI_ACK_EVENT] = "event",
[UCSI_ACK_CMD] = "command",
};
static inline const char *ucsi_ack_str(u8 ack)
{
return ucsi_ack_strs[(ack >= ARRAY_SIZE(ucsi_ack_strs)) ? 0 : ack];
}
static inline const char *ucsi_cci_str(u32 cci)
{
if (cci & GENMASK(7, 0)) {
if (cci & BIT(29))
return "Event pending (ACK completed)";
if (cci & BIT(31))
return "Event pending (command completed)";
return "Connector Change";
}
if (cci & BIT(29))
return "ACK completed";
if (cci & BIT(31))
return "Command completed";
return "";
}
#endif /* __UCSI_DEBUG_H */
#define CREATE_TRACE_POINTS
#include "trace.h"
#undef TRACE_SYSTEM
#define TRACE_SYSTEM ucsi
#if !defined(__UCSI_TRACE_H) || defined(TRACE_HEADER_MULTI_READ)
#define __UCSI_TRACE_H
#include <linux/tracepoint.h>
#include "ucsi.h"
#include "debug.h"
DECLARE_EVENT_CLASS(ucsi_log_ack,
TP_PROTO(u8 ack),
TP_ARGS(ack),
TP_STRUCT__entry(
__field(u8, ack)
),
TP_fast_assign(
__entry->ack = ack;
),
TP_printk("ACK %s", ucsi_ack_str(__entry->ack))
);
DEFINE_EVENT(ucsi_log_ack, ucsi_ack,
TP_PROTO(u8 ack),
TP_ARGS(ack)
);
DECLARE_EVENT_CLASS(ucsi_log_control,
TP_PROTO(struct ucsi_control *ctrl),
TP_ARGS(ctrl),
TP_STRUCT__entry(
__field(u64, ctrl)
),
TP_fast_assign(
__entry->ctrl = ctrl->raw_cmd;
),
TP_printk("control=%08llx (%s)", __entry->ctrl,
ucsi_cmd_str(__entry->ctrl))
);
DEFINE_EVENT(ucsi_log_control, ucsi_command,
TP_PROTO(struct ucsi_control *ctrl),
TP_ARGS(ctrl)
);
DECLARE_EVENT_CLASS(ucsi_log_command,
TP_PROTO(struct ucsi_control *ctrl, int ret),
TP_ARGS(ctrl, ret),
TP_STRUCT__entry(
__field(u64, ctrl)
__field(int, ret)
),
TP_fast_assign(
__entry->ctrl = ctrl->raw_cmd;
__entry->ret = ret;
),
TP_printk("%s -> %s (err=%d)", ucsi_cmd_str(__entry->ctrl),
__entry->ret < 0 ? "FAIL" : "OK",
__entry->ret < 0 ? __entry->ret : 0)
);
DEFINE_EVENT(ucsi_log_command, ucsi_run_command,
TP_PROTO(struct ucsi_control *ctrl, int ret),
TP_ARGS(ctrl, ret)
);
DEFINE_EVENT(ucsi_log_command, ucsi_reset_ppm,
TP_PROTO(struct ucsi_control *ctrl, int ret),
TP_ARGS(ctrl, ret)
);
DECLARE_EVENT_CLASS(ucsi_log_cci,
TP_PROTO(u32 cci),
TP_ARGS(cci),
TP_STRUCT__entry(
__field(u32, cci)
),
TP_fast_assign(
__entry->cci = cci;
),
TP_printk("CCI=%08x %s", __entry->cci, ucsi_cci_str(__entry->cci))
);
DEFINE_EVENT(ucsi_log_cci, ucsi_notify,
TP_PROTO(u32 cci),
TP_ARGS(cci)
);
DECLARE_EVENT_CLASS(ucsi_log_connector_status,
TP_PROTO(int port, struct ucsi_connector_status *status),
TP_ARGS(port, status),
TP_STRUCT__entry(
__field(int, port)
__field(u16, change)
__field(u8, opmode)
__field(u8, connected)
__field(u8, pwr_dir)
__field(u8, partner_flags)
__field(u8, partner_type)
__field(u32, request_data_obj)
__field(u8, bc_status)
),
TP_fast_assign(
__entry->port = port - 1;
__entry->change = status->change;
__entry->opmode = status->pwr_op_mode;
__entry->connected = status->connected;
__entry->pwr_dir = status->pwr_dir;
__entry->partner_flags = status->partner_flags;
__entry->partner_type = status->partner_type;
__entry->request_data_obj = status->request_data_obj;
__entry->bc_status = status->bc_status;
),
TP_printk("port%d status: change=%04x, opmode=%x, connected=%d, "
"sourcing=%d, partner_flags=%x, partner_type=%x, "
"request_data_obj=%08x, BC status=%x", __entry->port,
__entry->change, __entry->opmode, __entry->connected,
__entry->pwr_dir, __entry->partner_flags, __entry->partner_type,
__entry->request_data_obj, __entry->bc_status)
);
DEFINE_EVENT(ucsi_log_connector_status, ucsi_connector_change,
TP_PROTO(int port, struct ucsi_connector_status *status),
TP_ARGS(port, status)
);
DEFINE_EVENT(ucsi_log_connector_status, ucsi_register_port,
TP_PROTO(int port, struct ucsi_connector_status *status),
TP_ARGS(port, status)
);
#endif /* __UCSI_TRACE_H */
/* This part must be outside protection */
#undef TRACE_INCLUDE_PATH
#define TRACE_INCLUDE_PATH .
#undef TRACE_INCLUDE_FILE
#define TRACE_INCLUDE_FILE trace
#include <trace/define_trace.h>
/*
* USB Type-C Connector System Software Interface driver
*
* Copyright (C) 2017, Intel Corporation
* Author: Heikki Krogerus <heikki.krogerus@linux.intel.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*/
#include <linux/completion.h>
#include <linux/property.h>
#include <linux/device.h>
#include <linux/module.h>
#include <linux/delay.h>
#include <linux/slab.h>
#include <linux/usb/typec.h>
#include "ucsi.h"
#include "trace.h"
#define to_ucsi_connector(_cap_) container_of(_cap_, struct ucsi_connector, \
typec_cap)
/*
* UCSI_TIMEOUT_MS - PPM communication timeout
*
* Ideally we could use MIN_TIME_TO_RESPOND_WITH_BUSY (which is defined in UCSI
* specification) here as reference, but unfortunately we can't. It is very
* difficult to estimate the time it takes for the system to process the command
* before it is actually passed to the PPM.
*/
#define UCSI_TIMEOUT_MS 1000
/*
* UCSI_SWAP_TIMEOUT_MS - Timeout for role swap requests
*
* 5 seconds is close to the time it takes for CapsCounter to reach 0, so even
* if the PPM does not generate Connector Change events before that with
* partners that do not support USB Power Delivery, this should still work.
*/
#define UCSI_SWAP_TIMEOUT_MS 5000
enum ucsi_status {
UCSI_IDLE = 0,
UCSI_BUSY,
UCSI_ERROR,
};
struct ucsi_connector {
int num;
struct ucsi *ucsi;
struct work_struct work;
struct completion complete;
struct typec_port *port;
struct typec_partner *partner;
struct typec_capability typec_cap;
struct ucsi_connector_status status;
struct ucsi_connector_capability cap;
};
struct ucsi {
struct device *dev;
struct ucsi_ppm *ppm;
enum ucsi_status status;
struct completion complete;
struct ucsi_capability cap;
struct ucsi_connector *connector;
struct work_struct work;
/* PPM Communication lock */
struct mutex ppm_lock;
/* PPM communication flags */
unsigned long flags;
#define EVENT_PENDING 0
#define COMMAND_PENDING 1
#define ACK_PENDING 2
};
static inline int ucsi_sync(struct ucsi *ucsi)
{
if (ucsi->ppm && ucsi->ppm->sync)
return ucsi->ppm->sync(ucsi->ppm);
return 0;
}
static int ucsi_command(struct ucsi *ucsi, struct ucsi_control *ctrl)
{
int ret;
trace_ucsi_command(ctrl);
set_bit(COMMAND_PENDING, &ucsi->flags);
ret = ucsi->ppm->cmd(ucsi->ppm, ctrl);
if (ret)
goto err_clear_flag;
if (!wait_for_completion_timeout(&ucsi->complete,
msecs_to_jiffies(UCSI_TIMEOUT_MS))) {
dev_warn(ucsi->dev, "PPM NOT RESPONDING\n");
ret = -ETIMEDOUT;
}
err_clear_flag:
clear_bit(COMMAND_PENDING, &ucsi->flags);
return ret;
}
static int ucsi_ack(struct ucsi *ucsi, u8 ack)
{
struct ucsi_control ctrl;
int ret;
trace_ucsi_ack(ack);
set_bit(ACK_PENDING, &ucsi->flags);
UCSI_CMD_ACK(ctrl, ack);
ret = ucsi->ppm->cmd(ucsi->ppm, &ctrl);
if (ret)
goto out_clear_bit;
/* Waiting for ACK with ACK CMD, but not with EVENT for now */
if (ack == UCSI_ACK_EVENT)
goto out_clear_bit;
if (!wait_for_completion_timeout(&ucsi->complete,
msecs_to_jiffies(UCSI_TIMEOUT_MS)))
ret = -ETIMEDOUT;
out_clear_bit:
clear_bit(ACK_PENDING, &ucsi->flags);
if (ret)
dev_err(ucsi->dev, "%s: failed\n", __func__);
return ret;
}
static int ucsi_run_command(struct ucsi *ucsi, struct ucsi_control *ctrl,
void *data, size_t size)
{
struct ucsi_control _ctrl;
u8 data_length;
u16 error;
int ret;
ret = ucsi_command(ucsi, ctrl);
if (ret)
goto err;
switch (ucsi->status) {
case UCSI_IDLE:
ret = ucsi_sync(ucsi);
if (ret)
dev_warn(ucsi->dev, "%s: sync failed\n", __func__);
if (data)
memcpy(data, ucsi->ppm->data->message_in, size);
data_length = ucsi->ppm->data->cci.data_length;
ret = ucsi_ack(ucsi, UCSI_ACK_CMD);
if (!ret)
ret = data_length;
break;
case UCSI_BUSY:
/* The caller decides whether to cancel or not */
ret = -EBUSY;
break;
case UCSI_ERROR:
ret = ucsi_ack(ucsi, UCSI_ACK_CMD);
if (ret)
break;
_ctrl.raw_cmd = 0;
_ctrl.cmd.cmd = UCSI_GET_ERROR_STATUS;
ret = ucsi_command(ucsi, &_ctrl);
if (ret) {
dev_err(ucsi->dev, "reading error failed!\n");
break;
}
memcpy(&error, ucsi->ppm->data->message_in, sizeof(error));
/* Something has really gone wrong */
if (WARN_ON(ucsi->status == UCSI_ERROR)) {
ret = -ENODEV;
break;
}
ret = ucsi_ack(ucsi, UCSI_ACK_CMD);
if (ret)
break;
switch (error) {
case UCSI_ERROR_INCOMPATIBLE_PARTNER:
ret = -EOPNOTSUPP;
break;
case UCSI_ERROR_CC_COMMUNICATION_ERR:
ret = -ECOMM;
break;
case UCSI_ERROR_CONTRACT_NEGOTIATION_FAIL:
ret = -EPROTO;
break;
case UCSI_ERROR_DEAD_BATTERY:
dev_warn(ucsi->dev, "Dead battery condition!\n");
ret = -EPERM;
break;
/* The following mean a bug in this driver */
case UCSI_ERROR_INVALID_CON_NUM:
case UCSI_ERROR_UNREGONIZED_CMD:
case UCSI_ERROR_INVALID_CMD_ARGUMENT:
dev_warn(ucsi->dev,
"%s: possible UCSI driver bug - error 0x%x\n",
__func__, error);
ret = -EINVAL;
break;
default:
dev_warn(ucsi->dev,
"%s: error without status\n", __func__);
ret = -EIO;
break;
}
break;
}
err:
trace_ucsi_run_command(ctrl, ret);
return ret;
}
/* -------------------------------------------------------------------------- */
static void ucsi_pwr_opmode_change(struct ucsi_connector *con)
{
switch (con->status.pwr_op_mode) {
case UCSI_CONSTAT_PWR_OPMODE_PD:
typec_set_pwr_opmode(con->port, TYPEC_PWR_MODE_PD);
break;
case UCSI_CONSTAT_PWR_OPMODE_TYPEC1_5:
typec_set_pwr_opmode(con->port, TYPEC_PWR_MODE_1_5A);
break;
case UCSI_CONSTAT_PWR_OPMODE_TYPEC3_0:
typec_set_pwr_opmode(con->port, TYPEC_PWR_MODE_3_0A);
break;
default:
typec_set_pwr_opmode(con->port, TYPEC_PWR_MODE_USB);
break;
}
}
static int ucsi_register_partner(struct ucsi_connector *con)
{
struct typec_partner_desc partner;
if (con->partner)
return 0;
memset(&partner, 0, sizeof(partner));
switch (con->status.partner_type) {
case UCSI_CONSTAT_PARTNER_TYPE_DEBUG:
partner.accessory = TYPEC_ACCESSORY_DEBUG;
break;
case UCSI_CONSTAT_PARTNER_TYPE_AUDIO:
partner.accessory = TYPEC_ACCESSORY_AUDIO;
break;
default:
break;
}
partner.usb_pd = con->status.pwr_op_mode == UCSI_CONSTAT_PWR_OPMODE_PD;
con->partner = typec_register_partner(con->port, &partner);
if (!con->partner) {
dev_err(con->ucsi->dev, "con%d: failed to register partner\n",
con->num);
return -ENODEV;
}
return 0;
}
static void ucsi_unregister_partner(struct ucsi_connector *con)
{
typec_unregister_partner(con->partner);
con->partner = NULL;
}
static void ucsi_connector_change(struct work_struct *work)
{
struct ucsi_connector *con = container_of(work, struct ucsi_connector,
work);
struct ucsi *ucsi = con->ucsi;
struct ucsi_control ctrl;
int ret;
mutex_lock(&ucsi->ppm_lock);
UCSI_CMD_GET_CONNECTOR_STATUS(ctrl, con->num);
ret = ucsi_run_command(ucsi, &ctrl, &con->status, sizeof(con->status));
if (ret < 0) {
dev_err(ucsi->dev, "%s: GET_CONNECTOR_STATUS failed (%d)\n",
__func__, ret);
goto out_unlock;
}
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, con->status.pwr_dir);
/* Complete pending power role swap */
if (!completion_done(&con->complete))
complete(&con->complete);
}
if (con->status.change & UCSI_CONSTAT_PARTNER_CHANGE) {
switch (con->status.partner_type) {
case UCSI_CONSTAT_PARTNER_TYPE_UFP:
typec_set_data_role(con->port, TYPEC_HOST);
break;
case UCSI_CONSTAT_PARTNER_TYPE_DFP:
typec_set_data_role(con->port, TYPEC_DEVICE);
break;
default:
break;
}
/* Complete pending data role swap */
if (!completion_done(&con->complete))
complete(&con->complete);
}
if (con->status.change & UCSI_CONSTAT_CONNECT_CHANGE) {
if (con->status.connected)
ucsi_register_partner(con);
else
ucsi_unregister_partner(con);
}
ret = ucsi_ack(ucsi, UCSI_ACK_EVENT);
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(&ucsi->ppm_lock);
}
/**
* ucsi_notify - PPM notification handler
* @ucsi: Source UCSI Interface for the notifications
*
* Handle notifications from PPM of @ucsi.
*/
void ucsi_notify(struct ucsi *ucsi)
{
struct ucsi_cci *cci;
/* There is no requirement to sync here, but no harm either. */
ucsi_sync(ucsi);
cci = &ucsi->ppm->data->cci;
if (cci->error)
ucsi->status = UCSI_ERROR;
else if (cci->busy)
ucsi->status = UCSI_BUSY;
else
ucsi->status = UCSI_IDLE;
if (cci->cmd_complete && test_bit(COMMAND_PENDING, &ucsi->flags)) {
complete(&ucsi->complete);
} else if (cci->ack_complete && test_bit(ACK_PENDING, &ucsi->flags)) {
complete(&ucsi->complete);
} else if (cci->connector_change) {
struct ucsi_connector *con;
con = &ucsi->connector[cci->connector_change - 1];
if (!test_and_set_bit(EVENT_PENDING, &ucsi->flags))
schedule_work(&con->work);
}
trace_ucsi_notify(ucsi->ppm->data->raw_cci);
}
EXPORT_SYMBOL_GPL(ucsi_notify);
/* -------------------------------------------------------------------------- */
static int ucsi_reset_connector(struct ucsi_connector *con, bool hard)
{
struct ucsi_control ctrl;
UCSI_CMD_CONNECTOR_RESET(ctrl, con, hard);
return ucsi_run_command(con->ucsi, &ctrl, NULL, 0);
}
static int ucsi_reset_ppm(struct ucsi *ucsi)
{
struct ucsi_control ctrl;
unsigned long tmo;
int ret;
ctrl.raw_cmd = 0;
ctrl.cmd.cmd = UCSI_PPM_RESET;
trace_ucsi_command(&ctrl);
ret = ucsi->ppm->cmd(ucsi->ppm, &ctrl);
if (ret)
goto err;
tmo = jiffies + msecs_to_jiffies(UCSI_TIMEOUT_MS);
do {
/* Here sync is critical. */
ret = ucsi_sync(ucsi);
if (ret)
goto err;
if (ucsi->ppm->data->cci.reset_complete)
break;
/* If the PPM is still doing something else, reset it again. */
if (ucsi->ppm->data->raw_cci) {
dev_warn_ratelimited(ucsi->dev,
"Failed to reset PPM! Trying again..\n");
trace_ucsi_command(&ctrl);
ret = ucsi->ppm->cmd(ucsi->ppm, &ctrl);
if (ret)
goto err;
}
/* Letting the PPM settle down. */
msleep(20);
ret = -ETIMEDOUT;
} while (time_is_after_jiffies(tmo));
err:
trace_ucsi_reset_ppm(&ctrl, ret);
return ret;
}
static int ucsi_role_cmd(struct ucsi_connector *con, struct ucsi_control *ctrl)
{
int ret;
ret = ucsi_run_command(con->ucsi, ctrl, NULL, 0);
if (ret == -ETIMEDOUT) {
struct ucsi_control c;
/* PPM most likely stopped responding. Resetting everything. */
ucsi_reset_ppm(con->ucsi);
UCSI_CMD_SET_NTFY_ENABLE(c, UCSI_ENABLE_NTFY_ALL);
ucsi_run_command(con->ucsi, &c, NULL, 0);
ucsi_reset_connector(con, true);
}
return ret;
}
static int
ucsi_dr_swap(const struct typec_capability *cap, enum typec_data_role role)
{
struct ucsi_connector *con = to_ucsi_connector(cap);
struct ucsi_control ctrl;
int ret = 0;
if (!con->partner)
return -ENOTCONN;
mutex_lock(&con->ucsi->ppm_lock);
if ((con->status.partner_type == UCSI_CONSTAT_PARTNER_TYPE_DFP &&
role == TYPEC_DEVICE) ||
(con->status.partner_type == UCSI_CONSTAT_PARTNER_TYPE_UFP &&
role == TYPEC_HOST))
goto out_unlock;
UCSI_CMD_SET_UOR(ctrl, con, role);
ret = ucsi_role_cmd(con, &ctrl);
if (ret < 0)
goto out_unlock;
mutex_unlock(&con->ucsi->ppm_lock);
if (!wait_for_completion_timeout(&con->complete,
msecs_to_jiffies(UCSI_SWAP_TIMEOUT_MS)))
return -ETIMEDOUT;
return 0;
out_unlock:
mutex_unlock(&con->ucsi->ppm_lock);
return ret;
}
static int
ucsi_pr_swap(const struct typec_capability *cap, enum typec_role role)
{
struct ucsi_connector *con = to_ucsi_connector(cap);
struct ucsi_control ctrl;
int ret = 0;
if (!con->partner)
return -ENOTCONN;
mutex_lock(&con->ucsi->ppm_lock);
if (con->status.pwr_dir == role)
goto out_unlock;
UCSI_CMD_SET_PDR(ctrl, con, role);
ret = ucsi_role_cmd(con, &ctrl);
if (ret < 0)
goto out_unlock;
mutex_unlock(&con->ucsi->ppm_lock);
if (!wait_for_completion_timeout(&con->complete,
msecs_to_jiffies(UCSI_SWAP_TIMEOUT_MS)))
return -ETIMEDOUT;
mutex_lock(&con->ucsi->ppm_lock);
/* Something has gone wrong while swapping the role */
if (con->status.pwr_op_mode != UCSI_CONSTAT_PWR_OPMODE_PD) {
ucsi_reset_connector(con, true);
ret = -EPROTO;
}
out_unlock:
mutex_unlock(&con->ucsi->ppm_lock);
return ret;
}
static struct fwnode_handle *ucsi_find_fwnode(struct ucsi_connector *con)
{
struct fwnode_handle *fwnode;
int i = 1;
device_for_each_child_node(con->ucsi->dev, fwnode)
if (i++ == con->num)
return fwnode;
return NULL;
}
static int ucsi_register_port(struct ucsi *ucsi, int index)
{
struct ucsi_connector *con = &ucsi->connector[index];
struct typec_capability *cap = &con->typec_cap;
enum typec_accessory *accessory = cap->accessory;
struct ucsi_control ctrl;
int ret;
INIT_WORK(&con->work, ucsi_connector_change);
init_completion(&con->complete);
con->num = index + 1;
con->ucsi = ucsi;
/* Get connector capability */
UCSI_CMD_GET_CONNECTOR_CAPABILITY(ctrl, con->num);
ret = ucsi_run_command(ucsi, &ctrl, &con->cap, sizeof(con->cap));
if (ret < 0)
return ret;
if (con->cap.op_mode & UCSI_CONCAP_OPMODE_DRP)
cap->type = TYPEC_PORT_DRP;
else if (con->cap.op_mode & UCSI_CONCAP_OPMODE_DFP)
cap->type = TYPEC_PORT_DFP;
else if (con->cap.op_mode & UCSI_CONCAP_OPMODE_UFP)
cap->type = TYPEC_PORT_UFP;
cap->revision = ucsi->cap.typec_version;
cap->pd_revision = ucsi->cap.pd_version;
cap->prefer_role = TYPEC_NO_PREFERRED_ROLE;
if (con->cap.op_mode & UCSI_CONCAP_OPMODE_AUDIO_ACCESSORY)
*accessory++ = TYPEC_ACCESSORY_AUDIO;
if (con->cap.op_mode & UCSI_CONCAP_OPMODE_DEBUG_ACCESSORY)
*accessory = TYPEC_ACCESSORY_DEBUG;
cap->fwnode = ucsi_find_fwnode(con);
cap->dr_set = ucsi_dr_swap;
cap->pr_set = ucsi_pr_swap;
/* Register the connector */
con->port = typec_register_port(ucsi->dev, cap);
if (!con->port)
return -ENODEV;
/* Get the status */
UCSI_CMD_GET_CONNECTOR_STATUS(ctrl, con->num);
ret = ucsi_run_command(ucsi, &ctrl, &con->status, sizeof(con->status));
if (ret < 0) {
dev_err(ucsi->dev, "con%d: failed to get status\n", con->num);
return 0;
}
ucsi_pwr_opmode_change(con);
typec_set_pwr_role(con->port, con->status.pwr_dir);
switch (con->status.partner_type) {
case UCSI_CONSTAT_PARTNER_TYPE_UFP:
typec_set_data_role(con->port, TYPEC_HOST);
break;
case UCSI_CONSTAT_PARTNER_TYPE_DFP:
typec_set_data_role(con->port, TYPEC_DEVICE);
break;
default:
break;
}
/* Check if there is already something connected */
if (con->status.connected)
ucsi_register_partner(con);
trace_ucsi_register_port(con->num, &con->status);
return 0;
}
static void ucsi_init(struct work_struct *work)
{
struct ucsi *ucsi = container_of(work, struct ucsi, work);
struct ucsi_connector *con;
struct ucsi_control ctrl;
int ret;
int i;
mutex_lock(&ucsi->ppm_lock);
/* Reset the PPM */
ret = ucsi_reset_ppm(ucsi);
if (ret) {
dev_err(ucsi->dev, "failed to reset PPM!\n");
goto err;
}
/* Enable basic notifications */
UCSI_CMD_SET_NTFY_ENABLE(ctrl, UCSI_ENABLE_NTFY_CMD_COMPLETE |
UCSI_ENABLE_NTFY_ERROR);
ret = ucsi_run_command(ucsi, &ctrl, NULL, 0);
if (ret < 0)
goto err_reset;
/* Get PPM capabilities */
UCSI_CMD_GET_CAPABILITY(ctrl);
ret = ucsi_run_command(ucsi, &ctrl, &ucsi->cap, sizeof(ucsi->cap));
if (ret < 0)
goto err_reset;
if (!ucsi->cap.num_connectors) {
ret = -ENODEV;
goto err_reset;
}
/* Allocate the connectors. Released in ucsi_unregister_ppm() */
ucsi->connector = kcalloc(ucsi->cap.num_connectors + 1,
sizeof(*ucsi->connector), GFP_KERNEL);
if (!ucsi->connector) {
ret = -ENOMEM;
goto err_reset;
}
/* Register all connectors */
for (i = 0; i < ucsi->cap.num_connectors; i++) {
ret = ucsi_register_port(ucsi, i);
if (ret)
goto err_unregister;
}
/* Enable all notifications */
UCSI_CMD_SET_NTFY_ENABLE(ctrl, UCSI_ENABLE_NTFY_ALL);
ret = ucsi_run_command(ucsi, &ctrl, NULL, 0);
if (ret < 0)
goto err_unregister;
mutex_unlock(&ucsi->ppm_lock);
return;
err_unregister:
for (con = ucsi->connector; con->port; con++) {
ucsi_unregister_partner(con);
typec_unregister_port(con->port);
con->port = NULL;
}
err_reset:
ucsi_reset_ppm(ucsi);
err:
mutex_unlock(&ucsi->ppm_lock);
dev_err(ucsi->dev, "PPM init failed (%d)\n", ret);
}
/**
* ucsi_register_ppm - Register UCSI PPM Interface
* @dev: Device interface to the PPM
* @ppm: The PPM interface
*
* Allocates UCSI instance, associates it with @ppm and returns it to the
* caller, and schedules initialization of the interface.
*/
struct ucsi *ucsi_register_ppm(struct device *dev, struct ucsi_ppm *ppm)
{
struct ucsi *ucsi;
ucsi = kzalloc(sizeof(*ucsi), GFP_KERNEL);
if (!ucsi)
return ERR_PTR(-ENOMEM);
INIT_WORK(&ucsi->work, ucsi_init);
init_completion(&ucsi->complete);
mutex_init(&ucsi->ppm_lock);
ucsi->dev = dev;
ucsi->ppm = ppm;
/*
* Communication with the PPM takes a lot of time. It is not reasonable
* to initialize the driver here. Using a work for now.
*/
queue_work(system_long_wq, &ucsi->work);
return ucsi;
}
EXPORT_SYMBOL_GPL(ucsi_register_ppm);
/**
* ucsi_unregister_ppm - Unregister UCSI PPM Interface
* @ucsi: struct ucsi associated with the PPM
*
* Unregister UCSI PPM that was created with ucsi_register().
*/
void ucsi_unregister_ppm(struct ucsi *ucsi)
{
struct ucsi_control ctrl;
int i;
/* Make sure that we are not in the middle of driver initialization */
cancel_work_sync(&ucsi->work);
mutex_lock(&ucsi->ppm_lock);
/* Disable everything except command complete notification */
UCSI_CMD_SET_NTFY_ENABLE(ctrl, UCSI_ENABLE_NTFY_CMD_COMPLETE)
ucsi_run_command(ucsi, &ctrl, NULL, 0);
mutex_unlock(&ucsi->ppm_lock);
for (i = 0; i < ucsi->cap.num_connectors; i++) {
cancel_work_sync(&ucsi->connector[i].work);
ucsi_unregister_partner(&ucsi->connector[i]);
typec_unregister_port(ucsi->connector[i].port);
}
ucsi_reset_ppm(ucsi);
kfree(ucsi->connector);
kfree(ucsi);
}
EXPORT_SYMBOL_GPL(ucsi_unregister_ppm);
MODULE_AUTHOR("Heikki Krogerus <heikki.krogerus@linux.intel.com>");
MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("USB Type-C Connector System Software Interface driver");
#ifndef __DRIVER_USB_TYPEC_UCSI_H
#define __DRIVER_USB_TYPEC_UCSI_H
#include <linux/bitops.h>
#include <linux/types.h>
/* -------------------------------------------------------------------------- */
/* Command Status and Connector Change Indication (CCI) data structure */
struct ucsi_cci {
u8:1; /* reserved */
u8 connector_change:7;
u8 data_length;
u16:9; /* reserved */
u16 not_supported:1;
u16 cancel_complete:1;
u16 reset_complete:1;
u16 busy:1;
u16 ack_complete:1;
u16 error:1;
u16 cmd_complete:1;
} __packed;
/* Default fields in CONTROL data structure */
struct ucsi_command {
u8 cmd;
u8 length;
u64 data:48;
} __packed;
/* ACK Command structure */
struct ucsi_ack_cmd {
u8 cmd;
u8 length;
u8 cci_ack:1;
u8 cmd_ack:1;
u8:6; /* reserved */
} __packed;
/* Connector Reset Command structure */
struct ucsi_con_rst {
u8 cmd;
u8 length;
u8 con_num:7;
u8 hard_reset:1;
} __packed;
/* Set USB Operation Mode Command structure */
struct ucsi_uor_cmd {
u8 cmd;
u8 length;
u16 con_num:7;
u16 role:3;
#define UCSI_UOR_ROLE_DFP BIT(0)
#define UCSI_UOR_ROLE_UFP BIT(1)
#define UCSI_UOR_ROLE_DRP BIT(2)
u16:6; /* reserved */
} __packed;
struct ucsi_control {
union {
u64 raw_cmd;
struct ucsi_command cmd;
struct ucsi_uor_cmd uor;
struct ucsi_ack_cmd ack;
struct ucsi_con_rst con_rst;
};
};
#define __UCSI_CMD(_ctrl_, _cmd_) \
{ \
(_ctrl_).raw_cmd = 0; \
(_ctrl_).cmd.cmd = _cmd_; \
}
/* Helper for preparing ucsi_control for CONNECTOR_RESET command. */
#define UCSI_CMD_CONNECTOR_RESET(_ctrl_, _con_, _hard_) \
{ \
__UCSI_CMD(_ctrl_, UCSI_CONNECTOR_RESET) \
(_ctrl_).con_rst.con_num = (_con_)->num; \
(_ctrl_).con_rst.hard_reset = _hard_; \
}
/* Helper for preparing ucsi_control for ACK_CC_CI command. */
#define UCSI_CMD_ACK(_ctrl_, _ack_) \
{ \
__UCSI_CMD(_ctrl_, UCSI_ACK_CC_CI) \
(_ctrl_).ack.cci_ack = ((_ack_) == UCSI_ACK_EVENT); \
(_ctrl_).ack.cmd_ack = ((_ack_) == UCSI_ACK_CMD); \
}
/* Helper for preparing ucsi_control for SET_NOTIFY_ENABLE command. */
#define UCSI_CMD_SET_NTFY_ENABLE(_ctrl_, _ntfys_) \
{ \
__UCSI_CMD(_ctrl_, UCSI_SET_NOTIFICATION_ENABLE) \
(_ctrl_).cmd.data = _ntfys_; \
}
/* Helper for preparing ucsi_control for GET_CAPABILITY command. */
#define UCSI_CMD_GET_CAPABILITY(_ctrl_) \
{ \
__UCSI_CMD(_ctrl_, UCSI_GET_CAPABILITY) \
}
/* Helper for preparing ucsi_control for GET_CONNECTOR_CAPABILITY command. */
#define UCSI_CMD_GET_CONNECTOR_CAPABILITY(_ctrl_, _con_) \
{ \
__UCSI_CMD(_ctrl_, UCSI_GET_CONNECTOR_CAPABILITY) \
(_ctrl_).cmd.data = _con_; \
}
/* Helper for preparing ucsi_control for GET_CONNECTOR_STATUS command. */
#define UCSI_CMD_GET_CONNECTOR_STATUS(_ctrl_, _con_) \
{ \
__UCSI_CMD(_ctrl_, UCSI_GET_CONNECTOR_STATUS) \
(_ctrl_).cmd.data = _con_; \
}
#define __UCSI_ROLE(_ctrl_, _cmd_, _con_num_) \
{ \
__UCSI_CMD(_ctrl_, _cmd_) \
(_ctrl_).uor.con_num = _con_num_; \
(_ctrl_).uor.role = UCSI_UOR_ROLE_DRP; \
}
/* Helper for preparing ucsi_control for SET_UOR command. */
#define UCSI_CMD_SET_UOR(_ctrl_, _con_, _role_) \
{ \
__UCSI_ROLE(_ctrl_, UCSI_SET_UOR, (_con_)->num) \
(_ctrl_).uor.role |= (_role_) == TYPEC_HOST ? UCSI_UOR_ROLE_DFP : \
UCSI_UOR_ROLE_UFP; \
}
/* Helper for preparing ucsi_control for SET_PDR command. */
#define UCSI_CMD_SET_PDR(_ctrl_, _con_, _role_) \
{ \
__UCSI_ROLE(_ctrl_, UCSI_SET_PDR, (_con_)->num) \
(_ctrl_).uor.role |= (_role_) == TYPEC_SOURCE ? UCSI_UOR_ROLE_DFP : \
UCSI_UOR_ROLE_UFP; \
}
/* Commands */
#define UCSI_PPM_RESET 0x01
#define UCSI_CANCEL 0x02
#define UCSI_CONNECTOR_RESET 0x03
#define UCSI_ACK_CC_CI 0x04
#define UCSI_SET_NOTIFICATION_ENABLE 0x05
#define UCSI_GET_CAPABILITY 0x06
#define UCSI_GET_CONNECTOR_CAPABILITY 0x07
#define UCSI_SET_UOM 0x08
#define UCSI_SET_UOR 0x09
#define UCSI_SET_PDM 0x0a
#define UCSI_SET_PDR 0x0b
#define UCSI_GET_ALTERNATE_MODES 0x0c
#define UCSI_GET_CAM_SUPPORTED 0x0d
#define UCSI_GET_CURRENT_CAM 0x0e
#define UCSI_SET_NEW_CAM 0x0f
#define UCSI_GET_PDOS 0x10
#define UCSI_GET_CABLE_PROPERTY 0x11
#define UCSI_GET_CONNECTOR_STATUS 0x12
#define UCSI_GET_ERROR_STATUS 0x13
/* ACK_CC_CI commands */
#define UCSI_ACK_EVENT 1
#define UCSI_ACK_CMD 2
/* Bits for SET_NOTIFICATION_ENABLE command */
#define UCSI_ENABLE_NTFY_CMD_COMPLETE BIT(0)
#define UCSI_ENABLE_NTFY_EXT_PWR_SRC_CHANGE BIT(1)
#define UCSI_ENABLE_NTFY_PWR_OPMODE_CHANGE BIT(2)
#define UCSI_ENABLE_NTFY_CAP_CHANGE BIT(5)
#define UCSI_ENABLE_NTFY_PWR_LEVEL_CHANGE BIT(6)
#define UCSI_ENABLE_NTFY_PD_RESET_COMPLETE BIT(7)
#define UCSI_ENABLE_NTFY_CAM_CHANGE BIT(8)
#define UCSI_ENABLE_NTFY_BAT_STATUS_CHANGE BIT(9)
#define UCSI_ENABLE_NTFY_PARTNER_CHANGE BIT(11)
#define UCSI_ENABLE_NTFY_PWR_DIR_CHANGE BIT(12)
#define UCSI_ENABLE_NTFY_CONNECTOR_CHANGE BIT(14)
#define UCSI_ENABLE_NTFY_ERROR BIT(15)
#define UCSI_ENABLE_NTFY_ALL 0xdbe7
/* Error information returned by PPM in response to GET_ERROR_STATUS command. */
#define UCSI_ERROR_UNREGONIZED_CMD BIT(0)
#define UCSI_ERROR_INVALID_CON_NUM BIT(1)
#define UCSI_ERROR_INVALID_CMD_ARGUMENT BIT(2)
#define UCSI_ERROR_INCOMPATIBLE_PARTNER BIT(3)
#define UCSI_ERROR_CC_COMMUNICATION_ERR BIT(4)
#define UCSI_ERROR_DEAD_BATTERY BIT(5)
#define UCSI_ERROR_CONTRACT_NEGOTIATION_FAIL BIT(6)
/* Data structure filled by PPM in response to GET_CAPABILITY command. */
struct ucsi_capability {
u32 attributes;
#define UCSI_CAP_ATTR_DISABLE_STATE BIT(0)
#define UCSI_CAP_ATTR_BATTERY_CHARGING BIT(1)
#define UCSI_CAP_ATTR_USB_PD BIT(2)
#define UCSI_CAP_ATTR_TYPEC_CURRENT BIT(6)
#define UCSI_CAP_ATTR_POWER_AC_SUPPLY BIT(8)
#define UCSI_CAP_ATTR_POWER_OTHER BIT(10)
#define UCSI_CAP_ATTR_POWER_VBUS BIT(14)
u32 num_connectors:8;
u32 features:24;
#define UCSI_CAP_SET_UOM BIT(0)
#define UCSI_CAP_SET_PDM BIT(1)
#define UCSI_CAP_ALT_MODE_DETAILS BIT(2)
#define UCSI_CAP_ALT_MODE_OVERRIDE BIT(3)
#define UCSI_CAP_PDO_DETAILS BIT(4)
#define UCSI_CAP_CABLE_DETAILS BIT(5)
#define UCSI_CAP_EXT_SUPPLY_NOTIFICATIONS BIT(6)
#define UCSI_CAP_PD_RESET BIT(7)
u8 num_alt_modes;
u8 reserved;
u16 bc_version;
u16 pd_version;
u16 typec_version;
} __packed;
/* Data structure filled by PPM in response to GET_CONNECTOR_CAPABILITY cmd. */
struct ucsi_connector_capability {
u8 op_mode;
#define UCSI_CONCAP_OPMODE_DFP BIT(0)
#define UCSI_CONCAP_OPMODE_UFP BIT(1)
#define UCSI_CONCAP_OPMODE_DRP BIT(2)
#define UCSI_CONCAP_OPMODE_AUDIO_ACCESSORY BIT(3)
#define UCSI_CONCAP_OPMODE_DEBUG_ACCESSORY BIT(4)
#define UCSI_CONCAP_OPMODE_USB2 BIT(5)
#define UCSI_CONCAP_OPMODE_USB3 BIT(6)
#define UCSI_CONCAP_OPMODE_ALT_MODE BIT(7)
u8 provider:1;
u8 consumer:1;
u8:6; /* reserved */
} __packed;
struct ucsi_altmode {
u16 svid;
u32 mid;
} __packed;
/* Data structure filled by PPM in response to GET_CABLE_PROPERTY command. */
struct ucsi_cable_property {
u16 speed_supported;
u8 current_capability;
u8 vbus_in_cable:1;
u8 active_cable:1;
u8 directionality:1;
u8 plug_type:2;
#define UCSI_CABLE_PROPERTY_PLUG_TYPE_A 0
#define UCSI_CABLE_PROPERTY_PLUG_TYPE_B 1
#define UCSI_CABLE_PROPERTY_PLUG_TYPE_C 2
#define UCSI_CABLE_PROPERTY_PLUG_OTHER 3
u8 mode_support:1;
u8:2; /* reserved */
u8 latency:4;
u8:4; /* reserved */
} __packed;
/* Data structure filled by PPM in response to GET_CONNECTOR_STATUS command. */
struct ucsi_connector_status {
u16 change;
#define UCSI_CONSTAT_EXT_SUPPLY_CHANGE BIT(1)
#define UCSI_CONSTAT_POWER_OPMODE_CHANGE BIT(2)
#define UCSI_CONSTAT_PDOS_CHANGE BIT(5)
#define UCSI_CONSTAT_POWER_LEVEL_CHANGE BIT(6)
#define UCSI_CONSTAT_PD_RESET_COMPLETE BIT(7)
#define UCSI_CONSTAT_CAM_CHANGE BIT(8)
#define UCSI_CONSTAT_BC_CHANGE BIT(9)
#define UCSI_CONSTAT_PARTNER_CHANGE BIT(11)
#define UCSI_CONSTAT_POWER_DIR_CHANGE BIT(12)
#define UCSI_CONSTAT_CONNECT_CHANGE BIT(14)
#define UCSI_CONSTAT_ERROR BIT(15)
u16 pwr_op_mode:3;
#define UCSI_CONSTAT_PWR_OPMODE_NONE 0
#define UCSI_CONSTAT_PWR_OPMODE_DEFAULT 1
#define UCSI_CONSTAT_PWR_OPMODE_BC 2
#define UCSI_CONSTAT_PWR_OPMODE_PD 3
#define UCSI_CONSTAT_PWR_OPMODE_TYPEC1_5 4
#define UCSI_CONSTAT_PWR_OPMODE_TYPEC3_0 5
u16 connected:1;
u16 pwr_dir:1;
u16 partner_flags:8;
#define UCSI_CONSTAT_PARTNER_FLAG_USB BIT(0)
#define UCSI_CONSTAT_PARTNER_FLAG_ALT_MODE BIT(1)
u16 partner_type:3;
#define UCSI_CONSTAT_PARTNER_TYPE_DFP 1
#define UCSI_CONSTAT_PARTNER_TYPE_UFP 2
#define UCSI_CONSTAT_PARTNER_TYPE_CABLE 3 /* Powered Cable */
#define UCSI_CONSTAT_PARTNER_TYPE_CABLE_AND_UFP 4 /* Powered Cable */
#define UCSI_CONSTAT_PARTNER_TYPE_DEBUG 5
#define UCSI_CONSTAT_PARTNER_TYPE_AUDIO 6
u32 request_data_obj;
u8 bc_status:2;
#define UCSI_CONSTAT_BC_NOT_CHARGING 0
#define UCSI_CONSTAT_BC_NOMINAL_CHARGING 1
#define UCSI_CONSTAT_BC_SLOW_CHARGING 2
#define UCSI_CONSTAT_BC_TRICKLE_CHARGING 3
u8 provider_cap_limit_reason:4;
#define UCSI_CONSTAT_CAP_PWR_LOWERED 0
#define UCSI_CONSTAT_CAP_PWR_BUDGET_LIMIT 1
u8:2; /* reserved */
} __packed;
/* -------------------------------------------------------------------------- */
struct ucsi;
struct ucsi_data {
u16 version;
u16 reserved;
union {
u32 raw_cci;
struct ucsi_cci cci;
};
struct ucsi_control ctrl;
u32 message_in[4];
u32 message_out[4];
} __packed;
/*
* struct ucsi_ppm - Interface to UCSI Platform Policy Manager
* @data: memory location to the UCSI data structures
* @cmd: UCSI command execution routine
* @sync: Refresh UCSI mailbox (the data structures)
*/
struct ucsi_ppm {
struct ucsi_data *data;
int (*cmd)(struct ucsi_ppm *, struct ucsi_control *);
int (*sync)(struct ucsi_ppm *);
};
struct ucsi *ucsi_register_ppm(struct device *dev, struct ucsi_ppm *ppm);
void ucsi_unregister_ppm(struct ucsi *ucsi);
void ucsi_notify(struct ucsi *ucsi);
#endif /* __DRIVER_USB_TYPEC_UCSI_H */
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册