mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Paul Cercueil <paul@crapouillou.net>
To: Olivier Louvignes <olivier@mgcrea.io>,
	Maarten Lankhorst	 <maarten.lankhorst@linux.intel.com>,
	Maxime Ripard <mripard@kernel.org>,
	 Thomas Zimmermann <tzimmermann@suse.de>,
	Neil Armstrong <neil.armstrong@linaro.org>
Cc: David Airlie <airlied@gmail.com>, Simona Vetter <simona@ffwll.ch>,
	 Jessica Zhang <jesszhan0024@gmail.com>,
	Rob Herring <robh@kernel.org>,
	Krzysztof Kozlowski <krzk+dt@kernel.org>,
	 Conor Dooley <conor+dt@kernel.org>,
	"H . Nikolaus Schaller" <hns@goldelico.com>,
	 dri-devel@lists.freedesktop.org, devicetree@vger.kernel.org,
	 linux-mips@vger.kernel.org, linux-kernel@vger.kernel.org,
	Thomas Petazzoni	 <thomas.petazzoni@bootlin.com>
Subject: Re: [PATCH v3 3/5] drm/ingenic: Add a driver for the X1600 display controller
Date: Fri, 06 Nov 2026 14:34:08 +0100	[thread overview]
Message-ID: <0a2e8ea782fa819a547e0a3075303bd74a7fde72.camel@crapouillou.net> (raw)
In-Reply-To: <20261106112000.1612-4-olivier@mgcrea.io>

Hi Olivier,

