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 <[email protected]>
---
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

Reply via email to