☰
LCD驱动底层原理与实战:从接口时序到花屏调试
2026/10/21 18:40:57 网站建设 项目流程

这两年陆陆续续帮人看了不少LCD驱动相关的问题,发现一个很有意思的现象:很多人拿着一个屏幕模组,数据手册也看了,代码也抄了,但屏就是点不亮,或者亮了一半花屏。问题大多不是出在代码本身,而是出在对LCD驱动这件事整体认知的缺失上——你连手里这块屏是什么接口、什么控制器、数据是怎么进到显存里的都没弄明白,抄再多初始化序列都是白搭。

这篇东西我不打算写成某个具体型号的逐行注释,而是把LCD驱动这件事拆开,从接口类型、底层时序、初始化序列、显存操作到实际调试的坑,一条线讲下来。不管是单片机玩1602字符屏、12864点阵屏,还是带控制器的TFT彩屏(ILI9341、ST7789、ST7735这类),底层的逻辑是相通的。看懂了这套东西,换任何一块屏,你都能独立把驱动写出来。

1. 说实话,大多数人第一步就错了:先搞清楚你手上的是哪块屏

拿到屏幕先别急着接杜邦线、抄初始化代码,先回答一个问题:这块屏是什么接口?这决定了后面所有的时序工作方式。

LCD模组按接口可以粗暴分成几大类:

字符/段码类屏幕。比如1602、2004这类,内部控制器一般是HD44780或其兼容型号,接口是并行的(4位或8位模式),需要RS(寄存器选择)、RW(读写)、E(使能)三根控制线加上若干数据线。这类屏的驱动逻辑比较简单,本质上就是往控制器的DDRAM地址空间写ASCII码,屏幕自己负责把字符点阵画出来。

点阵单色屏。比如常见的12864,控制器可能是ST7920、KS0108,也有走SPI/I2C接口的。ST7920比较特殊,它支持并口也支持串行(SPI),串行模式下还会切换到一种带起始位标记的特殊时序。

彩色TFT屏。这是现在最主流的,也是问题最多的。控制芯片常见的有ILI9341(2.4寸240x320四线SPI最多)、ST7789(240x240/240x320常见)、ST7735(1.8寸那种128x160常见的)、ILI9488(3.5寸480x320)等。接口就更多了:四线SPI、三线SPI(没有DC线)、8080并口、6800并口、RGB并口、甚至MIPI DSI。同一个控制器芯片往往同时支持多种接口,靠硬件引脚的电平配置来切换。

这里插一句,为什么TFT屏用SPI接口的最多?因为引脚少,四根线加电源背光就能点亮,对单片机极其友好。代价是刷屏速度受限于SPI时钟,但日常显示传感器数据、做菜单界面完全够用。

回到"先搞清楚这是什么屏"这一步,具体怎么查?三种方式:

  1. 看模组丝印或者背面的型号标注,搜数据手册,找到控制器型号。
  2. 看引脚数量。四线SPI的引脚一般一共8到10个(VCC、GND、CS、RST、DC、MOSI、SCLK、BLK),并口屏引脚至少16个以上,RGB屏直接排线一大排。
  3. 看数据手册里的接口框图,确认当前这个模组用的是哪种接口模式。

这一步千万别省。我见过有人在ILI9341的屏上写ST7789的初始化代码,折腾了两天没点亮,最后发现控制器错了,换了初始化序列一分钟不到就亮。数据手册上必定写了初始化序列,但那是对应的控制器,不是"屏幕品牌"。

2. SPI时序里容易被忽视的细节:DC线、时钟极性和字节序

四线SPI是玩TFT屏最常用的接口,我就拿它拆开讲。四线指的是SCLK(时钟)、MOSI(主出从入)、CS(片选)、DC(数据/命令选择),另加一根RST复位线,一共五根控制线。

很多人对SPI的理解停留在"发时钟、送数据"这个层面,但TFT屏的SPI有一个关键不同:它用DC线的电平来区分当前发送的这个字节是命令还是数据。

  • DC拉低(0),当前字节被当作命令,比如"0x36"表示设置访问方向。
  • DC拉高(1),当前字节被当作该命令的参数(数据)。

