Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux

platform/x86: portwell-ec: Add hwmon support for voltage and temperature

Integrates voltage and temperature monitoring into the driver via the hwmon
subsystem, enabling standardized reporting via tools like lm-sensors.

Signed-off-by: Yen-Chi Huang <jesse.huang@portwell.com.tw>
Link: https://patch.msgid.link/0d73c577-1941-44b4-917e-3aed6f1f664a@portwell.com.tw
[ij: align struct member comments]
Reviewed-by: Ilpo Järvinen <ilpo.jarvinen@linux.intel.com>
Signed-off-by: Ilpo Järvinen <ilpo.jarvinen@linux.intel.com>

authored by

Yen-Chi Huang and committed by
Ilpo Järvinen
8236b466 8e54e493

+162 -8
+162 -8
drivers/platform/x86/portwell-ec.c
··· 5 5 * Tested on: 6 6 * - Portwell NANO-6064 7 7 * 8 - * This driver provides support for GPIO and Watchdog Timer 9 - * functionalities of the Portwell boards with ITE embedded controller (EC). 8 + * This driver supports Portwell boards with an ITE embedded controller (EC). 10 9 * The EC is accessed through I/O ports and provides: 10 + * - Temperature and voltage readings (hwmon) 11 11 * - 8 GPIO pins for control and monitoring 12 12 * - Hardware watchdog with 1-15300 second timeout range 13 13 * 14 - * It integrates with the Linux GPIO and Watchdog subsystems, allowing 15 - * userspace interaction with EC GPIO pins and watchdog control, 16 - * ensuring system stability and configurability. 14 + * It integrates with the Linux hwmon, GPIO and Watchdog subsystems. 17 15 * 18 16 * (C) Copyright 2025 Portwell, Inc. 19 17 * Author: Yen-Chi Huang (jesse.huang@portwell.com.tw) ··· 20 22 #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt 21 23 22 24 #include <linux/acpi.h> 25 + #include <linux/bits.h> 23 26 #include <linux/bitfield.h> 24 27 #include <linux/dmi.h> 25 28 #include <linux/gpio/driver.h> 29 + #include <linux/hwmon.h> 26 30 #include <linux/init.h> 27 31 #include <linux/io.h> 28 32 #include <linux/ioport.h> ··· 33 33 #include <linux/pm.h> 34 34 #include <linux/sizes.h> 35 35 #include <linux/string.h> 36 + #include <linux/units.h> 36 37 #include <linux/watchdog.h> 37 38 38 39 #define PORTWELL_EC_IOSPACE 0xe300 ··· 42 41 #define PORTWELL_GPIO_PINS 8 43 42 #define PORTWELL_GPIO_DIR_REG 0x2b 44 43 #define PORTWELL_GPIO_VAL_REG 0x2c 44 + 45 + #define PORTWELL_HWMON_TEMP_NUM 3 46 + #define PORTWELL_HWMON_VOLT_NUM 5 45 47 46 48 #define PORTWELL_WDT_EC_CONFIG_ADDR 0x06 47 49 #define PORTWELL_WDT_CONFIG_ENABLE 0x1 ··· 57 53 #define PORTWELL_EC_FW_VENDOR_LENGTH 3 58 54 #define PORTWELL_EC_FW_VENDOR_NAME "PWG" 59 55 56 + #define PORTWELL_EC_ADC_MAX 1023 57 + 60 58 static bool force; 61 59 module_param(force, bool, 0444); 62 60 MODULE_PARM_DESC(force, "Force loading EC driver without checking DMI boardname"); 61 + 62 + /* A sensor's metadata (label, scale, and register) */ 63 + struct pwec_sensor_prop { 64 + const char *label; 65 + u8 reg; 66 + u32 scale; 67 + }; 68 + 69 + /* Master configuration with properties for all possible sensors */ 70 + static const struct { 71 + const struct pwec_sensor_prop temp_props[PORTWELL_HWMON_TEMP_NUM]; 72 + const struct pwec_sensor_prop in_props[PORTWELL_HWMON_VOLT_NUM]; 73 + } pwec_master_data = { 74 + .temp_props = { 75 + { "CPU Temperature", 0x00, 0 }, 76 + { "System Temperature", 0x02, 0 }, 77 + { "Aux Temperature", 0x04, 0 }, 78 + }, 79 + .in_props = { 80 + { "Vcore", 0x20, 3000 }, 81 + { "3.3V", 0x22, 6000 }, 82 + { "5V", 0x24, 9600 }, 83 + { "12V", 0x30, 19800 }, 84 + { "VDIMM", 0x32, 3000 }, 85 + }, 86 + }; 87 + 88 + struct pwec_board_info { 89 + u32 temp_mask; /* bit N = temperature channel N */ 90 + u32 in_mask; /* bit N = voltage channel N */ 91 + }; 92 + 93 + static const struct pwec_board_info pwec_board_info_default = { 94 + .temp_mask = GENMASK(PORTWELL_HWMON_TEMP_NUM - 1, 0), 95 + .in_mask = GENMASK(PORTWELL_HWMON_VOLT_NUM - 1, 0), 96 + }; 97 + 98 + static const struct pwec_board_info pwec_board_info_nano = { 99 + .temp_mask = BIT(0) | BIT(1), 100 + .in_mask = GENMASK(4, 0), 101 + }; 63 102 64 103 static const struct dmi_system_id pwec_dmi_table[] = { 65 104 { ··· 110 63 .matches = { 111 64 DMI_MATCH(DMI_BOARD_NAME, "NANO-6064"), 112 65 }, 66 + .driver_data = (void *)&pwec_board_info_nano, 113 67 }, 114 68 { } 115 69 }; ··· 126 78 static u8 pwec_read(u8 address) 127 79 { 128 80 return inb(PORTWELL_EC_IOSPACE + address); 81 + } 82 + 83 + /* Ensure consistent 16-bit read across potential MSB rollover. */ 84 + static u16 pwec_read16_stable(u8 lsb_reg) 85 + { 86 + u8 lsb, msb, old_msb; 87 + 88 + do { 89 + old_msb = pwec_read(lsb_reg + 1); 90 + lsb = pwec_read(lsb_reg); 91 + msb = pwec_read(lsb_reg + 1); 92 + } while (msb != old_msb); 93 + 94 + return (msb << 8) | lsb; 129 95 } 130 96 131 97 /* GPIO functions */ ··· 267 205 .max_timeout = PORTWELL_WDT_EC_MAX_COUNT_SECOND, 268 206 }; 269 207 208 + /* HWMON functions */ 209 + 210 + static umode_t pwec_hwmon_is_visible(const void *drvdata, enum hwmon_sensor_types type, 211 + u32 attr, int channel) 212 + { 213 + const struct pwec_board_info *info = drvdata; 214 + 215 + switch (type) { 216 + case hwmon_temp: 217 + return (info->temp_mask & BIT(channel)) ? 0444 : 0; 218 + case hwmon_in: 219 + return (info->in_mask & BIT(channel)) ? 0444 : 0; 220 + default: 221 + return 0; 222 + } 223 + } 224 + 225 + static int pwec_hwmon_read(struct device *dev, enum hwmon_sensor_types type, 226 + u32 attr, int channel, long *val) 227 + { 228 + u16 tmp16; 229 + 230 + switch (type) { 231 + case hwmon_temp: 232 + *val = pwec_read(pwec_master_data.temp_props[channel].reg) * MILLIDEGREE_PER_DEGREE; 233 + return 0; 234 + case hwmon_in: 235 + tmp16 = pwec_read16_stable(pwec_master_data.in_props[channel].reg); 236 + *val = (tmp16 * pwec_master_data.in_props[channel].scale) / PORTWELL_EC_ADC_MAX; 237 + return 0; 238 + default: 239 + return -EOPNOTSUPP; 240 + } 241 + } 242 + 243 + static int pwec_hwmon_read_string(struct device *dev, enum hwmon_sensor_types type, 244 + u32 attr, int channel, const char **str) 245 + { 246 + switch (type) { 247 + case hwmon_temp: 248 + *str = pwec_master_data.temp_props[channel].label; 249 + return 0; 250 + case hwmon_in: 251 + *str = pwec_master_data.in_props[channel].label; 252 + return 0; 253 + default: 254 + return -EOPNOTSUPP; 255 + } 256 + } 257 + 258 + static const struct hwmon_channel_info *pwec_hwmon_info[] = { 259 + HWMON_CHANNEL_INFO(temp, 260 + HWMON_T_INPUT | HWMON_T_LABEL, 261 + HWMON_T_INPUT | HWMON_T_LABEL, 262 + HWMON_T_INPUT | HWMON_T_LABEL), 263 + HWMON_CHANNEL_INFO(in, 264 + HWMON_I_INPUT | HWMON_I_LABEL, 265 + HWMON_I_INPUT | HWMON_I_LABEL, 266 + HWMON_I_INPUT | HWMON_I_LABEL, 267 + HWMON_I_INPUT | HWMON_I_LABEL, 268 + HWMON_I_INPUT | HWMON_I_LABEL), 269 + NULL 270 + }; 271 + 272 + static const struct hwmon_ops pwec_hwmon_ops = { 273 + .is_visible = pwec_hwmon_is_visible, 274 + .read = pwec_hwmon_read, 275 + .read_string = pwec_hwmon_read_string, 276 + }; 277 + 278 + static const struct hwmon_chip_info pwec_chip_info = { 279 + .ops = &pwec_hwmon_ops, 280 + .info = pwec_hwmon_info, 281 + }; 282 + 270 283 static int pwec_firmware_vendor_check(void) 271 284 { 272 285 u8 buf[PORTWELL_EC_FW_VENDOR_LENGTH + 1]; ··· 356 219 357 220 static int pwec_probe(struct platform_device *pdev) 358 221 { 222 + struct device *hwmon_dev; 223 + void *drvdata = dev_get_platdata(&pdev->dev); 359 224 int ret; 360 225 361 226 if (!devm_request_region(&pdev->dev, PORTWELL_EC_IOSPACE, ··· 374 235 if (ret < 0) { 375 236 dev_err(&pdev->dev, "failed to register Portwell EC GPIO\n"); 376 237 return ret; 238 + } 239 + 240 + if (IS_REACHABLE(CONFIG_HWMON)) { 241 + hwmon_dev = devm_hwmon_device_register_with_info(&pdev->dev, 242 + "portwell_ec", drvdata, &pwec_chip_info, NULL); 243 + ret = PTR_ERR_OR_ZERO(hwmon_dev); 244 + if (ret) 245 + return ret; 377 246 } 378 247 379 248 ec_wdt_dev.parent = &pdev->dev; ··· 418 271 419 272 static int __init pwec_init(void) 420 273 { 274 + const struct dmi_system_id *match; 275 + const struct pwec_board_info *hwmon_data; 421 276 int ret; 422 277 423 - if (!dmi_check_system(pwec_dmi_table)) { 278 + match = dmi_first_match(pwec_dmi_table); 279 + if (!match) { 424 280 if (!force) 425 281 return -ENODEV; 426 - pr_warn("force load portwell-ec without DMI check\n"); 282 + hwmon_data = &pwec_board_info_default; 283 + pr_warn("force load portwell-ec without DMI check, using full display config\n"); 284 + } else { 285 + hwmon_data = match->driver_data; 427 286 } 428 287 429 288 ret = platform_driver_register(&pwec_driver); 430 289 if (ret) 431 290 return ret; 432 291 433 - pwec_dev = platform_device_register_simple("portwell-ec", -1, NULL, 0); 292 + pwec_dev = platform_device_register_data(NULL, "portwell-ec", PLATFORM_DEVID_NONE, 293 + hwmon_data, sizeof(*hwmon_data)); 434 294 if (IS_ERR(pwec_dev)) { 435 295 platform_driver_unregister(&pwec_driver); 436 296 return PTR_ERR(pwec_dev);