[Script] Switch GPIO output level on Intel HUB MBs from Linux

In short: below is opensource python script that is able to switch GPIO outputs on the standard Intel motherboards from Ubuntu running on it. Use at your own risk and only for careful motherboard tweaking!

This post is about driving GPIOs, but:

  • No any external devices, GPIOs of a motherboard are driven (so, extremely risky!)
  • Python language to make code concise and easy to run…
  • …but no *any* 3rd party libs or drivers, direct talking to hardware registers using Linux kernel as a mapping helper - and all it in just below 200 code lines!

I have a intel Q670-based motherboard with onboard serial controller that can be switched into different modes. The serial controller IC has configuration input pins, and the intel Q670 has GPIO output that drives the corresponding signal line. The configuration can be changed from MoBO BIOS settings, but I hardly want to change it in runtime without needing to reboot. I’ve studied some info sources implementing similar things for 2xx-series chipsets and below, and adpated it to a simple ready-to use script runnable on Linux.

Getting to GPIOs is actually 2 steps:

  • find base address of Sideband Register Access BAR (SBREG_BAR)
  • use standard layout to access GPIOs

On 2xx-series chipsets and below getting base adress was done by unhiding the “Primary to Sideband bridge (P2SB)” PCIe configuration regiters, but the method fails on 3xx+ chipsets from my experiments. Fortuneately, its base address is also provided by fiwmware via SBRG value in ACPI. While I’m not Ubuntu fan at all, it turns out that its the only distro having CONFIG_ACPI_DEBUGGER enable in its kernel, and its the simplest way to get that value in userspace:

# (timeout 1 cat& echo Evaluate SBRG >&0; wait) 0<>/sys/kernel/debug/acpi/acpidbg

- Evaluating \SBRG
Evaluation of \SBRG returned object 0000000026bb761f, external buffer length 18
 [Integer] = 00000000FD000000

Having this address - those GPIOs that are not locked can be managed by just writing words in some offsets from it. But how to find GPIOs that need to be switched to get something? Since my device was configurable from BIOS I’d gone “dump them all, switch from BIOS, dump again and compare dumps” way. “Dump them all” actually is dump all GPIO pad registers that are already in TX mode. And it worked! Now I can just control that device from Linux without needing to reboot. I’m sharing this here hoping that being able to switch GPIO output on just normal PC motherboards may enable somfun and hacking. Trying it without arguments (the read mode) shouldn’t break anything) The write mode with arguments is much more risky of course

I’ve never seen any other working intel PCH gpio management targeting 3xx-8xx chipsets. Actually, I tested it on 3xx, 5xx and 6xx but according to no changes in Intel docs it should work on 7xx and 8xx chipsets too.

Here is full source code - a single python file in a .zip:
intel_pch_gpio.zip (2.8 KB)

Same python code inline
#!/usr/bin/env python3
"""A script to read (and change) Intel 3xx-8xx PCH GPIO TX (output) lines"""

import ctypes
import os
import pathlib
import sys
import select
import mmap

ACPIDBG_BIDI_FILE = pathlib.Path("/sys/kernel/debug/acpi/acpidbg")
ACPI_OBJ_NAME = r"\SBRG"
PHYS_MEM = pathlib.Path("/dev/mem")
# values common for 300-800 series intel chipsets
P2SB_PORT_GPIO_COMMUNITY0 = 0x6E
P2SB_PORT_GPIO_COUNT = 6
P2SB_PORT_SIZE = 0x10000
MAX_PAD_COUNT = 96  # maximal pad count for all communities

class StrViaFields:
    def __str__(self):
        def pretty(value):
            if isinstance(value, int):
                return f"0x{value:X}"
            if isinstance(value, StrViaFields):
                return f"({str(value).strip()})"
            return value

        def format_field(field_name: str) -> str:
            if not field_name.strip("_01234567890"):
                return ""
            return f"  {field_name} {pretty(getattr(self, field_name))}"

        return "".join(format_field(field[0]) for field in self._fields_)


