1*a28b088eSMariano Abad // SPDX-License-Identifier: GPL-2.0-or-later 2*a28b088eSMariano Abad /* 3*a28b088eSMariano Abad * Hardware monitoring driver for LattePanda Sigma EC. 4*a28b088eSMariano Abad * 5*a28b088eSMariano Abad * The LattePanda Sigma is an x86 SBC made by DFRobot with an ITE IT8613E 6*a28b088eSMariano Abad * Embedded Controller that manages a CPU fan and thermal sensors. 7*a28b088eSMariano Abad * 8*a28b088eSMariano Abad * The BIOS declares the ACPI Embedded Controller (PNP0C09) with _STA 9*a28b088eSMariano Abad * returning 0 and provides only stub ECRD/ECWT methods that return Zero 10*a28b088eSMariano Abad * for all registers. Since the kernel's ACPI EC subsystem never initializes, 11*a28b088eSMariano Abad * ec_read() is not available and direct port I/O to the standard ACPI EC 12*a28b088eSMariano Abad * ports (0x62/0x66) is used instead. 13*a28b088eSMariano Abad * 14*a28b088eSMariano Abad * Because ACPI never initializes the EC, there is no concurrent firmware 15*a28b088eSMariano Abad * access to these ports, and no ACPI Global Lock or namespace mutex is 16*a28b088eSMariano Abad * required. The hwmon with_info API serializes all sysfs callbacks, 17*a28b088eSMariano Abad * so no additional driver-level locking is needed. 18*a28b088eSMariano Abad * 19*a28b088eSMariano Abad * The EC register map was discovered by dumping all 256 registers, 20*a28b088eSMariano Abad * identifying those that change in real-time, and validating by physically 21*a28b088eSMariano Abad * stopping the fan and observing the RPM register drop to zero. The map 22*a28b088eSMariano Abad * has been verified on BIOS version 5.27; other versions may differ. 23*a28b088eSMariano Abad * 24*a28b088eSMariano Abad * Copyright (c) 2026 Mariano Abad <weimaraner@gmail.com> 25*a28b088eSMariano Abad */ 26*a28b088eSMariano Abad 27*a28b088eSMariano Abad #include <linux/delay.h> 28*a28b088eSMariano Abad #include <linux/dmi.h> 29*a28b088eSMariano Abad #include <linux/hwmon.h> 30*a28b088eSMariano Abad #include <linux/io.h> 31*a28b088eSMariano Abad #include <linux/ioport.h> 32*a28b088eSMariano Abad #include <linux/module.h> 33*a28b088eSMariano Abad #include <linux/platform_device.h> 34*a28b088eSMariano Abad 35*a28b088eSMariano Abad #define DRIVER_NAME "lattepanda_sigma_ec" 36*a28b088eSMariano Abad 37*a28b088eSMariano Abad /* EC I/O ports (standard ACPI EC interface) */ 38*a28b088eSMariano Abad #define EC_DATA_PORT 0x62 39*a28b088eSMariano Abad #define EC_CMD_PORT 0x66 /* also status port */ 40*a28b088eSMariano Abad 41*a28b088eSMariano Abad /* EC commands */ 42*a28b088eSMariano Abad #define EC_CMD_READ 0x80 43*a28b088eSMariano Abad 44*a28b088eSMariano Abad /* EC status register bits */ 45*a28b088eSMariano Abad #define EC_STATUS_OBF 0x01 /* Output Buffer Full */ 46*a28b088eSMariano Abad #define EC_STATUS_IBF 0x02 /* Input Buffer Full */ 47*a28b088eSMariano Abad 48*a28b088eSMariano Abad /* EC register offsets for LattePanda Sigma (BIOS 5.27) */ 49*a28b088eSMariano Abad #define EC_REG_FAN_RPM_HI 0x2E 50*a28b088eSMariano Abad #define EC_REG_FAN_RPM_LO 0x2F 51*a28b088eSMariano Abad #define EC_REG_TEMP_BOARD 0x60 52*a28b088eSMariano Abad #define EC_REG_TEMP_CPU 0x70 53*a28b088eSMariano Abad #define EC_REG_FAN_DUTY 0x93 54*a28b088eSMariano Abad 55*a28b088eSMariano Abad /* 56*a28b088eSMariano Abad * EC polling uses udelay() because the EC typically responds within a 57*a28b088eSMariano Abad * few microseconds. The kernel's own ACPI EC driver (drivers/acpi/ec.c) 58*a28b088eSMariano Abad * likewise uses udelay() for busy-polling with a per-poll delay of 550us. 59*a28b088eSMariano Abad * 60*a28b088eSMariano Abad * usleep_range() was tested but caused EC protocol failures: the EC 61*a28b088eSMariano Abad * clears its status flags within microseconds, and sleeping for 50-100us 62*a28b088eSMariano Abad * between polls allowed the flags to transition past the expected state. 63*a28b088eSMariano Abad * 64*a28b088eSMariano Abad * The worst-case total busy-wait of 25ms covers EC recovery after errors. 65*a28b088eSMariano Abad * In practice the EC responds within 10us so the loop exits immediately. 66*a28b088eSMariano Abad */ 67*a28b088eSMariano Abad #define EC_TIMEOUT_US 25000 68*a28b088eSMariano Abad #define EC_POLL_US 1 69*a28b088eSMariano Abad 70*a28b088eSMariano Abad static bool force; 71*a28b088eSMariano Abad module_param(force, bool, 0444); 72*a28b088eSMariano Abad MODULE_PARM_DESC(force, 73*a28b088eSMariano Abad "Force loading on untested BIOS versions (default: false)"); 74*a28b088eSMariano Abad 75*a28b088eSMariano Abad static struct platform_device *lps_ec_pdev; 76*a28b088eSMariano Abad 77*a28b088eSMariano Abad static int ec_wait_ibf_clear(void) 78*a28b088eSMariano Abad { 79*a28b088eSMariano Abad int i; 80*a28b088eSMariano Abad 81*a28b088eSMariano Abad for (i = 0; i < EC_TIMEOUT_US; i++) { 82*a28b088eSMariano Abad if (!(inb(EC_CMD_PORT) & EC_STATUS_IBF)) 83*a28b088eSMariano Abad return 0; 84*a28b088eSMariano Abad udelay(EC_POLL_US); 85*a28b088eSMariano Abad } 86*a28b088eSMariano Abad return -ETIMEDOUT; 87*a28b088eSMariano Abad } 88*a28b088eSMariano Abad 89*a28b088eSMariano Abad static int ec_wait_obf_set(void) 90*a28b088eSMariano Abad { 91*a28b088eSMariano Abad int i; 92*a28b088eSMariano Abad 93*a28b088eSMariano Abad for (i = 0; i < EC_TIMEOUT_US; i++) { 94*a28b088eSMariano Abad if (inb(EC_CMD_PORT) & EC_STATUS_OBF) 95*a28b088eSMariano Abad return 0; 96*a28b088eSMariano Abad udelay(EC_POLL_US); 97*a28b088eSMariano Abad } 98*a28b088eSMariano Abad return -ETIMEDOUT; 99*a28b088eSMariano Abad } 100*a28b088eSMariano Abad 101*a28b088eSMariano Abad static int ec_read_reg(u8 reg, u8 *val) 102*a28b088eSMariano Abad { 103*a28b088eSMariano Abad int ret; 104*a28b088eSMariano Abad 105*a28b088eSMariano Abad ret = ec_wait_ibf_clear(); 106*a28b088eSMariano Abad if (ret) 107*a28b088eSMariano Abad return ret; 108*a28b088eSMariano Abad 109*a28b088eSMariano Abad outb(EC_CMD_READ, EC_CMD_PORT); 110*a28b088eSMariano Abad 111*a28b088eSMariano Abad ret = ec_wait_ibf_clear(); 112*a28b088eSMariano Abad if (ret) 113*a28b088eSMariano Abad return ret; 114*a28b088eSMariano Abad 115*a28b088eSMariano Abad outb(reg, EC_DATA_PORT); 116*a28b088eSMariano Abad 117*a28b088eSMariano Abad ret = ec_wait_obf_set(); 118*a28b088eSMariano Abad if (ret) 119*a28b088eSMariano Abad return ret; 120*a28b088eSMariano Abad 121*a28b088eSMariano Abad *val = inb(EC_DATA_PORT); 122*a28b088eSMariano Abad return 0; 123*a28b088eSMariano Abad } 124*a28b088eSMariano Abad 125*a28b088eSMariano Abad /* 126*a28b088eSMariano Abad * Read a 16-bit big-endian value from two consecutive EC registers. 127*a28b088eSMariano Abad * 128*a28b088eSMariano Abad * The EC may update the register pair between reading the high and low 129*a28b088eSMariano Abad * bytes, which could produce a corrupted value if the high byte rolls 130*a28b088eSMariano Abad * over (e.g., 0x0100 -> 0x00FF read as 0x01FF). Guard against this by 131*a28b088eSMariano Abad * re-reading the high byte after reading the low byte. If the high byte 132*a28b088eSMariano Abad * changed, re-read the low byte to get a consistent pair. 133*a28b088eSMariano Abad * See also lm90_read16() which uses the same approach. 134*a28b088eSMariano Abad */ 135*a28b088eSMariano Abad static int ec_read_reg16(u8 reg_hi, u8 reg_lo, u16 *val) 136*a28b088eSMariano Abad { 137*a28b088eSMariano Abad int ret; 138*a28b088eSMariano Abad u8 oldh, newh, lo; 139*a28b088eSMariano Abad 140*a28b088eSMariano Abad ret = ec_read_reg(reg_hi, &oldh); 141*a28b088eSMariano Abad if (ret) 142*a28b088eSMariano Abad return ret; 143*a28b088eSMariano Abad 144*a28b088eSMariano Abad ret = ec_read_reg(reg_lo, &lo); 145*a28b088eSMariano Abad if (ret) 146*a28b088eSMariano Abad return ret; 147*a28b088eSMariano Abad 148*a28b088eSMariano Abad ret = ec_read_reg(reg_hi, &newh); 149*a28b088eSMariano Abad if (ret) 150*a28b088eSMariano Abad return ret; 151*a28b088eSMariano Abad 152*a28b088eSMariano Abad if (oldh != newh) { 153*a28b088eSMariano Abad ret = ec_read_reg(reg_lo, &lo); 154*a28b088eSMariano Abad if (ret) 155*a28b088eSMariano Abad return ret; 156*a28b088eSMariano Abad } 157*a28b088eSMariano Abad 158*a28b088eSMariano Abad *val = ((u16)newh << 8) | lo; 159*a28b088eSMariano Abad return 0; 160*a28b088eSMariano Abad } 161*a28b088eSMariano Abad 162*a28b088eSMariano Abad static int 163*a28b088eSMariano Abad lps_ec_read_string(struct device *dev, 164*a28b088eSMariano Abad enum hwmon_sensor_types type, 165*a28b088eSMariano Abad u32 attr, int channel, 166*a28b088eSMariano Abad const char **str) 167*a28b088eSMariano Abad { 168*a28b088eSMariano Abad switch (type) { 169*a28b088eSMariano Abad case hwmon_fan: 170*a28b088eSMariano Abad *str = "CPU Fan"; 171*a28b088eSMariano Abad return 0; 172*a28b088eSMariano Abad case hwmon_temp: 173*a28b088eSMariano Abad *str = channel == 0 ? "Board Temp" : "CPU Temp"; 174*a28b088eSMariano Abad return 0; 175*a28b088eSMariano Abad default: 176*a28b088eSMariano Abad return -EOPNOTSUPP; 177*a28b088eSMariano Abad } 178*a28b088eSMariano Abad } 179*a28b088eSMariano Abad 180*a28b088eSMariano Abad static umode_t 181*a28b088eSMariano Abad lps_ec_is_visible(const void *drvdata, 182*a28b088eSMariano Abad enum hwmon_sensor_types type, 183*a28b088eSMariano Abad u32 attr, int channel) 184*a28b088eSMariano Abad { 185*a28b088eSMariano Abad switch (type) { 186*a28b088eSMariano Abad case hwmon_fan: 187*a28b088eSMariano Abad if (attr == hwmon_fan_input || attr == hwmon_fan_label) 188*a28b088eSMariano Abad return 0444; 189*a28b088eSMariano Abad break; 190*a28b088eSMariano Abad case hwmon_temp: 191*a28b088eSMariano Abad if (attr == hwmon_temp_input || attr == hwmon_temp_label) 192*a28b088eSMariano Abad return 0444; 193*a28b088eSMariano Abad break; 194*a28b088eSMariano Abad default: 195*a28b088eSMariano Abad break; 196*a28b088eSMariano Abad } 197*a28b088eSMariano Abad return 0; 198*a28b088eSMariano Abad } 199*a28b088eSMariano Abad 200*a28b088eSMariano Abad static int 201*a28b088eSMariano Abad lps_ec_read(struct device *dev, 202*a28b088eSMariano Abad enum hwmon_sensor_types type, 203*a28b088eSMariano Abad u32 attr, int channel, long *val) 204*a28b088eSMariano Abad { 205*a28b088eSMariano Abad u16 rpm; 206*a28b088eSMariano Abad u8 v; 207*a28b088eSMariano Abad int ret; 208*a28b088eSMariano Abad 209*a28b088eSMariano Abad switch (type) { 210*a28b088eSMariano Abad case hwmon_fan: 211*a28b088eSMariano Abad if (attr != hwmon_fan_input) 212*a28b088eSMariano Abad return -EOPNOTSUPP; 213*a28b088eSMariano Abad ret = ec_read_reg16(EC_REG_FAN_RPM_HI, 214*a28b088eSMariano Abad EC_REG_FAN_RPM_LO, &rpm); 215*a28b088eSMariano Abad if (ret) 216*a28b088eSMariano Abad return ret; 217*a28b088eSMariano Abad *val = rpm; 218*a28b088eSMariano Abad return 0; 219*a28b088eSMariano Abad 220*a28b088eSMariano Abad case hwmon_temp: 221*a28b088eSMariano Abad if (attr != hwmon_temp_input) 222*a28b088eSMariano Abad return -EOPNOTSUPP; 223*a28b088eSMariano Abad ret = ec_read_reg(channel == 0 ? EC_REG_TEMP_BOARD 224*a28b088eSMariano Abad : EC_REG_TEMP_CPU, 225*a28b088eSMariano Abad &v); 226*a28b088eSMariano Abad if (ret) 227*a28b088eSMariano Abad return ret; 228*a28b088eSMariano Abad /* EC reports unsigned 8-bit temperature in degrees Celsius */ 229*a28b088eSMariano Abad *val = (unsigned long)v * 1000; 230*a28b088eSMariano Abad return 0; 231*a28b088eSMariano Abad 232*a28b088eSMariano Abad default: 233*a28b088eSMariano Abad return -EOPNOTSUPP; 234*a28b088eSMariano Abad } 235*a28b088eSMariano Abad } 236*a28b088eSMariano Abad 237*a28b088eSMariano Abad static const struct hwmon_channel_info * const lps_ec_info[] = { 238*a28b088eSMariano Abad HWMON_CHANNEL_INFO(fan, HWMON_F_INPUT | HWMON_F_LABEL), 239*a28b088eSMariano Abad HWMON_CHANNEL_INFO(temp, 240*a28b088eSMariano Abad HWMON_T_INPUT | HWMON_T_LABEL, 241*a28b088eSMariano Abad HWMON_T_INPUT | HWMON_T_LABEL), 242*a28b088eSMariano Abad NULL 243*a28b088eSMariano Abad }; 244*a28b088eSMariano Abad 245*a28b088eSMariano Abad static const struct hwmon_ops lps_ec_ops = { 246*a28b088eSMariano Abad .is_visible = lps_ec_is_visible, 247*a28b088eSMariano Abad .read = lps_ec_read, 248*a28b088eSMariano Abad .read_string = lps_ec_read_string, 249*a28b088eSMariano Abad }; 250*a28b088eSMariano Abad 251*a28b088eSMariano Abad static const struct hwmon_chip_info lps_ec_chip_info = { 252*a28b088eSMariano Abad .ops = &lps_ec_ops, 253*a28b088eSMariano Abad .info = lps_ec_info, 254*a28b088eSMariano Abad }; 255*a28b088eSMariano Abad 256*a28b088eSMariano Abad static int lps_ec_probe(struct platform_device *pdev) 257*a28b088eSMariano Abad { 258*a28b088eSMariano Abad struct device *dev = &pdev->dev; 259*a28b088eSMariano Abad struct device *hwmon; 260*a28b088eSMariano Abad u8 test; 261*a28b088eSMariano Abad int ret; 262*a28b088eSMariano Abad 263*a28b088eSMariano Abad if (!devm_request_region(dev, EC_DATA_PORT, 1, DRIVER_NAME)) 264*a28b088eSMariano Abad return dev_err_probe(dev, -EBUSY, 265*a28b088eSMariano Abad "Failed to request EC data port 0x%x\n", 266*a28b088eSMariano Abad EC_DATA_PORT); 267*a28b088eSMariano Abad 268*a28b088eSMariano Abad if (!devm_request_region(dev, EC_CMD_PORT, 1, DRIVER_NAME)) 269*a28b088eSMariano Abad return dev_err_probe(dev, -EBUSY, 270*a28b088eSMariano Abad "Failed to request EC cmd port 0x%x\n", 271*a28b088eSMariano Abad EC_CMD_PORT); 272*a28b088eSMariano Abad 273*a28b088eSMariano Abad /* Sanity check: verify EC is responsive */ 274*a28b088eSMariano Abad ret = ec_read_reg(EC_REG_FAN_DUTY, &test); 275*a28b088eSMariano Abad if (ret) 276*a28b088eSMariano Abad return dev_err_probe(dev, ret, 277*a28b088eSMariano Abad "EC not responding on ports 0x%x/0x%x\n", 278*a28b088eSMariano Abad EC_DATA_PORT, EC_CMD_PORT); 279*a28b088eSMariano Abad 280*a28b088eSMariano Abad hwmon = devm_hwmon_device_register_with_info(dev, DRIVER_NAME, NULL, 281*a28b088eSMariano Abad &lps_ec_chip_info, NULL); 282*a28b088eSMariano Abad if (IS_ERR(hwmon)) 283*a28b088eSMariano Abad return dev_err_probe(dev, PTR_ERR(hwmon), 284*a28b088eSMariano Abad "Failed to register hwmon device\n"); 285*a28b088eSMariano Abad 286*a28b088eSMariano Abad dev_info(dev, "EC hwmon registered (fan duty: %u%%)\n", test); 287*a28b088eSMariano Abad return 0; 288*a28b088eSMariano Abad } 289*a28b088eSMariano Abad 290*a28b088eSMariano Abad /* DMI table with strict BIOS version match (override with force=1) */ 291*a28b088eSMariano Abad static const struct dmi_system_id lps_ec_dmi_table[] = { 292*a28b088eSMariano Abad { 293*a28b088eSMariano Abad .ident = "LattePanda Sigma", 294*a28b088eSMariano Abad .matches = { 295*a28b088eSMariano Abad DMI_MATCH(DMI_SYS_VENDOR, "LattePanda"), 296*a28b088eSMariano Abad DMI_MATCH(DMI_PRODUCT_NAME, "LattePanda Sigma"), 297*a28b088eSMariano Abad DMI_MATCH(DMI_BIOS_VERSION, "5.27"), 298*a28b088eSMariano Abad }, 299*a28b088eSMariano Abad }, 300*a28b088eSMariano Abad { } /* terminator */ 301*a28b088eSMariano Abad }; 302*a28b088eSMariano Abad MODULE_DEVICE_TABLE(dmi, lps_ec_dmi_table); 303*a28b088eSMariano Abad 304*a28b088eSMariano Abad /* Loose table (vendor + product only) for use with force=1 */ 305*a28b088eSMariano Abad static const struct dmi_system_id lps_ec_dmi_table_force[] = { 306*a28b088eSMariano Abad { 307*a28b088eSMariano Abad .ident = "LattePanda Sigma", 308*a28b088eSMariano Abad .matches = { 309*a28b088eSMariano Abad DMI_MATCH(DMI_SYS_VENDOR, "LattePanda"), 310*a28b088eSMariano Abad DMI_MATCH(DMI_PRODUCT_NAME, "LattePanda Sigma"), 311*a28b088eSMariano Abad }, 312*a28b088eSMariano Abad }, 313*a28b088eSMariano Abad { } /* terminator */ 314*a28b088eSMariano Abad }; 315*a28b088eSMariano Abad 316*a28b088eSMariano Abad static struct platform_driver lps_ec_driver = { 317*a28b088eSMariano Abad .probe = lps_ec_probe, 318*a28b088eSMariano Abad .driver = { 319*a28b088eSMariano Abad .name = DRIVER_NAME, 320*a28b088eSMariano Abad }, 321*a28b088eSMariano Abad }; 322*a28b088eSMariano Abad 323*a28b088eSMariano Abad static int __init lps_ec_init(void) 324*a28b088eSMariano Abad { 325*a28b088eSMariano Abad int ret; 326*a28b088eSMariano Abad 327*a28b088eSMariano Abad if (!dmi_check_system(lps_ec_dmi_table)) { 328*a28b088eSMariano Abad if (!force || !dmi_check_system(lps_ec_dmi_table_force)) 329*a28b088eSMariano Abad return -ENODEV; 330*a28b088eSMariano Abad pr_warn("%s: BIOS version not verified, loading due to force=1\n", 331*a28b088eSMariano Abad DRIVER_NAME); 332*a28b088eSMariano Abad } 333*a28b088eSMariano Abad 334*a28b088eSMariano Abad ret = platform_driver_register(&lps_ec_driver); 335*a28b088eSMariano Abad if (ret) 336*a28b088eSMariano Abad return ret; 337*a28b088eSMariano Abad 338*a28b088eSMariano Abad lps_ec_pdev = platform_device_register_simple(DRIVER_NAME, -1, 339*a28b088eSMariano Abad NULL, 0); 340*a28b088eSMariano Abad if (IS_ERR(lps_ec_pdev)) { 341*a28b088eSMariano Abad platform_driver_unregister(&lps_ec_driver); 342*a28b088eSMariano Abad return PTR_ERR(lps_ec_pdev); 343*a28b088eSMariano Abad } 344*a28b088eSMariano Abad 345*a28b088eSMariano Abad return 0; 346*a28b088eSMariano Abad } 347*a28b088eSMariano Abad 348*a28b088eSMariano Abad static void __exit lps_ec_exit(void) 349*a28b088eSMariano Abad { 350*a28b088eSMariano Abad platform_device_unregister(lps_ec_pdev); 351*a28b088eSMariano Abad platform_driver_unregister(&lps_ec_driver); 352*a28b088eSMariano Abad } 353*a28b088eSMariano Abad 354*a28b088eSMariano Abad module_init(lps_ec_init); 355*a28b088eSMariano Abad module_exit(lps_ec_exit); 356*a28b088eSMariano Abad 357*a28b088eSMariano Abad MODULE_AUTHOR("Mariano Abad <weimaraner@gmail.com>"); 358*a28b088eSMariano Abad MODULE_DESCRIPTION("Hardware monitoring driver for LattePanda Sigma EC"); 359*a28b088eSMariano Abad MODULE_LICENSE("GPL"); 360