// SPDX-License-Identifier: GPL-2.0 /* * Siemens SIMATIC IPC driver for CMOS battery monitoring * * Copyright (c) Siemens AG, 2023 * * Authors: * Gerd Haeussler * Henning Schild */ #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt #include #include #include #include #include #include #include #include #include #include #include #include #include #include "simatic-ipc-batt.h" #define BATT_DELAY_MS (1000 * 60 * 60 * 24) /* 24 h delay */ #define SIMATIC_IPC_BATT_LEVEL_FULL 3000 #define SIMATIC_IPC_BATT_LEVEL_CRIT 2750 #define SIMATIC_IPC_BATT_LEVEL_EMPTY 0 static struct simatic_ipc_batt { u8 devmode; long current_state; struct gpio_desc *gpios[3]; unsigned long last_updated_jiffies; } priv; static long simatic_ipc_batt_read_gpio(void) { long r = SIMATIC_IPC_BATT_LEVEL_FULL; if (priv.gpios[2]) { gpiod_set_value(priv.gpios[2], 1); msleep(150); } if (gpiod_get_value_cansleep(priv.gpios[0])) r = SIMATIC_IPC_BATT_LEVEL_EMPTY; else if (gpiod_get_value_cansleep(priv.gpios[1])) r = SIMATIC_IPC_BATT_LEVEL_CRIT; if (priv.gpios[2]) gpiod_set_value(priv.gpios[2], 0); return r; } #define SIMATIC_IPC_BATT_PORT_BASE 0x404D static struct resource simatic_ipc_batt_io_res = DEFINE_RES_IO_NAMED(SIMATIC_IPC_BATT_PORT_BASE, SZ_1, KBUILD_MODNAME); static long simatic_ipc_batt_read_io(struct device *dev) { long r = SIMATIC_IPC_BATT_LEVEL_FULL; struct resource *res = &simatic_ipc_batt_io_res; u8 val; if (!request_muxed_region(res->start, resource_size(res), res->name)) { dev_err(dev, "Unable to register IO resource at %pR\n", res); return -EBUSY; } val = inb(SIMATIC_IPC_BATT_PORT_BASE); release_region(simatic_ipc_batt_io_res.start, resource_size(&simatic_ipc_batt_io_res)); if (val & (1 << 7)) r = SIMATIC_IPC_BATT_LEVEL_EMPTY; else if (val & (1 << 6)) r = SIMATIC_IPC_BATT_LEVEL_CRIT; return r; } static long simatic_ipc_batt_read_value(struct device *dev) { unsigned long next_update; next_update = priv.last_updated_jiffies + msecs_to_jiffies(BATT_DELAY_MS); if (time_after(jiffies, next_update) || !priv.last_updated_jiffies) { if (priv.devmode == SIMATIC_IPC_DEVICE_227E) priv.current_state = simatic_ipc_batt_read_io(dev); else priv.current_state = simatic_ipc_batt_read_gpio(); priv.last_updated_jiffies = jiffies; if (priv.current_state < SIMATIC_IPC_BATT_LEVEL_FULL) dev_warn(dev, "CMOS battery needs to be replaced.\n"); } return priv.current_state; } static int simatic_ipc_batt_read(struct device *dev, enum hwmon_sensor_types type, u32 attr, int channel, long *val) { switch (attr) { case hwmon_in_input: *val = simatic_ipc_batt_read_value(dev); break; case hwmon_in_lcrit: *val = SIMATIC_IPC_BATT_LEVEL_CRIT; break; default: return -EOPNOTSUPP; } return 0; } static umode_t simatic_ipc_batt_is_visible(const void *data, enum hwmon_sensor_types type, u32 attr, int channel) { if (attr == hwmon_in_input || attr == hwmon_in_lcrit) return 0444; return 0; } static const struct hwmon_ops simatic_ipc_batt_ops = { .is_visible = simatic_ipc_batt_is_visible, .read = simatic_ipc_batt_read, }; static const struct hwmon_channel_info *simatic_ipc_batt_info[] = { HWMON_CHANNEL_INFO(in, HWMON_I_INPUT | HWMON_I_LCRIT), NULL }; static const struct hwmon_chip_info simatic_ipc_batt_chip_info = { .ops = &simatic_ipc_batt_ops, .info = simatic_ipc_batt_info, }; int simatic_ipc_batt_remove(struct platform_device *pdev, struct gpiod_lookup_table *table) { gpiod_remove_lookup_table(table); return 0; } EXPORT_SYMBOL_GPL(simatic_ipc_batt_remove); int simatic_ipc_batt_probe(struct platform_device *pdev, struct gpiod_lookup_table *table) { struct simatic_ipc_platform *plat; struct device *dev = &pdev->dev; struct device *hwmon_dev; unsigned long flags; int err; plat = pdev->dev.platform_data; priv.devmode = plat->devmode; switch (priv.devmode) { case SIMATIC_IPC_DEVICE_127E: case SIMATIC_IPC_DEVICE_227G: case SIMATIC_IPC_DEVICE_BX_39A: case SIMATIC_IPC_DEVICE_BX_21A: case SIMATIC_IPC_DEVICE_BX_59A: table->dev_id = dev_name(dev); gpiod_add_lookup_table(table); break; case SIMATIC_IPC_DEVICE_227E: goto nogpio; default: return -ENODEV; } priv.gpios[0] = devm_gpiod_get_index(dev, "CMOSBattery empty", 0, GPIOD_IN); if (IS_ERR(priv.gpios[0])) { err = PTR_ERR(priv.gpios[0]); priv.gpios[0] = NULL; goto out; } priv.gpios[1] = devm_gpiod_get_index(dev, "CMOSBattery low", 1, GPIOD_IN); if (IS_ERR(priv.gpios[1])) { err = PTR_ERR(priv.gpios[1]); priv.gpios[1] = NULL; goto out; } if (table->table[2].key) { flags = GPIOD_OUT_HIGH; if (priv.devmode == SIMATIC_IPC_DEVICE_BX_21A || priv.devmode == SIMATIC_IPC_DEVICE_BX_59A) flags = GPIOD_OUT_LOW; priv.gpios[2] = devm_gpiod_get_index(dev, "CMOSBattery meter", 2, flags); if (IS_ERR(priv.gpios[2])) { err = PTR_ERR(priv.gpios[2]); priv.gpios[2] = NULL; goto out; } } else { priv.gpios[2] = NULL; } nogpio: hwmon_dev = devm_hwmon_device_register_with_info(dev, KBUILD_MODNAME, &priv, &simatic_ipc_batt_chip_info, NULL); if (IS_ERR(hwmon_dev)) { err = PTR_ERR(hwmon_dev); goto out; } /* warn about aging battery even if userspace never reads hwmon */ simatic_ipc_batt_read_value(dev); return 0; out: simatic_ipc_batt_remove(pdev, table); return err; } EXPORT_SYMBOL_GPL(simatic_ipc_batt_probe); static int simatic_ipc_batt_io_remove(struct platform_device *pdev) { return simatic_ipc_batt_remove(pdev, NULL); } static int simatic_ipc_batt_io_probe(struct platform_device *pdev) { return simatic_ipc_batt_probe(pdev, NULL); } static struct platform_driver simatic_ipc_batt_driver = { .probe = simatic_ipc_batt_io_probe, .remove = simatic_ipc_batt_io_remove, .driver = { .name = KBUILD_MODNAME, }, }; module_platform_driver(simatic_ipc_batt_driver); MODULE_LICENSE("GPL"); MODULE_ALIAS("platform:" KBUILD_MODNAME); MODULE_AUTHOR("Henning Schild ");