mirror of
https://github.com/torvalds/linux.git
synced 2026-09-24 06:24:02 +02:00
Add support for reading and writing the oversampling ratio through the IIO oversampling_ratio attribute. The hardware supports averaging 2, 4, 8, or 16 samples, plus a ratio of 1 (no averaging). Temperature and supply channels share oversampling configuration at the type level (all temperature channels share one ratio, all supply channels share another), exposed through info_mask_shared_by_type. The hardware encoding uses sample_count / 2 in a 4-bit field within the CONFIG register. Per-channel averaging enable registers must also be updated to activate or deactivate averaging. 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>
1053 lines
27 KiB
C
1053 lines
27 KiB
C
// 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/interrupt.h>
|
|
#include <linux/limits.h>
|
|
#include <linux/minmax.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/events.h>
|
|
#include <linux/iio/iio.h>
|
|
|
|
#include "versal-sysmon.h"
|
|
|
|
/*
|
|
* Oversampling ratio values exposed to userspace via IIO.
|
|
* Actual number of samples averaged: 1=none, 2=2x, 4=4x, 8=8x, 16=16x.
|
|
*/
|
|
static const int sysmon_oversampling_avail[] = { 1, 2, 4, 8, 16 };
|
|
|
|
/* TEMP hysteresis mode bit in SYSMON_TEMP_EV_CFG */
|
|
#define SYSMON_TEMP_HYST_MASK 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, _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) | \
|
|
BIT(IIO_CHAN_INFO_OVERSAMPLING_RATIO), \
|
|
.info_mask_shared_by_type_available = \
|
|
BIT(IIO_CHAN_INFO_OVERSAMPLING_RATIO), \
|
|
.datasheet_name = _name, \
|
|
}
|
|
|
|
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_TEMP = 9,
|
|
};
|
|
|
|
/* Temperature event specification: rising threshold + hysteresis only */
|
|
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_ENABLE) |
|
|
BIT(IIO_EV_INFO_VALUE) |
|
|
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).
|
|
*
|
|
* 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_q8p7_to_millicelsius(s16 raw_data, int *val)
|
|
{
|
|
*val = (raw_data * MILLIDEGREE_PER_DEGREE) >> SYSMON_FRACTIONAL_SHIFT;
|
|
}
|
|
|
|
static void sysmon_millicelsius_to_q8p7(u32 *raw_data, int val)
|
|
{
|
|
*raw_data = (val << SYSMON_FRACTIONAL_SHIFT) / MILLIDEGREE_PER_DEGREE;
|
|
}
|
|
|
|
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 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) / (int)MILLI;
|
|
|
|
if (format)
|
|
tmp = clamp(tmp, S16_MIN, S16_MAX);
|
|
else
|
|
tmp = clamp(tmp, 0, U16_MAX);
|
|
|
|
*raw_data = (u16)tmp;
|
|
}
|
|
|
|
static int sysmon_supply_thresh_offset(unsigned long 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)
|
|
{
|
|
struct sysmon *sysmon = iio_priv(indio_dev);
|
|
unsigned int regval;
|
|
int ret;
|
|
|
|
guard(mutex)(&sysmon->lock);
|
|
|
|
if (mask == IIO_CHAN_INFO_OVERSAMPLING_RATIO) {
|
|
*val = (chan->type == IIO_TEMP) ? sysmon->temp_oversampling :
|
|
sysmon->supply_oversampling;
|
|
return IIO_VAL_INT;
|
|
}
|
|
|
|
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, ®val);
|
|
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, ®val);
|
|
if (ret)
|
|
return ret;
|
|
|
|
sysmon_supply_rawtoprocessed(regval, val);
|
|
return IIO_VAL_INT;
|
|
|
|
default:
|
|
return -EINVAL;
|
|
}
|
|
}
|
|
|
|
static u32 sysmon_get_event_mask(const struct iio_chan_spec *chan)
|
|
{
|
|
if (chan->type == IIO_TEMP)
|
|
return BIT(SYSMON_BIT_TEMP);
|
|
|
|
return BIT(chan->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);
|
|
|
|
return regmap_test_bits(sysmon->regmap, offset, BIT(shift));
|
|
}
|
|
|
|
static int sysmon_write_alarm_config(struct sysmon *sysmon,
|
|
unsigned long address, bool enable)
|
|
{
|
|
u32 shift = address % SYSMON_ALARM_BITS_PER_REG;
|
|
u32 offset = SYSMON_ALARM_OFFSET(address);
|
|
|
|
return regmap_assign_bits(sysmon->regmap, offset, BIT(shift), enable);
|
|
}
|
|
|
|
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)
|
|
{
|
|
struct sysmon *sysmon = iio_priv(indio_dev);
|
|
u32 mask = sysmon_get_event_mask(chan);
|
|
unsigned int imr;
|
|
int config_value;
|
|
int ret;
|
|
|
|
ret = regmap_read(sysmon->regmap, SYSMON_IMR, &imr);
|
|
if (ret)
|
|
return ret;
|
|
|
|
/* IMR bits are 1=masked, invert to get 1=enabled */
|
|
imr = ~imr;
|
|
|
|
switch (chan->type) {
|
|
case IIO_VOLTAGE:
|
|
config_value = sysmon_read_alarm_config(sysmon, chan->address);
|
|
if (config_value < 0)
|
|
return config_value;
|
|
return config_value && (imr & mask);
|
|
|
|
case IIO_TEMP:
|
|
/*
|
|
* Return the administrative state, not the hardware IMR.
|
|
* The IRQ handler temporarily masks the interrupt during
|
|
* the polling window; reading IMR would show it as disabled.
|
|
* temp_mask bit is set when administratively disabled.
|
|
*/
|
|
return !(sysmon->temp_mask & mask);
|
|
|
|
default:
|
|
return -EINVAL;
|
|
}
|
|
}
|
|
|
|
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);
|
|
struct sysmon *sysmon = iio_priv(indio_dev);
|
|
u32 mask = sysmon_get_event_mask(chan);
|
|
unsigned int alarm_config;
|
|
int ret;
|
|
|
|
guard(mutex)(&sysmon->lock);
|
|
|
|
switch (chan->type) {
|
|
case 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, mask);
|
|
|
|
return regmap_write(sysmon->regmap, SYSMON_IDR, mask);
|
|
|
|
case IIO_TEMP:
|
|
if (state) {
|
|
ret = regmap_write(sysmon->regmap, SYSMON_IER, mask);
|
|
if (ret)
|
|
return ret;
|
|
|
|
scoped_guard(spinlock_irq, &sysmon->irq_lock)
|
|
sysmon->temp_mask &= ~mask;
|
|
} else {
|
|
ret = regmap_write(sysmon->regmap, SYSMON_IDR, mask);
|
|
if (ret)
|
|
return ret;
|
|
|
|
scoped_guard(spinlock_irq, &sysmon->irq_lock)
|
|
sysmon->temp_mask |= mask;
|
|
}
|
|
return 0;
|
|
|
|
default:
|
|
return -EINVAL;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Recompute the lower threshold register from upper threshold and
|
|
* cached hysteresis. Called when either upper threshold or hysteresis
|
|
* is written.
|
|
*/
|
|
static int sysmon_update_temp_lower(struct sysmon *sysmon)
|
|
{
|
|
unsigned int upper_reg;
|
|
int upper_mc, lower_mc;
|
|
u32 raw_val;
|
|
int ret;
|
|
|
|
ret = regmap_read(sysmon->regmap, SYSMON_TEMP_TH_UP, &upper_reg);
|
|
if (ret)
|
|
return ret;
|
|
|
|
sysmon_q8p7_to_millicelsius(upper_reg, &upper_mc);
|
|
lower_mc = clamp(upper_mc - sysmon->temp_hysteresis, -256000, 255992);
|
|
sysmon_millicelsius_to_q8p7(&raw_val, lower_mc);
|
|
|
|
return regmap_write(sysmon->regmap, SYSMON_TEMP_TH_LOW, raw_val);
|
|
}
|
|
|
|
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;
|
|
int offset;
|
|
int ret;
|
|
|
|
guard(mutex)(&sysmon->lock);
|
|
|
|
switch (chan->type) {
|
|
case IIO_TEMP:
|
|
switch (info) {
|
|
case IIO_EV_INFO_VALUE:
|
|
ret = regmap_read(sysmon->regmap, SYSMON_TEMP_TH_UP, ®_val);
|
|
if (ret)
|
|
return ret;
|
|
|
|
sysmon_q8p7_to_millicelsius(reg_val, val);
|
|
|
|
return IIO_VAL_INT;
|
|
|
|
case IIO_EV_INFO_HYSTERESIS:
|
|
*val = sysmon->temp_hysteresis;
|
|
return IIO_VAL_INT;
|
|
|
|
default:
|
|
return -EINVAL;
|
|
}
|
|
|
|
case IIO_VOLTAGE:
|
|
offset = sysmon_supply_thresh_offset(chan->address, dir);
|
|
if (offset < 0)
|
|
return offset;
|
|
|
|
ret = regmap_read(sysmon->regmap, offset, ®_val);
|
|
if (ret)
|
|
return ret;
|
|
|
|
sysmon_supply_rawtoprocessed(reg_val, val);
|
|
|
|
return IIO_VAL_INT;
|
|
|
|
default:
|
|
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 raw_val;
|
|
int offset;
|
|
int ret;
|
|
|
|
guard(mutex)(&sysmon->lock);
|
|
|
|
switch (chan->type) {
|
|
case IIO_TEMP:
|
|
switch (info) {
|
|
case IIO_EV_INFO_VALUE:
|
|
/* Q8.7 signed range: -256000 to +255992 mC */
|
|
if (val < -256000 || val > 255992)
|
|
return -EINVAL;
|
|
|
|
sysmon_millicelsius_to_q8p7(&raw_val, val);
|
|
|
|
ret = regmap_write(sysmon->regmap, SYSMON_TEMP_TH_UP, raw_val);
|
|
if (ret)
|
|
return ret;
|
|
|
|
/* Recompute lower = upper - hysteresis */
|
|
return sysmon_update_temp_lower(sysmon);
|
|
|
|
case IIO_EV_INFO_HYSTERESIS:
|
|
if (val < 0)
|
|
return -EINVAL;
|
|
|
|
sysmon->temp_hysteresis = val;
|
|
|
|
return sysmon_update_temp_lower(sysmon);
|
|
|
|
default:
|
|
return -EINVAL;
|
|
}
|
|
|
|
case IIO_VOLTAGE:
|
|
offset = sysmon_supply_thresh_offset(chan->address, dir);
|
|
if (offset < 0)
|
|
return offset;
|
|
|
|
ret = regmap_read(sysmon->regmap, offset, ®_val);
|
|
if (ret)
|
|
return ret;
|
|
|
|
/* Clamp to prevent overflow in processedtoraw conversion */
|
|
if (val < -32768 || val > 32767)
|
|
return -EINVAL;
|
|
|
|
sysmon_supply_processedtoraw(val, reg_val, &raw_val);
|
|
|
|
/*
|
|
* The hardware threshold register returns FMT and MODE
|
|
* bits in the upper 16 bits on read, but only the lower
|
|
* 16-bit mantissa is used on write.
|
|
*/
|
|
return regmap_write(sysmon->regmap, offset, raw_val);
|
|
|
|
default:
|
|
return -EINVAL;
|
|
}
|
|
}
|
|
|
|
static int sysmon_set_avg_enable(struct sysmon *sysmon,
|
|
u32 base, u32 count, u32 val)
|
|
{
|
|
struct regmap *map = sysmon->regmap;
|
|
int ret;
|
|
|
|
for (unsigned int i = 0; i < count; i++) {
|
|
ret = regmap_write(map, base + i * SYSMON_REG_STRIDE, val);
|
|
if (ret)
|
|
return ret;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int sysmon_osr_write_temp(struct sysmon *sysmon, unsigned int val)
|
|
{
|
|
/*
|
|
* HW register encoding is sample_count / 2:
|
|
* 0=none, 1=2x, 2=4x, 4=8x, 8=16x (not log2-based).
|
|
*/
|
|
unsigned int hw_val = val >> 1;
|
|
unsigned int readback;
|
|
int ret;
|
|
|
|
ret = regmap_update_bits(sysmon->regmap, SYSMON_CONFIG,
|
|
SYSMON_CONFIG_TEMP_SAT_OSR,
|
|
FIELD_PREP(SYSMON_CONFIG_TEMP_SAT_OSR, hw_val));
|
|
if (ret)
|
|
return ret;
|
|
|
|
/*
|
|
* Readback fence: the SysMon CONFIG register resides in the
|
|
* PMC domain behind the NoC. A posted write may not reach the
|
|
* hardware before the next MMIO access. Reading the register
|
|
* back forces the interconnect to complete the write, preventing
|
|
* a bus hang on the subsequent access.
|
|
*/
|
|
regmap_read(sysmon->regmap, SYSMON_CONFIG, &readback);
|
|
|
|
return sysmon_set_avg_enable(sysmon, SYSMON_TEMP_EN_AVG_BASE,
|
|
SYSMON_TEMP_EN_AVG_COUNT,
|
|
hw_val ? ~0 : 0);
|
|
}
|
|
|
|
static int sysmon_osr_write_supply(struct sysmon *sysmon, unsigned int val)
|
|
{
|
|
/* HW encoding: sample_count / 2 (see sysmon_osr_write_temp) */
|
|
unsigned int hw_val = val >> 1;
|
|
unsigned int readback;
|
|
int ret;
|
|
|
|
ret = regmap_update_bits(sysmon->regmap, SYSMON_CONFIG,
|
|
SYSMON_CONFIG_SUPPLY_OSR,
|
|
FIELD_PREP(SYSMON_CONFIG_SUPPLY_OSR, hw_val));
|
|
if (ret)
|
|
return ret;
|
|
|
|
/* Readback fence -- see sysmon_osr_write_temp for details */
|
|
regmap_read(sysmon->regmap, SYSMON_CONFIG, &readback);
|
|
|
|
return sysmon_set_avg_enable(sysmon, SYSMON_SUPPLY_EN_AVG_BASE,
|
|
SYSMON_SUPPLY_EN_AVG_COUNT,
|
|
hw_val ? ~0 : 0);
|
|
}
|
|
|
|
static int sysmon_write_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 i;
|
|
int ret;
|
|
|
|
if (mask != IIO_CHAN_INFO_OVERSAMPLING_RATIO)
|
|
return -EINVAL;
|
|
|
|
for (i = 0; i < ARRAY_SIZE(sysmon_oversampling_avail); i++) {
|
|
if (val == sysmon_oversampling_avail[i])
|
|
break;
|
|
}
|
|
if (i == ARRAY_SIZE(sysmon_oversampling_avail))
|
|
return -EINVAL;
|
|
|
|
guard(mutex)(&sysmon->lock);
|
|
|
|
if (chan->type == IIO_TEMP) {
|
|
ret = sysmon_osr_write_temp(sysmon, val);
|
|
if (ret)
|
|
return ret;
|
|
sysmon->temp_oversampling = val;
|
|
} else {
|
|
ret = sysmon_osr_write_supply(sysmon, val);
|
|
if (ret)
|
|
return ret;
|
|
sysmon->supply_oversampling = val;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int sysmon_write_raw_get_fmt(struct iio_dev *indio_dev,
|
|
struct iio_chan_spec const *chan,
|
|
long mask)
|
|
{
|
|
if (mask == IIO_CHAN_INFO_OVERSAMPLING_RATIO)
|
|
return IIO_VAL_INT;
|
|
|
|
return -EINVAL;
|
|
}
|
|
|
|
static int sysmon_read_avail(struct iio_dev *indio_dev,
|
|
struct iio_chan_spec const *chan,
|
|
const int **vals, int *type,
|
|
int *length, long mask)
|
|
{
|
|
if (mask != IIO_CHAN_INFO_OVERSAMPLING_RATIO)
|
|
return -EINVAL;
|
|
|
|
*vals = sysmon_oversampling_avail;
|
|
*type = IIO_VAL_INT;
|
|
*length = ARRAY_SIZE(sysmon_oversampling_avail);
|
|
|
|
return IIO_AVAIL_LIST;
|
|
}
|
|
|
|
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,
|
|
.write_raw = sysmon_write_raw,
|
|
.write_raw_get_fmt = sysmon_write_raw_get_fmt,
|
|
.read_avail = sysmon_read_avail,
|
|
.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;
|
|
enum iio_event_direction dir;
|
|
|
|
for (unsigned int i = 0; i < indio_dev->num_channels; i++) {
|
|
if (indio_dev->channels[i].address != address)
|
|
continue;
|
|
|
|
chan = &indio_dev->channels[i];
|
|
/* Temp uses hysteresis mode (rising only), voltage uses window */
|
|
dir = (chan->type == IIO_TEMP) ? IIO_EV_DIR_RISING :
|
|
IIO_EV_DIR_EITHER;
|
|
iio_push_event(indio_dev,
|
|
IIO_UNMOD_EVENT_CODE(chan->type,
|
|
chan->channel,
|
|
IIO_EV_TYPE_THRESH,
|
|
dir),
|
|
iio_get_time_ns(indio_dev));
|
|
}
|
|
}
|
|
|
|
static int 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;
|
|
int ret;
|
|
|
|
switch (event) {
|
|
case SYSMON_BIT_TEMP:
|
|
sysmon_push_event(indio_dev, SYSMON_TEMP_MAX);
|
|
|
|
ret = regmap_write(sysmon->regmap, SYSMON_IDR, BIT(SYSMON_BIT_TEMP));
|
|
if (ret)
|
|
return ret;
|
|
|
|
sysmon->masked_temp |= BIT(SYSMON_BIT_TEMP);
|
|
return 0;
|
|
|
|
case SYSMON_BIT_ALARM0:
|
|
case SYSMON_BIT_ALARM1:
|
|
case SYSMON_BIT_ALARM2:
|
|
case SYSMON_BIT_ALARM3:
|
|
case SYSMON_BIT_ALARM4:
|
|
ret = regmap_read(sysmon->regmap, alarm_flag_offset, ®_val);
|
|
if (ret)
|
|
return ret;
|
|
|
|
alarm_flag_reg = 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);
|
|
ret = regmap_clear_bits(sysmon->regmap, alarm_reg_offset, BIT(bit));
|
|
if (ret)
|
|
return ret;
|
|
}
|
|
|
|
return regmap_write(sysmon->regmap, alarm_flag_offset, alarm_flag_reg);
|
|
|
|
default:
|
|
return -EINVAL;
|
|
}
|
|
}
|
|
|
|
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 status;
|
|
u32 ier;
|
|
|
|
status = isr & SYSMON_TEMP_INTR_MASK;
|
|
|
|
ier = ~status & sysmon->masked_temp;
|
|
sysmon->masked_temp &= status;
|
|
|
|
/* Only unmask if not administratively disabled by userspace */
|
|
ier &= ~sysmon->temp_mask;
|
|
|
|
regmap_write(sysmon->regmap, SYSMON_IER, ier);
|
|
}
|
|
|
|
/*
|
|
* 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;
|
|
|
|
/*
|
|
* If the ISR read fails, skip processing to avoid acting
|
|
* on undefined data.
|
|
*/
|
|
scoped_guard(spinlock_irq, &sysmon->irq_lock) {
|
|
if (regmap_read(sysmon->regmap, SYSMON_ISR, &isr))
|
|
break;
|
|
regmap_write(sysmon->regmap, SYSMON_ISR, isr);
|
|
sysmon_unmask_temp(sysmon, isr);
|
|
}
|
|
|
|
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 = iio_priv(indio_dev);
|
|
unsigned int isr, imr;
|
|
|
|
guard(spinlock)(&sysmon->irq_lock);
|
|
|
|
if (regmap_read(sysmon->regmap, SYSMON_ISR, &isr) ||
|
|
regmap_read(sysmon->regmap, SYSMON_IMR, &imr))
|
|
return IRQ_NONE;
|
|
|
|
isr &= ~imr;
|
|
if (!isr)
|
|
return IRQ_NONE;
|
|
|
|
regmap_write(sysmon->regmap, SYSMON_ISR, isr);
|
|
|
|
sysmon_handle_events(indio_dev, isr);
|
|
schedule_delayed_work(&sysmon->sysmon_unmask_work,
|
|
msecs_to_jiffies(SYSMON_UNMASK_WORK_DELAY_MS));
|
|
|
|
return IRQ_HANDLED;
|
|
}
|
|
|
|
static void sysmon_disable_interrupts(void *data)
|
|
{
|
|
struct sysmon *sysmon = data;
|
|
|
|
regmap_write(sysmon->regmap, SYSMON_IDR, SYSMON_INTR_ALL_MASK);
|
|
|
|
scoped_guard(spinlock_irq, &sysmon->irq_lock)
|
|
sysmon->masked_temp = 0;
|
|
|
|
cancel_delayed_work_sync(&sysmon->sysmon_unmask_work);
|
|
}
|
|
|
|
static int sysmon_init_interrupt(struct sysmon *sysmon,
|
|
struct device *dev,
|
|
struct iio_dev *indio_dev,
|
|
int irq)
|
|
{
|
|
unsigned int imr;
|
|
int ret;
|
|
|
|
/* Events not supported without IRQ (e.g. I2C path) */
|
|
if (!irq)
|
|
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_request_irq(dev, irq, sysmon_iio_irq, 0, "sysmon-irq", indio_dev);
|
|
if (ret)
|
|
return ret;
|
|
|
|
return devm_add_action_or_reset(dev, sysmon_disable_interrupts, sysmon);
|
|
}
|
|
|
|
/*
|
|
* Initialize the cached hysteresis for a temperature channel from the
|
|
* current hardware threshold registers: hysteresis = upper - lower.
|
|
*/
|
|
static int sysmon_init_hysteresis(struct sysmon *sysmon, int *hysteresis)
|
|
{
|
|
unsigned int upper_reg, lower_reg;
|
|
int upper_mc, lower_mc;
|
|
int ret;
|
|
|
|
ret = regmap_read(sysmon->regmap, SYSMON_TEMP_TH_UP, &upper_reg);
|
|
if (ret)
|
|
return ret;
|
|
|
|
ret = regmap_read(sysmon->regmap, SYSMON_TEMP_TH_LOW, &lower_reg);
|
|
if (ret)
|
|
return ret;
|
|
|
|
sysmon_q8p7_to_millicelsius(upper_reg, &upper_mc);
|
|
sysmon_q8p7_to_millicelsius(lower_reg, &lower_mc);
|
|
*hysteresis = upper_mc - lower_mc;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* sysmon_parse_fw() - Parse firmware nodes and configure IIO channels.
|
|
* @indio_dev: IIO device instance
|
|
* @dev: Parent device
|
|
* @irq: IRQ number (positive enables event channels, 0 disables)
|
|
*
|
|
* Reads voltage-channels and temperature-channels container nodes from
|
|
* firmware and builds the IIO channel array. Static temperature channels
|
|
* 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. 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.
|
|
*/
|
|
static int sysmon_parse_fw(struct iio_dev *indio_dev, struct device *dev, int irq)
|
|
{
|
|
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;
|
|
|
|
memcpy(sysmon_channels, temp_channels, sizeof(temp_channels));
|
|
|
|
/* Attach event spec to channel 0 when IRQ is available */
|
|
if (irq > 0) {
|
|
sysmon_channels[0].event_spec = sysmon_temp_events;
|
|
sysmon_channels[0].num_event_specs = ARRAY_SIZE(sysmon_temp_events);
|
|
}
|
|
|
|
idx = num_static;
|
|
|
|
/* Supply channels from DT */
|
|
fwnode_for_each_child_node_scoped(supply_node, child) {
|
|
ret = fwnode_property_read_u32(child, "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),
|
|
.info_mask_shared_by_type =
|
|
BIT(IIO_CHAN_INFO_OVERSAMPLING_RATIO),
|
|
.info_mask_shared_by_type_available =
|
|
BIT(IIO_CHAN_INFO_OVERSAMPLING_RATIO),
|
|
.event_spec = irq > 0 ?
|
|
sysmon_supply_events : NULL,
|
|
.num_event_specs = irq > 0 ?
|
|
ARRAY_SIZE(sysmon_supply_events) : 0,
|
|
.datasheet_name = label,
|
|
};
|
|
}
|
|
|
|
/* Temperature satellite channels from DT */
|
|
fwnode_for_each_child_node_scoped(temp_node, child) {
|
|
ret = fwnode_property_read_u32(child, "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) |
|
|
BIT(IIO_CHAN_INFO_OVERSAMPLING_RATIO),
|
|
.info_mask_shared_by_type_available =
|
|
BIT(IIO_CHAN_INFO_OVERSAMPLING_RATIO),
|
|
.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 irq;
|
|
int ret;
|
|
|
|
indio_dev = devm_iio_device_alloc(dev, sizeof(*sysmon));
|
|
if (!indio_dev)
|
|
return -ENOMEM;
|
|
|
|
sysmon = iio_priv(indio_dev);
|
|
sysmon->regmap = regmap;
|
|
sysmon->temp_oversampling = 1;
|
|
sysmon->supply_oversampling = 1;
|
|
|
|
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);
|
|
if (ret)
|
|
return ret;
|
|
ret = regmap_write(sysmon->regmap, SYSMON_ISR, SYSMON_INTR_ALL_MASK);
|
|
if (ret)
|
|
return ret;
|
|
|
|
irq = fwnode_irq_get(dev_fwnode(dev), 0);
|
|
if (irq == -EPROBE_DEFER)
|
|
return dev_err_probe(dev, irq, "failed to get IRQ\n");
|
|
|
|
indio_dev->name = "versal-sysmon";
|
|
indio_dev->modes = INDIO_DIRECT_MODE;
|
|
|
|
ret = sysmon_parse_fw(indio_dev, dev, irq);
|
|
if (ret)
|
|
return ret;
|
|
|
|
if (irq > 0) {
|
|
/* Set hysteresis mode for temperature threshold */
|
|
ret = regmap_set_bits(sysmon->regmap, SYSMON_TEMP_EV_CFG,
|
|
SYSMON_TEMP_HYST_MASK);
|
|
if (ret)
|
|
return ret;
|
|
|
|
/* Initialize cached hysteresis from hardware registers */
|
|
ret = sysmon_init_hysteresis(sysmon, &sysmon->temp_hysteresis);
|
|
if (ret)
|
|
return ret;
|
|
|
|
ret = sysmon_init_interrupt(sysmon, dev, indio_dev, irq);
|
|
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>");
|