USB Power Delivery for Makers

On 02 Jun 2024

As I age, most of the stuff that frightened throughout my life seem silly in retrospect. Two items are firmly cemented in that list though: the fear of losing the people I love and the fear of the USB specification.

In this blog post, we will investigate the USB Power Delivery (USB-PD) standard – the part of the spec that lets us pull up to 48 V and 5 A from a USB-C port. We will use this knowledge to build a trigger board, a device to harvest the USB-PD power and pipe it into our projects.

Come with me, and I promise that, by the end of this post, the USB specification will not out us as sure imposters to our friends. Instead, we will tame its power and remain ever secretly unsure of our skills.

USB Type-C Current

Before jumping into USB-PD, let’s take a look at some simpler ways to pull power from a USB port. In the days before USB-C, USB 2.0 simply stated that a compliant host port must provide 500 mA at 5 V. By USB 3.2, the current was bumped to 900 mA.

In this world, USB cables are asymmetrical – the plug at the power provider is different than the plug at power receiver. Think of how the two ends of those old USB printer cables look, or the two ends of a micro USB cable. The hardware makes it impossible to connect two ports providing power:

danger male-to-male plug

Then came USB-C with its hip symmetrical cable and reversible plug. We may now work with up to 3 A of current at 5 V! Except, now the plug hardware doesn’t prevent us from stringing two power sources together, and we need a mechanism to let both sides of the cable agree on who is providing (a source) and who is taking power (a sink).

Out of the 24 pins in a USB-C connector, two are dedicated to this operation: CC1 and CC2, the Configuration Channels. The spec defines how sources and sinks monitor the voltage on these pins (section 4.6.2.1 of [1]):

CC pin CC line inside a USB-C cable. Image from [1], red annotations by me

A USB-C source uses a pull-up resistor Rp to signal its current sourcing capabilities. Conversely, a sink uses a pull-down resistor Rd as a CC line termination.

Note how there are two CC pins, but a single CC line in the diagram above. This is because the plug is reversible – CC1 and CC2 are in opposite, symmetrical sides of the connector. The USB-C cable itself has a single CC line that connects either CC1 or CC2 at one side to either CC1 or CC2 at the other end.

Sources choose values of Rp to signal their maximum available current. Table 4-27 in [1] contains valid values of Rp. We makers most often design sink devices – we use USB-C to power our boards, charge our batteries – and for us the situation is surprisingly simple: all we need to do is to slap two 5.1 kΩ pull-down resistors between CC1 and CC2 and GND, and we’re in business for pulling up to 3 A from a USB-C source.

If you look up some open hardware schematics that use USB-C as a sink, chances are you’ll spot those lil’ 5.1 kΩ resistors. Here’s the USB-C schematic for Adafruit’s Feather RP2040:

Feather RP2040 Section of the Feather RP2040 schematic

And for the new ESP32-C6-DevKitC-1 development board from Espressif:

ESP32-C6-DevKitC-1 Section of the ESP32-C6-DevKitC-1 schematic

Somewhat reassuringly, sometimes even the greats get it wrong. But this is a pretty sweet deal: they get two resistors and we get 3 A at 5 V. This is very often all we need: we step it down to 3.3 V for our microcontroller and use it to charge our 4.2 V LiPo cells.

This is where things get more interesting. The USB-PD spec defines a way to negotiate even higher currents and other voltages than the 5 V we discussed until now. As of writing, the latest USB-PD 3.2 spec [2] offers us up to 5 A of current and 48 V, for a whopping 240 W of power.

Over the past couple of years I’ve been more interested in ultra-low power, tiny battery-operated devices, so I found it helpful to put these values in perspective:

Device Power
USB 2.0 port 2.5 W (500 mA @ 5V)
USB 3.2 port 4.5 W (900 mA @ 5V)
Philips Hue Smart Bulb 10 W - 16 W
USB Type-C (non-PD) 15 W (3 A @ 5 V)
300 RGB LEDs strip @ 12 V 50 W
Mini fridge 60 W - 100 W (average)
MacBook Pro 16” (2023) fast charging 140 W (5 A @ 28 V)
USB-PD 3.2 max power 240 W (5 A @ 48 V)
RTX 4090 high-end graphics card 450 W

USB-PD

Okay, now we’re ready to get to the meat of the post. The question we’ll answer is: how do I get those sweet 240 W out of a USB-C Power Delivery port?

As in the previous section, the solution will include the CC lines. But now, instead of fixed resistors, we will have to do a little dance involving back-and-forth signals between the source and sink.

The Physical Layer

The USB-PD communication happens over the CC line. Section 4.6.2.4 [1] contains the following diagram:

BMC Diagram USB-PD BMC controllers diagram. Image from [1]

Now, in addition to the Rp and Rd resistors from before, both the source and the sink have PD controllers. BMC (bi-phase mark coding) is the physical coding scheme that dictates how logic 1s and 0s are represented on the wire. It’s an interesting topic on its own how it combines both data and clock in a single signal. For us, it suffices to know that 1s and 0s are represented as transitions between two voltage levels on the CC line – this page explains it well.

Figure 4-39 [1] shows a daunting example of a BMC signal over the CC line:

BMC Signal BMC example. Image from [1], red annotations by me

Oof – what’s with all those different voltage levels? Let’s focus on the bold black line, the voltage measured at the source’s CC pin. Prior to cable attachment, the source’s CC line hovers at 1.8V (1). Once the cable is connected, the CC line settles at around 0.5V (2), because the sink now forms a voltage divider. This level (2) is the baseline DC voltage of the attached CC line. The actual BMC signal operates between a high level of roughly 1.1V (3) and a low level of around 0V (4). Not so bad.

For practical purposes, the BMC signal in the CC line is a digital signal between 0V and ~1.2V. In [2], we see some diagrams representing the BMC signal as such:

BMC Signal BMC as a digital signal. Image from [2]

Capturing the BMC Signal

We can hook up the CC line to an oscilloscope and marvel at the BMC signalling scheme. But if we want to decode the signal to inspect and debug it – which will prove itself crucial to our implementation later on – we better also pipe the signal to a logic analyzer.

The only logic analyzer I have around is a cheap knockoff that will straight up not have a good time dealing with 1.2 V as logic high. Before hooking it up, we must amplify the BMC signal to operate between 0 and 3.3 V-ish instead.

A smarter person would probably use an op-amp for this. I had a friendly MOSFET already stuck on my breadboard and used it to make a common source amplifier:

MOSFET Amplifier An N-channel MOSFET common source amplifier

I experimented a bit and settled on an ALD1103 and a 10 kΩ resistor:

Amplifier on the breadboard

How does the amplified signal look? Somewhere between bad and terrible. The rise and fall times for this setup are very slow for the 300 kHz BMC signal. Here you can see the original BMC signal in yellow and the amplified signal in blue:

BMC oscilloscope capture Oscilloscope capture of the BMC signal and its poorly amplified version

Although I wouldn’t put this in a real project, and it pains me to look at it, it kind of achieves the goal: the voltage swings are at least large enough to satisfy the logic analyzer thresholds. We’re also inverting the signal, but – lucky us – the BMC coding doesn’t care about that.

Believe it or not, this hack worked well enough that I didn’t think about it throughout the rest of the project, so I will call it a success. Really nothing as permanent as a janky, temporary solution.

Either way, we now can capture and analyze the BMC signal with our logic analyzer and hopefully decode those 1s and 0s into USB-PD messages. Luckily, some folks already built a BMC decoder that we can use: the PD3.1-analyzer. This plugin for the Saleae Logic software will decode our amplified BMC signal into USB-PD protocol messages.

The Protocol Layer