这就是四线SPI和三线SPI的本质区别。三线SPI没有DC线,它通过在每个字节前面额外发送一个"Data/Command"标志位来实现同样的功能。所以同一个控制器,在四线模式和三线模式下,底层发送函数是完全不同的。你抄代码的时候务必检查,手里的模组是几线模式,函数名和宏定义是不是对应的。

再一个是时钟极性(CPOL)和相位(CPHA)。大多数TFT LCD控制器默认工作在SPI Mode 0(CPOL=0,CPHA=0),也就是空闲时时钟线为低电平,在时钟上升沿采样数据。但注意,不一定全是Mode 0,有些屏手册里明确写了Mode 2或Mode 3。

判断和调整的方法:

// 以STM32硬件SPI为例,设置CPOL和CPHA SPI_InitTypeDef SPI_InitStruct; SPI_InitStruct.SPI_Direction = SPI_Direction_2Lines_FullDuplex; SPI_InitStruct.SPI_Mode = SPI_Mode_Master; SPI_InitStruct.SPI_DataSize = SPI_DataSize_8b; SPI_InitStruct.SPI_CPOL = SPI_CPOL_Low; // 空闲电平为低 SPI_InitStruct.SPI_CPHA = SPI_CPHA_1Edge; // 第一个边沿采样 SPI_InitStruct.SPI_NSS = SPI_NSS_Soft; SPI_InitStruct.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_8;

如果你用GPIO模拟SPI,那就更直接,代码里控制翻转顺序就行。模拟SPI的好处是时钟频率、极性完全可控,缺点是速度上不去,刷全屏会明显看到从上到下的扫描过程。用硬件SPI加DMA是标准做法,后面细说。

还有一个经常被坑的点:字节序。TFT屏的SPI一般要求高位在前(MSB first),绝大多数控制器如此。但在写RGB565颜色数据时,因为一个像素是16位,而SPI是按字节发送的,就会出现高字节先发还是低字节先发的区别。ILI9341在RGB565模式下,通常要求高字节在前,数据手册里画得非常清楚。很多花屏问题就出在这:你发颜色数据时高字节和低字节顺序反了,屏幕显示出来的颜色就完全错乱。

写一个稳健的底层发送函数,建议把命令写入和数据写入分开,避免在应用代码里反复判断DC状态:

// 底层命令/数据发送 static void LCD_WriteCmd(uint8_t cmd) { LCD_DC_LOW(); // DC = 0,命令模式 SPI_TransmitByte(cmd); } static void LCD_WriteData(uint8_t dat) { LCD_DC_HIGH(); // DC = 1,数据模式 SPI_TransmitByte(dat); }

如果你要做的是并口8080时序,那逻辑又不一样了,需要CS、WR(写使能)、RD(读使能)、RS(相当于DC)、DB0-DB15数据线,操作时先置RS再拉低WR,然后在数据线上放数据,最后拉高WR产生上升沿锁存。这个时序要求更严格,通常需要操作GPIO来实现,速度也会受限。

3. 初始化序列不是玄学:看懂那串"天书"寄存器到底在干什么

TFT彩屏拿到手,数据手册里总有一页"Initial Setting"或者"Initial Sequence",一长串命令加参数。很多人直接抄,抄完能亮就万岁,不能亮就怀疑人生。

我的建议是:先照着手册抄一遍保证能亮,然后花时间搞懂每一条都在干嘛。这对后面调试方向、亮度、颜色格式、扫描方式都至关重要。

拿ILI9341的手册初始化序列来举例,拆几条最常见的:

