mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
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


  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®