资讯中心

MIPI DSI command模式驱动详解:STM32与Linux DRM移植实战

📅 2026/10/7 19:47:14
MIPI DSI command模式驱动详解:STM32与Linux DRM移植实战
做显示驱动的人基本都会碰上这么一遭单片机那边屏幕跑得好好的一换安卓板就出现点不亮、花屏、闪烁然后就怀疑屏坏了、转接板有问题、线没焊好。其实很多问题的根子不在硬件而在 MIPI DSI 的 command 模式和 video 模式没搞明白。屏还是那块屏但主机端的驱动方式完全不同。这篇文章想把 MIPI DSI 的 command 模式从头到尾讲透包括它和 video 模式的核心区别、DSI 协议里的包和时序、怎么在 STM32 上真正驱动一块 command 模式的 MIPI 屏幕以及同样的原理迁移到安卓板 / Linux DRM 后要注意什么。如果你是做单片机显示、Android 屏参调试或者正在从单片机屏迁到安卓方案的路上这篇应该能帮你省不少折腾时间。1. 先分清 command 模式与 video 模式谁在看显存1.1 核心区别屏幕自己刷还是主机一直喂MIPI DSI 的 command 模式本质上是把屏幕当成一块带显存的外设来操作。模组内部的 driver IC 集成了一整块 GRAMGraphic RAM主机通过 DSI 总线把像素数据写到这块 GRAM 里然后面板自己定时读取 GRAM 内容并显示到 LCD 上。主机写完数据之后就可以去忙别的不用持续输出像素流。video 模式则完全不同。它更接近电脑显卡驱动显示器主机要按固定的行场时序持续不断地把像素数据发过去一旦主机停止发送屏幕上的内容就会丢失或者直接黑掉。所以在 video 模式下像素数据流是不能随便断的实时性要求非常高。这个区别直接决定了后面的所有设计。command 模式下的屏刷新率可以由面板自身控制适合低功耗、局部刷新、甚至静态画面长时间停留的场景video 模式则适合画面连续变化、需要严格同步的场景。1.2 为什么单片机系统更偏爱 command 模式单片机主频不高、内存也不大通常跑不了复杂的图形系统大部分时候是“更新一小块界面”或者“显示一张静态图”。如果用 video 模式单片机得不停地发数据CPU 和 DMA 几乎被占死功耗也压不住。command 模式的好处是我发一帧数据到屏的 GRAM 之后屏自己会持续刷新单片机可以进入低功耗模式或者去处理其他任务。等到画面需要变化了再通过 0x2C Memory Write 之类的命令更新一个窗口区域。这种工作方式和老式 SPI 接口、8080 并口屏的玩法很像所以从单片机转过来的人会感觉特别亲切。1.3 典型应用场景与选型倾向场景偏好模式原因小米手环类小屏低功耗设备command静态界面多面板自刷新省电手机主屏command支持局部刷新、屏下指纹多功耗可控车载中控、仪表video 为主动态画面多需要稳定时序工控 HMI、家电面板command 常见单片机驱动更新频率低电视、显示器video全屏视频流实时性优先从产品形态上看小尺寸、低分辨率、用 MCU 驱动的屏绝大多数支持 command 模式大尺寸、高刷新率、跑安卓系统的屏很多是 video 模式但也有一部分手机 OLED/LCD 屏仍然工作在 command 模式。选择哪种模式不是看屏大屏小而是看主控和功耗需求。2. command 模式的 DSI 协议基础包怎么发、时序怎么走2.1 DSI 总线不是 SPI两条差分对几种电状态MIPI DSI 物理层使用差分信号传输至少有一对时钟差分线CLKP/N和一对或多对数据差分线D0P/N、D1P/N 等。这和 SPI、I2C 这种单端信号完全不同不能用 GPIO 去模拟时序。DSI 总线上有两种传输状态LPLow Power状态和 HSHigh Speed状态。LP 状态下数据线以单端低摆幅信号传输速度不快但用来传命令包HS 状态下数据线和时钟线以差分高速信号传输用来大批量搬运像素数据。命令模式下控制命令比如 0x2A、0x2B、0x35通常可以通过 LP 状态发而像素内容必须切到 HS 状态高速传输否则刷一屏图要等很久。HS 和 LP 之间切换是有开销的包括 SoTStart of Transmission、EoTEnd of Transmission等时序所以 DSI 控制器内部有专门的协议状态机来管理这些切换不需要软件干预。2.2 短包和长包DSI 命令的两种包装形态DSI 协议传输的最小单位是包。小命令用短包大块数据用长包。短包一共 4 字节1 字节 Data Type数据类型、2 字节数据有时只有 1 字节有效、1 字节 ECC 校验。比如我们经常给屏发“Sleep Out0x11”如果用 DSI 短包发Data Type 就是 0x05DCS 短写无参数数据字段里放 0x11。长包则复杂一些先发 4 字节的包头Data Type 2 字节长度 1 字节 ECC然后发 payload 数据最后发 2 字节 CRC 校验。整个 payload 最大长度不能超过 65535 字节。所以刷一帧分辨率稍高一点的图时数据要拆成多个长包来发不能一包塞下整个一帧。2.3 常用 Data Type 与 DCS 命令速查这里要特别区分两个概念DSI 包的 Data Type 和 DCS 命令码。Data Type 是 DSI 协议层的“快递类型”DCS 命令码是显示控制层面的“内容”。发 0x2C Memory Write 时如果不需要参数用的是短包 Data Type 0x05如果要 1 个参数用 Data Type 0x15。Data Type含义典型用途0x05DCS Short Write无参数发 0x11、0x29 等无参命令0x15DCS Short Write带 1 个参数发 0x3A像素格式等带参数的短命令0x39DCS Long Write发大量初始化参数序列0x04DCS Read读取屏幕状态、ID、错误标志0x06Set Maximum Return Packet Size设置回包最大长度读操作前必须用0x2CMemory Write从当前窗口开始写像素数据0x3CMemory Write Continue继续写像素无需重新设置窗口DCS 命令码里0x2A Column Address Set 和 0x2B Page Address Set 用于设置写入窗口0x2C 是真正的写显存入口0x36 设置 MADCTLBGR/RGB、扫描方向0x3A 设置像素格式。整个套路和 SPI 屏的 ILI9341 非常相似——这是我从单片机转过来时最大感受协议换了个壳底层思想没变。2.4 command 模式刷屏的状态机一块 command 模式屏从上电到正常显示大致是这样的状态流上电复位等待稳定发初始化序列Sleep Out、像素格式、MADCTL、gamma 等等待必要延时打开显示Display On 0x29设置窗口Column/Page Address发 Memory Write 长包把一帧或一块像素写入 GRAM如果有 TE 同步则等 TE 信号后再启动下一帧更新。很多屏出厂会提供一长串初始化参数看起来像天书但拆开看就是一组 DCS Long Write 命令。单片机端要做的就是按 DSI 协议把这段序列原样发出去。3. STM32 驱动案例从 CubeMX 配置到一个像素落屏3.1 硬件接线与设计要点我以带 DSI 外设的 STM32F746/F769/H743 这类芯片为例。硬件连接其实很简单DSI_CLKP/D0P/D1P/D2P/D3P 对应接屏的差分对RESET 接 MCU 的 GPIO注意电平匹配背光由 PWM 或 GPIO 控制TE 引脚如果屏支持接 MCU 的 EXTI用于帧同步。实际走线时差分对要注意等长、阻抗控制一般按 100 欧姆差分阻抗设计并且尽量少打过孔。DSI 跑几百 Mbps 甚至更高如果板子走线太乱特别容易出现“单片机里看着发成功了但屏上就是花屏”的诡异问题。这个环节不要省。3.2 时钟和 lane 速率怎么算驱动 DSI 屏之前必须先把 lane 速率算出来否则后面怎么调都白搭。假设屏是 480x800RGB88824bpp目标刷新率 60Hz。带 blanking 的像素时钟通常不是 480x800x60 那么小要加上行场消隐区。假设每行总像素为 520每帧总行数为 840则像素时钟 520 x 840 x 60 ≈ 26.2 MHz一帧数据量 480 x 800 x 24 9,216,000 bit ≈ 9.2 Mbit60Hz 下的最小总带宽 9.2M x 60 ≈ 552 Mbps考虑 DSI 包开销、LP/HS 切换开销实际留 20% 余量总带宽大约 660 Mbps。如果使用 2 lane每 lane 需要约 330 Mbps如果 4 lane每 lane 约 165 Mbps。这个速率对 STM32 的 DSI 控制器来说非常轻松。在 CubeMX 配置 DSI 时你需要把计算出的 lane 速率填进去再让工具反推 PLL 分频系数。不同芯片的 DSI PLL 结构不一样不用死记关键是理解lane 速率 总带宽 / lane 数而且不要超过面板 datasheet 标称的最大值。command 模式如果要降低功耗还可以按实际刷新频率来算带宽而不是按 60Hz 死算。3.3 初始化序列与窗口刷新实现下面是典型的 command 模式初始化片段。为了简化我用封装函数说明思路。// 发送无参数短命令例如 Sleep Out 0x11 void dsi_cmd_no_param(uint8_t cmd) { HAL_DSI_ShortWrite(hdsi, 0, DSI_DCS_SHORT_PKT_WRITE_P0, cmd, 0); } // 发送带一个参数的短命令例如 Memory Access Control 0x36 void dsi_cmd_one_param(uint8_t cmd, uint8_t param) { HAL_DSI_ShortWrite(hdsi, 0, DSI_DCS_SHORT_PKT_WRITE_P1, cmd, param); } // 发送长命令例如初始化参数序列 void dsi_cmd_long(uint8_t cmd, uint8_t *params, uint16_t len) { HAL_DSI_LongWrite(hdsi, 0, DSI_DCS_LONG_PKT_WRITE, len 1, (uint32_t)cmd, (uint32_t)params); }这里 HAL_DSI_LongWrite 是 STM32 HAL 里的函数不同系列签名略有差异实际使用时以你的固件库为准。初始化序列看起来不像“操作寄存器”而像在发一串 DCS 指令这也是 command 模式的特点。初始化完成后设置显示窗口并用 Memory Write 写像素void dsi_set_window(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { uint8_t col_params[4]; uint8_t row_params[4]; col_params[0] x0 8; col_params[1] x0 0xFF; col_params[2] x1 8; col_params[3] x1 0xFF; row_params[0] y0 8; row_params[1] y0 0xFF; row_params[2] y1 8; row_params[3] y1 0xFF; dsi_cmd_long(0x2A, col_params, 4); dsi_cmd_long(0x2B, row_params, 4); }接下来发 0x2C 并传输像素数据。一帧数据量通常比较大不能只用一个 DSI 长包需要按每包不超过 4096 字节或者屏驱动 IC 推荐的包长来拆分。void dsi_copy_buffer(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint8_t *buf) { uint32_t total_bytes; uint32_t sent 0; uint32_t chunk; dsi_set_window(x0, y0, x1, y1); dsi_cmd_no_param(0x2C); // Memory Write total_bytes (x1 - x0 1) * (y1 - y0 1) * 2; // RGB565 示例 while (sent total_bytes) { chunk total_bytes - sent; if (chunk 4096) chunk 4096; // 使用 DSI Memory Write Continue 或者分块发送 HAL_DSI_LongWrite(hdsi, 0, DSI_DCS_LONG_PKT_WRITE, chunk, 0, (uint32_t)buf[sent]); sent chunk; } }实际产品中整帧搬运最好用 DMA不要用 CPU 一字节一字节搬。STM32 的 DSI host 通常会有一条和 DMA 配合的路径把内存中的 framebuffer 直接搬运到 DSI TX FIFO效率高很多。如果只是调试验证CPU 搬运也能跑但分辨率上去之后会卡。3.4 要不要 LTDC很多人在这里绕晕STM32 的 DSI 外设通常和 LTDCLCD-TFT 控制器放一起CubeMX 默认会把 DSI 挂在 LTDC 后面方便视频流直接送显示。但 command 模式不使用 LTDC 的实时输出因为主机不是“扫屏”而是“写显存”。所以配置 command 模式时可以完全不管 LTDC直接用 DSI host 的发送接口往屏里塞命令和像素数据。不过要注意如果你的面板本身只有 MIPI 接口、没有 GRAM而是纯 video 屏那必须开 LTDC 配合 DSI 的 video 模式。判断方法很简单看 datasheet 里“Display Interface”部分有没有写明支持 DCS Command Mode / MCU Interface如果有就能走 command 流程。4. 从单片机屏到安卓板Linux/DRM 下的移植要点4.1 安卓板为什么也要 command 模式很多人觉得安卓板屏幕都是 video 模式其实手机里的屏幕大量工作在 command 模式。因为安卓系统有状态栏、通知栏这些局部刷新率不高的内容command 模式配合面板自刷新可以明显降低功耗同时 TE 信号能避免撕裂体验也更好。所以当你把一块 command 模式的屏幕接到 RK、全志、高通这类安卓主控上时要做的不是“像单片机那样直接发 DSI 包”而是在 Android 的显示链路里告诉系统这块屏是 command 模式需要走“写显存”的路径。4.2 DRM panel 驱动里的 command 模式实现Linux 下控制器驱动通常已经由原厂提供你需要写的是 panel 驱动。在 DRM 框架里panel 驱动主要实现 drm_panel_funcs 这几个回调prepare上电、复位、发初始化序列enable发送 Display On启动刷新disable发送 Display Offunprepare复位或断电。初始化序列的发送底层就是调用 mipi_dsi_dcs_write 或者 mipi_dsi_dcs_write_buffer。这些函数把 DCS 命令封装成 DSI 包和你在 STM32 上手动构造的短包、长包是同一套协议。如果你在单片机端做过 DSI command 模式再看这些接口会觉得特别亲切。关键区别在于 mode_flags。在 panel 驱动的 mode 描述结构体里要设置 MIPI_DSI_MODE_COMMAND 而不是 MIPI_DSI_MODE_VIDEO。如果没有设置这个标志DRM 子系统可能会按 video 模式去调度导致屏参对但显示行为不对。static const struct drm_display_mode panel_mode { .clock 26200, // pixel clock in kHz .hdisplay 480, .hsync_start 480 20, .hsync_end 480 20 10, .htotal 520, .vdisplay 800, .vsync_start 800 10, .vsync_end 800 10 10, .vtotal 840, }; static const struct mipi_dsi_device_info panel_info { .type sample_cmd_panel, .channel 0, }; static int panel_probe(struct mipi_dsi_device *dsi) { dsi-lanes 2; dsi-format MIPI_DSI_FMT_RGB888; dsi-mode_flags MIPI_DSI_MODE_COMMAND | MIPI_DSI_MODE_LPM; // ... }LPMLow Power Mode这个 flag 也很重要。它表示命令和像素数据都可以通过 LP 状态低速发送功耗更低。但也有的屏幕必须在 HS 下传输像素数据否则速度不够或者信号不稳定。这个要根据屏厂建议来不能想当然。4.3 TE 同步与 Async 刷新command 模式下安卓系统如果每次翻页都全屏写显存带宽浪费很大也容易撕裂。硬件上一般有 TE 引脚面板会在刷新到特定位置时拉一个信号。主机可以把这个信号作为同步点只在 TE 到来之后启动下一次 Memory Write从而避免屏幕的“撕裂”。在 DRM 里对应逻辑通常隐藏在 panel 驱动和 DSI controller 驱动的配合中。应用层要实现“只更新脏区域”还需要上层合成器支持。实际调屏时如果发现画面撕裂、滚动模糊先确认 TE 引脚有没有正确接、屏驱动里有没有把 0x35 Set Tear On 发出去。很多新平台还支持 Panel Self Refresh、Allow Screen Power Off 等功能本质上都是把 command 模式配合 GRAM 的特性发扬光大。这部分调好了功耗能比 video 模式低不少。5. 调试实录常见问题与排查速查5.1 花屏、点不亮、颜色错乱该怎么办我列一个速查表都是实际项目里最容易踩的坑现象可能原因排查方向完全黑屏/白屏上电时序不对、初始化没跑完检查 RESET、VDDI、VCI 时序Sleep Out 后延时是否够画面错位、有拖影窗口设置0x2A/0x2B参数不对打印窗口坐标确认行列值是否正确颜色偏色MADCTL 的 BGR/RGB 位、像素格式不对检查 0x36、0x3ARGB888 和 RGB565 区别很大轻微花点、闪烁DSI lane 速率过高或信号质量差降低 lane 速率检查差分对等长和阻抗撕裂没有 TE 同步或刷新时序乱确认 0x35 命令和 TE 中断接法读不到屏 ID没有先发 Set Maximum Return Packet Size发 0x06 设置回包长度再发读命令动态画面卡顿全屏搬运开销大改局部刷新或者用 DMA、提高 lane 数5.2 我在实际项目中踩过的几个坑第一DSI 初始化命令不能照搬 SPI 屏的初始化脚本。SPI 屏的初始化参数可能带有地址字节DCS 写入时不需要地址命令码后面直接跟参数。我见过有人把 SPI 初始化脚本原样改成 DSI 长包发出去结果屏幕永远点不亮因为第一个参数被当成命令码解析了。第二LPM 和 HS 的切换不是你想切就能切。有的屏在 DSI Host 配置里允许 command 用 LP 发送但如果你的屏和主控距离较远、信号质量一般HS 下反而更稳定。不要盲目追求低功耗而把所有包都用 LP 发。第三TE 信号不是可选项。command 模式高刷新率下如果不接 TE屏的 GRAM 刷新时序和主机写入时序是不同步的非常容易出现一条横向的撕裂带。哪怕你用单片机做简单 GUI也建议把 TE 接上。5.3 排查工具和思路调试 DSI 屏时普通逻辑分析仪很难直接抓到差分对上的高速信号所以更多依赖控制器寄存器回读和屏幕表现来判断。STM32 的 DSI 外设有错误标志位比如超时、CRC 错误、ECC 错误调试时建议先把这些中断打开第一时间就能发现是总线问题还是时序问题。在 Linux 平台可以打开 DRM 和 DSI 的调试节点查看初始化是否成功、有没有发出 DCS 命令、TE 中断是否触发。很多时候屏厂提供的初始化代码能用但到了 Linux 下要拆分成 prepare/enable 等多个阶段不能一股脑全塞在 probe 里。6. 送几个实用的小建议如果你只是想把一块 command 模式的 MIPI 屏跑起来建议先从低速开始。lane rate 算出来之后故意降到理论值的 60% 左右去调通初始化等屏幕能正常显示静态内容了再逐步提升到目标速率。这样能排除很多信号完整性问题。另一个建议是把 DSI 相关的初始化命令封装成“命令表”的形式由一串结构体驱动而不是写几十个散落的 HAL 调用。命令表里每一项包含命令码、参数、延时这样从屏厂拿到新屏幕后只需要换表不用动代码。我在 STM32 和 Linux 两个平台上都用这种模式维护起来轻松很多。从单片机屏到安卓板屏参和时序的理解是相通的。你如果已经能在 STM32 上通过 command 模式把数据写进 GRAM再去看 Linux 的 mipi_dsi_dcs_write 接口基本就是换了个调用壳子底层的 DSI 包格式、DCS 命令、TE 机制完全一样。把这套核心搞明白后面再遇到什么 OLED、LTPS、高刷屏也不过是在这个基础上加一些新命令而已。

看完文章,想为自己的企业也做一次专业网站诊断?

尧图顾问免费为您评估现有网站,并给出建站/改版建议与报价方案。

免费获取方案