All of lore.kernel.org
 help / color / mirror / Atom feed
From: sashiko-bot@kernel.org
To: "Svyatoslav Ryhel" <clamor95@gmail.com>
Cc: dri-devel@lists.freedesktop.org, devicetree@vger.kernel.org,
	robh@kernel.org, conor+dt@kernel.org
Subject: Re: [PATCH v5 11/11] drm/tegra: Add support for 8-bit CPU interface
Date: Thu, 29 Oct 2026 15:55:09 +0000	[thread overview]
Message-ID: <sashiko-outbox-189642@kernel.org> (raw)
In-Reply-To: <20261029152909.234584-12-clamor95@gmail.com>

Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
- [High] drm/tegra: manual next_bridge calls in tegra_dbi_atomic_enable() break disable
- [High] drm/tegra: double drm_bridge_remove() and leak in tegra_dbi_host_attach()
- [High] drm/tegra: driver conflict and deferral deadlock in devm_tegra_dc_dbi_init()
- [Medium] drm/tegra: misconfigured pin select and mux in tegra_dbi_atomic_enable()
- [Low] drm/tegra: missing select DRM_MIPI_DBI causes link error in tegra_dbi_probe()
--

commit 38b0aba6a8c5c45d84b4bce85bd75f2063d5eedc
Author: Svyatoslav Ryhel <clamor95@gmail.com>

drm/tegra: Add support for 8-bit CPU interface

This commit adds a MIPI DBI Type B host and DRM bridge driver to support the
8-bit CPU interface on Tegra20/30 display controllers.

> diff --git a/drivers/gpu/drm/tegra/Makefile b/drivers/gpu/drm/tegra/Makefile
> index e399b40d64a1d..ae7fccb3d6421 100644
> --- a/drivers/gpu/drm/tegra/Makefile
> +++ b/drivers/gpu/drm/tegra/Makefile
> @@ -18,6 +18,7 @@ tegra-drm-y := \
>  	hdmi.o \
>  	mipi-phy.o \
>  	dsi.o \
> +	dbi.o \
>  	sor.o \
>  	dpaux.o \
>  	gr2d.o \

[Severity: Low]
Can building tegra-drm fail with an undefined reference to
devm_mipi_dbi_device_register() when CONFIG_DRM_MIPI_DBI is disabled or built
as a module while CONFIG_DRM_TEGRA is built-in?

Here dbi.o is unconditionally added to tegra-drm-y whenever CONFIG_DRM_TEGRA
is enabled, and tegra_dbi_probe() in drivers/gpu/drm/tegra/dbi.c calls
devm_mipi_dbi_device_register():

	dbidev = devm_mipi_dbi_device_register(dev, &priv->dbi_host, np);

However, devm_mipi_dbi_device_register() is defined in
drivers/gpu/drm/drm_mipi_dbi_bus.c, which is only compiled when
CONFIG_DRM_MIPI_DBI is enabled:

	obj-$(CONFIG_DRM_MIPI_DBI) += drm_mipi_dbi.o drm_mipi_dbi_bus.o

and include/drm/drm_mipi_dbi_bus.h does not provide fallback stubs.

Should config DRM_TEGRA in drivers/gpu/drm/tegra/Kconfig select
DRM_MIPI_DBI alongside DRM_MIPI_DSI and DRM_PANEL?

> diff --git a/drivers/gpu/drm/tegra/dbi.c b/drivers/gpu/drm/tegra/dbi.c
> new file mode 100644
> index 0000000000000..e129be0f21da5
> --- /dev/null
> +++ b/drivers/gpu/drm/tegra/dbi.c

[ ... ]