// 这段是ILI9341典型的初始化开头 LCD_WriteCmd(0x01); // Software Reset delay(120); LCD_WriteCmd(0x11); // Sleep Out delay(120); LCD_WriteCmd(0x36); // Memory Access Control LCD_WriteData(0x48); // BGR=1, row/col exchange方向控制 LCD_WriteCmd(0x3A); // Interface Pixel Format LCD_WriteData(0x55); // 16bit/pixel(RGB565) LCD_WriteCmd(0x29); // Display ON
  • 0x01 Software Reset:软件复位,让控制器回到默认状态。执行完后必须延时,手册一般要求120ms以上,有些快速初始化代码只延时5ms也能跑,但那是侥幸。这个寄存器直接决定后续命令能不能正确解析。复位后几乎所有寄存器都会恢复到上电默认值。

  • 0x11 Sleep Out:退出睡眠模式。屏幕没退出睡眠之前,内部的DC/DC转换器不在工作状态,即使你写了显存,背光也亮了,屏幕依然可能一片白或者一片黑。这一步做完也要延时,因为内部电荷泵需要时间建立电压。

  • 0x36 Memory Access Control(MADCTL):这是最容易搞错方向的地方。它控制GRAM的扫描方向以及RGB/BGR色彩顺序。一个字节的位定义大致如下:

    • Bit7: MY(行地址方向翻转)
    • Bit6: MX(列地址方向翻转)
    • Bit5: MV(行列交换,也就是屏是横着放还是竖着放)
    • Bit4: ML(行地址镜像)
    • Bit3: BGR(0=RGB,1=BGR,颜色格式)

    我通常的做法是:先把屏幕点亮,显示一个测试图案(比如红绿蓝三个色块加十字线),然后根据实际显示效果调整这个字节。比如屏幕的坐标X方向和实际需求相反,就把MX位取反;内容倒过来了,就改MY位。与其看手册猜来猜去,不如直接试,一次改一个bit,很快就能试出正确方向。这个字节同时在写显存地址递增方向上有作用,改完方向后你画矩形的起始坐标逻辑可能也要跟着换。

  • 0x3A Interface Pixel Format:设置像素格式。0x55是16位RGB565,0x66是18位RGB666。如果你的单片机引脚够多但SPI带宽有限,或者你只是显示简单界面,也可以选18位,但用得最多的还是RGB565。注意,这里的位数指的是控制器从SPI接口接收到的像素位数,不是你屏幕本身的物理色深。大部分屏物理上已经是18位甚至24位色,你把接口配成16位,控制器会自动做转换。但字节序问题前面说了,还是要自己保证。

  • 0x29 Display ON:打开显示。注意,打开显示之后屏幕才会去GRAM里读数据并刷新到LCD面板上。有些人初始化完了发现屏幕亮的但不是自己写的内容,就是因为在这条之后没有触发显存刷新,或者显存里本来就有随机数据。

完整初始化序列还有Gamma校正、电源控制等内容,这些一般照抄手册即可。但如果你发现初始化之后屏幕偏色明显、对比度很差,问题大概率出在Gamma寄存器。这些参数每个面板厂商都会给出微调值,不同批次的面板可能还有细微差异。调试的时候优先信手册给的,别自己乱调。

4. 显存操作的核心:行列地址设定与坐标窗口裁剪

控制器内部有一块GRAM,大小等于分辨率乘以像素深度。你通过SPI写入的数据并不是直接送到LCD面板,而是先写进这块GRAM,控制器再周期性地把GRAM内容刷到屏上。

关键命令是设置列地址和设置行地址。在ILI9341上对应的命令是0x2A(Column Address Set)和0x2B(Page/Row Address Set)。这两个命令决定了你接下来写入的像素数据落在GRAM的哪个矩形区域内。

假设我们要在屏幕上画一个从 (x1, y1) 到 (x2, y2) 的矩形区域:

// 设置列范围 x1 到 x2 LCD_WriteCmd(0x2A); LCD_WriteData(x1 >> 8); LCD_WriteData(x1 & 0xFF); LCD_WriteData(x2 >> 8); LCD_WriteData(x2 & 0xFF); // 设置行范围 y1 到 y2 LCD_WriteCmd(0x2B); LCD_WriteData(y1 >> 8); LCD_WriteData(y1 & 0xFF); LCD_WriteData(y2 >> 8); LCD_WriteData(y2 & 0xFF); // 写入GRAM起始 LCD_WriteCmd(0x2C); // Memory Write

执行完0x2C之后,后面你连续发的像素数据会按照"从左到右、从上到下"的顺序填满整个矩形窗口,写满之后地址指针自动停在窗口末尾。这里有个细节:同一个窗口内,你发的数据超过窗口容量时会怎样?不同控制器行为不完全一样,有些会回卷到窗口起点继续覆盖,有些则停止接收。所以发送数据量一定不能超过窗口大小,这是很多花屏、错位的根源。

