[PATCH v2 4/5] iio: adc: versal-sysmon: add threshold event support

Salih Erim
2026-05-02T11:20:25Z

Add threshold event support for temperature and supply voltage
channels.

Temperature events:
- Rising/falling threshold with configurable values
- Over-temperature (OT) alarm with separate thresholds
- Per-channel hysteresis configuration

Supply voltage events:
- Rising/falling threshold per supply channel
- Per-channel alarm enable via alarm configuration registers

The interrupt handler masks active threshold interrupts (which are
level-sensitive) and schedules a delayed worker to poll for condition
clear before unmasking. When no hardware IRQ is available (irq <= 0),
event channels are not created and interrupt init is skipped, since
the I2C regmap backend cannot be called from atomic context.

When disabling a supply channel alarm, the group interrupt remains
active if any other channel in the same alarm group still has an
alarm enabled.

Named constants replace magic numbers for hysteresis bit positions
(SYSMON_OT_HYST_BIT, SYSMON_TEMP_HYST_BIT) and alarm register width
(SYSMON_ALARM_BITS_PER_REG).

Hysteresis values are validated to single-bit range (0 or 1) before
writing to the hardware register.

Signed-off-by: Salih Erim <salih.erim@amd.com>
---
Changes in v2:
- Reverse Christmas Tree variable ordering in all functions
- Named constants for hysteresis bits: SYSMON_OT_HYST_BIT,
SYSMON_TEMP_HYST_BIT instead of magic 0x1/0x2
- SYSMON_ALARM_BITS_PER_REG replaces magic number 32
- SYSMON_ALARM_OFFSET() helper macro deduplicates alarm register
offset computation
- BIT() macro for shift expressions in conversion functions
- Hysteresis input validated to single-bit range (0 or 1)
- Event channels only created when irq > 0 (I2C safety)
- Group alarm interrupt stays active while any channel in the
group has an alarm enabled
- write_event_value returns -EINVAL for unhandled types
- IRQ_NONE returned for spurious interrupts
- Q8.7 write path uses multiplication instead of left-shift
to avoid undefined behavior with negative temperatures
- (u16) mask prevents garbage in reserved register bits
- regmap_write return values checked for IER/IDR writes
- devm cleanup ordering: cancel_work before request_irq
drivers/iio/adc/versal-sysmon-core.c | 539 ++++++++++++++++++++++++++-
drivers/iio/adc/versal-sysmon.h | 36 ++
2 files changed, 574 insertions(+), 1 deletion(-)

diff --git a/drivers/iio/adc/versal-sysmon-core.c b/drivers/iio/adc/versal-sysmon-core.c
index 37736c2900b..857fe21db7a 100644
--- a/drivers/iio/adc/versal-sysmon-core.c
+++ b/drivers/iio/adc/versal-sysmon-core.c
@@ -9,13 +9,24 @@
#include <linux/bitfield.h>
#include <linux/bits.h>
#include <linux/cleanup.h>
+#include <linux/iio/events.h>
#include <linux/iio/iio.h>
+#include <linux/interrupt.h>
+#include <linux/limits.h>
#include <linux/module.h>
#include <linux/property.h>
#include <linux/regmap.h>

#include "versal-sysmon.h"

+/* OT and TEMP hysteresis bit positions in SYSMON_TEMP_EV_CFG */
+#define SYSMON_OT_HYST_BIT BIT(0)
+#define SYSMON_TEMP_HYST_BIT BIT(1)
+
+/* Compute alarm register offset from a channel address */
+#define SYSMON_ALARM_OFFSET(addr) \
+ (SYSMON_ALARM_REG + ((addr) / SYSMON_ALARM_BITS_PER_REG) * SYSMON_REG_STRIDE)
+
#define SYSMON_CHAN_TEMP(_chan, _address, _ext) { \
.type = IIO_TEMP, \
.indexed = 1, \
@@ -32,6 +43,71 @@
.datasheet_name = _ext, \
}

