
1. 为什么这块0.96寸IPS屏值得你花时间搞定——不是所有小屏都叫“真彩”你手上那块标着“0.96寸IPS”的小屏幕大概率不是淘宝上十几块钱包邮的廉价TN屏。它背后是ST7735S驱动芯片走SPI接口分辨率160×80支持16位RGB565真彩色显示——这意味着它能同时显示2^1665536种颜色文字边缘不发虚图标有层次感哪怕只显示一个温度数值背景渐变也能看得出过渡。我第一次把STM32F103C8T6俗称“蓝丸”接上这块屏时用ILI9341的初始化代码硬套结果屏幕全白换用ST7735S专用序列后第一帧“Hello STM32”弹出来那一刻我才真正意识到驱动小屏不是“通电就能亮”而是要和芯片手册里每一个寄存器、每一组时序、每一段延时死磕。这项目的核心价值远不止于“让屏幕亮起来”。它是一把钥匙——打开嵌入式GUI开发的第一道门。你后续想移植LVGL做图形界面想加触摸实现交互想用DMA刷图不卡主循环甚至想把温湿度数据做成动态波形图所有这些都建立在“你能稳稳控制像素”这个基本功之上。而ST7735SSPI组合恰恰是入门门槛最低、资料最全、调试最直观的实战路径。它不像OLED需要I²C地址冲突排查也不像大尺寸LCD动辄要SDRAM控制器就四根线SCK、MOSI、CS、DC外加复位和背光控制硬件接线干净利落。我见过太多人卡在“屏幕不亮”这一步反复检查杜邦线、怀疑MCU损坏、重装Keil5芯片包……其实问题往往藏在三个地方SPI模式配置错CPOL/CPHA设反、初始化序列漏写关键寄存器比如Gamma校准、或者CS片选信号没拉低到位。这篇文章就是帮你把这三个坑一次性踩平。适合谁来跟着做如果你正在用STM32做毕业设计比如数字温湿度计、智能台灯、空气质量检测仪需要一块带色彩的显示屏提升作品质感如果你刚学完STM32 GPIO和定时器想找个“看得见摸得着”的项目验证学习成果或者你手头有块闲置的0.96寸IPS屏模块包装盒上印着ST7735S但找不到中文手册——那你来对了。不需要懂LVGL、不用配FreeRTOS从裸机开始一行行代码敲出来最后看到自己写的字符在屏幕上清晰显示那种掌控感比看任何教程都实在。2. 硬件连接与底层驱动设计SPI不是接上线就完事2.1 模块引脚定义与STM32引脚映射——别被丝印误导市面上常见的0.96寸IPS屏模块背面丝印通常标着“VCC、GND、SCL、SDA、RES、DC、CS、BLK”但这里有个经典陷阱“SCL/SDA”只是商家沿用OLED的习惯命名实际对应的是SPI的SCK时钟和MOSI主出从入。必须按ST7735S真实功能重新定义模块丝印实际功能推荐STM32引脚以F103C8T6为例关键说明VCC电源3.3VPA103.3V输出严禁接5VST7735S核心电压为3.3V5V会永久击穿GND地PA9GND与MCU共地避免信号抖动SCLSPI_SCKPA5SPI1_SCK必须接MCU硬件SPI时钟引脚软件模拟SPI会严重拖慢刷新率SDASPI_MOSIPA7SPI1_MOSI同上硬件SPI才能保证10MHz以上速率RES复位低电平有效PC13普通GPIO需外接10kΩ上拉电阻到3.3V否则上电瞬间可能不复位DC数据/命令选择PB0普通GPIO高电平写数据低电平写命令必须用推挽输出CS片选低电平有效PA4SPI1_NSS或PB12SPI2_NSS强烈推荐用硬件NSS避免软件控制CS导致SPI总线冲突BLK背光控制PB1PWM输出接100Ω限流电阻再驱动LED直接接MCU引脚易烧毁提示我实测过如果CS不用硬件NSS即PA4而是用任意GPIO软件拉低当SPI传输过程中发生中断比如串口接收CS可能提前释放导致ST7735S误判为新指令起始画面出现撕裂或乱码。这是新手最常忽略的硬件细节。2.2 STM32 SPI外设深度配置——时钟极性与相位是命门ST7735S要求SPI工作在Mode 0CPOL0, CPHA0空闲时SCK为低电平数据在SCK第一个上升沿采样。很多教程直接复制ILI9341的Mode 3配置CPOL1, CPHA1结果屏幕全黑。配置代码必须严格对应// 使用HAL库配置SPI1F103C8T6 SPI_HandleTypeDef hspi1; hspi1.Instance SPI1; hspi1.Init.Mode SPI_MODE_MASTER; // 主机模式 hspi1.Init.Direction SPI_DIRECTION_2LINES; // 全双工虽ST7735S只用MOSI hspi1.Init.DataSize SPI_DATASIZE_8BIT; // 每次传输8位 hspi1.Init.CLKPolarity SPI_POLARITY_LOW; // CPOL0空闲时SCK为低 hspi1.Init.CLKPhase SPI_PHASE_1EDGE; // CPHA0上升沿采样 hspi1.Init.NSS SPI_NSS_HARD_OUTPUT; // 硬件NSS由PA4自动控制 hspi1.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_4; // APB236MHz → SCK9MHz hspi1.Init.FirstBit SPI_FIRSTBIT_MSB; // MSB先传符合ST7735S要求 hspi1.Init.TIMode SPI_TIMODE_DISABLED; hspi1.Init.CRCCalculation SPI_CRCCALCULATION_DISABLED; if (HAL_SPI_Init(hspi1) ! HAL_OK) { Error_Handler(); // 初始化失败处理 }关键参数解析BaudRatePrescaler4APB2总线频率36MHz分频后SCK9MHz。ST7735S最大支持15MHz但实测9MHz最稳定超过12MHz时部分劣质模块会出现花屏。NSS设置为HARD_OUTPUT这是启用硬件片选的关键。若设为SOFTHAL_SPI_Transmit函数不会自动拉低CS需手动控制GPIO极易出错。FirstBitMSBST7735S所有指令和数据均MSB先行若设为LSB整个显示逻辑全乱。2.3 屏幕初始化序列——不是抄代码是读懂时序ST7735S的初始化不是简单发送几条指令而是一套精密的“唤醒流程”。官方英文手册第117页明确要求上电后必须等待≥10ms再发复位脉冲≥10ms低电平复位后等待≥120ms才能发第一条初始化指令。很多代码省略了这些延时导致屏幕偶发不亮。完整序列如下已按手册时序补全关键延时// 初始化函数核心步骤HAL库 void LCD_Init(void) { LCD_GPIO_Init(); // 初始化GPIO LCD_SPI_Init(); // 初始化SPI // 1. 硬件复位拉低RES至少10ms HAL_GPIO_WritePin(LCD_RES_GPIO_Port, LCD_RES_Pin, GPIO_PIN_RESET); HAL_Delay(15); // 留足余量 HAL_GPIO_WritePin(LCD_RES_GPIO_Port, LCD_RES_Pin, GPIO_PIN_SET); HAL_Delay(120); // 等待内部电路稳定 // 2. 发送初始化指令关键寄存器逐条写入 LCD_WriteCommand(0x01); // Software Reset HAL_Delay(150); LCD_WriteCommand(0x11); // Sleep Out HAL_Delay(150); LCD_WriteCommand(0x20); // Inversion Off关闭显示反转 LCD_WriteCommand(0x36); // Memory Data Access Control LCD_WriteData(0xC0); // 0xC0竖屏、RGB顺序、顶左起点适配0.96寸方向 LCD_WriteCommand(0x3A); // Interface Pixel Format LCD_WriteData(0x05); // 16-bit RGB565必须 LCD_WriteCommand(0xB2); // Porch Control LCD_WriteData(0x0C); LCD_WriteData(0x0C); LCD_WriteData(0x00); LCD_WriteData(0x33); LCD_WriteData(0x33); LCD_WriteCommand(0xB7); // Gate Control LCD_WriteData(0x35); LCD_WriteCommand(0xBB); // VCOMS Setting LCD_WriteData(0x19); LCD_WriteCommand(0xC0); // Power Control 1 LCD_WriteData(0x2C); LCD_WriteCommand(0xC1); // Power Control 2 LCD_WriteData(0x01); LCD_WriteCommand(0xC2); // NV Memory Write LCD_WriteData(0x12); LCD_WriteData(0x3B); LCD_WriteCommand(0xC3); // NV Memory Write LCD_WriteData(0x00); LCD_WriteData(0x13); LCD_WriteCommand(0xC4); // NV Memory Write LCD_WriteData(0x0A); LCD_WriteData(0x00); LCD_WriteCommand(0xC6); // Gamma Control LCD_WriteData(0x0F); LCD_WriteCommand(0xD0); // Power Control 3 LCD_WriteData(0xA4); LCD_WriteData(0xA1); LCD_WriteCommand(0xE0); // Positive Gamma Correction LCD_WriteData(0xD0); LCD_WriteData(0x04); LCD_WriteData(0x0D); LCD_WriteData(0x11); LCD_WriteData(0x13); LCD_WriteData(0x2B); LCD_WriteData(0x3F); LCD_WriteData(0x54); LCD_WriteData(0x4C); LCD_WriteData(0x4E); LCD_WriteData(0x1F); LCD_WriteData(0x07); LCD_WriteData(0x03); LCD_WriteData(0x02); LCD_WriteData(0x10); LCD_WriteCommand(0xE1); // Negative Gamma Correction LCD_WriteData(0xD0); LCD_WriteData(0x04); LCD_WriteData(0x0C); LCD_WriteData(0x11); LCD_WriteData(0x13); LCD_WriteData(0x2C); LCD_WriteData(0x3F); LCD_WriteData(0x44); LCD_WriteData(0x51); LCD_WriteData(0x4F); LCD_WriteData(0x1F); LCD_WriteData(0x07); LCD_WriteData(0x03); LCD_WriteData(0x02); LCD_WriteData(0x10); LCD_WriteCommand(0x29); // Display On HAL_Delay(100); LCD_WriteCommand(0x2C); // Memory Write准备写像素 }注意0x36寄存器决定屏幕方向。0xC0对应竖屏160×800xA0对应横屏80×160。如果你发现文字倒着显示90%是这里设错了。另外0x2C是“写显存”指令执行后SPI发送的所有数据都会被当作像素点写入GRAM这是后续绘图的基础。3. 核心绘图函数实现从点、线到字符——像素级控制的底层逻辑3.1 坐标系与GRAM寻址——理解“160×80”如何映射到内存ST7735S的GRAM图形内存是连续的12800个16位单元160×8012800。每个单元存储一个RGB565颜色值如0xF800纯红。但它的寻址不是简单的“X×160Y”而是分块管理。要写入指定坐标(x,y)必须先设置窗口// 设置GRAM写入窗口x0,y0到x1,y1 void LCD_SetWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { LCD_WriteCommand(0x2A); // Column Address Set LCD_WriteData(x0 8); // X起始高字节 LCD_WriteData(x0 0xFF); // X起始低字节 LCD_WriteData(x1 8); // X结束高字节 LCD_WriteData(x1 0xFF); // X结束低字节 LCD_WriteCommand(0x2B); // Page Address Set LCD_WriteData(y0 8); // Y起始高字节 LCD_WriteData(y0 0xFF); // Y起始低字节 LCD_WriteData(y1 8); // Y结束高字节 LCD_WriteData(y1 0xFF); // Y结束低字节 LCD_WriteCommand(0x2C); // Memory Write激活窗口 }关键点0x2A设置列X轴0x2B设置行Y轴范围0~159X和0~79Y。执行0x2C后后续SPI发送的数据会自动按行填充窗口区域无需重复发送坐标。如果只画一个点窗口设为(x,x,y,y)画一整屏窗口设为(0,159,0,79)。3.2 单点绘制与批量填充——DMA加速的伏笔最基础的点绘制函数// 在(x,y)位置画一个点color为RGB565值 void LCD_DrawPoint(uint16_t x, uint16_t y, uint16_t color) { if(x 159 || y 79) return; // 越界保护 LCD_SetWindow(x, y, x, y); LCD_WriteData(color); }但频繁调用LCD_SetWindow开销巨大。批量填充更高效// 填充矩形区域color为填充色 void LCD_FillRect(uint16_t x, uint16_t y, uint16_t width, uint16_t height, uint16_t color) { uint16_t x_end x width - 1; uint16_t y_end y height - 1; if(x_end 159 || y_end 79) return; LCD_SetWindow(x, y, x_end, y_end); uint32_t len width * height; uint8_t data[2]; data[0] color 8; // 高字节 data[1] color 0xFF; // 低字节 // 用HAL_SPI_Transmit发送len次每次2字节 for(uint32_t i 0; i len; i) { HAL_SPI_Transmit(hspi1, data, 2, HAL_MAX_DELAY); } }实操心得我最初用HAL_SPI_Transmit单次发送2字节刷满全屏12800点耗时约1.2秒。后来改用DMA方式见后文降到180ms。但DMA需要额外配置对新手建议先用此版本跑通再优化。3.3 字符显示原理——ASCII码如何变成屏幕上的“H”显示字符本质是查表渲染。我们用8×16点阵字体每个字符占16字节// 8×16 ASCII字体数组简化版仅含0-9,A-Z const uint8_t ascii_8x16[][16] { // 0 字模 {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, // ...此处省略具体字模实际需包含全部字符 }; // 在(x,y)位置显示单个ASCII字符 void LCD_ShowChar(uint16_t x, uint16_t y, char chr, uint16_t fc, uint16_t bc) { uint8_t i, j; uint16_t k chr - ; // ASCII偏移空格为0x20 if(k 95) return; // 超出字库范围 LCD_SetWindow(x, y, x7, y15); // 8×16字符窗口 for(i 0; i 16; i) { // 行 for(j 0; j 8; j) { // 列 uint8_t pixel (ascii_8x16[k][i] (0x80 j)) ? fc : bc; LCD_WriteData(pixel); } } }原理拆解chr - 将字符转为字库索引空格ASCII32索引0。ascii_8x16[k][i]取第i行字模数据0x80 j生成列掩码操作提取该位像素。若为1填前景色fc为0填背景色bc。每个字符需发送128次16行×8列所以显示“Hello”要发640次数据。注意字模数据必须用const声明并放在Flash中否则RAM不够。F103C8T6只有20KB RAM而完整ASCII字库约2KB。4. 性能优化与进阶技巧从“能用”到“好用”4.1 DMA加速GRAM写入——告别CPU忙等HAL_SPI_Transmit默认阻塞等待期间CPU无法干其他事。用DMA可解放CPU// 初始化DMASPI1_TX通道 hdma_spi1_tx.Instance DMA1_Channel3; hdma_spi1_tx.Init.Direction DMA_MEMORY_TO_PERIPH; hdma_spi1_tx.Init.PeriphInc DMA_PINC_DISABLE; hdma_spi1_tx.Init.MemInc DMA_MINC_ENABLE; hdma_spi1_tx.Init.PeriphDataAlignment DMA_PDATAALIGN_HALFWORD; hdma_spi1_tx.Init.MemDataAlignment DMA_MDATAALIGN_HALFWORD; hdma_spi1_tx.Init.Mode DMA_NORMAL; hdma_spi1_tx.Init.Priority DMA_PRIORITY_HIGH; HAL_DMA_Init(hdma_spi1_tx); __HAL_LINKDMA(hspi1, hdmatx, hdma_spi1_tx); // 绑定SPI与DMA // 使用DMA发送数据 void LCD_DMA_Send(uint16_t *data, uint16_t len) { HAL_SPI_Transmit_DMA(hspi1, (uint8_t*)data, len, HAL_MAX_DELAY); }关键配置PeriphDataAlignment DMA_PDATAALIGN_HALFWORDSPI外设数据宽度为16位RGB565必须匹配。MemDataAlignment DMA_MDATAALIGN_HALFWORD内存数据也按16位对齐。len单位是16位字数非字节数。实测对比方式刷满全屏12800点耗时CPU占用率适用场景HAL_SPI_Transmit1.2秒100%调试阶段逻辑验证DMA传输180ms5%正式项目需实时响应4.2 双缓冲机制——消除画面撕裂直接往GRAM写会导致“边写边显示”滚动文字时出现撕裂。引入双缓冲// 定义两个GRAM缓冲区各12800×2字节25.6KBF103C8T6 Flash足够 uint16_t lcd_buffer1[12800] __attribute__((section(.lcd_buf1))); uint16_t lcd_buffer2[12800] __attribute__((section(.lcd_buf2))); // 当前活跃缓冲区指针 uint16_t *lcd_active_buffer lcd_buffer1; // 切换缓冲区需在垂直消隐期操作此处简化为直接切换 void LCD_SwitchBuffer(void) { if(lcd_active_buffer lcd_buffer1) { lcd_active_buffer lcd_buffer2; // 将buffer2数据DMA发送到GRAM LCD_DMA_Send(lcd_buffer2, 12800); } else { lcd_active_buffer lcd_buffer1; LCD_DMA_Send(lcd_buffer1, 12800); } }提示真正的垂直消隐期需用TFT的VSYNC信号触发但0.96寸屏无此引脚。实践中只要保证LCD_SwitchBuffer不在LCD_DMA_Send执行中调用撕裂感已大幅降低。4.3 背光PWM调光——让屏幕不刺眼BLK引脚接PB1TIM3_CH2配置PWM// TIM3 PWM初始化72MHz主频分频后1MHz周期1000→1kHz htim3.Instance TIM3; htim3.Init.Prescaler 71; // 72MHz/(711)1MHz htim3.Init.CounterMode TIM_COUNTERMODE_UP; htim3.Init.Period 999; // 1000个计数→1kHz htim3.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; HAL_TIM_PWM_Init(htim3); sConfigOC.OCMode TIM_OCMODE_PWM1; sConfigOC.Pulse 500; // 初始占空比50% sConfigOC.OCPolarity TIM_OCPOLARITY_HIGH; HAL_TIM_PWM_ConfigChannel(htim3, sConfigOC, TIM_CHANNEL_2); HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_2);调节亮度只需改__HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_2, value)value范围0~999。5. 常见问题与排查技巧实录那些让我熬夜的坑5.1 屏幕全白/全黑——时序与电压的双重校验现象上电后屏幕常亮白或常黑无任何响应。排查路径电压测量用万用表测VCC与GND间电压必须为3.3V±0.1V。若为5V立即断电——ST7735S已损坏。复位信号抓取用示波器看RES引脚上电后应有≥10ms低电平脉冲。若没有检查上拉电阻是否虚焊。SPI波形分析测SCK和MOSI确认SCK频率≈9MHz且MOSI在SCK上升沿有稳定数据。若SCK无波形检查SPI时钟使能RCC-APB2ENR | RCC_APB2ENR_SPI1EN。根本原因我遇到过三次全白两次是VCC接错成5V模块未烧毁但性能下降一次是CS引脚悬空未接上拉/下拉导致ST7735S误入“休眠模式”。5.2 文字模糊/颜色失真——RGB565格式的陷阱现象显示字符边缘毛刺红色显示为橙色蓝色偏紫。根源RGB565格式中5位红、6位绿、5位蓝但新手常误用RGB888转换错误color ((r8) 0xF800) | ((g3) 0x07E0) | ((b3) 0x001F)正确color ((r3)11) | ((g2)5) | (b3)验证方法用纯色测试LCD_FillRect(0,0,160,80,0xF800)应为正红。若偏橙说明红分量不足若偏粉说明蓝分量溢出。5.3 刷新卡顿/闪烁——DMA与缓冲区的协同失效现象快速更新画面时出现明显闪烁或卡顿。解决方案禁用全局中断在LCD_DMA_Send前后加__disable_irq()/__enable_irq()防止DMA传输被中断打断。检查缓冲区对齐确保lcd_buffer1和lcd_buffer2地址为4字节对齐加__attribute__((aligned(4)))。DMA传输完成回调在HAL_SPI_TxCpltCallback中切换缓冲区而非在主循环中轮询。我踩过的坑曾因未禁用中断DMA传输中途被串口中断打断导致GRAM写入错位屏幕出现斜纹。修复后10Hz刷新率下完全稳定。5.4 Keil5编译报错“undefined identifier”——芯片包与标准库的兼容性现象#include stm32f1xx_hal.h报错提示HAL_SPI_Transmit未定义。解决步骤确认已安装STM32F1系列芯片包Pack Installer → STM32F1xx_DFP。Project → Options → Target → Device选择“STM32F103C8”非Generic。C/C → Define添加USE_HAL_DRIVER, STM32F103xB根据实际芯片型号调整。Output → Select Folder for Objects → 勾选“Create Batch File”生成编译日志定位具体缺失文件。终极方案若仍失败删除.uvprojx同目录下Objects和Listings文件夹Clean后再Rebuild——Keil缓存常导致此类问题。6. 从单色到真彩0.96寸IPS屏的延伸价值这块小屏的价值远超“显示几个数字”。我把它用在三个实际项目中验证了其扩展性数字温湿度计用DHT22采集数据每2秒刷新一次屏幕。ST7735S的16位色深让温度曲线图的渐变背景#FF0000→#00FF00过渡自然比单色OLED直观十倍。智能台灯调光界面通过触摸加XPT2046实现滑动调光屏幕实时显示当前亮度百分比和色温值。IPS屏的可视角度优势在此凸显——无论从哪个角度色彩都不偏移。简易示波器用STM32的ADC采集信号DMA存入内存再用LCD_DrawLine绘制波形。虽然分辨率有限但160×80的像素足够观察方波边沿和噪声毛刺。最后分享一个小技巧ST7735S支持局部刷新。比如只更新右下角的时间不必刷全屏。计算出时间区域坐标如x100,y65,width50,height15调用LCD_SetWindow限定范围再写入新数据——这样一次刷新从12800点降到750点速度提升17倍。这才是嵌入式开发的精髓不求大而全但求准而精。