From mboxrd@z Thu Jan 1 00:00:00 1970 Received: from smtp-out1.suse.de (smtp-out1.suse.de [195.135.223.130]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 11E7E371046; Mon, 28 Sep 2026 07:01:02 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=195.135.223.130 ARC-Seal:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1790578867; cv=none; b=Ba9rSQXc5Blxf4Knx9rtbnmBJQgLP0xde3LUXgVDVNttehaEEWFi0DOrnErwx6RKdYsUbHEQ9ljnlzD9LPrct1OhjNRp+dPdHtrbIa1/miSz5B8MdkOxXRPY0xA3xZE+MBwFcYSVn3LLNKKgCv4ay0mqW2D4JHk98WG07yhwXwY= ARC-Message-Signature:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1790578867; c=relaxed/simple; bh=DtAvmMW6IoUM+lXETRyb0aZpdiX4fCpgYQG1u07hZYg=; h=Message-ID:Date:MIME-Version:Subject:To:Cc:References:From: In-Reply-To:Content-Type; b=IBnX/2kpyF2DD4ZQmoeasQSrIxGXU91P07AF2DHMAzxoQXmRD46ZB5evwOiMlGBEFm9y0AzB6C5VybtubWi+msZHj0Rllk3HHkWWPkRXZTX23d9hDU6V6eZ1FwIuxRCsj94Rn48qnpDiM2agmmztK1pJfkKQL9nZ9Yy1ulhXzis= ARC-Authentication-Results:i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=suse.de; spf=pass smtp.mailfrom=suse.de; dkim=pass (1024-bit key) header.d=suse.de header.i=@suse.de header.b=YtXtfcKH; dkim=permerror (0-bit key) header.d=suse.de header.i=@suse.de header.b=OGpNR6aH; dkim=pass (1024-bit key) header.d=suse.de header.i=@suse.de header.b=hhqqgl8q; dkim=permerror (0-bit key) header.d=suse.de header.i=@suse.de header.b=rVEPx7XF; arc=none smtp.client-ip=195.135.223.130 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=suse.de Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=suse.de Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=suse.de header.i=@suse.de header.b="YtXtfcKH"; dkim=permerror (0-bit key) header.d=suse.de header.i=@suse.de header.b="OGpNR6aH"; dkim=pass (1024-bit key) header.d=suse.de header.i=@suse.de header.b="hhqqgl8q"; dkim=permerror (0-bit key) header.d=suse.de header.i=@suse.de header.b="rVEPx7XF" Received: from imap1.dmz-prg2.suse.org (imap1.dmz-prg2.suse.org [IPv6:2a07:de40:b281:104:10:150:64:97]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (4096 bits) server-digest SHA256) (No client certificate requested) by smtp-out1.suse.de (Postfix) with ESMTPS id C37C521D37; Mon, 28 Sep 2026 07:00:44 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=suse.de; s=susede2_rsa; t=1790578857; h=from:from:reply-to:date:date:message-id:message-id:to:to:cc:cc: mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references:autocrypt:autocrypt; bh=MA4MTwaisSXiHz+o5KWyYyRrd4/3P1lwG8upG+V8tr8=; b=YtXtfcKHDmmPzMGLZQtelhenQnu0+5mSQVOIz0vxK+oO3MBuugPNJ0Jxp+hjGqIm0cVCb9 i8AItfXofzxUOac2sWJZV1098kgZkXuMM9YKS7DyLRc6lSIl2yDKtkPAAik6RWlTF0kydW rN06F4oOTosHNzxymuIWHsGf7Cif1t8= DKIM-Signature: v=1; a=ed25519-sha256; c=relaxed/relaxed; d=suse.de; s=susede2_ed25519; t=1790578857; h=from:from:reply-to:date:date:message-id:message-id:to:to:cc:cc: mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references:autocrypt:autocrypt; bh=MA4MTwaisSXiHz+o5KWyYyRrd4/3P1lwG8upG+V8tr8=; b=OGpNR6aHL13N1YDzaP5K3qAUkGcNVNJrzqGJBW5d9WTAHvZ77LrhitiC8QtouTocT3ALaY 49xbYB1bhgKq/JAA== Authentication-Results: smtp-out1.suse.de; dkim=pass header.d=suse.de header.s=susede2_rsa header.b=hhqqgl8q; dkim=pass header.d=suse.de header.s=susede2_ed25519 header.b=rVEPx7XF DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=suse.de; s=susede2_rsa; t=1790578846; h=from:from:reply-to:date:date:message-id:message-id:to:to:cc:cc: mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references:autocrypt:autocrypt; bh=MA4MTwaisSXiHz+o5KWyYyRrd4/3P1lwG8upG+V8tr8=; b=hhqqgl8qLeFLmjjxIUPr5oVirR4XwTx2tDlxB+5rhek4KmGqbL/PFTA6On8Ltj2XPUTcnV ASQgnAL4AHaZO4mJaft0HWXEt1sq7jSxb+1Q1rZdYaCBT6WDgqd6NPouonQ46wTK/0FKw4 Sfoi2B5bMpa3TfCez1ylw4zckp063Bg= DKIM-Signature: v=1; a=ed25519-sha256; c=relaxed/relaxed; d=suse.de; s=susede2_ed25519; t=1790578846; h=from:from:reply-to:date:date:message-id:message-id:to:to:cc:cc: mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references:autocrypt:autocrypt; bh=MA4MTwaisSXiHz+o5KWyYyRrd4/3P1lwG8upG+V8tr8=; b=rVEPx7XFMvT/aRFBEYaZwGrQkRRfAWw8znMUOKDqP1jgUFdtw86ZXBznw0CGnye545+YBR 7k7pg1CgB7+nnXBQ== Received: from imap1.dmz-prg2.suse.org (localhost [127.0.0.1]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (4096 bits) server-digest SHA256) (No client certificate requested) by imap1.dmz-prg2.suse.org (Postfix) with ESMTPS id 1F19A133F1; Mon, 28 Sep 2026 07:00:43 +0000 (UTC) Received: from dovecot-director2.suse.de ([2a07:de40:b281:106:10:150:64:167]) by imap1.dmz-prg2.suse.org with ESMTPSA id nHqOLpsQumpjHwAAD6G6ig (envelope-from ); Mon, 28 Sep 2026 07:00:43 +0000 Message-ID: <522014e9-92ab-480e-b3f4-75d542b1c1d3@suse.de> Date: Mon, 28 Sep 2026 09:00:43 +0200 Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 User-Agent: Mozilla Thunderbird Subject: Re: [PATCH v2 04/14] drm/solomon: Add DRM driver for Solomon SSD16xx e-paper display controllers To: Devarsh Thakkar , Maarten Lankhorst , Maxime Ripard , David Airlie , Simona Vetter , Rob Herring , Krzysztof Kozlowski , Conor Dooley , Nishanth Menon , Vignesh Raghavendra , Tero Kristo Cc: dri-devel@lists.freedesktop.org, devicetree@vger.kernel.org, linux-kernel@vger.kernel.org References: <20260927182329.4193961-1-devarsht@ti.com> <20260927182329.4193961-5-devarsht@ti.com> Content-Language: en-US From: Thomas Zimmermann Autocrypt: addr=tzimmermann@suse.de; keydata= xsBNBFs50uABCADEHPidWt974CaxBVbrIBwqcq/WURinJ3+2WlIrKWspiP83vfZKaXhFYsdg XH47fDVbPPj+d6tQrw5lPQCyqjwrCPYnq3WlIBnGPJ4/jreTL6V+qfKRDlGLWFjZcsrPJGE0 BeB5BbqP5erN1qylK9i3gPoQjXGhpBpQYwRrEyQyjuvk+Ev0K1Jc5tVDeJAuau3TGNgah4Yc hdHm3bkPjz9EErV85RwvImQ1dptvx6s7xzwXTgGAsaYZsL8WCwDaTuqFa1d1jjlaxg6+tZsB 9GluwvIhSezPgnEmimZDkGnZRRSFiGP8yjqTjjWuf0bSj5rUnTGiyLyRZRNGcXmu6hjlABEB AAHNJ1Rob21hcyBaaW1tZXJtYW5uIDx0emltbWVybWFubkBzdXNlLmRlPsLAjgQTAQgAOAIb AwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgBYhBHIX+6yM6c9jRKFo5WgNwR1TC3ojBQJftODH AAoJEGgNwR1TC3ojx1wH/0hKGWugiqDgLNXLRD/4TfHBEKmxIrmfu9Z5t7vwUKfwhFL6hqvo lXPJJKQpQ2z8+X2vZm/slsLn7J1yjrOsoJhKABDi+3QWWSGkaGwRJAdPVVyJMfJRNNNIKwVb U6B1BkX2XDKDGffF4TxlOpSQzdtNI/9gleOoUA8+jy8knnDYzjBNOZqLG2FuTdicBXblz0Mf vg41gd9kCwYXDnD91rJU8tzylXv03E75NCaTxTM+FBXPmsAVYQ4GYhhgFt8S2UWMoaaABLDe 7l5FdnLdDEcbmd8uLU2CaG4W2cLrUaI4jz2XbkcPQkqTQ3EB67hYkjiEE6Zy3ggOitiQGcqp j//OwE0EWznS4AEIAMYmP4M/V+T5RY5at/g7rUdNsLhWv1APYrh9RQefODYHrNRHUE9eosYb T6XMryR9hT8XlGOYRwKWwiQBoWSDiTMo/Xi29jUnn4BXfI2px2DTXwc22LKtLAgTRjP+qbU6 3Y0xnQN29UGDbYgyyK51DW3H0If2a3JNsheAAK+Xc9baj0LGIc8T9uiEWHBnCH+RdhgATnWW GKdDegUR5BkDfDg5O/FISymJBHx2Dyoklv5g4BzkgqTqwmaYzsl8UxZKvbaxq0zbehDda8lv hFXodNFMAgTLJlLuDYOGLK2AwbrS3Sp0AEbkpdJBb44qVlGm5bApZouHeJ/+n+7r12+lqdsA EQEAAcLAdgQYAQgAIAIbDBYhBHIX+6yM6c9jRKFo5WgNwR1TC3ojBQJftOH6AAoJEGgNwR1T C3ojVSkIALpAPkIJPQoURPb1VWjh34l0HlglmYHvZszJWTXYwavHR8+k6Baa6H7ufXNQtThR yIxJrQLW6rV5lm7TjhffEhxVCn37+cg0zZ3j7zIsSS0rx/aMwi6VhFJA5hfn3T0TtrijKP4A SAQO9xD1Zk9/61JWk8OysuIh7MXkl0fxbRKWE93XeQBhIJHQfnc+YBLprdnxR446Sh8Wn/2D Ya8cavuWf2zrB6cZurs048xe0UbSW5AOSo4V9M0jzYI4nZqTmPxYyXbm30Kvmz0rYVRaitYJ 4kyYYMhuULvrJDMjZRvaNe52tkKAvMevcGdt38H4KSVXAylqyQOW5zvPc4/sq9c= In-Reply-To: <20260927182329.4193961-5-devarsht@ti.com> Content-Type: text/plain; charset=UTF-8; format=flowed Content-Transfer-Encoding: 8bit X-Spam-Level: X-Rspamd-Action: no action X-Rspamd-Server: rspamd2.dmz-prg2.suse.org X-Rspamd-Queue-Id: C37C521D37 X-Spamd-Result: default: False [-3.01 / 50.00]; BAYES_HAM(-3.00)[100.00%]; SUSPICIOUS_RECIPS(1.50)[]; NEURAL_HAM_LONG(-1.00)[-1.000]; R_DKIM_ALLOW(-0.20)[suse.de:s=susede2_rsa,suse.de:s=susede2_ed25519]; NEURAL_HAM_SHORT(-0.20)[-1.000]; MIME_GOOD(-0.10)[text/plain]; MX_GOOD(-0.01)[]; TO_MATCH_ENVRCPT_ALL(0.00)[]; DKIM_SIGNED(0.00)[suse.de:s=susede2_rsa,suse.de:s=susede2_ed25519]; FREEMAIL_TO(0.00)[ti.com,linux.intel.com,kernel.org,gmail.com,ffwll.ch]; ARC_NA(0.00)[]; RBL_SPAMHAUS_BLOCKED_OPENRESOLVER(0.00)[2a07:de40:b281:104:10:150:64:97:from]; RCPT_COUNT_TWELVE(0.00)[14]; MIME_TRACE(0.00)[0:+]; FREEMAIL_ENVRCPT(0.00)[gmail.com]; RCVD_TLS_ALL(0.00)[]; DKIM_TRACE(0.00)[suse.de:+]; RCVD_COUNT_TWO(0.00)[2]; FROM_EQ_ENVFROM(0.00)[]; FROM_HAS_DN(0.00)[]; TO_DN_SOME(0.00)[]; DNSWL_BLOCKED(0.00)[2a07:de40:b281:104:10:150:64:97:from,2a07:de40:b281:106:10:150:64:167:received]; RECEIVED_SPAMHAUS_BLOCKED_OPENRESOLVER(0.00)[2a07:de40:b281:106:10:150:64:167:received]; MID_RHS_MATCH_FROM(0.00)[]; RCVD_VIA_SMTP_AUTH(0.00)[]; TAGGED_RCPT(0.00)[dt]; DBL_BLOCKED_OPENRESOLVER(0.00)[imap1.dmz-prg2.suse.org:rdns,imap1.dmz-prg2.suse.org:helo,suse.de:dkim,suse.de:mid,bootlin.com:url,suse.com:url,crystalfontz.com:url,ti.com:email,ti.com:url,seeedstudio.com:url] X-Spam-Flag: NO X-Spam-Score: -3.01 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 > --- > > 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 > + * > + * References: https://github.com/Lesords/epaper > + */ > + > +#include > +#include > +#include > +#include > +#include > + > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > + > +/* ----------------------------------------------------------------------- > + * 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 > + 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. > + > + 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. > + > + 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. > + > +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 > +} > + > +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. > + > +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. > + > + /* > + * 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. > + > + 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. > + 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 "); > +MODULE_DESCRIPTION("DRM driver for Solomon SSD16xx e-paper display controller family"); > +MODULE_LICENSE("GPL"); -- -- 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)