iio: adc: add Versal SysMon driver

Add the core driver and MMIO platform driver for the AMD/Xilinx Versal
System Monitor (SysMon) block.

The SysMon block resides in the platform management controller (PMC) and
provides on-chip voltage and temperature monitoring through a 10-bit,
200 kSPS ADC. It can monitor up to 160 voltage channels and 64
temperature satellites distributed across the SoC, with a consistent
sample rate of 8 kSPS per channel regardless of how many channels are
enabled.

The hardware also provides four aggregate temperature registers that
are always present regardless of the device tree configuration: the
current max and min across all active satellites, and the peak and
trough values recorded since the last hardware reset.

The driver is split into two compilation units:
  - versal-sysmon-core: Channel parsing, IIO registration, read_raw
  - versal-sysmon: MMIO platform driver with custom regmap accessors

Voltage results are stored in a 19-bit modified floating-point format
and converted to millivolts. Temperature results are stored in Q8.7
signed fixed-point Celsius format and converted to millicelsius.

The MMIO regmap backend uses a custom reg_write accessor that
automatically unlocks the NPI (NoC programming interface) lock
register before each write, as required by the hardware. The regmap
is configured with fast_io since the underlying MMIO accessors are
safe to call from atomic context.

Co-developed-by: Michal Simek <michal.simek@amd.com>
Signed-off-by: Michal Simek <michal.simek@amd.com>
Signed-off-by: Salih Erim <salih.erim@amd.com>
Reviewed-by: Andy Shevchenko <andriy.shevchenko@intel.com>
Signed-off-by: Jonathan Cameron <jic23@kernel.org>
This commit is contained in:
Salih Erim 2026-06-24 15:37:19 +01:00 committed by Jonathan Cameron
parent bb6ea868b8
commit 50bf4fd85b
6 changed files with 481 additions and 0 deletions

View File

@ -29627,6 +29627,14 @@ F: Documentation/devicetree/bindings/memory-controllers/xlnx,versal-net-ddrmc5.y
F: drivers/edac/versalnet_edac.c
F: include/linux/cdx/edac_cdx_pcol.h
XILINX VERSAL SYSMON DRIVER
M: Salih Erim <salih.erim@amd.com>
M: Sai Krishna Potthuri <sai.krishna.potthuri@amd.com>
L: linux-iio@vger.kernel.org
S: Maintained
F: Documentation/devicetree/bindings/iio/adc/xlnx,versal-sysmon.yaml
F: drivers/iio/adc/versal-sysmon*
XILINX WATCHDOG DRIVER
M: Srinivas Neeli <srinivas.neeli@amd.com>
R: Shubhrajyoti Datta <shubhrajyoti.datta@amd.com>

View File

@ -1973,6 +1973,26 @@ config TWL6030_GPADC
This driver can also be built as a module. If so, the module will be
called twl6030-gpadc.
config VERSAL_SYSMON_CORE
tristate
select REGMAP
config VERSAL_SYSMON
tristate "AMD Versal SysMon driver"
depends on ARCH_ZYNQMP || COMPILE_TEST
depends on HAS_IOMEM
select VERSAL_SYSMON_CORE
help
Say yes here to have support for the AMD/Xilinx Versal System
Monitor (SysMon). This driver provides voltage and temperature
monitoring through the IIO subsystem.
The SysMon measures up to 160 supply voltages and reads up to
64 temperature satellites distributed across the SoC.
To compile this driver as a module, choose M here: the module
will be called versal-sysmon.
config VF610_ADC
tristate "Freescale vf610 ADC driver"
depends on HAS_IOMEM

View File

