USB-C device stack

The USB-C device stack is a hardware independent interface between a Type-C Port Controller (TCPC) and customer applications. It is a port of the Google ChromeOS Type-C Port Manager (TCPM) stack. It provides the following functionalities:

  • Uses the APIs provided by the Type-C Port Controller drivers to interact with the Type-C Port Controller.

  • Provides a programming interface that’s used by a customer applications. The APIs is described in include/zephyr/usb_c/usbc.h

Configuration options

The USB-C device stack supports implementation of Sink only, Source only, and Dual Role Power (DRP) devices.

List of samples for different purposes.

Implementing a Sink Type-C and Power Delivery USB-C device

The configuration of a USB-C Device is done in the stack layer and devicetree.

The following devicetree, structures and callbacks need to be defined:

  • Devicetree usb-c-connector node referencing a TCPC

  • Devicetree vbus node referencing a VBUS measurement device

  • User defined structure that encapsulates application specific data

  • Policy callbacks

For example, for the Sample USB-C Sink application:

Each Physical Type-C port is represented in the devicetree by a usb-c-connector compatible node:

1		port1: usbc-port@1 {
2			compatible = "usb-c-connector";
3			reg = <1>;
4			tcpc = <&ucpd1>;
5			vbus = <&vbus1>;
6			power-role = "sink";
7			sink-pdos = <PDO_FIXED(5000, 100, 0)>;
8		};

VBUS is measured by a device that’s referenced in the devicetree by a usb-c-vbus-adc compatible node:

1	vbus1: vbus {
2		compatible = "zephyr,usb-c-vbus-adc";
3		io-channels = <&adc2 8>;
4		output-ohms = <49900>;
5		full-ohms = <(330000 + 49900)>;
6	};

A user defined structure is defined and later registered with the subsystem and can be accessed from callback through an API:

 1/**
 2 * @brief A structure that encapsulates Port data.
 3 */
 4static struct port0_data_t {
 5	/** Sink Capabilities */
 6	uint32_t snk_caps[DT_PROP_LEN(USBC_PORT0_NODE, sink_pdos)];
 7	/** Number of Sink Capabilities */
 8	int snk_cap_cnt;
 9	/** Source Capabilities */
10	uint32_t src_caps[PDO_MAX_DATA_OBJECTS];
11	/** Number of Source Capabilities */
12	int src_cap_cnt;
13	/* Power Supply Ready flag */
14	atomic_t ps_ready;
15} port0_data = {.snk_caps = DT_PROP(USBC_PORT0_NODE, sink_pdos),
16		.snk_cap_cnt = DT_PROP_LEN(USBC_PORT0_NODE, sink_pdos),
17		.src_caps = {0},
18		.src_cap_cnt = 0,
19		.ps_ready = 0};
20

These callbacks are used by the subsystem to set or get application specific data:

 1static int port0_policy_cb_get_snk_cap(const struct device *dev, uint32_t **pdos, int *num_pdos)
 2{
 3	struct port0_data_t *dpm_data = usbc_get_dpm_data(dev);
 4
 5	*pdos = dpm_data->snk_caps;
 6	*num_pdos = dpm_data->snk_cap_cnt;
 7
 8	return 0;
 9}
10
11static void port0_policy_cb_set_src_cap(const struct device *dev, const uint32_t *pdos,
12					const int num_pdos)
13{
14	struct port0_data_t *dpm_data;
15	int num;
16	int i;
17
18	dpm_data = usbc_get_dpm_data(dev);
19
20	num = num_pdos;
21	if (num > PDO_MAX_DATA_OBJECTS) {
22		num = PDO_MAX_DATA_OBJECTS;
23	}
24
25	for (i = 0; i < num; i++) {
26		dpm_data->src_caps[i] = *(pdos + i);
27	}
28
29	dpm_data->src_cap_cnt = num;
30}
31
32static uint32_t port0_policy_cb_get_rdo(const struct device *dev)
33{
34	struct port0_data_t *dpm_data = usbc_get_dpm_data(dev);
35
36	return build_rdo(dpm_data);
37}

This callback is used by the subsystem to query if a certain action can be taken:

 1bool port0_policy_check(const struct device *dev, const enum usbc_policy_check_t policy_check)
 2{
 3	switch (policy_check) {
 4	case CHECK_POWER_ROLE_SWAP:
 5		/* Reject power role swaps */
 6		return false;
 7	case CHECK_DATA_ROLE_SWAP_TO_DFP:
 8		/* Reject data role swap to DFP */
 9		return false;
10	case CHECK_DATA_ROLE_SWAP_TO_UFP:
11		/* Accept data role swap to UFP */
12		return true;
13	case CHECK_SNK_AT_DEFAULT_LEVEL:
14		/* This device is always at the default power level */
15		return true;
16	default:
17		/* Reject all other policy checks */
18		return false;
19	}
20}

