Really, the most modern implementation of DisplayPort is the USB-C DisplayPort altmode, synonymous with “video over USB-C”, and we’d miss out if I were to skip it. Incidentally, our last two articles about talking USB-PD have given a few people a cool new toy to play with – people have commented on the articles, reached out to me for debugging help, and I’ve even seen people build the FUSB302B into their projects! Hot on the heels of that achievement, let’s reach further and conquer one more USB-C feature – one that isn’t yet openly available for us to hack on, even though it deserves to be.
For our long-time readers, it’s no surprise to see mundane capabilities denied to hackers. By now, we all know that many laptops and phones let you get a DisplayPort connection out of a USB-C port. Given that the USB-C specifications are openly available, and we’ve previously implemented a PD sink using those specifications, you’d expect that we could do DisplayPort with the same ease. Yet, the DisplayPort altmode specification is behind a VESA membership paywall, with a hefty pricetag – a practice of theirs that has been widely criticized, counter to their purpose as a standards organization and having resulted in some of their standards failing.
Not to worry, however – we can easily find an assortment of PDFs giving a high-level overview and some details of the DisplayPort altmode, and here’s my favorite! I also have a device running MicroPython with a FUSB302 chip connected, and a few DisplayPort altmode devices of mine that I can disassemble. This, turns out, is more than enough for us to reverse-engineer our way into an open-source DisplayPort altmode library!
DisplayPort Over USB-C Basics
The USB-C port has four high-speed pairs, and one auxiliary lower-speed pair (SBU). This beautifully maps onto the DisplayPort requirements, with up to four high-speed data transfer pairs, and one AUX configuration channel. One small quirk – there’s no pin for the HPD signal; instead, its status is forwarded inside of DisplayPort altmode messages over the PD channel. As a result, you can plug your device into a DisplayPort-capable USB-C, write a few magic words over PD, and get a DisplayPort signal on the USB-C TX/RX pins! No need to delve into DisplayPort internals whatsoever; the most you will need is to forward HPD as a PD message, and if your device uses a USB-C socket, have a cheap mux flip the signals according to the way your USB-C cable is plugged in.
Aside from DisplayPort, you also get USB 2.0 on the good old USB2 pins – perfect for plugging in a keyboard and mouse alongside your monitor. That’s not all you can extract, however – if you’re content with two-lane DisplayPort, you can ask the upstream device to provide you two lanes of DisplayPort on one pair of pins, and a USB3 port on another! This is how the majority of cheap USB-C docks work – they get two lanes of DisplayPort used for VGA or HDMI, USB3 for a high-speed port or a few peripherals, and USB2 for a whole bunch of other stuff, handling your power input on the side.
Judging from the PDF we have from ST, there are seven kinds of PD messages that we need to answer if we want to build a DisplayPort device – the diagram on page 13 shows them all. In the “All About USB-C: Replying Low-Level PD” article, we’ve learned two types of messages –
Source_Capabilities, which a USB-C PSU power profile advertisement, and the
Request message, which we’ve crafted to get one of those power profiles and get a higher voltage out of a USB-C port. From two to seven – this is well within our reach!
What do we need to do to reverse-engineer it, at the bare minimum? I’d say, the PDF seems to contain more than enough info on its own – the communication flow, different command codes and contents are described there. However, it will be way more comfortable if we are to have packet captures to reference!
Packets In Captivity
USB-C communications sniffing is an underexplored field – especially if high-speed signals are involved. For those, you need an interposer board that preserves signal integrity while letting you tap into the CC pins, and those aren’t quite dime a dozen. When it comes to commercial tools for USB-C sniffing, I feel like most of those are priced accounting for the fact that many people don’t understand USB-C. However, there’s certainly ways around it – in the comment section of the first PD talking article, [WF] has pointed us towards a way to sniff arbitrary USB-C packets with a logic analyzer and a simple extra circuit, with help of sigrok and Pulseview! We are making a device that can talk DisplayPort altmode, not just sniff it, but if you’d like to tap into a device of yours as you follow alongside this article, this ought to be enough.
That said, there might be an even simpler solution, if you’ve been following along, you might just own a FUSB302B, which is a USB-C PHY IC. Since the “Replying PD” article got published, I’ve been slowly building upon the capabilities of my personal MicroPython USB-PD “stack” – assortment of PD functions and code, rather, but I’ll call it a stack until a more fitting name is found. First, I’ve added packet listening capability – switching the FUSB302 into receive-only mode, reading its input FIFO as quickly as possible, and parsing the data on the fly. I’ve also added packet information parsing, so that you can see USB-C communications in the serial console, without having to read them first.
There’s a caveat, of course – I can’t easily do passthrough capture of USB-C packets, since I didn’t want to design an open-source passthrough board with CC tapping capability, and these boards aren’t quite available. That said, someone should do that – it’s a bit of a shame that the USB-C-Thru device never got funded! Instead, I’ve started by disassembling captive cable devices I own, then tapping into the CC pin – since, with a captive cable device, there’s only one CC pin possible. This means I don’t need to autodetect rotation, which is nice because, given the time that my MicroPython code could take to figure out the rotation, the USB-PD conversation might be over by that point already. So, my board’s CC1 wired to one of my USB dock’s CC pin, CC1 hardcoded to be the listening pin – what else?
You’ll want to disable the FUSB pullups – they’re going to be counterproductive here, as we’re tapping into an existing pullup/pulldown arrangement, and introducing one more pulldown will result in VBUS getting switched off. You’ll also want to disable GoodCRC responses – FUSB302 does them automatically, which helps when we use it as a sink, but here they’ll conflict with GoodCRC responses from both sides of the USB-C conversation; disabling the transmitter is even better. I’ve also enabled SOP’/” packet reception – these packets are used for emarkers, and while we don’t normally need to receive them, being able to sniff them now is good.
Now, we’re ready! Mind you, USB-C communications happen seriously fast. My MicroPython code isn’t speedy either – I use MicroPython because I chose hackability over execution speed. However, as a consequence, I can’t quite parse packets as they arrive – I will miss out on parts of the USB-C conversation if I do that, as, remember, even print statements take a bit of time. Instead, I read the input FIFO contents as quickly as possible, store packets in RAM, and parse them afterwards. On the upside, having packet captures in RAM also means that I end up with PD conversation recordings I can easily store and replay later, and you get a few of these captures as well!
There’s downsides to using this method of packet capture, of course – I might still not be able to capture communications that happen too fast, I only capture packets with valid CRC and will miss out on any garbled packets, and I don’t have timestamps for the packets received; using a logic analyzer would negate all of these. However, for our DisplayPort RE purposes, it’s more than good enough, and any parsing code I write, will be super helpful when building a library. CC pin wired up, code running and ready, let’s go!
This Is VDM Turf
Here you can see a power profile negotiation happening –
PS_RDY, things we’ve already done before. These are required if you are to talk PD for any purpose, so it’s not much of a surprise. However, there’s also a whole bunch of
Vendor_Defined messages, and these might put you on edge. Do not be afraid, though – be grateful to the USB-C standard instead, because, with the way it tells vendors to implement vendor-specific communications, these messages are better documented than you’d expect!
VDMs, or Vendor-Defined Messages, are responsible for any altmode summoning that goes beyond the regular “USB3, USB2 and PD” things you can get – you can use VDMs for anything that falls outside the standard, from custom altmodes to firmware updates. You can have them unstructured or structured – unstructured messages are basically freeform, while structured messages are kind of a template for a typical conversation that a vendor might actually want to implement. DisplayPort negotiation uses structured messages, and out of the seven commands involved in setting up the DisplayPort altmode, five of them are commands already defined in the USB-C standard! As for the two remaining ones, the PDF we have, very helpfully mentions their codes on page 8, and describes them in more detail on page 10-12.
These commands are a bit special – it’s not just that a GoodCRC response is required, the FUSB302B will take care of it for us, anyway. It’s also that every command can be either a request or a response, and either of the directions can carry extra data, depending on the specific command being used! Thankfully, all of this optional data is described in the PDF – which has been proving more and more helpful the deeper we go.
All in all, we won’t need to reverse-engineer that much, specifically – the main problem, in my assessment, would be the bitfields. Also, since we don’t have the full specification, we might make a crucial mistake or two – for instance, we don’t know how quickly we must answer these commands, or the specifics of handling the DisplayPort HPD signal properly. We can figure these out however, and I’ve got quite a variety of USB-C DisplayPort devices to get packet captures from!
Replay Works – Proper Implementation Time
Well, now we have DisplayPort conversation commands, captured from a real-life device – if we wanted to make a DisplayPort sink right now, we can just replay them, only adjusting small things like message ID! In fact, here’s a piece of code which does just that – sends back the commands that we captured, and I’ve successfully made it summon the DisplayPort altmode on my own laptop! Now, I didn’t verify the high-speed DisplayPort output, but I got voltage on SBU pins, which means that the AUX diffpair has been wired up to those – something that only happens after the DisplayPort altmode has been successfully summoned.
The fundamental part of the replay code is the request-response loop, which does rely on our parsing code to answer incoming messages. This is great for us – we’ll need exactly this kind of loop once we can actually construct our own replies, just that we’ll need a bit more sophisticated one. Until then, this is enough to get me by when it comes to a personal project of mine.
The next tasks are to actually make sense of these commands and implement a meaningful DisplayPort library! We’ll go through the seven commands required, explain each one of them, parse the ones we’ll receive, and implement the ones we’ll have to send back. Afterwards, we’ll tie them all together into the already existing loop, figure out USB-C high-speed lane rotation handling, and we’ll be ready to build open-source DisplayPort handling devices for any capable USB-C port in sight. After all, these days, even a PinePhone can do DisplayPort!