ARTICLE DETAIL

资讯详情

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

深入ST7735S:从寄存器配置到TFT驱动手写实战

深入ST7735S:从寄存器配置到TFT驱动手写实战 做嵌入式这几年我见过太多人卡在“屏幕点亮”这一关。尤其是一块1.8寸TFT彩屏芯片标着ST7735S128x160分辨率四个IO加几根杜邦线按理说十分钟就能跑起来但很多人第一步就栽了。某天早上换了一个引脚屏幕白得比A4纸还干净某次把屏幕转了个方向图像直接斜着飞出画面更气人的是同一段初始化代码淘宝店里能用自己刚焊好的板子上就是花屏。问题出在哪十有八九是你只复制了代码没复制代码背后的逻辑。这篇文章不打算让你照抄一段驱动就完事而是从寄存器层面拆一遍ST7735S的驱动过程。你会看到一条命令一条命令是怎么发出去的像素是怎么落到GRAM里的以及为什么一块屏幕的参数表写成那样。文章里所有的代码都按STM32寄存器版的方式写配套讲解也可以迁移到标准库、HAL库甚至换一颗MCU照样能用。适合正在学STM32、看过“STM32测频法”视频但还没搞懂屏幕的朋友也适合已经被“ST7735S中文数据手册”折磨了两天的人。读完你至少能做到不看别人的源码自己写一份能初始化、能刷屏、能画点画线的TFT驱动并且知道出问题时去哪里排查。1. 为什么我不建议直接复制粘贴ST7735S驱动代码1.1 复制粘贴一时爽排查火葬场“能用就行”这句话在点亮屏幕这件事上特别害人。我见过不少同学从开源项目里抽出一份ST7735S的初始化数组把IO换个引脚、把延时删掉几个屏幕上出了画面就觉得自己已经把屏幕吃透了。结果过两天要做一个小项目想把屏幕横过来放问题就来了横屏后画面只有上半屏有内容下半屏是黑边旋转90度之后颜色正常但整个画面被切成了上下两半刷新画面时屏幕上会残留一条一条的滚动条纹想用DMA刷屏发现DMA发送速度和SPI时钟不匹配画面越刷越糊。这些问题不是初始化数组抄错了而是你没有理解初始化数组到底在配置什么。寄存器是什么你可以把它理解成芯片内部的一排拨码开关。ST7735S作为一个微控制器控制的显示驱动芯片内部有几十个寄存器分别控制帧率、电源电压、扫描方向、像素格式、伽马曲线、窗口范围。你写初始化代码本质上就是在给这排拨码开关设置正确的状态。当你不知道每个开关的作用时代码就是一个黑盒。黑盒出了问题你只能回归测试、乱试参数运气好试出来了运气不好就卡在这里。所以我的建议是先花两个小时把ST7735S的几条核心指令搞明白再去考虑“点亮”这件事。磨刀不误砍柴工。1.2 寄存器操作、标准库、HAL到底该学哪个现在网上STM32教程分成三派寄存器派、标准库派、HAL派。经常有人问我到底是学寄存器还是学标准库这个问题放在驱动ST7735S上答案其实很清楚——不管你的SPI是用寄存器写的还是被HAL封装好的ST7735S和MCU之间的对话永远是“MCU向屏幕芯片写入一串特定含义的字节”。HAL库里封装的是SPI外设的收发流程它不会帮你决定那串字节应该是什么。我写这篇文章选择寄存器版SPI不是为了显得底层而是为了把“SPI外设是如何把数据哄出去的”这件事讲明白。当你用寄存器直接操作CR1、DR这些外设寄存器时你会很清楚何时拉高CS、何时切换DC、何时等待TXE标志位这些节奏正是屏幕驱动里最容易出问题的部分。你用标准库或者HAL库也没关系。重点是你头脑里要有“寄存器版本”的概念SPI是主模式还是从模式、时钟相位极性和极性怎么配、波特率预分频系数是多少、什么时候把数据塞进发送寄存器。这些概念在所有开发库里都存在只是名字叫法不同。等你能用寄存器把SPI发送写出个所以然再回头看HAL的HAL_SPI_Transmit你会觉得它不过是一层薄薄的包装而已。顺便说一句很多人问“Keil5兼容C51和STM32安装是不是要装两遍”其实就是在安装芯片支持包时没搞清目标。不管是C51还是STM32Keil5都是用Pack安装器装上对应厂家的芯片包互不干扰。STM32F103就装Keil.STM32F1xx_DFP装完之后在Device选型里能看到STM32F103C8就说明支持包到位了。1.3 认识你的屏ST7735S的“马甲问题”关于ST7735S有一个很容易踩的坑你买到的1.8寸TFT LCD模块驱动芯片不一定是ST7735S本尊。很多屏幕标着128x160分辨率背面的小玻璃上丝印可能是ST7735S也可能只是兼容IC。更麻烦的是同样是ST7735S不同屏幕模组厂商写进模块的初始化参数也可能不一样因为每种模组的偏置电压、伽马曲线、行列起始位置都有区别。你在淘宝买的低价屏商家一般只给一份初始化示例代码。这份代码多半是从别的项目里抄来的甚至可能和屏幕本身不匹配。点亮以后看似正常一旦做工业级产品就会发现颜色偏、暗部发紫、局部花屏。所以我买屏时一定会做两件事第一让卖家确认驱动IC的完整型号不是“ST7735S兼容”而是具体丝印第二想办法拿到屏幕模组原厂提供的初始化列表。ST7735S数据手册里的初始化列表和屏厂给的模组初始化列表往往是两回事前者面向芯片裸片后者面向模块装配后的实际显示区域。后面讲初始化时序时我会专门解释为什么会差出几行代码。2. 驱动一块TFT屏到底需要哪些底层知识2.1 从一颗像素到一整块屏幕GRAM和像素格式ST7735S内部有一块静态随机存储区叫GRAM全称是Graphic RAM。这块存储区的大小和屏幕分辨率、每个像素占用的位数直接相关。对128x160的屏如果用RGB565格式每像素16位GRAM容量就是128×160×16bit换算一下是40960字节。这个存储区在芯片内部你通过SPI写入的每一个像素数据最终都会落到GRAM的对应地址上再由芯片内部逻辑不断把GRAM内容扫描到液晶面板上。理解GRAM很重要因为屏幕刷新并不是你写一个像素它立即出现在面板上的。你的操作顺序是“选择窗口CASET/RASET→ 写入像素数据RAMWR→ 芯片自动把GRAM数据送显”。也就是说MCU只负责往GRAM里填数据什么时候轮到这行像素被扫描出来由ST7735S自己控制。像素格式直接影响一帧画面的数据量。ST7735S支持RGB444、RGB565、RGB666。RGB444每个像素12位数据可以通过3字节或者整理后的16位形式传输RGB565是16位是高5位红色、中间6位绿色、低5位蓝色RGB666每个像素18位通常以3字节传输每字节只利用高6位。我们最常使用的是RGB565一个像素占2字节128×160全屏刷新要写40960字节如果用SPI时钟10MHz理论刷一帧全屏要33毫秒左右考虑指令开销实际会到40~50毫秒这个速度做普通菜单、仪表盘、波形显示足够做游戏动画会偏慢。RGB565的好处是颜色转换简单且省内存适合MCU。比如你要画一个红色的点不需要调色板直接写0xF800画纯绿色写0x07E0纯蓝色写0x001F。代码里通常用一个宏来处理16位RGB到RGB565的转换#define RGB565(R, G, B) (((R 0xF8) 8) | ((G 0xFC) 3) | ((B 0xF8) 3))这个宏背后是位运算把RGB三通道的高位提取出来拼成一个16位整数。你只要记住ST7735S在RGB565模式下SCL的每个时钟周期移动两个字节先发高字节后发低字节这一点在处理图片数组时特别重要。2.2 SPI、RESET、DC、CS一根线一根线的讲究ST7735S的接口方式有很多种包括SPI、8位并行、16位并行等。1.8寸小屏模块几乎都引出了4线SPI接口SCK、MOSI也叫SDA、CS、DC外加RST和BLK/背光。理解每根线的职责比背接线表更重要。SCK串行时钟由MCU产生控制数据移位的节拍。MOSI主出从入MCU给ST7735S送数据/命令的通道。CS片选信号低电平有效。只有当CS拉低时ST7735S才接收SPI时钟和数据。DC数据/命令选择。DC为低时下一个字节被解释为命令DC为高时下一个字节被解释为数据。RST硬件复位低电平有效。上电后最好拉低至少10微秒再拉高让芯片内部逻辑彻底复位。BLK背光控制有些模块上电默认点亮不接也行。很多人把DC线当成普通的视觉控制线只想着“发命令就拉低发数据就拉高”这没有错但容易忽略一个关键点DC电平变化和被发送字节的时序配合必须在SPI写入之前稳定下来。也就是说先拉低DC再置CS为低再发起SPI传输而不是在SPI传输过程中切换DC。SPI的时钟极性和相位也容易踩坑。ST7735S的数据手册一般会写支持SPI Mode 0和Mode 3。Mode 0表示CPOL0、CPHA0时钟空闲为低数据在第一个跳变沿上升沿被采样在下降沿改变。Mode 3则相反空闲为高数据在下降沿采样。以我的经验用Mode 0最稳妥大部分示例代码也都是Mode 0。如果你把CPOL和CPHA配反了最典型的现象是屏幕偶尔能点亮但显示花屏或者第一个命令正常、后面的数据全错。因为芯片在错误的边沿采样到了不稳定的数据位命令和数据的边界就全乱了。2.3 坐标系的窗户Column/Row Address Set如果你只理解SPI怎么写字节还不够。ST7735S的GRAM地址是线性的但屏幕显示的是一个二维区域芯片提供了CASETColumn Address Set命令0x2A和RASETRow Address Set命令0x2B两个命令来设置写入窗口。可以这样理解GRAM是一整块连续的存储空间屏幕上的每个像素像素对应一个地址。但ST7735S不要求你从地址0开始画到底你可以通过CASET和RASET定义一个矩形窗口之后通过RAMWR命令连续写入的所有像素数据都会自动按窗口从左到右、从上到下填充。这样有三大好处局部刷新时不用清整屏性能提升明显可以方便地实现按区域填充颜色比如画矩形、波形图、数字仪表图像数据写入顺序固定不容易出现“图像左右颠倒”或“上下颠倒”的问题。CASET的参数是4个字节列起始地址高字节、列起始地址低字节、列结束地址高字节、列结束地址低字节。RASET同理只是作用于行。以128x160屏幕为例全屏窗口设置是// CASET: 列地址从0到127 tft_write_cmd(0x2A); tft_write_data(0x00); tft_write_data(0x00); tft_write_data(0x00); tft_write_data(0x7F); // RASET: 行地址从0到159 tft_write_cmd(0x2B); tft_write_data(0x00); tft_write_data(0x00); tft_write_data(0x00); tft_write_data(0x9F);窗口设置完紧接着发RAMWR命令0x2C后面跟的每一个“像素数据”都会按顺序落到窗口覆盖的GRAM区域。如果你窗口设置成了只覆盖一个40x40的方块那么写1600个像素就填满了这个方块多余的像素数据会被芯片丢弃或产生不可预测的写入结果具体行为取决于芯片实现所以写驱动时一定要控制好窗口和写入数据量的匹配。3. ST7735S核心寄存器详解与初始化时序3.1 ST7735S寄存器速查表网上流传的初始化代码里会看到大量0x01、0x11、0x36、0x3A、0x2A、0x2B、0x2C、0x29这样的数字。这些数字是ST7735S的指令码而不是普通的像素数据。初学者如果手里拿着“ST7735S中文数据手册”逐条翻很快就能对上号。我这里把必须掌握的核心指令列成一张速查表建议收藏备用。命令码名称参数数量作用0x01SWRESET0软件复位内部所有寄存器恢复默认值0x11SLPOUT0退出睡眠模式开始内部振荡器0x36MADCTL1设置扫描方向、RGB/BGR顺序0x3ACOLMOD1设置像素格式如RGB565/RGB6660x2ACASET4设置列地址窗口0x2BRASET4设置行地址窗口0x2CRAMWR可变开始写入像素数据到GRAM0x29DISPON0开启显示屏幕开始显示GRAM内容0x20DISPOFF0关闭显示屏幕变白或黑0xB1FRMCTR13设置正常模式帧率0xB2FRMCTR23设置空闲模式帧率0xB3FRMCTR36设置部分模式帧率0xC0PWCTR13设置电源电路控制参数10xC5VMCTR11设置VCOM电压影响对比度0xE0GMCTRP116设置正伽马曲线寄存器组0xE1GMCTRN116设置负伽马曲线寄存器组MADCTL比较特别它的8位参数每一位都有意义。比如0x80是MY位置1表示行扫描方向翻转0x40是MX位置1表示列扫描方向翻转0x20是MV位置1表示行列交换配合MX/MY可以实现横竖屏切换0x08是BGR位置1时芯片按BGR颜色顺序解释数据置0时按RGB顺序解释。这些位对应的效果用一句通俗的话说就是控制“GRAM里地址增加到下一个像素时屏幕上具体往哪个方向跑”。所以当你想要横屏时不能只把屏幕物理旋转90度还必须对应修改MADCTL的参数否则画面会斜着开始。3.2 初始化序列为什么是“玄学”不少教程直接给一长串初始化数组然后说“照抄就行”。这句“照抄就行”坑了很多人。ST7735S的初始化序列不是随便发的它遵循一套固定的内部流程懂了这个流程你才能应对不同模组之间的参数差异。完整流程一般是这样硬件复位SWRESET等待至少120ms发送SLPOUT退出睡眠等待至少120ms配置帧率寄存器FRMCTR1/2/3配置电源控制寄存器PWCTR1~5和VMCTR1配置伽马曲线寄存器GMCTRP1/GMCTRN1设置像素格式COLMOD设置显示方向MADCTL设置窗口CASET/RASET发DISPON开显示最后做一次清屏。为什么需要这么复杂因为ST7735S不仅负责缓存像素数据还负责控制LCD面板的偏置电压、公共电极电压和像素矩阵的开关时序。这些电压参数不对屏幕上就算收到了正确的像素数据颜色也会是“阴阳脸”或者全白。帧率寄存器决定内部振荡器频率和LCD行扫描的时序伽马寄存器决定灰阶电压的分配曲线直接影响亮部和暗部的层次。我见过有人把初始化序列删到只剩几条命令屏幕居然也亮了。这种情况不是他多聪明而是模组默认寄存器值恰好能用。可一旦换一个批次的屏幕同样的精简初始化就会出问题。所以宁可保留完整的初始化序列也不要做“精简优化”。除非你能逐条验证寄存器确实不影响显示效果否则别乱删。还有一点同样是ST7735S淘宝模块和正点原子、微雪这些大厂模块的初始化参数可能有细微差别。最典型的是CASET/RASET的偏移。有些模组玻璃厂在切割时没有从0开始第一列实际对应GRAM地址2那你设置窗口时就要让开始地址偏移。这时候你抄一份针对别的模组的初始化代码画面就会偏一个像素或者整块错位。这个问题的解法我在第5章会专门展开。3.3 命令发送、数据发送与延时设计驱动代码的基础单元就两个写命令、写数据。写命令就是拉低DC然后通过SPI发送一个字节写数据就是拉高DC发送一个或多个字节。有些命令自带参数参数全部用写数据的方式发送。以STM32寄存器版SPI为背景写数据的基本过程如下// STM32F103 SPI1 寄存器版写一个字节 void SPI1_WriteByte(uint8_t dat) { // 等待发送缓冲区为空 while((SPI1-SR SPI_SR_TXE) 0); // 数据写入发送寄存器 SPI1-DR dat; // 等待发送完成 while((SPI1-SR SPI_SR_BSY) 1); }但实际驱动屏幕时我们不能只写一个字节就结束。每个字节前面的CS和DC状态必须正确void tft_write_cmd(uint8_t cmd) { LCD_CS_LOW(); // 片选拉低选中屏幕 LCD_DC_LOW(); // DC拉低表示接下来是命令 SPI1_WriteByte(cmd); LCD_CS_HIGH(); // 片选拉高结束本次操作 } void tft_write_data(uint8_t dat) { LCD_CS_LOW(); LCD_DC_HIGH(); // DC拉高表示接下来是数据 SPI1_WriteByte(dat); LCD_CS_HIGH(); }这里有一个提高效率的技巧如果你要连续写多个数据字节比如第一行像素的颜色数据不要在每次写字节之间都拉高CS再拉低CS。保持CS为低连续写多个字节最后统一拉高CS能显著降低SPI总线上的空闲占空比。命令阶段的数据排列固定一次连续写完对稳定性和速度都好。延时设计同样关键。芯片在复位和退出睡眠后需要时间让内部电路稳定。经验值是SWRESET后延时120ms以上SLPOUT后延时120ms以上设置完伽马和电源寄存器后再延时10~20ms清屏完成后再延时20ms再开显示。有些芯片用150ms更稳特别是电源纹波大的板子短延时可能导致上电瞬间还没稳定就发初始化命令结果屏幕白屏。设计驱动初始化函数时建议把延时统一封装static void tft_delay(uint32_t ms) { // 这里依赖你的工程有多按时基比如SysTick或HAL_Delay delay_ms(ms); }初始化时把时序加进去形成完整的初始化函数。这一点后面代码封装部分会继续展开。4. 手把手实现寄存器级驱动与代码封装4.1 硬件连接与SPI配置STM32F103为例先明确一个硬件方案。我用的是STM32F103C8T6最小系统板屏幕是1.8寸TFT LCDSPI接口。引脚可以不固定但本文示例固定为屏幕引脚接STM32说明VCC3.3V屏幕供电别接5VGNDGND共地必须接CSPA4软件控制片选RESETPA3复位引脚DCPA2数据/命令选择SDA/MOSIPA7SPI1_MOSISCL/SCKPA5SPI1_SCKBLK3.3V或PB0背光控制直接接3.3V则常亮PA5和PA7是SPI1的硬件引脚CS可以用硬件NSS但我更建议用普通GPIO软件控制片选这样移植到任意一个IO口都方便。RESET和DC也推荐用GPIO控制别用SPI的NSS功能否则逻辑会混在一起。用寄存器方式初始化GPIO和SPI1代码如下void SPI1_GPIO_Init(void) { // 使能GPIOA时钟与SPI1时钟 RCC-APB2ENR | RCC_APB2ENR_IOPAEN; RCC-APB2ENR | RCC_APB2ENR_SPI1EN; // PA5 SCK, PA7 MOSI 配置为复用推挽输出50MHz GPIOA-CRL ~(0xFUL (5 * 4)); GPIOA-CRL | (0xBUL (5 * 4)); // CNF10, MODE11 GPIOA-CRL ~(0xFUL (7 * 4)); GPIOA-CRL | (0xBUL (7 * 4)); // PA4 CS, PA3 RESET, PA2 DC 配置为通用推挽输出 GPIOA-CRL ~(0xFUL (4 * 4)); GPIOA-CRL | (0x3UL (4 * 4)); GPIOA-CRL ~(0xFUL (3 * 4)); GPIOA-CRL | (0x3UL (3 * 4)); GPIOA-CRL ~(0xFUL (2 * 4)); GPIOA-CRL | (0x3UL (2 * 4)); } void SPI1_Init(void) { SPI1_GPIO_Init(); // 关闭SPI SPI1-CR1 0; // 配置CR1 // SSM1, SSI1软件管理NSS避免NSS引脚干扰 // MSTR1主机模式 // BR011波特率预分频16如果APB272MHzSPI时钟4.5MHz // SPE0暂不使能 SPI1-CR1 | SPI_CR1_MSTR | SPI_CR1_SSM | SPI_CR1_SSI | (3 3); // 配置CR28位数据格式Motorola模式 SPI1-CR2 0; // 使能SPI SPI1-CR1 | SPI_CR1_SPE; }这里SPI时钟预分频设为16是因为很多ST7735S模组在SPI时钟超过10MHz时容易出错4.5MHz是安全速度。后面你想刷屏更快可以把BR位改成001预分频4即18MHz。但注意并不是所有屏幕都能吃满18MHz稳妥的做法是从低速跑通再提速度。注意SPI Mode 0也就是CPOL0、CPHA0。上面寄存器代码没有显式设置CPOL和CPHA位CR1里这两位的默认值就是0正好是Mode 0。如果你用STM32CubeMX图形化界面上选择Mode 0即可。4.2 从底层命令函数开始有了SPI底层屏幕驱动就有了“手”。接下来要写的就是屏幕协议层函数。先抽象出CS、DC、RST操作#define LCD_CS_LOW() GPIOA-BRR (1 4) #define LCD_CS_HIGH() GPIOA-BSRR (1 4) #define LCD_DC_LOW() GPIOA-BRR (1 2) #define LCD_DC_HIGH() GPIOA-BSRR (1 2) #define LCD_RST_LOW() GPIOA-BRR (1 3) #define LCD_RST_HIGH() GPIOA-BSRR (1 3)写命令和写数据函数void tft_write_cmd(uint8_t cmd) { LCD_CS_LOW(); LCD_DC_LOW(); SPI1_WriteByte(cmd); LCD_CS_HIGH(); } void tft_write_data(uint8_t dat) { LCD_CS_LOW(); LCD_DC_HIGH(); SPI1_WriteByte(dat); LCD_CS_HIGH(); } void tft_write_data_buf(uint8_t *buf, uint32_t len) { LCD_CS_LOW(); LCD_DC_HIGH(); while(len--) { SPI1_WriteByte(*buf); } LCD_CS_HIGH(); }这里面有一个细节tft_write_data_buf在连续写大量数据时不要每写一个字节都判断一下BSY因为BSY标志在连续传输时可能不按你预期变化。最好只用TXE标志判断是否可以把下一个字节写入发送寄存器等最后一次写完后再统一等待BSY清零。然后是复位函数void tft_reset(void) { LCD_RST_LOW(); tft_delay(20); LCD_RST_HIGH(); tft_delay(120); }有些模组会把复位引脚和MCU的复位脚接在一起这样MCU复位时屏幕也会复位。但这种接法在MCU单独软复位的场景下会影响屏幕状态建议还是用一个独立GPIO控制运行期想复位屏幕随时可以拉一下。窗口设置函数是后面所有绘制API的基础void tft_set_window(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { tft_write_cmd(0x2A); // CASET tft_write_data(x0 8); tft_write_data(x0 0xFF); tft_write_data(x1 8); tft_write_data(x1 0xFF); tft_write_cmd(0x2B); // RASET tft_write_data(y0 8); tft_write_data(y0 0xFF); tft_write_data(y1 8); tft_write_data(y1 0xFF); }在绘制像素之前先调用这个函数再写一个像素数据芯片就知道当前像素落在屏幕的哪个坐标上。4.3 初始化与清屏接下来是ST7735S的初始化函数。以一个常见的模组参数表为例我把它拆成几个小块复位、帧率配置、电源配置、伽马配置、像素格式、方向、开显示。void st7735s_init(void) { tft_reset(); // 退出睡眠 tft_write_cmd(0x11); // SLPOUT tft_delay(120); // 帧率配置 tft_write_cmd(0xB1); // FRMCTR1 tft_write_data(0x01); tft_write_data(0x2C); tft_write_data(0x2D); tft_write_cmd(0xB2); // FRMCTR2 tft_write_data(0x01); tft_write_data(0x2C); tft_write_data(0x2D); tft_write_cmd(0xB3); // FRMCTR3 tft_write_data(0x01); tft_write_data(0x2C); tft_write_data(0x2D); tft_write_data(0x01); tft_write_data(0x2C); tft_write_data(0x2D); // 显示反相控制 tft_write_cmd(0xB4); // INVCTR tft_write_data(0x07); // 电源配置 tft_write_cmd(0xC0); // PWCTR1 tft_write_data(0xA2); tft_write_data(0x02); tft_write_data(0x84); tft_write_cmd(0xC1); // PWCTR2 tft_write_data(0xC5); tft_write_cmd(0xC2); // PWCTR3 tft_write_data(0x0A); tft_write_data(0x00); tft_write_cmd(0xC3); // PWCTR4 tft_write_data(0x8A); tft_write_data(0x2A); tft_write_cmd(0xC4); // PWCTR5 tft_write_data(0x8A); tft_write_data(0xEE); tft_write_cmd(0xC5); // VMCTR1 tft_write_data(0x0E); // 伽马配置 tft_write_cmd(0xE0); // GMCTRP1 { static const uint8_t gamma_p[] { 0x0F, 0x2A, 0x28, 0x07, 0x02, 0x5E, 0x90, 0x0A, 0x0E, 0xC0, 0x04, 0x06, 0x05, 0x02, 0xC3 }; for(uint8_t i 0; i sizeof(gamma_p); i) tft_write_data(gamma_p[i]); } tft_write_cmd(0xE1); // GMCTRN1 { static const uint8_t gamma_n[] { 0x00, 0x15, 0x17, 0x18, 0x49, 0x21, 0x0F, 0x05, 0x05, 0x41, 0x1B, 0x0E, 0x19, 0x34, 0xCB }; for(uint8_t i 0; i sizeof(gamma_n); i) tft_write_data(gamma_n[i]); } // 像素格式RGB565 tft_write_cmd(0x3A); // COLMOD tft_write_data(0x05); // 显示方向竖屏RGB顺序 tft_write_cmd(0x36); // MADCTL tft_write_data(0xC8); // 开显示 tft_write_cmd(0x29); // DISPON tft_delay(50); // 清屏成黑色 st7735s_fill_screen(0x0000); }看完这串代码你可能会问为什么MADCTL写0xC80xC8二进制是11001000MY位、MX位、BGR位都是1。也就是说列扫描方向反转、行扫描方向反转同时颜色顺序设置为BGR。这个值对应竖屏显示具体的扫描方向要看模组装配时玻璃的走线方向不同厂家的屏可能正好相反。如果你的屏显示内容左右镜像就把MX位改一下上下颠倒就改MY位。清屏函数可以这样写void st7735s_fill_screen(uint16_t color) { uint32_t i; uint16_t pixel_count 128 * 160; tft_set_window(0, 0, 127, 159); tft_write_cmd(0x2C); // RAMWR LCD_CS_LOW(); LCD_DC_HIGH(); for(i 0; i pixel_count; i) { SPI1_WriteByte(color 8); SPI1_WriteByte(color 0xFF); } LCD_CS_HIGH(); }清屏时一次性把全屏窗口设置好然后连续写40960字节的像素数据。注意这里没有在每写两个字节之间反复拉CS而是保持CS为低一口气写完速度会快很多。4.4 绘制API点、线、矩形、字符有了底层窗口函数画点、画矩形、画字符都是在这个基础上做的。最简单的单像素绘制void st7735s_draw_pixel(uint16_t x, uint16_t y, uint16_t color) { if(x 128 || y 160) return; tft_set_window(x, y, x, y); tft_write_cmd(0x2C); tft_write_data(color 8); tft_write_data(color 0xFF); }画矩形填充时要注意效率。如果每画一个像素都发一遍CASET/RASET和RAMWR性能会低到不可用画面还会闪烁。应该先设置一次窗口为整个矩形区域然后连续写填充像素。void st7735s_fill_rect(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color) { uint32_t i; uint32_t count (uint32_t)(x1 - x0 1) * (y1 - y0 1); tft_set_window(x0, y0, x1, y1); tft_write_cmd(0x2C); LCD_CS_LOW(); LCD_DC_HIGH(); for(i 0; i count; i) { SPI1_WriteByte(color 8); SPI1_WriteByte(color 0xFF); } LCD_CS_HIGH(); }画线算法可以先用整数改进的Bresenham做直线但这篇文章重点在驱动芯片画线不是重点。你只需要理解画线的本质就是根据直线方程去调画点函数。跑通之后可以再优化成按线段连续写像素。显示字符是另一个常用功能。ST7735S内部没有字库所以你需要自己准备ASCII字模。一个5x8的字符可以压缩成8个字节每个字节的低5位对应一列像素。但初学者更容易理解的方式是一个字符用8行每行8位宽static const uint8_t ascii_5x7[94][5] { // 空格 {0x00, 0x00, 0x00, 0x00, 0x00}, // ! 0x21 {0x00, 0x00, 0x5F, 0x00, 0x00}, // ... };绘制一个字符void st7735s_draw_char(uint16_t x, uint16_t y, char ch, uint16_t color, uint16_t bgcolor) { const uint8_t *chmap; uint8_t i, j; uint16_t pixel; if(ch 0x20 || ch 0x7E) return; chmap ascii_5x7[ch - 0x20]; for(i 0; i 7; i) { for(j 0; j 5; j) { pixel (chmap[i] (4 - j)) 0x01; if(pixel) st7735s_draw_pixel(x j, y i, color); else st7735s_draw_pixel(x j, y i, bgcolor); } } }这个实现简单直观但每个像素都单独设置窗口和写命令速度很慢。实际项目中你可以把整行字符做成一个字模缓冲一次性填充。优化思路是先准备好一整块与字符区域等大的像素数据再调用类似st7735s_fill_rect的批量写函数。4.5 封装成驱动层让上层UI与硬件解耦复制粘贴来的代码最大问题是把所有功能堆在一个main.c里初始化、显示、缓存、协议全揉成一团。我建议在一开始就按“硬件平台层”和“屏幕驱动层”做隔离。屏幕驱动层对外只暴露几个基础接口void st7735s_init(void); void st7735s_set_rotation(uint8_t rotation); void st7735s_fill_screen(uint16_t color); void st7735s_fill_rect(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color); void st7735s_draw_pixel(uint16_t x, uint16_t y, uint16_t color); void st7735s_draw_char(uint16_t x, uint16_t y, char ch, uint16_t color, uint16_t bgcolor); void st7735s_draw_string(uint16_t x, uint16_t y, const char *str, uint16_t color, uint16_t bgcolor);这样做的价值在项目后期才会体现。比如你从1.8寸屏换到2.4寸ILI9341屏上层UI代码只需要调用相同命名的接口具体实现换一个文件又比如你要把LVGL移植到STM32上LVGL的显示接口底层也要调用“画点”“填充矩形”这类原语提前做好封装能省下大量移植时间。旋转方向接口也值得专门封装。ST7735S的横竖屏切换本质上就是改MADCTL和窗口尺寸。旋转0度、90度、180度、270度对应不同的MY、MX、MV位组合同时屏幕宽高会互换。封装时最好把宽和高变成动态变量不要写死在128和160上static uint16_t g_lcd_width 128; static uint16_t g_lcd_height 160; void st7735s_set_rotation(uint8_t rot) { switch(rot) { case 0: tft_write_cmd(0x36); tft_write_data(0xC8); // 竖屏 g_lcd_width 128; g_lcd_height 160; break; case 1: tft_write_cmd(0x36); tft_write_data(0xA8); // 横屏 g_lcd_width 160; g_lcd_height 128; break; // 其他角度类似 } }画点函数里用g_lcd_width和g_lcd_height做边界判定这样旋转之后不会出现坐标溢出。代码封装不是为了显得架构好而是为了让自己和队友以后少改代码。驱动屏幕的核心逻辑一旦稳定上层画界面就只是调函数的活。5. 实测中常见的坑与调试技巧5.1 白屏、背光亮了但无图像这是最多人遇到的问题背光亮了屏幕白花花一片没有任何画面。排查思路从上电开始第一步检查电源。屏幕供电必须是3.3V如果接了5V芯片可能已经烧了如果供电电压不够也会白屏。最简单的方法用手摸芯片和屏幕玻璃如果异常发热说明电源有问题。第二步检查复位。用逻辑分析仪或者示波器看RESET引脚的波形确认复位时序是“先低后高且延时足够”。如果用万用表看只能看到一直高电平看不到脉冲所以调试屏幕强烈建议配逻辑分析仪几十块钱的裸机逻辑分析仪就能抓SPI时序。第三步检查SPI通信。重点看SCK的时钟频率和DC/CS信号的相对时序。很多白屏是因为SPI波特率配置太高ST7735S读到了错误数据。把SPI时钟降到1MHz左右如果画面出来说明是速度问题再往上慢慢加。第四步是命令是否真的发出去了。用逻辑分析仪抓CS拉低期间的第一个字节看看是不是0x01复位、0x11退出睡眠、0x29开显示。如果第一个字节就不对问题在SPI配置或DC电平如果命令都对但屏幕还是白问题多半在初始化序列不完整或某个参数不对。还有一个高频坑有些1.8寸屏模块上电后默认显示的是RAM里随机内容屏幕不一定纯白可能是雪花状或者淡灰。这时候你初始化做了但清屏函数没调用显示区域还是乱七八糟的残留数据。我的建议是在初始化最后强制清屏并且清屏时把窗口设成全屏这只是除隐患不是为了视觉效果。5.2 颜色偏红/偏蓝/偏色屏幕能点亮但颜色不对最常见的原因是颜色字节顺序反了或者说BGR和RGB搞错了。ST7735S的MADCTL寄存器里有一位BGR位置0时芯片按RGB顺序解释数据置1时按BGR顺序解释。如果你的红色像素显示成蓝色蓝色像素显示成红色基本就是这一位反了。对应解决办法是改MADCTL参数当前MADCTL是0xC8二进制11001000BGR位bit3为1即BGR顺序如果颜色反了把bit3改成0即RGB顺序参数变成0xC0如果你不确定可以直接在代码里让两个值切换测试看哪个颜色显示正常。其实在RGB565模式下屏幕的“R”通道和“G”“B”通道是按照芯片自己的约定映射到玻璃像素上的不同批次玻璃可能映射不同。所以这个代码在你这块屏上正常在另一块屏上偏蓝也不是怪事。还有一种偏色来自像素格式配置错误。比如你初始化里写的是0x06RGB666但上层代码却按RGB565发数据每个像素位宽对不上颜色就会错乱成一团。确保COLMOD和你的发数据格式一致。用RGB565就写0x05然后用16位色值。伽马寄存器配置不当也会导致偏色但这种偏色一般是整体对比度不对暗部偏紫、亮部偏绿等。如果你发现RGB顺序已经改对、像素格式也对但颜色还是“暖得奇怪”可以尝试调整GMCTRP1和GMCTRN1的数组。不过这两个数组是原厂调好的强烈不建议自己改。我遇到过一次屏幕整体偏紫最后发现是初始化序列里PWCTR3和VMCTR1被改了恢复成原厂数组后颜色立刻正常。5.3 图像偏移/显示错位屏幕画面能亮但图像整体左移、右移、或者被分割成错位的两块这是窗口设置和GRAM起始地址不匹配导致的。不同模组厂商出产的玻璃面板物理像素的起始地址不一定是0,0。很多模块的GRAM是132x162但可视区域是128x160。如果你把CASET/RASET设置为0~127、0~159而实际可视区域的起始GRAM地址是2,1那画面就会整体偏移两个像素。怎么找偏移网上的通用做法是先把全屏填充成一种纯色比如红色然后看屏幕四周有没有不亮的边框。如果左侧有暗边说明CASET起始地址要加偏移如果上方有暗边说明RASET起始地址要加偏移。常见的偏移值有0、1、2几个像素参考模组资料里的“Display Offset”参数。下面是一个带偏移的窗口设置示例假设列偏移2行偏移1void tft_set_window_offset(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { uint16_t offset_x 2; uint16_t offset_y 1; tft_write_cmd(0x2A); tft_write_data((x0 offset_x) 8); tft_write_data((x0 offset_x) 0xFF); tft_write_data((x1 offset_x) 8); tft_write_data((x1 offset_x) 0xFF); tft_write_cmd(0x2B); tft_write_data((y0 offset_y) 8); tft_write_data((y0 offset_y) 0xFF); tft_write_data((y1 offset_y) 8); tft_write_data((y1 offset_y) 0xFF); }还有一类错位来自MADCTL的扫描方向设置。屏幕显示方向和面板走线方向不匹配时图像会左右镜像、上下颠倒甚至从右下角开始填充。排查时只需要把MY/MX/MV几个位逐一翻转测试直到画面位置和坐标方向一致。5.4 刷屏慢SPI时钟与DC/CS引脚处理屏幕能显示但刷新全屏要一两秒那体验很糟糕。刷屏慢的根本原因是数据传输时间太长。128x160全屏RGB565的数据量是40960字节如果SPI时钟只有1MHz光传输数据就要0.3秒以上再加上地址设置和抗拒肉眼可见地龟速。提速手段从易到难排列提高SPI波特率。把SPI预分频从16降到8或4比如从4.5MHz提到9MHz全屏传输时间能缩短一半。前提是屏幕能稳定工作个别模块需要时序裕量提到18MHz容易花屏可以先用逻辑分析仪验证时序幅值和边沿。减少窗口设置次数。清屏时只设置一次CASET/RASET然后连续写40960字节。不要每写两字节就发一次RAMWR命令。尽量用DMA。SPI发送时不占用CPUCPU可以去干别的事。STM32F103的SPI1有DMA通道配置好后只需要把像素数据放到内存里启动DMA传输然后等完成中断。避免不必要的CS翻转。连续传输阶段把CS一直拉低不要每次写字节都拉高CS再拉低。DMA传输代码篇幅较长但思路很简单把你要发送的一整块像素数据放到一个数组或内存区域配置DMA通道的外设地址为SPI1-DR内存地址为数组首地址传输方向为内存到外设传输长度为数据字节数最后使能DMA。传输完成标志通常在DMA中断里触发清屏函数在等待时要加超时保护避免DMA配置错误导致死循环。5.5 故障排查速查表我把常见问题整理成一张速查表方便你在调试时快速定位现象优先检查排查方向白屏无任何输出电源、复位、初始化命令供电是否3.3V复位延时是否够SPI时钟是否太高屏幕能亮但全是雪花CS/DC时序DC切换和SPI字节边界是否错位CS是否没拉低颜色反了MADCTL的BGR位改成相反的RGB/BGR顺序测试图像整体偏移CASET/RASET偏移检查模组起始行列偏移图像上下颠倒MADCTL的MY位翻MY位测试图像左右镜像MADCTL的MX位翻MX位测试清屏后残留旧画面RAMWR窗口设置确保窗口覆盖整个屏幕并写满全部像素闪屏严重刷屏方式减少中间窗口切换尝试整屏局部缓冲双倍缓存字符显示乱码字模顺序/编码确认ASCII字模取模方向一致确认汉字编码转成UTF-8/GBK每次排查屏幕问题我习惯于先抓SPI波形再把MADCTL状态打出来最后检查窗口范围。这样避免凭肉眼乱试参数。5.6 从驱动到应用给LVGL留一条后路最后一个建议是别把驱动写成死胡同。现在很多人想在STM32上跑LVGL做美观的UI但LVGL不直接操作ST7735S而是要求你在底层实现“画点”或“填充矩形”的接口并且能提供一个“将指定区域的像素缓冲写入屏幕”的函数。这个函数和你驱动里设置窗口、连续写像素的方式完全一致。所以你在封装驱动时最好预留一个“写区域缓冲”的接口void st7735s_write_ram(uint8_t *data, uint32_t len) { tft_write_cmd(0x2C); tft_write_data_buf(data, len); }将来LVGL的flush_cb回调本质上就是根据坐标和尺寸设置窗口然后调用这个st7735s_write_ram把颜色缓冲发过去。你前期把这些底层函数做得越干净后期移植LVGL就越轻松。说到LVGL移植经常会遇到一个问题LVGL画界面速度慢。这往往不是LVGL本身的问题而是底层的“写像素”函数没有使用批量写入。你要让LVGL的flush函数最终落到“整块区域批量发送”而不是一次画一个小点。ST7735S的窗口机制和批量写命令正是为这种需求设计的。最后再分享一个小技巧。调试屏幕时不要直接拿一整张图片去测试先画三条不同颜色的粗线条红、绿、蓝这样颜色顺序对不对一眼就能看出来再画一个从左上角到右下角的对角斜线窗口偏移和扫描方向问题立刻现形。这张“测试图”我用了几十个项目比任何调试打印都直观。屏幕驱动这件事说白了就是两件事第一把初始化序列按正确的时序发给芯片第二用窗口和像素写入命令把你想显示的内容填进GRAM。把这两点吃透了你就不再是那个只会复制粘贴的人。下次再遇到一块陌生的TFT屏你可以先看它的脚位定义翻一翻数据手册然后在几分钟之内写出属于自己的驱动。那种感觉比直接抄代码爽多了。
返回列表