新闻  |   论坛  |   博客  |   在线研讨会
从0开始点亮DSI屏幕
toradex | 2026-03-18 18:34:28    阅读:136   发布文章

By Toradex 胡珊逢

简介

在嵌入式领域中,DSI 接口显示屏变得越来越主流。以树莓派为代表的诸多开发板均提供了 MIPI DSI 显示接口。Toradex 的在其新的 Verdin 和 Aquila 家族产品中,也基本都兼容 DSI 显示接口。但是 DSI 屏幕的配置往往比 LVDS 屏幕更加复杂。文章将介绍如何在 Verdin iMX8M Plus 上从零开始驱动一块 DSI 屏幕,其中也将提供一些相关的调试技巧。

为何选择 DSI 屏幕

随着嵌入式系统向更高分辨率、更低功耗和更轻薄的设计演进,传统的并行 RGB 和 LVDS 等接口已逐渐接近其性能极限。

MIPI DSI(显示串行接口)采用高速差分通道协议替代了宽并行总线,每通道传输速率最高可达 4.5 Gbit/s。这使得全高清面板仅需单通道即可驱动,显著减少了引脚数量、电磁干扰和PCB复杂度——这些优势对于医疗、工业和智慧城市应用中的紧凑型设备至关重要。

与旧式接口不同,MIPI DSI 屏幕具有专门的控制器,可以支持双向通信:开发者可向面板控制器发送初始化指令或亮度调节、色彩调节等命令,实现对显示内容的灵活实时控制。

i.MX8M Plus MIPI DSI 架构

如下图显示,MIPI DSI 架构包括多个组件。LCDIF 可以从内存中读取需要显示的图像信息,它们可以直接显示在 TFT LCD 面板上。MIPI DSI 接收来自 LCDIF 的图形信息,并将它们串化为符合 MIPI DSI 规范的格式。除了传输图形信息外,MIPI DSI 还发送或接收 DSI 命令。在后面的配置中会涉及到 LCDIF 的内容。

编辑

MIPI DSI 屏幕

本次测试使用型号为 WKS50095 的 DSI 屏幕。它的分辨率为 720 x 1280,是一块竖直显示的面板。面板驱动芯片为 ILI9881D。i.MX8M Plus MIPI DSI 和 ILI9881D 通信,完成对屏幕的初始化以及显示内容。和传统的 LVDS 屏幕不同,为了点亮 DSI 屏幕,除了需要屏幕的 datasheet 外,我们还需要:

  1. 驱动芯片的 datasheet

  2. 驱动芯片初始化序列

为了点亮 DSI 屏幕,i.MX8M Plus 需要以 DSI 命令的方式先向驱动芯片例如这里的 ILI9881D 写入初始化序列。通常这是一些二进制序列,因此需要借助 datasheet 才可能了解它们的含义。例如下面是 WKS50095 供应商提供的初始化序列的伪代码。首先写入一个 4 字节的命令,0xFF-0x98-0x81-0x03。它的含义是 ILI9881D 切换到 Page 3。然后第二命令是两个字节,0x01-0x00,它是指往 Page 3 上地址位 0x01 的寄存器写入 0x00。

SSD2828_WritePackageSize(4);
SPI_WriteData(0xFF);
SPI_WriteData(0x98);
SPI_WriteData(0x81);
SPI_WriteData(0x03);

SSD2828_WritePackageSize(2);
SPI_WriteData(0x01);
SPI_WriteData(0x00);

但需要注意的是,即便是获得了 ILI9881D 的 datasheet,用户可能也无法了解初始化序列中所有寄存器的配置含义。这些寄存器的定义有些是属于 MIPI DSI 通用规范,另外一些则是每个厂商自己的定义。后者可能是非公开内容。有了供应商提供的初始化序列,点亮屏幕一般都不存在问题。

Device Tree

驱动 DSI 屏幕的软件工作主要是来自 device tree 和内核驱动。在 device tree 中,不仅要直接配置 MIPI DSI panel 节点,还要完成相关的设置如背光和连接的其他节点如 lcdif。这里我们以使用 kernel 6.6 的 BSP 7 为例进行说明。在 imx8mp.dtsi 文件中,mipi_dsi 的 port@0 已经连接到 lcdif1 输出端口 lcdif1_disp。因此,在配置 DSI 屏幕的 device tree 中,我们只需要把 MIPI DSI 的 port@1 连接到屏幕的 panel 定义的端口。