+#define SYSMON_CHAN_TEMP_EVENT(_chan, _address, _ext, _events) { \
+ .type = IIO_TEMP, \
+ .indexed = 1, \
+ .address = _address, \
+ .channel = _chan, \
+ .event_spec = _events, \
+ .num_event_specs = ARRAY_SIZE(_events), \
+ .scan_type = { \
+ .sign = 's', \
+ .realbits = 15, \
+ .storagebits = 16, \
+ .endianness = IIO_CPU, \
+ }, \
+ .datasheet_name = _ext, \
+}
+
+enum sysmon_alarm_bit {
+ SYSMON_BIT_ALARM0 = 0,
+ SYSMON_BIT_ALARM1 = 1,
+ SYSMON_BIT_ALARM2 = 2,
+ SYSMON_BIT_ALARM3 = 3,
+ SYSMON_BIT_ALARM4 = 4,
+ SYSMON_BIT_OT = 8,
+ SYSMON_BIT_TEMP = 9,
+};
+
+/* Temperature event specifications */
+static const struct iio_event_spec sysmon_temp_events[] = {
+ {
+ .type = IIO_EV_TYPE_THRESH,
+ .dir = IIO_EV_DIR_RISING,
+ .mask_separate = BIT(IIO_EV_INFO_VALUE),
+ },
+ {
+ .type = IIO_EV_TYPE_THRESH,
+ .dir = IIO_EV_DIR_FALLING,
+ .mask_separate = BIT(IIO_EV_INFO_VALUE),
+ },
+ {
+ .type = IIO_EV_TYPE_THRESH,
+ .dir = IIO_EV_DIR_EITHER,
+ .mask_separate = BIT(IIO_EV_INFO_ENABLE) |
+ BIT(IIO_EV_INFO_HYSTERESIS),
+ },
+};
+
+/* Supply event specifications */
+static const struct iio_event_spec sysmon_supply_events[] = {
+ {
+ .type = IIO_EV_TYPE_THRESH,
+ .dir = IIO_EV_DIR_RISING,
+ .mask_separate = BIT(IIO_EV_INFO_VALUE),
+ },
+ {
+ .type = IIO_EV_TYPE_THRESH,
+ .dir = IIO_EV_DIR_FALLING,
+ .mask_separate = BIT(IIO_EV_INFO_VALUE),
+ },
+ {
+ .type = IIO_EV_TYPE_THRESH,
+ .dir = IIO_EV_DIR_EITHER,
+ .mask_separate = BIT(IIO_EV_INFO_ENABLE),
+ },
+};
+
/* Static temperature channels (always present) */
static const struct iio_chan_spec temp_channels[] = {
SYSMON_CHAN_TEMP(0, SYSMON_TEMP_MAX, "temp"),
@@ -40,11 +116,24 @@ static const struct iio_chan_spec temp_channels[] = {
SYSMON_CHAN_TEMP(3, SYSMON_TEMP_MIN_MIN, "min_min"),
};

+/* Temperature event channels (threshold alarms) */
+static const struct iio_chan_spec temp_event_channels[] = {
+ SYSMON_CHAN_TEMP_EVENT(4, SYSMON_ADDR_TEMP_EVENT, "temp",
+ sysmon_temp_events),
+ SYSMON_CHAN_TEMP_EVENT(5, SYSMON_ADDR_OT_EVENT, "ot",
+ sysmon_temp_events),
+};
+
static void sysmon_q8p7_to_millicelsius(int raw_data, int *val)
{
*val = ((s16)raw_data * SYSMON_MILLI) >> SYSMON_FRACTIONAL_SHIFT;
}

+static void sysmon_millicelsius_to_q8p7(u32 *raw_data, int val)
+{
+ *raw_data = (u16)((val * (int)BIT(SYSMON_FRACTIONAL_SHIFT)) / SYSMON_MILLI);
+}
+
static void sysmon_supply_rawtoprocessed(int raw_data, int *val)
{
int mantissa, format, exponent;
@@ -62,6 +151,49 @@ static void sysmon_supply_rawtoprocessed(int raw_data, int *val)
*val = (mantissa * SYSMON_MILLI) >> exponent;
}

+static void sysmon_supply_processedtoraw(int val, u32 reg_val, u32 *raw_data)
+{
+ int exponent = FIELD_GET(SYSMON_MODE_MASK, reg_val);
+ int format = FIELD_GET(SYSMON_FMT_MASK, reg_val);
+ int scale, tmp;
+
+ scale = BIT(SYSMON_SUPPLY_MANTISSA_BITS - exponent);
+ tmp = (val * scale) / SYSMON_MILLI;
+
+ if (format)
+ tmp = clamp(tmp, (int)S16_MIN, (int)S16_MAX);
+ else
+ tmp = clamp(tmp, 0, (int)U16_MAX);
+
+ *raw_data = tmp & U16_MAX;
+}
+
+static int sysmon_temp_thresh_offset(int address,
+ enum iio_event_direction dir)
+{
+ switch (address) {
+ case SYSMON_ADDR_TEMP_EVENT:
+ return (dir == IIO_EV_DIR_RISING) ? SYSMON_TEMP_TH_UP :
+ SYSMON_TEMP_TH_LOW;
+ case SYSMON_ADDR_OT_EVENT:
+ return (dir == IIO_EV_DIR_RISING) ? SYSMON_OT_TH_UP :
+ SYSMON_OT_TH_LOW;
+ default:
+ return -EINVAL;
+ }
+}
+
+static int sysmon_supply_thresh_offset(int address,
+ enum iio_event_direction dir)
+{
+ if (dir == IIO_EV_DIR_RISING)
+ return (address * SYSMON_REG_STRIDE) + SYSMON_SUPPLY_TH_UP;
+ if (dir == IIO_EV_DIR_FALLING)
+ return (address * SYSMON_REG_STRIDE) + SYSMON_SUPPLY_TH_LOW;
+
+ return -EINVAL;
+}
+
static int sysmon_read_raw(struct iio_dev *indio_dev,
struct iio_chan_spec const *chan, int *val,
int *val2, long mask)
@@ -103,6 +235,209 @@ static int sysmon_read_raw(struct iio_dev *indio_dev,
}
}