> +static int tegra_dbi_dbi_command(struct mipi_dbi *dbi, u8 *cmd, u8 *param, size_t num)
> +{
> +	struct tegra_dbi_priv *priv = mipi_dbi_to_tegra_dbi(dbi);
> +	u8 command = *cmd;
> +
> +	/* Blocks all commands when interface is not active */
> +	if (!priv->active)
> +		return -ENODEV;
> +
> +	tegra_dbi_write(priv, TEGRA_DBI_COMMAND, command);
> +
> +	for (int i = 0; i < num; i++)
> +		tegra_dbi_write(priv, TEGRA_DBI_DATA, param[i]);
> +
> +	return 0;
> +}
> +
> +static int tegra_dbi_host_attach(struct mipi_dbi_host *host, struct mipi_dbi_device *dbidev)
> +{
> +	struct tegra_dbi_priv *priv = dbi_host_to_tegra_dbi(host);
> +	struct device *dev = priv->dev;
> +	struct drm_bridge *bridge;
> +	struct device_node *ep;
> +	int ret;
> +
> +	bridge = of_drm_get_bridge_by_endpoint(dev_of_node(dev), 1, -1);
> +	if (IS_ERR(bridge))
> +		return PTR_ERR(bridge);
> +
> +	bridge->type = DRM_MODE_CONNECTOR_DPI;
> +	priv->next_bridge = bridge;
> +
> +	/* get input ep (port0/endpoint0) */
> +	ret = -EINVAL;
> +	ep = of_graph_get_endpoint_by_regs(dev_of_node(dev), 0, 0);
> +	if (ep) {
> +		ret = of_property_read_u32(ep, "bus-width", &priv->bus_width);
> +		of_node_put(ep);
> +	}
> +
> +	if (ret)
> +		priv->bus_width = 24;
> +
> +	return devm_drm_bridge_add(dev, &priv->bridge);
> +}
> +
> +static void tegra_dbi_host_detach(struct mipi_dbi_host *host, struct mipi_dbi_device *dbidev)
> +{
> +	struct tegra_dbi_priv *priv = dbi_host_to_tegra_dbi(host);
> +
> +	drm_bridge_remove(&priv->bridge);
> +	drm_bridge_put(priv->next_bridge);
> +}

[Severity: High]
Won't using devm_drm_bridge_add() in tegra_dbi_host_attach() alongside an
explicit drm_bridge_remove() in tegra_dbi_host_detach() cause
drm_bridge_remove() to run twice on priv->bridge during unbind, as well as
leak priv->next_bridge if devm_drm_bridge_add() fails?

In tegra_dbi_probe(), priv is allocated with devm_drm_bridge_alloc(), which
initializes priv->bridge.refcount to 1 and registers a devres action on
priv->dev to call drm_bridge_put_void(&priv->bridge).

When tegra_dbi_host_attach() calls devm_drm_bridge_add(dev, &priv->bridge),
it increments priv->bridge.refcount to 2 and registers a devres action on
priv->dev to call drm_bridge_remove_void(&priv->bridge).

When the DBI device or tegra-dbi unbinds, mipi_dbi_device_unregister()
invokes:

mipi_dbi_device_unregister()
  mipi_dbi_detach()
    tegra_dbi_host_detach()
      drm_bridge_remove(&priv->bridge)

which destroys bridge->hpd_mutex and bridge->hpd_state_mutex and drops
priv->bridge.refcount from 2 to 1.

When priv->dev's devres then runs drm_bridge_remove_void(&priv->bridge), it
calls drm_bridge_remove(&priv->bridge) a second time, destroying both mutexes
again and dropping priv->bridge.refcount from 1 to 0, which frees priv via
__drm_bridge_free(). Immediately afterward, the drm_bridge_put_void() devres
action from devm_drm_bridge_alloc() runs on the already-freed priv->bridge.

Also, if devm_drm_bridge_add() fails in tegra_dbi_host_attach(), the
reference on priv->next_bridge acquired by of_drm_get_bridge_by_endpoint() is
never released with drm_bridge_put(). Should tegra_dbi_host_attach() use
drm_bridge_add() instead of devm_drm_bridge_add()?

[ ... ]

