Skip to contentSkip to conversation

Add DRM driver for Solomon SSD16xx e-paper display controllers

PATCH Superseded Devarsh Thakkar 5 participants 31 messages Sep 27 – Oct 5

Subsystems (4)

v2 of 3 Superseded Latest is v3 →

lore
Share & tools
lore
#0 lore raw

v2 cover Add DRM driver for Solomon SSD16xx e-paper display controllers

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.

The driver is designed and architected keeping the bi-stable nature and low-power requirements of epaper display based systems in mind 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
- Pixel format support: XRGB8888 (color path) and R1 (native 1bpp, zero-copy path)
- 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 plane properties for refresh mode, colour mode, and session control (clear-on-init/close/disable, init-refresh waveform); rotation is set at probe time from a device tree property, and border waveform uses a fixed per-panel default

Changes from v1[0]:
- Restructured into 14 modular patches covering bindings, driver core, features, and platform integration (was 6 patches in v1)
- Added comprehensive device-tree bindings support for vendor prefixes and SSD16xx controller configurations
- Refactored driver architecture from drm/tiny panel driver to drm/solomon bridge driver
- Switch to using GEM_SHMEM helpers
- Refactored data entry mode implementation
- Removed runtime-configurable rotation/border-waveform DRM properties and their module params; rotation is now set via device tree, and border waveform now uses an enhanced fixed per-panel default
- Added support for Solomon SSD1677 controller variant
- Improved session management with plane properties
- Added comprehensive power management support
- Support for panels with reversed RAM X order
- Dropped RGB888, RGB565, R8, NV12, NV16, YUYV, and UYVY format support; driver now only exposes XRGB8888 and R1

This has been tested against my development branch [1] using attached test suite [2] and logs[3], individual results[4] and result summary[5] uploaded.

[0]: [V1 patch series]
https://lore.kernel.org/all/[email protected]/

[1]: [Development Branch]
https://github.com/devarsht/linux-next/tree/epaper_drm_misc_next_staging_27_09_26

[2]: [Test Suite]
https://github.com/devarsht/linux-next/tree/epaper_drm_misc_next_staging_27_09_26/tests_27sept_new

[3]: [Test Logs]
https://github.com/devarsht/linux-next/tree/epaper_drm_misc_next_staging_27_09_26/tests_27sept_new/logs

[4]: [Test Results]
https://github.com/devarsht/linux-next/tree/epaper_drm_misc_next_staging_27_09_26/tests_27sept_new/results

[5]: [Result Summary]
https://github.com/devarsht/linux-next/blob/epaper_drm_misc_next_staging_27_09_26/tests_27sept_new/results/run_all_20250529_190854.summary

Devarsh Thakkar (13):
dt-bindings: vendor-prefixes: Add Dalian Good Display Co., Ltd.
dt-bindings: display: Add Solomon SSD16xx e-paper controller binding
dt-bindings: display: solomon,ssd16xx: Add Solomon SSD1677 controller drm/solomon: Add DRM driver for Solomon SSD16xx e-paper display
controllers
drm/solomon: ssd16xx: Add clear_on_init/close/disable session
management
drm/solomon: ssd16xx: Add support for Solomon SSD1677 controller drm/solomon: ssd16xx: Add power management support drm/solomon: ssd16xx: Expose refresh mode as plane property drm/solomon: ssd16xx: Expose color mode as plane property drm/solomon: ssd16xx: Expose session management as plane properties
MAINTAINERS: Add entry for Solomon SSD16xx DRM driver
arm64: defconfig: Enable DRM_SSD16XX for AM62L3 EVM
arm64: dts: ti: Add AM62L3 EVM overlay for GDEY042T81 e-paper display

LiangCheng Wang (1):
drm/solomon: ssd16xx: support panels whose RAM X order is reversed

 .../bindings/display/solomon,ssd16xx.yaml     |   88 +
 .../devicetree/bindings/vendor-prefixes.yaml  |    2 +
 MAINTAINERS                                   |    8 +
 arch/arm64/boot/dts/ti/Makefile               |    6 +
 .../ti/k3-am62l3-evm-epaper-gdey042t81.dtso   |   96 +
 arch/arm64/configs/defconfig                  |    1 +
 drivers/gpu/drm/solomon/Kconfig               |   13 +
 drivers/gpu/drm/solomon/Makefile              |    1 +
 drivers/gpu/drm/solomon/ssd16xx.c             | 2607 +++++++++++++++++
 9 files changed, 2822 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/display/solomon,ssd16xx.yaml
 create mode 100644 arch/arm64/boot/dts/ti/k3-am62l3-evm-epaper-gdey042t81.dtso
 create mode 100644 drivers/gpu/drm/solomon/ssd16xx.c
Signature · 1 line
2.39.1
#1 lore raw

PATCH v2 1/14 dt-bindings: vendor-prefixes: Add Dalian Good Display Co., Ltd.

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.

---
Changes from v1:
- No change

 Documentation/devicetree/bindings/vendor-prefixes.yaml | 2 ++
 1 file changed, 2 insertions(+)
Documentation/devicetree/bindings/vendor-prefixes.yaml +2 -0
index ba2002969373..3e8062aa92f7 100644
@@ -686,6 +686,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,.*":
Signature · 1 line
2.39.1
#2 lore raw

PATCH v2 2/14 dt-bindings: display: Add Solomon SSD16xx e-paper controller binding

In reply to Devarsh Thakkar

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

---
Changes from v1:
- Added fallback compatible for the controller
- Wrap lines to 80

 .../bindings/display/solomon,ssd16xx.yaml     | 87 +++++++++++++++++++
 1 file changed, 87 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/display/solomon,ssd16xx.yaml
Documentation/devicetree/bindings/display/solomon,ssd16xx.yaml new file +87 -0
new file mode 100644
index 000000000000..b31fb862de04
@@ -0,0 +1,87 @@
+# 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 <[email protected]>
+
+description:
+  The SSD16xx family includes e-paper display controllers (SSD1673,
+  SSD1680, SSD1681, SSD1683, SSD1677) with integrated gate driver,
+  source driver and timing controller. Different panels use these
+  controllers in monochrome or 3-color (black/white/red) configurations.
+  The controller uses a 4-wire SPI interface and requires GPIO pins for
+  reset, busy status, and data/command control. A 3-wire mode is also
+  available where the D/C bit is transmitted along the SPI data line
+  before each byte.
+
+allOf:
+  - $ref: /schemas/spi/spi-peripheral-props.yaml#
+
+properties:
+  compatible:
+    items:
+      - enum:
+          # Good Display 4.2" 400x300 monochrome e-ink panel
+          - gooddisplay,gdey042t81
+      - enum:
+          - solomon,ssd1683
+
+  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:
+    $ref: /schemas/types.yaml#/definitions/uint32
+    enum: [0, 90, 180, 270]
+    description:
+      Display rotation in degrees clockwise.
+
+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", "solomon,ssd1683";
+            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>;
+        };
+    };
Signature · 1 line
2.39.1
#3 lore raw

Re: [PATCH v2 02/14] dt-bindings: display: Add Solomon SSD16xx e-paper controller binding

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider: - [Medium] dt-bindings: redefined standard rotation property semantics (clockwise vs CCW) - [Medium]…

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Medium] dt-bindings: redefined standard rotation property semantics (clockwise vs CCW)
- [Medium] dt-bindings: missing panel-common.yaml reference incorrectly rejects panel properties
- [Medium] dt-bindings: missing spi-3wire property definition breaks validation for 3-wire mode --

commit 504b4d897333eddd3593a4ba35eae49d79d7d739
Author: Devarsh Thakkar <[email protected]>
dt-bindings: display: Add Solomon SSD16xx e-paper controller binding

This commit adds a device tree binding for the Solomon Systech SSD16xx e-Paper display controllers. It covers the 4-wire SPI interface and required GPIO pins while also noting optional 3-wire mode support.

Quoted 4 lines
Documentation/devicetree/bindings/display/solomon,ssd16xx.yaml new file +0 -0
new file mode 100644

[ ... ]

Documentation/devicetree/bindings/display/solomon,ssd16xx.yaml line 23

Quoted 2 lines
Documentation/devicetree/bindings/display/solomon,ssd16xx.yaml new file +2 -0
+allOf:
+  - $ref: /schemas/spi/spi-peripheral-props.yaml#

[Severity: Medium]
Is this missing a reference to /schemas/display/panel/panel-common.yaml#?

Because this schema acts as a display panel node and sets
unevaluatedProperties: false later in the file, omitting the panel-common reference strictly forbids standard panel properties like power-supply or port. This might prevent valid hardware board configurations from passing schema validation.

Documentation/devicetree/bindings/display/solomon,ssd16xx.yaml line 26

Quoted 3 lines
Documentation/devicetree/bindings/display/solomon,ssd16xx.yaml new file +3 -0
+
+properties:
+  compatible:

[Severity: Medium]
Should spi-3wire: true be explicitly declared here in the properties section?

The commit message notes that the controller supports 3-wire mode, but neither this schema nor the referenced spi-peripheral-props.yaml define spi-3wire. Because unevaluatedProperties is set to false, configuring a peripheral in 3-wire mode with the standard spi-3wire property will erroneously fail schema validation.

[ ... ]

Documentation/devicetree/bindings/display/solomon,ssd16xx.yaml line 61

Quoted 5 lines
Documentation/devicetree/bindings/display/solomon,ssd16xx.yaml new file +5 -0
+  rotation:
+    $ref: /schemas/types.yaml#/definitions/uint32
+    enum: [0, 90, 180, 270]
+    description:
+      Display rotation in degrees clockwise.

[Severity: Medium]
Does defining rotation as clockwise conflict with standard device tree semantics?

The device tree standard for displays (panel-common.yaml) explicitly defines rotation as counter-clockwise. Defining it as clockwise here creates a semantic mismatch that could lead to inverted rotation at runtime if the DRM driver or generic kernel helpers parse it assuming the standard CCW semantics.

[ ... ]

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=2
#4 lore raw

Re: [PATCH v2 02/14] dt-bindings: display: Add Solomon SSD16xx e-paper controller binding

In reply to Devarsh Thakkar
Quoted 3 lines · Devarsh Thakkar

Add device tree binding for the Solomon Systech SSD16xx family of e-Paper display controllers (SSD1683 [1], SSD1673 [3], SSD1680 [4], SSD1681 [5]).

A nit, subject: drop second/last, redundant "binding". The "dt-bindings" prefix is already stating that these are bindings. See also:
https://elixir.bootlin.com/linux/v7.1-rc7/source/Documentation/devicetree/bindings/submitting-patches.rst#L23

Documentation/devicetree/bindings/display/solomon,ssd16xx.yaml

Quoted 38 lines

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

---
Changes from v1:
- Added fallback compatible for the controller
- Wrap lines to 80

 .../bindings/display/solomon,ssd16xx.yaml     | 87 +++++++++++++++++++
 1 file changed, 87 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/display/solomon,ssd16xx.yaml
Documentation/devicetree/bindings/display/solomon,ssd16xx.yaml new file +0 -0
new file mode 100644
index 000000000000..b31fb862de04

Filename must match compatible.

Quoted 22 lines
Documentation/devicetree/bindings/display/solomon,ssd16xx.yaml new file +21 -0
@@ -0,0 +1,87 @@
+# 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 <[email protected]>
+
+description:
+  The SSD16xx family includes e-paper display controllers (SSD1673,
+  SSD1680, SSD1681, SSD1683, SSD1677) with integrated gate driver,
+  source driver and timing controller. Different panels use these
+  controllers in monochrome or 3-color (black/white/red) configurations.
+  The controller uses a 4-wire SPI interface and requires GPIO pins for
+  reset, busy status, and data/command control. A 3-wire mode is also
+  available where the D/C bit is transmitted along the SPI data line
+  before each byte.
+

Documentation/devicetree/bindings/display/solomon,ssd16xx.yaml line 31

Quoted 10 lines
Documentation/devicetree/bindings/display/solomon,ssd16xx.yaml new file +10 -0
+allOf:
+  - $ref: /schemas/spi/spi-peripheral-props.yaml#
+
+properties:
+  compatible:
+    items:
+      - enum:
+          # Good Display 4.2" 400x300 monochrome e-ink panel
+          - gooddisplay,gdey042t81
+      - enum:

No, fallback cannot be enumeration.

Quoted 21 lines
Documentation/devicetree/bindings/display/solomon,ssd16xx.yaml new file +21 -0
+          - solomon,ssd1683
+
+  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

Documentation/devicetree/bindings/display/solomon,ssd16xx.yaml line 62

Quoted 10 lines
Documentation/devicetree/bindings/display/solomon,ssd16xx.yaml new file +10 -0
+    description:
+      GPIO connected to the D/C (Data/Command) pin. Low for command,
+      high for data.
+
+  rotation:
+    $ref: /schemas/types.yaml#/definitions/uint32
+    enum: [0, 90, 180, 270]
+    description:
+      Display rotation in degrees clockwise.
+

Is this display controller or panel? If panel, then missing ref to panel-common. If display controller, then description is misleading.

Best regards,
Krzysztof

#5 lore raw

Re: [PATCH v2 02/14] dt-bindings: display: Add Solomon SSD16xx e-paper controller binding

Hi Krzysztof,

Thanks for the review.

Quoted 10 lines · Krzysztof Kozlowski
Quoted 3 lines · Devarsh Thakkar

Add device tree binding for the Solomon Systech SSD16xx family of e-Paper display controllers (SSD1683 [1], SSD1673 [3], SSD1680 [4], SSD1681 [5]).

A nit, subject: drop second/last, redundant "binding". The "dt-bindings" prefix is already stating that these are bindings. See also:
https://elixir.bootlin.com/linux/v7.1-rc7/source/Documentation/devicetree/bindings/submitting-patches.rst#L23

Agreed.

Quoted 33 lines
Quoted 33 lines

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

---
Changes from v1:
- Added fallback compatible for the controller
- Wrap lines to 80

  .../bindings/display/solomon,ssd16xx.yaml     | 87 +++++++++++++++++++
  1 file changed, 87 insertions(+)

create mode 100644 Documentation/devicetree/bindings/display/solomon,ssd16xx.yaml

Quoted 8 lines

Documentation/devicetree/bindings/display/solomon,ssd16xx.yaml

Quoted 5 lines
Documentation/devicetree/bindings/display/solomon,ssd16xx.yaml new file +0 -0
new file mode 100644
index 000000000000..b31fb862de04

Filename must match compatible.

Sure, will fix.

Quoted 26 lines
Quoted 26 lines
Documentation/devicetree/bindings/display/solomon,ssd16xx.yaml new file +25 -0
@@ -0,0 +1,87 @@
+# 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 <[email protected]>
+
+description:
+  The SSD16xx family includes e-paper display controllers (SSD1673,
+  SSD1680, SSD1681, SSD1683, SSD1677) with integrated gate driver,
+  source driver and timing controller. Different panels use these
+  controllers in monochrome or 3-color (black/white/red) configurations.
+  The controller uses a 4-wire SPI interface and requires GPIO pins for
+  reset, busy status, and data/command control. A 3-wire mode is also
+  available where the D/C bit is transmitted along the SPI data line
+  before each byte.
+
+allOf:
+  - $ref: /schemas/spi/spi-peripheral-props.yaml#
+
+properties:
Quoted 10 lines

Documentation/devicetree/bindings/display/solomon,ssd16xx.yaml line 31

Quoted 6 lines
Documentation/devicetree/bindings/display/solomon,ssd16xx.yaml new file +6 -0
+  compatible:
+    items:
+      - enum:
+          # Good Display 4.2" 400x300 monochrome e-ink panel
+          - gooddisplay,gdey042t81
+      - enum:

No, fallback cannot be enumeration.

Quoted 1 line
Documentation/devicetree/bindings/display/solomon,ssd16xx.yaml new file +1 -0
+          - solomon,ssd1683

Agreed, will fix in this patch itself.

Quoted 25 lines
Quoted 25 lines
Documentation/devicetree/bindings/display/solomon,ssd16xx.yaml new file +25 -0
+
+  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:
Quoted 9 lines

Documentation/devicetree/bindings/display/solomon,ssd16xx.yaml line 62

Quoted 5 lines
Documentation/devicetree/bindings/display/solomon,ssd16xx.yaml new file +5 -0
+    $ref: /schemas/types.yaml#/definitions/uint32
+    enum: [0, 90, 180, 270]
+    description:
+      Display rotation in degrees clockwise.
+

Is this display controller or panel? If panel, then missing ref to panel-common. If display controller, then description is misleading.

Yes, I think it should be possible to ref panel-common, Thanks for pointing.

Regards
Devarsh

Quoted 3 lines

Best regards,
Krzysztof

#6 lore raw

PATCH v2 3/14 dt-bindings: display: solomon,ssd16xx: Add Solomon SSD1677 controller

In reply to Devarsh Thakkar

Add solomon,ssd1677 to the controller fallback compatible enum.

The SSD1677 supports displays up to 960x680 pixels with 10-bit pixel-level X/Y RAM addressing. It shares the same command address space and GPIO interface as the SSD1683 but differs in addressing model, deep sleep behaviour, and initialisation sequence.

---
Changes from v1:
- New patch: Added support for SSD1677 controller variant in bindings

 .../bindings/display/solomon,ssd16xx.yaml           | 13 +++++++------
 1 file changed, 7 insertions(+), 6 deletions(-)
Documentation/devicetree/bindings/display/solomon,ssd16xx.yaml +7 -6
index b31fb862de04..1a94ad9b0d94 100644
@@ -24,12 +24,13 @@ allOf:
 
 properties:
   compatible:
-    items:
-      - enum:
-          # Good Display 4.2" 400x300 monochrome e-ink panel
-          - gooddisplay,gdey042t81
-      - enum:
-          - solomon,ssd1683
+    oneOf:
+      - items:
+          - enum:
+              # Good Display 4.2" 400x300 monochrome e-ink panel
+              - gooddisplay,gdey042t81
+          - const: solomon,ssd1683
+      - const: solomon,ssd1677
 
   reg:
     maxItems: 1
Signature · 1 line
2.39.1
#7 lore raw

Re: [PATCH v2 03/14] dt-bindings: display: solomon,ssd16xx: Add Solomon SSD1677 controller

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider: - [Medium] dt-bindings: display: solomon,ssd16xx: commit message and schema mismatch for ssd1677 --

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] dt-bindings: display: solomon,ssd16xx: commit message and schema mismatch for ssd1677 --

commit 0557e7fb08a4970f824714f1d57d93f2cdcd0204
Author: Devarsh Thakkar <[email protected]>

dt-bindings: display: solomon,ssd16xx: Add Solomon SSD1677 controller

Adds support for the Solomon SSD1677 controller to the solomon,ssd16xx binding. The commit message indicates adding it to the controller fallback enum, but the schema restructuring implements it as a standalone constant.

Quoted 11 lines
Documentation/devicetree/bindings/display/solomon,ssd16xx.yaml +0 -3
index b31fb862de042..1a94ad9b0d944 100644
@@ -24,12 +24,13 @@ allOf:
 
 properties:
   compatible:
-    items:
-      - enum:
-          # Good Display 4.2" 400x300 monochrome e-ink panel

Documentation/devicetree/bindings/display/solomon,ssd16xx.yaml allOf: · line 33

Quoted 10 lines
Documentation/devicetree/bindings/display/solomon,ssd16xx.yaml +7 -3
-          - gooddisplay,gdey042t81
-      - enum:
-          - solomon,ssd1683
+    oneOf:
+      - items:
+          - enum:
+              # Good Display 4.2" 400x300 monochrome e-ink panel
+              - gooddisplay,gdey042t81
+          - const: solomon,ssd1683
+      - const: solomon,ssd1677

[Severity: Medium]
The commit message explicitly states the intent to "Add solomon,ssd1677 to the controller fallback compatible enum." However, the patch implements it as a standalone const under a new oneOf block.

In dt-schema, a bare const validates an array containing exactly one element. While restructuring the schema to avoid falsely linking the new ssd1677 controller with the existing ssd1683-based panel is structurally correct, this creates a mismatch between the commit message's stated intent and the implemented schema constraints.

Developers attempting to use the compatible string as a fallback (as advertised in the commit message) like:

	compatible = "vendor,panel", "solomon,ssd1677";

will encounter dt-schema validation failures during make dtbs_check. Can the commit message be updated to reflect the standalone nature of this new compatible, or does the schema need to permit fallback usage?

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=3
#8 lore raw

Re: [PATCH v2 03/14] dt-bindings: display: solomon,ssd16xx: Add Solomon SSD1677 controller

In reply to Devarsh Thakkar
Quoted 25 lines · Devarsh Thakkar

Add solomon,ssd1677 to the controller fallback compatible enum.

The SSD1677 supports displays up to 960x680 pixels with 10-bit pixel-level X/Y RAM addressing. It shares the same command address space and GPIO interface as the SSD1683 but differs in addressing model, deep sleep behaviour, and initialisation sequence.

---
Changes from v1:
- New patch: Added support for SSD1677 controller variant in bindings

 .../bindings/display/solomon,ssd16xx.yaml           | 13 +++++++------
 1 file changed, 7 insertions(+), 6 deletions(-)
Documentation/devicetree/bindings/display/solomon,ssd16xx.yaml +0 -2
index b31fb862de04..1a94ad9b0d94 100644
@@ -24,12 +24,13 @@ allOf:
 
 properties:
   compatible:
-    items:
-      - enum:

Documentation/devicetree/bindings/display/solomon,ssd16xx.yaml allOf: · line 32

Quoted 10 lines
Documentation/devicetree/bindings/display/solomon,ssd16xx.yaml +6 -4
-          # Good Display 4.2" 400x300 monochrome e-ink panel
-          - gooddisplay,gdey042t81
-      - enum:
-          - solomon,ssd1683
+    oneOf:
+      - items:
+          - enum:
+              # Good Display 4.2" 400x300 monochrome e-ink panel
+              - gooddisplay,gdey042t81
+          - const: solomon,ssd1683

You just added all these lines!

Best regards,
Krzysztof

#9 lore raw

PATCH v2 4/14 drm/solomon: Add DRM driver for Solomon SSD16xx e-paper display controllers

In reply to Devarsh Thakkar

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),
- 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

DRM framework integration:
- drm_atomic_helper-based modesetting with damage tracking
- Single primary plane, CRTC, simple encoder, SPI connector
- drm_gem_shmem backend for display buffers
- fbdev fallback via drm_fbdev_shmem_setup

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

---

Changes from v1:
- Converted from drm/tiny/panel-ssd16xx.c to drm/solomon/ssd16xx.c
- Reorganized code for better maintainability
- Improved hardware abstraction for multi-controller support
- Enhanced initialization sequence with better power state management
- Switch to using GEM_SHMEM helpers
- Refactored data entry mode implementation
- Removed drm properties for runtime rotation, border waveforms
- Removed module params for each of the drm properties
- Improved SPDX license header formatting
- Better separation of concerns between hardware and DRM integration

 drivers/gpu/drm/solomon/Kconfig   |   13 +
 drivers/gpu/drm/solomon/Makefile  |    1 +
 drivers/gpu/drm/solomon/ssd16xx.c | 1849 +++++++++++++++++++++++++++++
 3 files changed, 1863 insertions(+)
 create mode 100644 drivers/gpu/drm/solomon/ssd16xx.c
drivers/gpu/drm/solomon/Kconfig +13 -0
index 400a6cab3a67..de2b14819f81 100644
@@ -30,3 +30,16 @@ config DRM_SSD130X_SPI
 	  Say Y here if the SSD13xx OLED display is connected via SPI bus.
 
 	  If M is selected the module will be called ssd130x-spi.
+
+config DRM_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_SHMEM_HELPER
+	help
+	  DRM driver for Solomon SSD16xx family e-paper display controllers
+	  (SSD1673, SSD1680, SSD1681, SSD1683, SSD1677). Supports panels
+	  such as the GDEY042T81 4.2" 400x300.
+
+	  If M is selected the module will be called ssd16xx.
drivers/gpu/drm/solomon/Makefile +1 -0
index b5fc792257d7..72384620785b 100644
@@ -1,3 +1,4 @@
 obj-$(CONFIG_DRM_SSD130X)	+= ssd130x.o
 obj-$(CONFIG_DRM_SSD130X_I2C)	+= ssd130x-i2c.o
 obj-$(CONFIG_DRM_SSD130X_SPI)	+= ssd130x-spi.o
+obj-$(CONFIG_DRM_SSD16XX)	+= ssd16xx.o
drivers/gpu/drm/solomon/ssd16xx.c new file +1849 -0

1,849 lines not on this page · raw message

Signature · 1 line
2.39.1
#10 lore raw

Re: [PATCH v2 04/14] drm/solomon: Add DRM driver for Solomon SSD16xx e-paper display controllers

Thank you for your contribution! Sashiko AI review found 12 potential issue(s) to consider: - [Critical] drm/solomon: critical heap buffer overflow in format conversion - [High] drm/solomon…

Thank you for your contribution! Sashiko AI review found 12 potential issue(s) to consider:
- [Critical] drm/solomon: critical heap buffer overflow in format conversion
- [High] drm/solomon: sleeping in atomic context during plane and crtc updates
- [High] drm/solomon: redundant hardware reset and full redraw on every atomic commit
- [High] drm/solomon: mapping local stack variables for SPI DMA transfers
- [High] drm/solomon: illegal direct dereference of iosys_map vaddr
- [High] drm/solomon: use-after-free of last_fb during crtc_atomic_flush
- [Medium] drm/solomon: ssd16xx_convert_fb_to_3color ignores orientation causing scrambled output
- [Medium] drm/solomon: DRM_SSD16XX selects DRM_GEM_SHMEM_HELPER without MMU dependency
- [Medium] drm/solomon: scrambled image on reverse rotations due to missing intra-byte bit reversal
- [Medium] drm/solomon: missing drm_dev_enter protection in plane_atomic_update
- [Medium] drm/solomon: data race on device->init_refresh_pending flag
- [Medium] drm/solomon: incorrect endianness conversion reading XRGB8888 pixels
--

commit c0d6d51051f544c36f758eda4ee2441c54d494b8
Author: Devarsh Thakkar <[email protected]>

drm/solomon: Add DRM driver for Solomon SSD16xx e-paper display controllers

This commit introduces a new DRM driver for the Solomon Systech SSD16xx family of e-paper display controllers. It supports panels like the Gooddisplay GDEY042T81 and implements hardware features such as deep sleep modes, 4-way rotation, and single primary plane updates.

Quoted 3 lines
drivers/gpu/drm/solomon/Kconfig +0 -0

[ ... ]

drivers/gpu/drm/solomon/Kconfig config DRM_SSD130X_SPI · line 35

Quoted 7 lines
drivers/gpu/drm/solomon/Kconfig +6 -0
@@ -30,3 +30,16 @@ config DRM_SSD130X_SPI
+config DRM_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_SHMEM_HELPER

[Severity: Medium]
Does selecting DRM_GEM_SHMEM_HELPER here without inheriting its required depends on MMU dependency cause Kconfig unmet direct dependency warnings and potential build failures on MMU-less architectures?

Quoted 3 lines
drivers/gpu/drm/solomon/ssd16xx.c new file +0 -0

[ ... ]