class RegisterFileStruct(ctypes.Structure, StrViaFields):
    pass


class PadCfgDw(RegisterFileStruct):
    _fields_ = [
        ("TX", ctypes.c_uint32, 1),
        ("RX", ctypes.c_uint32, 1),
        ("_0", ctypes.c_uint32, 6),
        ("TXDisable", ctypes.c_uint32, 1),
        ("RXDisable", ctypes.c_uint32, 1),
        ("Native", ctypes.c_uint32, 6),
        ("_1", ctypes.c_uint32, 1),
        ("_2", ctypes.c_uint32, 15),  # don't decode fields below
        # ('GPINMI', ctypes.c_uint32, 1),
        # ('GPISMI', ctypes.c_uint32, 1),
        # ('GPISCI', ctypes.c_uint32, 1),
        # ('GPIAPIC', ctypes.c_uint32, 1),
        # ('RXTXNative', ctypes.c_uint32, 2),
        # ('RXInv', ctypes.c_uint32, 1),
        # ('PreGlitch', ctypes.c_uint32, 1),
        # ('RXEvent', ctypes.c_uint32, 3),
        # ('RXRaw1', ctypes.c_uint32, 1),
        # ('RXStSel', ctypes.c_uint32, 1),
        # ('PadRst', ctypes.c_uint32, 2),
        ("InterruptSel", ctypes.c_uint8),
        ("_3", ctypes.c_uint8 * 3),  # don't decode fields below
        # ('StandbyTerm', ctypes.c_uint32, 2),
        # ('Termination', ctypes.c_uint32, 4),
        # ('Standby', ctypes.c_uint32, 18),
        ("_4", ctypes.c_uint8 * 8),
    ]


assert ctypes.sizeof(PadCfgDw) == 0x10, f"Unexpected {ctypes.sizeof(PadCfgDw)=}"


class GPIOCommunity(RegisterFileStruct):
    _fields_ = [
        ("_0", ctypes.c_uint8 * 0xC),  # ends 0x0C
        ("PADBAR", ctypes.c_uint32),  # ends 0x10
    ]


def p2sbaddr(gpioidx, offset):
    addr = (P2SB_PORT_GPIO_COMMUNITY0 - gpioidx) * P2SB_PORT_SIZE + offset
    assert addr > 0
    assert addr % 4 == 0
    return addr