> +static void tegra_dbi_atomic_enable(struct drm_bridge *bridge,
> +				    struct drm_atomic_commit *state)
> +{
> +	struct tegra_dbi_priv *priv = bridge_to_tegra_dbi(bridge);
> +	struct drm_bridge *next_bridge = priv->next_bridge;
> +	struct tegra_dc *dc = priv->dc;
> +	u32 value;

[ ... ]

> +	/* Unshadow to set up command and data pins */
> +	tegra_dc_writel(dc, WRITE_MUX | READ_MUX, DC_CMD_STATE_ACCESS);
> +
> +	if (priv->reg)
> +		tegra_dbi_init_secondary_interface(priv);
> +	else
> +		tegra_dbi_init_primary_interface(priv);
> +
> +	priv->active = true;
> +
> +	/*
> +	 * DBI requires that the next bridge be enabled here in order to
> +	 * send DCS commands, since the interface pins will subsequently be
> +	 * reconfigured for video data.
> +	 */
> +	if (next_bridge->funcs && next_bridge->funcs->atomic_enable)
> +		next_bridge->funcs->atomic_enable(next_bridge, state);
> +
> +	tegra_dbi_pin_set(dc, &priv->index->cs, 0);
> +
> +	priv->active = false;
> +
> +	tegra_dc_writel(dc, 0, DC_CMD_STATE_ACCESS);
> +
> +	for (int i = 0; i <  ARRAY_SIZE(com_pin_output); i++)
> +		tegra_dc_writel(dc, priv->com_pin_backup[i].value,
> +				priv->com_pin_backup[i].offset);
> +
> +	tegra_dc_commit(dc);
> +
> +	/* Set LDC pin to V Pulse 1 */
> +	value = tegra_dc_readl(dc, DC_COM_PIN_OUTPUT_SELECT(6));
> +	value |= PIN_OUTPUT_SELECT_F4(PIN_OUTPUT_SELECT_LDC);
> +	tegra_dc_writel(dc, value, DC_COM_PIN_OUTPUT_SELECT(6));
> +
> +	value = tegra_dc_readl(dc, DC_CMD_DISPLAY_COMMAND);
> +	value &= ~DISP_CTRL_MODE_MASK;
> +	value |= DISP_CTRL_MODE_C_DISPLAY;
> +	tegra_dc_writel(dc, value, DC_CMD_DISPLAY_COMMAND);
> +}
> +
> +static void tegra_dbi_atomic_disable(struct drm_bridge *bridge,
> +				     struct drm_atomic_commit *state)
> +{
> +	struct tegra_dbi_priv *priv = bridge_to_tegra_dbi(bridge);
> +	struct drm_bridge *next_bridge = priv->next_bridge;
> +	struct tegra_dc *dc = priv->dc;
> +
> +	for (int i = 0; i < ARRAY_SIZE(com_pin_output); i++)
> +		priv->com_pin_backup[i].value = tegra_dc_readl(dc,
> +			priv->com_pin_backup[i].offset);
> +
> +	tegra_dc_writel(dc, WRITE_MUX, DC_CMD_STATE_ACCESS);
> +
> +	if (priv->reg)
> +		tegra_dbi_init_secondary_interface(priv);
> +	else
> +		tegra_dbi_init_primary_interface(priv);
> +
> +	priv->active = true;
> +
> +	/*
> +	 * DBI requires that the next bridge be disabled here in order to
> +	 * send DCS commands, since the interface pins will subsequently be
> +	 * reconfigured for video data.
> +	 */
> +	if (next_bridge->funcs && next_bridge->funcs->atomic_disable)
> +		next_bridge->funcs->atomic_disable(next_bridge, state);
> +
> +	priv->active = false;
> +
> +	tegra_dc_writel(dc, 0, DC_CMD_STATE_ACCESS);
> +
> +	for (int i = 0; i <  ARRAY_SIZE(com_pin_output); i++)
> +		tegra_dc_writel(dc, priv->com_pin_backup[i].value,
> +				priv->com_pin_backup[i].offset);
> +
> +	tegra_dc_commit(dc);
> +}

[Severity: High]
Because tegra_dbi_attach() attaches priv->next_bridge to the encoder's
bridge_chain via drm_bridge_attach(), won't the DRM bridge chain helpers
already invoke next_bridge->funcs->atomic_enable() and atomic_disable(),
causing panel DCS commands during disable to fail with -ENODEV?

When disabling the display, drm_atomic_bridge_chain_disable() iterates
encoder->bridge_chain in reverse order, so it calls
priv->next_bridge->funcs->atomic_disable() (for example,
panel_bridge_atomic_disable() -> drm_panel_disable()) before
tegra_dbi_atomic_disable() runs. At that point priv->active is still false
and the DC pins are still in video mode, so every MIPI DCS command issued by
the panel's disable callback is rejected by tegra_dbi_dbi_command() with
-ENODEV, while drm_panel_disable() still marks panel->enabled = false.

When drm_atomic_bridge_chain_disable() subsequently calls
tegra_dbi_atomic_disable(), it sets priv->active = true and manually calls
next_bridge->funcs->atomic_disable(next_bridge, state) a second time, which
sees panel->enabled == false, logs "Skipping disable of already disabled
panel", and returns without sending the panel's DCS disable sequence.

Similarly, during enable, drm_atomic_bridge_chain_enable() calls
tegra_dbi_atomic_enable(), which manually calls
next_bridge->funcs->atomic_enable(next_bridge, state), and then
drm_atomic_bridge_chain_enable() advances to priv->next_bridge in
encoder->bridge_chain and calls next_bridge->funcs->atomic_enable() a second
time while priv->active is false.

