* [PATCH 4/4] commands: mipi_dsi: add command for easier DCS experimentation
2026-11-08 10:10 [PATCH 1/4] video: dw_mipi_dsi: disable the panel before powering down the host Ahmad Fatoum
2026-11-08 10:10 ` [PATCH 2/4] video: dw_mipi_dsi: refuse transfers while the host is powered down Ahmad Fatoum
2026-11-08 10:10 ` [PATCH 3/4] video: dw_mipi_dsi: give transfers in video mode a frame to complete Ahmad Fatoum
@ 2026-11-08 10:10 ` Ahmad Fatoum
2026-11-09 9:24 ` Sascha Hauer
2 siblings, 1 reply; 5+ messages in thread
From: Ahmad Fatoum @ 2026-11-08 10:10 UTC (permalink / raw)
To: barebox; +Cc: Ahmad Fatoum
To simplify MIPI DSI panel bring up, add a mipi_dsi command along the
same lines as the mipi_dbi, which lists DSI devices, sends DCS writes
and reads back registers, so this can be done from the shell.
Like the mipi_dbi command, the register dump only reads known read
commands. The panel executes the command byte of a DCS read request:
dumping 0x00-0xff unconditionally soft-reset the OTM8009A on the
STM32MP157C-DK2 and put it into sleep mode.
Tested on the STM32MP157C-DK2 (OTM8009A) and the Powkiddy RGB30 (ST7703).
Signed-off-by: Ahmad Fatoum <a.fatoum@barebox.org>
---
arch/arm/configs/stm32mp_defconfig | 1 +
commands/Kconfig | 27 +++
commands/Makefile | 1 +
commands/mipi_dsi.c | 355 +++++++++++++++++++++++++++++
test/py/test_mipi_dsi.py | 78 +++++++
5 files changed, 462 insertions(+)
create mode 100644 commands/mipi_dsi.c
create mode 100644 test/py/test_mipi_dsi.py
diff --git a/arch/arm/configs/stm32mp_defconfig b/arch/arm/configs/stm32mp_defconfig
index e1aef7c86fc0..13aed489ccea 100644
--- a/arch/arm/configs/stm32mp_defconfig
+++ b/arch/arm/configs/stm32mp_defconfig
@@ -95,6 +95,7 @@ CONFIG_CMD_GPIO=y
CONFIG_CMD_LED=y
CONFIG_CMD_POWEROFF=y
CONFIG_CMD_SPI=y
+CONFIG_CMD_MIPI_DSI=y
CONFIG_CMD_USBGADGET=y
CONFIG_CMD_WD=y
CONFIG_CMD_BAREBOX_UPDATE=y
diff --git a/commands/Kconfig b/commands/Kconfig
index a43f0fbd4d40..eb4067d8108c 100644
--- a/commands/Kconfig
+++ b/commands/Kconfig
@@ -2263,6 +2263,33 @@ config CMD_MIPI_DBI
-d DEVICE select specific device (default is first registered)
-w issue write command
+config CMD_MIPI_DSI
+ bool
+ depends on DRIVER_VIDEO_MIPI_DSI
+ select PRINTF_HEXSTR
+ prompt "mipi_dsi command"
+ help
+ read/write DCS registers of MIPI DSI devices
+
+ Usage: mipi_dsi [-lvdinwgLH] [-c CH] [-p on|off] [CMD] [DATA...]
+
+ Without CMD, dump the standard DCS registers.
+ CMD and DATA bytes are given in hex.
+
+ Options:
+ -l list all MIPI DSI devices
+ -v list with host, channel, lanes, format and mode flags
+ -d DEVICE select specific device (default is first registered)
+ -i read the panel identification registers
+ -n LEN number of bytes to read (default: known length, else 1)
+ -w issue write command
+ -g send generic packets: CMD and DATA are the payload,
+ reads take up to two bytes
+ -p on|off send Turn On or Shutdown Peripheral
+ -c CH use virtual channel CH (0-3)
+ -L send in low power mode
+ -H send in high speed mode
+
config CMD_LED_TRIGGER
bool
depends on LED_TRIGGERS
diff --git a/commands/Makefile b/commands/Makefile
index 7dae582225f5..f91f6335ff8e 100644
--- a/commands/Makefile
+++ b/commands/Makefile
@@ -82,6 +82,7 @@ obj-$(CONFIG_CMD_I2C) += i2c.o
obj-$(CONFIG_CMD_SPI) += spi.o
obj-$(CONFIG_CMD_PWM) += pwm.o
obj-$(CONFIG_CMD_MIPI_DBI) += mipi_dbi.o
+obj-$(CONFIG_CMD_MIPI_DSI) += mipi_dsi.o
obj-$(CONFIG_CMD_UBI) += ubi.o
obj-$(CONFIG_CMD_UBIFORMAT) += ubiformat.o
obj-$(CONFIG_CMD_MENU) += menu.o
diff --git a/commands/mipi_dsi.c b/commands/mipi_dsi.c
new file mode 100644
index 000000000000..ccef279b9830
--- /dev/null
+++ b/commands/mipi_dsi.c
@@ -0,0 +1,355 @@
+// SPDX-License-Identifier: GPL-2.0-only
+// SPDX-FileCopyrightText: © 2025 Ahmad Fatoum
+
+#include <command.h>
+#include <getopt.h>
+#include <video/mipi_dsi.h>
+#include <video/mipi_display.h>
+
+#define MIPI_DSI_CMD_MAX_LEN 64
+
+struct dcs_read {
+ u8 cmd;
+ u8 len;
+};
+
+/*
+ * Panels execute the command byte of a DCS read request like that of
+ * a write, so only ever send commands known to be reads.
+ */
+static const struct dcs_read dcs_reads[] = {
+ { MIPI_DCS_GET_DISPLAY_ID, 3 },
+ { MIPI_DCS_GET_RED_CHANNEL, 1 },
+ { MIPI_DCS_GET_GREEN_CHANNEL, 1 },
+ { MIPI_DCS_GET_BLUE_CHANNEL, 1 },
+ { MIPI_DCS_GET_DISPLAY_STATUS, 4 },
+ { MIPI_DCS_GET_POWER_MODE, 1 },
+ { MIPI_DCS_GET_ADDRESS_MODE, 1 },
+ { MIPI_DCS_GET_DISPLAY_MODE, 1 },
+ { MIPI_DCS_GET_SIGNAL_MODE, 1 },
+ { MIPI_DCS_GET_DIAGNOSTIC_RESULT, 1 },
+ { MIPI_DCS_GET_SCANLINE, 2 },
+ { MIPI_DCS_GET_DISPLAY_BRIGHTNESS, 1 },
+ { MIPI_DCS_GET_CONTROL_DISPLAY, 1 },
+ { MIPI_DCS_GET_CABC_MIN_BRIGHTNESS, 1 },
+};
+
+/* display ID, DDB and the nonstandard, but widespread ID1-ID3 */
+static const struct dcs_read dcs_ids[] = {
+ { MIPI_DCS_GET_DISPLAY_ID, 3 },
+ { MIPI_DCS_READ_DDB_START, 5 },
+ { 0xda, 1 },
+ { 0xdb, 1 },
+ { 0xdc, 1 },
+};
+
+static const struct dcs_read *dcs_read_find(const struct dcs_read *reads,
+ size_t nreads, u8 cmd)
+{
+ for (int i = 0; i < nreads; i++) {
+ if (reads[i].cmd == cmd)
+ return &reads[i];
+ }
+
+ return NULL;
+}
+
+static unsigned int dcs_read_len(u8 cmd)
+{
+ const struct dcs_read *reg;
+
+ reg = dcs_read_find(dcs_reads, ARRAY_SIZE(dcs_reads), cmd) ?:
+ dcs_read_find(dcs_ids, ARRAY_SIZE(dcs_ids), cmd);
+
+ return reg ? reg->len : 1;
+}
+
+/*
+ * DCS reads send CMD, generic reads send CMD and up to one more
+ * parameter byte in @tx.
+ */
+static int mipi_dsi_command_show(struct mipi_dsi_device *dsi, bool generic,
+ const u8 *tx, size_t tx_len, size_t len,
+ bool prefix)
+{
+ u8 buf[MIPI_DSI_CMD_MAX_LEN];
+ ssize_t ret;
+
+ ret = mipi_dsi_set_maximum_return_packet_size(dsi, len);
+ if (!ret && generic)
+ ret = mipi_dsi_generic_read(dsi, tx, tx_len, buf, len);
+ else if (!ret)
+ ret = mipi_dsi_dcs_read(dsi, tx[0], buf, len);
+
+ if (prefix)
+ printf("%02x: ", tx[0]);
+
+ if (ret < 0) {
+ printf("read failed: %pe\n", ERR_PTR(ret));
+ return ret;
+ }
+
+ printf("%*phN\n", (int)ret, buf);
+
+ return 0;
+}
+
+static int mipi_dsi_command_show_list(struct mipi_dsi_device *dsi,
+ const struct dcs_read *reads,
+ size_t nreads)
+{
+ int ret, err = 0;
+
+ for (int i = 0; i < nreads; i++) {
+ ret = mipi_dsi_command_show(dsi, false, &reads[i].cmd, 1,
+ reads[i].len, true);
+ if (ret == -ENETDOWN)
+ return ret;
+ if (ret)
+ err = ret;
+ if (ctrlc())
+ return -EINTR;
+ }
+
+ return err;
+}
+
+static const char * const mipi_dsi_formats[] = {
+ [MIPI_DSI_FMT_RGB888] = "RGB888",
+ [MIPI_DSI_FMT_RGB666] = "RGB666",
+ [MIPI_DSI_FMT_RGB666_PACKED] = "RGB666_PACKED",
+ [MIPI_DSI_FMT_RGB565] = "RGB565",
+};
+
+static const char * const mipi_dsi_mode_flags[] = {
+ "video", "video-burst", "video-sync-pulse", "video-auto-vert",
+ "video-hse", "video-no-hfp", "video-no-hbp", "video-no-hsa",
+ "vsync-flush", "no-eot-packet", "clock-non-continuous", "lpm",
+};
+
+static void mipi_dsi_list(bool verbose)
+{
+ struct mipi_dsi_device *dsi;
+ struct device *dev;
+
+ bus_for_each_device(&mipi_dsi_bus_type, dev) {
+ const char *format = "unknown";
+
+ dsi = to_mipi_dsi_device(dev);
+ printf("%s%s", dev_name(dev),
+ dsi->attached ? "" : " (not attached)");
+
+ if (dsi->format < ARRAY_SIZE(mipi_dsi_formats))
+ format = mipi_dsi_formats[dsi->format];
+
+ if (verbose) {
+ printf(": host %s, channel %u, %u lanes, %s, flags",
+ dev_name(dsi->host->dev), dsi->channel,
+ dsi->lanes, format);
+
+ for (int i = 0; i < ARRAY_SIZE(mipi_dsi_mode_flags); i++) {
+ if (dsi->mode_flags & BIT(i))
+ printf(" %s", mipi_dsi_mode_flags[i]);
+ }
+ }
+
+ printf("\n");
+ }
+}
+
+struct mipi_dsi_opts {
+ bool write, info, generic;
+ int peripheral; /* 1: turn on, 0: shut down, -1: none */
+ unsigned int len; /* 0: the known length of the register or 1 */
+};
+
+static int mipi_dsi_execute(struct mipi_dsi_device *dsi,
+ const struct mipi_dsi_opts *opts,
+ int argc, char *argv[])
+{
+ u8 buf[MIPI_DSI_CMD_MAX_LEN];
+ unsigned int len;
+ int ret, i;
+
+ if (opts->peripheral >= 0) {
+ if (opts->write || opts->info || opts->generic || argc)
+ return COMMAND_ERROR_USAGE;
+ if (opts->peripheral)
+ return mipi_dsi_turn_on_peripheral(dsi);
+ return mipi_dsi_shutdown_peripheral(dsi);
+ }
+
+ if (opts->info) {
+ if (opts->write || opts->generic || argc)
+ return COMMAND_ERROR_USAGE;
+ return mipi_dsi_command_show_list(dsi, dcs_ids,
+ ARRAY_SIZE(dcs_ids));
+ }
+
+ if (!argc) {
+ if (opts->write || opts->generic)
+ return COMMAND_ERROR_USAGE;
+ return mipi_dsi_command_show_list(dsi, dcs_reads,
+ ARRAY_SIZE(dcs_reads));
+ }
+
+ if (argc > sizeof(buf)) {
+ printf("can only send up to %zu bytes at once\n", sizeof(buf));
+ return -EOVERFLOW;
+ }
+
+ for (i = 0; i < argc; i++) {
+ ret = kstrtou8(argv[i], 16, &buf[i]);
+ if (ret < 0)
+ return ret;
+ }
+
+ if (opts->write) {
+ if (opts->generic)
+ ret = mipi_dsi_generic_write(dsi, buf, argc);
+ else
+ ret = mipi_dsi_dcs_write_buffer(dsi, buf, argc);
+ return ret < 0 ? ret : 0;
+ }
+
+ if (argc > (opts->generic ? 2 : 1))
+ return COMMAND_ERROR_USAGE;
+
+ len = opts->len;
+ if (!len)
+ len = opts->generic ? 1 : dcs_read_len(buf[0]);
+
+ return mipi_dsi_command_show(dsi, opts->generic, buf, argc, len,
+ false);
+}
+
+static int do_mipi_dsi(int argc, char *argv[])
+{
+ struct mipi_dsi_opts opts = { .peripheral = -1 };
+ struct mipi_dsi_device *dsi;
+ struct device *dev;
+ bool list = false, verbose = false, lpm = false, hs = false;
+ unsigned long mode_flags;
+ unsigned int channel, vc = UINT_MAX;
+ int opt, ret;
+
+ dev = list_first_entry_or_null(&mipi_dsi_bus_type.device_list,
+ struct device, bus_list);
+
+ while ((opt = getopt(argc, argv, "c:d:gHilLn:p:vw")) > 0) {
+ switch (opt) {
+ case 'c':
+ ret = kstrtouint(optarg, 0, &vc);
+ if (ret)
+ return ret;
+ if (vc > 3)
+ return -EINVAL;
+ break;
+ case 'd':
+ dev = bus_find_device(&mipi_dsi_bus_type, NULL,
+ optarg, device_match_name);
+ break;
+ case 'g':
+ opts.generic = true;
+ break;
+ case 'H':
+ hs = true;
+ break;
+ case 'i':
+ opts.info = true;
+ break;
+ case 'l':
+ list = true;
+ break;
+ case 'L':
+ lpm = true;
+ break;
+ case 'n':
+ ret = kstrtouint(optarg, 0, &opts.len);
+ if (ret)
+ return ret;
+ if (!opts.len || opts.len > MIPI_DSI_CMD_MAX_LEN)
+ return -EINVAL;
+ break;
+ case 'p':
+ if (!strcmp(optarg, "on"))
+ opts.peripheral = 1;
+ else if (!strcmp(optarg, "off"))
+ opts.peripheral = 0;
+ else
+ return COMMAND_ERROR_USAGE;
+ break;
+ case 'v':
+ verbose = true;
+ break;
+ case 'w':
+ opts.write = true;
+ break;
+ default:
+ return COMMAND_ERROR_USAGE;
+ }
+ }
+
+ if (list || verbose) {
+ mipi_dsi_list(verbose);
+ return 0;
+ }
+
+ if (lpm && hs)
+ return COMMAND_ERROR_USAGE;
+
+ if (!dev)
+ return -ENODEV;
+
+ dsi = to_mipi_dsi_device(dev);
+ if (!dsi->attached) {
+ printf("%s: not attached to its DSI host\n", dev_name(dev));
+ return -ENODEV;
+ }
+
+ mode_flags = dsi->mode_flags;
+ channel = dsi->channel;
+ if (lpm)
+ dsi->mode_flags |= MIPI_DSI_MODE_LPM;
+ if (hs)
+ dsi->mode_flags &= ~MIPI_DSI_MODE_LPM;
+ if (vc != UINT_MAX)
+ dsi->channel = vc;
+
+ ret = mipi_dsi_execute(dsi, &opts, argc - optind, argv + optind);
+
+ dsi->mode_flags = mode_flags;
+ dsi->channel = channel;
+
+ if (ret == -ENETDOWN)
+ printf("DSI host is powered down, enable the display first\n");
+
+ return ret;
+}
+
+BAREBOX_CMD_HELP_START(mipi_dsi)
+BAREBOX_CMD_HELP_TEXT("Without CMD, dump the standard DCS registers.")
+BAREBOX_CMD_HELP_TEXT("CMD and DATA bytes are given in hex.")
+BAREBOX_CMD_HELP_TEXT("")
+BAREBOX_CMD_HELP_TEXT("Options:")
+BAREBOX_CMD_HELP_OPT ("-l\t", "list all MIPI DSI devices")
+BAREBOX_CMD_HELP_OPT ("-v\t", "list with host, channel, lanes, format and mode flags")
+BAREBOX_CMD_HELP_OPT ("-d DEVICE", "select specific device (default is first registered)")
+BAREBOX_CMD_HELP_OPT ("-i\t", "read the panel identification registers")
+BAREBOX_CMD_HELP_OPT ("-n LEN\t", "number of bytes to read (default: known length, else 1)")
+BAREBOX_CMD_HELP_OPT ("-w\t", "issue write command")
+BAREBOX_CMD_HELP_OPT ("-g\t", "send generic packets: CMD and DATA are the payload,")
+BAREBOX_CMD_HELP_OPT ("\t", "reads take up to two bytes")
+BAREBOX_CMD_HELP_OPT ("-p on|off", "send Turn On or Shutdown Peripheral")
+BAREBOX_CMD_HELP_OPT ("-c CH\t", "use virtual channel CH (0-3)")
+BAREBOX_CMD_HELP_OPT ("-L\t", "send in low power mode")
+BAREBOX_CMD_HELP_OPT ("-H\t", "send in high speed mode")
+BAREBOX_CMD_HELP_END
+
+BAREBOX_CMD_START(mipi_dsi)
+ .cmd = do_mipi_dsi,
+ BAREBOX_CMD_DESC("interact with MIPI DSI device")
+ BAREBOX_CMD_OPTS("[-lvdinwgLH] [-c CH] [-p on|off] [CMD] [DATA...]")
+ BAREBOX_CMD_GROUP(CMD_GRP_HWMANIP)
+ BAREBOX_CMD_HELP(cmd_mipi_dsi_help)
+BAREBOX_CMD_END
diff --git a/test/py/test_mipi_dsi.py b/test/py/test_mipi_dsi.py
new file mode 100644
index 000000000000..b66cf256de3a
--- /dev/null
+++ b/test/py/test_mipi_dsi.py
@@ -0,0 +1,78 @@
+# SPDX-License-Identifier: GPL-2.0-only
+
+import pytest
+from .helper import skip_disabled
+
+DISPLAY_ON = 1 << 2
+
+
+@pytest.fixture(autouse=True)
+def mipi_dsi(barebox, barebox_config):
+ skip_disabled(barebox_config, "CONFIG_CMD_MIPI_DSI")
+
+ devices = barebox.run_check("mipi_dsi -l")
+ if not any(d and "not attached" not in d for d in devices):
+ pytest.skip("no attached MIPI DSI device")
+
+ _, _, ret = barebox.run("test -e /dev/fb0")
+ if ret != 0:
+ pytest.skip("no framebuffer to enable the DSI link")
+
+ barebox.run_check("fb0.enable=1")
+ yield
+ barebox.run_check("fb0.enable=1")
+
+
+def power_mode(barebox):
+ out = barebox.run_check("mipi_dsi 0a")
+ return int(out[-1], 16)
+
+
+def test_mipi_dsi_identify(barebox):
+ out = barebox.run_check("mipi_dsi -i")
+ assert any(line.startswith("04: ") and len(line) == 10 for line in out)
+
+
+def test_mipi_dsi_read_len(barebox):
+ assert len(barebox.run_check("mipi_dsi 04")[-1]) == 6
+ assert len(barebox.run_check("mipi_dsi -n 1 04")[-1]) == 2
+
+
+def test_mipi_dsi_dump(barebox):
+ out = barebox.run_check("mipi_dsi")
+ assert any(line.startswith("0a: ") for line in out)
+
+
+def test_mipi_dsi_list_verbose(barebox):
+ out = barebox.run_check("mipi_dsi -l -v")
+ assert any(" lanes, " in line for line in out)
+
+
+@pytest.mark.parametrize("opts", ["", "-g", "-L", "-H"])
+def test_mipi_dsi_display_off_on(barebox, opts):
+ assert power_mode(barebox) & DISPLAY_ON
+
+ barebox.run_check(f"mipi_dsi {opts} -w 28")
+ assert not power_mode(barebox) & DISPLAY_ON
+
+ barebox.run_check(f"mipi_dsi {opts} -w 29")
+ assert power_mode(barebox) & DISPLAY_ON
+
+
+def test_mipi_dsi_channel(barebox):
+ barebox.run_check("mipi_dsi -c 0 0a")
+
+ _, _, ret = barebox.run("mipi_dsi -c 1 0a")
+ assert ret != 0
+
+
+def test_mipi_dsi_peripheral(barebox):
+ barebox.run_check("mipi_dsi -p on")
+
+
+def test_mipi_dsi_powered_down(barebox):
+ barebox.run_check("fb0.enable=0")
+
+ out, _, ret = barebox.run("mipi_dsi -w 29")
+ assert ret != 0
+ assert "DSI host is powered down" in "\n".join(out)
--
2.47.3
^ permalink raw reply [flat|nested] 5+ messages in thread