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