资讯详情

STM32CubeIDE驱动ST7735S:从SPI初始化到DMA刷屏完整指南

📅 2026/9/14 12:31:11 | 华诺云谱 👁 阅读
STM32CubeIDE驱动ST7735S:从SPI初始化到DMA刷屏完整指南
简介这是基于STM32CubeIDE非常详细地从零开始驱动ST7735S液晶屏的完整资料包面向嵌入式初学者及需要快速点亮LCD显示界面的开发者。屏幕采用的驱动芯片为ST7735S属于1.8英寸TFT全彩屏通过SPI接口通信分辨率128乘以160颜色格式为RGB565即红色占高5位、绿色占中间6位、蓝色占低5位。资料配有外部接口原理图、工程源码和调试记录清晰梳理了从引脚连接到软件配置再到显示输出的完整流程。压缩包共269个文件大小7.38MB其中包含51个头文件和25个C源文件以及.ioc、.project、.cproject等CubeIDE工程配置还保留了.ld、.map、.o、.elf、.bin等编译链接产物方便对照复现整个构建过程另有PDF、txt文档用于辅助阅读和说明。目前已有388人学习下载。整套资料按实际项目组织目录结构清晰读者可依据这份资源理解ST7735S的多种接口时序掌握SPI屏幕驱动的移植方法并借助调试记录快速排查类似问题。1. 点亮ST7735S这件事麻烦不在画点在初始化标题里那句“STM32CubeIDE一步一步驱动LCD”看着像教程真上手一次就明白这个屏用STM32CubeIDE点亮90%的功夫花在把初始化序列按顺序发对剩下10%才是画点、填充和显示字符。ST7735S是一颗1.8寸TFT彩屏的驱动IC160×128分辨率靠SPI串口和主控通信总共只需要SCK、MOSI、D/C、CS四根信号线加上复位和背光也就是六根线。它在内部自带一块GRAM主机画像素的本质是把颜色值写进远端内存而这块内存的地址由CASET/RASET命令圈定。适合正在做显示界面、需要理解SPI时序和屏幕寄存器的人本文不借助Arduino库直接在STM32CubeIDE里用HAL库把整个链路跑通从原理图引脚到代码调试一次说完。2. ST7735S的GRAM和SPI总线先把寄存器模型讲透2.1 数据手册里的命令地址体系CASET/RASET/RAMWRST7735S内部有一块SRAM做显存160×128个像素点每个像素在16位模式下占2字节颜色格式是RGB565。理解这颗芯片的关键是接受一个事实主机不能直接改显存字节只能通过命令去操作。想要把一个点变成红色不是发一个“红”字而是先告诉芯片列地址范围再告诉行地址范围最后用RAMWR命令把0xF800这样的颜色值灌进去。芯片内部的行列译码器会用移位时钟逐点推进指针写满一个地址后自动跳到下一个。这里有三条命令必须记牢后面所有画点、画线、清屏函数都是从它们组合出来的。CASET是0x2A作用是设置列地址X方向的起始和结束RASET是0x2B设置行地址Y方向的起始和结束RAMWR是0x2C启动连续写显存模式。地址范围宽高都算在内比如清全屏就是CASET设成0到159、RASET设成0到127然后往GRAM里连续写160×128个RGB565值。命令命令码参数长度作用CASET0x2A4字节设置列地址起始和结束参数是X0高位、X0低位、X1高位、X1低位RASET0x2B4字节设置行地址起始和结束参数是Y0高位、Y0低位、Y1高位、Y1低位RAMWR0x2C连续数据启动写GRAM之后SPI上发多少字节就写多少个像素RDDID0x04读回3字节读芯片ID用于验证SPI通信是否打通MADCTL0x361字节扫描方向和RGB/BGR顺序实现旋转COLMOD0x3A1字节像素格式0x05表示16位/像素除了这六条初始化还会用到电源控制、伽马、帧率等一串寄存器那些不属于日常绘图路径但会影响显示质量。先把模型建立起来屏幕是一台带显存从机主机发命令和参数芯片自己刷屏。写代码时所有通信都围绕“发命令”和“发数据”两个动作不会有其他玄学。2.2 D/C信号线、SPI Mode 0和字节顺序SPI总线本身是纯字节流芯片靠D/C信号线区分当前收到的是命令还是数据。D/C拉低当前传输的第一个字节被解释为命令码D/C拉高后面所有字节都被解释为数据。一个完整操作通常是先拉低D/C发命令码再拉高D/C发参数。比如设置16位像素格式就是D/C低发0x3AD/C高发0x05。时序模式上ST7735S手册支持Mode 0和Mode 3大多数屏厂示例驱动用Mode 0。在STM32CubeIDE里对应CPOLLow、CPHA1 Edge也就是时钟空闲为低、数据在第一个边沿采样。要注意一点F103的SPI1挂在APB2上72MHz主频经过分频后是9MHz而ST7735S的SPI时钟上限一般是10MHz左右9MHz安全。之前见过有人把分频设成2得到36MHz屏幕花得像雪花屏还以为是线松了。字节顺序必须MSB FirstST7735S手册上所有寄存器、颜色数据都是高位先出HAL库里默认就是这个设置不要改成LSB。2.3 读ID验证通信0x04 RDDID最实用引脚对没对上、焊接有没有虚焊与其盯着白屏猜不如先发一条RDDID命令把芯片ID读回来。ST7735S支持0x04命令从MISO回3个字节其中第3字节能区分ST7735S和ST7735R。注意F103的SPI是全双工需要PA6作为MISO但市面上很多廉价1.8寸屏模块并没有引出SDO引脚只留了SDA、SCL、RES、DC、CS、BLK六个脚这种情况读ID就无从谈起只能靠初始化后是否出画面来判断。uint8_t lcd_read_id(void) { uint8_t cmd 0x04; /* RDDID 命令 */ uint8_t rx[3] {0}; LCD_CS_LOW; LCD_DC_LOW; /* D/C0发送命令字节 */ HAL_SPI_Transmit(hspi1, cmd, 1, 10); LCD_DC_HIGH; /* D/C1之后都是数据周期 */ HAL_SPI_Receive(hspi1, rx, 3, 10); LCD_CS_HIGH; return rx[2]; /* 第3字节才是ID */ }这段代码的逻辑是先拉低CS选中从机D/C拉低把0x04当作命令发出去然后D/C拉高HAL_SPI_Receive在收发数据的同时置MISO为高阻输入把芯片回送的3个字节收进来。ST7735S读回常见值是0x0C或0x88开头ST7735R可能回0x88/0x8C不同批次甚至回0x00所以读到非0xFF就说明SPI链路大概率正常。如果全是0xFF优先检查PA5到SCL、PA7到SDA的连通性以及CS是不是被拉低后真的进了片选状态。提示读不到ID不一定是线接错很多模块把SDO引脚内部悬空读回数据本来就是0xFF。读ID只是辅助手段不要当成唯一判据。3. STM32CubeIDE配置SPI1与LCD原理图接线引脚先对上3.1 LCD显示屏引脚介绍ST7735S六线接线表从模块上看到的引脚比芯片手册简单得多通常就七个焊盘VCC、GND、SCL、SDA、RES、DC、CS有的还有BLK背光控制。VCC接3.3V个别模块标注5V兼容但ST7735S逻辑电平是1.8V到3.3V稳妥起见全用3.3V。BLK是背光LED的使能端一般内部已经做过处理外部串一个100Ω电阻到3.3V想调亮度就接到PWM引脚频率设1kHz占空比控制亮度。屏幕引脚功能接STM32F103C8T6备注VCC电源正3.3V板载LDO供电GND电源地GND与MCU共地否则SPI电平无参考SCLSPI时钟PA5SPI1_SCKSDASPI主出从入PA7SPI1_MOSIRES复位PA2GPIO输出控制DC命令/数据选择PA30命令1数据CS片选PA4GPIO输出控制BLK背光使能PA1GPIO或PWM高电平点亮原理图上还有两个容易忽略的点。第一屏幕排线从FPC引出后走线尽量短超过10cm就要考虑把SPI时钟降到4.5MHz不然波形反射会在数据线跳变沿出现毛刺。第二VCC引脚旁边加一个100nF陶瓷电容靠近屏幕电源脚放置LCD刷新时瞬间电流能到几十毫安电源纹波会导致横条纹。有人画原理图只连了线和插座省掉滤波电容结果画面出现一条条水平亮线查了半天是电源噪声耦合进了内部电荷泵。3.2 在STM32CubeIDE里按这套参数配SPI1STM32CubeIDE 2.2.0版本下新建工程时选好MCU型号STM32F103C8T6双击ioc文件打开设备配置视图。左侧Categories里找到Connectivity-SPI1打开SPI1的开关时钟配置会自动把PA5和PA7分配过来。右边参数区按下面这张表设置其他保持默认。配置项值影响ModeFull-Duplex Master全双工主机模式MISO可以备而不接Data Size8 BitsST7735S所有寄存器都是8位宽Prescaler872MHz / 8 9MHz小于芯片极限10MHzCPOLLow时钟空闲电平为低CPHA1 Edge第一个边沿采样NSSSoftwarePA4用普通GPIO自己控不用硬件NSSFirst BitMSB FirstST7735S规格要求配完之后点右上角的Generate Code生成初始化代码会自动生成MX_SPI1_Init函数。如果你发现生成代码按钮灰色不可点多半是ioc文件没保存先按CtrlS再回来点。生成的代码里SPI初始化部分是固定结构检查一下BaudRatePrescaler是否真的变成了SPI_BAUDRATEPRESCALER_8这个数值决定了实际的SCK频率。hspi1.Instance SPI1; hspi1.Init.Mode SPI_MODE_MASTER; hspi1.Init.Direction SPI_DIRECTION_2LINES; hspi1.Init.DataSize SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity SPI_POLARITY_LOW; hspi1.Init.CLKPhase SPI_PHASE_1EDGE; hspi1.Init.NSS SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_8; hspi1.Init.FirstBit SPI_FIRSTBIT_MSB;分频系数是CubeIDE里最容易改错的一项。Prescaler8在72MHz主频下得到9MHzPrescaler4得到18MHz这个值已经超过ST7735S允许范围屏幕大概率不显示或者出现横纹。如果APB2时钟不是72MHz而是36MHzPrescaler4也只有9MHz要先在Clock Configuration里确认APB2 External clock是72MHz。3.3 复位、背光与电源暗屏和白屏的两个重灾区RES引脚用普通GPIO控制不要直接接RC电路。上电后先拉低50ms再拉高ST7735S内部需要完整的复位时序才能正确启动。很多白屏案例是RES引脚悬空芯片上电后内部状态机停在未知状态初始化命令被吞掉。背光更隐蔽BLK引脚如果不接屏幕背光不亮看起来像黑屏如果BLK接到了3.3V而模块上没有限流电阻背光灯珠电流可能超标。加个100Ω串联电阻既能保护灯珠也能在调试时用万用表量电压判断背光是否正常。电源方面再强调一次屏幕模块和MCU必须共地。只接了电源线、信号线但漏接GNDSPI信号电平没有参考地逻辑会完全错乱。业余条件下用杜邦线连接时容易出现这种问题画PCB时把GND铺铜连好电源脚用100nF10µF两级滤波。4. 代码驱动ST7735S初始化序列、画点函数和字符显示4.1 先写写命令/写数据两个原子操作整个驱动建立在两个函数上一个是写命令一个是写数据。它们的区别只有一个D/C引脚的逻辑电平。写命令时D/C拉低写数据时D/C拉高。CS引脚在每个操作周期内拉低再拉高保证一个操作完整结束、下个操作干净开始。void lcd_write_cmd(uint8_t cmd) { LCD_CS_LOW; LCD_DC_LOW; /* D/C0命令周期 */ HAL_SPI_Transmit(hspi1, cmd, 1, 10); LCD_CS_HIGH; } void lcd_write_data(uint8_t *buf, uint16_t len) { LCD_CS_LOW; LCD_DC_HIGH; /* D/C1数据周期 */ HAL_SPI_Transmit(hspi1, buf, len, 10); LCD_CS_HIGH; } void lcd_wr_cmd_data(uint8_t cmd, uint8_t *param, uint8_t n) { lcd_write_cmd(cmd); lcd_write_data(param, n); }第三个函数把“命令参数”打包是初始化序列最常用的入口。HAL_SPI_Transmit的第三个参数是数据长度单位是字节数而不是位别写成位宽。超时时间设10ms足够SPI一字节只有微秒级出现超时说明总线被其他外设占用或者时钟没配出来。这里有个小坑CS拉低后D/C必须在第一个字节发送之前稳定代码里先LCD_DC_LOW再调Transmit顺序不能反。4.2 一口气看完整初始化序列从SLPOUT到DISPON初始化序列是最容易抄错的部分。不同模块厂商给的序列大同小异核心是先把芯片从睡眠状态唤醒再设置扫描方向、像素格式、电源和伽马。我习惯把序列拆成两段一段只做唤醒和颜色格式另一段做伽马和帧率调试时分开验证。下面这段是一个能直接跑通的完整序列适用于大部分ST7735S模块。static void lcd_init_sequence(void) { uint8_t p[6]; lcd_write_cmd(0x01); /* SWRESET 软件复位 */ HAL_Delay(120); lcd_write_cmd(0x11); /* SLPOUT 退出睡眠 */ HAL_Delay(120); p[0] 0x01; p[1] 0x2C; p[2] 0x2D; lcd_wr_cmd_data(0xB1, p, 3); /* FRMCTR1 正常模式帧率 */ lcd_wr_cmd_data(0xB2, p, 3); /* FRMCTR2 空闲模式帧率 */ p[0] 0x01; p[1] 0x2C; p[2] 0x2D; p[3] 0x01; p[4] 0x2C; p[5] 0x2D; lcd_wr_cmd_data(0xB3, p, 6); /* FRMCTR3 部分模式帧率 */ p[0] 0x07; lcd_wr_cmd_data(0xB4, p, 1); /* INVCTR 反色控制 */ p[0] 0xA2; p[1] 0x02; p[2] 0x84; lcd_wr_cmd_data(0xC0, p, 3); /* PWCTR1 电源控制 */ p[0] 0xC5; lcd_wr_cmd_data(0xC1, p, 1); /* PWCTR2 VGH/VGL升压 */ p[0] 0x0A; p[1] 0x00; lcd_wr_cmd_data(0xC2, p, 2); /* PWCTR3 正常模式电源 */ p[0] 0x8A; p[1] 0x2A; lcd_wr_cmd_data(0xC3, p, 2); /* PWCTR4 空闲模式电源 */ p[0] 0x8A; p[1] 0xEE; lcd_wr_cmd_data(0xC4, p, 2); /* PWCTR5 部分模式电源 */ p[0] 0x0E; lcd_wr_cmd_data(0xC5, p, 1); /* VMCTR1 VCOM电压 */ p[0] 0x0F; p[1] 0x1A; p[2] 0x0F; p[3] 0x18; p[4] 0x2F; p[5] 0x28; p[6] 0x20; p[7] 0x22; p[8] 0x1F; p[9] 0x1B; p[10] 0x23; p[11] 0x37; p[12] 0x00; p[13] 0x07; p[14] 0x02; p[15] 0x10; lcd_wr_cmd_data(0xE0, p, 16); /* GMCTRP1 正极性伽马 */ p[0] 0x0F; p[1] 0x1B; p[2] 0x0F; p[3] 0x17; p[4] 0x33; p[5] 0x2C; p[6] 0x29; p[7] 0x2E; p[8] 0x30; p[9] 0x30; p[10] 0x39; p[11] 0x3F; p[12] 0x00; p[13] 0x07; p[14] 0x03; p[15] 0x10; lcd_wr_cmd_data(0xE1, p, 16); /* GMCTRN1 负极性伽马 */ p[0] 0xC0; /* MADCTL 扫描方向BGR */ lcd_wr_cmd_data(0x36, p, 1); p[0] 0x05; /* COLMOD 16位RGB565 */ lcd_wr_cmd_data(0x3A, p, 1); lcd_write_cmd(0x13); /* NORON 退出部分模式 */ HAL_Delay(10); lcd_write_cmd(0x29); /* DISPON 开显示 */ HAL_Delay(100); }这段序列里0xB1到0xB4是帧率控制决定屏幕刷新率ST7735S内部用行扫描方式刷新面板这个寄存器配错会出现闪烁或水波纹0xC0到0xC5是电源管理内部电荷泵给栅极和VCOM供电配错会偏色或者花屏0xE0和0xE1是伽马校正曲线这是一个16字节查找表厂家推荐的数值可以直接用个人调色时微调中间几个字节能改变对比度。最需要注意的是0x36那条0xC0表示行扫描反向加BGR颜色顺序。如果你的模块显示出来的颜色红蓝互换把0xC0改成0x00如果竖屏显示方向反了后面讲到旋转时会有具体表格。4.3 圈地址再灌颜色画点、矩形填充与清屏有了初始化序列剩下的绘图函数全都是CASET和RASET的组合。先设置窗口再连续灌像素数据。一个矩形填充的完整代码如下void lcd_fill_rect(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color) { uint8_t p[4]; p[0] x 8; p[1] x 0xFF; p[2] (x w - 1) 8; p[3] (x w - 1) 0xFF; lcd_wr_cmd_data(0x2A, p, 4); /* CASET: 列范围 */ p[0] y 8; p[1] y 0xFF; p[2] (y h - 1) 8; p[3] (y h - 1) 0xFF; lcd_wr_cmd_data(0x2B, p, 4); /* RASET: 行范围 */ lcd_write_cmd(0x2C); /* RAMWR 开始写 */ for (uint32_t i 0; i (uint32_t)w * h; i) { uint8_t d[2]; d[0] color 8; /* RGB565 高字节 */ d[1] color 0xFF; /* RGB565 低字节 */ lcd_write_data(d, 2); } }这里的坐标可以从0到159、0到127。CASET参数是起始列加结束列包含端点所以结束列是起始列加宽度减一。RGB565的拆法是一红5位、绿6位、蓝5位0xF800是红色0x07E0是绿色0x001F是蓝色。这个函数每像素调一次HAL_SPI_Transmit开销很大刷全屏大约要几十毫秒只适合画按钮、画色块。实际项目里我用一个160字节的行缓冲每次填一行再一次性发送速度能快一个数量级这个优化后面DMA章节会展开说。画点函数就更简单把w和h参数都设成1调用同一个矩形填充即可。常见错误是坐标越界比如在横屏模式下x写成200屏幕内部的行列指针会回绕画面出现奇怪的斜条纹。初始化时约定好一套坐标方向所有函数共用不要混用。4.4 LCD屏显示中文16×16字模与MADCTL旋转英文字符和数字用5×7字体点阵每个字符占5字节每字节的低5位表示一列像素。中文字符一个16×16点阵占32字节取模工具生成后直接按字节顺序写入显存。中文显示的关键不在驱动而在字模来源和编码转换。常见做法是用PCtoLCD2002这类工具把需要显示的汉字取模成数组烧进STM32F103的Flash。F103C8T6自带64KB Flash放几百个常用汉字足够如果要做全字库就得外挂W25Q16 SPI Flash把字库文件按地址烧录进去。void lcd_draw_char(uint16_t x, uint16_t y, uint8_t ch, uint16_t color, uint16_t bg) { const uint8_t *glyph font5x7[ch - 0x20]; for (int row 0; row 7; row) { uint8_t line glyph[row]; for (int col 0; col 5; col) { if (line (0x01 col)) lcd_draw_pixel(x col, y row, color); else lcd_draw_pixel(x col, y row, bg); } } } void lcd_draw_cn16(uint16_t x, uint16_t y, uint16_t index, uint16_t color, uint16_t bg) { const uint8_t *gb cn_font16[index]; for (int row 0; row 16; row) { uint8_t hi gb[row * 2]; uint8_t lo gb[row * 2 1]; for (int col 0; col 8; col) { if (hi (0x80 col)) lcd_draw_pixel(x col, y row, color); else lcd_draw_pixel(x col, y row, bg); if (lo (0x80 col)) lcd_draw_pixel(x 8 col, y row, color); else lcd_draw_pixel(x 8 col, y row, bg); } } }字符函数里的取模方向是“逐行式高位在前”也就是每行从左到右每字节最高位对应最左像素。如果取模工具设置成“列行式”或者“低位在前”显示出来就是镜像或者颠倒。中文函数每行拆成两个字节分别对应左半边8像素和右半边8像素这种布局和取模工具默认选项一致。旋转屏幕只需要改一条MADCTL寄存器0x00竖屏0x60横屏顺时针90度0xC0竖屏反转180度0xA0横屏逆时针90度改完初始化代码里0x36的参数即可显示函数不需要动。5. 调试ST7735S白屏与花屏分三种现象逐项排查5.1 白屏、花屏、错位分别先查哪里调试LCD和调试串口完全是两种体验。串口错了最多乱码LCD错了会白屏、花屏、黑屏、色偏每种现象对应不同原因。我把常见的现象整理成一张表按出现频率排好了现象先查什么常见原因全白屏背光使能、复位时序BLK悬空、RES低电平时间不足50ms、初始化没跑到DISPON全黑屏背光、电源地BLK一直低、VCC/GND接反、模块电源没起花屏/彩色噪点SPI时钟频率、接线长度SCL/SDA线序接反、分频设太小、杜邦线过长干扰全屏纯色但颜色错MADCTL的BGR位0x36参数需要加0x08或者不加图形错位/显示一半CASET/RASET坐标坐标越界、窗口参数高低字节颠倒闪烁/水波纹帧率寄存器、电源纹波0xB1配错、VCC旁路电容缺失白屏是最常见的现象。如果背光已经亮了但没有任何像素说明显存内容没有被正确写入优先检查初始化序列是否执行完整。我碰到过一次很隐蔽的坑HAL_Delay在系统时钟没配好时不会工作SLPOUT后面的一百多毫秒延迟直接变成0芯片还没准备好就开始发后续命令结果前几条寄存器被吞掉白屏。这种问题用示波器量RES和SCK的时序才能发现。花屏则优先怀疑SPI速率把分频从8改成16时钟降到4.5MHz再看如果画面变正常就是信号完整性导致的。5.2 用逻辑分析仪验证写命令时序和帧耗时逻辑分析仪是调试SPI屏幕最实用的工具不用太贵采样率50MHz的就行。接线时把SCL、SDA、CS、DC四根线夹到分析仪通道上触发条件设成CS下降沿然后跑一遍初始化代码。抓到的波形上DC信号为低的时段对应的是命令字节DC为高的时段是数据。先用肉眼确认一条RDDID命令的完整时序CS拉低DC低SCK翻转8次发出0x04随后DC拉高再翻转24次读回3字节最后CS拉高。四根线的相对顺序和代码完全一致就说明基本通信没问题。如果波形里CS拉低后SCK完全没有翻转问题在CubeIDE的GPIO配置PA5没有被正确复用成SPI1_SCK。如果SCK有翻转但SDA上数据全错位检查MSB/LSB设置是否改成LSB First。此外还能用分析仪测帧耗时在初始化完成后跑一次全屏填充测量CS从第一次拉低到最后一次拉高的总时间减去HAL函数本身的调用开销就是刷一屏的原始时间。F103在9MHz SPI下刷160×128理论耗时约36毫秒如果测得超过100毫秒说明程序在循环里做了太多无效操作比如每像素都拉CS或者调用HAL函数后面优化空间很大。5.3 二分定位初始化失败只写一半代码把范围缩小初始化序列有十几条寄存器命令任何一条出错都可能导致后续操作不正常。常见的调试习惯是一条一条命令反复对比效率低。我一般用二分法把初始化序列拆成前后两段前半段只做唤醒和设像素格式然后直接写一个蓝色全屏。如果蓝色显示正常说明SPI通信、坐标窗口、写显存这三条链路都通问题一定在剩下的帧率/伽马寄存器里如果蓝色都出不来就回退检查最基础的命令收发。static void lcd_init_part1(void) { lcd_write_cmd(0x01); /* SWRESET */ HAL_Delay(120); lcd_write_cmd(0x11); /* SLPOUT */ HAL_Delay(120); uint8_t p[4]; p[0] 0x05; lcd_wr_cmd_data(0x3A, p, 1); /* 16位颜色 */ p[0] 0; p[1] 0; p[2] 0; p[3] 0x9F; lcd_wr_cmd_data(0x2A, p, 4); /* 全屏列窗口 */ p[0] 0; p[1] 0; p[2] 0; p[3] 0x7F; lcd_wr_cmd_data(0x2B, p, 4); /* 全屏行窗口 */ lcd_write_cmd(0x2C); /* 开始连续写显存 */ for (uint16_t i 0; i 160 * 128; i) { uint8_t c[2] {0x00, 0x1F}; /* RGB565 蓝色 */ lcd_write_data(c, 2); } }这段代码执行后屏幕应该是纯蓝色。注意它没有发DISPON命令如果芯片的显示关断位是默认状态这里也可能不出画面那就在开头加上0x29命令。这本身就是一次排查0x29加在哪个位置会直接影响现象。一旦蓝屏出现就把原来的完整序列后半段加上再去看伽马和帧率。逐个分组加回去每次加一组就重新烧录运行最多四五轮就能定位到具体寄存器比对着手册盲猜快得多。6. DMA刷ST7735S整屏提速用SysTick把帧率算出来6.1 双行缓冲加SPI DMA的最小改动复制代码的时候要先意识到一个问题F103C8T6只有20KB SRAM160×128×2字节的整屏缓冲要40KB放不下。所以整屏DMA方案在F103上不可行常见做法是双行缓冲加逐行DMA每行160像素占320字节两个行缓冲加起来640字节余量很足。发送完一行再发下一行DMA传送期间CPU可以准备第二行内容。static uint8_t linebuf[2][160 * 2]; static uint8_t dma_busy; void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi-Instance SPI1) { dma_busy 0; /* 当前行DMA发送完成 */ } } void lcd_refresh_line_rows(uint16_t y0, uint8_t rows) { for (uint16_t r 0; r rows; r) { uint16_t y y0 r; /* 这里填充linebuf[0]第y行的颜色数据 */ for (int i 0; i 160; i) { linebuf[0][i * 2] 0xF8; /* 红色行数据示例 */ linebuf[0][i * 2 1] 0x00; } LCD_CS_LOW; LCD_DC_HIGH; dma_busy 1; HAL_SPI_Transmit_DMA(hspi1, linebuf[0], 160 * 2); while (dma_busy); /* 等当前行发完再切下一行 */ } }帧率的理论上限可以直接算9MHz时钟下320字节需要284µs整屏128行共36.4ms约27FPS。上面代码每次等待DMA完成会损失一点切换时间实测能到20FPS左右。想再快就用乒乓缓冲发送linebuf[0]时填充linebuf[1]DMA完成回调里交换指针把等待时间从关键路径上拿掉。代价是代码复杂度上升刷屏函数和DMA回调之间要做好状态同步。6.2 帧率和CPU占用怎么测测帧率不需要外接任何仪器HAL_GetTick提供毫秒级时间戳刷一屏前后各打一次点就能算出消耗时间。把刷新函数放进主循环用SysTick计时while (1) { uint32_t t0 HAL_GetTick(); lcd_refresh_line_rows(0, 128); uint32_t cost HAL_GetTick() - t0; /* cost就是整屏刷新耗时单位ms */ }用CubeIDE的调试器在cost变量上打断点或者用串口把数值打印出来反复复位看数据是否稳定。如果一屏耗时超过60ms说明不是DMA没生效就是还有额外等待如果在等待DMA完成后又调了HAL_Delay时间会被叠加进刷新周期画面上出现明显的停顿感。想测CPU占用就把刷新任务和另一个LED翻转任务时间片对比DMA让出来的时间足够跑一些轻量计算和触摸扫描。6.3 验证手段渐变填充和棋盘格不闪屏幕驱动改完、时序调完最后一件事是验证显示效果。最有效的是两种画面16位颜色渐变和棋盘格。渐变用R分量从0递增到31、G分量从0递增到63、B分量从0递增到31逐像素写入能看出每个颜色通道有没有断档也能确认RGB565的字节顺序是否对。棋盘格用16×16像素的格子交替红蓝屏幕出现规则方格说明坐标窗口计算没有累积误差同时肉眼观察刷新过程有没有闪烁。把前面这些代码串起来LCD驱动在STM32CubeIDE里就算是完全跑通了。后续真正做界面时只需要在这一层往上面叠加图形库或者控件逻辑底层SPI收发和DMA切换不会再动。F103在9MHz SPI下做到20FPS的纯色刷新余量已经够大多数仪表盘和菜单界面使用如果还需要更快换192MHz主频的F303或者带硬件FSMC接口的F407刷新路径从代码层迁移到内存映射层那是另一个方案了。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。