Skip to content

Commit f3a4cbb

Browse files
committed
laptop13: Support touchscreen disable/enable on Linux
Signed-off-by: Daniel Schaefer <dhs@frame.work>
1 parent c559f9a commit f3a4cbb

6 files changed

Lines changed: 227 additions & 1 deletion

File tree

Cargo.lock

Lines changed: 29 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

framework_lib/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ clap-verbosity-flag = { version = "3.0" }
6464
clap_complete = "4.5"
6565
nvml-wrapper = { version = "0.11.0", optional = true }
6666

67+
[target.'cfg(target_os = "linux")'.dependencies]
68+
# Linux GPIO character device — used for SoC GPIO control (e.g. Sakura
69+
# touchscreen enable on GPP_B18). /dev/gpiochipN, no libgpiod C dep.
70+
gpiocdev = "0.8"
71+
6772
[target.'cfg(windows)'.dependencies.windows]
6873
version = "0.62.0"
6974
features = [

framework_lib/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ pub mod camera;
2121
pub mod inputmodule;
2222
#[cfg(target_os = "linux")]
2323
pub mod nvme;
24+
#[cfg(target_os = "linux")]
25+
pub mod soc_gpio;
2426
#[cfg(feature = "hidapi")]
2527
pub mod touchpad;
2628
#[cfg(feature = "hidapi")]

framework_lib/src/smbios.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,8 @@ pub fn get_platform() -> Option<Platform> {
208208
"Laptop 13 (AMD Ryzen AI 300 Series)" => Some(Platform::Framework13AmdAi300),
209209
"Laptop 12 (13th Gen Intel Core)" => Some(Platform::Framework12IntelGen13),
210210
"Laptop 13 (Intel Core Ultra Series 1)" => Some(Platform::IntelCoreUltra1),
211-
"Framework Laptop 13 Pro (Intel Core Ultra Series 3)" => Some(Platform::IntelCoreUltra3),
211+
"Framework Laptop 13 Pro (Intel Core Ultra Series 3)"
212+
| "Laptop 13 Pro (Intel Core Ultra Series 3)" => Some(Platform::IntelCoreUltra3),
212213
"Laptop 16 (AMD Ryzen 7040 Series)" => Some(Platform::Framework16Amd7080),
213214
"Laptop 16 (AMD Ryzen AI 300 Series)" => Some(Platform::Framework16AmdAi300),
214215
"Desktop (AMD Ryzen AI Max 300 Series)" => Some(Platform::FrameworkDesktopAmdAiMax300),

framework_lib/src/soc_gpio.rs

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
//! SoC GPIO control via the Linux GPIO character device.
2+
//!
3+
//! Some Framework platforms wire control lines (e.g. touchscreen enable)
4+
//! to SoC pins that aren't exposed through the EC. We drive those by
5+
//! resolving the pad name in pinctrl-debugfs to a `/dev/gpiochipN` line
6+
//! offset and using libgpiod's v2 character device API.
7+
//!
8+
//! Discovery is intentionally by *hardware pad name* (e.g. "GPP_B_18")
9+
//! rather than chip+offset — gpiochip enumeration order shifts depending
10+
//! on which other GPIO providers (cros-ec, USB-LJCA, etc.) probed first,
11+
//! and the Intel pinctrl HID changes per SoC generation. The pad name
12+
//! is the only identifier guaranteed to stay stable.
13+
14+
use std::fs;
15+
use std::path::PathBuf;
16+
17+
use gpiocdev::line::Value;
18+
use gpiocdev::Request;
19+
20+
/// Locate the `/dev/gpiochipN` and line offset for a given pinctrl pad
21+
/// name (e.g. "GPP_B_18"). Returns `None` if the pad isn't present, the
22+
/// pinctrl debugfs isn't readable (not root / no debugfs), or the pad is
23+
/// firmware-locked.
24+
fn locate_pin(pin_name: &str) -> Option<(PathBuf, u32)> {
25+
// 1. Find the pinctrl directory whose `pins` file mentions our pad.
26+
let needle = format!(" ({})", pin_name);
27+
let entries = match fs::read_dir("/sys/kernel/debug/pinctrl") {
28+
Ok(e) => e,
29+
Err(e) => {
30+
error!(
31+
"Cannot read /sys/kernel/debug/pinctrl ({}); is debugfs mounted and is the process running as root?",
32+
e
33+
);
34+
return None;
35+
}
36+
};
37+
38+
let mut found: Option<(String, u32)> = None;
39+
for entry in entries.flatten() {
40+
let pins_path = entry.path().join("pins");
41+
let Ok(contents) = fs::read_to_string(&pins_path) else {
42+
continue;
43+
};
44+
for line in contents.lines() {
45+
if !line.contains(&needle) {
46+
continue;
47+
}
48+
// pinctrl-intel annotates locked pads with " [LOCKED ...]" — see
49+
// drivers/pinctrl/intel/pinctrl-intel.c:intel_pin_dbg_show().
50+
if line.contains("[LOCKED") {
51+
error!(
52+
"{} is firmware-locked (PADCFGLOCK); cannot toggle from Linux",
53+
pin_name
54+
);
55+
return None;
56+
}
57+
// Format: "pin <N> (<NAME>) ..."
58+
let off = line
59+
.split_whitespace()
60+
.nth(1)
61+
.and_then(|t| t.parse::<u32>().ok());
62+
if let Some(off) = off {
63+
let pctl = entry.file_name().to_string_lossy().into_owned();
64+
found = Some((pctl, off));
65+
break;
66+
}
67+
}
68+
if found.is_some() {
69+
break;
70+
}
71+
}
72+
73+
let (pctl_name, offset) = match found {
74+
Some(v) => v,
75+
None => {
76+
error!("pad {} not found in pinctrl debugfs", pin_name);
77+
return None;
78+
}
79+
};
80+
81+
// 2. Map pinctrl device name (e.g. "INTC10BC:04") -> /dev/gpiochipN.
82+
// /sys/bus/gpio/devices/gpiochipN is itself a symlink whose target
83+
// lives under the parent platform/ACPI device, e.g.
84+
// gpiochip4 -> ../../../devices/platform/INTC10BC:04/gpiochip4
85+
// so canonicalising the entry itself reveals the controller.
86+
// `firmware_node` is a more semantic alternative (it points at the
87+
// ACPI handle directly) and we fall back to it if the canonical
88+
// parent walk somehow doesn't include the controller name.
89+
let dir = match fs::read_dir("/sys/bus/gpio/devices") {
90+
Ok(d) => d,
91+
Err(e) => {
92+
error!("Cannot read /sys/bus/gpio/devices: {}", e);
93+
return None;
94+
}
95+
};
96+
for entry in dir.flatten() {
97+
let candidates = [
98+
fs::canonicalize(entry.path()).ok(),
99+
fs::read_link(entry.path().join("firmware_node"))
100+
.ok()
101+
.map(|p| entry.path().join(p)),
102+
];
103+
let owned = candidates
104+
.iter()
105+
.flatten()
106+
.any(|p| p.to_string_lossy().contains(&pctl_name));
107+
if owned {
108+
let chip_name = entry.file_name();
109+
let chip_path = PathBuf::from(format!("/dev/{}", chip_name.to_string_lossy()));
110+
return Some((chip_path, offset));
111+
}
112+
}
113+
114+
error!(
115+
"no /dev/gpiochipN matches pinctrl controller {} (pad {})",
116+
pctl_name, pin_name
117+
);
118+
None
119+
}
120+
121+
/// Drive a SoC pad as an output to the given level. Releases the line on
122+
/// return; Intel pinctrl preserves PADCFG state across release, so the
123+
/// level stays asserted in hardware.
124+
fn drive_pad(pin_name: &str, value: bool) -> Option<()> {
125+
let (chip, offset) = locate_pin(pin_name)?;
126+
debug!(
127+
"Driving {} on {} line {} -> {}",
128+
pin_name,
129+
chip.display(),
130+
offset,
131+
value as u8
132+
);
133+
134+
let level = if value { Value::Active } else { Value::Inactive };
135+
match Request::builder()
136+
.on_chip(&chip)
137+
.with_consumer("framework_tool")
138+
.with_line(offset)
139+
.as_output(level)
140+
.request()
141+
{
142+
Ok(req) => {
143+
// Drop releases the request fd; the kernel keeps PADCFG bits
144+
// set on Intel pinctrl, so the line stays driven.
145+
drop(req);
146+
Some(())
147+
}
148+
Err(e) => {
149+
error!(
150+
"failed to request {} (line {}): {}",
151+
chip.display(),
152+
offset,
153+
e
154+
);
155+
None
156+
}
157+
}
158+
}
159+
160+
/// Toggle the touchscreen enable line on Framework Laptop 13
161+
/// (Intel Core Ultra Series 3 / "Sakura"). The touch IC's enable pin is
162+
/// wired to SoC GPP_B18; driving it low disables the IC, high re-enables.
163+
pub fn sakura_touchscreen(enable: bool) -> Option<()> {
164+
drive_pad("GPP_B_18", enable)
165+
}

framework_lib/src/touchscreen.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,30 @@ pub fn print_himax_fw_ver() -> Option<()> {
329329
}
330330

331331
pub fn enable_touch(enable: bool) -> Option<()> {
332+
use crate::util::Platform;
333+
334+
// Framework Laptop 13 Pro (Intel Core Ultra Series 3) doesn't use
335+
// an HID-controllable touch IC; the panel enable signal is wired
336+
// to SoC GPP_B18, which we drive directly.
337+
if matches!(
338+
crate::smbios::get_platform(),
339+
Some(Platform::IntelCoreUltra3)
340+
) {
341+
#[cfg(target_os = "linux")]
342+
{
343+
return crate::soc_gpio::sakura_touchscreen(enable);
344+
}
345+
#[cfg(not(target_os = "linux"))]
346+
{
347+
error!(
348+
"--touchscreen-enable on Framework Laptop 13 (Intel Core Ultra Series 3) \
349+
requires Linux (drives SoC GPP_B18 via /dev/gpiochip)"
350+
);
351+
return None;
352+
}
353+
}
354+
355+
// ILI HID-feature-report path (Laptop 12 etc.).
332356
#[cfg(target_os = "windows")]
333357
let device = touchscreen_win::NativeWinTouchScreen::open_device(VENDOR_USAGE_PAGE, 0)?;
334358
#[cfg(not(target_os = "windows"))]

0 commit comments

Comments
 (0)