-
-
Save kylin-ink/1d5478cd8c4d36cf60c507319427f163 to your computer and use it in GitHub Desktop.
Compile and install Linux Kernel 5.9 with workaround patch for Lenovo Legion 5 15ARH05 Touchpad
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! /bin/bash | |
# | |
# Lenovo Legion 5 Touchpad Fix | |
# | |
# This script applies a workaround patch to the MSFT Touchpad to a newly downloaded Linux Kernel. It then patches, compiles and installs it. | |
# This is for those wanting a fix for the touchpad, but can't wait for it to hit mainstream branch/distros | |
# | |
# The original bug and related paatch is reported here https://bugs.launchpad.net/ubuntu/+source/linux/+bug/1887190 | |
# | |
# The patch itself is not mine and was taken directly from here https://www.spinics.net/lists/linux-input/msg69458.html | |
# | |
# For convenience you run this script with `bash <(curl https://gist.githubusercontent.com/jbuncle/7dacde983b3c33b3b816b10e2fd2308a/raw/build-patched-kernel.sh)` | |
# | |
# After copying the config from your current kernel, this script will load the interactive Linux/x86 Kernel Configuration, I just exit | |
# and saved this menu to generate a .config file. | |
# | |
# This script is based on 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 | |
# Install build deps | |
sudo apt-get install git fakeroot build-essential ncurses-dev xz-utils libssl-dev bc flex bison libelf-dev dwarves | |
PROC=`getconf _NPROCESSORS_ONLN` | |
prompt_kernel_version() { | |
while true; do | |
echo "What version of the kernel do you want to use, 5.8 or 5.9?" | |
read -p "" BUILD_MODE | |
case $BUILD_MODE in | |
"5.9" ) | |
KERNEL_VERSION='5.9' | |
break | |
;; | |
"5.8" ) | |
KERNEL_VERSION='5.8' | |
break; | |
;; | |
* ) | |
echo "Please enter either 5.9 or 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 | |
#sudo yes '' | make oldconfig | |
# Use localmodconfig to use the modules for this system, and build them in | |
#sudo yes '' | make localmodconfig | |
#sudo yes '' | make localyesconfig | |
# Allow adjustments to kernel config | |
make -j ${PROC} menuconfig | |
} | |
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} | |
echo "Using version ${VERSION}" | |
cd /tmp/ | |
test -d kernel-build-${VERSION} || mkdir kernel-build-${VERSION} | |
cd kernel-build-${VERSION} | |
# Check for existing download | |
if [ ! -d linux-${VERSION} ] ; then | |
# Fetch kernel source | |
test -f linux-${VERSION}.tar.xz || (echo "Downloading kernel" ; wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-${VERSION}.tar.xz) | |
# Extract kernel | |
echo "Extracting kernel" | |
tar xf linux-${VERSION}.tar.xz | |
cd linux-${VERSION} | |
# 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 | |
else | |
cd linux-${VERSION} | |
fi | |
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} | |
# Installer freshly built deb packages | |
sudo dpkg -i ../linux-image-${VERSION}.0${LOCALVERSION}_${VERSION}.0${LOCALVERSION}-1_amd64.deb | |
sudo dpkg -i ../linux-headers-${VERSION}.0${LOCALVERSION}_${VERSION}.0${LOCALVERSION}-1_amd64.deb | |
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 "" | |
} | |
while true; do | |
echo "" | |
echo "This script aims so patch the kernel to fix the touchpad on Lenovo Legion 5, written for Ubuntu 20" | |
echo "" | |
echo "What do you want to do?" | |
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 "" | |
echo "Build module [m] or the whole kernel [k]?" | |
read -p "" BUILD_MODE | |
case $BUILD_MODE in | |
[mM]* ) | |
build_and_install_module | |
break | |
;; | |
[kK]* ) | |
build_and_install_kernel | |
break; | |
;; | |
* ) | |
echo "Please answer m or k." | |
;; | |
esac | |
done |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment