ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

STM32F103C8T6 HAL库软件SPI驱动ST7735彩屏实战

STM32F103C8T6 HAL库软件SPI驱动ST7735彩屏实战

简介:本资源面向嵌入式开发初学者与STM32进阶开发者,提供基于STM32F103C8T6微控制器、HAL库与软件SPI方式驱动1.8寸ST7735 TFT LCD彩屏的完整工程代码,帮助读者解决硬件SPI接口不足或无需高速传输时的屏幕驱动问题,掌握GPIO模拟SPI时序、ST7735初始化序列与像素数据发送等核心技能。压缩包共86个文件,约4.14MB,涵盖c与h源码、o与d编译中间文件、crf与axf等构建产物,以及uvprojx、uvoptx等MDK工程配置、ioc与mxproject等CubeMX工程文件、hex烧录文件、sct分散加载脚本和map映射文件,目录结构清晰,便于直接编译与二次开发。目前已有5731人学习下载,读者可从中获得可运行的驱动工程、软件SPI实现思路、ST7735命令与显示配置参考,以及嵌入式显示技术的实践范例。

1. 当硬件 SPI 引脚被占满:用 STM32F103C8T6 的 HAL 库手搓软件 SPI 点亮 1.8 寸 ST7735 彩屏

手上这块 STM32F103C8T6 最小系统板,SPI1 的 PA5/PA6/PA7 已经被 Flash 和另一路传感器占死,SPI2 又要留给板载无线模块,可项目偏偏还要再挂一块 1.8 寸的 ST7735 TFT 彩屏做状态显示。这种“总线不够用”的场面,做嵌入式的应该都不陌生。硬件 SPI 走不通,那就退一步用普通 GPIO 模拟时序,也就是常说的软件 SPI。它牺牲的是刷新率,换来的是引脚随便挑、时序完全可控。这篇笔记就围绕 STM32F103C8T6 + HAL 库 + 软件 SPI + ST7735 这条链路,把驱动从零搭起来,重点讲清楚时序怎么模拟、初始化表怎么发、刷屏为什么慢、以及那些让人调半天的坑。适合已经能点灯、会用 CubeMX 生成工程,但被硬件外设资源卡住的开发者。

2. 软件 SPI 到底慢在哪:先算清时序账再动手

2.1 硬件 SPI 和软件 SPI 的本质差别

硬件 SPI 是外设自己在移位,CPU 只要往 DR 寄存器写一个字节,剩下的 8 个时钟周期由硬件按分频系数自动打出来,CPU 可以同时干别的。软件 SPI 则是 CPU 亲自去翻转 SCLK,每发一位就要写一次 GPIO、拉高、拉低,全程占用 CPU。以 STM32F103C8T6 常见的 72MHz 主频为例,硬件 SPI 分频到 18MHz 很轻松,而软件 SPI 用 HAL_GPIO_WritePin 这种函数调用方式,一次翻转大概要几百纳秒,实际位速率往往只有 1~3MHz,差了一个数量级。

这个差距直接决定了适用边界:如果你要刷全屏动画、跑 30fps 的 UI,软件 SPI 基本没戏;但如果只是显示参数、画个曲线、偶尔刷新一屏文字,1~2MHz 完全够用。ST7735 的写时序本身对时钟要求不苛刻,手册里给的写周期最小 66ns,也就是理论上能跑到 15MHz,但软件 SPI 根本摸不到这个上限,所以不用担心“太快了屏不认”,反而要担心“太慢了刷一屏要几百毫秒”。

2.2 用寄存器直写替代 HAL 函数调用

既然瓶颈在 GPIO 翻转速度,第一件事就是把 HAL_GPIO_WritePin 换掉。这个函数内部有参数检查、结构体寻址,一次调用几十条指令。直接操作 BSRR 寄存器,一条写操作就能置位或复位,速度能提升好几倍。

// 软件 SPI 引脚定义,全部用寄存器直写加速 // 假设 SCL=PB13, SDA(MOSI)=PB15, DC=PB12, RES=PB14, CS=PB11 #define LCD_SCL_H() (GPIOB->BSRR = GPIO_PIN_13) #define LCD_SCL_L() (GPIOB->BRR = GPIO_PIN_13) #define LCD_SDA_H() (GPIOB->BSRR = GPIO_PIN_15) #define LCD_SDA_L() (GPIOB->BRR = GPIO_PIN_15) #define LCD_DC_H() (GPIOB->BSRR = GPIO_PIN_12) #define LCD_DC_L() (GPIOB->BRR = GPIO_PIN_12) #define LCD_RES_H() (GPIOB->BSRR = GPIO_PIN_14) #define LCD_RES_L() (GPIOB->BRR = GPIO_PIN_14) #define LCD_CS_H() (GPIOB->BSRR = GPIO_PIN_11) #define LCD_CS_L() (GPIOB->BRR = GPIO_PIN_11) // 软件 SPI 发送一个字节,MSB 先行,模式 0(空闲低,上升沿采样) void LCD_SPI_WriteByte(uint8_t data) { for (uint8_t i = 0; i < 8; i++) { LCD_SCL_L(); // 时钟拉低,准备数据 if (data & 0x80) { LCD_SDA_H(); // 高位先出 } else { LCD_SDA_L(); } data <<= 1; LCD_SCL_H(); // 上升沿,从机采样 } }

这段代码的逻辑很直白:每个 bit 先把时钟拉低,把数据位摆到 SDA 上,再拉高时钟让从机采样。ST7735 支持 SPI 模式 0 和模式 3,这里用模式 0(CPOL=0,CPHA=0),也就是空闲低电平、上升沿采样。参数上唯一要改的是引脚宏,换成你实际接线的端口和引脚号即可。注意 BSRR 写 1 置位、BRR 写 1 复位,这两个寄存器都是只写,不会影响其他引脚状态,比读改写安全。

2.3 时钟频率怎么估、怎么调

软件 SPI 的实际速率取决于循环里每条指令的周期数。上面这个循环,每次迭代大概 10~15 条指令,72MHz 下约 200ns 一位,8 位就是 1.6μs,折合位速率约 5MHz。但这是理想值,实际受 Flash 等待周期、编译优化等级影响。如果你发现刷屏明显卡顿,可以在循环里加 __NOP() 主动降速,反而更稳;如果追求速度,把编译优化开到 -O2 或 -O3,别用 -O0 调试。

提示:软件 SPI 不需要配置 GPIO 的复用功能,全部设成推挽输出、无上下拉、高速模式即可。速度等级选 High 能减少边沿时间,对波形质量有帮助。

3. ST7735 初始化:那张让人头大的寄存器表怎么发

3.1 复位、延时和命令/数据切换

ST7735 上电后必须先硬复位,RES 拉低至少 10μs,再拉高,然后等 120ms 让内部电荷泵稳定。这个延时不能省,省了就会出现花屏或者全白。复位之后,所有操作都靠 DC 引脚区分:DC 拉低表示接下来发的是命令,DC 拉高表示发的是数据或参数。

// 写命令:DC 拉低,CS 拉低,发一个字节,CS 拉高 void LCD_WriteCmd(uint8_t cmd) { LCD_DC_L(); LCD_CS_L(); LCD_SPI_WriteByte(cmd); LCD_CS_H(); } // 写数据:DC 拉高 void LCD_WriteData(uint8_t data) { LCD_DC_H(); LCD_CS_L(); LCD_SPI_WriteByte(data); LCD_CS_H(); } // 硬复位序列 void LCD_Reset(void) { LCD_RES_H(); HAL_Delay(10); LCD_RES_L(); HAL_Delay(20); // 低电平保持至少 10us,这里给足 20ms LCD_RES_H(); HAL_Delay(120); // 等待内部稳压器稳定 }

