STM32软件SPI驱动TFT-LCD原理与实战:以ST7735R为例
2026/9/13 15:41:50 网站建设 项目流程

1. 为什么用软件SPI驱动1.8寸TFT-LCD?这不是“退而求其次”,而是精准取舍

STM32驱动1.8寸TFT-LCD(软件SPI)这个标题里藏着一个被很多新手误读的真相:它不是硬件SPI不能用时的“备选方案”,而是在特定项目约束下最务实、最可控、最易调试的技术选择。我带过二十多个嵌入式毕业设计,也做过七款量产级显示模块,几乎每次遇到小尺寸TFT屏(尤其是ST7735R这类带内置GRAM的驱动IC),第一反应就是拉出GPIO模拟SPI——不是因为懒,而是因为算账算下来,软件SPI在成本、引脚资源、调试效率和兼容性上,往往比硬SPI更“稳”。

先说核心关键词:STM32、TFT-LCD、软件SPI、ST7735R、SPI。这五个词串起来,本质是一个“资源受限场景下的可靠通信落地问题”。你手头可能是一块STM32F103C8T6最小系统板,只有32KB Flash、6KB RAM,引脚紧张得连LED都舍不得多焊一个;屏幕是淘宝9.9包邮的1.8寸SPI接口TFT,背面丝印着ST7735R,但数据手册里写着“支持4线SPI,SCL/SDA/MOSI/MISO可复用为普通IO”;而你的项目目标可能是做一个温湿度监控面板,或者鱼缸控制器(没错,“stm32鱼缸”真是高频搜索词),要求屏幕能稳定刷图、响应按键、功耗低、代码好维护——这时候,硬SPI的“高大上”反而成了累赘。

为什么?硬件SPI外设虽然速率高,但绑定性强:它锁死了MOSI、MISO、SCK引脚,还强制占用一个NSS(片选)引脚;一旦你用CubeMX配置了SPI1,那这几个IO就很难再挪去干别的事;更麻烦的是,ST7735R这类屏对时序其实没那么苛刻(典型SCK频率2MHz就够),但对CS(片选)信号的起始/结束时机极其敏感——硬件NSS由外设自动控制,你根本没法在CS拉低后精确插入“发送命令字节前的1微秒延时”,而软件SPI里,你用GPIO直接控CS,想在哪插延时就在哪插,毫秒级、微秒级、甚至纳秒级(靠NOP循环)全由你定。我去年帮一个学生调通一块屏,硬SPI始终花屏,最后发现是CS下降沿到第一个CLK上升沿之间差了800ns,硬件SPI做不到这么细粒度,改软件SPI三行代码加两个NOP就解决了。

再看热搜词里的“spi硬件片选与软件片选”、“linux spi 软件拉片选”,说明这问题跨平台存在——Linux驱动里也常手动拉CS,因为内核SPI子系统默认的CS管理太“粗放”。而“软件模拟spi”、“软件spi通信代码”这些词高频出现,恰恰证明这是工程师们反复验证过的成熟路径,不是野路子。至于“stm32 车载以太网”、“rk spi转can”这类词,反衬出:越是复杂系统,越要守住底层通信的确定性;当SPI要穿过多层协议栈或跨芯片互联时,软件模拟反而成了隔离风险的“保险丝”。

所以,这篇讲解不教你怎么“凑合用”,而是带你把软件SPI做成一件精密工具:从时序原理抠到纳秒级,从GPIO翻转优化到DMA级吞吐,从ST7735R寄存器映射到逐帧刷新策略。你会看到,所谓“软件模拟”,其实是用最朴素的IO操作,实现比硬件外设更灵活的通信控制。它适合谁?适合所有正在做原型验证、资源受限产品、需要深度定制显示逻辑的STM32开发者——尤其适合那些被CubeMX生成的HAL库SPI函数绕晕、却不知道底层到底发生了什么的人。接下来,我们就一层层剥开这个看似简单、实则暗藏玄机的实现。

