十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ST7789S驱动IC详解:SPI接口初始化与寄存器配置实战

ST7789S驱动IC详解:SPI接口初始化与寄存器配置实战 简介面向嵌入式开发者和物联网硬件爱好者围绕ST7789S显示控制器的驱动资源提供了基于SPI接口的彩色TFT屏幕驱动与底层控制方案适用于240×240、135×240等小型显示面板的初始化、数据传输和图像刷新读者需具备基础单片机与SPI通信知识尤其适合在资源受限的设备上快速搭建显示系统。压缩包共4个文件以3份PDF技术文档和1个C语言源文件为主分别覆盖芯片规格、模组接口定义和实际驱动代码包体仅3MB结构紧凑便于查阅目前已有2056人学习。透过PDF中的时序参数与初始化序列结合C语言驱动源码中的寄存器配置、帧缓冲和绘图函数可以理解从设置显示区域、翻转扫描方向到开关显示、调整背光与休眠模式的完整控制流程文档还针对黑屏、花屏、颜色错误等显示异常给出了检查硬件连接、初始化序列和数据传输过程的排查方向这些内容对将驱动移植到FreeRTOS、Linux或裸机环境以及优化刷新率和功耗都有直接的参考价值。 ST7789S是一款在小尺寸TFT-LCD屏幕上极其常见的驱动IC很多0.96寸、1.3寸、1.54寸的IPS屏幕模组都在用这颗芯片。玩单片机的人大概率都见过它——淘宝上那些几块钱一片的彩色屏幕拆开驱动IC一看型号十有八九就是ST7789S或者它的兄弟ST7789V。这篇文章我就围绕这颗驱动IC的驱动程序把初始化流程、关键寄存器配置、时序问题、常见坑一次讲清楚。先说明一下ST7789S和ST7789V在指令集上高度兼容但寄存器默认值和部分时序参数有细微差异如果你的屏幕在某些代码下显示异常可以先检查驱动IC的准确丝印型号再对照对应芯片手册修正参数。这篇文章主要基于ST7789S但里面的思路对ST7789V、ILI9341甚至GC9A01都有参考价值。1. 驱动方案选型为什么绝大多数人选择SPI接口ST7789S这颗IC支持的接口方式不少——SPI、并行8080接口、RGB接口都有。但在实际项目里大家几乎清一色选SPI原因很直接省引脚、接线简单、单片机端实现容易。以最常见的4线SPI为例屏幕模组上只需要SCLK、MOSI、CS、DC四根信号线加上RST和BLK控制线总共六根线就能点亮。如果是并行8080接口至少需要D0到D7八根数据线再加上RD、WR、RS、CS等控制线轻轻松松占用十几个GPIO。对于ESP32、STM32这种IO资源不算特别宽裕的芯片来说SPI几乎是唯一合理的选择。ST7789S的SPI模式有两种3线SPI不带DC线靠命令头区分数据和命令和4线SPI带DC线。4线SPI更通用代码写起来也更直观——DC拉低写命令DC拉高写数据。我个人的习惯是优先用4线SPI虽然多占一个引脚但调试时省心太多。另外一个需要考虑的点是SPI时钟频率。ST7789S的手册上写的是SPI时钟最高可以达到几十MHz但实际使用中并不是时钟越高越好。很多玩ESP32的朋友把SPI时钟调到80MHz结果屏幕出现雪花、条纹或者颜色错乱其实就是时序裕量不够。常规做法是先保守一点用20MHz到40MHz跑通然后再根据实际情况逐步提高。如果是STM32F103那种72MHz主频的芯片SPI时钟分频后跑到18MHz或36MHz显示效果已经足够流畅。2. 初始化序列详解那些必须配置的关键寄存器ST7789S上电之后不会自动进入可用的显示状态必须由主机发送一串初始化命令屏幕才能正常显示。这一段初始化序列是驱动代码里最核心的部分也是大家最容易出问题的地方。从工程实践的角度来说初始化序列可以拆成四个阶段软件复位与睡眠唤醒、显示参数配置、内存访问模式设置、清屏并开显示。下面我逐个阶段拆开讲并给出每一段的实际代码示例。2.1 上电时序与软件复位ST7789S对上电时序是有要求的需要注意屏幕VDD和背光之间先后顺序。这里用最常见的4线SPI模式、RGB565颜色格式、240x240分辨率为例// 引脚初始化 void lcd_pins_init(void) { gpio_init(LCD_RST_PIN, GPIO_MODE_OUTPUT); gpio_init(LCD_DC_PIN, GPIO_MODE_OUTPUT); gpio_init(LCD_CS_PIN, GPIO_MODE_OUTPUT); gpio_init(LCD_BLK_PIN, GPIO_MODE_OUTPUT); gpio_set_level(LCD_CS_PIN, 1); // CS默认拉高释放总线 gpio_set_level(LCD_BLK_PIN, 0); // 背光先关闭 } // 复位序列 void lcd_reset(void) { gpio_set_level(LCD_RST_PIN, 0); vTaskDelay(pdMS_TO_TICKS(50)); // 拉低至少10ms gpio_set_level(LCD_RST_PIN, 1); vTaskDelay(pdMS_TO_TICKS(120)); // 等待内部模拟电路稳定 }上电后首先拉低RST至少10ms然后拉高并等待120ms左右这是让芯片内部的DCDC和模拟电路稳定下来。很多人忽略了这个等待时间复位后立刻发命令导致第一条命令丢失屏幕永远白屏。我调试时遇到过好几次类似的情况最后发现都是这个延时不够导致的。复位完成后紧接着发送软件复位命令0x01再等待150ms左右。然后发送睡眠唤醒命令0x11再次等待120ms。注意0x11是退出睡眠模式这之后屏幕并没有立即点亮还需要继续配置寄存器。2.2 CASET/RASET与像素格式配置接下来是设置显示区域和像素格式。这里有一个非常关键的寄存器组合CASET0x2A列地址设置、RASET0x2B行地址设置、RAMWR0x2C写显存。// 设置显示窗口全屏240x240 void lcd_set_window(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { lcd_write_cmd(0x2A); // CASET lcd_write_data(x0 8); lcd_write_data(x0 0xFF); lcd_write_data(x1 8); lcd_write_data(x1 0xFF); lcd_write_cmd(0x2B); // RASET lcd_write_data(y0 8); lcd_write_data(y0 0xFF); lcd_write_data(y1 8); lcd_write_data(y1 0xFF); lcd_write_cmd(0x2C); // 准备写显存 }关于0x36MADCTL内存访问控制这个寄存器值得单独拎出来说。它控制扫描方向、颜色顺序RGB还是BGR和行列交换。很多屏幕模组出厂时物理接线方向不同同一个初始化代码在这个屏上正常换个屏就镜像了就是MADCTL没有调对。// MADCTL常用值 #define MADCTL_MY 0x80 // 行扫描方向 #define MADCTL_MX 0x40 // 列扫描方向 #define MADCTL_MV 0x20 // 行列交换 #define MADCTL_BGR 0x08 // BGR颜色顺序实测经验是绝大多数的ST7789S模组默认需要设置BGR位否则红蓝互换。RGB和BGR这个问题几乎每个新手都会遇到屏幕上显示的颜色明显不对红色变成了蓝色那就是颜色顺序没配对。至于扫描方向四个组合0x00、0xC0、0xA0、0x60可以直接通过枚举测试的方式来确定配合显示一个带方向文字的测试图能很快找到正确的值。像素格式寄存器是0x3AST7789S支持12位、16位和18位RGB。我们最常用的是0x05RGB565即16位色。因为RGB565每个像素两个字节颜色信息足够丰富又比18位模式省三分之一的数据量是大多数MCU项目的首选。2.3 电源控制与显示开光这一阶段的寄存器设置比较讲究不同厂商的模组给出的推荐值可能不一样。以我自己手头这块1.3寸240x240分辨率的ST7789S模组为例初始化序列中与电源相关的寄存器需要这样设置lcd_write_cmd(0x36); // MADCTL 内存访问控制 lcd_write_data(0x00); lcd_write_cmd(0x3A); // COLMOD 像素格式 lcd_write_data(0x05); // RGB565 lcd_write_cmd(0xB2); // PORCTRL 孔隙控制 lcd_write_data(0x0C); lcd_write_data(0x0C); lcd_write_data(0x00); lcd_write_data(0x33); lcd_write_data(0x33); lcd_write_cmd(0xB7); // GCTRL 门控 lcd_write_data(0x35); lcd_write_cmd(0xBB); // VCOMS 电压设置 lcd_write_data(0x19); lcd_write_cmd(0xC0); // LCMCTRL 电源控制 lcd_write_data(0x2C); lcd_write_cmd(0xC2); // VDVVRHEN 设置 lcd_write_data(0x01); lcd_write_cmd(0xC3); // VRHS 设置 lcd_write_data(0x12); lcd_write_cmd(0xC4); // VDVS 设置 lcd_write_data(0x20); lcd_write_cmd(0xC6); // FRCTRL 帧率控制 lcd_write_data(0x0F); lcd_write_cmd(0xD0); // PWCTRL1 电源控制1 lcd_write_data(0xA4); lcd_write_data(0xA1); lcd_write_cmd(0xE0); // PVGAMCTRL 正电压Gamma校正 lcd_write_data(0xD0); lcd_write_data(0x04); lcd_write_data(0x0D); lcd_write_data(0x11); lcd_write_data(0x13); lcd_write_data(0x2B); lcd_write_data(0x3F); lcd_write_data(0x54); lcd_write_data(0x4C); lcd_write_data(0x18); lcd_write_data(0x0D); lcd_write_data(0x0B); lcd_write_data(0x1F); lcd_write_data(0x23); lcd_write_cmd(0xE1); // NVGAMCTRL 负电压Gamma校正 lcd_write_data(0xD0); lcd_write_data(0x04); lcd_write_data(0x0C); lcd_write_data(0x11); lcd_write_data(0x13); lcd_write_data(0x2C); lcd_write_data(0x3F); lcd_write_data(0x44); lcd_write_data(0x51); lcd_write_data(0x2F); lcd_write_data(0x1F); lcd_write_data(0x1F); lcd_write_data(0x20); lcd_write_data(0x23); lcd_write_cmd(0x21); // 开启反色显示模式 lcd_write_cmd(0x11); // 退出睡眠 vTaskDelay(pdMS_TO_TICKS(120)); lcd_write_cmd(0x29); // 开启显示 vTaskDelay(pdMS_TO_TICKS(50));这里有几个地方需要特别说明。第一0x21INVON反色命令。为什么大部分ST7789S模组的初始化都要加这一条因为ST7789S的TFT面板默认是常白模式如果不取反显示出来的效果是白底黑字——字是黑的背景是白的。加上0x21之后才变成正常的黑底白字。这个命令是官方驱动里习惯性带的但如果没有加屏幕也能显示只是亮暗反转问题不太明显。如果发现屏幕出现显示反色问题优先查这一条有没有执行。第二Gamma寄存器组0xE0和0xE1这两组共28个字节的值是模组厂针对自家液晶面板调好的出厂参数。不同厂商的屏幕虽然都是ST7789SGamma曲线不一定完全一样。强烈建议从你的模组卖家那里要一份初始化代码优先使用模组厂提供的参数不要照抄别人的。这组参数直接决定了屏幕的颜色还原、对比度和暗部细节。第三帧率寄存器0xC6设置为0x0F对应60Hz左右的刷新率。这个值不是固定的有些模组设置0x01帧率就掉到30Hz左右快速滚屏时能明显感觉不流畅。如果画面流畅度有问题优先检查这个寄存器。2.4 清屏操作与显存写入初始化完成后显存里是随机数据所以需要先清屏。ST7789S没有独立的清屏命令只能往整个显存区域写一遍全零或全F的数据。240x240分辨率、RGB565格式全屏数据量是2402402115200字节。void lcd_fill(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color) { lcd_set_window(x0, y0, x1, y1); uint32_t pixels (x1 - x0 1) * (y1 - y0 1); for (uint32_t i 0; i pixels; i) { lcd_write_data(color 8); lcd_write_data(color 0xFF); } } // 清屏为白色 lcd_fill(0, 0, 239, 239, 0xFFFF);注意我在这里把颜色高低字节手动拆开送了。ST7789S在RGB565格式下期望的数据顺序是高字节在前、低字节在后。如果你用DMA或硬件SPI直接发送一个16位数组还需要确认数组在内存中的字节序。在Cortex-M上一个uint16_t数组在内存里实际是按小端存储的直接发送就会出现颜色错乱的问题。常用的处理办法是把颜色值用__REV16字节翻转或者在写入前统一转成大端字节序的数组。3. 核心驱动函数的设计从单像素到DMA加速3.1 底层读写原语无论上层怎么封装驱动最终的落点都在两个函数上写命令和写数据。这两个函数的实现质量直接影响整个屏幕的显示性能。void lcd_write_cmd(uint8_t cmd) { gpio_set_level(LCD_DC_PIN, 0); // DC低电平表示命令 gpio_set_level(LCD_CS_PIN, 0); spi_transfer_byte(cmd); gpio_set_level(LCD_CS_PIN, 1); } void lcd_write_data(uint8_t data) { gpio_set_level(LCD_DC_PIN, 1); // DC高电平表示数据 gpio_set_level(LCD_CS_PIN, 0); spi_transfer_byte(data); gpio_set_level(LCD_CS_PIN, 1); }如果一次只传一个字节CS引脚可以每发送一个字节都拉高再拉低。但在填充大区域时这种方式效率太低。更合理的做法是只有命令阶段的CS需要单独控制数据阶段保持CS拉低连续发送整块数据。这也是一些驱动代码里写显存的函数性能差距巨大的原因。3.2 矩形填充与图片显示矩形填充是最常用的显示原语几乎所有UI渲染都建立在它之上。填充函数的优化重点有两个一是尽量减少命令发送次数二是尽量利用硬件的连续传输能力。比如我要在屏幕上画一个100x100的红色矩形最高效的做法是先发一次CASET和RASET命令锁定区域然后连续发送100100220000字节的数据。中间不需要再重复任何命令。前面给出的lcd_fill函数已经体现了这个思路实际项目中还可以进一步优化比如在循环里把颜色值预先拼成两个字节减少每次的移位操作。显示图片的思路也一样。一张240x240的RGB565图片图片数据是一个const uint8_t数组直接用SPI DMA发送给屏幕即可。ESP32上典型代码如下void lcd_show_image(const uint8_t* img) { lcd_set_window(0, 0, 239, 239); gpio_set_level(LCD_DC_PIN, 1); gpio_set_level(LCD_CS_PIN, 0); spi_transfer_dma(img, 240 * 240 * 2); gpio_set_level(LCD_CS_PIN, 1); }这个函数在40MHz SPI时钟下全屏图片刷新大约需要2402402*8/4000000023ms实际加上命令发送和DMA启动开销大约30ms以内可以完成一帧差不多33fps。这个帧率对大部分嵌入式GUI场景来说已经够用了。3.3 滚动显示与区域刷新ST7789S支持垂直滚动功能对于做长列表滚动或者文字跑马灯效果特别有用。滚动功能需要配置三个寄存器0x33VSCRDEF垂直滚动定义、0x37VSCRSADD滚动起始地址、0x44SETTEARSCAN设置Tear扫描线。// 设置滚动区域整个屏幕顶部固定为0滚动区域高度240 void lcd_scroll_config(uint16_t top_fixed, uint16_t scroll_area_height) { lcd_write_cmd(0x33); lcd_write_data(top_fixed 8); lcd_write_data(top_fixed 0xFF); lcd_write_data(scroll_area_height 8); lcd_write_data(scroll_area_height 0xFF); lcd_write_data(0x00); // 底部固定区域为0 } // 设置滚动起始地址 void lcd_scroll_start(uint16_t line) { lcd_write_cmd(0x37); lcd_write_data(line 8); lcd_write_data(line 0xFF); }垂直滚动比逐像素刷新显存高效得多因为它只改变显示RAM的扫描起点不需要重写显存数据。对于类似股票行情列表这类需要平滑滚动的场景这是最佳方案。需要注意的是滚动起点变化的单位是像素行每次调整的步长决定了滚动速度。通过定时器控制起点地址递增的速率就能实现任意速度的平滑滚动。3.4 硬件SPI和DMA的事务处理陷阱用硬件SPI加DMA时有一个很隐蔽的坑DMA传输完成后SPI外设的TXFIFO可能还有残存数据没完全发出去。如果在DMA完成中断里立刻拉起CS或者切换DC最后几个字节可能被截断导致屏幕尾部出现一条颜色残缺的直线。规避的办法有两个要么在DMA完成中断里加一个小的忙等待等SPI外设的busy标志位清除要么在发送完大量数据后习惯性地加一个极短的延时再操作CS。第二种方法简单粗暴实测下来也很稳妥。另外还要注意SPI模式的配置。ST7789S要求SPI mode 0CPOL0CPHA0或mode 3CPOL1CPHA1都能正常工作但我强烈建议统一用mode 0。因为mode 0在时钟空闲时是低电平SCLK默认态比较安静对信号完整性有好处。更重要的是在部分MCU上SPI模式寄存器配置错误会导致数据移位、颜色整体偏色排查起来比想象中麻烦。4. 常见问题与排查技巧实录驱动ST7789S这几年我在各个社区和实际项目里见过不少问题很多问题反复出现。这里整理一份速查表大家遇到问题时可以直接对照排查。现象可能原因排查方法屏幕完全不亮RST时序不对、供电不足、初始化序列缺失检查复位延时是否足够用万用表确认VCC电压在2.8V-3.3V示波器抓RST波形白屏但有背光初始化序列未完成或SPI通信异常确认0x11和0x29命令是否发送成功检查DC线是否接对降低SPI时钟再试颜色反相黑白颠倒)缺少0x21反色命令在初始化序列中加入0x21红蓝颜色互换MADCTL寄存器缺少BGR位把0x36命令的参数加上0x08显示有横条纹或闪烁帧率设置过高或供电纹波大0xC6寄存器降低数值试一下屏幕电源加10uF去耦电容画面出现斜纹或雪花SPI时序裕量不足降低SPI时钟频率检查杜邦线是否过长或接触不良屏幕左右镜像或上下颠倒MADCTL扫描方向不对参考2.2小节枚举MADCTL参数测试局部刷新后残影写入窗口CASET/RASET不匹配检查每次刷新前lcd_set_window设置的范围是否正确4.1 背光亮了但屏幕不显示的排查思路这是最常见的问题而且绝大多数情况出在初始化时序上。首先用逻辑分析仪或者示波器确认RST引脚确实有先低后高的脉冲且高电平持续时间足够。然后用SPI数据分析仪抓取MOSI上的数据对照ST7789S手册逐条检查命令是否正确发送。一个容易忽略的细节是DC引脚的电平切换时机。DC信号必须要在SCLK第一个边沿之前稳定下来如果DC和SCLK之间的建立时间不够屏幕会把命令当成数据或者把数据当成命令。早期我为了省事拿了一个普通的GPIO口模拟DC信号结果由于这路GPIO的翻转速度跟不上SPI时钟经常出现数据错位。后来换成硬件SPI配合在命令/数据切换前加一个小延时问题就消失了。4.2 显示颜色偏色问题偏色问题分两类。一类是红蓝颠倒上面已经说过是MADCTL的BGR位没设置。另一类是整体颜色发暗、对比度低、暗部细节丢失这类问题几乎都是Gamma寄存器参数不对。Gamma寄存器的值跟屏幕玻璃面板的物理特性强相关不同厂商的模组差异很大。我见过最快的一个案例是用了ST7789V的Gamma参数来驱动ST7789S的屏幕结果整个画面发灰白色不够亮黑色不够深。后来从模组厂要到对应的参数替换后效果立马正常。所以我一直建议如果你手头的初始化代码是在网上抄的而且屏幕显示效果隐隐觉得不对先去问卖家要他们出厂测试的初始化代码。另外还有一个小坑是背光PWM频率过低导致的显示闪烁。很多人会把屏幕背光接到PWM引脚做亮度调节如果PWM频率只有几百Hz在摄像头拍摄或者人眼余光观察时会看到明显的滚动条纹。建议背光PWM频率至少设置在1kHz以上或者干脆不用PWM直接拉高让背光全亮。4.3 触摸屏和屏幕共用SPI总线的注意事项如果你的项目里屏幕和触摸屏芯片比如常见的CST816T、GT911共用一条SPI总线需要特别留意片选信号的时序关系。ST7789S的MISO引脚在有些模组上是悬空的有些则被复用为其他功能。多个设备共用总线时要确保CS信号正确切换否则可能出现屏幕上突然出现一条不正常的色带。更稳妥的做法是把屏幕的CS和触摸屏的CS分别连接到两个独立的GPIO操作完一个设备后延时1ms左右再操作另一个设备给设备内部状态机一个稳定的过渡时间。这种方法虽然损失了一点点性能但稳定性大幅提高。5. 性能优化实践让ST7789S跑得更快前面已经提到过SPI时钟、DMA和缓存优化这里再补充几个进阶的操作。5.1 局部刷新降低无效数据量240x240全屏刷新一帧约需115200字节数据。在40MHz时钟下理论上传输时间约23ms。如果只做UI界面中某个进度条或数值的局部更新完全没有必要刷新整个屏幕。通过CASET/RASET把更新范围限定在变化区域可以大幅减少数据量。类似进度条增量更新这种场景刷新时间可以压缩到1ms以内。实现局部刷新时注意一下ST7789S写入窗口后显存指针会自动在窗口内按行递增写完最后一行后指针回到窗口左上角。如果写入的数据量大于窗口容量超出部分会写入相邻区域造成视觉上的花屏。所以每次刷新前都要重新设置窗口即使窗口跟上次完全一样也建议重新下发一遍CASET/RASET命令保证状态可靠。5.2 帧缓冲与双缓冲策略对于大多数MCU项目显存完全放在屏幕内部MCU只需要在需要时写入更改的片段。但如果你的应用涉及复杂的GUI渲染比如多窗口叠加、大量图形元素可以考虑在MCU侧开辟一块帧缓冲先在内存里完成所有渲染再把帧缓冲一次性刷入屏幕。这种方式的好处是渲染和刷新可以异步进行不会出现屏幕上一半是新画面一半是旧画面的撕裂感。缺点是RAM占用大。240x240x2字节115200字节对ESP32来说问题不大但对RAM只有20KB的STM32F103来说就非常紧张了。这种情况下可以退而求其次使用行缓冲的方案——只缓存几十行渲染完一批刷一批在内存占用和刷新效果之间取一个平衡。5.3 充分利用硬件加速能力ST7789S内部有一些硬件加速功能比如地址自动递增和窗口裁剪。地址自动递增是默认开启的不需要额外配置。窗口裁剪能力则可以用在重复性图案填充上例如画一个棋盘格背景只需要把CASET/RASET设置在一个格子上循环写一小块数据即可MCU端几乎不消耗计算量。硬件滚动功能前面已经提到了这里补充一个使用技巧如果你要做整屏平滑滚动推荐配合0x44命令开启Tearing Effect Line这样可以避免滚动时画面撕裂。当然这个效果依赖面板扫描和MCU写入之间的同步如果时序控制得不好反而会引入额外的闪烁。6. 跨平台移植的关键设计最后聊聊驱动代码的跨平台移植问题。ST7789S的驱动代码结构其实高度一致引脚控制、SPI发送、延时函数、命令序列这四个模块抽离出来后理论上可以很轻松地移植到任何平台。我自己的工程里会把驱动拆成两层。底层是平台适配层只包含SPI初始化、引脚电平控制、延时这三个函数。上层是显示逻辑层负责命令序列、填充算法、图像显示等功能。换平台时只需要重写底层不到一百行代码上层逻辑完全不用动。这种分层结构看起来很基础但确实能省下大量重复造轮子的时间。尤其是当你从STM32换到ESP32、从ESP32换到树莓派Pico时会发现所有LCD屏幕驱动代码的本质都一样——无非就是拉引脚、发SPI、按寄存器手册配置。抓牢这个本质任何屏幕驱动在你手里都不会是难事。本文还有配套的精品资源点击获取
返回列表