I have a USB-C power bank that can both accept and deliver power over it’s USB-C port. In the past with other laptops, I was able to control which happens in software.
With my Framework laptop, it by default has the laptop charging the power bank, but I can’t seem to find a way to reverse this.
I’ve seen talk that elsewhere that in Linux the USB-C power direction can be controlled via /sys/class/typec however this sysfs folder is empty on my system. Does anyone else have any experience with controlling USB-C power direction on the Framework laptop when running Linux?
I’m running Arch Linux with kernel version 5.17.5
The relevant kernel modules are loaded, so that’s not the problem:
The type-c output voltages this power bank supports are:
5V@3A, 9V@2A, 15V@2A, or 20V@1.5A
This is all negotiated via USB-PD as per the standard.
It’s very much expected that any laptop able to charge over USB-C is able to control it’s source vs sink behavior in software when connected to another device that can perform either role.
Here’s a reference to how it’s normally done under Linux on the command line:
but as I noted /sys/class/typec is empty on my system, so either something is not set up right in software on my system, or perhaps it needs to be done differently on the Framework (perhaps it’s different with the thunderbolt controllers handling usb-c?)
Aha, just built ectool from github.com/DHowett/framework-ec and it has seems to have lots of promising options for controlling USB-C behaviour.
However they don’t seem to work?
$ sudo ./build/bds/util/ectool typecstatus 0
EC returned error result code 1
EC returned error result code 3
EC returned error result code 3
EC returned error result code 1
$ sudo ./build/bds/util/ectool usbpd 0
EC returned error result code 3
EC returned error result code 3
EC returned error result code 1
Unfortunately, hx20 isn’t using the EC for Type-C or PD state management; it instead offloads those responsibilities to two Cypress CCG5 chips (one per side, each controlling two ports.)
I can’t find any documentation on the protocol that chip is using, and it is very possible that it’s been customized since it supports field firmware updates and Cypress ships an SDK.
There’s a trove of information in the cypress5525 code in the embedded controller firmware. Therein might lie the secrets you’re seeking . . . but they’re not going to be terribly easy to extract.
That’s an interesting clue. I notice over in board/hx20/cypress5525.{c,h} there are plenty of references to UCSI, which is the standard for controlling this all normally, which the Linux kernel uses to run the drivers that would populate /sys/class/typec providing the standard way of controlling this all.
I also see board/hx20/ucsi.c in the EC firmware there which has been maintained reasonably by the look of it. From a skim of the code it looks to me like the EC is set up to tunnel UCSI communication between the host OS and the Cypress CCG5. It looks like the EC firmware should be implementing what should be a standard UCSI interface for controlling type-c behavior.
Seems like the question is… why isn’t my Linux kernel finding it? Maybe an ACPI issue? Or perhaps UCSI is exposed to the host OS by a different means than the standard ACPI?
It looks like UCSI should be exposed via ACPI device PNP0CA0, and should report a specific GUID via _DSM. Looking at the hx20's ACPI tables, that is certainly what’s happening. Everything checks out on the ACPI side… including the EC communication through ER1/the “customer memory map”.
EDIT: with dyndbg tracing module acpi +p:
[ 4103.506382] ucsi_acpi USBC000:00: Adding to IOMMU failed: -19 (ENODEV)
Ah, right. /sys/devices/LNXSYSTM:00/LNXSYBUS:00/USBC000:00 is indeed there, with a modalias file showing acpi:USBC000:PNP0CA0: confirming it’s identity.
I’m not seeing the EC’s UCSI debug messages indicating that the host is trying to communicate with it, which is somewhat puzzling. There’s only four places that ucsi and ucsi_acpi will return ENODEV (assuming that the return is coming from inside the house):
One is before it tries to communicate with the device
(1) If it fails to get IORESOURCE_MEM
Two are in early communication
(2) If there are no connectors
(3) If there’s no UCSI version response
One is after communication begins
(4) If an ACPI notification handler could not be installed.
Given that I’m not seeing ADBG or EC logs before 2/3, I suspect 1 . . . but the I/O region seems fine.
[13402.566646] acpi_iommu_configure_id:1609: ucsi_acpi USBC000:00: Adding to IOMMU failed: -19
with dyndbg and reloading the ucsi_acpi module, with line number pointing here, though doesn’t seem that telling and I’m not so familiar with the kernel’s handling of acpi and i/o regions. Been a long time since I dabbled in the kernel. From your description 1 does sound more likely to me, hm.
EDIT: It still would be nice to have UCSI working properly for software control of the type-c interface, but I found a workaround for what originally got me heading down this road: It turns out that holding the button on my power bank for about 3 seconds initiates a role swap from that side. The manual never said anything about that functionality, just had to stumble across it, hah. It’s fun how USB-PD lets either side initiate swapping roles.
So, I did a bit more debugging and figured out that it’s getting as far as querying the version before it fails - step 3 above - and I have an idea as to why:
With my limited understanding of the UCSI mailbox implementation, it seems as though ACPI and the EC disagree on where UCSI exchanges are supposed to be recorded:
ssdt 8
Case (One)
{
\_SB.PC00.LPCB.EC0.ADDR = 0x8033
// Write data (16 bytes)
\_SB.PC00.LPCB.EC0.ADDR = 0x801B
// Write control (8 bytes)
// ... notify ...
ADBG ("OPM write to EC")
}
Case (0x02)
{
\_SB.PC00.LPCB.EC0.ADDR = 0x8023
// Read data (16 bytes)
\_SB.PC00.LPCB.EC0.ADDR = 0x8017
// Read CCI (4 bytes)
ADBG ("OPM read to EC")
}
It uses 0x8033 for outgoing data and 0x801B for outgoing control, 0x8023 for incoming data and 0x8017 for incoming CCI.
ec
// NOTE(Dustin): These are in the "customer" memmap region, accessed via (0x8000 | offset)
#define EC_MEMMAP_UCSI_CCI 0x14
#define EC_MEMMAP_UCSI_COMMAND 0x18
#define EC_MEMMAP_UCSI_CONTROL_DATA_LEN 0x19
#define EC_MEMMAP_UCSI_CONTROL_SPECIFIC 0x1A
#define EC_MEMMAP_UCSI_MESSAGE_IN 0x20
#define EC_MEMMAP_UCSI_MESSAGE_OUT 0x30
Those aren’t the same at all!
This is predicated on the assumption that the UCSI command is supposed to be written as the first byte of the control data during OPM → EC write; if it is not, then those offsets may be correct.
It’s easier to replace the EC firmware than the SSDT at the moment, so I’m going to give it a try later.
EDIT: I of course forgot that the MEC LPC protocol requires you to submit addresss | 0x3 for each I/O operation to indicate some important transfer flags. That handily explains why each address is, in fact, ORed with 0x3. Sorry.
When you say reading UCSI version, do you mean reading from EC_MEMMAP_UCSI_VERSION? I notice you mention the address should be ORed with 0x3, but the bits set in EC_MEMMAP_UCSI_VERSION (0x12) overlap with that. There’d be no way to distinguish address 0x10 from 0x12 given that ORing. Perhaps this is related to the problem? I don’t see anything that looks like it could be referencing EC_MEMMAP_UCSI_VERSION’s address in SSDT8 though, so maybe the driver code to read the version simply doesn’t have the address? I could just be barking up the wrong tree of course.
Nah, in this case it looks like Linux is issuing _DSM(..., 2) to trigger a UCSI read, and then pulling VER1 and VER2 from the USBC opregion. Unfortunately, those values are never populated, as SSDT8 never writes to them. This is before it issues any other commands.
I think what ends up happening is this:
Linux issues _DSM(..., 2 /* read */); the opregion is either default-initialized or zeroed.
This flows right into a read from 0x8020 (or, EM1 0x20) via ACPI bytecode
This read populates opregion fields MGI0..MGIF and CCI0..CCI3 with whatever’s in EM1 0x20
Linux reads VER1..VER2, which was not touched, so it gets 00 00
The ucsi_acpi driver bails out because that is an invalid version.
Skipping the version check by forcing it to the correct version from EM1 0x12 results in some success in enumerating the ports and their initial states, but it quickly goes off the rails and the driver starts to complain as UCSI_GET_PDOS, UCSI_GET_CONNECTOR_STATUS and more fail to generate valid responses. I wonder why it starts out “okay”.