有了窗口裁剪能力,最直接的优化就是局部刷新。比如你做一个电压显示界面,数值每分钟变一次,完全没必要每次把整屏重刷一遍。先计算出新数值所在的矩形区域,设置窗口到那一小块,然后把新的数字字符数据只写到那个区域,其他地方保持不动。

字节序问题再次出现:写入RGB565像素时,高位在前(比如红色0xF800,先发0xF8再发0x00)。如果和你实际情况不符,颜色会整个乱掉。

还有一个常见坑:写入速度不要太激进。很多人初始化SPI时直接压到最高速率,刷屏时发现数据错位、颜色串位。原因可能是布线的寄生电容太大、杜邦线太长、或者上拉电阻配置不当。解决方法很朴素:先降到较低频率(比如1MHz)验证整套流程没问题,再逐级往上抬。我在面包板上用10cm杜邦线驱动2.4寸屏,SPI跑36MHz就偶尔出现错位,降到18MHz就完全稳定。你要是做成PCB,走线短并且规整,跑更高频率完全没问题。

5. 字符、图形与漂亮界面的基础:从画点函数开始搭

LCD驱动最底层最基础的就是一个单像素操作,但我不建议你在应用层直接调用单像素函数,那样效率太低。标准的做法是:画点函数只用来做底层,上层所有图形和字符都建立在"填充矩形"这个操作上。

比如画字符'A',点阵数据是一个8x8或8x16的数组,每个bit代表一个像素是前景色还是背景色:

// 8x16 ASCII字符点阵示例,每行一个字节,高位为左侧 const uint8_t font_8x16[16] = { 0x00, 0x00, 0x10, 0x10, 0x28, 0x28, 0x44, 0x44, 0x7C, 0x82, 0x82, 0x82, 0x82, 0x00, 0x00, 0x00 }; // 显示一个字符到 (x, y),color为前景色,bg_color为背景色 void LCD_ShowChar(uint16_t x, uint16_t y, char ch, uint16_t color, uint16_t bg_color) { const uint8_t *p = &font_8x16[(ch - 32) * 16]; for (uint8_t row = 0; row < 16; row++) { uint8_t line = p[row]; for (uint8_t col = 0; col < 8; col++) { if (line & (0x80 >> col)) { LCD_DrawPoint(x + col, y + row, color); } else { LCD_DrawPoint(x + col, y + row, bg_color); } } } }

如果每个字符都这样逐点画,效率很低,但显示少量文字场景无所谓。想快一点,就先设置窗口到字符所在区域,然后把这一整块的点阵数据一次性批量发送过去。格雷码、字模数据用C数组存着,字符显示就变成纯数据传输,速度能快上一个量级。

画线的算法,常用的是Bresenham画线,不引入浮点运算,适合单片机跑。画实心圆则可以先求圆形边界,然后按水平扫描线填充。这些算法网上随便一搜一大把,但有一件事要提醒:在做这些图形函数前,一定先把坐标系和旋转方向吃透。MADCTL寄存器改一次,屏幕的物理坐标和GRAM坐标映射就变一次,你的画线算法里x和y哪个是宽哪个是高也得跟着改,否则画出来的圆是歪的。

界面设计上,我个人的经验是:TFT屏上做UI,能用色块区分就不要用复杂的背景图。单片机显存有限,复杂的背景图要么外挂Flash和字库芯片,要么预先压缩存储。而色块加描边加字符,稍微排一下版,实际观感完全不输花里胡哨的背景图,而且刷新速度快几个量级。

6. 点亮之后才是开始:实测中的花屏、白屏和偏色排查记录

屏幕点亮以后,调试工作才算正式开始。下面这些是我在帮人排查过程中遇到频率最高的几类问题,每一个我都可以单独写一篇文章,这里把最核心的排查思路列出来。

6.1 通电后屏幕背光亮但全白/全黑无内容

先确认初始化是否真的执行完并延时足够。很多人直接在初始化函数后面立刻写显存,Sleep Out到Display On之间的120ms延时没给够,控制器还处于内部稳压建立阶段。

再一个常见原因是GPIO复用没配好。比如你用的单片机引脚同时有下载口或串口功能,初始化之前不小心把外设复用了,SPI时钟或MOSI没真正输出到LCD引脚上,表现就是屏幕完全无响应。这种问题在共享引脚的单片机上尤其多。