[Severity: Medium]
In tegra_dbi_atomic_enable() and tegra_dbi_atomic_disable(), there also
appear to be a couple of register configuration issues:

First, at the end of tegra_dbi_atomic_enable():

	/* Set LDC pin to V Pulse 1 */
	value = tegra_dc_readl(dc, DC_COM_PIN_OUTPUT_SELECT(6));
	value |= PIN_OUTPUT_SELECT_F4(PIN_OUTPUT_SELECT_LDC);
	tegra_dc_writel(dc, value, DC_COM_PIN_OUTPUT_SELECT(6));

This unconditionally modifies PIN_LDC in DC_COM_PIN_OUTPUT_SELECT(6) without
clearing PIN_OUTPUT_SELECT_MASK(PIN_OUTPUT_SELECT_LDC) first, and without
checking priv->reg. When the secondary interface (priv->reg == 1) is used,
tegra_dbi_idx[1] uses PIN_LVP0 in DC_COM_PIN_OUTPUT_SELECT(4) as the D/C
pin rather than PIN_LDC. Does the secondary interface need V_PULSE1 routed to
its own D/C pin instead of PIN_LDC?

Second, tegra_dbi_atomic_disable() writes only WRITE_MUX to
DC_CMD_STATE_ACCESS:

	tegra_dc_writel(dc, WRITE_MUX, DC_CMD_STATE_ACCESS);

whereas tegra_dbi_atomic_enable() writes WRITE_MUX | READ_MUX before calling
tegra_dbi_init_primary_interface() or tegra_dbi_init_secondary_interface().
Without READ_MUX set, won't the tegra_dc_readl() calls in those init
functions read from the shadowed assembly registers while tegra_dc_writel()
writes to the active registers?

[ ... ]

