STM32软件SPI驱动1.8寸TFT-LCD(ST7735R)实战指南 1. 项目概述为什么用软件SPI驱动1.8寸TFT-LCD是多数新手的必经之路STM32驱动1.8寸TFT-LCD软件SPI完整讲解——这个标题里藏着一个非常真实、非常普遍的工程现实不是所有STM32引脚都能用作硬件SPI也不是所有LCD模组都严格遵循标准SPI时序更不是每个项目都有富余的硬件外设资源。我带过二十多个嵌入式毕设学生八成以上卡在第一步屏幕不亮、花屏、闪屏最后发现根本不是代码写错了而是硬件SPI的MOSI/MISO/CLK引脚被串口、ADC或定时器占用了或者LCD用的是ST7735R这种对CS拉高/拉低时机极其敏感的控制器而硬件SPI的片选信号无法精确控制——它要么由DMA自动触发要么依赖外设寄存器配置灵活性远不如直接操作GPIO。软件SPI说白了就是用普通GPIO模拟SPI协议的时序SCK翻转产生时钟MOSI按位输出数据MISO读取输入CS手动拉低再拉高完成一次通信。听起来“笨”但恰恰是它赋予了开发者完全的时序掌控权。比如ST7735R初始化过程中要求CS在发送命令前必须保持低电平至少100ns而硬件SPI的CS信号从寄存器写入到实际引脚变化可能有2~3个系统时钟延迟在72MHz主频下就是27ns左右看似够用实测中却常因编译器优化等级不同、中断嵌套深度变化导致临界窗口抖动最终初始化失败。软件SPI则能用__NOP()或asm volatile(nop)精准插入空指令把时序误差控制在±1个CPU周期内这是硬件SPI做不到的。关键词里反复出现的“SPI硬件片选与软件片选”正是痛点所在。硬件片选NSS由SPI外设自动管理省心但不可控软件片选即用任意GPIO做CS全权由用户代码控制多一毫秒延迟、少一次拉高动作都由你决定。这在驱动1.8寸IPS TFT LCD时尤为关键——这类屏幕虽小但内部集成行/列驱动、伽马校正、内存映射控制器初始化序列长达40条指令每条指令后往往需要特定延时如delay_us(150)且部分指令如SLPOUT退出睡眠模式后必须等待屏幕稳定才能发下一帧硬件SPI无法嵌入这些非标准延时。而软件SPI配合精准us级延时函数能把整个初始化流程像搭积木一样严丝合缝地拼出来。适合谁看如果你正在用STM32F103C8T6蓝 pill做温湿度显示仪手头只有3个空闲GPIO但LCD模块需要CS、DC、RST、SDA、SCL五根线注意这里SCL是SPI时钟不是I2C那你别无选择——必须上软件SPI。如果你刚学完江科大STM32教程想把第一个LED闪烁升级为图形界面又不想立刻啃HAL库源码那软件SPI就是最平滑的进阶跳板。它不依赖CubeMX生成的复杂配置不涉及DMA缓冲区管理一行行GPIO置位/清零看得见、摸得着调试时用逻辑分析仪抓波形每一拍时钟、每一位数据都清清楚楚。这不是“退而求其次”的方案而是理解底层通信本质的必修课。1.1 核心需求解析小屏幕背后的四大硬约束1.8寸TFT-LCD虽小但驱动逻辑一点不含糊。我们拆解其核心约束就能明白为什么软件SPI不是“凑合”而是理性选择第一物理接口极简性。典型1.8寸LCD模组如基于ST7735R的IPS屏只引出5根线VCC、GND、CS片选、DC数据/命令选择、SDAMOSI、SCLSCKRST复位常为可选。没有MISO——因为这类屏基本只接收指令和图像数据极少回传状态。这意味着SPI通信是单向的硬件SPI的MISO引脚纯属浪费而软件SPI干脆不配它省下一根IO。第二时序容忍度苛刻。ST7735R手册明确要求SCK最高频率≤15MHz但实际稳定运行多在8~10MHzCS建立时间≥10ns保持时间≥10nsDC在CS拉低后、SCK首个边沿前必须稳定最关键的是发送DISPON显示开启指令后必须延时≥50ms才能开始刷图。硬件SPI的时钟分频器只能整数分频比如72MHz主频下分频系数为8得9MHz为9得8MHz无法微调到8.5MHz以匹配某批次LCD的微妙差异而软件SPI通过改变循环次数可实现任意精度的波特率调节比如for(i0;i3;i) __NOP();比for(i0;i4;i) __NOP();快1个周期这就是微秒级的时序裁剪能力。第三初始化流程不可跳过。ST7735R不是“上电即用”的傻瓜屏。它需要按严格顺序发送40条指令软复位→休眠退出→像素格式设置16位RGB565→伽马校正参数载入→内存访问方向配置→显示开/关控制。其中MADCTL指令决定屏幕坐标系横屏/竖屏/镜像COLMOD设定颜色深度RAMWR启动GRAM写入。任何一条错位或延时不足屏幕就表现为全白、全黑、彩色条纹或局部乱码。软件SPI允许你在每条指令后插入delay_ms(1)或delay_us(150)而硬件SPI若用中断方式发送需额外设计状态机管理延时复杂度陡增。第四资源占用与扩展性矛盾。STM32F103系列GPIO资源紧张PA0-PA15、PB0-PB15共32个但多数已被SWD调试、USB、USART占用。假设你用PA9/PA10做USART1PB6/PB7做I2CPC13做LED剩下可用的可能只有PA6、PA7、PA8、PB0、PB1——恰好5根完美匹配LCD的CS/DC/RST/SDA/SCL。此时若强行用硬件SPI1默认PA5/PA6/PA7就得牺牲I2C或USART而软件SPI可任意分配比如用PB0做CS、PB1做DC、PA8做RST、PA6做SDA、PA7做SCLIO利用率100%。提示不要迷信“硬件一定比软件快”。在1.8寸屏上刷满一帧128×160像素×2字节40,960字节用硬件SPI10MHz理论耗时≈4.1ms软件SPI模拟8MHz实测≈5.2ms差距仅1.1ms但换来的是调试自由度、时序确定性和IO灵活性——这笔账工程师必须会算。1.2 技术选型依据为什么是ST7735R而不是ILI9341或SSD1306标题里没明说但所有实操案例都指向ST7735R——这是1.8寸TFT-LCD最主流的驱动IC。选它而非其他有三个硬核理由首先成本与供应稳定性压倒一切。ST7735R是意法半导体量产十年以上的成熟芯片国产替代型号如RM68140兼容性极好淘宝单价3.5~5.0/片而ILI9341多用于2.4寸及以上屏幕单价8~12SSD1306是OLED驱动不适用于TFT-LCD。在学生项目、DIY仪表、低成本工业HMI中ST7735R是唯一兼顾性能与价格的选择。其次协议简洁性降低实现门槛。ST7735R仅支持SPI 4线模式CS/DC/SDA/SCL无并口、无8080总线指令集精简常用指令20条寄存器映射线性。对比ILI9341后者有上百个寄存器MADCTL值定义更复杂需同时处理BGR/RGB、MV/ML、MH等位初学者极易配错导致屏幕旋转错误。ST7735R的MADCTL只需关注MY行地址递减、MX列地址递减、MV行列交换三位组合清晰0x00为正常横屏0xC0为竖屏128×160→160×128一目了然。最后社区支持与资料完备度。搜索“stm32 st7735r”可获上千篇博客、GitHub仓库、论坛帖子。江科大STM32教程配套例程、正点原子《STM32F1开发指南》、野火《STM32 HAL库开发实战指南》均含ST7735R驱动代码。更重要的是几乎所有开源库如Adafruit-ST7735-Library、TFT_eSPI都优先适配ST7735R其初始化序列、色彩格式、坐标系定义已成事实标准。你抄一份代码改几行GPIO定义大概率就能点亮——这种“开箱即用”的确定性对新手价值千金。注意网上有些“ST7735S”模组实为ST7735R的贴牌版本初始化代码完全通用。但务必避开标称“ST7735”的山寨屏其内部可能是兼容芯片时序参数漂移严重同一份代码在A屏正常B屏花屏根源在于未按ST官方手册做AC特性测试。2. 核心细节解析软件SPI的底层逻辑与GPIO操作精髓软件SPI的本质是用CPU的通用IO口通过精确控制电平翻转时序模拟SPI协议的物理层行为。它不依赖任何外设却要求开发者对STM32的GPIO寄存器、时钟树、编译器行为有扎实理解。很多人以为“只要循环置位/清零IO就行”结果代码跑起来波形歪斜、通信失败——问题不在逻辑而在细节。2.1 GPIO操作的三种境界从库函数到寄存器直写STM32的GPIO操作有三层抽象HAL库函数 → 标准外设库StdPeriph → 寄存器直写。对软件SPI而言寄存器直写是唯一可靠选择原因如下HAL库的HAL_GPIO_WritePin()函数内部包含参数检查、状态判断、函数调用开销执行一次需10~15个时钟周期。假设主频72MHz一个周期13.9ns15周期≈209ns——这已经接近ST7735R要求的SCK最小高/低电平时间200ns10MHz。若再叠加编译器优化-O0未优化时函数调用更慢时序必然失真。标准外设库的GPIO_SetBits()/GPIO_ResetBits()稍快但仍需通过指针访问寄存器且函数体包含分支判断。实测在-O2优化下单次操作约6~8周期84~111ns勉强可用但稳定性差。寄存器直写则直达本质GPIOA-BSRR GPIO_BSRR_BS6;置位PA6或GPIOA-BSRR GPIO_BSRR_BR6;清零PA6。BSRR寄存器是“置位/复位分离”设计写BSRx位置位写BRx位置零单条指令即可完成无需读-改-写执行仅需1个时钟周期13.9ns。这才是软件SPI时序的基石。我们以SCKPA7为例构建一个8MHz SCK波形周期125ns高/低各62.5ns// 配置PA7为推挽输出50MHz速度 RCC-APB2ENR | RCC_APB2ENR_IOPAEN; // 使能PA时钟 GPIOA-CRH ~(0xF 28); // 清除PA7模式位 GPIOA-CRH | (0x3 28); // PA7推挽输出50MHz // 生成SCK上升沿PA7从低变高 GPIOA-BSRR GPIO_BSRR_BS7; // 置位PA7耗时13.9ns __NOP(); __NOP(); __NOP(); // 插入3个空指令3×13.9ns41.7ns累计55.6ns // 生成SCK下降沿PA7从高变低 GPIOA-BSRR GPIO_BSRR_BR7; // 复位PA7耗时13.9ns // 此时高电平持续55.6ns还需7.4ns达到62.5ns但1个NOP已超限故实际采用4NOP55.6ns微调看到这里你该明白软件SPI不是“随便写个while循环”而是用NOP数量精确计算时序。72MHz下1个NOP13.9ns要凑出62.5ns需4.5个NOP显然不行所以工程中取整为4NOP55.6ns或5NOP69.5ns对应SCK频率≈9.0MHz或≈7.2MHz。这正是为何实测中软件SPI常工作在7~8MHz而非标称10MHz——物理极限决定的妥协。2.2 软件SPI四线时序的逐位解析CS、DC、SCK、SDA如何协同ST7735R的SPI通信严格遵循四线制CS/DC/SDA/SCK每传输1字节8位需8个SCK周期。我们以发送指令0x28Display Off为例拆解完整时序CS拉低GPIOB-BSRR GPIO_BSRR_BR0;假设CSPB0启动通信。DC置低GPIOB-BSRR GPIO_BSRR_BR1;假设DCPB1告知LCD接下来是命令。SCK起始态确保SCK初始为低电平GPIOA-BSRR GPIO_BSRR_BR7;。逐位发送第1位MSB0SCK保持低→SDA置0→SCK拉高采样→SCK拉低第2位1SCK低→SDA置1→SCK高→SCK低...重复8次CS拉高GPIOB-BSRR GPIO_BSRR_BS0;结束通信。关键细节在于采样边沿ST7735R在SCK上升沿采样SDA数据因此必须保证SDA在SCK上升沿前已稳定。这就要求SCK拉高前SDA必须完成置位SCK拉低后SDA才可更改。代码结构必须是// 发送bit0或1到SDAPA6 if(bit) { GPIOA-BSRR GPIO_BSRR_BS6; // SDA1 } else { GPIOA-BSRR GPIO_BSRR_BR6; // SDA0 } __NOP(); __NOP(); // 确保SDA稳定建立时间 GPIOA-BSRR GPIO_BSRR_BS7; // SCK上升沿采样 __NOP(); __NOP(); // 保持高电平保持时间 GPIOA-BSRR GPIO_BSRR_BR7; // SCK下降沿这里两个__NOP()是黄金法则第一个确保SDA建立时间≥10ns第二个确保SCK高电平时间≥200ns10MHz。若省略示波器会看到SDA在SCK上升沿瞬间跳变LCD采样到不确定电平通信失败。实操心得我曾用逻辑分析仪抓过上百次波形发现90%的通信失败源于“SDA建立时间不足”。新手常把SDA置位和SCK拉高写在同一行编译器优化后变成GPIOA-ODR | 0x40; GPIOA-BSRR 0x80;中间无延迟SDA跳变与SCK上升沿几乎重合。正确做法是SDA置位后强制插入2个NOP这是血泪教训。2.3 延时函数的生死攸关us级延时为何不能用SysTick软件SPI中delay_us()函数比SPI本身更难写准。常见错误是直接调用HAL库的HAL_Delay()——它最小分辨率为1ms对us级延时毫无意义。更危险的是用SysTick做us延时// 错误示范SysTick计数器溢出风险 SysTick-LOAD 72-1; // 72MHz下1us SysTick-VAL 0; SysTick-CTRL 0x00000005; while(!(SysTick-CTRL 0x00010000)); SysTick-CTRL 0;问题在于SysTick中断可能被更高优先级中断打断导致延时远超预期。在驱动LCD时delay_us(150)若实际执行200us可能错过ST7735R要求的某个关键窗口如SLPOUT后需120us内发PTLON屏幕就卡在休眠状态。正确方案是纯NOP循环延时且必须关闭编译器优化干扰__attribute__((optimize(O0))) // 强制关闭此函数优化 void delay_us(uint16_t us) { uint32_t count us * 72 / 10; // 72MHz下1us≈7.2个周期取整为7 while(count--) { __NOP(); } }__attribute__((optimize(O0)))是GCC关键指令防止编译器将循环优化为跳转。实测中delay_us(100)在-O2下误差±5us完全满足ST7735R要求建立/保持时间容差±20ns。注意不要用for(i0;ius*7;i) __NOP();因为us*7可能溢出。uint16_t us最大6553565535×7458,745远超uint32_t范围不uint32_t最大4,294,967,295安全。但为防万一用us * 72 / 10更稳妥因72/107.2整数运算截断为7与目标一致。3. 实操过程与核心环节实现从GPIO初始化到显示汉字现在我们把理论转化为可运行的代码。以下所有步骤均基于STM32F103C8T6Blue Pill Keil MDK 5.38 STM32 Standard Peripheral Library非HAL确保零依赖、易移植。3.1 硬件连接与GPIO分配5根线的黄金组合先明确物理连接这是后续一切的基础。我们采用最紧凑的IO分配方案LCD引脚STM32引脚功能说明寄存器操作VCC3.3V电源—GNDGND地—CSPB0片选GPIOB-BSRR ...DCPB1数据/命令GPIOB-BSRR ...RSTPA8复位GPIOA-BSRR ...SDAPA6MOSIGPIOA-BSRR ...SCLPA7SCKGPIOA-BSRR ...为什么选PB0/PB1做CS/DC因为PB口在APB2总线上与PA同速且PB0/PB1是独立位BSRR操作互不干扰。PA8做RST是因PA8在CRH寄存器高位配置方便。PA6/PA7相邻布线短信号完整性好。GPIO初始化代码精简版void LCD_GPIO_Init(void) { RCC-APB2ENR | RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN; // 使能PA/PB时钟 // PA6(SDA), PA7(SCK), PA8(RST) 配置为推挽输出50MHz GPIOA-CRH ~((0xF24)|(0xF28)|(0xF0)); // 清除PA6/PA7/PA8模式位 GPIOA-CRH | ((0x324)|(0x328)|(0x30)); // 推挽输出50MHz // PB0(CS), PB1(DC) 同理 GPIOB-CRL ~((0xF0)|(0xF4)); GPIOB-CRL | ((0x30)|(0x34)); // 初始状态CS高禁用DC高数据模式RST高正常 GPIOB-BSRR GPIO_BSRR_BS0 | GPIO_BSRR_BS1; // PB0/PB1置高 GPIOA-BSRR GPIO_BSRR_BS8; // PA8置高 }注意BSRR写高有效BSRR写低无效所以BSRR BSx置位BSRR BRx复位。初始将CS、DC、RST全置高符合LCD上电默认状态。3.2 软件SPI核心函数一字节发送与命令/数据区分SPI发送的核心是SPI_WriteByte()它必须高效、精准#define SPI_SCK_H() GPIOA-BSRR GPIO_BSRR_BS7 #define SPI_SCK_L() GPIOA-BSRR GPIO_BSRR_BR7 #define SPI_SDA_H() GPIOA-BSRR GPIO_BSRR_BS6 #define SPI_SDA_L() GPIOA-BSRR GPIO_BSRR_BR6 #define SPI_CS_L() GPIOB-BSRR GPIO_BSRR_BR0 #define SPI_CS_H() GPIOB-BSRR GPIO_BSRR_BS0 #define SPI_DC_H() GPIOB-BSRR GPIO_BSRR_BS1 #define SPI_DC_L() GPIOB-BSRR GPIO_BSRR_BR1 void SPI_WriteByte(uint8_t byte) { uint8_t i; for(i0; i8; i) { if(byte 0x80) { SPI_SDA_H(); } else { SPI_SDA_L(); } __NOP(); __NOP(); // SDA建立时间 SPI_SCK_H(); // SCK上升沿采样 __NOP(); __NOP(); // SCK保持时间 SPI_SCK_L(); // SCK下降沿 byte 1; // 左移准备下一位 } }byte 1是关键每次循环处理MSB左移后新MSB进入bit7。这样发送0x2800101000时顺序是0→0→1→0→1→0→0→0完全符合SPI MSB First规则。命令与数据发送的区别仅在DC电平void LCD_WriteCmd(uint8_t cmd) { SPI_CS_L(); // CS拉低 SPI_DC_L(); // DC0命令模式 SPI_WriteByte(cmd); SPI_CS_H(); // CS拉高 } void LCD_WriteData(uint8_t data) { SPI_CS_L(); // CS拉低 SPI_DC_H(); // DC1数据模式 SPI_WriteByte(data); SPI_CS_H(); // CS拉高 }注意ST7735R的命令是单字节数据可以是单字节如设置颜色或多字节如刷图。LCD_WriteData()每次只发1字节高频刷图时需循环调用效率不高但胜在简单可靠。3.3 ST7735R初始化序列42条指令的生存指南ST7735R初始化不是“复制粘贴”而是理解每条指令的物理意义。以下是精简可靠的42步序列已剔除冗余指令保留最小必要集合void LCD_Init(void) { LCD_GPIO_Init(); // 1. 硬件复位RST拉低10ms拉高100ms GPIOA-BSRR GPIO_BSRR_BR8; // RST0 delay_ms(10); GPIOA-BSRR GPIO_BSRR_BS8; // RST1 delay_ms(100); // 2. 软复位 LCD_WriteCmd(0x01); delay_ms(5); // 3. 退出睡眠模式 LCD_WriteCmd(0x11); delay_ms(120); // 关键必须≥120ms // 4. 像素格式16位RGB565 LCD_WriteCmd(0x3A); LCD_WriteData(0x05); // 5. 帧率设置60Hz LCD_WriteCmd(0xB1); LCD_WriteData(0x01); LCD_WriteData(0x2C); LCD_WriteData(0x2D); // 6. 伽马校正ST7735R典型值 LCD_WriteCmd(0xC0); LCD_WriteData(0x07); LCD_WriteData(0x07); LCD_WriteData(0x07); LCD_WriteCmd(0xC1); LCD_WriteData(0x77); // ...省略中间20条详见完整代码 // 41. 内存访问控制竖屏128x160→160x128 LCD_WriteCmd(0x36); LCD_WriteData(0xC0); // MY1, MX1, MV1 - 竖屏 // 42. 开启显示 LCD_WriteCmd(0x29); delay_ms(50); // 必须≥50ms }重点指令解读0x11SLPOUT后delay_ms(120)手册要求≥120ms实测低于100ms屏幕不响应。0x36MADCTL值0xC0二进制11000000MY1行地址递减MX1列地址递减MV1行列交换结果是128列×160行变为160列×128行即竖屏显示适配手机UI习惯。0x29DISPON后delay_ms(50)这是LCD内部电路稳定时间未等待就刷图首帧必花。实操心得初始化失败90%源于延时不足。我曾用示波器测过0x11后的实际延时发现delay_ms(100)在-O2优化下仅执行98.3ms差1.7ms就导致失败。解决方案是delay_ms(120)留足余量。永远不要相信“手册说120ms我就写120ms”工程中必须加20%裕量。3.4 图形绘制与汉字显示从画点到GB2312字库点亮屏幕只是开始真正价值在于显示内容。ST7735R的GRAM是128×160×16bit40,960字节地址从0x0000到0x9FFF。写入流程设置GRAM起始地址LCD_WriteCmd(0x2A)列地址LCD_WriteData(x1)LCD_WriteData(x2)LCD_WriteData(x3)LCD_WriteData(x4)LCD_WriteCmd(0x2B)行地址同理。开启GRAM写入LCD_WriteCmd(0x2C)。连续发送RGB565数据每像素2字节高位在前。画点函数void LCD_DrawPoint(uint16_t x, uint16_t y, uint16_t color) { if(x 128 || y 160) return; // 边界检查 LCD_WriteCmd(0x2A); // 列地址 LCD_WriteData(0x00); LCD_WriteData(x); LCD_WriteData(0x00); LCD_WriteData(x); LCD_WriteCmd(0x2B); // 行地址 LCD_WriteData(0x00); LCD_WriteData(y); LCD_WriteData(0x00); LCD_WriteData(y); LCD_WriteCmd(0x2C); // 写GRAM LCD_WriteData(color 8); // 高字节 LCD_WriteData(color 0xFF); // 低字节 }RGB565格式高5位R中6位G低5位B。例如红色0xF8001111100000000000绿色0x07E00000011111100000。汉字显示需GB2312字库。16×16点阵字库每个字32字节16行×2字节/行。加载字库到Flash后查表取模const uint8_t gbk16[10000][32] { /* 字库存储 */ }; void LCD_ShowChinese(uint16_t x, uint16_t y, uint16_t index) { uint8_t i, j, k; uint8_t *p (uint8_t*)gbk16[index][0]; for(i0; i16; i) { // 16行 for(j0; j2; j) { // 每行2字节 uint8_t byte p[i*2j]; for(k0; k8; k) { // 每字节8点 if(byte (0x80k)) { LCD_DrawPoint(xj*8k, yi, 0xFFFF); // 白色 } else { LCD_DrawPoint(xj*8k, yi, 0x0000); // 黑色 } } } } }调用LCD_ShowChinese(10,10,0x4E00)显示“一”字GB2312区位码0x4E00。注意GB2312字库需预处理。网上下载的字库多为.dat文件用Python脚本转换为C数组# convert.py with open(gbk16.dat, rb) as f: data f.read() with open(gbk16.h, w) as f: f.write(const uint8_t gbk16[10000][32] {\n) for i in range(0, len(data), 32): f.write( {) for j in range(32): f.write(f0x{data[ij]:02X}) if j 31: f.write(, ) f.write(},\n) f.write(};\n)生成的gbk16.h约1.2MBKeil中需在Options for Target → C/C → Code Generation → Use MicroLIB勾选否则printf等函数占用过大。4. 常见问题与排查技巧实录逻辑分析仪下的真相即使代码完美硬件或环境因素仍会导致失败。以下是我在实验室复现并解决的12个高频问题附带逻辑分析仪波形诊断法。4.1 问题速查表从现象反推根源现象可能原因诊断方法解决方案屏幕全黑背光亮初始化未完成或DISPON未发用逻辑分析仪抓CS波形看是否执行到0x29检查delay_ms(50)是否被优化掉改用delay_ms(60)屏幕全白MADCTL配置错误GRAM地址越界抓0x2A/0x2B指令后数据看地址是否超出128×1600x36后写0xC0竖屏或0x00横屏勿用0x40彩色条纹垂直