[PATCH v2 2/2] usb: typec: tcpm: add support for Sink Cap Extended msg response

From: Amit Sunil Dhamne via B4 Relay

Date: Mon Feb 23 2026 - 15:05:55 EST


From: Amit Sunil Dhamne <amitsd@xxxxxxxxxx>

Add support for responding to Sink Cap Extended msg request. To achieve
this, include parsing support for DT properties related to Sink Cap
Extended. The request for Sink Cap Ext is a control message while the
response is an extended message (chunked). As the Sink Caps Extended
Data Block size (24 Byte) is less than MaxExtendedMsgChunkLen (26 Byte),
a single chunk is sufficient to complete this AMS.

Supporting sink cap extended messages while responding to a
Get_Sink_Caps_Extended request when port is in Sink role is required in
order to be compliant with at least USB PD Rev3.1 Ver1.8.

Signed-off-by: Amit Sunil Dhamne <amitsd@xxxxxxxxxx>
Reviewed-by: Badhri Jagan Sridharan <badhri@xxxxxxxxxx>
---
drivers/usb/typec/tcpm/tcpm.c | 253 +++++++++++++++++++++++++++++++++++++++++-
include/linux/usb/pd.h | 82 +++++++++++++-
2 files changed, 332 insertions(+), 3 deletions(-)

diff --git a/drivers/usb/typec/tcpm/tcpm.c b/drivers/usb/typec/tcpm/tcpm.c
index b7828160b81d31294a58aa10578e9d8b84f05ac1..bf32d33c606e2e51421034433b251e918f0571bf 100644
--- a/drivers/usb/typec/tcpm/tcpm.c
+++ b/drivers/usb/typec/tcpm/tcpm.c
@@ -12,6 +12,7 @@
#include <linux/jiffies.h>
#include <linux/kernel.h>
#include <linux/kthread.h>
+#include <linux/minmax.h>
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/power_supply.h>
@@ -188,7 +189,8 @@
S(STRUCTURED_VDMS), \
S(COUNTRY_INFO), \
S(COUNTRY_CODES), \
- S(REVISION_INFORMATION)
+ S(REVISION_INFORMATION), \
+ S(GETTING_SINK_EXTENDED_CAPABILITIES)

#define GENERATE_ENUM(e) e
#define GENERATE_STRING(s) #s
@@ -229,6 +231,7 @@ enum pd_msg_request {
PD_MSG_DATA_SINK_CAP,
PD_MSG_DATA_SOURCE_CAP,
PD_MSG_DATA_REV,
+ PD_MSG_EXT_SINK_CAP_EXT
};

