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>,
<linux-arm-kernel@lists.infradead.org>, <devarsht@ti.com>,
LiangCheng Wang <zaq14760@gmail.com>
Subject: [PATCH v3 07/14] drm/solomon: ssd16xx: Add power management support
Date: Tue, 20 Oct 2026 03:07:19 +0530 [thread overview]
Message-ID: <20261019213726.4145532-8-devarsht@ti.com> (raw)
In-Reply-To: <20261019213726.4145532-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 in v3:
- Check for damage before taking the runtime PM reference, so the panel is
not woken up when there is nothing to draw
- Put the panel to sleep in remove() with pm_runtime_force_suspend(),
independent of clear_on_disable. Previously it could stay powered after
unbind
- Return the error from pm_runtime_force_suspend() in system suspend
- Do not clear ->initialized again in system resume; force_resume() already
re-initializes the hardware
- Do not fail runtime suspend if the deep sleep command fails, as that
would block all later resumes; the next resume resets the panel anyway
---
drivers/gpu/drm/solomon/ssd16xx.c | 209 +++++++++++++++++++++++++++++-
1 file changed, 206 insertions(+), 3 deletions(-)
diff --git a/drivers/gpu/drm/solomon/ssd16xx.c b/drivers/gpu/drm/solomon/ssd16xx.c
index ff0e459082de..99f11b7ec03e 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>
@@ -40,6 +41,7 @@
/* SPI command codes (common) */
#define SSD16XX_CMD_DRIVER_OUTPUT_CONTROL 0x01
#define SSD16XX_CMD_BOOSTER_SOFT_START 0x0C
+#define SSD16XX_CMD_DEEP_SLEEP_MODE 0x10
#define SSD16XX_CMD_DATA_ENTRY_MODE 0x11
#define SSD16XX_CMD_SW_RESET 0x12
#define SSD16XX_CMD_MASTER_ACTIVATION 0x20
@@ -52,6 +54,9 @@
#define SSD16XX_CMD_SET_RAM_X_ADDRESS_COUNTER 0x4E
#define SSD16XX_CMD_SET_RAM_Y_ADDRESS_COUNTER 0x4F
+/* Autosuspend delay in ms */
+#define SSD16XX_PM_AUTOSUSPEND_DELAY_MS 35000
+
/*
* Data Entry Mode (command 0x11) AM/IDY/IDX bit encoding (common).
*
@@ -368,8 +373,10 @@ struct ssd16xx_device {
bool first_clear_done; /* clear_on_init has already fired once */
bool cleared_on_disable; /* Avoid redundant clear in atomic_disable */
bool cleared_on_close; /* Avoid redundant clear in 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 */
@@ -1466,12 +1473,26 @@ static void ssd16xx_plane_atomic_update(struct drm_plane *plane,
drm_dbg(&device->drm, "plane_atomic_update: fb=%p\n", fb);
- if (!fb || !device->initialized)
+ if (!fb)
goto out_exit;
+ /* Nothing to draw: don't wake the panel up. */
if (!drm_atomic_helper_damage_merged(old_state, new_state, &rect))
goto out_exit;
+ /* Resume lets the runtime_resume callback re-run hw_init if needed. */
+ 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);
+ goto out_exit;
+ }
+
+ if (!device->initialized) {
+ drm_err(&device->drm, "plane_atomic_update: hw not initialized after resume\n");
+ pm_runtime_put_autosuspend(device->drm.dev);
+ goto out_exit;
+ }
+
drm_dbg(&device->drm, "plane_atomic_update: calling fb_dirty rect=(%d,%d)-(%d,%d)\n",
rect.x1, rect.y1, rect.x2, rect.y2);
/*
@@ -1529,6 +1550,9 @@ static void ssd16xx_plane_atomic_update(struct drm_plane *plane,
device->border_waveform_pending = true;
}
+ pm_runtime_mark_last_busy(device->drm.dev);
+ pm_runtime_put_autosuspend(device->drm.dev);
+
out_exit:
drm_dev_exit(idx);
}
@@ -1590,6 +1614,10 @@ static void ssd16xx_crtc_atomic_disable(struct drm_crtc *crtc,
if (device->clear_on_disable < 0 || device->cleared_on_disable)
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,
@@ -1597,10 +1625,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->cleared_on_disable = true;
+ pm_runtime_put_sync_suspend(device->drm.dev);
out:
drm_dev_exit(idx);
}
@@ -1614,6 +1644,13 @@ static void ssd16xx_crtc_atomic_enable(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, "crtc_atomic_enable: failed to resume: %d\n", ret);
+ drm_dev_exit(idx);
+ return;
+ }
+
drm_dbg(&device->drm, "atomic_enable: %dx%d orientation=%u°\n",
device->width, device->height, device->orientation);
@@ -1621,6 +1658,7 @@ static void ssd16xx_crtc_atomic_enable(struct drm_crtc *crtc,
ret = ssd16xx_hw_init(device);
if (ret) {
drm_err(&device->drm, "atomic_enable: hw_init failed: %d\n", ret);
+ pm_runtime_put_autosuspend(device->drm.dev);
drm_dev_exit(idx);
return;
}
@@ -1645,6 +1683,8 @@ static void ssd16xx_crtc_atomic_enable(struct drm_crtc *crtc,
device->init_refresh_pending = true;
}
+ pm_runtime_mark_last_busy(device->drm.dev);
+ pm_runtime_put_autosuspend(device->drm.dev);
drm_dev_exit(idx);
}
@@ -1744,11 +1784,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->cleared_on_close = true;
+ /* sync suspend — bypass autosuspend, sleep immediately after clear */
+ pm_runtime_put_sync_suspend(drm->dev);
drm_dev_exit(idx);
}
@@ -2011,9 +2059,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 from a previous
+ * module load, which would cause pm_runtime_resume_and_get() to
+ * return -EINVAL on every subsequent call.
+ */
+ /* Use level1 (RAM retained) for runtime PM idle. */
+ device->deep_sleep_mode = device->controller_cfg->deep_sleep_mode_level1;
+
+ 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;
+ }
drm_dbg(drm, "SSD16xx e-paper display initialized (%dx%d, %d° rotation)\n",
device->width, device->height, device->orientation);
@@ -2029,6 +2094,12 @@ 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);
+ /*
+ * Put the panel to sleep regardless of clear_on_disable, which can make
+ * atomic_disable skip touching the hardware.
+ */
+ pm_runtime_force_suspend(&spi->dev);
}
static void ssd16xx_shutdown(struct spi_device *spi)
@@ -2036,8 +2107,139 @@ 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_force_suspend(&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;
+
+ /* Use level2 deep sleep for system suspend; SSD1683 loses RAM but saves more power. */
+ 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. */
+ ret = pm_runtime_force_suspend(dev);
+ if (ret) {
+ drm_mode_config_helper_resume(drm);
+ return ret;
+ }
+ 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 force_resume if we force_suspended; the level1→level2 upgrade path did not. */
+ if (device->pm_force_suspended) {
+ pm_runtime_force_resume(dev);
+ device->pm_force_suspended = false;
+ }
+
+ /* Restore level1 for subsequent runtime PM cycles. */
+ device->deep_sleep_mode = device->controller_cfg->deep_sleep_mode_level1;
+
+ 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;
+
+ /*
+ * Don't propagate SPI errors: a failure here would latch runtime_error
+ * and block all future resumes; the next resume resets the panel anyway.
+ */
+ if (err)
+ dev_warn(dev, "failed to enter deep sleep: %d\n", err);
+
+ return 0;
+}
+
+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 },
{ }
@@ -2054,6 +2256,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-10-19 21:38 UTC|newest]
Thread overview: 23+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-19 21:37 [PATCH v3 00/14] Add DRM driver for Solomon SSD16xx e-paper display controllers Devarsh Thakkar
2026-10-19 21:37 ` [PATCH v3 01/14] dt-bindings: vendor-prefixes: Add Dalian Good Display Co., Ltd Devarsh Thakkar
2026-10-19 21:37 ` [PATCH v3 02/14] dt-bindings: display: Add Solomon SSD16xx e-paper controller Devarsh Thakkar
2026-10-19 21:37 ` [PATCH v3 03/14] dt-bindings: display: solomon,ssd16xx: Add Solomon SSD1677 controller Devarsh Thakkar
2026-10-19 21:44 ` sashiko-bot
2026-10-19 21:37 ` [PATCH v3 04/14] drm/solomon: Add DRM driver for Solomon SSD16xx e-paper display controllers Devarsh Thakkar
2026-10-19 22:02 ` sashiko-bot
2026-10-19 21:37 ` [PATCH v3 05/14] drm/solomon: ssd16xx: Add clear_on_init/close/disable session management Devarsh Thakkar
2026-10-19 21:59 ` sashiko-bot
2026-10-19 21:37 ` [PATCH v3 06/14] drm/solomon: ssd16xx: Add support for Solomon SSD1677 controller Devarsh Thakkar
2026-10-19 21:37 ` Devarsh Thakkar [this message]
2026-10-19 21:57 ` [PATCH v3 07/14] drm/solomon: ssd16xx: Add power management support sashiko-bot
2026-10-19 21:37 ` [PATCH v3 08/14] drm/solomon: ssd16xx: Expose refresh mode as plane property Devarsh Thakkar
2026-10-19 22:02 ` sashiko-bot
2026-10-19 21:37 ` [PATCH v3 09/14] drm/solomon: ssd16xx: Expose color " Devarsh Thakkar
2026-10-19 21:52 ` sashiko-bot
2026-10-19 21:37 ` [PATCH v3 10/14] drm/solomon: ssd16xx: Expose session management as plane properties Devarsh Thakkar
2026-10-19 21:58 ` sashiko-bot
2026-10-19 21:37 ` [PATCH v3 11/14] drm/solomon: ssd16xx: support panels whose RAM X order is reversed Devarsh Thakkar
2026-10-19 21:37 ` [PATCH v3 12/14] MAINTAINERS: Add entry for Solomon SSD16xx DRM driver Devarsh Thakkar
2026-10-19 21:37 ` [PATCH v3 13/14] arm64: defconfig: Enable DRM_SSD16XX for AM62L3 EVM Devarsh Thakkar
2026-10-19 21:37 ` [PATCH v3 14/14] arm64: dts: ti: [DO_NOT_MERGE] Add AM62L3 EVM overlay for GDEY042T81 e-paper display Devarsh Thakkar
2026-10-19 21:59 ` sashiko-bot
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=20261019213726.4145532-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-arm-kernel@lists.infradead.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 \
--cc=zaq14760@gmail.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