@ -169,6 +169,8 @@ obj-$(CONFIG_TI_TLC4541) += ti-tlc4541.o
obj-$(CONFIG_TI_TSC2046) += ti-tsc2046.o
obj-$(CONFIG_TWL4030_MADC) += twl4030-madc.o
obj-$(CONFIG_TWL6030_GPADC) += twl6030-gpadc.o
obj-$(CONFIG_VERSAL_SYSMON_CORE) += versal-sysmon-core.o
obj-$(CONFIG_VERSAL_SYSMON) += versal-sysmon.o
obj-$(CONFIG_VF610_ADC) += vf610_adc.o
obj-$(CONFIG_VIPERBOARD_ADC) += viperboard_adc.o
obj-$(CONFIG_XILINX_AMS) += xilinx-ams.o

View File

@ -0,0 +1,292 @@
// SPDX-License-Identifier: GPL-2.0
/*
* AMD Versal SysMon core driver
*
* Copyright (C) 2019 - 2022, Xilinx, Inc.
* Copyright (C) 2022 - 2026, Advanced Micro Devices, Inc.
*/
#include <linux/array_size.h>
#include <linux/bitfield.h>
#include <linux/bitops.h>
#include <linux/cleanup.h>
#include <linux/device.h>
#include <linux/err.h>
#include <linux/module.h>
#include <linux/overflow.h>
#include <linux/property.h>
#include <linux/regmap.h>
#include <linux/string.h>
#include <linux/sysfs.h>
#include <linux/units.h>
#include <linux/iio/iio.h>
#include "versal-sysmon.h"
#define SYSMON_CHAN_TEMP(_chan, _address, _name) \
{ \
.type = IIO_TEMP, \
.indexed = 1, \
.address = _address, \
.channel = _chan, \
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW), \
.info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SCALE), \
.datasheet_name = _name, \
}
/*
* Static temperature channels (always present).
*
* These are hardware-computed aggregate registers across all active
* temperature satellites:
* temp: current max temperature across all active satellites
* min: current min temperature across all active satellites
* max_max: highest peak recorded since last hardware reset
* min_min: lowest trough recorded since last hardware reset
*/
static const struct iio_chan_spec temp_channels[] = {
SYSMON_CHAN_TEMP(0, SYSMON_TEMP_MAX, "temp"),
SYSMON_CHAN_TEMP(1, SYSMON_TEMP_MIN, "min"),
SYSMON_CHAN_TEMP(2, SYSMON_TEMP_MAX_MAX, "max_max"),
SYSMON_CHAN_TEMP(3, SYSMON_TEMP_MIN_MIN, "min_min"),
};
static void sysmon_supply_rawtoprocessed(int raw_data, int *val)
{
int mantissa, format, exponent;
mantissa = FIELD_GET(SYSMON_MANTISSA_MASK, raw_data);
exponent = SYSMON_SUPPLY_MANTISSA_BITS - FIELD_GET(SYSMON_MODE_MASK, raw_data);
format = FIELD_GET(SYSMON_FMT_MASK, raw_data);
/*
* When format bit is set the mantissa is two's complement
* (per hardware spec); sign-extend to int for correct arithmetic.
*/
if (format)
mantissa = sign_extend32(mantissa, 15);
*val = (mantissa * (int)MILLI) >> exponent;
}
static int sysmon_read_raw(struct iio_dev *indio_dev,
struct iio_chan_spec const *chan,
int *val, int *val2, long mask)
{
struct sysmon *sysmon = iio_priv(indio_dev);
unsigned int regval;
int ret;
guard(mutex)(&sysmon->lock);
switch (chan->type) {
case IIO_TEMP:
if (mask == IIO_CHAN_INFO_SCALE) {
/* Q8.7 to millicelsius: raw * 1000 / 128 */
*val = MILLIDEGREE_PER_DEGREE;
*val2 = BIT(SYSMON_FRACTIONAL_SHIFT);
return IIO_VAL_FRACTIONAL;
}
if (mask != IIO_CHAN_INFO_RAW)
return -EINVAL;
ret = regmap_read(sysmon->regmap, chan->address, &regval);
if (ret)
return ret;
*val = sign_extend32(regval, 15);
return IIO_VAL_INT;
case IIO_VOLTAGE:
if (mask != IIO_CHAN_INFO_PROCESSED)
return -EINVAL;
ret = regmap_read(sysmon->regmap,
chan->address * SYSMON_REG_STRIDE +
SYSMON_SUPPLY_BASE, &regval);
if (ret)
return ret;
sysmon_supply_rawtoprocessed(regval, val);
return IIO_VAL_INT;
default:
return -EINVAL;
}
}
static int sysmon_read_label(struct iio_dev *indio_dev,
struct iio_chan_spec const *chan,
char *label)
{
if (chan->datasheet_name)
return sysfs_emit(label, "%s\n", chan->datasheet_name);
return -EINVAL;
}
static const struct iio_info sysmon_iio_info = {
.read_raw = sysmon_read_raw,
.read_label = sysmon_read_label,
};
/**
* sysmon_parse_fw() - Parse firmware nodes and configure IIO channels.
* @indio_dev: IIO device instance
* @dev: Parent device
*
* Reads voltage-channels and temperature-channels container nodes from
* firmware and builds the IIO channel array. Static temperature channels
* are prepended, followed by supply and satellite channels from DT.
*
* Return: 0 on success, negative errno on failure.
*/
static int sysmon_parse_fw(struct iio_dev *indio_dev, struct device *dev)
{
unsigned int num_chan, num_static, num_supply, num_temp;
unsigned int idx, temp_chan_idx, volt_chan_idx;
struct iio_chan_spec *sysmon_channels;
const char *label;
u32 reg;
int ret;
struct fwnode_handle *supply_node __free(fwnode_handle) =
device_get_named_child_node(dev, "voltage-channels");
num_supply = fwnode_get_child_node_count(supply_node);
struct fwnode_handle *temp_node __free(fwnode_handle) =
device_get_named_child_node(dev, "temperature-channels");
num_temp = fwnode_get_child_node_count(temp_node);
num_static = ARRAY_SIZE(temp_channels);
num_chan = size_add(num_temp, size_add(num_static, num_supply));
sysmon_channels = devm_kcalloc(dev, num_chan, sizeof(*sysmon_channels), GFP_KERNEL);
if (!sysmon_channels)
return -ENOMEM;
/* Static temperature channels first */
memcpy(sysmon_channels, temp_channels, sizeof(temp_channels));
idx = num_static;
/* Supply channels from DT */
fwnode_for_each_child_node_scoped(supply_node, child) {
ret = fwnode_property_read_u32(child, "reg", &reg);
if (ret)
return dev_err_probe(dev, ret,
"missing reg for supply channel\n");
if (reg > SYSMON_SUPPLY_IDX_MAX)
return dev_err_probe(dev, -EINVAL,
"supply reg %u exceeds max %u\n",
reg, SYSMON_SUPPLY_IDX_MAX);
ret = fwnode_property_read_string(child, "label", &label);
if (ret)
return dev_err_probe(dev, ret,
"missing label for supply channel\n");
sysmon_channels[idx++] = (struct iio_chan_spec) {
.type = IIO_VOLTAGE,
.indexed = 1,
.address = reg,
.info_mask_separate = BIT(IIO_CHAN_INFO_PROCESSED),
.datasheet_name = label,
};
}
/* Temperature satellite channels from DT */
fwnode_for_each_child_node_scoped(temp_node, child) {
ret = fwnode_property_read_u32(child, "reg", &reg);
if (ret)
return dev_err_probe(dev, ret,
"missing reg for temp channel\n");
if (reg < 1 || reg > SYSMON_TEMP_SAT_MAX)
return dev_err_probe(dev, -EINVAL,
"temp reg %u out of range [1..%u]\n",
reg, SYSMON_TEMP_SAT_MAX);
ret = fwnode_property_read_string(child, "label", &label);
if (ret)
return dev_err_probe(dev, ret,
"missing label for temp channel\n");
sysmon_channels[idx++] = (struct iio_chan_spec) {
.type = IIO_TEMP,
.indexed = 1,
.address = SYSMON_TEMP_SAT_BASE +
(reg - 1) * SYSMON_REG_STRIDE,
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW),
.info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SCALE),
.datasheet_name = label,
};
}
indio_dev->num_channels = idx;
indio_dev->info = &sysmon_iio_info;
/*
* Assign per-type sequential channel numbers.
* IIO sysfs uses type prefix (in_tempN, in_voltageN)
* so numbers only need to be unique within each type.
*/
temp_chan_idx = 0;
volt_chan_idx = 0;
for (unsigned int idx = 0; idx < indio_dev->num_channels; idx++) {
if (sysmon_channels[idx].type == IIO_TEMP)
sysmon_channels[idx].channel = temp_chan_idx++;
else
sysmon_channels[idx].channel = volt_chan_idx++;
}
indio_dev->channels = sysmon_channels;
return 0;
}
/**
* devm_versal_sysmon_core_probe() - Initialize Versal SysMon core
* @dev: Parent device
* @regmap: Register map for hardware access
*
* Return: 0 on success, negative errno on failure.
*/
int devm_versal_sysmon_core_probe(struct device *dev, struct regmap *regmap)
{
struct iio_dev *indio_dev;
struct sysmon *sysmon;
int ret;
indio_dev = devm_iio_device_alloc(dev, sizeof(*sysmon));
if (!indio_dev)
return -ENOMEM;
sysmon = iio_priv(indio_dev);
sysmon->regmap = regmap;
ret = devm_mutex_init(dev, &sysmon->lock);
if (ret)
return ret;
/* Disable all interrupts and clear pending status */
ret = regmap_write(sysmon->regmap, SYSMON_IDR, SYSMON_INTR_ALL_MASK);
if (ret)
return ret;
ret = regmap_write(sysmon->regmap, SYSMON_ISR, SYSMON_INTR_ALL_MASK);
if (ret)
return ret;
indio_dev->name = "versal-sysmon";
indio_dev->modes = INDIO_DIRECT_MODE;
ret = sysmon_parse_fw(indio_dev, dev);
if (ret)
return ret;
return devm_iio_device_register(dev, indio_dev);
}
EXPORT_SYMBOL_NS_GPL(devm_versal_sysmon_core_probe, "VERSAL_SYSMON");
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("AMD Versal SysMon Core Driver");
MODULE_AUTHOR("Salih Erim <salih.erim@amd.com>");

