TLP on a System76 Darter Pro: four overrides and nine pitfalls

The install is trivial; the tuning is where you lose a weekend. My full TLP config on an Arrow Lake System76 laptop, and every trap I hit getting there — from a PCIe ASPM setting that broke the card reader to a platform_profile knob that was never connected to anything.

TLP is one of those packages where the install is trivial and the tuning is where you lose a weekend. I've been running it on a System76 Darter Pro for a few months now, and the config I ended up with is much smaller than the one I started with — most of my edits were reverts after something broke.

This is the full current state of that config, plus every trap I walked into getting there.

The machine

Host:    System76 Darter Pro (darp11-b)
BIOS:    2025-04-03_04c4e20
CPU:     Intel Core Ultra 7 255H (6P + 8E + 2LPE, 16 threads, up to 6.5 GHz)
GPU:     Intel Arc 140T integrated (i915 driver)
Wi-Fi:   Intel BE200 (Gale Peak), Wi-Fi 7, iwlwifi
BT:      Intel BE200 Bluetooth, USB 8087:0036
Reader:  O2 Micro OZ711 SD/MMC controller
RAM:     64 GB
Disk:    2 TB NVMe, btrfs on @ subvol
Battery: 4650 mAh design / 4554 mAh full (18 cycles)
OS:      Arch Linux, kernel 6.18 LTS, GNOME 50.4 on Wayland
TLP:     1.10.2

Arrow Lake-H is new enough that a lot of the power-management advice you find for Linux laptops was written for hardware that behaves differently. That turned out to matter.

Rule one: don't touch /etc/tlp.conf

My /etc/tlp.conf has exactly zero uncommented lines:

$ grep -vE '^\s*#|^\s*$' /etc/tlp.conf
$

Everything lives in /etc/tlp.d/. TLP reads drop-ins in lexical order after the main file, so a small numbered file per concern wins over one 900-line config you diff against upstream every release. When tlp updates and ships a new tlp.conf, I get no .pacnew conflict, because I never edited it.

tlp-stat -c tells you exactly which file won for every setting, which makes this arrangement debuggable:

$ tlp-stat -c | grep tlp.d
/etc/tlp.d/10-typec-display.conf L0002: RUNTIME_PM_DENYLIST="00:07.0 00:0d.0 00:0d.2"
/etc/tlp.d/20-darter-pro.conf L0002: START_CHARGE_THRESH_BAT0="75"
/etc/tlp.d/20-darter-pro.conf L0003: STOP_CHARGE_THRESH_BAT0="80"
/etc/tlp.d/20-darter-pro.conf L0006: CPU_HWP_DYN_BOOST_ON_AC="1"
/etc/tlp.d/20-darter-pro.conf L0013: USB_EXCLUDE_BTUSB="1"

Four effective overrides. That's the whole customisation surface.

The config

/etc/tlp.d/10-typec-display.conf

# Type-C / Thunderbolt controllers get runtime-suspended on battery, which
# breaks external display hotplug. Keep them awake.
RUNTIME_PM_DENYLIST="00:07.0 00:0d.0 00:0d.2"

Those three IDs are:

| ID | Device |

| --- | --- |

| 00:07.0 | USB Type-C Subsystem PCIe Root Port #16 |

| 00:0d.0 | USB xHCI controller |

| 00:0d.2 | Thunderbolt DMA0 |

/etc/tlp.d/20-darter-pro.conf

# Battery health: stop at 80% day to day. Use `sudo tlp fullcharge` before
# travelling.
START_CHARGE_THRESH_BAT0=75
STOP_CHARGE_THRESH_BAT0=80

# On AC, let HWP ramp harder on bursty load (compiles).
CPU_HWP_DYN_BOOST_ON_AC=1

# PCIe ASPM on battery — DISABLED, see the pitfalls section.
# PCIE_ASPM_ON_BAT=powersave

# Bluetooth is on the USB bus. Autosuspend makes headsets stutter and wake
# slowly.
USB_EXCLUDE_BTUSB=1

/etc/udev/rules.d/99-sdhci-no-aspm.rules

Not a TLP file, but part of the same fix:

ACTION=="add", SUBSYSTEM=="pci", KERNEL=="0000:2d:00.0", ATTR{link/l1_aspm}="0", ATTR{link/l0s_aspm}="0"

Service state

