USB-C PD on Linux via UCSI: what does each Framework board support? (call for testers)

Follow-up to my earlier thread, focused specifically on UCSI:
What PD Controllers and can Discover Identity USB power delivery commands be used?

Background
There, DHowett confirmed that the Framework Laptop exposes UCSI to the OS and that the EC tunnels
commands to the Infineon CCG PD controllers. I’ve since read the EC source and the UCSI 3.1 spec
(USB-IF, 2026). Technical details are in EC issue #78: UCSI: Discover Identity / cable e-marker passthrough, and full v2+ MESSAGE_IN in EC tunnel · Issue #78 · FrameworkComputer/EmbeddedController

Why it matters
Since UCSI 3.0, the PD controller is required to support charger reporting (all PDOs incl. PPS,
AVS and EPR), cable e-marker info, power metering on ports that power devices, and charge-power
control from the OS. With that, Linux could offer:

  • a charger inspector: all modes, plus “this 45 W charger can’t run the laptop at full power”
  • a cable tester: speed, 3 A/5 A, EPR support
  • a power meter for devices you charge from the laptop
  • eco / normal / fast charging, and choosing which charger to use

Linux already has the interfaces (/sys/class/typec, /sys/class/usb_power_delivery, UCSI).
What’s unknown is what each board’s CCG firmware actually supports today.

Call for testers
Please run this read-only script and post the output with your board model:

#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
"""
ucsi-report.py - collect what a laptop's UCSI firmware reports, for comparing Framework boards.

Read-only: sends only GET_CAPABILITY and GET_CONNECTOR_CAPABILITY through the kernel's UCSI
debugfs interface (no PD traffic, no role or power changes). Needs root, debugfs mounted, and a
kernel with UCSI debugfs (CONFIG_DEBUG_FS, recent kernels). Output is plain text for pasting.

Usage:  sudo python3 ucsi-report.py > ucsi-report.txt
"""
import glob, os, re, shutil, subprocess, sys

DBG = "/sys/kernel/debug/usb/ucsi"

OPT_FEATURES = [  # UCSI 3.1 Table 6-91, bmOptionalFeatures
    "SET_CCOM", "SET_POWER_LEVEL", "Alt mode details", "Alt mode override",
    "PDO details", "Cable details", "External supply notification",
    "PD reset notification", "GET_PD_MESSAGE", "Get Attention VDO",
    "FW update request", "Negotiated power level change", "Security request",
    "Set re-timer mode", "Chunking support",
]
ATTRIBUTES = {0: "Disabled state", 1: "Battery Charging", 2: "USB Power Delivery",
              6: "USB Type-C current"}
OPMODE = ["Rp only", "Rd only", "DRP", "Audio accessory", "Debug accessory",
          "USB2", "USB3", "Alternate mode"]
EXTMODE = {0: "USB4", 1: "EPR source", 2: "EPR sink"}


def bits(v, off, n):
    return (v >> off) & ((1 << n) - 1)


def bcd(v):
    return f"{bits(v, 8, 8):x}.{bits(v, 4, 4):x}.{bits(v, 0, 4):x}"


def read(path):
    try:
        with open(path) as f:
            return f.read().strip()
    except OSError:
        return None


def ucsi_cmd(dev, cmd):
    """Send a UCSI command via debugfs and return the 128-bit response as int (or None)."""
    try:
        with open(os.path.join(dev, "command"), "w") as f:
            f.write(hex(cmd))
    except OSError as e:
        return None, f"error: {e.strerror}"
    r = read(os.path.join(dev, "response"))
    if not r:
        return None, "no response"
    return int(r, 16), r


def section(title):
    print(f"\n=== {title} ===")