Quoted 3 lines
drivers/gpu/drm/solomon/ssd16xx.c new file +3 -0
+static void ssd16xx_wait_for_device(struct ssd16xx_device *device,
+				    int *err)
+{

[ ... ]

drivers/gpu/drm/solomon/ssd16xx.c line 449

Quoted 8 lines
drivers/gpu/drm/solomon/ssd16xx.c new file +8 -0
+	while (gpiod_get_value_cansleep(device->busy) == 1) {
+		if (time_after(jiffies, timeout_jiffies)) {
+			drm_err(&device->drm, "Busy wait timed out after %lums\n",
+				jiffies_to_msecs(jiffies) - start_ms);
+			*err = -ETIMEDOUT;
+			return;
+		}
+		usleep_range(1000, 2000);

[Severity: High]
Can this sleep inside an atomic context? The DRM subsystem guidelines strictly dictate that plane_atomic_update and crtc_atomic_flush are atomic context paths where sleeping is forbidden.

This driver calls usleep_range, spi_sync, and drm_gem_vmap (which takes a sleeping ww_mutex) from within these callbacks. Violating this invariant causes system instability, kernel warnings, and potential deadlocks when these functions run in non-preemptible contexts.

[ ... ]

Quoted 6 lines
drivers/gpu/drm/solomon/ssd16xx.c new file +6 -0
+static void ssd16xx_send_cmd(struct ssd16xx_device *device, u8 cmd,
+			     int *err)
+{
+	u16 word;
+	struct spi_transfer xfer = {};
+	struct spi_message msg;

drivers/gpu/drm/solomon/ssd16xx.c line 484

Quoted 10 lines
drivers/gpu/drm/solomon/ssd16xx.c new file +10 -0
+
+	if (*err)
+		return;
+
+	spi_message_init(&msg);
+	spi_message_add_tail(&xfer, &msg);
+
+	if (device->dc) {
+		/* 4-wire SPI: D/C# GPIO low selects command mode */
+		xfer.tx_buf = &cmd;

[Severity: High]
Is it safe to assign pointers to local stack variables (like &cmd, &word, and &data) to the tx_buf of an spi_transfer?

The SPI subsystem maps these buffers for DMA via spi_sync. Passing stack memory to the DMA API is explicitly forbidden; on architectures without coherent DMA caches, this causes cache-line sharing corruption and triggers DMA API debug panics.

[ ... ]

drivers/gpu/drm/solomon/ssd16xx.c line 761

Quoted 6 lines
drivers/gpu/drm/solomon/ssd16xx.c new file +6 -0
+static u8 ssd16xx_pixel_luma(struct iosys_map *src,
+			     struct drm_framebuffer *fb,
+			     unsigned int x, unsigned int y)
+{
+	u32 *line = (u32 *)(src->vaddr + y * fb->pitches[0]);
+	u32 px = line[x];

[Severity: High]
Does this directly dereference an iosys_map structure bypassing the required abstraction API? iosys_map is explicitly designed to abstract memory that may reside in I/O space. Direct CPU dereference of an IOMEM pointer without iosys_map_rd or similar helpers will trigger exceptions and kernel panics on architectures that require specialized I/O accessors.

[Severity: Medium]
Will this native-endian u32 memory dereference convert endianness correctly on big-endian hardware?

DRM formats like XRGB8888 are strictly little-endian byte arrays in memory. On big-endian CPUs, this native memory load will reverse the byte sequence, causing the subsequent bitwise shifts to extract incorrect colors.

[ ... ]

Quoted 9 lines
drivers/gpu/drm/solomon/ssd16xx.c new file +9 -0
+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;

[ ... ]

drivers/gpu/drm/solomon/ssd16xx.c line 857

Quoted 3 lines
drivers/gpu/drm/solomon/ssd16xx.c new file +3 -0
+	/* XRGB8888 */
+	for (y = rect->y1; y < rect->y2; y++) {
+		for (x = rect->x1; x < rect->x2; x++) {

[Severity: Medium]
Does this hardcoded row-major traversal ignore the device orientation?

While the 1bpp monochrome conversion correctly alters pixel packing geometry based on display orientation, this function seems to ignore it completely. This can result in the RED RAM being populated in an incompatible geometric layout, rendering scrambled red ink for non-native orientations.

[ ... ]

Quoted 5 lines
drivers/gpu/drm/solomon/ssd16xx.c new file +5 -0
+static void ssd16xx_convert_fb_to_1bpp(u8 *dst, struct iosys_map *src,
+				       struct drm_framebuffer *fb,
+				       struct drm_rect *rect,
+				       unsigned int orientation)
+{

[ ... ]

drivers/gpu/drm/solomon/ssd16xx.c line 946

Quoted 4 lines
drivers/gpu/drm/solomon/ssd16xx.c new file +4 -0
+		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));

[Severity: Medium]
Does this bit packing logic properly handle reverse rotations?

To implement 90 and 180 degree rotations, the hardware is configured to decrement RAM addresses (XDEC_YDEC), but the hardware does not internally reverse the pixel-to-bit assignment within each 8-pixel byte. Packing pixels in a strictly forward bit sequence here means the first pixel from the reversed iteration ends up in bit 7 instead of bit 0, resulting in the 8 pixels of every byte being displayed in reverse order.

[ ... ]

Quoted 12 lines
drivers/gpu/drm/solomon/ssd16xx.c new file +12 -0
+static int ssd16xx_fb_dirty(struct drm_framebuffer *fb, struct drm_rect *rect,
+			    struct ssd16xx_device *device,
+			    const struct iosys_map *src_map)
+{
+	const u8 *ctrl2_tbl = device->controller_cfg->ctrl2_refresh;
+	struct iosys_map map = *src_map;
+	int err = 0;
+	unsigned int data_size = (device->width * device->height) / 8;
+	u8 *mono_buffer = NULL;
+	u8 *red_buffer = NULL;
+	u16 ram_x_start, ram_x_end, ram_y_start, ram_y_end;
+

drivers/gpu/drm/solomon/ssd16xx.c line 1010

Quoted 10 lines
drivers/gpu/drm/solomon/ssd16xx.c new file +10 -0
+	/*
+	 * Process full display area.  The rect for convert_fb_to_1bpp uses
+	 * the framebuffer (logical) dimensions - the pixel iteration walks
+	 * the source fb coordinate space. RAM window registers below use
+	 * the physical panel dimensions (device->width/height).
+	 */
+	rect->x1 = 0;
+	rect->y1 = 0;
+	rect->x2 = fb->width;
+	rect->y2 = fb->height;

[Severity: Critical]
Can this cause a heap buffer overflow in the format conversion?

The TX buffers are statically allocated to exactly fit the physical panel dimensions in ssd16xx_alloc_tx_bufs. By overriding the provided CRTC plane boundaries with the full, unconstrained framebuffer dimensions here, if userspace supplies a framebuffer larger than the CRTC, the conversion loop will write out of bounds.

Additionally, at native resolutions using portrait orientations (like 300x400), the 37.5-byte rows are padded to 38 bytes per row (400 * 38 = 15,200 bytes), which overflows the 15,000 byte allocation.

[ ... ]

Quoted 3 lines
drivers/gpu/drm/solomon/ssd16xx.c new file +3 -0
+static void ssd16xx_plane_atomic_update(struct drm_plane *plane,
+					struct drm_atomic_commit *state)
+{

[ ... ]

drivers/gpu/drm/solomon/ssd16xx.c line 1271

Quoted 6 lines
drivers/gpu/drm/solomon/ssd16xx.c new file +6 -0
+	saved_mode = device->refresh_mode;
+	saved_border_waveform_idx = device->border_waveform_update_idx;
+	if (device->init_refresh_pending) {
+		device->refresh_mode = device->refresh_mode_init;
+		device->border_waveform_update_idx = device->border_waveform_init_idx;
+	}

[Severity: Medium]
Is there a data race on the device->init_refresh_pending flag?

The boolean flag is modified in ssd16xx_drm_master_set and ssd16xx_drm_master_drop under the protection of the DRM core's master_mutex. Simultaneously, it is read and modified here under the DRM modeset locks. Since these are separate lock classes, a concurrent atomic commit and SET_MASTER ioctl could trigger a data race.

[ ... ]

drivers/gpu/drm/solomon/ssd16xx.c line 1290

Quoted 11 lines
drivers/gpu/drm/solomon/ssd16xx.c new file +11 -0
+	if (device->fast_lut_pending) {
+		ret = ssd16xx_preload_fast_lut(device);
+		if (ret) {
+			drm_err(&device->drm,
+				"plane_atomic_update: fast LUT preload failed: %d\n", ret);
+		}
+
+		device->fast_lut_pending = false;
+	}
+
+	ret = ssd16xx_fb_dirty(fb, &rect, device, &shadow_state->data[0]);

[Severity: Medium]
Is this missing drm_dev_enter protection?

Initiating hardware interactions (such as SPI writes and GPIO polls) without drm_dev_enter validation can lead to race conditions during teardown from the commit workqueue. If the SPI device is unbound, devres frees hardware resources, and omitting drm_dev_enter here could trigger a use-after-free on the SPI device or GPIOs.

drivers/gpu/drm/solomon/ssd16xx.c line 1294

Quoted 4 lines
drivers/gpu/drm/solomon/ssd16xx.c new file +4 -0
+	if (ret)
+		drm_err(&device->drm, "plane_atomic_update: display update failed: %d\n", ret);
+	else
+		device->last_fb = fb;

[Severity: High]
Does storing a raw pointer to the framebuffer without incrementing its reference count risk a use-after-free?

When the plane is later disabled by userspace, last_fb can become a dangling pointer if the framebuffer is cleaned up by the core. A subsequent atomic commit on the CRTC will trigger ssd16xx_crtc_atomic_flush, which falls back to this dangling last_fb pointer.

[ ... ]

Quoted 8 lines
drivers/gpu/drm/solomon/ssd16xx.c new file +8 -0
+static void ssd16xx_crtc_atomic_flush(struct drm_crtc *crtc,
+				      struct drm_atomic_commit *state)
+{
+	struct ssd16xx_device *device = crtc_to_ssd16xx_device(crtc);
+	struct drm_framebuffer *fb;
+	struct drm_rect full;
+	int ret, idx;
+

drivers/gpu/drm/solomon/ssd16xx.c line 1423

Quoted 10 lines
drivers/gpu/drm/solomon/ssd16xx.c new file +10 -0
+	if (!device->initialized)
+		return;
+
+	if (!drm_dev_enter(&device->drm, &idx))
+		return;
+
+	drm_dbg(&device->drm, "atomic_flush: reinit, orientation=%u°\n",
+		device->orientation);
+
+	ret = ssd16xx_hw_init(device);

[Severity: High]
Does this perform a redundant hardware reset and full redraw on every atomic commit?

Because this is called by the DRM core on every atomic commit that affects the CRTC, its unconditional execution of ssd16xx_hw_init and ssd16xx_fb_dirty is highly destructive. plane_atomic_update already transmits the damaged regions; triggering a full hardware reset and complete framebuffer re-transmission here completely breaks damage tracking, causes display flicker, and doubles the transmission latency.

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=4
#11 lore raw

Re: [PATCH v2 04/14] drm/solomon: Add DRM driver for Solomon SSD16xx e-paper display controllers

In reply to Devarsh Thakkar

Hi

Am 27.09.26 um 20:23 schrieb Devarsh Thakkar:

Quoted 1,345 lines

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),
- 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

DRM framework integration:
- drm_atomic_helper-based modesetting with damage tracking
- Single primary plane, CRTC, simple encoder, SPI connector
- drm_gem_shmem backend for display buffers
- fbdev fallback via drm_fbdev_shmem_setup

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

---

Changes from v1:
- Converted from drm/tiny/panel-ssd16xx.c to drm/solomon/ssd16xx.c
- Reorganized code for better maintainability
- Improved hardware abstraction for multi-controller support
- Enhanced initialization sequence with better power state management
- Switch to using GEM_SHMEM helpers
- Refactored data entry mode implementation
- Removed drm properties for runtime rotation, border waveforms
- Removed module params for each of the drm properties
- Improved SPDX license header formatting
- Better separation of concerns between hardware and DRM integration

  drivers/gpu/drm/solomon/Kconfig   |   13 +
  drivers/gpu/drm/solomon/Makefile  |    1 +
  drivers/gpu/drm/solomon/ssd16xx.c | 1849 +++++++++++++++++++++++++++++
  3 files changed, 1863 insertions(+)

create mode 100644 drivers/gpu/drm/solomon/ssd16xx.c

drivers/gpu/drm/solomon/Kconfig +13 -0
index 400a6cab3a67..de2b14819f81 100644
@@ -30,3 +30,16 @@ config DRM_SSD130X_SPI
  	  Say Y here if the SSD13xx OLED display is connected via SPI bus.
 
  	  If M is selected the module will be called ssd130x-spi.
+
+config DRM_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_SHMEM_HELPER
+	help
+	  DRM driver for Solomon SSD16xx family e-paper display controllers
+	  (SSD1673, SSD1680, SSD1681, SSD1683, SSD1677). Supports panels
+	  such as the GDEY042T81 4.2" 400x300.
+
+	  If M is selected the module will be called ssd16xx.
drivers/gpu/drm/solomon/Makefile +1 -0
index b5fc792257d7..72384620785b 100644
@@ -1,3 +1,4 @@
  obj-$(CONFIG_DRM_SSD130X)	+= ssd130x.o
  obj-$(CONFIG_DRM_SSD130X_I2C)	+= ssd130x-i2c.o
  obj-$(CONFIG_DRM_SSD130X_SPI)	+= ssd130x-spi.o
+obj-$(CONFIG_DRM_SSD16XX)	+= ssd16xx.o
drivers/gpu/drm/solomon/ssd16xx.c new file +1234 -0
new file mode 100644
index 000000000000..d3af055c6739
@@ -0,0 +1,1849 @@
+// 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 <[email protected]>
+ *
+ * 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_encoder.h>
+#include <drm/drm_fb_helper.h>
+#include <drm/drm_fbdev_shmem.h>
+#include <drm/drm_framebuffer.h>
+#include <drm/drm_gem_atomic_helper.h>
+#include <drm/drm_gem_framebuffer_helper.h>
+#include <drm/drm_gem_shmem_helper.h>
+#include <drm/drm_managed.h>
+#include <drm/drm_probe_helper.h>
+#include <drm/drm_print.h>
+
+/* -----------------------------------------------------------------------
+ * 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 */
+
+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_device_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 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;
+
+	/*
+	 * Default colour mode for this panel.
+	 * For BW-only panels this must be SSD16XX_COLOR_MODE_BW.
+	 * For BWR panels this can be set to SSD16XX_COLOR_MODE_3COLOR to
+	 * enable red ink by default;
+	 */
+	enum ssd16xx_color_mode default_color_mode;
+
+	/* Panel-specific display mode (resolution and physical dimensions) */
+	const struct drm_display_mode *mode;
+};
+
+struct ssd16xx_device {
+	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_device_config *device_cfg;
+	struct drm_display_mode *mode;
+	u32 width;
+	u32 height;
+
+	bool initialized;
+	bool init_refresh_pending; /* First frame after refresh_mode_init enable */
+
+	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 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 */
+};
+
+static inline struct ssd16xx_device *to_ssd16xx_device(struct drm_device *drm)
+{
+	return container_of(drm, struct ssd16xx_device, drm);
+}
+
+static inline struct ssd16xx_device *crtc_to_ssd16xx_device(struct drm_crtc *crtc)
+{
+	return container_of(crtc, struct ssd16xx_device, crtc);
+}
+
+static inline struct ssd16xx_device *plane_to_ssd16xx_device(struct drm_plane *plane)
+{
+	return container_of(plane, struct ssd16xx_device, 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_device_config ssd16xx_device_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,
+		.default_border_waveform_update = SSD16XX_BORDER_VCOM,
+		.default_border_refresh_on_every_update = true,
+		.default_refresh_mode_init = SSD16XX_REFRESH_FULL,
+		.red_supported = false,  /* 2-colour black/white panel */
+		.default_color_mode = SSD16XX_COLOR_MODE_BW,
+		.mode = &gdey042t81_mode,
+	},
+};
+
+static void ssd16xx_wait_for_device(struct ssd16xx_device *device,
+				    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(device->busy);
+	drm_dbg(&device->drm, "BUSY initial value: %d\n", busy_val);
+
+	while (gpiod_get_value_cansleep(device->busy) == 1) {
+		if (time_after(jiffies, timeout_jiffies)) {
+			drm_err(&device->drm, "Busy wait timed out after %lums\n",
+				jiffies_to_msecs(jiffies) - start_ms);
+			*err = -ETIMEDOUT;
+			return;
+		}
+		usleep_range(1000, 2000);
+	}
+
+	drm_dbg(&device->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_device *device, 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 (device->dc) {
+		/* 4-wire SPI: D/C# GPIO low selects command mode */
+		xfer.tx_buf = &cmd;
+		xfer.len = 1;
+		gpiod_set_value_cansleep(device->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(device->spi, &msg, err);
+}
+
+static void ssd16xx_send_data(struct ssd16xx_device *device, 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 (device->dc) {
+		/* 4-wire SPI: D/C# GPIO high selects data mode */
+		xfer.tx_buf = &data;
+		xfer.len = 1;
+		gpiod_set_value_cansleep(device->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(device->spi, &msg, err);
+}
+
+static void ssd16xx_send_x_param(struct ssd16xx_device *device, u16 x,
+				 int *err)
+{
+	if (*err)
+		return;
+
+	if (device->controller_cfg->ram_x_address_bits == 8) {
+		ssd16xx_send_data(device, (u8)x, err);
+	} else {
+		ssd16xx_send_data(device, x & 0xFF, err);
+		ssd16xx_send_data(device, (x >> 8) & 0xFF, err);
+	}
+}
+
+static void ssd16xx_send_y_param(struct ssd16xx_device *device, u16 y,
+				 int *err)
+{
+	if (*err)
+		return;
+
+	if (device->controller_cfg->ram_y_address_bits == 8) {
+		ssd16xx_send_data(device, (u8)y, err);
+	} else {
+		ssd16xx_send_data(device, y & 0xFF, err);
+		ssd16xx_send_data(device, (y >> 8) & 0xFF, err);
+	}
+}
+
+static void ssd16xx_send_data_bulk(struct ssd16xx_device *device,
+				   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 (device->dc) {
+		/* 4-wire SPI: D/C# GPIO high selects data mode */
+		xfer.tx_buf = data;
+		xfer.len = len;
+		gpiod_set_value_cansleep(device->dc, 1);
+		ssd16xx_spi_sync(device->spi, &msg, err);
+	} else {
+		/* 3-wire (9-bit): expand u8 → u16 with D/C#=1 in bit 8. */
+		size_t i;
+		u16 *buf = device->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(device->spi, &msg, err);
+	}
+}
+
+static void ssd16xx_display_update(struct ssd16xx_device *device,
+				   u8 ctrl1_byte1, u8 ctrl1_byte2, u8 ctrl2_mode,
+				   int *err)
+{
+	if (*err)
+		return;
+
+	drm_dbg(&device->drm,
+		"display_update: Setting ctrl1=0x%02x,0x%02x mode=0x%02x\n",
+		ctrl1_byte1, ctrl1_byte2, ctrl2_mode);
+
+	ssd16xx_send_cmd(device, SSD16XX_CMD_DISPLAY_UPDATE_CONTROL1, err);
+	ssd16xx_send_data(device, ctrl1_byte1, err);
+	ssd16xx_send_data(device, ctrl1_byte2, err);
+
+	ssd16xx_send_cmd(device, SSD16XX_CMD_DISPLAY_UPDATE_CONTROL2, err);
+	ssd16xx_send_data(device, ctrl2_mode, err);
+	ssd16xx_send_cmd(device, SSD16XX_CMD_MASTER_ACTIVATION, err);
+
+	drm_dbg(&device->drm,
+		"display_update: Master activation sent, waiting...\n");
+
+	ssd16xx_wait_for_device(device, err);
+}
+
+static void ssd16xx_hw_reset(struct ssd16xx_device *device)
+{
+	gpiod_set_value_cansleep(device->reset, 1);
+	usleep_range(10000, 11000);
+	gpiod_set_value_cansleep(device->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_device *device)
+{
+	int err = 0;
+
+	ssd16xx_send_cmd(device, SSD16XX_CMD_DISPLAY_UPDATE_CONTROL1, &err);
+	ssd16xx_send_data(device, device->controller_cfg->ctrl1_bypass_red_ram, &err);
+	ssd16xx_send_data(device, SSD16XX_CTRL1_BYTE2_DEFAULT, &err);
+
+	ssd16xx_send_cmd(device, SSD16XX_CMD_DISPLAY_UPDATE_CONTROL2, &err);
+	ssd16xx_send_data(device, device->controller_cfg->ctrl2_load_temp_lut, &err);
+
+	ssd16xx_send_cmd(device, SSD16XX_CMD_MASTER_ACTIVATION, &err);
+	ssd16xx_wait_for_device(device, &err);
+
+	return err;
+}
+
+static int ssd16xx_hw_init(struct ssd16xx_device *device)
+{
+	int err = 0;
+	u8 data_entry_mode;
+	/*
+	 * Driver Output Control MUX ratio = (gate lines - 1).
+	 * Use the actual device height, not the controller maximum —
+	 * a smaller device must only drive its own gate lines.
+	 */
+
+	ssd16xx_hw_reset(device);
+
+	/* Software reset */
+	ssd16xx_send_cmd(device, SSD16XX_CMD_SW_RESET, &err);
+	ssd16xx_wait_for_device(device, &err);
+
+	/* Driver output control (0x01): MUX ratio and scan direction. */
+	ssd16xx_send_cmd(device, SSD16XX_CMD_DRIVER_OUTPUT_CONTROL, &err);
+	ssd16xx_send_y_param(device, device->height - 1, &err);
+	ssd16xx_send_data(device, device->device_cfg->driver_output_ctrl_byte3, &err);
+
+	/* Internal temperature sensor (SSD1683/SSD1680 only; not present in SSD1673) */
+	if (device->controller_cfg->has_temp_sensor_ctrl) {
+		ssd16xx_send_cmd(device, SSD1683_CMD_TEMPERATURE_SENSOR_CONTROL, &err);
+		ssd16xx_send_data(device, 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 (device->refresh_mode == SSD16XX_REFRESH_FAST) {
+		ssd16xx_send_cmd(device, SSD16XX_CMD_DISPLAY_UPDATE_CONTROL1, &err);
+		ssd16xx_send_data(device, device->controller_cfg->ctrl1_bypass_red_ram, &err);
+		ssd16xx_send_data(device, SSD16XX_CTRL1_BYTE2_DEFAULT, &err);
+
+		ssd16xx_send_cmd(device, SSD16XX_CMD_DISPLAY_UPDATE_CONTROL2, &err);
+		ssd16xx_send_data(device, device->controller_cfg->ctrl2_load_temp_lut, &err);
+
+		ssd16xx_send_cmd(device, SSD16XX_CMD_MASTER_ACTIVATION, &err);
+		ssd16xx_wait_for_device(device, &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 (device->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(device, SSD16XX_CMD_DATA_ENTRY_MODE, &err);
+	ssd16xx_send_data(device, data_entry_mode, &err);
+	drm_dbg(&device->drm, "hw_init: orientation=%u° data_entry=0x%02x\n",
+		device->orientation, data_entry_mode);
+
+	ssd16xx_wait_for_device(device, &err);
+
+	if (err)
+		drm_err(&device->drm, "Hardware initialization failed: %d\n", err);
+
+	return err;
+}
+
+/*
+ * ssd16xx_pixel_luma() - return ITU-R BT.601 luminance (0-255) for one pixel.
+ *
+ * Currently only XRGB8888 is supported.  The function is retained as a named
+ * helper to make it straightforward to add further formats in the future.
+ * R1 is never passed here — it is already 1bpp and handled directly by callers.
+ */
+static u8 ssd16xx_pixel_luma(struct iosys_map *src,
+			     struct drm_framebuffer *fb,
+			     unsigned int x, unsigned int y)
+{
+	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);
+}
+
+/*
+ * ssd16xx_pixel_is_white() - test whether an XRGB8888 or R1 pixel is white.
+ *
+ * For XRGB8888: ITU-R BT.601 luminance > 127 maps to white.
+ * For R1: the pixel's bit value directly encodes white (1) or black (0).
+ */
+static bool ssd16xx_pixel_is_white(struct iosys_map *src,
+				   struct drm_framebuffer *fb,
+				   unsigned int x, unsigned int y)
+{
+	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 an XRGB8888 pixel is dominated by red.
+ *
+ * Returns true when red exceeds 50% intensity and is strictly greater than
+ * both green and blue (dominant red hue). R1 carries no colour information
+ * and always returns false.
+ */
+static bool ssd16xx_pixel_is_red(struct iosys_map *src,
+				 struct drm_framebuffer *fb,
+				 unsigned int x, unsigned int y)
+{
+	u32 *line;
+	u32 px;
+	u8 r, g, b;
+
+	if (fb->format->format != DRM_FORMAT_XRGB8888)
+		return false;
+
+	line = (u32 *)(src->vaddr + y * fb->pitches[0]);
+	px = line[x];
+	r = (px >> 16) & 0xFF;
+	g = (px >> 8) & 0xFF;
+	b = px & 0xFF;
+
+	return r > 127 && r > g && r > b;
+}
+
+/*
+ * 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)
+ *
+ * Supports XRGB8888 and R1 formats.
+ *
+ * XRGB8888: pixels with a dominant red channel (r > 127 && r > g && r > b)
+ * map to red ink; remaining pixels threshold to white/black via BT.601 luma.
+ *
+ * R1 on a 3-color panel: the 1-bits are interpreted as red ink on a white
+ * background.  BW RAM is set to all-white and RED RAM receives the R1 data
+ * directly (1=red, 0=no red).  This matches the behaviour of a user who
+ * explicitly selects color_mode=3-color and submits a 1-bit mask to place
+ * red ink.
+ */
+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;
+
+	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);
+
+		/* White background — no black pixels, only red ink shows */
+		memset(bw_dst, 0xFF, data_size);
+		/* RED RAM: copy R1 data directly (1=red ink, 0=no red) */
+		for (y = rect->y1; y < rect->y2; y++) {
+			u8 *line = src->vaddr + y * src_pitch + (rect->x1 / 8);
+
+			memcpy(red_dst + dst_idx, line, width_bytes);
+			dst_idx += width_bytes;
+		}
+		return;
+	}
+
+	/* XRGB8888 */
+	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));
+			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;
+			}
+		}
+		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;
+		}
+	}
+}
+
+/*
+ * Convert framebuffer to 1-bit monochrome for e-paper display.
+ *
+ * Supports XRGB8888 (thresholded via ITU-R BT.601 luma at 127) and R1
+ * (native 1bpp, zero-copy fast path for aligned landscape frames).
+ *
+ * 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_device *device,
+			    const struct iosys_map *src_map)
+{
+	const u8 *ctrl2_tbl = device->controller_cfg->ctrl2_refresh;
+	struct iosys_map map = *src_map;
+	int err = 0;
+	unsigned int data_size = (device->width * device->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.  The rect for convert_fb_to_1bpp uses
+	 * the framebuffer (logical) dimensions - the pixel iteration walks
+	 * the source fb coordinate space. RAM window registers below use
+	 * the physical panel dimensions (device->width/height).
+	 */
+	rect->x1 = 0;
+	rect->y1 = 0;
+	rect->x2 = fb->width;
+	rect->y2 = fb->height;
+
+	drm_dbg(&device->drm,
+		"fb_dirty: fb=%dx%d, refresh_mode=%d, orientation=%d\n",
+		fb->width, fb->height, device->refresh_mode, device->orientation);
+
+	mono_buffer = device->tx_buf;
+	memset(mono_buffer, 0, data_size);
+
+	/* 3-colour FULL/FAST: populate red channel. */
+	if (device->color_mode == SSD16XX_COLOR_MODE_3COLOR &&
+	    (device->refresh_mode == SSD16XX_REFRESH_FULL ||
+	     device->refresh_mode == SSD16XX_REFRESH_FAST)) {
+		red_buffer = device->tx_red_buf;
+		memset(red_buffer, 0, data_size);
+	}
+
+	/*
+	 * R1 format interpretation depends on color_mode:
+	 *
+	 *   color_mode = 3-color (red_buffer != NULL):
+	 *     R1 bits are treated as red ink — 1 = red pixel on a white
+	 *     background, 0 = no red.  This allows applications to submit a
+	 *     1-bit mask to place red ink on a BWR panel.
+	 *     Handled by the R1 path in ssd16xx_convert_fb_to_3color().
+	 *
+	 *   color_mode = BW (red_buffer == NULL):
+	 *     R1 bits are treated as luma — 1 = white, 0 = black.
+	 *     Handled by the native fast path in ssd16xx_convert_fb_to_1bpp().
+	 */
+	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, device->orientation);
+
+	drm_dbg(&device->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;
+	/*
+	 * X end depends on the controller's addressing model:
+	 *   Byte-addressed (ram_x_address_bits == 8, e.g. SSD1683):
+	 *     XEnd = device_width/8 - 1  (byte offset into RAM row)
+	 *   Pixel-addressed (ram_x_address_bits != 8, e.g. SSD1677):
+	 *     XEnd = device_width - 1    (direct pixel index)
+	 * The model is controller-specific; the value is device-specific.
+	 */
+	if (device->controller_cfg->ram_x_address_bits == 8)
+		ram_x_end = (device->width / 8) - 1;
+	else
+		ram_x_end = device->width - 1;
+	ram_y_start = 0;
+	ram_y_end = device->height - 1;
+
+	switch (device->orientation) {
+	case 90:
+	case 180:
+		/* 90°/180°: XDEC_YDEC mode, send end-before-start; cursor at (max, max). */
+		ssd16xx_send_cmd(device, SSD16XX_CMD_SET_RAM_X_ADDRESS_START_END, &err);
+		ssd16xx_send_x_param(device, ram_x_end, &err);
+		ssd16xx_send_x_param(device, ram_x_start, &err);
+
+		ssd16xx_send_cmd(device, SSD16XX_CMD_SET_RAM_Y_ADDRESS_START_END, &err);
+		ssd16xx_send_y_param(device, ram_y_end, &err);
+		ssd16xx_send_y_param(device, ram_y_start, &err);
+
+		ssd16xx_send_cmd(device, SSD16XX_CMD_SET_RAM_X_ADDRESS_COUNTER, &err);
+		ssd16xx_send_x_param(device, ram_x_end, &err);
+
+		ssd16xx_send_cmd(device, SSD16XX_CMD_SET_RAM_Y_ADDRESS_COUNTER, &err);
+		ssd16xx_send_y_param(device, ram_y_end, &err);
+		break;
+
+	default: /* 0°/270° */
+		/* 0°/270°: XINC_YINC mode, cursor at (0, 0). */
+		ssd16xx_send_cmd(device, SSD16XX_CMD_SET_RAM_X_ADDRESS_START_END, &err);
+		ssd16xx_send_x_param(device, ram_x_start, &err);
+		ssd16xx_send_x_param(device, ram_x_end, &err);
+
+		ssd16xx_send_cmd(device, SSD16XX_CMD_SET_RAM_Y_ADDRESS_START_END, &err);
+		ssd16xx_send_y_param(device, ram_y_start, &err);
+		ssd16xx_send_y_param(device, ram_y_end, &err);
+
+		ssd16xx_send_cmd(device, SSD16XX_CMD_SET_RAM_X_ADDRESS_COUNTER, &err);
+		ssd16xx_send_x_param(device, ram_x_start, &err);
+
+		ssd16xx_send_cmd(device, SSD16XX_CMD_SET_RAM_Y_ADDRESS_COUNTER, &err);
+		ssd16xx_send_y_param(device, ram_y_start, &err);
+		break;
+	}
+
+	ssd16xx_send_cmd(device, SSD16XX_CMD_WRITE_RAM_BW, &err);
+	ssd16xx_send_data_bulk(device, mono_buffer, data_size, &err);
+
+	/* Re-send border waveform when: every-update mode, init frame
+	 */
+	drm_dbg(&device->drm,
+		"fb_dirty: border check: every_update=%d init_pending=%d border_pending=%d idx=%d hw=0x%02x\n",
+		device->border_refresh_on_every_update, device->init_refresh_pending,
+		device->border_waveform_pending, device->border_waveform_update_idx,
+		device->controller_cfg->border_waveform_table[device->border_waveform_update_idx]);
+	if (device->border_refresh_on_every_update || device->init_refresh_pending ||
+	    device->border_waveform_pending) {
+		u8 idx = device->border_waveform_update_idx;
+		u8 border = device->controller_cfg->border_waveform_table[idx];
+
+		drm_dbg(&device->drm, "fb_dirty: Sending border waveform: 0x%02x\n",
+			border);
+		ssd16xx_send_cmd(device, SSD16XX_CMD_BORDER_WAVEFORM_CONTROL, &err);
+		ssd16xx_send_data(device, border, &err);
+		device->border_waveform_pending = false;
+	}
+
+	switch (device->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(device, SSD1683_CMD_WRITE_RAM_RED, &err);
+		if (red_buffer) {
+			/* 3-colour: write red channel before activating */
+			ssd16xx_send_data_bulk(device, red_buffer, data_size, &err);
+			ssd16xx_display_update(device, device->controller_cfg->ctrl1_normal,
+					       SSD16XX_CTRL1_BYTE2_DEFAULT,
+					       ctrl2_tbl[SSD16XX_REFRESH_FULL], &err);
+		} else {
+			ssd16xx_send_data_bulk(device, mono_buffer, data_size, &err);
+			ssd16xx_display_update(device, device->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(device, SSD1683_CMD_WRITE_RAM_RED, &err);
+		if (red_buffer) {
+			/* 3-colour: write red channel before activating */
+			ssd16xx_send_data_bulk(device, red_buffer, data_size, &err);
+			ssd16xx_display_update(device, device->controller_cfg->ctrl1_normal,
+					       SSD16XX_CTRL1_BYTE2_DEFAULT,
+					       ctrl2_tbl[SSD16XX_REFRESH_FAST], &err);
+		} else {
+			ssd16xx_send_data_bulk(device, mono_buffer, data_size, &err);
+			ssd16xx_display_update(device, device->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(&device->drm,
+			"fb_dirty: partial pre-update: mono[0]=0x%02x (BW=new, RED=prev)\n",
+			mono_buffer[0]);
+		ssd16xx_display_update(device, device->controller_cfg->ctrl1_normal,
+				       SSD16XX_CTRL1_BYTE2_DEFAULT,
+				       ctrl2_tbl[SSD16XX_REFRESH_PARTIAL], &err);
+		ssd16xx_send_cmd(device, SSD1683_CMD_WRITE_RAM_RED, &err);
+		ssd16xx_send_data_bulk(device, mono_buffer, data_size, &err);
+		drm_dbg(&device->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 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,
+	DRM_GEM_SHADOW_PLANE_FUNCS,
+};
+
+static int ssd16xx_plane_atomic_check(struct drm_plane *plane,
+				      struct drm_atomic_commit *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_commit *state)
+{

drivers/gpu/drm/solomon/ssd16xx.c line 1244

Quoted 10 lines
drivers/gpu/drm/solomon/ssd16xx.c new file +10 -0
+	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 drm_shadow_plane_state *shadow_state = to_drm_shadow_plane_state(new_state);
+	struct ssd16xx_device *device = plane_to_ssd16xx_device(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;
+

The atomic_update function is missing drm_dev_enter() and drm_dev exit().

Your driver also needs lock the gem buffer with drm_gem_fb_begin_cpu_access() and drm_gem_fb_end_cpu_access() whenever you read its data. See [1] for an example. Otherwise a concurrent exporter could write into the memory.

[1] https://elixir.bootlin.com/linux/v7.2.8/source/drivers/gpu/drm/ast/ast_cursor.c#L198

drivers/gpu/drm/solomon/ssd16xx.c line 1249

Quoted 5 lines
drivers/gpu/drm/solomon/ssd16xx.c new file +5 -0
+	drm_dbg(&device->drm, "plane_atomic_update: fb=%p, initialized=%d\n",
+		fb, device->initialized);
+
+	if (!fb || !device->initialized)
+		return;

Your driver should have initialized HW long before it comes here.

drivers/gpu/drm/solomon/ssd16xx.c line 1257

Quoted 8 lines
drivers/gpu/drm/solomon/ssd16xx.c new file +8 -0
+
+	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(&device->drm, "plane_atomic_update: no damage, using full screen\n");
+	}

The correct pattern is:

  if (damage_merged()) {
    // do screen update.
  }

your driver is not allowed to setup its own rectangles and update those instead.

Quoted 56 lines
drivers/gpu/drm/solomon/ssd16xx.c new file +56 -0
+
+	drm_dbg(&device->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 = device->refresh_mode;
+	saved_border_waveform_idx = device->border_waveform_update_idx;
+	if (device->init_refresh_pending) {
+		device->refresh_mode = device->refresh_mode_init;
+		device->border_waveform_update_idx = device->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 (device->fast_lut_pending) {
+		ret = ssd16xx_preload_fast_lut(device);
+		if (ret) {
+			drm_err(&device->drm,
+				"plane_atomic_update: fast LUT preload failed: %d\n", ret);
+		}
+
+		device->fast_lut_pending = false;
+	}
+
+	ret = ssd16xx_fb_dirty(fb, &rect, device, &shadow_state->data[0]);
+	if (ret)
+		drm_err(&device->drm, "plane_atomic_update: display update failed: %d\n", ret);
+	else
+		device->last_fb = fb;
+
+	device->refresh_mode = saved_mode;
+	device->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 (device->init_refresh_pending) {
+		device->init_refresh_pending = false;
+		device->border_waveform_pending = true;
+	}
+}
+
+static const struct drm_plane_helper_funcs ssd16xx_plane_helper_funcs = {
+	DRM_GEM_SHADOW_PLANE_HELPER_FUNCS,
+	.atomic_check = ssd16xx_plane_atomic_check,
+	.atomic_update = ssd16xx_plane_atomic_update,

drivers/gpu/drm/solomon/ssd16xx.c line 1323

Quoted 10 lines
drivers/gpu/drm/solomon/ssd16xx.c new file +10 -0
+};
+
+/* -----------------------------------------------------------------------------
+ * CRTC Functions
+ */
+
+static void ssd16xx_crtc_destroy(struct drm_crtc *crtc)
+{
+	drm_crtc_cleanup(crtc);
+}

No need for this wrapper AFAICT.

Quoted 19 lines
drivers/gpu/drm/solomon/ssd16xx.c new file +19 -0
+
+static const struct drm_crtc_funcs ssd16xx_crtc_funcs = {
+	.atomic_create_state = drm_atomic_helper_crtc_create_state,
+	.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_device *device = crtc_to_ssd16xx_device(crtc);
+
+	/* Accept only our device's native mode (landscape or portrait) */
+	if ((mode->hdisplay == device->mode->hdisplay &&
+	     mode->vdisplay == device->mode->vdisplay) ||
+	    (mode->hdisplay == device->mode->vdisplay &&

drivers/gpu/drm/solomon/ssd16xx.c line 1352

Quoted 10 lines
drivers/gpu/drm/solomon/ssd16xx.c new file +10 -0
+	     mode->vdisplay == device->mode->hdisplay))
+		return MODE_OK;
+
+	return MODE_BAD;
+}
+
+static int ssd16xx_crtc_atomic_check(struct drm_crtc *crtc,
+				     struct drm_atomic_commit *state)
+{
+	return 0;

Since the CRTC and primary plane need to move in sync, here's a good place to test that with drm_atomic_helper_check_crtc_primary_plane(). See [2] for an example.

[2] https://elixir.bootlin.com/linux/v7.2.8/source/drivers/gpu/drm/sysfb/drm_sysfb_modeset.c#L496

drivers/gpu/drm/solomon/ssd16xx.c line 1365

Quoted 13 lines
drivers/gpu/drm/solomon/ssd16xx.c new file +13 -0
+}
+
+static void ssd16xx_crtc_atomic_disable(struct drm_crtc *crtc,
+					struct drm_atomic_commit *state)
+{
+	struct ssd16xx_device *device = crtc_to_ssd16xx_device(crtc);
+	int idx;
+
+	if (!drm_dev_enter(&device->drm, &idx))
+		return;
+
+	drm_dev_exit(idx);
+}

No need for this helper AFAICT.

Quoted 9 lines
drivers/gpu/drm/solomon/ssd16xx.c new file +9 -0
+
+static void ssd16xx_crtc_atomic_enable(struct drm_crtc *crtc,
+				       struct drm_atomic_commit *state)
+{
+	struct ssd16xx_device *device = crtc_to_ssd16xx_device(crtc);
+	int ret, idx;
+
+	if (!drm_dev_enter(&device->drm, &idx))
+		return;

drivers/gpu/drm/solomon/ssd16xx.c line 1384

Quoted 10 lines
drivers/gpu/drm/solomon/ssd16xx.c new file +10 -0
+
+	drm_dbg(&device->drm, "atomic_enable: %dx%d orientation=%u°\n",
+		device->width, device->height, device->orientation);
+
+	ret = ssd16xx_hw_init(device);
+	if (ret) {
+		drm_err(&device->drm, "crtc_atomic_enable: HW init failed: %d\n", ret);
+		goto out;
+	}
+	device->initialized = true;

This is also problematic. The driver should have initialized the device hardware when probing it.  If this fails, probe need to fail with an error.  There should then not be a modesetting pipeline at all.

Quoted 33 lines
drivers/gpu/drm/solomon/ssd16xx.c new file +33 -0
+
+	/*
+	 * 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 device default refresh_mode.
+	 */
+	if (device->refresh_mode_init >= 0) {
+		drm_dbg(&device->drm,
+			"atomic_enable: refresh_mode_init=%d, using for first frame\n",
+			device->refresh_mode_init);
+		device->init_refresh_pending = true;
+	}
+
+out:
+	drm_dev_exit(idx);
+}
+
+/*
+ * Re-initialize hardware and redraw the current framebuffer when the
+ * display orientation changes at runtime
+ */
+static void ssd16xx_crtc_atomic_flush(struct drm_crtc *crtc,
+				      struct drm_atomic_commit *state)
+{
+	struct ssd16xx_device *device = crtc_to_ssd16xx_device(crtc);
+	struct drm_framebuffer *fb;
+	struct drm_rect full;
+	int ret, idx;
+
+	if (!device->initialized)
+		return;
+
+	if (!drm_dev_enter(&device->drm, &idx))

drivers/gpu/drm/solomon/ssd16xx.c line 1427

Quoted 10 lines
drivers/gpu/drm/solomon/ssd16xx.c new file +10 -0
+		return;
+
+	drm_dbg(&device->drm, "atomic_flush: reinit, orientation=%u°\n",
+		device->orientation);
+
+	ret = ssd16xx_hw_init(device);
+	if (ret) {
+		drm_err(&device->drm, "Orientation re-init failed: %d\n", ret);
+		goto out;
+	}

Another one of those HW inits. IIRC there are many more of them.

Quoted 7 lines
drivers/gpu/drm/solomon/ssd16xx.c new file +7 -0
+
+	fb = device->primary_plane.state ? device->primary_plane.state->fb
+					: device->last_fb;
+	if (fb) {
+		struct drm_gem_object *obj = drm_gem_fb_get_obj(fb, 0);
+		struct iosys_map map;
+

drivers/gpu/drm/solomon/ssd16xx.c line 1444

Quoted 10 lines
drivers/gpu/drm/solomon/ssd16xx.c new file +10 -0
+		full.x1 = 0;
+		full.y1 = 0;
+		full.x2 = fb->width;
+		full.y2 = fb->height;
+
+		ret = drm_gem_vmap(obj, &map);
+		if (!ret) {
+			ret = ssd16xx_fb_dirty(fb, &full, device, &map);
+			drm_gem_vunmap(obj, &map);
+		}

Screen pixel updates belong in the plane's atomic_update.

Quoted 405 lines
drivers/gpu/drm/solomon/ssd16xx.c new file +405 -0
+		if (ret)
+			drm_err(&device->drm, "atomic_flush: display update failed: %d\n", ret);
+		else
+			device->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_device *device = to_ssd16xx_device(connector->dev);
+	bool mode_is_portrait = (device->mode->hdisplay < device->mode->vdisplay);
+	bool orient_is_portrait = (device->orientation == 90 || device->orientation == 270);
+
+	drm_dbg(&device->drm,
+		"connector_get_modes: orientation=%u° mode=%ux%u mode_portrait=%d orient_portrait=%d\n",
+		device->orientation, device->mode->hdisplay, device->mode->vdisplay,
+		mode_is_portrait, orient_is_portrait);
+
+	/* 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(&device->drm, device->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, device->mode);
+}
+
+static const struct drm_connector_helper_funcs ssd16xx_connector_helper_funcs = {
+	.get_modes = ssd16xx_connector_get_modes,
+};
+
+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,
+};
+
+static const u32 ssd16xx_formats[] = {
+	DRM_FORMAT_XRGB8888,  /* 32-bit RGB with padding (preferred) */
+	DRM_FORMAT_R1,        /* 1-bit monochrome (native, zero-copy path) */
+};
+
+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_device *device = to_ssd16xx_device(drm);
+
+	if (device->refresh_mode_init >= 0)
+		device->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_device *device = to_ssd16xx_device(drm);
+
+	device->init_refresh_pending = false;
+}
+
+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_SHMEM_DRIVER_OPS,
+	DRM_FBDEV_SHMEM_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_device *device)
+{
+	/*
+	 * Allocate for the actual physical panel size (width × height are
+	 * always the physical dimensions, never swapped for orientation).
+	 */
+	size_t frame_size = DIV_ROUND_UP(device->width * device->height, 8);
+
+	device->tx_buf = drmm_kmalloc(&device->drm, frame_size, GFP_KERNEL);
+	if (!device->tx_buf)
+		return -ENOMEM;
+
+	if (device->device_cfg->red_supported) {
+		device->tx_red_buf = drmm_kmalloc(&device->drm, frame_size, GFP_KERNEL);
+		if (!device->tx_red_buf)
+			return -ENOMEM;
+	}
+
+	if (!device->dc) {
+		device->tx_buf9 = drmm_kmalloc_array(&device->drm, frame_size,
+						     sizeof(u16), GFP_KERNEL);
+		if (!device->tx_buf9)
+			return -ENOMEM;
+	}
+
+	return 0;
+}
+
+static int ssd16xx_probe(struct spi_device *spi)
+{
+	struct device *dev = &spi->dev;
+	struct ssd16xx_device *device;
+	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;
+	}
+
+	device = devm_drm_dev_alloc(dev, &ssd16xx_drm_driver,
+				    struct ssd16xx_device, drm);
+	if (IS_ERR(device))
+		return PTR_ERR(device);
+
+	drm = &device->drm;
+	device->spi = spi;
+	device->model = model;
+	spi_set_drvdata(spi, device);
+
+	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:
+		device->controller = SSD1683;
+		break;
+	default:
+		drm_err(drm, "Unknown panel model: %d\n", model);
+		return -EINVAL;
+	}
+
+	if (device->controller >= ARRAY_SIZE(ssd16xx_controller_configs) ||
+	    !ssd16xx_controller_configs[device->controller].max_width)
+		return -EINVAL;
+	device->controller_cfg = &ssd16xx_controller_configs[device->controller];
+
+	if (model >= ARRAY_SIZE(ssd16xx_device_configs))
+		return -EINVAL;
+	device->device_cfg = &ssd16xx_device_configs[model];
+
+	mode = devm_kmemdup(dev, device->device_cfg->mode,
+			    sizeof(*device->device_cfg->mode), GFP_KERNEL);
+	if (!mode)
+		return -ENOMEM;
+
+	device->refresh_mode = device->device_cfg->default_refresh_mode;
+	device->color_mode   = device->device_cfg->default_color_mode;
+	device->border_waveform_init_idx   = device->device_cfg->default_border_waveform_init;
+	device->border_waveform_update_idx = device->device_cfg->default_border_waveform_update;
+	device->border_refresh_on_every_update =
+		device->device_cfg->default_border_refresh_on_every_update;
+	device->refresh_mode_init = device->device_cfg->default_refresh_mode_init;
+
+	/* 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;
+	}
+	device->orientation = dt_rotation;
+
+	device->width = mode->hdisplay;
+	device->height = mode->vdisplay;
+
+	drm_dbg(drm, "Using %s orientation (%u°, physical %ux%u)\n",
+		(device->orientation == 90 || device->orientation == 270) ?
+		 "portrait" : "landscape", device->orientation, device->width, device->height);
+
+	/* Swap mode dimensions for portrait so clients see logical size. */
+	if (device->orientation == 90 || device->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);
+	}
+	device->mode = mode;
+
+	/* Validate panel dimensions against controller hardware limits. */
+	if (device->width > device->controller_cfg->max_width ||
+	    device->height > device->controller_cfg->max_height) {
+		drm_err(drm, "panel %ux%u exceeds controller max %ux%u\n",
+			device->width, device->height,
+			device->controller_cfg->max_width,
+			device->controller_cfg->max_height);
+		return -EINVAL;
+	}
+
+	/*
+	 * For byte-addressed X (SSD1683, ram_x_address_bits == 8), each X
+	 * address covers 8 pixels.  The panel width must be a multiple of 8
+	 * so that (width/8 - 1) gives the correct byte-aligned window end.
+	 */
+	if (device->controller_cfg->ram_x_address_bits == 8 &&
+	    device->width % 8 != 0) {
+		drm_err(drm, "panel width %u not a multiple of 8 required for byte-addressed X controller)\n",
+			device->width);
+		return -EINVAL;
+	}
+
+	/* Acquire GPIOs. */
+	device->reset = devm_gpiod_get(dev, "reset", GPIOD_OUT_HIGH);
+	if (IS_ERR(device->reset))
+		return dev_err_probe(dev, PTR_ERR(device->reset), "Failed to get RESET GPIO\n");
+
+	device->busy = devm_gpiod_get(dev, "busy", GPIOD_IN);
+	if (IS_ERR(device->busy))
+		return dev_err_probe(dev, PTR_ERR(device->busy), "Failed to get BUSY GPIO\n");
+
+	device->dc = devm_gpiod_get_optional(dev, "dc", GPIOD_OUT_LOW);
+	if (IS_ERR(device->dc))
+		return dev_err_probe(dev, PTR_ERR(device->dc), "Failed to get DC GPIO\n");
+	if (!device->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(device);
+	if (ret)
+		return ret;
+
+	ssd16xx_hw_reset(device);
+
+	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(device->width, device->height);
+	drm->mode_config.max_width = max(device->width, device->height);
+	drm->mode_config.min_height = min(device->width, device->height);
+	drm->mode_config.max_height = max(device->width, device->height);
+
+	drm_connector_helper_add(&device->connector, &ssd16xx_connector_helper_funcs);
+	ret = drm_connector_init(drm, &device->connector, &ssd16xx_connector_funcs,
+				 DRM_MODE_CONNECTOR_SPI);
+	if (ret)
+		return ret;
+
+	ret = drm_universal_plane_init(drm, &device->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(&device->primary_plane, &ssd16xx_plane_helper_funcs);
+	drm_plane_enable_fb_damage_clips(&device->primary_plane);
+
+	ret = drm_crtc_init_with_planes(drm, &device->crtc, &device->primary_plane,
+					NULL, &ssd16xx_crtc_funcs, NULL);
+	if (ret)
+		return ret;
+	drm_crtc_helper_add(&device->crtc, &ssd16xx_crtc_helper_funcs);
+
+	ret = drmm_encoder_init(drm, &device->encoder, NULL, DRM_MODE_ENCODER_NONE, NULL);
+	if (ret)
+		return ret;
+	device->encoder.possible_crtcs = drm_crtc_mask(&device->crtc);
+
+	ret = drm_connector_attach_encoder(&device->connector, &device->encoder);
+	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",
+		device->width, device->height, device->orientation);
+
+	drm_client_setup(drm, NULL);
+
+	return 0;
+}
+
+static void ssd16xx_remove(struct spi_device *spi)
+{
+	struct ssd16xx_device *device = spi_get_drvdata(spi);
+
+	drm_dev_unplug(&device->drm);
+	drm_atomic_helper_shutdown(&device->drm);
+}
+
+static void ssd16xx_shutdown(struct spi_device *spi)
+{
+	struct ssd16xx_device *device = spi_get_drvdata(spi);
+
+	drm_atomic_helper_shutdown(&device->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 <[email protected]>");
+MODULE_DESCRIPTION("DRM driver for Solomon SSD16xx e-paper display controller family");
+MODULE_LICENSE("GPL");
Signature · 6 lines
--
Thomas Zimmermann
Graphics Driver Developer
SUSE Software Solutions Germany GmbH
Frankenstr. 146, 90461 Nürnberg, Germany, www.suse.com
GF: Stefan Gaiser, Jochen Jaser, Abhinav Puri, (HRB 36809, AG Nürnberg)
#12 lore raw

Re: [PATCH v2 04/14] drm/solomon: Add DRM driver for Solomon SSD16xx e-paper display controllers

Hi Thomas,

Thanks for the review.

Quoted 1,512 lines · Thomas Zimmermann

Hi

Am 27.09.26 um 20:23 schrieb Devarsh Thakkar:

Quoted 1,498 lines

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),
   - 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

DRM framework integration:
   - drm_atomic_helper-based modesetting with damage tracking
   - Single primary plane, CRTC, simple encoder, SPI connector
   - drm_gem_shmem backend for display buffers
   - fbdev fallback via drm_fbdev_shmem_setup

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

---

Changes from v1:
   - Converted from drm/tiny/panel-ssd16xx.c to drm/solomon/ssd16xx.c
   - Reorganized code for better maintainability
   - Improved hardware abstraction for multi-controller support
   - Enhanced initialization sequence with better power state management
   - Switch to using GEM_SHMEM helpers
   - Refactored data entry mode implementation
   - Removed drm properties for runtime rotation, border waveforms
   - Removed module params for each of the drm properties
   - Improved SPDX license header formatting
   - Better separation of concerns between hardware and DRM integration

  drivers/gpu/drm/solomon/Kconfig   |   13 + drivers/gpu/drm/solomon/Makefile  |    1 + drivers/gpu/drm/solomon/ssd16xx.c | 1849 +++++++++++++++++++++++++++++

  3 files changed, 1863 insertions(+)

  create mode 100644 drivers/gpu/drm/solomon/ssd16xx.c

drivers/gpu/drm/solomon/Kconfig +0 -0
index 400a6cab3a67..de2b14819f81 100644
@@ -30,3 +30,16 @@ config DRM_SSD130X_SPI

        Say Y here if the SSD13xx OLED display is connected via SPI bus. If M is selected the module will be called ssd130x-spi.

drivers/gpu/drm/solomon/Kconfig +13 -0
+
+config DRM_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_SHMEM_HELPER
+    help
+      DRM driver for Solomon SSD16xx family e-paper display controllers
+      (SSD1673, SSD1680, SSD1681, SSD1683, SSD1677). Supports panels
+      such as the GDEY042T81 4.2" 400x300.
+
+      If M is selected the module will be called ssd16xx.
drivers/gpu/drm/solomon/Makefile +0 -0
index b5fc792257d7..72384620785b 100644
@@ -1,3 +1,4 @@

  obj-$(CONFIG_DRM_SSD130X)    += ssd130x.o obj-$(CONFIG_DRM_SSD130X_I2C)    += ssd130x-i2c.o obj-$(CONFIG_DRM_SSD130X_SPI)    += ssd130x-spi.o

drivers/gpu/drm/solomon/Makefile +1 -0
+obj-$(CONFIG_DRM_SSD16XX)    += ssd16xx.o
drivers/gpu/drm/solomon/ssd16xx.c new file +3 -0
new file mode 100644
index 000000000000..d3af055c6739
@@ -0,0 +1,1849 @@
+// 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 <[email protected]>
+ *
+ * References: https://github.com/Lesords/epaper
+ */

drivers/gpu/drm/solomon/ssd16xx.c new file +47 -2
+
+#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_encoder.h>
+#include <drm/drm_fb_helper.h>
+#include <drm/drm_fbdev_shmem.h>
+#include <drm/drm_framebuffer.h>
+#include <drm/drm_gem_atomic_helper.h>
+#include <drm/drm_gem_framebuffer_helper.h>
+#include <drm/drm_gem_shmem_helper.h>
+#include <drm/drm_managed.h>
+#include <drm/drm_probe_helper.h>
+#include <drm/drm_print.h>
+
+/* 
-----------------------------------------------------------------------
+ * 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) */

drivers/gpu/drm/solomon/ssd16xx.c new file +2 -0
+
+/* POR reset value: GD=0 (G0 first), SM=0 (interlaced), TB=0 (G0- 
Quoted 1 line

G299) */

drivers/gpu/drm/solomon/ssd16xx.c new file +22 -0
+#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 */

drivers/gpu/drm/solomon/ssd16xx.c new file +4 -1
+#define SSD16XX_BUSY_WAIT_TIMEOUT_MS        6000
+
+/* 
-----------------------------------------------------------------------
+ * SSD1683 / SSD1680 specific: commands, data values, and bit 

definitions.
+ *
-----------------------------------------------------------------------
+ */

drivers/gpu/drm/solomon/ssd16xx.c new file +6 -0
+
+/*
+ * 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) */

drivers/gpu/drm/solomon/ssd16xx.c new file +6 -0
+
+/*
+ * Temperature Sensor Selection (command 0x18).
+ */
+#define SSD1683_CMD_TEMPERATURE_SENSOR_CONTROL        0x18
+#define SSD1683_TEMP_SENSOR_INTERNAL            0x80  /* Bit 7: use 

internal sensor */

drivers/gpu/drm/solomon/ssd16xx.c new file +10 -0
+
+/*
+ * 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) */

drivers/gpu/drm/solomon/ssd16xx.c new file +5 -0
+
+/*
+ * 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) */

drivers/gpu/drm/solomon/ssd16xx.c new file +3 -0
+
+/*
+ * Display Update Control 2 (0x22) BIT(3) — "Display Mode 

2" (partial/BW).
+ */

drivers/gpu/drm/solomon/ssd16xx.c new file +25 -0
+#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 */

drivers/gpu/drm/solomon/ssd16xx.c new file +3 -0
+
+/*
+ * 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.
+ */

drivers/gpu/drm/solomon/ssd16xx.c new file +17 -0
+#define SSD1683_CTRL2_LOAD_TEMP_LUT        (SSD16XX_CTRL2_ENABLE_CLK | \
+                         SSD16XX_CTRL2_LOAD_LUT | \
+                         SSD16XX_CTRL2_LOAD_TEMPERATURE | \
+                         SSD16XX_CTRL2_DISABLE_CLK)  /* 0xB1 */
+
+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) */

drivers/gpu/drm/solomon/ssd16xx.c new file +4 -0
+};
+
+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 */

drivers/gpu/drm/solomon/ssd16xx.c new file +40 -0
+};
+
+/* 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) */

drivers/gpu/drm/solomon/ssd16xx.c new file +4 -0
+};
+
+struct ssd16xx_device_config {
+    /* Data Entry Mode - controls X/Y increment direction for 

landscape (0°) */
+    u8 data_entry_mode;

drivers/gpu/drm/solomon/ssd16xx.c new file +11 -0
+
+    /* 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 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;

drivers/gpu/drm/solomon/ssd16xx.c new file +10 -0
+
+    /*
+     * Default colour mode for this panel.
+     * For BW-only panels this must be SSD16XX_COLOR_MODE_BW.
+     * For BWR panels this can be set to SSD16XX_COLOR_MODE_3COLOR to
+     * enable red ink by default;
+     */
+    enum ssd16xx_color_mode default_color_mode;
+
+    /* Panel-specific display mode (resolution and physical 

dimensions) */
+    const struct drm_display_mode *mode;

drivers/gpu/drm/solomon/ssd16xx.c new file +25 -0
+};
+
+struct ssd16xx_device {
+    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_device_config *device_cfg;
+    struct drm_display_mode *mode;
+    u32 width;
+    u32 height;
+
+    bool initialized;
+    bool init_refresh_pending; /* First frame after refresh_mode_init 

enable */
+
+    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 refresh_mode_init; /* -1=disabled, else use this mode for the first frame */

drivers/gpu/drm/solomon/ssd16xx.c new file +6 -0
+
+    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 */

drivers/gpu/drm/solomon/ssd16xx.c new file +3 -0
+};
+
+static inline struct ssd16xx_device *to_ssd16xx_device(struct 

drm_device *drm)

drivers/gpu/drm/solomon/ssd16xx.c new file +5 -0
+{
+    return container_of(drm, struct ssd16xx_device, drm);
+}
+
+static inline struct ssd16xx_device *crtc_to_ssd16xx_device(struct 

drm_crtc *crtc)

drivers/gpu/drm/solomon/ssd16xx.c new file +5 -0
+{
+    return container_of(crtc, struct ssd16xx_device, crtc);
+}
+
+static inline struct ssd16xx_device *plane_to_ssd16xx_device(struct 

drm_plane *plane)

drivers/gpu/drm/solomon/ssd16xx.c new file +5 -0
+{
+    return container_of(plane, struct ssd16xx_device, 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, +    },

drivers/gpu/drm/solomon/ssd16xx.c new file +51 -0
+};
+
+/* 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_device_config ssd16xx_device_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,
+        .default_border_waveform_update = SSD16XX_BORDER_VCOM,
+        .default_border_refresh_on_every_update = true,
+        .default_refresh_mode_init = SSD16XX_REFRESH_FULL,
+        .red_supported = false,  /* 2-colour black/white panel */
+        .default_color_mode = SSD16XX_COLOR_MODE_BW,
+        .mode = &gdey042t81_mode,
+    },
+};
+
+static void ssd16xx_wait_for_device(struct ssd16xx_device *device,
+                    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(device->busy);
+    drm_dbg(&device->drm, "BUSY initial value: %d\n", busy_val);
+
+    while (gpiod_get_value_cansleep(device->busy) == 1) {
+        if (time_after(jiffies, timeout_jiffies)) {
+            drm_err(&device->drm, "Busy wait timed out after %lums\n",
+                jiffies_to_msecs(jiffies) - start_ms);
+            *err = -ETIMEDOUT;
+            return;
+        }
+        usleep_range(1000, 2000);
+    }
+
+    drm_dbg(&device->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)

drivers/gpu/drm/solomon/ssd16xx.c new file +175 -0
+{
+    int ret;
+
+    if (*err)
+        return;
+
+    ret = spi_sync(spi, msg);
+    if (ret < 0)
+        *err = ret;
+}
+
+static void ssd16xx_send_cmd(struct ssd16xx_device *device, 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 (device->dc) {
+        /* 4-wire SPI: D/C# GPIO low selects command mode */
+        xfer.tx_buf = &cmd;
+        xfer.len = 1;
+        gpiod_set_value_cansleep(device->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(device->spi, &msg, err);
+}
+
+static void ssd16xx_send_data(struct ssd16xx_device *device, 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 (device->dc) {
+        /* 4-wire SPI: D/C# GPIO high selects data mode */
+        xfer.tx_buf = &data;
+        xfer.len = 1;
+        gpiod_set_value_cansleep(device->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(device->spi, &msg, err);
+}
+
+static void ssd16xx_send_x_param(struct ssd16xx_device *device, u16 x,
+                 int *err)
+{
+    if (*err)
+        return;
+
+    if (device->controller_cfg->ram_x_address_bits == 8) {
+        ssd16xx_send_data(device, (u8)x, err);
+    } else {
+        ssd16xx_send_data(device, x & 0xFF, err);
+        ssd16xx_send_data(device, (x >> 8) & 0xFF, err);
+    }
+}
+
+static void ssd16xx_send_y_param(struct ssd16xx_device *device, u16 y,
+                 int *err)
+{
+    if (*err)
+        return;
+
+    if (device->controller_cfg->ram_y_address_bits == 8) {
+        ssd16xx_send_data(device, (u8)y, err);
+    } else {
+        ssd16xx_send_data(device, y & 0xFF, err);
+        ssd16xx_send_data(device, (y >> 8) & 0xFF, err);
+    }
+}
+
+static void ssd16xx_send_data_bulk(struct ssd16xx_device *device,
+                   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 (device->dc) {
+        /* 4-wire SPI: D/C# GPIO high selects data mode */
+        xfer.tx_buf = data;
+        xfer.len = len;
+        gpiod_set_value_cansleep(device->dc, 1);
+        ssd16xx_spi_sync(device->spi, &msg, err);
+    } else {
+        /* 3-wire (9-bit): expand u8 → u16 with D/C#=1 in bit 8. */
+        size_t i;
+        u16 *buf = device->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(device->spi, &msg, err);
+    }
+}
+
+static void ssd16xx_display_update(struct ssd16xx_device *device,
+                   u8 ctrl1_byte1, u8 ctrl1_byte2, u8 ctrl2_mode,
+                   int *err)
+{
+    if (*err)
+        return;
+
+    drm_dbg(&device->drm,
+        "display_update: Setting ctrl1=0x%02x,0x%02x mode=0x%02x\n",
+        ctrl1_byte1, ctrl1_byte2, ctrl2_mode);
+
+    ssd16xx_send_cmd(device, SSD16XX_CMD_DISPLAY_UPDATE_CONTROL1, err);
+    ssd16xx_send_data(device, ctrl1_byte1, err);
+    ssd16xx_send_data(device, ctrl1_byte2, err);
+
+    ssd16xx_send_cmd(device, SSD16XX_CMD_DISPLAY_UPDATE_CONTROL2, err);
+    ssd16xx_send_data(device, ctrl2_mode, err);
+    ssd16xx_send_cmd(device, SSD16XX_CMD_MASTER_ACTIVATION, err);
+
+    drm_dbg(&device->drm,
+        "display_update: Master activation sent, waiting...\n");
+
+    ssd16xx_wait_for_device(device, err);
+}
+
+static void ssd16xx_hw_reset(struct ssd16xx_device *device)
+{
+    gpiod_set_value_cansleep(device->reset, 1);
+    usleep_range(10000, 11000);
+    gpiod_set_value_cansleep(device->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).
+ */

drivers/gpu/drm/solomon/ssd16xx.c new file +6 -0
+static int ssd16xx_preload_fast_lut(struct ssd16xx_device *device)
+{
+    int err = 0;
+
+    ssd16xx_send_cmd(device, SSD16XX_CMD_DISPLAY_UPDATE_CONTROL1, &err);
+    ssd16xx_send_data(device, device->controller_cfg- 
Quoted 1 line

ctrl1_bypass_red_ram, &err);

+    ssd16xx_send_data(device, SSD16XX_CTRL1_BYTE2_DEFAULT, &err); +
+    ssd16xx_send_cmd(device, SSD16XX_CMD_DISPLAY_UPDATE_CONTROL2, &err); +    ssd16xx_send_data(device, device->controller_cfg-

Quoted 1 line

ctrl2_load_temp_lut, &err);

drivers/gpu/drm/solomon/ssd16xx.c new file +27 -0
+
+    ssd16xx_send_cmd(device, SSD16XX_CMD_MASTER_ACTIVATION, &err);
+    ssd16xx_wait_for_device(device, &err);
+
+    return err;
+}
+
+static int ssd16xx_hw_init(struct ssd16xx_device *device)
+{
+    int err = 0;
+    u8 data_entry_mode;
+    /*
+     * Driver Output Control MUX ratio = (gate lines - 1).
+     * Use the actual device height, not the controller maximum —
+     * a smaller device must only drive its own gate lines.
+     */
+
+    ssd16xx_hw_reset(device);
+
+    /* Software reset */
+    ssd16xx_send_cmd(device, SSD16XX_CMD_SW_RESET, &err);
+    ssd16xx_wait_for_device(device, &err);
+
+    /* Driver output control (0x01): MUX ratio and scan direction. */
+    ssd16xx_send_cmd(device, SSD16XX_CMD_DRIVER_OUTPUT_CONTROL, &err);
+    ssd16xx_send_y_param(device, device->height - 1, &err);
+    ssd16xx_send_data(device, device->device_cfg- 
Quoted 1 line

driver_output_ctrl_byte3, &err);

+
+    /* Internal temperature sensor (SSD1683/SSD1680 only; not present in SSD1673) */
+    if (device->controller_cfg->has_temp_sensor_ctrl) {
+        ssd16xx_send_cmd(device, SSD1683_CMD_TEMPERATURE_SENSOR_CONTROL, &err);
+        ssd16xx_send_data(device, 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 (device->refresh_mode == SSD16XX_REFRESH_FAST) {
+        ssd16xx_send_cmd(device, SSD16XX_CMD_DISPLAY_UPDATE_CONTROL1, &err);
+        ssd16xx_send_data(device, device->controller_cfg-

Quoted 1 line

ctrl1_bypass_red_ram, &err);

+        ssd16xx_send_data(device, SSD16XX_CTRL1_BYTE2_DEFAULT, &err); +
+        ssd16xx_send_cmd(device, SSD16XX_CMD_DISPLAY_UPDATE_CONTROL2, &err);
+        ssd16xx_send_data(device, device->controller_cfg-

Quoted 1 line

ctrl2_load_temp_lut, &err);

drivers/gpu/drm/solomon/ssd16xx.c new file +11 -0
+
+        ssd16xx_send_cmd(device, SSD16XX_CMD_MASTER_ACTIVATION, &err);
+        ssd16xx_wait_for_device(device, &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 (device->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(device, SSD16XX_CMD_DATA_ENTRY_MODE, &err); +    ssd16xx_send_data(device, data_entry_mode, &err);
+    drm_dbg(&device->drm, "hw_init: orientation=%u° data_entry=0x%02x\n",
+        device->orientation, data_entry_mode);

drivers/gpu/drm/solomon/ssd16xx.c new file +5 -0
+
+    ssd16xx_wait_for_device(device, &err);
+
+    if (err)
+        drm_err(&device->drm, "Hardware initialization failed: %d\n", 

err);

drivers/gpu/drm/solomon/ssd16xx.c new file +6 -0
+
+    return err;
+}
+
+/*
+ * ssd16xx_pixel_luma() - return ITU-R BT.601 luminance (0-255) for 

one pixel.
+ *
+ * Currently only XRGB8888 is supported.  The function is retained as a named
+ * helper to make it straightforward to add further formats in the future.
+ * R1 is never passed here — it is already 1bpp and handled directly by callers.
+ */

drivers/gpu/drm/solomon/ssd16xx.c new file +13 -0
+static u8 ssd16xx_pixel_luma(struct iosys_map *src,
+                 struct drm_framebuffer *fb,
+                 unsigned int x, unsigned int y)
+{
+    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);
+}
+
+/*
+ * ssd16xx_pixel_is_white() - test whether an XRGB8888 or R1 pixel is 

white.
+ *
+ * For XRGB8888: ITU-R BT.601 luminance > 127 maps to white.
+ * For R1: the pixel's bit value directly encodes white (1) or black (0).
+ */

drivers/gpu/drm/solomon/ssd16xx.c new file +14 -0
+static bool ssd16xx_pixel_is_white(struct iosys_map *src,
+                   struct drm_framebuffer *fb,
+                   unsigned int x, unsigned int y)
+{
+    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 an XRGB8888 pixel is 

dominated by red.
+ *
+ * Returns true when red exceeds 50% intensity and is strictly greater than
+ * both green and blue (dominant red hue). R1 carries no colour information
+ * and always returns false.
+ */

drivers/gpu/drm/solomon/ssd16xx.c new file +22 -0
+static bool ssd16xx_pixel_is_red(struct iosys_map *src,
+                 struct drm_framebuffer *fb,
+                 unsigned int x, unsigned int y)
+{
+    u32 *line;
+    u32 px;
+    u8 r, g, b;
+
+    if (fb->format->format != DRM_FORMAT_XRGB8888)
+        return false;
+
+    line = (u32 *)(src->vaddr + y * fb->pitches[0]);
+    px = line[x];
+    r = (px >> 16) & 0xFF;
+    g = (px >> 8) & 0xFF;
+    b = px & 0xFF;
+
+    return r > 127 && r > g && r > b;
+}
+
+/*
+ * 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)
+ *
+ * Supports XRGB8888 and R1 formats.
+ *
+ * XRGB8888: pixels with a dominant red channel (r > 127 && r > g && r > b)
+ * map to red ink; remaining pixels threshold to white/black via BT.601 luma.
+ *
+ * R1 on a 3-color panel: the 1-bits are interpreted as red ink on a white
+ * background.  BW RAM is set to all-white and RED RAM receives the R1 data
+ * directly (1=red, 0=no red).  This matches the behaviour of a user who
+ * explicitly selects color_mode=3-color and submits a 1-bit mask to place
+ * red ink.
+ */

drivers/gpu/drm/solomon/ssd16xx.c new file +78 -0
+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;
+
+    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);
+
+        /* White background — no black pixels, only red ink shows */
+        memset(bw_dst, 0xFF, data_size);
+        /* RED RAM: copy R1 data directly (1=red ink, 0=no red) */
+        for (y = rect->y1; y < rect->y2; y++) {
+            u8 *line = src->vaddr + y * src_pitch + (rect->x1 / 8);
+
+            memcpy(red_dst + dst_idx, line, width_bytes);
+            dst_idx += width_bytes;
+        }
+        return;
+    }
+
+    /* XRGB8888 */
+    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));
+            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;
+            }
+        }
+        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;
+        }
+    }
+}
+
+/*
+ * Convert framebuffer to 1-bit monochrome for e-paper display.
+ *
+ * Supports XRGB8888 (thresholded via ITU-R BT.601 luma at 127) and R1
+ * (native 1bpp, zero-copy fast path for aligned landscape frames).
+ *
+ * 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;

drivers/gpu/drm/solomon/ssd16xx.c new file +17 -0
+
+        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;
+    }

drivers/gpu/drm/solomon/ssd16xx.c new file +3 -0
+}
+
+static int ssd16xx_fb_dirty(struct drm_framebuffer *fb, struct 

drm_rect *rect,
+                struct ssd16xx_device *device,
+                const struct iosys_map *src_map)

drivers/gpu/drm/solomon/ssd16xx.c new file +23 -0
+{
+    const u8 *ctrl2_tbl = device->controller_cfg->ctrl2_refresh;
+    struct iosys_map map = *src_map;
+    int err = 0;
+    unsigned int data_size = (device->width * device->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.  The rect for convert_fb_to_1bpp uses
+     * the framebuffer (logical) dimensions - the pixel iteration walks
+     * the source fb coordinate space. RAM window registers below use
+     * the physical panel dimensions (device->width/height).
+     */
+    rect->x1 = 0;
+    rect->y1 = 0;
+    rect->x2 = fb->width;
+    rect->y2 = fb->height;
+
+    drm_dbg(&device->drm,
+        "fb_dirty: fb=%dx%d, refresh_mode=%d, orientation=%d\n",
+        fb->width, fb->height, device->refresh_mode, device- 
Quoted 1 line

orientation);

drivers/gpu/drm/solomon/ssd16xx.c new file +24 -0
+
+    mono_buffer = device->tx_buf;
+    memset(mono_buffer, 0, data_size);
+
+    /* 3-colour FULL/FAST: populate red channel. */
+    if (device->color_mode == SSD16XX_COLOR_MODE_3COLOR &&
+        (device->refresh_mode == SSD16XX_REFRESH_FULL ||
+         device->refresh_mode == SSD16XX_REFRESH_FAST)) {
+        red_buffer = device->tx_red_buf;
+        memset(red_buffer, 0, data_size);
+    }
+
+    /*
+     * R1 format interpretation depends on color_mode:
+     *
+     *   color_mode = 3-color (red_buffer != NULL):
+     *     R1 bits are treated as red ink — 1 = red pixel on a white
+     *     background, 0 = no red.  This allows applications to submit a
+     *     1-bit mask to place red ink on a BWR panel.
+     *     Handled by the R1 path in ssd16xx_convert_fb_to_3color().
+     *
+     *   color_mode = BW (red_buffer == NULL):
+     *     R1 bits are treated as luma — 1 = white, 0 = black.
+     *     Handled by the native fast path in 

ssd16xx_convert_fb_to_1bpp().
+     */
+    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, device->orientation);
+
+    drm_dbg(&device->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);

drivers/gpu/drm/solomon/ssd16xx.c new file +22 -0
+
+    /* Set RAM window and cursor for current orientation. */
+    ram_x_start = 0;
+    /*
+     * X end depends on the controller's addressing model:
+     *   Byte-addressed (ram_x_address_bits == 8, e.g. SSD1683):
+     *     XEnd = device_width/8 - 1  (byte offset into RAM row)
+     *   Pixel-addressed (ram_x_address_bits != 8, e.g. SSD1677):
+     *     XEnd = device_width - 1    (direct pixel index)
+     * The model is controller-specific; the value is device-specific.
+     */
+    if (device->controller_cfg->ram_x_address_bits == 8)
+        ram_x_end = (device->width / 8) - 1;
+    else
+        ram_x_end = device->width - 1;
+    ram_y_start = 0;
+    ram_y_end = device->height - 1;
+
+    switch (device->orientation) {
+    case 90:
+    case 180:
+        /* 90°/180°: XDEC_YDEC mode, send end-before-start; cursor at 

(max, max). */
+        ssd16xx_send_cmd(device, SSD16XX_CMD_SET_RAM_X_ADDRESS_START_END, &err); +        ssd16xx_send_x_param(device, ram_x_end, &err); +        ssd16xx_send_x_param(device, ram_x_start, &err); +
+        ssd16xx_send_cmd(device, SSD16XX_CMD_SET_RAM_Y_ADDRESS_START_END, &err); +        ssd16xx_send_y_param(device, ram_y_end, &err); +        ssd16xx_send_y_param(device, ram_y_start, &err); +
+        ssd16xx_send_cmd(device, SSD16XX_CMD_SET_RAM_X_ADDRESS_COUNTER, &err);
+        ssd16xx_send_x_param(device, ram_x_end, &err); +
+        ssd16xx_send_cmd(device, SSD16XX_CMD_SET_RAM_Y_ADDRESS_COUNTER, &err);
+        ssd16xx_send_y_param(device, ram_y_end, &err); +        break;
+
+    default: /* 0°/270° */
+        /* 0°/270°: XINC_YINC mode, cursor at (0, 0). */ +        ssd16xx_send_cmd(device, SSD16XX_CMD_SET_RAM_X_ADDRESS_START_END, &err); +        ssd16xx_send_x_param(device, ram_x_start, &err); +        ssd16xx_send_x_param(device, ram_x_end, &err); +
+        ssd16xx_send_cmd(device, SSD16XX_CMD_SET_RAM_Y_ADDRESS_START_END, &err); +        ssd16xx_send_y_param(device, ram_y_start, &err); +        ssd16xx_send_y_param(device, ram_y_end, &err); +
+        ssd16xx_send_cmd(device, SSD16XX_CMD_SET_RAM_X_ADDRESS_COUNTER, &err);
+        ssd16xx_send_x_param(device, ram_x_start, &err); +
+        ssd16xx_send_cmd(device, SSD16XX_CMD_SET_RAM_Y_ADDRESS_COUNTER, &err);
+        ssd16xx_send_y_param(device, ram_y_start, &err); +        break;
+    }

drivers/gpu/drm/solomon/ssd16xx.c new file +8 -0
+
+    ssd16xx_send_cmd(device, SSD16XX_CMD_WRITE_RAM_BW, &err);
+    ssd16xx_send_data_bulk(device, mono_buffer, data_size, &err);
+
+    /* Re-send border waveform when: every-update mode, init frame
+     */
+    drm_dbg(&device->drm,
+        "fb_dirty: border check: every_update=%d init_pending=%d 

border_pending=%d idx=%d hw=0x%02x\n",
+        device->border_refresh_on_every_update, device-

Quoted 1 line

init_refresh_pending,

+        device->border_waveform_pending, device-

Quoted 1 line

border_waveform_update_idx,

+        device->controller_cfg->border_waveform_table[device-

Quoted 1 line

border_waveform_update_idx]);

+    if (device->border_refresh_on_every_update || device-

Quoted 1 line

init_refresh_pending ||

+        device->border_waveform_pending) {
+        u8 idx = device->border_waveform_update_idx;
+        u8 border = device->controller_cfg->border_waveform_table[idx]; +
+        drm_dbg(&device->drm, "fb_dirty: Sending border waveform:
0x%02x\n",
+            border);
+        ssd16xx_send_cmd(device, SSD16XX_CMD_BORDER_WAVEFORM_CONTROL, &err);
+        ssd16xx_send_data(device, border, &err);
+        device->border_waveform_pending = false;
+    }
+
+    switch (device->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(device, SSD1683_CMD_WRITE_RAM_RED, &err); +        if (red_buffer) {
+            /* 3-colour: write red channel before activating */ +            ssd16xx_send_data_bulk(device, red_buffer, data_size, &err); +            ssd16xx_display_update(device, device->controller_cfg-

Quoted 1 line

ctrl1_normal,

+                           SSD16XX_CTRL1_BYTE2_DEFAULT, +                           ctrl2_tbl[SSD16XX_REFRESH_FULL], &err); +        } else {
+            ssd16xx_send_data_bulk(device, mono_buffer, data_size, &err);
+            ssd16xx_display_update(device, device->controller_cfg-

Quoted 1 line

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(device, SSD1683_CMD_WRITE_RAM_RED, &err); +        if (red_buffer) {
+            /* 3-colour: write red channel before activating */ +            ssd16xx_send_data_bulk(device, red_buffer, data_size, &err); +            ssd16xx_display_update(device, device->controller_cfg-

Quoted 1 line

ctrl1_normal,

+                           SSD16XX_CTRL1_BYTE2_DEFAULT, +                           ctrl2_tbl[SSD16XX_REFRESH_FAST], &err); +        } else {
+            ssd16xx_send_data_bulk(device, mono_buffer, data_size, &err);
+            ssd16xx_display_update(device, device->controller_cfg-

Quoted 1 line

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(&device->drm,
+            "fb_dirty: partial pre-update: mono[0]=0x%02x (BW=new, RED=prev)\n",
+            mono_buffer[0]);
+        ssd16xx_display_update(device, device->controller_cfg-

Quoted 1 line

ctrl1_normal,

+                       SSD16XX_CTRL1_BYTE2_DEFAULT,
+                       ctrl2_tbl[SSD16XX_REFRESH_PARTIAL], &err); +        ssd16xx_send_cmd(device, SSD1683_CMD_WRITE_RAM_RED, &err); +        ssd16xx_send_data_bulk(device, mono_buffer, data_size, &err); +        drm_dbg(&device->drm,
+            "fb_dirty: partial post-update: wrote RED baseline mono[0]=0x%02x\n",
+            mono_buffer[0]);
+        break;
+    }

drivers/gpu/drm/solomon/ssd16xx.c new file +31 -1
+
+    return err;
+}
+
+/* 
-----------------------------------------------------------------------------
+ * Plane Functions
+ */
+
+static void ssd16xx_plane_destroy(struct drm_plane *plane)
+{
+    drm_plane_cleanup(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,
+    DRM_GEM_SHADOW_PLANE_FUNCS,
+};
+
+static int ssd16xx_plane_atomic_check(struct drm_plane *plane,
+                      struct drm_atomic_commit *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);

drivers/gpu/drm/solomon/ssd16xx.c new file +6 -0
+}
+
+static void ssd16xx_plane_atomic_update(struct drm_plane *plane,
+                    struct drm_atomic_commit *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 drm_shadow_plane_state *shadow_state =

Quoted 8 lines

to_drm_shadow_plane_state(new_state);
+    struct ssd16xx_device *device = plane_to_ssd16xx_device(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;
+

The atomic_update function is missing drm_dev_enter() and drm_dev exit().

Quoted 5 lines

Your driver also needs lock the gem buffer with drm_gem_fb_begin_cpu_access() and drm_gem_fb_end_cpu_access() whenever you read its data. See [1] for an example. Otherwise a concurrent exporter could write into the memory.

[1] https://elixir.bootlin.com/linux/v7.2.8/source/drivers/gpu/drm/ast/ ast_cursor.c#L198

Agreed, thanks for pointing.

Quoted 9 lines
Quoted 6 lines

+    drm_dbg(&device->drm, "plane_atomic_update: fb=%p, initialized=%d\n",
+        fb, device->initialized);
+
+    if (!fb || !device->initialized)
+        return;

Your driver should have initialized HW long before it comes here.

Yes driver follows a lazy init model, but it should still have been initialized by this.

Quoted 10 lines
Quoted 9 lines

+
+    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(&device->drm, "plane_atomic_update: no damage, using full screen\n");
+    }

Quoted 8 lines

The correct pattern is:

  if (damage_merged()) {
    // do screen update.
  }

your driver is not allowed to setup its own rectangles and update those instead.

Agreed, thanks for pointing.

Quoted 70 lines
Quoted 70 lines

+
+    drm_dbg(&device->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 = device->refresh_mode;
+    saved_border_waveform_idx = device->border_waveform_update_idx; +    if (device->init_refresh_pending) {
+        device->refresh_mode = device->refresh_mode_init; +        device->border_waveform_update_idx = device-

Quoted 1 line

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 (device->fast_lut_pending) {
+        ret = ssd16xx_preload_fast_lut(device);
+        if (ret) {
+            drm_err(&device->drm,
+                "plane_atomic_update: fast LUT preload failed: %d\n", ret);
+        }

drivers/gpu/drm/solomon/ssd16xx.c new file +7 -0
+
+        device->fast_lut_pending = false;
+    }
+
+    ret = ssd16xx_fb_dirty(fb, &rect, device, &shadow_state->data[0]);
+    if (ret)
+        drm_err(&device->drm, "plane_atomic_update: display update 

failed: %d\n", ret);
+    else
+        device->last_fb = fb;

drivers/gpu/drm/solomon/ssd16xx.c new file +16 -0
+
+    device->refresh_mode = saved_mode;
+    device->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 (device->init_refresh_pending) {
+        device->init_refresh_pending = false;
+        device->border_waveform_pending = true;
+    }
+}
+
+static const struct drm_plane_helper_funcs ssd16xx_plane_helper_funcs 

= {
+    DRM_GEM_SHADOW_PLANE_HELPER_FUNCS,
+    .atomic_check = ssd16xx_plane_atomic_check, +    .atomic_update = ssd16xx_plane_atomic_update,

drivers/gpu/drm/solomon/ssd16xx.c new file +4 -1
+};
+
+/* 
-----------------------------------------------------------------------------
+ * CRTC Functions
Quoted 9 lines

drivers/gpu/drm/solomon/ssd16xx.c line 1323

Quoted 6 lines

+ */

drivers/gpu/drm/solomon/ssd16xx.c new file +5 -0
+
+static void ssd16xx_crtc_destroy(struct drm_crtc *crtc)
+{
+    drm_crtc_cleanup(crtc);
+}

No need for this wrapper AFAICT.

Agreed, thanks for pointing.

Quoted 33 lines
Quoted 20 lines
drivers/gpu/drm/solomon/ssd16xx.c new file +11 -0
+
+static const struct drm_crtc_funcs ssd16xx_crtc_funcs = {
+    .atomic_create_state = drm_atomic_helper_crtc_create_state,
+    .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)

drivers/gpu/drm/solomon/ssd16xx.c new file +7 -0
+{
+    struct ssd16xx_device *device = crtc_to_ssd16xx_device(crtc);
+
+    /* Accept only our device's native mode (landscape or portrait) */
+    if ((mode->hdisplay == device->mode->hdisplay &&
+         mode->vdisplay == device->mode->vdisplay) ||
+        (mode->hdisplay == device->mode->vdisplay &&

drivers/gpu/drm/solomon/ssd16xx.c line 1352

Quoted 10 lines
drivers/gpu/drm/solomon/ssd16xx.c new file +10 -0
+         mode->vdisplay == device->mode->hdisplay))
+        return MODE_OK;
+
+    return MODE_BAD;
+}
+
+static int ssd16xx_crtc_atomic_check(struct drm_crtc *crtc,
+                     struct drm_atomic_commit *state)
+{
+    return 0;

Since the CRTC and primary plane need to move in sync, here's a good place to test that with drm_atomic_helper_check_crtc_primary_plane(). See [2] for an example.

Quoted 2 lines

[2] https://elixir.bootlin.com/linux/v7.2.8/source/drivers/gpu/drm/ sysfb/drm_sysfb_modeset.c#L496

Agreed, thanks for pointing.

Quoted 17 lines
Quoted 4 lines
drivers/gpu/drm/solomon/ssd16xx.c new file +3 -0
+}
+
+static void ssd16xx_crtc_atomic_disable(struct drm_crtc *crtc,

drivers/gpu/drm/solomon/ssd16xx.c line 1365

Quoted 10 lines
drivers/gpu/drm/solomon/ssd16xx.c new file +10 -0
+                    struct drm_atomic_commit *state)
+{
+    struct ssd16xx_device *device = crtc_to_ssd16xx_device(crtc);
+    int idx;
+
+    if (!drm_dev_enter(&device->drm, &idx))
+        return;
+
+    drm_dev_exit(idx);
+}

No need for this helper AFAICT.

Agreed, thanks for pointing.

Quoted 14 lines
Quoted 14 lines
drivers/gpu/drm/solomon/ssd16xx.c new file +14 -0
+
+static void ssd16xx_crtc_atomic_enable(struct drm_crtc *crtc,
+                       struct drm_atomic_commit *state)
+{
+    struct ssd16xx_device *device = crtc_to_ssd16xx_device(crtc);
+    int ret, idx;
+
+    if (!drm_dev_enter(&device->drm, &idx))
+        return;
+
+    drm_dbg(&device->drm, "atomic_enable: %dx%d orientation=%u°\n",
+        device->width, device->height, device->orientation);
+
+    ret = ssd16xx_hw_init(device);
Quoted 9 lines
Quoted 6 lines

+    if (ret) {
+        drm_err(&device->drm, "crtc_atomic_enable: HW init failed:
%d\n", ret);
+        goto out;
+    }
+    device->initialized = true;

This is also problematic. The driver should have initialized the device hardware when probing it.  If this fails, probe need to fail with an error.  There should then not be a modesetting pipeline at all.

This was deliberate. We wanted to lazy init the ssd16xx controller when userspace actually want to start display, at probe we just check necessary prequisities are met. I see similar pattern followed by other drivers as well [0]

Quoted 38 lines
Quoted 38 lines

+
+    /*
+     * 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 device default refresh_mode. +     */
+    if (device->refresh_mode_init >= 0) {
+        drm_dbg(&device->drm,
+            "atomic_enable: refresh_mode_init=%d, using for first frame\n",
+            device->refresh_mode_init);
+        device->init_refresh_pending = true;
+    }

drivers/gpu/drm/solomon/ssd16xx.c new file +25 -0
+
+out:
+    drm_dev_exit(idx);
+}
+
+/*
+ * Re-initialize hardware and redraw the current framebuffer when the
+ * display orientation changes at runtime
+ */
+static void ssd16xx_crtc_atomic_flush(struct drm_crtc *crtc,
+                      struct drm_atomic_commit *state)
+{
+    struct ssd16xx_device *device = crtc_to_ssd16xx_device(crtc);
+    struct drm_framebuffer *fb;
+    struct drm_rect full;
+    int ret, idx;
+
+    if (!device->initialized)
+        return;
+
+    if (!drm_dev_enter(&device->drm, &idx))
+        return;
+
+    drm_dbg(&device->drm, "atomic_flush: reinit, orientation=%u°\n",
+        device->orientation);
Quoted 9 lines
Quoted 6 lines

+
+    ret = ssd16xx_hw_init(device);
+    if (ret) {
+        drm_err(&device->drm, "Orientation re-init failed: %d\n", ret); +        goto out;
+    }

Another one of those HW inits. IIRC there are many more of them.

Thanks for pointing, I think above can be removed.

Quoted 11 lines
Quoted 11 lines
drivers/gpu/drm/solomon/ssd16xx.c new file +11 -0
+
+    fb = device->primary_plane.state ? device->primary_plane.state->fb
+                    : device->last_fb;
+    if (fb) {
+        struct drm_gem_object *obj = drm_gem_fb_get_obj(fb, 0);
+        struct iosys_map map;
+
+        full.x1 = 0;
+        full.y1 = 0;
+        full.x2 = fb->width;
+        full.y2 = fb->height;
Quoted 9 lines
Quoted 6 lines

+
+        ret = drm_gem_vmap(obj, &map);
+        if (!ret) {
+            ret = ssd16xx_fb_dirty(fb, &full, device, &map); +            drm_gem_vunmap(obj, &map);
+        }

Screen pixel updates belong in the plane's atomic_update.

Thanks for pointing, I think we can get rid of these, they were initially added to prepare base for runtime rotation but the latter was removed from this series and I think I missed to update these functions.

[0]:
https://elixir.bootlin.com/linux/v7.3-rc5/source/drivers/gpu/drm/solomon/ssd130x.c#L1545
https://elixir.bootlin.com/linux/v7.3-rc5/source/drivers/gpu/drm/tiny/repaper.c#L941

Regards
Devarsh

Quoted 446 lines
Quoted 445 lines

+        if (ret)
+            drm_err(&device->drm, "atomic_flush: display update
failed: %d\n", ret);
+        else
+            device->last_fb = fb;
+    }

drivers/gpu/drm/solomon/ssd16xx.c new file +21 -1
+
+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_device *device = to_ssd16xx_device(connector->dev);
+    bool mode_is_portrait = (device->mode->hdisplay < device->mode- 
Quoted 1 line

vdisplay);

+    bool orient_is_portrait = (device->orientation == 90 || device-

Quoted 1 line

orientation == 270);

+
+    drm_dbg(&device->drm,
+        "connector_get_modes: orientation=%u° mode=%ux%u mode_portrait=%d orient_portrait=%d\n",
+        device->orientation, device->mode->hdisplay, device->mode-

Quoted 1 line

vdisplay,

+        mode_is_portrait, orient_is_portrait);

drivers/gpu/drm/solomon/ssd16xx.c new file +20 -0
+
+    /* 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(&device->drm, device->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, device- 
Quoted 1 line

mode);

drivers/gpu/drm/solomon/ssd16xx.c new file +3 -0
+}
+
+static const struct drm_connector_helper_funcs 

ssd16xx_connector_helper_funcs = {
+    .get_modes = ssd16xx_connector_get_modes,

drivers/gpu/drm/solomon/ssd16xx.c new file +7 -0
+};
+
+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,

drivers/gpu/drm/solomon/ssd16xx.c new file +5 -0
+};
+
+static const u32 ssd16xx_formats[] = {
+    DRM_FORMAT_XRGB8888,  /* 32-bit RGB with padding (preferred) */
+    DRM_FORMAT_R1,        /* 1-bit monochrome (native, zero-copy 

path) */

drivers/gpu/drm/solomon/ssd16xx.c new file +6 -0
+};
+
+DEFINE_DRM_GEM_FOPS(ssd16xx_fops);
+
+/*
+ * ssd16xx_drm_master_set - arm init refresh when a new master takes 

control.
+ */

drivers/gpu/drm/solomon/ssd16xx.c new file +11 -0
+static void ssd16xx_drm_master_set(struct drm_device *drm,
+                   struct drm_file *file, bool from_open)
+{
+    struct ssd16xx_device *device = to_ssd16xx_device(drm);
+
+    if (device->refresh_mode_init >= 0)
+        device->init_refresh_pending = true;
+}
+
+/*
+ * ssd16xx_drm_master_drop - clear display and disarm init refresh 

when the
+ * master client exits.
+ */

drivers/gpu/drm/solomon/ssd16xx.c new file +29 -0
+static void ssd16xx_drm_master_drop(struct drm_device *drm,
+                    struct drm_file *file)
+{
+    struct ssd16xx_device *device = to_ssd16xx_device(drm);
+
+    device->init_refresh_pending = false;
+}
+
+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_SHMEM_DRIVER_OPS,
+    DRM_FBDEV_SHMEM_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,

drivers/gpu/drm/solomon/ssd16xx.c new file +16 -0
+};
+
+static int ssd16xx_alloc_tx_bufs(struct ssd16xx_device *device)
+{
+    /*
+     * Allocate for the actual physical panel size (width × height are
+     * always the physical dimensions, never swapped for orientation).
+     */
+    size_t frame_size = DIV_ROUND_UP(device->width * device->height, 8);
+
+    device->tx_buf = drmm_kmalloc(&device->drm, frame_size, GFP_KERNEL);
+    if (!device->tx_buf)
+        return -ENOMEM;
+
+    if (device->device_cfg->red_supported) {
+        device->tx_red_buf = drmm_kmalloc(&device->drm, frame_size, 

GFP_KERNEL);
+        if (!device->tx_red_buf)
+            return -ENOMEM;
+    }

drivers/gpu/drm/solomon/ssd16xx.c new file +69 -0
+
+    if (!device->dc) {
+        device->tx_buf9 = drmm_kmalloc_array(&device->drm, frame_size,
+                             sizeof(u16), GFP_KERNEL);
+        if (!device->tx_buf9)
+            return -ENOMEM;
+    }
+
+    return 0;
+}
+
+static int ssd16xx_probe(struct spi_device *spi)
+{
+    struct device *dev = &spi->dev;
+    struct ssd16xx_device *device;
+    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;
+    }
+
+    device = devm_drm_dev_alloc(dev, &ssd16xx_drm_driver,
+                    struct ssd16xx_device, drm);
+    if (IS_ERR(device))
+        return PTR_ERR(device);
+
+    drm = &device->drm;
+    device->spi = spi;
+    device->model = model;
+    spi_set_drvdata(spi, device);
+
+    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:
+        device->controller = SSD1683;
+        break;
+    default:
+        drm_err(drm, "Unknown panel model: %d\n", model);
+        return -EINVAL;
+    }
+
+    if (device->controller >= ARRAY_SIZE(ssd16xx_controller_configs) ||
+        !ssd16xx_controller_configs[device->controller].max_width)
+        return -EINVAL;
+    device->controller_cfg = &ssd16xx_controller_configs[device- 
Quoted 1 line

controller];

drivers/gpu/drm/solomon/ssd16xx.c new file +13 -0
+
+    if (model >= ARRAY_SIZE(ssd16xx_device_configs))
+        return -EINVAL;
+    device->device_cfg = &ssd16xx_device_configs[model];
+
+    mode = devm_kmemdup(dev, device->device_cfg->mode,
+                sizeof(*device->device_cfg->mode), GFP_KERNEL);
+    if (!mode)
+        return -ENOMEM;
+
+    device->refresh_mode = device->device_cfg->default_refresh_mode;
+    device->color_mode   = device->device_cfg->default_color_mode;
+    device->border_waveform_init_idx   = device->device_cfg- 
Quoted 1 line

default_border_waveform_init;

+    device->border_waveform_update_idx = device->device_cfg-

Quoted 1 line

default_border_waveform_update;

+    device->border_refresh_on_every_update =
+        device->device_cfg->default_border_refresh_on_every_update; +    device->refresh_mode_init = device->device_cfg-

Quoted 1 line

default_refresh_mode_init;

+
+    /* 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;
+    }
+    device->orientation = dt_rotation;

drivers/gpu/drm/solomon/ssd16xx.c new file +7 -0
+
+    device->width = mode->hdisplay;
+    device->height = mode->vdisplay;
+
+    drm_dbg(drm, "Using %s orientation (%u°, physical %ux%u)\n",
+        (device->orientation == 90 || device->orientation == 270) ?
+         "portrait" : "landscape", device->orientation, device- 
Quoted 1 line

width, device->height);

drivers/gpu/drm/solomon/ssd16xx.c new file +34 -0
+
+    /* Swap mode dimensions for portrait so clients see logical size. */
+    if (device->orientation == 90 || device->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);
+    }
+    device->mode = mode;
+
+    /* Validate panel dimensions against controller hardware limits. */
+    if (device->width > device->controller_cfg->max_width ||
+        device->height > device->controller_cfg->max_height) {
+        drm_err(drm, "panel %ux%u exceeds controller max %ux%u\n",
+            device->width, device->height,
+            device->controller_cfg->max_width,
+            device->controller_cfg->max_height);
+        return -EINVAL;
+    }
+
+    /*
+     * For byte-addressed X (SSD1683, ram_x_address_bits == 8), each X
+     * address covers 8 pixels.  The panel width must be a multiple of 8
+     * so that (width/8 - 1) gives the correct byte-aligned window end.
+     */
+    if (device->controller_cfg->ram_x_address_bits == 8 &&
+        device->width % 8 != 0) {
+        drm_err(drm, "panel width %u not a multiple of 8 required for 

byte-addressed X controller)\n",
+            device->width);
+        return -EINVAL;
+    }
+
+    /* Acquire GPIOs. */
+    device->reset = devm_gpiod_get(dev, "reset", GPIOD_OUT_HIGH); +    if (IS_ERR(device->reset))
+        return dev_err_probe(dev, PTR_ERR(device->reset), "Failed to get RESET GPIO\n");
+
+    device->busy = devm_gpiod_get(dev, "busy", GPIOD_IN); +    if (IS_ERR(device->busy))
+        return dev_err_probe(dev, PTR_ERR(device->busy), "Failed to get BUSY GPIO\n");
+
+    device->dc = devm_gpiod_get_optional(dev, "dc", GPIOD_OUT_LOW); +    if (IS_ERR(device->dc))
+        return dev_err_probe(dev, PTR_ERR(device->dc), "Failed to get DC GPIO\n");
+    if (!device->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");
+    }

drivers/gpu/drm/solomon/ssd16xx.c new file +19 -0
+
+    ret = ssd16xx_alloc_tx_bufs(device);
+    if (ret)
+        return ret;
+
+    ssd16xx_hw_reset(device);
+
+    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(device->width, device->height);
+    drm->mode_config.max_width = max(device->width, device->height);
+    drm->mode_config.min_height = min(device->width, device->height);
+    drm->mode_config.max_height = max(device->width, device->height);
+
+    drm_connector_helper_add(&device->connector, 

&ssd16xx_connector_helper_funcs);
+    ret = drm_connector_init(drm, &device->connector, &ssd16xx_connector_funcs,
+                 DRM_MODE_CONNECTOR_SPI);
+    if (ret)
+        return ret;
+
+    ret = drm_universal_plane_init(drm, &device->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(&device->primary_plane, &ssd16xx_plane_helper_funcs);
+    drm_plane_enable_fb_damage_clips(&device->primary_plane); +
+    ret = drm_crtc_init_with_planes(drm, &device->crtc, &device-

Quoted 1 line

primary_plane,

+                    NULL, &ssd16xx_crtc_funcs, NULL);
+    if (ret)
+        return ret;
+    drm_crtc_helper_add(&device->crtc, &ssd16xx_crtc_helper_funcs); +
+    ret = drmm_encoder_init(drm, &device->encoder, NULL, DRM_MODE_ENCODER_NONE, NULL);
+    if (ret)
+        return ret;
+    device->encoder.possible_crtcs = drm_crtc_mask(&device->crtc); +
+    ret = drm_connector_attach_encoder(&device->connector, &device-

Quoted 1 line

encoder);

+    if (ret)
+        return ret;

drivers/gpu/drm/solomon/ssd16xx.c new file +8 -0
+
+    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",
+        device->width, device->height, device->orientation);

drivers/gpu/drm/solomon/ssd16xx.c new file +23 -0
+
+    drm_client_setup(drm, NULL);
+
+    return 0;
+}
+
+static void ssd16xx_remove(struct spi_device *spi)
+{
+    struct ssd16xx_device *device = spi_get_drvdata(spi);
+
+    drm_dev_unplug(&device->drm);
+    drm_atomic_helper_shutdown(&device->drm);
+}
+
+static void ssd16xx_shutdown(struct spi_device *spi)
+{
+    struct ssd16xx_device *device = spi_get_drvdata(spi);
+
+    drm_atomic_helper_shutdown(&device->drm);
+}
+
+static const struct of_device_id ssd16xx_of_match[] = {
+    { .compatible = "gooddisplay,gdey042t81", .data = (void 

*)GDEY042T81 },
+    { }

drivers/gpu/drm/solomon/ssd16xx.c new file +23 -0
+};
+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 <[email protected]>");
+MODULE_DESCRIPTION("DRM driver for Solomon SSD16xx e-paper display 

controller family");
+MODULE_LICENSE("GPL");

#13 lore raw

PATCH v2 5/14 drm/solomon: ssd16xx: Add clear_on_init/close/disable session management

In reply to Devarsh Thakkar

E-paper displays are bistable: the last rendered image persists indefinitely across sessions and power cycles. This creates a session management choice with no LCD/OLED equivalent since unlike volatile displays, content visibility outlives the rendering process. Whether this persistence is a feature or a liability depends entirely on the application.

Add session-lifetime display clearing with three configurable hooks:
clear_on_init: blank the display when a new DRM master opens the device. Useful for kiosks and applications that need a clean slate on startup.
Example: a hospital patient room sign must start with a clean slate when a new patient is admitted, discarding the previous patient's name, dietary restrictions, and risk warnings.

clear_on_close: blank the display when the DRM master exits (fd close).
Useful for applications that should not leave content visible after exit.
Example: a transit departure board at a remote bus stop must erase stale schedule data when the service daemon is stopped — displaying yesterday's departure times actively misleads commuters.

clear_on_disable: blank the display on CRTC disable / DPMS off. Provides
an independent clear point separate from master lifecycle. Example: When userspace trigger DPMS off, similar to LCD screen, here with the epaper too, the display would be cleared.

These hooks help enabling diverse use cases:

- A conference badge sets clear_on_init but leaves clear_on_close disabled, leveraging e-paper's bistability to keep the attendee's name
visible indefinitely after the rendering app exits — saving power while remaining useful.

- A hospital patient room sign sets both clear_on_init and
clear_on_close: a clean slate for the new patient and automatic erasure
on discharge so protected health information does not persist on the display outside the door.

- An airport boarding pass kiosk sets clear_on_close so the previous passenger's name, booking reference, and seat assignment are erased the
moment the session ends — preventing the next person in the queue from seeing personal travel information.

- A meeting room booking display sets clear_on_disable so the schedule is wiped when the room session ends via DPMS off, independent of whether
the calendar sync daemon continues running.

Each is an integer selecting the refresh waveform for the clear (-1 = disabled, 0 = partial, 1 = full, 2 = fast), defaulting to -1 (disabled) in the panel configuration so the behaviour is opt-in. A display_cleared_on_deinit guard prevents redundant double-clears when multiple paths fire for the same session (e.g. compositor does DPMS off then exits). first_clear_done ensures clear_on_init fires exactly once per client session; master_drop resets it so the next client gets a fresh clear.

All three default to -1 (disabled) in the panel configuration so the behaviour is opt-in per panel and does not affect panels that have not set an explicit default.

---
Changes from v1:
- New patch: Separated session management feature (clear_on_init, etc.) into dedicated patch for better reviewability

 drivers/gpu/drm/solomon/ssd16xx.c | 168 +++++++++++++++++++++++++++++-
 1 file changed, 167 insertions(+), 1 deletion(-)
drivers/gpu/drm/solomon/ssd16xx.c +167 -1
index d3af055c6739..c478309d08e9 100644
@@ -298,6 +298,18 @@ struct ssd16xx_device_config {
 	/* 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.
@@ -347,6 +359,8 @@ struct ssd16xx_device {
 
 	bool initialized;
 	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 */
@@ -360,6 +374,9 @@ struct ssd16xx_device {
 	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) */
@@ -418,6 +435,9 @@ static const struct ssd16xx_device_config ssd16xx_device_configs[] = {
 		.default_border_waveform_init   = SSD16XX_BORDER_LUT1,
 		.default_border_waveform_update = SSD16XX_BORDER_VCOM,
 		.default_border_refresh_on_every_update = true,
+		.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 */
 		.default_color_mode = SSD16XX_COLOR_MODE_BW,
@@ -746,6 +766,111 @@ static int ssd16xx_hw_init(struct ssd16xx_device *device)
 	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_device *device, u8 ctrl2)
+{
+	const u8 *bw_tbl = device->controller_cfg->border_waveform_table;
+	int err = 0;
+	unsigned int data_size = (device->width * device->height) / 8;
+	u8 *white_buffer = device->tx_buf;
+
+	memset(white_buffer, 0xFF, data_size);
+
+	ssd16xx_send_cmd(device, SSD16XX_CMD_SET_RAM_X_ADDRESS_COUNTER, &err);
+	ssd16xx_send_x_param(device, 0x00, &err);
+
+	ssd16xx_send_cmd(device, SSD16XX_CMD_SET_RAM_Y_ADDRESS_COUNTER, &err);
+	ssd16xx_send_y_param(device, 0x00, &err);
+
+	ssd16xx_send_cmd(device, SSD16XX_CMD_WRITE_RAM_BW, &err);
+	ssd16xx_send_data_bulk(device, white_buffer, data_size, &err);
+
+	ssd16xx_send_cmd(device, SSD1683_CMD_WRITE_RAM_RED, &err);
+	ssd16xx_send_data_bulk(device, white_buffer, data_size, &err);
+
+	/* Set border waveform for the clear operation */
+	drm_dbg(&device->drm, "clear_display: Set border init waveform: 0x%02x\n",
+		bw_tbl[device->border_waveform_init_idx]);
+	ssd16xx_send_cmd(device, SSD16XX_CMD_BORDER_WAVEFORM_CONTROL, &err);
+	ssd16xx_send_data(device,
+			  bw_tbl[device->border_waveform_init_idx],
+			  &err);
+
+	/* 3-colour mode: CTRL1_NORMAL (read both RAMs); BW mode: bypass RED. */
+	ssd16xx_display_update(device,
+			       device->color_mode == SSD16XX_COLOR_MODE_3COLOR
+					? device->controller_cfg->ctrl1_normal
+					: device->controller_cfg->ctrl1_bypass_red_ram,
+			       SSD16XX_CTRL1_BYTE2_DEFAULT, ctrl2, &err);
+
+	/* Restore border waveform to update/preservation value */
+	drm_dbg(&device->drm, "clear_display: Restored border update waveform: 0x%02x\n",
+		bw_tbl[device->border_waveform_update_idx]);
+	ssd16xx_send_cmd(device, SSD16XX_CMD_BORDER_WAVEFORM_CONTROL, &err);
+	ssd16xx_send_data(device,
+			  bw_tbl[device->border_waveform_update_idx],
+			  &err);
+
+	return err;
+}
+
+static u8 ssd16xx_refresh_mode_to_ctrl2(struct ssd16xx_device *device,
+					enum ssd16xx_refresh_mode mode)
+{
+	if (mode < ARRAY_SIZE(device->controller_cfg->ctrl2_refresh))
+		return device->controller_cfg->ctrl2_refresh[mode];
+	return device->controller_cfg->ctrl2_refresh[SSD16XX_REFRESH_FULL];
+}
+
+/*
+ * Clear display on new DRM master open (if clear_on_init >= 0).
+ * Guarded by device->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_device *device)
+{
+	int ret;
+
+	if (device->clear_on_init < 0 || device->first_clear_done)
+		return 0;
+
+	drm_dbg(&device->drm, "clear_on_init: running, mode=%d\n",
+		device->clear_on_init);
+	ret = ssd16xx_clear_display(device,
+				    ssd16xx_refresh_mode_to_ctrl2(device, device->clear_on_init));
+	if (ret)
+		return ret;
+
+	device->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_device *device)
+{
+	int ret;
+
+	if (device->clear_on_close < 0)
+		return 0;
+
+	drm_dbg(&device->drm, "clear_on_close: running, mode=%d\n",
+		device->clear_on_close);
+	ret = ssd16xx_clear_display(device,
+				    ssd16xx_refresh_mode_to_ctrl2(device, device->clear_on_close));
+	if (ret)
+		return ret;
+
+	return 0;
+}
+
 /*
  * ssd16xx_pixel_luma() - return ITU-R BT.601 luminance (0-255) for one pixel.
  *
@@ -1356,11 +1481,26 @@ static void ssd16xx_crtc_atomic_disable(struct drm_crtc *crtc,
 					struct drm_atomic_commit *state)
 {
 	struct ssd16xx_device *device = crtc_to_ssd16xx_device(crtc);
-	int idx;
+	int ret, idx;
 
 	if (!drm_dev_enter(&device->drm, &idx))
 		return;
 
+	if (device->clear_on_disable < 0 || device->display_cleared_on_deinit)
+		goto out;
+
+	drm_dbg(&device->drm, "clear_on_disable: running, mode=%d\n",
+		device->clear_on_disable);
+	ret = ssd16xx_clear_display(device,
+				    ssd16xx_refresh_mode_to_ctrl2(device,
+								  device->clear_on_disable));
+	if (ret) {
+		drm_err(&device->drm, "atomic_disable: clear failed: %d\n", ret);
+		goto out;
+	}
+
+	device->display_cleared_on_deinit = true;
+out:
 	drm_dev_exit(idx);
 }
 
@@ -1383,6 +1523,11 @@ static void ssd16xx_crtc_atomic_enable(struct drm_crtc *crtc,
 	}
 	device->initialized = true;
 
+	/* Clear display on first app launch if configured */
+	ret = ssd16xx_clear_display_on_init(device);
+	if (ret)
+		drm_err(&device->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
@@ -1523,6 +1668,9 @@ static void ssd16xx_drm_master_set(struct drm_device *drm,
 {
 	struct ssd16xx_device *device = to_ssd16xx_device(drm);
 
+	device->display_cleared_on_deinit = false;
+	device->first_clear_done = false;
+
 	if (device->refresh_mode_init >= 0)
 		device->init_refresh_pending = true;
 }
@@ -1535,8 +1683,23 @@ static void ssd16xx_drm_master_drop(struct drm_device *drm,
 				    struct drm_file *file)
 {
 	struct ssd16xx_device *device = to_ssd16xx_device(drm);
+	int ret, idx;
 
 	device->init_refresh_pending = false;
+	device->first_clear_done = false;
+
+	if (device->clear_on_close < 0 || device->display_cleared_on_deinit)
+		return;
+
+	if (!drm_dev_enter(drm, &idx))
+		return;
+
+	ret = ssd16xx_clear_display_on_exit(device);
+	if (ret)
+		drm_err(drm, "master_drop: clear on close failed: %d\n", ret);
+
+	device->display_cleared_on_deinit = true;
+	drm_dev_exit(idx);
 }
 
 static struct drm_driver ssd16xx_drm_driver = {
@@ -1671,6 +1834,9 @@ static int ssd16xx_probe(struct spi_device *spi)
 	device->border_waveform_update_idx = device->device_cfg->default_border_waveform_update;
 	device->border_refresh_on_every_update =
 		device->device_cfg->default_border_refresh_on_every_update;
+	device->clear_on_init    = device->device_cfg->default_clear_on_init;
+	device->clear_on_close   = device->device_cfg->default_clear_on_close;
+	device->clear_on_disable = device->device_cfg->default_clear_on_disable;
 	device->refresh_mode_init = device->device_cfg->default_refresh_mode_init;
 
 	/* Parse "rotation" DT property; swap mode dimensions for portrait. */
Signature · 1 line
2.39.1
#14 lore raw

Re: [PATCH v2 05/14] drm/solomon: ssd16xx: Add clear_on_init/close/disable session management

Thank you for your contribution! Sashiko AI review found 6 potential issue(s) to consider: - [High] drm: sleeping in atomic context during CRTC enable/disable - [High] drm/solomon: unsynchronized…

Thank you for your contribution! Sashiko AI review found 6 potential issue(s) to consider:
- [High] drm: sleeping in atomic context during CRTC enable/disable
- [High] drm/solomon: unsynchronized clear in master_drop races with atomic commits
- [High] drm/solomon: clear_display hardcodes RAM address counters to 0x00
- [Medium] drm/solomon: clear_display writes 0xFF to RED RAM causing red screen
- [Medium] drm/solomon: display_cleared_on_deinit flag is not reset on enable
- [Medium] drm/solomon: clear_on_init bypasses active CRTCs during master handover --

commit 90ac1ffc13e17b560cc95b731a7e77c4602e7b77
Author: Devarsh Thakkar <[email protected]>

drm/solomon: ssd16xx: Add clear_on_init/close/disable session management

E-paper displays are bistable: the last rendered image persists indefinitely across sessions and power cycles. This creates a session management choice with no LCD/OLED equivalent since unlike volatile displays, content visibility outlives the rendering process. Add session-lifetime display clearing with three configurable hooks: clear_on_init, clear_on_close, and clear_on_disable.

Quoted 4 lines
drivers/gpu/drm/solomon/ssd16xx.c +0 -0
index d3af055c67398..c478309d08e9f 100644

[ ... ]

Quoted 8 lines
drivers/gpu/drm/solomon/ssd16xx.c +4 -0
@@ -746,6 +766,111 @@ static int ssd16xx_hw_init(struct ssd16xx_device *device)
 	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

drivers/gpu/drm/solomon/ssd16xx.c static int ssd16xx_hw_init(struct ssd16xx_device *device) · line 782

Quoted 10 lines
drivers/gpu/drm/solomon/ssd16xx.c +10 -0
+ * to the update value to preserve the border during subsequent updates.
+ */
+static int ssd16xx_clear_display(struct ssd16xx_device *device, u8 ctrl2)
+{
+	const u8 *bw_tbl = device->controller_cfg->border_waveform_table;
+	int err = 0;
+	unsigned int data_size = (device->width * device->height) / 8;
+	u8 *white_buffer = device->tx_buf;
+
+	memset(white_buffer, 0xFF, data_size);

[Severity: High]
Can this unsynchronized memset cause a data race?

When a DRM master drops, ssd16xx_drm_master_drop calls ssd16xx_clear_display_on_exit. If an atomic commit is concurrently executing in a background worker, both paths will mutate device->tx_buf without locks, which could interleave SPI commands and corrupt the controller state.

drivers/gpu/drm/solomon/ssd16xx.c static int ssd16xx_hw_init(struct ssd16xx_device *device) · line 788

Quoted 6 lines
drivers/gpu/drm/solomon/ssd16xx.c +6 -0
+
+	ssd16xx_send_cmd(device, SSD16XX_CMD_SET_RAM_X_ADDRESS_COUNTER, &err);
+	ssd16xx_send_x_param(device, 0x00, &err);
+
+	ssd16xx_send_cmd(device, SSD16XX_CMD_SET_RAM_Y_ADDRESS_COUNTER, &err);
+	ssd16xx_send_y_param(device, 0x00, &err);

[Severity: High]
Does hardcoding the RAM X and Y address counters to 0x00 break decrement-mode writes and omit window setup?

In portrait orientation (decrement entry modes), starting at 0x00 might immediately underflow the counter and write out-of-bounds.

Also, if the previous operation was a partial update, could the RAM window remain restricted, causing the clear operation to wrap around and fail to clear the full screen?

drivers/gpu/drm/solomon/ssd16xx.c static int ssd16xx_hw_init(struct ssd16xx_device *device) · line 794

Quoted 6 lines
drivers/gpu/drm/solomon/ssd16xx.c +6 -0
+
+	ssd16xx_send_cmd(device, SSD16XX_CMD_WRITE_RAM_BW, &err);
+	ssd16xx_send_data_bulk(device, white_buffer, data_size, &err);
+
+	ssd16xx_send_cmd(device, SSD1683_CMD_WRITE_RAM_RED, &err);
+	ssd16xx_send_data_bulk(device, white_buffer, data_size, &err);

[Severity: Medium]
Does this write 0xFF to the RED RAM unconditionally?

On 3-color (BWR) panels where a 1 bit represents red ink, this would clear the screen to solid red instead of white. On 2-color panels, it sends an unsupported command (0x26), which could corrupt hardware state.

[ ... ]

Quoted 8 lines
drivers/gpu/drm/solomon/ssd16xx.c +1 -1
@@ -1356,11 +1481,26 @@ static void ssd16xx_crtc_atomic_disable(struct drm_crtc *crtc,
 					struct drm_atomic_commit *state)
 {
 	struct ssd16xx_device *device = crtc_to_ssd16xx_device(crtc);
-	int idx;
+	int ret, idx;
 
 	if (!drm_dev_enter(&device->drm, &idx))

drivers/gpu/drm/solomon/ssd16xx.c static void ssd16xx_crtc_atomic_disable(struct drm_crtc *crtc, · line 1496

Quoted 10 lines
drivers/gpu/drm/solomon/ssd16xx.c +8 -0
 		return;
 
+	if (device->clear_on_disable < 0 || device->display_cleared_on_deinit)
+		goto out;
+
+	drm_dbg(&device->drm, "clear_on_disable: running, mode=%d\n",
+		device->clear_on_disable);
+	ret = ssd16xx_clear_display(device,
+				    ssd16xx_refresh_mode_to_ctrl2(device,
+								  device->clear_on_disable));

[Severity: High]
Can this cause a sleep in atomic context?

The CRTC atomic_disable callback executes in atomic context where sleeping is forbidden, but it conditionally calls ssd16xx_clear_display, which relies on SPI transactions and polling with usleep_range in ssd16xx_wait_for_device.

drivers/gpu/drm/solomon/ssd16xx.c static void ssd16xx_crtc_atomic_disable(struct drm_crtc *crtc, · line 1502

Quoted 6 lines
drivers/gpu/drm/solomon/ssd16xx.c +6 -0
+	if (ret) {
+		drm_err(&device->drm, "atomic_disable: clear failed: %d\n", ret);
+		goto out;
+	}
+
+	device->display_cleared_on_deinit = true;

[Severity: Medium]
Is the display_cleared_on_deinit flag missing a reset on enable?

DPMS off sets the flag to true, but since atomic_enable does not reset it, subsequent DPMS toggles during a single session might silently skip the clear operation.

drivers/gpu/drm/solomon/ssd16xx.c static void ssd16xx_crtc_atomic_enable(struct drm_crtc *crtc, · line 1527

Quoted 10 lines
drivers/gpu/drm/solomon/ssd16xx.c +3 -0
+out:
 	drm_dev_exit(idx);
 }
 
@@ -1383,6 +1523,11 @@ static void ssd16xx_crtc_atomic_enable(struct drm_crtc *crtc,
 	}
 	device->initialized = true;
 
+	/* Clear display on first app launch if configured */
+	ret = ssd16xx_clear_display_on_init(device);

[Severity: High]
Can this cause a sleep in atomic context?

Similar to atomic_disable, the CRTC atomic_enable callback executes in atomic context where sleeping is forbidden, but calling ssd16xx_clear_display_on_init leads to ssd16xx_clear_display which eventually polls with usleep_range.

[Severity: Medium]
Does this bypass active CRTCs during master handover?

The clear_on_init hook is inside atomic_enable, which is only called on transitions from disabled to enabled. If a new DRM master takes over without triggering a full modeset (e.g. the CRTC remains active), the clean-slate clear might not fire.

Quoted 6 lines
drivers/gpu/drm/solomon/ssd16xx.c +3 -0
+	if (ret)
+		drm_err(&device->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
Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=5
#15 lore raw

PATCH v2 6/14 drm/solomon: ssd16xx: Add support for Solomon SSD1677 controller

In reply to Devarsh Thakkar

Add infrastructure to support the Solomon SSD1677 e-paper controller (up to 960x680 pixels, 10-bit pixel-level X/Y RAM addressing).

The SSD1677 shares the same command address space as SSD1683 but differs in several controller-level characteristics:

- RAM X/Y addressing: SSD1677 uses 10-bit pixel-level addresses whereas SSD1683 uses 6-bit byte-level X and 9-bit Y. The existing ram_x_address_bits / ram_y_address_bits fields in ssd16xx_controller_config already handle the different format; ssd16xx_fb_dirty is updated to compute the correct end address from device->width using the addressing model:
byte-addressed: XEnd = panel_width/8 - 1
pixel-addressed: XEnd = panel_width - 1

- Deep sleep: only mode 0x03 is documented in SSD1677 (no level-1

    RAM-retain mode 0x01 as in SSD1683).  Both level1/level2 point
    to SSD1683_DEEP_SLEEP_MODE_2 (0x03).

- ctrl2 full refresh uses 0xF4 (omits DISABLE_ANALOG/DISABLE_CLK bits) per the reference implementation. 0xFF for partial refresh is listed as valid in the SSD1677 datasheet; BIT(3) semantics are undocumented — included pending hardware verification.

- Border waveform byte encoding is identical to SSD1683; the same

    ssd1683_border_waveform_table[] is reused.

A new booster_soft_start_data / booster_soft_start_len field is added to ssd16xx_device_config for panels that require BOOSTER_SOFT_START (0x0C) initialisation. The command exists in both SSD1683 and SSD1677 but the tuning values are board/panel-specific; panels that do not need it set the pointer to NULL. hw_init sends the command when non-NULL.

Controller limit validation is added in probe: panel dimensions must not exceed the controller hardware maximum, and for byte-addressed X controllers the panel width must be a multiple of 8.

Driver Output Control MUX, tx_buf allocation, and fb_dirty RAM window end addresses now use the actual panel dimensions rather than the controller maximum, correctly handling panels smaller than the controller's rated capacity.

---
Changes from v1:
- New patch: Extracted SSD1677 controller support into dedicated patch

 drivers/gpu/drm/solomon/ssd16xx.c | 78 +++++++++++++++++++++++++++----
 1 file changed, 68 insertions(+), 10 deletions(-)
drivers/gpu/drm/solomon/ssd16xx.c +68 -10
index c478309d08e9..93fa064cf589 100644
@@ -39,6 +39,7 @@
 
 /* SPI command codes (common) */
 #define SSD16XX_CMD_DRIVER_OUTPUT_CONTROL		0x01
+#define SSD16XX_CMD_BOOSTER_SOFT_START			0x0C
 #define SSD16XX_CMD_DATA_ENTRY_MODE			0x11
 #define SSD16XX_CMD_SW_RESET				0x12
 #define SSD16XX_CMD_MASTER_ACTIVATION			0x20
@@ -181,6 +182,7 @@
 
 enum ssd16xx_controller {
 	SSD1683 = 1,
+	SSD1677,
 };
 
 enum ssd16xx_model {
@@ -332,6 +334,10 @@ struct ssd16xx_device_config {
 	 */
 	enum ssd16xx_color_mode default_color_mode;
 
+	/* Optional BOOSTER_SOFT_START (0x0C) tuning bytes; NULL if unused. */
+	const u8 *booster_soft_start_data;
+	u8        booster_soft_start_len;
+
 	/* Panel-specific display mode (resolution and physical dimensions) */
 	const struct drm_display_mode *mode;
 };
@@ -420,6 +426,29 @@ static const struct ssd16xx_controller_config ssd16xx_controller_configs[] = {
 		},
 		.ctrl2_load_temp_lut     = SSD1683_CTRL2_LOAD_TEMP_LUT,
 	},
+	[SSD1677] = {
+		/*
+		 * 10-bit pixel-level X/Y addressing; only deep sleep mode
+		 * 0x03 is supported (no RAM-retain mode). Border waveform
+		 * encoding matches SSD1683.
+		 */
+		.max_width  = 960,
+		.max_height = 680,
+		.ram_x_address_bits = 10,
+		.ram_y_address_bits = 10,
+		.has_temp_sensor_ctrl    = true,
+		.deep_sleep_mode_level1  = SSD1683_DEEP_SLEEP_MODE_2,  /* 0x03 */
+		.deep_sleep_mode_level2  = SSD1683_DEEP_SLEEP_MODE_2,  /* 0x03 */
+		.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] = 0xFF,
+			[SSD16XX_REFRESH_FULL]    = 0xF4,
+			[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 */
@@ -553,28 +582,44 @@ static void ssd16xx_send_data(struct ssd16xx_device *device, u8 data,
 static void ssd16xx_send_x_param(struct ssd16xx_device *device, u16 x,
 				 int *err)
 {
+	u8 bits = device->controller_cfg->ram_x_address_bits;
+	u16 max_val = (1U << bits) - 1;
+
 	if (*err)
 		return;
 
-	if (device->controller_cfg->ram_x_address_bits == 8) {
+	if (x > max_val)
+		x = max_val;
+
+	if (bits <= 8) {
 		ssd16xx_send_data(device, (u8)x, err);
 	} else {
+		u8 hi_mask = (1U << (bits - 8)) - 1;
+
 		ssd16xx_send_data(device, x & 0xFF, err);
-		ssd16xx_send_data(device, (x >> 8) & 0xFF, err);
+		ssd16xx_send_data(device, (x >> 8) & hi_mask, err);
 	}
 }
 
 static void ssd16xx_send_y_param(struct ssd16xx_device *device, u16 y,
 				 int *err)
 {
+	u8 bits = device->controller_cfg->ram_y_address_bits;
+	u16 max_val = (1U << bits) - 1;
+
 	if (*err)
 		return;
 
-	if (device->controller_cfg->ram_y_address_bits == 8) {
+	if (y > max_val)
+		y = max_val;
+
+	if (bits <= 8) {
 		ssd16xx_send_data(device, (u8)y, err);
 	} else {
+		u8 hi_mask = (1U << (bits - 8)) - 1;
+
 		ssd16xx_send_data(device, y & 0xFF, err);
-		ssd16xx_send_data(device, (y >> 8) & 0xFF, err);
+		ssd16xx_send_data(device, (y >> 8) & hi_mask, err);
 	}
 }
 
@@ -691,21 +736,34 @@ static int ssd16xx_hw_init(struct ssd16xx_device *device)
 
 	ssd16xx_hw_reset(device);
 
-	/* Software reset */
+	/* Software reset (0x12): resets command/parameter registers to defaults. */
 	ssd16xx_send_cmd(device, SSD16XX_CMD_SW_RESET, &err);
 	ssd16xx_wait_for_device(device, &err);
 
-	/* Driver output control (0x01): MUX ratio and scan direction. */
-	ssd16xx_send_cmd(device, SSD16XX_CMD_DRIVER_OUTPUT_CONTROL, &err);
-	ssd16xx_send_y_param(device, device->height - 1, &err);
-	ssd16xx_send_data(device, device->device_cfg->driver_output_ctrl_byte3, &err);
-
 	/* Internal temperature sensor (SSD1683/SSD1680 only; not present in SSD1673) */
 	if (device->controller_cfg->has_temp_sensor_ctrl) {
 		ssd16xx_send_cmd(device, SSD1683_CMD_TEMPERATURE_SENSOR_CONTROL, &err);
 		ssd16xx_send_data(device, SSD1683_TEMP_SENSOR_INTERNAL, &err);
 	}
 
+	/*
+	 * Booster soft-start (0x0C): panel-specific charge pump tuning.
+	 * Some panels (e.g. PIXPAPER 4.26m on SSD1677) require this step;
+	 * others (e.g. GDEY042T81 on SSD1683) omit it.
+	 */
+	if (device->device_cfg->booster_soft_start_data) {
+		ssd16xx_send_cmd(device, SSD16XX_CMD_BOOSTER_SOFT_START, &err);
+		ssd16xx_send_data_bulk(device,
+				       device->device_cfg->booster_soft_start_data,
+				       device->device_cfg->booster_soft_start_len,
+				       &err);
+	}
+
+	/* Driver output control (0x01): MUX ratio and scan direction. */
+	ssd16xx_send_cmd(device, SSD16XX_CMD_DRIVER_OUTPUT_CONTROL, &err);
+	ssd16xx_send_y_param(device, device->height - 1, &err);
+	ssd16xx_send_data(device, device->device_cfg->driver_output_ctrl_byte3, &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
Signature · 1 line
2.39.1
#16 lore raw

Re: [PATCH v2 06/14] drm/solomon: ssd16xx: Add support for Solomon SSD1677 controller

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider: - [High] drm/solomon: heap buffer overflow during 1bpp framebuffer conversion - [High] drm/solomon: illegal…

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] drm/solomon: heap buffer overflow during 1bpp framebuffer conversion
- [High] drm/solomon: illegal DMA mapping of .rodata via SPI tx_buf
--

commit 097c6bbe33ac16a0e3a2bddb2a05e16d23d7914c
Author: Devarsh Thakkar <[email protected]>

drm/solomon: ssd16xx: Add support for Solomon SSD1677 controller

Add infrastructure to support the Solomon SSD1677 e-paper controller (up to 960x680 pixels, 10-bit pixel-level X/Y RAM addressing).

Quoted 4 lines
drivers/gpu/drm/solomon/ssd16xx.c +0 -0
index c478309d08e9f..93fa064cf5894 100644

[ ... ]

drivers/gpu/drm/solomon/ssd16xx.c static const struct ssd16xx_controller_config ssd16xx_controller_configs[] = { · line 437

Quoted 13 lines
drivers/gpu/drm/solomon/ssd16xx.c +9 -0
@@ -420,6 +426,29 @@ static const struct ssd16xx_controller_config ssd16xx_controller_configs[] = {
 		},
 		.ctrl2_load_temp_lut     = SSD1683_CTRL2_LOAD_TEMP_LUT,
 	},
+	[SSD1677] = {
+		/*
+		 * 10-bit pixel-level X/Y addressing; only deep sleep mode
+		 * 0x03 is supported (no RAM-retain mode). Border waveform
+		 * encoding matches SSD1683.
+		 */
+		.max_width  = 960,
+		.max_height = 680,
+		.ram_x_address_bits = 10,

[Severity: High]
Does this configuration allow panels with widths that are not a multiple of 8, leading to a heap buffer overflow during framebuffer conversion?

Because ram_x_address_bits is 10, it bypasses the multiple-of-8 width check in ssd16xx_probe():

	if (device->controller_cfg->ram_x_address_bits == 8 &&
	    device->width % 8 != 0) {
		drm_err(drm, "panel width %u not a multiple of 8 required for byte-addressed X controller)\n",
			device->width);
		return -EINVAL;
	}

This permits device->width to not be a multiple of 8. However, ssd16xx_alloc_tx_bufs() calculates the buffer size using global packing:

	size_t frame_size = DIV_ROUND_UP(device->width * device->height, 8);
	device->tx_buf = drmm_kmalloc(&device->drm, frame_size, GFP_KERNEL);

But ssd16xx_convert_fb_to_1bpp() pads each row to a byte boundary:

	for (y = rect->y1; y < rect->y2; y++) {
		...
		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;
		}
	}

This padding means it writes DIV_ROUND_UP(width, 8) * height bytes in total, which exceeds the allocated frame_size, causing memory corruption past the end of device->tx_buf.

[ ... ]

Quoted 16 lines
drivers/gpu/drm/solomon/ssd16xx.c +2 -1
@@ -691,21 +736,34 @@ static int ssd16xx_hw_init(struct ssd16xx_device *device)
 
 	ssd16xx_hw_reset(device);
 
-	/* Software reset */
+	/* Software reset (0x12): resets command/parameter registers to defaults. */
 	ssd16xx_send_cmd(device, SSD16XX_CMD_SW_RESET, &err);
 	ssd16xx_wait_for_device(device, &err);
 
 	/* Internal temperature sensor (SSD1683/SSD1680 only; not present in SSD1673) */
 	if (device->controller_cfg->has_temp_sensor_ctrl) {
 		ssd16xx_send_cmd(device, SSD1683_CMD_TEMPERATURE_SENSOR_CONTROL, &err);
 		ssd16xx_send_data(device, SSD1683_TEMP_SENSOR_INTERNAL, &err);
 	}
 
+	/*

drivers/gpu/drm/solomon/ssd16xx.c static int ssd16xx_hw_init(struct ssd16xx_device *device) · line 759

Quoted 10 lines
drivers/gpu/drm/solomon/ssd16xx.c +10 -0
+	 * Booster soft-start (0x0C): panel-specific charge pump tuning.
+	 * Some panels (e.g. PIXPAPER 4.26m on SSD1677) require this step;
+	 * others (e.g. GDEY042T81 on SSD1683) omit it.
+	 */
+	if (device->device_cfg->booster_soft_start_data) {
+		ssd16xx_send_cmd(device, SSD16XX_CMD_BOOSTER_SOFT_START, &err);
+		ssd16xx_send_data_bulk(device,
+				       device->device_cfg->booster_soft_start_data,
+				       device->device_cfg->booster_soft_start_len,
+				       &err);

[Severity: High]
Does this pass a .rodata pointer directly to the SPI core, causing an illegal DMA mapping?

The booster_soft_start_data pointer comes from the device configuration, which points to static const tuning data. When this is passed to ssd16xx_send_data_bulk() under 4-wire SPI mode:

	if (device->dc) {
		/* 4-wire SPI: D/C# GPIO high selects data mode */
		xfer.tx_buf = data;
		xfer.len = len;
		gpiod_set_value_cansleep(device->dc, 1);
		ssd16xx_spi_sync(device->spi, &msg, err);

The SPI core will attempt to DMA map the xfer.tx_buf. Mapping .rodata for DMA is prohibited and can cause kernel panics or mapping failures.

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=6
#17 lore raw

PATCH v2 7/14 drm/solomon: ssd16xx: Add power management support

In reply to Devarsh Thakkar

Add runtime and system suspend/resume power management to the SSD16xx e-paper display driver.

Runtime PM uses a two-level deep-sleep strategy:
- Level 1 (runtime_suspend after 35s idle): light sleep with RAM

    retained (SSD1683 mode 0x01), allowing fast resume via SPI wakeup

- Level 2 (system_suspend): maximum power savings (mode 0x03), requires full GPIO hardware reset to resume

An autosuspend delay of 35 seconds keeps the panel active across typical userspace update bursts, avoiding unnecessary deep-sleep/wake cycles.

On resume from level-2 (system suspend), the driver must fully reset the panel via HWRESET (the only exit from deep sleep since SPI is inactive in mode 0x03), re-run hw_init, and redraw the last frame.

For SSD1677 where level1 == level2 (both 0x03), the system suspend path skips the redundant "upgrade" since the panel is already in the deepest sleep state from runtime suspend.

---
Changes from v1:
- Extracted power management features into dedicated patch for improved code organization

 drivers/gpu/drm/solomon/ssd16xx.c | 240 ++++++++++++++++++++++++++++--
 1 file changed, 225 insertions(+), 15 deletions(-)
drivers/gpu/drm/solomon/ssd16xx.c +225 -15
index 93fa064cf589..6255f434248a 100644
@@ -12,6 +12,7 @@
 #include <linux/delay.h>
 #include <linux/module.h>
 #include <linux/of.h>
+#include <linux/pm_runtime.h>
 #include <linux/property.h>
 #include <linux/spi/spi.h>
 
@@ -39,6 +40,7 @@
 
 /* SPI command codes (common) */
 #define SSD16XX_CMD_DRIVER_OUTPUT_CONTROL		0x01
+#define SSD16XX_CMD_DEEP_SLEEP_MODE			0x10
 #define SSD16XX_CMD_BOOSTER_SOFT_START			0x0C
 #define SSD16XX_CMD_DATA_ENTRY_MODE			0x11
 #define SSD16XX_CMD_SW_RESET				0x12
@@ -52,6 +54,9 @@
 #define SSD16XX_CMD_SET_RAM_X_ADDRESS_COUNTER		0x4E
 #define SSD16XX_CMD_SET_RAM_Y_ADDRESS_COUNTER		0x4F
 
+/* Runtime PM autosuspend delay (ms): keep display active across typical gaps */
+#define SSD16XX_PM_AUTOSUSPEND_DELAY_MS			35000
+
 /*
  * Data Entry Mode (command 0x11) AM/IDY/IDX bit encoding (common).
  *
@@ -245,11 +250,14 @@ struct ssd16xx_controller_config {
 
 	/*
 	 * Deep sleep mode byte values for command 0x10.
-	 *   deep_sleep_mode_level1: lower-power sleep, RAM content retained
-	 *     (MODE_1 on SSD1683/SSD1680; used for runtime idle / app-close).
-	 *   deep_sleep_mode_level2: maximum power savings, RAM may be lost
-	 *     (MODE_2 on SSD1683/SSD1680; used for system suspend).
-	 * Chips with a single sleep mode set both fields to the same value.
+	 *   deep_sleep_mode_level1: used for runtime idle (autosuspend).
+	 *     SSD1683/SSD1680: 0x01 — RAM retained, minimal wake latency.
+	 *     SSD1677: 0x03 — only one mode documented; RAM may be lost.
+	 *   deep_sleep_mode_level2: used for system suspend (max power savings).
+	 *     SSD1683/SSD1680: 0x03 — RAM lost, full re-init required on resume.
+	 *     SSD1677: 0x03 — same as level1 (single sleep mode).
+	 * When level1 == level2 the system-suspend "upgrade" path is skipped;
+	 * the device is already in the deepest available mode.
 	 */
 	u8 deep_sleep_mode_level1;
 	u8 deep_sleep_mode_level2;
@@ -367,8 +375,10 @@ struct ssd16xx_device {
 	bool init_refresh_pending; /* First frame after refresh_mode_init enable */
 	bool first_clear_done;  /* clear_on_init has already fired once */
 	bool display_cleared_on_deinit; /* Avoid redundant clear in atomic_disable/master_drop */
+	bool pm_force_suspended;  /* pm_runtime_force_suspend was called in pm_suspend */
 
-	int orientation; /* Display orientation in degrees: 0/90/180/270 */
+	int orientation;      /* Display orientation in degrees: 0/90/180/270 */
+	u8 deep_sleep_mode;   /* Deep sleep mode to use on next disable */
 	enum ssd16xx_refresh_mode refresh_mode; /* Active refresh mode */
 	enum ssd16xx_color_mode color_mode;     /* Active color mode (BW or 3-color) */
 	bool fast_lut_pending; /* LUT pre-load needed before next fast refresh */
@@ -1425,11 +1435,16 @@ static void ssd16xx_plane_atomic_update(struct drm_plane *plane,
 	struct drm_rect rect;
 	int ret;
 
-	drm_dbg(&device->drm, "plane_atomic_update: fb=%p, initialized=%d\n",
-		fb, device->initialized);
+	drm_dbg(&device->drm, "plane_atomic_update: fb=%p\n", fb);
+
+	if (!fb)
+		return;
 
-	if (!fb || !device->initialized)
+	ret = pm_runtime_resume_and_get(device->drm.dev);
+	if (ret < 0) {
+		drm_err(&device->drm, "plane_atomic_update: failed to resume: %d\n", ret);
 		return;
+	}
 
 	if (!drm_atomic_helper_damage_merged(old_state, new_state, &rect)) {
 		rect.x1 = 0;
@@ -1488,6 +1503,9 @@ static void ssd16xx_plane_atomic_update(struct drm_plane *plane,
 		device->init_refresh_pending = false;
 		device->border_waveform_pending = true;
 	}
+
+	pm_runtime_mark_last_busy(device->drm.dev);
+	pm_runtime_put_autosuspend(device->drm.dev);
 }
 
 static const struct drm_plane_helper_funcs ssd16xx_plane_helper_funcs = {
@@ -1547,6 +1565,10 @@ static void ssd16xx_crtc_atomic_disable(struct drm_crtc *crtc,
 	if (device->clear_on_disable < 0 || device->display_cleared_on_deinit)
 		goto out;
 
+	ret = pm_runtime_resume_and_get(device->drm.dev);
+	if (ret < 0)
+		goto out;
+
 	drm_dbg(&device->drm, "clear_on_disable: running, mode=%d\n",
 		device->clear_on_disable);
 	ret = ssd16xx_clear_display(device,
@@ -1554,10 +1576,12 @@ static void ssd16xx_crtc_atomic_disable(struct drm_crtc *crtc,
 								  device->clear_on_disable));
 	if (ret) {
 		drm_err(&device->drm, "atomic_disable: clear failed: %d\n", ret);
+		pm_runtime_put_autosuspend(device->drm.dev);
 		goto out;
 	}
 
 	device->display_cleared_on_deinit = true;
+	pm_runtime_put_sync_suspend(device->drm.dev);
 out:
 	drm_dev_exit(idx);
 }
@@ -1574,12 +1598,27 @@ static void ssd16xx_crtc_atomic_enable(struct drm_crtc *crtc,
 	drm_dbg(&device->drm, "atomic_enable: %dx%d orientation=%u°\n",
 		device->width, device->height, device->orientation);
 
-	ret = ssd16xx_hw_init(device);
-	if (ret) {
-		drm_err(&device->drm, "crtc_atomic_enable: HW init failed: %d\n", ret);
-		goto out;
+	ret = pm_runtime_resume_and_get(device->drm.dev);
+	if (ret < 0) {
+		drm_err(&device->drm, "crtc_atomic_enable: failed to resume: %d\n", ret);
+		drm_dev_exit(idx);
+		return;
+	}
+
+	/*
+	 * If the device is not yet initialized (fresh from deep sleep),
+	 * pm_runtime_resume calls hw_init. The fallback below handles the case where
+	 * runtime PM is disabled and the resume callback never fires."
+	 *
+	 */
+	if (!device->initialized) {
+		ret = ssd16xx_hw_init(device);
+		if (ret) {
+			drm_err(&device->drm, "crtc_atomic_enable: HW init failed: %d\n", ret);
+			goto out;
+		}
+		device->initialized = true;
 	}
-	device->initialized = true;
 
 	/* Clear display on first app launch if configured */
 	ret = ssd16xx_clear_display_on_init(device);
@@ -1599,6 +1638,8 @@ static void ssd16xx_crtc_atomic_enable(struct drm_crtc *crtc,
 	}
 
 out:
+	pm_runtime_mark_last_busy(device->drm.dev);
+	pm_runtime_put_autosuspend(device->drm.dev);
 	drm_dev_exit(idx);
 }
 
@@ -1620,6 +1661,13 @@ static void ssd16xx_crtc_atomic_flush(struct drm_crtc *crtc,
 	if (!drm_dev_enter(&device->drm, &idx))
 		return;
 
+	ret = pm_runtime_resume_and_get(device->drm.dev);
+	if (ret < 0) {
+		drm_err(&device->drm, "atomic_flush: failed to resume: %d\n", ret);
+		drm_dev_exit(idx);
+		return;
+	}
+
 	drm_dbg(&device->drm, "atomic_flush: reinit, orientation=%u°\n",
 		device->orientation);
 
@@ -1652,6 +1700,8 @@ static void ssd16xx_crtc_atomic_flush(struct drm_crtc *crtc,
 	}
 
 out:
+	pm_runtime_mark_last_busy(device->drm.dev);
+	pm_runtime_put_autosuspend(device->drm.dev);
 	drm_dev_exit(idx);
 }
 
@@ -1752,11 +1802,19 @@ static void ssd16xx_drm_master_drop(struct drm_device *drm,
 	if (!drm_dev_enter(drm, &idx))
 		return;
 
+	ret = pm_runtime_resume_and_get(drm->dev);
+	if (ret < 0) {
+		drm_dev_exit(idx);
+		return;
+	}
+
 	ret = ssd16xx_clear_display_on_exit(device);
 	if (ret)
 		drm_err(drm, "master_drop: clear on close failed: %d\n", ret);
 
 	device->display_cleared_on_deinit = true;
+	/* sync suspend — bypass autosuspend, sleep immediately after clear */
+	pm_runtime_put_sync_suspend(drm->dev);
 	drm_dev_exit(idx);
 }
 
@@ -2017,9 +2075,26 @@ static int ssd16xx_probe(struct spi_device *spi)
 
 	drm_mode_config_reset(drm);
 
+	/*
+	 * Mark the device active before enabling runtime PM. The SPI device
+	 * persists across module reload cycles; pm_runtime_enable() alone
+	 * does not clear a stale dev->power.runtime_error left by a previous
+	 * failed hw_init, which would cause pm_runtime_resume_and_get() to
+	 * return -EINVAL on every subsequent call.
+	 */
+	pm_runtime_set_active(dev);
+	pm_runtime_enable(dev);
+	pm_runtime_set_autosuspend_delay(dev, SSD16XX_PM_AUTOSUSPEND_DELAY_MS);
+	pm_runtime_use_autosuspend(dev);
+
 	ret = drm_dev_register(drm, 0);
-	if (ret)
+	if (ret) {
+		pm_runtime_disable(dev);
 		return ret;
+	}
+
+	/* Default to runtime sleep mode (RAM retained if supported) */
+	device->deep_sleep_mode = device->controller_cfg->deep_sleep_mode_level1;
 
 	drm_dbg(drm, "SSD16xx e-paper display initialized (%dx%d, %d° rotation)\n",
 		device->width, device->height, device->orientation);
@@ -2035,6 +2110,8 @@ static void ssd16xx_remove(struct spi_device *spi)
 
 	drm_dev_unplug(&device->drm);
 	drm_atomic_helper_shutdown(&device->drm);
+	pm_runtime_dont_use_autosuspend(&spi->dev);
+	pm_runtime_disable(&spi->dev);
 }
 
 static void ssd16xx_shutdown(struct spi_device *spi)
@@ -2042,8 +2119,140 @@ static void ssd16xx_shutdown(struct spi_device *spi)
 	struct ssd16xx_device *device = spi_get_drvdata(spi);
 
 	drm_atomic_helper_shutdown(&device->drm);
+	pm_runtime_dont_use_autosuspend(&spi->dev);
+	pm_runtime_disable(&spi->dev);
 }
 
+static int ssd16xx_pm_suspend(struct device *dev)
+{
+	struct ssd16xx_device *device = dev_get_drvdata(dev);
+	struct drm_device *drm = &device->drm;
+	int ret;
+
+	/*
+	 * Select maximum-savings sleep mode for system suspend.
+	 * On SSD1683/SSD1680: level2 = 0x03 (RAM lost) gives deeper savings
+	 *   than the runtime-idle level1 = 0x01 (RAM retained).
+	 * On SSD1677: only one deep sleep mode exists; level1 == level2 == 0x03.
+	 */
+	device->deep_sleep_mode = device->controller_cfg->deep_sleep_mode_level2;
+	dev_dbg(dev, "system suspend: entering deep sleep mode 0x%02x\n",
+		device->deep_sleep_mode);
+
+	ret = drm_mode_config_helper_suspend(drm);
+	if (ret)
+		return ret;
+
+	/*
+	 * If the autosuspend timer already fired the device is RPM_SUSPENDED
+	 * at level1.  pm_runtime_force_suspend() would skip its callback, so
+	 * level2 would never reach the hardware.
+	 *
+	 * SPI is unresponsive in deep sleep; HWRESET is the only exit.
+	 * After the reset, level2 can be sent directly without a full hw_init.
+	 *
+	 * Skip this for controllers where level1 == level2 (e.g. SSD1677 which
+	 * has only one deep sleep mode): the device is already in the deepest
+	 * available state and no upgrade is needed.
+	 */
+	if (pm_runtime_status_suspended(dev)) {
+		int err = 0;
+
+		if (device->controller_cfg->deep_sleep_mode_level1 !=
+		    device->controller_cfg->deep_sleep_mode_level2) {
+			dev_dbg(dev, "system suspend: upgrading level1->level2 via HWRESET\n");
+			ssd16xx_hw_reset(device);
+			ssd16xx_wait_for_device(device, &err);
+			ssd16xx_send_cmd(device, SSD16XX_CMD_DEEP_SLEEP_MODE, &err);
+			ssd16xx_send_data(device,
+					  device->controller_cfg->deep_sleep_mode_level2,
+					  &err);
+		} else {
+			dev_dbg(dev, "system suspend: already at deepest sleep mode\n");
+		}
+		device->initialized = false;
+		device->pm_force_suspended = false;  /* Skip force_resume on resume */
+		return err;
+	}
+
+	/* Force runtime-suspended state for clean resume. */
+	pm_runtime_force_suspend(dev);
+	device->pm_force_suspended = true;  /* Resume must call force_resume */
+
+	return 0;
+}
+
+static int ssd16xx_pm_resume(struct device *dev)
+{
+	struct ssd16xx_device *device = dev_get_drvdata(dev);
+	struct drm_device *drm = &device->drm;
+
+	dev_dbg(dev, "system resume: restoring state\n");
+
+	/*
+	 * Only call force_resume if we actually called force_suspend during
+	 * system suspend. The MODE_1->MODE_2 upgrade path skips force_suspend
+	 * since the device is already RPM_SUSPENDED.
+	 */
+	if (device->pm_force_suspended) {
+		pm_runtime_force_resume(dev);
+		device->pm_force_suspended = false;
+	}
+
+	/* Restore Mode 1 (RAM retained) for subsequent runtime PM cycles. */
+	device->deep_sleep_mode = device->controller_cfg->deep_sleep_mode_level1;
+
+	/* Hardware must be re-initialized after waking from system sleep */
+	device->initialized = false;
+
+	return drm_mode_config_helper_resume(drm);
+}
+
+static int ssd16xx_pm_runtime_suspend(struct device *dev)
+{
+	struct ssd16xx_device *device = dev_get_drvdata(dev);
+	int err = 0;
+
+	dev_dbg(dev, "runtime suspend: entering deep sleep mode 0x%02x\n",
+		device->deep_sleep_mode);
+
+	ssd16xx_send_cmd(device, SSD16XX_CMD_DEEP_SLEEP_MODE, &err);
+	ssd16xx_send_data(device, device->deep_sleep_mode, &err);
+	device->initialized = false;
+
+	return err;
+}
+
+static int ssd16xx_pm_runtime_resume(struct device *dev)
+{
+	struct ssd16xx_device *device = dev_get_drvdata(dev);
+	int ret;
+
+	dev_dbg(dev, "runtime resume: initialized=%d%s\n",
+		device->initialized,
+		!device->initialized ? " (running hw_init)" : "");
+
+	/*
+	 * pm_runtime_suspend clears initialized after sending the deep sleep
+	 * command.  If a new app opens before the suspend fires (the put was
+	 * cancelled), initialized is still true and hw_init is unnecessary.
+	 */
+	if (!device->initialized) {
+		ret = ssd16xx_hw_init(device);
+		if (ret)
+			return ret;
+
+		device->initialized = true;
+	}
+
+	return 0;
+}
+
+static const struct dev_pm_ops ssd16xx_pm_ops = {
+	SYSTEM_SLEEP_PM_OPS(ssd16xx_pm_suspend, ssd16xx_pm_resume)
+	RUNTIME_PM_OPS(ssd16xx_pm_runtime_suspend, ssd16xx_pm_runtime_resume, NULL)
+};
+
 static const struct of_device_id ssd16xx_of_match[] = {
 	{ .compatible = "gooddisplay,gdey042t81", .data = (void *)GDEY042T81 },
 	{ }
@@ -2060,6 +2269,7 @@ static struct spi_driver ssd16xx_spi_driver = {
 	.driver = {
 		.name = "ssd16xx",
 		.of_match_table = ssd16xx_of_match,
+		.pm = pm_ptr(&ssd16xx_pm_ops),
 	},
 	.probe = ssd16xx_probe,
 	.remove = ssd16xx_remove,
Signature · 1 line
2.39.1
#18 lore raw

Re: [PATCH v2 07/14] drm/solomon: ssd16xx: Add power management support

Thank you for your contribution! Sashiko AI review found 7 potential issue(s) to consider: - [High] drm/pm: System suspend aborts without rolling back DRM state - [High] drm/pm: Transient runtime…

Thank you for your contribution! Sashiko AI review found 7 potential issue(s) to consider:
- [High] drm/pm: System suspend aborts without rolling back DRM state
- [High] drm/pm: Transient runtime suspend failures permanently brick display
- [High] drm/pm: Ignoring pm_runtime_force_suspend error underflows disable_depth
- [High] drm/atomic: Sleeping functions called from atomic context
- [Medium] drm/pm: Double hardware initialization during system resume
- [Medium] drm/pm: Hardware remains powered indefinitely upon driver removal
- [Medium] drm/pm: Autosuspend timer is never started after probe
--

commit 6dc9572b54afb59f2f6005e0c8a2cc022b179d13
Author: Devarsh Thakkar <[email protected]>
Subject: drm/solomon: ssd16xx: Add power management support

This commit adds runtime and system suspend/resume power management to the SSD16xx e-paper display driver. It implements a two-level deep-sleep strategy to allow light sleep across typical userspace update bursts and maximum power savings on system suspend.

Quoted 4 lines
drivers/gpu/drm/solomon/ssd16xx.c +0 -0
index 93fa064cf5894..6255f434248ae 100644

[ ... ]

Quoted 4 lines
drivers/gpu/drm/solomon/ssd16xx.c +0 -0
@@ -1425,11 +1435,16 @@ static void ssd16xx_plane_atomic_update(struct drm_plane *plane,
 	struct drm_rect rect;
 	int ret;
 

drivers/gpu/drm/solomon/ssd16xx.c static void ssd16xx_plane_atomic_update(struct drm_plane *plane, · line 1444

Quoted 10 lines
drivers/gpu/drm/solomon/ssd16xx.c +6 -3
-	drm_dbg(&device->drm, "plane_atomic_update: fb=%p, initialized=%d\n",
-		fb, device->initialized);
+	drm_dbg(&device->drm, "plane_atomic_update: fb=%p\n", fb);
+
+	if (!fb)
+		return;
 
-	if (!fb || !device->initialized)
+	ret = pm_runtime_resume_and_get(device->drm.dev);
+	if (ret < 0) {

[Severity: High]
Does this call pm_runtime_resume_and_get() from an atomic context?

According to DRM subsystem guidelines, plane atomic update callbacks execute in an atomic context where sleeping is forbidden. Since pm_runtime_resume_and_get() can sleep (and the underlying hardware reset uses usleep_range()), could this lead to kernel warnings or deadlocks?

Quoted 6 lines
drivers/gpu/drm/solomon/ssd16xx.c +2 -0
+		drm_err(&device->drm, "plane_atomic_update: failed to resume: %d\n", ret);
 		return;
+	}
 
 	if (!drm_atomic_helper_damage_merged(old_state, new_state, &rect)) {
 		rect.x1 = 0;

[ ... ]

drivers/gpu/drm/solomon/ssd16xx.c static void ssd16xx_crtc_atomic_flush(struct drm_crtc *crtc, · line 1665

Quoted 6 lines
drivers/gpu/drm/solomon/ssd16xx.c +2 -0
@@ -1620,6 +1661,13 @@ static void ssd16xx_crtc_atomic_flush(struct drm_crtc *crtc,
 	if (!drm_dev_enter(&device->drm, &idx))
 		return;
 
+	ret = pm_runtime_resume_and_get(device->drm.dev);
+	if (ret < 0) {

[Severity: High]
Similar to the plane atomic update, does this call pm_runtime_resume_and_get() from the atomic flush path?

CRTC atomic callbacks also run in an atomic context where sleeping is forbidden, so this sleeping call might trigger kernel warnings or deadlocks.

Quoted 8 lines
drivers/gpu/drm/solomon/ssd16xx.c +5 -0
+		drm_err(&device->drm, "atomic_flush: failed to resume: %d\n", ret);
+		drm_dev_exit(idx);
+		return;
+	}
+
 	drm_dbg(&device->drm, "atomic_flush: reinit, orientation=%u°\n",
 		device->orientation);
 

[ ... ]

Quoted 6 lines
drivers/gpu/drm/solomon/ssd16xx.c +2 -0
@@ -2017,9 +2075,26 @@ static int ssd16xx_probe(struct spi_device *spi)
 
 	drm_mode_config_reset(drm);
 
+	/*
+	 * Mark the device active before enabling runtime PM. The SPI device

drivers/gpu/drm/solomon/ssd16xx.c static int ssd16xx_probe(struct spi_device *spi) · line 2089

Quoted 10 lines
drivers/gpu/drm/solomon/ssd16xx.c +10 -0
+	 * 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);
+

[Severity: Medium]
Is the autosuspend timer started after probe?

The device is marked as RPM_ACTIVE via pm_runtime_set_active(), but it doesn't appear pm_runtime_put_autosuspend() or pm_request_idle() is called. Could this cause the device to linger in the active state until userspace attaches and performs an update?

Quoted 12 lines
drivers/gpu/drm/solomon/ssd16xx.c +6 -1
 	ret = drm_dev_register(drm, 0);
-	if (ret)
+	if (ret) {
+		pm_runtime_disable(dev);
 		return ret;
+	}
+
+	/* Default to runtime sleep mode (RAM retained if supported) */
+	device->deep_sleep_mode = device->controller_cfg->deep_sleep_mode_level1;
 
 	drm_dbg(drm, "SSD16xx e-paper display initialized (%dx%d, %d° rotation)\n",
 		device->width, device->height, device->orientation);

[ ... ]

drivers/gpu/drm/solomon/ssd16xx.c static void ssd16xx_remove(struct spi_device *spi) · line 2115

Quoted 7 lines
drivers/gpu/drm/solomon/ssd16xx.c +2 -0
@@ -2035,6 +2110,8 @@ static void ssd16xx_remove(struct spi_device *spi)
 
 	drm_dev_unplug(&device->drm);
 	drm_atomic_helper_shutdown(&device->drm);
+	pm_runtime_dont_use_autosuspend(&spi->dev);
+	pm_runtime_disable(&spi->dev);
 }

[Severity: Medium]
If the driver is unloaded while the device is runtime active, and clear_on_disable is disabled, does this leave the hardware powered on indefinitely?

Calling pm_runtime_disable() cancels the pending autosuspend timer. Since a synchronous suspend isn't forced before removal here, could the display controller remain physically powered and active after the module is unloaded?

Quoted 2 lines
drivers/gpu/drm/solomon/ssd16xx.c +0 -0
 static void ssd16xx_shutdown(struct spi_device *spi)

[ ... ]

Quoted 9 lines
drivers/gpu/drm/solomon/ssd16xx.c +9 -0
+	if (pm_runtime_status_suspended(dev)) {
+		int err = 0;
+
+		if (device->controller_cfg->deep_sleep_mode_level1 !=
+		    device->controller_cfg->deep_sleep_mode_level2) {
+			dev_dbg(dev, "system suspend: upgrading level1->level2 via HWRESET\n");
+			ssd16xx_hw_reset(device);
+			ssd16xx_wait_for_device(device, &err);
+			ssd16xx_send_cmd(device, SSD16XX_CMD_DEEP_SLEEP_MODE, &err);

drivers/gpu/drm/solomon/ssd16xx.c static void ssd16xx_shutdown(struct spi_device *spi) · line 2176

Quoted 10 lines
drivers/gpu/drm/solomon/ssd16xx.c +10 -0
+			ssd16xx_send_data(device,
+					  device->controller_cfg->deep_sleep_mode_level2,
+					  &err);
+		} else {
+			dev_dbg(dev, "system suspend: already at deepest sleep mode\n");
+		}
+		device->initialized = false;
+		device->pm_force_suspended = false;  /* Skip force_resume on resume */
+		return err;
+	}

[Severity: High]
If an error (like a SPI timeout) occurs when sending the deep sleep command, does returning the error here abort the suspend without rolling back the DRM state?

The DRM suspend state was allocated earlier by drm_mode_config_helper_suspend(drm). If we return err, is that state permanently leaked, leaving the display frozen since the resume callback will be skipped?

drivers/gpu/drm/solomon/ssd16xx.c static void ssd16xx_shutdown(struct spi_device *spi) · line 2183

Quoted 7 lines
drivers/gpu/drm/solomon/ssd16xx.c +7 -0
+
+	/* Force runtime-suspended state for clean resume. */
+	pm_runtime_force_suspend(dev);
+	device->pm_force_suspended = true;  /* Resume must call force_resume */
+
+	return 0;
+}

[Severity: High]
Should the return value of pm_runtime_force_suspend() be checked here?

If it fails, the PM core re-enables runtime PM before returning. If we ignore the error and proceed to call pm_runtime_force_resume() on wake, could the unconditional enable drop disable_depth below zero and trigger kernel warnings?

Quoted 14 lines
drivers/gpu/drm/solomon/ssd16xx.c +14 -0
+
+static int ssd16xx_pm_resume(struct device *dev)
+{
+	struct ssd16xx_device *device = dev_get_drvdata(dev);
+	struct drm_device *drm = &device->drm;
+
+	dev_dbg(dev, "system resume: restoring state\n");
+
+	/*
+	 * Only call force_resume if we actually called force_suspend during
+	 * system suspend. The MODE_1->MODE_2 upgrade path skips force_suspend
+	 * since the device is already RPM_SUSPENDED.
+	 */
+	if (device->pm_force_suspended) {

drivers/gpu/drm/solomon/ssd16xx.c static void ssd16xx_shutdown(struct spi_device *spi) · line 2207

Quoted 10 lines
drivers/gpu/drm/solomon/ssd16xx.c +10 -0
+		pm_runtime_force_resume(dev);
+		device->pm_force_suspended = false;
+	}
+
+	/* Restore Mode 1 (RAM retained) for subsequent runtime PM cycles. */
+	device->deep_sleep_mode = device->controller_cfg->deep_sleep_mode_level1;
+
+	/* Hardware must be re-initialized after waking from system sleep */
+	device->initialized = false;
+

[Severity: Medium]
Does setting device->initialized to false here overwrite the state just initialized by the pm_runtime_force_resume() callback?

Overwriting this might cause the subsequent atomic enable to incorrectly fire a redundant hardware reset, introducing unnecessary sleep delays in the performance-critical system resume path.

Quoted 7 lines
drivers/gpu/drm/solomon/ssd16xx.c +7 -0
+	return drm_mode_config_helper_resume(drm);
+}
+
+static int ssd16xx_pm_runtime_suspend(struct device *dev)
+{
+	struct ssd16xx_device *device = dev_get_drvdata(dev);
+	int err = 0;

drivers/gpu/drm/solomon/ssd16xx.c static void ssd16xx_shutdown(struct spi_device *spi) · line 2224

Quoted 10 lines
drivers/gpu/drm/solomon/ssd16xx.c +10 -0
+
+	dev_dbg(dev, "runtime suspend: entering deep sleep mode 0x%02x\n",
+		device->deep_sleep_mode);
+
+	ssd16xx_send_cmd(device, SSD16XX_CMD_DEEP_SLEEP_MODE, &err);
+	ssd16xx_send_data(device, device->deep_sleep_mode, &err);
+	device->initialized = false;
+
+	return err;
+}

[Severity: High]
If a transient SPI timeout or hardware error occurs, does returning the error code here propagate the failure to the PM core?

This could transition the device to an unrecoverable RPM_ERROR state, causing all future atomic updates to instantly fail and essentially bricking the display until the module is reloaded.

[ ... ]

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=7
#19 lore raw

PATCH v2 8/14 drm/solomon: ssd16xx: Expose refresh mode as plane property

In reply to Devarsh Thakkar

Add a DRM plane property "refresh_mode" allowing userspace to select between partial, full and fast display update modes at runtime.

E-paper displays support multiple refresh strategies with different tradeoffs:
- partial: fastest updates (~300-500ms), minimal flicker, ghosting accumulates over time
- full: cleanest image (~1.5-2s), full black-white cycle eliminates ghosting, most flicker
- fast: intermediate (~1s), skips LUT reload per frame (loaded once at init)

The property uses an enum with values matching the internal ssd16xx_refresh_mode enum. The default is set from the panel configuration (typically "partial" for interactive use).

---
Changes from v1:
- New patch: Extracted refresh mode plane into dedicated patch as plane property

 drivers/gpu/drm/solomon/ssd16xx.c | 140 +++++++++++++++++++++++++++++-
 1 file changed, 139 insertions(+), 1 deletion(-)
drivers/gpu/drm/solomon/ssd16xx.c +139 -1
index 6255f434248a..dc2b8fe536af 100644
@@ -400,8 +400,20 @@ struct ssd16xx_device {
 	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 *refresh_mode_property;
 };
 
+/* Plane state subclass: refresh_mode lives on the plane, not the connector,
+ * so plane_atomic_update() can diff against its own old/new state.
+ */
+struct ssd16xx_plane_state {
+	struct drm_shadow_plane_state base;
+	enum ssd16xx_refresh_mode refresh_mode; /* Pending refresh mode */
+};
+
+#define to_ssd16xx_plane_state(s) \
+	container_of((s), struct ssd16xx_plane_state, base.base)
+
 static inline struct ssd16xx_device *to_ssd16xx_device(struct drm_device *drm)
 {
 	return container_of(drm, struct ssd16xx_device, drm);
@@ -1397,11 +1409,91 @@ static void ssd16xx_plane_destroy(struct drm_plane *plane)
 	drm_plane_cleanup(plane);
 }
 
+static void ssd16xx_plane_atomic_destroy_state(struct drm_plane *plane,
+					       struct drm_plane_state *state);
+
+static struct drm_plane_state *ssd16xx_plane_atomic_create_state(struct drm_plane *plane)
+{
+	struct ssd16xx_device *device = plane_to_ssd16xx_device(plane);
+	struct ssd16xx_plane_state *ssd_state;
+
+	ssd_state = kzalloc(sizeof(*ssd_state), GFP_KERNEL);
+	if (!ssd_state)
+		return ERR_PTR(-ENOMEM);
+
+	__drm_gem_shadow_plane_state_init(plane, &ssd_state->base);
+	ssd_state->refresh_mode = device->refresh_mode;
+
+	return &ssd_state->base.base;
+}
+
+static struct drm_plane_state *
+ssd16xx_plane_atomic_duplicate_state(struct drm_plane *plane)
+{
+	struct ssd16xx_plane_state *old_ssd_state, *ssd_state;
+
+	if (WARN_ON(!plane->state))
+		return NULL;
+
+	old_ssd_state = to_ssd16xx_plane_state(plane->state);
+	ssd_state = kmemdup(old_ssd_state, sizeof(*ssd_state), GFP_KERNEL);
+	if (!ssd_state)
+		return NULL;
+
+	__drm_gem_duplicate_shadow_plane_state(plane, &ssd_state->base);
+	return &ssd_state->base.base;
+}
+
+static void ssd16xx_plane_atomic_destroy_state(struct drm_plane *plane,
+					       struct drm_plane_state *state)
+{
+	struct ssd16xx_plane_state *ssd_state = to_ssd16xx_plane_state(state);
+
+	__drm_gem_destroy_shadow_plane_state(&ssd_state->base);
+	kfree(ssd_state);
+}
+
+static int ssd16xx_plane_atomic_get_property(struct drm_plane *plane,
+					     const struct drm_plane_state *state,
+					     struct drm_property *property,
+					     uint64_t *val)
+{
+	struct ssd16xx_device *device = plane_to_ssd16xx_device(plane);
+	const struct ssd16xx_plane_state *ssd_state = to_ssd16xx_plane_state(state);
+
+	if (property == device->refresh_mode_property) {
+		*val = ssd_state->refresh_mode;
+		return 0;
+	}
+	return -EINVAL;
+}
+
+static int ssd16xx_plane_atomic_set_property(struct drm_plane *plane,
+					     struct drm_plane_state *state,
+					     struct drm_property *property,
+					     uint64_t val)
+{
+	struct ssd16xx_device *device = plane_to_ssd16xx_device(plane);
+	struct ssd16xx_plane_state *ssd_state = to_ssd16xx_plane_state(state);
+
+	if (property == device->refresh_mode_property) {
+		if (val > SSD16XX_REFRESH_FAST)
+			return -EINVAL;
+		ssd_state->refresh_mode = val;
+		return 0;
+	}
+	return -EINVAL;
+}
+
 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,
-	DRM_GEM_SHADOW_PLANE_FUNCS,
+	.atomic_create_state = ssd16xx_plane_atomic_create_state,
+	.atomic_duplicate_state = ssd16xx_plane_atomic_duplicate_state,
+	.atomic_destroy_state = ssd16xx_plane_atomic_destroy_state,
+	.atomic_get_property = ssd16xx_plane_atomic_get_property,
+	.atomic_set_property = ssd16xx_plane_atomic_set_property,
 };
 
 static int ssd16xx_plane_atomic_check(struct drm_plane *plane,
@@ -1429,6 +1521,8 @@ static void ssd16xx_plane_atomic_update(struct drm_plane *plane,
 	struct drm_plane_state *new_state = drm_atomic_get_new_plane_state(state, plane);
 	struct drm_shadow_plane_state *shadow_state = to_drm_shadow_plane_state(new_state);
 	struct ssd16xx_device *device = plane_to_ssd16xx_device(plane);
+	struct ssd16xx_plane_state *new_ssd = to_ssd16xx_plane_state(new_state);
+	struct ssd16xx_plane_state *old_ssd = to_ssd16xx_plane_state(old_state);
 	enum ssd16xx_refresh_mode saved_mode;
 	u8 saved_border_waveform_idx;
 	struct drm_framebuffer *fb = new_state->fb;
@@ -1456,6 +1550,25 @@ static void ssd16xx_plane_atomic_update(struct drm_plane *plane,
 
 	drm_dbg(&device->drm, "plane_atomic_update: calling fb_dirty rect=(%d,%d)-(%d,%d)\n",
 		rect.x1, rect.y1, rect.x2, rect.y2);
+
+	/* Diff refresh_mode against the plane's own previous state to see
+	 * whether fast LUT preload is needed.
+	 */
+	if (old_ssd->refresh_mode != new_ssd->refresh_mode) {
+		/*
+		 * Fast refresh omits LOAD_LUT each update; arm the
+		 * one-shot flag so LUT is pre-loaded before the first
+		 * fast frame.
+		 */
+		if (new_ssd->refresh_mode == SSD16XX_REFRESH_FAST &&
+		    old_ssd->refresh_mode != SSD16XX_REFRESH_FULL)
+			device->fast_lut_pending = true;
+		else
+			device->fast_lut_pending = false;
+	}
+
+	device->refresh_mode = new_ssd->refresh_mode;
+
 	/*
 	 * When refresh_mode_init was set, use the specified mode for this first
 	 * frame only, then restore the user-configured refresh_mode so
@@ -1753,6 +1866,28 @@ static const struct drm_connector_helper_funcs ssd16xx_connector_helper_funcs =
 	.get_modes = ssd16xx_connector_get_modes,
 };
 
+static const struct drm_prop_enum_list ssd16xx_refresh_mode_enum[] = {
+	{ SSD16XX_REFRESH_PARTIAL, "partial" },
+	{ SSD16XX_REFRESH_FULL,    "full"    },
+	{ SSD16XX_REFRESH_FAST,    "fast"    },
+};
+
+static int ssd16xx_plane_create_properties(struct ssd16xx_device *device)
+{
+	struct drm_device *drm = &device->drm;
+	struct drm_mode_object *plane_obj = &device->primary_plane.base;
+
+	device->refresh_mode_property =
+		drm_property_create_enum(drm, 0, "refresh_mode",
+					 ssd16xx_refresh_mode_enum,
+					 ARRAY_SIZE(ssd16xx_refresh_mode_enum));
+	if (!device->refresh_mode_property)
+		return -ENOMEM;
+	drm_object_attach_property(plane_obj,
+				   device->refresh_mode_property,
+				   device->refresh_mode);
+	return 0;
+}
 static const struct drm_connector_funcs ssd16xx_connector_funcs = {
 	.reset = drm_atomic_helper_connector_reset,
 	.fill_modes = drm_helper_probe_single_connector_modes,
@@ -2073,6 +2208,9 @@ static int ssd16xx_probe(struct spi_device *spi)
 	if (ret)
 		return ret;
 
+	ret = ssd16xx_plane_create_properties(device);
+	if (ret)
+		return ret;
 	drm_mode_config_reset(drm);
 
 	/*
Signature · 1 line
2.39.1
#20 lore raw

Re: [PATCH v2 08/14] drm/solomon: ssd16xx: Expose refresh mode as plane property

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider: - [High] drm/ssd16xx: hardware desync when refresh_mode is updated on disabled plane - [High] drm/ssd16xx…

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] drm/ssd16xx: hardware desync when refresh_mode is updated on disabled plane
- [High] drm/ssd16xx: double vunmap risk due to kmemdup of shadow plane state
- [Low] drm/ssd16xx: build failure due to undefined __drm_gem_shadow_plane_state_init --

commit f164596b389c43d5ba1998c30d9e04e66112e4d1
Author: Devarsh Thakkar <[email protected]>

drm/solomon: ssd16xx: Expose refresh mode as plane property

This commit adds a DRM plane property "refresh_mode" to allow userspace to select between partial, full, and fast display update modes at runtime. It uses an enum to match the internal refresh modes and updates the default setting from the panel configuration.

Quoted 4 lines
drivers/gpu/drm/solomon/ssd16xx.c +0 -0
index 6255f434248ae..dc2b8fe536af3 100644

[ ... ]

Quoted 7 lines
drivers/gpu/drm/solomon/ssd16xx.c +3 -0
@@ -1397,11 +1409,91 @@ static void ssd16xx_plane_destroy(struct drm_plane *plane)
 	drm_plane_cleanup(plane);
 }
 
+static void ssd16xx_plane_atomic_destroy_state(struct drm_plane *plane,
+					       struct drm_plane_state *state);
+

drivers/gpu/drm/solomon/ssd16xx.c static void ssd16xx_plane_destroy(struct drm_plane *plane) · line 1424

Quoted 10 lines
drivers/gpu/drm/solomon/ssd16xx.c +10 -0
+static struct drm_plane_state *ssd16xx_plane_atomic_create_state(struct drm_plane *plane)
+{
+	struct ssd16xx_device *device = plane_to_ssd16xx_device(plane);
+	struct ssd16xx_plane_state *ssd_state;
+
+	ssd_state = kzalloc(sizeof(*ssd_state), GFP_KERNEL);
+	if (!ssd_state)
+		return ERR_PTR(-ENOMEM);
+
+	__drm_gem_shadow_plane_state_init(plane, &ssd_state->base);

[Severity: Low]
Does this cause a build failure? It appears __drm_gem_shadow_plane_state_init() is not defined anywhere in the kernel.

If the module were somehow compiled, would failing to initialize the base plane state leave plane_state->plane as NULL and cause a NULL pointer dereference in the core DRM atomic helpers?

Quoted 5 lines
drivers/gpu/drm/solomon/ssd16xx.c +5 -0
+	ssd_state->refresh_mode = device->refresh_mode;
+
+	return &ssd_state->base.base;
+}
+

drivers/gpu/drm/solomon/ssd16xx.c static void ssd16xx_plane_destroy(struct drm_plane *plane) · line 1439

Quoted 10 lines
drivers/gpu/drm/solomon/ssd16xx.c +10 -0
+static struct drm_plane_state *
+ssd16xx_plane_atomic_duplicate_state(struct drm_plane *plane)
+{
+	struct ssd16xx_plane_state *old_ssd_state, *ssd_state;
+
+	if (WARN_ON(!plane->state))
+		return NULL;
+
+	old_ssd_state = to_ssd16xx_plane_state(plane->state);
+	ssd_state = kmemdup(old_ssd_state, sizeof(*ssd_state), GFP_KERNEL);

[Severity: High]
Does this kmemdup() risk a double free or double unmap of the shadow plane state?

The struct drm_shadow_plane_state contains map and data arrays which track active kernel memory mappings of the framebuffer. These are explicitly documented as "Transitional state - do not export or duplicate".

If the new state encounters an error before drm_gem_begin_shadow_fb_access() overwrites them, won't the error cleanup paths unmap those stale pointers, resulting in a double vunmap of the old state's memory?

Quoted 6 lines
drivers/gpu/drm/solomon/ssd16xx.c +6 -0
+	if (!ssd_state)
+		return NULL;
+
+	__drm_gem_duplicate_shadow_plane_state(plane, &ssd_state->base);
+	return &ssd_state->base.base;
+}

[ ... ]

Quoted 12 lines
drivers/gpu/drm/solomon/ssd16xx.c +8 -0
@@ -1456,6 +1550,25 @@ static void ssd16xx_plane_atomic_update(struct drm_plane *plane,
 
 	drm_dbg(&device->drm, "plane_atomic_update: calling fb_dirty rect=(%d,%d)-(%d,%d)\n",
 		rect.x1, rect.y1, rect.x2, rect.y2);
+
+	/* Diff refresh_mode against the plane's own previous state to see
+	 * whether fast LUT preload is needed.
+	 */
+	if (old_ssd->refresh_mode != new_ssd->refresh_mode) {
+		/*
+		 * Fast refresh omits LOAD_LUT each update; arm the
+		 * one-shot flag so LUT is pre-loaded before the first

drivers/gpu/drm/solomon/ssd16xx.c static void ssd16xx_plane_atomic_update(struct drm_plane *plane, · line 1570

Quoted 10 lines
drivers/gpu/drm/solomon/ssd16xx.c +10 -0
+		 * fast frame.
+		 */
+		if (new_ssd->refresh_mode == SSD16XX_REFRESH_FAST &&
+		    old_ssd->refresh_mode != SSD16XX_REFRESH_FULL)
+			device->fast_lut_pending = true;
+		else
+			device->fast_lut_pending = false;
+	}
+
+	device->refresh_mode = new_ssd->refresh_mode;

[Severity: High]
Can this hardware configuration desynchronize from the software state, leading to a missing LUT preload and corrupted display output?

Earlier in ssd16xx_plane_atomic_update(), there is an early return when the plane is being disabled:

    if (!fb)
        return;

If a userspace atomic commit disables the plane while simultaneously changing the refresh_mode property, won't this early return skip the state synchronization here?

When the plane is later enabled, wouldn't pm_runtime_resume() call ssd16xx_hw_init(), read the stale device->refresh_mode, and skip the required FAST mode LUT preload?

Quoted 4 lines
drivers/gpu/drm/solomon/ssd16xx.c +1 -0
+
 	/*
 	 * When refresh_mode_init was set, use the specified mode for this first
 	 * frame only, then restore the user-configured refresh_mode so
Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=8
#21 lore raw

PATCH v2 9/14 drm/solomon: ssd16xx: Expose color mode as plane property

In reply to Devarsh Thakkar

Add a DRM plane property "color_mode" for panels that support 3-color (black/white/red) display modes.

The property allows runtime selection between:
- bw (black/white): R1 format represents black (0) and white (1)
- 3color: R1 format represents red ink (1) on white background

This property is only created on panels with red_supported = true. The default is set from the panel configuration's default_color_mode field (typically BW for monochrome panels).

---
Changes from v1:
- Extracted color mode property into dedicated patch as plane property

 drivers/gpu/drm/solomon/ssd16xx.c | 42 +++++++++++++++++++++++++++++++
 1 file changed, 42 insertions(+)
drivers/gpu/drm/solomon/ssd16xx.c +42 -0
index dc2b8fe536af..3815af0cee5c 100644
@@ -401,6 +401,7 @@ struct ssd16xx_device {
 
 	struct drm_framebuffer *last_fb;        /* Last drawn FB for reinit redraws */
 	struct drm_property *refresh_mode_property;
+	struct drm_property *color_mode_property;
 };
 
 /* Plane state subclass: refresh_mode lives on the plane, not the connector,
@@ -409,6 +410,7 @@ struct ssd16xx_device {
 struct ssd16xx_plane_state {
 	struct drm_shadow_plane_state base;
 	enum ssd16xx_refresh_mode refresh_mode; /* Pending refresh mode */
+	enum ssd16xx_color_mode color_mode;     /* Pending color mode */
 };
 
 #define to_ssd16xx_plane_state(s) \
@@ -1423,6 +1425,7 @@ static struct drm_plane_state *ssd16xx_plane_atomic_create_state(struct drm_plan
 
 	__drm_gem_shadow_plane_state_init(plane, &ssd_state->base);
 	ssd_state->refresh_mode = device->refresh_mode;
+	ssd_state->color_mode = device->color_mode;
 
 	return &ssd_state->base.base;
 }
@@ -1465,6 +1468,10 @@ static int ssd16xx_plane_atomic_get_property(struct drm_plane *plane,
 		*val = ssd_state->refresh_mode;
 		return 0;
 	}
+	if (property == device->color_mode_property) {
+		*val = ssd_state->color_mode;
+		return 0;
+	}
 	return -EINVAL;
 }
 
@@ -1482,6 +1489,18 @@ static int ssd16xx_plane_atomic_set_property(struct drm_plane *plane,
 		ssd_state->refresh_mode = val;
 		return 0;
 	}
+	if (property == device->color_mode_property) {
+		if (val > SSD16XX_COLOR_MODE_3COLOR)
+			return -EINVAL;
+		if (val == SSD16XX_COLOR_MODE_3COLOR &&
+		    !device->device_cfg->red_supported) {
+			drm_dbg(&device->drm,
+				"3-color not supported by this panel\n");
+			return -EINVAL;
+		}
+		ssd_state->color_mode = val;
+		return 0;
+	}
 	return -EINVAL;
 }
 
@@ -1568,6 +1587,7 @@ static void ssd16xx_plane_atomic_update(struct drm_plane *plane,
 	}
 
 	device->refresh_mode = new_ssd->refresh_mode;
+	device->color_mode = new_ssd->color_mode;
 
 	/*
 	 * When refresh_mode_init was set, use the specified mode for this first
@@ -1872,6 +1892,11 @@ static const struct drm_prop_enum_list ssd16xx_refresh_mode_enum[] = {
 	{ SSD16XX_REFRESH_FAST,    "fast"    },
 };
 
+static const struct drm_prop_enum_list ssd16xx_color_mode_enum[] = {
+	{ SSD16XX_COLOR_MODE_BW,     "black-white" },
+	{ SSD16XX_COLOR_MODE_3COLOR, "3-color"     },
+};
+
 static int ssd16xx_plane_create_properties(struct ssd16xx_device *device)
 {
 	struct drm_device *drm = &device->drm;
@@ -1886,6 +1911,23 @@ static int ssd16xx_plane_create_properties(struct ssd16xx_device *device)
 	drm_object_attach_property(plane_obj,
 				   device->refresh_mode_property,
 				   device->refresh_mode);
+
+	/*
+	 * Color mode: for BW-only panels the property exists but only
+	 * "black-white" is selectable; the 3-color option is restricted
+	 * to panels with a physical red plane.
+	 */
+	device->color_mode_property =
+		drm_property_create_enum(drm, 0, "color_mode",
+					 ssd16xx_color_mode_enum,
+					 device->device_cfg->red_supported
+						? ARRAY_SIZE(ssd16xx_color_mode_enum)
+						: 1);
+	if (!device->color_mode_property)
+		return -ENOMEM;
+	drm_object_attach_property(plane_obj,
+				   device->color_mode_property,
+				   device->color_mode);
 	return 0;
 }
 static const struct drm_connector_funcs ssd16xx_connector_funcs = {
Signature · 1 line
2.39.1
#22 lore raw

Re: [PATCH v2 09/14] drm/solomon: ssd16xx: Expose color mode as plane property

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider: - [High] drm/solomon: ssd16xx: 3-color mode with partial refresh corrupts R1 pixels - [Medium] drm/solomon…

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] drm/solomon: ssd16xx: 3-color mode with partial refresh corrupts R1 pixels
- [Medium] drm/solomon: ssd16xx: Commit message contradicts color_mode property creation --

commit 50bd4d2129cba69722affa1695b7f8c68a0578a8
Author: Devarsh Thakkar <[email protected]>

drm/solomon: ssd16xx: Expose color mode as plane property

This commit introduces a new DRM plane property called "color_mode" to support panels with 3-color (black/white/red) display modes. It allows switching between standard black/white representation and a 3-color representation where the R1 format designates red ink on a white background.

Quoted 8 lines

Add a DRM plane property "color_mode" for panels that support 3-color (black/white/red) display modes.

The property allows runtime selection between:
- bw (black/white): R1 format represents black (0) and white (1)
- 3color: R1 format represents red ink (1) on white background

This property is only created on panels with red_supported = true.

[Severity: Medium]
The commit message states the property is only created on panels where red is supported.

However, the code in ssd16xx_plane_create_properties() appears to create it unconditionally for all panels (while restricting the selectable enum options if unsupported).

Is this description accurate?

Quoted 4 lines

The default is set from the panel configuration's default_color_mode field (typically BW for monochrome panels).

drivers/gpu/drm/solomon/ssd16xx.c +0 -0
index dc2b8fe536af3..3815af0cee5c5 100644

[ ... ]
@@ -1568,6 +1587,7 @@ static void ssd16xx_plane_atomic_update(struct drm_plane *plane,
}

device->refresh_mode = new_ssd->refresh_mode;
+ device->color_mode = new_ssd->color_mode;

[Severity: High]
When a user sets color_mode to 3COLOR and refresh_mode to PARTIAL, device->color_mode is updated here before calling ssd16xx_fb_dirty().

However, looking at ssd16xx_fb_dirty():

drivers/gpu/drm/solomon/ssd16xx.c:ssd16xx_fb_dirty() {
    ...
    if (device->color_mode == SSD16XX_COLOR_MODE_3COLOR &&
        (device->refresh_mode == SSD16XX_REFRESH_FULL ||
         device->refresh_mode == SSD16XX_REFRESH_FAST)) {
            red_buffer = device->tx_red_buf;
    }
    ...
    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, device->orientation);
    ...
}

Because red_buffer is only initialized for FULL or FAST refresh modes, a PARTIAL refresh in 3-color mode leaves red_buffer as NULL.

This triggers the fallback to ssd16xx_convert_fb_to_1bpp(), which copies R1 bits directly and interprets them as luma (0=black, 1=white) instead of the 3-color expectation (0=white, 1=red).

Can this lead to corrupted R1 pixel colors during a partial refresh?

/*
* When refresh_mode_init was set, use the specified mode for this first
[ ... ]

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=9
#23 lore raw

PATCH v2 10/14 drm/solomon: ssd16xx: Expose session management as plane properties

In reply to Devarsh Thakkar

Add DRM plane properties for the clear_on_* and refresh_mode_init session management flags, allowing userspace to control automatic display clear/refresh behaviour at lifecycle transitions:

- clear_on_init: clear display when a new DRM master opens
- clear_on_close: clear display when the displaying client exits
- clear_on_disable: clear display when CRTC is disabled
- refresh_mode_init: override refresh mode for the first frame after a new master opens (e.g. force full refresh to clear ghosting before normal partial updates)

Each property is an enum with values:
disabled / partial / full / fast

These map to the internal representation (-1 for disabled, 0/1/2 for partial/full/fast refresh modes respectively).

Defaults are seeded from the panel configuration at probe time and can be adjusted at runtime via standard DRM property interfaces (e.g. modetest, drm_info, or compositor property APIs).

---
Changes from v1:
- New patch: Extracted session management properties into dedicated patch as plane properties

 drivers/gpu/drm/solomon/ssd16xx.c | 134 ++++++++++++++++++++++++++++++
 1 file changed, 134 insertions(+)
drivers/gpu/drm/solomon/ssd16xx.c +134 -0
index 3815af0cee5c..6331f367d864 100644
@@ -205,6 +205,18 @@ enum ssd16xx_color_mode {
 	SSD16XX_COLOR_MODE_3COLOR = 1, /* 3-colour BWR; RED RAM used for red pixels */
 };
 
+/*
+ * Clear/refresh-mode-init session property values exposed to userspace.
+ * "disabled" means the action is skipped; the others select which refresh
+ * mode to use for the clear/init operation.
+ */
+enum ssd16xx_session_mode {
+	SSD16XX_SESSION_DISABLED = 0,
+	SSD16XX_SESSION_PARTIAL,
+	SSD16XX_SESSION_FULL,
+	SSD16XX_SESSION_FAST,
+};
+
 /* Border waveform enum indices (0-9); mapped to HW bytes via
  * controller_cfg->border_waveform_table[]
  */
@@ -402,6 +414,10 @@ struct ssd16xx_device {
 	struct drm_framebuffer *last_fb;        /* Last drawn FB for reinit redraws */
 	struct drm_property *refresh_mode_property;
 	struct drm_property *color_mode_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;
 };
 
 /* Plane state subclass: refresh_mode lives on the plane, not the connector,
@@ -411,6 +427,10 @@ struct ssd16xx_plane_state {
 	struct drm_shadow_plane_state base;
 	enum ssd16xx_refresh_mode refresh_mode; /* Pending refresh mode */
 	enum ssd16xx_color_mode color_mode;     /* Pending color mode */
+	int clear_on_init;    /* Session: -1=disabled, 0=partial, 1=full, 2=fast */
+	int clear_on_close;
+	int clear_on_disable;
+	int refresh_mode_init;
 };
 
 #define to_ssd16xx_plane_state(s) \
@@ -1426,6 +1446,10 @@ static struct drm_plane_state *ssd16xx_plane_atomic_create_state(struct drm_plan
 	__drm_gem_shadow_plane_state_init(plane, &ssd_state->base);
 	ssd_state->refresh_mode = device->refresh_mode;
 	ssd_state->color_mode = device->color_mode;
+	ssd_state->clear_on_init = device->clear_on_init;
+	ssd_state->clear_on_close = device->clear_on_close;
+	ssd_state->clear_on_disable = device->clear_on_disable;
+	ssd_state->refresh_mode_init = device->refresh_mode_init;
 
 	return &ssd_state->base.base;
 }
@@ -1456,6 +1480,24 @@ static void ssd16xx_plane_atomic_destroy_state(struct drm_plane *plane,
 	kfree(ssd_state);
 }
 
+/*
+ * Convert internal clear_on / refresh_mode_init value (-1=disabled, 0/1/2=mode)
+ * to the userspace-facing enum (0=disabled, 1=partial, 2=full, 3=fast).
+ */
+static inline unsigned int ssd16xx_internal_to_session_prop(int internal_val)
+{
+	if (internal_val < 0)
+		return SSD16XX_SESSION_DISABLED;
+	return (unsigned int)internal_val + 1;
+}
+
+static inline int ssd16xx_session_prop_to_internal(uint64_t prop_val)
+{
+	if (prop_val == SSD16XX_SESSION_DISABLED)
+		return -1;
+	return (int)prop_val - 1;
+}
+
 static int ssd16xx_plane_atomic_get_property(struct drm_plane *plane,
 					     const struct drm_plane_state *state,
 					     struct drm_property *property,
@@ -1472,6 +1514,22 @@ static int ssd16xx_plane_atomic_get_property(struct drm_plane *plane,
 		*val = ssd_state->color_mode;
 		return 0;
 	}
+	if (property == device->clear_on_init_property) {
+		*val = ssd16xx_internal_to_session_prop(ssd_state->clear_on_init);
+		return 0;
+	}
+	if (property == device->clear_on_close_property) {
+		*val = ssd16xx_internal_to_session_prop(ssd_state->clear_on_close);
+		return 0;
+	}
+	if (property == device->clear_on_disable_property) {
+		*val = ssd16xx_internal_to_session_prop(ssd_state->clear_on_disable);
+		return 0;
+	}
+	if (property == device->refresh_mode_init_property) {
+		*val = ssd16xx_internal_to_session_prop(ssd_state->refresh_mode_init);
+		return 0;
+	}
 	return -EINVAL;
 }
 
@@ -1501,6 +1559,30 @@ static int ssd16xx_plane_atomic_set_property(struct drm_plane *plane,
 		ssd_state->color_mode = val;
 		return 0;
 	}
+	if (property == device->clear_on_init_property) {
+		if (val > SSD16XX_SESSION_FAST)
+			return -EINVAL;
+		ssd_state->clear_on_init = ssd16xx_session_prop_to_internal(val);
+		return 0;
+	}
+	if (property == device->clear_on_close_property) {
+		if (val > SSD16XX_SESSION_FAST)
+			return -EINVAL;
+		ssd_state->clear_on_close = ssd16xx_session_prop_to_internal(val);
+		return 0;
+	}
+	if (property == device->clear_on_disable_property) {
+		if (val > SSD16XX_SESSION_FAST)
+			return -EINVAL;
+		ssd_state->clear_on_disable = ssd16xx_session_prop_to_internal(val);
+		return 0;
+	}
+	if (property == device->refresh_mode_init_property) {
+		if (val > SSD16XX_SESSION_FAST)
+			return -EINVAL;
+		ssd_state->refresh_mode_init = ssd16xx_session_prop_to_internal(val);
+		return 0;
+	}
 	return -EINVAL;
 }
 
@@ -1588,6 +1670,10 @@ static void ssd16xx_plane_atomic_update(struct drm_plane *plane,
 
 	device->refresh_mode = new_ssd->refresh_mode;
 	device->color_mode = new_ssd->color_mode;
+	device->clear_on_init = new_ssd->clear_on_init;
+	device->clear_on_close = new_ssd->clear_on_close;
+	device->clear_on_disable = new_ssd->clear_on_disable;
+	device->refresh_mode_init = new_ssd->refresh_mode_init;
 
 	/*
 	 * When refresh_mode_init was set, use the specified mode for this first
@@ -1897,6 +1983,13 @@ static const struct drm_prop_enum_list ssd16xx_color_mode_enum[] = {
 	{ SSD16XX_COLOR_MODE_3COLOR, "3-color"     },
 };
 
+static const struct drm_prop_enum_list ssd16xx_session_mode_enum[] = {
+	{ SSD16XX_SESSION_DISABLED, "disabled" },
+	{ SSD16XX_SESSION_PARTIAL,  "partial"  },
+	{ SSD16XX_SESSION_FULL,     "full"     },
+	{ SSD16XX_SESSION_FAST,     "fast"     },
+};
+
 static int ssd16xx_plane_create_properties(struct ssd16xx_device *device)
 {
 	struct drm_device *drm = &device->drm;
@@ -1928,6 +2021,47 @@ static int ssd16xx_plane_create_properties(struct ssd16xx_device *device)
 	drm_object_attach_property(plane_obj,
 				   device->color_mode_property,
 				   device->color_mode);
+
+	device->clear_on_init_property =
+		drm_property_create_enum(drm, 0, "clear_on_init",
+					 ssd16xx_session_mode_enum,
+					 ARRAY_SIZE(ssd16xx_session_mode_enum));
+	if (!device->clear_on_init_property)
+		return -ENOMEM;
+	drm_object_attach_property(plane_obj,
+				   device->clear_on_init_property,
+				   ssd16xx_internal_to_session_prop(device->clear_on_init));
+
+	device->clear_on_close_property =
+		drm_property_create_enum(drm, 0, "clear_on_close",
+					 ssd16xx_session_mode_enum,
+					 ARRAY_SIZE(ssd16xx_session_mode_enum));
+	if (!device->clear_on_close_property)
+		return -ENOMEM;
+	drm_object_attach_property(plane_obj,
+				   device->clear_on_close_property,
+				   ssd16xx_internal_to_session_prop(device->clear_on_close));
+
+	device->clear_on_disable_property =
+		drm_property_create_enum(drm, 0, "clear_on_disable",
+					 ssd16xx_session_mode_enum,
+					 ARRAY_SIZE(ssd16xx_session_mode_enum));
+	if (!device->clear_on_disable_property)
+		return -ENOMEM;
+	drm_object_attach_property(plane_obj,
+				   device->clear_on_disable_property,
+				   ssd16xx_internal_to_session_prop(device->clear_on_disable));
+
+	device->refresh_mode_init_property =
+		drm_property_create_enum(drm, 0, "refresh_mode_init",
+					 ssd16xx_session_mode_enum,
+					 ARRAY_SIZE(ssd16xx_session_mode_enum));
+	if (!device->refresh_mode_init_property)
+		return -ENOMEM;
+	drm_object_attach_property(plane_obj,
+				   device->refresh_mode_init_property,
+				   ssd16xx_internal_to_session_prop(device->refresh_mode_init));
+
 	return 0;
 }
 static const struct drm_connector_funcs ssd16xx_connector_funcs = {
Signature · 1 line
2.39.1
#24 lore raw

Re: [PATCH v2 10/14] drm/solomon: ssd16xx: Expose session management as plane properties

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider: - [Medium] drm/solomon: stale plane properties used in CRTC hooks due to sync ordering - [Medium]…

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] drm/solomon: stale plane properties used in CRTC hooks due to sync ordering
- [Medium] drm/solomon: unprotected concurrent access to global device properties --

commit 16a8f4fa23daecbcef4ac45581fba368964d7807
Author: Devarsh Thakkar <[email protected]>

drm/solomon: ssd16xx: Expose session management as plane properties

This commit adds DRM plane properties for session management flags, allowing userspace to control automatic display clear/refresh behavior at lifecycle transitions.

Quoted 4 lines
drivers/gpu/drm/solomon/ssd16xx.c +0 -0
index 3815af0cee5c5..6331f367d8649 100644

[ ... ]

drivers/gpu/drm/solomon/ssd16xx.c static void ssd16xx_plane_atomic_update(struct drm_plane *plane, · line 1676

Quoted 8 lines
drivers/gpu/drm/solomon/ssd16xx.c +4 -0
@@ -1588,6 +1670,10 @@ static void ssd16xx_plane_atomic_update(struct drm_plane *plane,
 
 	device->refresh_mode = new_ssd->refresh_mode;
 	device->color_mode = new_ssd->color_mode;
+	device->clear_on_init = new_ssd->clear_on_init;
+	device->clear_on_close = new_ssd->clear_on_close;
+	device->clear_on_disable = new_ssd->clear_on_disable;
+	device->refresh_mode_init = new_ssd->refresh_mode_init;

[Severity: Medium]
Since ssd16xx_plane_atomic_update() returns early if there is no framebuffer:

static void ssd16xx_plane_atomic_update(...)

{
        ...
        if (!fb)
                return;

Will these device property assignments be skipped entirely when the plane is being disabled?

Additionally, doesn't drm_atomic_helper_commit_tail_rpm() call the CRTC enable and disable hooks before the plane update? If a user submits an atomic commit that updates clear_on_disable and disables the CRTC at the same time, won't ssd16xx_crtc_atomic_disable() read the stale global device property before this plane update has a chance to synchronize the new atomic state?

[Severity: Medium]
Is it safe to write to global device properties like device->clear_on_close here? This code executes under DRM modeset locks, but if another client closes its DRM file descriptor, wouldn't ssd16xx_drm_master_drop() read these same properties under drm_device->master_mutex?

Since they don't share locks, could this lead to a data race where the master drop path reads torn or stale values while an atomic commit is processing?

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=10
#25 lore raw

PATCH v2 11/14 drm/solomon: ssd16xx: support panels whose RAM X order is reversed

In reply to Devarsh Thakkar

From: LiangCheng Wang <[email protected]>

Some panels have their first column wired to the controller's last source output, so the controller scans RAM X in the opposite direction and the image comes out horizontally mirrored. SSD1677 offers no source-direction control that I could find - command 0x01 only affects the gate side - so it has to be handled while packing the framebuffer.

Add an x_mirror flag to struct ssd16xx_device_config and honour it in both branches of ssd16xx_convert_fb_to_1bpp(): the scanline order in the landscape path, and the inner loop in the portrait one, which is what maps to RAM X there. The DRM_FORMAT_R1 memcpy fast path cannot mirror while copying, so it is skipped when the flag is set. Enable it for the pixpaper-426m entry.

Tested on a Mayqueen PIXPAPER 4.26m (SSD1677, 800x480): a pattern with four differently sized corner blocks renders correctly at rotation 0, and a DT rotation of 90 gives a clean clockwise portrait. Doing only the landscape path leaves the portrait output transposed.

---
Changes from v1:
- New patch introduced to prepare base for pixpaper

 drivers/gpu/drm/solomon/ssd16xx.c | 22 ++++++++++++++++------
 1 file changed, 16 insertions(+), 6 deletions(-)
drivers/gpu/drm/solomon/ssd16xx.c +16 -6
index 6331f367d864..8fe09dca589e 100644
@@ -358,6 +358,9 @@ struct ssd16xx_device_config {
 	const u8 *booster_soft_start_data;
 	u8        booster_soft_start_len;
 
+	/* Controller scans this panel's RAM X in the reverse direction. */
+	bool x_mirror;
+
 	/* Panel-specific display mode (resolution and physical dimensions) */
 	const struct drm_display_mode *mode;
 };
@@ -513,6 +516,7 @@ static const struct ssd16xx_device_config ssd16xx_device_configs[] = {
 		.default_clear_on_disable = -1,
 		.default_refresh_mode_init = SSD16XX_REFRESH_FULL,
 		.red_supported = false,  /* 2-colour black/white panel */
+		.x_mirror = false,
 		.default_color_mode = SSD16XX_COLOR_MODE_BW,
 		.mode = &gdey042t81_mode,
 	},
@@ -1121,10 +1125,11 @@ static void ssd16xx_convert_fb_to_3color(u8 *bw_dst, u8 *red_dst,
 static void ssd16xx_convert_fb_to_1bpp(u8 *dst, struct iosys_map *src,
 				       struct drm_framebuffer *fb,
 				       struct drm_rect *rect,
-				       unsigned int orientation)
+				       unsigned int orientation,
+				       bool x_mirror)
 {
 	u32 format = fb->format->format;
-	int x, y;
+	int x, y, i;
 	u8 byte = 0;
 	unsigned int bit_pos = 0;
 	unsigned int dst_idx = 0;
@@ -1146,7 +1151,8 @@ static void ssd16xx_convert_fb_to_1bpp(u8 *dst, struct iosys_map *src,
 	 * 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) {
+	if (format == DRM_FORMAT_R1 && orientation == 0 && rect->x1 % 8 == 0 &&
+	    !x_mirror) {
 		unsigned int src_pitch = fb->pitches[0];
 		unsigned int width_bytes = drm_rect_width(rect) / 8;
 
@@ -1168,7 +1174,8 @@ static void ssd16xx_convert_fb_to_1bpp(u8 *dst, struct iosys_map *src,
 		 * 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++) {
+			for (i = 0; i < drm_rect_height(rect); i++) {
+				y = x_mirror ? rect->y2 - 1 - i : rect->y1 + i;
 				if (ssd16xx_pixel_is_white(src, fb, x, y))
 					byte |= (1 << (7 - bit_pos));
 				if (++bit_pos == 8) {
@@ -1194,7 +1201,8 @@ static void ssd16xx_convert_fb_to_1bpp(u8 *dst, struct iosys_map *src,
 		 * 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++) {
+			for (i = 0; i < drm_rect_width(rect); i++) {
+				x = x_mirror ? rect->x2 - 1 - i : rect->x1 + i;
 				if (ssd16xx_pixel_is_white(src, fb, x, y))
 					byte |= (1 << (7 - bit_pos));
 				if (++bit_pos == 8) {
@@ -1267,7 +1275,9 @@ static int ssd16xx_fb_dirty(struct drm_framebuffer *fb, struct drm_rect *rect,
 	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, device->orientation);
+		ssd16xx_convert_fb_to_1bpp(mono_buffer, &map, fb, rect,
+					   device->orientation,
+					   device->device_cfg->x_mirror);
 
 	drm_dbg(&device->drm,
 		"fb_dirty: mono[0..3]=0x%02x 0x%02x 0x%02x 0x%02x (data_size=%u)\n",
Signature · 1 line
2.39.1
#26 lore raw

PATCH v2 12/14 MAINTAINERS: Add entry for Solomon SSD16xx DRM driver

In reply to Devarsh Thakkar

Add maintainer entry for the ssd16xx DRM driver covering the SSD16xx e-paper controller family (SSD1673/SSD1680/SSD1681/SSD1683/SSD1677).

Also add LiangCheng Wang as reviewer too since pixpaper uses ssd1677 supported by this driver.

---
Changes from v1:
- Updated MAINTAINERS entry for new drm/solomon driver structure
- Added LiangCheng Wang as reviewer per discussion in mailing list

 MAINTAINERS | 8 ++++++++
 1 file changed, 8 insertions(+)
MAINTAINERS +8 -0
index 4d108339f44e..5019f290e9cd 100644
@@ -8566,6 +8566,14 @@ 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 <[email protected]>
+R:	LiangCheng Wang <[email protected]>
+S:	Maintained
+T:	git https://gitlab.freedesktop.org/drm/misc/kernel.git
+F:	Documentation/devicetree/bindings/display/solomon,ssd16xx.yaml
+F:	drivers/gpu/drm/solomon/ssd16xx.c
+
 DRM DRIVER FOR ST-ERICSSON MCDE
 M:	Linus Walleij <[email protected]>
 S:	Maintained
Signature · 1 line
2.39.1
#27 lore raw

PATCH v2 13/14 arm64: defconfig: Enable DRM_SSD16XX for AM62L3 EVM

In reply to Devarsh Thakkar

Enable the SSD16xx e-paper DRM driver as a module in the arm64 defconfig for use with the AM62L3 EVM and BeagleBadge boards which comes with ssd16xx based epaper display mounted on the board [0].

[0]:
https://www.beagleboard.org/boards/beaglebadge
https://www.beagleboard.org/blog/2026-03-09-introducing-beaglebadge-a-new-vision-for-open-source-wearables-and-iot

---
Changes from v1:
- Updated defconfig for DRM_SSD16XX (renamed from DRM_PANEL_SSD16XX)

 arch/arm64/configs/defconfig | 1 +
 1 file changed, 1 insertion(+)
arch/arm64/configs/defconfig +1 -0
index 6858b75a05f4..f10e295d5aae 100644
@@ -1050,6 +1050,7 @@ CONFIG_ROCKCHIP_DW_HDMI_QP=y
 CONFIG_ROCKCHIP_DW_MIPI_DSI=y
 CONFIG_ROCKCHIP_INNO_HDMI=y
 CONFIG_ROCKCHIP_LVDS=y
+CONFIG_DRM_SSD16XX=m
 CONFIG_DRM_STM=m
 CONFIG_DRM_STM_LVDS=m
 CONFIG_DRM_SUN4I=m
Signature · 1 line
2.39.1
#28 lore raw

PATCH v2 14/14 arm64: dts: ti: Add AM62L3 EVM overlay for GDEY042T81 e-paper display

In reply to Devarsh Thakkar

Add device tree overlay for the Good Display GDEY042T81 4.2" e-paper panel connected to the AM62L3 EVM via SPI.

The overlay configures SPI1 with the SSD1683-based GDEY042T81 panel at 2MHz, with reset, busy, and DC GPIOs connected to MCU GPIO0.

---
Changes from v1:
- No changes: Minor improvements to DTS overlay structure

NOTE: This should only be merged post driver is merged

 arch/arm64/boot/dts/ti/Makefile               |  6 ++
 .../ti/k3-am62l3-evm-epaper-gdey042t81.dtso   | 96 +++++++++++++++++++
 2 files changed, 102 insertions(+)
 create mode 100644 arch/arm64/boot/dts/ti/k3-am62l3-evm-epaper-gdey042t81.dtso
arch/arm64/boot/dts/ti/Makefile +6 -0
index 795978e962ee..90e7a687ca86 100644
@@ -82,6 +82,9 @@ dtb-$(CONFIG_ARCH_K3) += k3-am62d2-evm.dtb
 dtb-$(CONFIG_ARCH_K3) += k3-am62l3-beaglebadge.dtb
 dtb-$(CONFIG_ARCH_K3) += k3-am62l3-evm.dtb
 
+# Overlays for AM62Lx EVM
+dtb-$(CONFIG_ARCH_K3) += k3-am62l3-evm-epaper-gdey042t81.dtbo
+
 # Boards with AM62Px SoC
 dtb-$(CONFIG_ARCH_K3) += k3-am62p5-sk.dtb
 dtb-$(CONFIG_ARCH_K3) += k3-am62p5-var-som-symphony.dtb
@@ -319,6 +322,8 @@ k3-am62a7-sk-csi2-ov5640-dtbs := k3-am62a7-sk.dtb \
 k3-am62a7-sk-csi2-tevi-ov5640-dtbs := k3-am62a7-sk.dtb \
 	k3-am62x-sk-csi2-tevi-ov5640.dtbo
 k3-am62a7-sk-hdmi-audio-dtbs := k3-am62a7-sk.dtb k3-am62x-sk-hdmi-audio.dtbo
+k3-am62l3-evm-epaper-gdey042t81-dtbs := k3-am62l3-evm.dtb \
+	k3-am62l3-evm-epaper-gdey042t81.dtbo
 k3-am62p5-sk-csi2-imx219-dtbs := k3-am62p5-sk.dtb \
 	k3-am62x-sk-csi2-imx219.dtbo
 k3-am62p5-sk-csi2-ov5640-dtbs := k3-am62p5-sk.dtb \
@@ -458,6 +463,7 @@ dtb- += k3-am625-beagleplay-csi2-ov5640.dtb \
 	k3-am62a7-sk-csi2-imx219.dtb \
 	k3-am62a7-sk-csi2-ov5640.dtb \
 	k3-am62a7-sk-hdmi-audio.dtb \
+	k3-am62l3-evm-epaper-gdey042t81.dtb \
 	k3-am62p5-sk-csi2-imx219.dtb \
 	k3-am62p5-sk-csi2-ov5640.dtb \
 	k3-am62p5-sk-csi2-tevi-ov5640.dtb \
arch/arm64/boot/dts/ti/k3-am62l3-evm-epaper-gdey042t81.dtso new file +96 -0

96 lines not on this page · raw message

Signature · 1 line
2.39.1
#29 lore raw

Re: [DO_NOT_MERGE PATCH v2 14/14] arm64: dts: ti: Add AM62L3 EVM overlay for GDEY042T81 e-paper display

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider: - [Medium] dt: commit message claims MCU GPIO0 but uses Main GPIO0 - [Low] dt: incorrect inline comment for…

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Medium] dt: commit message claims MCU GPIO0 but uses Main GPIO0
- [Low] dt: incorrect inline comment for busy_pins_default pad
- [Low] dt: raw integer used for GPIO flags instead of macro
--

commit d0e18d861dbf41de9b7a268b82e7b844f57b80dc
Author: Devarsh Thakkar <[email protected]>

arm64: dts: ti: Add AM62L3 EVM overlay for GDEY042T81 e-paper display

Add device tree overlay for the Good Display GDEY042T81 4.2" e-paper panel connected to the AM62L3 EVM via SPI.

Quoted 2 lines

The overlay configures SPI1 with the SSD1683-based GDEY042T81 panel at 2MHz, with reset, busy, and DC GPIOs connected to MCU GPIO0.

[Severity: Medium]
Is the commit message accurate regarding the GPIO domain?

The commit message states that the display GPIOs are connected to MCU GPIO0, but the device tree nodes explicitly reference &gpio0, which corresponds to the Main domain GPIO0 controller in the K3 architecture (the MCU domain would be &mcu_gpio0 or &wkup_gpio0).

Additionally, the configured pad offsets (0x0080, 0x007c, 0x00b8) belong to the Main domain GPMC0 peripheral, verifying that the physical hardware routing targets the Main domain.

Could the commit message be updated to reflect that Main GPIO0 is being used?

Quoted 6 lines
arch/arm64/boot/dts/ti/k3-am62l3-evm-epaper-gdey042t81.dtso new file +0 -0
new file mode 100644
index 0000000000000..50975a2cdd2a1
@@ -0,0 +1,96 @@

[ ... ]

arch/arm64/boot/dts/ti/k3-am62l3-evm-epaper-gdey042t81.dtso line 46

Quoted 5 lines
arch/arm64/boot/dts/ti/k3-am62l3-evm-epaper-gdey042t81.dtso new file +5 -0
+	busy_pins_default: epaper-busy-default-pins {
+		pinctrl-single,pins = <
+			AM62LX_IOPAD(0x00b8, PIN_INPUT, 7) /* (L21) GPMC0_AD0.GPIO0_31 */
+		>;
+	};

[Severity: Low]
Does this inline comment misidentify the pad name?

The overlay correctly uses the offset 0x00b8 for the busy-gpios pin, but the comment next to it reads GPMC0_AD0.GPIO0_31. In the AM62L pad configuration, offset 0x00b8 actually corresponds to the GPMC0_CLK pad. The GPMC0_AD0 pad resides at offset 0x0078.

Should the comment be updated to prevent confusion during future pinmux debugging?

[ ... ]

arch/arm64/boot/dts/ti/k3-am62l3-evm-epaper-gdey042t81.dtso line 67

Quoted 5 lines
arch/arm64/boot/dts/ti/k3-am62l3-evm-epaper-gdey042t81.dtso new file +5 -0
+/* Drive GPIO0_89 high to select the SPI1 path on the VOUT0 FET switch */
+&gpio0 {
+	fet-sel-hog {
+		gpio-hog;
+		gpios = <89 0>;

[Severity: Low]
Should this use the GPIO_ACTIVE_HIGH macro instead of a raw integer?

While functionally correct (0 equates to GPIO_ACTIVE_HIGH), standard kernel device tree styling strictly prefers using the named macro for readability and consistency, especially since <dt-bindings/gpio/gpio.h> is included at the top of the file.

Quoted 4 lines
arch/arm64/boot/dts/ti/k3-am62l3-evm-epaper-gdey042t81.dtso new file +4 -0
+		output-high;
+		line-name = "GPIO_EXP_CONN_FET_SEL";
+	};
+};
Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=14
Tested-by on the v2 series
#30 lore raw
In reply to Devarsh Thakkar

Hi Devarsh,

Setup: Raspberry Pi 5, rpi-7.2.y with your patches 01-12/14 cherry-picked and the panel entry added locally, SPI0 CE0 at 5 and 20 MHz, DC/RST/BUSY on GPIO5/6/26, CONFIG_DRM_SSD16XX=y, drm.debug=0x02, no compositor.

Bring-up is clean: it probes, the connector comes up at 800x480, and reset/BUSY and runtime PM all work. The three issues I reported on 31 July [1] are all gone - the mirroring is fixed by 11/14 [2], rotation 180 draws, and refresh_mode takes effect.

All four DT rotation values work, and so do the plane properties - refresh_mode, color_mode, clear_on_init/close/disable and refresh_mode_init. SPI is fine at both 5 and 20 MHz.

[1] https://lore.kernel.org/all/[email protected]/
[2] https://lore.kernel.org/all/[email protected]/

Regards,
LiangCheng