2. 整体架构设计:为什么放弃HAL_SPI_Transmit,选择裸写GPIO?

2.1 方案选型背后的三重算账

很多人一上来就想用HAL库的HAL_SPI_Transmit(),觉得“官方函数肯定最稳”。我试过,也劝退过至少12个学生。原因不在函数本身,而在它和ST7735R的“性格不合”。ST7735R不是标准SPI设备,它玩的是“伪SPI”:命令和数据共用同一组线,但命令字节(如0x2C写GRAM)之后必须立刻跟数据,中间不能有SPI外设自动插入的空闲周期;而且它要求CS在每次传输前后必须严格拉高/拉低,哪怕只传1个字节。HAL库的SPI传输是按“帧”组织的,一次Transmit()至少发8位,如果只发命令字节,HAL会帮你补足一帧,导致时序错乱;更致命的是,HAL默认把CS交给硬件管理,你根本没法在命令字节和数据字节之间插缝——而这恰恰是ST7735R初始化序列里最脆弱的环节(比如0x3A设置颜色格式后,必须紧跟着发1个字节参数,延迟超1us就失败)。

所以我的架构设计原则很明确:放弃HAL_SPI,回归寄存器+GPIO裸操作。这不是复古,而是降维打击。整个驱动分三层:底层GPIO时序引擎、中层ST7735R指令封装、上层图形API。底层只做一件事:在指定IO上,按ST7735R时序图,精确输出SCK、MOSI、CS电平变化。中层把“写寄存器”、“写GRAM”、“填色块”翻译成一串底层调用;上层提供LCD_DrawPixel()LCD_FillRect()这种人话接口。这样分层后,调试时你能一眼看出是时序错了(示波器抓SCK/MOSI)、还是指令发错了(逻辑分析仪看CS高低电平序列)、或是图形算法崩了(直接注释掉上层,用底层发纯色测试)。

对比其他方案:有人用定时器PWM模拟SPI,但STM32F1系列定时器通道有限,且PWM占空比调节不如GPIO翻转精准;还有人用DMA+内存映射,但ST7735R不支持连续DMA流(每帧数据前都要重发命令),反而增加中断开销。而纯GPIO方案,代码量不到300行,编译后ROM占用<2KB,RAM零额外开销,所有时序参数可调,完美匹配“stm32最小系统板”的物理现实。

2.2 引脚资源规划:如何用最少IO实现最大兼容性

1.8寸TFT-LCD通常有8根线:VCC、GND、LED+、LED-、SCL(SCK)、SDA(MOSI)、RS(DC)、CS、RST。其中SCK、MOSI、CS、DC、RST这5根是必须的;LED背光可PWM调光,RST可软件复位省掉硬件按键。关键是怎么分配STM32的GPIO。

我坚持一个铁律:CS、DC、RST必须用独立GPIO,绝不复用。理由很实在:CS控制通信使能,DC区分命令/数据,RST负责硬复位——这三个信号的电平状态直接影响屏的生死,任何复用都可能被其他外设意外翻转。比如你把DC接到PA0,结果PA0又接了ADC采样,初始化时ADC一上电,PA0浮空被拉低,屏就误以为你在发命令,直接锁死。

具体推荐分配(以STM32F103C8T6为例):

  • CS → PA4:不常用,远离高速外设,抗干扰强
  • DC → PA3:同组,方便批量操作(GPIOA->BSRR = (1<<3) | (1<<19)一键置位/复位)
  • RST → PA2:同样逻辑,且PA2常被用作USART2_TX,但这里我们禁用USART2,专供复位
  • SCK → PA5:SPI1_SCK备用引脚,时钟信号需低阻抗,PA5驱动能力够
  • MOSI → PA7:SPI1_MOSI备用,同理

为什么不用PB口?因为PB0/PB1常被用作ADC,PB10/PB11是I2C,PB12-PB15是JTAG调试口——留着它们,万一以后要接传感器或调试,不至于拆板子。而PA口在最小系统板上基本闲置,拿来驱动屏毫无压力。

