diff --git a/drivers/iio/adc/Kconfig b/drivers/iio/adc/Kconfig index 71ab0a06aa82..0d3dafb83b2c 100644 --- a/drivers/iio/adc/Kconfig +++ b/drivers/iio/adc/Kconfig @@ -872,6 +872,26 @@ config QCOM_SPMI_ADC5 To compile this driver as a module, choose M here: the module will be called qcom-spmi-adc5. +config QCOM_SPMI_ADC5_GEN3 + tristate "Qualcomm Technologies Inc. SPMI PMIC5 GEN3 ADC" + depends on SPMI + select REGMAP_SPMI + select QCOM_VADC_COMMON + help + This is the IIO Voltage PMIC5 Gen3 ADC driver for Qualcomm Technologies Inc. + + The driver supports multiple channels read. The ADC is a 16-bit + sigma-delta ADC. The hardware supports calibrated results for + conversion requests and clients include reading voltage phone + power, on board system thermistors connected to the PMIC ADC, + PMIC die temperature, charger temperature, battery current, USB voltage + input, voltage signals connected to supported PMIC GPIO inputs. The + hardware supports internal pull-up for thermistors and can choose between + a 100k, 30k and 400k pull up using the ADC channels. + + To compile this driver as a module, choose M here: the module will + be called qcom-spmi-adc5-gen3. + config RCAR_GYRO_ADC tristate "Renesas R-Car GyroADC driver" depends on ARCH_RCAR_GEN2 || COMPILE_TEST diff --git a/drivers/iio/adc/Makefile b/drivers/iio/adc/Makefile index 39d806f6d457..073f81002995 100644 --- a/drivers/iio/adc/Makefile +++ b/drivers/iio/adc/Makefile @@ -77,6 +77,7 @@ obj-$(CONFIG_NAU7802) += nau7802.o obj-$(CONFIG_NPCM_ADC) += npcm_adc.o obj-$(CONFIG_PALMAS_GPADC) += palmas_gpadc.o obj-$(CONFIG_QCOM_SPMI_ADC5) += qcom-spmi-adc5.o +obj-$(CONFIG_QCOM_SPMI_ADC5_GEN3) += qcom-spmi-adc5-gen3.o obj-$(CONFIG_QCOM_SPMI_IADC) += qcom-spmi-iadc.o obj-$(CONFIG_QCOM_VADC_COMMON) += qcom-vadc-common.o obj-$(CONFIG_QCOM_SPMI_VADC) += qcom-spmi-vadc.o diff --git a/drivers/iio/adc/qcom-spmi-adc5-gen3.c b/drivers/iio/adc/qcom-spmi-adc5-gen3.c new file mode 100644 index 000000000000..f25b1f4fa751 --- /dev/null +++ b/drivers/iio/adc/qcom-spmi-adc5-gen3.c @@ -0,0 +1,1737 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (c) 2021, The Linux Foundation. All rights reserved. + * Copyright (c) 2022, Qualcomm Innovation Center, Inc. All rights reserved. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +static LIST_HEAD(adc_tm_device_list); + +#define ADC5_GEN3_HS 0x45 +#define ADC5_GEN3_HS_BUSY BIT(7) +#define ADC5_GEN3_HS_READY BIT(0) + +#define ADC5_GEN3_STATUS1 0x46 +#define ADC5_GEN3_STATUS1_CONV_FAULT BIT(7) +#define ADC5_GEN3_STATUS1_THR_CROSS BIT(6) +#define ADC5_GEN3_STATUS1_EOC BIT(1) + +#define ADC5_GEN3_TM_EN_STS 0x47 + +#define ADC5_GEN3_TM_HIGH_STS 0x48 + +#define ADC5_GEN3_TM_LOW_STS 0x49 + +#define ADC5_GEN3_EOC_STS 0x4a + +#define ADC5_GEN3_EOC_CLR 0x4b + +#define ADC5_GEN3_TM_HIGH_STS_CLR 0x4c + +#define ADC5_GEN3_TM_LOW_STS_CLR 0x4d + +#define ADC5_GEN3_SID 0x4f +#define ADC5_GEN3_SID_MASK 0xf + +#define ADC5_GEN3_PERPH_CH 0x50 +#define ADC5_GEN3_CHAN_CONV_REQ BIT(7) + +#define ADC5_GEN3_TIMER_SEL 0x51 + +#define ADC5_GEN3_DIG_PARAM 0x52 +#define ADC5_GEN3_DIG_PARAM_CAL_SEL_MASK GENMASK(5, 4) +#define ADC5_GEN3_DIG_PARAM_CAL_SEL_SHIFT 4 +#define ADC5_GEN3_DIG_PARAM_DEC_RATIO_SEL_MASK GENMASK(3, 2) +#define ADC5_GEN3_DIG_PARAM_DEC_RATIO_SEL_SHIFT 2 + +#define ADC5_GEN3_FAST_AVG 0x53 +#define ADC5_GEN3_FAST_AVG_CTL_EN BIT(7) +#define ADC5_GEN3_FAST_AVG_CTL_SAMPLES_MASK GENMASK(2, 0) + +#define ADC5_GEN3_ADC_CH_SEL_CTL 0x54 + +#define ADC5_GEN3_DELAY_CTL 0x55 +#define ADC5_GEN3_HW_SETTLE_DELAY_MASK 0xf + +#define ADC5_GEN3_CH_EN 0x56 + +#define ADC5_GEN3_LOW_THR0 0x57 + +#define ADC5_GEN3_LOW_THR1 0x58 + +#define ADC5_GEN3_HIGH_THR0 0x59 + +#define ADC5_GEN3_HIGH_THR1 0x5a + +#define ADC5_GEN3_CH0_DATA0 0x5c +#define ADC5_GEN3_CH0_DATA1 0x5d +#define ADC5_GEN3_CH1_DATA0 0x5e +#define ADC5_GEN3_CH1_DATA1 0x5f +#define ADC5_GEN3_CH2_DATA0 0x60 +#define ADC5_GEN3_CH2_DATA1 0x61 +#define ADC5_GEN3_CH3_DATA0 0x62 +#define ADC5_GEN3_CH3_DATA1 0x63 +#define ADC5_GEN3_CH4_DATA0 0x64 +#define ADC5_GEN3_CH4_DATA1 0x65 +#define ADC5_GEN3_CH5_DATA0 0x66 +#define ADC5_GEN3_CH5_DATA1 0x67 +#define ADC5_GEN3_CH6_DATA0 0x68 +#define ADC5_GEN3_CH6_DATA1 0x69 +#define ADC5_GEN3_CH7_DATA0 0x6a +#define ADC5_GEN3_CH7_DATA1 0x6b + +#define ADC5_GEN3_CONV_REQ 0xe5 +#define ADC5_GEN3_CONV_REQ_REQ BIT(0) + +#define ADC5_GEN3_SID_OFFSET 0x8 +#define ADC5_GEN3_CHANNEL_MASK 0xff +#define V_CHAN(x) (((x).sid << ADC5_GEN3_SID_OFFSET) | (x).channel) + +#define ADC_TM5_GEN3_LOWER_MASK(n) ((n) & GENMASK(7, 0)) +#define ADC_TM5_GEN3_UPPER_MASK(n) (((n) & GENMASK(15, 8)) >> 8) + +#define ADC_TM5_GEN3_CHANS_MAX 7 + +enum adc5_cal_method { + ADC5_NO_CAL = 0, + ADC5_RATIOMETRIC_CAL, + ADC5_ABSOLUTE_CAL +}; + +enum adc_time_select { + MEAS_INT_DISABLE = 0, + MEAS_INT_IMMEDIATE, + MEAS_INT_50MS, + MEAS_INT_100MS, + MEAS_INT_1S, + MEAS_INT_NONE, +}; + +enum adc_tm_type { + ADC_TM_NONE = 0, + ADC_TM, + ADC_TM_IIO, + ADC_TM_NON_THERMAL, + ADC_TM_INVALID, +}; + +static struct adc_tm_reverse_scale_fn adc_tm_rscale_fn[] = { + [SCALE_R_ABSOLUTE] = {adc_tm_absolute_rthr_gen3}, +}; + +/** + * struct adc5_channel_prop - ADC channel property. + * @channel: channel number, refer to the channel list. + * @cal_method: calibration method. + * @decimation: sampling rate supported for the channel. + * @sid: slave id of PMIC owning the channel, for PMIC5 Gen2 and above. + * @prescale: channel scaling performed on the input signal. + * @hw_settle_time: the time between AMUX being configured and the + * start of conversion. + * @avg_samples: ability to provide single result from the ADC + * that is an average of multiple measurements. + * @scale_fn_type: Represents the scaling function to convert voltage + * physical units desired by the client for the channel. + * @datasheet_name: Channel name used in device tree. + * @chip: pointer to top-level ADC device structure. + * @adc_tm: indicates TM type if the channel is used for TM measurements. + * @tm_chan_index: TM channel number used (ranging from 1-7). + * @timer: time period of recurring TM measurement. + * @tzd: pointer to thermal device corresponding to TM channel. + * @high_thr_en: TM high threshold crossing detection enabled. + * @low_thr_en: TM low threshold crossing detection enabled. + * @high_thr_triggered: indicates if high TM threshold has been triggered. + * @low_thr_triggered: indicates if low TM threshold has been triggered. + * @high_thr_voltage: upper threshold voltage for TM. + * @low_thr_voltage: lower threshold voltage for TM. + * @last_temp: last temperature that caused threshold violation, + * or a thermal TM channel. + * @last_temp_set: indicates if last_temp is stored. + * @req_wq: workqueue holding queued notification tasks for a non-thermal + * TM channel. + * @work: scheduled work for handling non-thermal TM client notification. + * @thr_list: list of client thresholds configured for non-thermal TM channel. + * @adc_rscale_fn: reverse scaling function to convert voltage to raw code + * for non-thermal TM channels. + */ +struct adc5_channel_prop { + unsigned int channel; + enum adc5_cal_method cal_method; + unsigned int decimation; + unsigned int sid; + unsigned int prescale; + unsigned int hw_settle_time; + unsigned int avg_samples; + + enum vadc_scale_fn_type scale_fn_type; + const char *datasheet_name; + + struct adc5_chip *chip; + /* TM properties */ + int adc_tm; + unsigned int tm_chan_index; + unsigned int timer; + struct thermal_zone_device *tzd; + int high_thr_en; + int low_thr_en; + bool high_thr_triggered; + bool low_thr_triggered; + int64_t high_thr_voltage; + int64_t low_thr_voltage; + int last_temp; + bool last_temp_set; + struct workqueue_struct *req_wq; + struct work_struct work; + struct list_head thr_list; + enum adc_tm_rscale_fn_type adc_rscale_fn; +}; + +/** + * struct adc5_chip - ADC private structure. + * @regmap: SPMI ADC5 peripheral register map field. + * @dev: SPMI ADC5 device. + * @base: base address for the ADC peripheral. + * @debug_base: base address for the reserved ADC peripheral, + * to dump for debug purposes alone. + * @nchannels: number of ADC channels. + * @chan_props: array of ADC channel properties. + * @iio_chans: array of IIO channels specification. + * @complete: ADC result notification after interrupt is received. + * @lock: ADC lock for access to the peripheral. + * @data: software configuration data. + * @list: list item, used to add this device to gloal list of ADC_TM devices. + * @device_list: pointer to list of ADC_TM devices. + * @n_tm_channels: number of ADC channels used for TM measurements. + * @tm_handler_work: scheduled work for handling TM threshold violation. + */ +struct adc5_chip { + struct regmap *regmap; + struct device *dev; + u16 base; + u16 debug_base; + unsigned int nchannels; + struct adc5_channel_prop *chan_props; + struct iio_chan_spec *iio_chans; + struct completion complete; + struct mutex lock; + const struct adc5_data *data; + /* TM properties */ + unsigned int n_tm_channels; + struct list_head list; + struct list_head *device_list; + struct work_struct tm_handler_work; +}; + +static const struct u32_fract adc5_prescale_ratios[] = { + { .numerator = 1, .denominator = 1 }, + { .numerator = 1, .denominator = 3 }, + { .numerator = 1, .denominator = 6 }, + { .numerator = 1, .denominator = 16 }, + /* Prescale ratios for current channels below */ + { .numerator = 32, .denominator = 100 }, /* IIN_FB */ + { .numerator = 14, .denominator = 100 }, /* ICHG_SMB */ + { .numerator = 28, .denominator = 100 }, /* IIN_SMB */ + { .numerator = 1000, .denominator = 305185 }, /* ICHG_FB */ +}; + +static int adc5_read(struct adc5_chip *adc, u16 offset, u8 *data, int len) +{ + int ret; + + ret = regmap_bulk_read(adc->regmap, adc->base + offset, data, len); + if (ret < 0) + pr_err("adc read to register 0x%x of length:%d failed, ret=%d\n", offset, len, ret); + + return ret; +} + +static int adc5_write(struct adc5_chip *adc, u16 offset, u8 *data, int len) +{ + int ret; + + ret = regmap_bulk_write(adc->regmap, adc->base + offset, data, len); + if (ret < 0) + pr_err("adc write to register 0x%x of length:%d failed, ret=%d\n", offset, len, + ret); + + return ret; +} + +static int adc5_prescaling_from_dt(u32 numerator, u32 denominator) +{ + unsigned int pre; + + for (pre = 0; pre < ARRAY_SIZE(adc5_prescale_ratios); pre++) { + if (adc5_prescale_ratios[pre].numerator == numerator && + adc5_prescale_ratios[pre].denominator == denominator) + break; + } + + if (pre == ARRAY_SIZE(adc5_prescale_ratios)) + return -ENOENT; + + return pre; +} + +static int adc5_hw_settle_time_from_dt(u32 value, + const unsigned int *hw_settle) +{ + unsigned int i; + + for (i = 0; i < VADC_HW_SETTLE_SAMPLES_MAX; i++) { + if (value == hw_settle[i]) + return i; + } + + return -ENOENT; +} + +static int adc5_avg_samples_from_dt(u32 value) +{ + if (!is_power_of_2(value) || value > ADC5_AVG_SAMPLES_MAX) + return -EINVAL; + + return __ffs(value); +} + +static int adc5_decimation_from_dt(u32 value, + const unsigned int *decimation) +{ + unsigned int i; + + for (i = 0; i < ADC5_DECIMATION_SAMPLES_MAX; i++) { + if (value == decimation[i]) + return i; + } + + return -ENOENT; +} + +static int adc5_gen3_read_voltage_data(struct adc5_chip *adc, u16 *data) +{ + int ret; + u8 rslt[2]; + + ret = adc5_read(adc, ADC5_GEN3_CH0_DATA0, rslt, 2); + if (ret < 0) + return ret; + + *data = (rslt[1] << 8) | rslt[0]; + + if (*data == ADC5_USR_DATA_CHECK) { + pr_err("Invalid data:%#x\n", *data); + return -EINVAL; + } + + pr_debug("voltage raw code:0x%x\n", *data); + + return 0; +} + +static void adc5_gen3_update_dig_param(struct adc5_chip *adc, + struct adc5_channel_prop *prop, u8 *data) +{ + /* Update calibration select */ + *data &= ~ADC5_GEN3_DIG_PARAM_CAL_SEL_MASK; + *data |= (prop->cal_method << ADC5_GEN3_DIG_PARAM_CAL_SEL_SHIFT); + + /* Update decimation ratio select */ + *data &= ~ADC5_GEN3_DIG_PARAM_DEC_RATIO_SEL_MASK; + *data |= (prop->decimation << ADC5_GEN3_DIG_PARAM_DEC_RATIO_SEL_SHIFT); +} + +static int adc5_gen3_configure(struct adc5_chip *adc, + struct adc5_channel_prop *prop) +{ + int ret; + u8 conv_req = 0, buf[7]; + + ret = adc5_read(adc, ADC5_GEN3_SID, buf, sizeof(buf)); + if (ret < 0) + return ret; + + /* Write SID */ + buf[0] &= (u8) ~ADC5_GEN3_SID_MASK; + buf[0] &= prop->sid; + + /* Use channel 0 by default for immediate conversion */ + buf[1] = 0; + + buf[2] = prop->timer; + + /* Digital param selection */ + adc5_gen3_update_dig_param(adc, prop, &buf[3]); + + /* Update fast average sample value */ + buf[4] &= (u8) ~ADC5_GEN3_FAST_AVG_CTL_SAMPLES_MASK; + buf[4] |= prop->avg_samples; + + /* + * Select ADC channel and indicate there is an actual + * conversion request + */ + buf[5] = ADC5_GEN3_CHAN_CONV_REQ | prop->channel; + + /* Select HW settle delay for channel */ + buf[6] &= (u8) ~ADC5_GEN3_HW_SETTLE_DELAY_MASK; + buf[6] |= prop->hw_settle_time; + + reinit_completion(&adc->complete); + + ret = adc5_write(adc, ADC5_GEN3_SID, buf, sizeof(buf)); + if (ret < 0) + return ret; + + conv_req = ADC5_GEN3_CONV_REQ_REQ; + ret = adc5_write(adc, ADC5_GEN3_CONV_REQ, &conv_req, 1); + + return ret; +} + +#define ADC5_GEN3_HS_DELAY_MIN_US 100 +#define ADC5_GEN3_HS_DELAY_MAX_US 110 +#define ADC5_GEN3_HS_RETRY_COUNT 20 + +static int adc5_gen3_poll_wait_hs(struct adc5_chip *adc) +{ + int ret, count; + u8 status = 0; + + for (count = 0; count < ADC5_GEN3_HS_RETRY_COUNT; count++) { + ret = adc5_read(adc, ADC5_GEN3_HS, &status, 1); + if (ret < 0) + return ret; + + if (status == ADC5_GEN3_HS_READY) + break; + + usleep_range(ADC5_GEN3_HS_DELAY_MIN_US, + ADC5_GEN3_HS_DELAY_MAX_US); + } + + if (count == ADC5_GEN3_HS_RETRY_COUNT) { + pr_err("Setting HS ready bit timed out, status:%#x\n", status); + return -ETIMEDOUT; + } + + return 0; +} + +#define ADC5_GEN3_CONV_TIMEOUT_MS 501 + +static int adc5_gen3_do_conversion(struct adc5_chip *adc, + struct adc5_channel_prop *prop, + u16 *data_volt) +{ + int ret; + unsigned long rc; + unsigned int time_pending_ms; + u8 val; + + mutex_lock(&adc->lock); + ret = adc5_gen3_poll_wait_hs(adc); + if (ret < 0) + goto unlock; + + ret = adc5_gen3_configure(adc, prop); + if (ret < 0) { + pr_err("ADC configure failed with %d\n", ret); + goto unlock; + } + + /* No support for polling mode at present*/ + rc = wait_for_completion_timeout(&adc->complete, + msecs_to_jiffies(ADC5_GEN3_CONV_TIMEOUT_MS)); + if (!rc) { + pr_err("Reading ADC channel %s timed out\n", + prop->datasheet_name); + ret = -ETIMEDOUT; + goto unlock; + } + + time_pending_ms = jiffies_to_msecs(rc); + pr_debug("ADC channel %s EOC took %u ms\n", prop->datasheet_name, + ADC5_GEN3_CONV_TIMEOUT_MS - time_pending_ms); + + ret = adc5_gen3_read_voltage_data(adc, data_volt); + if (ret < 0) + goto unlock; + + val = BIT(0); + ret = adc5_write(adc, ADC5_GEN3_EOC_CLR, &val, 1); + if (ret < 0) + goto unlock; + + /* To indicate conversion request is only to clear a status */ + val = ~ADC5_GEN3_CHAN_CONV_REQ | prop->channel; + ret = adc5_write(adc, ADC5_GEN3_PERPH_CH, &val, 1); + if (ret < 0) + goto unlock; + + val = ADC5_GEN3_CONV_REQ_REQ; + ret = adc5_write(adc, ADC5_GEN3_CONV_REQ, &val, 1); + +unlock: + mutex_unlock(&adc->lock); + + return ret; +} + +#define ADC_OFFSET_DUMP 8 +#define ADC_SDAM_REG_DUMP 32 +static void adc5_gen3_dump_regs_debug(struct adc5_chip *adc) +{ + int rc = 0, i = 0, j = 0, offset; + u8 buf[8]; + + for (j = 0; j < 2; j++) { + if (!j) { + offset = 0; + pr_debug("ADC SDAM DUMP\n"); + } else { + if (adc->debug_base) + offset = adc->debug_base; + else + break; + pr_debug("SDAM 20 DUMP\n"); + } + + for (i = 0; i < ADC_SDAM_REG_DUMP; i++) { + rc = adc5_read(adc, offset, buf, sizeof(buf)); + if (rc < 0) { + pr_err("debug register dump failed\n"); + return; + } + offset += ADC_OFFSET_DUMP; + pr_debug("Buf[%d]: %*ph\n", i, sizeof(buf), buf); + } + } +} + +static irqreturn_t adc5_gen3_isr(int irq, void *dev_id) +{ + struct adc5_chip *adc = dev_id; + u8 status, tm_status[2]; + int ret; + + ret = adc5_read(adc, ADC5_GEN3_STATUS1, &status, 1); + if (ret < 0) { + pr_err("adc read status failed with %d\n", ret); + goto handler_end; + } + + if (status & ADC5_GEN3_STATUS1_EOC) + complete(&adc->complete); + + ret = adc5_read(adc, ADC5_GEN3_TM_HIGH_STS, tm_status, 2); + if (ret < 0) { + pr_err("adc read TM status failed with %d\n", ret); + goto handler_end; + } + + if (tm_status[0] || tm_status[1]) + schedule_work(&adc->tm_handler_work); + + pr_debug("Interrupt status:%#x, high:%#x, low:%#x\n", + status, tm_status[0], tm_status[1]); + + if (status & ADC5_GEN3_STATUS1_CONV_FAULT) { + pr_err("Unexpected conversion fault\n"); + adc5_gen3_dump_regs_debug(adc); + } + +handler_end: + return IRQ_HANDLED; +} + +static void tm_handler_work(struct work_struct *work) +{ + struct adc5_chip *adc = container_of(work, struct adc5_chip, + tm_handler_work); + struct adc5_channel_prop *chan_prop; + u8 tm_status[2], buf[14], val; + int ret, i; + + mutex_lock(&adc->lock); + + ret = adc5_read(adc, ADC5_GEN3_TM_HIGH_STS, tm_status, 2); + if (ret < 0) { + pr_err("adc read TM status failed with %d\n", ret); + goto work_unlock; + } + + ret = adc5_write(adc, ADC5_GEN3_TM_HIGH_STS_CLR, tm_status, 2); + if (ret < 0) { + pr_err("adc write TM status failed with %d\n", ret); + goto work_unlock; + } + + /* To indicate conversion request is only to clear a status */ + val = (u8) ~ADC5_GEN3_CHAN_CONV_REQ; + ret = adc5_write(adc, ADC5_GEN3_PERPH_CH, &val, 1); + + val = ADC5_GEN3_CONV_REQ_REQ; + ret = adc5_write(adc, ADC5_GEN3_CONV_REQ, &val, 1); + if (ret < 0) { + pr_err("adc write conv_req failed with %d\n", ret); + goto work_unlock; + } + + ret = adc5_read(adc, ADC5_GEN3_CH1_DATA0, buf, sizeof(buf)); + if (ret < 0) { + pr_err("adc read data failed with %d\n", ret); + goto work_unlock; + } + + mutex_unlock(&adc->lock); + + for (i = 0; i < adc->nchannels; i++) { + bool upper_set = false, lower_set = false; + u8 data_low = 0, data_high = 0; + u16 code = 0; + int temp; + + chan_prop = &adc->chan_props[i]; + if (!chan_prop->adc_tm) + continue; + + mutex_lock(&adc->lock); + if ((tm_status[0] & 0x1) && (chan_prop->high_thr_en)) + upper_set = true; + + if ((tm_status[1] & 0x1) && (chan_prop->low_thr_en)) + lower_set = true; + + tm_status[0] >>= 1; + tm_status[1] >>= 1; + mutex_unlock(&adc->lock); + + if (!(upper_set || lower_set)) + continue; + + data_low = buf[2 * i]; + data_high = buf[2 * i + 1]; + code = ((data_high << 8) | data_low); + pr_debug("ADC_TM threshold code:0x%x\n", code); + + if (chan_prop->adc_tm == ADC_TM_NON_THERMAL) { + if (lower_set) { + mutex_lock(&adc->lock); + chan_prop->low_thr_en = 0; + chan_prop->low_thr_triggered = true; + mutex_unlock(&adc->lock); + queue_work(chan_prop->req_wq, + &chan_prop->work); + } + + if (upper_set) { + mutex_lock(&adc->lock); + chan_prop->high_thr_en = 0; + chan_prop->high_thr_triggered = true; + mutex_unlock(&adc->lock); + queue_work(chan_prop->req_wq, + &chan_prop->work); + } + } else { + ret = qcom_adc5_hw_scale(chan_prop->scale_fn_type, + &adc5_prescale_ratios[chan_prop->prescale], + adc->data, code, &temp); + + if (ret < 0) { + pr_err("Invalid temperature reading, ret=%d, code=0x%x\n", + ret, code); + continue; + } + pr_debug("notifying thermal, temp:%d\n", temp); + chan_prop->last_temp = temp; + chan_prop->last_temp_set = true; + thermal_zone_device_update(chan_prop->tzd, THERMAL_TRIP_VIOLATED); + } + } + + return; + +work_unlock: + mutex_unlock(&adc->lock); +} + +static int adc5_gen3_of_xlate(struct iio_dev *indio_dev, + const struct of_phandle_args *iiospec) +{ + struct adc5_chip *adc = iio_priv(indio_dev); + int i, v_channel; + + for (i = 0; i < adc->nchannels; i++) { + v_channel = V_CHAN(adc->chan_props[i]); + if (v_channel == iiospec->args[0]) + return i; + } + + return -ENOENT; +} + +static int adc5_gen3_read_raw(struct iio_dev *indio_dev, + struct iio_chan_spec const *chan, int *val, int *val2, + long mask) +{ + struct adc5_chip *adc = iio_priv(indio_dev); + struct adc5_channel_prop *prop; + u16 adc_code_volt; + int ret; + + prop = &adc->chan_props[chan->address]; + + switch (mask) { + case IIO_CHAN_INFO_PROCESSED: + ret = adc5_gen3_do_conversion(adc, prop, + &adc_code_volt); + if (ret < 0) + return ret; + + ret = qcom_adc5_hw_scale(prop->scale_fn_type, + &adc5_prescale_ratios[prop->prescale], + adc->data, + adc_code_volt, val); + if (ret < 0) + return ret; + + return IIO_VAL_INT; + + case IIO_CHAN_INFO_RAW: + ret = adc5_gen3_do_conversion(adc, prop, + &adc_code_volt); + if (ret < 0) + return ret; + + *val = (int)adc_code_volt; + return IIO_VAL_INT; + + default: + return -EINVAL; + } + + return 0; +} + +static const struct iio_info adc5_gen3_info = { + .read_raw = adc5_gen3_read_raw, + .of_xlate = adc5_gen3_of_xlate, +}; + +/* Used by thermal clients to read ADC channel temperature */ +int adc_tm_gen3_get_temp(void *data, int *temp) +{ + int ret; + struct adc5_channel_prop *prop = data; + struct adc5_chip *adc; + u16 adc_code_volt; + + if (!prop || !prop->chip) + return -EINVAL; + + adc = prop->chip; + + if (prop->last_temp_set) { + pr_debug("last_temp: %d\n", prop->last_temp); + prop->last_temp_set = false; + *temp = prop->last_temp; + return 0; + } + + ret = adc5_gen3_do_conversion(adc, prop, &adc_code_volt); + if (ret < 0) + return ret; + + return qcom_adc5_hw_scale(prop->scale_fn_type, + &adc5_prescale_ratios[prop->prescale], + adc->data, + adc_code_volt, temp); +} + +static int adc_tm5_gen3_configure(struct adc5_channel_prop *prop) +{ + int ret; + u8 conv_req = 0, buf[12]; + u32 mask = 0; + struct adc5_chip *adc = prop->chip; + + mutex_lock(&adc->lock); + ret = adc5_gen3_poll_wait_hs(adc); + if (ret < 0) + goto tm_config_unlock; + + ret = adc5_read(adc, ADC5_GEN3_SID, buf, sizeof(buf)); + if (ret < 0) + goto tm_config_unlock; + + /* Write SID */ + buf[0] &= (u8) ~ADC5_GEN3_SID_MASK; + buf[0] &= prop->sid; + + buf[1] = prop->tm_chan_index; + + buf[2] = prop->timer; + + /* Digital param selection */ + adc5_gen3_update_dig_param(adc, prop, &buf[3]); + + /* Update fast average sample value */ + buf[4] &= (u8) ~ADC5_GEN3_FAST_AVG_CTL_SAMPLES_MASK; + buf[4] |= prop->avg_samples; + + /* + * Select ADC channel and indicate there is an actual + * conversion request + */ + buf[5] = ADC5_GEN3_CHAN_CONV_REQ | prop->channel; + + /* Select HW settle delay for channel */ + buf[6] &= (u8) ~ADC5_GEN3_HW_SETTLE_DELAY_MASK; + buf[6] |= prop->hw_settle_time; + + buf[7] = prop->high_thr_en << 1 | prop->low_thr_en; + + mask = lower_32_bits(prop->low_thr_voltage); + buf[8] = ADC_TM5_GEN3_LOWER_MASK(mask); + buf[9] = ADC_TM5_GEN3_UPPER_MASK(mask); + + mask = lower_32_bits(prop->high_thr_voltage); + buf[10] = ADC_TM5_GEN3_LOWER_MASK(mask); + buf[11] = ADC_TM5_GEN3_UPPER_MASK(mask); + + ret = adc5_write(adc, ADC5_GEN3_SID, buf, sizeof(buf)); + if (ret < 0) + goto tm_config_unlock; + + conv_req = ADC5_GEN3_CONV_REQ_REQ; + ret = adc5_write(adc, ADC5_GEN3_CONV_REQ, &conv_req, 1); + +tm_config_unlock: + mutex_unlock(&adc->lock); + + return ret; +} + +static int adc_tm5_gen3_set_trip_temp(void *data, + int low_temp, int high_temp) +{ + struct adc5_channel_prop *prop = data; + struct adc5_chip *adc = prop->chip; + struct adc_tm_config tm_config; + int ret; + + if (!prop) + return -EINVAL; + + pr_debug("channel:%s :low_temp(mdegC):%d, high_temp(mdegC):%d\n", + prop->datasheet_name, low_temp, high_temp); + + adc = prop->chip; + tm_config.high_thr_temp = tm_config.low_thr_temp = 0; + if (high_temp != INT_MAX) + tm_config.high_thr_temp = high_temp; + if (low_temp != INT_MIN) + tm_config.low_thr_temp = low_temp; + + if ((high_temp == INT_MAX) && (low_temp == INT_MIN)) { + pr_err("No trips to set\n"); + return -EINVAL; + } + + pr_debug("requested a low temp- %d and high temp- %d\n", + tm_config.low_thr_temp, tm_config.high_thr_temp); + + adc_tm_scale_therm_voltage_100k_gen3(&tm_config); + + /* + * Thresholds are forward scaled to confirm their + * temperatures will actually cause a violation, before + * being written. + */ + pr_debug("high_thr:0x%llx, low_thr:0x%llx\n", + tm_config.high_thr_voltage, tm_config.low_thr_voltage); + + mutex_lock(&adc->lock); + + if (high_temp != INT_MAX) { + prop->low_thr_voltage = tm_config.low_thr_voltage; + prop->low_thr_en = 1; + } else { + prop->low_thr_en = 0; + } + + if (low_temp > -INT_MAX) { + prop->high_thr_voltage = tm_config.high_thr_voltage; + prop->high_thr_en = 1; + } else { + prop->high_thr_en = 0; + } + + ret = adc_tm5_gen3_configure(prop); + if (ret < 0) + pr_err("Error during adc-tm configure:%d\n", ret); + + mutex_unlock(&adc->lock); + + return ret; +} + +#define MAX_PROP_NAME_LEN 32 +struct adc5_chip *get_adc_tm_gen3(struct device *dev, const char *name) +{ + struct platform_device *pdev; + struct adc5_chip *chip; + struct device_node *node = NULL; + char prop_name[MAX_PROP_NAME_LEN]; + + scnprintf(prop_name, MAX_PROP_NAME_LEN, "qcom,%s-adc_tm", name); + + node = of_parse_phandle(dev->of_node, prop_name, 0); + if (node == NULL) + return ERR_PTR(-ENODEV); + + list_for_each_entry(chip, &adc_tm_device_list, list) { + pdev = to_platform_device(chip->dev); + if (pdev->dev.of_node == node) { + of_node_put(node); + return chip; + } + } + of_node_put(node); + return ERR_PTR(-EPROBE_DEFER); +} +EXPORT_SYMBOL(get_adc_tm_gen3); + +static int32_t adc_tm_add_to_list(struct adc5_chip *chip, + uint32_t dt_index, + struct adc_tm_param *param) +{ + struct adc_tm_client_info *client_info = NULL; + bool client_info_exists = false; + + list_for_each_entry(client_info, + &chip->chan_props[dt_index].thr_list, list) { + if (client_info->param->id == param->id) { + client_info->low_thr_requested = param->low_thr; + client_info->high_thr_requested = param->high_thr; + client_info->state_request = param->state_request; + client_info->notify_low_thr = false; + client_info->notify_high_thr = false; + client_info_exists = true; + } + } + + if (!client_info_exists) { + client_info = devm_kzalloc(chip->dev, + sizeof(struct adc_tm_client_info), GFP_KERNEL); + if (!client_info) + return -ENOMEM; + + client_info->param->id = (uintptr_t) client_info; + client_info->low_thr_requested = param->low_thr; + client_info->high_thr_requested = param->high_thr; + client_info->state_request = param->state_request; + + list_add_tail(&client_info->list, + &chip->chan_props[dt_index].thr_list); + } + return 0; +} + +static int32_t adc_tm_gen3_manage_thresholds(struct adc5_channel_prop *prop) +{ + int high_thr = INT_MAX, low_thr = INT_MIN; + struct adc_tm_client_info *client_info = NULL; + struct list_head *thr_list; + uint32_t scale_type = 0; + struct adc_tm_config tm_config; + + prop->high_thr_en = 0; + prop->low_thr_en = 0; + + /* + * Reset the high_thr_set and low_thr_set of all + * clients since the thresholds will be recomputed. + */ + list_for_each(thr_list, &prop->thr_list) { + client_info = list_entry(thr_list, + struct adc_tm_client_info, list); + client_info->high_thr_set = false; + client_info->low_thr_set = false; + } + + /* Find the min of high_thr and max of low_thr */ + list_for_each(thr_list, &prop->thr_list) { + client_info = list_entry(thr_list, + struct adc_tm_client_info, list); + + if ((client_info->state_request == ADC_TM_HIGH_THR_ENABLE) || + (client_info->state_request == + ADC_TM_HIGH_LOW_THR_ENABLE)) + if (client_info->high_thr_requested < high_thr) + high_thr = client_info->high_thr_requested; + + if ((client_info->state_request == ADC_TM_LOW_THR_ENABLE) || + (client_info->state_request == + ADC_TM_HIGH_LOW_THR_ENABLE)) + if (client_info->low_thr_requested > low_thr) + low_thr = client_info->low_thr_requested; + + pr_debug("threshold compared is high:%d and low:%d\n", + client_info->high_thr_requested, + client_info->low_thr_requested); + pr_debug("current threshold is high:%d and low:%d\n", + high_thr, low_thr); + } + + /* Check which of the high_thr and low_thr got set */ + list_for_each(thr_list, &prop->thr_list) { + client_info = list_entry(thr_list, + struct adc_tm_client_info, list); + + if ((client_info->state_request == ADC_TM_HIGH_THR_ENABLE) || + (client_info->state_request == + ADC_TM_HIGH_LOW_THR_ENABLE)) + if (high_thr == client_info->high_thr_requested) { + prop->high_thr_en = 1; + client_info->high_thr_set = true; + } + + if ((client_info->state_request == ADC_TM_LOW_THR_ENABLE) || + (client_info->state_request == + ADC_TM_HIGH_LOW_THR_ENABLE)) + if (low_thr == client_info->low_thr_requested) { + prop->low_thr_en = 1; + client_info->low_thr_set = true; + } + } + + tm_config.high_thr_voltage = (int64_t)high_thr; + tm_config.low_thr_voltage = (int64_t)low_thr; + + scale_type = prop->adc_rscale_fn; + if (scale_type >= SCALE_RSCALE_NONE) + return -EBADF; + + adc_tm_rscale_fn[scale_type].chan(&tm_config); + + prop->low_thr_voltage = tm_config.low_thr_voltage; + prop->high_thr_voltage = tm_config.high_thr_voltage; + + pr_debug("threshold written is high:%d and low:%d\n", + high_thr, low_thr); + + return 0; +} + +/* Used to notify non-thermal clients of threshold crossing */ +void notify_adc_tm_fn(struct work_struct *work) +{ + struct adc5_channel_prop *prop = container_of(work, + struct adc5_channel_prop, work); + struct adc_tm_client_info *client_info = NULL; + struct adc5_chip *chip; + struct list_head *thr_list; + int ret; + + chip = prop->chip; + + mutex_lock(&chip->lock); + if (prop->low_thr_triggered) { + /* adjust thr, calling manage_thr */ + list_for_each(thr_list, &prop->thr_list) { + client_info = list_entry(thr_list, + struct adc_tm_client_info, list); + if (client_info->low_thr_set) { + client_info->low_thr_set = false; + client_info->notify_low_thr = true; + if (client_info->state_request == + ADC_TM_HIGH_LOW_THR_ENABLE) + client_info->state_request = + ADC_TM_HIGH_THR_ENABLE; + else + client_info->state_request = + ADC_TM_LOW_THR_DISABLE; + } + } + prop->low_thr_triggered = false; + } + + if (prop->high_thr_triggered) { + /* adjust thr, calling manage_thr */ + list_for_each(thr_list, &prop->thr_list) { + client_info = list_entry(thr_list, + struct adc_tm_client_info, list); + if (client_info->high_thr_set) { + client_info->high_thr_set = false; + client_info->notify_high_thr = true; + if (client_info->state_request == + ADC_TM_HIGH_LOW_THR_ENABLE) + client_info->state_request = + ADC_TM_LOW_THR_ENABLE; + else + client_info->state_request = + ADC_TM_HIGH_THR_DISABLE; + } + } + prop->high_thr_triggered = false; + } + ret = adc_tm_gen3_manage_thresholds(prop); + if (ret < 0) + pr_err("Error in reverse scaling:%d\n", ret); + + ret = adc_tm5_gen3_configure(prop); + if (ret < 0) + pr_err("Error during adc-tm configure:%d\n", ret); + + mutex_unlock(&chip->lock); + + list_for_each_entry(client_info, &prop->thr_list, list) { + if (client_info->notify_low_thr && + client_info->param->threshold_notification != NULL) { + pr_debug("notify kernel with low state for channel 0x%x\n", + prop->channel); + client_info->param->threshold_notification( + ADC_TM_LOW_STATE, + client_info->param->btm_ctx); + client_info->notify_low_thr = false; + } + + if (client_info->notify_high_thr && + client_info->param->threshold_notification != NULL) { + pr_debug("notify kernel with high state for channel 0x%x\n", + prop->channel); + client_info->param->threshold_notification( + ADC_TM_HIGH_STATE, + client_info->param->btm_ctx); + client_info->notify_high_thr = false; + } + } +} + +/* Used by non-thermal clients to configure an ADC_TM channel */ +int32_t adc_tm_channel_measure_gen3(struct adc5_chip *chip, + struct adc_tm_param *param) + +{ + int ret, i; + uint32_t v_channel, dt_index = 0; + bool chan_found = false; + + if (param == NULL) + return -EINVAL; + + if (param->threshold_notification == NULL) { + pr_debug("No notification for high/low temp\n"); + return -EINVAL; + } + + for (i = 0; i < chip->nchannels; i++) { + v_channel = V_CHAN(chip->chan_props[i]); + if (v_channel == param->channel) { + dt_index = i; + chan_found = true; + break; + } + } + + if (!chan_found) { + pr_err("not a valid ADC_TM channel\n"); + return -EINVAL; + } + + mutex_lock(&chip->lock); + + /* add channel client to channel list */ + adc_tm_add_to_list(chip, dt_index, param); + + /* set right thresholds for the sensor */ + ret = adc_tm_gen3_manage_thresholds(&chip->chan_props[dt_index]); + if (ret < 0) + pr_err("Error in reverse scaling:%d\n", ret); + + /* configure channel */ + ret = adc_tm5_gen3_configure(&chip->chan_props[dt_index]); + if (ret < 0) { + pr_err("Error during adc-tm configure:%d\n", ret); + goto fail_unlock; + } + +fail_unlock: + mutex_unlock(&chip->lock); + return ret; +} +EXPORT_SYMBOL(adc_tm_channel_measure_gen3); + +/* Used by non-thermal clients to release an ADC_TM channel */ +int32_t adc_tm_disable_chan_meas_gen3(struct adc5_chip *chip, + struct adc_tm_param *param) +{ + int ret, i; + uint32_t dt_index = 0, v_channel; + struct adc_tm_client_info *client_info = NULL; + + if (param == NULL) + return -EINVAL; + + for (i = 0; i < chip->nchannels; i++) { + v_channel = V_CHAN(chip->chan_props[i]); + if (v_channel == param->channel) { + dt_index = i; + break; + } + } + + if (i == chip->nchannels) { + pr_err("not a valid ADC_TM channel\n"); + return -EINVAL; + } + + mutex_lock(&chip->lock); + list_for_each_entry(client_info, + &chip->chan_props[i].thr_list, list) { + if (client_info->param->id == param->id) { + client_info->state_request = + ADC_TM_HIGH_LOW_THR_DISABLE; + ret = adc_tm_gen3_manage_thresholds(&chip->chan_props[i]); + if (ret < 0) { + pr_err("Error in reverse scaling:%d\n", + ret); + goto fail; + } + ret = adc_tm5_gen3_configure(&chip->chan_props[i]); + if (ret < 0) { + pr_err("Error during adc-tm configure:%d\n", + ret); + goto fail; + } + } + } + +fail: + mutex_unlock(&chip->lock); + return ret; +} +EXPORT_SYMBOL(adc_tm_disable_chan_meas_gen3); + +static struct thermal_zone_of_device_ops adc_tm_ops = { + .get_temp = adc_tm_gen3_get_temp, + .set_trips = adc_tm5_gen3_set_trip_temp, +}; + +static struct thermal_zone_of_device_ops adc_tm_ops_iio = { + .get_temp = adc_tm_gen3_get_temp, +}; + +static int adc_tm_register_tzd(struct adc5_chip *adc) +{ + unsigned int i, channel; + struct thermal_zone_device *tzd; + + for (i = 0; i < adc->nchannels; i++) { + channel = V_CHAN(adc->chan_props[i]); + + switch (adc->chan_props[i].adc_tm) { + case ADC_TM_NONE: + continue; + case ADC_TM: + tzd = devm_thermal_zone_of_sensor_register( + adc->dev, channel, + &adc->chan_props[i], &adc_tm_ops); + break; + case ADC_TM_IIO: + tzd = devm_thermal_zone_of_sensor_register( + adc->dev, channel, + &adc->chan_props[i], &adc_tm_ops_iio); + break; + case ADC_TM_NON_THERMAL: + tzd = NULL; + break; + default: + pr_err("Invalid ADC_TM type:%d for dt_ch:%d\n", + adc->chan_props[i].adc_tm, adc->chan_props[i].channel); + continue; + } + + if (IS_ERR(tzd)) { + pr_err("Error registering TZ zone:%ld for dt_ch:%d\n", + PTR_ERR(tzd), adc->chan_props[i].channel); + continue; + } + adc->chan_props[i].tzd = tzd; + } + + return 0; +} + +struct adc5_channels { + const char *datasheet_name; + unsigned int prescale_index; + enum iio_chan_type type; + long info_mask; + enum vadc_scale_fn_type scale_fn_type; +}; + +/* In these definitions, _pre refers to an index into adc5_prescale_ratios. */ +#define ADC5_CHAN(_dname, _type, _mask, _pre, _scale) \ + { \ + .datasheet_name = _dname, \ + .prescale_index = _pre, \ + .type = _type, \ + .info_mask = _mask, \ + .scale_fn_type = _scale, \ + }, \ + +#define ADC5_CHAN_TEMP(_dname, _pre, _scale) \ + ADC5_CHAN(_dname, IIO_TEMP, \ + BIT(IIO_CHAN_INFO_PROCESSED), \ + _pre, _scale) \ + +#define ADC5_CHAN_VOLT(_dname, _pre, _scale) \ + ADC5_CHAN(_dname, IIO_VOLTAGE, \ + BIT(IIO_CHAN_INFO_PROCESSED), \ + _pre, _scale) \ + +#define ADC5_CHAN_CUR(_dname, _pre, _scale) \ + ADC5_CHAN(_dname, IIO_CURRENT, \ + BIT(IIO_CHAN_INFO_PROCESSED), \ + _pre, _scale) \ + +static const struct adc5_channels adc5_chans_pmic[ADC5_MAX_CHANNEL] = { + [ADC5_GEN3_OFFSET_REF] = ADC5_CHAN_VOLT("ref_gnd", 0, + SCALE_HW_CALIB_DEFAULT) + [ADC5_GEN3_1P25VREF] = ADC5_CHAN_VOLT("vref_1p25", 0, + SCALE_HW_CALIB_DEFAULT) + [ADC5_GEN3_VPH_PWR] = ADC5_CHAN_VOLT("vph_pwr", 1, + SCALE_HW_CALIB_DEFAULT) + [ADC5_GEN3_VBAT_SNS_QBG] = ADC5_CHAN_VOLT("vbat_sns", 3, + SCALE_HW_CALIB_DEFAULT) + [ADC5_GEN3_AMUX3_THM] = ADC5_CHAN_TEMP("smb_temp", 0, + SCALE_HW_CALIB_PM7_SMB_TEMP) + [ADC5_GEN3_CHG_TEMP] = ADC5_CHAN_TEMP("chg_temp", 0, + SCALE_HW_CALIB_PM7_CHG_TEMP) + [ADC5_GEN3_IIN_FB] = ADC5_CHAN_CUR("iin_fb", 4, + SCALE_HW_CALIB_CUR) + [ADC5_GEN3_ICHG_SMB] = ADC5_CHAN_CUR("ichg_smb", 5, + SCALE_HW_CALIB_CUR) + [ADC5_GEN3_IIN_SMB] = ADC5_CHAN_CUR("iin_smb", 6, + SCALE_HW_CALIB_CUR) + [ADC5_GEN3_ICHG_FB] = ADC5_CHAN_CUR("ichg_fb", 7, + SCALE_HW_CALIB_CUR_RAW) + [ADC5_GEN3_DIE_TEMP] = ADC5_CHAN_TEMP("die_temp", 0, + SCALE_HW_CALIB_PMIC_THERM_PM7) + [ADC5_GEN3_AMUX1_THM_100K_PU] = ADC5_CHAN_TEMP("amux_thm1_pu2", 0, + SCALE_HW_CALIB_THERM_100K_PU_PM7) + [ADC5_GEN3_AMUX2_THM_100K_PU] = ADC5_CHAN_TEMP("amux_thm2_pu2", 0, + SCALE_HW_CALIB_THERM_100K_PU_PM7) + [ADC5_GEN3_AMUX3_THM_100K_PU] = ADC5_CHAN_TEMP("amux_thm3_pu2", 0, + SCALE_HW_CALIB_THERM_100K_PU_PM7) + [ADC5_GEN3_AMUX4_THM_100K_PU] = ADC5_CHAN_TEMP("amux_thm4_pu2", 0, + SCALE_HW_CALIB_THERM_100K_PU_PM7) + [ADC5_GEN3_AMUX5_THM_100K_PU] = ADC5_CHAN_TEMP("amux_thm5_pu2", 0, + SCALE_HW_CALIB_THERM_100K_PU_PM7) + [ADC5_GEN3_AMUX6_THM_100K_PU] = ADC5_CHAN_TEMP("amux_thm6_pu2", 0, + SCALE_HW_CALIB_THERM_100K_PU_PM7) + [ADC5_GEN3_AMUX1_GPIO_100K_PU] = ADC5_CHAN_TEMP("amux1_gpio_pu2", 0, + SCALE_HW_CALIB_THERM_100K_PU_PM7) + [ADC5_GEN3_AMUX2_GPIO_100K_PU] = ADC5_CHAN_TEMP("amux2_gpio_pu2", 0, + SCALE_HW_CALIB_THERM_100K_PU_PM7) + [ADC5_GEN3_AMUX3_GPIO_100K_PU] = ADC5_CHAN_TEMP("amux3_gpio_pu2", 0, + SCALE_HW_CALIB_THERM_100K_PU_PM7) + [ADC5_GEN3_AMUX4_GPIO_100K_PU] = ADC5_CHAN_TEMP("amux4_gpio_pu2", 0, + SCALE_HW_CALIB_THERM_100K_PU_PM7) +}; + +static int adc5_get_dt_channel_data(struct adc5_chip *adc, + struct adc5_channel_prop *prop, + struct device_node *node, + const struct adc5_data *data) +{ + const char *name = node->name, *channel_name; + u32 chan, value, varr[2]; + u32 sid = 0; + int ret; + struct device *dev = adc->dev; + + ret = of_property_read_u32(node, "reg", &chan); + if (ret < 0) { + dev_err(dev, "invalid channel number %s\n", name); + return ret; + } + + /* + * Value read from "reg" is virtual channel number + * virtual channel number = (sid << 8 | channel number). + */ + sid = (chan >> ADC5_GEN3_SID_OFFSET); + chan = (chan & ADC5_GEN3_CHANNEL_MASK); + + if (chan > ADC5_OFFSET_EXT2 || + !data->adc_chans[chan].datasheet_name) { + dev_err(dev, "%s invalid channel number %d\n", name, chan); + return -EINVAL; + } + + /* the channel has DT description */ + prop->channel = chan; + prop->sid = sid; + + channel_name = of_get_property(node, + "label", NULL) ? : node->name; + if (!channel_name) { + pr_err("Invalid channel name\n"); + return -EINVAL; + } + prop->datasheet_name = channel_name; + + ret = of_property_read_u32(node, "qcom,decimation", &value); + if (!ret) { + ret = adc5_decimation_from_dt(value, data->decimation); + if (ret < 0) { + dev_err(dev, "%02x invalid decimation %d\n", + chan, value); + return ret; + } + prop->decimation = ret; + } else { + prop->decimation = ADC5_DECIMATION_DEFAULT; + } + + ret = of_property_read_u32_array(node, "qcom,pre-scaling", varr, 2); + if (!ret) { + ret = adc5_prescaling_from_dt(varr[0], varr[1]); + if (ret < 0) { + dev_err(dev, "%02x invalid pre-scaling <%d %d>\n", + chan, varr[0], varr[1]); + return ret; + } + prop->prescale = ret; + } else { + prop->prescale = + adc->data->adc_chans[prop->channel].prescale_index; + } + + ret = of_property_read_u32(node, "qcom,hw-settle-time", &value); + if (!ret) { + ret = adc5_hw_settle_time_from_dt(value, + data->hw_settle_1); + if (ret < 0) { + dev_err(dev, "%02x invalid hw-settle-time %d us\n", + chan, value); + return ret; + } + prop->hw_settle_time = ret; + } else { + prop->hw_settle_time = VADC_DEF_HW_SETTLE_TIME; + } + + ret = of_property_read_u32(node, "qcom,avg-samples", &value); + if (!ret) { + ret = adc5_avg_samples_from_dt(value); + if (ret < 0) { + dev_err(dev, "%02x invalid avg-samples %d\n", + chan, value); + return ret; + } + prop->avg_samples = ret; + } else { + prop->avg_samples = VADC_DEF_AVG_SAMPLES; + } + + prop->scale_fn_type = -EINVAL; + ret = of_property_read_u32(node, "qcom,scale-fn-type", &value); + if (!ret && value < SCALE_HW_CALIB_INVALID) + prop->scale_fn_type = value; + + if (of_property_read_bool(node, "qcom,ratiometric")) + prop->cal_method = ADC5_RATIOMETRIC_CAL; + else + prop->cal_method = ADC5_ABSOLUTE_CAL; + + prop->timer = MEAS_INT_IMMEDIATE; + prop->adc_tm = ADC_TM_NONE; + + ret = of_property_read_u32(node, "qcom,adc-tm-type", &value); + if (!ret && value < ADC_TM_INVALID) + prop->adc_tm = value; + + if (prop->adc_tm == ADC_TM_NON_THERMAL) { + ret = of_property_read_u32(node, "qcom,rscale-type", + &prop->adc_rscale_fn); + if (ret < 0) + prop->adc_rscale_fn = SCALE_RSCALE_NONE; + } + + if (prop->adc_tm && prop->adc_tm != ADC_TM_IIO) { + adc->n_tm_channels++; + if (adc->n_tm_channels > ADC_TM5_GEN3_CHANS_MAX) { + pr_err("Number of TM nodes greater than channels supported:%d\n", + adc->n_tm_channels); + return -EINVAL; + } + prop->tm_chan_index = adc->n_tm_channels; + prop->timer = MEAS_INT_1S; + + if (prop->adc_tm == ADC_TM_NON_THERMAL) { + prop->req_wq = alloc_workqueue( + "adc_tm_notify_wq", WQ_HIGHPRI, 0); + if (!prop->req_wq) { + pr_err("Requesting priority wq failed\n"); + return -ENOMEM; + } + INIT_WORK(&prop->work, notify_adc_tm_fn); + } + INIT_LIST_HEAD(&prop->thr_list); + } + + dev_dbg(dev, "%02x name %s\n", chan, name); + + return 0; +} + +static const struct adc5_data adc5_gen3_data_pmic = { + .name = "pm-adc5-gen3", + .full_scale_code_volt = 0x70e4, + .full_scale_code_cur = 0x2710, + .adc_chans = adc5_chans_pmic, + .decimation = (unsigned int [ADC5_DECIMATION_SAMPLES_MAX]) + {85, 340, 1360}, + .hw_settle_1 = (unsigned int [VADC_HW_SETTLE_SAMPLES_MAX]) + {15, 100, 200, 300, 400, 500, 600, 700, + 1000, 2000, 4000, 8000, 16000, 32000, + 64000, 128000}, +}; + +static const struct of_device_id adc5_match_table[] = { + { + .compatible = "qcom,spmi-adc5-gen3", + .data = &adc5_gen3_data_pmic, + }, + { } +}; +MODULE_DEVICE_TABLE(of, adc5_match_table); + +static int adc5_get_dt_data(struct adc5_chip *adc, struct device_node *node) +{ + const struct adc5_channels *adc_chan; + struct iio_chan_spec *iio_chan; + struct adc5_channel_prop prop, *chan_props; + struct device_node *child; + unsigned int index = 0; + const struct of_device_id *id; + const struct adc5_data *data; + int ret; + + adc->nchannels = of_get_available_child_count(node); + if (!adc->nchannels) + return -EINVAL; + + adc->iio_chans = devm_kcalloc(adc->dev, adc->nchannels, + sizeof(*adc->iio_chans), GFP_KERNEL); + if (!adc->iio_chans) + return -ENOMEM; + + adc->chan_props = devm_kcalloc(adc->dev, adc->nchannels, + sizeof(*adc->chan_props), GFP_KERNEL); + if (!adc->chan_props) + return -ENOMEM; + + chan_props = adc->chan_props; + adc->n_tm_channels = 0; + iio_chan = adc->iio_chans; + id = of_match_node(adc5_match_table, node); + if (id) + data = id->data; + else + data = &adc5_gen3_data_pmic; + adc->data = data; + + for_each_available_child_of_node(node, child) { + ret = adc5_get_dt_channel_data(adc, &prop, child, data); + if (ret < 0) { + of_node_put(child); + return ret; + } + + prop.chip = adc; + if (prop.scale_fn_type == -EINVAL) + prop.scale_fn_type = + data->adc_chans[prop.channel].scale_fn_type; + *chan_props = prop; + adc_chan = &data->adc_chans[prop.channel]; + + iio_chan->channel = prop.channel; + iio_chan->datasheet_name = prop.datasheet_name; + iio_chan->extend_name = prop.datasheet_name; + iio_chan->info_mask_separate = adc_chan->info_mask; + iio_chan->type = adc_chan->type; + iio_chan->address = index; + iio_chan++; + chan_props++; + index++; + } + + return 0; +} + +static int adc5_gen3_probe(struct platform_device *pdev) +{ + struct device_node *node = pdev->dev.of_node; + struct device *dev = &pdev->dev; + struct iio_dev *indio_dev; + struct adc5_chip *adc; + struct regmap *regmap; + const char *irq_name; + const __be32 *prop_addr; + int ret, irq_eoc, i; + u32 reg; + + regmap = dev_get_regmap(dev->parent, NULL); + if (!regmap) + return -ENODEV; + + ret = of_property_read_u32(node, "reg", ®); + if (ret < 0) + return ret; + + indio_dev = devm_iio_device_alloc(dev, sizeof(*adc)); + if (!indio_dev) + return -ENOMEM; + + adc = iio_priv(indio_dev); + adc->regmap = regmap; + adc->dev = dev; + + prop_addr = of_get_address(dev->of_node, 0, NULL, NULL); + if (!prop_addr) { + pr_err("invalid IO resource\n"); + return -EINVAL; + } + adc->base = be32_to_cpu(*prop_addr); + + prop_addr = of_get_address(dev->of_node, 1, NULL, NULL); + if (!prop_addr) + pr_debug("invalid debug resource\n"); + else + adc->debug_base = be32_to_cpu(*prop_addr); + + platform_set_drvdata(pdev, adc); + + indio_dev->info = &adc5_gen3_info; + + init_completion(&adc->complete); + mutex_init(&adc->lock); + + ret = adc5_get_dt_data(adc, node); + if (ret < 0) { + pr_err("adc get dt data failed\n"); + goto fail; + } + + adc_tm_register_tzd(adc); + + irq_eoc = platform_get_irq(pdev, 0); + irq_name = "pm-adc5"; + if (adc->data->name) + irq_name = adc->data->name; + + ret = devm_request_irq(dev, irq_eoc, adc5_gen3_isr, 0, + irq_name, adc); + if (ret < 0) + goto fail; + + if (adc->n_tm_channels) + INIT_WORK(&adc->tm_handler_work, tm_handler_work); + + indio_dev->dev.parent = dev; + indio_dev->dev.of_node = node; + indio_dev->name = pdev->name; + indio_dev->modes = INDIO_DIRECT_MODE; + indio_dev->channels = adc->iio_chans; + indio_dev->num_channels = adc->nchannels; + + list_add_tail(&adc->list, &adc_tm_device_list); + adc->device_list = &adc_tm_device_list; + + return devm_iio_device_register(dev, indio_dev); + +fail: + i = 0; + while (i < adc->nchannels) { + if (adc->chan_props[i].req_wq) + destroy_workqueue(adc->chan_props[i].req_wq); + i++; + } + return ret; +} + +static int adc5_gen3_exit(struct platform_device *pdev) +{ + struct adc5_chip *adc = platform_get_drvdata(pdev); + u8 data = 0; + int i; + + mutex_lock(&adc->lock); + for (i = 0; i < adc->nchannels; i++) { + if (adc->chan_props[i].req_wq) + destroy_workqueue(adc->chan_props[i].req_wq); + adc->chan_props[i].timer = MEAS_INT_DISABLE; + } + + /* Disable all available channels */ + for (i = 0; i < 8; i++) { + data = MEAS_INT_DISABLE; + adc5_write(adc, ADC5_GEN3_TIMER_SEL, &data, 1); + + /* To indicate there is an actual conversion request */ + data = ADC5_GEN3_CHAN_CONV_REQ | i; + adc5_write(adc, ADC5_GEN3_PERPH_CH, &data, 1); + + data = ADC5_GEN3_CONV_REQ_REQ; + adc5_write(adc, ADC5_GEN3_CONV_REQ, &data, 1); + } + + mutex_unlock(&adc->lock); + + if (adc->n_tm_channels) + cancel_work_sync(&adc->tm_handler_work); + + mutex_destroy(&adc->lock); + + list_del(&adc->list); + + return 0; +} + +static struct platform_driver adc5_gen3_driver = { + .driver = { + .name = "qcom-spmi-adc5-gen3.c", + .of_match_table = adc5_match_table, + }, + .probe = adc5_gen3_probe, + .remove = adc5_gen3_exit, +}; +module_platform_driver(adc5_gen3_driver); + +MODULE_ALIAS("platform:qcom-spmi-adc5-gen3"); +MODULE_DESCRIPTION("Qualcomm Technologies Inc. PMIC5 Gen3 ADC driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/iio/adc/qcom-vadc-common.c b/drivers/iio/adc/qcom-vadc-common.c index 3e81aebfb018..d88e530021f7 100644 --- a/drivers/iio/adc/qcom-vadc-common.c +++ b/drivers/iio/adc/qcom-vadc-common.c @@ -1139,6 +1139,95 @@ static int qcom_vadc_scale_hw_chg5_temp( return 0; } +void adc_tm_scale_therm_voltage_100k_gen3(struct adc_tm_config *param) +{ + int temp, ret; + int64_t resistance = 0; + + /* + * High temperature maps to lower threshold voltage. + * Same API can be used for resistance-temperature table + */ + resistance = qcom_vadc_map_temp_voltage(adcmap7_100k, + ARRAY_SIZE(adcmap7_100k), + param->high_thr_temp); + + param->low_thr_voltage = resistance * RATIO_MAX_ADC7; + param->low_thr_voltage = div64_s64(param->low_thr_voltage, + (resistance + R_PU_100K)); + + /* + * low_thr_voltage is ADC raw code corresponding to upper temperature + * threshold. + * Instead of returning the ADC raw code obtained at this point,we first + * do a forward conversion on the (low voltage / high temperature) threshold code, + * to temperature, to check if that code, when read by TM, would translate to + * a temperature greater than or equal to the upper temperature limit (which is + * expected). If it is instead lower than the upper limit (not expected for correct + * TM functionality), we lower the raw code of the threshold written by 1 + * to ensure TM does see a violation when it reads raw code corresponding + * to the upper limit temperature specified. + */ + ret = qcom_vadc7_scale_hw_calib_therm(NULL, NULL, param->low_thr_voltage, &temp); + if (ret < 0) + return; + + if (temp < param->high_thr_temp) + param->low_thr_voltage--; + + /* + * Low temperature maps to higher threshold voltage + * Same API can be used for resistance-temperature table + */ + resistance = qcom_vadc_map_temp_voltage(adcmap7_100k, + ARRAY_SIZE(adcmap7_100k), + param->low_thr_temp); + + param->high_thr_voltage = resistance * RATIO_MAX_ADC7; + param->high_thr_voltage = div64_s64(param->high_thr_voltage, + (resistance + R_PU_100K)); + + /* + * high_thr_voltage is ADC raw code corresponding to lower temperature + * threshold. + * Similar to what is done above for low_thr voltage, we first + * do a forward conversion on the (high voltage / low temperature)threshold code, + * to temperature, to check if that code, when read by TM, would translate to a + * temperature less than or equal to the lower temperature limit (which is expected). + * If it is instead greater than the lower limit (not expected for correct + * TM functionality), we increase the raw code of the threshold written by 1 + * to ensure TM does see a violation when it reads raw code corresponding + * to the lower limit temperature specified. + */ + ret = qcom_vadc7_scale_hw_calib_therm(NULL, NULL, param->high_thr_voltage, &temp); + if (ret < 0) + return; + + if (temp > param->low_thr_temp) + param->high_thr_voltage++; +} +EXPORT_SYMBOL(adc_tm_scale_therm_voltage_100k_gen3); + +int32_t adc_tm_absolute_rthr_gen3(struct adc_tm_config *tm_config) +{ + int64_t low_thr = 0, high_thr = 0; + + low_thr = tm_config->low_thr_voltage; + low_thr *= ADC5_FULL_SCALE_CODE; + + low_thr = div64_s64(low_thr, ADC_VDD_REF); + tm_config->low_thr_voltage = low_thr; + + high_thr = tm_config->high_thr_voltage; + high_thr *= ADC5_FULL_SCALE_CODE; + + high_thr = div64_s64(high_thr, ADC_VDD_REF); + tm_config->high_thr_voltage = high_thr; + + return 0; +} +EXPORT_SYMBOL(adc_tm_absolute_rthr_gen3); + int qcom_vadc_scale(enum vadc_scale_fn_type scaletype, const struct vadc_linear_graph *calib_graph, const struct u32_fract *prescale, diff --git a/include/linux/adc-tm-clients.h b/include/linux/adc-tm-clients.h new file mode 100644 index 000000000000..3a84cea95e8b --- /dev/null +++ b/include/linux/adc-tm-clients.h @@ -0,0 +1,102 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Copyright (c) 2012-2021, The Linux Foundation. All rights reserved. + */ + +#ifndef __QCOM_ADC_TM_H_CLIENTS__ +#define __QCOM_ADC_TM_H_CLIENTS__ + +#include +#include + +struct adc_tm_chip; +struct adc5_chip; + +/** + * enum adc_tm_state - This lets the client know whether the threshold + * that was crossed was high/low. + * %ADC_TM_HIGH_STATE: Client is notified of crossing the requested high + * voltage threshold. + * %ADC_TM_COOL_STATE: Client is notified of crossing the requested cool + * temperature threshold. + * %ADC_TM_LOW_STATE: Client is notified of crossing the requested low + * voltage threshold. + * %ADC_TM_WARM_STATE: Client is notified of crossing the requested high + * temperature threshold. + */ +enum adc_tm_state { + ADC_TM_HIGH_STATE = 0, + ADC_TM_COOL_STATE = ADC_TM_HIGH_STATE, + ADC_TM_LOW_STATE, + ADC_TM_WARM_STATE = ADC_TM_LOW_STATE, + ADC_TM_STATE_NUM, +}; + +/** + * enum adc_tm_state_request - Request to enable/disable the corresponding + * high/low voltage/temperature thresholds. + * %ADC_TM_HIGH_THR_ENABLE: Enable high voltage threshold. + * %ADC_TM_COOL_THR_ENABLE = Enables cool temperature threshold. + * %ADC_TM_LOW_THR_ENABLE: Enable low voltage/temperature threshold. + * %ADC_TM_WARM_THR_ENABLE = Enables warm temperature threshold. + * %ADC_TM_HIGH_LOW_THR_ENABLE: Enable high and low voltage/temperature + * threshold. + * %ADC_TM_HIGH_THR_DISABLE: Disable high voltage/temperature threshold. + * %ADC_TM_COOL_THR_ENABLE = Disables cool temperature threshold. + * %ADC_TM_LOW_THR_DISABLE: Disable low voltage/temperature threshold. + * %ADC_TM_WARM_THR_ENABLE = Disables warm temperature threshold. + * %ADC_TM_HIGH_THR_DISABLE: Disable high and low voltage/temperature + * threshold. + */ +enum adc_tm_state_request { + ADC_TM_HIGH_THR_ENABLE = 0, + ADC_TM_COOL_THR_ENABLE = ADC_TM_HIGH_THR_ENABLE, + ADC_TM_LOW_THR_ENABLE, + ADC_TM_WARM_THR_ENABLE = ADC_TM_LOW_THR_ENABLE, + ADC_TM_HIGH_LOW_THR_ENABLE, + ADC_TM_HIGH_THR_DISABLE, + ADC_TM_COOL_THR_DISABLE = ADC_TM_HIGH_THR_DISABLE, + ADC_TM_LOW_THR_DISABLE, + ADC_TM_WARM_THR_DISABLE = ADC_TM_LOW_THR_DISABLE, + ADC_TM_HIGH_LOW_THR_DISABLE, + ADC_TM_THR_NUM, +}; + +struct adc_tm_param { + unsigned long id; + int low_thr; + int high_thr; + uint32_t channel; + enum adc_tm_state_request state_request; + void *btm_ctx; + void (*threshold_notification)(enum adc_tm_state state, + void *ctx); +}; + +struct device; + +/* Public API */ + +#if IS_ENABLED(CONFIG_QCOM_SPMI_ADC5_GEN3) +struct adc5_chip *get_adc_tm_gen3(struct device *dev, const char *name); +int32_t adc_tm_channel_measure_gen3(struct adc5_chip *chip, + struct adc_tm_param *param); +int32_t adc_tm_disable_chan_meas_gen3(struct adc5_chip *chip, + struct adc_tm_param *param); +#else +static inline struct adc5_chip *get_adc_tm_gen3( + struct device *dev, const char *name) +{ return ERR_PTR(-ENXIO); } + +static inline int32_t adc_tm_channel_measure_gen3( + struct adc5_chip *chip, + struct adc_tm_param *param) +{ return -ENXIO; } +static inline int32_t adc_tm_disable_chan_meas_gen3( + struct adc5_chip *chip, + struct adc_tm_param *param) +{ return -ENXIO; } + +#endif + +#endif /* __QCOM_ADC_TM_H_CLIENTS__ */ diff --git a/include/linux/iio/adc/qcom-vadc-common.h b/include/linux/iio/adc/qcom-vadc-common.h index c58870892a30..6c5f7ed4cf64 100644 --- a/include/linux/iio/adc/qcom-vadc-common.h +++ b/include/linux/iio/adc/qcom-vadc-common.h @@ -6,6 +6,7 @@ #ifndef QCOM_VADC_COMMON_H #define QCOM_VADC_COMMON_H +#include #include #include @@ -60,6 +61,8 @@ #define R_PU_100K 100000 #define RATIO_MAX_ADC7 BIT(14) +#define ADC_VDD_REF 1875000 + /* * VADC_CALIB_ABSOLUTE: uses the 625mV and 1.25V as reference channels. * VADC_CALIB_RATIOMETRIC: uses the reference voltage (1.8V) and GND for @@ -85,6 +88,54 @@ struct vadc_linear_graph { s32 gnd; }; +/** + * enum adc_tm_rscale_fn_type - Scaling function used to convert the + * channels input voltage/temperature to corresponding ADC code that is + * applied for thresholds. Check the corresponding channels scaling to + * determine the appropriate temperature/voltage units that are passed + * to the scaling function. Example battery follows the power supply + * framework that needs its units to be in decidegreesC so it passes + * deci-degreesC. PA_THERM clients pass the temperature in degrees. + * The order below should match the one in the driver for + * adc_tm_rscale_fn[]. + */ +enum adc_tm_rscale_fn_type { + SCALE_R_ABSOLUTE = 0, + SCALE_RSCALE_NONE, +}; + +/** + * struct adc_tm_config - Represent ADC Thermal Monitor configuration. + * @high_thr_temp: Temperature at which high threshold notification is required. + * @low_thr_temp: Temperature at which low threshold notification is required. + * @low_thr_voltage : Low threshold voltage ADC code used for reverse + * calibration. + * @high_thr_voltage: High threshold voltage ADC code used for reverse + * calibration. + */ +struct adc_tm_config { + int high_thr_temp; + int low_thr_temp; + int64_t high_thr_voltage; + int64_t low_thr_voltage; +}; + +struct adc_tm_reverse_scale_fn { + int32_t (*chan)(struct adc_tm_config *tm_config); +}; + +struct adc_tm_client_info { + struct list_head list; + struct adc_tm_param *param; + int32_t low_thr_requested; + int32_t high_thr_requested; + bool notify_low_thr; + bool notify_high_thr; + bool high_thr_set; + bool low_thr_set; + enum adc_tm_state_request state_request; +}; + /** * enum vadc_scale_fn_type - Scaling function to convert ADC code to * physical scaled units for the channel. @@ -201,4 +252,8 @@ int qcom_adc5_decimation_from_dt(u32 value, const unsigned int *decimation); int qcom_vadc_decimation_from_dt(u32 value); +void adc_tm_scale_therm_voltage_100k_gen3(struct adc_tm_config *param); + +int32_t adc_tm_absolute_rthr_gen3(struct adc_tm_config *tm_config); + #endif /* QCOM_VADC_COMMON_H */