hwmon: Add KEBA battery monitoring controller support

The KEBA battery monitoring controller is found in the system FPGA of
KEBA PLC devices. It puts a load on the coin cell battery to check the
state of the battery. If the coin cell battery is nearly empty, then
the user space is signaled with a hwmon alarm.

The auxiliary device for this driver is instantiated by the cp500 misc
driver.

Signed-off-by: Gerhard Engleder <eg@keba.com>
Link: https://lore.kernel.org/r/20250409190830.60489-1-gerhard@engleder-embedded.com
Signed-off-by: Guenter Roeck <linux@roeck-us.net>
This commit is contained in:
Gerhard Engleder 2025-04-09 21:08:30 +02:00 committed by Guenter Roeck
parent 0d01110e63
commit 7e581c193b
5 changed files with 219 additions and 0 deletions

View File

@ -106,6 +106,7 @@ Hardware Monitoring Kernel Drivers
jc42
k10temp
k8temp
kbatt
lan966x
lineage-pem
lm25066

View File

@ -0,0 +1,60 @@
.. SPDX-License-Identifier: GPL-2.0
Kernel driver kbatt
===================
Supported chips:
* KEBA battery monitoring controller (IP core in FPGA)
Prefix: 'kbatt'
Authors:
Gerhard Engleder <eg@keba.com>
Petar Bojanic <boja@keba.com>
Description
-----------
The KEBA battery monitoring controller is an IP core for FPGAs, which
monitors the health of a coin cell battery. The coin cell battery is
typically used to supply the RTC during power off to keep the current
time. E.g., the CP500 FPGA includes this IP core to monitor the coin cell
battery of PLCs and the corresponding cp500 driver creates an auxiliary
device for the kbatt driver.
This driver provides information about the coin cell battery health to
user space. Actually the user space shall be informed that the coin cell
battery is nearly empty and needs to be replaced.
The coin cell battery must be tested actively to get to know if its nearly
empty or not. Therefore, a load is put on the coin cell battery and the
resulting voltage is evaluated. This evaluation is done by some hard wired
analog logic, which compares the voltage to a defined limit. If the
voltage is above the limit, then the coin cell battery is assumed to be
ok. If the voltage is below the limit, then the coin cell battery is
nearly empty (or broken, removed, ...) and shall be replaced by a new one.
The KEBA battery monitoring controller allows to start the test of the
coin cell battery and to get the result if the voltage is above or below
the limit. The actual voltage is not available. Only the information if
the voltage is below a limit is available.
The test load, which is put on the coin cell battery for the health check,
is similar to the load during power off. Therefore, the lifetime of the
coin cell battery is reduced directly by the duration of each test. To
limit the negative impact to the lifetime the test is limited to at most
once every 10 seconds. The test load is put on the coin cell battery for
100ms. Thus, in worst case the coin cell battery lifetime is reduced by
1% of the uptime or 3.65 days per year. As the coin cell battery lasts
multiple years, this lifetime reduction negligible.
This driver only provides a single alarm attribute, which is raised when
the coin cell battery is nearly empty.
====================== ==== ===================================================
Attribute R/W Contents
====================== ==== ===================================================
in0_min_alarm R voltage of coin cell battery under load is below
limit
====================== ==== ===================================================

View File

@ -335,6 +335,16 @@ config SENSORS_K10TEMP
This driver can also be built as a module. If so, the module
will be called k10temp.
config SENSORS_KBATT
tristate "KEBA battery controller support"
depends on KEBA_CP500
help
This driver supports the battery monitoring controller found in
KEBA system FPGA devices.
This driver can also be built as a module. If so, the module
will be called kbatt.
config SENSORS_FAM15H_POWER
tristate "AMD Family 15h processor power"
depends on X86 && PCI && CPU_SUP_AMD

View File

@ -110,6 +110,7 @@ obj-$(CONFIG_SENSORS_IT87) += it87.o
obj-$(CONFIG_SENSORS_JC42) += jc42.o
obj-$(CONFIG_SENSORS_K8TEMP) += k8temp.o
obj-$(CONFIG_SENSORS_K10TEMP) += k10temp.o
obj-$(CONFIG_SENSORS_KBATT) += kbatt.o
obj-$(CONFIG_SENSORS_LAN966X) += lan966x-hwmon.o
obj-$(CONFIG_SENSORS_LENOVO_EC) += lenovo-ec-sensors.o
obj-$(CONFIG_SENSORS_LINEAGE) += lineage-pem.o

147
drivers/hwmon/kbatt.c Normal file
View File

