From 31960cf2507485b20434b0040a22754a0c2fe911 Mon Sep 17 00:00:00 2001 From: Shashank Babu Chinta Venkata Date: Tue, 12 Oct 2021 12:33:11 -0700 Subject: [PATCH 1/8] soc: qcom: panel_event_notifier: add panel_event_notifier driver Add a notification frameowrk to notify all clients interested in getting various display panel events like power on, power off, FPS change, etc. Clients can use these notifications for power saving or align its operations with display panel power state. Change-Id: Ic4ed7f3207111824a5da84ed2a5a2e0e50e925f0 Signed-off-by: Shashank Babu Chinta Venkata --- drivers/soc/qcom/panel_event_notifier.c | 184 ++++++++++++++++++ include/linux/soc/qcom/panel_event_notifier.h | 81 ++++++++ 2 files changed, 265 insertions(+) create mode 100644 drivers/soc/qcom/panel_event_notifier.c create mode 100644 include/linux/soc/qcom/panel_event_notifier.h 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 From 50499dcf75d17b98a43f03e53fa58161ac4e513b Mon Sep 17 00:00:00 2001 From: Shyam Kumar Thella Date: Wed, 26 Feb 2020 16:17:55 +0530 Subject: [PATCH 2/8] hwmon: Add QTI AMOLED ECM driver AMOLED ECM (Embedded Current Measurement) is a method to measure OLED display power consumption with a time granularity in sub-frame or multiple frames of image data. ECM can be for a short period or for a longer period. Change-Id: I7ea0b7d8d5961ec015c5a79ee2037ff36329f775 Signed-off-by: Shyam Kumar Thella --- drivers/hwmon/Kconfig | 12 + drivers/hwmon/Makefile | 1 + drivers/hwmon/qti_amoled_ecm.c | 808 +++++++++++++++++++++++++++++++++ 3 files changed, 821 insertions(+) create mode 100644 drivers/hwmon/qti_amoled_ecm.c 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..5fc0fe3bb8ef --- /dev/null +++ b/drivers/hwmon/qti_amoled_ecm.c @@ -0,0 +1,808 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (c) 2020, 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 + +/* 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_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 ECM_WRITE_TO_SDAM 0x60 + #define WRITE_SDAM0_DATA BIT(0) + #define WRITE_SDAM1_DATA BIT(1) + #define OVERWRITE_SDAM0_DATA BIT(4) + #define OVERWRITE_SDAM1_DATA BIT(5) + +#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 + * @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 + */ +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; + u32 num_sdams; + u32 base; + u8 ab_revision; + bool enable; + bool abort; +}; + +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_MODE, 0x00 }, + /* Valid only when ECM uses 2 SDAMs */ + { ECM_SEND_IRQ, 0x03 }, + { ECM_WRITE_TO_SDAM, 0x33 } +}; + +static int ecm_reset_sdam_config(struct amoled_ecm *ecm) +{ + int rc, i; + + for (i = 0; i < ARRAY_SIZE(ecm_reset_config); i++) { + rc = 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; + } + } + + 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 = 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 = 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 = 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.frames = 0; + ecm->data.time_period_ms = 0; + 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; + int rc; + + if (!data->num_m_samples || !data->m_cumulative) { + pr_err("num_m_samples=%u m_cumulative:%u disabling ECM\n", + data->num_m_samples, data->m_cumulative); + data->avg_current = -EINVAL; + + rc = amoled_ecm_disable(ecm); + if (rc < 0) + pr_err("Failed to disable AMOLED ECM, rc=%d\n", rc); + + return; + } + + mutex_lock(&ecm->sdam_lock); + + 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; + } + } + + 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: + data->avg_current = -EIO; + break; + case ECM_MODE_CONTINUOUS: + if (data->num_m_samples < ECM_MIN_M_SAMPLES) { + data->avg_current = -EIO; + break; + } + + ecm->abort = true; + schedule_delayed_work(&ecm->average_work, 0); + break; + default: + data->avg_current = -EINVAL; + return -EINVAL; + } + + rc = amoled_ecm_disable(ecm); + if (rc < 0) + pr_err("Failed to disable AMOLED ECM, rc=%d\n", rc); + + return rc; +} + +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 = nvmem_device_read(ecm->sdam[0].nvmem, + (ECM_SDAM0_INDEX + sdam_num), 1, &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 = 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 = 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 = 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 rc; +} + +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); + + return PTR_ERR_OR_ZERO(hwmon_dev); +} + +static int qti_amoled_ecm_remove(struct platform_device *pdev) +{ + return 0; +} + +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"); From d5109e4d3d651e1911034cbebf7ce00649fd59cd Mon Sep 17 00:00:00 2001 From: Subbaraman Narayanamurthy Date: Wed, 9 Sep 2020 20:19:37 -0700 Subject: [PATCH 3/8] hwmon: qti_amoled_ecm: Fix initial value for ECM_WRITE_TO_SDAM Whenever ECM is enabled by the user, some SDAM registers are initialized for the proper operation. Change the initial value for ECM_WRITE_TO_SDAM as per the hardware recommendation. Change-Id: I7a414bb83640178055f3c5a463daa6328aaef69c Signed-off-by: Subbaraman Narayanamurthy --- drivers/hwmon/qti_amoled_ecm.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/hwmon/qti_amoled_ecm.c b/drivers/hwmon/qti_amoled_ecm.c index 5fc0fe3bb8ef..220dd515e630 100644 --- a/drivers/hwmon/qti_amoled_ecm.c +++ b/drivers/hwmon/qti_amoled_ecm.c @@ -183,7 +183,7 @@ static struct amoled_ecm_sdam_config ecm_reset_config[] = { { ECM_MODE, 0x00 }, /* Valid only when ECM uses 2 SDAMs */ { ECM_SEND_IRQ, 0x03 }, - { ECM_WRITE_TO_SDAM, 0x33 } + { ECM_WRITE_TO_SDAM, 0x03 } }; static int ecm_reset_sdam_config(struct amoled_ecm *ecm) From ad61c8386118f46fb986a94b2555e017ee56e318 Mon Sep 17 00:00:00 2001 From: Subbaraman Narayanamurthy Date: Wed, 9 Sep 2020 20:18:12 -0700 Subject: [PATCH 4/8] hwmon: qti_amoled_ecm: add debug log for nvmem writes Add a wrapper function ecm_nvmem_device_write() which can log the nvmem writes. This is useful for debugging. Change-Id: Icc4ee6ff50b46f92e0169660e3400d80f7d4da37 Signed-off-by: Subbaraman Narayanamurthy --- drivers/hwmon/qti_amoled_ecm.c | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/drivers/hwmon/qti_amoled_ecm.c b/drivers/hwmon/qti_amoled_ecm.c index 220dd515e630..91b8cd101ce1 100644 --- a/drivers/hwmon/qti_amoled_ecm.c +++ b/drivers/hwmon/qti_amoled_ecm.c @@ -186,12 +186,25 @@ static struct amoled_ecm_sdam_config ecm_reset_config[] = { { ECM_WRITE_TO_SDAM, 0x03 } }; +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; for (i = 0; i < ARRAY_SIZE(ecm_reset_config); i++) { - rc = nvmem_device_write(ecm->sdam[0].nvmem, + rc = ecm_nvmem_device_write(ecm->sdam[0].nvmem, ecm_reset_config[i].reg, 1, &ecm_reset_config[i].reset_val); if (rc < 0) { @@ -212,7 +225,7 @@ static int amoled_ecm_enable(struct amoled_ecm *ecm) int rc; if (data->frames) { - rc = nvmem_device_write(ecm->sdam[0].nvmem, + 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", @@ -241,7 +254,7 @@ static int amoled_ecm_enable(struct amoled_ecm *ecm) return rc; } - rc = nvmem_device_write(ecm->sdam[0].nvmem, ECM_MODE, 1, + 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); @@ -286,7 +299,7 @@ static int amoled_ecm_disable(struct amoled_ecm *ecm) return rc; } - rc = nvmem_device_write(ecm->sdam[0].nvmem, ECM_AVERAGE_LSB, 2, + 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); @@ -587,7 +600,7 @@ static irqreturn_t sdam_full_irq_handler(int irq, void *_ecm) } overwrite &= ~(OVERWRITE_SDAM0_DATA << sdam_num); - rc = nvmem_device_write(ecm->sdam[0].nvmem, ECM_WRITE_TO_SDAM, + 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", @@ -622,7 +635,7 @@ static irqreturn_t sdam_full_irq_handler(int irq, void *_ecm) } overwrite |= (OVERWRITE_SDAM0_DATA << sdam_num); - rc = nvmem_device_write(ecm->sdam[0].nvmem, ECM_WRITE_TO_SDAM, + 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", @@ -641,7 +654,7 @@ static irqreturn_t sdam_full_irq_handler(int irq, void *_ecm) data->num_m_samples++; buf[0] = (ECM_SDAM0_FULL << sdam_num); - rc = nvmem_device_write(ecm->sdam[0].nvmem, ECM_STATUS_CLR, 1, + 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", From 48c3279d8d60c423e245ff2c270a724b56f7723e Mon Sep 17 00:00:00 2001 From: Fenglin Wu Date: Wed, 16 Sep 2020 07:19:13 +0800 Subject: [PATCH 5/8] hwmon: amoled-ecm: add support to re-enable ECM dynamically When ECM is enabled, disable ECM if display goes blank, and re-enable ECM once display restores to unblank. This helps in keeping ECM HW enabled only when display is active. Meanwhile, update the logic a little bit on calculating the average current. When any of the accumulated values is invalid, just ignore the calculation instead of disabling the ECM. Change-Id: Ibfe973df4dabd1b1cd48927ce567a094da6382a3 Signed-off-by: Fenglin Wu --- drivers/hwmon/qti_amoled_ecm.c | 168 ++++++++++++++++++++++++++++----- 1 file changed, 146 insertions(+), 22 deletions(-) diff --git a/drivers/hwmon/qti_amoled_ecm.c b/drivers/hwmon/qti_amoled_ecm.c index 91b8cd101ce1..9954598ae8d3 100644 --- a/drivers/hwmon/qti_amoled_ecm.c +++ b/drivers/hwmon/qti_amoled_ecm.c @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-only /* - * Copyright (c) 2020, The Linux Foundation. All rights reserved. + * Copyright (c) 2020-2021, The Linux Foundation. All rights reserved. */ #define pr_fmt(fmt) "AMOLED_ECM: %s: " fmt, __func__ @@ -19,6 +19,8 @@ #include #include +#include + /* AMOLED AB register definitions */ #define AB_REVISION2 0x01 @@ -150,11 +152,14 @@ struct amoled_ecm_data { * @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; @@ -163,11 +168,14 @@ struct amoled_ecm { 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[] = { @@ -309,8 +317,6 @@ static int amoled_ecm_disable(struct amoled_ecm *ecm) cancel_delayed_work(&ecm->average_work); - ecm->data.frames = 0; - ecm->data.time_period_ms = 0; ecm->data.avg_current = 0; ecm->data.m_cumulative = 0; ecm->data.num_m_samples = 0; @@ -327,25 +333,17 @@ 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; - int rc; - - if (!data->num_m_samples || !data->m_cumulative) { - pr_err("num_m_samples=%u m_cumulative:%u disabling ECM\n", - data->num_m_samples, data->m_cumulative); - data->avg_current = -EINVAL; - - rc = amoled_ecm_disable(ecm); - if (rc < 0) - pr_err("Failed to disable AMOLED ECM, rc=%d\n", rc); - - return; - } mutex_lock(&ecm->sdam_lock); - data->avg_current = data->m_cumulative / data->num_m_samples; - - pr_debug("avg_current=%u mA\n", data->avg_current); + if (!data->num_m_samples || !data->m_cumulative) { + pr_warn("Invalid data, num_m_samples=%u m_cumulative:%u\n", + data->num_m_samples, data->m_cumulative); + data->avg_current = -EINVAL; + } 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; @@ -399,6 +397,9 @@ static ssize_t enable_store(struct device *dev, 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; @@ -726,9 +727,124 @@ static int amoled_ecm_parse_dt(struct amoled_ecm *ecm) } } - 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; @@ -793,13 +909,21 @@ static int qti_amoled_ecm_probe(struct platform_device *pdev) 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 PTR_ERR_OR_ZERO(hwmon_dev); + return qti_amoled_register_panel_notifier(ecm); } static int qti_amoled_ecm_remove(struct platform_device *pdev) { - return 0; + 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[] = { From b731ccd999dbc4ac22d2bf40c07347b8479ff80b Mon Sep 17 00:00:00 2001 From: Fenglin Wu Date: Tue, 9 Mar 2021 07:40:48 +0800 Subject: [PATCH 6/8] hwmon: qti_amoled_ecm: return avg_current as 0 upon error exceptions Avg_current is an unsigned 16-bit data and assigning it a negative value would make it being shown as a very big positive value which doesn't make sense for ECM current measurement. Instead, assign it to 0 upon error exceptions and print out corresponding messages. Change-Id: I438667e40d63e4142860b7de4e7cf7495d3c424e Signed-off-by: Fenglin Wu --- drivers/hwmon/qti_amoled_ecm.c | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/drivers/hwmon/qti_amoled_ecm.c b/drivers/hwmon/qti_amoled_ecm.c index 9954598ae8d3..1fc019987694 100644 --- a/drivers/hwmon/qti_amoled_ecm.c +++ b/drivers/hwmon/qti_amoled_ecm.c @@ -337,9 +337,9 @@ static void ecm_average_work(struct work_struct *work) mutex_lock(&ecm->sdam_lock); if (!data->num_m_samples || !data->m_cumulative) { - pr_warn("Invalid data, num_m_samples=%u m_cumulative:%u\n", + pr_warn_ratelimited("Invalid data, num_m_samples=%u m_cumulative:%u\n", data->num_m_samples, data->m_cumulative); - data->avg_current = -EINVAL; + 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); @@ -510,11 +510,14 @@ static int handle_ecm_abort(struct amoled_ecm *ecm) switch (mode) { case ECM_MODE_MULTI_FRAMES: - data->avg_current = -EIO; + 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) { - data->avg_current = -EIO; + pr_warn_ratelimited("Too few samples %u for continuous mode\n", + data->num_m_samples); + data->avg_current = 0; break; } @@ -522,7 +525,8 @@ static int handle_ecm_abort(struct amoled_ecm *ecm) schedule_delayed_work(&ecm->average_work, 0); break; default: - data->avg_current = -EINVAL; + pr_err_ratelimited("Invalid ECM operation mode: %u\n", mode); + data->avg_current = 0; return -EINVAL; } From c96271dd6d4ef76c1e906e15479a3ad2f3e0da71 Mon Sep 17 00:00:00 2001 From: Subbaraman Narayanamurthy Date: Wed, 12 May 2021 15:18:47 -0700 Subject: [PATCH 7/8] hwmon: qti_amoled_ecm: update ecm_reset_sdam_config() Currently ECM_SEND_IRQ and ECM_WRITE_TO_SDAM offsets in first SDAM is initialized to 0x03 to enable storing samples in two SDAMs for ECM. With an additional SDAM for ECM, these registers should be initialized accordingly. Support this by updating those registers with a value based on number of SDAMs. Change-Id: Icd262907e4fb71f8696b34f9ebf2463e281a2f66 Signed-off-by: Subbaraman Narayanamurthy --- drivers/hwmon/qti_amoled_ecm.c | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/drivers/hwmon/qti_amoled_ecm.c b/drivers/hwmon/qti_amoled_ecm.c index 1fc019987694..0e2374deb9a0 100644 --- a/drivers/hwmon/qti_amoled_ecm.c +++ b/drivers/hwmon/qti_amoled_ecm.c @@ -77,12 +77,15 @@ #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 @@ -189,9 +192,6 @@ static struct amoled_ecm_sdam_config ecm_reset_config[] = { { ECM_SDAM0_INDEX, 0x6C }, { ECM_SDAM1_INDEX, 0x46 }, { ECM_MODE, 0x00 }, - /* Valid only when ECM uses 2 SDAMs */ - { ECM_SEND_IRQ, 0x03 }, - { ECM_WRITE_TO_SDAM, 0x03 } }; static int ecm_nvmem_device_write(struct nvmem_device *nvmem, @@ -210,6 +210,7 @@ static int ecm_nvmem_device_write(struct nvmem_device *nvmem, 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, @@ -222,6 +223,23 @@ static int ecm_reset_sdam_config(struct amoled_ecm *ecm) } } + 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; From 131656a2b76c73f0bf4e0e1568a9b4daf67c5ffa Mon Sep 17 00:00:00 2001 From: Subbaraman Narayanamurthy Date: Wed, 12 May 2021 19:03:17 -0700 Subject: [PATCH 8/8] hwmon: qti_amoled_ecm: Support getting sdam_index for more than 2 SDAMs Currently, sdam_num is used to read SDAM_INDEX for reading samples from each SDAM. However, it works well only when 2 SDAMs are used. If ECM supports more than 2 SDAMs, SDAM_INDEX read for the third SDAM (and after) would be incorrect. Fix it. Change-Id: Ibf2219b80a7480a0fda11df974786c1d123ba492 Signed-off-by: Subbaraman Narayanamurthy --- drivers/hwmon/qti_amoled_ecm.c | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/drivers/hwmon/qti_amoled_ecm.c b/drivers/hwmon/qti_amoled_ecm.c index 0e2374deb9a0..8713e80846e6 100644 --- a/drivers/hwmon/qti_amoled_ecm.c +++ b/drivers/hwmon/qti_amoled_ecm.c @@ -55,6 +55,7 @@ #define ECM_SDAM0_INDEX 0x52 #define ECM_SDAM1_INDEX 0x53 +#define ECM_SDAM2_INDEX 0x61 #define ECM_MODE 0x54 #define ECM_CONTINUOUS 0 @@ -191,6 +192,7 @@ static struct amoled_ecm_sdam_config ecm_reset_config[] = { { ECM_STATUS_CLR, 0xFF }, { ECM_SDAM0_INDEX, 0x6C }, { ECM_SDAM1_INDEX, 0x46 }, + { ECM_SDAM2_INDEX, 0x46 }, { ECM_MODE, 0x00 }, }; @@ -555,6 +557,27 @@ static int handle_ecm_abort(struct amoled_ecm *ecm) 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; @@ -593,8 +616,7 @@ static irqreturn_t sdam_full_irq_handler(int irq, void *_ecm) } } - rc = nvmem_device_read(ecm->sdam[0].nvmem, - (ECM_SDAM0_INDEX + sdam_num), 1, &sdam_index); + 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;