TL;DR: Run the script below to disable/enable USB expansion cards. When disabled, the expansion bays are plugged in but not drawing (as much) power.
I only have the microSD one and would appreciate help testing this to see how it works for the HDMI and USB-A expansion cards. /usr/bin/turn_usb --vendor-id=090c --product-id=3350 --driver-type='usb-storage' off
As noted in the linux battery life tuning thread, having anything besides the USB-C ports plugged into the expansion bays causes excess power draw. In my case, I have the microSD card, and it costs me about 1 Watt. From my investigation, the primary reason for the power draw is that it prevents the CPU from reaching the C10 sleep state, instead being stuck in C9.
I donāt use the SD slot very often, so I wanted to find a way to keep it plugged in, but disabled by default to lower the power draw. That sent me down the rabbit hole to find a programmatic solution to keep the expansion bay physically installed, but disable the driver for it.
I ended up creating the below script where I can call turn_usb --vendor-id=foo --product-id bar off
to achieve exactly this! Itās approximately the same as unplugging the USB device, but itās still physically attached. Running powertop shows me savings of ~1 watt when I run the script, (and my device enters into C10).
If I want to use the device, I can just run turn_usb on
to undo the changes.
DISCLAIMER
This is a bash script that needs to run as root. Bad things could happen. For example, maybe thereās a bug and it unmounts your primary hard drive instead. Please note that this has the same effects as unplugging the USB device, so youāll want to unmount any drives before trying the script. No support/warranties etc. Please donāt run the script if you donāt understand how it works and are willing to troubleshoot
Help needed
I only have access to the microSD card. Could folks try this with other expansion cards and:
- report back the vendor-id, product-id, and driver-type args used
- Try turning the port off, seeing if the power draw goes down
- Keep the port off, then plug something in (hdmi cable, USB-A thingy) etc, and report back what happens
- Unplug what you plugged in (just for safety)
- Run the script to turn the port on, see if you can re-connect your accessory
If things donāt work, I would appreciate a lsusb -tv
dump and the dump of the relevant uevent files (see the script)
Installation
- Open a terminal, and elevate to root permissions
- Navigate to /usr/bin/ and run:
touch ./turn_usb
- Give the file executable permissions by anyone:
chmod 755 ./turn_usb
- Open the file in your favorite text editor and paste in the contents of the script below
- run
lsusb
to determine the vendor id and product id of what USB device you want. For example, my lsusb output has this partial snippet:
Bus 002 Device 002: ID 090c:3350 Silicon Motion, Inc. - Taiwan (formerly Feiya Technology Corp.) USB DISK
Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
And so for the microSD card, the vendor id is 090c, and the product id is 3350
- Run the script. In my case itās
turn_usb --vendor-id=090c --product-id=3350 off
- If it doesnāt work, you may need to figure out what type of device it is. Run the following command:
grep -iFl "PRODUCT=${short_vendor_id}/${short_product_id}" /sys/bus/usb/drivers/**/*/uevent
where short_vendor_id and short_product_id are the values from above, but just with any leading zeroās removed. That will print out a bunch of matching files (and some permission denied errors). Once you find the correct uevent file, that will tell you the driver type. For example, runninggrep -iFl "PRODUCT=90c/3350" /sys/bus/usb/drivers/**/*/uevent
on my machine returns the file/sys/bus/usb/drivers/usb-storage/2-2:1.0/uevent
Once you have this file, then the driver-type is the folder name after /sys/bus/usb/drivers. For example, in the above case that would be usb-storage
And thatās about it!
Run every boot
The state resets every time you turn off your machine, so I made a small service to run my command once every boot. Youāll need to modify it to suit your desires
[Unit]
Description=Disable microsd
[Service]
Type=oneshot
ExecStart=/usr/bin/turn_usb --vendor-id=090c --product-id=3350 --driver-type='usb-storage' off
Restart=no
[Install]
WantedBy=multi-user.target
The script
Version 1.0, last modified July 19, 2022
#! /usr/bin/env bash
usage() {
echo "
Bind/unbind a USB device from a given vendor:product id
Usage: $(basename "${BASH_SOURCE[0]}") [-hd] [--driver-type] --device-id=123 --product-id=abc on|off
" >&2
exit 1
}
# Variables used in the script
## Command line arguments
driver_type="usb-storage" # The sub-folder in /sys/bus/usb/drivers/ which is responsible for the usb device
dry_run= # If set to 1, print the message but don't actually bind/unbind
# vendor_id and product_id combine to identify the USB device requested to modify
# Users can use lsusb to figure out the parameters: https://wiki.debian.org/HowToIdentifyADevice/USB
vendor_id=
product_id=
usb_command= # set to either "bind" or "unbind"
## local variables
TEMP= # Used for parsing the command line arguments. Copy/pasta from the getopt template
short_vendor_id= # same as vendor_id, but with leading 0's stripped off
short_product_id= # same as vendor_id, but with leading 0's stripped off
usb_id= # The bus/port/? identifier. The main work of the script is to take the vendor/product id
# and figure out which usb_id corresponds to this device for this particular boot.
part_paths= # Split up a path (e.g. /sys/bus/foo) into an array (sys bus foo)
new_usb_id= # Temporary variable to see if this value is longer than the existing $usb_id. If so, replace usb_id with this value
# Step 1: Parse command line arguments into appropriate variables
TEMP=$(getopt -o 'dh' --longoptions 'dry-run,help,vendor-id:,product-id:,driver-type:' -n "$0" -- "$@")
if [ $? -ne 0 ]; then
usage
fi
eval set -- "$TEMP"
unset TEMP
while true; do
case "$1" in
'-h'|'--help')
usage
shift 1
continue
;;
'-d'|'--dry-run')
dry_run=1
shift 1
continue
;;
'--vendor-id')
vendor_id="$2"
shift 2
continue
;;
'--product-id')
product_id="$2"
shift 2
continue
;;
'--driver-type')
driver_type="$2"
shift 2
continue
;;
'--')
shift
break
;;
*)
usage
;;
esac
done
if [ "$#" -ne 1 ]; then
usage
fi
if [ "$1" == "on" ]; then
usb_command="bind"
elif [ "$1" == "off" ]; then
usb_command="unbind"
else
echo "missing on/off" >&2
exit 1
fi
if [ -z "$vendor_id" ]; then
echo "missing vendor-id" >&2
exit 1
fi
if [ -z "$product_id" ]; then
echo "missing product-id" >&2
exit 1
fi
# Strip leading zeros from the vendor:product id
short_vendor_id=$(printf "%X" "0x${vendor_id}")
short_product_id=$(printf "%X" "0x${product_id}")
# Step 2: Figure out the usb id for the device
# For both bound and unbound devices, the device will have at least one entry
# in /sys/bus/usb/devices/${usb_id}/uevent
# So, we begin by searching through all these files for the ones matching our USB device
# This will probably match multiple times. E.g. the device might be registered
# on /usb/2-2 and usb-storage/2-2:1-0
# We want to unbind at the lowest place in the tree, so loop through each match
# and choose the one with the longest usb_id
while IFS= read -r line; do
path_parts=($(echo "$line" | cut -d "/" --output-delimiter=" " -f 1-))
new_usb_id="${path_parts[-2]}"
if [ "${#path_parts[@]}" -lt 3 ]; then
# Usually this happens if you try to bind/unbind/rebind too quickly
continue
fi
new_usb_id="${path_parts[-2]}"
if [ "${#new_usb_id}" -gt "${#usb_id}" ]; then
usb_id=${new_usb_id}
# We can detect the driver type automatically when unbinding via the uevent file, but not when binding
# For consistency's sake, just make --driver-type a command line argument and require the user to set it
# new_driver_type=$(awk '{ match($0, /DRIVER=(.*)/, arr); if (arr[1] != "") print arr[1] }' "$line")
# =${new_driver_type:=usb-storage}
fi
done <<< $(grep -iFl "PRODUCT=${short_vendor_id}/${short_product_id}" /sys/bus/usb/devices/*/uevent)
if [ -z "$usb_id" ]; then
echo "could not calculate the usb-id" >&2
exit 1
fi
# Step 3: Check to see if the device has already been bound/unbound and just no-op if there's nothing to do
if [ -d "/sys/bus/usb/drivers/${driver_type}/${usb_id}" ]; then
if [ "$usb_command" == "bind" ]; then
echo "The device is already bound. Nothing to do"
exit 0
fi
elif [ "$usb_command" == "unbind" ]; then
echo "The device is already unbound. Nothing to do"
exit 0
fi
# Step 4; Bind/unbind the USB device by writing the usb_id to the correct bind or unbind file in sys/bus/usb
echo "Calling ${usb_command} for ${driver_type}/${usb_id}"
if [ -z "$dry_run" ]; then
echo -n "$usb_id" | sudo tee /sys/bus/usb/drivers/${driver_type}/${usb_command} > /dev/null
fi