View File

@ -0,0 +1,92 @@
// SPDX-License-Identifier: GPL-2.0
/*
* AMD Versal SysMon MMIO platform driver
*
* Copyright (C) 2019 - 2022, Xilinx, Inc.
* Copyright (C) 2022 - 2026, Advanced Micro Devices, Inc.
*/
#include <linux/err.h>
#include <linux/io.h>
#include <linux/mod_devicetable.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/regmap.h>
#include <linux/types.h>
#include "versal-sysmon.h"
struct sysmon_mmio {
void __iomem *base;
};
static int sysmon_mmio_reg_read(void *context, unsigned int reg, unsigned int *val)
{
struct sysmon_mmio *mmio = context;
*val = readl(mmio->base + reg);
return 0;
}
static int sysmon_mmio_reg_write(void *context, unsigned int reg, unsigned int val)
{
struct sysmon_mmio *mmio = context;
/* NPI must be unlocked before any register write except to NPI_LOCK */
if (reg != SYSMON_NPI_LOCK)
writel(SYSMON_NPI_UNLOCK_CODE, mmio->base + SYSMON_NPI_LOCK);
writel(val, mmio->base + reg);
return 0;
}
static const struct regmap_config sysmon_mmio_regmap_config = {
.reg_bits = 32,
.val_bits = 32,
.reg_stride = SYSMON_REG_STRIDE,
.max_register = SYSMON_MAX_REG,
.reg_read = sysmon_mmio_reg_read,
.reg_write = sysmon_mmio_reg_write,
.fast_io = true,
};
static int sysmon_platform_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct sysmon_mmio *mmio;
struct regmap *regmap;
mmio = devm_kzalloc(dev, sizeof(*mmio), GFP_KERNEL);
if (!mmio)
return -ENOMEM;
mmio->base = devm_platform_ioremap_resource(pdev, 0);
if (IS_ERR(mmio->base))
return PTR_ERR(mmio->base);
regmap = devm_regmap_init(dev, NULL, mmio, &sysmon_mmio_regmap_config);
if (IS_ERR(regmap))
return PTR_ERR(regmap);
return devm_versal_sysmon_core_probe(dev, regmap);
}
static const struct of_device_id sysmon_of_match_table[] = {
{ .compatible = "xlnx,versal-sysmon" },
{ }
};
MODULE_DEVICE_TABLE(of, sysmon_of_match_table);
static struct platform_driver sysmon_platform_driver = {
.probe = sysmon_platform_probe,
.driver = {
.name = "versal-sysmon",
.of_match_table = sysmon_of_match_table,
},
};
module_platform_driver(sysmon_platform_driver);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("AMD Versal SysMon Platform Driver");
MODULE_IMPORT_NS("VERSAL_SYSMON");
MODULE_AUTHOR("Salih Erim <salih.erim@amd.com>");