With the physical layer sorted out – that is, how 1s and 0s are encoded on the wire – , we can now turn to the protocol layer. This layer defines the types and contents of the messages exchanged between the source and the sink to negotiate power.

Let’s make things more concrete by looking at a basic example. Here’s a simplified high-level overview of a typical exchange between a source and a sink in USB-PD 2.0:

Example messages A basic, happy-path USB-PD 2.0 negotiation

With that exchange in mind, we will look in detail at these messages. But first, let’s talk about what’s common between all of them.

Every USB-PD message will contain the following parts (section 5.6 of [2]):

Part Size (bits) Description
Preamble 64 A fixed sequence of alternating 0s and 1s. Used to signal the start of frame and clock to the receiver (see Syncword)
SOP 20 Start of Packet. A sequence of 5-bit symbols that represent to whom the message is addressed to
Message Header 16 Contains the message type and the number of data objects. A Data Message will contain one or more data objects; A Control Message has zero Data Objects
[Optional] Data Objects 32 each Optional message payload. Its interpretation depends on the message type
CRC 32 Cyclic Redundancy Check
EOP 5 End of Packet. A fixed 5-bit symbol that lets the receiver know the message is over

In the following logic analyzer capture, you can see those parts in a USB-PD message:

Logic analyzer capture example Logic analyzer capture and decoding of a BMC signal. This is a PD message requesting power from the source

From now on, we mostly care about the Message Header and the Data Objects, as, for our purposes, the other components are either constant or determined from it. Let’s go back to the example messages and see what they contain.

Source Capabilities Message & PDOs

This message is sent by the source to advertise its power capabilities. It goes by the name of Source_Capabilities (section 6.4.1 of [2]). The message contains a list of Power Data Objects (PDOs) in the Data Objects part of the message. PDOs are an important currency in USB-PD, and a term that pops up everywhere. Let’s spend some time understanding them.

Each PDO describes a specific power profile that the source can provide. Take a look at this photo of my Anker charger:

Anker Charger

And here’s the Source Capabilities message from the same charger:

Anker Source Capabilities

Not the easiest to read, but we can see the same data in table format:

Anker Source Capabilities Table

Aha! We see the same information printed on the charger in the form of its advertised PDOs. But if you look closely to the last PDO, you will see that it’s not like the others. Whereas the first five PDOs are indeed as printed on the charger – i.e.: fixed voltage – the last one is a Programmable Power Supply (PPS) PDO – a USB-PD 3.0 feature.

This particular PPS PDO allows us to request a voltage between 3.3V and 11V in 20 mV steps. This can be very interesting for makers, as we will see later.

Other PDO types are possible. This is the Source_Capabilities message from a 140 W UGREEN charger:

UGREEN Source Capabilities

This one offers a PPS PDO of up to 21V. But it also offers something even juicer: a 28 V @ 5 A fixed PDO for a total of 140 W. This is supported by a USB-PD 3.1 feature called Extended Power Range (EPR), which lets us go over the 100 W limit of USB-PD 3.0 PDOs.

It is through these Extended Power Range PDOs that the USB-PD 3.1+ spec lets us pull the maximum power of 240 W, from a 48 V @ 5 A PDO. There also exists the Adjustable Voltage Supply (AVS) PDO, letting us request fine-grained voltages in the Extended Power Range domain.

As of writing, I haven’t managed to get any AVS nor EPR-capable sources with PDOs over 28 V to play with. There are some EPR-capable chargers offering 28 V @ 5 A out there, such as the 140 W Macbook pro 16” charger and the UGREEN 140 W. Some will advertise higher wattages, like the UGREEN 200W, but it just means total wattage – the maximum power per port is still 100 W.

The new Framework 180 W charger is the only one I am aware that offers up to 36V @ 5A, and supports AVS mode between 15 - 36 V. It looks pretty sweet, but sadly it does’t seem to support PPS PDOs. So we’d be out of luck if we needed, say, 7 V.