@ -0,0 +1,147 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Copyright (C) 2025 KEBA Industrial Automation GmbH
*
* Driver for KEBA battery monitoring controller FPGA IP core
*/
#include <linux/hwmon.h>
#include <linux/io.h>
#include <linux/delay.h>
#include <linux/module.h>
#include <linux/device.h>
#include <linux/auxiliary_bus.h>
#include <linux/misc/keba.h>
#include <linux/mutex.h>
#define KBATT "kbatt"
#define KBATT_CONTROL_REG 0x4
#define KBATT_CONTROL_BAT_TEST 0x01
#define KBATT_STATUS_REG 0x8
#define KBATT_STATUS_BAT_OK 0x01
#define KBATT_MAX_UPD_INTERVAL (10 * HZ)
#define KBATT_SETTLE_TIME_US (100 * USEC_PER_MSEC)
struct kbatt {
/* update lock */
struct mutex lock;
void __iomem *base;
unsigned long next_update; /* in jiffies */
bool alarm;
};
static bool kbatt_alarm(struct kbatt *kbatt)
{
mutex_lock(&kbatt->lock);
if (!kbatt->next_update || time_after(jiffies, kbatt->next_update)) {
/* switch load on */
iowrite8(KBATT_CONTROL_BAT_TEST,
kbatt->base + KBATT_CONTROL_REG);
/* wait some time to let things settle */
fsleep(KBATT_SETTLE_TIME_US);
/* check battery state */
if (ioread8(kbatt->base + KBATT_STATUS_REG) &
KBATT_STATUS_BAT_OK)
kbatt->alarm = false;
else
kbatt->alarm = true;
/* switch load off */
iowrite8(0, kbatt->base + KBATT_CONTROL_REG);
kbatt->next_update = jiffies + KBATT_MAX_UPD_INTERVAL;
}
mutex_unlock(&kbatt->lock);
return kbatt->alarm;
}
static int kbatt_read(struct device *dev, enum hwmon_sensor_types type,
u32 attr, int channel, long *val)
{
struct kbatt *kbatt = dev_get_drvdata(dev);
*val = kbatt_alarm(kbatt) ? 1 : 0;
return 0;
}
static umode_t kbatt_is_visible(const void *data, enum hwmon_sensor_types type,
u32 attr, int channel)
{
if (channel == 0 && attr == hwmon_in_min_alarm)
return 0444;
return 0;
}
static const struct hwmon_channel_info *kbatt_info[] = {
HWMON_CHANNEL_INFO(in,
/* 0: input minimum alarm channel */
HWMON_I_MIN_ALARM),
NULL
};
static const struct hwmon_ops kbatt_hwmon_ops = {
.is_visible = kbatt_is_visible,
.read = kbatt_read,
};
static const struct hwmon_chip_info kbatt_chip_info = {
.ops = &kbatt_hwmon_ops,
.info = kbatt_info,
};
static int kbatt_probe(struct auxiliary_device *auxdev,
const struct auxiliary_device_id *id)
{
struct keba_batt_auxdev *kbatt_auxdev =
container_of(auxdev, struct keba_batt_auxdev, auxdev);
struct device *dev = &auxdev->dev;
struct device *hwmon_dev;
struct kbatt *kbatt;
int retval;
kbatt = devm_kzalloc(dev, sizeof(*kbatt), GFP_KERNEL);
if (!kbatt)
return -ENOMEM;
retval = devm_mutex_init(dev, &kbatt->lock);
if (retval)
return retval;
kbatt->base = devm_ioremap_resource(dev, &kbatt_auxdev->io);
if (IS_ERR(kbatt->base))
return PTR_ERR(kbatt->base);
hwmon_dev = devm_hwmon_device_register_with_info(dev, KBATT, kbatt,
&kbatt_chip_info,
NULL);
return PTR_ERR_OR_ZERO(hwmon_dev);
}
static const struct auxiliary_device_id kbatt_devtype_aux[] = {
{ .name = "keba.batt" },
{}
};
MODULE_DEVICE_TABLE(auxiliary, kbatt_devtype_aux);
static struct auxiliary_driver kbatt_driver_aux = {
.name = KBATT,
.id_table = kbatt_devtype_aux,
.probe = kbatt_probe,
};
module_auxiliary_driver(kbatt_driver_aux);
MODULE_AUTHOR("Petar Bojanic <boja@keba.com>");
MODULE_AUTHOR("Gerhard Engleder <eg@keba.com>");
MODULE_DESCRIPTION("KEBA battery monitoring controller driver");
MODULE_LICENSE("GPL");