This callback is used by the subsystem to notify the application of an event:

 1static void port0_notify(const struct device *dev, const enum usbc_policy_notify_t policy_notify)
 2{
 3	struct port0_data_t *dpm_data = usbc_get_dpm_data(dev);
 4
 5	switch (policy_notify) {
 6	case PROTOCOL_ERROR:
 7		break;
 8	case MSG_DISCARDED:
 9		break;
10	case MSG_ACCEPT_RECEIVED:
11		break;
12	case MSG_REJECTED_RECEIVED:
13		break;
14	case MSG_NOT_SUPPORTED_RECEIVED:
15		break;
16	case TRANSITION_PS:
17		atomic_set_bit(&dpm_data->ps_ready, 0);
18		break;
19	case PD_CONNECTED:
20		break;
21	case NOT_PD_CONNECTED:
22		break;
23	case POWER_CHANGE_0A0:
24		LOG_INF("PWR 0A");
25		break;
26	case POWER_CHANGE_DEF:
27		LOG_INF("PWR DEF");
28		break;
29	case POWER_CHANGE_1A5:
30		LOG_INF("PWR 1A5");
31		break;
32	case POWER_CHANGE_3A0:
33		LOG_INF("PWR 3A0");
34		break;
35	case DATA_ROLE_IS_UFP:
36		break;
37	case DATA_ROLE_IS_DFP:
38		break;
39	case PORT_PARTNER_NOT_RESPONSIVE:
40		LOG_INF("Port Partner not PD Capable");
41		break;
42	case SNK_TRANSITION_TO_DEFAULT:
43		break;
44	case HARD_RESET_RECEIVED:
45		break;
46	case SENDER_RESPONSE_TIMEOUT:
47		break;
48	case SOURCE_CAPABILITIES_RECEIVED:
49		break;
50	default:
51	}
52}

Registering the callbacks:

 1	/* Register USB-C Callbacks */
 2
 3	/* Register Policy Check callback */
 4	usbc_set_policy_cb_check(usbc_port0, port0_policy_check);
 5	/* Register Policy Notify callback */
 6	usbc_set_policy_cb_notify(usbc_port0, port0_notify);
 7	/* Register Policy Get Sink Capabilities callback */
 8	usbc_set_policy_cb_get_snk_cap(usbc_port0, port0_policy_cb_get_snk_cap);
 9	/* Register Policy Set Source Capabilities callback */
10	usbc_set_policy_cb_set_src_cap(usbc_port0, port0_policy_cb_set_src_cap);
11	/* Register Policy Get Request Data Object callback */
12	usbc_set_policy_cb_get_rdo(usbc_port0, port0_policy_cb_get_rdo);

Register the user defined structure:

1	/* Set Application port data object. This object is passed to the policy callbacks */
2	port0_data.ps_ready = ATOMIC_INIT(0);
3	usbc_set_dpm_data(usbc_port0, &port0_data);

Start the USB-C subsystem:

1	/* Start the USB-C Subsystem */
2	usbc_start(usbc_port0);

Implementing a Source Type-C and Power Delivery USB-C device

The configuration of a USB-C Device is done in the stack layer and devicetree.

Define the following devicetree, structures and callbacks:

  • Devicetree usb-c-connector node referencing a TCPC

  • Devicetree vbus node referencing a VBUS measurement device

  • Devicetree pwrctrl node for VBUS and VCONN power control

  • User defined structure that encapsulates application specific data

  • Policy callbacks

For example, for the Sample USB-C Source application:

Each Physical Type-C port is represented in the devicetree by a usb-c-connector compatible node:

 1		port1: usbc-port@1 {
 2			compatible = "usb-c-connector";
 3			reg = <1>;
 4			tcpc = <&ucpd1>;
 5			vbus = <&vbus1>;
 6			power-role = "source";
 7			typec-power-opmode = "3.0A";
 8			source-pdos = <PDO_FIXED(5000, 100, 0) PDO_FIXED(9000, 100, 0)
 9				       PDO_FIXED(15000, 100, 0)>;
10		};

VBUS is measured by a device that’s referenced in the devicetree by a usb-c-vbus-adc compatible node:

1	vbus1: vbus {
2		compatible = "zephyr,usb-c-vbus-adc";
3		io-channels = <&adc1 9>;
4		output-ohms = <49900>;
5		full-ohms = <(330000 + 49900)>;
6
7		/* Pin B13 is used to control VBUS Discharge for Port1 */
8		discharge-gpios = <&gpiob 13 GPIO_ACTIVE_HIGH>;
9	};

Power control for VBUS and VCONN can be managed by a device referenced in the devicetree by a zephyr,usb-c-pwrctrl compatible node:

 1	pwrctrl1: pwrctrl {
 2		compatible = "zephyr,usb-c-pwrctrl";
 3
 4		/* Pin D3 is used to enable VBUS Source */
 5		source-en-gpios = <&gpiod 3 GPIO_ACTIVE_HIGH>;
 6
 7		/* Pin A1 is used for DCDC_EN */
 8		dcdc-en-gpios = <&gpioa 1 GPIO_ACTIVE_HIGH>;
 9
10		/* Pin D4 enables VCONN on CC1 */
11		vconn1-en-gpios = <&gpiod 4 GPIO_ACTIVE_LOW>;
12
13		/* Pin B9 enables VCONN on CC2 */
14		vconn2-en-gpios = <&gpiob 9 GPIO_ACTIVE_LOW>;
15
16		/* PWM for voltage selection */
17		pwms = <&pwm15 1 PWM_USEC(50) PWM_POLARITY_INVERTED>;
18	};

A user defined structure is defined and later registered with the subsystem and can be accessed from callback through an API:

 1/**
 2 * @brief A structure that encapsulates Port data.
 3 */
 4static struct port0_data_t {
 5	/** Power controller device */
 6	const struct device *pwrctrl;
 7	/** Source Capabilities */
 8	uint32_t src_caps[DT_PROP_LEN(USBC_PORT0_NODE, source_pdos)];
 9	/** Number of Source Capabilities */
10	int src_cap_cnt;
11	/** CC Rp value */
12	int rp;
13	/** Sink Request RDO */
14	union pd_rdo sink_request;
15	/** Requested Object Pos */
16	int obj_pos;
17	/** VCONN CC line*/
18	enum tc_cc_polarity vconn_pol;
19	/** True if power supply is ready */
20	bool ps_ready;
21	/** True if power supply should transition to a new level */
22	bool ps_tran_start;
23	/** Log Sink Requested RDO to console */
24	atomic_t show_sink_request;
25} port0_data = {
26	.rp = DT_ENUM_IDX(USBC_PORT0_NODE, typec_power_opmode),
27	.src_caps = DT_PROP(USBC_PORT0_NODE, source_pdos),
28	.src_cap_cnt = DT_PROP_LEN(USBC_PORT0_NODE, source_pdos),
29};
30

