Getting started with USB-Power Delivery Sink

Target description

This tutorial helps to:

  • Use the X-NUCLEO-SNK1M1 or X-NUCLEO-USBPDM1 shield that includes a TCPP01-M12 protection circuit and provides a USB Type-C® connector
  • Create a USB-PD Sink Device with the NUCLEO-G071RB board and the X-NUCLEO-SNK1M1 or X-NUCLEO-USBPDM1 shield by using STM32CubeIDE software


  • Computer with Windows 7 (or higher)


  • NUCLEO-G071RB (tested on rev B01) [1]
  • X-NUCLEO-SNK1M1 (tested on Rev 1) [2] or X-NUCLEO-USBPDM1 [3]
  • A USB-PD source device to test our USB-PD device (it can be a PD capable wall charger, a power bank, etc.)
  • USB cable Type-A to Micro-B
  • USB Type-C® cable


  • STM32CubeIDE (tested with V1.8.0) [4]
  • STM32CubeMonitor-UCPD (tested with V1.2.0) [5]
  • X-CUBE-TCPP MCU Firmware Package (for example projects) [6]


Create a USB-PD Sink Device

Clock.png Total 45min

1. Creating the project

Clock.png 5min

Open STM32CubeIDE and create a New STM32 Project. As a target selection, choose the STM32G071RB from the MCU/MPU Selector Tab

Click "Next", then enter your project's name. Leave the other fields as default and click "Finish".


2. Configuring the system

Clock.png 15min

At this point, your project is created and you are left with the STM32CubeMX view. In the next steps, we configure the peripherals and options needed for the project.

2.1. Configure UCPD peripheral

In the Connectivity tab, select the UCPD1 peripheral and enable it in sink mode. Under the NVIC Settings tab, enable UCPD global interrupts.

Info white.png Information
You do not need to include “Dead Battery Signals” in the configuration as this is managed by the TCPP01 protection device on the X-NUCLEO-SNK1M1 shield.

Under the DMA Settings tab, add UCPD1_RX and UCPD1_TX DMA requests.

2.2. Configure FreeRTOS Middleware

In the Middleware section, enable FreeRTOS with CMSIS_V1 interface. Under the Config Parameters tab, change "TOTAL_HEAP_SIZE" to 7000 bytes.


Then, under the Include Parameters tab, Enable eTaskGetState include definition.


Info white.png Information

If an STM32G4 is used of a G0, LIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY needs to be set to 3 instead of STM32CubeMX's default value 5. In some cases with STM32G4, leaving it to 5 will get the code execution stuck in vPortValidateInterruptPriority function.

2.3. Configure USBPD Middleware

In the Middleware section, enable USBPD with the following configuration:

  • Port Configuration: Port 0: UCPD1
  • Stack Configuration: PD3 Full Stack
  • Timer service Source: TIM1

Under the PDO General Definitions tab, verify the following configuration:

  • Number of Sink PDOs for port 0: 1
  • Port 0 Sink PDO 1 0x26019096 (correspond to a 5V / 1.5A, dual-role data sink)

The following table is extracted from USB Power Delivery Specification, Table 6-14 Fixed Supply PDO - Sink. Used values and associated decoding for this project have been added to the table.

Bit(s) Description Used value Decoding
B31..30 Fixed supply 00b Fixed
B29 Dual-Role Power 0b No
B28 Higher capability 0b No
B27 Unconstrained Power 0b No
B26 USB Communications Capable 0b No
B25 Dual-Role Data 1b Yes
B24..23 Fast Role Swap support 00b No
B22..20 Reserved - Must be set to zero 0b No
B19..10 Voltage in 50mV units 0001100100b 5V
B9..0 Maximum current in 10mA units 0010010110b 1.5A

2.4. Configure ADC peripheral

For the Power Delivery stack to work, VBUS must be monitored. To do it, an ADC needs to be configured to measure the VBUS voltage.
In the Analog section, enable ADC1 peripheral channel 9 (connected to pin PB1). Configure the ADC with the following configuration:

  • Clock pre-scaler : Synch / 4
  • Continuous conversion mode: Enabled
  • Overrun behavior: Overrun data overwritten
  • Sampling time Common 1 & 2: 160.5 Cycles

