深入ST7735:SPI通信背后的显示驱动真相
你有没有遇到过这样的情况?接上一块1.8寸TFT屏,代码烧进去,屏幕要么全白、要么花屏,甚至毫无反应。明明用的是热门库(比如Adafruit GFX),引脚也对了,为什么就是点不亮?
问题往往不出在“画图”上,而藏在最底层——SPI数据是怎么传的?命令和数据是如何被正确识别的?
今天我们就以广泛应用的ST7735驱动芯片为例,彻底讲清楚它通过SPI接口与MCU通信的核心机制。这不是一份复制手册的参数罗列,而是一次从工程实践出发的深度拆解,带你真正理解:
为什么有时候改一个时钟极性,就能让死屏瞬间复活。
一块小屏幕,五个关键信号线
先别急着写代码。我们来看看 ST7735 模块通常需要连接哪些引脚:
- SCK / SCLK:串行时钟,主控提供
- MOSI / SDA:主设备发送数据(到屏幕)
- CS#:片选,低电平有效
- DC#:数据/命令选择(Data or Command)
- RST#:硬件复位
其中前三个是标准SPI信号,但后两个——尤其是DC引脚——才是理解整个通信逻辑的关键突破口。
很多人以为 SPI 就是发字节,其实不然。对于像 ST7735 这样的智能显示控制器来说,每一个字节都有“身份”:它是命令?还是数据?这个判断就靠 DC 引脚来决定。
命令 vs 数据:DC引脚的“开关哲学”
设想一下,如果你只能通过一根线给别人传信息,怎么让他知道你当前说的是“指令”还是“内容”?
ST7735 的解决方案很巧妙:用一根额外的控制线——DC(Data/Command)来标明身份。
| DC状态 | 含义 | 示例 |
|---|---|---|
| 0 | 接下来是命令 | 0x2C表示开始写显存 |
| 1 | 接下来是数据 | 0xF800是红色像素值(RGB565) |
这就像两个人打电话:
- 当你说“现在我说的是菜单编号”,对方就知道接下来的数字是命令;
- 然后你说“现在我说的是菜名”,后面的词才当作内容处理。
所以,在代码中你会发现类似这样的操作:
TFT_CS_LOW(); // 开始通信 TFT_DC_CMD(); // “我要发命令了” HAL_SPI_Transmit(&hspi1, &cmd, 1, 10); TFT_CS_HIGH(); // 结束紧接着:
TFT_CS_LOW(); TFT_DC_DATA(); // “这次是数据” HAL_SPI_Transmit(&hspi1, buffer, len, 100); TFT_CS_HIGH();注意:CS可以拉高拉低分段传输,但一旦进入某个命令的数据阶段,就不能轻易断开CS,否则可能导致状态机错乱。
举个典型例子:你想写显存(命令0x2C),后面必须紧跟大量像素数据。如果中途CS被释放,ST7735 可能会认为这次“写操作”已经结束,后续数据将被忽略或误判。
SPI模式选哪个?CPOL 和 CPHA 到底多重要?
SPI有四种模式,由CPOL(Clock Polarity)和CPHA(Clock Phase)决定。
| Mode | CPOL | CPHA | 空闲电平 | 采样边沿 |
|---|---|---|---|---|
| 0 | 0 | 0 | 低 | 上升沿 |
| 1 | 0 | 1 | 低 | 下降沿 |
| 2 | 1 | 0 | 高 | 下降沿 |
| 3 | 1 | 1 | 高 | 上升沿 |
那么问题来了:ST7735 到底工作在哪种模式?
答案是:取决于模组设计,但绝大多数使用 Mode 3(CPOL=1, CPHA=1)。
这意味着:
- SCLK 空闲时为高电平
- 数据在上升沿采样
如果你的MCU配置成 Mode 0,即使其他都对,也可能完全无法通信——因为时序对不上。
🔧 实战建议:
调试初期若发现初始化失败、读回ID异常等问题,优先尝试切换 SPI 模式。可以用示波器抓一下 SCLK 波形,观察空闲状态;或者直接在代码里强制设置为 Mode 3 测试。
例如在 STM32CubeMX 中配置 SPI:
hspi1.Init.CLKPolarity = SPI_POLARITY_HIGH; // CPOL = 1 hspi1.Init.CLKPhase = SPI_PHASE_2EDGE; // CPHA = 1 → Mode 3CS 片选不只是“使能”:它是事务的边界
很多初学者习惯每发一个字节就拉一次 CS,比如这样:
ST7735_WriteCommand(0x28); // 关显示 TFT_CS_LOW(); TFT_DC_CMD(); ... TFT_CS_HIGH(); ST7735_WriteCommand(0x11); // 退出睡眠 TFT_CS_LOW(); TFT_DC_CMD(); ... TFT_CS_HIGH();虽然功能上没问题,但从协议角度看,这是一种“碎片化通信”。更规范的做法是:将一组相关操作打包在一个CS低电平周期内完成。
比如初始化序列中连续多个命令,完全可以共用一个 CS 片选:
TFT_CS_LOW(); TFT_DC_CMD(); HAL_SPI_Transmit(&hspi1, init_cmds, 5, 10); // 一次性发5条命令 TFT_CS_HIGH();这样做有两个好处:
1. 减少 GPIO 操作开销,提升效率
2. 避免因频繁片选导致内部状态机异常(某些命令需连续执行)
当然,并非所有场景都能合并。例如命令之后要跟大量数据时,仍需先发命令(DC=0),再切DC=1发数据。
初始化为何失败?不同版本的坑你踩过几个?
你以为只要调用tft.init()就万事大吉?现实往往没那么简单。
ST7735 有多个变种:ST7735S、ST7735R、ST7735B,它们的初始化流程略有差异,主要体现在以下几个方面:
| 差异点 | 影响说明 |
|---|---|
| Gamma 曲线设置 | 不同面板对比度响应不同,设错会导致发灰或过亮 |
| MADCTL 方向控制 | 屏幕旋转90°/180°时需调整内存映射方式 |
| VCOM 调节电压 | 影响黑白清晰度,部分模组需特殊指令开启 |
📌 典型案例:
某开发者使用 ST7735R 模组,却套用了 ST7735S 的初始化代码,结果屏幕始终偏绿。排查半天才发现 gamma 设置寄存器地址不一致。
✅ 正确做法:
- 查清自己模块的具体型号(看背面丝印或采购链接)
- 使用对应版本的init code(常以数组形式存在于驱动库中)
- 必要时参考官方 datasheet 修改 delay 时间或参数值
性能瓶颈在哪?别让CPU扛图像传输
假设你要刷新一整屏 128×160 × 16bit = 40KB 图像数据。
如果用普通轮询方式通过 SPI 发送,且 SPI 频率为 8MHz:
- 每秒可传约 1MB 数据 → 理论上传完需 40ms
- 但实际上加上函数调用、CS切换、软件延迟等,可能超过 60ms
- 最终帧率仅约 15~16fps,动画明显卡顿
如何破局?
方案一:提高SPI速率
ST7735 官方支持最高15MHz,部分增强版可达 27MHz。只要布线合理,STM32、ESP32 等主流MCU均可轻松跑满。
⚠️ 注意事项:
- 提速前确保电源稳定,避免因噪声引起误码
- 若出现间歇性花屏,可适当加宽建立/保持时间(添加nop延时)
方案二:启用DMA传输(强烈推荐)
让SPI外设自动从内存搬运数据,解放CPU。
以 STM32 HAL 库为例:
void ST7735_WritePixels_DMA(uint16_t *pixels, size_t len) { TFT_CS_LOW(); TFT_DC_DATA(); HAL_SPI_Transmit_DMA(&hspi1, (uint8_t*)pixels, len * 2); // 16bit → byte count }配合中断回调,在传输完成后拉高 CS:
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi == &hspi1) { TFT_CS_HIGH(); } }这样一来,CPU只需发起请求即可去做别的事,显著提升系统响应能力。
实际开发中的那些“隐性雷区”
除了基本通信,还有一些容易被忽视的设计细节,直接影响稳定性:
1. 电源设计:别低估内部升压需求
ST7735 内部需要约 4.8V ~ 5V 的 VGH/VGL 电压来驱动液晶。虽然多数模块已集成电荷泵,但仍要求输入 VCC 至少3.3V,且电流能力足够。
❌ 错误做法:
- 使用LDO供电,负载能力不足
- 未加去耦电容,导致电压跌落
✅ 正确做法:
- 在 VCC 引脚附近放置10μF + 100nF并联滤波电容
- 使用DC-DC或带载能力强的LDO(如AMS1117-3.3)
2. PCB布局:短、直、远离干扰源
SPI虽为低速总线(相对而言),但在 10MHz+ 下仍是高频信号。
建议:
- SCK、MOSI 走线尽量等长、短直
- 远离 PWM 背光线路、开关电源走线
- RST、DC 引脚可串联 1kΩ 电阻防干扰
3. 字节顺序:RGB565大小端问题
有些MCU默认按大端(Big Endian)组织数据,而ST7735期望的是MSB先行。
若发现颜色颠倒(红变蓝、绿变紫),检查是否需要交换高低字节:
// 假设 color = 0xF800 (Red) uint8_t data[2] = { (color >> 8), color & 0xFF }; // 正确顺序:高字节先发或者使用attribute((packed)) 结构体保证对齐。
从原理到应用:一次完整的绘图流程
让我们把前面的知识串起来,看看一次典型的“画点”操作背后发生了什么。
场景:绘制一个红色矩形
复位芯片
c HAL_GPIO_WritePin(RST_GPIO_Port, RST_Pin, 0); HAL_Delay(10); HAL_GPIO_WritePin(RST_GPIO_Port, RST_Pin, 1); HAL_Delay(120); // 等待内部电路稳定发送初始化序列
- 设置电压、伽马、方向、开启振荡器等
- 多数由预定义数组完成设置绘图窗口(CASET / RASET)
```c
ST7735_WriteCommand(0x2A); // CASET: Column Address Set
ST7735_WriteData(0x00); // X Start H
ST7735_WriteData(0x00); // X Start L
ST7735_WriteData(0x00);
ST7735_WriteData(0x7F); // X End = 127
ST7735_WriteCommand(0x2B); // RASET: Row Address Set
ST7735_WriteData(0x00);
ST7735_WriteData(0x00);
ST7735_WriteData(0x00);
ST7735_WriteData(0x9F); // Y End = 159
```
启动写显存
c ST7735_WriteCommand(0x2C); // Write Memory Start连续写入红色像素(0xF800)
c uint16_t red = 0xF800; uint8_t buf[2] = {red >> 8, red & 0xFF}; for (int i = 0; i < 128*160; i++) { ST7735_WriteDataBuffer(buf, 2); // 或用DMA批量发送 }屏幕自动刷新
整个过程依赖于精确的命令/数据切换 + 时序匹配 + CS管理,任何一个环节出错都会导致显示异常。
写在最后:掌握底层,才能驾驭复杂
ST7735 看似简单,但它背后体现的是嵌入式系统中一个经典范式:
如何用有限资源实现高效人机交互。
当你不再只是调用tft.println("Hello"),而是清楚每一帧背后有多少次GPIO翻转、多少字节经由SPI传输、多少微秒被精确延迟,你就真正掌握了这项技能。
无论你是做智能手表界面、工业仪表盘,还是教学实验平台,深入理解SPI通信机制 + 显示控制器行为,都将让你在调试时快人一步,在优化时稳扎稳打。
下次再遇到“点不亮”的屏幕,不妨问问自己:
是不是 DC 搞反了?
是不是 SPI 模式错了?
是不是忘了等复位完成?
往往答案就在这些细节里。
如果你正在使用 ST7735 或计划接入类似TFT模块,欢迎在评论区分享你的经验或困惑,我们一起探讨最佳实践。