These callbacks are used by the subsystem to set or get application specific data:

  1/**
  2 * @brief PE calls this function when it needs to set the Rp on CC
  3 */
  4int port0_policy_cb_get_src_rp(const struct device *dev, enum tc_rp_value *rp)
  5{
  6	struct port0_data_t *dpm_data = usbc_get_dpm_data(dev);
  7
  8	*rp = dpm_data->rp;
  9
 10	return 0;
 11}
 12
 13/**
 14 * @brief PE calls this function to Enable (5V) or Disable (0V) the
 15 *	  Power Supply
 16 */
 17int port0_policy_cb_src_en(const struct device *dev, bool en)
 18{
 19	struct port0_data_t *dpm_data = usbc_get_dpm_data(dev);
 20
 21	source_ctrl_set(dpm_data->pwrctrl, en ? SOURCE_5V : SOURCE_0V);
 22
 23	return 0;
 24}
 25
 26/**
 27 * @brief PE calls this function to Enable or Disable VCONN
 28 */
 29int port0_policy_cb_vconn_en(const struct device *tcpc_dev, const struct device *usbc_dev,
 30			     enum tc_cc_polarity pol, bool en)
 31{
 32	struct port0_data_t *dpm_data = usbc_get_dpm_data(usbc_dev);
 33
 34	dpm_data->vconn_pol = pol;
 35
 36	if (en == false) {
 37		/* Disable VCONN on CC1 and CC2 */
 38		vconn_ctrl_set(dpm_data->pwrctrl, VCONN_OFF);
 39	} else if (pol == TC_POLARITY_CC1) {
 40		/* set VCONN on CC1 */
 41		vconn_ctrl_set(dpm_data->pwrctrl, VCONN1_ON);
 42	} else {
 43		/* set VCONN on CC2 */
 44		vconn_ctrl_set(dpm_data->pwrctrl, VCONN2_ON);
 45	}
 46
 47	return 0;
 48}
 49
 50/**
 51 * @brief PE calls this function to get the Source Caps that will be sent
 52 *	  to the Sink
 53 */
 54int port0_policy_cb_get_src_caps(const struct device *dev, const uint32_t **pdos,
 55				 uint32_t *num_pdos)
 56{
 57	struct port0_data_t *dpm_data = usbc_get_dpm_data(dev);
 58
 59	*pdos = dpm_data->src_caps;
 60	*num_pdos = dpm_data->src_cap_cnt;
 61
 62	return 0;
 63}
 64
 65/**
 66 * @brief PE calls this function to verify that a Sink's request if valid
 67 */
 68static enum usbc_snk_req_reply_t port0_policy_cb_check_sink_request(const struct device *dev,
 69								    const uint32_t request_msg)
 70{
 71	struct port0_data_t *dpm_data = usbc_get_dpm_data(dev);
 72	union pd_fixed_supply_pdo_source pdo;
 73	uint32_t obj_pos;
 74	uint32_t op_current;
 75
 76	dpm_data->sink_request.raw_value = request_msg;
 77	obj_pos = dpm_data->sink_request.fixed.object_pos;
 78	op_current =
 79		PD_CONVERT_FIXED_PDO_CURRENT_TO_MA(dpm_data->sink_request.fixed.operating_current);
 80
 81	if (obj_pos == 0 || obj_pos > dpm_data->src_cap_cnt) {
 82		return SNK_REQUEST_REJECT;
 83	}
 84
 85	pdo.raw_value = dpm_data->src_caps[obj_pos - 1];
 86
 87	if (dpm_data->sink_request.fixed.operating_current > pdo.max_current) {
 88		return SNK_REQUEST_REJECT;
 89	}
 90
 91	dpm_data->obj_pos = obj_pos;
 92
 93	atomic_set_bit(&port0_data.show_sink_request, 0);
 94
 95	/*
 96	 * Clear PS ready. This will be set to true after PS is ready after
 97	 * it transitions to the new level.
 98	 */
 99	port0_data.ps_ready = false;
100
101	return SNK_REQUEST_VALID;
102}
103
104/**
105 * @brief PE calls this function to check if the Power Supply is at the requested
106 *	  level
107 */
108static bool port0_policy_cb_is_ps_ready(const struct device *dev)
109{
110	struct port0_data_t *dpm_data = usbc_get_dpm_data(dev);
111
112	/* Return true to inform that the Power Supply is ready */
113	return dpm_data->ps_ready;
114}
115
116/**
117 * @brief PE calls this function to check if the Present Contract is still
118 *	  valid
119 */
120static bool port0_policy_cb_present_contract_is_valid(const struct device *dev,
121						      const uint32_t present_contract)
122{
123	struct port0_data_t *dpm_data = usbc_get_dpm_data(dev);
124	union pd_fixed_supply_pdo_source pdo;
125	union pd_rdo request;
126	uint32_t obj_pos;
127	uint32_t op_current;
128
129	request.raw_value = present_contract;
130	obj_pos = request.fixed.object_pos;
131	op_current = PD_CONVERT_FIXED_PDO_CURRENT_TO_MA(request.fixed.operating_current);
132
133	if (obj_pos == 0 || obj_pos > dpm_data->src_cap_cnt) {
134		return false;
135	}
136
137	pdo.raw_value = dpm_data->src_caps[obj_pos - 1];
138
139	if (request.fixed.operating_current > pdo.max_current) {
140		return false;
141	}
142
143	return true;
144}
145

This callback is used by the subsystem to query if a certain action can be taken:

 1bool port0_policy_check(const struct device *dev, const enum usbc_policy_check_t policy_check)
 2{
 3	struct port0_data_t *dpm_data = usbc_get_dpm_data(dev);
 4
 5	switch (policy_check) {
 6	case CHECK_POWER_ROLE_SWAP:
 7		/* Reject power role swaps */
 8		return false;
 9	case CHECK_DATA_ROLE_SWAP_TO_DFP:
10		/* Accept data role swap to DFP */
11		return true;
12	case CHECK_DATA_ROLE_SWAP_TO_UFP:
13		/* Reject data role swap to UFP */
14		return false;
15	case CHECK_SRC_PS_AT_DEFAULT_LEVEL:
16		/*
17		 * This check is sent from the PE_SRC_Transition_to_default
18		 * state and requires the following:
19		 *	1: Vconn should be turned ON
20		 *	2: Return TRUE when Power Supply is at default level
21		 */
22
23		/* Power on VCONN */
24		vconn_ctrl_set(dpm_data->pwrctrl, dpm_data->vconn_pol);
25
26		/* PS should be at default level after receiving a Hard Reset */
27		return true;
28	default:
29		/* Reject all other policy checks */
30		return false;
31	}
32}

