从点不亮到流畅显示:我在STM32上用HAL库驱动ST7735的实战全记录
最近在做一个基于STM32F103C8T6的小型人机界面项目,需要接入一块1.8英寸的彩色TFT屏。市面上最常见的就是ST7735控制器的模组了——便宜、小巧、接口简单。理想很丰满,但真正动手才发现,“接上线就能亮”只是幻想。
我花了整整三天时间,才把这块看似简单的屏幕从黑屏、花屏、乱码一步步调到正常显示。期间踩遍了SPI时序不对、初始化序列错配、DC引脚控制混乱等几乎所有坑。今天这篇记录,不是什么高大上的技术论文,而是一个普通嵌入式工程师的真实调试手记,希望能帮你少走弯路。
为什么选ST7735?又为什么这么难搞?
先说结论:ST7735本身是一款非常优秀的TFT驱动IC,尤其适合资源有限的MCU平台。它支持RGB565格式、128×160分辨率,仅需4根SPI线(SCK、MOSI、CS、DC)即可工作,连RST都可以省掉。
那问题出在哪?
答案是:“不同厂家、不同批次、不同标签颜色”的模组,内部初始化流程可能完全不同。
你没看错。同样是写着“ST7735”的屏幕,有“Green Tab”、“Black Tab”、“Red Tab”甚至“White Tab”版本,它们的初始化命令序列差异巨大。网上随便搜一份代码,很可能只能点亮某一类模组,换一块就罢工。
更坑的是,这些信息往往藏在数据手册的角落里,或者根本就没写清楚。于是我们只能靠试、靠猜、靠逻辑分析仪抓波形。
硬件连接:别小看这几根线
我的开发环境如下:
- MCU:STM32F103C8T6(蓝 pill)
- 显示屏:1.8” SPI TFT,背面贴着绿色胶布(即所谓“Green Tab”版)
- 接口方式:四线SPI + DC + RST + CS
- 供电:3.3V(来自板载LDO)
连接关系如下:
| STM32 | ST7735 |
|---|---|
| PB13 (SCK) | SCK |
| PB15 (MOSI) | MOSI |
| PB12 | CS |
| PB14 | DC |
| PB11 | RST |
| GND | GND |
| 3.3V | VCC, LED |
⚠️ 注意:虽然ST7735标称兼容5V信号输入,但我建议全部使用3.3V系统,避免电平冲突风险。如果你的主控是5V(如Arduino),务必加电平转换!
SPI配置在CubeMX中设置为:
- Mode: Master
- Clock Polarity: Low (CPOL=0)
- Clock Phase: 1 Edge (CPHA=0) → 即SPI Mode 0
- Baud Rate Prescaler: fpclk/16 (初始调试设慢点,后面再提速)
- First Bit: MSB
- NSS: Software (由GPIO手动控制CS)
这个Mode 0很重要!有些模组要求Mode 3,但绝大多数Green/Black Tab版本都用Mode 0。错了直接通信失败。
驱动框架设计:命令和数据必须分家
ST7735最核心的设计之一就是通过DC引脚区分命令和数据:
- DC = 0:接下来传输的是命令字节(比如
0x2C表示开始写显存) - DC = 1:接下来传输的是数据内容(比如像素颜色值)
这看起来很简单,但在代码实现时很容易出错。很多人图省事,在SPI传输过程中动态切换DC电平,结果导致第一个字节就被误判。
所以我的做法是:封装两个独立函数,确保每次传输前状态明确。
// st7735.h #ifndef __ST7735_H #define __ST7735_H #include "stm32f1xx_hal.h" // 引脚定义(根据实际PCB修改) #define ST7735_CS_PORT GPIOB #define ST7735_CS_PIN GPIO_PIN_12 #define ST7735_DC_PORT GPIOB #define ST7735_DC_PIN GPIO_PIN_13 #define ST7735_RST_PORT GPIOB #define ST7735_RST_PIN GPIO_PIN_11 // 模式宏 #define CMD_MODE() HAL_GPIO_WritePin(ST7735_DC_PORT, ST7735_DC_PIN, GPIO_PIN_RESET) #define DAT_MODE() HAL_GPIO_WritePin(ST7735_DC_PORT, ST7735_DC_PIN, GPIO_PIN_SET) // 片选操作 #define CS_LOW() HAL_GPIO_WritePin(ST7735_CS_PORT, ST7735_CS_PIN, GPIO_PIN_RESET) #define CS_HIGH() HAL_GPIO_WritePin(ST7735_CS_PORT, ST7735_CS_PIN, GPIO_PIN_SET) // 函数声明 void ST7735_Init(void); void ST7735_WriteCmd(uint8_t cmd); void ST7735_WriteData(uint8_t data); void ST7735_WriteBuffer(uint8_t *buffer, uint16_t len); void ST7735_SetAddressWindow(uint8_t x, uint8_t y, uint8_t w, uint8_t h); void ST7735_FillRect(uint8_t x, uint8_t y, uint8_t w, uint8_t h, uint16_t color); #endif对应的底层传输函数:
// st7735.c #include "st7735.h" #include <string.h> extern SPI_HandleTypeDef hspi1; // 假设使用SPI1 void ST7735_WriteCmd(uint8_t cmd) { CS_LOW(); CMD_MODE(); // 先设为命令模式 HAL_SPI_Transmit(&hspi1, &cmd, 1, HAL_MAX_DELAY); CS_HIGH(); } void ST7735_WriteData(uint8_t data) { CS_LOW(); DAT_MODE(); // 设为数据模式 HAL_SPI_Transmit(&hspi1, &data, 1, HAL_MAX_DELAY); CS_HIGH(); } void ST7735_WriteBuffer(uint8_t *buffer, uint16_t len) { CS_LOW(); DAT_MODE(); HAL_SPI_Transmit(&hspi1, buffer, len, HAL_MAX_DELAY); CS_HIGH(); }这里的关键点在于:每次传输都完整经历“拉低CS → 设置DC → 发送 → 拉高CS”全过程。虽然效率不高,但稳定性极佳,特别适合调试阶段。
初始化序列:成败在此一举
这是整个驱动中最关键的部分。ST7735上电后处于睡眠状态,必须按特定顺序发送几十条寄存器配置命令才能唤醒。
而不同厂商的模组,所需的初始化流程天差地别。
以下是我实测可用的“Green Tab”版本初始化代码(适用于大多数淘宝常见绿屏):
void ST7735_Reset(void) { HAL_GPIO_WritePin(ST7735_RST_PORT, ST7735_RST_PIN, GPIO_PIN_RESET); HAL_Delay(10); HAL_GPIO_WritePin(ST7735_RST_PORT, ST7735_RST_PIN, GPIO_PIN_SET); HAL_Delay(120); // 必须等待足够长时间! } void ST7735_Init(void) { ST7735_Reset(); // --- 开始初始化序列 --- ST7735_WriteCmd(0x11); // Sleep Out HAL_Delay(120); ST7735_WriteCmd(0xB1); ST7735_WriteData(0x01); ST7735_WriteData(0x2C); ST7735_WriteData(0x2D); ST7735_WriteCmd(0xB2); ST7735_WriteData(0x01); ST7735_WriteData(0x2C); ST7735_WriteData(0x2D); ST7735_WriteCmd(0xB3); ST7735_WriteData(0x01); ST7735_WriteData(0x2C); ST7735_WriteData(0x2D); ST7735_WriteData(0x01); ST7735_WriteData(0x2C); ST7735_WriteData(0x2D); ST7735_WriteCmd(0xB4); // Inversion Off ST7735_WriteData(0x07); ST7735_WriteCmd(0xC0); ST7735_WriteData(0xA2); ST7735_WriteData(0x02); ST7735_WriteData(0x84); ST7735_WriteData(0xC5); // VL63 ST7735_WriteCmd(0xC1); ST7735_WriteData(0xC5); ST7735_WriteCmd(0xC2); ST7735_WriteData(0x0A); ST7735_WriteData(0x00); ST7735_WriteCmd(0xC3); ST7735_WriteData(0x8A); ST7735_WriteData(0x2A); ST7735_WriteData(0x8A); ST7735_WriteCmd(0xC4); ST7735_WriteData(0x8A); ST7735_WriteData(0xEE); ST7735_WriteCmd(0xC5); // VCOM ST7735_WriteData(0x0E); ST7735_WriteCmd(0x36); // MADCTL: 内存访问控制 ST7735_WriteData(0xC0); // 旋转方向 + BGR顺序 ST7735_WriteCmd(0x3A); // COLMOD: 接口像素格式 ST7735_WriteData(0x05); // 16位色 (RGB565) ST7735_WriteCmd(0x21); // Display Inversion On HAL_Delay(10); ST7735_WriteCmd(0x13); // Normal Display Mode On ST7735_WriteCmd(0x29); // Display On HAL_Delay(10); }📌重点说明几个关键命令:
0x36 (MADCTL):决定屏幕旋转方向和颜色排列。常用值:0x00: 0°, RGB0x60: 90°, RGB0xA0: 180°, RGB0xC0: 270°, BGR ← 我这块屏需要这个0x3A (COLMOD):必须设为0x05启用16位色模式,否则颜色会异常。0x11和0x29:分别是“退出睡眠”和“开启显示”,缺一不可。- 所有延时都不能删!特别是复位后的120ms,是芯片启动所需时间。
如果换成了“Black Tab”模组,你可能需要换成另一套初始化序列(例如先发0x0C,0x0F等)。建议准备多套init函数备用。
实际测试:画个红色方块验证
为了快速验证是否成功,我写了个最简单的填充函数:
void ST7735_FillRect(uint8_t x, uint8_t y, uint8_t w, uint8_t h, uint16_t color) { uint8_t x1 = x + w - 1; uint8_t y1 = y + h - 1; ST7735_SetAddressWindow(x, y, x1, y1); uint32_t total_pixels = w * h; uint8_t *buffer = malloc(total_pixels * 2); // 每像素2字节 if (!buffer) return; for (int i = 0; i < total_pixels; i++) { buffer[2*i] = color >> 8; // 高字节 buffer[2*i + 1] = color & 0xFF; // 低字节 } ST7735_WriteCmd(0x2C); // 开始写GRAM ST7735_WriteBuffer(buffer, total_pixels * 2); free(buffer); }配合地址窗口设置函数:
void ST7735_SetAddressWindow(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1) { ST7735_WriteCmd(0x2A); // Column Address Set ST7735_WriteData(0x00); ST7735_WriteData(x0 + 2); // XSTART偏移修正 ST7735_WriteData(0x00); ST7735_WriteData(x1 + 2); // XEND ST7735_WriteCmd(0x2B); // Row Address Set ST7735_WriteData(0x00); ST7735_WriteData(y0 + 1); ST7735_WriteData(0x00); ST7735_WriteData(y1 + 1); }🔍 注意:很多ST7735模组的实际可视区域是128×160,但它物理RAM是132×162,所以通常要加偏移(+1或+2)才能对齐。这也是常被忽略的细节。
主函数中调用:
int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_SPI1_Init(); ST7735_Init(); ST7735_FillRect(10, 10, 50, 50, 0xF800); // 红色矩形(RGB565) while (1) {} }当看到屏幕上真的出现一个鲜红的方块时,那一刻的成就感,懂的人都懂。
踩过的坑与避坑指南
❌ 问题1:屏幕全黑,什么也不显示
排查思路:
- 是否执行了0x11和0x29?
- RST有没有正确释放?有没有加足够延时?
- SPI Mode是否正确?用逻辑分析仪看一下SCK空闲电平是不是低?
✅ 解决方案:加入完整的复位流程 + 使用示波器确认SPI波形。
❌ 问题2:显示花屏、条纹、雪花
典型表现:颜色错乱、图像撕裂、部分区域乱码。
原因:
- SPI速率太快(>8MHz在F1上容易出错)
- DC引脚控制时机错误
- 地址窗口未正确设置,写到了无效区域
✅ 解决方案:
- 将SPI降频至4MHz以下测试;
- 检查ST7735_WriteCmd/Data中CS和DC的顺序;
- 绘图前务必调用SetAddressWindow。
❌ 问题3:刷新卡顿,CPU占用100%
原因:每发一个像素都要调一次HAL_SPI_Transmit,加上CS反复切换,开销极大。
✅ 优化建议:
- 改用DMA传输(配合SPI双缓冲更佳);
- 对静态内容做缓存,只刷新变动区域;
- 使用局部更新而非全屏重绘。
未来可以引入LVGL这类轻量GUI库,进一步提升交互体验。
总结:稳定比炫技更重要
回顾这次调试过程,最大的体会是:嵌入式开发没有银弹,细节决定成败。
一块小小的TFT屏背后,藏着SPI时序、电源管理、初始化流程、内存映射等多个技术点的协同。任何一个环节出问题,都会表现为“点不亮”。
但我相信,只要你掌握了以下几点,就能应对绝大多数ST7735模组:
- 硬件连接准确无误(尤其是DC、CS、RST);
- SPI配置为Mode 0,初始速率不宜过高;
- 初始化序列必须匹配你的模组类型(Green/Black Tab);
- 每个传输步骤都要有明确的状态控制;
- 善用延时和逻辑分析仪定位问题。
现在我已经把这套驱动打包成一个可复用模块,后续还会加入字体渲染、图片解码、触摸响应等功能。如果你也在做类似项目,欢迎留言交流经验。
毕竟,每一个能点亮屏幕的夜晚,都是值得庆祝的胜利。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考