[TRACKING] Controlling Power Direction for USB C

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:

$ lsmod | grep typec
typec_ucsi             53248  1 ucsi_acpi
typec                  65536  1 typec_ucsi
roles                  16384  1 typec_ucsi

That’s odd. What output voltage does your ‘power bank’ provide ?
There’s not much use in using a ‘power bank’ if can’t power the laptop.

Even if the laptop was fully charged and the power bank not I wouldn’t expect that.

However I would expect the power bank to have the option to charge or feed rather than a primary option in the laptop.

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?)

1 Like

Have you looked into ectool?

@DHowett , any thoughts on controlling usb-pd negotiation?

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

Version information for ec is as follows:

RO version:    hx20_v0.0.1-24c42a7
RW version:    
Firmware copy: RO
Build info:    hx20_v0.0.1-24c42a7 2021-06-30 06:29:08 runner@fv-az177-983
Tool version:  v0.0.1-d5b5b50 2022-04-12 08:14:25

Perhaps I need to update the EC firmware? I’ve not been keeping track of the EC firmware situation.

Getting the same results with my copy of ectool. Hopefully @DHowett has an answer whenever he’s ready to chime in.

Just updated from BIOS 3.02 to 3.07 which came along with a new EC firmware version:

Build info:    hx20_v0.0.1-369d3c3 2021-12-13 21:47:58 runner@fv-az209-518

but same result currently, both in terms of the ectool error and lack of anything in /sys/class/typec for the more standard way.

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. :slight_smile:


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?

$ lsmod | grep ucsi
ucsi_acpi              16384  0
typec_ucsi             53248  1 ucsi_acpi
typec                  69632  1 typec_ucsi
roles                  16384  1 typec_ucsi
$ ls /sys/class/typec
$ ls /sys/class/typec_mux
$ sudo dmesg | grep -i ucsi

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)
1 Like

Looks like that’s missing from the ACPI tables that Linux is seeing on my system? I did a dump via

sudo cat /sys/firmware/acpi/tables/DSDT > dsdt.dat
iasl -d dsdt.dat

and no sign of PNP0CA0 in the resulting dsdt.dsl file.

Also no sign of it in /sys/devices/LNXSYSTM:00/LNXSYBUS:00/ which lists other ACPI devices.

1 Like

You’ll find it in SSDT8, and at /sys/devices/LNXSYSTM:00/LNXSYBUS:00/USBC000:00.

1 Like

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.


I was able to reproduce that same logged error:

[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.


// 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_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.

Lol if that’s the worst of your problems, you’re doing alright!

Thanks for looking into this, would definitely be nice to have proper control of this working. Let me know when you have something for me to test.

HA! It fails to read the UCSI version, but if you skip over that check it works perfectly fine a bit.

==> port2/data_role <==
[host] device

==> port2/power_operation_mode <==

==> port2/power_role <==
[source] sink

==> port2/preferred_role <==

==> port2/supported_accessory_modes <==

==> port2/uevent <==

==> port2/usb_power_delivery_revision <==

==> port2/usb_typec_revision <==

==> port2/vconn_source <==

==> port2/waiting_for_supplier <==

Nice work!

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:

  1. Linux issues _DSM(..., 2 /* read */); the opregion is either default-initialized or zeroed.
  2. This flows right into a read from 0x8020 (or, EM1 0x20) via ACPI bytecode
  3. This read populates opregion fields MGI0..MGIF and CCI0..CCI3 with whatever’s in EM1 0x20
  4. Linux reads VER1..VER2, which was not touched, so it gets 00 00
  5. 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”.