$ systemctl is-enabled tlp
enabled
$ systemctl is-enabled systemd-rfkill.service systemd-rfkill.socket
masked
masked

No power-profiles-daemon installed. No tlp-rdw either — I don't need per-network radio switching.

Relevant kernel command line

acpi_backlight=native zswap.enabled=0

The pitfalls

1. PCIE_ASPM_ON_BAT=powersave broke the SD reader and possibly the Wi-Fi

This is the big one, and it's the reason the setting is commented out rather than deleted — I want the next person reading the file (me, in a year) to see that it was tried.

Setting PCIe Active State Power Management to powersave on battery made the O2 Micro card reader's link fail to come out of L1 in time. The symptom was an interrupt storm from 0000:2d:00.0 — the kind that shows up as the machine getting inexplicably warm and jittery on battery with nothing running. In the same window I was also getting what looked like BE200 firmware crashes. Whether the two were causally linked or just both fell out of aggressive link power management on a new platform, I never proved.

The fix was two-part: revert PCIE_ASPM_ON_BAT to default (which leaves the kernel's BIOS-provided policy alone), and hard-disable ASPM on the reader specifically via udev, so it stays off regardless of what any future policy change does.

$ cat /sys/module/pcie_aspm/parameters/policy
[default] performance powersave powersupersave

$ cat /sys/bus/pci/devices/0000:2d:00.0/link/l1_aspm
0
$ cat /sys/bus/pci/devices/0000:2d:00.0/link/l0s_aspm
0

The lesson generalises: global PCIe ASPM policy is a blunt instrument. One flaky device poisons the setting for the whole bus. Targeting the offender at the device level and leaving the policy at default gets you most of the saving with none of the roulette.

2. Runtime PM suspends the Thunderbolt controller and external displays stop hotplugging

RUNTIME_PM_ON_BAT="auto" is a TLP default and it is mostly fine. On this machine it is not fine for the Type-C subsystem: plug a monitor into a USB-C port while on battery and nothing happens. Unplug and replug, still nothing. Plug the charger in and suddenly the display appears.

That "works on AC, not on battery" shape is the tell for a runtime-PM problem. The RUNTIME_PM_DENYLIST above pins the three controllers on. Note this is the PCI-address denylist, not RUNTIME_PM_DRIVER_DENYLIST — the driver-level list already excludes xhci_hcd by default, and that wasn't enough, because the root port and the Thunderbolt DMA engine are separate devices.

Getting the addresses right matters and they are machine-specific. lspci and look for the Type-C root port, the xHCI controller behind it, and the Thunderbolt DMA function.

3. Bluetooth audio stutters because btusb gets autosuspended

USB_AUTOSUSPEND=1 is a TLP default. The BE200's Bluetooth side is a USB device (8087:0036), and autosuspending it produces exactly the symptoms you'd expect and would probably blame on PipeWire: audio dropouts, seconds-long delays reconnecting a headset after it idles.

USB_EXCLUDE_BTUSB=1 fixes it. Worth knowing that TLP ships USB_EXCLUDE_AUDIO=1 by default, which covers USB DACs but does nothing for Bluetooth — different subsystem, and the exclusion is by driver.

4. PLATFORM_PROFILE_ON_AC / _ON_BAT do nothing on this laptop

TLP defaults to PLATFORM_PROFILE_ON_AC="performance" and PLATFORM_PROFILE_ON_BAT="balanced", and it will happily report those in tlp-stat -c as configured settings. On this machine they are inert, because the ACPI interface they drive doesn't exist:

$ ls /sys/firmware/acpi/platform_profile
ls: cannot access '/sys/firmware/acpi/platform_profile': No such file or directory

tlp-stat -c shows configuration, not effect. Any setting that targets a sysfs path your firmware doesn't expose is silently a no-op. Before you spend an afternoon tuning a knob, check that the knob is connected to anything.

5. scaling_governor = powersave is not the problem you think it is

This one catches people who came from acpi-cpufreq. On battery:

$ cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_driver
intel_pstate
$ cat /sys/devices/system/cpu/intel_pstate/status
active
$ cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
powersave
$ cat /sys/devices/system/cpu/cpu0/cpufreq/energy_performance_preference
balance_power

intel_pstate in active mode with HWP has exactly two governors, powersave and performance, and powersave does not mean what the name suggests — it's the normal dynamic-scaling mode and it will still take the CPU to full turbo. The actual tuning knob is the energy performance preference, which TLP sets via CPU_ENERGY_PERF_POLICY_ON_AC / _ON_BAT:

$ cat /sys/devices/system/cpu/cpu0/cpufreq/energy_performance_available_preferences
default performance balance_performance balance_power power

I left TLP's defaults here (balance_performance on AC, balance_power on battery) because they're sensible. The frequency range stays fully open in both cases — 400 MHz to 6.5 GHz, max_perf_pct=100, turbo enabled. There is no reason to cap frequency on a modern HWP part; capping it makes race-to-idle worse and usually costs you battery rather than saving it.

The one thing I did change is CPU_HWP_DYN_BOOST_ON_AC=1, which lets the kernel bias HWP upward for a task that just woke from I/O wait. It's an AC-only setting on purpose:

$ cat /sys/devices/system/cpu/intel_pstate/hwp_dynamic_boost   # on battery
0

6. systemd-rfkill fights TLP over radio state

If both are enabled they take turns restoring Wi-Fi and Bluetooth state across suspend, and you get a laptop that occasionally wakes with Bluetooth off for no reason. Mask both units — the socket as well as the service, or socket activation just brings it back:

sudo systemctl mask systemd-rfkill.service systemd-rfkill.socket

7. Charge thresholds need an escape hatch, and they're firmware-dependent

75/80 is a good daily window; it's also useless the morning you need to actually leave the house with a full battery. The escape hatch:

sudo tlp fullcharge

That's a one-shot override — it charges to 100% and the thresholds come back on the next TLP invocation. There's also sudo tlp setcharge 75 100 BAT0 if you want it to stick for a while.

Two things to check on your own hardware. First, threshold support is firmware-specific; on this machine it works through the standard sysfs interface:

$ cat /sys/class/power_supply/BAT0/charge_control_end_threshold
80

Second, TLP defaults to RESTORE_THRESHOLDS_ON_BAT=1, which re-applies thresholds when you unplug. On some firmware, writing thresholds while discharging causes the EC to briefly report nonsense. It's been fine here.

8. Suspend mode: deep is selected, and that's a choice

$ cat /sys/power/mem_sleep
s2idle [deep]

Arrow Lake supports both. s2idle (modern standby) drains noticeably more on Linux than S3 does, because it depends on every device actually entering its low-power state — and per pitfall 2, that's precisely what doesn't reliably happen here. deep is the boring, predictable choice, and TLP doesn't manage this at all: it's a kernel/firmware setting, so don't go looking for a TLP option.

9. A firmware load error that looks alarming and isn't

iwlwifi 0000:2e:00.0: Direct firmware load for iwlwifi-gl-c0-fm-c0-c99.ucode failed with error -2

This appears every boot. iwlwifi probes downward through firmware API versions and -c99 is just the top of the range it asks for; the load that matters succeeds a line later. It has nothing to do with TLP. I mention it only because when you're hunting a power bug you will grep the journal for warnings, you will find this, and you will waste twenty minutes on it.

What I deliberately left alone

Everything else is TLP's default, and the defaults on 1.10 are good. Specifically I did not touch:

  • WIFI_PWR_ON_BAT=on — Wi-Fi power saving on battery works fine with the BE200 and saves real power.
  • SOUND_POWER_SAVE_ON_AC/BAT=1 — no audible pop on this codec.
  • DISK_APM_LEVEL_* / SATA_LINKPWR_* — this machine has one NVMe drive; the SATA settings are inert and the APM level doesn't apply to NVMe.
  • NMI_WATCHDOG=0 — default, and correct unless you're debugging kernel hangs.
  • TLP_PROFILE_AC/BAT — the PRF/BAL profile shorthand, left as shipped.

The general principle: TLP's defaults are tuned by people with a much larger hardware sample than mine. The right workflow is to run stock, find something that's actually wrong, and write the smallest possible drop-in that fixes it — not to start from a fully-annotated config and turn every dial to powersave.

Where it landed

Idle on battery with GNOME up and a browser open, the machine pulls around 12–13 W, which the firmware estimates at a bit over three hours from 77%. That's unremarkable for a 16-thread H-series part with a 14" 1920×1200 panel, and I'd rather have unremarkable and stable than a config that squeezes out another watt and drops my external monitor once a week.

Four overrides and one udev rule. Most of the work was finding out which four.

No comments yet