Info white.png Information
If you are using an STM32G4 board, you have to enable "ADC1_5" peripheral channel instead of "ADC1_9".

Then, under the GPIO Settings tab, rename PB1 pin to VSENSE, a more descriptive name for this pin, used for VBUS monitoring:


Under the User Constants tab, Add a constant named VDDA_APPLI of value 3300.


2.5. Configure GPIOs

For the X-NUCLEO-SNK1M1 shield, two additional GPIO settings are required(not in X-NUCLEO-USBPDM1 because the settings are forced by jumpers).

  • PB6 (DB_OUT for dead battery disabling) GPIO output to HIGH
  • PC10 (VCC_OUT pin to power on the TCPP01‑M12) GPIO output to HIGH

To set this configuration, left-click pins PB6 and PC10 in the Pinout view, and set pins to GPIO_Output. In the System Core section, under GPIO, change the GPIO output level for both pins to High, and set a User label DB_OUT for PB6 AND VCC_OUT for PC10.


For a real application, these GPIO settings must be performed after the UCPD initialization.

2.6. Configure Clocks

Under the Clock Configuration main tab, change system clock mux to PLLCLK. It sets the HCLK clock to 64 MHz.


Info white.png Information
The mandatory settings for the simple USB-PD sink application are finished.

The following part is highly recommended for debugging

2.7. [OPTIONAL] Configure Tracer for debug

2.7.1. Configure LPUART

On the STM32G0 Nucleo-64 board, the Virtual COM port connected to the ST-LINK is the LPUART1.

Warning white.png Warning
The default STM32CubeMX pins used by LPUART1 must be changed to match the STM32G0 Nucleo-64 hardware:
  • PA2 for TX
  • PA3 for RX.

In the Connectivity section, enable LPUART1 in Asynchronous mode, and baud rate 921600 bauds. Leave the rest as default.


In the pinout view, left-click PA2 and PA3 to remap them to LPUART1_TX and LPUART1_RX.


Under the DMA Configuration tab, add a request for LPUART1_TX. Use DMA1 channel 3.


Finally, under the NVIC Settings tab, enable LPUART1 global interrupts.


2.7.2. Configure embedded tracer

In the Utilities section, select TRACER_EMB and use LPUART1 as the trace source.


Then, go back to the USBPD middleware configuration and check the Tracer Source checkbox.


2.7.3. Configure UCPD monitor firmware responder for debug

The firmware interactive stack responder can be activated if interaction with the USB-PD stack is needed, using the UCPD monitor tool. STM32CubeMonUCPD. GUI can be activated only with tracer. In the "Utilities" section, enable GUI_INTERFACE, then enter free text to describe the board.


3. Configure project

Clock.png 5min

Under the Project Manager main tab, configure the minimum stack size to 0xC00 under the Project tab. This is a first value, which can be tuned later, depending on application needs.


Under the Advanced Settings tab, change the LPUART driver to LL.


4. Generate code

Clock.png 5min

Save your file with Ctrl+S and select generate code if prompted. You can also generate code from the STM32CubeIDE menu by clicking Project/Generate Code, or by pressing Alt+K.


A warning appears, informing that a proper HAL time base is not defined. It is safer to use a dedicated timer as a HAL time base source.
For this demonstration, the below warning can be ignored by clicking Yes.


Info white.png Information
This becomes the recommended standard way of working in the forthcoming firmware package deliveries, especially when using CMSIS OS V2, which defines Systick as FreeRTOS™ time base.

For this demonstration, the warning can be ignored by clicking Yes.

5. Simple USB-PD application

Clock.png 10min