mipi_dsi: mipi_dsi@32e60000 {
  ....
  port@0 {
      dsim_from_lcdif: endpoint {
          remote-endpoint = <&lcdif_to_dsim>;
      };
  };
};

lcdif1: lcd-controller@32e80000 {
  ....
  lcdif1_disp: port {
      lcdif_to_dsim: endpoint {
          remote-endpoint = <&dsim_from_lcdif>;
      };
  };
};

下面是在 device tree 中 三者的连接顺序。

lcdif -> mipi_dsi -> panel

用于屏幕的 device tree 我们这里采用device tree overlays的方式进行配置,直接集成到单个 device tree 文件中的配置也基本是一致的。在 dts 文件中,最主要 mipi_dsi 配置如下:

&mipi_dsi {
#address-cells = <1>;
#size-cells = <0>;
status = "okay";


port@1 {
      reg = <1>;
      mipi_dsi_out: endpoint {
          remote-endpoint = <&panel_in>;
      };
  };


panel@0 {
reg = <0>;
compatible = "test,wks50095";

pinctrl-names = "default";
pinctrl-0 = <&pinctrl_gpio_10_dsi>;
backlight = <&backlight>;
reset-gpios = <&gpio4 28 GPIO_ACTIVE_LOW>;
wait-until-enabled;
dsi,lanes = <4>;
video-mode = <2>;
port {
          panel_in: endpoint {
              remote-endpoint = <&mipi_dsi_out>;
          };
      };


};
};

在 mipi_dsi 中增加子节点panel@0compatible = "test,wks50095";可以找到对应的驱动进行加载,这也是后面需要自己编写的驱动源码。&pinctrl_gpio_10_dsi用于配置复位屏幕的 GPIO。&backlight对应的屏幕的背光 PWM。由于 NXP 目前 kernel 6.6 的 DSI 驱动中,无法的 prepare 阶段传输初始化序列,所以添加了wait-until-enabled;。这可以将初始化序列传输移动到 enable 阶段进行。接下来的内核驱动部分我们将继续说明。panel 也会定义 port,通过 remote-endpoint 和 mipi_dsi 的 port@1 连接。

完整的 dts 可以从这里下载。device tree overlays 的编译方法请参考Build Device Tree Overlays from Source Code

内核驱动

panel 的驱动位于drivers/gpu/drm/panel/目录中,可以创建一个panel-mipi-dsi.c文件。在 mipi_dsi_panel_of_match 添加和前面 device tree 对应的 compatible 字符串。

static const struct of_device_id mipi_dsi_panel_of_match[] = {
  ......
{ .compatible = "wisecoco,top055fhd01a", .data = &top055fhd01a_desc},
  { .compatible = "test,wks50095", .data = &wks50095_desc},
{ }
};

wks50095_desc结构体中配置 DSI 屏幕的参数,如时序、色彩格式、初始化序列入口。

static const struct mipi_dsi_panel_panel_desc wks50095_desc= {
.mode = &wks50095_mode,
.lanes = 4,
.flags = MIPI_DSI_MODE_VIDEO | MIPI_DSI_MODE_VIDEO_SYNC_PULSE,
.format = MIPI_DSI_FMT_RGB888,
.supply_names = ts8550b_supply_names,
.num_supplies = ARRAY_SIZE(ts8550b_supply_names),
.panel_sleep_delay = 200,
.init_sequence = wks50095_init_sequence,
.panel_has_backlight = false
};

wks50095_mode中是屏幕时序信息。部分时序可以从屏幕的 datasheet 直接读取,部分 front/back porch 内容可能不会明确地在屏幕 datasheet 中有描述。此时可以从初始化序列中结合屏幕控制器 ILI9881D 寄存器说明反推出这些信息。如果初始化序列中也没有,那么就使用 ILI9881D 寄存器的默认值。下面是 WKS50095 按上面方法得到的时序。

编辑

wks50095_init_sequence是 WKS50095 的初始化序列。把前面的伪代码改为 i.MX8M Plus 发送 MIPI DSI 命令的格式,配置的寄存器和顺序同前面的伪代码中一致。