enum adev_actions {
@@ -337,6 +340,42 @@ struct pd_timings {
u32 snk_bc12_cmpletion_time;
};

+/* Convert microwatt to watt */
+#define UW_TO_W(pow) ((pow) / 1000000)
+
+/*
+ * struct pd_identifier - Contains info about PD identifiers
+ * @vid: Vendor ID (assigned by USB-IF)
+ * @pid: Product ID (assigned by manufacturer)
+ * @xid: Value assigned by USB-IF for product
+ */
+struct pd_identifier {
+ u16 vid;
+ u16 pid;
+ u32 xid;
+};
+
+/*
+ * struct sink_caps_ext_data - Sink extended capability data
+ * @load_step: Indicates the load step slew rate. Value of 0 indicates 150mA/us
+ * & 1 indicates 500 mA/us
+ * @load_char: Snk overload characteristics
+ * @compliance: Types of sources the sink has been tested & certified on
+ * @modes: Charging caps & power sources supported
+ * @spr_min_pdp: Sink Minimum PDP for SPR mode (in Watts)
+ * @spr_op_pdp: Sink Operational PDP for SPR mode (in Watts)
+ * @spr_max_pdp: Sink Maximum PDP for SPR mode (in Watts)
+ */
+struct sink_caps_ext_data {
+ u8 load_step;
+ u16 load_char;
+ u8 compliance;
+ u8 modes;
+ u8 spr_min_pdp;
+ u8 spr_op_pdp;
+ u8 spr_max_pdp;
+};
+
struct tcpm_port {
struct device *dev;

@@ -585,6 +624,9 @@ struct tcpm_port {

/* Indicates maximum (revision, version) supported */
struct pd_revision_info pd_rev;
+
+ struct pd_identifier pd_ident;
+ struct sink_caps_ext_data sink_caps_ext;
#ifdef CONFIG_DEBUG_FS
struct dentry *dentry;
struct mutex logbuffer_lock; /* log buffer access lock */
@@ -1367,6 +1409,64 @@ static int tcpm_pd_send_sink_caps(struct tcpm_port *port)
return tcpm_pd_transmit(port, TCPC_TX_SOP, &msg);
}

+static int tcpm_pd_send_sink_cap_ext(struct tcpm_port *port)
+{
+ u16 operating_snk_watt = port->operating_snk_mw / 1000;
+ struct sink_caps_ext_data *data = &port->sink_caps_ext;
+ struct pd_identifier *pd_ident = &port->pd_ident;
+ struct sink_caps_ext_msg skedb = {0};
+ struct pd_message msg;
+ u8 data_obj_cnt;
+
+ if (!port->self_powered)
+ data->spr_op_pdp = operating_snk_watt;
+
+ /*
+ * SPR Sink Minimum PDP indicates the minimum power required to operate
+ * a sink device in its lowest level of functionality without requiring
+ * power from the battery. We can use the operating_snk_watt value to
+ * populate it, as operating_snk_watt indicates device's min operating
+ * power.
+ */
+ data->spr_min_pdp = operating_snk_watt;
+
+ if (data->spr_op_pdp < data->spr_min_pdp ||
+ data->spr_max_pdp < data->spr_op_pdp) {
+ tcpm_log(port,
+ "Invalid PDP values, Min PDP:%u, Op PDP:%u, Max PDP:%u",
+ data->spr_min_pdp, data->spr_op_pdp, data->spr_max_pdp);
+ return -EOPNOTSUPP;
+ }
+
+ memset(&msg, 0, sizeof(msg));
+ skedb.vid = cpu_to_le16(pd_ident->vid);
+ skedb.pid = cpu_to_le16(pd_ident->pid);
+ skedb.xid = cpu_to_le32(pd_ident->xid);
+ skedb.skedb_ver = SKEDB_VER_1_0;
+ skedb.load_step = data->load_step;
+ skedb.load_char = cpu_to_le16(data->load_char);
+ skedb.compliance = data->compliance;
+ skedb.modes = data->modes;
+ skedb.spr_min_pdp = data->spr_min_pdp;
+ skedb.spr_op_pdp = data->spr_op_pdp;
+ skedb.spr_max_pdp = data->spr_max_pdp;
+ memcpy(msg.ext_msg.data, &skedb, sizeof(skedb));
+ msg.ext_msg.header = PD_EXT_HDR_LE(sizeof(skedb),
+ 0, /* Denotes if request chunk */
+ 0, /* Chunk Number */
+ 1 /* Chunked */);
+
+ data_obj_cnt = count_chunked_data_objs(sizeof(skedb));
+ msg.header = cpu_to_le16(PD_HEADER(PD_EXT_SINK_CAP_EXT,
+ port->pwr_role,
+ port->data_role,
+ port->negotiated_rev,
+ port->message_id,
+ data_obj_cnt,
+ 1 /* Denotes if ext header */));
+ return tcpm_pd_transmit(port, TCPC_TX_SOP, &msg);
+}
+
static void mod_tcpm_delayed_work(struct tcpm_port *port, unsigned int delay_ms)
{
if (delay_ms) {
@@ -3655,6 +3755,19 @@ static void tcpm_pd_ctrl_request(struct tcpm_port *port,
PD_MSG_CTRL_NOT_SUPP,
NONE_AMS);
break;
+ case PD_CTRL_GET_SINK_CAP_EXT:
+ /* This is an unsupported message if port type is SRC */
+ if (port->negotiated_rev >= PD_REV30 &&
+ port->port_type != TYPEC_PORT_SRC)
+ tcpm_pd_handle_msg(port, PD_MSG_EXT_SINK_CAP_EXT,
+ GETTING_SINK_EXTENDED_CAPABILITIES);
+ else
+ tcpm_pd_handle_msg(port,
+ port->negotiated_rev < PD_REV30 ?
+ PD_MSG_CTRL_REJECT :
+ PD_MSG_CTRL_NOT_SUPP,
+ NONE_AMS);
+ break;
default:
tcpm_pd_handle_msg(port,
port->negotiated_rev < PD_REV30 ?
@@ -3907,6 +4020,16 @@ static bool tcpm_send_queued_message(struct tcpm_port *port)
ret);
tcpm_ams_finish(port);
break;
+ case PD_MSG_EXT_SINK_CAP_EXT:
+ ret = tcpm_pd_send_sink_cap_ext(port);
+ if (ret == -EOPNOTSUPP)
+ tcpm_pd_send_control(port, PD_CTRL_NOT_SUPP, TCPC_TX_SOP);
+ else if (ret < 0)
+ tcpm_log(port,
+ "Unable to transmit sink cap extended, ret=%d",
+ ret);
+ tcpm_ams_finish(port);
+ break;
default:
break;
}
@@ -7291,6 +7414,129 @@ static void tcpm_fw_get_timings(struct tcpm_port *port, struct fwnode_handle *fw
port->timings.snk_bc12_cmpletion_time = val;
}

+static void tcpm_fw_get_pd_ident(struct tcpm_port *port)
+{
+ struct pd_identifier *pd_ident = &port->pd_ident;
+ u32 *vdo;
+
+ /* First 3 vdo values contain info regarding USB PID, VID & XID */
+ if (port->nr_snk_vdo >= 3)
+ vdo = port->snk_vdo;
+ else if (port->nr_snk_vdo_v1 >= 3)
+ vdo = port->snk_vdo_v1;
+ else
+ return;
+
+ pd_ident->vid = PD_IDH_VID(vdo[0]);
+ pd_ident->pid = PD_PRODUCT_PID(vdo[2]);
+ pd_ident->xid = PD_CSTAT_XID(vdo[1]);
+ tcpm_log(port, "vid:%#x pid:%#x xid:%#x",
+ pd_ident->vid, pd_ident->pid, pd_ident->xid);
+}
+
+static void tcpm_parse_snk_pdos(struct tcpm_port *port)
+{
+ struct sink_caps_ext_data *caps = &port->sink_caps_ext;
+ u32 max_mv, max_ma;
+ u8 avs_tier1_pdp, avs_tier2_pdp;
+ int i, pdo_itr;
+ u32 *snk_pdos;
+
+ for (i = 0; i < port->pd_count; ++i) {
+ snk_pdos = port->pd_list[i]->sink_desc.pdo;
+ for (pdo_itr = 0; pdo_itr < PDO_MAX_OBJECTS && snk_pdos[pdo_itr];
+ ++pdo_itr) {
+ u32 pdo = snk_pdos[pdo_itr];
+ u8 curr_snk_pdp = 0;
+
+ switch (pdo_type(pdo)) {
+ case PDO_TYPE_FIXED:
+ max_mv = pdo_fixed_voltage(pdo);
+ max_ma = pdo_fixed_current(pdo);
+ curr_snk_pdp = UW_TO_W(max_mv * max_ma);
+ break;
+ case PDO_TYPE_BATT:
+ curr_snk_pdp = UW_TO_W(pdo_max_power(pdo));
+ break;
+ case PDO_TYPE_VAR:
+ max_mv = pdo_max_voltage(pdo);
+ max_ma = pdo_max_current(pdo);
+ curr_snk_pdp = UW_TO_W(max_mv * max_ma);
+ break;
+ case PDO_TYPE_APDO:
+ if (pdo_apdo_type(pdo) == APDO_TYPE_PPS) {
+ max_mv = pdo_pps_apdo_max_voltage(pdo);
+ max_ma = pdo_pps_apdo_max_current(pdo);
+ curr_snk_pdp = UW_TO_W(max_mv * max_ma);
+ caps->modes |= SINK_MODE_PPS;
+ } else if (pdo_apdo_type(pdo) ==
+ APDO_TYPE_SPR_AVS) {
+ avs_tier1_pdp = UW_TO_W(SPR_AVS_TIER1_MAX_VOLT_MV
+ * pdo_spr_avs_apdo_9v_to_15v_max_current_ma(pdo));
+ avs_tier2_pdp = UW_TO_W(SPR_AVS_TIER2_MAX_VOLT_MV
+ * pdo_spr_avs_apdo_15v_to_20v_max_current_ma(pdo));
+ curr_snk_pdp = max(avs_tier1_pdp, avs_tier2_pdp);
+ caps->modes |= SINK_MODE_AVS;
+ }
+ break;
+ default:
+ tcpm_log(port, "Invalid source PDO type, ignoring");
+ continue;
+ }
+
+ caps->spr_max_pdp = max(caps->spr_max_pdp,
+ curr_snk_pdp);
+ }
+ }
+}
+
+static void tcpm_fw_get_sink_caps_ext(struct tcpm_port *port,
+ struct fwnode_handle *fwnode)
+{
+ struct sink_caps_ext_data *caps = &port->sink_caps_ext;
+ int ret;
+ u32 val;
+
+ /*
+ * Load step represents the change in current per usec that a given
+ * source can tolerate while maintaining Vbus within the vSrcValid
+ * range. For a sink this represents the "preferred" load-step value. It
+ * can only have 2 values (150 mA/usec or 500 mA/usec) with 150 mA/usec
+ * being the default.
+ */
+ ret = fwnode_property_read_u32(fwnode, "sink-load-step", &val);
+ if (!ret)
+ caps->load_step = val == 500 ? 1 : 0;
+
+ fwnode_property_read_u16(fwnode, "sink-load-characteristics",
+ &caps->load_char);
+ fwnode_property_read_u8(fwnode, "sink-compliance", &caps->compliance);
+ caps->modes = SINK_MODE_VBUS;
+
+ /*
+ * As per "6.5.13.14" SPR Sink Operational PDP definition, for battery
+ * powered devices, this value will correspond to the PDP of the
+ * charging adapter either shipped or recommended for use with it. For
+ * batteryless sink devices SPR Operational PDP indicates the power
+ * required to operate all the device's functional modes. Hence, this
+ * value may be considered equal to port's operating_snk_mw. As
+ * operating_sink_mw can change as per the pd set used thus, OP PDP
+ * is determined when populating Sink Caps Extended Data Block.
+ */
+ if (port->self_powered) {
+ fwnode_property_read_u32(fwnode, "charging-adapter-pdp-milliwatt",
+ &val);
+ caps->spr_op_pdp = (u8)(val / 1000);
+ caps->modes |= SINK_MODE_BATT;
+ }
+
+ tcpm_parse_snk_pdos(port);
+ tcpm_log(port,
+ "load-step:%#x load-char:%#x compl:%#x op-pdp:%#x max-pdp:%#x",
+ caps->load_step, caps->load_char, caps->compliance,
+ caps->spr_op_pdp, caps->spr_max_pdp);
+}
+
static int tcpm_fw_get_caps(struct tcpm_port *port, struct fwnode_handle *fwnode)
{
struct fwnode_handle *capabilities, *caps = NULL;
@@ -7464,6 +7710,9 @@ static int tcpm_fw_get_caps(struct tcpm_port *port, struct fwnode_handle *fwnode
}
}

+ if (port->port_type != TYPEC_PORT_SRC)
+ tcpm_fw_get_sink_caps_ext(port, fwnode);
+
put_caps:
if (caps != fwnode)
fwnode_handle_put(caps);
@@ -7506,6 +7755,8 @@ static int tcpm_fw_get_snk_vdos(struct tcpm_port *port, struct fwnode_handle *fw
return ret;
}

+ tcpm_fw_get_pd_ident(port);
+
return 0;
}

diff --git a/include/linux/usb/pd.h b/include/linux/usb/pd.h
index 6ccd1b2af993e60d2c68fcd3cef928858d9dd7e3..5a98983195cbb99735d7c5cf10cc43f69fd1b9bd 100644
--- a/include/linux/usb/pd.h
+++ b/include/linux/usb/pd.h
@@ -34,7 +34,8 @@ enum pd_ctrl_msg_type {
PD_CTRL_FR_SWAP = 19,
PD_CTRL_GET_PPS_STATUS = 20,
PD_CTRL_GET_COUNTRY_CODES = 21,
- /* 22-23 Reserved */
+ PD_CTRL_GET_SINK_CAP_EXT = 22,
+ /* 23 Reserved */
PD_CTRL_GET_REVISION = 24,
/* 25-31 Reserved */
};
@@ -72,7 +73,8 @@ enum pd_ext_msg_type {
PD_EXT_PPS_STATUS = 12,
PD_EXT_COUNTRY_INFO = 13,
PD_EXT_COUNTRY_CODES = 14,
- /* 15-31 Reserved */
+ PD_EXT_SINK_CAP_EXT = 15,
+ /* 16-31 Reserved */
};

#define PD_REV10 0x0
@@ -205,6 +207,72 @@ struct pd_message {
};
} __packed;

+/*
+ * count_chunked_data_objs - Helper to calculate number of Data Objects on a 4
+ * byte boundary.
+ * @size: Size of data block for extended message. Should *not* include extended
+ * header size.
+ */
+static inline u8 count_chunked_data_objs(u32 size)
+{
+ size += offsetof(struct pd_chunked_ext_message_data, data);
+ return ((size / 4) + (size % 4 ? 1 : 0));
+}
+
+/* Sink Caps Extended Data Block Version */
+#define SKEDB_VER_1_0 1
+
+/* Sink Caps Extended Sink Modes */
+#define SINK_MODE_PPS BIT(0)
+#define SINK_MODE_VBUS BIT(1)
+#define SINK_MODE_AC_SUPPLY BIT(2)
+#define SINK_MODE_BATT BIT(3)
+#define SINK_MODE_BATT_UL BIT(4) /* Unlimited battery power supply */
+#define SINK_MODE_AVS BIT(5)
+
+/**
+ * struct sink_caps_ext_msg - Sink extended capability PD message
+ * @vid: Vendor ID
+ * @pid: Product ID
+ * @xid: Value assigned by USB-IF for product
+ * @fw: Firmware version
+ * @hw: Hardware version
+ * @skedb_ver: Sink Caps Extended Data Block (SKEDB) Version
+ * @load_step: Indicates the load step slew rate.
+ * @load_char: Sink overload characteristics
+ * @compliance: Types of sources the sink has been tested & certified on
+ * @touch_temp: Indicates the IEC standard to which the touch temperature
+ * conforms to (if applicable).
+ * @batt_info: Indicates number batteries and hot swappable ports
+ * @modes: Charging caps & power sources supported
+ * @spr_min_pdp: Sink Minimum PDP for SPR mode
+ * @spr_op_pdp: Sink Operational PDP for SPR mode
+ * @spr_max_pdp: Sink Maximum PDP for SPR mode
+ * @epr_min_pdp: Sink Minimum PDP for EPR mode
+ * @epr_op_pdp: Sink Operational PDP for EPR mode
+ * @epr_max_pdp: Sink Maximum PDP for EPR mode
+ */
+struct sink_caps_ext_msg {
+ __le16 vid;
+ __le16 pid;
+ __le32 xid;
+ u8 fw;
+ u8 hw;
+ u8 skedb_ver;
+ u8 load_step;
+ __le16 load_char;
+ u8 compliance;
+ u8 touch_temp;
+ u8 batt_info;
+ u8 modes;
+ u8 spr_min_pdp;
+ u8 spr_op_pdp;
+ u8 spr_max_pdp;
+ u8 epr_min_pdp;
+ u8 epr_op_pdp;
+ u8 epr_max_pdp;
+} __packed;
+
/* PDO: Power Data Object */
#define PDO_MAX_OBJECTS 7

@@ -329,6 +397,11 @@ enum pd_apdo_type {
#define PDO_SPR_AVS_APDO_9V_TO_15V_MAX_CURR GENMASK(19, 10) /* 10mA unit */
#define PDO_SPR_AVS_APDO_15V_TO_20V_MAX_CURR GENMASK(9, 0) /* 10mA unit */

+/* SPR AVS has two different current ranges 9V - 15V, 15V - 20V */
+#define SPR_AVS_TIER1_MIN_VOLT_MV 9000
+#define SPR_AVS_TIER1_MAX_VOLT_MV 15000
+#define SPR_AVS_TIER2_MAX_VOLT_MV 20000
+
static inline enum pd_pdo_type pdo_type(u32 pdo)
{
return (pdo >> PDO_TYPE_SHIFT) & PDO_TYPE_MASK;
@@ -339,6 +412,11 @@ static inline unsigned int pdo_fixed_voltage(u32 pdo)
return ((pdo >> PDO_FIXED_VOLT_SHIFT) & PDO_VOLT_MASK) * 50;
}

+static inline unsigned int pdo_fixed_current(u32 pdo)
+{
+ return ((pdo >> PDO_FIXED_CURR_SHIFT) & PDO_CURR_MASK) * 10;
+}
+
static inline unsigned int pdo_min_voltage(u32 pdo)
{
return ((pdo >> PDO_VAR_MIN_VOLT_SHIFT) & PDO_VOLT_MASK) * 50;

--
2.53.0.371.g1d285c8824-goog