From: Devarsh Thakkar <devarsht@ti.com>
To: Thomas Zimmermann <tzimmermann@suse.de>,
Maarten Lankhorst <maarten.lankhorst@linux.intel.com>,
Maxime Ripard <mripard@kernel.org>,
David Airlie <airlied@gmail.com>, Simona Vetter <simona@ffwll.ch>,
"Rob Herring" <robh@kernel.org>,
Krzysztof Kozlowski <krzk+dt@kernel.org>,
"Conor Dooley" <conor+dt@kernel.org>, Nishanth Menon <nm@ti.com>,
Vignesh Raghavendra <vigneshr@ti.com>,
Tero Kristo <kristo@kernel.org>
Cc: <dri-devel@lists.freedesktop.org>, <devicetree@vger.kernel.org>,
<linux-kernel@vger.kernel.org>
Subject: Re: [PATCH v2 04/14] drm/solomon: Add DRM driver for Solomon SSD16xx e-paper display controllers
Date: Tue, 29 Sep 2026 22:13:00 +0530 [thread overview]
Message-ID: <de65fec6-92f1-4170-915d-36604dcfd0c5@ti.com> (raw)
In-Reply-To: <522014e9-92ab-480e-b3f4-75d542b1c1d3@suse.de>
Hi Thomas,
Thanks for the review.
On 28/09/26 12:30, Thomas Zimmermann wrote:
> Hi
>
> Am 27.09.26 um 20:23 schrieb 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
>>
>> Signed-off-by: Devarsh Thakkar <devarsht@ti.com>
>> ---
>>
>> 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
>>
>> diff --git a/drivers/gpu/drm/solomon/Kconfig b/drivers/gpu/drm/
>> solomon/Kconfig
>> index 400a6cab3a67..de2b14819f81 100644
>> --- a/drivers/gpu/drm/solomon/Kconfig
>> +++ b/drivers/gpu/drm/solomon/Kconfig
>> @@ -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.
>> diff --git a/drivers/gpu/drm/solomon/Makefile b/drivers/gpu/drm/
>> solomon/Makefile
>> index b5fc792257d7..72384620785b 100644
>> --- a/drivers/gpu/drm/solomon/Makefile
>> +++ b/drivers/gpu/drm/solomon/Makefile
>> @@ -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
>> diff --git a/drivers/gpu/drm/solomon/ssd16xx.c b/drivers/gpu/drm/
>> solomon/ssd16xx.c
>> new file mode 100644
>> index 000000000000..d3af055c6739
>> --- /dev/null
>> +++ b/drivers/gpu/drm/solomon/ssd16xx.c
>> @@ -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 <devarsht@ti.com>
>> + *
>> + * References: https://github.com/Lesords/epaper
>> + */
>> +
>> +#include <linux/delay.h>
>> +#include <linux/module.h>
>> +#include <linux/of.h>
>> +#include <linux/property.h>
>> +#include <linux/spi/spi.h>
>> +
>> +#include <drm/clients/drm_client_setup.h>
>> +#include <drm/drm_atomic.h>
>> +#include <drm/drm_atomic_helper.h>
>> +#include <drm/drm_damage_helper.h>
>> +#include <drm/drm_drv.h>
>> +#include <drm/drm_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)
>> +{
>> + 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
>
>
Agreed, thanks for pointing.
>> + 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.
>> +
>> + 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.
>
Agreed, thanks for pointing.
>
>> +
>> + 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,
>> +};
>> +
>> +/*
>> -----------------------------------------------------------------------------
>> + * CRTC Functions
>> + */
>> +
>> +static void ssd16xx_crtc_destroy(struct drm_crtc *crtc)
>> +{
>> + drm_crtc_cleanup(crtc);
>> +}
>
> No need for this wrapper AFAICT.
>
Agreed, thanks for pointing.
>> +
>> +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 &&
>> + 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
>
Agreed, thanks for pointing.
>
>> +}
>> +
>> +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.
>
Agreed, thanks for pointing.
>> +
>> +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);
>> + 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]
>> +
>> + /*
>> + * 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))
>> + 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.
>
Thanks for pointing, I think above can be removed.
>> +
>> + 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;
>> +
>> + 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
>> + 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 <devarsht@ti.com>");
>> +MODULE_DESCRIPTION("DRM driver for Solomon SSD16xx e-paper display
>> controller family");
>> +MODULE_LICENSE("GPL");
>
next prev parent reply other threads:[~2026-09-29 16:43 UTC|newest]
Thread overview: 19+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-27 18:23 [PATCH v2 00/14] " Devarsh Thakkar
2026-09-27 18:23 ` [PATCH v2 01/14] dt-bindings: vendor-prefixes: Add Dalian Good Display Co., Ltd Devarsh Thakkar
2026-09-27 18:23 ` [PATCH v2 02/14] dt-bindings: display: Add Solomon SSD16xx e-paper controller binding Devarsh Thakkar
2026-10-01 6:28 ` Krzysztof Kozlowski
2026-09-27 18:23 ` [PATCH v2 03/14] dt-bindings: display: solomon,ssd16xx: Add Solomon SSD1677 controller Devarsh Thakkar
2026-10-01 6:26 ` Krzysztof Kozlowski
2026-09-27 18:23 ` [PATCH v2 04/14] drm/solomon: Add DRM driver for Solomon SSD16xx e-paper display controllers Devarsh Thakkar
2026-09-28 7:00 ` Thomas Zimmermann
2026-09-29 16:43 ` Devarsh Thakkar [this message]
2026-09-27 18:23 ` [PATCH v2 05/14] drm/solomon: ssd16xx: Add clear_on_init/close/disable session management Devarsh Thakkar
2026-09-27 18:23 ` [PATCH v2 06/14] drm/solomon: ssd16xx: Add support for Solomon SSD1677 controller Devarsh Thakkar
2026-09-27 18:23 ` [PATCH v2 07/14] drm/solomon: ssd16xx: Add power management support Devarsh Thakkar
2026-09-27 18:23 ` [PATCH v2 08/14] drm/solomon: ssd16xx: Expose refresh mode as plane property Devarsh Thakkar
2026-09-27 18:23 ` [PATCH v2 09/14] drm/solomon: ssd16xx: Expose color " Devarsh Thakkar
2026-09-27 18:23 ` [PATCH v2 10/14] drm/solomon: ssd16xx: Expose session management as plane properties Devarsh Thakkar
2026-09-27 18:23 ` [PATCH v2 11/14] drm/solomon: ssd16xx: support panels whose RAM X order is reversed Devarsh Thakkar
2026-09-27 18:23 ` [PATCH v2 12/14] MAINTAINERS: Add entry for Solomon SSD16xx DRM driver Devarsh Thakkar
2026-09-27 18:23 ` [PATCH v2 13/14] arm64: defconfig: Enable DRM_SSD16XX for AM62L3 EVM Devarsh Thakkar
2026-09-27 18:23 ` [DO_NOT_MERGE PATCH v2 14/14] arm64: dts: ti: Add AM62L3 EVM overlay for GDEY042T81 e-paper display Devarsh Thakkar
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=de65fec6-92f1-4170-915d-36604dcfd0c5@ti.com \
--to=devarsht@ti.com \
--cc=airlied@gmail.com \
--cc=conor+dt@kernel.org \
--cc=devicetree@vger.kernel.org \
--cc=dri-devel@lists.freedesktop.org \
--cc=kristo@kernel.org \
--cc=krzk+dt@kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=maarten.lankhorst@linux.intel.com \
--cc=mripard@kernel.org \
--cc=nm@ti.com \
--cc=robh@kernel.org \
--cc=simona@ffwll.ch \
--cc=tzimmermann@suse.de \
--cc=vigneshr@ti.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®