Request Message

After receiving the source’s PDOs via the Source_Capabilities message, the sink will elect one of them and send a Request message (section 6.4.2 [2]) to the source. There are variations of the contents of this message depending on the PDO we want to request. In the Extended Power Range domain, a separate message EPR_Request (section 6.4.9 [2]) is used altogether.

Table 6.26 [2] shows us how to request a PPS PDO by building a PPS Request Data Object (RDO):

Request message PPS request message. From [2]

Here’s the raw message I used to request 11 V @ 1 A from the Anker charger above:

PPS request

For us the most important fields are:

  • Object Position: 6, as we’re requesting the 6th PDO
  • Output Voltage: set to 11000 mV / 20 mV
  • Operating Current: set to 1000 mA / 50 mA

Let’s double check if our decoder’s raw value above matches our expectation:

>>> hex(6 << 28 | (11000 // 20) << 9 | (1000 // 50) << 0)
'0x60044c14'

Accept & Ready Messages

If all goes well with the Request message, the source will send back an Accept message (section 6.3.3 [2]). This is a Control Message, meaning it has no Data Objects. Shortly after, the source will send a PS_RDY message (section 6.3.6 [2]) to signal that the requested voltage is now present on the bus.

Some Other Message Types

The spec defines many other features backed by many other message types. Some examples:

Message Type Description
Get_Source_Cap Sent by the sink to request the source’s capabilities
Soft_Reset Sent by either source or sink to signal a reset in the USB-PD logic, often due to communication error
Reject Sent by either source or sink to signal a failure to fullfil a request

Other messages are used to support more advanced features like Power Swap, in which the source and the sink exchange roles. Figure 8-135 [2] contains an authoritative state machine for the sink, and while it’s a bit intimidating, it is a great reference.

Extended Messages

Longer messages may be split into multiple chunks. This is signaled by the Extended bit in the Message Header (section 6.2.1.2 [2]). The receiver is expected to repeatedly request the next chunk and rebuild the message from them.

Some Raw Logic Analyzer Captures

I dumped some full captures of the USB-PD negotiation with different PDOs and sources in this repo. Some interesting cases:

Timing Matters

Many USB-PD operations are time-sensitive. For example, after receiving a message that expects a response, the sink has roughly 30 ms (tSenderResponse, Section 6.6.2 [2]) to respond. Sometimes even adding a bunch of log messages may cause us to miss this deadline, causing a failure in the negotiation and a hard reset from the sender.

I captured one of those failures in anker100w-safe5v-fallback-success-then-failure-too-much-logging.sal. In this scenario, after a successful negotiation, the source still sent some unsolicited Source_Capabilities messages, to which I couldn’t respond in time because I was too busy celebrating the succesful negotiation with STDOUT.

Some other timers are worth mentioning:

Timer Section Description
tPPSTimeout 6.6.19.2 [2] When negotiating a PPS PDO, we need to periodically se-rend the same Request at least each 12 seconds.
tSourceEPR KeepAlive 6.6.21.3 [2] When operating in the Extended Power Range domain, the sink must send an EPR_KeepAlive message at least every 750 ms

The Current Limit

While USB-PD 3.2 goes up to 240 W, there is a global upper limit of 5 A for the current. This means two things:

  • If we want higher power, we must increase the voltage, up to the maximum of 48 V
  • At lower voltages, we don’t have as much room for power. At 12 V, we can pull a maximum of 60 W

USB-PD Sniffers

gregdavill/PD-sniffer and dojoe/Twonkie are DIY-friendly reimplementations of Chromium project’s Twinkie. These are boards that place themselves between source and sinks, decode and dump the USB-PD messages between them.

Twonkie The Twonkie USB-PD. Image from dojoe/Twonkie

I haven’t built those, mostly because I didn’t know they existed when I started this project, but they make sniffing the USB-PD messages considerably easier than using an amplifier and logic analyzer like I did here. If you’re only interested in the protocol layer and above, these seem like excellent choices.

Software References

The Ralim/usb-pd contains a great implementation of a USB-PD sink stack using the FUSB302 IC for the physical layer. It was extracted from the also excellent Ralim/IronOS, an open source firmware for USB-C soldering irons – one of the first places where USB-PD started shining for makers.

Clara Hobbs’ work on PD Buddy Sink also laid the groundwork for many other USB-PD projects.

Introducing: GIMME DANGER!!

I wanted to try my hand at designing a USB-PD board and the software stack to go with it. The idea was to build a all-in-one board that would let me pull power from a USB-PD source and use it to power LED strips and other smart home devices through ESPHome and Home Assistant.

All hardware design files and software are available in the rbaron/gimme-danger.

GIMME DANGER!! front GIMME DANGER!! PCB front

The Hardware

  • ESP32-S3 module
  • FUSB302B USB-PD controller
  • INA219 current sensor
  • 5 output PWM channels
  • 2x5.5mm barrel jack for raw output voltage access
  • Optional OLED display

The PWM drivers are rated for 30 V. The FUSB302B is only rated for 20 V, with an absolute maximum of 28 V. Although I have been using it with EPR mode @ 28 V, the safe voltage limit is 20 V. As EPR USB-PD gets more popular, I hope we start to see more controllers that are rated for higher voltages and accessible to makers.

GIMME DANGER!! back GIMME DANGER!! PCB back

When designing a USB-PD capable board, we need to pay attention to details that are not as critical in the low-power boards I’m used to build. I highly recommend this video from Phil’s Lab on USB-C Power Delivery Hardware Design. I am an absolute fan of his channel and always learn a lot from his videos.

The Software

I implemented a custom ESPHome component to handle the USB-PD negotiation through the FUSB302B. The FUSB302B handles the physical layer, and its the component’s job to build the messages, timers, and state machines to negotiate power with the source.

The custom fusb302 component lives in the ESPHome fork github.com/rbaron/tree/fusb302. To use it, all wee need is to import the external_component into a typical ESPHome YAML config file. For example:

external_components:
  - source:
      type: git
      url: https://github.com/rbaron/esphome
      ref: fusb302
    components: [fusb302]

fusb302:
  voltage: 11000 mV
  current: 1000 mA
  start_power_negotiation_on_boot: false
  on_pd_negotiation_success:
    - lambda: |-
        ESP_LOGW("fusb302_trigger", "PD negotiation success outcome: %d", success);
  on_pd_negotiation_failure:
    then:
      - lambda: |-
          ESP_LOGW("fusb302_trigger", "PD negotiation failure outcome: %d", success);

Beside the ESP32-S3 in GIMME DANGER!!I, also tested this component succesfully on a regular ESP32 board, a ESP32-C6 board and an RP2040 board. I would love to contribute this component upstream, but with the high power involved and the potentially costly side effects of bugs, I would be more comfortable if more experienced folks used it first before advertising it to all users.

I also tried to test the implementation on a bunch of different USB-PD sources – some of them are kinda liberal with the specs. Here’s my humble test bench:

Test bench

Demos

12 V LED Strip

Here’s GIMMER DANGER!! controlling a 12 V LED strip through Home Assistant:

A Makeshift Programmable Power Supply

With the PPS PDOs, we can request voltages in a fine grained manner. We can even change the voltage on the fly. Here’s a potentiometer controlling the voltage set point, and a button that triggers the USB-PD power negotiation for that set point:

A WiFi-Controller Power Supply

Here’s a terrible and fun idea: a Power Supply that you can control through Home Assistant:

A 12 V RGB LED Strip

The code for all demos are in rbaron/gimme-danger/tree/main/code/esphome.

Docs

  • [1] USB Type-C Spec R2.3 - October 2023
  • [2] USB Power Delivery Specification Revision 3.2, Version 1.0 - October 2023