View File

@ -0,0 +1,67 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* AMD Versal SysMon driver
*
* Copyright (C) 2019 - 2022, Xilinx, Inc.
* Copyright (C) 2022 - 2026, Advanced Micro Devices, Inc.
*/
#ifndef _VERSAL_SYSMON_H_
#define _VERSAL_SYSMON_H_
#include <linux/bits.h>
#include <linux/mutex.h>
struct device;
struct regmap;
/* Register offsets (sorted by address) */
#define SYSMON_NPI_LOCK 0x000C
#define SYSMON_ISR 0x0044
#define SYSMON_IDR 0x0050
#define SYSMON_TEMP_MAX 0x1030
#define SYSMON_TEMP_MIN 0x1034
#define SYSMON_SUPPLY_BASE 0x1040
#define SYSMON_TEMP_MIN_MIN 0x1F8C
#define SYSMON_TEMP_MAX_MAX 0x1F90
#define SYSMON_TEMP_SAT_BASE 0x1FAC
#define SYSMON_MAX_REG 0x24C0
/* NPI unlock value written to SYSMON_NPI_LOCK */
#define SYSMON_NPI_UNLOCK_CODE 0xF9E8D7C6
/* Register stride: 4 bytes per 32-bit register */
#define SYSMON_REG_STRIDE 4
#define SYSMON_SUPPLY_IDX_MAX 159
#define SYSMON_TEMP_SAT_MAX 64
#define SYSMON_INTR_ALL_MASK GENMASK(31, 0)
/* Supply voltage conversion register fields */
#define SYSMON_MANTISSA_MASK GENMASK(15, 0)
#define SYSMON_FMT_MASK BIT(16)
#define SYSMON_MODE_MASK GENMASK(18, 17)
/* Q8.7 fractional shift */
#define SYSMON_FRACTIONAL_SHIFT 7U
#define SYSMON_SUPPLY_MANTISSA_BITS 16
/**
* struct sysmon - Driver data for Versal SysMon
* @regmap: register map for hardware access
* @lock: protects read-modify-write sequences on threshold registers
* and cached state that spans multiple regmap calls
*/
struct sysmon {
struct regmap *regmap;
/*
* Protects read-modify-write sequences on threshold registers
* and cached state (oversampling ratios, hysteresis values)
* that spans multiple regmap calls.
*/
struct mutex lock;
};
int devm_versal_sysmon_core_probe(struct device *dev, struct regmap *regmap);
#endif /* _VERSAL_SYSMON_H_ */