Now that the peripherals are initialized by STM32CubeMX, some minimum level of the application needs to be added:

  • ADC needs to be calibrated, and conversion needs to start.
  • Handlers for the interrupt needs to be completed, to wake up the UCPD peripheral.
  • BSP_USBPD_PWR_VBUSGetVoltage function needs to be completed with the right coefficient depending on the VBUS divider bridge.
  • USBPD_DPM_SNK_EvaluateCapabilities function needs to be completed to answer one source capability message.
  • TCPP01‑M12 dead battery pin needs to be disabled and GPIO driven HIGH to see the source Rp, or the jumper must be set on the shield.
Warning white.png Warning
This application is a very basic example that requests the first default 5V PDO. If you want to request a specific voltage from a source, some user code that matches the sink and source PDO needs to be added. Please refer to Advanced USB-PD application section after completing this basic example for a more complete application.

5.1. Modification in main.c

Info white.png Information
You can double-click the code zones to select it all, then copy it with Ctrl+C.

Add the following code between the /* USER CODE BEGIN-END ADC1_Init 2 */ tags:

/* USER CODE END ADC1_Init 2 */
Info white.png Information
If you are using an STM32G4 board, you have to add "HAL_ADCEx_Calibration_Start(&hadc1), sConfig.SingleDiff);" instead of "HAL_ADCEx_Calibration_Start(&hadc1);".

5.2. Modification in usbpd_dpm_user.c

Add the following code USBPD_DPM_GetDataInfo function:

case USBPD_CORE_DATATYPE_SNK_PDO: /*!< Handling of port Sink PDO, requested by get sink capa*/
USBPD_PWR_IF_GetPortPDOs(PortNum, DataId, Ptr, Size);
*Size *= 4;

Add the following code between the /* USER CODE BEGIN-END USBPD_DPM_SNK_EvaluateCapabilities */ tags:

/* USER CODE BEGIN USBPD_DPM_SNK_EvaluateCapabilities */
/* Initialize RDO */
rdo.d32 = 0;
/* Prepare the requested pdo */
rdo.FixedVariableRDO.ObjectPosition = 1;
rdo.FixedVariableRDO.OperatingCurrentIn10mAunits = 50;
rdo.FixedVariableRDO.MaxOperatingCurrent10mAunits = 50;
rdo.FixedVariableRDO.CapabilityMismatch = 0;
*PtrRequestData = rdo.d32;
/* USER CODE END USBPD_DPM_SNK_EvaluateCapabilities */

5.3. Modification in usbpd_pwr_user.c

Add the following code between the /* USER CODE BEGIN-END include */ tags:

/* USER CODE BEGIN include */
#include "main.h"
/* USER CODE END include */

Add the following code between the /* USER CODE BEGIN-END BSP_USBPD_PWR_VBUSGetVoltage */ tags:

/* Check if instance is valid */
int32_t ret = BSP_ERROR_NONE;
if ((Instance >= USBPD_PWR_INSTANCES_NBR) || (NULL == pVoltage))
*pVoltage = 0;
uint32_t val;
LL_ADC_REG_ReadConversionData12(ADC1), \
/* X-NUCLEO-USBPDM board is used */
/* Value is multiplied by 5.97 (Divider R6/R7 (40.2K/200K) for VSENSE) */
val *= 597;
val /= 100;
*pVoltage = val;
return ret;

6. Configure the shield's jumpers

Place jumpers on the X-NUCLEO-SNK1M1 shield as shown in the picture.


If you are instead using the X-NUCLEO-USBPDM1 shield, please follow this configuration:


7. Compile and run the application

The compilation must be performed without error or warnings.
Build the application by clicking on the Built Button.png button (or select Project/Build Project).
Run the application by clicking on the DownloadRun Button.png button (or select Run/Run)

8. Establish the first explicit contract

Clock.png 5min

With your application running on the board, launch the STM32CubeMonitor-UCPD application. The user's board must appear in the list when clicking "Refresh list of connected boards", so double click on the corresponding line (or click "NEXT").


Note: The ComPort may be different. It depends on the number of boards installed on the computer. Then double click on the desired UCPD port, here Port 0, or select it and click "NEXT".


