很多人玩单片机、做小项目,做到一半卡在屏幕上。代码逻辑看着没问题,屏幕就是不亮,或者亮了之后花屏、偏色、闪屏。追根溯源,大部分问题都出在LCD屏驱这一层。LCD屏驱,说白了就是负责把主控发过来的数据,按照屏幕要求的时序和格式,转换成液晶面板上每个像素点电压的那颗驱动芯片,以及围绕它展开的初始化、读写、调试这一整套工作。这篇文章不聊玄乎的架构,就讲实际做项目时最常用到的LCD屏幕驱动方案,从选型、接口、初始化时序到调试手段,把容易踩的坑一并捋清楚。适合刚接触嵌入式的学生、做产品的工程师,以及所有被屏幕折磨过的开发者参考。
1. 从选型说起:屏幕驱动方案怎么定
1.1 一颗屏能不能玩好,九成看驱动芯片
很多朋友选屏幕的时候光看尺寸、分辨率和价格,拿到手才发现驱动IC是个完全没听过的小众型号,资料少、寄存器又怪,开发起来非常痛苦。我做项目这些年,一个特别深的感受是:屏幕本身只是一块玻璃加液晶,真正决定你开发效率的,是贴在玻璃上的那颗驱动IC。
常见的小尺寸LCD,基本都是一颗驱动IC直接集成在屏的FPC排线上,主控通过SPI、I2C或者并口跟这颗IC通信。这颗IC负责管理像素矩阵的刷新、伽马校正、背光控制、扫描方向转换等等。换句话说,屏幕上所有你看到的行为,都是这颗IC在背后执行指令的结果。所以选屏幕,本质上是选驱动IC。
现在市面上最常见的几个系列,比如ST77xx系列、ILI93xx系列、GC9A01这类,都是被大量项目验证过的“老黄牛”型号,网上资料多,别人踩过的坑也基本都发过帖,用起来省心得多。偏门IC也不是不能用,但一旦遇到白屏或者色彩异常,你可能连寄存器手册都找不到中文版本,更别说遇到问题时的排查思路了。
1.2 三种常见接口怎么选:SPI、I2C、并口
主控和屏驱之间通信,接口大概分三类:SPI、I2C和并口。选择哪种接口,主要看主控的资源、刷新率需求以及引脚余量。
SPI是最常用的,四线制(SCLK、MOSI、CS、DC),少一根MISO也可以,很多屏幕为了省引脚直接不要回读功能。SPI的好处是接线少、速率高,STM32这类主控的硬件SPI轻松跑到十几兆甚至几十兆,对于240x320这种分辨率、65K色的屏幕,刷新率能做到10到20帧左右,做菜单界面、仪表盘完全够用。缺点就是单路串行数据传输,传一帧数据量比较大时,CPU会被占用不少,所以很多人会用DMA配合SPI来缓解。
I2C只在极小尺寸、低分辨率的屏上用,比如0.96寸、1.3寸的OLED或LCD。I2C速度上限一般就几兆,刷新率很低,胜在只需要两根线,适合做那种只显示几个数字、图标的低功耗小设备。如果你要做动效或者流畅的曲线刷新,I2C基本可以放弃。
并口(比如经典的8080并口)在小尺寸屏幕上其实出现得也不少了,主要是那些分辨率稍高、刷新要求也高的屏。并口一次传8位或16位数据,总线频率虽然不高,但吞吐量比SPI大好几倍。代价是引脚数量多,MCU别的不多就引脚多,那用并口就很划算。不过现在很多MCU都内置了LCD并口控制器,比如某些系列的单片机,这时选并口屏就是个很自然的选择。
1.3 供电与电平匹配:新手最容易忽视的一环
屏幕驱动这块,有一个特别容易出问题但又特别容易被忽视的点:供电和电平匹配。
大多数小尺寸LCD的驱动IC逻辑供电是2.8V或3.3V,背光LED供电可能是3.0V到3.3V,而面板内部还有DC-DC升压电路产生LCD偏置电压(VCOM、VGH、VGL这些)。如果你的主控是5V系统,那跟屏的IO通信就必须加电平转换,或者用那种带电平容忍的引脚。我见过很多板子,3.3V主控接5V屏幕,结果屏幕发烫、显示异常,甚至直接烧掉驱动IC。
另外,很多屏的背光电压和逻辑电压是分开的。如果背光电源不稳定,你会看到亮度波动或者闪烁,但这种闪烁和刷新率无关,是电源纹波导致的。所以做屏幕驱动的时候,电源纹波、电容的摆放位置这些都要上心。建议在屏幕供电引脚附近放一个10uF和一个100nF的组合电容,IO线上串22到33欧的电阻,这招能解决很大一部分莫名的显示问题。
2. 常用屏驱芯片怎么挑:对比与选型逻辑
2.1 小尺寸串口方案:ST77xx、ILI93xx这些绕不开的系列
先看一组最常见的驱动IC和它们的典型配置,这些型号基本覆盖了0.96寸到3.5寸的大部分屏幕。
以ST77xx系列为例,我个人比较常用的是ST7735S和ST7789V。ST7735S常见于1.8寸128x160的屏,ST7789V则常配在1.69寸、1.9寸、2.4寸这些240x320或者240x280的屏上。这两个IC都支持SPI和并口,内部带GRAM,支持RGB565和RGB666颜色格式,ST7789V甚至在某些配置下支持RGB666的8位并口直通模式。它们的初始化寄存器不算复杂,网上能找到大量参考代码,改一下引脚定义就能跑起来,特别适合项目快速起步。
ILI93xx系列里,ILI9341是2.4寸、2.8寸、3.5寸屏的常客,支持SPI和16位并口,最大分辨率240x320。这个IC的寄存器比ST77xx多一些,尤其是伽马校正和电源控制相关寄存器,调得好能明显改善色彩还原。ILI9488则常用于3.5寸480x320的屏,支持SPI、3线SPI、8位/16位并口,因为分辨率更高,SPI模式下的刷新会比较吃力,想流畅显示最好是上并口。
GC9A01也是绕不开的一个,圆形屏大多用这颗IC,分辨率通常是240x240,支持SPI。这颗IC的特性是周边电路简单,初始化序列短,很多圆形表盘项目、小型GUI界面上很常见。
2.2 中尺寸与高刷场景:RGB接口和MIPI接口的驱动IC
再往上走,4.3寸、5寸、7寸这类屏,主控大多是带LCD控制器的应用处理器,或者高性能MCU。这时候屏驱IC的接口就不是SPI/并口能解决的了,常见的是RGB接口和MIPI DSI接口。
RGB接口的驱动IC,比如常见的RGB屏驱动方案,主控直接输出像素时钟(PCLK)、行同步(HSYNC)、场同步(VSYNC)和数据线(一般为16位或24位),屏幕侧的驱动IC负责把这些数据锁存到面板上。这种方案最大的特点是实时刷新,不需要像SPI一样先把整帧数据搬进GRAM,所以刷新率可以做高,适合显示动态视频或流畅图形界面。
MIPI DSI接口则更常见于手机、平板这类设备,走差分信号,速度快,引脚少,但绝大多数单片机主控都不带这个接口,需要额外加桥接芯片。做工业产品或者消费电子原型时,如果不是从方案层面就定好了带DSI接口的主控,我一般不推荐小团队碰MIPI屏,调试门槛确实高一些,光一个MIPI信号质量就够折腾很久。
2.3 选型时一定要看明白的五项参数
把常见驱动IC和选型参数列在一起看,会更直观。用一个表格整理我选型时必看的五个项目:
| 参数 | 要关注什么 | 选型建议 |
|---|---|---|
| 分辨率与尺寸 | 像素点数量,决定GRAM大小和刷新压力 | 小屏选240x240、240x320;超过480x320就要考虑RGB接口 |
| 接口类型 | SPI、I2C、8080并口、RGB、MIPI | 低成本低引脚选SPI;高刷新选并口或RGB;超大屏考虑MIPI |
| 颜色格式 | RGB565、RGB666、RGB888 | 颜色要求高的选RGB666以上,注意驱动IC是否支持 |
| 驱动IC资料丰富度 | 初始化代码、寄存器手册、常见问题帖 | 新手避开冷门IC,选ST77xx、ILI93xx这些主流系列 |
| 供电与电平范围 | 逻辑电压、背光电压、是否支持宽压 | 先确认和主控电平是否兼容,再决定要不要电平转换 |
其实多数人并不会在选型这一步卡太久,因为市面上成品屏幕模组已经把屏和驱动IC绑定好了,你买的实际是“IC方案+屏幕尺寸+分辨率”的组合。但理解了这块,你就知道为什么同样尺寸的屏,有的贵一些,有的便宜很多,因为驱动IC本身的价格和供货情况差异很大。比如缺芯那阵子,某国产屏驱价格翻了几倍,很多产品被迫改了方案,这一点做产品的人才有切身体会。
3. 初始化时序与寄存器配置:把屏幕从白屏点亮的关键路径
3.1 复位流程:为什么“拉低—延时—拉高—再延时”是标准动作
很多人拿到一块新屏幕,第一步就是往屏驱里写初始化指令,结果写完还是白屏。这时候我一般会建议先检查复位时序。
绝大多数LCD驱动IC都有硬件复位引脚(RESX之类),低电平有效。主控需要在开机后给这个引脚一个明确的复位脉冲,时序一般是:拉低复位引脚,等待至少10到20毫秒(有些IC要求更长),然后拉高,再等待50到120毫秒,让内部模拟电路稳定下来。这个延时不能省,尤其是电源刚上电的时候,屏驱内部的升压电路、振荡器都需要时间稳定。如果上电后立刻写寄存器,很可能SCL时钟来了,IC内部还没进入就绪状态,指令被吃掉,屏幕自然不亮。
我见过一个案例,A同学做智能手表,屏幕99%是好的,就是不亮,查了半天发现是把复位引脚悬空了,以为用软件复位指令就行。实际上硬件复位是驱动IC从未知状态恢复到默认状态的安全通道,特别是电源扰动或者程序跑飞之后,硬件复位能让你快速恢复显示,这个引脚一定要接好。
3.2 寄存器初始化序列:从写命令到出画面的顺序逻辑
初始化序列看着是一长串魔法数字,但其实是有一套逻辑顺序的。以ST7789V为例,典型的初始化流程大致是:
- 软件复位(0x01);
- 关闭显示(0x28);
- 设置像素格式(0x3A,一般设为0x05即RGB565,或0x06即RGB666);
- 设置内存访问控制(0x36,配置扫描方向、RGB/BGR顺序、行/列交换等);
- 设置显示窗口(CASET/RASET,或者0x2A/0x2B);
- 调整伽马曲线、电源控制(不同的IC寄存器差异比较大);
- 退出睡眠模式(0x11),等待延时;
- 开显示(0x29)。
为什么顺序不能乱?因为很多寄存器的生效是有依赖关系的。比如睡眠模式和正常模式是两套不同的电源策略,如果你在睡眠状态下写电源控制寄存器,某些IC会直接忽略或者要等退出睡眠后才生效。又比如设置显示窗口必须在开启显示之前,否则你设定的窗口可能被内部彻底清空重来。所以网上那些初始化序列,你最好先搞懂每一步在做什么,再根据自己的屏幕去调整,而不是无脑复制。
3.3 像素格式、扫描方向与颜色顺序的配置细节
像素格式这个坑,我栽过不止一次。比如你MCU侧发送的数据是RGB565(两个字节一个像素),但屏驱还处于RGB444或者RGB666的默认状态,那显示出来的颜色就完全不对,偏色严重、画面发紫发绿,甚至出现横条纹。解决办法是把0x3A寄存器设置成和你发送数据格式一致的模式。多数情况下设成RGB565就够用了,除非你的主控侧硬件输出就是18位色。
扫描方向和颜色顺序(BGR/ RGB)是最容易让人抓狂的。0x36寄存器里有MY、MX、MV这几个位控制扫描方向,还有BGR位控制颜色通道顺序。当你的屏是竖着显示还是横着显示、是从左到右还是从上到下,这些位决定你的图片是不是倒着的、镜像的。我做项目时经常遇到一种情况:初始化代码拷贝自别人的工程,屏幕尺寸一样,但显示的内容就是翻转了90度。原因就是别人用的屏幕面板和你的内部走线方向不一致,初始化里0x36寄存器配置不同。这时候按需修改MY/MX/MV三个bit就好,手动查寄存器表挨个试,一般5分钟能搞定。
还有一点,像素格式和扫描方向虽然看起来是小配置,但一旦感觉到了,显示任何图片和文字都会歪。所以我的习惯是在调具体功能之前,先把一整屏的纯红色、纯绿色、纯蓝色刷一遍,确认颜色和方向都对,再往下做别的。这招能帮你把“颜色问题”和“逻辑问题”分开,排查效率高很多。
4. 读写驱动的实现细节:从寄存器到出画面的代码逻辑
4.1 SPI写命令和写数据的时序处理
代码层面,驱动LCD最核心的两个操作就是“写命令”和“写数据”。SPI接口下,这两者靠DC引脚(数据/命令选择)来区分。
如果DC引脚为低,SPI线上传输的是命令字节;DC为高,传输的是数据。具体实现上,我习惯封装两个底层函数:一个写命令,一个写数据。代码示例如下:
// 假设使用硬件SPI,且已经初始化好 #define LCD_CS_ENABLE() GPIO_ResetBits(LCD_CS_PORT, LCD_CS_PIN) #define LCD_CS_DISABLE() GPIO_SetBits(LCD_CS_PORT, LCD_CS_PIN) #define LCD_DC_CMD() GPIO_ResetBits(LCD_DC_PORT, LCD_DC_PIN) #define LCD_DC_DATA() GPIO_SetBits(LCD_DC_PORT, LCD_DC_PIN) void lcd_write_cmd(uint8_t cmd) { LCD_CS_ENABLE(); LCD_DC_CMD(); spi_transmit_byte(cmd); LCD_CS_DISABLE(); } void lcd_write_data(uint8_t data) { LCD_CS_ENABLE(); LCD_DC_DATA(); spi_transmit_byte(data); LCD_CS_DISABLE(); }这里有个细节,很多人的驱动发数据是逐字节发送的,效率很低。因为SPI每次传输之间有CS片选切换和函数调用开销,一帧240x320的RGB565数据是153600字节,逐字节发送的话光函数调用开销就非常可观了。更好的做法是连续传输,把整块数据准备好,然后一次DMA发出去:
void lcd_write_data_buffer(uint8_t *buf, uint32_t len) { LCD_CS_ENABLE(); LCD_DC_DATA(); spi_dma_transmit(buf, len); // 阻塞等待DMA完成或使用中断标志 LCD_CS_DISABLE(); }注意,如果MCU的SPI发送是先写发送数据寄存器再等标志位,那中间会有大量空等的时间。用DMA配合乒乓缓冲区,可以把刷新帧率提升一半以上,这一点在稍后帧率计算里也有体现。
4.2 绘制一帧画面:缓冲区、分块刷新与帧率计算
理解了写命令和写数据之后,画一帧图就很简单:先设置窗口(CASET/RASET),然后把像素数据按窗口大小连续送到屏驱的GRAM里。
但是有一个工程上的问题:你很多时候并没有一整个完整的帧缓冲区,因为240x320的RGB565帧缓冲要150KB,普通单片机根本放不下。所以我实际项目里用的是分块刷新。先计算出屏幕要显示的区域,比如一个进度条增长、一个波形滚动,只把变化的那一小块窗口重新写入GRAM。
分块刷新有两个好处。第一是内存占用低,不需要完整帧缓冲。第二是刷新局部画面时,主控只需要发送变化区域的数据,速度反而比全屏刷新快很多。
如果非要做全屏动画,那就得用行缓冲。比如刷一行数据到RAM,然后SPI发出去,再刷下一行,代替完整帧缓冲。这个方案的帧率上限主要取决于SPI速率、行消隐期间的等待以及主控搬运开销。
我简单算一笔账:240x320、RGB565,一帧就是 240x320x2 = 153600 字节。如果SPI时钟是40MHz,理论传输一帧至少需要 153600x8/40MHz ≈ 30.72毫秒。加上命令开销和延时,实际全屏刷新大概在35到45毫秒左右,也就是22到28帧。看起来还行,但这些是用DMA且SPI跑满的情况下才能达到的。如果你用手动逐字节发送,SPI利用率可能只有50%都不到,帧率直接掉到10帧出头。
4.3 简单图形加速:区域填充、矩形绘制与屏幕旋转
驱动LCD,除了往屏幕写图片,更常用的是画矩形、填充区域、画线、画字符。虽然很多GUI库(比如LVGL、u8g2)帮你做了这些事,但理解底层实现总没坏处。
区域填充是基础。设置好窗口,然后循环发送同一种颜色的数据即可。举个例子,清屏就是设置整个屏幕窗口,然后发送 240x320 个RGB565像素。如果每次都逐像素算颜色再发,会非常慢。聪明的做法是先把一个16位颜色值复制到一块小缓冲区,比如一行或者64字节,然后把这缓冲区循环发多次,从而避免反复计算同一个颜色值。
矩形绘制就是多画几条线,每条线其实就是一行或一列的填充,没什么特别。屏幕旋转则借助我前面说的0x36寄存器的MY/MX/MV位。如果你要在横屏和竖屏之间切换,改这几个bit比在软件层把所有坐标转换一遍要快得多。但要注意,旋转后窗口设置的坐标范围也要跟着换,不然就会花屏。
软件层的坐标转换也绕不开,很多主控需要把触摸坐标、显示坐标和屏幕物理方向对齐。这里我的建议是:底层只做“写某个矩形区域”的原子操作,上层所有坐标变换都放到自己的图形层去处理,不要混在一起,否则后期调试旋转问题会想砸电脑。
5. 调试手段与经典问题排查
5.1 调试三件套:示波器、逻辑分析仪、串口回读
屏幕驱动的问题,很多时候肉眼看不出来。白屏、花屏、颜色偏,各有各的原因,没有工具就是瞎猜。我调试LCD屏驱用得最多的是三样东西:示波器、逻辑分析仪和串口打印。
示波器主要用于看电源和时钟信号质量。上电瞬间,电源有没有跌落?SCLK频率实际是多少?波形是不是明显失真?这些信息对排查“屏幕偶发白屏”“高温下闪烁”这类疑难杂症特别有用。讲个实际例子,某项目在低温下屏幕显示不稳定,示波器一测,发现是背光供电电路的启动电流不足,导致电压跌落,换了个大电容和效率更高的升压芯片就解决了。
逻辑分析仪用来抓SPI时序。把CS、SCLK、MOSI、DC这几根线都接到逻辑分析仪上,然后跑初始化代码,你可以直观看到命令是否发送、数据长度是否正确、时序是否有毛刺。很多花屏问题,其实就是初始化序列在某个命令上多了一个字节或者少了一个字节,逻辑分析仪上一眼就能看出来。注意逻辑分析仪的采样率至少要是SPI时钟的4到5倍,不然波形会失真。
最后是串口回读。很多SPI屏驱其实支持读寄存器,只是很多模块没有把MISO线引出来。如果你的屏幕模组刚好带MISO,那一定要用起来。读一读0x04(读取当前显示状态)、0x09(读设备ID)之类的寄存器,能快速确认IC是不是正常工作、软件复位是不是成功。不能读也没关系,用示波器或分析仪看波形也能间接判断。
5.2 白屏、花屏、偏色、残影的排查思路
我把这几年积累的屏幕驱动问题排查经验整理成几个典型场景。
白屏是最常见的,但原因也最多。优先检查硬件复位是否正确,再查电源电压是否达到IC要求,接着看初始化序列有没有执行到开显示指令(0x29)那一步,最后检查SPI速率是不是太高导致数据错误。有一次我遇到一个白屏问题,查到最后发现是SPI初始化的时候,把CPOL和CPHA配置反了。SPI模式不对,数据在错误的时钟边沿被采样,驱动IC收到的命令全是乱的,自然也白屏。
花屏多半是因为数据传输长度和窗口设置不匹配。比如你设置窗口是100x100,但实际发了100x200的数据量,GRAM写指针就会越界,显示内容出现错位和花屏。还有一种是扫描方向配置和面板物理方向不一致,显示画面整体偏移、折叠,像拼图错位。
偏色一般是像素格式配置错误,或者颜色排序BGR/RGB反了。快速验证方法就是刷纯色,比如发红色数据0xF800,如果屏幕上显示的是蓝色,那就是BGR位配反了。残影和烧屏更偏向液晶面板本身,常见于长时间显示同一画面的场景,跟驱动IC关系不大,可以考虑开启屏幕自带的充电共享功能或者调整VCOM电压。
5.3 常用问题速查表
| 现象 | 常见原因 | 排查方向 |
|---|---|---|
| 白屏 | 复位时序不对 / SPI模式错误 / 初始化未执行完成 | 检查复位引脚、SPI的CPOL/CPHA、确认开显示指令已执行 |
| 花屏 | 窗口设置和数据长度不匹配 / 扫描方向不对 | 核对CASET/RASET窗口、检查0x36寄存器配置 |
| 偏色 | 像素格式错误 / RGB/BGR顺序反了 | 刷纯色测试,检查0x3A和0x36的BGR位 |
| 横条纹闪烁 | 电源纹波大 / 背光驱动不够稳定 | 示波器测电源和背光,检查电容布局 |
| 局部残影 | 长时间显示固定图案 / VCOM设置不佳 | 开启充电共享、调整VCOM或伽马设置 |
| 刷新缓慢 | SPI速率低 / 未用DMA / 逐字节发送 | 提高SPI时钟,改用DMA发送缓冲区 |
这里再强调一个细节:很多驱动IC在初始化之后需要一个较长的延时,尤其是退出睡眠模式(0x11)到开显示(0x29)之间,必须等待至少120毫秒。如果这段延时不够,屏幕可能不会稳定工作,表现为第一次上电白屏、断电重启又正常。这种幽灵Bug特别磨人,我把这个延时封在初始化函数里,同时通过日志打印每一步执行状态,避免以后再踩。
6. 写在最后:一些个人体会
做LCD屏驱这么长时间,我最大的体会是:屏幕驱动不在于代码有多花哨,而在于对时序和寄存器的敬畏。每一颗驱动IC都有自己的脾气,初始化顺序、延时要求、电源特性都需要你去看数据手册,而不是网上抄一份代码就万事大吉。
最后分享一个很实用的小技巧。如果你手头有多块不同型号的屏幕,可以做一个通用的适配层,把“写命令”“写数据”“设置窗口”“清屏”“填充区域”这些基础函数抽象出来,不同IC只是底层寄存器配置不同,上层API保持一致。这样以后换屏、升级方案的时候,你的应用层代码一行都不用改,改的只是几个初始化配置和底层函数。这个架构看起来没什么技术含量,但做产品迭代的时候,能帮你省下大量重复劳动。
我刚开始玩单片机那阵子也是天天跟白屏较劲,后来慢慢把电源、复位、SPI模式、像素格式这四个问题理顺之后,屏幕驱动对于我来说就再也不是玄学了。希望这篇文章能让你少走一些弯路。