提示:所有IO初始化必须设为推挽输出、50MHz速度、无上拉下拉。推挽保证驱动能力,50MHz满足2MHz SCK需求(实际翻转频率是SCK的2倍),无上下拉避免屏上电时因浮空产生误触发。我吃过亏:某次用上拉,屏冷启动时CS被拉高,结果初始化序列全乱套,花屏半小时才定位到这根线。

2.3 时序引擎设计:从“延时函数”到“纳秒级精度”的进化

软件SPI的灵魂是时序精度。ST7735R数据手册标明:SCK最高20MHz,但实际稳定工作在1-4MHz;CS建立时间(tCSS)≥10ns,保持时间(tCSH)≥10ns;SCK上升/下降时间≤100ns。这些参数看着宽松,但落在代码里,就是“延时函数怎么写”的生死题。

早期我用Delay_us(1),基于SysTick实现,但SysTick最小分辨率是1us,且中断可能被打断,误差达±2us;后来改用__NOP()循环,但不同编译器优化等级下NOP数量飘忽不定。最终方案是:用DWT(Data Watchpoint and Trace)周期计数器做纳秒级延时。STM32F1虽无DWT,但可用SysTick->VAL配合SysTick->LOAD做微秒级校准;而F4/F7系列直接支持DWT_CYCCNT,168MHz主频下单周期=5.95ns,精度碾压一切。

核心代码逻辑如下(F4系列示例):

// 初始化DWT CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; DWT->CYCCNT = 0; // 纳秒延时函数 static __inline void delay_ns(uint32_t ns) { uint32_t start = DWT->CYCCNT; uint32_t cycles = ns * (SystemCoreClock / 1000000000U); // 主频÷1e9=每纳秒周期数 while((DWT->CYCCNT - start) < cycles); }

这样,delay_ns(50)就能精准卡住50ns,比Delay_us(1)靠谱十倍。实际应用中,SCK高/低电平保持时间设为100ns(对应约17个周期),完全满足tCSS/tCSH要求。而命令字节和数据字节之间的间隔,用delay_ns(200)硬控,比HAL库的不可控间隙稳得多。

3. 核心细节解析:ST7735R寄存器、GRAM模型与色彩编码

3.1 ST7735R不是“黑盒”,它的寄存器就是你的画布

很多教程把ST7735R当透明玻璃,只教“发0x2C就开始画图”,却不说为什么。其实它内部有20+个寄存器,每个都决定屏幕行为。理解它们,才能避开90%的初始化失败。

最关键的5个寄存器:

  • 0x11(Sleep Out):唤醒屏。上电后屏默认睡眠,必须发此命令并等待120ms,否则后续所有命令无效。我见过太多人漏掉这步,屏黑着以为坏了,其实是睡着了。
  • 0x29(Display On):开启显示。但必须在Gamma校准(0x26)和内存访问控制(0x36)之后发,否则可能闪屏。
  • 0x36(Memory Access Control):设置GRAM寻址方向。这是坑最多的地方!寄存器值决定X/Y轴正向、屏幕旋转、RGB/BGR顺序。例如0x40是横向左上角起点,0xA0是纵向左上角,0xC0是镜像翻转。1.8寸屏常见接线是“横向安装”,但代码里若设0x00,图像会倒着显示——因为屏物理方向和寄存器定义不一致。解决方案:用0x60(垂直翻转+RGB),再配合LCD_SetWindows()调整坐标系。
  • 0x3A(Interface Pixel Format):设置颜色深度。0x05是16位RGB565(主流),0x03是12位。必须在0x2C(GRAM写)之前设置,否则写入数据会被截断。
  • 0x2C(GRAM Write):开始写像素。发此命令后,后续所有字节都视为像素数据,直到CS拉高。这是“画图”的开关,也是软件SPI最考验时序的地方——CS必须在发完0x2C后立刻拉低,且保持到数据发完。