Click on the TRACES button in the bottom right corner to get protocol traces. You can then plug a power delivery source into the USB Type-C® receptacle of the X-NUCLEO-SKN1M1 shield. The screen may look like this:


The figure above shows the communication between the STM32G0 and the power delivery source on the right panel. It is possible to verify the correct sequence to reach an explicit contract:

  1. The capabilities are sent by the source (IN green message).
  2. The request is sent by the STM32G0 (OUT orange message).
  3. The ACCEPT and the PS_RDY are sent by the source (IN green message).
  4. The contract negotiation ends by the POWER_EXPLICIT_CONTRACT notification (blue message).

For more details on how to use this tool, refer to UM2468. And for more details on the protocol, refer to UM2552. Note that this trace is very helpful for debugging and application development.

9. [OPTIONAL] Advanced USB-PD application

Warning white.png Warning
This section is optional and targets the user who wants to choose a specific PDO from a source. The first parts of the wiki need to be completed before referring to this section.

From this point, you built the most simple sink possible, which does not handle matching the sink and source PDO. This must be done with user code. This is done in USBPD_DPM_SNK_EvaluateCapabilities function. For now, this function does nothing more than to ask for the first available source PDO, which is 5V.
To select a specific PDO (for example 9V), you must select the corresponding source PDO number. This can be done with user code that matches the sink PDO defined previously in STM32CubeMX, and the corresponding available source PDO.
This section provides the code of a possible implementation of this system.

You need to:

  • Add all the sink PDO you want to support in STM32CubeMX
  • Add the user code provided below in your application

9.1. Add the supported sink PDO

Re-open the STM32CubeMX view. In the Middleware section, for the USBPD middleware, under the PDO General Definitions tab, add the number of PDO you want your sink to support. For this example, we defined 3PDO, corresponding to a simple sink configuration of 5V/1.5A, 9V/1.5A, and 15V/1.5A. This was done by setting the PDO to:

  • 0x00019096 (Fixed PDO: 5V 1.5A)
  • 0x0002D096 (Fixed PDO: 9V 1.5A)
  • 0x0004B096 (Fixed PDO: 15V 1.5A)

9.2. Get the usbpd_user_services.c/h files on GitHub

Get the usbpd_user_services.c/h files on GitHub by visiting the following links:

- usbpd_user_services.c

- usbpd_user_services.h

Copy the usbpd_user_services.c/h files in your application USBPD folder.

Then, to tell STM32CubeMX to include these files upon code generation, create a file named ".extSettings" at the project's root folder (please mind the dot character in the filename) and fill it with the following code:


9.3. Modification in usbpd_dpm_user.c

Add the following code between the /* USER CODE BEGIN-END Includes */ tags:

/* USER CODE BEGIN Includes */
#include "usbpd_user_services.h"
/* USER CODE END Includes */

Add the following code between the /* USER CODE BEGIN-END USBPD_DPM_SetDataInfo */ tags, in the switch:

    /* Case Received Source PDO values Data information :
  case USBPD_CORE_DATATYPE_RCV_SRC_PDO:         /*!< Storage of Received Source PDO values        */
      USBPD_USER_SERV_StoreSRCPDO(PortNum, Ptr, Size);

Add the following code between the /* USER CODE BEGIN-END USBPD_DPM_SNK_EvaluateCapabilities */ tags (replace the previously added code):

/* USER CODE BEGIN USBPD_DPM_SNK_EvaluateCapabilities */
  USBPD_USER_SERV_EvaluateCapa(PortNum, PtrRequestData, PtrPowerObjectType);
/* USER CODE END USBPD_DPM_SNK_EvaluateCapabilities */

You can now re-generate the code, build it and run it. Your application asks for the highest voltage matching PDO (voltage/current supported by the source and the sink).
You can modify this user code further for example to choose the highest power PDO instead of the highest voltage.

Info white.png Information
To add more Power Delivery functionalities, you could implement user code in the sections showing "ADVICE..." in the trace.

You can find other applicative examples on GitHub: x-cube-tcpp

10. References