From nobody Fri Oct 3 07:42:26 2025 Received: from smtp.kernel.org (aws-us-west-2-korg-mail-1.web.codeaurora.org [10.30.226.201]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 990453596D; Thu, 4 Sep 2025 08:33:42 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=10.30.226.201 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1756974822; cv=none; b=Ze5k6sv2lc4j+MFeR3ID6Rtk45PDSqxQV9zKXNFkzFlP/rMrE/kRyryNM3ag/MJJMk9XbgO0Y0dEwXSCCvRQnlW3D4Hx1Sae/kbmZJJrG/SIhEoHCn6o56YJO8SWyUjzArezaok4aAvp+L9fetkfY1mptVuNpn/iEWHg1lCvKmo= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1756974822; c=relaxed/simple; bh=FE/gdwNAY3JF9hk3KjfinxbltctdVQmEHNet3Q6roMU=; h=From:Date:Subject:MIME-Version:Content-Type:Message-Id:References: In-Reply-To:To:Cc; b=t/yTrI57sVc3beMkvXv+xOlgKTIARbxmFnT6YeGHEd2Uurtl1Om5n6wF27OAF+s4OlP6+n9osC93VnI01DJKXtxw56K1RF/NAup7IdgQvx03aoRMsX/L5TXodR7mCGQSqsFFtMo2qqYIE6gtNi3KzW2ZU+A+XvNHqiS3QgiD614= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b=tqJ/oEfe; arc=none smtp.client-ip=10.30.226.201 Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b="tqJ/oEfe" Received: by smtp.kernel.org (Postfix) with ESMTPS id 4C70EC4CEF4; Thu, 4 Sep 2025 08:33:42 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=kernel.org; s=k20201202; t=1756974822; bh=FE/gdwNAY3JF9hk3KjfinxbltctdVQmEHNet3Q6roMU=; h=From:Date:Subject:References:In-Reply-To:To:Cc:Reply-To:From; b=tqJ/oEfemnRgk0h/XuYDL4mLys8Z1CS59HlI6W9ur+H0j3oV7313gazPPQQ4KeQOF oZjklgcgjVKXnljw8aukQVqsLfUtKS3XFaxN1HKwlEwcEVWMZtTPxoURciR6KA/snX fsG8J62rr4kDH0NqYd/O0GOllMWuVhXJwt7mxW2DhLKb8NfDJEBKuhwp3CNEr1Iuyl xZxnkMfGp2KuiV6w1VAMc+/bkLOQ5WvG6LVGSNyrZpQOYzQCGYlApOzwk1ELTZEL/C pNSM02yPO2iPYSwklSqXdNvLGDpV6inCCFMXfbi154Gz61mGcf9wE358UxaHUZrW8B azgEYmxd+W2Bw== Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 3C0D8CA1012; Thu, 4 Sep 2025 08:33:42 +0000 (UTC) From: Cryolitia PukNgae via B4 Relay Date: Thu, 04 Sep 2025 16:33:41 +0800 Subject: [PATCH v8 1/2] hwmon: add GPD devices sensor driver Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Message-Id: <20250904-gpd_fan-v8-1-0752584f16da@uniontech.com> References: <20250904-gpd_fan-v8-0-0752584f16da@uniontech.com> In-Reply-To: <20250904-gpd_fan-v8-0-0752584f16da@uniontech.com> To: Jean Delvare , Guenter Roeck , Jonathan Corbet , Cryolitia PukNgae Cc: linux-kernel@vger.kernel.org, linux-hwmon@vger.kernel.org, linux-doc@vger.kernel.org, Celeste Liu , Yao Zi , Derek John Clark , Jun Zhan , Cheng Nie , =?utf-8?q?Marcin_Str=C4=85gowski?= , someone5678 , Justin Weiss , Antheas Kapenekakis , command_block , derjohn , Crashdummyy X-Mailer: b4 0.14.2 X-Developer-Signature: v=1; a=ed25519-sha256; t=1756974821; l=21278; i=cryolitia@uniontech.com; s=20250730; h=from:subject:message-id; bh=ywPFSzDXsrcBPoEME+uVnQJDnJFV85qAmYTbzYeV74Q=; b=3puly8OiOQSXr2nBwJJKCYfBWaIDLVaBgmzUIPz97+34/rIU1J26pqcz9xNNdyASvIyShNYlt qsIBw5OwwmrCe1cEl2s9VUGG2mOzmUdP+Yqgsi2SjT/uZHXCOZcENPv X-Developer-Key: i=cryolitia@uniontech.com; a=ed25519; pk=tZ+U+kQkT45GRGewbMSB4VPmvpD+KkHC/Wv3rMOn/PU= X-Endpoint-Received: by B4 Relay for cryolitia@uniontech.com/20250730 with auth_id=474 X-Original-From: Cryolitia PukNgae Reply-To: cryolitia@uniontech.com From: Cryolitia PukNgae Sensors driver for GPD Handhelds that expose fan reading and control via hwmon sysfs. Shenzhen GPD Technology Co., Ltd. manufactures a series of handheld devices. This driver implements these functions through x86 port-mapped IO. Tested-by: Marcin Str=C4=85gowski Tested-by: someone5678 Tested-by: Justin Weiss Tested-by: Antheas Kapenekakis Tested-by: command_block Tested-by: derjohn Tested-by: Crashdummyy Signed-off-by: Cryolitia PukNgae --- MAINTAINERS | 6 + drivers/hwmon/Kconfig | 10 + drivers/hwmon/Makefile | 1 + drivers/hwmon/gpd-fan.c | 786 ++++++++++++++++++++++++++++++++++++++++++++= ++++ 4 files changed, 803 insertions(+) diff --git a/MAINTAINERS b/MAINTAINERS index 6dcfbd11efef87927041f5cf58d70633dbb4b18d..14a616be5ff08aaeee52436dff5= 4a86c4a81e5fb 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -10421,6 +10421,12 @@ F: drivers/phy/samsung/phy-gs101-ufs.c F: include/dt-bindings/clock/google,gs101.h K: [gG]oogle.?[tT]ensor =20 +GPD FAN DRIVER +M: Cryolitia PukNgae +L: linux-hwmon@vger.kernel.org +S: Maintained +F: drivers/hwmon/gpd-fan.c + GPD POCKET FAN DRIVER M: Hans de Goede L: platform-driver-x86@vger.kernel.org diff --git a/drivers/hwmon/Kconfig b/drivers/hwmon/Kconfig index 9d28fcf7cd2a6f9e2f54694a717bd85ff4047b46..a552a5ced64d0fee2c80a5399ce= 9d1f0dbd7d763 100644 --- a/drivers/hwmon/Kconfig +++ b/drivers/hwmon/Kconfig @@ -769,6 +769,16 @@ config SENSORS_GL520SM This driver can also be built as a module. If so, the module will be called gl520sm. =20 +config SENSORS_GPD + tristate "GPD handhelds" + depends on X86 + help + If you say yes here you get support for fan readings and + control over GPD handheld devices. + + Can also be built as a module. In that case it will be + called gpd-fan. + config SENSORS_G760A tristate "GMT G760A" depends on I2C diff --git a/drivers/hwmon/Makefile b/drivers/hwmon/Makefile index cd8bc4752b4dbf015c6eb46157626f4e8f87dfae..051981eb8a5089608e9eb351a1d= 5857805c728c8 100644 --- a/drivers/hwmon/Makefile +++ b/drivers/hwmon/Makefile @@ -88,6 +88,7 @@ obj-$(CONFIG_SENSORS_GIGABYTE_WATERFORCE) +=3D gigabyte_w= aterforce.o obj-$(CONFIG_SENSORS_GL518SM) +=3D gl518sm.o obj-$(CONFIG_SENSORS_GL520SM) +=3D gl520sm.o obj-$(CONFIG_SENSORS_GSC) +=3D gsc-hwmon.o +obj-$(CONFIG_SENSORS_GPD) +=3D gpd-fan.o obj-$(CONFIG_SENSORS_GPIO_FAN) +=3D gpio-fan.o obj-$(CONFIG_SENSORS_GXP_FAN_CTRL) +=3D gxp-fan-ctrl.o obj-$(CONFIG_SENSORS_HIH6130) +=3D hih6130.o diff --git a/drivers/hwmon/gpd-fan.c b/drivers/hwmon/gpd-fan.c new file mode 100644 index 0000000000000000000000000000000000000000..4a9ae049a78524caa0fd608c119= eb34c333429ae --- /dev/null +++ b/drivers/hwmon/gpd-fan.c @@ -0,0 +1,786 @@ +// SPDX-License-Identifier: GPL-2.0+ + +/* Platform driver for GPD devices that expose fan control via hwmon sysfs. + * + * Fan control is provided via pwm interface in the range [0-255]. + * Each model has a different range in the EC, the written value is scaled= to + * accommodate for that. + * + * Based on this repo: + * https://github.com/Cryolitia/gpd-fan-driver + * + * Copyright (c) 2024 Cryolitia PukNgae + */ + +#include +#include +#include +#include +#include +#include +#include + +#define DRIVER_NAME "gpdfan" +#define GPD_PWM_CTR_OFFSET 0x1841 + +static char *gpd_fan_board =3D ""; +module_param(gpd_fan_board, charp, 0444); + +// EC read/write locker, protecting single EC access +// Should never access EC at the same time, otherwise system down. +static DEFINE_MUTEX(gpd_fan_atomic_lock); + +// EC read/write locker, protecting a sequence of EC operations +static DEFINE_MUTEX(gpd_fan_sequence_lock); + +enum gpd_board { + win_mini, + win4_6800u, + win_max_2, + duo, +}; + +enum FAN_PWM_ENABLE { + DISABLE =3D 0, + MANUAL =3D 1, + AUTOMATIC =3D 2, +}; + +static struct { + enum FAN_PWM_ENABLE pwm_enable; + u8 pwm_value; + + const struct gpd_fan_drvdata *drvdata; +} gpd_driver_priv; + +struct gpd_fan_drvdata { + const char *board_name; // Board name for module param comparison + const enum gpd_board board; + + const u8 addr_port; + const u8 data_port; + const u16 manual_control_enable; + const u16 rpm_read; + const u16 pwm_write; + const u16 pwm_max; +}; + +static struct gpd_fan_drvdata gpd_win_mini_drvdata =3D { + .board_name =3D "win_mini", + .board =3D win_mini, + + .addr_port =3D 0x4E, + .data_port =3D 0x4F, + .manual_control_enable =3D 0x047A, + .rpm_read =3D 0x0478, + .pwm_write =3D 0x047A, + .pwm_max =3D 244, +}; + +static struct gpd_fan_drvdata gpd_duo_drvdata =3D { + .board_name =3D "duo", + .board =3D duo, + + .addr_port =3D 0x4E, + .data_port =3D 0x4F, + .manual_control_enable =3D 0x047A, + .rpm_read =3D 0x0478, + .pwm_write =3D 0x047A, + .pwm_max =3D 244, +}; + +static struct gpd_fan_drvdata gpd_win4_drvdata =3D { + .board_name =3D "win4", + .board =3D win4_6800u, + + .addr_port =3D 0x2E, + .data_port =3D 0x2F, + .manual_control_enable =3D 0xC311, + .rpm_read =3D 0xC880, + .pwm_write =3D 0xC311, + .pwm_max =3D 127, +}; + +static struct gpd_fan_drvdata gpd_wm2_drvdata =3D { + .board_name =3D "wm2", + .board =3D win_max_2, + + .addr_port =3D 0x4E, + .data_port =3D 0x4F, + .manual_control_enable =3D 0x0275, + .rpm_read =3D 0x0218, + .pwm_write =3D 0x1809, + .pwm_max =3D 184, +}; + +static const struct dmi_system_id dmi_table[] =3D { + { + // GPD Win Mini + // GPD Win Mini with AMD Ryzen 8840U + .matches =3D { + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), + DMI_MATCH(DMI_PRODUCT_NAME, "G1617-01") + }, + .driver_data =3D &gpd_win_mini_drvdata, + }, + { + // GPD Win Mini + // GPD Win Mini with AMD Ryzen HX370 + .matches =3D { + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), + DMI_MATCH(DMI_PRODUCT_NAME, "G1617-02") + }, + .driver_data =3D &gpd_win_mini_drvdata, + }, + { + // GPD Win Mini + // GPD Win Mini with AMD Ryzen HX370 + .matches =3D { + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), + DMI_MATCH(DMI_PRODUCT_NAME, "G1617-02-L") + }, + .driver_data =3D &gpd_win_mini_drvdata, + }, + { + // GPD Win 4 with AMD Ryzen 6800U + .matches =3D { + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), + DMI_MATCH(DMI_PRODUCT_NAME, "G1618-04"), + DMI_MATCH(DMI_BOARD_VERSION, "Default string"), + }, + .driver_data =3D &gpd_win4_drvdata, + }, + { + // GPD Win 4 with Ryzen 7840U + .matches =3D { + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), + DMI_MATCH(DMI_PRODUCT_NAME, "G1618-04"), + DMI_MATCH(DMI_BOARD_VERSION, "Ver. 1.0"), + }, + // Since 7840U, win4 uses the same drvdata as wm2 + .driver_data =3D &gpd_wm2_drvdata, + }, + { + // GPD Win 4 with Ryzen 7840U (another) + .matches =3D { + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), + DMI_MATCH(DMI_PRODUCT_NAME, "G1618-04"), + DMI_MATCH(DMI_BOARD_VERSION, "Ver.1.0"), + }, + .driver_data =3D &gpd_wm2_drvdata, + }, + { + // GPD Win Max 2 with Ryzen 6800U + // GPD Win Max 2 2023 with Ryzen 7840U + // GPD Win Max 2 2024 with Ryzen 8840U + .matches =3D { + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), + DMI_MATCH(DMI_PRODUCT_NAME, "G1619-04"), + }, + .driver_data =3D &gpd_wm2_drvdata, + }, + { + // GPD Win Max 2 with AMD Ryzen HX370 + .matches =3D { + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), + DMI_MATCH(DMI_PRODUCT_NAME, "G1619-05"), + }, + .driver_data =3D &gpd_wm2_drvdata, + }, + { + // GPD Duo + .matches =3D { + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), + DMI_MATCH(DMI_PRODUCT_NAME, "G1622-01"), + }, + .driver_data =3D &gpd_duo_drvdata, + }, + { + // GPD Duo (another) + .matches =3D { + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), + DMI_MATCH(DMI_PRODUCT_NAME, "G1622-01-L"), + }, + .driver_data =3D &gpd_duo_drvdata, + }, + { + // GPD Pocket 4 + .matches =3D { + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), + DMI_MATCH(DMI_PRODUCT_NAME, "G1628-04"), + }, + .driver_data =3D &gpd_win_mini_drvdata, + }, + { + // GPD Pocket 4 (another) + .matches =3D { + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), + DMI_MATCH(DMI_PRODUCT_NAME, "G1628-04-L"), + }, + .driver_data =3D &gpd_win_mini_drvdata, + }, + {} +}; + +static const struct gpd_fan_drvdata *gpd_module_drvdata[] =3D { + &gpd_win_mini_drvdata, &gpd_win4_drvdata, &gpd_wm2_drvdata, NULL +}; + +// Helper functions to handle EC read/write +static int gpd_ecram_read(u16 offset, u8 *val) +{ + u16 addr_port =3D gpd_driver_priv.drvdata->addr_port; + u16 data_port =3D gpd_driver_priv.drvdata->data_port; + int ret; + + ret =3D mutex_lock_interruptible(&gpd_fan_atomic_lock); + if (ret) + return ret; + + outb(0x2E, addr_port); + outb(0x11, data_port); + outb(0x2F, addr_port); + outb((u8)((offset >> 8) & 0xFF), data_port); + + outb(0x2E, addr_port); + outb(0x10, data_port); + outb(0x2F, addr_port); + outb((u8)(offset & 0xFF), data_port); + + outb(0x2E, addr_port); + outb(0x12, data_port); + outb(0x2F, addr_port); + *val =3D inb(data_port); + + mutex_unlock(&gpd_fan_atomic_lock); + return 0; +} + +static int gpd_ecram_write(u16 offset, u8 value) +{ + u16 addr_port =3D gpd_driver_priv.drvdata->addr_port; + u16 data_port =3D gpd_driver_priv.drvdata->data_port; + int ret; + + ret =3D mutex_lock_interruptible(&gpd_fan_atomic_lock); + if (ret) + return ret; + + outb(0x2E, addr_port); + outb(0x11, data_port); + outb(0x2F, addr_port); + outb((u8)((offset >> 8) & 0xFF), data_port); + + outb(0x2E, addr_port); + outb(0x10, data_port); + outb(0x2F, addr_port); + outb((u8)(offset & 0xFF), data_port); + + outb(0x2E, addr_port); + outb(0x12, data_port); + outb(0x2F, addr_port); + outb(value, data_port); + + mutex_unlock(&gpd_fan_atomic_lock); + return 0; +} + +static int gpd_generic_read_rpm(void) +{ + const struct gpd_fan_drvdata *const drvdata =3D gpd_driver_priv.drvdata; + u8 high, low; + int ret; + + ret =3D gpd_ecram_read(drvdata->rpm_read, &high); + if (ret) + return ret; + + ret =3D gpd_ecram_read(drvdata->rpm_read + 1, &low); + if (ret) + return ret; + + return (u16)high << 8 | low; +} + +static int gpd_win4_init_ec(void) +{ + u8 chip_id, chip_ver; + u8 ret; + + ret =3D gpd_ecram_read(0x2000, &chip_id); + if (ret) + return ret; + + if (chip_id =3D=3D 0x55) { + ret =3D gpd_ecram_read(0x1060, &chip_ver); + if (ret) + return ret; + + ret =3D gpd_ecram_write(0x1060, chip_ver | 0x80); + } + + return ret; +} + +static int gpd_win4_read_rpm(void) +{ + int ret; + + ret =3D gpd_generic_read_rpm(); + + if (ret =3D=3D 0) + // Re-init EC when speed is 0 + ret =3D gpd_win4_init_ec(); + + return ret; +} + +static int gpd_wm2_read_rpm(void) +{ + u8 ret; + + for (u16 pwm_ctr_offset =3D GPD_PWM_CTR_OFFSET; + pwm_ctr_offset <=3D GPD_PWM_CTR_OFFSET + 2; pwm_ctr_offset++) { + u8 PWMCTR; + + ret =3D gpd_ecram_read(pwm_ctr_offset, &PWMCTR); + if (ret) + return ret; + + if (PWMCTR !=3D 0xB8) { + ret =3D gpd_ecram_write(pwm_ctr_offset, 0xB8); + if (ret) + return ret; + } + } + + return gpd_generic_read_rpm(); +} + +// Read value for fan1_input +static int gpd_read_rpm(void) +{ + switch (gpd_driver_priv.drvdata->board) { + case win_mini: + case duo: + return gpd_generic_read_rpm(); + case win4_6800u: + return gpd_win4_read_rpm(); + case win_max_2: + return gpd_wm2_read_rpm(); + } + + return 0; +} + +static int gpd_wm2_read_pwm(void) +{ + const struct gpd_fan_drvdata *const drvdata =3D gpd_driver_priv.drvdata; + int ret; + u8 var; + + ret =3D gpd_ecram_read(drvdata->pwm_write, &var); + if (ret) + return ret; + + // Match gpd_generic_write_pwm(u8) below + return DIV_ROUND_CLOSEST((var - 1) * 255, (drvdata->pwm_max - 1)); +} + +// Read value for pwm1 +static int gpd_read_pwm(void) +{ + switch (gpd_driver_priv.drvdata->board) { + case win_mini: + case duo: + case win4_6800u: + return gpd_driver_priv.pwm_value; + case win_max_2: + return gpd_wm2_read_pwm(); + } + return 0; +} + +// PWM value's range in EC is 1 - pwm_max, cast 0 - 255 to it. +static inline u8 gpd_cast_pwm_range(u8 val) +{ + const struct gpd_fan_drvdata *const drvdata =3D gpd_driver_priv.drvdata; + + return DIV_ROUND_CLOSEST(val * (drvdata->pwm_max - 1), 255) + 1; +} + +static int gpd_generic_write_pwm(u8 val) +{ + const struct gpd_fan_drvdata *const drvdata =3D gpd_driver_priv.drvdata; + u8 pwm_reg; + + pwm_reg =3D gpd_cast_pwm_range(val); + return gpd_ecram_write(drvdata->pwm_write, pwm_reg); +} + +static int gpd_win_mini_write_pwm(u8 val) +{ + if (gpd_driver_priv.pwm_enable !=3D MANUAL) + return -EPERM; + + return gpd_generic_write_pwm(val); +} + +static int gpd_duo_write_pwm_twice(u8 val) +{ + const struct gpd_fan_drvdata *const drvdata =3D gpd_driver_priv.drvdata; + u8 pwm_reg; + int ret; + + pwm_reg =3D gpd_cast_pwm_range(val); + ret =3D gpd_ecram_write(drvdata->pwm_write, pwm_reg); + if (ret) + return ret; + + return gpd_ecram_write(drvdata->pwm_write + 1, pwm_reg); +} + +static int gpd_duo_write_pwm(u8 val) +{ + if (gpd_driver_priv.pwm_enable !=3D MANUAL) + return -EPERM; + + return gpd_duo_write_pwm_twice(val); +} + +static int gpd_wm2_write_pwm(u8 val) +{ + if (gpd_driver_priv.pwm_enable !=3D MANUAL) + return -EPERM; + + return gpd_generic_write_pwm(val); +} + +// Write value for pwm1 +static int gpd_write_pwm(u8 val) +{ + switch (gpd_driver_priv.drvdata->board) { + case win_mini: + return gpd_win_mini_write_pwm(val); + case duo: + return gpd_duo_write_pwm(val); + case win4_6800u: + return gpd_generic_write_pwm(val); + case win_max_2: + return gpd_wm2_write_pwm(val); + } + + return 0; +} + +static int gpd_win_mini_set_pwm_enable(enum FAN_PWM_ENABLE pwm_enable) +{ + const struct gpd_fan_drvdata *drvdata; + + switch (pwm_enable) { + case DISABLE: + return gpd_generic_write_pwm(255); + case MANUAL: + return gpd_generic_write_pwm(gpd_driver_priv.pwm_value); + case AUTOMATIC: + drvdata =3D gpd_driver_priv.drvdata; + return gpd_ecram_write(drvdata->pwm_write, 0); + } + + return 0; +} + +static int gpd_duo_set_pwm_enable(enum FAN_PWM_ENABLE pwm_enable) +{ + const struct gpd_fan_drvdata *drvdata; + + switch (pwm_enable) { + case DISABLE: + return gpd_duo_write_pwm_twice(255); + case MANUAL: + return gpd_duo_write_pwm_twice(gpd_driver_priv.pwm_value); + case AUTOMATIC: + drvdata =3D gpd_driver_priv.drvdata; + return gpd_ecram_write(drvdata->pwm_write, 0); + } + + return 0; +} + +static int gpd_wm2_set_pwm_enable(enum FAN_PWM_ENABLE enable) +{ + const struct gpd_fan_drvdata *const drvdata =3D gpd_driver_priv.drvdata; + int ret; + + switch (enable) { + case DISABLE: + ret =3D gpd_generic_write_pwm(255); + + if (ret) + return ret; + + return gpd_ecram_write(drvdata->manual_control_enable, 1); + case MANUAL: + ret =3D gpd_generic_write_pwm(gpd_driver_priv.pwm_value); + + if (ret) + return ret; + + return gpd_ecram_write(drvdata->manual_control_enable, 1); + case AUTOMATIC: + return gpd_ecram_write(drvdata->manual_control_enable, 0); + } + + return 0; +} + +// Write value for pwm1_enable +static int gpd_set_pwm_enable(enum FAN_PWM_ENABLE enable) +{ + if (enable =3D=3D MANUAL) + // Set pwm_value to max firstly when switching to manual mode, in + // consideration of device safety. + gpd_driver_priv.pwm_value =3D 255; + + switch (gpd_driver_priv.drvdata->board) { + case win_mini: + case win4_6800u: + return gpd_win_mini_set_pwm_enable(enable); + case duo: + return gpd_duo_set_pwm_enable(enable); + case win_max_2: + return gpd_wm2_set_pwm_enable(enable); + } + + return 0; +} + +static umode_t gpd_fan_hwmon_is_visible(__always_unused const void *drvdat= a, + enum hwmon_sensor_types type, u32 attr, + __always_unused int channel) +{ + if (type =3D=3D hwmon_fan && attr =3D=3D hwmon_fan_input) { + return 0444; + } else if (type =3D=3D hwmon_pwm) { + switch (attr) { + case hwmon_pwm_enable: + case hwmon_pwm_input: + return 0644; + default: + return 0; + } + } + return 0; +} + +static int gpd_fan_hwmon_read(__always_unused struct device *dev, + enum hwmon_sensor_types type, u32 attr, + __always_unused int channel, long *val) +{ + int ret; + + ret =3D mutex_lock_interruptible(&gpd_fan_sequence_lock); + if (ret) + return ret; + + if (type =3D=3D hwmon_fan) { + if (attr =3D=3D hwmon_fan_input) { + ret =3D gpd_read_rpm(); + + if (ret < 0) + goto OUT; + + *val =3D ret; + ret =3D 0; + goto OUT; + } + } else if (type =3D=3D hwmon_pwm) { + switch (attr) { + case hwmon_pwm_enable: + *val =3D gpd_driver_priv.pwm_enable; + ret =3D 0; + goto OUT; + case hwmon_pwm_input: + ret =3D gpd_read_pwm(); + + if (ret < 0) + goto OUT; + + *val =3D ret; + ret =3D 0; + goto OUT; + } + } + + ret =3D -EOPNOTSUPP; + +OUT: + mutex_unlock(&gpd_fan_sequence_lock); + return ret; +} + +static int gpd_fan_hwmon_write(__always_unused struct device *dev, + enum hwmon_sensor_types type, u32 attr, + __always_unused int channel, long val) +{ + int ret; + + ret =3D mutex_lock_interruptible(&gpd_fan_sequence_lock); + if (ret) + return ret; + + if (type =3D=3D hwmon_pwm) { + switch (attr) { + case hwmon_pwm_enable: + if (!in_range(val, 0, 3)) { + ret =3D -EINVAL; + goto OUT; + } + + gpd_driver_priv.pwm_enable =3D val; + + ret =3D gpd_set_pwm_enable(gpd_driver_priv.pwm_enable); + goto OUT; + case hwmon_pwm_input: + if (!in_range(val, 0, 255)) { + ret =3D -ERANGE; + goto OUT; + } + + gpd_driver_priv.pwm_value =3D val; + + ret =3D gpd_write_pwm(val); + goto OUT; + } + } + + ret =3D -EOPNOTSUPP; + +OUT: + mutex_unlock(&gpd_fan_sequence_lock); + return ret; +} + +static const struct hwmon_ops gpd_fan_ops =3D { + .is_visible =3D gpd_fan_hwmon_is_visible, + .read =3D gpd_fan_hwmon_read, + .write =3D gpd_fan_hwmon_write, +}; + +static const struct hwmon_channel_info *gpd_fan_hwmon_channel_info[] =3D { + HWMON_CHANNEL_INFO(fan, HWMON_F_INPUT), + HWMON_CHANNEL_INFO(pwm, HWMON_PWM_INPUT | HWMON_PWM_ENABLE), + NULL +}; + +static struct hwmon_chip_info gpd_fan_chip_info =3D { + .ops =3D &gpd_fan_ops, + .info =3D gpd_fan_hwmon_channel_info +}; + +static int gpd_fan_probe(struct platform_device *pdev) +{ + struct device *dev =3D &pdev->dev; + const struct resource *region; + const struct resource *res; + const struct device *hwdev; + + res =3D platform_get_resource(pdev, IORESOURCE_IO, 0); + if (IS_ERR(res)) + return dev_err_probe(dev, PTR_ERR(res), + "Failed to get platform resource\n"); + + region =3D devm_request_region(dev, res->start, + resource_size(res), DRIVER_NAME); + if (IS_ERR(region)) + return dev_err_probe(dev, PTR_ERR(region), + "Failed to request region\n"); + + hwdev =3D devm_hwmon_device_register_with_info(dev, + DRIVER_NAME, + NULL, + &gpd_fan_chip_info, + NULL); + if (IS_ERR(hwdev)) + return dev_err_probe(dev, PTR_ERR(region), + "Failed to register hwmon device\n"); + + return 0; +} + +static void gpd_fan_remove(__always_unused struct platform_device *pdev) +{ + gpd_driver_priv.pwm_enable =3D AUTOMATIC; + gpd_set_pwm_enable(AUTOMATIC); +} + +static struct platform_driver gpd_fan_driver =3D { + .probe =3D gpd_fan_probe, + .remove =3D gpd_fan_remove, + .driver =3D { + .name =3D KBUILD_MODNAME, + }, +}; + +static struct platform_device *gpd_fan_platform_device; + +static int __init gpd_fan_init(void) +{ + const struct gpd_fan_drvdata *match =3D NULL; + + for (const struct gpd_fan_drvdata **p =3D gpd_module_drvdata; *p; p++) { + if (strcmp(gpd_fan_board, (*p)->board_name) =3D=3D 0) { + match =3D *p; + break; + } + } + + if (!match) { + const struct dmi_system_id *dmi_match =3D + dmi_first_match(dmi_table); + if (dmi_match) + match =3D dmi_match->driver_data; + } + + if (!match) + return -ENODEV; + + gpd_driver_priv.pwm_enable =3D AUTOMATIC; + gpd_driver_priv.pwm_value =3D 255; + gpd_driver_priv.drvdata =3D match; + + struct resource gpd_fan_resources[] =3D { + { + .start =3D match->addr_port, + .end =3D match->data_port, + .flags =3D IORESOURCE_IO, + }, + }; + + gpd_fan_platform_device =3D platform_create_bundle(&gpd_fan_driver, + gpd_fan_probe, + gpd_fan_resources, + 1, NULL, 0); + + if (IS_ERR(gpd_fan_platform_device)) { + pr_warn("Failed to create platform device\n"); + return PTR_ERR(gpd_fan_platform_device); + } + + return 0; +} + +static void __exit gpd_fan_exit(void) +{ + platform_device_unregister(gpd_fan_platform_device); + platform_driver_unregister(&gpd_fan_driver); +} + +MODULE_DEVICE_TABLE(dmi, dmi_table); + +module_init(gpd_fan_init); +module_exit(gpd_fan_exit); + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("Cryolitia PukNgae "); +MODULE_DESCRIPTION("GPD Devices fan control driver"); --=20 2.51.0