diff --git a/drivers/hwmon/Kconfig b/drivers/hwmon/Kconfig index f2b038fa3b84..9d41ef85a197 100644 --- a/drivers/hwmon/Kconfig +++ b/drivers/hwmon/Kconfig @@ -1571,6 +1571,18 @@ config SENSORS_PWM_FAN This driver can also be built as a module. If so, the module will be called pwm-fan. +config SENSORS_QTI_AMOLED_ECM + tristate "Qualcomm Technologies, Inc. AMOLED Current Monitor" + depends on SPMI + help + Qualcomm Technologies, Inc. ECM driver supports measurement of + OLED display power with a time granularity in sub-frame or + multiple frames of image data. The measurement can be for a short + period (N frames) or a long period (continuous mode). + + To compile this driver as module, choose M here: the module will + be called qti_amoled_ecm. + config SENSORS_RASPBERRYPI_HWMON tristate "Raspberry Pi voltage monitor" depends on RASPBERRYPI_FIRMWARE || (COMPILE_TEST && !RASPBERRYPI_FIRMWARE) diff --git a/drivers/hwmon/Makefile b/drivers/hwmon/Makefile index 8a03289e2aa4..0bc0b5b7c4d2 100644 --- a/drivers/hwmon/Makefile +++ b/drivers/hwmon/Makefile @@ -167,6 +167,7 @@ obj-$(CONFIG_SENSORS_PC87427) += pc87427.o obj-$(CONFIG_SENSORS_PCF8591) += pcf8591.o obj-$(CONFIG_SENSORS_POWR1220) += powr1220.o obj-$(CONFIG_SENSORS_PWM_FAN) += pwm-fan.o +obj-$(CONFIG_SENSORS_QTI_AMOLED_ECM) += qti_amoled_ecm.o obj-$(CONFIG_SENSORS_RASPBERRYPI_HWMON) += raspberrypi-hwmon.o obj-$(CONFIG_SENSORS_S3C) += s3c-hwmon.o obj-$(CONFIG_SENSORS_SBTSI) += sbtsi_temp.o diff --git a/drivers/hwmon/qti_amoled_ecm.c b/drivers/hwmon/qti_amoled_ecm.c new file mode 100644 index 000000000000..8713e80846e6 --- /dev/null +++ b/drivers/hwmon/qti_amoled_ecm.c @@ -0,0 +1,989 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (c) 2020-2021, The Linux Foundation. All rights reserved. + */ + +#define pr_fmt(fmt) "AMOLED_ECM: %s: " fmt, __func__ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +/* AMOLED AB register definitions */ +#define AB_REVISION2 0x01 + +/* AMOLED ECM register definitions */ +#define AB_ECM_EN_CTL 0xA0 + #define ECM_EN BIT(7) + +#define AB_ECM_COUNTER_CTL 0xA1 + #define ECM_COUNTER_START BIT(7) + +/* AMOLED ECM SDAM Offsets */ +#define ECM_SDAM_START_BASE 0x40 +#define ECM_SDAMX_SAMPLE_START_ADDR 0x46 + +#define ECM_FAULT_LOG 0x48 +#define ECM_ROUTINE_LOG 0x49 + +#define ECM_ACTIVE_SDAM 0x4D + #define ECM_SDAM0_ACTIVE BIT(0) + #define ECM_SDAM1_ACTIVE BIT(1) + +#define ECM_SAMPLE_CNT_LSB 0x4E +#define ECM_SAMPLE_CNT_MSB 0x4F + +#define ECM_STATUS_SET 0x50 +#define ECM_STATUS_CLR 0x51 + #define ECM_ONGOING BIT(0) + #define ECM_DONE BIT(1) + #define ECM_ABORT BIT(2) + #define ECM_SDAM0_FULL BIT(3) + #define ECM_SDAM1_FULL BIT(4) + +#define ECM_SDAM0_INDEX 0x52 +#define ECM_SDAM1_INDEX 0x53 +#define ECM_SDAM2_INDEX 0x61 + +#define ECM_MODE 0x54 + #define ECM_CONTINUOUS 0 + #define ECM_N_ESWIRE 1 + #define ECM_M_ASWIRE 2 + #define ECM_ESWIRE_ASWIRE 3 + #define ECM_USE_TIMER 4 + +#define ECM_N_ESWIRE_COUNT_LSB 0x55 +#define ECM_N_ESWIRE_COUNT_MSB 0x56 +#define ECM_M_ASWIRE_COUNT_LSB 0x57 +#define ECM_M_ASWIRE_COUNT_MSB 0x58 +#define ECM_ESWIRE_ASWIRE_SKIP_COUNT_LSB 0x59 +#define ECM_ESWIRE_ASWIRE_SKIP_COUNT_MSB 0x5A +#define ECM_TIMER_LSB 0x5B +#define ECM_TIMER_MSB 0x5C +#define ECM_TIMER_SKIP_LSB 0x5D +#define ECM_TIMER_SKIP_MSB 0x5E + +#define ECM_SEND_IRQ 0x5F + #define SEND_SDAM0_IRQ BIT(0) + #define SEND_SDAM1_IRQ BIT(1) + #define SEND_SDAM2_IRQ BIT(2) + +#define ECM_WRITE_TO_SDAM 0x60 + #define WRITE_SDAM0_DATA BIT(0) + #define WRITE_SDAM1_DATA BIT(1) + #define WRITE_SDAM2_DATA BIT(2) + #define OVERWRITE_SDAM0_DATA BIT(4) + #define OVERWRITE_SDAM1_DATA BIT(5) + #define OVERWRITE_SDAM2_DATA BIT(6) + +#define ECM_AVERAGE_LSB 0x61 +#define ECM_AVERAGE_MSB 0x62 +#define ECM_MIN_LSB 0x63 +#define ECM_MIN_MSB 0x64 +#define ECM_MAX_LSB 0x65 +#define ECM_MAX_MSB 0x66 + +#define ECM_SDAM0_SAMPLE_START_ADDR 0x6C +#define ECM_SDAM_SAMPLE_END_ADDR 0xBF + +/* ECM specific definitions */ +#define ECM_SAMPLE_GAIN_V1 15 +#define ECM_SAMPLE_GAIN_V2 16 +#define ECM_MIN_M_SAMPLES 10 +#define AMOLED_AB_REVISION_1P0 0 +#define AMOLED_AB_REVISION_2P0 1 + +enum amoled_ecm_mode { + ECM_MODE_CONTINUOUS = 0, + ECM_MODE_MULTI_FRAMES, + ECM_MODE_IDLE, +}; + +struct amoled_ecm_sdam_config { + u8 reg; + u8 reset_val; +}; + +/** + * struct amoled_ecm_sdam - AMOLED ECM sdam data structure + * @nvmem: Pointer to nvmem device + * @start_addr: Start address of ECM samples in SDAM + * @irq_name: Interrupt name for SDAM + * @irq: Interrupt associated with the SDAM + */ +struct amoled_ecm_sdam { + struct nvmem_device *nvmem; + u32 start_addr; + const char *irq_name; + int irq; +}; + +/** + * struct amoled_ecm_data - Structure for AMOLED ECM data + * @m_cumulative: Cumulative of M sample values + * @num_m_samples: Number of M samples available + * @time_period_ms: Time period(in milli seconds) for ECM request + * @frames: Number of frames for ECM request + * @avg_current: AMOLED ECM average calculated + * @mode: AMOLED ECM mode of operation + */ +struct amoled_ecm_data { + unsigned long long m_cumulative; + u32 num_m_samples; + u32 time_period_ms; + u16 frames; + u16 avg_current; + enum amoled_ecm_mode mode; +}; + +/** + * struct amoled ecm - Structure for AMOLED ECM device + * @regmap: Pointer for regmap structure + * @dev: Pointer for AMOLED ECM device + * @data: AMOLED ECM data structure + * @sdam: Pointer for array of ECM sdams + * @sdam_lock: Locking for mutual exclusion + * @average_work: Delayed work to calculate ECM average + * @active_panel: Active DRM panel which sends panel notifications + * @notifier_cookie: The cookie from panel notifier + * @num_sdams: Number of SDAMs used for AMOLED ECM + * @base: Base address of the AMOLED ECM module + * @ab_revision: Revision of the AMOLED AB module + * @enable: Flag to enable/disable AMOLED ECM + * @abort: Flag to indicated AMOLED ECM has aborted + * @reenable: Flag to reenable ECM when display goes unblank + */ +struct amoled_ecm { + struct regmap *regmap; + struct device *dev; + struct amoled_ecm_data data; + struct amoled_ecm_sdam *sdam; + struct mutex sdam_lock; + struct delayed_work average_work; + struct drm_panel *active_panel; + void *notifier_cookie; + u32 num_sdams; + u32 base; + u8 ab_revision; + bool enable; + bool abort; + bool reenable; +}; + +static struct amoled_ecm_sdam_config ecm_reset_config[] = { + { ECM_FAULT_LOG, 0x00 }, + { ECM_ROUTINE_LOG, 0x00 }, + { ECM_ACTIVE_SDAM, 0x01 }, + { ECM_SAMPLE_CNT_LSB, 0x00 }, + { ECM_SAMPLE_CNT_MSB, 0x00 }, + { ECM_STATUS_SET, 0x00 }, + { ECM_STATUS_CLR, 0xFF }, + { ECM_SDAM0_INDEX, 0x6C }, + { ECM_SDAM1_INDEX, 0x46 }, + { ECM_SDAM2_INDEX, 0x46 }, + { ECM_MODE, 0x00 }, +}; + +static int ecm_nvmem_device_write(struct nvmem_device *nvmem, + unsigned int offset, + size_t bytes, void *buf) +{ + size_t i; + u8 *ptr = buf; + + for (i = 0; i < bytes; i++) + pr_debug("Wrote %#x to %#x\n", *ptr++, offset + i); + + return nvmem_device_write(nvmem, offset, bytes, buf); +} + +static int ecm_reset_sdam_config(struct amoled_ecm *ecm) +{ + int rc, i; + u8 val = 0, val2 = 0; + + for (i = 0; i < ARRAY_SIZE(ecm_reset_config); i++) { + rc = ecm_nvmem_device_write(ecm->sdam[0].nvmem, + ecm_reset_config[i].reg, + 1, &ecm_reset_config[i].reset_val); + if (rc < 0) { + pr_err("Failed to write %u to SDAM, rc=%d\n", + ecm_reset_config[i].reg, rc); + return rc; + } + } + + for (i = 0; i < ecm->num_sdams; i++) { + val |= (SEND_SDAM0_IRQ << i); + val2 |= (WRITE_SDAM0_DATA << i); + } + + rc = ecm_nvmem_device_write(ecm->sdam[0].nvmem, ECM_SEND_IRQ, 1, &val); + if (rc < 0) { + pr_err("Failed to write %u to ECM_SEND_IRQ, rc=%d\n", val, rc); + return rc; + } + + rc = ecm_nvmem_device_write(ecm->sdam[0].nvmem, ECM_WRITE_TO_SDAM, 1, + &val2); + if (rc < 0) + pr_err("Failed to write %u to ECM_WRITE_TO_SDAM, rc=%d\n", val2, + rc); + + usleep_range(10000, 12000); + + return rc; +} + +static int amoled_ecm_enable(struct amoled_ecm *ecm) +{ + struct amoled_ecm_data *data = &ecm->data; + int rc; + + if (data->frames) { + rc = ecm_nvmem_device_write(ecm->sdam[0].nvmem, + ECM_N_ESWIRE_COUNT_LSB, 2, &data->frames); + if (rc < 0) { + pr_err("Failed to write swire count to SDAM, rc=%d\n", + rc); + return rc; + } + + data->mode = ECM_MODE_MULTI_FRAMES; + } else { + if (!data->time_period_ms) + return -EINVAL; + + data->mode = ECM_MODE_CONTINUOUS; + } + + if ((ecm->ab_revision != AMOLED_AB_REVISION_1P0) && + (ecm->ab_revision != AMOLED_AB_REVISION_2P0)) { + pr_err("ECM is not supported for AB version %u\n", + ecm->ab_revision); + return -ENODEV; + } + + rc = ecm_reset_sdam_config(ecm); + if (rc < 0) { + pr_err("Failed to reset ECM SDAM configuration, rc=%d\n", rc); + return rc; + } + + rc = ecm_nvmem_device_write(ecm->sdam[0].nvmem, ECM_MODE, 1, + &data->mode); + if (rc < 0) { + pr_err("Failed to write ECM mode to SDAM, rc=%d\n", rc); + return rc; + } + + rc = regmap_write(ecm->regmap, ecm->base + AB_ECM_EN_CTL, ECM_EN); + if (rc < 0) { + pr_err("Failed to enable ECM, rc=%d\n", rc); + return rc; + } + + rc = regmap_write(ecm->regmap, ecm->base + AB_ECM_COUNTER_CTL, + ECM_COUNTER_START); + if (rc < 0) { + pr_err("Failed to enable ECM counter, rc=%d\n", rc); + return rc; + } + + if (data->mode == ECM_MODE_CONTINUOUS) + schedule_delayed_work(&ecm->average_work, + msecs_to_jiffies(data->time_period_ms)); + + ecm->enable = true; + + return rc; +} + +static int amoled_ecm_disable(struct amoled_ecm *ecm) +{ + int rc; + + rc = regmap_write(ecm->regmap, ecm->base + AB_ECM_COUNTER_CTL, 0); + if (rc < 0) { + pr_err("Failed to disable ECM counter, rc=%d\n", rc); + return rc; + } + + rc = regmap_write(ecm->regmap, ecm->base + AB_ECM_EN_CTL, 0); + if (rc < 0) { + pr_err("Failed to disable ECM, rc=%d\n", rc); + return rc; + } + + rc = ecm_nvmem_device_write(ecm->sdam[0].nvmem, ECM_AVERAGE_LSB, 2, + &ecm->data.avg_current); + if (rc < 0) { + pr_err("Failed to write ECM average to SDAM, rc=%d\n", rc); + return rc; + } + pr_debug("ECM_AVERAGE:%u\n", ecm->data.avg_current); + + cancel_delayed_work(&ecm->average_work); + + ecm->data.avg_current = 0; + ecm->data.m_cumulative = 0; + ecm->data.num_m_samples = 0; + ecm->data.mode = ECM_MODE_IDLE; + + ecm->abort = false; + ecm->enable = false; + + return rc; +} + +static void ecm_average_work(struct work_struct *work) +{ + struct amoled_ecm *ecm = container_of(work, struct amoled_ecm, + average_work.work); + struct amoled_ecm_data *data = &ecm->data; + + mutex_lock(&ecm->sdam_lock); + + if (!data->num_m_samples || !data->m_cumulative) { + pr_warn_ratelimited("Invalid data, num_m_samples=%u m_cumulative:%u\n", + data->num_m_samples, data->m_cumulative); + data->avg_current = 0; + } else { + data->avg_current = data->m_cumulative / data->num_m_samples; + pr_debug("avg_current=%u mA\n", data->avg_current); + } + + data->m_cumulative = 0; + data->num_m_samples = 0; + + mutex_unlock(&ecm->sdam_lock); + + /* + * If ECM is not aborted and still enabled, run it one more time + */ + + if (!ecm->abort && ecm->enable) + schedule_delayed_work(&ecm->average_work, + msecs_to_jiffies(ecm->data.time_period_ms)); +} + +static ssize_t enable_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct amoled_ecm *ecm = dev_get_drvdata(dev); + + return scnprintf(buf, PAGE_SIZE, "%d\n", ecm->enable); +} + +static ssize_t enable_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct amoled_ecm *ecm = dev_get_drvdata(dev); + bool val; + int rc; + + rc = kstrtobool(buf, &val); + if (rc < 0) + return rc; + + if (ecm->enable == val) { + pr_err("AMOLED ECM is already %s\n", + val ? "enabled" : "disabled"); + return -EINVAL; + } + + if (val) { + rc = amoled_ecm_enable(ecm); + if (rc < 0) { + pr_err("Failed to enable AMOLED ECM, rc=%d\n", rc); + return rc; + } + } else { + rc = amoled_ecm_disable(ecm); + if (rc < 0) { + pr_err("Failed to disable AMOLED ECM, rc=%d\n", rc); + return rc; + } + + ecm->data.frames = 0; + ecm->data.time_period_ms = 0; + } + + return count; +} +static DEVICE_ATTR_RW(enable); + +static ssize_t frames_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct amoled_ecm *ecm = dev_get_drvdata(dev); + + return scnprintf(buf, PAGE_SIZE, "%u\n", ecm->data.frames); +} + +static ssize_t frames_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct amoled_ecm *ecm = dev_get_drvdata(dev); + u16 val; + int rc; + + if (ecm->enable) { + pr_err("Failed to configure frames, ECM is already running\n"); + return -EINVAL; + } + + rc = kstrtou16(buf, 0, &val); + if ((rc < 0) || !val) + return -EINVAL; + + ecm->data.frames = val; + + return count; +} +static DEVICE_ATTR_RW(frames); + +static ssize_t time_period_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct amoled_ecm *ecm = dev_get_drvdata(dev); + + return scnprintf(buf, PAGE_SIZE, "%u\n", ecm->data.time_period_ms); +} + +static ssize_t time_period_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct amoled_ecm *ecm = dev_get_drvdata(dev); + u32 val; + int rc; + + if (ecm->enable) { + pr_err("Failed to configure time_period, ECM is already running\n"); + return -EINVAL; + } + + rc = kstrtou32(buf, 0, &val); + if ((rc < 0) || !val) + return -EINVAL; + + ecm->data.time_period_ms = val; + + return count; +} +static DEVICE_ATTR_RW(time_period); + +static ssize_t avg_current_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct amoled_ecm *ecm = dev_get_drvdata(dev); + + return scnprintf(buf, PAGE_SIZE, "%u\n", ecm->data.avg_current); +} +static DEVICE_ATTR_RO(avg_current); + +static struct attribute *amoled_ecm_attrs[] = { + &dev_attr_enable.attr, + &dev_attr_frames.attr, + &dev_attr_time_period.attr, + &dev_attr_avg_current.attr, + NULL, +}; + +static const struct attribute_group amoled_ecm_group = { + .name = "amoled_ecm", + .attrs = amoled_ecm_attrs, +}; +__ATTRIBUTE_GROUPS(amoled_ecm); + +static int get_sdam_from_irq(struct amoled_ecm *ecm, int irq) +{ + int i; + + for (i = 0; i < ecm->num_sdams; i++) + if (ecm->sdam[i].irq == irq) + return i; + + return -ENOENT; +} + +static int handle_ecm_abort(struct amoled_ecm *ecm) +{ + struct amoled_ecm_data *data = &ecm->data; + int rc; + u8 mode = data->mode; + + switch (mode) { + case ECM_MODE_MULTI_FRAMES: + pr_warn_ratelimited("Multiple frames mode is not supported\n"); + data->avg_current = 0; + break; + case ECM_MODE_CONTINUOUS: + if (data->num_m_samples < ECM_MIN_M_SAMPLES) { + pr_warn_ratelimited("Too few samples %u for continuous mode\n", + data->num_m_samples); + data->avg_current = 0; + break; + } + + ecm->abort = true; + schedule_delayed_work(&ecm->average_work, 0); + break; + default: + pr_err_ratelimited("Invalid ECM operation mode: %u\n", mode); + data->avg_current = 0; + return -EINVAL; + } + + rc = amoled_ecm_disable(ecm); + if (rc < 0) + pr_err("Failed to disable AMOLED ECM, rc=%d\n", rc); + + return rc; +} + +static int get_sdam_index(struct nvmem_device *nvmem, int sdam_num, u8 *index) +{ + unsigned int addr; + + switch (sdam_num) { + case 0: + addr = ECM_SDAM0_INDEX; + break; + case 1: + addr = ECM_SDAM1_INDEX; + break; + case 2: + addr = ECM_SDAM2_INDEX; + break; + default: + return -EINVAL; + } + + return nvmem_device_read(nvmem, addr, 1, index); +} + +static irqreturn_t sdam_full_irq_handler(int irq, void *_ecm) +{ + struct amoled_ecm *ecm = _ecm; + struct amoled_ecm_data *data = &ecm->data; + u64 cumulative = 0, m_sample; + int rc, i, sdam_num, sdam_start, num_ecm_samples, max_samples; + u16 ecm_sample, gain; + u8 buf[2], int_status, sdam_index, overwrite; + + sdam_num = get_sdam_from_irq(ecm, irq); + if (sdam_num < 0) { + pr_err("Invalid SDAM interrupt, err=%d\n", sdam_num); + return IRQ_HANDLED; + } + + rc = nvmem_device_read(ecm->sdam[0].nvmem, ECM_STATUS_SET, 1, + &int_status); + if (rc < 0) { + pr_err("Failed to read interrupt status from SDAM, rc=%d\n", + rc); + return IRQ_HANDLED; + } + + pr_debug("ECM_STATUS_SET: %#x\n", int_status); + + if (data->mode != ECM_MODE_CONTINUOUS && + data->mode != ECM_MODE_MULTI_FRAMES) + return IRQ_HANDLED; + + if (int_status & ECM_ABORT) { + rc = handle_ecm_abort(ecm); + if (rc < 0) { + pr_err("Failed to handle ECM_ABORT interrupt, rc=%d\n", + rc); + return IRQ_HANDLED; + } + } + + rc = get_sdam_index(ecm->sdam[0].nvmem, sdam_num, &sdam_index); + if (rc < 0) { + pr_err("Failed to read SDAM index, rc=%d\n", rc); + goto irq_exit; + } + + pr_debug("sdam_num:%d sdam_index:%#x\n", sdam_num, sdam_index); + + sdam_start = ecm->sdam[sdam_num].start_addr; + max_samples = (ECM_SDAM_SAMPLE_END_ADDR + 1 - sdam_start) / 2; + num_ecm_samples = (sdam_index + 1 - sdam_start) / 2; + + if (!num_ecm_samples || (num_ecm_samples > max_samples)) { + pr_err("Incorrect number of ECM samples, num_ecm_samples:%d max_samples:%d\n", + num_ecm_samples, max_samples); + return IRQ_HANDLED; + } + + mutex_lock(&ecm->sdam_lock); + + rc = nvmem_device_read(ecm->sdam[0].nvmem, ECM_WRITE_TO_SDAM, 1, + &overwrite); + if (rc < 0) { + pr_err("Failed to read ECM_WRITE_TO_SDAM from SDAM, rc=%d\n", + rc); + goto irq_exit; + } + + overwrite &= ~(OVERWRITE_SDAM0_DATA << sdam_num); + rc = ecm_nvmem_device_write(ecm->sdam[0].nvmem, ECM_WRITE_TO_SDAM, + 1, &overwrite); + if (rc < 0) { + pr_err("Failed to write ECM_WRITE_TO_SDAM to SDAM, rc=%d\n", + rc); + goto irq_exit; + } + + /* + * For AMOLED AB peripheral, + * Revision 1.0: + * ECM measured current = 15 times of each LSB + * + * Revision 2.0: + * ECM measured current = 16 times of each LSB + */ + + if (ecm->ab_revision == AMOLED_AB_REVISION_1P0) + gain = ECM_SAMPLE_GAIN_V1; + else + gain = ECM_SAMPLE_GAIN_V2; + + for (i = sdam_start; i < sdam_index; i += 2) { + rc = nvmem_device_read(ecm->sdam[sdam_num].nvmem, i, 2, buf); + if (rc < 0) { + pr_err("Failed to read SDAM sample, rc=%d\n", rc); + goto irq_exit; + } + + ecm_sample = (buf[1] << 8) | buf[0]; + + cumulative += ((ecm_sample * 1000) / gain) / 1000; + } + + overwrite |= (OVERWRITE_SDAM0_DATA << sdam_num); + rc = ecm_nvmem_device_write(ecm->sdam[0].nvmem, ECM_WRITE_TO_SDAM, + 1, &overwrite); + if (rc < 0) { + pr_err("Failed to write ECM_WRITE_TO_SDAM to SDAM, rc=%d\n", + rc); + goto irq_exit; + } + + if (!cumulative) { + pr_err("Error, No ECM samples captured. Cumulative:%lu\n", + cumulative); + goto irq_exit; + } + + m_sample = cumulative / num_ecm_samples; + data->m_cumulative += m_sample; + data->num_m_samples++; + + buf[0] = (ECM_SDAM0_FULL << sdam_num); + rc = ecm_nvmem_device_write(ecm->sdam[0].nvmem, ECM_STATUS_CLR, 1, + &buf[0]); + if (rc < 0) { + pr_err("Failed to clear interrupt status in SDAM, rc=%d\n", + rc); + goto irq_exit; + } + + if ((data->mode == ECM_MODE_MULTI_FRAMES) && + (sdam_index < max_samples)) + schedule_delayed_work(&ecm->average_work, 0); + +irq_exit: + mutex_unlock(&ecm->sdam_lock); + + return IRQ_HANDLED; +} + +static int amoled_ecm_parse_dt(struct amoled_ecm *ecm) +{ + int rc = 0, i; + u32 val; + u8 buf[20]; + + rc = of_property_read_u32(ecm->dev->of_node, "reg", &val); + if (rc < 0) { + pr_err("Failed to get reg, rc = %d\n", rc); + return rc; + } + ecm->base = val; + + rc = of_property_count_strings(ecm->dev->of_node, "nvmem-names"); + if (rc < 0) { + pr_err("Could not find nvmem device\n"); + return rc; + } + ecm->num_sdams = rc; + + ecm->sdam = devm_kcalloc(ecm->dev, ecm->num_sdams, + sizeof(*ecm->sdam), GFP_KERNEL); + if (!ecm->sdam) + return -ENOMEM; + + for (i = 0; i < ecm->num_sdams; i++) { + scnprintf(buf, sizeof(buf), "ecm-sdam%d", i); + + rc = of_irq_get_byname(ecm->dev->of_node, buf); + if (rc < 0) { + pr_err("Failed to get irq for ecm sdam, err=%d\n", rc); + return -EINVAL; + } + + ecm->sdam[i].irq_name = devm_kstrdup(ecm->dev, buf, + GFP_KERNEL); + if (!ecm->sdam[i].irq_name) + return -ENOMEM; + + ecm->sdam[i].irq = rc; + + scnprintf(buf, sizeof(buf), "amoled-ecm-sdam%d", i); + + ecm->sdam[i].nvmem = devm_nvmem_device_get(ecm->dev, buf); + if (IS_ERR(ecm->sdam[i].nvmem)) { + rc = PTR_ERR(ecm->sdam[i].nvmem); + if (rc != -EPROBE_DEFER) + pr_err("Failed to get nvmem device, rc=%d\n", + rc); + ecm->sdam[i].nvmem = NULL; + return rc; + } + } + + return 0; +} + +#if IS_ENABLED(CONFIG_QCOM_PANEL_EVENT_NOTIFIER) +static void panel_event_notifier_callback(enum panel_event_notifier_tag tag, + struct panel_event_notification *notification, void *data) +{ + struct amoled_ecm *ecm = data; + int rc; + + if (!notification) { + pr_err("Invalid panel notification\n"); + return; + } + + pr_debug("panel event received, type: %d\n", notification->notif_type); + switch (notification->notif_type) { + case DRM_PANEL_EVENT_BLANK: + if (ecm->enable) { + rc = amoled_ecm_disable(ecm); + if (rc < 0) { + pr_err("Failed to disable ECM for display BLANK, rc=%d\n", + rc); + return; + } + + ecm->reenable = true; + pr_debug("Disabled ECM for display BLANK\n"); + } + break; + case DRM_PANEL_EVENT_UNBLANK: + if (ecm->reenable) { + rc = amoled_ecm_enable(ecm); + if (rc < 0) { + pr_err("Failed to re-enable ECM for display UNBLANK, rc=%d\n", + rc); + return; + } + + ecm->reenable = false; + pr_debug("Enabled ECM for display UNBLANK\n"); + } + break; + default: + pr_debug("Ignore panel event: %d\n", notification->notif_type); + break; + } +} + +static int qti_amoled_register_panel_notifier(struct amoled_ecm *ecm) +{ + struct device_node *np = ecm->dev->of_node; + struct device_node *pnode; + struct drm_panel *panel; + void *cookie = NULL; + int i, count, rc; + + count = of_count_phandle_with_args(np, "display-panels", NULL); + if (count <= 0) + return 0; + + for (i = 0; i < count; i++) { + pnode = of_parse_phandle(np, "display-panels", i); + if (!pnode) + return -ENODEV; + + panel = of_drm_find_panel(pnode); + of_node_put(pnode); + if (!IS_ERR(panel)) { + ecm->active_panel = panel; + break; + } + } + + if (!ecm->active_panel) { + rc = PTR_ERR(panel); + if (rc != -EPROBE_DEFER) + pr_err("failed to find active panel, rc=%d\n", rc); + + return rc; + } + + cookie = panel_event_notifier_register( + PANEL_EVENT_NOTIFICATION_PRIMARY, + PANEL_EVENT_NOTIFIER_CLIENT_ECM, + ecm->active_panel, + panel_event_notifier_callback, + (void *)ecm); + if (IS_ERR(cookie)) { + rc = PTR_ERR(cookie); + pr_err("failed to register panel event notifier, rc=%d\n", rc); + return rc; + } + + pr_debug("register panel notifier successfully\n"); + ecm->notifier_cookie = cookie; + return 0; +} + +static int qti_amoled_unregister_panel_notifier(struct amoled_ecm *ecm) +{ + if (ecm->notifier_cookie) + panel_event_notifier_unregister(ecm->notifier_cookie); + + return 0; +} +#else +static inline int qti_amoled_register_panel_notifier(struct amoled_ecm *ecm) +{ + return 0; +} + +static inline int qti_amoled_unregister_panel_notifier(struct amoled_ecm *ecm) +{ + return 0; +} +#endif + +static int qti_amoled_ecm_probe(struct platform_device *pdev) +{ + struct device *hwmon_dev; + struct amoled_ecm *ecm; + int rc, i; + unsigned int temp; + + ecm = devm_kzalloc(&pdev->dev, sizeof(*ecm), GFP_KERNEL); + if (!ecm) + return -ENOMEM; + + ecm->dev = &pdev->dev; + + ecm->regmap = dev_get_regmap(pdev->dev.parent, NULL); + if (!ecm->regmap) { + dev_err(&pdev->dev, "Failed to get regmap\n"); + return -EINVAL; + } + + rc = amoled_ecm_parse_dt(ecm); + if (rc < 0) { + dev_err(&pdev->dev, "Failed to parse AMOLED ECM rc=%d\n", rc); + return rc; + } + + rc = regmap_read(ecm->regmap, ecm->base + AB_REVISION2, &temp); + if (rc < 0) { + dev_err(&pdev->dev, "Failed to read AB revision, rc=%d\n", rc); + return rc; + } + ecm->ab_revision = temp; + + ecm->enable = false; + ecm->abort = false; + + ecm->data.m_cumulative = 0; + ecm->data.num_m_samples = 0; + ecm->data.time_period_ms = 0; + ecm->data.frames = 0; + ecm->data.avg_current = 0; + ecm->data.mode = ECM_MODE_IDLE; + + INIT_DELAYED_WORK(&ecm->average_work, ecm_average_work); + + mutex_init(&ecm->sdam_lock); + + dev_set_drvdata(ecm->dev, ecm); + + for (i = 0; i < ecm->num_sdams; i++) { + rc = devm_request_threaded_irq(ecm->dev, ecm->sdam[i].irq, + NULL, sdam_full_irq_handler, + IRQF_ONESHOT, ecm->sdam[i].irq_name, ecm); + if (rc < 0) { + dev_err(&pdev->dev, "Failed to request IRQ(%s), rc=%d\n", + ecm->sdam[i].irq_name, rc); + return rc; + } + + ecm->sdam[i].start_addr = i ? ECM_SDAMX_SAMPLE_START_ADDR + : ECM_SDAM0_SAMPLE_START_ADDR; + } + + hwmon_dev = devm_hwmon_device_register_with_groups(&pdev->dev, + "amoled_ecm", ecm, amoled_ecm_groups); + if (IS_ERR_OR_NULL(hwmon_dev)) { + rc = PTR_ERR(hwmon_dev); + pr_err("failed to register hwmon device for amoled-ecm, rc=%d\n", + rc); + return rc; + } + + return qti_amoled_register_panel_notifier(ecm); +} + +static int qti_amoled_ecm_remove(struct platform_device *pdev) +{ + struct amoled_ecm *ecm = dev_get_drvdata(&pdev->dev); + + return qti_amoled_unregister_panel_notifier(ecm); +} + +static const struct of_device_id amoled_ecm_match_table[] = { + { .compatible = "qcom,amoled-ecm", }, + { }, +}; + +static struct platform_driver qti_amoled_ecm_driver = { + .driver = { + .name = "qti_amoled_ecm", + .of_match_table = amoled_ecm_match_table, + }, + .probe = qti_amoled_ecm_probe, + .remove = qti_amoled_ecm_remove, +}; +module_platform_driver(qti_amoled_ecm_driver); + +MODULE_DESCRIPTION("QTI AMOLED ECM driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/soc/qcom/panel_event_notifier.c b/drivers/soc/qcom/panel_event_notifier.c new file mode 100644 index 000000000000..f0feefa36ded --- /dev/null +++ b/drivers/soc/qcom/panel_event_notifier.c @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (c) 2021, The Linux Foundation. All rights reserved. + * + */ + +#include +#include +#include +#include + +struct panel_event_notifier_entry { + panel_event_notifier_handler handler; + void *pvt_data; + struct drm_panel *panel; + enum panel_event_notifier_tag tag; +}; + +static DEFINE_MUTEX(panel_event_notifier_entries_lock); +static struct panel_event_notifier_entry + panel_event_notifier_entries[PANEL_EVENT_NOTIFIER_CLIENT_MAX]; + +static bool panel_event_notifier_tag_valid(enum panel_event_notifier_tag tag) +{ + return tag > PANEL_EVENT_NOTIFICATION_NONE && + tag < PANEL_EVENT_NOTIFICATION_MAX; +} + +/** + * panel_event_notifier_register: responsible for registering clients + * interested in panel event notifications. + * clients register with a client handle and tag + * suggesting the notifications they are + * interested in and a callback which is + * triggered when the interested panel + * notifications are received. + * + * @tag: The tag for which the caller would like to receive panel events for. + * + * @client_handle: handle to recongnize the client registering for + * notifications. + * + * @panel: struct drm_panel for which the panel events are requested for. + * + * @handler: The handler that will be invoked when a panel event notification is + * received pertaining to @tag. The handler will be invoked with a + * pointer to private data registered by the client that is needed + * for servicing the notification. + * + * @pvt_data: The data that should be passed to @handler when a notification + * occurs + * + * On success, the function will return a cookie. + * + */ +void *panel_event_notifier_register(enum panel_event_notifier_tag tag, + enum panel_event_notifier_client client_handle, + struct drm_panel *panel, + panel_event_notifier_handler handler, void *pvt_data) +{ + struct panel_event_notifier_entry *entry; + + if (!panel_event_notifier_tag_valid(tag) || !handler) { + pr_err("Invalid tag or handler found while registering\n"); + return ERR_PTR(-EINVAL); + } + + if (client_handle < 0 || + client_handle >= PANEL_EVENT_NOTIFIER_CLIENT_MAX) { + pr_err("Invalid client handle used for registering\n"); + return ERR_PTR(-EINVAL); + } + + mutex_lock(&panel_event_notifier_entries_lock); + entry = &panel_event_notifier_entries[client_handle]; + if (entry->handler) { + mutex_unlock(&panel_event_notifier_entries_lock); + return ERR_PTR(-EEXIST); + } + entry->panel = panel; + entry->handler = handler; + entry->pvt_data = pvt_data; + entry->tag = tag; + mutex_unlock(&panel_event_notifier_entries_lock); + + pr_debug("client %d registered successfully\n", client_handle); + return entry; +} +EXPORT_SYMBOL(panel_event_notifier_register); + +/** + * panel_event_notifier_unregister: responsible for unregistering clients. + * + * @cookie: cookie used for unregistering client. + */ +void panel_event_notifier_unregister(void *cookie) +{ + struct panel_event_notifier_entry *entry = cookie; + + if (!cookie) + return; + + mutex_lock(&panel_event_notifier_entries_lock); + entry->panel = NULL; + entry->handler = NULL; + entry->pvt_data = NULL; + entry->tag = PANEL_EVENT_NOTIFICATION_NONE; + mutex_unlock(&panel_event_notifier_entries_lock); +} +EXPORT_SYMBOL(panel_event_notifier_unregister); + +/** + * panel_event_notifion_trigger: responsible for triggering notifications. + * Contains tag which notifies the panel + * notification is on premiary or secondary + * display and a notifcation carrying necessary + * data for clients to consume while servicing the + * notification. A handler registered by the + * client will be triggered to notify the event. + * + * @tag: tag suggesting the panel on which notification is triggered whether + *I primary or secondary display panel. + * + * @notification: contains data required for client to address the notification. + */ +void panel_event_notification_trigger(enum panel_event_notifier_tag tag, + struct panel_event_notification *notification) +{ + struct panel_event_notifier_entry *entry; + panel_event_notifier_handler handler = NULL; + void *pvt_data; + int i; + + if (!panel_event_notifier_tag_valid(tag)) { + pr_err("Invalid panel notifier tag\n"); + return; + } + + for (i = 0; i < PANEL_EVENT_NOTIFIER_CLIENT_MAX; i++) { + mutex_lock(&panel_event_notifier_entries_lock); + entry = &panel_event_notifier_entries[i]; + if (notification->panel != entry->panel) { + pr_debug("invalid panel found notification_panel:0x%x entry_panel:0x%x\n", + notification->panel, entry->panel); + mutex_unlock(&panel_event_notifier_entries_lock); + continue; + } + + /* skip client entries not subscribed to tag */ + if (entry->tag != tag) { + pr_err("tag mismatch entry->tag:%d tag:%d\n", entry->tag, tag); + mutex_unlock(&panel_event_notifier_entries_lock); + continue; + } + + handler = entry->handler; + pvt_data = entry->pvt_data; + mutex_unlock(&panel_event_notifier_entries_lock); + + pr_debug("triggering notification for tag:%d, type:%d\n", + tag, notification->notif_type); + + if (handler) + handler(tag, notification, pvt_data); + + } +} +EXPORT_SYMBOL(panel_event_notification_trigger); + +static int __init panel_event_notifier_init(void) +{ + pr_debug("Panel event notifier initialized\n"); + return 0; +} +module_init(panel_event_notifier_init); + +static void __exit panel_event_notifier_exit(void) +{ + pr_debug("Panel event notifier exited\n"); +} +module_exit(panel_event_notifier_exit); + +MODULE_LICENSE("GPL v2"); +MODULE_DESCRIPTION("Qualcomm Technologies, Inc. Panel event notifier"); diff --git a/include/linux/soc/qcom/panel_event_notifier.h b/include/linux/soc/qcom/panel_event_notifier.h new file mode 100644 index 000000000000..db2b24921adf --- /dev/null +++ b/include/linux/soc/qcom/panel_event_notifier.h @@ -0,0 +1,81 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Copyright (c) 2021, The Linux Foundation. All rights reserved. + * + */ + +#ifndef __PANEL_EVENT_NOTIFIER_H +#define __PANEL_EVENT_NOTIFIER_H + +#include +#include +#include +#include + +enum panel_event_notifier_tag { + PANEL_EVENT_NOTIFICATION_NONE, + PANEL_EVENT_NOTIFICATION_PRIMARY, + PANEL_EVENT_NOTIFICATION_SECONDARY, + PANEL_EVENT_NOTIFICATION_MAX +}; + +enum panel_event_notifier_client { + PANEL_EVENT_NOTIFIER_CLIENT_PRIMARY_TOUCH, + PANEL_EVENT_NOTIFIER_CLIENT_SECONDARY_TOUCH, + PANEL_EVENT_NOTIFIER_CLIENT_ECM, + PANEL_EVENT_NOTIFIER_CLIENT_MAX +}; + +enum panel_event_notification_type { + DRM_PANEL_EVENT_NONE, + DRM_PANEL_EVENT_BLANK, + DRM_PANEL_EVENT_UNBLANK, + DRM_PANEL_EVENT_BLANK_LP, + DRM_PANEL_EVENT_FPS_CHANGE, + DRM_PANEL_EVENT_MAX +}; + +struct panel_event_notification_data { + u32 old_fps; + u32 new_fps; + bool early_trigger; +}; + +struct panel_event_notification { + enum panel_event_notification_type notif_type; + struct panel_event_notification_data notif_data; + struct drm_panel *panel; +}; + +typedef void (*panel_event_notifier_handler)(enum panel_event_notifier_tag tag, + struct panel_event_notification *notification, + void *pvt_data); + +#if IS_ENABLED(CONFIG_QCOM_PANEL_EVENT_NOTIFIER) +void *panel_event_notifier_register(enum panel_event_notifier_tag tag, + enum panel_event_notifier_client client_handle, + struct drm_panel *panel, + panel_event_notifier_handler notif_handler, void *pvt_data); +void panel_event_notifier_unregister(void *cookie); +void panel_event_notification_trigger(enum panel_event_notifier_tag tag, + struct panel_event_notification *notification); + +#else +static inline void *panel_event_notifier_register(enum panel_event_notifier_tag tag, + enum panel_event_notifier_client client_handle, + struct drm_panel *panel, + panel_event_notifier_handler notif_handler, void *pvt_data) +{ + return ERR_PTR(-EOPNOTSUPP); +} + +static inline void panel_event_notifier_unregister(void *cookie) +{ +} +static inline void panel_event_notification_trigger( + enum panel_event_notifier_tag tag, + struct panel_event_notification *notification) +{ +} +#endif +#endif