Adding a TCPC to Zephyr for USB Power Delivery

The Zephyr RTOS has support for USB-C Power Delivery through the software port of the Google ChromeOS Type-C Port Manager.

USB Power Delivery is a protocol used on USB Type-C connectors to negotiate power at different voltage levels – typically 5V, 9V, 12V, 15V, 20V, but can be as high as 48V/240W with the new PD revision 3.1.

As USB-C becomes the universal standard for low power chargers, it allows different devices such as phones, laptops, LCD screens, mini PCs, lights, label printers, bike lights etc to use a common charger, reducing e-waste and end-user confusion of what plug pack came with what device.

The standard supports bi-directional charging. The device can be configured as a sink, source or dual role.

Power Delivery communication occur over the Configuration Channels (CC1 & CC2) of the USB-Type-C connector using Biphase Mark Coding at 300kBaud half duplex. Hence normally an interface, commonly called the USB Type-C controller is used for communications. This may be incorporated on the silicon of your MCU, or supplied as a external interface I.C.

Semiconductor manufacturers initially created USB Type-C controllers such as the FUSB302B with their own propriety MCU interface register sets. This required a different hardware abstraction layer for each different device. The USB Implementers forum then developed a specification for a standalised Type-C port Manager that has allowed for common firmware to be used. In this case, OnSemi introduced the FUSB307B TCPC or USB Type-C Port Controller compliant with USB−PD Interface Specification
Rev 1.0,

A common firmware stack called the Type-C Port Manager (TCPM) can then communicate with the TCPC.

Hardware

In this article, we will be using a custom FUSB307 breakout board with the Nordic nRF52840DK.

Custom FUSB307B Breakout Board for testing Zephyr TCPC support on different MCU targets.

The complete source code for this example can be found at https://github.com/craigpeacock/Zephyr_TCPM

Support for the FUSB307B was added to the Zephyr on May 27th 2025 and hence is relatively new. The following code has been complied with native Zephyr and not the nRF Connect SDK.

Device Tree Overlay

On Zephyr, hardware is configured in the device tree. Binding information for the required nodes can be found in links below.

The I2C TCPC (fusb307_tcpc0) is set up as a device subnode of the I2C bus it is connected to. In our case with the nRF52840dk, we connect it to i2c0.

&i2c0 {
	compatible = "nordic,nrf-twim";
	status = "okay";
	pinctrl-0 = <&i2c0_default>;
	pinctrl-1 = <&i2c0_sleep>;
	pinctrl-names = "default", "sleep";
	clock-frequency = <100000>;
	fusb307_tcpc0: fusb307_tcpc0@50 {
		compatible = "onnn,fusb307-tcpc";
		reg = <0x50>;
		irq-gpios = <&gpio0 31 GPIO_ACTIVE_LOW>;
		status = "okay";
		fusb307_vbus0: fusb307_vbus0 {
			compatible = "zephyr,usb-c-vbus-tcpci";
			status = "okay";
		};
	};
};

The fusb307 is configured with the I2C address of 0x50. The irq-gpios property specifies the IRQ pin used by the FUSB307 TCPC – in our case P0_31. The VBUS USB bus voltage is measured using the fusb307, as specified by the fusb307_vbus0 subnode.

If they are not already specified, you will need to set up the GPIO pins assigned to the I2C0 interface. On the nRF52840dk, I have reassigned these to pin 2 for SDA and pin 3 for SCL to make wiring easier:

&pinctrl {

	i2c0_default: i2c0_default {
		group1 {
			psels = <NRF_PSEL(TWIM_SDA, 0, 30)>,
				<NRF_PSEL(TWIM_SCL, 0, 29)>;
			bias-pull-up;
		};
	};

	i2c0_sleep: i2c0_sleep {
		group1 {
			psels = <NRF_PSEL(TWIM_SDA, 0, 30)>,
				<NRF_PSEL(TWIM_SCL, 0, 29)>;
			low-power-enable;
		};
	};
};

Now that we have the TCPC set up, we can specify the port. Each Physical Type-C port is represented in the devicetree by a usb-c-connector compatible node.

/ {
	aliases {
		usbc-port0 = &port0;
	};

	ports {
		#address-cells = <1>;
		#size-cells = <0>;
		port0: usbc-port@0 {
			compatible = "usb-c-connector";
			reg = <0>;
			tcpc = <&fusb307_tcpc0>;
			vbus = <&fusb307_vbus0>;
			power-role = "sink";
			sink-pdos = <PDO_FIXED(9000, 100, 0) PDO_FIXED(5000, 100, 0) PDO_FIXED(15000, 100, 0) PDO_FIXED(20000, 100, 0)>;
		};
	};
};