Le vendredi 06 novembre 2026 à 12:19 +0100, Olivier Louvignes a écrit :
> The X1600's display controller has none of the registers ingenic-drm
> programs: it fetches each frame through a descriptor whose address it
> takes from SRD_CHAIN_ADDR, and keeps its interface, TFT timings and smart
> panel set-up in separate register blocks (X1600 programming manual,
> chapter 10; the X2000's controller without the composer). This driver
> handles parallel RGB panels on a 24-bit bus, with one full-screen RGB565
> plane, and takes the colour order (RGB or BGR) from the panel's bus
> format.
> 
> Each descriptor points at itself, so a frame repeats until a flip hands
> the controller the other one. The controller takes it "at appropriate
> time", so a flip's event waits until the descriptor-site register
> reports the new descriptor at the end of a frame, and a commit waits for
> that event rather than for a vblank before the old framebuffer is
> released: the controller can keep reading it for a frame after the flip.
> 
> Tested on a HiBy R1 with its 480x800 panel: the fbdev console and
> r1player, blanking and unblanking, and 60 fbdev pans in 0.97 s, one flip
> per frame at about 61 Hz, none timing out.
> 
> Signed-off-by: Olivier Louvignes <olivier@mgcrea.io>
> ---
> v3:
> - ingenic,x1600-dpu; the clock taken unnamed, as the binding no longer
>   has clock-names (Krzysztof).
> - "display controller" in the Kconfig prompt, the header and the
>   descriptions.
> 
> v2:
> - A commit_tail that waits for flip_done, not for a vblank, before the
>   planes are cleaned up.
> - A CRTC being enabled sends its event once the controller runs, not
>   from atomic_flush() before atomic_enable().
> - Reject a plane that is not visible.
> - atomic_disable() leaves the controller alone when atomic_enable()
>   could not start the clock.
> 
>  MAINTAINERS                                 |   1 +
>  drivers/gpu/drm/Makefile                    |   1 +
>  drivers/gpu/drm/ingenic/Kconfig             |  22 +
>  drivers/gpu/drm/ingenic/Makefile            |   1 +
>  drivers/gpu/drm/ingenic/ingenic-x1600-drm.c | 744 ++++++++++++++++++++
>  5 files changed, 769 insertions(+)
>  create mode 100644 drivers/gpu/drm/ingenic/ingenic-x1600-drm.c
> 
> diff --git a/MAINTAINERS b/MAINTAINERS
> index a5160860ae26..5d992133e0d5 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -13180,6 +13180,7 @@ INGENIC X1600 DRM DRIVER
>  M:	Olivier Louvignes <olivier@mgcrea.io>
>  S:	Maintained
>  F:	Documentation/devicetree/bindings/display/ingenic,x1600-dpu.yaml
> +F:	drivers/gpu/drm/ingenic/ingenic-x1600-drm.c
>  
>  INJOINIC IP5xxx POWER BANK IC DRIVER
>  M:	Samuel Holland <samuel@sholland.org>
> diff --git a/drivers/gpu/drm/Makefile b/drivers/gpu/drm/Makefile
> index 2c6bc093a923..37dc781761d6 100644
> --- a/drivers/gpu/drm/Makefile
> +++ b/drivers/gpu/drm/Makefile
> @@ -206,6 +206,7 @@ obj-$(CONFIG_DRM_STM) += stm/
>  obj-$(CONFIG_DRM_STI) += sti/
>  obj-y 			+= imx/
>  obj-$(CONFIG_DRM_INGENIC) += ingenic/
> +obj-$(CONFIG_DRM_INGENIC_X1600) += ingenic/
>  obj-$(CONFIG_DRM_LOGICVC) += logicvc/
>  obj-$(CONFIG_DRM_MEDIATEK) += mediatek/
>  obj-$(CONFIG_DRM_MESON)	+= meson/
> diff --git a/drivers/gpu/drm/ingenic/Kconfig b/drivers/gpu/drm/ingenic/Kconfig
> index 987ed42f6f7a..85ac9ca62d7d 100644
> --- a/drivers/gpu/drm/ingenic/Kconfig
> +++ b/drivers/gpu/drm/ingenic/Kconfig
> @@ -20,6 +20,28 @@ config DRM_INGENIC
>  
>  	  If M is selected the module will be called ingenic-drm.
>  
> +config DRM_INGENIC_X1600
> +	tristate "DRM support for the Ingenic X1600 display controller"
> +	depends on MIPS || COMPILE_TEST
> +	depends on DRM
> +	depends on OF
> +	depends on COMMON_CLK
> +	select APERTURE_HELPERS
> +	select DRM_BRIDGE
> +	select DRM_CLIENT_SELECTION
> +	select DRM_PANEL_BRIDGE
> +	select DRM_KMS_HELPER
> +	select DRM_DISPLAY_HELPER
> +	select DRM_BRIDGE_CONNECTOR
> +	select DRM_GEM_DMA_HELPER
> +	help
> +	  Choose this option for DRM support for the display controller of
> +	  the Ingenic X1600 SoC. It drives parallel RGB panels on a 24-bit
> +	  bus from one full-screen RGB565 plane; the controller's smart panel
> +	  interface is not supported.
> +
> +	  If M is selected the module will be called ingenic-x1600-drm.
> +
>  if DRM_INGENIC
>  
>  config DRM_INGENIC_IPU
> diff --git a/drivers/gpu/drm/ingenic/Makefile
> b/drivers/gpu/drm/ingenic/Makefile
> index f10cc1c5a5f2..90072ad13158 100644
> --- a/drivers/gpu/drm/ingenic/Makefile
> +++ b/drivers/gpu/drm/ingenic/Makefile
> @@ -2,3 +2,4 @@ obj-$(CONFIG_DRM_INGENIC) += ingenic-drm.o
>  ingenic-drm-y = ingenic-drm-drv.o
>  ingenic-drm-$(CONFIG_DRM_INGENIC_IPU) += ingenic-ipu.o
>  obj-$(CONFIG_DRM_INGENIC_DW_HDMI) += ingenic-dw-hdmi.o
> +obj-$(CONFIG_DRM_INGENIC_X1600) += ingenic-x1600-drm.o
> diff --git a/drivers/gpu/drm/ingenic/ingenic-x1600-drm.c
> b/drivers/gpu/drm/ingenic/ingenic-x1600-drm.c
> new file mode 100644
> index 000000000000..e69c968ebcd9
> --- /dev/null
> +++ b/drivers/gpu/drm/ingenic/ingenic-x1600-drm.c
> @@ -0,0 +1,744 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * DRM driver for the display controller (DPU) of the Ingenic X1600
> + *
> + * Copyright (c) 2026 Olivier Louvignes <olivier@mgcrea.io>
> + *
> + * The X1600's display controller (X1600 programming manual, chapter 10) is
> + * the simple read channel of the X2000's, without its composer. It shares no
> + * registers with the JZ47xx and X1000 controllers that ingenic-drm drives:
> it
> + * fetches each frame through a descriptor whose address is in
> SRD_CHAIN_ADDR,
> + * and its interface and timings sit in register blocks of their own. It
> + * drives parallel RGB (TFT) panels and smart panels; this driver handles
> + * parallel RGB panels on a 24-bit bus, from RGB565 framebuffers.
> + */
> +
> +#include <linux/aperture.h>
> +#include <linux/bitfield.h>
> +#include <linux/bits.h>
> +#include <linux/clk.h>
> +#include <linux/completion.h>
> +#include <linux/dma-mapping.h>
> +#include <linux/interrupt.h>
> +#include <linux/io.h>
> +#include <linux/media-bus-format.h>
> +#include <linux/module.h>
> +#include <linux/of.h>
> +#include <linux/platform_device.h>
> +#include <linux/spinlock.h>
> +
> +#include <drm/clients/drm_client_setup.h>
> +#include <drm/drm_atomic.h>
> +#include <drm/drm_atomic_helper.h>
> +#include <drm/drm_bridge.h>
> +#include <drm/drm_bridge_connector.h>
> +#include <drm/drm_crtc.h>
> +#include <drm/drm_drv.h>
> +#include <drm/drm_encoder.h>
> +#include <drm/drm_fb_dma_helper.h>
> +#include <drm/drm_fbdev_dma.h>
> +#include <drm/drm_fourcc.h>
> +#include <drm/drm_framebuffer.h>
> +#include <drm/drm_gem_dma_helper.h>
> +#include <drm/drm_gem_framebuffer_helper.h>
> +#include <drm/drm_of.h>
> +#include <drm/drm_plane.h>
> +#include <drm/drm_print.h>
> +#include <drm/drm_probe_helper.h>
> +#include <drm/drm_vblank.h>
> +
> +#define X1600_LCD_SRD_CHAIN_ADDR	0x1000
> +#define X1600_LCD_SRD_CHAIN_START	0x1004
> +#define X1600_LCD_CTRL			0x2000
> +#define X1600_LCD_ST			0x2004
> +#define X1600_LCD_CLR_ST		0x2008
> +#define X1600_LCD_INTC			0x200c
> +#define X1600_LCD_INT_FLAG		0x2010
> +#define X1600_LCD_COM_CONFIG		0x2014
> +#define X1600_LCD_CHAIN_SITE		0x2204
> +#define X1600_LCD_DISP_COM		0x8000
> +#define X1600_LCD_TFT_HSYNC		0x9000
> +#define X1600_LCD_TFT_VSYNC		0x9004
> +#define X1600_LCD_TFT_HDE		0x9008
> +#define X1600_LCD_TFT_VDE		0x900c
> +#define X1600_LCD_TFT_TRAN_CFG		0x9010
> +
> +/* CTRL: stop the simple read channel at the end of the frame. */
> +#define X1600_LCD_CTRL_GEN_STP_SRD	BIT(4)
> +
> +/* ST; CLR_ST (write 1 to clear), INTC and INT_FLAG have the same bits. */
> +#define X1600_LCD_ST_WORKING		BIT(0)
> +#define X1600_LCD_ST_STOP_SRD_ACK	BIT(7)
> +#define X1600_LCD_ST_TFT_UNDR		BIT(8)
> +#define X1600_LCD_ST_DISP_END		BIT(17)	/* a frame was
> displayed */
> +
> +#define X1600_LCD_COM_CONFIG_BURST_LEN	GENMASK(5, 4)
> +#define X1600_LCD_BURST_LEN_32		3
> +
> +#define X1600_LCD_DISP_COM_IF		GENMASK(1, 0)
> +#define X1600_LCD_DISP_COM_IF_TFT	1
> +#define X1600_LCD_DISP_COM_DITHER_EN	BIT(4)
> +#define X1600_LCD_DISP_COM_TFT_CLKGATE	BIT(5)
> +#define X1600_LCD_DISP_COM_SLCD_CLKGATE	BIT(6)
> +
> +/* TFT_HSYNC, TFT_VSYNC: sync length and total; TFT_HDE, TFT_VDE: active
> area. */
> +#define X1600_LCD_TIMING_START		GENMASK(27, 16)
> +#define X1600_LCD_TIMING_END		GENMASK(11, 0)
> +
> +#define X1600_LCD_TFT_COLOR_EVEN	GENMASK(21, 19)
> +#define X1600_LCD_TFT_COLOR_ODD		GENMASK(18, 16)
> +#define X1600_LCD_TFT_PIX_CLK_INV	BIT(10)
> +#define X1600_LCD_TFT_DE_DL		BIT(9)
> +#define X1600_LCD_TFT_HSYNC_DL		BIT(8)
> +#define X1600_LCD_TFT_VSYNC_DL		BIT(7)
> +#define X1600_LCD_TFT_MODE		GENMASK(2, 0)
> +#define X1600_LCD_TFT_MODE_24BIT	0
> +#define X1600_LCD_TFT_COLOR_RGB		0
> +#define X1600_LCD_TFT_COLOR_BGR		2
> +
> +#define X1600_LCD_DESC_CFG_FMT		GENMASK(22, 19)
> +#define X1600_LCD_DESC_FMT_RGB565	2
> +/* An interrupt at the end of the frame: DISP_END needs this and INTC set. */
> +#define X1600_LCD_DESC_CTRL_EOD_MSK	BIT(17)
> +
> +#define X1600_LCD_MAX_PIXELS		(1280 * 720)
> +/* A smaller frame breaks a continuous descriptor chain (manual, 10.7). */
> +#define X1600_LCD_MIN_PIXELS		500
> +#define X1600_LCD_STOP_TIMEOUT_MS	100
> +
> +/*
> + * The controller reads a descriptor at the start of each frame and then
> + * follows its next field. Each descriptor points at itself, so a frame
> + * repeats until SRD_CHAIN_ADDR names another one.
> + */
> +struct x1600_lcd_desc {
> +	u32 next;
> +	u32 addr;
> +	u32 stride;	/* in pixels */
> +	u32 cfg;
> +	u32 ctrl;
> +} __aligned(64);
> +
> +struct x1600_lcd {
> +	struct drm_device drm;
> +	struct drm_plane plane;
> +	struct drm_crtc crtc;
> +	struct drm_encoder encoder;
> +
> +	void __iomem *base;
> +	struct clk *clk;
> +
> +	struct x1600_lcd_desc *desc;
> +	dma_addr_t desc_phys;
> +	unsigned int cur;		/* the descriptor last handed over */
> +	bool running;
> +	struct completion stopped;
> +	/* Serialises the read-modify-writes of INTC. */
> +	spinlock_t irq_lock;
> +
> +	/* Under drm.event_lock: sent once the controller reads event_desc.
> */
> +	struct drm_pending_vblank_event *event;
> +	u32 event_desc;
> +
> +	u32 bus_flags;
> +	u32 color_order;
> +};
> +
> +static const u32 x1600_lcd_formats[] = {
> +	DRM_FORMAT_RGB565,
> +};
> +
> +static struct x1600_lcd *to_x1600_lcd(struct drm_device *drm)
> +{
> +	return container_of(drm, struct x1600_lcd, drm);
> +}
> +
> +static dma_addr_t x1600_lcd_desc_addr(struct x1600_lcd *lcd, unsigned int i)
> +{
> +	return lcd->desc_phys + i * sizeof(*lcd->desc);
> +}
> +
> +static void x1600_lcd_irq_enable(struct x1600_lcd *lcd, u32 mask, bool
> enable)
> +{
> +	unsigned long flags;
> +	u32 val;
> +
> +	spin_lock_irqsave(&lcd->irq_lock, flags);
> +	val = readl(lcd->base + X1600_LCD_INTC);
> +	if (enable)
> +		val |= mask;
> +	else
> +		val &= ~mask;
> +	/* Seen on the R1: a bit still set when it is masked keeps the line
> up. */
> +	writel(mask, lcd->base + X1600_LCD_CLR_ST);
> +	writel(val, lcd->base + X1600_LCD_INTC);
> +	spin_unlock_irqrestore(&lcd->irq_lock, flags);
> +}
> +
> +/* Stops at the end of the frame; false if the controller did not report it.
> */
> +static bool x1600_lcd_stop(struct x1600_lcd *lcd)
> +{
> +	unsigned long left;
> +
> +	reinit_completion(&lcd->stopped);
> +	x1600_lcd_irq_enable(lcd, X1600_LCD_ST_STOP_SRD_ACK, true);
> +	writel(X1600_LCD_CTRL_GEN_STP_SRD, lcd->base + X1600_LCD_CTRL);
> +	left = wait_for_completion_timeout(&lcd->stopped,
> +					  
> msecs_to_jiffies(X1600_LCD_STOP_TIMEOUT_MS));
> +	x1600_lcd_irq_enable(lcd, X1600_LCD_ST_STOP_SRD_ACK, false);
> +	return left;
> +}
> +
> +static u32 x1600_lcd_timing(unsigned int start, unsigned int end)
> +{
> +	return FIELD_PREP(X1600_LCD_TIMING_START, start) |
> +	       FIELD_PREP(X1600_LCD_TIMING_END, end);
> +}
> +
> +/* Sends the commit's event once the controller has read the new descriptor.
> */
> +static void x1600_lcd_queue_event(struct x1600_lcd *lcd, struct
> drm_crtc_state *crtc_state)
> +{
> +	struct drm_pending_vblank_event *event = crtc_state->event;
> +	struct drm_crtc *crtc = &lcd->crtc;
> +
> +	if (!event)
> +		return;
> +
> +	crtc_state->event = NULL;
> +
> +	/*
> +	 * The controller takes a new descriptor "at appropriate time"
> (manual,
> +	 * 10.6.4): wait until it reports reading the one just handed over.
> +	 */
> +	spin_lock_irq(&crtc->dev->event_lock);
> +	if (lcd->running && !WARN_ON(lcd->event) && drm_crtc_vblank_get(crtc)
> == 0) {
> +		lcd->event = event;
> +		lcd->event_desc = x1600_lcd_desc_addr(lcd, lcd->cur);
> +	} else {
> +		drm_crtc_send_vblank_event(crtc, event);
> +	}
> +	spin_unlock_irq(&crtc->dev->event_lock);
> +}
> +
> +static void x1600_lcd_crtc_atomic_enable(struct drm_crtc *crtc,
> +					 struct drm_atomic_commit *state)
> +{
> +	struct drm_crtc_state *crtc_state =
> drm_atomic_get_new_crtc_state(state, crtc);
> +	struct x1600_lcd *lcd = to_x1600_lcd(crtc->dev);
> +	struct drm_display_mode *mode = &crtc->state->adjusted_mode;

This bothers me a bit, shouldn't it be using 'crtc_state'?

> +	unsigned int hde = mode->htotal - mode->hsync_start;
> +	unsigned int vde = mode->vtotal - mode->vsync_start;
> +	u32 val;
> +	int ret;
> +
> +	ret = clk_set_rate(lcd->clk, mode->crtc_clock * 1000);
> +	if (ret)
> +		dev_warn(lcd->drm.dev, "Unable to set the pixel clock: %d\n",
> ret);
> +	ret = clk_prepare_enable(lcd->clk);
> +	if (ret) {
> +		dev_err(lcd->drm.dev, "Unable to start the pixel clock:
> %d\n", ret);
> +		x1600_lcd_queue_event(lcd, crtc_state);
> +		return;
> +	}
> +
> +	writel(readl(lcd->base + X1600_LCD_ST), lcd->base +
> X1600_LCD_CLR_ST);

Not fond of the writel(readl()) nesting here.

> +	val = readl(lcd->base + X1600_LCD_COM_CONFIG) &
> ~X1600_LCD_COM_CONFIG_BURST_LEN;
> +	writel(val | FIELD_PREP(X1600_LCD_COM_CONFIG_BURST_LEN,
> X1600_LCD_BURST_LEN_32),
> +	       lcd->base + X1600_LCD_COM_CONFIG);
> +
> +	/* In the manual's order: timings, output format, then the interface.
> */
> +	writel(x1600_lcd_timing(mode->hsync_end - mode->hsync_start, mode-
> >htotal),
> +	       lcd->base + X1600_LCD_TFT_HSYNC);
> +	writel(x1600_lcd_timing(mode->vsync_end - mode->vsync_start, mode-
> >vtotal),
> +	       lcd->base + X1600_LCD_TFT_VSYNC);
> +	writel(x1600_lcd_timing(hde, hde + mode->hdisplay),
> +	       lcd->base + X1600_LCD_TFT_HDE);
> +	writel(x1600_lcd_timing(vde, vde + mode->vdisplay),
> +	       lcd->base + X1600_LCD_TFT_VDE);
> +
> +	val = FIELD_PREP(X1600_LCD_TFT_COLOR_EVEN, lcd->color_order) |
> +	      FIELD_PREP(X1600_LCD_TFT_COLOR_ODD, lcd->color_order) |
> +	      FIELD_PREP(X1600_LCD_TFT_MODE, X1600_LCD_TFT_MODE_24BIT);
> +	if (lcd->bus_flags & DRM_BUS_FLAG_PIXDATA_DRIVE_NEGEDGE)
> +		val |= X1600_LCD_TFT_PIX_CLK_INV;
> +	if (lcd->bus_flags & DRM_BUS_FLAG_DE_LOW)
> +		val |= X1600_LCD_TFT_DE_DL;
> +	/* The *_DL bits set the level outside the pulse. */
> +	if (mode->flags & DRM_MODE_FLAG_NHSYNC)
> +		val |= X1600_LCD_TFT_HSYNC_DL;
> +	if (mode->flags & DRM_MODE_FLAG_NVSYNC)
> +		val |= X1600_LCD_TFT_VSYNC_DL;
> +	writel(val, lcd->base + X1600_LCD_TFT_TRAN_CFG);
> +
> +	/* No dithering: it only applies to 888 sources. */
> +	val = readl(lcd->base + X1600_LCD_DISP_COM);
> +	val &= ~(X1600_LCD_DISP_COM_IF | X1600_LCD_DISP_COM_DITHER_EN);
> +	val |= FIELD_PREP(X1600_LCD_DISP_COM_IF, X1600_LCD_DISP_COM_IF_TFT) |
> +	       X1600_LCD_DISP_COM_TFT_CLKGATE |
> X1600_LCD_DISP_COM_SLCD_CLKGATE;
> +	writel(val, lcd->base + X1600_LCD_DISP_COM);

This whole block above looks like out of place. Why not do it in
x1600_lcd_encoder_atomic_mode_set()?

Bonus point, you wouldn't need the 'bus_flags' and 'color_order' variables.

> +
> +	writel(x1600_lcd_desc_addr(lcd, lcd->cur), lcd->base +
> X1600_LCD_SRD_CHAIN_ADDR);
> +	writel(1, lcd->base + X1600_LCD_SRD_CHAIN_START);
> +	lcd->running = true;
> +
> +	drm_crtc_vblank_on(crtc);
> +	x1600_lcd_queue_event(lcd, crtc_state);
> +}
> +
> +static void x1600_lcd_crtc_atomic_disable(struct drm_crtc *crtc,
> +					  struct drm_atomic_commit *state)
> +{
> +	struct x1600_lcd *lcd = to_x1600_lcd(crtc->dev);
> +
> +	/* atomic_enable() could not start the clock: nothing runs. */
> +	if (!lcd->running)
> +		return;
> +
> +	if (!x1600_lcd_stop(lcd))
> +		dev_warn(lcd->drm.dev, "The controller did not stop\n");
> +	lcd->running = false;
> +
> +	spin_lock_irq(&crtc->dev->event_lock);
> +	if (lcd->event) {
> +		drm_crtc_send_vblank_event(crtc, lcd->event);
> +		drm_crtc_vblank_put(crtc);
> +		lcd->event = NULL;
> +	}
> +	spin_unlock_irq(&crtc->dev->event_lock);
> +
> +	drm_crtc_vblank_off(crtc);
> +	clk_disable_unprepare(lcd->clk);
> +}
> +
> +static int x1600_lcd_crtc_atomic_check(struct drm_crtc *crtc,
> +				       struct drm_atomic_commit *state)
> +{
> +	struct drm_crtc_state *crtc_state =
> drm_atomic_get_new_crtc_state(state, crtc);
> +
> +	/* Only when enabled: drm_atomic_helper_shutdown() disables both at
> once. */
> +	if (!crtc_state->enable)
> +		return 0;
> +
> +	return drm_atomic_helper_check_crtc_primary_plane(crtc_state);
> +}
> +
> +static void x1600_lcd_crtc_atomic_flush(struct drm_crtc *crtc,
> +					struct drm_atomic_commit *state)
> +{
> +	struct drm_crtc_state *crtc_state =
> drm_atomic_get_new_crtc_state(state, crtc);
> +
> +	/* The planes come first: a CRTC being enabled sends it once started.
> */
> +	if (crtc_state->active && drm_atomic_crtc_needs_modeset(crtc_state))
> +		return;
> +
> +	x1600_lcd_queue_event(to_x1600_lcd(crtc->dev), crtc_state);
> +}
> +
> +static enum drm_mode_status
> +x1600_lcd_crtc_mode_valid(struct drm_crtc *crtc, const struct
> drm_display_mode *mode)
> +{
> +	unsigned int pixels = mode->hdisplay * mode->vdisplay;
> +
> +	if (mode->htotal > FIELD_MAX(X1600_LCD_TIMING_END) ||
> +	    mode->vtotal > FIELD_MAX(X1600_LCD_TIMING_END))
> +		return MODE_BAD;
> +
> +	/* The sync pulses' end points must not be 0. */
> +	if (mode->hsync_end == mode->hsync_start)
> +		return MODE_HSYNC_NARROW;
> +	if (mode->vsync_end == mode->vsync_start)
> +		return MODE_VSYNC_NARROW;
> +
> +	if (pixels > X1600_LCD_MAX_PIXELS)
> +		return MODE_MEM;

The max resolution is 1280x720 so you should check mode->hdisplay / mode-
>vdisplay individually. Otherwise bogus resolutions like 1920x480 would be
accepted.

> +	if (pixels < X1600_LCD_MIN_PIXELS)
> +		return MODE_BAD;
> +
> +	return MODE_OK;
> +}
> +
> +static int x1600_lcd_enable_vblank(struct drm_crtc *crtc)
> +{
> +	struct x1600_lcd *lcd = to_x1600_lcd(crtc->dev);
> +
> +	x1600_lcd_irq_enable(lcd, X1600_LCD_ST_DISP_END, true);
> +	return 0;
> +}
> +
> +static void x1600_lcd_disable_vblank(struct drm_crtc *crtc)
> +{
> +	struct x1600_lcd *lcd = to_x1600_lcd(crtc->dev);
> +
> +	x1600_lcd_irq_enable(lcd, X1600_LCD_ST_DISP_END, false);
> +}
> +
> +static irqreturn_t x1600_lcd_irq(int irq, void *data)
> +{
> +	struct x1600_lcd *lcd = data;
> +	u32 st = readl(lcd->base + X1600_LCD_INT_FLAG);
> +
> +	if (!st)
> +		return IRQ_NONE;
> +
> +	writel(st, lcd->base + X1600_LCD_CLR_ST);
> +	if (st & X1600_LCD_ST_STOP_SRD_ACK)
> +		complete(&lcd->stopped);
> +
> +	if (st & X1600_LCD_ST_DISP_END) {
> +		drm_crtc_handle_vblank(&lcd->crtc);
> +
> +		spin_lock(&lcd->drm.event_lock);

spin_lock() can sleep with CONFIG_PREEMPT_RT so it looks wrong in an IRQ
handler. I *think* you don't need to lock it here anyway, as the spinlock is
locked in other places using spin_lock_irq().

> +		if (lcd->event &&
> +		    readl(lcd->base + X1600_LCD_CHAIN_SITE) == lcd-
> >event_desc) {
> +			drm_crtc_send_vblank_event(&lcd->crtc, lcd->event);
> +			drm_crtc_vblank_put(&lcd->crtc);
> +			lcd->event = NULL;
> +		}
> +		spin_unlock(&lcd->drm.event_lock);
> +	}
> +
> +	return IRQ_HANDLED;
> +}
> +
> +static int x1600_lcd_plane_atomic_check(struct drm_plane *plane,
> +					struct drm_atomic_commit *state)
> +{
> +	struct drm_plane_state *plane_state =
> drm_atomic_get_new_plane_state(state, plane);
> +	struct drm_crtc_state *crtc_state;
> +	int ret;
> +
> +	if (!plane_state->crtc)
> +		return 0;
> +
> +	crtc_state = drm_atomic_get_new_crtc_state(state, plane_state->crtc);
> +	if (WARN_ON(!crtc_state))
> +		return -EINVAL;
> +
> +	/* The plane covers the whole screen: no scaling, no position. */
> +	ret = drm_atomic_helper_check_plane_state(plane_state, crtc_state,
> +						  DRM_PLANE_NO_SCALING,
> +						  DRM_PLANE_NO_SCALING,
> +						  false, true);
> +	if (ret)
> +		return ret;
> +
> +	/* The controller scans out a whole frame from the framebuffer. */
> +	if (crtc_state->enable && !plane_state->visible)
> +		return -EINVAL;
> +
> +	return 0;
> +}
> +
> +static void x1600_lcd_plane_atomic_update(struct drm_plane *plane,
> +					  struct drm_atomic_commit *state)
> +{
> +	struct x1600_lcd *lcd = to_x1600_lcd(plane->dev);
> +	struct drm_plane_state *plane_state =
> drm_atomic_get_new_plane_state(state, plane);
> +	struct drm_framebuffer *fb = plane_state->fb;
> +	unsigned int next = lcd->running ? !lcd->cur : lcd->cur;
> +	struct x1600_lcd_desc *desc = &lcd->desc[next];
> +
> +	if (!fb)
> +		return;
> +
> +	desc->next = x1600_lcd_desc_addr(lcd, next);
> +	desc->addr = drm_fb_dma_get_gem_addr(fb, plane_state, 0);
> +	desc->stride = fb->pitches[0] / fb->format->cpp[0];
> +	desc->cfg = FIELD_PREP(X1600_LCD_DESC_CFG_FMT,
> X1600_LCD_DESC_FMT_RGB565);
> +	desc->ctrl = X1600_LCD_DESC_CTRL_EOD_MSK;
> +
> +	if (!lcd->running)
> +		return;
> +
> +	/*
> +	 * The manual hands over a descriptor by writing SRD_CHAIN_ADDR; the
> +	 * vendor kernel also sets SRD_CHAIN_START, and so does this.
> +	 */
> +	wmb();
> +	writel(x1600_lcd_desc_addr(lcd, next), lcd->base +
> X1600_LCD_SRD_CHAIN_ADDR);
> +	writel(1, lcd->base + X1600_LCD_SRD_CHAIN_START);
> +	lcd->cur = next;
> +}
> +
> +/*
> + * Panels with 18 data lines take the top six bits of each colour of the
> + * 888 mode: in the 666 mode, the HiBy R1's panel shows wrong colours.
> + */
> +static int x1600_lcd_color_order(const struct drm_display_info *info)
> +{
> +	if (!info->num_bus_formats)
> +		return X1600_LCD_TFT_COLOR_RGB;
> +
> +	switch (info->bus_formats[0]) {
> +	case MEDIA_BUS_FMT_RGB666_1X18:
> +	case MEDIA_BUS_FMT_RGB888_1X24:
> +		return X1600_LCD_TFT_COLOR_RGB;
> +	case MEDIA_BUS_FMT_BGR666_1X18:
> +	case MEDIA_BUS_FMT_BGR888_1X24:
> +		return X1600_LCD_TFT_COLOR_BGR;
> +	default:
> +		return -EINVAL;
> +	}
> +}
> +
> +static int x1600_lcd_encoder_atomic_check(struct drm_encoder *encoder,
> +					  struct drm_crtc_state *crtc_state,
> +					  struct drm_connector_state
> *conn_state)
> +{
> +	const struct drm_display_info *info = &conn_state->connector-
> >display_info;
> +
> +	if (x1600_lcd_color_order(info) < 0) {
> +		drm_dbg_kms(encoder->dev, "Unsupported bus format 0x%04x\n",
> +			    info->bus_formats[0]);
> +		return -EINVAL;
> +	}
> +
> +	return 0;
> +}
> +
> +static void x1600_lcd_encoder_atomic_mode_set(struct drm_encoder *encoder,
> +					      struct drm_crtc_state
> *crtc_state,
> +					      struct drm_connector_state
> *conn_state)
> +{
> +	struct x1600_lcd *lcd = to_x1600_lcd(encoder->dev);
> +	const struct drm_display_info *info = &conn_state->connector-
> >display_info;
> +
> +	lcd->bus_flags = info->bus_flags;
> +	lcd->color_order = x1600_lcd_color_order(info);
> +}
> +
> +static const struct drm_plane_funcs x1600_lcd_plane_funcs = {
> +	.update_plane		= drm_atomic_helper_update_plane,
> +	.disable_plane		= drm_atomic_helper_disable_plane,
> +	.atomic_create_state	= drm_atomic_helper_plane_create_state,
> +	.destroy		= drm_plane_cleanup,
> +	.atomic_duplicate_state	=
> drm_atomic_helper_plane_duplicate_state,
> +	.atomic_destroy_state	= drm_atomic_helper_plane_destroy_state,
> +};
> +
> +static const struct drm_plane_helper_funcs x1600_lcd_plane_helper_funcs = {
> +	.atomic_check		= x1600_lcd_plane_atomic_check,
> +	.atomic_update		= x1600_lcd_plane_atomic_update,
> +};
> +
> +static const struct drm_crtc_funcs x1600_lcd_crtc_funcs = {
> +	.set_config		= drm_atomic_helper_set_config,
> +	.page_flip		= drm_atomic_helper_page_flip,
> +	.atomic_create_state	= drm_atomic_helper_crtc_create_state,
> +	.destroy		= drm_crtc_cleanup,
> +	.atomic_duplicate_state	=
> drm_atomic_helper_crtc_duplicate_state,
> +	.atomic_destroy_state	= drm_atomic_helper_crtc_destroy_state,
> +	.enable_vblank		= x1600_lcd_enable_vblank,
> +	.disable_vblank		= x1600_lcd_disable_vblank,
> +};
> +
> +static const struct drm_crtc_helper_funcs x1600_lcd_crtc_helper_funcs = {
> +	.atomic_check		= x1600_lcd_crtc_atomic_check,
> +	.atomic_enable		= x1600_lcd_crtc_atomic_enable,
> +	.atomic_disable		= x1600_lcd_crtc_atomic_disable,
> +	.atomic_flush		= x1600_lcd_crtc_atomic_flush,
> +	.mode_valid		= x1600_lcd_crtc_mode_valid,
> +};
> +
> +static const struct drm_encoder_helper_funcs x1600_lcd_encoder_helper_funcs =
> {
> +	.atomic_check		= x1600_lcd_encoder_atomic_check,
> +	.atomic_mode_set	= x1600_lcd_encoder_atomic_mode_set,
> +};
> +
> +/*
> + * drm_atomic_helper_commit_tail(), waiting for the flip instead of a vblank:
> + * the old framebuffer stays in use until the controller reads the new
> + * descriptor, which can be a frame later.
> + */
> +static void x1600_lcd_commit_tail(struct drm_atomic_commit *state)
> +{
> +	struct drm_device *drm = state->dev;
> +
> +	drm_atomic_helper_commit_modeset_disables(drm, state);
> +	drm_atomic_helper_commit_planes(drm, state, 0);
> +	drm_atomic_helper_commit_modeset_enables(drm, state);
> +	drm_atomic_helper_fake_vblank(state);
> +	drm_atomic_helper_commit_hw_done(state);
> +	drm_atomic_helper_wait_for_flip_done(drm, state);
> +	drm_atomic_helper_cleanup_planes(drm, state);
> +}
> +
> +static const struct drm_mode_config_funcs x1600_lcd_mode_config_funcs = {
> +	.fb_create		= drm_gem_fb_create,
> +	.atomic_check		= drm_atomic_helper_check,
> +	.atomic_commit		= drm_atomic_helper_commit,
> +};
> +
> +static const struct drm_mode_config_helper_funcs
> x1600_lcd_mode_config_helpers = {
> +	.atomic_commit_tail	= x1600_lcd_commit_tail,
> +};
> +
> +DEFINE_DRM_GEM_DMA_FOPS(x1600_lcd_fops);
> +
> +static const struct drm_driver x1600_lcd_driver = {
> +	.driver_features	= DRIVER_MODESET | DRIVER_GEM |
> DRIVER_ATOMIC,
> +	.name			= "ingenic-x1600-drm",
> +	.desc			= "DRM driver for the Ingenic X1600 display
> controller",
> +	.major			= 1,
> +	.minor			= 0,
> +	.fops			= &x1600_lcd_fops,
> +	DRM_GEM_DMA_DRIVER_OPS,
> +	DRM_FBDEV_DMA_DRIVER_OPS,
> +};
> +
> +static int x1600_lcd_probe(struct platform_device *pdev)
> +{
> +	struct device *dev = &pdev->dev;
> +	struct drm_connector *connector;
> +	struct drm_bridge *bridge;
> +	struct drm_device *drm;
> +	struct x1600_lcd *lcd;
> +	int irq, ret;
> +
> +	lcd = devm_drm_dev_alloc(dev, &x1600_lcd_driver, struct x1600_lcd,
> drm);
> +	if (IS_ERR(lcd))
> +		return PTR_ERR(lcd);
> +
> +	drm = &lcd->drm;
> +	platform_set_drvdata(pdev, lcd);
> +	init_completion(&lcd->stopped);
> +	spin_lock_init(&lcd->irq_lock);
> +
> +	lcd->base = devm_platform_ioremap_resource(pdev, 0);
> +	if (IS_ERR(lcd->base))
> +		return PTR_ERR(lcd->base);
> +
> +	lcd->clk = devm_clk_get(dev, NULL);
> +	if (IS_ERR(lcd->clk))
> +		return dev_err_probe(dev, PTR_ERR(lcd->clk), "Unable to get
> clock\n");

You enable it later in the probe, so you could just use devm_clk_get_enabled()
here.

> +
> +	irq = platform_get_irq(pdev, 0);
> +	if (irq < 0)
> +		return irq;
> +
> +	bridge = devm_drm_of_get_bridge(dev, dev->of_node, 0, 0);
> +	if (IS_ERR(bridge))
> +		return dev_err_probe(dev, PTR_ERR(bridge), "Unable to get the
> panel\n");
> +
> +	lcd->desc = dmam_alloc_coherent(dev, 2 * sizeof(*lcd->desc),
> +					&lcd->desc_phys, GFP_KERNEL);
> +	if (!lcd->desc)
> +		return -ENOMEM;
> +
> +	ret = drmm_mode_config_init(drm);
> +	if (ret)
> +		return ret;
> +
> +	/* Framebuffers, not modes: fbdev's double buffer is taller than
> 720p. */
> +	drm->mode_config.max_width = FIELD_MAX(X1600_LCD_TIMING_END);
> +	drm->mode_config.max_height = FIELD_MAX(X1600_LCD_TIMING_END);
> +	drm->mode_config.funcs = &x1600_lcd_mode_config_funcs;
> +	drm->mode_config.helper_private = &x1600_lcd_mode_config_helpers;
> +
> +	ret = drm_universal_plane_init(drm, &lcd->plane, 1,
> &x1600_lcd_plane_funcs,
> +				       x1600_lcd_formats,
> ARRAY_SIZE(x1600_lcd_formats),
> +				       NULL, DRM_PLANE_TYPE_PRIMARY, NULL);
> +	if (ret)
> +		return ret;
> +	drm_plane_helper_add(&lcd->plane, &x1600_lcd_plane_helper_funcs);
> +
> +	ret = drm_crtc_init_with_planes(drm, &lcd->crtc, &lcd->plane, NULL,
> +					&x1600_lcd_crtc_funcs, NULL);
> +	if (ret)
> +		return ret;
> +	drm_crtc_helper_add(&lcd->crtc, &x1600_lcd_crtc_helper_funcs);
> +
> +	ret = drmm_encoder_init(drm, &lcd->encoder, NULL,
> DRM_MODE_ENCODER_DPI, NULL);
> +	if (ret)
> +		return ret;
> +	lcd->encoder.possible_crtcs = drm_crtc_mask(&lcd->crtc);
> +	drm_encoder_helper_add(&lcd->encoder,
> &x1600_lcd_encoder_helper_funcs);
> +
> +	ret = drm_bridge_attach(&lcd->encoder, bridge, NULL,
> +				DRM_BRIDGE_ATTACH_NO_CONNECTOR);
> +	if (ret)
> +		return ret;
> +
> +	connector = drm_bridge_connector_init(drm, &lcd->encoder);
> +	if (IS_ERR(connector))
> +		return PTR_ERR(connector);
> +	drm_connector_attach_encoder(connector, &lcd->encoder);
> +
> +	ret = drm_vblank_init(drm, 1);
> +	if (ret)
> +		return ret;
> +
> +	drm_mode_config_reset(drm);

I saw a patch from Maxime replacing these - you should be calling
drm_mode_config_create_initial_state(drm) here (and handle the error).

> +
> +	/* Take over from a boot framebuffer, and stop what it left running.
> */
> +	ret = aperture_remove_all_conflicting_devices(x1600_lcd_driver.name);
> +	if (ret)
> +		return ret;
> +
> +	ret = clk_prepare_enable(lcd->clk);
> +	if (ret)
> +		return ret;
> +
> +	/* No interrupt from what the boot loader left enabled or pending. */
> +	writel(0, lcd->base + X1600_LCD_INTC);
> +	writel(readl(lcd->base + X1600_LCD_ST), lcd->base +
> X1600_LCD_CLR_ST);
> +
> +	ret = devm_request_irq(dev, irq, x1600_lcd_irq, 0, dev_name(dev),
> lcd);
> +	if (ret) {
> +		clk_disable_unprepare(lcd->clk);
> +		return dev_err_probe(dev, ret, "Unable to request the
> IRQ\n");
> +	}
> +
> +	/*
> +	 * Seen on the R1: after an underrun (TFT_UNDR) the controller can
> stop
> +	 * answering GEN_STP_SRD, and only a reset brings it back.
> +	 */
> +	if (readl(lcd->base + X1600_LCD_ST) & X1600_LCD_ST_WORKING &&
> !x1600_lcd_stop(lcd))
> +		dev_err(dev, "The controller did not stop%s\n",
> +			readl(lcd->base + X1600_LCD_ST) &
> X1600_LCD_ST_TFT_UNDR ?
> +			" after an underrun" : "");
> +	clk_disable_unprepare(lcd->clk);
> +
> +	ret = drm_dev_register(drm, 0);
> +	if (ret)
> +		return ret;
> +
> +	drm_client_setup_with_fourcc(drm, DRM_FORMAT_RGB565);
> +	return 0;
> +}
> +
> +static void x1600_lcd_remove(struct platform_device *pdev)
> +{
> +	struct x1600_lcd *lcd = platform_get_drvdata(pdev);
> +
> +	drm_dev_unregister(&lcd->drm);
> +	drm_atomic_helper_shutdown(&lcd->drm);
> +}
> +
> +static void x1600_lcd_shutdown(struct platform_device *pdev)
> +{
> +	struct x1600_lcd *lcd = platform_get_drvdata(pdev);
> +
> +	drm_atomic_helper_shutdown(&lcd->drm);
> +}
> +
> +static const struct of_device_id x1600_lcd_of_match[] = {
> +	{ .compatible = "ingenic,x1600-dpu" },
> +	{ }
> +};
> +MODULE_DEVICE_TABLE(of, x1600_lcd_of_match);
> +
> +static struct platform_driver x1600_lcd_platform_driver = {
> +	.driver = {
> +		.name = "ingenic-x1600-drm",
> +		.of_match_table = x1600_lcd_of_match,
> +	},
> +	.probe = x1600_lcd_probe,
> +	.remove = x1600_lcd_remove,
> +	.shutdown = x1600_lcd_shutdown,
> +};
> +module_platform_driver(x1600_lcd_platform_driver);
> +
> +MODULE_AUTHOR("Olivier Louvignes <olivier@mgcrea.io>");
> +MODULE_DESCRIPTION("DRM driver for the Ingenic X1600 display controller");
> +MODULE_LICENSE("GPL");

Cheers,
-Paul

  reply	other threads:[~2026-11-06 13:34 UTC|newest]

Thread overview: 10+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-11-06 11:19 [PATCH v3 0/5] drm/ingenic: Add the X1600 display controller and the HiBy R1 panel Olivier Louvignes
2026-11-06 11:19 ` [PATCH v3 1/5] dt-bindings: vendor-prefixes: Add HiBy Olivier Louvignes
2026-11-06 11:19 ` [PATCH v3 2/5] dt-bindings: display: Add the Ingenic X1600 display controller Olivier Louvignes
2026-11-06 11:19 ` [PATCH v3 3/5] drm/ingenic: Add a driver for the " Olivier Louvignes
2026-11-06 13:34   ` Paul Cercueil [this message]
2026-11-06 14:38     ` Olivier Louvignes
2026-11-06 15:06       ` Paul Cercueil
2026-11-06 19:13         ` Olivier Louvignes
2026-11-06 11:19 ` [PATCH v3 4/5] dt-bindings: display: panel: Add the HiBy LG35583 Olivier Louvignes
2026-11-06 11:20 ` [PATCH v3 5/5] drm/panel: " Olivier Louvignes

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=0a2e8ea782fa819a547e0a3075303bd74a7fde72.camel@crapouillou.net \
    --to=paul@crapouillou.net \
    --cc=airlied@gmail.com \
    --cc=conor+dt@kernel.org \
    --cc=devicetree@vger.kernel.org \
    --cc=dri-devel@lists.freedesktop.org \
    --cc=hns@goldelico.com \
    --cc=jesszhan0024@gmail.com \
    --cc=krzk+dt@kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-mips@vger.kernel.org \
    --cc=maarten.lankhorst@linux.intel.com \
    --cc=mripard@kernel.org \
    --cc=neil.armstrong@linaro.org \
    --cc=olivier@mgcrea.io \
    --cc=robh@kernel.org \
    --cc=simona@ffwll.ch \
    --cc=thomas.petazzoni@bootlin.com \
    --cc=tzimmermann@suse.de \
    /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®