这里 CS 每次操作都拉高拉低,是为了兼容多从机场景。如果总线上只有这一块屏,可以把 CS 一直拉低,省掉两次翻转,刷屏能快一点。DC 的切换必须在 CS 有效期间完成,顺序别搞反。

3.2 关键初始化命令逐条说明

ST7735 的初始化表各家屏厂略有差异,但核心命令是通用的。下面这张表列出必须配置的几条,参数按 1.8 寸 128x160 分辨率、RGB565 常见模组给。

命令作用典型参数说明
0x11Sleep Out无退出睡眠,之后必须延时 120ms
0x3APixel Format0x050x05 表示 RGB565,16 位色
0x36Memory Access Ctrl0xC8控制扫描方向,不同安装方向要改
0x21Display Inversion无多数模组需要开反显,否则颜色发白
0x13Normal Display无正常显示模式
0x29Display On无开显示,之前所有配置才生效
void LCD_Init(void) { LCD_Reset(); LCD_WriteCmd(0x11); // Sleep Out HAL_Delay(120); LCD_WriteCmd(0x3A); // 像素格式 LCD_WriteData(0x05); // RGB565 LCD_WriteCmd(0x36); // 扫描方向 LCD_WriteData(0xC8); // 横屏还是竖屏靠这个字节 LCD_WriteCmd(0x21); // 反显,很多模组必须开 LCD_WriteCmd(0x13); // 正常显示 LCD_WriteCmd(0x29); // 开显示 }

0x36 这个字节是坑最多的地方。它的 bit7 控制行地址顺序,bit6 控制列地址顺序,bit5 控制行列交换,bit3 控制 RGB/BGR 顺序。0xC8 对应的是常见的竖屏、BGR 顺序。如果你发现红色显示成蓝色,就是 bit3 没配对,改成 0xC0 或 0xC8 试一下。这个没有统一标准,以实际屏为准。

3.3 设置显示窗口和连续写像素

ST7735 刷屏前必须先设定地址窗口,告诉它接下来要往哪块区域写。窗口用列地址和行地址两组命令设定,每组先发起始再发结束。

// 设置显示窗口,x0/y0 起始,x1/y1 结束 void LCD_SetWindow(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1) { LCD_WriteCmd(0x2A); // 列地址设置 LCD_WriteData(0x00); LCD_WriteData(x0); LCD_WriteData(0x00); LCD_WriteData(x1); LCD_WriteCmd(0x2B); // 行地址设置 LCD_WriteData(0x00); LCD_WriteData(y0); LCD_WriteData(0x00); LCD_WriteData(y1); LCD_WriteCmd(0x2C); // 开始写显存 } // 填充一块区域为指定颜色 void LCD_Fill(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1, uint16_t color) { LCD_SetWindow(x0, y0, x1, y1); uint32_t total = (uint32_t)(x1 - x0 + 1) * (y1 - y0 + 1); LCD_DC_H(); LCD_CS_L(); for (uint32_t i = 0; i < total; i++) { LCD_SPI_WriteByte(color >> 8); // 高字节先发 LCD_SPI_WriteByte(color & 0xFF); } LCD_CS_H(); }

注意 0x2A 和 0x2B 的参数都是两个字节,高字节在前。很多模组的显存其实有偏移,比如 1.8 寸屏常见偏移是列 2、行 1,如果显示内容整体偏了,就在 x0/x1 上加偏移量。这个偏移量因屏而异,没有统一值,试出来就固定写死。

4. 避坑排查:软件 SPI 驱动 ST7735 最容易翻车的 5 个点

4.1 全白或全黑,一点反应没有

现象:上电后屏幕背光亮,但内容全白或全黑,初始化命令发了也没用。

原因:九成是复位时序不对,或者 RES 引脚根本没接对。ST7735 对复位低电平宽度有要求,太短不生效;另外有些模组 RES 内部有上拉,如果你 GPIO 初始化成开漏又没外部上拉,拉不低。

解决:先用万用表量 RES 引脚在复位期间是不是真的到了低电平。把低电平时间从 10μs 加到 20ms 试。GPIO 一律配推挽输出,别用开漏。

4.2 颜色整体偏色,红蓝互换

现象:画红色出来是蓝色,画蓝色出来是红色。

原因:0x36 命令的 BGR 位没配对。RGB 面板和 BGR 面板的像素字节顺序不同,配错了就红蓝颠倒。

解决:把 0x36 的参数在 0xC0 和 0xC8 之间切换,或者干脆在软件里把 RGB565 的高低字节对调。推荐改 0x36,效率更高。

4.3 刷屏有拖影、局部花屏

现象:填充大面积颜色时,屏幕出现斜纹、拖影,或者边缘有残留。

原因:软件 SPI 速率太快,超过了当前 GPIO 驱动能力或排线质量,波形边沿变缓导致采样错误。也可能是 CS 在连续写像素时被意外拉高。

解决:在 SPI 位循环里加 __NOP() 降速,或者把 GPIO 速度等级从 High 降到 Medium。检查 LCD_Fill 里 CS 是不是全程拉低,别在循环里反复拉。

4.4 显示内容整体偏移几个像素

现象:图像位置不对,右边或下边多出一条杂色边。

原因:ST7735 显存有物理偏移,常见的是列偏移 2、行偏移 1,但不同批次模组不一样。

解决:在 LCD_SetWindow 里给 x0/x1 加列偏移、y0/y1 加行偏移。偏移量从 0 开始试,每次加 1,直到图像对齐。这个值试出来就写死,别每次动态算。

4.5 一刷屏程序就卡死或复位

现象:调用 LCD_Fill 后程序跑飞,或者看门狗复位。

原因:填充循环里没有喂狗,或者 total 计算溢出。128x160 全屏是 20480 像素,循环 40960 次写字节,如果看门狗超时设得短,中途就复位了。

解决:大块填充时在循环里定期喂狗,或者把填充拆成小块。total 用 uint32_t,别用 uint16_t,否则 128x160 会溢出成 0。

5. 把刷新率再压一压:软件 SPI 下的实用优化技巧

软件 SPI 刷全屏慢是硬伤,但实际项目里很少需要全屏刷新。我一般的做法是把屏幕分区,只刷变化的部分。比如状态栏固定不动,只刷中间的数据区,这样每次写的像素数可能只有全屏的十分之一,体感就流畅了。配合 LCD_SetWindow 精确框定区域,ST7735 会自动在窗口内递增地址,不用每次重设。

另一个技巧是预先把要显示的字模或图标存成 RGB565 数组放 Flash,刷的时候直接按字节发,省掉运行时颜色转换。下面这个函数把一块 RGB565 数组推到指定区域,比逐个像素算颜色快不少。

// 把 RGB565 数组推到指定区域,数据高字节在前 void LCD_DrawImage(uint8_t x, uint8_t y, uint8_t w, uint8_t h, const uint8_t *img) { LCD_SetWindow(x, y, x + w - 1, y + h - 1); LCD_DC_H(); LCD_CS_L(); uint32_t len = (uint32_t)w * h * 2; // 每像素 2 字节 for (uint32_t i = 0; i < len; i++) { LCD_SPI_WriteByte(img[i]); } LCD_CS_H(); }

验证驱动是否正常,我习惯先填三原色:红、绿、蓝各刷一屏,看颜色对不对;再画一个边框,看有没有偏移;最后刷一张渐变图,看有没有拖影。这三步过了,驱动基本就稳了。还有一点血泪经验:软件 SPI 的引脚千万别选在 JTAG 默认复用的 PA13/PA14/PA15/PB3/PB4 上,否则调试器一连上,引脚功能就被抢了,屏幕直接没反应。要非用不可,记得先关 JTAG 复用。这套方案值不值得做,取决于你的刷新需求——如果只是参数显示,它足够稳;如果要跑动画,还是老老实实腾一路硬件 SPI 出来。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进