Within the usb-c-connector node, the tcpc property specifies the TCPC used for the port and the vbus property specifies the device used to measure the USB port’s Vbus. This can be an ADC, but in our case the FUSB307 can also handle this.

The power-role is specified as a sink – i.e. our device is requesting power from an external USB-C PD supply.

The sink PDOs (Power Delivery Objects) are specified in the sink-pdos property. The PDO_TYPE can be PDO_FIXED, PDO_BATT, PDO_VAR or PDO_PPS_APDO. The parameters following are different for each type (check zephyr\include\zephyr\dt-bindings\usb-c\pd.h). The fixed PDO object takes the format of <PDO_TYPE(Voltage, Current, Flags)>.

More information on Zephyr USB-C Device Stack can be found at USB-C device stack – Zephyr Project.

Application (main.c)

The example application has been copied verbatim from the Zephyr examples with some extra debugging messages added. It starts the TCPM stack, obtains the capabilities from the PD source, prints them and sends a Request Data Object (RDO) requesting a power contract.

It appears the example code provided does not yet parse the specified PDOs specified the device tree by iterating the src_caps member of the port0_data_t structure, but rather the build_rdo() function will build a hardcoded request data object (RDO) and select object position 1 (5V). Hence if you are expecting to change the sink-pdos in the devicetree to select what PDO is requested, you will be disappointed – it won’t work.

Output

This is the output when a simple mains charger is attached advertising three PDOs:

