-
-
Save jbuncle/7dacde983b3c33b3b816b10e2fd2308a to your computer and use it in GitHub Desktop.
#! /bin/bash | |
# | |
# Lenovo Legion 5 Patch Utility | |
# | |
# This script aims to simplify the application of various patches required for Lenovo Legion 5 15ARH05. | |
# For convenience you can run this script with `bash <(curl https://gist.githubusercontent.com/jbuncle/7dacde983b3c33b3b816b10e2fd2308a/raw/build-patched-kernel.sh)` | |
# | |
# References: | |
# - Original bug and related patch: https://bugs.launchpad.net/ubuntu/+source/linux/+bug/1887190 | |
# - Touchpad patch file: https://www.spinics.net/lists/linux-input/msg69458.html | |
# - Tutorial for building the linux kernel: https://www.freecodecamp.org/news/building-and-installing-the-latest-linux-kernel-from-source-6d8df5345980/, then adapted based on https://wiki.ubuntu.com/KernelTeam/GitKernelBuild | |
# | |
set -e | |
KERNEL_VERSION_511='5.11-rc6' | |
PROC=`getconf _NPROCESSORS_ONLN` | |
install_build_deps() { | |
sudo apt-get install git fakeroot build-essential ncurses-dev xz-utils libssl-dev bc flex bison libelf-dev dwarves | |
} | |
prompt_kernel_version() { | |
while true; do | |
echo "" | |
echo "==============================================================" | |
echo "" | |
echo "What version of the kernel do you want to use, '5.8', '5.9' or '${KERNEL_VERSION_511}?'" | |
echo "5.8 and 5.9 are stable and will be patched with the required changes to allow the touchpad to work." | |
echo "5.11 is currently a release candidate and therefore not fully stable, however it will not need patching as it contains the necessary changes already." | |
echo "" | |
echo "Enter '5.8', '5.9' or '${KERNEL_VERSION_511}':" | |
read -p "" BUILD_MODE | |
case $BUILD_MODE in | |
"5.9" ) | |
KERNEL_VERSION='5.9' | |
break | |
;; | |
"5.8" ) | |
KERNEL_VERSION='5.8' | |
break; | |
;; | |
"${KERNEL_VERSION_511}" ) | |
KERNEL_VERSION=${KERNEL_VERSION_511} | |
break; | |
;; | |
* ) | |
echo "Please enter either 5.11-rc6, 5.9, 5.8." | |
;; | |
esac | |
done | |
} | |
# | |
# Generate Kernel configuration based on current systems existing config. | |
# | |
generate_config() { | |
# Copy in old config | |
cp /boot/config-`uname -r` .config | |
echo "" | |
echo "About to run 'make oldconfig', do you want to accept all new kernel options automatically or do you want to be prompted for each one? [y/n]" | |
echo " y - (Default) Accept all new options" | |
echo " n - Prompt for each option" | |
read -p "" ACCEPT_NEW_CONFIG | |
case $ACCEPT_NEW_CONFIG in | |
[Nn]* ) | |
make oldconfig | |
;; | |
* ) | |
yes '' | make oldconfig | |
;; | |
esac | |
# Allow adjustments to kernel config | |
make -j ${PROC} menuconfig | |
} | |
# | |
# Check if we have an existing download of the kernel in the current directory | |
# | |
has_kernel() { | |
if [ "${1}" = "${KERNEL_VERSION_511}" ] ; then | |
test -f linux-${1}.tar.gz | |
else | |
test -f linux-${1}.tar.xz | |
fi | |
} | |
# | |
# Download kernel into current directory | |
# | |
download_kernel() { | |
echo "Downloading kernel" | |
if [ "${1}" = "${KERNEL_VERSION_511}" ] ; then | |
wget https://git.kernel.org/torvalds/t/linux-${1}.tar.gz | |
else | |
wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-${1}.tar.xz | |
fi | |
} | |
# | |
# Extract kernel | |
# | |
extract_kernel() { | |
echo "Extracting kernel" | |
if [ "${1}" = "${KERNEL_VERSION_511}" ] ; then | |
tar xf linux-${1}.tar.gz | |
else | |
tar xf linux-${1}.tar.xz | |
fi | |
} | |
# | |
# Apply touchpad kernel patch | |
# | |
apply_patch() { | |
# Apply a suggested patch for Lenovo Legion 5 touchpad | |
echo "Downloading patch" | |
wget https://gist.githubusercontent.com/jbuncle/7dacde983b3c33b3b816b10e2fd2308a/raw/touchpad-kernel-workaround.patch | |
echo "Applying patch" | |
# TODO: Use downloaded script | |
patch -p1 -i touchpad-kernel-workaround.patch | |
} | |
# | |
# Fetches kernel using given version, extracts and enters the source directory. | |
# | |
get_kernel_source() { | |
echo "Using version ${1}" | |
cd /tmp/ | |
test -d kernel-build-${1} || mkdir kernel-build-${1} | |
cd kernel-build-${1} | |
# Check for existing download | |
if [ ! -d linux-${1} ] ; then | |
# Fetch kernel source | |
has_kernel ${1} || download_kernel ${1} | |
# Extract kernel | |
extract_kernel ${1} | |
cd linux-${1} | |
if [ "${1}" != "${KERNEL_VERSION_511}" ] ; then | |
apply_patch | |
else | |
echo "Not applying patches (using version '${KERNEL_VERSION_511}' which should already contain required updates )" | |
fi | |
else | |
echo "Using existing downloaded source" | |
cd linux-${1} | |
fi | |
} | |
build_and_install_kernel() { | |
LOCALVERSION=-touchpad-patch | |
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" | |
prompt_kernel_version | |
VERSION=${KERNEL_VERSION} | |
get_kernel_source ${VERSION} | |
generate_config | |
# Build kernel deb packages | |
echo "Running 'make clean'" | |
make -j ${PROC} clean | |
echo "Creating linux kernel deb packages with 'make deb-pkg'" | |
sudo make -j ${PROC} CONFIG_HID=y CONFIG_I2C_HID=y deb-pkg LOCALVERSION=${LOCALVERSION} | |
cd .. | |
# Locate the freshly built deb packages | |
LINUX_IMG_DEB=$(ls linux-image-*_amd64.deb | grep -v "\-dbg_" | head -n 1) | |
LINUX_IMG_HEADERS=$(ls linux-headers-*_amd64.deb | head -n 1) | |
# Install the new kernel with deb package manager | |
sudo dpkg -i ${LINUX_IMG_DEB} | |
sudo dpkg -i ${LINUX_IMG_HEADERS} | |
echo "" | |
echo "==========" | |
echo "Now update '/etc/default/grub', setting GRUB_CMDLINE_LINUX_DEFAULT to contain 'i2c_hid.polling_mode=1' e.g.:" | |
echo ' GRUB_CMDLINE_LINUX_DEFAULT="quiet splash i2c_hid.polling_mode=1"' | |
echo "(Don't forget to run 'sudo update-grub')" | |
echo 'Then reboot and select the new kernel' | |
echo "==========" | |
echo "" | |
# TODO: Prompt update and prompt reboot | |
} | |
build_and_install_module() { | |
KERNEL_PATH=$(modinfo --filename i2c-hid) | |
BUILD_SPACE=/tmp/build/i2c-hid | |
MODULE_DOWNLOAD=https://bugs.launchpad.net/ubuntu/+source/linux/+bug/1887190/+attachment/5422562/+files/i2c-hid_standalone.zip | |
MODULE_EXP_CHECKSUM=30cec04d640cbfe0f0f3bc8e68478ebf | |
# Check module is active | |
lsmod | grep i2c_hid || (echo "Module i2c_hid doesn't seem to be enabled"; exit) | |
# Download patched module source | |
if [ ! -d ${BUILD_SPACE} ] ; then | |
cd /tmp | |
rm -rf i2c-hid_standalone i2c-hid_standalone.zip | |
mkdir -p $(dirname ${BUILD_SPACE}) | |
echo "Downloading module source code" | |
wget https://bugs.launchpad.net/ubuntu/+source/linux/+bug/1887190/+attachment/5422562/+files/i2c-hid_standalone.zip | |
DOWNLOAD_MD5=$(md5sum i2c-hid_standalone.zip | awk '{ print $1 }') | |
if [ "${DOWNLOAD_MD5}" != "${MODULE_EXP_CHECKSUM}" ] ; then | |
echo "Checksum '${DOWNLOAD_MD5}' failed to match '${MODULE_EXP_CHECKSUM}'" | |
fi | |
unzip -d ${BUILD_SPACE} i2c-hid_standalone.zip | |
rm i2c-hid_standalone.zip | |
fi | |
cd ${BUILD_SPACE}/i2c-hid_standalone | |
echo "Building module" | |
make -j ${PROC} | |
# Backup old, but don't overwrite | |
test -f "${KERNEL_PATH}.old" || (echo "Backing up existing module to ${KERNEL_PATH}.old" ; sudo cp "${KERNEL_PATH}" "${KERNEL_PATH}.old") | |
# Replace module | |
echo "Replacing module with patched version" | |
sudo cp ${BUILD_SPACE}/i2c-hid_standalone/i2c-hid.ko ${KERNEL_PATH} | |
# Remove existing module if enabled | |
echo "Removing module" | |
sudo rmmod i2c-hid || true | |
# Insert module into the kernel Temporarilty set with polling_mode | |
echo "Re-enabling with polling_mode=1 (temporary)" | |
sudo insmod ${KERNEL_PATH} polling_mode=1 | |
echo "" | |
echo "==========" | |
# For some reason, when I did it this way, I had to use "i2c_hid" instead of "i2c-hid" | |
echo "To activate the patch permanently update '/etc/default/grub', setting GRUB_CMDLINE_LINUX_DEFAULT to contain 'i2c_hid.polling_mode=1' e.g.:" | |
echo 'GRUB_CMDLINE_LINUX_DEFAULT="quiet splash i2c_hid.polling_mode=1"' | |
echo "(Don't forget to run 'sudo update-grub')" | |
echo "==========" | |
echo "" | |
} | |
brightness_fix() { | |
LINE='Option \"RegistryDwords\" \"EnableBrightnessControl=1\"' | |
FILE='/usr/share/X11/xorg.conf.d/10-nvidia.conf' | |
AFTER='Option \"AllowEmptyInitialConfiguration\"' | |
echo "Applying brightness fix" | |
echo "Checking nvidia config" | |
(grep -q "${LINE}" ${FILE} && echo "Nvidia config already updated") || sudo sed -i "s/${AFTER}/${AFTER}\n ${LINE}/" "${FILE}" | |
echo "Checking Grub option" | |
(grep -q "video.use_native_backlight=1" /etc/default/grub && echo "Grub already updated") || (\ | |
echo "Inserting option\n"; \ | |
sudo sed -i "s/GRUB_CMDLINE_LINUX_DEFAULT=\"quiet splash/GRUB_CMDLINE_LINUX_DEFAULT=\"quiet splash video.use_native_backlight=1/" /etc/default/grub && \ | |
echo "Updating grub\n" && \ | |
sudo update-grub && \ | |
echo "You will need to reboot\n" \ | |
) | |
echo "Done" | |
} | |
help() { | |
echo " m - (Riskier but faster) Build the i2c-hid module alone and insert it into you current kernel (this could break your kernel - ideally make sure you have other kernels available)." | |
echo " This may not work if kernel module signature verification is enabled" | |
echo " k - (Safer but slower) Compile and install the whole kernel (alongside your existing kernel) with a patched i2c-hid module" | |
echo " b - Apply screen brightness fix (issue where screen brightness cannot be adjusted)" | |
echo "" | |
echo "Note, to build the latest kernel (${KERNEL_VERSION_511}) you will need to choose 'k'" | |
echo "" | |
} | |
do_prompt() { | |
echo "" | |
echo "This utility script is intended for use with Ubuntu 20 on Lenovo Legion 5" | |
echo "" | |
echo "What do you want to do?" | |
echo "Build patched module [m], build patched kernel [k], apply brightness fix [b], more info [h]:" | |
read -p "" BUILD_MODE | |
case $BUILD_MODE in | |
[mM]* ) | |
install_build_deps | |
build_and_install_module | |
;; | |
[kK]* ) | |
install_build_deps | |
build_and_install_kernel | |
;; | |
[bB]* ) | |
brightness_fix | |
;; | |
[hH]* | "-h" | * ) | |
help | |
do_prompt | |
;; | |
esac | |
} | |
do_prompt |
diff --git a/drivers/hid/i2c-hid/i2c-hid-core.c b/drivers/hid/i2c-hid/i2c-hid-core.c | |
index dbd04492825d..0bb8075424b6 100644 | |
--- a/drivers/hid/i2c-hid/i2c-hid-core.c | |
+++ b/drivers/hid/i2c-hid/i2c-hid-core.c | |
@@ -36,6 +36,8 @@ | |
#include <linux/hid.h> | |
#include <linux/mutex.h> | |
#include <linux/acpi.h> | |
+#include <linux/kthread.h> | |
+#include <linux/gpio/driver.h> | |
#include <linux/of.h> | |
#include <linux/regulator/consumer.h> | |
@@ -60,6 +62,24 @@ | |
#define I2C_HID_PWR_ON 0x00 | |
#define I2C_HID_PWR_SLEEP 0x01 | |
+/* polling mode */ | |
+#define I2C_POLLING_DISABLED 0 | |
+#define I2C_POLLING_GPIO_PIN 1 | |
+#define POLLING_INTERVAL 10 | |
+ | |
+static u8 polling_mode; | |
+module_param(polling_mode, byte, 0444); | |
+MODULE_PARM_DESC(polling_mode, "How to poll - 0 disabled; 1 based on GPIO pin's status"); | |
+ | |
+static unsigned int polling_interval_active_us = 4000; | |
+module_param(polling_interval_active_us, uint, 0644); | |
+MODULE_PARM_DESC(polling_interval_active_us, | |
+ "Poll every {polling_interval_active_us} us when the touchpad is active. Default to 4000 us"); | |
+ | |
+static unsigned int polling_interval_idle_ms = 10; | |
+module_param(polling_interval_idle_ms, uint, 0644); | |
+MODULE_PARM_DESC(polling_interval_ms, | |
+ "Poll every {polling_interval_idle_ms} ms when the touchpad is idle. Default to 10 ms"); | |
/* debug option */ | |
static bool debug; | |
module_param(debug, bool, 0444); | |
@@ -158,6 +178,8 @@ struct i2c_hid { | |
struct i2c_hid_platform_data pdata; | |
+ struct task_struct *polling_thread; | |
+ | |
bool irq_wake_enabled; | |
struct mutex reset_lock; | |
}; | |
@@ -772,7 +794,9 @@ static int i2c_hid_start(struct hid_device *hid) | |
i2c_hid_free_buffers(ihid); | |
ret = i2c_hid_alloc_buffers(ihid, bufsize); | |
- enable_irq(client->irq); | |
+ | |
+ if (polling_mode == I2C_POLLING_DISABLED) | |
+ enable_irq(client->irq); | |
if (ret) | |
return ret; | |
@@ -814,6 +838,86 @@ struct hid_ll_driver i2c_hid_ll_driver = { | |
}; | |
EXPORT_SYMBOL_GPL(i2c_hid_ll_driver); | |
+static int get_gpio_pin_state(struct irq_desc *irq_desc) | |
+{ | |
+ struct gpio_chip *gc = irq_data_get_irq_chip_data(&irq_desc->irq_data); | |
+ | |
+ return gc->get(gc, irq_desc->irq_data.hwirq); | |
+} | |
+ | |
+static bool interrupt_line_active(struct i2c_client *client) | |
+{ | |
+ unsigned long trigger_type = irq_get_trigger_type(client->irq); | |
+ struct irq_desc *irq_desc = irq_to_desc(client->irq); | |
+ | |
+ /* | |
+ * According to Windows Precsiontion Touchpad's specs | |
+ * https://docs.microsoft.com/en-us/windows-hardware/design/component-guidelines/windows-precision-touchpad-device-bus-connectivity, | |
+ * GPIO Interrupt Assertion Leve could be either ActiveLow or | |
+ * ActiveHigh. | |
+ */ | |
+ if (trigger_type & IRQF_TRIGGER_LOW) | |
+ return !get_gpio_pin_state(irq_desc); | |
+ | |
+ return get_gpio_pin_state(irq_desc); | |
+} | |
+ | |
+static int i2c_hid_polling_thread(void *i2c_hid) | |
+{ | |
+ struct i2c_hid *ihid = i2c_hid; | |
+ struct i2c_client *client = ihid->client; | |
+ unsigned int polling_interval_idle; | |
+ | |
+ while (1) { | |
+ /* | |
+ * re-calculate polling_interval_idle | |
+ * so the module parameters polling_interval_idle_ms can be | |
+ * changed dynamically through sysfs as polling_interval_active_us | |
+ */ | |
+ polling_interval_idle = polling_interval_idle_ms * 1000; | |
+ if (test_bit(I2C_HID_READ_PENDING, &ihid->flags)) | |
+ usleep_range(50000, 100000); | |
+ | |
+ if (kthread_should_stop()) | |
+ break; | |
+ | |
+ while (interrupt_line_active(client)) { | |
+ i2c_hid_get_input(ihid); | |
+ usleep_range(polling_interval_active_us, | |
+ polling_interval_active_us + 100); | |
+ } | |
+ | |
+ usleep_range(polling_interval_idle, | |
+ polling_interval_idle + 1000); | |
+ } | |
+ | |
+ do_exit(0); | |
+ return 0; | |
+} | |
+ | |
+static int i2c_hid_init_polling(struct i2c_hid *ihid) | |
+{ | |
+ struct i2c_client *client = ihid->client; | |
+ | |
+ if (!irq_get_trigger_type(client->irq)) { | |
+ dev_warn(&client->dev, | |
+ "Failed to get GPIO Interrupt Assertion Level, could not enable polling mode for %s", | |
+ client->name); | |
+ return -1; | |
+ } | |
+ | |
+ ihid->polling_thread = kthread_create(i2c_hid_polling_thread, ihid, | |
+ "I2C HID polling thread"); | |
+ | |
+ if (ihid->polling_thread) { | |
+ pr_info("I2C HID polling thread"); | |
+ wake_up_process(ihid->polling_thread); | |
+ return 0; | |
+ } | |
+ | |
+ return -1; | |
+} | |
+ | |
static int i2c_hid_init_irq(struct i2c_client *client) | |
{ | |
struct i2c_hid *ihid = i2c_get_clientdata(client); | |
@@ -997,6 +1101,15 @@ static void i2c_hid_fwnode_probe(struct i2c_client *client, | |
pdata->post_power_delay_ms = val; | |
} | |
+static void free_irq_or_stop_polling(struct i2c_client *client, | |
+ struct i2c_hid *ihid) | |
+{ | |
+ if (polling_mode != I2C_POLLING_DISABLED) | |
+ kthread_stop(ihid->polling_thread); | |
+ else | |
+ free_irq(client->irq, ihid); | |
+} | |
+ | |
static int i2c_hid_probe(struct i2c_client *client, | |
const struct i2c_device_id *dev_id) | |
{ | |
@@ -1090,7 +1203,11 @@ static int i2c_hid_probe(struct i2c_client *client, | |
if (ret < 0) | |
goto err_regulator; | |
- ret = i2c_hid_init_irq(client); | |
+ if (polling_mode != I2C_POLLING_DISABLED) | |
+ ret = i2c_hid_init_polling(ihid); | |
+ else | |
+ ret = i2c_hid_init_irq(client); | |
+ | |
if (ret < 0) | |
goto err_regulator; | |
@@ -1129,7 +1246,7 @@ static int i2c_hid_probe(struct i2c_client *client, | |
hid_destroy_device(hid); | |
err_irq: | |
- free_irq(client->irq, ihid); | |
+ free_irq_or_stop_polling(client, ihid); | |
err_regulator: | |
regulator_bulk_disable(ARRAY_SIZE(ihid->pdata.supplies), | |
@@ -1146,7 +1263,7 @@ static int i2c_hid_remove(struct i2c_client *client) | |
hid = ihid->hid; | |
hid_destroy_device(hid); | |
- free_irq(client->irq, ihid); | |
+ free_irq_or_stop_polling(client, ihid); | |
if (ihid->bufsize) | |
i2c_hid_free_buffers(ihid); | |
@@ -1162,7 +1279,7 @@ static void i2c_hid_shutdown(struct i2c_client *client) | |
struct i2c_hid *ihid = i2c_get_clientdata(client); | |
i2c_hid_set_power(client, I2C_HID_PWR_SLEEP); | |
- free_irq(client->irq, ihid); | |
+ free_irq_or_stop_polling(client, ihid); | |
} | |
#ifdef CONFIG_PM_SLEEP | |
@@ -1183,15 +1300,16 @@ static int i2c_hid_suspend(struct device *dev) | |
/* Save some power */ | |
i2c_hid_set_power(client, I2C_HID_PWR_SLEEP); | |
- disable_irq(client->irq); | |
- | |
- if (device_may_wakeup(&client->dev)) { | |
- wake_status = enable_irq_wake(client->irq); | |
- if (!wake_status) | |
- ihid->irq_wake_enabled = true; | |
- else | |
- hid_warn(hid, "Failed to enable irq wake: %d\n", | |
- wake_status); | |
+ if (polling_mode == I2C_POLLING_DISABLED) { | |
+ disable_irq(client->irq); | |
+ if (device_may_wakeup(&client->dev)) { | |
+ wake_status = enable_irq_wake(client->irq); | |
+ if (!wake_status) | |
+ ihid->irq_wake_enabled = true; | |
+ else | |
+ hid_warn(hid, "Failed to enable irq wake: %d\n", | |
+ wake_status); | |
+ } | |
} else { | |
regulator_bulk_disable(ARRAY_SIZE(ihid->pdata.supplies), | |
ihid->pdata.supplies); | |
@@ -1208,7 +1326,7 @@ static int i2c_hid_resume(struct device *dev) | |
struct hid_device *hid = ihid->hid; | |
int wake_status; | |
- if (!device_may_wakeup(&client->dev)) { | |
+ if (!device_may_wakeup(&client->dev) || polling_mode != I2C_POLLING_DISABLED) { | |
ret = regulator_bulk_enable(ARRAY_SIZE(ihid->pdata.supplies), | |
ihid->pdata.supplies); | |
if (ret) | |
@@ -1225,7 +1343,8 @@ static int i2c_hid_resume(struct device *dev) | |
wake_status); | |
} | |
- enable_irq(client->irq); | |
+ if (polling_mode == I2C_POLLING_DISABLED) | |
+ enable_irq(client->irq); | |
/* Instead of resetting device, simply powers the device on. This | |
* solves "incomplete reports" on Raydium devices 2386:3118 and |
I have recently purchased Lenovo Legion 5i Pro, I am trying to run Ubuntu 18.04, but the touchpad doesnt work at all. I tried all the fixes online, can you please try to explain me how to get it working or how to use your fix to get is working?
@yashpatel1392 Nope. Ubuntu 18.04 is not supported. You need at least linux kernel 5.11. Better try some rolling distro like Manjaro Linux. You can try this but I highly recommend you to use linux kernel 5.12 or above for legion laptops.
su # Make yourself root user. cd /sys/class/gpio/ echo 386 > export cd gpio386 echo out > direction # Your touchpad should start working
Thanks for the response. I have updated my kernel to be 5.12. So, should your solution work?
I have recently purchased Lenovo Legion 5i Pro, I am trying to run Ubuntu 18.04, but the touchpad doesnt work at all. I tried all the fixes online, can you please try to explain me how to get it working or how to use your fix to get is working?
@yashpatel1392 Nope. Ubuntu 18.04 is not supported. You need at least linux kernel 5.11. Better try some rolling distro like Manjaro Linux. You can try this but I highly recommend you to use linux kernel 5.12 or above for legion laptops.
su # Make yourself root user. cd /sys/class/gpio/ echo 386 > export cd gpio386 echo out > direction # Your touchpad should start working
Thanks for the response. I have updated my kernel to be 5.12. So, should your solution work?
@yashpatel1392 With linux kernel 5.12 you DO NOT NEED ANY workaround at all. Maybe you just disabled your touchpad with F10?
@yashpatel1392 Nope. Ubuntu 18.04 is not supported. You need at least linux kernel 5.11. Better try some rolling distro like Manjaro Linux. You can try this but I highly recommend you to use linux kernel 5.12 or above for legion laptops.