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: 16+ 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:23 ` [PATCH v2 03/14] dt-bindings: display: solomon,ssd16xx: Add Solomon SSD1677 controller Devarsh Thakkar
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-28 7:00 ` Thomas Zimmermann
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:23 ` [PATCH v2 06/14] drm/solomon: ssd16xx: Add support for Solomon SSD1677 controller Devarsh Thakkar
2026-09-27 18:23 ` Devarsh Thakkar [this message]
2026-09-27 18:23 ` [PATCH v2 08/14] drm/solomon: ssd16xx: Expose refresh mode as plane property Devarsh Thakkar
2026-09-27 18:23 ` [PATCH v2 09/14] drm/solomon: ssd16xx: Expose color " Devarsh Thakkar
2026-09-27 18:23 ` [PATCH v2 10/14] drm/solomon: ssd16xx: Expose session management as plane properties Devarsh Thakkar
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
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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®