def main(argv: list[str]):
    # GPIOS are mapped into Sideband Register Access BAR (SBREG_BAR)
    # Its base address is hidden in PCIe config space, but can be accessed via SBRG ACPI value
    sbreg_bar = int(os.getenv("SBREG_BAR", "0x0"), 16)
    if not sbreg_bar:
        try:
            acpi_dbg = ACPIDBG_BIDI_FILE.open("r+b", buffering=0)
        except Exception:
            print(f"""Problem opening {ACPIDBG_BIDI_FILE},
         esnure that CONFIG_ACPI_DEBUGGER is enabled in kernel config or acpi_dbg module loaded,
         debugfs mounted to /sys/kernel/debug, and you are running with suitable root-like access.
         note: CONFIG_ACPI_DEBUGGER is enabled in ubuntu, but not in other distros by default

         Or provide the SBREG_BAR environment variable having its value before running this.
         Some 3x0-5x0 PCHs have SBREG_BAR=0xFD000000 some 6x0 PCHs have SBREG_BAR=0xE0000000""")
            os._exit(1)
        with acpi_dbg:
            acpi_dbg.write(f"Evaluate {ACPI_OBJ_NAME}\n".encode())
            os.set_blocking(acpi_dbg.fileno(), False)
            concat_result = ""
            while select.select([acpi_dbg.fileno()], [], [], 1.0)[0]:
                concat_result += acpi_dbg.read().decode()
            for line in concat_result.splitlines():
                if not line:
                    continue
                print(line)
                lparts = line.split("=")
                if len(lparts) == 2:
                    sbreg_bar = int(lparts[1], 16)
                    break
            else:
                raise RuntimeError(
                    f"Address result not found in evaluation of {ACPI_OBJ_NAME=}\n"
                    "looks like incompatible hardware platform."
                    "Only Intel 3x0+ PCHs are known to be supported (practically tested up to 6x0 PCHs)"
                )
    print(f"SBREG_BAR=0x{sbreg_bar:X} based GPIOS, only TX pads filtered:")
    with (
        PHYS_MEM.open("r+b") as phys_mem_file,
        mmap.mmap(
            phys_mem_file.fileno(),
            P2SB_PORT_SIZE * (P2SB_PORT_GPIO_COMMUNITY0 + 1),
            offset=sbreg_bar,
        ) as regs_map,
        memoryview(regs_map).cast("I") as regs_u32,
    ):
        for gpioidx in range(0, P2SB_PORT_GPIO_COUNT):

            def load(type, offset):
                a = p2sbaddr(gpioidx, offset)
                obj = type.from_buffer(regs_u32, a)
                info = f"{type.__name__}@{a:06X}:"
                if not isinstance(obj, ctypes.Array):
                    info += f" {obj}"
                print(info)
                return obj

            gpio = load(GPIOCommunity, 0)
            if (gpio.PADBAR < 0x600) or (0 != gpio.PADBAR % 0x80):
                print(
                    f"Unexpected {gpio.PADBAR=:0x}hex, seems this platform lacks GPIOCommunity{gpioidx}"
                )
                continue
            pads = load(PadCfgDw * MAX_PAD_COUNT, gpio.PADBAR)
            for idx, p in enumerate(pads):
                # only TX pads filtering
                if p.RXDisable and not p.TXDisable and not p.Native:
                    gpiopath = f"{gpioidx}~x{idx:02X}"
                    print(gpiopath, p)
                    cmd_to_search = f"{gpiopath}="
                    if cmd_to_search in argv:
                        target_value = argv[1 + argv.index(cmd_to_search)]
                        print(f"Asked setting {gpiopath} to {target_value=}")
                        target_int = int(target_value)
                        if p.TX == target_int:
                            print("value already set, nothing to change")
                        else:
                            p.TX = target_int
                            print("setting done, new value:")
                            print(gpiopath, p)
        if not argv:
            print(
                "\n"
                "Arguments not passed, so only reading was done. If you truly understand which TX GPIO\n"
                "to write you can do it by passing 2 arguments AT YOUR OWN RISK OF PERMANENT HARDWARE DAMAGE!\n"
                "first is 'GPIO line id from first column, folowed by =', second is the desired value - '0' or '1'\n"
                f"Example {sys.argv[0]} 4~x6B= 0\n"
            )
        sys.stdout.flush()
        os._exit(0)  # need exit without destroying mappings linked from ctypes objects


if __name__ == "__main__":
    main(sys.argv[1:])

Here is example output of a tool changing GPIO ouput x1A of GPIOCommunity4 on my Q670:

- Evaluating \SBRG
Evaluation of \SBRG returned object 00000000eaafa37e, external buffer length 18
 [Integer] = 00000000E0000000
