| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * Siemens SIMATIC IPC driver for CMOS battery monitoring |
| * |
| * Copyright (c) Siemens AG, 2023 |
| * |
| * Authors: |
| * Gerd Haeussler <gerd.haeussler.ext@siemens.com> |
| * Henning Schild <henning.schild@siemens.com> |
| */ |
| |
| #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt |
| |
| #include <linux/delay.h> |
| #include <linux/io.h> |
| #include <linux/ioport.h> |
| #include <linux/gpio/machine.h> |
| #include <linux/gpio/consumer.h> |
| #include <linux/hwmon.h> |
| #include <linux/hwmon-sysfs.h> |
| #include <linux/jiffies.h> |
| #include <linux/kernel.h> |
| #include <linux/module.h> |
| #include <linux/platform_device.h> |
| #include <linux/platform_data/x86/simatic-ipc-base.h> |
| #include <linux/sizes.h> |
| |
| #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, |
| }; |
| |
| void simatic_ipc_batt_remove(struct platform_device *pdev, struct gpiod_lookup_table *table) |
| { |
| gpiod_remove_lookup_table(table); |
| } |
| 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 void simatic_ipc_batt_io_remove(struct platform_device *pdev) |
| { |
| 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_new = simatic_ipc_batt_io_remove, |
| .driver = { |
| .name = KBUILD_MODNAME, |
| }, |
| }; |
| |
| module_platform_driver(simatic_ipc_batt_driver); |
| |
| MODULE_DESCRIPTION("CMOS core battery driver for Siemens Simatic IPCs"); |
| MODULE_LICENSE("GPL"); |
| MODULE_ALIAS("platform:" KBUILD_MODNAME); |
| MODULE_AUTHOR("Henning Schild <henning.schild@siemens.com>"); |