*** Booting Zephyr OS build v4.1.0-5062-g450345aea3bf ***
[00:00:00.251,708] <inf> main: Number of Sink Capabilies 4
[00:00:00.251,708] <inf> main: Capability = 0002D00A
[00:00:00.251,708] <inf> main: Starting the USB-C Subsystem
[00:00:00.251,770] <err> usbc_stack: TCPC initialization failed: -11
[00:00:00.256,683] <inf> tcpc_fusb307: Initializing FUSB307 chip: fusb307_tcpc0@50
[00:00:00.256,835] <err> usbc_stack: TCPC initialization failed: -11
[00:00:00.259,033] <inf> tcpc_fusb307: Initialized chip is: 0779:0133:0202
[00:00:00.262,023] <inf> tcpc_fusb307: FUSB307 TCPC initialized
[00:00:00.262,023] <inf> usbc_stack: ErrorRecovery
[00:00:00.264,038] <dbg> tcpc_fusb307: fusb307_tcpc_get_cc: CC changed values: 0->0, 0->0
[00:00:00.270,324] <dbg> tcpc_fusb307: fusb307_tcpc_get_cc: CC changed values: 0->0, 0->0
[00:00:00.505,676] <inf> usbc_stack: Unattached.SNK
[00:00:00.506,866] <dbg> tcpc_fusb307: fusb307_tcpc_get_cc: CC changed values: 0->0, 0->0
[00:00:03.494,934] <dbg> tcpc_fusb307: fusb307_tcpc_get_cc: CC changed values: 0->0, 0->7
[00:00:03.495,971] <inf> usbc_stack: AttachWait.SNK
[00:00:03.635,833] <dbg> tcpc_fusb307: fusb307_alert_work_cb: power status: 0c
[00:00:03.700,500] <inf> usbc_stack: Attached.SNK
[00:00:03.704,650] <inf> usbc_stack: PE_SNK_Startup
[00:00:03.704,650] <inf> main: No PD Explicit Contract is in place
[00:00:03.704,681] <inf> usbc_stack: PRL_INIT
[00:00:03.704,681] <inf> usbc_stack: PRL_HR_Wait_for_Request
[00:00:03.704,711] <inf> usbc_stack: PRL_Tx_PHY_Layer_Reset
[00:00:03.705,108] <inf> usbc_stack: PRL_Tx_Wait_for_Message_Request
[00:00:03.706,298] <dbg> tcpc_fusb307: fusb307_tcpc_get_cc: CC changed values: 0->0, 7->7
[00:00:03.706,970] <inf> usbc_stack: PE_SNK_Discovery
[00:00:03.709,136] <inf> usbc_stack: PE_SNK_Wait_For_Capabilities
[00:00:03.727,600] <inf> main: PWR 3A0: Sink SubPower state a 5V / 3A
[00:00:03.830,902] <dbg> tcpc_fusb307: fusb307_alert_work_cb: MSG pending
[00:00:03.834,014] <inf> usbc_stack: RECV 33a1/3
[00:00:03.834,045] <inf> usbc_stack:    [0]0801912c
[00:00:03.834,045] <inf> usbc_stack:    [1]0002d12c
[00:00:03.834,045] <inf> usbc_stack:    [2]0003c12c
[00:00:03.835,174] <inf> usbc_stack: PE_SNK_Evaluate_Capability
[00:00:03.835,205] <inf> main: Source Capabilities Received
[00:00:03.835,205] <inf> usbc_stack: PE_SNK_Select_Capability
[00:00:03.835,235] <inf> main: port0_policy_cb_get_rdo()
[00:00:03.835,235] <inf> main: A PD Explicit Contract is in place
[00:00:03.837,188] <inf> usbc_stack: PRL_Tx_Wait_for_PHY_response
[00:00:03.839,263] <inf> usbc_stack: PRL_Tx_Wait_for_Message_Request
[00:00:03.841,613] <dbg> tcpc_fusb307: fusb307_alert_work_cb: MSG pending
[00:00:03.842,498] <inf> usbc_stack: RECV 05a3/0
[00:00:03.843,658] <inf> usbc_stack: PE_SNK_Transition_Sink
[00:00:03.991,424] <dbg> tcpc_fusb307: fusb307_alert_work_cb: MSG pending
[00:00:03.992,370] <inf> usbc_stack: RECV 07a6/0
[00:00:03.993,499] <inf> main: Transition the Power Supply
[00:00:03.993,530] <inf> usbc_stack: PE_SNK_Ready
[00:00:04.251,983] <inf> main: Source Caps:
[00:00:04.251,983] <inf> main: PDO 0:
[00:00:04.252,014] <inf> main:  Type:              FIXED
[00:00:04.252,014] <inf> main:  Current:           3000
[00:00:04.252,014] <inf> main:  Voltage:           5000
[00:00:04.252,014] <inf> main:  Peak Current:      0
[00:00:04.252,044] <inf> main:  Uchunked Support:  0
[00:00:04.252,044] <inf> main:  Dual Role Data:    0
[00:00:04.252,075] <inf> main:  USB Comms:         0
[00:00:04.252,075] <inf> main:  Unconstrained Pwr: 1
[00:00:04.252,075] <inf> main:  USB Suspend:       0
[00:00:04.252,075] <inf> main:  Dual Role Power:   0
[00:00:04.302,185] <inf> main: PDO 1:
[00:00:04.302,185] <inf> main:  Type:              FIXED
[00:00:04.302,185] <inf> main:  Current:           3000
[00:00:04.302,215] <inf> main:  Voltage:           9000
[00:00:04.302,215] <inf> main:  Peak Current:      0
[00:00:04.302,215] <inf> main:  Uchunked Support:  0
[00:00:04.302,246] <inf> main:  Dual Role Data:    0
[00:00:04.302,246] <inf> main:  USB Comms:         0
[00:00:04.302,246] <inf> main:  Unconstrained Pwr: 0
[00:00:04.302,246] <inf> main:  USB Suspend:       0
[00:00:04.302,276] <inf> main:  Dual Role Power:   0
[00:00:04.352,355] <inf> main: PDO 2:
[00:00:04.352,355] <inf> main:  Type:              FIXED
[00:00:04.352,386] <inf> main:  Current:           3000
[00:00:04.352,386] <inf> main:  Voltage:           12000
[00:00:04.352,386] <inf> main:  Peak Current:      0
[00:00:04.352,416] <inf> main:  Uchunked Support:  0
[00:00:04.352,416] <inf> main:  Dual Role Data:    0
[00:00:04.352,416] <inf> main:  USB Comms:         0
[00:00:04.352,416] <inf> main:  Unconstrained Pwr: 0
[00:00:04.352,447] <inf> main:  USB Suspend:       0
[00:00:04.352,447] <inf> main:  Dual Role Power:   0

And for a charger advertising two Augmented PDOs (APDO) where the output voltage is adjustable in Programmable Power Supply (PPS) Mode.