> +static struct platform_driver tegra_dbi_driver = {
> +	.driver.name = "tegra-dbi",
> +	.probe = tegra_dbi_probe,
> +};
> +
> +static void tegra_dbi_release(void *pdev)
> +{
> +	platform_device_unregister(pdev);
> +	platform_driver_unregister(&tegra_dbi_driver);
> +}
> +
> +int devm_tegra_dc_dbi_init(struct device *dev)
> +{
> +	struct device_node *dbi_np __free(device_node) =
> +		of_get_available_child_by_name(dev_of_node(dev), "dbi");
> +	struct platform_device_info dbi_info = { 0 };
> +	struct platform_device *dbi;
> +	int ret;
> +
> +	/* Missing node means that DBI is not used, continue */
> +	if (!dbi_np)
> +		return 0;
> +
> +	ret = platform_driver_register(&tegra_dbi_driver);
> +	if (ret)
> +		return dev_err_probe(dev, ret,
> +				     "Failed to register DBI driver\n");
> +
> +	dbi_info.parent = dev;
> +	dbi_info.name = "tegra-dbi";
> +	dbi_info.id = PLATFORM_DEVID_AUTO;
> +	dbi_info.fwnode = of_fwnode_handle(dbi_np);
> +	dbi_info.of_node_reused = true;
> +
> +	dbi = platform_device_register_full(&dbi_info);
> +	if (IS_ERR(dbi)) {
> +		platform_driver_unregister(&tegra_dbi_driver);
> +		return dev_err_probe(dev, PTR_ERR(dbi),
> +				     "Failed to create DBI device\n");
> +	}
> +
> +	ret = devm_add_action_or_reset(dev, tegra_dbi_release, dbi);
> +	if (ret)
> +		return ret;
> +
> +	return 0;
> +}
> diff --git a/drivers/gpu/drm/tegra/dc.c b/drivers/gpu/drm/tegra/dc.c
> index 5bbf2c028edd8..768cf82776632 100644
> --- a/drivers/gpu/drm/tegra/dc.c
> +++ b/drivers/gpu/drm/tegra/dc.c
> @@ -3382,6 +3382,10 @@ static int tegra_dc_probe(struct platform_device *pdev)
>  		}
>  	}
>  
> +	err = devm_tegra_dc_dbi_init(&pdev->dev);
> +	if (err)
> +		return dev_err_probe(&pdev->dev, err, "failed to init DBI\n");
> +
>  	err = tegra_dc_rgb_probe(dc);
>  	if (err < 0 && err != -ENODEV)
>  		return dev_err_probe(&pdev->dev, err,

[Severity: High]
Can devm_tegra_dc_dbi_init() deadlock on probe deferral if the child MIPI DBI
panel driver is not yet bound when tegra_dc_rgb_probe() runs, and also fail
with -EBUSY if both display controllers have an available "dbi" child node?

In tegra_dc_probe(), devm_tegra_dc_dbi_init() is called immediately before
tegra_dc_rgb_probe(dc). In tegra_dbi_probe(), devm_mipi_dbi_device_register()
creates the child mipi_dbi_device, but priv->bridge is only registered later
in tegra_dbi_host_attach() when the panel driver binds to that DBI device and
calls devm_mipi_dbi_attach().

If the panel driver is built as a module, has not registered yet, or defers
its own probe (for example waiting on a regulator, GPIO, or backlight),
devm_tegra_dc_dbi_init() still returns 0, and then tegra_dc_rgb_probe(dc) ->
tegra_output_probe() -> drm_of_find_panel_or_bridge() returns -EPROBE_DEFER
because priv->bridge has not been added yet. When tegra_dc_probe() fails with
-EPROBE_DEFER, devres on &pdev->dev runs tegra_dbi_release(), which
unregisters the tegra-dbi platform device and destroys the child
mipi_dbi_device. With the mipi_dbi_device removed, how can the panel driver
ever bind to it and call tegra_dbi_host_attach() to resolve the deferral?

Additionally, tegra_dbi_driver is a single static platform_driver registered
inside devm_tegra_dc_dbi_init(). If both display controllers on Tegra20/30
have an available "dbi" child node, won't the second DC's call to
platform_driver_register(&tegra_dbi_driver) fail with -EBUSY, and unbinding
one DC unregister tegra_dbi_driver while the other DC is still using it?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261029152909.234584-1-clamor95@gmail.com?part=11

      reply	other threads:[~2026-10-29 15:55 UTC|newest]

Thread overview: 15+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-29 15:28 [PATCH v5 00/11] drm/tegra: Add support for Tegra20/Tegra30 8-bit CPU interface Svyatoslav Ryhel
2026-10-29 15:28 ` [PATCH v5 01/11] drm: Add MIPI DBI bus support Svyatoslav Ryhel
2026-10-29 15:29 ` [PATCH v5 02/11] dt-bindings: display: panel: Document Hitachi TX10D07VM0BAA and LG LH400WV3 panels Svyatoslav Ryhel
2026-10-29 15:29 ` [PATCH v5 03/11] drm/panel: Add Hitachi TX10D07VM0BAA MIPI DBI Type B panel driver Svyatoslav Ryhel
2026-10-29 15:40   ` sashiko-bot
2026-10-29 15:29 ` [PATCH v5 04/11] drm/panel: Add LG LH400WV3-SD04 " Svyatoslav Ryhel
2026-10-29 15:29 ` [PATCH v5 05/11] drm/tegra: dc: Refactor and expand header Svyatoslav Ryhel
2026-10-29 15:29 ` [PATCH v5 06/11] dt-bindings: display: tegra: Document Tegra20 DSI port Svyatoslav Ryhel
2026-10-29 15:29 ` [PATCH v5 07/11] drm/tegra: Make panel parsing by tegra_output_probe() optional Svyatoslav Ryhel
2026-10-29 15:41   ` sashiko-bot
2026-10-29 15:29 ` [PATCH v5 08/11] drm/tegra: Convert to use atomic versions of drm_encoder_helper_funcs Svyatoslav Ryhel
2026-10-29 15:29 ` [PATCH v5 09/11] drm/tegra: rgb: Parameterize configuration based on bus flags Svyatoslav Ryhel
2026-10-29 15:29 ` [PATCH v5 10/11] dt-bindings: display: tegra: Document 8-bit CPU parallel interface Svyatoslav Ryhel
2026-10-29 15:29 ` [PATCH v5 11/11] drm/tegra: Add support for 8-bit CPU interface Svyatoslav Ryhel
2026-10-29 15:55   ` sashiko-bot [this message]

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=sashiko-outbox-189642@kernel.org \
    --to=sashiko-bot@kernel.org \
    --cc=clamor95@gmail.com \
    --cc=conor+dt@kernel.org \
    --cc=devicetree@vger.kernel.org \
    --cc=dri-devel@lists.freedesktop.org \
    --cc=robh@kernel.org \
    --cc=sashiko-reviews@lists.linux.dev \
    /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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.