From: Devarsh Thakkar <devarsht@ti.com>
To: Maarten Lankhorst <maarten.lankhorst@linux.intel.com>,
Maxime Ripard <mripard@kernel.org>,
Thomas Zimmermann <tzimmermann@suse.de>,
David Airlie <airlied@gmail.com>, Simona Vetter <simona@ffwll.ch>,
Rob Herring <robh@kernel.org>,
Krzysztof Kozlowski <krzk+dt@kernel.org>,
Conor Dooley <conor+dt@kernel.org>, Nishanth Menon <nm@ti.com>,
Vignesh Raghavendra <vigneshr@ti.com>,
Tero Kristo <kristo@kernel.org>
Cc: <dri-devel@lists.freedesktop.org>, <devicetree@vger.kernel.org>,
<linux-kernel@vger.kernel.org>, Devarsh Thakkar <devarsht@ti.com>
Subject: [PATCH v2 07/14] drm/solomon: ssd16xx: Add power management support
Date: Sun, 27 Sep 2026 23:53:22 +0530 [thread overview]
Message-ID: <20260927182329.4193961-8-devarsht@ti.com> (raw)
In-Reply-To: <20260927182329.4193961-1-devarsht@ti.com>
Add runtime and system suspend/resume power management to the SSD16xx
e-paper display driver.
Runtime PM uses a two-level deep-sleep strategy:
- Level 1 (runtime_suspend after 35s idle): light sleep with RAM
retained (SSD1683 mode 0x01), allowing fast resume via SPI wakeup
- Level 2 (system_suspend): maximum power savings (mode 0x03),
requires full GPIO hardware reset to resume
An autosuspend delay of 35 seconds keeps the panel active across typical
userspace update bursts, avoiding unnecessary deep-sleep/wake cycles.
On resume from level-2 (system suspend), the driver must fully reset
the panel via HWRESET (the only exit from deep sleep since SPI is
inactive in mode 0x03), re-run hw_init, and redraw the last frame.
For SSD1677 where level1 == level2 (both 0x03), the system suspend
path skips the redundant "upgrade" since the panel is already in the
deepest sleep state from runtime suspend.
Signed-off-by: Devarsh Thakkar <devarsht@ti.com>
---
Changes from v1:
- Extracted power management features into dedicated patch for improved
code organization
drivers/gpu/drm/solomon/ssd16xx.c | 240 ++++++++++++++++++++++++++++--
1 file changed, 225 insertions(+), 15 deletions(-)
diff --git a/drivers/gpu/drm/solomon/ssd16xx.c b/drivers/gpu/drm/solomon/ssd16xx.c
index 93fa064cf589..6255f434248a 100644
--- a/drivers/gpu/drm/solomon/ssd16xx.c
+++ b/drivers/gpu/drm/solomon/ssd16xx.c
@@ -12,6 +12,7 @@
#include <linux/delay.h>
#include <linux/module.h>
#include <linux/of.h>
+#include <linux/pm_runtime.h>
#include <linux/property.h>
#include <linux/spi/spi.h>
@@ -39,6 +40,7 @@
/* SPI command codes (common) */
#define SSD16XX_CMD_DRIVER_OUTPUT_CONTROL 0x01
+#define SSD16XX_CMD_DEEP_SLEEP_MODE 0x10
#define SSD16XX_CMD_BOOSTER_SOFT_START 0x0C
#define SSD16XX_CMD_DATA_ENTRY_MODE 0x11
#define SSD16XX_CMD_SW_RESET 0x12
@@ -52,6 +54,9 @@
#define SSD16XX_CMD_SET_RAM_X_ADDRESS_COUNTER 0x4E
#define SSD16XX_CMD_SET_RAM_Y_ADDRESS_COUNTER 0x4F
+/* Runtime PM autosuspend delay (ms): keep display active across typical gaps */
+#define SSD16XX_PM_AUTOSUSPEND_DELAY_MS 35000
+
/*
* Data Entry Mode (command 0x11) AM/IDY/IDX bit encoding (common).
*
@@ -245,11 +250,14 @@ struct ssd16xx_controller_config {
/*
* Deep sleep mode byte values for command 0x10.
- * deep_sleep_mode_level1: lower-power sleep, RAM content retained
- * (MODE_1 on SSD1683/SSD1680; used for runtime idle / app-close).
- * deep_sleep_mode_level2: maximum power savings, RAM may be lost
- * (MODE_2 on SSD1683/SSD1680; used for system suspend).
- * Chips with a single sleep mode set both fields to the same value.
+ * deep_sleep_mode_level1: used for runtime idle (autosuspend).
+ * SSD1683/SSD1680: 0x01 — RAM retained, minimal wake latency.
+ * SSD1677: 0x03 — only one mode documented; RAM may be lost.
+ * deep_sleep_mode_level2: used for system suspend (max power savings).
+ * SSD1683/SSD1680: 0x03 — RAM lost, full re-init required on resume.
+ * SSD1677: 0x03 — same as level1 (single sleep mode).
+ * When level1 == level2 the system-suspend "upgrade" path is skipped;
+ * the device is already in the deepest available mode.
*/
u8 deep_sleep_mode_level1;
u8 deep_sleep_mode_level2;
@@ -367,8 +375,10 @@ struct ssd16xx_device {
bool init_refresh_pending; /* First frame after refresh_mode_init enable */
bool first_clear_done; /* clear_on_init has already fired once */
bool display_cleared_on_deinit; /* Avoid redundant clear in atomic_disable/master_drop */
+ bool pm_force_suspended; /* pm_runtime_force_suspend was called in pm_suspend */
- int orientation; /* Display orientation in degrees: 0/90/180/270 */
+ int orientation; /* Display orientation in degrees: 0/90/180/270 */
+ u8 deep_sleep_mode; /* Deep sleep mode to use on next disable */
enum ssd16xx_refresh_mode refresh_mode; /* Active refresh mode */
enum ssd16xx_color_mode color_mode; /* Active color mode (BW or 3-color) */
bool fast_lut_pending; /* LUT pre-load needed before next fast refresh */
@@ -1425,11 +1435,16 @@ static void ssd16xx_plane_atomic_update(struct drm_plane *plane,
struct drm_rect rect;
int ret;
- drm_dbg(&device->drm, "plane_atomic_update: fb=%p, initialized=%d\n",
- fb, device->initialized);
+ drm_dbg(&device->drm, "plane_atomic_update: fb=%p\n", fb);
+
+ if (!fb)
+ return;
- if (!fb || !device->initialized)
+ ret = pm_runtime_resume_and_get(device->drm.dev);
+ if (ret < 0) {
+ drm_err(&device->drm, "plane_atomic_update: failed to resume: %d\n", ret);
return;
+ }
if (!drm_atomic_helper_damage_merged(old_state, new_state, &rect)) {
rect.x1 = 0;
@@ -1488,6 +1503,9 @@ static void ssd16xx_plane_atomic_update(struct drm_plane *plane,
device->init_refresh_pending = false;
device->border_waveform_pending = true;
}
+
+ pm_runtime_mark_last_busy(device->drm.dev);
+ pm_runtime_put_autosuspend(device->drm.dev);
}
static const struct drm_plane_helper_funcs ssd16xx_plane_helper_funcs = {
@@ -1547,6 +1565,10 @@ static void ssd16xx_crtc_atomic_disable(struct drm_crtc *crtc,
if (device->clear_on_disable < 0 || device->display_cleared_on_deinit)
goto out;
+ ret = pm_runtime_resume_and_get(device->drm.dev);
+ if (ret < 0)
+ goto out;
+
drm_dbg(&device->drm, "clear_on_disable: running, mode=%d\n",
device->clear_on_disable);
ret = ssd16xx_clear_display(device,
@@ -1554,10 +1576,12 @@ static void ssd16xx_crtc_atomic_disable(struct drm_crtc *crtc,
device->clear_on_disable));
if (ret) {
drm_err(&device->drm, "atomic_disable: clear failed: %d\n", ret);
+ pm_runtime_put_autosuspend(device->drm.dev);
goto out;
}
device->display_cleared_on_deinit = true;
+ pm_runtime_put_sync_suspend(device->drm.dev);
out:
drm_dev_exit(idx);
}
@@ -1574,12 +1598,27 @@ static void ssd16xx_crtc_atomic_enable(struct drm_crtc *crtc,
drm_dbg(&device->drm, "atomic_enable: %dx%d orientation=%u°\n",
device->width, device->height, device->orientation);
- ret = ssd16xx_hw_init(device);
- if (ret) {
- drm_err(&device->drm, "crtc_atomic_enable: HW init failed: %d\n", ret);
- goto out;
+ ret = pm_runtime_resume_and_get(device->drm.dev);
+ if (ret < 0) {
+ drm_err(&device->drm, "crtc_atomic_enable: failed to resume: %d\n", ret);
+ drm_dev_exit(idx);
+ return;
+ }
+
+ /*
+ * If the device is not yet initialized (fresh from deep sleep),
+ * pm_runtime_resume calls hw_init. The fallback below handles the case where
+ * runtime PM is disabled and the resume callback never fires."
+ *
+ */
+ if (!device->initialized) {
+ ret = ssd16xx_hw_init(device);
+ if (ret) {
+ drm_err(&device->drm, "crtc_atomic_enable: HW init failed: %d\n", ret);
+ goto out;
+ }
+ device->initialized = true;
}
- device->initialized = true;
/* Clear display on first app launch if configured */
ret = ssd16xx_clear_display_on_init(device);
@@ -1599,6 +1638,8 @@ static void ssd16xx_crtc_atomic_enable(struct drm_crtc *crtc,
}
out:
+ pm_runtime_mark_last_busy(device->drm.dev);
+ pm_runtime_put_autosuspend(device->drm.dev);
drm_dev_exit(idx);
}
@@ -1620,6 +1661,13 @@ static void ssd16xx_crtc_atomic_flush(struct drm_crtc *crtc,
if (!drm_dev_enter(&device->drm, &idx))
return;
+ ret = pm_runtime_resume_and_get(device->drm.dev);
+ if (ret < 0) {
+ drm_err(&device->drm, "atomic_flush: failed to resume: %d\n", ret);
+ drm_dev_exit(idx);
+ return;
+ }
+
drm_dbg(&device->drm, "atomic_flush: reinit, orientation=%u°\n",
device->orientation);
@@ -1652,6 +1700,8 @@ static void ssd16xx_crtc_atomic_flush(struct drm_crtc *crtc,
}
out:
+ pm_runtime_mark_last_busy(device->drm.dev);
+ pm_runtime_put_autosuspend(device->drm.dev);
drm_dev_exit(idx);
}
@@ -1752,11 +1802,19 @@ static void ssd16xx_drm_master_drop(struct drm_device *drm,
if (!drm_dev_enter(drm, &idx))
return;
+ ret = pm_runtime_resume_and_get(drm->dev);
+ if (ret < 0) {
+ drm_dev_exit(idx);
+ return;
+ }
+
ret = ssd16xx_clear_display_on_exit(device);
if (ret)
drm_err(drm, "master_drop: clear on close failed: %d\n", ret);
device->display_cleared_on_deinit = true;
+ /* sync suspend — bypass autosuspend, sleep immediately after clear */
+ pm_runtime_put_sync_suspend(drm->dev);
drm_dev_exit(idx);
}
@@ -2017,9 +2075,26 @@ static int ssd16xx_probe(struct spi_device *spi)
drm_mode_config_reset(drm);
+ /*
+ * Mark the device active before enabling runtime PM. The SPI device
+ * persists across module reload cycles; pm_runtime_enable() alone
+ * does not clear a stale dev->power.runtime_error left by a previous
+ * failed hw_init, which would cause pm_runtime_resume_and_get() to
+ * return -EINVAL on every subsequent call.
+ */
+ pm_runtime_set_active(dev);
+ pm_runtime_enable(dev);
+ pm_runtime_set_autosuspend_delay(dev, SSD16XX_PM_AUTOSUSPEND_DELAY_MS);
+ pm_runtime_use_autosuspend(dev);
+
ret = drm_dev_register(drm, 0);
- if (ret)
+ if (ret) {
+ pm_runtime_disable(dev);
return ret;
+ }
+
+ /* Default to runtime sleep mode (RAM retained if supported) */
+ device->deep_sleep_mode = device->controller_cfg->deep_sleep_mode_level1;
drm_dbg(drm, "SSD16xx e-paper display initialized (%dx%d, %d° rotation)\n",
device->width, device->height, device->orientation);
@@ -2035,6 +2110,8 @@ static void ssd16xx_remove(struct spi_device *spi)
drm_dev_unplug(&device->drm);
drm_atomic_helper_shutdown(&device->drm);
+ pm_runtime_dont_use_autosuspend(&spi->dev);
+ pm_runtime_disable(&spi->dev);
}
static void ssd16xx_shutdown(struct spi_device *spi)
@@ -2042,8 +2119,140 @@ static void ssd16xx_shutdown(struct spi_device *spi)
struct ssd16xx_device *device = spi_get_drvdata(spi);
drm_atomic_helper_shutdown(&device->drm);
+ pm_runtime_dont_use_autosuspend(&spi->dev);
+ pm_runtime_disable(&spi->dev);
}
+static int ssd16xx_pm_suspend(struct device *dev)
+{
+ struct ssd16xx_device *device = dev_get_drvdata(dev);
+ struct drm_device *drm = &device->drm;
+ int ret;
+
+ /*
+ * Select maximum-savings sleep mode for system suspend.
+ * On SSD1683/SSD1680: level2 = 0x03 (RAM lost) gives deeper savings
+ * than the runtime-idle level1 = 0x01 (RAM retained).
+ * On SSD1677: only one deep sleep mode exists; level1 == level2 == 0x03.
+ */
+ device->deep_sleep_mode = device->controller_cfg->deep_sleep_mode_level2;
+ dev_dbg(dev, "system suspend: entering deep sleep mode 0x%02x\n",
+ device->deep_sleep_mode);
+
+ ret = drm_mode_config_helper_suspend(drm);
+ if (ret)
+ return ret;
+
+ /*
+ * If the autosuspend timer already fired the device is RPM_SUSPENDED
+ * at level1. pm_runtime_force_suspend() would skip its callback, so
+ * level2 would never reach the hardware.
+ *
+ * SPI is unresponsive in deep sleep; HWRESET is the only exit.
+ * After the reset, level2 can be sent directly without a full hw_init.
+ *
+ * Skip this for controllers where level1 == level2 (e.g. SSD1677 which
+ * has only one deep sleep mode): the device is already in the deepest
+ * available state and no upgrade is needed.
+ */
+ if (pm_runtime_status_suspended(dev)) {
+ int err = 0;
+
+ if (device->controller_cfg->deep_sleep_mode_level1 !=
+ device->controller_cfg->deep_sleep_mode_level2) {
+ dev_dbg(dev, "system suspend: upgrading level1->level2 via HWRESET\n");
+ ssd16xx_hw_reset(device);
+ ssd16xx_wait_for_device(device, &err);
+ ssd16xx_send_cmd(device, SSD16XX_CMD_DEEP_SLEEP_MODE, &err);
+ ssd16xx_send_data(device,
+ device->controller_cfg->deep_sleep_mode_level2,
+ &err);
+ } else {
+ dev_dbg(dev, "system suspend: already at deepest sleep mode\n");
+ }
+ device->initialized = false;
+ device->pm_force_suspended = false; /* Skip force_resume on resume */
+ return err;
+ }
+
+ /* Force runtime-suspended state for clean resume. */
+ pm_runtime_force_suspend(dev);
+ device->pm_force_suspended = true; /* Resume must call force_resume */
+
+ return 0;
+}
+
+static int ssd16xx_pm_resume(struct device *dev)
+{
+ struct ssd16xx_device *device = dev_get_drvdata(dev);
+ struct drm_device *drm = &device->drm;
+
+ dev_dbg(dev, "system resume: restoring state\n");
+
+ /*
+ * Only call force_resume if we actually called force_suspend during
+ * system suspend. The MODE_1->MODE_2 upgrade path skips force_suspend
+ * since the device is already RPM_SUSPENDED.
+ */
+ if (device->pm_force_suspended) {
+ pm_runtime_force_resume(dev);
+ device->pm_force_suspended = false;
+ }
+
+ /* Restore Mode 1 (RAM retained) for subsequent runtime PM cycles. */
+ device->deep_sleep_mode = device->controller_cfg->deep_sleep_mode_level1;
+
+ /* Hardware must be re-initialized after waking from system sleep */
+ device->initialized = false;
+
+ return drm_mode_config_helper_resume(drm);
+}
+
+static int ssd16xx_pm_runtime_suspend(struct device *dev)
+{
+ struct ssd16xx_device *device = dev_get_drvdata(dev);
+ int err = 0;
+
+ dev_dbg(dev, "runtime suspend: entering deep sleep mode 0x%02x\n",
+ device->deep_sleep_mode);
+
+ ssd16xx_send_cmd(device, SSD16XX_CMD_DEEP_SLEEP_MODE, &err);
+ ssd16xx_send_data(device, device->deep_sleep_mode, &err);
+ device->initialized = false;
+
+ return err;
+}
+
+static int ssd16xx_pm_runtime_resume(struct device *dev)
+{
+ struct ssd16xx_device *device = dev_get_drvdata(dev);
+ int ret;
+
+ dev_dbg(dev, "runtime resume: initialized=%d%s\n",
+ device->initialized,
+ !device->initialized ? " (running hw_init)" : "");
+
+ /*
+ * pm_runtime_suspend clears initialized after sending the deep sleep
+ * command. If a new app opens before the suspend fires (the put was
+ * cancelled), initialized is still true and hw_init is unnecessary.
+ */
+ if (!device->initialized) {
+ ret = ssd16xx_hw_init(device);
+ if (ret)
+ return ret;
+
+ device->initialized = true;
+ }
+
+ return 0;
+}
+
+static const struct dev_pm_ops ssd16xx_pm_ops = {
+ SYSTEM_SLEEP_PM_OPS(ssd16xx_pm_suspend, ssd16xx_pm_resume)
+ RUNTIME_PM_OPS(ssd16xx_pm_runtime_suspend, ssd16xx_pm_runtime_resume, NULL)
+};
+
static const struct of_device_id ssd16xx_of_match[] = {
{ .compatible = "gooddisplay,gdey042t81", .data = (void *)GDEY042T81 },
{ }
@@ -2060,6 +2269,7 @@ static struct spi_driver ssd16xx_spi_driver = {
.driver = {
.name = "ssd16xx",
.of_match_table = ssd16xx_of_match,
+ .pm = pm_ptr(&ssd16xx_pm_ops),
},
.probe = ssd16xx_probe,
.remove = ssd16xx_remove,
--
2.39.1
next prev parent reply other threads:[~2026-09-27 18:26 UTC|newest]
Thread overview: 32+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-27 18:23 [PATCH v2 00/14] Add DRM driver for Solomon SSD16xx e-paper display controllers Devarsh Thakkar
2026-09-27 18:23 ` [PATCH v2 01/14] dt-bindings: vendor-prefixes: Add Dalian Good Display Co., Ltd Devarsh Thakkar
2026-09-27 18:23 ` [PATCH v2 02/14] dt-bindings: display: Add Solomon SSD16xx e-paper controller binding Devarsh Thakkar
2026-09-27 18:37 ` sashiko-bot
2026-10-01 6:28 ` Krzysztof Kozlowski
2026-10-05 16:36 ` Devarsh Thakkar
2026-09-27 18:23 ` [PATCH v2 03/14] dt-bindings: display: solomon, ssd16xx: Add Solomon SSD1677 controller Devarsh Thakkar
2026-09-27 18:23 ` [PATCH v2 03/14] dt-bindings: display: solomon,ssd16xx: " Devarsh Thakkar
2026-09-27 18:35 ` sashiko-bot
2026-10-01 6:26 ` Krzysztof Kozlowski
2026-09-27 18:23 ` [PATCH v2 04/14] drm/solomon: Add DRM driver for Solomon SSD16xx e-paper display controllers Devarsh Thakkar
2026-09-27 18:42 ` sashiko-bot
2026-09-28 7:00 ` Thomas Zimmermann
2026-09-29 16:43 ` Devarsh Thakkar
2026-09-27 18:23 ` [PATCH v2 05/14] drm/solomon: ssd16xx: Add clear_on_init/close/disable session management Devarsh Thakkar
2026-09-27 18:38 ` sashiko-bot
2026-09-27 18:23 ` [PATCH v2 06/14] drm/solomon: ssd16xx: Add support for Solomon SSD1677 controller Devarsh Thakkar
2026-09-27 18:40 ` sashiko-bot
2026-09-27 18:23 ` Devarsh Thakkar [this message]
2026-09-27 18:41 ` [PATCH v2 07/14] drm/solomon: ssd16xx: Add power management support sashiko-bot
2026-09-27 18:23 ` [PATCH v2 08/14] drm/solomon: ssd16xx: Expose refresh mode as plane property Devarsh Thakkar
2026-09-27 18:43 ` sashiko-bot
2026-09-27 18:23 ` [PATCH v2 09/14] drm/solomon: ssd16xx: Expose color " Devarsh Thakkar
2026-09-27 18:43 ` sashiko-bot
2026-09-27 18:23 ` [PATCH v2 10/14] drm/solomon: ssd16xx: Expose session management as plane properties Devarsh Thakkar
2026-09-27 18:38 ` sashiko-bot
2026-09-27 18:23 ` [PATCH v2 11/14] drm/solomon: ssd16xx: support panels whose RAM X order is reversed Devarsh Thakkar
2026-09-27 18:23 ` [PATCH v2 12/14] MAINTAINERS: Add entry for Solomon SSD16xx DRM driver Devarsh Thakkar
2026-09-27 18:23 ` [PATCH v2 13/14] arm64: defconfig: Enable DRM_SSD16XX for AM62L3 EVM Devarsh Thakkar
2026-09-27 18:23 ` [DO_NOT_MERGE PATCH v2 14/14] arm64: dts: ti: Add AM62L3 EVM overlay for GDEY042T81 e-paper display Devarsh Thakkar
2026-09-27 18:40 ` sashiko-bot
2026-10-02 8:56 ` [PATCH v2 00/14] Add DRM driver for Solomon SSD16xx e-paper display controllers LiangCheng Wang
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260927182329.4193961-8-devarsht@ti.com \
--to=devarsht@ti.com \
--cc=airlied@gmail.com \
--cc=conor+dt@kernel.org \
--cc=devicetree@vger.kernel.org \
--cc=dri-devel@lists.freedesktop.org \
--cc=kristo@kernel.org \
--cc=krzk+dt@kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=maarten.lankhorst@linux.intel.com \
--cc=mripard@kernel.org \
--cc=nm@ti.com \
--cc=robh@kernel.org \
--cc=simona@ffwll.ch \
--cc=tzimmermann@suse.de \
--cc=vigneshr@ti.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.