[00:00:53.070,678] <inf> main: Transition the Power Supply
[00:00:53.070,709] <inf> usbc_stack: PE_SNK_Ready
[00:00:53.807,159] <inf> main: Source Caps:
[00:00:53.807,159] <inf> main: PDO 0:
[00:00:53.807,189] <inf> main:  Type:              FIXED
[00:00:53.807,189] <inf> main:  Current:           3000
[00:00:53.807,189] <inf> main:  Voltage:           5000
[00:00:53.807,189] <inf> main:  Peak Current:      0
[00:00:53.807,220] <inf> main:  Uchunked Support:  0
[00:00:53.807,220] <inf> main:  Dual Role Data:    0
[00:00:53.807,220] <inf> main:  USB Comms:         0
[00:00:53.807,250] <inf> main:  Unconstrained Pwr: 1
[00:00:53.807,250] <inf> main:  USB Suspend:       0
[00:00:53.807,250] <inf> main:  Dual Role Power:   0
[00:00:53.857,360] <inf> main: PDO 1:
[00:00:53.857,360] <inf> main:  Type:              FIXED
[00:00:53.857,360] <inf> main:  Current:           3000
[00:00:53.857,391] <inf> main:  Voltage:           9000
[00:00:53.857,391] <inf> main:  Peak Current:      0
[00:00:53.857,391] <inf> main:  Uchunked Support:  0
[00:00:53.857,391] <inf> main:  Dual Role Data:    0
[00:00:53.857,421] <inf> main:  USB Comms:         0
[00:00:53.857,421] <inf> main:  Unconstrained Pwr: 0
[00:00:53.857,421] <inf> main:  USB Suspend:       0
[00:00:53.857,452] <inf> main:  Dual Role Power:   0
[00:00:53.907,531] <inf> main: PDO 2:
[00:00:53.907,531] <inf> main:  Type:              FIXED
[00:00:53.907,531] <inf> main:  Current:           3000
[00:00:53.907,562] <inf> main:  Voltage:           12000
[00:00:53.907,562] <inf> main:  Peak Current:      0
[00:00:53.907,562] <inf> main:  Uchunked Support:  0
[00:00:53.907,562] <inf> main:  Dual Role Data:    0
[00:00:53.907,592] <inf> main:  USB Comms:         0
[00:00:53.907,592] <inf> main:  Unconstrained Pwr: 0
[00:00:53.907,592] <inf> main:  USB Suspend:       0
[00:00:53.907,623] <inf> main:  Dual Role Power:   0
[00:00:53.957,702] <inf> main: PDO 3:
[00:00:53.957,702] <inf> main:  Type:              FIXED
[00:00:53.957,702] <inf> main:  Current:           3000
[00:00:53.957,733] <inf> main:  Voltage:           15000
[00:00:53.957,733] <inf> main:  Peak Current:      0
[00:00:53.957,733] <inf> main:  Uchunked Support:  0
[00:00:53.957,733] <inf> main:  Dual Role Data:    0
[00:00:53.957,763] <inf> main:  USB Comms:         0
[00:00:53.957,763] <inf> main:  Unconstrained Pwr: 0
[00:00:53.957,763] <inf> main:  USB Suspend:       0
[00:00:53.957,763] <inf> main:  Dual Role Power:   0
[00:00:54.007,873] <inf> main: PDO 4:
[00:00:54.007,873] <inf> main:  Type:              FIXED
[00:00:54.007,873] <inf> main:  Current:           3250
[00:00:54.007,904] <inf> main:  Voltage:           20000
[00:00:54.007,904] <inf> main:  Peak Current:      0
[00:00:54.007,904] <inf> main:  Uchunked Support:  0
[00:00:54.007,904] <inf> main:  Dual Role Data:    0
[00:00:54.007,934] <inf> main:  USB Comms:         0
[00:00:54.007,934] <inf> main:  Unconstrained Pwr: 0
[00:00:54.007,934] <inf> main:  USB Suspend:       0
[00:00:54.007,934] <inf> main:  Dual Role Power:   0
[00:00:54.058,044] <inf> main: PDO 5:
[00:00:54.058,044] <inf> main:  Type:              AUGMENTED
[00:00:54.058,044] <inf> main:  Min Voltage:       3300
[00:00:54.058,074] <inf> main:  Max Voltage:       11000
[00:00:54.058,074] <inf> main:  Max Current:       3000
[00:00:54.058,074] <inf> main:  PPS Power Limited: 0
[00:00:54.108,184] <inf> main: PDO 6:
[00:00:54.108,184] <inf> main:  Type:              AUGMENTED
[00:00:54.108,184] <inf> main:  Min Voltage:       3300
[00:00:54.108,215] <inf> main:  Max Voltage:       21000
[00:00:54.108,215] <inf> main:  Max Current:       2850
[00:00:54.108,215] <inf> main:  PPS Power Limited: 0

Conclusion

The USB-C TCPM stack in Zephyr is quite functional with a TCPC such as the FUSB307B. The example source code is pretty basic and only requests the first PDO by default, but proves the stack is working. It is then up to the developer to add their desired functionality (i.e. augmented PPS support).



1 Comment

Leave a Reply

Your email address will not be published.


*