如果确认时序没问题,把眼睛凑近屏幕用强光照射,仔细看有没有极淡的图案。如果有很淡的内容,说明LCD面板本身在被刷新,只是对比度或者背光亮度设置不对,问题大概率在对比度寄存器或Gamma设置。如果强光下也什么都看不到,那多半是初始化序列问题,换个版本试试。

6.2 屏幕有内容但花屏,像雪花一样

两种可能。

第一种是SPI速率过高,数据线到达控制器时建立时间不够,导致部分bit被错读。先降速试试。降速能解决,那就回到布线问题,而不是继续调软件。

第二种是行地址/列地址设置没配对。写数据时窗口范围设错了,数据写到GRAM外面去了,GRAM里原本的内容和新增内容交错,就会出现花屏。

还有第三种隐藏情况:如果用的是三线SPI模式,每个数据字节前有个DC标志位,有些控制器还要求标志位在时钟的特定相位出现。很多人在三线模式下直接复用四线模式的发送函数,结果DC位被当成了数据,花屏就是必然。

6.3 颜色不对,红蓝互换或者整体偏色

红蓝互换几乎可以肯定就是MADCTL里的BGR位没设对。把0x36的参数改成含有BGR=1的值,比如从0x00改成0x08,或者从0x48改成0x40,颜色就会恢复正常。整体偏色则要考虑接口像素格式命令(0x3A),如果你发了RGB565但配了18位格式,高低位的截断规则会让颜色偏到离谱。还有一种很隐蔽的偏色原因:初始化序列中Gamma设置只适配了某一种面板,你的屏虽然控制器一样但面板不是同批次,Gamma参数就有偏差。这时候别乱调Gamma,先确认前面两个基本配置,再做细微修正。

6.4 屏幕显示方向不对

这个问题最好解决,但最容易把人绕晕。你想要的"正常"方向可能是横屏,也可能是竖屏,每个人的习惯不一样。唯一的判断标准是数据手册里定义的原点(0,0)位置,以及你希望内容从哪个角开始。修改0x36的MY/MX/MV位即可。

一个超好用的验证方法:先在屏幕左上角画一个已知图形,比如红色横线加蓝色竖线组成的十字,然后观察屏幕上十字的位置和朝向。这样你可以很快确定当前的坐标映射关系,而不是盯着屏幕猜"现在X轴是哪个方向"。

7. 驱动性能优化:刷屏提速的几条实际路径

如果你只是显示静态内容,驱动写到这里已经够用了。但如果你要显示动态波形、动画、或者摄像头数据,就必须要考虑刷屏性能了。这里的瓶颈通常不在控制器,而在MCU到控制器的传输链路。

第一条路径:硬件SPI加DMA。这是性价比最高、效果最明显的优化。用DMA搬运显存数据,CPU可以在SPI搬运的同时去做其他计算。在STM32上,设置SPI的TxDMA请求,然后把显存缓冲区的地址和长度配置给DMA控制器,启动传输后CPU就能去渲染下一帧了。实测下来,在72MHz主频的MCU上刷一块320x240的屏,用模拟SPI大约200ms一帧,用硬件SPI不加DMA大约80ms,加上DMA后CPU占用率直线下降,帧率还能再快一点。注意DMA传输的数据长度是字节数,不是像素数,RGB565下像素数要乘以2。

第二条路径:局部窗口刷新。前面已经提到了,改动UI的哪一块就只重绘那一块。整个屏幕区域设好窗口后,只发送变化区域的像素。比如一个实时曲线图,真正变化的只有最新那几十个点,重刷整个曲线区比重刷全屏少一个数量级的传输量。这个方法不需要改底层SPI,只需要在应用层规划好哪些区域需要刷新。

第三条路径:双缓冲思路。注意,大部分廉价TFT控制器本身没有硬件双缓冲,GRAM只有一份。所谓双缓冲是在MCU的RAM里维护一份"影子显存"(shadow buffer),先把所有绘图操作都发生在内存中,然后一次性把整块或者变更区域拷贝到控制器GRAM里。这样做的最大好处是避免"边画边传"时画面撕裂——比如你正在画一个矩形,画到一半传输被打断,屏幕上就会出现半个矩形。如果MCU的RAM足够大(比如STM32F407的196KB RAM,能装下一块240x320x2=153600字节的帧缓冲),这个方案非常推荐。RAM不够的话,就用局部窗口刷新加绘制顺序优化来减少撕裂感。