+static int sysmon_get_event_mask(unsigned long address)
+{
+ if (address == SYSMON_ADDR_TEMP_EVENT)
+ return BIT(SYSMON_BIT_TEMP);
+ if (address == SYSMON_ADDR_OT_EVENT)
+ return BIT(SYSMON_BIT_OT);
+
+ return BIT(address / SYSMON_ALARM_BITS_PER_REG);
+}
+
+static int sysmon_read_alarm_config(struct sysmon *sysmon,
+ unsigned long address)
+{
+ u32 shift = address % SYSMON_ALARM_BITS_PER_REG;
+ u32 offset = SYSMON_ALARM_OFFSET(address);
+ unsigned int reg_val;
+ int ret;
+
+ ret = regmap_read(sysmon->regmap, offset, &reg_val);
+ if (ret)
+ return ret;
+
+ return reg_val & BIT(shift);
+}
+
+static int sysmon_write_alarm_config(struct sysmon *sysmon,
+ unsigned long address, u32 val)
+{
+ u32 shift = address % SYSMON_ALARM_BITS_PER_REG;
+ u32 offset = SYSMON_ALARM_OFFSET(address);
+
+ return regmap_update_bits(sysmon->regmap, offset,
+ BIT(shift), val << shift);
+}
+
+static int sysmon_read_event_config(struct iio_dev *indio_dev,
+ const struct iio_chan_spec *chan,
+ enum iio_event_type type,
+ enum iio_event_direction dir)
+{
+ u32 alarm_event_mask = sysmon_get_event_mask(chan->address);
+ struct sysmon *sysmon = iio_priv(indio_dev);
+ unsigned int imr;
+ int config_value;
+ int ret;
+
+ ret = regmap_read(sysmon->regmap, SYSMON_IMR, &imr);
+ if (ret)
+ return ret;
+ imr = ~imr;
+
+ if (chan->type == IIO_VOLTAGE) {
+ config_value = sysmon_read_alarm_config(sysmon, chan->address);
+ if (config_value < 0)
+ return config_value;
+ return config_value && (imr & alarm_event_mask);
+ }
+
+ return !!(imr & alarm_event_mask);
+}
+
+static int sysmon_write_event_config(struct iio_dev *indio_dev,
+ const struct iio_chan_spec *chan,
+ enum iio_event_type type,
+ enum iio_event_direction dir, bool state)
+{
+ u32 offset = SYSMON_ALARM_OFFSET(chan->address);
+ u32 ier = sysmon_get_event_mask(chan->address);
+ struct sysmon *sysmon = iio_priv(indio_dev);
+ unsigned int alarm_config;
+ int ret;
+
+ guard(mutex)(&sysmon->lock);
+ guard(spinlock_irqsave)(&sysmon->irq_lock);
+
+ if (chan->type == IIO_VOLTAGE) {
+ ret = sysmon_write_alarm_config(sysmon, chan->address, state);
+ if (ret)
+ return ret;
+
+ ret = regmap_read(sysmon->regmap, offset, &alarm_config);
+ if (ret)
+ return ret;
+
+ if (alarm_config)
+ return regmap_write(sysmon->regmap, SYSMON_IER, ier);
+ else
+ return regmap_write(sysmon->regmap, SYSMON_IDR, ier);
+ } else if (chan->type == IIO_TEMP) {
+ if (state) {
+ ret = regmap_write(sysmon->regmap, SYSMON_IER, ier);
+ if (ret)
+ return ret;
+ sysmon->temp_mask &= ~ier;
+ } else {
+ ret = regmap_write(sysmon->regmap, SYSMON_IDR, ier);
+ if (ret)
+ return ret;
+ sysmon->temp_mask |= ier;
+ }
+ }
+
+ return 0;
+}
+
+static int sysmon_read_event_value(struct iio_dev *indio_dev,
+ const struct iio_chan_spec *chan,
+ enum iio_event_type type,
+ enum iio_event_direction dir,
+ enum iio_event_info info, int *val,
+ int *val2)
+{
+ struct sysmon *sysmon = iio_priv(indio_dev);
+ unsigned int reg_val;
+ u32 mask, shift;
+ int offset;
+ int ret;
+
+ guard(mutex)(&sysmon->lock);
+
+ if (chan->type == IIO_TEMP) {
+ if (info == IIO_EV_INFO_VALUE) {
+ offset = sysmon_temp_thresh_offset(chan->address, dir);
+ if (offset < 0)
+ return offset;
+ ret = regmap_read(sysmon->regmap, offset, &reg_val);
+ if (ret)
+ return ret;
+ sysmon_q8p7_to_millicelsius(reg_val, val);
+ return IIO_VAL_INT;
+ }
+ if (info == IIO_EV_INFO_HYSTERESIS) {
+ mask = (chan->address == SYSMON_ADDR_OT_EVENT) ?
+ SYSMON_OT_HYST_BIT : SYSMON_TEMP_HYST_BIT;
+ shift = (chan->address == SYSMON_ADDR_OT_EVENT) ? 0 : 1;
+ ret = regmap_read(sysmon->regmap, SYSMON_TEMP_EV_CFG,
+ &reg_val);
+ if (ret)
+ return ret;
+ *val = (reg_val & mask) >> shift;
+ return IIO_VAL_INT;
+ }
+ } else if (chan->type == IIO_VOLTAGE) {
+ offset = sysmon_supply_thresh_offset(chan->address, dir);
+ if (offset < 0)
+ return offset;
+ ret = regmap_read(sysmon->regmap, offset, &reg_val);
+ if (ret)
+ return ret;
+ sysmon_supply_rawtoprocessed(reg_val, val);
+ return IIO_VAL_INT;
+ }
+
+ return -EINVAL;
+}
+
+static int sysmon_write_event_value(struct iio_dev *indio_dev,
+ const struct iio_chan_spec *chan,
+ enum iio_event_type type,
+ enum iio_event_direction dir,
+ enum iio_event_info info, int val, int val2)
+{
+ struct sysmon *sysmon = iio_priv(indio_dev);
+ unsigned int reg_val;
+ u32 mask, shift;
+ u32 raw_val;
+ int offset;
+ int ret;
+
+ guard(mutex)(&sysmon->lock);
+
+ if (chan->type == IIO_TEMP) {
+ if (info == IIO_EV_INFO_VALUE) {
+ offset = sysmon_temp_thresh_offset(chan->address, dir);
+ if (offset < 0)
+ return offset;
+ sysmon_millicelsius_to_q8p7(&raw_val, val);
+ return regmap_write(sysmon->regmap, offset, raw_val);
+ }
+ if (info == IIO_EV_INFO_HYSTERESIS) {
+ mask = (chan->address == SYSMON_ADDR_OT_EVENT) ?
+ SYSMON_OT_HYST_BIT : SYSMON_TEMP_HYST_BIT;
+ shift = (chan->address == SYSMON_ADDR_OT_EVENT) ? 0 : 1;
+ if (val & ~1)
+ return -EINVAL;
+ return regmap_update_bits(sysmon->regmap,
+ SYSMON_TEMP_EV_CFG,
+ mask, val << shift);
+ }
+ } else if (chan->type == IIO_VOLTAGE) {
+ offset = sysmon_supply_thresh_offset(chan->address, dir);
+ if (offset < 0)
+ return offset;
+ ret = regmap_read(sysmon->regmap, offset, &reg_val);
+ if (ret)
+ return ret;
+ sysmon_supply_processedtoraw(val, reg_val, &raw_val);
+ return regmap_write(sysmon->regmap, offset, raw_val);
+ }
+
+ return -EINVAL;
+}
+
static int sysmon_read_label(struct iio_dev *indio_dev,
struct iio_chan_spec const *chan,
char *label)
@@ -116,8 +451,182 @@ static int sysmon_read_label(struct iio_dev *indio_dev,
static const struct iio_info sysmon_iio_info = {
.read_raw = sysmon_read_raw,
.read_label = sysmon_read_label,
+ .read_event_config = sysmon_read_event_config,
+ .write_event_config = sysmon_write_event_config,
+ .read_event_value = sysmon_read_event_value,
+ .write_event_value = sysmon_write_event_value,
};

+static void sysmon_push_event(struct iio_dev *indio_dev, u32 address)
+{
+ const struct iio_chan_spec *chan;
+ unsigned int i;
+
+ for (i = 0; i < indio_dev->num_channels; i++) {
+ if (indio_dev->channels[i].address != address)
+ continue;
+
+ chan = &indio_dev->channels[i];
+ iio_push_event(indio_dev,
+ IIO_UNMOD_EVENT_CODE(chan->type,
+ chan->channel,
+ IIO_EV_TYPE_THRESH,
+ IIO_EV_DIR_EITHER),
+ iio_get_time_ns(indio_dev));
+ }
+}
+
+static void sysmon_handle_event(struct iio_dev *indio_dev, u32 event)
+{
+ u32 alarm_flag_offset = SYSMON_ALARM_FLAG + (event * SYSMON_REG_STRIDE);
+ u32 alarm_reg_offset = SYSMON_ALARM_REG + (event * SYSMON_REG_STRIDE);
+ struct sysmon *sysmon = iio_priv(indio_dev);
+ unsigned long alarm_flag_reg;
+ unsigned int reg_val;
+ u32 address, bit;
+
+ switch (event) {
+ case SYSMON_BIT_TEMP:
+ sysmon_push_event(indio_dev, SYSMON_ADDR_TEMP_EVENT);
+ regmap_write(sysmon->regmap, SYSMON_IDR, BIT(SYSMON_BIT_TEMP));
+ sysmon->masked_temp |= BIT(SYSMON_BIT_TEMP);
+ break;
+
+ case SYSMON_BIT_OT:
+ sysmon_push_event(indio_dev, SYSMON_ADDR_OT_EVENT);
+ regmap_write(sysmon->regmap, SYSMON_IDR, BIT(SYSMON_BIT_OT));
+ sysmon->masked_temp |= BIT(SYSMON_BIT_OT);
+ break;
+
+ case SYSMON_BIT_ALARM0:
+ case SYSMON_BIT_ALARM1:
+ case SYSMON_BIT_ALARM2:
+ case SYSMON_BIT_ALARM3:
+ case SYSMON_BIT_ALARM4:
+ regmap_read(sysmon->regmap, alarm_flag_offset, &reg_val);
+ alarm_flag_reg = (unsigned long)reg_val;
+
+ for_each_set_bit(bit, &alarm_flag_reg,
+ SYSMON_ALARM_BITS_PER_REG) {
+ address = bit + (SYSMON_ALARM_BITS_PER_REG * event);
+ sysmon_push_event(indio_dev, address);
+ regmap_update_bits(sysmon->regmap, alarm_reg_offset,
+ BIT(bit), 0);
+ }
+ regmap_write(sysmon->regmap, alarm_flag_offset, alarm_flag_reg);
+ break;
+
+ default:
+ break;
+ }
+}
+
+static void sysmon_handle_events(struct iio_dev *indio_dev,
+ unsigned long events)
+{
+ unsigned int bit;
+
+ for_each_set_bit(bit, &events, SYSMON_NO_OF_EVENTS)
+ sysmon_handle_event(indio_dev, bit);
+}
+
+static void sysmon_unmask_temp(struct sysmon *sysmon, unsigned int isr)
+{
+ unsigned int unmask, status;
+
+ status = isr & SYSMON_TEMP_INTR_MASK;
+
+ unmask = (sysmon->masked_temp ^ status) & sysmon->masked_temp;
+ sysmon->masked_temp &= status;
+
+ unmask &= ~sysmon->temp_mask;
+
+ regmap_write(sysmon->regmap, SYSMON_IER, unmask);
+}
+
+/*
+ * Versal threshold interrupts are level-sensitive. Active threshold
+ * interrupts are masked in the handler and polled via delayed work
+ * until the condition clears, then unmasked.
+ */
+static void sysmon_unmask_worker(struct work_struct *work)
+{
+ struct sysmon *sysmon = container_of(work, struct sysmon,
+ sysmon_unmask_work.work);
+ unsigned int isr;
+
+ spin_lock_irq(&sysmon->irq_lock);
+ regmap_read(sysmon->regmap, SYSMON_ISR, &isr);
+ regmap_write(sysmon->regmap, SYSMON_ISR, isr);
+ sysmon_unmask_temp(sysmon, isr);
+ spin_unlock_irq(&sysmon->irq_lock);
+
+ if (sysmon->masked_temp)
+ schedule_delayed_work(&sysmon->sysmon_unmask_work,
+ msecs_to_jiffies(SYSMON_UNMASK_WORK_DELAY_MS));
+ else
+ regmap_write(sysmon->regmap, SYSMON_STATUS_RESET, 1);
+}
+
+static irqreturn_t sysmon_iio_irq(int irq, void *data)
+{
+ struct iio_dev *indio_dev = data;
+ struct sysmon *sysmon;
+ unsigned int isr, imr;
+
+ sysmon = iio_priv(indio_dev);
+ spin_lock(&sysmon->irq_lock);
+
+ regmap_read(sysmon->regmap, SYSMON_ISR, &isr);
+ regmap_read(sysmon->regmap, SYSMON_IMR, &imr);
+
+ isr &= ~imr;
+ regmap_write(sysmon->regmap, SYSMON_ISR, isr);
+
+ if (isr) {
+ sysmon_handle_events(indio_dev, isr);
+ schedule_delayed_work(&sysmon->sysmon_unmask_work,
+ msecs_to_jiffies(SYSMON_UNMASK_WORK_DELAY_MS));
+ }
+
+ spin_unlock(&sysmon->irq_lock);
+
+ return isr ? IRQ_HANDLED : IRQ_NONE;
+}
+
+static void sysmon_cancel_work(void *data)
+{
+ struct sysmon *sysmon = data;
+
+ cancel_delayed_work_sync(&sysmon->sysmon_unmask_work);
+}
+
+static int sysmon_init_interrupt(struct sysmon *sysmon)
+{
+ unsigned int imr;
+ int ret;
+
+ /* Events not supported without IRQ (e.g. I2C path) */
+ if (sysmon->irq <= 0)
+ return 0;
+
+ INIT_DELAYED_WORK(&sysmon->sysmon_unmask_work, sysmon_unmask_worker);
+
+ ret = regmap_read(sysmon->regmap, SYSMON_IMR, &imr);
+ if (ret)
+ return ret;
+ sysmon->temp_mask = imr & SYSMON_TEMP_INTR_MASK;
+
+ ret = devm_add_action_or_reset(sysmon->dev, sysmon_cancel_work,
+ sysmon);
+ if (ret)
+ return ret;
+
+ return devm_request_irq(sysmon->dev, sysmon->irq,
+ sysmon_iio_irq, 0, "sysmon-irq",
+ sysmon->indio_dev);
+}
+
/**
* sysmon_parse_fw() - Parse firmware nodes and configure IIO channels.
* @indio_dev: IIO device instance
@@ -125,7 +634,13 @@ static const struct iio_info sysmon_iio_info = {
*
* Reads supply-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.
+ * and event channels are prepended, followed by supply and satellite
+ * channels from DT.
+ *
+ * Event channels and per-channel event specs are only added when the
+ * device has an IRQ (irq > 0). I2C devices have no interrupt line,
+ * and the I2C regmap cannot be called from atomic context, so events
+ * are not supported on that path.
*
* Return: 0 on success, negative errno on failure.
*/
@@ -133,8 +648,11 @@ static int sysmon_parse_fw(struct iio_dev *indio_dev, struct device *dev)
{
unsigned int idx, temp_chan_idx, volt_chan_idx;
struct fwnode_handle *supply_node, *temp_node;
+ struct sysmon *sysmon = iio_priv(indio_dev);
unsigned int num_supply = 0, num_temp = 0;
struct iio_chan_spec *sysmon_channels;
+ bool has_events = sysmon->irq > 0;
+ unsigned int num_events;
const char *label;
u32 reg;
int ret;
@@ -147,8 +665,11 @@ static int sysmon_parse_fw(struct iio_dev *indio_dev, struct device *dev)
if (temp_node)
num_temp = fwnode_get_child_node_count(temp_node);

+ num_events = has_events ? ARRAY_SIZE(temp_event_channels) : 0;
+
sysmon_channels = devm_kcalloc(dev,
ARRAY_SIZE(temp_channels) +
+ num_events +
num_supply + num_temp,
sizeof(*sysmon_channels), GFP_KERNEL);
if (!sysmon_channels) {
@@ -161,6 +682,13 @@ static int sysmon_parse_fw(struct iio_dev *indio_dev, struct device *dev)
memcpy(sysmon_channels, temp_channels, sizeof(temp_channels));
idx += ARRAY_SIZE(temp_channels);

+ /* Temperature event channels (only when IRQ is available) */
+ if (has_events) {
+ memcpy(sysmon_channels + idx, temp_event_channels,
+ sizeof(temp_event_channels));
+ idx += ARRAY_SIZE(temp_event_channels);
+ }
+
/* Supply channels from DT */
if (supply_node) {
fwnode_for_each_child_node_scoped(supply_node, child) {
@@ -187,6 +715,10 @@ static int sysmon_parse_fw(struct iio_dev *indio_dev, struct device *dev)
.info_mask_separate =
BIT(IIO_CHAN_INFO_RAW) |
BIT(IIO_CHAN_INFO_PROCESSED),
+ .event_spec = has_events ?
+ sysmon_supply_events : NULL,
+ .num_event_specs = has_events ?
+ ARRAY_SIZE(sysmon_supply_events) : 0,
.scan_type = {
.realbits = 19,
.storagebits = 32,
@@ -295,6 +827,7 @@ int sysmon_core_probe(struct device *dev, struct regmap *regmap, int irq)
ret = devm_mutex_init(dev, &sysmon->lock);
if (ret)
return ret;
+ spin_lock_init(&sysmon->irq_lock);

/* Disable all interrupts and clear pending status */
ret = regmap_write(sysmon->regmap, SYSMON_IDR, SYSMON_INTR_ALL_MASK);
@@ -311,6 +844,10 @@ int sysmon_core_probe(struct device *dev, struct regmap *regmap, int irq)
if (ret)
return ret;

+ ret = sysmon_init_interrupt(sysmon);
+ if (ret)
+ return ret;
+
return devm_iio_device_register(dev, indio_dev);
}
EXPORT_SYMBOL_GPL(sysmon_core_probe);
diff --git a/drivers/iio/adc/versal-sysmon.h b/drivers/iio/adc/versal-sysmon.h
index fc4d2338328..4f20173de77 100644
--- a/drivers/iio/adc/versal-sysmon.h
+++ b/drivers/iio/adc/versal-sysmon.h
@@ -12,16 +12,30 @@
#include <linux/iio/iio.h>
#include <linux/mutex.h>
#include <linux/regmap.h>
+#include <linux/spinlock.h>
+#include <linux/workqueue.h>

/* Register offsets (sorted by address) */
#define SYSMON_NPI_LOCK 0x000C
#define SYSMON_ISR 0x0044
+#define SYSMON_IMR 0x0048
+#define SYSMON_IER 0x004C
#define SYSMON_IDR 0x0050
+#define SYSMON_ALARM_FLAG 0x1018
#define SYSMON_TEMP_MAX 0x1030
#define SYSMON_TEMP_MIN 0x1034
#define SYSMON_SUPPLY_BASE 0x1040
+#define SYSMON_ALARM_REG 0x1940
+#define SYSMON_TEMP_TH_LOW 0x1970
+#define SYSMON_TEMP_TH_UP 0x1974
+#define SYSMON_OT_TH_LOW 0x1978
+#define SYSMON_OT_TH_UP 0x197C
+#define SYSMON_SUPPLY_TH_LOW 0x1980
+#define SYSMON_SUPPLY_TH_UP 0x1C80
+#define SYSMON_TEMP_EV_CFG 0x1F84
#define SYSMON_TEMP_MIN_MIN 0x1F8C
#define SYSMON_TEMP_MAX_MAX 0x1F90
+#define SYSMON_STATUS_RESET 0x1F94
#define SYSMON_TEMP_SAT_BASE 0x1FAC
#define SYSMON_MAX_REG 0x24C0

@@ -33,8 +47,12 @@

#define SYSMON_SUPPLY_IDX_MAX 159
#define SYSMON_TEMP_SAT_MAX 64
+#define SYSMON_NO_OF_EVENTS 32
#define SYSMON_INTR_ALL_MASK GENMASK(31, 0)

+/* ISR/IMR temperature and OT alarm mask (bits 9:8) */
+#define SYSMON_TEMP_INTR_MASK GENMASK(9, 8)
+
/* Supply voltage conversion register fields */
#define SYSMON_MANTISSA_MASK GENMASK(15, 0)
#define SYSMON_FMT_MASK BIT(16)
@@ -47,13 +65,26 @@
/* Signed milli scale (MILLI from linux/units.h is unsigned long) */
#define SYSMON_MILLI 1000

+/* Event address IDs for temp event channels */
+#define SYSMON_ADDR_TEMP_EVENT 160
+#define SYSMON_ADDR_OT_EVENT 161
+
+/* Bits per alarm register */
+#define SYSMON_ALARM_BITS_PER_REG 32
+
+#define SYSMON_UNMASK_WORK_DELAY_MS 500
+
/**
* struct sysmon - Driver data for Versal SysMon
* @dev: pointer to device struct
* @indio_dev: pointer to the iio device (needed for work callbacks)
* @regmap: register map for hardware access
* @lock: mutex for serializing user-space access
+ * @irq_lock: spinlock for interrupt register access
* @irq: interrupt number
+ * @masked_temp: currently masked temperature alarm bits
+ * @temp_mask: temperature interrupt configuration mask
+ * @sysmon_unmask_work: re-enables events after alarm condition clears
*/
struct sysmon {
struct device *dev;
@@ -61,7 +92,12 @@ struct sysmon {
struct regmap *regmap;
/* Serializes access to device registers and state */
struct mutex lock;
+ /* Protects interrupt mask register updates */
+ spinlock_t irq_lock;
int irq;
+ unsigned int masked_temp;
+ unsigned int temp_mask;
+ struct delayed_work sysmon_unmask_work;
};

int sysmon_core_probe(struct device *dev, struct regmap *regmap, int irq);
--
2.48.1