SBREG_BAR=0xE0000000 based GPIOS, only TX pads filtered:
GPIOCommunity@6E0000:   PADBAR 0x700
PadCfgDw_Array_96@6E0700:
0~x08   TX 0x0  RX 0x0  TXDisable 0x0  RXDisable 0x1  Native 0x0  InterruptSel 0x21
GPIOCommunity@6D0000:   PADBAR 0x700
PadCfgDw_Array_96@6D0700:
1~x17   TX 0x0  RX 0x0  TXDisable 0x0  RXDisable 0x1  Native 0x0  InterruptSel 0x67
1~x19   TX 0x1  RX 0x0  TXDisable 0x0  RXDisable 0x1  Native 0x0  InterruptSel 0x69
1~x1B   TX 0x0  RX 0x0  TXDisable 0x0  RXDisable 0x1  Native 0x0  InterruptSel 0x6B
1~x1C   TX 0x1  RX 0x0  TXDisable 0x0  RXDisable 0x1  Native 0x0  InterruptSel 0x6C
GPIOCommunity@6C0000:   PADBAR 0x700
PadCfgDw_Array_96@6C0700:
GPIOCommunity@6B0000:   PADBAR 0x700
PadCfgDw_Array_96@6B0700:
GPIOCommunity@6A0000:   PADBAR 0x700
PadCfgDw_Array_96@6A0700:
4~x0A   TX 0x1  RX 0x0  TXDisable 0x0  RXDisable 0x1  Native 0x0  InterruptSel 0x3A
4~x15   TX 0x1  RX 0x0  TXDisable 0x0  RXDisable 0x1  Native 0x0  InterruptSel 0x45
4~x19   TX 0x1  RX 0x0  TXDisable 0x0  RXDisable 0x1  Native 0x0  InterruptSel 0x49
4~x1A   TX 0x0  RX 0x0  TXDisable 0x0  RXDisable 0x1  Native 0x0  InterruptSel 0x4A
Asked setting 4~x1A to target_value='1'
setting done, new value:
4~x1A   TX 0x1  RX 0x0  TXDisable 0x0  RXDisable 0x1  Native 0x0  InterruptSel 0x4A
4~x1B   TX 0x1  RX 0x0  TXDisable 0x0  RXDisable 0x1  Native 0x0  InterruptSel 0x4B
4~x1C   TX 0x0  RX 0x0  TXDisable 0x0  RXDisable 0x1  Native 0x0  InterruptSel 0x4C
4~x1D   TX 0x0  RX 0x0  TXDisable 0x0  RXDisable 0x1  Native 0x0  InterruptSel 0x4D
4~x39   TX 0x1  RX 0x0  TXDisable 0x0  RXDisable 0x1  Native 0x0  InterruptSel 0x67
4~x3A   TX 0x0  RX 0x0  TXDisable 0x0  RXDisable 0x1  Native 0x0  InterruptSel 0x68
4~x3B   TX 0x0  RX 0x0  TXDisable 0x0  RXDisable 0x1  Native 0x0  InterruptSel 0x69
4~x3D   TX 0x1  RX 0x0  TXDisable 0x0  RXDisable 0x1  Native 0x0  InterruptSel 0x6B
4~x3E   TX 0x0  RX 0x0  TXDisable 0x0  RXDisable 0x1  Native 0x0  InterruptSel 0x6C
4~x3F   TX 0x1  RX 0x0  TXDisable 0x0  RXDisable 0x1  Native 0x0  InterruptSel 0x6D
4~x40   TX 0x0  RX 0x0  TXDisable 0x0  RXDisable 0x1  Native 0x0  InterruptSel 0x6E
4~x41   TX 0x0  RX 0x0  TXDisable 0x0  RXDisable 0x1  Native 0x0  InterruptSel 0x6F
4~x42   TX 0x1  RX 0x0  TXDisable 0x0  RXDisable 0x1  Native 0x0  InterruptSel 0x70
4~x43   TX 0x1  RX 0x0  TXDisable 0x0  RXDisable 0x1  Native 0x0  InterruptSel 0x71
4~x44   TX 0x0  RX 0x0  TXDisable 0x0  RXDisable 0x1  Native 0x0  InterruptSel 0x72
4~x45   TX 0x0  RX 0x0  TXDisable 0x0  RXDisable 0x1  Native 0x0  InterruptSel 0x73
GPIOCommunity@690000:   PADBAR 0x700
PadCfgDw_Array_96@690700:

Edit by Fernando: Thread title customized and shortened

1 Like

@galkinvv
Thank you very much for having started this interesting thread.
As you can see, I have shortened the title. If you don’t like it, you can change the title at any time by editing your initial post.

1 Like