第四种做法:调整SPI频率和位宽。有些MCU的SPI支持DMA模式下自动以更高的位宽操作,或者在支持QSPI接口的屏上,4线QSPI可以同时传4个bit,速度提升接近4倍。但QSPI屏需要控制器的额外支持,大部分普通TFT屏不支持。这个属于进阶玩法,有需要再说。

还有一个容易忽略的点:初始化完成后,把所有用不到的GPIO口(特别是接到LCD的CS、DC、RST线)设置成正确的推挽输出状态,不要悬空。悬空的引脚可能因为外部干扰产生毛刺信号,导致控制器误判命令,屏幕会随机花屏。这个坑在环境电磁干扰比较强的场合尤其明显。

8. 踩坑备忘:几个花了我不少时间才悟出来的细节

最后把我自己实际踩过、帮别人排查过的几个冷门细节集中列一下,都是文档上不会特意写,但实际操作却极其影响成败的点。

  • 背光控制不能直接接MCU的GPIO驱动大电流LED。很多模组的背光LED需要几十毫安电流,GPIO直接驱动要么亮度不够,要么把引脚烧了。接一个三极管或MOS管做开关,用GPIO控制基极/栅极,背光电源从主电压供。如果你要PWM调光,三极管开关电路也能照常工作。

  • 3.3V和5V逻辑电平匹配问题。很多STC、AVR、老式51单片机是5V逻辑,而LCD模组是3.3V供电,它的SPI引脚不一定是5V容忍。直接怼5V信号有概率损坏控制器。稳妥的做法是加电平转换芯片,或者给5V单片机的SPI引脚串电阻分压。反过来,3.3V单片机驱动5V逻辑的模组,一般是能工作的,但看时序余量,有些屏就是不亮。做产品前先确认逻辑电平匹配,别拿运气当设计。

  • RST引脚的复位时序。初始化代码最常见的实现是拉低RST、延时、拉高、再延时。有些控制器要求复位脉宽最低10us,有些要求50us,这个RA、RB参数必须按手册来。如果复位脉宽不够或者复位结束后延时太短,第一次初始化大概率失败。极少数屏还会出现"第二次上电偶尔不亮"的情况,就是RST时序余量不够。解决办法很简单:把复位脉宽放宽到10ms量级,上电后先延时100ms再拉高RST。

  • 不停电直接复位和彻底断电复位的区别。调试过程中,按复位键只复位了MCU,LCD控制器并没有复位,它还保留着之前的GRAM内容和寄存器状态。这时候重新执行初始化序列,有些控制器会因为状态已经在运行模式而跳过某些关键步骤,导致显示异常。规范做法是MCU上电后先对LCD执行一次完整的硬件复位再初始化。如果你发现"上电后正常,软复位后不正常",基本就是这个问题。

  • 看门狗中断里不要再刷屏。如果你在单片机工程里开了看门狗,确保看门狗喂狗不能和刷屏的DMA传输抢同一个SPI资源。否则DMA传输被喂狗中断打断,屏幕就会花。除非你的SPI驱动做了完整的异常处理,否则不要在中断上下文里发起刷屏操作。

  • 显示内容闪烁的一个隐藏原因:电压跌落。屏幕刷新瞬间,控制器内部电流会明显增大,如果供电回路的走线太细、或者电源稳压余量不足,瞬间压降会导致控制器内部逻辑异常,表现出来就是周期性闪烁。把示波器接到LCD供电脚看,刷新时如果有几十毫伏以上的跌落,就要优化电源旁路电容,通常在VCC脚就近放一个10uF钽电容加一个0.1uF陶瓷电容能解决。

说到底,LCD驱动不是背代码,是做时序、做状态机、做资源规划。你把底层的"命令-数据-延时"这套节奏吃透了,把显存访问的基本规则搞清楚了,剩下的字符显示、图形绘制、局部刷新都是往上叠砖。遇到问题的时候不要急着换屏幕、换代码,先从电源、时序、电平、寄存器几个基础维度逐项排查,大多数问题都能定位到具体环节。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询