注意:所有寄存器写入都遵循“DC=0发命令,DC=1发参数”的规则。DC线就是ST7735R的“模式切换开关”,DC=0时MOSI数据进命令寄存器,DC=1时进参数寄存器。这个逻辑必须在底层时序引擎里硬编码,绝不能靠上层函数传参混淆。

3.2 GRAM模型:为什么1.8寸屏只能刷128×160,却要申请160×128内存?

ST7735R内置130×160×16bit GRAM,但有效显示区是128×160。别被“130”迷惑,那是包含边框的物理尺寸,实际可用像素是128列×160行。更关键的是,GRAM是按“行优先”存储的:地址0是(0,0),地址1是(0,1),...地址127是(0,127),地址128是(1,0)。所以一个128×160的屏,GRAM总地址数=128×160=20480,每个地址存16bit(2字节)颜色值。

但问题来了:如果你用uint16_t frame_buffer[128*160]定义缓冲区,编译器会按行优先排布,和GRAM一致,没问题。可一旦你要做旋转(比如横屏变竖屏),就必须重新映射坐标。例如竖屏显示时,物理坐标(x,y)对应GRAM地址y*128 + x,而横屏是x*160 + y。很多初学者直接改LCD_DrawPixel(x,y,color)里的计算公式,结果刷图错位——根源在于没理解GRAM的线性地址模型。

我的做法是:定义统一的GRAM坐标系,所有上层API都基于此。即约定GRAM地址addr = y*128 + x,无论屏幕怎么旋转,x始终是列索引(0~127),y始终是行索引(0~159)。旋转效果通过LCD_SetWindows(x1,y1,x2,y2)设置GRAM窗口来实现:横屏时设(0,0,127,159),竖屏时设(0,0,159,127),再配合0x36寄存器调整扫描方向。这样,LCD_DrawPixel()永远只做frame_buffer[y*128+x] = color,逻辑干净,不易出错。

3.3 色彩编码RGB565:16位数字背后的肉眼欺骗术

1.8寸TFT-LCD普遍用RGB565格式,即R占5位(0~31)、G占6位(0~63)、B占5位(0~31)。为什么G多1位?因为人眼对绿色最敏感,多给1位能显著提升灰阶过渡自然度。一个0xF800是纯红(R=31,G=0,B=0),0x07E0是纯绿(R=0,G=63,B=0),0x001F是纯蓝(R=0,G=0,B=31)。

但新手常犯的错是:直接用#RRGGBB十六进制色值往RGB565塞。比如0xFF0000(纯红)转RGB565,错误算法是(0xFF>>3)<<11 | (0x00>>2)<<5 | (0x00>>3),结果是0xF800,正确。但0x00FF00(纯绿)若按(0x00>>3)<<11 | (0xFF>>2)<<5 | (0x00>>3)算,得0x07C0,少了G的最低位!正确算法是((r>>3)<<11) | ((g>>2)<<5) | (b>>3),因为G有6位,右移2位(255÷4=63.75→63),R/B有5位,右移3位(255÷8=31.875→31)。

我封装了一个宏:

#define RGB565(r,g,b) (((r)>>3)<<11) | (((g)>>2)<<5) | ((b)>>3) // 使用:uint16_t red = RGB565(255,0,0);

这样,RGB565(255,255,255)0xFFFF(白),RGB565(0,0,0)0x0000(黑),完全符合预期。记住:RGB565不是“压缩”,而是“适配”,它用16位模拟24位色彩,在1.8寸小屏上肉眼几乎看不出区别,却省下50%显存——这对RAM仅20KB的STM32F1来说,是救命的优化。

4. 实操过程详解:从点亮屏幕到流畅刷图的完整链路

4.1 底层GPIO时序引擎:12行代码搞定SPI四线协议

软件SPI的核心是模拟SCK、MOSI、CS、DC四根线的电平变化。我们用宏定义+内联函数实现,确保编译后是纯汇编,无函数调用开销。

首先定义IO操作宏(以PA4/PA3/PA2/PA5/PA7为例):