This callback is used by the subsystem to notify the application of an event:

 1static void port0_notify(const struct device *dev, const enum usbc_policy_notify_t policy_notify)
 2{
 3	struct port0_data_t *dpm_data = usbc_get_dpm_data(dev);
 4
 5	switch (policy_notify) {
 6	case PROTOCOL_ERROR:
 7		break;
 8	case MSG_DISCARDED:
 9		break;
10	case MSG_ACCEPT_RECEIVED:
11		break;
12	case MSG_REJECTED_RECEIVED:
13		break;
14	case MSG_NOT_SUPPORTED_RECEIVED:
15		break;
16	case TRANSITION_PS:
17		dpm_data->ps_tran_start = true;
18		break;
19	case PD_CONNECTED:
20		break;
21	case NOT_PD_CONNECTED:
22		break;
23	case DATA_ROLE_IS_UFP:
24		break;
25	case DATA_ROLE_IS_DFP:
26		break;
27	case PORT_PARTNER_NOT_RESPONSIVE:
28		LOG_INF("Port Partner not PD Capable");
29		break;
30	case HARD_RESET_RECEIVED:
31		/*
32		 * This notification is sent from the PE_SRC_Transition_to_default
33		 * state and requires the following:
34		 *	1: Vconn should be turned OFF
35		 *	2: Reset of the local hardware
36		 */
37
38		/* Power off VCONN */
39		vconn_ctrl_set(dpm_data->pwrctrl, VCONN_OFF);
40		/* Transition PS to Default level */
41		source_ctrl_set(dpm_data->pwrctrl, SOURCE_5V);
42		break;
43	default:
44	}
45}

Registering the callbacks:

 1	/* Register USB-C Callbacks */
 2
 3	/* Register Policy Check callback */
 4	usbc_set_policy_cb_check(usbc_port0, port0_policy_check);
 5	/* Register Policy Notify callback */
 6	usbc_set_policy_cb_notify(usbc_port0, port0_notify);
 7	/* Register Policy callback to set the Rp on CC lines */
 8	usbc_set_policy_cb_get_src_rp(usbc_port0, port0_policy_cb_get_src_rp);
 9	/* Register Policy callback to enable or disable power supply */
10	usbc_set_policy_cb_src_en(usbc_port0, port0_policy_cb_src_en);
11	/* Register Policy callback to enable or disable vconn */
12	usbc_set_vconn_control_cb(usbc_port0, port0_policy_cb_vconn_en);
13	/* Register Policy callback to send the source caps to the sink */
14	usbc_set_policy_cb_get_src_caps(usbc_port0, port0_policy_cb_get_src_caps);
15	/* Register Policy callback to check if the sink request is valid */
16	usbc_set_policy_cb_check_sink_request(usbc_port0, port0_policy_cb_check_sink_request);
17	/* Register Policy callback to check if the power supply is ready */
18	usbc_set_policy_cb_is_ps_ready(usbc_port0, port0_policy_cb_is_ps_ready);
19	/* Register Policy callback to check if Present Contract is still valid */
20	usbc_set_policy_cb_present_contract_is_valid(usbc_port0,
21						     port0_policy_cb_present_contract_is_valid);
22

Register the user defined structure:

1	/* Set Application port data object. This object is passed to the policy callbacks */
2	usbc_set_dpm_data(usbc_port0, &port0_data);

Start the USB-C subsystem:

1	/* Start the USB-C Subsystem */
2	usbc_start(usbc_port0);

Implementing a Dual Role Power (DRP) USB-C device

DRP devices can operate as both Source and Sink, automatically negotiating the appropriate role with the port partner. When unattached, the device toggles between Source (Rp) and Sink (Rd) CC line advertisements to detect and connect with any partner type. Once an attach is detected, the device enters the appropriate attached state (Attached.SRC or Attached.SNK) and starts the corresponding Policy Engine state machine (PE_SRC or PE_SNK) to negotiate power delivery.

Configuration is similar to Source and Sink devices, with the following key differences:

  • Set power-role = "dual" in the devicetree usb-c-connector node

  • Implement callbacks for both Source and Sink operations

The DRP toggle behavior can be configured via Kconfig:

See Basic USB-C DRP for a complete example.

API reference

USB-C Device API

SINK callback reference

Sink_callbacks

SOURCE callback reference

Source_callbacks