* [PATCH 0/6] Add DRM driver for Solomon SSD16xx e-paper display controllers
@ 2026-04-30 18:33 Devarsh Thakkar
2026-04-30 18:33 ` [PATCH 1/6] dt-bindings: vendor-prefixes: Add Dalian Good Display Co., Ltd Devarsh Thakkar
` (5 more replies)
0 siblings, 6 replies; 7+ messages in thread
From: Devarsh Thakkar @ 2026-04-30 18:33 UTC (permalink / raw)
To: David Airlie, Simona Vetter, Maarten Lankhorst, Maxime Ripard,
Thomas Zimmermann, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Neil Armstrong, Bjorn Andersson, dri-devel, devicetree,
linux-kernel
Cc: praneeth, vigneshr, s-jain1, s-wang12, r-donadkar, r-sharma3,
devarsht, afd
This series adds a DRM driver for the Solomon Systech SSD16xx family of
e-paper display controllers (SSD1673, SSD1680, SSD1681, SSD1683), starting
with support for the Gooddisplay GDEY042T81, a 4.2" 400×300 black-and-white
e-paper panel driven by the SSD1683 controller.
E-paper displays are bistable: they retain the last image indefinitely
without power, making them well-suited for applications where display
content changes infrequently — dashboards, e-labels, signage, e-readers
and e-badges and low-power IoT interfaces. The driver is designed
and architected keeping this bi-stable nature and low-power requirements
along with having adequate flexibility to support varying use-case
requirements for the e-paper display use-cases.
The driver integrates with the DRM atomic modesetting framework and
supports:
- SPI communication in both 4-wire mode (separate D/C GPIO) and
3-wire mode (9-bit SPI with embedded D/C# bit)
- Full 4-way rotation via the Data Entry Mode register, with no GPU
or memory-copy overhead
- Three independently selectable refresh waveforms: partial (~300–500 ms),
full (~1.5–2 s), and fast (~1.0–1.5 s)
- 3-colour BWR pixel classification for panels with a red RAM plane
- Broad pixel format coverage: XRGB8888, RGB888, RGB565, R8, NV12,
NV16, YUYV, UYVY, R1 (native 1bpp)
- Runtime PM with configurable autosuspend: the controller enters
Mode 1 deep sleep (RAM retained) after an idle timeout, waking
transparently on the next frame update
- System suspend/resume: switches to Mode 2 deep sleep (maximum
power savings) on system suspend and rebuilds the pipeline on resume
- Runtime-configurable DRM connector properties for refresh mode,
rotation, border waveform, clear-on-init/close/disable, colour mode,
and init-refresh waveform, each with a corresponding module parameter
for boot-time or sysfs configuration
Tested on AM62L3 EVM using Gooddisplay GDEY042T81 using following branch
and test suite:
Test suite:
https://github.com/devarsht/linux-next/tree/drm_ssd16xx_epaper/ssd16xx_tests
Test scripts:
https://github.com/devarsht/linux-next/blob/drm_ssd16xx_epaper/ssd16xx_tests/run_all_tests.sh (parent script)
https://github.com/devarsht/linux-next/blob/drm_ssd16xx_epaper/ssd16xx_tests/test*.sh (Individual test scripts)
Testi console logs and summary:
https://github.com/devarsht/linux-next/blob/drm_ssd16xx_epaper/ssd16xx_tests/logs.txt
https://github.com/devarsht/linux-next/blob/drm_ssd16xx_epaper/ssd16xx_tests/results/run_all_20260430_224250.summary
Individual test results:
Supported Pixel Formats tests:
https://github.com/devarsht/linux-next/blob/drm_ssd16xx_epaper/ssd16xx_tests/results/test_formats
Integration (Misc) tests:
https://github.com/devarsht/linux-next/blob/drm_ssd16xx_epaper/ssd16xx_tests/results/test_integration_20260430_223910.result
https://github.com/devarsht/linux-next/blob/drm_ssd16xx_epaper/ssd16xx_tests/results/test_integration_20260430_225207.result
Module params tests:
https://github.com/devarsht/linux-next/blob/drm_ssd16xx_epaper/ssd16xx_tests/results/test_module_params_20260430_224700.result
PM Automated tests:
https://github.com/devarsht/linux-next/blob/drm_ssd16xx_epaper/ssd16xx_tests/results/test_pm_20260430_225028.result
DRM properties tests:
https://github.com/devarsht/linux-next/blob/drm_ssd16xx_epaper/ssd16xx_tests/results/test_properties_20260430_224329.result
Devarsh Thakkar (6):
dt-bindings: vendor-prefixes: Add Dalian Good Display Co., Ltd.
dt-bindings/display: Add Solomon SSD16xx e-paper controller binding
drm/tiny: Add DRM driver for Solomon SSD16xx e-paper display
controllers
drm/tiny: panel-ssd16xx: Add power management support
MAINTAINERS: Add entry for Solomon SSD16xx DRM driver
arm64: defconfig: Enable DRM_PANEL_SSD16XX
.../bindings/display/solomon,ssd16xx.yaml | 81 +
.../devicetree/bindings/vendor-prefixes.yaml | 2 +
MAINTAINERS | 7 +
arch/arm64/configs/defconfig | 1 +
drivers/gpu/drm/tiny/Kconfig | 13 +
drivers/gpu/drm/tiny/Makefile | 1 +
drivers/gpu/drm/tiny/panel-ssd16xx.c | 2731 +++++++++++++++++
7 files changed, 2836 insertions(+)
create mode 100644 Documentation/devicetree/bindings/display/solomon,ssd16xx.yaml
create mode 100644 drivers/gpu/drm/tiny/panel-ssd16xx.c
--
2.39.1
^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH 1/6] dt-bindings: vendor-prefixes: Add Dalian Good Display Co., Ltd.
2026-04-30 18:33 [PATCH 0/6] Add DRM driver for Solomon SSD16xx e-paper display controllers Devarsh Thakkar
@ 2026-04-30 18:33 ` Devarsh Thakkar
2026-04-30 18:33 ` [PATCH 2/6] dt-bindings/display: Add Solomon SSD16xx e-paper controller binding Devarsh Thakkar
` (4 subsequent siblings)
5 siblings, 0 replies; 7+ messages in thread
From: Devarsh Thakkar @ 2026-04-30 18:33 UTC (permalink / raw)
To: David Airlie, Simona Vetter, Maarten Lankhorst, Maxime Ripard,
Thomas Zimmermann, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Neil Armstrong, Bjorn Andersson, dri-devel, devicetree,
linux-kernel
Cc: praneeth, vigneshr, s-jain1, s-wang12, r-donadkar, r-sharma3,
devarsht, afd
Add vendor prefix 'gooddisplay' for Dalian Good Display Co., Ltd., a
manufacturer of e-paper display panels and modules.
This prefix will be used for the GDEY042T81 e-paper display panel in
the solomon,ssd16xx binding.
Signed-off-by: Devarsh Thakkar <devarsht@ti.com>
---
Documentation/devicetree/bindings/vendor-prefixes.yaml | 2 ++
1 file changed, 2 insertions(+)
diff --git a/Documentation/devicetree/bindings/vendor-prefixes.yaml b/Documentation/devicetree/bindings/vendor-prefixes.yaml
index 00bffb9c4469..04daa3245919 100644
--- a/Documentation/devicetree/bindings/vendor-prefixes.yaml
+++ b/Documentation/devicetree/bindings/vendor-prefixes.yaml
@@ -662,6 +662,8 @@ patternProperties:
description: GOcontroll Modular Embedded Electronics B.V.
"^goldelico,.*":
description: Golden Delicious Computers GmbH & Co. KG
+ "^gooddisplay,.*":
+ description: Dalian Good Display Co., Ltd.
"^goodix,.*":
description: Shenzhen Huiding Technology Co., Ltd.
"^google,.*":
--
2.39.1
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH 2/6] dt-bindings/display: Add Solomon SSD16xx e-paper controller binding
2026-04-30 18:33 [PATCH 0/6] Add DRM driver for Solomon SSD16xx e-paper display controllers Devarsh Thakkar
2026-04-30 18:33 ` [PATCH 1/6] dt-bindings: vendor-prefixes: Add Dalian Good Display Co., Ltd Devarsh Thakkar
@ 2026-04-30 18:33 ` Devarsh Thakkar
2026-04-30 18:33 ` [PATCH 3/6] drm/tiny: Add DRM driver for Solomon SSD16xx e-paper display controllers Devarsh Thakkar
` (3 subsequent siblings)
5 siblings, 0 replies; 7+ messages in thread
From: Devarsh Thakkar @ 2026-04-30 18:33 UTC (permalink / raw)
To: David Airlie, Simona Vetter, Maarten Lankhorst, Maxime Ripard,
Thomas Zimmermann, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Neil Armstrong, Bjorn Andersson, dri-devel, devicetree,
linux-kernel
Cc: praneeth, vigneshr, s-jain1, s-wang12, r-donadkar, r-sharma3,
devarsht, afd
Add device tree binding for the Solomon Systech SSD16xx family of e-Paper
display controllers (SSD1683 [1], SSD1673 [3], SSD1680 [4], SSD1681 [5]).
The binding covers the 4-wire SPI interface with required GPIO pins for
reset (active-low), busy status (active-high), and data/command selection.
The spi-max-frequency is capped at 20 MHz per the SSD1683 datasheet [1]
which is also the maximum supported frequency amongst the family.
An optional rotation property allows static display orientation to be
configured from device tree.
The data/command selection GPIO pin is also kept as optional since various
SSD16XX controllers such as SSD1683 [1] support 3-wire mode too with
data/command selection bit transmitted as first bit before sending the data
sequence.
The first supported compatible is for Gooddisplay GDEY042T81 which is 4.2",
400x300 resolution black/white e-paper display using SSD1683 controller.
Links:
[1] : https://www.crystalfontz.com/controllers/SolomonSystech/SSD1683
[2] : https://files.seeedstudio.com/wiki/Other_Display/42-epaper/GDEY042T81.pdf
[3] : https://www.crystalfontz.com/controllers/SolomonSystech/SSD1673
[4] : https://www.crystalfontz.com/controllers/SolomonSystech/SSD1680
[5] : https://www.crystalfontz.com/controllers/SolomonSystech/SSD1681
Signed-off-by: Devarsh Thakkar <devarsht@ti.com>
---
.../bindings/display/solomon,ssd16xx.yaml | 81 +++++++++++++++++++
1 file changed, 81 insertions(+)
create mode 100644 Documentation/devicetree/bindings/display/solomon,ssd16xx.yaml
diff --git a/Documentation/devicetree/bindings/display/solomon,ssd16xx.yaml b/Documentation/devicetree/bindings/display/solomon,ssd16xx.yaml
new file mode 100644
index 000000000000..2433ddb859d6
--- /dev/null
+++ b/Documentation/devicetree/bindings/display/solomon,ssd16xx.yaml
@@ -0,0 +1,81 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/display/solomon,ssd16xx.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Solomon Systech SSD16xx controller based e-paper display panels
+
+maintainers:
+ - Devarsh Thakkar <devarsht@ti.com>
+
+description:
+ The SSD16xx family includes e-paper display controllers (SSD1680, SSD1681,
+ SSD1673, SSD1683) with integrated gate driver, source driver and timing controller.
+ Different panels use these controllers in monochrome or 3-color configurations.
+ The controller uses a 4-wire SPI interface and requires GPIO pins for reset,
+ busy status, and data/command control with a 3-wire mode also available
+ where D/C bit is transmitted along the SPI data line before sending actual data.
+
+allOf:
+ - $ref: /schemas/spi/spi-peripheral-props.yaml#
+
+properties:
+ compatible:
+ enum:
+ - gooddisplay,gdey042t81 # 4.2" 400x300 monochrome (SSD1683 controller)
+
+ reg:
+ maxItems: 1
+
+ spi-max-frequency:
+ maximum: 20000000
+
+ reset-gpios:
+ maxItems: 1
+ description:
+ GPIO connected to the RSTB (reset) pin. Active low.
+
+ busy-gpios:
+ maxItems: 1
+ description:
+ GPIO connected to the BUSY pin. Active high when the controller is
+ busy updating the display.
+
+ dc-gpios:
+ maxItems: 1
+ description:
+ GPIO connected to the D/C (Data/Command) pin. Low for command, high
+ for data.
+
+ rotation:
+ enum: [0, 90, 180, 270]
+ description:
+ Display rotation in degrees counter clockwise (0,90,180,270)
+ $ref: /schemas/types.yaml#/definitions/uint32
+
+required:
+ - compatible
+ - reg
+ - reset-gpios
+ - busy-gpios
+
+unevaluatedProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/gpio/gpio.h>
+
+ spi {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ display@0 {
+ compatible = "gooddisplay,gdey042t81";
+ reg = <0>;
+ spi-max-frequency = <2000000>;
+ reset-gpios = <&gpio1 17 GPIO_ACTIVE_LOW>;
+ busy-gpios = <&gpio1 18 GPIO_ACTIVE_HIGH>;
+ dc-gpios = <&gpio1 19 GPIO_ACTIVE_HIGH>;
+ };
+ };
--
2.39.1
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH 3/6] drm/tiny: Add DRM driver for Solomon SSD16xx e-paper display controllers
2026-04-30 18:33 [PATCH 0/6] Add DRM driver for Solomon SSD16xx e-paper display controllers Devarsh Thakkar
2026-04-30 18:33 ` [PATCH 1/6] dt-bindings: vendor-prefixes: Add Dalian Good Display Co., Ltd Devarsh Thakkar
2026-04-30 18:33 ` [PATCH 2/6] dt-bindings/display: Add Solomon SSD16xx e-paper controller binding Devarsh Thakkar
@ 2026-04-30 18:33 ` Devarsh Thakkar
2026-04-30 18:33 ` [PATCH 4/6] drm/tiny: panel-ssd16xx: Add power management support Devarsh Thakkar
` (2 subsequent siblings)
5 siblings, 0 replies; 7+ messages in thread
From: Devarsh Thakkar @ 2026-04-30 18:33 UTC (permalink / raw)
To: David Airlie, Simona Vetter, Maarten Lankhorst, Maxime Ripard,
Thomas Zimmermann, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Neil Armstrong, Bjorn Andersson, dri-devel, devicetree,
linux-kernel
Cc: praneeth, vigneshr, s-jain1, s-wang12, r-donadkar, r-sharma3,
devarsht, afd
Add a DRM driver for the Solomon Systech SSD16xx family of e-paper display
controllers (SSD1680 [4], SSD1673 [3], SSD1681 [5], SSD1683 [1]) based
e-paper display panels, starting with support for the Gooddisplay
GDEY042T81 which is 4.2", 400x300 resolution black/white e-paper display
using SSD1683 controller.
Hardware features supported:
- SPI communication with optional D/C GPIO (4-wire mode) or 9-bit SPI
with embedded D/C# bit (3-wire mode) command/data signaling
- BUSY GPIO polling with configurable timeout
- Hardware reset via dedicated reset GPIO
- Internal temperature sensor readout
- Border waveform control with 10 configurable modes:
LUT0-3 (GS transition waveforms), fixed voltage levels (VSS/VSH1/VSL/
VSH2), VCOM follow, and HiZ (floating, default)
- Deep sleep modes: Mode 1 (RAM retained) and Mode 2 (RAM lost)
- Data entry modes for all four orientations
- Dual display RAM planes: BW RAM (primary image) and RED RAM
(partial refresh baseline / 3-color BWR red pixels)
Refresh modes:
Three independently selectable refresh waveforms:
- Partial (~300-500 ms): low-latency BW update using RED RAM as
waveform transition baseline
- Full (~1.5-2 s): complete pixel-cycle reset with temperature-
compensated LUT reload
- Fast (~1.0-1.5 s): abbreviated waveform using a pre-loaded LUT
Pixel format conversions:
The driver accepts all common DRM formats and converts to 1bpp:
- R1 (native 1bpp), color formats (XRGB8888, RGB888, RGB565),
packed YUV (YUYV, UYVY), planar YUV (NV12, NV16), grayscale (R8)
- 3-color BWR: classifies pixels as black, white, or red by dominant
channel; writes separate 1bpp BW and RED buffers
Orientation / rotation:
- Full 4-way rotation: 0° (landscape), 90° (portrait CW),
180° (landscape inverted), 270° (portrait CCW)
- Implemented via the Data Entry Mode register — no GPU or memory
rotation overhead
- Configurable via device tree 'rotation' property, module parameter
(overrides DT), or runtime DRM connector property (highest priority)
DRM framework integration:
- drm_atomic_helper-based modesetting with damage tracking
- Single primary plane, CRTC, simple encoder, SPI connector
- drm_gem_dma backend for physically contiguous SPI DMA buffers
- fbdev fallback via DRM_FBDEV_DMA_DRIVER_OPS
E-paper panels are bistable: they retain the last displayed image
indefinitely without power. Ghosting artefacts may accumulate over time,
and based on user policy the driver can switch to a full refresh mode to
clear them. Similarly, based on user policy, the driver can clear the
screen on every application start or stop to provide a clean slate for the
next application although some applications may require preserving the
last displayed context when going to suspend or exiting. To provide such
flexibility, the driver introduces the following DRM connector properties
along with corresponding module parameters:
DRM connector properties (runtime configurable):
- rotation: Display orientation in degrees (0, 90, 180, 270).
- refresh_mode: Select update waveform (0=partial, 1=full, 2=fast).
Affects all subsequent frames until changed.
- refresh_mode_init: Waveform for the first frame after CRTC enable or a
new DRM master taking control (0=disabled, 1=partial, 2=full, 3=fast).
After the first frame the driver reverts to refresh_mode. Armed via
drm_driver.master_set; disarmed by master_drop if the client exits
without submitting a frame.
- clear_on_init: Clear display on first DRM client open (0=disabled,
1=partial, 2=full, 3=fast).
- clear_on_close: Clear display when the displaying client exits
(0=disabled, 1=partial, 2=full, 3=fast). Fires via
drm_driver.master_drop when a client drops DRM master.
It also disarms init_refresh_pending so no stale init refresh leaks to
the next client.
- clear_on_disable: Clear display on CRTC disable / DPMS off
(0=disabled, 1=partial, 2=full, 3=fast). Fires via
crtc_atomic_disable. Independent of clear_on_close — a
display_cleared_on_deinit guard prevents a redundant second clear
when both fire for the same session (e.g. compositor does DPMS off
then exits).
- color_mode: Selects whether a red-plane-capable panel is driven in
3-color (black/white/red) or black-white-only mode (0=black-white,
1=3-color). 3-color is only valid for panels with red plane support;
black-white mode bypasses the RED RAM regardless of panel capability,
allowing a BWR panel to be driven as a faster BW display.
- border_waveform_init: Border waveform during clear/init operations
(enum 0-9: LUT0-3, VSS, VSH1, VSL, VSH2, VCOM, HiZ).
- border_waveform_refresh: Border waveform during display updates
(enum 0-9, same values).
- border_refresh_on_every_update: When disabled, border waveform
command is sent only when the border state changes (0=disabled,
1=enabled).
Module parameters (override DT / panel config defaults):
Each DRM connector property above has a corresponding module parameter
that sets its initial value at load time, providing the same control via
sysfs and the kernel command line without requiring a DRM-capable userspace
or handling. All parameters default to -1 (use panel config default) so
that a bare insmod with no arguments works correctly out of the box.
Links:
[1] : https://www.crystalfontz.com/controllers/SolomonSystech/SSD1683
[2] : https://files.seeedstudio.com/wiki/Other_Display/42-epaper/GDEY042T81.pdf
[3] : https://www.crystalfontz.com/controllers/SolomonSystech/SSD1673
[4] : https://www.crystalfontz.com/controllers/SolomonSystech/SSD1680
[5] : https://www.crystalfontz.com/controllers/SolomonSystech/SSD1681
Signed-off-by: Devarsh Thakkar <devarsht@ti.com>
---
drivers/gpu/drm/tiny/Kconfig | 13 +
drivers/gpu/drm/tiny/Makefile | 1 +
drivers/gpu/drm/tiny/panel-ssd16xx.c | 2548 ++++++++++++++++++++++++++
3 files changed, 2562 insertions(+)
create mode 100644 drivers/gpu/drm/tiny/panel-ssd16xx.c
diff --git a/drivers/gpu/drm/tiny/Kconfig b/drivers/gpu/drm/tiny/Kconfig
index f0e72d4b6a47..d25363962a1e 100644
--- a/drivers/gpu/drm/tiny/Kconfig
+++ b/drivers/gpu/drm/tiny/Kconfig
@@ -215,3 +215,16 @@ config TINYDRM_SHARP_MEMORY
* 4.40" Sharp Memory LCD (LS044Q7DH01)
If M is selected the module will be called sharp_memory.
+
+config DRM_PANEL_SSD16XX
+ tristate "DRM support for Solomon SSD16xx e-ink display controllers"
+ depends on DRM && SPI
+ select DRM_CLIENT_SELECTION
+ select DRM_KMS_HELPER
+ select DRM_GEM_DMA_HELPER
+ help
+ DRM driver for Solomon SSD16xx family e-ink display controllers
+ (SSD1680, SSD1681, SSD1683). Supports the GDEY042T81 4.2" 400x300
+ monochrome e-ink panel.
+
+ If M is selected the module will be called panel-ssd16xx.
diff --git a/drivers/gpu/drm/tiny/Makefile b/drivers/gpu/drm/tiny/Makefile
index 48d30bf6152f..3f24e5c6a79a 100644
--- a/drivers/gpu/drm/tiny/Makefile
+++ b/drivers/gpu/drm/tiny/Makefile
@@ -15,3 +15,4 @@ obj-$(CONFIG_TINYDRM_ILI9486) += ili9486.o
obj-$(CONFIG_TINYDRM_MI0283QT) += mi0283qt.o
obj-$(CONFIG_TINYDRM_REPAPER) += repaper.o
obj-$(CONFIG_TINYDRM_SHARP_MEMORY) += sharp-memory.o
+obj-$(CONFIG_DRM_PANEL_SSD16XX) += panel-ssd16xx.o
diff --git a/drivers/gpu/drm/tiny/panel-ssd16xx.c b/drivers/gpu/drm/tiny/panel-ssd16xx.c
new file mode 100644
index 000000000000..b232837c54ff
--- /dev/null
+++ b/drivers/gpu/drm/tiny/panel-ssd16xx.c
@@ -0,0 +1,2548 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * DRM driver for e-paper display panels using Solomon SSD16xx family controllers
+ *
+ * Copyright (C) 2026 Texas Instruments Incorporated - https://www.ti.com/
+ *
+ * Author: Devarsh Thakkar <devarsht@ti.com>
+ *
+ * References: https://github.com/Lesords/epaper
+ */
+
+#include <linux/delay.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/property.h>
+#include <linux/spi/spi.h>
+
+#include <drm/clients/drm_client_setup.h>
+#include <drm/drm_atomic.h>
+#include <drm/drm_atomic_helper.h>
+#include <drm/drm_damage_helper.h>
+#include <drm/drm_drv.h>
+#include <drm/drm_fb_helper.h>
+#include <drm/drm_fbdev_dma.h>
+#include <drm/drm_fb_dma_helper.h>
+#include <drm/drm_framebuffer.h>
+#include <drm/drm_gem_dma_helper.h>
+#include <drm/drm_gem_framebuffer_helper.h>
+#include <drm/drm_probe_helper.h>
+#include <drm/drm_simple_kms_helper.h>
+#include <drm/drm_print.h>
+
+static int rotation = -1;
+module_param(rotation, int, 0644);
+MODULE_PARM_DESC(rotation,
+ "Display rotation (-1=use DT, 0/180=landscape, 90/270=portrait)");
+
+static int refresh_mode = -1;
+module_param(refresh_mode, int, 0644);
+MODULE_PARM_DESC(refresh_mode,
+ "Refresh mode (-1=panel default, 0=partial ~300-500ms, 1=full ~1.5-2s, 2=fast ~1.0-1.5s)");
+
+static int border_waveform_init_lut = -1;
+module_param(border_waveform_init_lut, int, 0644);
+MODULE_PARM_DESC(border_waveform_init_lut,
+ "Border waveform index during clear/init (-1=panel default, 0-9=enum index)");
+
+static int border_waveform_lut = -1;
+module_param(border_waveform_lut, int, 0644);
+MODULE_PARM_DESC(border_waveform_lut,
+ "Border waveform index during display updates (-1=panel default, 0-9=enum index)");
+
+static bool border_refresh_on_every_update;
+module_param(border_refresh_on_every_update, bool, 0644);
+MODULE_PARM_DESC(border_refresh_on_every_update,
+ "Re-send border waveform command before each display update (default: false)");
+
+static int clear_on_init = -1;
+module_param(clear_on_init, int, 0644);
+MODULE_PARM_DESC(clear_on_init,
+ "Clear display on first app launch (-1=disabled, 0=partial, 1=full, 2=fast)");
+
+static int clear_on_close = -1;
+module_param(clear_on_close, int, 0644);
+MODULE_PARM_DESC(clear_on_close,
+ "Clear display on app close/CRTC disable (-1=disabled, 0=partial, 1=full, 2=fast)");
+
+static int clear_on_disable = -1;
+module_param(clear_on_disable, int, 0644);
+MODULE_PARM_DESC(clear_on_disable,
+ "Clear display on CRTC disable/DPMS off (-1=disabled, 0=partial, 1=full, 2=fast)");
+
+static int refresh_mode_init = -1;
+module_param(refresh_mode_init, int, 0644);
+MODULE_PARM_DESC(refresh_mode_init,
+ "Skip baseline establishment on first enable (-1=disabled, 0=partial, 1=full, 2=fast)");
+
+static int color_mode = -1;
+module_param(color_mode, int, 0644);
+MODULE_PARM_DESC(color_mode,
+ "Color mode (-1=panel default, 0=black-white, 1=3-color; 3-color only valid for panels with red plane support)");
+
+/* -----------------------------------------------------------------------
+ * SSD16xx family common: commands, data values, and bit definitions.
+ * These apply equally to SSD1673, SSD1680, and SSD1683.
+ * -----------------------------------------------------------------------
+ */
+
+/* SPI command codes (common) */
+#define SSD16XX_CMD_DRIVER_OUTPUT_CONTROL 0x01
+#define SSD16XX_CMD_DATA_ENTRY_MODE 0x11
+#define SSD16XX_CMD_SW_RESET 0x12
+#define SSD16XX_CMD_MASTER_ACTIVATION 0x20
+#define SSD16XX_CMD_DISPLAY_UPDATE_CONTROL1 0x21
+#define SSD16XX_CMD_DISPLAY_UPDATE_CONTROL2 0x22
+#define SSD16XX_CMD_WRITE_RAM_BW 0x24
+#define SSD16XX_CMD_BORDER_WAVEFORM_CONTROL 0x3C
+#define SSD16XX_CMD_SET_RAM_X_ADDRESS_START_END 0x44
+#define SSD16XX_CMD_SET_RAM_Y_ADDRESS_START_END 0x45
+#define SSD16XX_CMD_SET_RAM_X_ADDRESS_COUNTER 0x4E
+#define SSD16XX_CMD_SET_RAM_Y_ADDRESS_COUNTER 0x4F
+
+/*
+ * Data Entry Mode (command 0x11) AM/IDY/IDX bit encoding (common).
+ *
+ * Bit 2 (AM): Address update direction: 0 = X direction, 1 = Y direction
+ * ID[1:0] when AM=0 (X-direction modes, address counter advances in X):
+ * 00 = X decrement, Y decrement 01 = X increment, Y decrement
+ * 10 = X decrement, Y increment 11 = X increment, Y increment (default)
+ *
+ * Rotation to data entry mode mapping (actual implementation uses two modes,
+ * with scan direction controlled via RAM cursor positioning and manual tweaking):
+ * 0°/270° → 0x03 (X++, Y++) Landscape/Portrait-CW: cursor at (0, 0)
+ * 90°/180° → 0x00 (X--, Y--) Portrait-CCW/Upside-down: cursor at (max, max)
+ *
+ * The pixel packing in convert_fb_to_1bpp is grouped by physical layout:
+ * - Portrait (90°/270°): column-major packing, rightmost column first
+ * - Landscape (0°/180°): row-major packing, top to bottom, left to right
+ * Hardware cursor position and scan mode handle the final orientation.
+ */
+#define SSD16XX_DATA_ENTRY_XDEC_YDEC 0x00 /* X--, Y-- (X-mode) */
+#define SSD16XX_DATA_ENTRY_XINC_YINC 0x03 /* X++, Y++ (X-mode, default) */
+
+/* POR reset value: GD=0 (G0 first), SM=0 (interlaced), TB=0 (G0->G299) */
+#define SSD16XX_DRIVER_OUTPUT_CTRL_DEFAULT 0x00
+
+/* Display Update Control 1 (0x21) byte 2 default (common) */
+#define SSD16XX_CTRL1_BYTE2_DEFAULT 0x00
+
+/*
+ * Display Update Control 2 (0x22) individual bit definitions (common).
+ * NOTE: BIT(3) is NOT common — see SSD1683_CTRL2_MODE2 in the SSD1683
+ * section below; it has a completely different meaning in SSD1673.
+ */
+#define SSD16XX_CTRL2_ENABLE_CLK BIT(7)
+#define SSD16XX_CTRL2_ENABLE_ANALOG BIT(6)
+#define SSD16XX_CTRL2_LOAD_TEMPERATURE BIT(5)
+#define SSD16XX_CTRL2_LOAD_LUT BIT(4)
+#define SSD16XX_CTRL2_DISPLAY BIT(2)
+#define SSD16XX_CTRL2_DISABLE_ANALOG BIT(1)
+#define SSD16XX_CTRL2_DISABLE_CLK BIT(0)
+
+#define SSD16XX_SPI_BITS_PER_WORD 8
+#define SSD16XX_SPI_SPEED_DEFAULT 1000000
+
+/* Maximum time to wait for the BUSY pin to deassert after a display update */
+#define SSD16XX_BUSY_WAIT_TIMEOUT_MS 6000
+
+/* -----------------------------------------------------------------------
+ * SSD1683 / SSD1680 specific: commands, data values, and bit definitions.
+ * -----------------------------------------------------------------------
+ */
+
+/*
+ * Deep Sleep Mode values (command 0x10).
+ */
+#define SSD1683_DEEP_SLEEP_MODE_1 0x01 /* RAM retained */
+#define SSD1683_DEEP_SLEEP_MODE_2 0x03 /* RAM lost (max power) */
+
+/*
+ * Temperature Sensor Selection (command 0x18).
+ */
+#define SSD1683_CMD_TEMPERATURE_SENSOR_CONTROL 0x18
+#define SSD1683_TEMP_SENSOR_INTERNAL 0x80 /* Bit 7: use internal sensor */
+
+/*
+ * Write RED RAM (command 0x26).
+ */
+#define SSD1683_CMD_WRITE_RAM_RED 0x26
+
+/*
+ * Border Waveform Control (command 0x3C) byte values.
+ */
+#define SSD1683_BORDER_WAVEFORM_LUT0 0x00 /* GS Transition LUT0 (black) */
+#define SSD1683_BORDER_WAVEFORM_LUT1 0x01 /* GS Transition LUT1 (white) */
+#define SSD1683_BORDER_WAVEFORM_LUT2 0x02 /* GS Transition LUT2 (black) */
+#define SSD1683_BORDER_WAVEFORM_LUT3 0x03 /* GS Transition LUT3 (gray) */
+#define SSD1683_BORDER_WAVEFORM_FIXLVL_VSS 0x40 /* Fix Level VSS (0V, black) */
+#define SSD1683_BORDER_WAVEFORM_FIXLVL_VSH1 0x50 /* Fix Level VSH1 (+15V, black) */
+#define SSD1683_BORDER_WAVEFORM_FIXLVL_VSL 0x60 /* Fix Level VSL (-15V, white) */
+#define SSD1683_BORDER_WAVEFORM_FIXLVL_VSH2 0x70 /* Fix Level VSH2 (+15V alt, black) */
+#define SSD1683_BORDER_WAVEFORM_VCOM 0x80 /* Follow VCOM (-2V~-3V, preserve) */
+#define SSD1683_BORDER_WAVEFORM_HIZ 0xC0 /* HiZ (floating, default) */
+
+/*
+ * Display Update Control 1 (0x21) byte 1 — RED RAM control.
+ */
+#define SSD1683_CTRL1_NORMAL 0x00 /* Both BW and RED RAMs enabled */
+#define SSD1683_CTRL1_BYPASS_RED_RAM 0x40 /* Bypass RED RAM (force RED=0) */
+
+/*
+ * Display Update Control 2 (0x22) BIT(3) — "Display Mode 2" (partial/BW).
+ */
+#define SSD1683_CTRL2_MODE2 BIT(3)
+
+/* Composite CTRL2 sequences for each refresh mode */
+#define SSD1683_CTRL2_FULL_REFRESH (SSD16XX_CTRL2_ENABLE_CLK | \
+ SSD16XX_CTRL2_ENABLE_ANALOG | \
+ SSD16XX_CTRL2_LOAD_TEMPERATURE | \
+ SSD16XX_CTRL2_LOAD_LUT | \
+ SSD16XX_CTRL2_DISPLAY | \
+ SSD16XX_CTRL2_DISABLE_ANALOG | \
+ SSD16XX_CTRL2_DISABLE_CLK) /* 0xF7, ~1.5-2s */
+
+#define SSD1683_CTRL2_FAST_REFRESH (SSD16XX_CTRL2_ENABLE_CLK | \
+ SSD16XX_CTRL2_ENABLE_ANALOG | \
+ SSD16XX_CTRL2_DISPLAY | \
+ SSD16XX_CTRL2_DISABLE_ANALOG | \
+ SSD16XX_CTRL2_DISABLE_CLK) /* 0xC7, ~1.0-1.5s */
+
+#define SSD1683_CTRL2_PARTIAL_REFRESH (SSD16XX_CTRL2_ENABLE_CLK | \
+ SSD16XX_CTRL2_ENABLE_ANALOG | \
+ SSD16XX_CTRL2_LOAD_TEMPERATURE | \
+ SSD16XX_CTRL2_LOAD_LUT | \
+ SSD1683_CTRL2_MODE2 | \
+ SSD16XX_CTRL2_DISPLAY | \
+ SSD16XX_CTRL2_DISABLE_ANALOG | \
+ SSD16XX_CTRL2_DISABLE_CLK) /* 0xFF, ~300-500ms */
+
+/*
+ * Standalone LUT pre-load sequence (0x91 = ENABLE_CLK | LOAD_LUT | LOAD_TEMPERATURE |
+ * DISABLE_CLK).
+ * Pre-loads the OTP LUT without triggering a display update. Required for
+ * FAST refresh mode (0xC7) which omits LOAD_LUT from each update cycle.
+ */
+#define SSD1683_CTRL2_LOAD_TEMP_LUT (SSD16XX_CTRL2_ENABLE_CLK | \
+ SSD16XX_CTRL2_LOAD_LUT | \
+ SSD16XX_CTRL2_LOAD_TEMPERATURE | \
+ SSD16XX_CTRL2_DISABLE_CLK) /* 0xB1 */
+
+MODULE_IMPORT_NS("DMA_BUF");
+
+enum ssd16xx_controller {
+ SSD1683 = 1,
+};
+
+enum ssd16xx_model {
+ GDEY042T81 = 1,
+};
+
+enum ssd16xx_refresh_mode {
+ SSD16XX_REFRESH_PARTIAL = 0, /* Partial refresh (~300-500ms) */
+ SSD16XX_REFRESH_FULL, /* Full refresh (~1.5-2s) */
+ SSD16XX_REFRESH_FAST, /* Fast refresh, skip temp load (~1.0-1.5s) */
+};
+
+enum ssd16xx_color_mode {
+ SSD16XX_COLOR_MODE_BW = 0, /* Black/white only; RED RAM always bypassed */
+ SSD16XX_COLOR_MODE_3COLOR = 1, /* 3-colour BWR; RED RAM used for red pixels */
+};
+
+/* Border waveform enum indices (0-9); mapped to HW bytes via
+ * controller_cfg->border_waveform_table[]
+ */
+enum ssd16xx_border_waveform {
+ SSD16XX_BORDER_LUT0 = 0, /* GS Transition LUT0 (black) */
+ SSD16XX_BORDER_LUT1, /* GS Transition LUT1 (white) */
+ SSD16XX_BORDER_LUT2, /* GS Transition LUT2 (black) */
+ SSD16XX_BORDER_LUT3, /* GS Transition LUT3 (gray) */
+ SSD16XX_BORDER_VSS, /* Fix Level VSS (black) */
+ SSD16XX_BORDER_VSH1, /* Fix Level VSH1 (black) */
+ SSD16XX_BORDER_VSL, /* Fix Level VSL (white) */
+ SSD16XX_BORDER_VSH2, /* Fix Level VSH2 (black) */
+ SSD16XX_BORDER_VCOM, /* Follow VCOM (preserve) */
+ SSD16XX_BORDER_HIZ, /* HiZ (floating, default) */
+};
+
+/* SSD1683/SSD1680 border waveform byte encoding for command 0x3C */
+static const u8 ssd1683_border_waveform_table[] = {
+ [SSD16XX_BORDER_LUT0] = SSD1683_BORDER_WAVEFORM_LUT0,
+ [SSD16XX_BORDER_LUT1] = SSD1683_BORDER_WAVEFORM_LUT1,
+ [SSD16XX_BORDER_LUT2] = SSD1683_BORDER_WAVEFORM_LUT2,
+ [SSD16XX_BORDER_LUT3] = SSD1683_BORDER_WAVEFORM_LUT3,
+ [SSD16XX_BORDER_VSS] = SSD1683_BORDER_WAVEFORM_FIXLVL_VSS,
+ [SSD16XX_BORDER_VSH1] = SSD1683_BORDER_WAVEFORM_FIXLVL_VSH1,
+ [SSD16XX_BORDER_VSL] = SSD1683_BORDER_WAVEFORM_FIXLVL_VSL,
+ [SSD16XX_BORDER_VSH2] = SSD1683_BORDER_WAVEFORM_FIXLVL_VSH2,
+ [SSD16XX_BORDER_VCOM] = SSD1683_BORDER_WAVEFORM_VCOM,
+ [SSD16XX_BORDER_HIZ] = SSD1683_BORDER_WAVEFORM_HIZ,
+};
+
+struct ssd16xx_controller_config {
+ u16 max_width;
+ u16 max_height;
+ u8 ram_x_address_bits;
+ u8 ram_y_address_bits;
+
+ /*
+ * has_temp_sensor_ctrl: controller supports command 0x18 (Temperature
+ * Sensor Selection). Present in SSD1683/SSD1680; absent in SSD1673
+ * which uses command 0x1A (direct temperature write) instead.
+ */
+ bool has_temp_sensor_ctrl;
+
+ /*
+ * 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.
+ */
+ u8 deep_sleep_mode_level1;
+ u8 deep_sleep_mode_level2;
+
+ /*
+ * border_waveform_table: chip-specific byte values for the 10 logical
+ * border waveform modes (indexed by enum ssd16xx_border_waveform).
+ * The encoding of command 0x3C differs between SSD1683/SSD1680 and
+ * SSD1673, so each controller provides its own translation table.
+ */
+ const u8 *border_waveform_table;
+
+ /*
+ * Display Update Control 1 (cmd 0x21) byte 1 values.
+ * ctrl1_normal: both BW and RED RAMs participate in the waveform.
+ * ctrl1_bypass_red_ram: RED RAM bypassed; waveform driven from BW RAM only.
+ * SSD1673 has no RED RAM so both fields carry the same value.
+ */
+ u8 ctrl1_normal;
+ u8 ctrl1_bypass_red_ram;
+
+ /*
+ * Display Update Control 2 (cmd 0x22) composite sequences for each
+ * refresh mode (indexed by enum ssd16xx_refresh_mode) and the
+ * standalone LUT pre-load sequence used before fast refresh.
+ * Values differ between SSD1683/SSD1680 and SSD1673 (MODE2 bit, etc.).
+ */
+ u8 ctrl2_refresh[3]; /* indexed by SSD16XX_REFRESH_PARTIAL/FULL/FAST */
+ u8 ctrl2_load_temp_lut; /* standalone LUT pre-load (no display update) */
+};
+
+struct ssd16xx_panel_config {
+ /* Data Entry Mode - controls X/Y increment direction for landscape (0°) */
+ u8 data_entry_mode;
+
+ /* Driver Output Control - third byte (scan direction) */
+ u8 driver_output_ctrl_byte3;
+
+ /* Default refresh mode for this panel */
+ enum ssd16xx_refresh_mode default_refresh_mode;
+
+ /* Default border waveform during clear/init (enum index 0-9) */
+ enum ssd16xx_border_waveform default_border_waveform_init;
+
+ /* Default border waveform during display updates (enum index 0-9) */
+ enum ssd16xx_border_waveform default_border_waveform_update;
+
+ /* Whether to re-send border waveform command before each display update */
+ bool default_border_refresh_on_every_update;
+
+ /*
+ * Default clear-on-init behaviour.
+ * -1=disabled, 0=partial, 1=full, 2=fast (matches enum ssd16xx_refresh_mode)
+ */
+ int default_clear_on_init;
+
+ /* Default clear-on-close behaviour (-1=disabled, 0=partial, 1=full, 2=fast) */
+ int default_clear_on_close;
+
+ /* Default clear-on-disable behaviour (-1=disabled, 0=partial, 1=full, 2=fast) */
+ int default_clear_on_disable;
+
+ /*
+ * Default refresh-mode-init: -1=disabled, else skip baseline establishment
+ * and start directly in this refresh mode.
+ */
+ int default_refresh_mode_init;
+
+ /*
+ * Whether this panel has a physical red colour plane (3-colour BWR).
+ * false: 2-colour black/white only; the RED RAM is always bypassed.
+ * true: 3-colour panel; full-refresh writes to the RED RAM so that
+ * red pixels are driven through the red waveform.
+ */
+ bool red_supported;
+
+ /* Panel-specific display mode (resolution and physical dimensions) */
+ const struct drm_display_mode *mode;
+};
+
+struct ssd16xx_panel {
+ struct drm_device drm;
+
+ struct drm_plane primary_plane;
+ struct drm_crtc crtc;
+ struct drm_encoder encoder;
+ struct drm_connector connector;
+
+ struct spi_device *spi;
+ struct gpio_desc *reset;
+ struct gpio_desc *busy;
+ struct gpio_desc *dc;
+
+ enum ssd16xx_model model;
+ enum ssd16xx_controller controller;
+ const struct ssd16xx_controller_config *controller_cfg;
+ const struct ssd16xx_panel_config *panel_cfg;
+ struct drm_display_mode *mode;
+ u32 width;
+ u32 height;
+
+ bool initialized;
+ bool reinit_pending; /* HW re-init required after orientation change */
+ 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 */
+
+ int orientation; /* Display orientation in degrees: 0/90/180/270 */
+ 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 */
+
+ /* Border waveform (as enum indices) */
+ int border_waveform_init_idx; /* Border waveform during clear/init */
+ int border_waveform_update_idx; /* Border waveform during display updates */
+ bool border_refresh_on_every_update; /* Re-send border cmd each display update */
+ bool border_waveform_pending; /* One-shot: send border cmd on next update */
+
+ /* Display control */
+ int clear_on_init; /* -1=disabled, 0=partial, 1=full, 2=fast */
+ int clear_on_close; /* -1=disabled, 0=partial, 1=full, 2=fast */
+ int clear_on_disable; /* -1=disabled, 0=partial, 1=full, 2=fast */
+ int refresh_mode_init; /* -1=disabled, else use this mode for the first frame */
+
+ u8 *tx_buf; /* 1bpp frame buffer (mono + white) */
+ u8 *tx_red_buf; /* 1bpp red-channel buffer (3-color panels only) */
+ u16 *tx_buf9; /* 9-bit SPI expansion buffer (3-wire mode only) */
+
+ struct drm_framebuffer *last_fb; /* Last drawn FB for reinit redraws */
+ struct drm_property *rotation_property;
+ struct drm_property *refresh_mode_property;
+ struct drm_property *border_waveform_init_property;
+ struct drm_property *border_waveform_update_property;
+ struct drm_property *border_refresh_on_every_update_property;
+ struct drm_property *clear_on_init_property;
+ struct drm_property *clear_on_close_property;
+ struct drm_property *clear_on_disable_property;
+ struct drm_property *refresh_mode_init_property;
+ struct drm_property *color_mode_property;
+};
+
+static inline struct ssd16xx_panel *to_ssd16xx_panel(struct drm_device *drm)
+{
+ return container_of(drm, struct ssd16xx_panel, drm);
+}
+
+static inline struct ssd16xx_panel *crtc_to_ssd16xx_panel(struct drm_crtc *crtc)
+{
+ return container_of(crtc, struct ssd16xx_panel, crtc);
+}
+
+static inline struct ssd16xx_panel *plane_to_ssd16xx_panel(struct drm_plane *plane)
+{
+ return container_of(plane, struct ssd16xx_panel, primary_plane);
+}
+
+static const struct ssd16xx_controller_config ssd16xx_controller_configs[] = {
+ [SSD1683] = {
+ .max_width = 400,
+ .max_height = 300,
+ .ram_x_address_bits = 8,
+ .ram_y_address_bits = 16,
+ .has_temp_sensor_ctrl = true,
+ .deep_sleep_mode_level1 = SSD1683_DEEP_SLEEP_MODE_1,
+ .deep_sleep_mode_level2 = SSD1683_DEEP_SLEEP_MODE_2,
+ .border_waveform_table = ssd1683_border_waveform_table,
+ .ctrl1_normal = SSD1683_CTRL1_NORMAL,
+ .ctrl1_bypass_red_ram = SSD1683_CTRL1_BYPASS_RED_RAM,
+ .ctrl2_refresh = {
+ [SSD16XX_REFRESH_PARTIAL] = SSD1683_CTRL2_PARTIAL_REFRESH,
+ [SSD16XX_REFRESH_FULL] = SSD1683_CTRL2_FULL_REFRESH,
+ [SSD16XX_REFRESH_FAST] = SSD1683_CTRL2_FAST_REFRESH,
+ },
+ .ctrl2_load_temp_lut = SSD1683_CTRL2_LOAD_TEMP_LUT,
+ },
+};
+
+/* GDEY042T81: 4.2" 400x300 panel, 84.8x63.6mm active area */
+static const struct drm_display_mode gdey042t81_mode = {
+ DRM_SIMPLE_MODE(400, 300, 85, 64),
+};
+
+static const struct ssd16xx_panel_config ssd16xx_panel_configs[] = {
+ [GDEY042T81] = {
+ .data_entry_mode = SSD16XX_DATA_ENTRY_XINC_YINC,
+ .driver_output_ctrl_byte3 = SSD16XX_DRIVER_OUTPUT_CTRL_DEFAULT,
+ .default_refresh_mode = SSD16XX_REFRESH_PARTIAL,
+ .default_border_waveform_init = SSD16XX_BORDER_LUT1, /* white, clean clear */
+ .default_border_waveform_update = SSD16XX_BORDER_HIZ, /* floating, preserve */
+ .default_border_refresh_on_every_update = false,
+ .default_clear_on_init = -1,
+ .default_clear_on_close = -1,
+ .default_clear_on_disable = -1,
+ .default_refresh_mode_init = SSD16XX_REFRESH_FULL,
+ .red_supported = false, /* 2-colour black/white panel */
+ .mode = &gdey042t81_mode,
+ },
+};
+
+static void ssd16xx_wait_for_panel(struct ssd16xx_panel *panel,
+ int *err)
+{
+ unsigned long timeout_jiffies = jiffies +
+ msecs_to_jiffies(SSD16XX_BUSY_WAIT_TIMEOUT_MS);
+ unsigned long start_ms = jiffies_to_msecs(jiffies);
+ int busy_val;
+
+ if (*err)
+ return;
+
+ busy_val = gpiod_get_value_cansleep(panel->busy);
+ drm_dbg(&panel->drm, "BUSY initial value: %d\n", busy_val);
+
+ while (gpiod_get_value_cansleep(panel->busy) == 1) {
+ if (time_after(jiffies, timeout_jiffies)) {
+ drm_err(&panel->drm, "Busy wait timed out after %lums\n",
+ jiffies_to_msecs(jiffies) - start_ms);
+ *err = -ETIMEDOUT;
+ return;
+ }
+ usleep_range(100, 200);
+ }
+
+ drm_dbg(&panel->drm, "BUSY became ready after %lums\n",
+ jiffies_to_msecs(jiffies) - start_ms);
+}
+
+static void ssd16xx_spi_sync(struct spi_device *spi, struct spi_message *msg,
+ int *err)
+{
+ int ret;
+
+ if (*err)
+ return;
+
+ ret = spi_sync(spi, msg);
+ if (ret < 0)
+ *err = ret;
+}
+
+static void ssd16xx_send_cmd(struct ssd16xx_panel *panel, u8 cmd,
+ int *err)
+{
+ u16 word;
+ struct spi_transfer xfer = {};
+ struct spi_message msg;
+
+ if (*err)
+ return;
+
+ spi_message_init(&msg);
+ spi_message_add_tail(&xfer, &msg);
+
+ if (panel->dc) {
+ /* 4-wire SPI: D/C# GPIO low selects command mode */
+ xfer.tx_buf = &cmd;
+ xfer.len = 1;
+ gpiod_set_value_cansleep(panel->dc, 0);
+ } else {
+ /*
+ * 3-wire SPI (9-bit): bit 8 is the D/C# bit.
+ * D/C# = 0 means the following 8 bits are a command.
+ */
+ word = cmd; /* bit 8 = 0 for command */
+ xfer.tx_buf = &word;
+ xfer.len = sizeof(u16);
+ xfer.bits_per_word = 9;
+ }
+
+ ssd16xx_spi_sync(panel->spi, &msg, err);
+}
+
+static void ssd16xx_send_data(struct ssd16xx_panel *panel, u8 data,
+ int *err)
+{
+ u16 word;
+ struct spi_transfer xfer = {};
+ struct spi_message msg;
+
+ if (*err)
+ return;
+
+ spi_message_init(&msg);
+ spi_message_add_tail(&xfer, &msg);
+
+ if (panel->dc) {
+ /* 4-wire SPI: D/C# GPIO high selects data mode */
+ xfer.tx_buf = &data;
+ xfer.len = 1;
+ gpiod_set_value_cansleep(panel->dc, 1);
+ } else {
+ /*
+ * 3-wire SPI (9-bit): bit 8 is the D/C# bit.
+ * D/C# = 1 means the following 8 bits are data.
+ */
+ word = 0x100 | data;
+ xfer.tx_buf = &word;
+ xfer.len = sizeof(u16);
+ xfer.bits_per_word = 9;
+ }
+
+ ssd16xx_spi_sync(panel->spi, &msg, err);
+}
+
+static void ssd16xx_send_x_param(struct ssd16xx_panel *panel, u16 x,
+ int *err)
+{
+ if (*err)
+ return;
+
+ if (panel->controller_cfg->ram_x_address_bits == 8) {
+ ssd16xx_send_data(panel, (u8)x, err);
+ } else {
+ ssd16xx_send_data(panel, x & 0xFF, err);
+ ssd16xx_send_data(panel, (x >> 8) & 0xFF, err);
+ }
+}
+
+static void ssd16xx_send_y_param(struct ssd16xx_panel *panel, u16 y,
+ int *err)
+{
+ if (*err)
+ return;
+
+ if (panel->controller_cfg->ram_y_address_bits == 8) {
+ ssd16xx_send_data(panel, (u8)y, err);
+ } else {
+ ssd16xx_send_data(panel, y & 0xFF, err);
+ ssd16xx_send_data(panel, (y >> 8) & 0xFF, err);
+ }
+}
+
+static void ssd16xx_send_data_bulk(struct ssd16xx_panel *panel,
+ const u8 *data, size_t len,
+ int *err)
+{
+ struct spi_transfer xfer = {};
+ struct spi_message msg;
+
+ if (*err)
+ return;
+
+ if (!data || !len)
+ return;
+
+ spi_message_init(&msg);
+ spi_message_add_tail(&xfer, &msg);
+
+ if (panel->dc) {
+ /* 4-wire SPI: D/C# GPIO high selects data mode */
+ xfer.tx_buf = data;
+ xfer.len = len;
+ gpiod_set_value_cansleep(panel->dc, 1);
+ ssd16xx_spi_sync(panel->spi, &msg, err);
+ } else {
+ /* 3-wire (9-bit): expand u8 → u16 with D/C#=1 in bit 8. */
+ size_t i;
+ u16 *buf = panel->tx_buf9;
+
+ for (i = 0; i < len; i++)
+ buf[i] = 0x100 | data[i];
+
+ xfer.tx_buf = buf;
+ xfer.len = len * sizeof(u16);
+ xfer.bits_per_word = 9;
+ ssd16xx_spi_sync(panel->spi, &msg, err);
+ }
+}
+
+static void ssd16xx_display_update(struct ssd16xx_panel *panel,
+ u8 ctrl1_byte1, u8 ctrl1_byte2, u8 ctrl2_mode,
+ int *err)
+{
+ if (*err)
+ return;
+
+ drm_dbg(&panel->drm,
+ "display_update: Setting ctrl1=0x%02x,0x%02x mode=0x%02x\n",
+ ctrl1_byte1, ctrl1_byte2, ctrl2_mode);
+
+ ssd16xx_send_cmd(panel, SSD16XX_CMD_DISPLAY_UPDATE_CONTROL1, err);
+ ssd16xx_send_data(panel, ctrl1_byte1, err);
+ ssd16xx_send_data(panel, ctrl1_byte2, err);
+
+ ssd16xx_send_cmd(panel, SSD16XX_CMD_DISPLAY_UPDATE_CONTROL2, err);
+ ssd16xx_send_data(panel, ctrl2_mode, err);
+ ssd16xx_send_cmd(panel, SSD16XX_CMD_MASTER_ACTIVATION, err);
+
+ drm_dbg(&panel->drm,
+ "display_update: Master activation sent, waiting...\n");
+
+ ssd16xx_wait_for_panel(panel, err);
+}
+
+static void ssd16xx_hw_reset(struct ssd16xx_panel *panel)
+{
+ gpiod_set_value_cansleep(panel->reset, 1);
+ usleep_range(10000, 11000);
+ gpiod_set_value_cansleep(panel->reset, 0);
+ usleep_range(10000, 11000);
+}
+
+/*
+ * ssd16xx_preload_fast_lut() - pre-load the OTP LUT for fast refresh mode.
+ *
+ * Fast refresh (CTRL2 = 0xC7) omits the LOAD_LUT step on every update to save
+ * time. It relies on the LUT being loaded upfront via this standalone sequence
+ * (CTRL2 = 0xB1: ENABLE_CLK | LOAD_LUT | SSD16XX_CTRL2_LOAD_TEMPERATURE | DISABLE_CLK,
+ * no display update).
+ *
+ * Must be called when:
+ * a) hw_init runs with refresh_mode == FAST, and
+ * b) switching to fast refresh from a mode that did not leave a valid Mode1
+ * LUT in the controller (i.e. previous mode was not FULL refresh, which
+ * carries LOAD_LUT in its own CTRL2 sequence).
+ */
+static int ssd16xx_preload_fast_lut(struct ssd16xx_panel *panel)
+{
+ int err = 0;
+
+ ssd16xx_send_cmd(panel, SSD16XX_CMD_DISPLAY_UPDATE_CONTROL1, &err);
+ ssd16xx_send_data(panel, panel->controller_cfg->ctrl1_bypass_red_ram, &err);
+ ssd16xx_send_data(panel, SSD16XX_CTRL1_BYTE2_DEFAULT, &err);
+
+ ssd16xx_send_cmd(panel, SSD16XX_CMD_DISPLAY_UPDATE_CONTROL2, &err);
+ ssd16xx_send_data(panel, panel->controller_cfg->ctrl2_load_temp_lut, &err);
+
+ ssd16xx_send_cmd(panel, SSD16XX_CMD_MASTER_ACTIVATION, &err);
+ ssd16xx_wait_for_panel(panel, &err);
+
+ return err;
+}
+
+static int ssd16xx_hw_init(struct ssd16xx_panel *panel)
+{
+ int err = 0;
+ u16 ram_height = panel->controller_cfg->max_height;
+ u8 data_entry_mode;
+
+ ssd16xx_hw_reset(panel);
+
+ /* Software reset */
+ ssd16xx_send_cmd(panel, SSD16XX_CMD_SW_RESET, &err);
+ ssd16xx_wait_for_panel(panel, &err);
+
+ /* Driver output control (0x01): MUX ratio and scan direction. */
+ ssd16xx_send_cmd(panel, SSD16XX_CMD_DRIVER_OUTPUT_CONTROL, &err);
+ ssd16xx_send_y_param(panel, ram_height - 1, &err);
+ ssd16xx_send_data(panel, panel->panel_cfg->driver_output_ctrl_byte3, &err);
+
+ /* Internal temperature sensor (SSD1683/SSD1680 only; not present in SSD1673) */
+ if (panel->controller_cfg->has_temp_sensor_ctrl) {
+ ssd16xx_send_cmd(panel, SSD1683_CMD_TEMPERATURE_SENSOR_CONTROL, &err);
+ ssd16xx_send_data(panel, SSD1683_TEMP_SENSOR_INTERNAL, &err);
+ }
+
+ /*
+ * For FAST refresh mode, pre-load the LUT once here during initialization.
+ * FAST mode ctrl2 (0xC7) omits LOAD_LUT on every update for speed, so the
+ * LUT must be loaded upfront. FULL (0xF7) and PARTIAL (0xFF) load LUT on
+ * every update, so no preload is needed for those modes.
+ */
+ if (panel->refresh_mode == SSD16XX_REFRESH_FAST) {
+ ssd16xx_send_cmd(panel, SSD16XX_CMD_DISPLAY_UPDATE_CONTROL1, &err);
+ ssd16xx_send_data(panel, panel->controller_cfg->ctrl1_bypass_red_ram, &err);
+ ssd16xx_send_data(panel, SSD16XX_CTRL1_BYTE2_DEFAULT, &err);
+
+ ssd16xx_send_cmd(panel, SSD16XX_CMD_DISPLAY_UPDATE_CONTROL2, &err);
+ ssd16xx_send_data(panel, panel->controller_cfg->ctrl2_load_temp_lut, &err);
+
+ ssd16xx_send_cmd(panel, SSD16XX_CMD_MASTER_ACTIVATION, &err);
+ ssd16xx_wait_for_panel(panel, &err);
+ }
+
+ /*
+ * Set Data Entry Mode (0x11) based on orientation. This controls
+ * how the RAM address counter auto-advances after each byte write.
+ *
+ * Implementation uses two data entry modes:
+ * - 90°/180° use XDEC_YDEC (0x00): X--, Y-- with cursor at (max, max)
+ * - 0°/270° use XINC_YINC (0x03): X++, Y++ with cursor at (0, 0)
+ *
+ * The convert_fb_to_1bpp packing is grouped by physical layout:
+ * - Portrait orientations (90°/270°): column-major packing
+ * - Landscape orientations (0°/180°): row-major packing
+ *
+ * Final scan direction and image orientation are controlled by the
+ * combination of data entry mode and RAM cursor position set in fb_dirty.
+ *
+ * The RAM address window and cursor are NOT set here; fb_dirty
+ * always programmes them (with the correct end-before-start order
+ * for decrement modes) immediately before writing frame data.
+ */
+ switch (panel->orientation) {
+ case 90:
+ case 180:
+ data_entry_mode = SSD16XX_DATA_ENTRY_XDEC_YDEC;
+ break;
+ default: /* 0°/270° */
+ data_entry_mode = SSD16XX_DATA_ENTRY_XINC_YINC;
+ break;
+ }
+
+ ssd16xx_send_cmd(panel, SSD16XX_CMD_DATA_ENTRY_MODE, &err);
+ ssd16xx_send_data(panel, data_entry_mode, &err);
+ drm_dbg(&panel->drm, "hw_init: orientation=%u° data_entry=0x%02x\n",
+ panel->orientation, data_entry_mode);
+
+ ssd16xx_wait_for_panel(panel, &err);
+
+ if (err)
+ drm_err(&panel->drm, "Hardware initialization failed: %d\n", err);
+
+ return err;
+}
+
+/*
+ * Clear display by writing all-white to both BW and RED RAM.
+ * The ctrl2 argument selects the waveform (full/partial/fast refresh).
+ * Border waveform is set to init value before clearing, then restored
+ * to the update value to preserve the border during subsequent updates.
+ */
+static int ssd16xx_clear_display(struct ssd16xx_panel *panel, u8 ctrl2)
+{
+ const u8 *bw_tbl = panel->controller_cfg->border_waveform_table;
+ int err = 0;
+ unsigned int data_size = (panel->width * panel->height) / 8;
+ u8 *white_buffer = panel->tx_buf;
+
+ memset(white_buffer, 0xFF, data_size);
+
+ ssd16xx_send_cmd(panel, SSD16XX_CMD_SET_RAM_X_ADDRESS_COUNTER, &err);
+ ssd16xx_send_x_param(panel, 0x00, &err);
+
+ ssd16xx_send_cmd(panel, SSD16XX_CMD_SET_RAM_Y_ADDRESS_COUNTER, &err);
+ ssd16xx_send_y_param(panel, 0x00, &err);
+
+ ssd16xx_send_cmd(panel, SSD16XX_CMD_WRITE_RAM_BW, &err);
+ ssd16xx_send_data_bulk(panel, white_buffer, data_size, &err);
+
+ ssd16xx_send_cmd(panel, SSD1683_CMD_WRITE_RAM_RED, &err);
+ ssd16xx_send_data_bulk(panel, white_buffer, data_size, &err);
+
+ /* Set border waveform for the clear operation */
+ drm_dbg(&panel->drm, "clear_display: Set border init waveform: 0x%02x\n",
+ bw_tbl[panel->border_waveform_init_idx]);
+ ssd16xx_send_cmd(panel, SSD16XX_CMD_BORDER_WAVEFORM_CONTROL, &err);
+ ssd16xx_send_data(panel,
+ bw_tbl[panel->border_waveform_init_idx],
+ &err);
+
+ /* 3-colour mode: CTRL1_NORMAL (read both RAMs); BW mode: bypass RED. */
+ ssd16xx_display_update(panel,
+ panel->color_mode == SSD16XX_COLOR_MODE_3COLOR
+ ? panel->controller_cfg->ctrl1_normal
+ : panel->controller_cfg->ctrl1_bypass_red_ram,
+ SSD16XX_CTRL1_BYTE2_DEFAULT, ctrl2, &err);
+
+ /* Restore border waveform to update/preservation value */
+ drm_dbg(&panel->drm, "clear_display: Restored border update waveform: 0x%02x\n",
+ bw_tbl[panel->border_waveform_update_idx]);
+ ssd16xx_send_cmd(panel, SSD16XX_CMD_BORDER_WAVEFORM_CONTROL, &err);
+ ssd16xx_send_data(panel,
+ bw_tbl[panel->border_waveform_update_idx],
+ &err);
+
+ return err;
+}
+
+static u8 ssd16xx_refresh_mode_to_ctrl2(struct ssd16xx_panel *panel,
+ enum ssd16xx_refresh_mode mode)
+{
+ if (mode < ARRAY_SIZE(panel->controller_cfg->ctrl2_refresh))
+ return panel->controller_cfg->ctrl2_refresh[mode];
+ return panel->controller_cfg->ctrl2_refresh[SSD16XX_REFRESH_FULL];
+}
+
+/*
+ * Clear display on new DRM master open (if clear_on_init >= 0).
+ * Guarded by panel->first_clear_done; master_drop resets it unconditionally
+ * so each new client session gets a fresh clear.
+ */
+static int ssd16xx_clear_display_on_init(struct ssd16xx_panel *panel)
+{
+ int ret;
+
+ if (panel->clear_on_init < 0 || panel->first_clear_done)
+ return 0;
+
+ drm_dbg(&panel->drm, "clear_on_init: running, mode=%d\n",
+ panel->clear_on_init);
+ ret = ssd16xx_clear_display(panel,
+ ssd16xx_refresh_mode_to_ctrl2(panel, panel->clear_on_init));
+ if (ret)
+ return ret;
+
+ panel->first_clear_done = true;
+ return 0;
+}
+
+/*
+ * Clear display when the displaying client exits (if clear_on_close >= 0).
+ * Called from ssd16xx_drm_master_drop().
+ */
+static int ssd16xx_clear_display_on_exit(struct ssd16xx_panel *panel)
+{
+ int ret;
+
+ if (panel->clear_on_close < 0)
+ return 0;
+
+ drm_dbg(&panel->drm, "clear_on_close: running, mode=%d\n",
+ panel->clear_on_close);
+ ret = ssd16xx_clear_display(panel,
+ ssd16xx_refresh_mode_to_ctrl2(panel, panel->clear_on_close));
+ if (ret)
+ return ret;
+
+ return 0;
+}
+
+/*
+ * ssd16xx_pixel_luma() - return ITU-R BT.601 luminance (0-255) for one pixel.
+ *
+ * For colour formats the result is (299*R + 587*G + 114*B) / 1000;
+ * for luma-only formats the luma byte is returned directly.
+ *
+ * R1 is never passed here — it is already 1bpp and is handled directly by
+ * the callers.
+ */
+static u8 ssd16xx_pixel_luma(struct iosys_map *src,
+ struct drm_framebuffer *fb,
+ unsigned int x, unsigned int y)
+{
+ switch (fb->format->format) {
+ case DRM_FORMAT_XRGB8888: {
+ u32 *line = (u32 *)(src->vaddr + y * fb->pitches[0]);
+ u32 px = line[x];
+ u8 r = (px >> 16) & 0xFF, g = (px >> 8) & 0xFF, b = px & 0xFF;
+
+ return (u8)((299u * r + 587u * g + 114u * b) / 1000u);
+ }
+ case DRM_FORMAT_RGB888: {
+ u8 *line = (u8 *)(src->vaddr + y * fb->pitches[0]);
+ u8 r = line[x * 3], g = line[x * 3 + 1], b = line[x * 3 + 2];
+
+ return (u8)((299u * r + 587u * g + 114u * b) / 1000u);
+ }
+ case DRM_FORMAT_RGB565: {
+ u16 *line = (u16 *)(src->vaddr + y * fb->pitches[0]);
+ u16 px = line[x];
+ u8 r = ((px >> 11) & 0x1F) << 3;
+ u8 g = ((px >> 5) & 0x3F) << 2;
+ u8 b = (px & 0x1F) << 3;
+
+ return (u8)((299u * r + 587u * g + 114u * b) / 1000u);
+ }
+ case DRM_FORMAT_R8: {
+ u8 *line = (u8 *)(src->vaddr + y * fb->pitches[0]);
+
+ return line[x];
+ }
+ case DRM_FORMAT_NV12:
+ case DRM_FORMAT_NV16:
+ return ((u8 *)(src->vaddr))[y * fb->pitches[0] + x];
+ case DRM_FORMAT_YUYV: {
+ u8 *line = (u8 *)(src->vaddr + y * fb->pitches[0]);
+
+ return line[x * 2];
+ }
+ case DRM_FORMAT_UYVY: {
+ u8 *line = (u8 *)(src->vaddr + y * fb->pitches[0]);
+
+ return line[x * 2 + 1];
+ }
+ default:
+ return 0;
+ }
+}
+
+/*
+ * ssd16xx_pixel_is_white() - test whether a pixel maps to white in 1bpp output.
+ *
+ * Uses fixed threshold of 127. Pixels with luma strictly greater than 127
+ * are rendered white.
+ */
+static bool ssd16xx_pixel_is_white(struct iosys_map *src,
+ struct drm_framebuffer *fb,
+ unsigned int x, unsigned int y)
+{
+ /* R1 is already binarised; avoid the luma computation entirely. */
+ if (fb->format->format == DRM_FORMAT_R1) {
+ u8 *line = (u8 *)(src->vaddr + y * fb->pitches[0]);
+
+ return !!(line[x / 8] & (1 << (7 - (x % 8))));
+ }
+ return ssd16xx_pixel_luma(src, fb, x, y) > 127;
+}
+
+/*
+ * ssd16xx_pixel_is_red() - test whether a pixel is dominated by the red channel.
+ *
+ * Only meaningful for formats that carry RGB information (XRGB8888, RGB888,
+ * RGB565). For luma-only and monochrome formats there is no red channel, so
+ * the function always returns false; callers should use ssd16xx_pixel_is_white()
+ * to obtain the BW value for those formats.
+ *
+ * Returns true when the red component exceeds 50% intensity AND is strictly
+ * greater than both green and blue (dominant red hue).
+ */
+static bool ssd16xx_pixel_is_red(struct iosys_map *src,
+ struct drm_framebuffer *fb,
+ unsigned int x, unsigned int y)
+{
+ u32 format = fb->format->format;
+
+ switch (format) {
+ case DRM_FORMAT_XRGB8888: {
+ u32 *line = (u32 *)(src->vaddr + y * fb->pitches[0]);
+ u32 px = line[x];
+ u8 r = (px >> 16) & 0xFF;
+ u8 g = (px >> 8) & 0xFF;
+ u8 b = px & 0xFF;
+
+ return r > 127 && r > g && r > b;
+ }
+ case DRM_FORMAT_RGB888: {
+ u8 *line = (u8 *)(src->vaddr + y * fb->pitches[0]);
+ u8 r = line[x * 3];
+ u8 g = line[x * 3 + 1];
+ u8 b = line[x * 3 + 2];
+
+ return r > 127 && r > g && r > b;
+ }
+ case DRM_FORMAT_RGB565: {
+ u16 *line = (u16 *)(src->vaddr + y * fb->pitches[0]);
+ u16 px = line[x];
+ u8 r = ((px >> 11) & 0x1F) << 3;
+ u8 g = ((px >> 5) & 0x3F) << 2;
+ u8 b = (px & 0x1F) << 3;
+
+ return r > 127 && r > g && r > b;
+ }
+ default:
+ return false; /* No colour channel information */
+ }
+}
+
+/*
+ * ssd16xx_convert_fb_to_3color() - split a framebuffer into BW and RED planes.
+ * @bw_dst: output buffer for the black/white RAM plane (1=white, 0=black)
+ * @red_dst: output buffer for the red RAM plane (1=red, 0=not red)
+ * @src: mapped framebuffer memory
+ * @fb: DRM framebuffer descriptor
+ * @rect: region to convert (must be aligned to 8-pixel boundaries)
+ *
+ * Each output buffer must be at least rect_width/8 * rect_height bytes.
+ * Pixels are classified as:
+ * - red: written to red_dst as 1, bw_dst as 0 (black)
+ * - white: written to bw_dst as 1, red_dst as 0
+ * - black: written to both as 0
+ *
+ * For monochrome formats (R1) where no colour information is available the
+ * source data is copied verbatim to bw_dst and red_dst is cleared to 0xFF
+ * (all-white = no red pixels).
+ */
+static void ssd16xx_convert_fb_to_3color(u8 *bw_dst, u8 *red_dst,
+ struct iosys_map *src,
+ struct drm_framebuffer *fb,
+ struct drm_rect *rect)
+{
+ unsigned int x, y;
+ u8 bw_byte = 0, red_byte = 0;
+ unsigned int bit_pos = 0;
+ unsigned int dst_idx = 0;
+
+ drm_dbg(fb->dev,
+ "convert_3color: fmt=%p4cc rect=(%d,%d)-(%d,%d) path=%s\n",
+ &fb->format->format,
+ rect->x1, rect->y1, rect->x2, rect->y2,
+ fb->format->format == DRM_FORMAT_R1 ? "R1-direct" : "color-pixel");
+
+ /*
+ * R1 is already monochrome — no colour channel exists.
+ * Copy BW data directly and leave the red plane all-white (transparent).
+ */
+ if (fb->format->format == DRM_FORMAT_R1) {
+ unsigned int src_pitch = fb->pitches[0];
+ unsigned int width_bytes = drm_rect_width(rect) / 8;
+ unsigned int data_size = width_bytes * drm_rect_height(rect);
+
+ for (y = rect->y1; y < rect->y2; y++) {
+ u8 *line = src->vaddr + y * src_pitch + (rect->x1 / 8);
+
+ memcpy(bw_dst + dst_idx, line, width_bytes);
+ dst_idx += width_bytes;
+ }
+ memset(red_dst, 0xFF, data_size); /* 0xFF = all white: no red pixels */
+ return;
+ }
+
+ /* Use fixed threshold of 127 for grayscale to monochrome conversion. */
+ for (y = rect->y1; y < rect->y2; y++) {
+ for (x = rect->x1; x < rect->x2; x++) {
+ bool is_red = ssd16xx_pixel_is_red(src, fb, x, y);
+
+ if (is_red)
+ red_byte |= (1 << (7 - bit_pos));
+ else if (ssd16xx_pixel_is_white(src, fb, x, y))
+ bw_byte |= (1 << (7 - bit_pos));
+ /* else: black pixel — both bits remain 0 */
+ if (++bit_pos == 8) {
+ bw_dst[dst_idx] = bw_byte;
+ red_dst[dst_idx] = red_byte;
+ dst_idx++;
+ bw_byte = 0;
+ red_byte = 0;
+ bit_pos = 0;
+ }
+ }
+
+ /* Flush any partial byte at the end of each row */
+ if (bit_pos > 0) {
+ bw_dst[dst_idx] = bw_byte;
+ red_dst[dst_idx] = red_byte;
+ dst_idx++;
+ bw_byte = 0;
+ red_byte = 0;
+ bit_pos = 0;
+ }
+ }
+}
+
+/*
+ * ssd16xx_convert_r8_to_red_only() - map an R8 framebuffer to the RED RAM plane.
+ *
+ * Used when the panel has a physical red colour plane (red_supported == true)
+ * and the framebuffer format is DRM_FORMAT_R8. Pixels with value >= 128 are
+ * treated as red ink; the BW RAM is set to all-white so that only red ink
+ * appears on the white background.
+ *
+ * Hardware orientation is handled by the caller via RAM counter positioning;
+ * data is written in normal row-major order here (same as convert_fb_to_3color).
+ */
+static void ssd16xx_convert_r8_to_red_only(u8 *bw_dst, u8 *red_dst,
+ struct iosys_map *src,
+ struct drm_framebuffer *fb,
+ struct drm_rect *rect)
+{
+ unsigned int src_pitch = fb->pitches[0];
+ unsigned int width = drm_rect_width(rect);
+ unsigned int height = drm_rect_height(rect);
+ unsigned int data_size = DIV_ROUND_UP(width, 8) * height;
+ unsigned int dst_idx = 0;
+ unsigned int x, y;
+ u8 red_byte = 0;
+ unsigned int bit_pos = 0;
+
+ /* BW RAM: all-white background - no black ink, only red ink shows */
+ memset(bw_dst, 0xFF, data_size);
+
+ /* RED RAM: R8 >= 128 -> red ink (1-bit set) */
+ for (y = rect->y1; y < rect->y2; y++) {
+ u8 *line = src->vaddr + y * src_pitch;
+
+ for (x = rect->x1; x < rect->x2; x++) {
+ if (line[x] >= 128)
+ red_byte |= (1 << (7 - bit_pos));
+ if (++bit_pos == 8) {
+ red_dst[dst_idx++] = red_byte;
+ red_byte = 0;
+ bit_pos = 0;
+ }
+ }
+ if (bit_pos > 0) {
+ red_dst[dst_idx++] = red_byte;
+ red_byte = 0;
+ bit_pos = 0;
+ }
+ }
+}
+
+/*
+ * Convert framebuffer to 1-bit monochrome for e-paper display.
+ *
+ * Supported formats: XRGB8888, RGB888, RGB565, R8, NV12, NV16, YUYV, UYVY, R1.
+ * For colour and luma formats, Otsu's global binarisation method computes an
+ * optimal per-image threshold from the luminance histogram.
+ * R1 is the controller's native format and bypasses conversion entirely.
+ *
+ * Output layout:
+ * 0°/180° landscape: row-major, left-to-right, top-to-bottom
+ * 90°/270° CW portrait: column-major, rightmost column first
+ */
+static void ssd16xx_convert_fb_to_1bpp(u8 *dst, struct iosys_map *src,
+ struct drm_framebuffer *fb,
+ struct drm_rect *rect,
+ unsigned int orientation)
+{
+ u32 format = fb->format->format;
+ int x, y;
+ u8 byte = 0;
+ unsigned int bit_pos = 0;
+ unsigned int dst_idx = 0;
+
+ /* Use fixed threshold of 127 for grayscale to monochrome conversion. */
+ drm_dbg(fb->dev,
+ "convert_1bpp: fmt=%p4cc rect=(%d,%d)-(%d,%d) orient=%u° path=%s\n",
+ &fb->format->format,
+ rect->x1, rect->y1, rect->x2, rect->y2,
+ orientation,
+ (format == DRM_FORMAT_R1 && orientation == 0 && rect->x1 % 8 == 0) ? "R1-fast" :
+ (orientation == 90 || orientation == 270) ? "portrait" : "landscape");
+
+ /*
+ * R1 fast path: 0° landscape with byte-aligned rect.
+ * R1 is already 1bpp so landscape rows map directly to output bytes via
+ * memcpy — no per-pixel computation needed. rect->x1 must be a
+ * multiple of 8 so that (rect->x1 / 8) gives the correct byte offset;
+ * if not, the generic pixel-by-pixel loop below handles non-aligned
+ * rects safely.
+ */
+ if (format == DRM_FORMAT_R1 && orientation == 0 && rect->x1 % 8 == 0) {
+ unsigned int src_pitch = fb->pitches[0];
+ unsigned int width_bytes = drm_rect_width(rect) / 8;
+
+ for (y = rect->y1; y < rect->y2; y++) {
+ u8 *src_line = src->vaddr + y * src_pitch + (rect->x1 / 8);
+
+ memcpy(dst + dst_idx, src_line, width_bytes);
+ dst_idx += width_bytes;
+ }
+ return;
+ }
+
+ switch (orientation) {
+ case 90:
+ case 270:
+ /*
+ * Portrait (90° or 270°): column-major packing.
+ * Each portrait source column becomes one physical RAM row.
+ * The data entry mode and cursor position control scan direction.
+ */
+ for (x = rect->x2 - 1; x >= (int)rect->x1; x--) {
+ for (y = rect->y1; y < rect->y2; y++) {
+ if (ssd16xx_pixel_is_white(src, fb, x, y))
+ byte |= (1 << (7 - bit_pos));
+ if (++bit_pos == 8) {
+ dst[dst_idx++] = byte;
+ byte = 0;
+ bit_pos = 0;
+ }
+ }
+ if (bit_pos > 0) {
+ dst[dst_idx++] = byte;
+ byte = 0;
+ bit_pos = 0;
+ }
+ }
+ break;
+
+ case 0:
+ case 180:
+ default:
+ /*
+ * Landscape (0° or 180°): row-major packing.
+ * Each landscape source row becomes one physical RAM row.
+ * The data entry mode and cursor position control scan direction.
+ */
+ for (y = rect->y1; y < rect->y2; y++) {
+ for (x = rect->x1; x < rect->x2; x++) {
+ if (ssd16xx_pixel_is_white(src, fb, x, y))
+ byte |= (1 << (7 - bit_pos));
+ if (++bit_pos == 8) {
+ dst[dst_idx++] = byte;
+ byte = 0;
+ bit_pos = 0;
+ }
+ }
+ if (bit_pos > 0) {
+ dst[dst_idx++] = byte;
+ byte = 0;
+ bit_pos = 0;
+ }
+ }
+ break;
+ }
+}
+
+static int ssd16xx_fb_dirty(struct drm_framebuffer *fb, struct drm_rect *rect,
+ struct ssd16xx_panel *panel)
+{
+ const u8 *ctrl2_tbl = panel->controller_cfg->ctrl2_refresh;
+ struct drm_gem_dma_object *dma_obj = drm_fb_dma_get_gem_obj(fb, 0);
+ struct iosys_map map;
+ int err = 0;
+ unsigned int data_size = (panel->width * panel->height) / 8;
+ u8 *mono_buffer = NULL;
+ u8 *red_buffer = NULL;
+ u16 ram_x_start, ram_x_end, ram_y_start, ram_y_end;
+
+ /* Process full display area; convert handles orientation traversal. */
+ rect->x1 = 0;
+ rect->y1 = 0;
+ rect->x2 = panel->width;
+ rect->y2 = panel->height;
+
+ drm_dbg(&panel->drm,
+ "fb_dirty: fb=%dx%d, refresh_mode=%d, orientation=%d\n",
+ fb->width, fb->height, panel->refresh_mode, panel->orientation);
+
+ mono_buffer = panel->tx_buf;
+ memset(mono_buffer, 0, data_size);
+
+ /* 3-colour FULL/FAST: populate red channel. */
+ if (panel->color_mode == SSD16XX_COLOR_MODE_3COLOR &&
+ (panel->refresh_mode == SSD16XX_REFRESH_FULL ||
+ panel->refresh_mode == SSD16XX_REFRESH_FAST)) {
+ red_buffer = panel->tx_red_buf;
+ memset(red_buffer, 0, data_size);
+ }
+
+ iosys_map_set_vaddr(&map, dma_obj->vaddr);
+
+ if (red_buffer && fb->format->format == DRM_FORMAT_R8)
+ ssd16xx_convert_r8_to_red_only(mono_buffer, red_buffer, &map, fb, rect);
+ else if (red_buffer)
+ ssd16xx_convert_fb_to_3color(mono_buffer, red_buffer, &map, fb, rect);
+ else
+ ssd16xx_convert_fb_to_1bpp(mono_buffer, &map, fb, rect, panel->orientation);
+
+ drm_dbg(&panel->drm,
+ "fb_dirty: mono[0..3]=0x%02x 0x%02x 0x%02x 0x%02x (data_size=%u)\n",
+ mono_buffer[0], mono_buffer[1], mono_buffer[2], mono_buffer[3],
+ data_size);
+
+ /* Set RAM window and cursor for current orientation. */
+ ram_x_start = 0;
+ ram_x_end = (panel->controller_cfg->max_width / 8) - 1;
+ ram_y_start = 0;
+ ram_y_end = panel->controller_cfg->max_height - 1;
+
+ switch (panel->orientation) {
+ case 90:
+ case 180:
+ /* 90°/180°: XDEC_YDEC mode, send end-before-start; cursor at (max, max). */
+ ssd16xx_send_cmd(panel, SSD16XX_CMD_SET_RAM_X_ADDRESS_START_END, &err);
+ ssd16xx_send_x_param(panel, ram_x_end, &err);
+ ssd16xx_send_x_param(panel, ram_x_start, &err);
+
+ ssd16xx_send_cmd(panel, SSD16XX_CMD_SET_RAM_Y_ADDRESS_START_END, &err);
+ ssd16xx_send_y_param(panel, ram_y_end, &err);
+ ssd16xx_send_y_param(panel, ram_y_start, &err);
+
+ ssd16xx_send_cmd(panel, SSD16XX_CMD_SET_RAM_X_ADDRESS_COUNTER, &err);
+ ssd16xx_send_x_param(panel, ram_x_end, &err);
+
+ ssd16xx_send_cmd(panel, SSD16XX_CMD_SET_RAM_Y_ADDRESS_COUNTER, &err);
+ ssd16xx_send_y_param(panel, ram_y_end, &err);
+ break;
+
+ default: /* 0°/270° */
+ /* 0°/270°: XINC_YINC mode, cursor at (0, 0). */
+ ssd16xx_send_cmd(panel, SSD16XX_CMD_SET_RAM_X_ADDRESS_START_END, &err);
+ ssd16xx_send_x_param(panel, ram_x_start, &err);
+ ssd16xx_send_x_param(panel, ram_x_end, &err);
+
+ ssd16xx_send_cmd(panel, SSD16XX_CMD_SET_RAM_Y_ADDRESS_START_END, &err);
+ ssd16xx_send_y_param(panel, ram_y_start, &err);
+ ssd16xx_send_y_param(panel, ram_y_end, &err);
+
+ ssd16xx_send_cmd(panel, SSD16XX_CMD_SET_RAM_X_ADDRESS_COUNTER, &err);
+ ssd16xx_send_x_param(panel, ram_x_start, &err);
+
+ ssd16xx_send_cmd(panel, SSD16XX_CMD_SET_RAM_Y_ADDRESS_COUNTER, &err);
+ ssd16xx_send_y_param(panel, ram_y_start, &err);
+ break;
+ }
+
+ ssd16xx_send_cmd(panel, SSD16XX_CMD_WRITE_RAM_BW, &err);
+ ssd16xx_send_data_bulk(panel, mono_buffer, data_size, &err);
+
+ /* Re-send border waveform when: every-update mode, init frame, or
+ * the border_waveform_update property just changed (one-shot).
+ */
+ drm_dbg(&panel->drm,
+ "fb_dirty: border check: every_update=%d init_pending=%d border_pending=%d idx=%d hw=0x%02x\n",
+ panel->border_refresh_on_every_update, panel->init_refresh_pending,
+ panel->border_waveform_pending, panel->border_waveform_update_idx,
+ panel->controller_cfg->border_waveform_table[panel->border_waveform_update_idx]);
+ if (panel->border_refresh_on_every_update || panel->init_refresh_pending ||
+ panel->border_waveform_pending) {
+ u8 idx = panel->border_waveform_update_idx;
+ u8 border = panel->controller_cfg->border_waveform_table[idx];
+
+ drm_dbg(&panel->drm, "fb_dirty: Sending border waveform: 0x%02x\n",
+ border);
+ ssd16xx_send_cmd(panel, SSD16XX_CMD_BORDER_WAVEFORM_CONTROL, &err);
+ ssd16xx_send_data(panel, border, &err);
+ panel->border_waveform_pending = false;
+ }
+
+ switch (panel->refresh_mode) {
+ case SSD16XX_REFRESH_FULL:
+ /*
+ * BW full refresh: write RED RAM BEFORE display_update
+ * to avoid a post-BUSY write timing issue on some
+ * controller revisions that silently corrupts RED RAM.
+ * RED RAM is then bypassed (CTRL1_BYPASS_RED_RAM) so
+ * stale RED RAM content does not affect the output.
+ */
+ ssd16xx_send_cmd(panel, SSD1683_CMD_WRITE_RAM_RED, &err);
+ if (red_buffer) {
+ /* 3-colour: write red channel before activating */
+ ssd16xx_send_data_bulk(panel, red_buffer, data_size, &err);
+ ssd16xx_display_update(panel, panel->controller_cfg->ctrl1_normal,
+ SSD16XX_CTRL1_BYTE2_DEFAULT,
+ ctrl2_tbl[SSD16XX_REFRESH_FULL], &err);
+ } else {
+ ssd16xx_send_data_bulk(panel, mono_buffer, data_size, &err);
+ ssd16xx_display_update(panel, panel->controller_cfg->ctrl1_bypass_red_ram,
+ SSD16XX_CTRL1_BYTE2_DEFAULT,
+ ctrl2_tbl[SSD16XX_REFRESH_FULL], &err);
+ }
+ break;
+ case SSD16XX_REFRESH_FAST:
+ /*
+ * Fast refresh: LUT pre-loaded during hw_init; BYPASS_RED_RAM
+ * so RED RAM does not affect the current output.
+ * Write RED RAM BEFORE display_update (same reasoning as FULL)
+ * so it holds the just-displayed frame as a valid reference for
+ * any subsequent PARTIAL refresh.
+ */
+
+ ssd16xx_send_cmd(panel, SSD1683_CMD_WRITE_RAM_RED, &err);
+ if (red_buffer) {
+ /* 3-colour: write red channel before activating */
+ ssd16xx_send_data_bulk(panel, red_buffer, data_size, &err);
+ ssd16xx_display_update(panel, panel->controller_cfg->ctrl1_normal,
+ SSD16XX_CTRL1_BYTE2_DEFAULT,
+ ctrl2_tbl[SSD16XX_REFRESH_FAST], &err);
+ } else {
+ ssd16xx_send_data_bulk(panel, mono_buffer, data_size, &err);
+ ssd16xx_display_update(panel, panel->controller_cfg->ctrl1_bypass_red_ram,
+ SSD16XX_CTRL1_BYTE2_DEFAULT,
+ ctrl2_tbl[SSD16XX_REFRESH_FAST], &err);
+ }
+ break;
+ case SSD16XX_REFRESH_PARTIAL:
+ default:
+ /*
+ * Partial refresh: both RAMs used for transition waveforms.
+ * RED RAM must hold the PREVIOUS frame (= current display
+ * content) so the controller can compute pixel transitions.
+ * Write RED RAM AFTER display_update so it captures the
+ * just-displayed frame as the reference for the next partial.
+ */
+ drm_dbg(&panel->drm,
+ "fb_dirty: partial pre-update: mono[0]=0x%02x (BW=new, RED=prev)\n",
+ mono_buffer[0]);
+ ssd16xx_display_update(panel, panel->controller_cfg->ctrl1_normal,
+ SSD16XX_CTRL1_BYTE2_DEFAULT,
+ ctrl2_tbl[SSD16XX_REFRESH_PARTIAL], &err);
+ ssd16xx_send_cmd(panel, SSD1683_CMD_WRITE_RAM_RED, &err);
+ ssd16xx_send_data_bulk(panel, mono_buffer, data_size, &err);
+ drm_dbg(&panel->drm,
+ "fb_dirty: partial post-update: wrote RED baseline mono[0]=0x%02x\n",
+ mono_buffer[0]);
+ break;
+ }
+
+ return err;
+}
+
+/* -----------------------------------------------------------------------------
+ * Plane Functions
+ */
+
+static void ssd16xx_plane_destroy(struct drm_plane *plane)
+{
+ drm_plane_cleanup(plane);
+}
+
+static void ssd16xx_plane_reset(struct drm_plane *plane)
+{
+ drm_atomic_helper_plane_reset(plane);
+}
+
+static const struct drm_plane_funcs ssd16xx_plane_funcs = {
+ .update_plane = drm_atomic_helper_update_plane,
+ .disable_plane = drm_atomic_helper_disable_plane,
+ .destroy = ssd16xx_plane_destroy,
+ .reset = ssd16xx_plane_reset,
+ .atomic_duplicate_state = drm_atomic_helper_plane_duplicate_state,
+ .atomic_destroy_state = drm_atomic_helper_plane_destroy_state,
+};
+
+static int ssd16xx_plane_atomic_check(struct drm_plane *plane,
+ struct drm_atomic_state *state)
+{
+ struct drm_plane_state *new_plane_state =
+ drm_atomic_get_new_plane_state(state, plane);
+ struct drm_crtc_state *crtc_state;
+
+ if (!new_plane_state->crtc)
+ return 0;
+
+ crtc_state = drm_atomic_get_new_crtc_state(state, new_plane_state->crtc);
+
+ return drm_atomic_helper_check_plane_state(new_plane_state, crtc_state,
+ DRM_PLANE_NO_SCALING,
+ DRM_PLANE_NO_SCALING,
+ false, false);
+}
+
+static void ssd16xx_plane_atomic_update(struct drm_plane *plane,
+ struct drm_atomic_state *state)
+{
+ struct drm_plane_state *old_state = drm_atomic_get_old_plane_state(state, plane);
+ struct drm_plane_state *new_state = drm_atomic_get_new_plane_state(state, plane);
+ struct ssd16xx_panel *panel = plane_to_ssd16xx_panel(plane);
+ enum ssd16xx_refresh_mode saved_mode;
+ u8 saved_border_waveform_idx;
+ struct drm_framebuffer *fb = new_state->fb;
+ struct drm_rect rect;
+ int ret;
+
+ drm_dbg(&panel->drm, "plane_atomic_update: fb=%p, initialized=%d\n",
+ fb, panel->initialized);
+
+ if (!fb || !panel->initialized)
+ return;
+
+ /*
+ * If a rotation change is pending, skip the update here — crtc_atomic_flush
+ * will re-init the hardware for the new orientation and redraw.
+ */
+ if (panel->reinit_pending) {
+ drm_dbg(&panel->drm, "plane_atomic_update: skipping (reinit pending)\n");
+ return;
+ }
+
+ if (!drm_atomic_helper_damage_merged(old_state, new_state, &rect)) {
+ rect.x1 = 0;
+ rect.y1 = 0;
+ rect.x2 = fb->width;
+ rect.y2 = fb->height;
+ drm_dbg(&panel->drm, "plane_atomic_update: no damage, using full screen\n");
+ }
+
+ drm_dbg(&panel->drm, "plane_atomic_update: calling fb_dirty rect=(%d,%d)-(%d,%d)\n",
+ rect.x1, rect.y1, rect.x2, rect.y2);
+ /*
+ * When refresh_mode_init was set, use the specified mode for this first
+ * frame only, then restore the user-configured refresh_mode so
+ * subsequent updates continue with the configured mode.
+ */
+ saved_mode = panel->refresh_mode;
+ saved_border_waveform_idx = panel->border_waveform_update_idx;
+ if (panel->init_refresh_pending) {
+ panel->refresh_mode = panel->refresh_mode_init;
+ panel->border_waveform_update_idx = panel->border_waveform_init_idx;
+ }
+
+ /*
+ * Fast refresh (0xC7) omits LOAD_LUT on every update cycle and relies
+ * on the LUT being pre-loaded upfront. The property setter arms
+ * fast_lut_pending whenever the user switches into fast mode. Consume
+ * the flag here (once) before the first fast-refresh frame so the
+ * controller's LUT is in the correct state.
+ */
+ if (panel->fast_lut_pending) {
+ ret = ssd16xx_preload_fast_lut(panel);
+ if (ret) {
+ drm_err(&panel->drm,
+ "plane_atomic_update: fast LUT preload failed: %d\n", ret);
+ }
+
+ panel->fast_lut_pending = false;
+ }
+
+ ret = ssd16xx_fb_dirty(fb, &rect, panel);
+ if (ret)
+ drm_err(&panel->drm, "plane_atomic_update: display update failed: %d\n", ret);
+ else
+ panel->last_fb = fb;
+
+ panel->refresh_mode = saved_mode;
+ panel->border_waveform_update_idx = saved_border_waveform_idx;
+
+ /*
+ * If this was the init frame (which used border_waveform_init_idx
+ * inside fb_dirty), arm border_waveform_pending so the normal
+ * (non-init) border value is sent at the start of the next update.
+ */
+ if (panel->init_refresh_pending) {
+ panel->init_refresh_pending = false;
+ panel->border_waveform_pending = true;
+ }
+}
+
+static const struct drm_plane_helper_funcs ssd16xx_plane_helper_funcs = {
+ .atomic_check = ssd16xx_plane_atomic_check,
+ .atomic_update = ssd16xx_plane_atomic_update,
+};
+
+/* -----------------------------------------------------------------------------
+ * CRTC Functions
+ */
+
+static void ssd16xx_crtc_destroy(struct drm_crtc *crtc)
+{
+ drm_crtc_cleanup(crtc);
+}
+
+static const struct drm_crtc_funcs ssd16xx_crtc_funcs = {
+ .reset = drm_atomic_helper_crtc_reset,
+ .destroy = ssd16xx_crtc_destroy,
+ .set_config = drm_atomic_helper_set_config,
+ .page_flip = drm_atomic_helper_page_flip,
+ .atomic_duplicate_state = drm_atomic_helper_crtc_duplicate_state,
+ .atomic_destroy_state = drm_atomic_helper_crtc_destroy_state,
+};
+
+static enum drm_mode_status ssd16xx_crtc_mode_valid(struct drm_crtc *crtc,
+ const struct drm_display_mode *mode)
+{
+ struct ssd16xx_panel *panel = crtc_to_ssd16xx_panel(crtc);
+
+ /* Accept only our panel's native mode (landscape or portrait) */
+ if ((mode->hdisplay == panel->mode->hdisplay &&
+ mode->vdisplay == panel->mode->vdisplay) ||
+ (mode->hdisplay == panel->mode->vdisplay &&
+ mode->vdisplay == panel->mode->hdisplay))
+ return MODE_OK;
+
+ return MODE_BAD;
+}
+
+static int ssd16xx_crtc_atomic_check(struct drm_crtc *crtc,
+ struct drm_atomic_state *state)
+{
+ return 0;
+}
+
+static void ssd16xx_crtc_atomic_disable(struct drm_crtc *crtc,
+ struct drm_atomic_state *state)
+{
+ struct ssd16xx_panel *panel = crtc_to_ssd16xx_panel(crtc);
+ int ret, idx;
+
+ if (!drm_dev_enter(&panel->drm, &idx))
+ return;
+
+ if (panel->clear_on_disable < 0 || panel->display_cleared_on_deinit)
+ goto out;
+
+ drm_dbg(&panel->drm, "clear_on_disable: running, mode=%d\n",
+ panel->clear_on_disable);
+ ret = ssd16xx_clear_display(panel,
+ ssd16xx_refresh_mode_to_ctrl2(panel,
+ panel->clear_on_disable));
+ if (ret) {
+ drm_err(&panel->drm, "atomic_disable: clear failed: %d\n", ret);
+ goto out;
+ }
+
+ panel->display_cleared_on_deinit = true;
+out:
+ drm_dev_exit(idx);
+}
+
+static void ssd16xx_crtc_atomic_enable(struct drm_crtc *crtc,
+ struct drm_atomic_state *state)
+{
+ struct ssd16xx_panel *panel = crtc_to_ssd16xx_panel(crtc);
+ struct drm_crtc_state *crtc_state = drm_atomic_get_new_crtc_state(state, crtc);
+ int ret, idx;
+
+ if (!drm_dev_enter(&panel->drm, &idx))
+ return;
+
+ panel->display_cleared_on_deinit = false;
+
+ drm_dbg(&panel->drm, "atomic_enable: %dx%d\n",
+ crtc_state->mode.hdisplay, crtc_state->mode.vdisplay);
+
+ panel->width = crtc_state->mode.hdisplay;
+ panel->height = crtc_state->mode.vdisplay;
+
+ ret = ssd16xx_hw_init(panel);
+ if (ret) {
+ drm_err(&panel->drm, "crtc_atomic_enable: HW init failed: %d\n", ret);
+ goto out;
+ }
+ panel->initialized = true;
+
+ /* Clear display on first app launch if configured */
+ ret = ssd16xx_clear_display_on_init(panel);
+ if (ret)
+ drm_err(&panel->drm, "crtc_atomic_enable: clear on init failed: %d\n", ret);
+
+ /*
+ * If refresh_mode_init is set, arm init_refresh_pending so
+ * plane_atomic_update uses the specified mode for the first frame
+ * then restores the user-configured or panel default refresh_mode.
+ */
+ if (panel->refresh_mode_init >= 0) {
+ drm_dbg(&panel->drm,
+ "atomic_enable: refresh_mode_init=%d, using for first frame\n",
+ panel->refresh_mode_init);
+ panel->init_refresh_pending = true;
+ }
+
+out:
+ drm_dev_exit(idx);
+}
+
+/*
+ * Re-initialize hardware and redraw the current framebuffer when the
+ * display orientation changes at runtime via the rotation connector property.
+ * Called by the DRM atomic helper after atomic_enable/disable have run.
+ */
+static void ssd16xx_crtc_atomic_flush(struct drm_crtc *crtc,
+ struct drm_atomic_state *state)
+{
+ struct ssd16xx_panel *panel = crtc_to_ssd16xx_panel(crtc);
+ struct drm_framebuffer *fb;
+ struct drm_rect full;
+ int ret, idx;
+
+ if (!panel->reinit_pending || !panel->initialized)
+ return;
+
+ if (!drm_dev_enter(&panel->drm, &idx))
+ return;
+
+ panel->reinit_pending = false;
+
+ drm_dbg(&panel->drm, "atomic_flush: reinit, orientation=%u°\n",
+ panel->orientation);
+
+ ret = ssd16xx_hw_init(panel);
+ if (ret) {
+ drm_err(&panel->drm, "Orientation re-init failed: %d\n", ret);
+ goto out;
+ }
+
+ fb = panel->primary_plane.state ? panel->primary_plane.state->fb
+ : panel->last_fb;
+ if (fb) {
+ full.x1 = 0;
+ full.y1 = 0;
+ full.x2 = fb->width;
+ full.y2 = fb->height;
+ ret = ssd16xx_fb_dirty(fb, &full, panel);
+ if (ret)
+ drm_err(&panel->drm, "atomic_flush: display update failed: %d\n", ret);
+ else
+ panel->last_fb = fb;
+ }
+
+out:
+ drm_dev_exit(idx);
+}
+
+static const struct drm_crtc_helper_funcs ssd16xx_crtc_helper_funcs = {
+ .mode_valid = ssd16xx_crtc_mode_valid,
+ .atomic_check = ssd16xx_crtc_atomic_check,
+ .atomic_disable = ssd16xx_crtc_atomic_disable,
+ .atomic_enable = ssd16xx_crtc_atomic_enable,
+ .atomic_flush = ssd16xx_crtc_atomic_flush,
+};
+
+/* -----------------------------------------------------------------------------
+ * Connector Functions
+ */
+
+static int ssd16xx_connector_get_modes(struct drm_connector *connector)
+{
+ struct ssd16xx_panel *panel = to_ssd16xx_panel(connector->dev);
+ bool mode_is_portrait = (panel->mode->hdisplay < panel->mode->vdisplay);
+ bool orient_is_portrait = (panel->orientation == 90 || panel->orientation == 270);
+
+ drm_dbg(&panel->drm, "connector_get_modes: orientation=%u°\n",
+ panel->orientation);
+
+ /* For portrait, swap dimensions so clients see logical size. */
+ if (mode_is_portrait != orient_is_portrait) {
+ struct drm_display_mode *mode;
+
+ mode = drm_mode_duplicate(&panel->drm, panel->mode);
+ if (!mode)
+ return 0;
+ swap(mode->hdisplay, mode->vdisplay);
+ swap(mode->hsync_start, mode->vsync_start);
+ swap(mode->hsync_end, mode->vsync_end);
+ swap(mode->htotal, mode->vtotal);
+ swap(mode->width_mm, mode->height_mm);
+ mode->type |= DRM_MODE_TYPE_PREFERRED;
+ drm_mode_set_name(mode);
+ drm_mode_probed_add(connector, mode);
+ return 1;
+ }
+
+ return drm_connector_helper_get_modes_fixed(connector, panel->mode);
+}
+
+static const struct drm_connector_helper_funcs ssd16xx_connector_helper_funcs = {
+ .get_modes = ssd16xx_connector_get_modes,
+};
+
+/* Enum values for the rotation connector property (degrees clockwise) */
+static const struct drm_prop_enum_list ssd16xx_rotation_enum[] = {
+ { 0, "0" },
+ { 90, "90" },
+ { 180, "180" },
+ { 270, "270" },
+};
+
+/* Enum values for the refresh_mode connector property */
+static const struct drm_prop_enum_list ssd16xx_refresh_mode_enum[] = {
+ { SSD16XX_REFRESH_PARTIAL, "partial" },
+ { SSD16XX_REFRESH_FULL, "full" },
+ { SSD16XX_REFRESH_FAST, "fast" },
+};
+
+/*
+ * Enum for clear_on_init, clear_on_close, refresh_mode_init properties.
+ * Value 0 = disabled; values 1-3 = partial/full/fast (refresh mode + 1).
+ * The +1 offset allows a single enum to represent both "disabled" and the
+ * three refresh modes without sign-extending the DRM property value.
+ */
+static const struct drm_prop_enum_list ssd16xx_init_refresh_enum[] = {
+ { 0, "disabled" },
+ { 1, "partial" },
+ { 2, "full" },
+ { 3, "fast" },
+};
+
+/* Enum values for the color_mode connector property */
+static const struct drm_prop_enum_list ssd16xx_color_mode_enum[] = {
+ { SSD16XX_COLOR_MODE_BW, "black-white" },
+ { SSD16XX_COLOR_MODE_3COLOR, "3-color" },
+};
+
+/* Enum values for border_waveform connector properties (one per HW mode) */
+static const struct drm_prop_enum_list ssd16xx_border_waveform_enum[] = {
+ { SSD16XX_BORDER_LUT0, "lut0_black" },
+ { SSD16XX_BORDER_LUT1, "lut1_white" },
+ { SSD16XX_BORDER_LUT2, "lut2_black" },
+ { SSD16XX_BORDER_LUT3, "lut3_gray" },
+ { SSD16XX_BORDER_VSS, "vss_black" },
+ { SSD16XX_BORDER_VSH1, "vsh1_black" },
+ { SSD16XX_BORDER_VSL, "vsl_white" },
+ { SSD16XX_BORDER_VSH2, "vsh2_black" },
+ { SSD16XX_BORDER_VCOM, "vcom_preserve" },
+ { SSD16XX_BORDER_HIZ, "hiz_float" },
+};
+
+static int ssd16xx_connector_create_properties(struct ssd16xx_panel *panel)
+{
+ struct drm_device *drm = &panel->drm;
+ struct drm_connector *connector = &panel->connector;
+
+ panel->rotation_property =
+ drm_property_create_enum(drm, 0, "rotation",
+ ssd16xx_rotation_enum,
+ ARRAY_SIZE(ssd16xx_rotation_enum));
+ if (!panel->rotation_property)
+ return -ENOMEM;
+ drm_object_attach_property(&connector->base,
+ panel->rotation_property, panel->orientation);
+
+ panel->refresh_mode_property =
+ drm_property_create_enum(drm, 0, "refresh_mode",
+ ssd16xx_refresh_mode_enum,
+ ARRAY_SIZE(ssd16xx_refresh_mode_enum));
+ if (!panel->refresh_mode_property)
+ return -ENOMEM;
+ drm_object_attach_property(&connector->base,
+ panel->refresh_mode_property, panel->refresh_mode);
+
+ panel->border_waveform_init_property =
+ drm_property_create_enum(drm, 0, "border_waveform_init",
+ ssd16xx_border_waveform_enum,
+ ARRAY_SIZE(ssd16xx_border_waveform_enum));
+ if (!panel->border_waveform_init_property)
+ return -ENOMEM;
+ drm_object_attach_property(&connector->base,
+ panel->border_waveform_init_property,
+ panel->border_waveform_init_idx);
+
+ panel->border_waveform_update_property =
+ drm_property_create_enum(drm, 0, "border_waveform_update",
+ ssd16xx_border_waveform_enum,
+ ARRAY_SIZE(ssd16xx_border_waveform_enum));
+ if (!panel->border_waveform_update_property)
+ return -ENOMEM;
+ drm_object_attach_property(&connector->base,
+ panel->border_waveform_update_property,
+ panel->border_waveform_update_idx);
+
+ panel->border_refresh_on_every_update_property =
+ drm_property_create_bool(drm, 0, "border_refresh_on_every_update");
+ if (!panel->border_refresh_on_every_update_property)
+ return -ENOMEM;
+ drm_object_attach_property(&connector->base,
+ panel->border_refresh_on_every_update_property,
+ panel->border_refresh_on_every_update);
+
+ panel->clear_on_init_property =
+ drm_property_create_enum(drm, 0, "clear_on_init",
+ ssd16xx_init_refresh_enum,
+ ARRAY_SIZE(ssd16xx_init_refresh_enum));
+ if (!panel->clear_on_init_property)
+ return -ENOMEM;
+ /* Property value 0=disabled, 1-3=mode; field is -1/0/1/2 → val = field+1 */
+ drm_object_attach_property(&connector->base,
+ panel->clear_on_init_property,
+ panel->clear_on_init + 1);
+
+ panel->clear_on_close_property =
+ drm_property_create_enum(drm, 0, "clear_on_close",
+ ssd16xx_init_refresh_enum,
+ ARRAY_SIZE(ssd16xx_init_refresh_enum));
+ if (!panel->clear_on_close_property)
+ return -ENOMEM;
+ drm_object_attach_property(&connector->base,
+ panel->clear_on_close_property,
+ panel->clear_on_close + 1);
+
+ panel->clear_on_disable_property =
+ drm_property_create_enum(drm, 0, "clear_on_disable",
+ ssd16xx_init_refresh_enum,
+ ARRAY_SIZE(ssd16xx_init_refresh_enum));
+ if (!panel->clear_on_disable_property)
+ return -ENOMEM;
+ drm_object_attach_property(&connector->base,
+ panel->clear_on_disable_property,
+ panel->clear_on_disable + 1);
+
+ panel->refresh_mode_init_property =
+ drm_property_create_enum(drm, 0, "refresh_mode_init",
+ ssd16xx_init_refresh_enum,
+ ARRAY_SIZE(ssd16xx_init_refresh_enum));
+ if (!panel->refresh_mode_init_property)
+ return -ENOMEM;
+ drm_object_attach_property(&connector->base,
+ panel->refresh_mode_init_property,
+ panel->refresh_mode_init + 1);
+
+ /*
+ * color_mode: only expose 3-color option on panels that physically have
+ * a red plane; on BW-only panels the property still exists for
+ * consistency but userspace can only set "black-white".
+ */
+ panel->color_mode_property =
+ drm_property_create_enum(drm, 0, "color_mode",
+ ssd16xx_color_mode_enum,
+ panel->panel_cfg->red_supported
+ ? ARRAY_SIZE(ssd16xx_color_mode_enum)
+ : 1);
+ if (!panel->color_mode_property)
+ return -ENOMEM;
+ drm_object_attach_property(&connector->base,
+ panel->color_mode_property,
+ panel->color_mode);
+
+ return 0;
+}
+
+static int ssd16xx_connector_atomic_get_property(struct drm_connector *connector,
+ const struct drm_connector_state *state,
+ struct drm_property *property,
+ uint64_t *val)
+{
+ struct ssd16xx_panel *panel = to_ssd16xx_panel(connector->dev);
+
+ drm_dbg(&panel->drm, "get_property: %s\n", property->name);
+
+ if (property == panel->rotation_property) {
+ *val = panel->orientation;
+ return 0;
+ }
+ if (property == panel->refresh_mode_property) {
+ *val = panel->refresh_mode;
+ return 0;
+ }
+ if (property == panel->border_waveform_init_property) {
+ *val = panel->border_waveform_init_idx;
+ return 0;
+ }
+ if (property == panel->border_waveform_update_property) {
+ *val = panel->border_waveform_update_idx;
+ return 0;
+ }
+ if (property == panel->border_refresh_on_every_update_property) {
+ *val = panel->border_refresh_on_every_update;
+ return 0;
+ }
+ if (property == panel->clear_on_init_property) {
+ *val = panel->clear_on_init + 1; /* field -1/0/1/2 → val 0/1/2/3 */
+ return 0;
+ }
+ if (property == panel->clear_on_close_property) {
+ *val = panel->clear_on_close + 1;
+ return 0;
+ }
+ if (property == panel->clear_on_disable_property) {
+ *val = panel->clear_on_disable + 1;
+ return 0;
+ }
+ if (property == panel->refresh_mode_init_property) {
+ *val = panel->refresh_mode_init + 1;
+ return 0;
+ }
+ if (property == panel->color_mode_property) {
+ *val = panel->color_mode;
+ return 0;
+ }
+
+ return -EINVAL;
+}
+
+static int ssd16xx_connector_atomic_set_property(struct drm_connector *connector,
+ struct drm_connector_state *state,
+ struct drm_property *property,
+ uint64_t val)
+{
+ struct ssd16xx_panel *panel = to_ssd16xx_panel(connector->dev);
+
+ drm_dbg(&panel->drm, "set_property: %s = %llu\n", property->name, val);
+
+ if (property == panel->rotation_property) {
+ if (val != 0 && val != 90 && val != 180 && val != 270)
+ return -EINVAL;
+ panel->orientation = val;
+ /*
+ * Flag hardware re-init needed. crtc_atomic_flush will call
+ * ssd16xx_hw_init() with the new orientation and redraw.
+ */
+ panel->reinit_pending = true;
+ return 0;
+ }
+ if (property == panel->refresh_mode_property) {
+ if (val > SSD16XX_REFRESH_FAST)
+ return -EINVAL;
+ /*
+ * Fast refresh (0xC7) omits LOAD_LUT on every update and relies
+ * on the LUT being pre-loaded upfront. Arm the one-shot flag
+ * when switching into fast mode so the next plane_atomic_update
+ * loads the LUT before the first fast-refresh cycle. Clear it
+ * when switching away so a fresh pre-load happens if the user
+ * returns to fast mode later.
+ */
+ if (val == SSD16XX_REFRESH_FAST &&
+ panel->refresh_mode != SSD16XX_REFRESH_FULL)
+ panel->fast_lut_pending = true;
+ else
+ panel->fast_lut_pending = false;
+ panel->refresh_mode = val;
+ return 0;
+ }
+ if (property == panel->border_waveform_init_property) {
+ if (val >= ARRAY_SIZE(ssd1683_border_waveform_table))
+ return -EINVAL;
+ panel->border_waveform_init_idx = val;
+ return 0;
+ }
+ if (property == panel->border_waveform_update_property) {
+ const u8 *bw_tbl = panel->controller_cfg->border_waveform_table;
+ bool changed = (int)val != panel->border_waveform_update_idx;
+
+ if (val >= ARRAY_SIZE(ssd1683_border_waveform_table))
+ return -EINVAL;
+ drm_dbg(&panel->drm,
+ "set_property: border_waveform_update old=%d new=%llu hw=0x%02x -> 0x%02x %s\n",
+ panel->border_waveform_update_idx, val,
+ bw_tbl[panel->border_waveform_update_idx],
+ bw_tbl[val],
+ changed ? "(arming pending)" : "(no change)");
+ /* Arm one-shot flag so the new border value is sent on the very
+ * next display update, even if border_refresh_on_every_update is
+ * not set. Cleared in fb_dirty after the command is sent.
+ */
+ if ((int)val != panel->border_waveform_update_idx)
+ panel->border_waveform_pending = true;
+ panel->border_waveform_update_idx = val;
+ return 0;
+ }
+ if (property == panel->border_refresh_on_every_update_property) {
+ panel->border_refresh_on_every_update = !!val;
+ return 0;
+ }
+ if (property == panel->clear_on_init_property) {
+ if (val > 3)
+ return -EINVAL;
+ panel->clear_on_init = (int)val - 1; /* val 0/1/2/3 → field -1/0/1/2 */
+ panel->first_clear_done = false; /* allow re-fire on next enable */
+ return 0;
+ }
+ if (property == panel->clear_on_close_property) {
+ if (val > 3)
+ return -EINVAL;
+ panel->clear_on_close = (int)val - 1;
+ return 0;
+ }
+ if (property == panel->clear_on_disable_property) {
+ if (val > 3)
+ return -EINVAL;
+ panel->clear_on_disable = (int)val - 1;
+ return 0;
+ }
+ if (property == panel->refresh_mode_init_property) {
+ if (val > 3)
+ return -EINVAL;
+ panel->refresh_mode_init = (int)val - 1;
+ return 0;
+ }
+ if (property == panel->color_mode_property) {
+ if (val > SSD16XX_COLOR_MODE_3COLOR)
+ return -EINVAL;
+ if (val == SSD16XX_COLOR_MODE_3COLOR && !panel->panel_cfg->red_supported) {
+ drm_dbg(&panel->drm,
+ "set_property: 3-color mode not supported by this panel\n");
+ return -EINVAL;
+ }
+ panel->color_mode = val;
+ return 0;
+ }
+
+ return -EINVAL;
+}
+
+static const struct drm_connector_funcs ssd16xx_connector_funcs = {
+ .reset = drm_atomic_helper_connector_reset,
+ .fill_modes = drm_helper_probe_single_connector_modes,
+ .destroy = drm_connector_cleanup,
+ .atomic_duplicate_state = drm_atomic_helper_connector_duplicate_state,
+ .atomic_destroy_state = drm_atomic_helper_connector_destroy_state,
+ .atomic_get_property = ssd16xx_connector_atomic_get_property,
+ .atomic_set_property = ssd16xx_connector_atomic_set_property,
+};
+
+static const u32 ssd16xx_formats[] = {
+ DRM_FORMAT_XRGB8888, /* 32-bit RGB with padding (preferred) */
+ DRM_FORMAT_RGB888, /* 24-bit packed RGB */
+ DRM_FORMAT_RGB565, /* 16-bit RGB (5:6:5) */
+ DRM_FORMAT_R8, /* 8-bit grayscale */
+ DRM_FORMAT_NV12, /* YUV 4:2:0 planar */
+ DRM_FORMAT_NV16, /* YUV 4:2:2 planar */
+ DRM_FORMAT_YUYV, /* Packed YUV 4:2:2 (Y0 U0 Y1 V0) */
+ DRM_FORMAT_UYVY, /* Packed YUV 4:2:2 (U0 Y0 V0 Y1) */
+ DRM_FORMAT_R1, /* 1-bit monochrome (native, 8 pixels/byte) */
+};
+
+DEFINE_DRM_GEM_FOPS(ssd16xx_fops);
+
+/*
+ * ssd16xx_drm_master_set - arm init refresh when a new master takes control.
+ */
+static void ssd16xx_drm_master_set(struct drm_device *drm,
+ struct drm_file *file, bool from_open)
+{
+ struct ssd16xx_panel *panel = to_ssd16xx_panel(drm);
+
+ panel->display_cleared_on_deinit = false;
+ panel->first_clear_done = false;
+
+ if (panel->refresh_mode_init >= 0)
+ panel->init_refresh_pending = true;
+}
+
+/*
+ * ssd16xx_drm_master_drop - clear display and disarm init refresh when the
+ * master client exits.
+ */
+static void ssd16xx_drm_master_drop(struct drm_device *drm,
+ struct drm_file *file)
+{
+ struct ssd16xx_panel *panel = to_ssd16xx_panel(drm);
+ int ret;
+
+ panel->init_refresh_pending = false;
+ panel->first_clear_done = false;
+
+ if (panel->clear_on_close < 0 || panel->display_cleared_on_deinit)
+ return;
+
+ ret = ssd16xx_clear_display_on_exit(panel);
+ if (ret)
+ drm_err(drm, "master_drop: clear on close failed: %d\n", ret);
+
+ panel->display_cleared_on_deinit = true;
+}
+
+static struct drm_driver ssd16xx_drm_driver = {
+ .driver_features = DRIVER_GEM | DRIVER_MODESET | DRIVER_ATOMIC,
+ .fops = &ssd16xx_fops,
+ .name = "ssd16xx",
+ .desc = "DRM driver for SSD16xx e-paper controller family",
+ .major = 1,
+ .minor = 0,
+ .master_set = ssd16xx_drm_master_set,
+ .master_drop = ssd16xx_drm_master_drop,
+ DRM_GEM_DMA_DRIVER_OPS,
+ DRM_FBDEV_DMA_DRIVER_OPS,
+};
+
+static const struct drm_mode_config_funcs ssd16xx_mode_config_funcs = {
+ .fb_create = drm_gem_fb_create_with_dirty,
+ .atomic_check = drm_atomic_helper_check,
+ .atomic_commit = drm_atomic_helper_commit,
+};
+
+/*
+ * Use the RPM commit-tail variant so that drm_atomic_helper_commit_modeset_enables
+ * (which calls crtc_atomic_enable) runs before drm_atomic_helper_commit_planes.
+ * Without this, the standard commit_tail calls commit_planes before
+ * modeset_enables, so plane_atomic_update would see initialized == false on the
+ * first commit and silently drop the frame.
+ */
+static const struct drm_mode_config_helper_funcs ssd16xx_mode_config_helper_funcs = {
+ .atomic_commit_tail = drm_atomic_helper_commit_tail_rpm,
+};
+
+static int ssd16xx_alloc_tx_bufs(struct ssd16xx_panel *panel)
+{
+ struct device *dev = &panel->spi->dev;
+ size_t frame_size = (panel->controller_cfg->max_width *
+ panel->controller_cfg->max_height) / 8;
+
+ panel->tx_buf = devm_kmalloc(dev, frame_size, GFP_KERNEL);
+ if (!panel->tx_buf)
+ return -ENOMEM;
+
+ if (panel->panel_cfg->red_supported) {
+ panel->tx_red_buf = devm_kmalloc(dev, frame_size, GFP_KERNEL);
+ if (!panel->tx_red_buf)
+ return -ENOMEM;
+ }
+
+ if (!panel->dc) {
+ panel->tx_buf9 = devm_kmalloc_array(dev, frame_size,
+ sizeof(u16), GFP_KERNEL);
+ if (!panel->tx_buf9)
+ return -ENOMEM;
+ }
+
+ return 0;
+}
+
+static int ssd16xx_probe(struct spi_device *spi)
+{
+ struct device *dev = &spi->dev;
+ struct ssd16xx_panel *panel;
+ struct drm_device *drm;
+ const struct spi_device_id *spi_id;
+ struct drm_display_mode *mode;
+ const void *match;
+ enum ssd16xx_model model;
+ u32 dt_rotation = 0;
+ int ret;
+
+ match = device_get_match_data(dev);
+ if (match) {
+ model = (enum ssd16xx_model)(uintptr_t)match;
+ } else {
+ spi_id = spi_get_device_id(spi);
+ model = (enum ssd16xx_model)spi_id->driver_data;
+ }
+
+ if (!dev->coherent_dma_mask) {
+ ret = dma_coerce_mask_and_coherent(dev, DMA_BIT_MASK(64));
+ if (ret) {
+ dev_warn(dev, "Failed to set DMA mask: %d\n", ret);
+ return ret;
+ }
+ }
+
+ panel = devm_drm_dev_alloc(dev, &ssd16xx_drm_driver,
+ struct ssd16xx_panel, drm);
+ if (IS_ERR(panel))
+ return PTR_ERR(panel);
+
+ drm = &panel->drm;
+ panel->spi = spi;
+ panel->model = model;
+ spi_set_drvdata(spi, panel);
+
+ spi->mode = SPI_MODE_0;
+ spi->bits_per_word = SSD16XX_SPI_BITS_PER_WORD;
+
+ if (!spi->max_speed_hz) {
+ drm_warn(drm, "spi-max-frequency not specified, using %u Hz\n",
+ SSD16XX_SPI_SPEED_DEFAULT);
+ spi->max_speed_hz = SSD16XX_SPI_SPEED_DEFAULT;
+ }
+
+ ret = spi_setup(spi);
+ if (ret < 0) {
+ drm_err(drm, "SPI setup failed: %d\n", ret);
+ return ret;
+ }
+
+ switch (model) {
+ case GDEY042T81:
+ panel->controller = SSD1683;
+ break;
+ default:
+ drm_err(drm, "Unknown panel model: %d\n", model);
+ return -EINVAL;
+ }
+
+ if (panel->controller >= ARRAY_SIZE(ssd16xx_controller_configs) ||
+ !ssd16xx_controller_configs[panel->controller].max_width)
+ return -EINVAL;
+ panel->controller_cfg = &ssd16xx_controller_configs[panel->controller];
+
+ if (model >= ARRAY_SIZE(ssd16xx_panel_configs))
+ return -EINVAL;
+ panel->panel_cfg = &ssd16xx_panel_configs[model];
+
+ mode = devm_kmemdup(dev, panel->panel_cfg->mode,
+ sizeof(*panel->panel_cfg->mode), GFP_KERNEL);
+ if (!mode)
+ return -ENOMEM;
+
+ panel->refresh_mode = panel->panel_cfg->default_refresh_mode;
+ /* Default color mode: 3-color for panels with red plane, BW otherwise */
+ panel->color_mode = panel->panel_cfg->red_supported
+ ? SSD16XX_COLOR_MODE_3COLOR
+ : SSD16XX_COLOR_MODE_BW;
+ panel->border_waveform_init_idx = panel->panel_cfg->default_border_waveform_init;
+ panel->border_waveform_update_idx = panel->panel_cfg->default_border_waveform_update;
+ panel->border_refresh_on_every_update =
+ panel->panel_cfg->default_border_refresh_on_every_update;
+ panel->clear_on_init = panel->panel_cfg->default_clear_on_init;
+ panel->clear_on_close = panel->panel_cfg->default_clear_on_close;
+ panel->clear_on_disable = panel->panel_cfg->default_clear_on_disable;
+ panel->refresh_mode_init = panel->panel_cfg->default_refresh_mode_init;
+
+ /* Module parameter overrides for border/display control */
+ if (border_waveform_init_lut >= 0 &&
+ border_waveform_init_lut < (int)ARRAY_SIZE(ssd1683_border_waveform_table))
+ panel->border_waveform_init_idx = border_waveform_init_lut;
+ if (border_waveform_lut >= 0 &&
+ border_waveform_lut < (int)ARRAY_SIZE(ssd1683_border_waveform_table))
+ panel->border_waveform_update_idx = border_waveform_lut;
+ if (border_refresh_on_every_update)
+ panel->border_refresh_on_every_update = true;
+ if (clear_on_init >= 0 && clear_on_init <= 2)
+ panel->clear_on_init = clear_on_init;
+ if (clear_on_close >= 0 && clear_on_close <= 2)
+ panel->clear_on_close = clear_on_close;
+ if (clear_on_disable >= 0 && clear_on_disable <= 2)
+ panel->clear_on_disable = clear_on_disable;
+ if (refresh_mode_init >= 0 && refresh_mode_init <= 2)
+ panel->refresh_mode_init = refresh_mode_init;
+
+ /* Module parameter overrides panel default refresh mode when set */
+ if (refresh_mode >= 0) {
+ if (refresh_mode > SSD16XX_REFRESH_FAST)
+ drm_warn(drm, "Invalid refresh_mode module param %d, ignored\n",
+ refresh_mode);
+ else
+ panel->refresh_mode = refresh_mode;
+ }
+
+ /* Module parameter overrides panel default color mode when set */
+ if (color_mode >= 0) {
+ if (color_mode > SSD16XX_COLOR_MODE_3COLOR)
+ drm_warn(drm, "Invalid color_mode module param %d, ignored\n",
+ color_mode);
+ else if (color_mode == SSD16XX_COLOR_MODE_3COLOR &&
+ !panel->panel_cfg->red_supported)
+ drm_warn(drm,
+ "color_mode=3-color requested but panel has no red plane, ignored\n");
+ else
+ panel->color_mode = color_mode;
+ }
+
+ /* Parse "rotation" DT property; swap mode dimensions for portrait. */
+ device_property_read_u32(dev, "rotation", &dt_rotation);
+ if (dt_rotation != 0 && dt_rotation != 90 && dt_rotation != 180 && dt_rotation != 270) {
+ drm_warn(drm, "Invalid DT rotation %u, defaulting to 0°\n", dt_rotation);
+ dt_rotation = 0;
+ }
+ panel->orientation = dt_rotation;
+
+ /* Module parameter overrides DT rotation when set */
+ if (rotation >= 0) {
+ if (rotation != 0 && rotation != 90 && rotation != 180 && rotation != 270)
+ drm_warn(drm, "Invalid rotation module param %d, ignored\n",
+ rotation);
+ else
+ panel->orientation = rotation;
+ }
+
+ drm_dbg(drm, "Using %s orientation (%u°, %ux%u logical)\n",
+ (panel->orientation == 90 || panel->orientation == 270) ? "portrait" : "landscape",
+ panel->orientation, mode->hdisplay, mode->vdisplay);
+
+ /* Swap mode dimensions for portrait so clients see logical size. */
+ if (panel->orientation == 90 || panel->orientation == 270) {
+ swap(mode->hdisplay, mode->vdisplay);
+ swap(mode->hsync_start, mode->vsync_start);
+ swap(mode->hsync_end, mode->vsync_end);
+ swap(mode->htotal, mode->vtotal);
+ swap(mode->width_mm, mode->height_mm);
+ drm_dbg(drm, "Mode dimensions swapped for portrait: %ux%u\n",
+ mode->hdisplay, mode->vdisplay);
+ } else {
+ drm_dbg(drm, "Mode dimensions unchanged: %ux%u\n",
+ mode->hdisplay, mode->vdisplay);
+ }
+ panel->mode = mode;
+ panel->width = mode->hdisplay;
+ panel->height = mode->vdisplay;
+
+ /* Acquire GPIOs. */
+ panel->reset = devm_gpiod_get(dev, "reset", GPIOD_OUT_HIGH);
+ if (IS_ERR(panel->reset))
+ return dev_err_probe(dev, PTR_ERR(panel->reset), "Failed to get RESET GPIO\n");
+
+ panel->busy = devm_gpiod_get(dev, "busy", GPIOD_IN);
+ if (IS_ERR(panel->busy))
+ return dev_err_probe(dev, PTR_ERR(panel->busy), "Failed to get BUSY GPIO\n");
+
+ panel->dc = devm_gpiod_get_optional(dev, "dc", GPIOD_OUT_LOW);
+ if (IS_ERR(panel->dc))
+ return dev_err_probe(dev, PTR_ERR(panel->dc), "Failed to get DC GPIO\n");
+ if (!panel->dc) {
+ if (!spi_is_bpw_supported(spi, 9))
+ return dev_err_probe(dev, -EINVAL,
+ "3-wire SPI mode requires 9-bit word support\n");
+ drm_dbg(drm, "dc-gpios not specified, using 3-wire (9-bit) SPI mode\n");
+ }
+
+ ret = ssd16xx_alloc_tx_bufs(panel);
+ if (ret)
+ return ret;
+
+ ssd16xx_hw_reset(panel);
+
+ ret = drmm_mode_config_init(drm);
+ if (ret)
+ return ret;
+
+ drm->mode_config.funcs = &ssd16xx_mode_config_funcs;
+ drm->mode_config.helper_private = &ssd16xx_mode_config_helper_funcs;
+ drm->mode_config.min_width = min(panel->width, panel->height);
+ drm->mode_config.max_width = max(panel->width, panel->height);
+ drm->mode_config.min_height = min(panel->width, panel->height);
+ drm->mode_config.max_height = max(panel->width, panel->height);
+
+ drm_connector_helper_add(&panel->connector, &ssd16xx_connector_helper_funcs);
+ ret = drm_connector_init(drm, &panel->connector, &ssd16xx_connector_funcs,
+ DRM_MODE_CONNECTOR_SPI);
+ if (ret)
+ return ret;
+
+ ret = drm_universal_plane_init(drm, &panel->primary_plane, 0,
+ &ssd16xx_plane_funcs,
+ ssd16xx_formats, ARRAY_SIZE(ssd16xx_formats),
+ NULL, DRM_PLANE_TYPE_PRIMARY, NULL);
+ if (ret)
+ return ret;
+ drm_plane_helper_add(&panel->primary_plane, &ssd16xx_plane_helper_funcs);
+ drm_plane_enable_fb_damage_clips(&panel->primary_plane);
+
+ ret = drm_crtc_init_with_planes(drm, &panel->crtc, &panel->primary_plane,
+ NULL, &ssd16xx_crtc_funcs, NULL);
+ if (ret)
+ return ret;
+ drm_crtc_helper_add(&panel->crtc, &ssd16xx_crtc_helper_funcs);
+
+ ret = drm_simple_encoder_init(drm, &panel->encoder, DRM_MODE_ENCODER_NONE);
+ if (ret)
+ return ret;
+ panel->encoder.possible_crtcs = drm_crtc_mask(&panel->crtc);
+
+ ret = drm_connector_attach_encoder(&panel->connector, &panel->encoder);
+ if (ret)
+ return ret;
+
+ ret = ssd16xx_connector_create_properties(panel);
+ if (ret)
+ return ret;
+
+ drm_mode_config_reset(drm);
+
+ ret = drm_dev_register(drm, 0);
+ if (ret)
+ return ret;
+
+ drm_dbg(drm, "SSD16xx e-paper display initialized (%dx%d, %d° rotation)\n",
+ panel->width, panel->height, panel->orientation);
+
+ drm_client_setup(drm, NULL);
+
+ return 0;
+}
+
+static void ssd16xx_remove(struct spi_device *spi)
+{
+ struct ssd16xx_panel *panel = spi_get_drvdata(spi);
+
+ drm_dev_unplug(&panel->drm);
+ drm_atomic_helper_shutdown(&panel->drm);
+}
+
+static void ssd16xx_shutdown(struct spi_device *spi)
+{
+ struct ssd16xx_panel *panel = spi_get_drvdata(spi);
+
+ drm_atomic_helper_shutdown(&panel->drm);
+}
+
+static const struct of_device_id ssd16xx_of_match[] = {
+ { .compatible = "gooddisplay,gdey042t81", .data = (void *)GDEY042T81 },
+ { }
+};
+MODULE_DEVICE_TABLE(of, ssd16xx_of_match);
+
+static const struct spi_device_id ssd16xx_id[] = {
+ { "gdey042t81", GDEY042T81 },
+ { }
+};
+MODULE_DEVICE_TABLE(spi, ssd16xx_id);
+
+static struct spi_driver ssd16xx_spi_driver = {
+ .driver = {
+ .name = "ssd16xx",
+ .of_match_table = ssd16xx_of_match,
+ },
+ .probe = ssd16xx_probe,
+ .remove = ssd16xx_remove,
+ .shutdown = ssd16xx_shutdown,
+ .id_table = ssd16xx_id,
+};
+module_spi_driver(ssd16xx_spi_driver);
+
+MODULE_AUTHOR("Devarsh Thakkar <devarsht@ti.com>");
+MODULE_DESCRIPTION("DRM driver for Solomon SSD16xx e-paper display controller family");
+MODULE_LICENSE("GPL");
--
2.39.1
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH 4/6] drm/tiny: panel-ssd16xx: Add power management support
2026-04-30 18:33 [PATCH 0/6] Add DRM driver for Solomon SSD16xx e-paper display controllers Devarsh Thakkar
` (2 preceding siblings ...)
2026-04-30 18:33 ` [PATCH 3/6] drm/tiny: Add DRM driver for Solomon SSD16xx e-paper display controllers Devarsh Thakkar
@ 2026-04-30 18:33 ` Devarsh Thakkar
2026-04-30 18:33 ` [PATCH 5/6] MAINTAINERS: Add entry for Solomon SSD16xx DRM driver Devarsh Thakkar
2026-04-30 18:33 ` [PATCH 6/6] arm64: defconfig: Enable DRM_PANEL_SSD16XX Devarsh Thakkar
5 siblings, 0 replies; 7+ messages in thread
From: Devarsh Thakkar @ 2026-04-30 18:33 UTC (permalink / raw)
To: David Airlie, Simona Vetter, Maarten Lankhorst, Maxime Ripard,
Thomas Zimmermann, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Neil Armstrong, Bjorn Andersson, dri-devel, devicetree,
linux-kernel
Cc: praneeth, vigneshr, s-jain1, s-wang12, r-donadkar, r-sharma3,
devarsht, afd
Add system suspend/resume and runtime PM with idle timeout support to
the SSD16xx driver.
E-paper panels are bistable: they continue to display the last rendered
image indefinitely even in deep sleep or when power is cut to the
controller IC. This makes it practical to suspend the controller
aggressively — including while a display application is still open and the
user is simply reading — without any visible disruption. The driver
exploits this by firing the autosuspend timer after an idle timeout,
putting the controller into deep sleep regardless of CRTC state, and waking
it transparently on the next frame update.
The SSD16xx family supports two deep sleep modes:
Mode 1 (RAM retained): used for runtime idle. Display RAM survives the
sleep, so resume only requires a hardware reset and re-initialisation; no
full redraw is needed, keeping wake latency minimal.
Mode 2 (RAM lost): used for system suspend. Maximises power savings at
the cost of a complete controller re-init and repaint on resume.
Runtime PM: Each hardware-touching callback wraps its SPI access with
pm_runtime_resume_and_get() / pm_runtime_put_autosuspend() pairs so the PM
reference count drops to zero after every update. This allows the
autosuspend timer to fire while the CRTC is still enabled, putting the
controller into Mode 1 deep sleep between updates without requiring the
application to close or the display pipeline to be torn down.
An autosuspend delay of 35 seconds keeps the panel active across typical
user-interaction gaps while still capturing long idle periods.
System suspend/resume: On system suspend the driver switches to Mode 2 and
quiesces the atomic pipeline via drm_mode_config_helper_suspend(). If the
autosuspend timer already fired (device is RPM_SUSPENDED), the driver wakes
the panel via HWRESET (the only exit from deep sleep since SPI is
unresponsive), sends Mode 2 directly, and tracks the state with a
pm_force_suspended flag so the matching force_resume is called only when
needed. On resume, Mode 1 is restored for subsequent runtime PM cycles and
the atomic pipeline is rebuilt via drm_mode_config_helper_resume().
Per-client initialisation refresh: A drm_driver.master_set callback arms
init_refresh_pending when a new client opens the DRM device as a DRM
master, ensuring its first frame uses the configured init-refresh waveform.
A paired master_drop callback clears the flag on fd close by a DRM master.
This is kept separate from the runtime resume path since Mode 1 retains RAM
and a full refresh on every runtime wake would be wasteful.
Signed-off-by: Devarsh Thakkar <devarsht@ti.com>
---
drivers/gpu/drm/tiny/panel-ssd16xx.c | 203 +++++++++++++++++++++++++--
1 file changed, 193 insertions(+), 10 deletions(-)
diff --git a/drivers/gpu/drm/tiny/panel-ssd16xx.c b/drivers/gpu/drm/tiny/panel-ssd16xx.c
index b232837c54ff..6bf763667d82 100644
--- a/drivers/gpu/drm/tiny/panel-ssd16xx.c
+++ b/drivers/gpu/drm/tiny/panel-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>
@@ -88,6 +89,7 @@ MODULE_PARM_DESC(color_mode,
/* SPI command codes (common) */
#define SSD16XX_CMD_DRIVER_OUTPUT_CONTROL 0x01
+#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
@@ -100,6 +102,9 @@ MODULE_PARM_DESC(color_mode,
#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).
*
@@ -405,8 +410,10 @@ struct ssd16xx_panel {
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 */
@@ -1526,10 +1533,9 @@ static void ssd16xx_plane_atomic_update(struct drm_plane *plane,
struct drm_rect rect;
int ret;
- drm_dbg(&panel->drm, "plane_atomic_update: fb=%p, initialized=%d\n",
- fb, panel->initialized);
+ drm_dbg(&panel->drm, "plane_atomic_update: fb=%p\n", fb);
- if (!fb || !panel->initialized)
+ if (!fb)
return;
/*
@@ -1541,6 +1547,12 @@ static void ssd16xx_plane_atomic_update(struct drm_plane *plane,
return;
}
+ ret = pm_runtime_resume_and_get(panel->drm.dev);
+ if (ret < 0) {
+ drm_err(&panel->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;
rect.y1 = 0;
@@ -1598,6 +1610,9 @@ static void ssd16xx_plane_atomic_update(struct drm_plane *plane,
panel->init_refresh_pending = false;
panel->border_waveform_pending = true;
}
+
+ pm_runtime_mark_last_busy(panel->drm.dev);
+ pm_runtime_put_autosuspend(panel->drm.dev);
}
static const struct drm_plane_helper_funcs ssd16xx_plane_helper_funcs = {
@@ -1656,6 +1671,10 @@ static void ssd16xx_crtc_atomic_disable(struct drm_crtc *crtc,
if (panel->clear_on_disable < 0 || panel->display_cleared_on_deinit)
goto out;
+ ret = pm_runtime_resume_and_get(panel->drm.dev);
+ if (ret < 0)
+ goto out;
+
drm_dbg(&panel->drm, "clear_on_disable: running, mode=%d\n",
panel->clear_on_disable);
ret = ssd16xx_clear_display(panel,
@@ -1663,10 +1682,12 @@ static void ssd16xx_crtc_atomic_disable(struct drm_crtc *crtc,
panel->clear_on_disable));
if (ret) {
drm_err(&panel->drm, "atomic_disable: clear failed: %d\n", ret);
+ pm_runtime_put_autosuspend(panel->drm.dev);
goto out;
}
panel->display_cleared_on_deinit = true;
+ pm_runtime_put_sync_suspend(panel->drm.dev);
out:
drm_dev_exit(idx);
}
@@ -1682,6 +1703,12 @@ static void ssd16xx_crtc_atomic_enable(struct drm_crtc *crtc,
return;
panel->display_cleared_on_deinit = false;
+ ret = pm_runtime_resume_and_get(panel->drm.dev);
+ if (ret < 0) {
+ drm_err(&panel->drm, "crtc_atomic_enable: failed to resume: %d\n", ret);
+ drm_dev_exit(idx);
+ return;
+ }
drm_dbg(&panel->drm, "atomic_enable: %dx%d\n",
crtc_state->mode.hdisplay, crtc_state->mode.vdisplay);
@@ -1689,12 +1716,19 @@ static void ssd16xx_crtc_atomic_enable(struct drm_crtc *crtc,
panel->width = crtc_state->mode.hdisplay;
panel->height = crtc_state->mode.vdisplay;
- ret = ssd16xx_hw_init(panel);
- if (ret) {
- drm_err(&panel->drm, "crtc_atomic_enable: HW init failed: %d\n", ret);
- goto out;
+ /*
+ * pm_runtime_resume_and_get() triggers pm_runtime_resume which runs
+ * hw_init and sets initialized. If runtime PM is disabled the callback
+ * never fires, so fall back to running hw_init directly here.
+ */
+ if (!panel->initialized) {
+ ret = ssd16xx_hw_init(panel);
+ if (ret) {
+ drm_err(&panel->drm, "crtc_atomic_enable: HW init failed: %d\n", ret);
+ goto out;
+ }
+ panel->initialized = true;
}
- panel->initialized = true;
/* Clear display on first app launch if configured */
ret = ssd16xx_clear_display_on_init(panel);
@@ -1714,6 +1748,8 @@ static void ssd16xx_crtc_atomic_enable(struct drm_crtc *crtc,
}
out:
+ pm_runtime_mark_last_busy(panel->drm.dev);
+ pm_runtime_put_autosuspend(panel->drm.dev);
drm_dev_exit(idx);
}
@@ -1730,7 +1766,7 @@ static void ssd16xx_crtc_atomic_flush(struct drm_crtc *crtc,
struct drm_rect full;
int ret, idx;
- if (!panel->reinit_pending || !panel->initialized)
+ if (!panel->reinit_pending)
return;
if (!drm_dev_enter(&panel->drm, &idx))
@@ -1738,6 +1774,13 @@ static void ssd16xx_crtc_atomic_flush(struct drm_crtc *crtc,
panel->reinit_pending = false;
+ ret = pm_runtime_resume_and_get(panel->drm.dev);
+ if (ret < 0) {
+ drm_err(&panel->drm, "atomic_flush: failed to resume: %d\n", ret);
+ drm_dev_exit(idx);
+ return;
+ }
+
drm_dbg(&panel->drm, "atomic_flush: reinit, orientation=%u°\n",
panel->orientation);
@@ -1762,6 +1805,8 @@ static void ssd16xx_crtc_atomic_flush(struct drm_crtc *crtc,
}
out:
+ pm_runtime_mark_last_busy(panel->drm.dev);
+ pm_runtime_put_autosuspend(panel->drm.dev);
drm_dev_exit(idx);
}
@@ -2189,11 +2234,17 @@ static void ssd16xx_drm_master_drop(struct drm_device *drm,
if (panel->clear_on_close < 0 || panel->display_cleared_on_deinit)
return;
+ ret = pm_runtime_resume_and_get(drm->dev);
+ if (ret < 0)
+ return;
+
ret = ssd16xx_clear_display_on_exit(panel);
if (ret)
drm_err(drm, "master_drop: clear on close failed: %d\n", ret);
panel->display_cleared_on_deinit = true;
+ /* sync suspend — bypass autosuspend, sleep immediately after clear */
+ pm_runtime_put_sync_suspend(drm->dev);
}
static struct drm_driver ssd16xx_drm_driver = {
@@ -2496,6 +2547,20 @@ static int ssd16xx_probe(struct spi_device *spi)
if (ret)
return ret;
+ /* Default to runtime sleep mode (RAM retained if supported) */
+ panel->deep_sleep_mode = panel->controller_cfg->deep_sleep_mode_level1;
+ /*
+ * 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);
+
drm_dbg(drm, "SSD16xx e-paper display initialized (%dx%d, %d° rotation)\n",
panel->width, panel->height, panel->orientation);
@@ -2508,6 +2573,8 @@ static void ssd16xx_remove(struct spi_device *spi)
{
struct ssd16xx_panel *panel = spi_get_drvdata(spi);
+ pm_runtime_dont_use_autosuspend(&spi->dev);
+ pm_runtime_disable(&spi->dev);
drm_dev_unplug(&panel->drm);
drm_atomic_helper_shutdown(&panel->drm);
}
@@ -2516,9 +2583,124 @@ static void ssd16xx_shutdown(struct spi_device *spi)
{
struct ssd16xx_panel *panel = spi_get_drvdata(spi);
+ pm_runtime_dont_use_autosuspend(&spi->dev);
+ pm_runtime_disable(&spi->dev);
drm_atomic_helper_shutdown(&panel->drm);
}
+static int ssd16xx_pm_suspend(struct device *dev)
+{
+ struct drm_device *drm = dev_get_drvdata(dev);
+ struct ssd16xx_panel *panel = to_ssd16xx_panel(drm);
+ int ret;
+
+ /* System suspend: Mode 2 (max savings, RAM lost, full re-init on resume). */
+ panel->deep_sleep_mode = panel->controller_cfg->deep_sleep_mode_level2;
+ dev_dbg(dev, "system suspend: entering deep sleep mode 0x%02x\n",
+ panel->deep_sleep_mode);
+
+ ret = drm_mode_config_helper_suspend(drm);
+ if (ret)
+ return ret;
+
+ /*
+ * With per-update PM gating the panel may already be in MODE_1 deep
+ * sleep (autosuspend fired while the user was reading).
+ * pm_runtime_force_suspend() skips its callback when the device is
+ * already RPM_SUSPENDED, so MODE_2 would never reach the hardware.
+ *
+ * Per the datasheet, HWRESET is the only way to exit deep sleep (SPI
+ * is unresponsive in both modes). After the reset, MODE_2 can be
+ * sent immediately — no full hw_init is required. The RPM state
+ * remains RPM_SUSPENDED; pm_runtime_force_suspend() is skipped since
+ * the device is already in the right state.
+ */
+ if (pm_runtime_status_suspended(dev)) {
+ int err = 0;
+
+ dev_dbg(dev, "system suspend: upgrading MODE_1 -> MODE_2 via HWRESET\n");
+ ssd16xx_hw_reset(panel);
+ ssd16xx_send_cmd(panel, SSD16XX_CMD_DEEP_SLEEP_MODE, &err);
+ ssd16xx_send_data(panel, panel->controller_cfg->deep_sleep_mode_level2, &err);
+ panel->pm_force_suspended = false; /* Skip force_resume on resume */
+ return err;
+ }
+
+ /* Force runtime-suspended state for clean resume. */
+ pm_runtime_force_suspend(dev);
+ panel->pm_force_suspended = true; /* Resume must call force_resume */
+
+ return 0;
+}
+
+static int ssd16xx_pm_resume(struct device *dev)
+{
+ struct drm_device *drm = dev_get_drvdata(dev);
+ struct ssd16xx_panel *panel = to_ssd16xx_panel(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 (panel->pm_force_suspended) {
+ pm_runtime_force_resume(dev);
+ panel->pm_force_suspended = false;
+ }
+
+ /* Restore Mode 1 (RAM retained) for subsequent runtime PM cycles. */
+ panel->deep_sleep_mode = panel->controller_cfg->deep_sleep_mode_level1;
+
+ return drm_mode_config_helper_resume(drm);
+}
+
+static int ssd16xx_pm_runtime_suspend(struct device *dev)
+{
+ struct ssd16xx_panel *panel = to_ssd16xx_panel(dev_get_drvdata(dev));
+ int err = 0;
+
+ dev_dbg(dev, "runtime suspend: entering deep sleep mode 0x%02x\n",
+ panel->deep_sleep_mode);
+
+ ssd16xx_send_cmd(panel, SSD16XX_CMD_DEEP_SLEEP_MODE, &err);
+ ssd16xx_send_data(panel, panel->deep_sleep_mode, &err);
+ panel->initialized = false;
+
+ return err;
+}
+
+static int ssd16xx_pm_runtime_resume(struct device *dev)
+{
+ struct ssd16xx_panel *panel = to_ssd16xx_panel(dev_get_drvdata(dev));
+ int ret;
+
+ dev_dbg(dev, "runtime resume: initialized=%d%s\n",
+ panel->initialized,
+ !panel->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 (!panel->initialized) {
+ ret = ssd16xx_hw_init(panel);
+ if (ret)
+ return ret;
+
+ panel->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 },
{ }
@@ -2535,6 +2717,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
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH 5/6] MAINTAINERS: Add entry for Solomon SSD16xx DRM driver
2026-04-30 18:33 [PATCH 0/6] Add DRM driver for Solomon SSD16xx e-paper display controllers Devarsh Thakkar
` (3 preceding siblings ...)
2026-04-30 18:33 ` [PATCH 4/6] drm/tiny: panel-ssd16xx: Add power management support Devarsh Thakkar
@ 2026-04-30 18:33 ` Devarsh Thakkar
2026-04-30 18:33 ` [PATCH 6/6] arm64: defconfig: Enable DRM_PANEL_SSD16XX Devarsh Thakkar
5 siblings, 0 replies; 7+ messages in thread
From: Devarsh Thakkar @ 2026-04-30 18:33 UTC (permalink / raw)
To: David Airlie, Simona Vetter, Maarten Lankhorst, Maxime Ripard,
Thomas Zimmermann, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Neil Armstrong, Bjorn Andersson, dri-devel, devicetree,
linux-kernel
Cc: praneeth, vigneshr, s-jain1, s-wang12, r-donadkar, r-sharma3,
devarsht, afd
Add maintainer entry for the panel-ssd16xx DRM driver covering the SSD16xx
e-ink display controller family (SSD1673, SSD1680, SSD1681, SSD1683).
Signed-off-by: Devarsh Thakkar <devarsht@ti.com>
---
MAINTAINERS | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index 8e80296449ba..3a01544dbc50 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -8377,6 +8377,13 @@ F: Documentation/devicetree/bindings/display/solomon,ssd-common.yaml
F: Documentation/devicetree/bindings/display/solomon,ssd13*.yaml
F: drivers/gpu/drm/solomon/ssd130x*
+DRM DRIVER FOR SOLOMON SSD16XX E-PAPER DISPLAY CONTROLLERS
+M: Devarsh Thakkar <devarsht@ti.com>
+S: Maintained
+T: git https://gitlab.freedesktop.org/drm/misc/kernel.git
+F: Documentation/devicetree/bindings/display/solomon,ssd16xx.yaml
+F: drivers/gpu/drm/tiny/panel-ssd16xx.c
+
DRM DRIVER FOR ST-ERICSSON MCDE
M: Linus Walleij <linusw@kernel.org>
S: Maintained
--
2.39.1
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH 6/6] arm64: defconfig: Enable DRM_PANEL_SSD16XX
2026-04-30 18:33 [PATCH 0/6] Add DRM driver for Solomon SSD16xx e-paper display controllers Devarsh Thakkar
` (4 preceding siblings ...)
2026-04-30 18:33 ` [PATCH 5/6] MAINTAINERS: Add entry for Solomon SSD16xx DRM driver Devarsh Thakkar
@ 2026-04-30 18:33 ` Devarsh Thakkar
5 siblings, 0 replies; 7+ messages in thread
From: Devarsh Thakkar @ 2026-04-30 18:33 UTC (permalink / raw)
To: David Airlie, Simona Vetter, Maarten Lankhorst, Maxime Ripard,
Thomas Zimmermann, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Neil Armstrong, Bjorn Andersson, dri-devel, devicetree,
linux-kernel
Cc: praneeth, vigneshr, s-jain1, s-wang12, r-donadkar, r-sharma3,
devarsht, afd
Enable the Solomon SSD16xx e-ink display controller driver as a module
in the arm64 defconfig, making it available by default on TI AM6x SoC
platforms that use this driver.
Signed-off-by: Devarsh Thakkar <devarsht@ti.com>
---
arch/arm64/configs/defconfig | 1 +
1 file changed, 1 insertion(+)
diff --git a/arch/arm64/configs/defconfig b/arch/arm64/configs/defconfig
index 430219656640..0815384f955f 100644
--- a/arch/arm64/configs/defconfig
+++ b/arch/arm64/configs/defconfig
@@ -995,6 +995,7 @@ CONFIG_DRM_PANEL_RAYDIUM_RM67191=m
CONFIG_DRM_PANEL_RAYDIUM_RM692E5=m
CONFIG_DRM_PANEL_SAMSUNG_ATNA33XC20=m
CONFIG_DRM_PANEL_SITRONIX_ST7703=m
+CONFIG_DRM_PANEL_SSD16XX=m
CONFIG_DRM_PANEL_STARTEK_KD070FHFID015=m
CONFIG_DRM_PANEL_TRULY_NT35597_WQXGA=m
CONFIG_DRM_PANEL_VISIONOX_VTDR6130=m
--
2.39.1
^ permalink raw reply related [flat|nested] 7+ messages in thread
end of thread, other threads:[~2026-04-30 18:34 UTC | newest]
Thread overview: 7+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-04-30 18:33 [PATCH 0/6] Add DRM driver for Solomon SSD16xx e-paper display controllers Devarsh Thakkar
2026-04-30 18:33 ` [PATCH 1/6] dt-bindings: vendor-prefixes: Add Dalian Good Display Co., Ltd Devarsh Thakkar
2026-04-30 18:33 ` [PATCH 2/6] dt-bindings/display: Add Solomon SSD16xx e-paper controller binding Devarsh Thakkar
2026-04-30 18:33 ` [PATCH 3/6] drm/tiny: Add DRM driver for Solomon SSD16xx e-paper display controllers Devarsh Thakkar
2026-04-30 18:33 ` [PATCH 4/6] drm/tiny: panel-ssd16xx: Add power management support Devarsh Thakkar
2026-04-30 18:33 ` [PATCH 5/6] MAINTAINERS: Add entry for Solomon SSD16xx DRM driver Devarsh Thakkar
2026-04-30 18:33 ` [PATCH 6/6] arm64: defconfig: Enable DRM_PANEL_SSD16XX Devarsh Thakkar
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox