简介:这份资源面向使用STM32F103C8T6进行嵌入式显示开发的初学者与进阶开发者,提供基于HAL库、以软件SPI方式驱动1.8寸ST7735 TFT LCD彩屏的完整工程。它解决了硬件SPI资源紧张或引脚受限时如何点亮彩屏的问题,涵盖GPIO模拟时钟、MISO、MOSI与NSS信号,以及ST7735初始化序列、像素数据与控制命令收发等关键环节。压缩包共86个文件,约4.14MB,包含c与h源码、MDK-ARM工程文件、ioc配置、启动汇编、map与hex产物及说明文档,目录按Src、Inc、MDK-ARM等模块组织,便于直接编译与二次移植。目前已有5731人学习下载。读者可借此掌握HAL库SPI配置、软件SPI时序模拟、LCD驱动分层写法与显示刷新思路,是理解SPI通信与嵌入式显示技术的实用参考。
1. 当硬件 SPI 引脚被占满,软件 SPI 怎么把 ST7735 点亮
手上这块 STM32F103C8T6 最小系统板,SPI1 的 PA5/PA6/PA7 已经被 Flash 和另一路传感器占死,偏偏项目又要求加一块 1.8 寸 ST7735 TFT 彩屏做状态显示。硬件 SPI 没得用,只能拿普通 GPIO 模拟时序,也就是常说的软件 SPI。很多人一听软件 SPI 就摇头,觉得刷屏肯定卡成幻灯片。实际测下来,72MHz 主频下用软件 SPI 驱动 128×160 的 ST7735,全屏刷新能压到 30ms 出头,做 UI 状态面板完全够用。这篇笔记就把 HAL 库 + 软件 SPI + ST7735 这条链路从引脚分配到驱动分层、从初始化序列到刷屏优化,按我自己踩过的顺序讲一遍,适合手里有 F103C8T6、屏幕是 ST7735、又不想动硬件 SPI 的嵌入式开发者照着复现。
2. 软件 SPI 驱动 ST7735 的引脚分配与分层设计
2.1 为什么选软件 SPI 而不是硬挤硬件 SPI
先把这个决策说清楚,不然后面全是白干。STM32F103C8T6 只有 SPI1 和 SPI2 两路硬件 SPI,SPI2 在 PB13/PB14/PB15,如果你板子上这几个脚已经接了别的外设,或者 PCB 已经打样改不了,那硬件 SPI 就是死路。软件 SPI 的本质是用 GPIO 翻转模拟 SCK 和 MOSI,牺牲 CPU 时间换引脚自由度。ST7735 的写时序对时钟极性不敏感,只要求数据在 SCK 上升沿被锁存,所以软件 SPI 实现起来比驱动那些对时序苛刻的 ADC 简单得多。
代价也要讲明白:软件 SPI 刷屏时 CPU 全程参与,没法用 DMA 卸载。但 ST7735 单次写像素是 16 位,128×160 全屏 20480 个像素,按每次翻转加判断算 8 个指令周期,72MHz 下理论极限约 18ms,实际加上循环开销 30ms 左右。这个数字对状态面板、参数显示、简单波形足够,但你要拿它播视频,那确实不行。
2.2 引脚分配表与硬件连接
我一般这样分配,兼顾布线和后续扩展:
| 信号 | ST7735 引脚 | STM32 引脚 | 说明 |
|---|---|---|---|
| SCK | SCL | PA0 | 软件 SPI 时钟,推挽输出 |
| MOSI | SDA | PA1 | 软件 SPI 数据,推挽输出 |
| CS | CS | PA2 | 片选,低有效 |
| DC | DC | PA3 | 数据/命令选择,0 命令 1 数据 |
| RES | RESET | PA4 | 复位,低有效 |
| BLK | BLK | PB0 | 背光,高电平点亮 |
注意 RES 和 BLK 不是 SPI 信号,但必须接。BLK 如果直接接 3.3V 常亮也行,但接 GPIO 能省电、能做呼吸灯效果。CS 在单屏场景下可以常拉低省一个脚,但保留它方便以后挂第二块屏。
2.3 驱动分层:把时序和业务拆开
软件 SPI 最容易写成一坨,所有东西塞在 main 里。我习惯分三层:底层 GPIO 翻转、中间 ST7735 命令封装、上层绘图 API。这样换引脚只改底层,换屏幕型号只改中间层。
底层就是两个函数,一个写字节,一个写 16 位:
// 软件 SPI 写一个字节,MSB 先行 static void soft_spi_write_byte(uint8_t data) { for (uint8_t i = 0; i < 8; i++) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET); // SCK 拉低 if (data & 0x80) HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_SET); // MOSI 高 else HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_RESET); // MOSI 低 data <<= 1; HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET); // SCK 拉高,上升沿锁存 } }逻辑说明:ST7735 在 SCK 上升沿采样 MOSI,所以先摆好数据再拉高 SCK。参数上唯一能调的是循环展开,把 8 次循环手动展开能再快 15% 左右,但代码可读性下降,我一般先不展开,等真卡了再优化。
中间层封装命令和数据写:
// 写命令:DC=0,CS 拉低 static void st7735_write_cmd(uint8_t cmd) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_3, GPIO_PIN_RESET); // DC=0 命令 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_RESET); // CS 拉低 soft_spi_write_byte(cmd); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_SET); // CS 拉高 } // 写数据:DC=1 static void st7735_write_data(uint8_t data) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_3, GPIO_PIN_SET); // DC=1 数据 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_RESET); soft_spi_write_byte(data); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_SET); }这里 CS 每次写完拉高是保守做法。如果你只挂一块屏,可以把 CS 常拉低,省掉两次 GPIO 操作,刷屏能快几个百分点。DC 的切换必须在 CS 有效之前或同时,顺序反了屏幕会花。
3. ST7735 初始化序列与最小刷屏验证
3.1 上电复位时序不能省
ST7735 对上电顺序有要求,省了复位大概率白屏。我一般这样走:先给 RES 拉低至少 10ms,再拉高,等 120ms 让内部电荷泵稳定,然后才能发初始化命令。这个 120ms 是数据手册里的典型值,实际我遇到过某批屏 80ms 也能起来,但别赌,老老实实等。
static void st7735_reset(void) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); // RES 低 HAL_Delay(20); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); // RES 高 HAL_Delay(120); // 等电荷泵稳定 }3.2 关键初始化命令逐条说明
初始化序列网上一搜一大把,但很多人直接复制粘贴,不知道哪条能改哪条不能动。我挑几条必须理解的讲:
st7735_write_cmd(0x11); // Sleep Out,退出睡眠 HAL_Delay(120); // 必须等,否则后续命令不生效 st7735_write_cmd(0x3A); // 像素格式 st7735_write_data(0x05); // 0x05 = 16位 RGB565 st7735_write_cmd(0x36); // 内存访问控制 st7735_write_data(0xC8); // 行列交换 + RGB顺序,方向不对就改这里 st7735_write_cmd(0x29); // Display On0x36 这条是方向玄学重灾区。0xC8 对应横屏还是竖屏、颜色红蓝是否颠倒,全看这一字节。如果你发现红色显示成蓝色,把 0xC8 改成 0xC0 或 0x08 试。这个没有统一答案,跟屏幕排线方向有关,血泪经验是拿一个纯红矩形去试,比看手册快。
3.3 最小验证:刷一个纯色块
初始化完别急着写复杂 UI,先刷个纯色确认链路通:
static void st7735_fill_color(uint16_t color) { st7735_set_window(0, 0, 127, 159); // 设置全屏窗口 st7735_write_cmd(0x2C); // Memory Write for (uint32_t i = 0; i < 128 * 160; i++) { st7735_write_data(color >> 8); // 高字节 st7735_write_data(color & 0xFF); // 低字节 } }逻辑说明:0x2C 是写显存命令,发完之后连续写数据就会自动填充窗口区域。参数上 color 是 RGB565,比如红色 0xF800。这里每次写两个字节,如果嫌慢可以合并成一个 16 位写函数,少一半函数调用开销。
如果刷出来是花屏,先查 0x3A 是不是 0x05,再查 0x36 方向字节。如果全白或全黑,查背光 BLK 和复位时序。如果颜色对但位置偏,查 set_window 的坐标范围,ST7735 有些批次有 2 像素偏移,需要加 offset。
4. 刷屏速度优化与常见翻车排查
4.1 把 GPIO 翻转从 HAL 换成寄存器直写
HAL_GPIO_WritePin 内部有参数检查,一次调用几十个周期。刷屏是高频操作,换成 BSRR 寄存器直写能快一倍以上:
// 用 BSRR 直写,PA0=SCK, PA1=MOSI #define SCK_H() (GPIOA->BSRR = GPIO_PIN_0) #define SCK_L() (GPIOA->BRR = GPIO_PIN_0) #define MOSI_H() (GPIOA->BSRR = GPIO_PIN_1) #define MOSI_L() (GPIOA->BRR = GPIO_PIN_1)BSRR 写 1 置位,BRR 写 1 复位,都是单周期指令。改完之后全屏刷新从 30ms 降到 18ms 左右。注意 BSRR 和 BRR 是 32 位寄存器,写的时候别把其他位带进去,用宏隔离最安全。
4.2 避坑:软件 SPI 驱动 ST7735 的 5 个常见问题
现象一:屏幕全白,背光亮但无内容。原因九成是复位时序没等够,或者 0x11 Sleep Out 后没等 120ms 就发后续命令。解决:把复位后的延时加到 150ms,Sleep Out 后也加 120ms,宁可多等。
现象二:花屏,有内容但像素错乱。原因通常是 0x3A 像素格式没设成 0x05,或者软件 SPI 时序里 SCK 上升沿和 MOSI 建立时间冲突。解决:确认 0x3A=0x05,然后在 MOSI 摆好数据后加一个 __NOP() 再拉高 SCK,给足建立时间。
现象三:颜色红蓝颠倒。原因就是 0x36 的 RGB/BGR 位设反了。解决:0xC8 和 0xC0 互换试,或者 0x08 和 0x00 互换,拿纯红纯蓝各刷一次就能定位。
现象四:刷屏时程序卡死或看门狗复位。原因是大循环里没喂狗,或者软件 SPI 占用 CPU 太久导致其他中断堆积。解决:刷屏循环里每行喂一次狗,或者把刷屏拆成多次小批量,中间让出 CPU。
现象五:显示偏移,右边或下边有黑边。原因是 ST7735 不同批次显存有 offset,常见是列偏移 2、行偏移 1。解决:在 set_window 里加 offset 补偿,比如 x 从 2 开始、y 从 1 开始,具体值拿全屏填充试出来。
4.3 用局部刷新代替全屏刷新
状态面板通常只有几个数字在变,全屏刷是浪费。我一般把屏幕分成若干矩形区域,只刷变化的那块:
// 只刷新一个 32x16 的数字区域 void refresh_number_area(uint16_t x, uint16_t y, uint16_t color) { st7735_set_window(x, y, x + 31, y + 15); st7735_write_cmd(0x2C); for (uint32_t i = 0; i < 32 * 16; i++) { st7735_write_data(color >> 8); st7735_write_data(color & 0xFF); } }这样每次只写 512 个像素,耗时不到 1ms,CPU 占用可以忽略。参数上区域大小按你 UI 元素定,但别小于 8×8,否则 set_window 的开销占比太高,反而不划算。
5. 把驱动封装成可复用模块的几个技巧
写到能亮屏只是第一步,真正省时间的是把驱动封成换块屏、换个引脚都不用重写业务代码的模块。我自己的习惯是定义一个 st7735_t 结构体,把引脚端口、引脚号、屏幕宽高、偏移量全塞进去,底层函数全部接收这个结构体指针。这样同一份代码,改一个初始化结构体就能驱动第二块屏,甚至换成 ST7789 也只需要改初始化序列和偏移参数。
具体做法是底层 soft_spi_write_byte 不再写死 GPIOA,而是从结构体里取端口和引脚号。代价是每次翻转多一次指针解引用,实测全屏刷新会慢 2ms 左右。这个取舍看你项目:如果只有一块屏,写死更快;如果产品线有多块屏,封装带来的维护收益远大于 2ms。
另一个技巧是把初始化序列做成表驱动。把命令、数据长度、数据内容、延时打包成数组,初始化时遍历执行。这样换屏幕型号只改一张表,不用动逻辑。表驱动唯一的坑是延时字段要留够,我一般把 Sleep Out 后的延时单独拎出来硬编码,不放进表里,避免表里数值被误改。
验证模块是否封装干净,有个简单办法:把引脚定义全部注释掉,看编译报错是不是只集中在底层一个文件。如果是,说明分层到位;如果业务代码里还有 GPIOA 字样,那就是漏了。这个自检习惯帮我省过很多次返工。
最后说个我自己的教训:早期我图快,把初始化序列和刷屏逻辑全写在 main.c 里,结果换块屏要改十几个地方,还漏了一处导致调了一下午。后来强制自己先分层再写功能,虽然开头多花二十分钟,但后面每次改动都是几分钟的事。软件 SPI 驱动 ST7735 本身不难,难的是别让它变成一坨改不动的代码。希望帮到你。
本文还有配套的精品资源,点击获取