This started as an annoyance. One particular USB-C charger would only ever give my laptop 15W, despite being rated for 65W, and despite the same laptop happily pulling 65W from a different brick. The cable was fine. The wall was fine. Something in the negotiation was going wrong, and the only way to find out what was to listen in on the conversation.
USB Power Delivery is a real protocol with a real handshake, and it all happens on a single wire: the CC (Configuration Channel) line. Before any high voltage or high current flows, the source and the sink talk to each other and agree on what's safe. Get a probe on that line and you can watch the whole negotiation.
The physical layer is the fun part
PD messages on the CC line are sent using BMC, Biphase Mark Coding, at around 300kHz. The trick of BMC is that a transition happens at the start of every bit period, and a one has an extra transition in the middle whilst a zero does not. That makes it self-clocking: you don't need a separate clock line, because the clock is baked into the edges. It also means a flat stretch of signal is a zero and a busy stretch is a one, which you can almost read by eye once you know the trick.
I put a logic analyser on the CC line at 24 MS/s, which is plenty for a 300kHz signal, and triggered on the first edge after plugging in. The capture is dense but legible.
Decoding BMC by hand is tedious, so I let the software do it. Sigrok's PulseView has a USB PD decoder stack that will take the raw edges, recover the BMC bits, assemble them into packets, check the CRC, and hand you decoded messages. The first time it lit up with a clean Source_Capabilities message I may have made a noise.
What the messages actually say
The handshake, once decoded, reads like a polite negotiation:
Source -> Sink : Source_Capabilities
PDO 1: Fixed 5V 3.00A
PDO 2: Fixed 9V 3.00A
PDO 3: Fixed 15V 3.00A
PDO 4: Fixed 20V 3.25A
Sink -> Source : Request (PDO 4, 20V 3.25A)
Source -> Sink : Accept
Source -> Sink : PS_RDY
The source advertises a menu of Power Data Objects, the fixed voltage/current pairs it can supply. The sink picks one by index, the source accepts, and once it has actually ramped the voltage it sends PS_RDY to say the new voltage is live. Only then does the laptop start pulling serious current. It's a careful little protocol, and it has to be, because the alternative is putting 20V across a device that only asked for 5.
On the good charger, my laptop requested PDO 4, got 20V at 3.25A, and pulled its 65W. Clean. On the misbehaving charger, the decode told the story immediately.
The bug
The bad charger's Source_Capabilities only advertised three PDOs, and the highest was 9V 1.67A. That's 15W. My laptop wasn't being throttled. It was being offered nothing better and taking the best on the menu, exactly as designed.
So why did a 65W-rated brick advertise only 15W? I cracked it open. It was a multi-port charger, and the high-power PDOs were gated by which combination of ports had something plugged in. With a device in the second port, the controller dropped the first port to a 15W profile so the total stayed within budget. Entirely sensible behaviour, completely undocumented on the casing, and invisible until you read the actual capabilities message.
# the offending capabilities, second port occupied
PDO 1: Fixed 5V 3.00A (15W)
PDO 2: Fixed 9V 1.67A (15W)
I'd been blaming the cable for weeks.
Worth the afternoon
The fix was trivial once I understood it: don't share the charger when I need full speed. But the understanding was the point. USB-C has a reputation for being a confusing soup of cables that may or may not do what you want, and a lot of that confusion is just an invisible negotiation going on that you were never shown.
A cheap logic analyser and an open-source decoder turn that invisible conversation into plain text. If you've ever had a charger that "should" work and doesn't, the answer is almost always sitting right there in the Source_Capabilities message, waiting for someone to listen.