static void wks50095_init_sequence(struct mipi_dsi_device *dsi)
{
dsi->mode_flags |= MIPI_DSI_MODE_LPM;


/* Page 3 Configuration */
MIPI_DSI_SEQ(dsi, 0xFF, 0x98, 0x81, 0x03);
MIPI_DSI_SEQ(dsi, 0x01, 0x00);
MIPI_DSI_SEQ(dsi, 0x02, 0x00);
MIPI_DSI_SEQ(dsi, 0x03, 0x73);
MIPI_DSI_SEQ(dsi, 0x04, 0x00);

通过查询 wait_until_enabled 的状态,将初始化序列号发送mipi_dsi_panel_enable函数中进行。

static int mipi_dsi_panel_enable(struct drm_panel *panel)
{
  ...
if (dsi_panel->wait_until_enabled) {
pr_debug("%s - %s:%d\n", __func__, __FILE__, __LINE__);
dsi_panel->desc->init_sequence(dsi_panel->dsi);
dsi_panel->prepared = true;
}

完成上面内容的配置,那么在内核驱动中就实现了 WKS50095 屏幕的驱动。最后修改drivers/gpu/drm/panel/目录下的 Makefie 和 Kconfig 文件编译添加的驱动。

  • Makefile

obj-$(CONFIG_DRM_PANEL_MIPI_DSI) += panel-mipi-dsi.o

  • Kconfig

config DRM_PANEL_MIPI_DSI
tristate "MIPI DSI panel support"
depends on DRM_MIPI_DSI
default n
help
 Support for MIPI DSI panels

最后重新编译内核和所有驱动模块,并部署到 Verdin iMX8MP 模块上。因为上面使用了 device tree overlays 的方式,所以也需要修改 /boot/overlays.txt 文件使其生效。

Weston 配置

WKS50095 是一块竖直分辨率的屏幕,weston 默认使用的 g2d 渲染器无法正确处理,所以改用 GPU 渲染。修改 /etc/xdg/weston/weston.ini。

[core]
#use-g2d=false
renderer=gl

[output]
name=DSI-1
mode=720x1280
transform=normal


重启后就可以看到 DSI 屏幕显示的内容。

编辑

调试

调试期间往往会由于各种因素导致屏幕无法点亮,下面是本次调试过程用的一些方法帮助确定问题。

  • 驱动加载

lsmod命令可以检查驱动是否加载。如果没有加载可以尝试使用 modprobe 手动加载。当单独编译 kernel moduesl 时,可能由于符号文件不一致也不能自己加载。depmod 命令则可以进行更新。

~# lsmod |grep panel_mipi_dsi
panel_mipi_dsi         65536  0

~# modprobe panel_mipi_dsi

~# depmod -a

  • 开启动态调试

对于 BSP 中于编译好的驱动,可以通过设置 dyndbg 的方式开启它的调试输出,而无需修改代码。

# setenv tdxargs dyndbg="\"file
drivers/gpu/drm/imx/lcdifv3/lcdifv3-crtc.c +p\""

  • 静态调试

而像 panel-mipi-dsi.c 这样本身就需要自己写的代码,在一开始就可以在其中加入调试输出函数。打开 DEBUG 定义。在必要的地方插入 pr_debug,这样就可以追踪到程序执行到的位置。

#define DEBUG
...
pr_debug("%s - %s:%d\n", __func__, __FILE__, __LINE__);

__LINE__可以显示代码的行数。

[    9.009656] mipi_dsi_panel_probe - drivers/gpu/drm/panel/panel-mipi-dsi.c:917
[    9.009717] mipi_dsi_panel_probe - drivers/gpu/drm/panel/panel-mipi-dsi.c:924

  • probe 函数

驱动中的 probe 函数是在加载时第一个被调用的函数。在其中插入一些调试函数,可以确定加载情况。如果 probe 函数没有被调用,通常是它所依赖的其他驱动没有完成初始化。

static int mipi_dsi_panel_probe(struct mipi_dsi_device *dsi)
{
const struct mipi_dsi_panel_panel_desc *desc;
struct mipi_dsi_panel *dsi_panel;
int ret, i;


dump_stack();
panic("DIAGNOSTIC: Probe reached for panel %s\n", dsi->name);

例如前面配置 device tree overlays 时,mipi_dsi 的 port@1 曾经写为mipi_dsi/ports/port@1这种层级,从而导致 mipi_dsi 初始化失败,也就不会去调用 panel(panel-mipi-dsi.c)驱动。它的 probbe 函数一直没有被执行。

总结

本文以一块实际的 DSI 为例介绍如何为 Verdin iMX8M Plus 编写驱动和 device tree overlays 文件,使其可以显示内容。同时也介绍一些调试技巧,方便用户移植其他 DSI 屏幕驱动。


*博客内容为网友个人发布,仅代表博主个人观点,如有侵权请联系工作人员删除。

参与讨论
登录后参与讨论
推荐文章
最近访客