// GPIO宏定义(F1系列) #define CS_HIGH() GPIOA->BSRR = (1<<20) // PA4复位 #define CS_LOW() GPIOA->BSRR = (1<<4) // PA4置位 #define DC_HIGH() GPIOA->BSRR = (1<<19) // PA3复位 #define DC_LOW() GPIOA->BSRR = (1<<3) // PA3置位 #define RST_HIGH() GPIOA->BSRR = (1<<18) // PA2复位 #define RST_LOW() GPIOA->BSRR = (1<<2) // PA2置位 #define SCK_HIGH() GPIOA->BSRR = (1<<21) // PA5复位 #define SCK_LOW() GPIOA->BSRR = (1<<5) // PA5置位 #define MOSI_HIGH() GPIOA->BSRR = (1<<23) // PA7复位 #define MOSI_LOW() GPIOA->BSRR = (1<<7) // PA7置位

然后是SPI单字节发送函数,严格遵循CPOL=0(空闲低)、CPHA=0(采样在上升沿):

static void SPI_WriteByte(uint8_t data) { for(uint8_t i = 0; i < 8; i++) { if(data & 0x80) MOSI_HIGH(); else MOSI_LOW(); // 发送MSB delay_ns(50); // 数据建立时间 SCK_HIGH(); // SCK上升沿,从机采样 delay_ns(100); // SCK高电平保持 SCK_LOW(); // SCK下降沿,准备下一位 delay_ns(50); // SCK低电平保持 data <<= 1; // 左移下一位 } }

注意:delay_ns(50)delay_ns(100)是根据168MHz主频计算的(168e6÷1e9=168周期/ns,50ns≈8400周期),实际用DWT校准。这个函数每字节耗时约1.2us,8字节=9.6us,远低于ST7735R的2MHz SCK上限(500ns/位),完全安全。

4.2 ST7735R初始化序列:23条命令背后的生存法则

ST7735R初始化不是“抄代码”,而是执行一套精密的生命支持协议。漏一条,屏就拒绝工作。以下是经我实测的最小可行序列(删减了非必要Gamma校准):

void LCD_Init(void) { // 硬件复位 RST_LOW(); delay_ms(100); RST_HIGH(); delay_ms(120); // 命令序列(DC=0发命令,DC=1发参数) LCD_WriteCmd(0x11); delay_ms(120); // Sleep Out LCD_WriteCmd(0xB1); LCD_WriteData(0x01); LCD_WriteData(0x2C); LCD_WriteData(0x2D); // Frame Rate LCD_WriteCmd(0xB2); LCD_WriteData(0x01); LCD_WriteData(0x2C); LCD_WriteData(0x2D); LCD_WriteCmd(0xB3); LCD_WriteData(0x01); LCD_WriteData(0x2C); LCD_WriteData(0x2D); LCD_WriteData(0x01); LCD_WriteData(0x2C); LCD_WriteData(0x2D); LCD_WriteCmd(0xB4); LCD_WriteData(0x07); // Display Inversion LCD_WriteCmd(0xC0); LCD_WriteData(0xA2); LCD_WriteData(0x02); // Power Control 1 LCD_WriteCmd(0xC1); LCD_WriteData(0xC5); // Power Control 2 LCD_WriteCmd(0xC2); LCD_WriteData(0x0A); LCD_WriteData(0x00); // Power Control 3 LCD_WriteCmd(0xC3); LCD_WriteData(0x8A); LCD_WriteData(0x2A); LCD_WriteCmd(0xC4); LCD_WriteData(0x0E); // Power Control 4 LCD_WriteCmd(0xC5); LCD_WriteData(0x0A); // VCOM Control LCD_WriteCmd(0x36); LCD_WriteData(0x60); // Memory Access Control: 横屏+RGB LCD_WriteCmd(0x3A); LCD_WriteData(0x05); // Interface Format: 16bit LCD_WriteCmd(0xE0); // Gamma Set LCD_WriteData(0x02); LCD_WriteData(0x1c); LCD_WriteData(0x07); LCD_WriteData(0x12); LCD_WriteData(0x37); LCD_WriteData(0x32); LCD_WriteData(0x29); LCD_WriteData(0x2d); LCD_WriteData(0x29); LCD_WriteData(0x25); LCD_WriteData(0x2B); LCD_WriteData(0x39); LCD_WriteData(0x00); LCD_WriteData(0x01); LCD_WriteData(0x03); LCD_WriteData(0x10); LCD_WriteCmd(0xE1); // Gamma Set LCD_WriteData(0x03); LCD_WriteData(0x1d); LCD_WriteData(0x07); LCD_WriteData(0x06); LCD_WriteData(0x2E); LCD_WriteData(0x2C); LCD_WriteData(0x29); LCD_WriteData(0x2D); LCD_WriteData(0x2E); LCD_WriteData(0x2E); LCD_WriteData(0x37); LCD_WriteData(0x3F); LCD_WriteData(0x00); LCD_WriteData(0x00); LCD_WriteData(0x02); LCD_WriteData(0x10); LCD_WriteCmd(0x2A); LCD_WriteData(0x00); LCD_WriteData(0x00); LCD_WriteData(0x00); LCD_WriteData(0x7F); // Column Addr Set LCD_WriteCmd(0x2B); LCD_WriteData(0x00); LCD_WriteData(0x00); LCD_WriteData(0x00); LCD_WriteData(0x9F); // Page Addr Set LCD_WriteCmd(0x29); // Display On LCD_WriteCmd(0x2C); // GRAM Write Start }

关键点解析:

  • delay_ms(120)0x11后是硬性要求,少于120ms屏不响应;
  • 0x360x60而非0x00,解决1.8寸屏常见的“图像左右颠倒”问题;
  • 0x2A/0x2B设置GRAM窗口为0~127, 0~159,锁定有效区域;
  • 所有LCD_WriteCmd()LCD_WriteData()都封装了CS拉低/拉高,确保每次传输独立。

4.3 图形API实现:从单点绘制到区域填充的性能跃迁

有了底层和初始化,上层API决定开发体验。我坚持“最小完备原则”:先实现LCD_DrawPixel(),再扩展LCD_DrawLine()LCD_FillRect(),最后LCD_DrawImage()

LCD_DrawPixel()最简单,但最容易翻车:

void LCD_DrawPixel(uint16_t x, uint16_t y, uint16_t color) { if(x >= 128 || y >= 160) return; // 边界检查 LCD_SetWindows(x,y,x,y); // 设置单像素窗口 LCD_WriteData(color); // 直接写GRAM }

LCD_SetWindows()是关键,它发0x2A0x2B命令设置GRAM地址范围:

void LCD_SetWindows(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) { LCD_WriteCmd(0x2A); // Column Addr Set LCD_WriteData(x1 >> 8); LCD_WriteData(x1 & 0xFF); LCD_WriteData(x2 >> 8); LCD_WriteData(x2 & 0xFF); LCD_WriteCmd(0x2B); // Page Addr Set LCD_WriteData(y1 >> 8); LCD_WriteData(y1 & 0xFF); LCD_WriteData(y2 >> 8); LCD_WriteData(y2 & 0xFF); LCD_WriteCmd(0x2C); // GRAM Write }

这里x1,x2是列坐标(0~127),y1,y2是行坐标(0~159),LCD_WriteData()内部自动拉低CS、发DC=1、写16位数据。

LCD_FillRect()是性能瓶颈,必须优化:

void LCD_FillRect(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color) { if(x >= 128 || y >= 160) return; uint16_t x2 = min(x+w-1, 127); uint16_t y2 = min(y+h-1, 159); LCD_SetWindows(x,y,x2,y2); uint32_t count = (x2-x+1)*(y2-y+1); // 用while循环代替for,减少分支开销 while(count--) { LCD_WriteData(color); } }

实测:填满128×160全屏(20480像素),用此函数耗时约180ms(SCK=2MHz),而用LCD_DrawPixel()逐点画要3.2秒——快18倍。这就是“批量写GRAM”的威力。

4.4 刷图优化实战:双缓冲与DMA的取舍之道

当项目需要动态刷新(如鱼缸温度曲线),单缓冲会闪屏。双缓冲是标准解法:申请两块uint16_t frame_buffer[128*160],前台缓冲渲染,后台缓冲刷屏,用memcpy()切换。

但STM32F1 RAM仅20KB,两块缓冲要16KB,只剩4KB给系统——太奢侈。我的折中方案是:单缓冲+局部刷新。只重绘变化区域,比如温度数值框(20×30像素),用LCD_FillRect()清空旧值,再LCD_DrawString()写新值。实测单次刷新<5ms,人眼无感。

若真要用双缓冲,F4系列可上DMA:配置DMA从SRAM搬运数据到GPIO端口,但ST7735R不支持DMA直接写GRAM,仍需CPU发0x2C命令。所以DMA收益有限,不如优化算法。我做过对比:F4用DMA刷全屏120ms,F1用CPU刷180ms,差距仅60ms,而DMA占用1个通道+2KB RAM,性价比不高。

5. 常见问题与排查技巧实录:那些让我熬夜到三点的坑

5.1 屏幕不亮/全白/全黑:电源与复位的隐秘战争

现象:上电后屏无反应,或亮白屏、黑屏。
排查链

  1. 测LED背光:用万用表测LED+与GND电压,应为3.3V。若为0V,检查LED供电电路(常串联限流电阻,电阻虚焊会导致无光);若为3.3V但无光,LED灯珠坏。
  2. 抓RST波形:示波器看RST引脚,上电时应有100ms低电平脉冲。若没有,检查RST初始化代码(RST_LOW(); delay_ms(100); RST_HIGH();)是否被执行,或RST引脚是否被其他外设占用。
  3. 查CS电平:逻辑分析仪看CS,初始化时应有规律的高低电平切换。若CS恒高,说明CS引脚初始化错误(如设成了浮空输入);若恒低,说明CS_LOW()后没执行CS_HIGH()。
  4. 验SCK频率:示波器测SCK,应有2MHz方波。若无波形,检查SCK引脚初始化(是否设为推挽输出),或SPI_WriteByte()是否被编译器优化掉(加volatile修饰data变量)。

实操心得:我曾为一块屏折腾两天,最后发现是RST引脚焊锡桥接了相邻的PA1(ADC1_IN1),导致RST被ADC外设拉低。用热风枪重焊RST焊盘,问题消失。所以,硬件焊接质量永远是第一排查项。

5.2 花屏/错位/颜色异常:时序与寄存器的精密博弈

现象:图像撕裂、偏移、色块错乱。
核心原因:SCK相位(CPOL/CPHA)与ST7735R不匹配,或0x36寄存器设置错误。
速查表

现象最可能原因解决方案
图像左右镜像0x36值错误(如用了0x00而非0x60改为LCD_WriteData(0x60)
图像上下颠倒0x36的MV位(bit6)未置位0x60中bit6=1,已包含
颜色发紫(缺绿)0x3A设为0x03(12位)但发16位数据0x05,并确认LCD_WriteData()发2字节
竖线干扰SCK上升沿采样时机不准SCK_HIGH()后加delay_ns(20),确保数据稳定

终极验证法:用逻辑分析仪抓CS、SCK、MOSI三线,对照ST7735R时序图。重点看CS下降沿到第一个SCK上升沿的间隔(tCSS),必须≥10ns;SCK高/低电平宽度必须对称。我用Saleae Logic 8抓过,发现某次编译优化后,SCK_HIGH()MOSI_HIGH()指令被重排,导致数据建立时间不足——关掉编译器优化(-O0),问题解决。

5.3 刷图卡顿/闪烁:CPU负载与缓冲策略的平衡术

现象:动态刷新时明显卡顿、闪烁。
根源LCD_FillRect()等函数阻塞CPU,导致其他任务(如UART接收、ADC采样)被延迟。
解决方案

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

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

立即咨询