"); //-->
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 外,我们还需要:
驱动芯片的 datasheet
驱动芯片初始化序列
为了点亮 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 文件中的配置也基本是一致的。在 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@0。compatible = "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/目录中,可以创建一个文件。在 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 屏幕驱动。
*博客内容为网友个人发布,仅代表博主个人观点,如有侵权请联系工作人员删除。