def main():
    if os.geteuid() != 0:
        sys.exit("Run as root (sudo) - debugfs and some DMI fields need it.")

    section("System")
    for f in ("sys_vendor", "product_name", "product_version", "board_name",
              "bios_vendor", "bios_version", "bios_date"):
        print(f"{f:16} {read('/sys/class/dmi/id/' + f)}")
    print(f"{'kernel':16} {os.uname().release}")

    section("Kernel log (UCSI)")
    try:
        log = subprocess.run(["dmesg"], capture_output=True, text=True).stdout
        lines = [l for l in log.splitlines() if re.search(r"ucsi", l, re.I)]
        seen = set()
        for l in lines:
            key = re.sub(r"^\[[^\]]*\]\s*", "", l)
            if key not in seen:
                seen.add(key)
                print(key)
            if len(seen) >= 15:
                print("... (truncated)")
                break
    except OSError:
        print("dmesg not available")

    devs = sorted(glob.glob(os.path.join(DBG, "*")))
    if not devs:
        section("UCSI debugfs")
        print(f"Not found at {DBG}. Is debugfs mounted and is ucsi loaded on this kernel?")
    for dev in devs:
        section(f"UCSI device {os.path.basename(dev)}")
        cap, raw = ucsi_cmd(dev, 0x06)  # GET_CAPABILITY
        print(f"GET_CAPABILITY raw: {raw}")
        if cap is None:
            continue
        attrs = bits(cap, 0, 32)
        nconn = bits(cap, 32, 7)
        opt = bits(cap, 40, 24)
        print(f"  bmAttributes     : 0x{attrs:08x} "
              f"({', '.join(n for b, n in ATTRIBUTES.items() if attrs >> b & 1) or '-'})")
        print(f"  connectors       : {nconn}")
        print(f"  alt modes        : {bits(cap, 64, 8)}")
        print(f"  PD version (bcd) : {bcd(bits(cap, 96, 16))}")
        print(f"  Type-C version   : {bcd(bits(cap, 112, 16))}")
        print(f"  BC version       : {bcd(bits(cap, 80, 16))}")
        print(f"  bmOptionalFeatures: 0x{opt:06x}")
        for i, name in enumerate(OPT_FEATURES):
            print(f"    [{'x' if opt >> i & 1 else ' '}] bit {i:2}  {name}")

        for c in range(1, nconn + 1):
            cc, raw = ucsi_cmd(dev, 0x07 | (c << 16))  # GET_CONNECTOR_CAPABILITY
            print(f"  connector {c}: GET_CONNECTOR_CAPABILITY raw: {raw}")
            if cc is None:
                continue
            om = bits(cc, 0, 8)
            ext = bits(cc, 14, 8)
            print(f"    op modes : {', '.join(n for i, n in enumerate(OPMODE) if om >> i & 1) or '-'}")
            print(f"    provider={bits(cc, 8, 1)} consumer={bits(cc, 9, 1)} "
                  f"swapDFP={bits(cc, 10, 1)} swapUFP={bits(cc, 11, 1)} "
                  f"swapSRC={bits(cc, 12, 1)} swapSNK={bits(cc, 13, 1)}")
            print(f"    extended : {', '.join(n for b, n in EXTMODE.items() if ext >> b & 1) or '-'}"
                  f"  (valid on UCSI >= 2.0)")
            print(f"    partner PD revision field: {bits(cc, 27, 2)} (UCSI >= 2.0)")

    section("framework_tool (if installed)")
    ft = shutil.which("framework_tool")
    if not ft:
        print("not installed - see github.com/FrameworkComputer/framework-system")
    else:
        for args in (["--versions"], ["--pd-info"]):
            out = subprocess.run([ft, *args], capture_output=True, text=True)
            print(f"$ framework_tool {' '.join(args)}")
            print((out.stdout or out.stderr).strip())


if __name__ == "__main__":
    main()

sudo python3 ucsi-report.py > ucsi-report.txt

It only sends GET_CAPABILITY and GET_CONNECTOR_CAPABILITY (no PD traffic, no role or power
changes) and shows:

  • which optional UCSI features the firmware claims (GET_PD_MESSAGE, cable details, PDO details…)
  • PD and Type-C versions, plus EPR / USB4 support per port
  • BIOS version and CCG PD firmware versions (via framework_tool, if installed)

Optional, to also capture the UCSI version:

echo 'module typec_ucsi +p' | sudo tee /sys/kernel/debug/dynamic_debug/control
sudo modprobe -r ucsi_acpi && sudo modprobe ucsi_acpi

I’ll collect the results in a table in this thread:

Board BIOS PD FW (base) UCSI ver PD ver GET_PD_MESSAGE SET_POWER_LEVEL Cable details PDO details EPR sink Errors in log
Laptop 13 AMD 7040 (A7) 03.20 0.0.1C (3.7.0.0D0) not captured (2.0+ fields empty → likely 1.2) 3.1 ✗ ✗ ✓ claimed, fails (-5) ✓ claimed, GET_PDOS fails (-70) ✗ unknown error 0, unknown error 256

Questions for Framework

  1. Which UCSI version and optional features does each board’s PD firmware report? Could this be
    published per board, like the expansion card slot pages?
  2. Is GET_CABLE_PROPERTY (failed (-5) on several boards) a PD firmware or EC tunnel issue?
  3. Would community EC PRs for the UCSI tunnel be accepted? (For UCSI ≥2.0 the EC currently copies
    34 of the 256 allowed MESSAGE_IN bytes.)

FW 13 7840U. Additional extension added for upload whitelist purposes.
ucsi-report.txt.scad (3.6 KB)