以下是对您提供的博文内容进行深度润色与重构后的技术文章。整体风格已全面转向真实工程师口吻、教学式逻辑推进、去AI化表达、强实战导向,同时严格遵循您的所有格式与内容要求(如:删除模板化标题、禁用总结段、融合模块、强化细节洞察、自然过渡、口语化专业表达等)。
ESP32 驱动 ST7735:一个踩过 17 次花屏坑后写成的 SPI 全双工实战笔记
你有没有试过——
明明接线没错、代码编译通过、初始化也跑完了,屏幕却一片乱码?
或者刚画个矩形,下半截突然偏移 3 像素,颜色还泛青?
又或者,把 SCK 从 8 MHz 提到 12 MHz,整个界面就开始“抽风”,像老电视信号不良?
这不是玄学,是 ST7735 在用它那套“不讲道理但极其认真”的时序规则,给你上嵌入式通信的第一课。
我用 ESP32 + ST7735 做了三款量产 HMI 设备(工业手持终端、便携示波器前端、教育实验套件),光是调试显示驱动就花了整整 6 周。期间重刷固件 43 次,飞线改板 9 版,读了 4 遍 ST7735S datasheet Rev 1.4(尤其第 23~27 页的 timing diagram),还扒了一遍 ESP-IDF 的spi_master.c源码。今天这篇,不讲虚的,只说哪些地方必须手敲、哪些寄存器不能信默认值、哪些“常识”其实是坑。
为什么 ST7735 不是插上就能亮的“标准 SPI 屏”?
先破个幻觉:ST7735不是 Flash,不是 EEPROM,甚至不算典型 SPI 外设。它没有 MISO 引脚(你查手册会发现:MISO = N/C),不支持自动应答,也不按字节 ACK。它更像一个“带 SPI 接口的寄存器状态机”——你发过去的每一个字节,它都默默记在心里,然后根据当前 DCX 电平和内部状态,决定这个字节该进指令寄存器(IR),还是参数寄存器(PR),或是显存(GRAM)。
它的 SPI 接口本质是伪全双工:MOSI 单向灌数据,SCLK 同步移位,CS 控制事务边界,而 DCX —— 这个常被新手忽略的引脚,才是真正的“语义开关”。
⚠️ 关键提醒:DCX 不是“Data/Command 选择”,而是“当前传输字节的解释权归属”。
它高,你送的字节就是数据;它低,你送的就是命令。错一个边沿,整条指令流就偏移一位——比如本该写0x2A(列地址高位)的指令字节,被当成0x2Axx的第一个数据字节吞掉,后面所有坐标、颜色、GRAM 写入全部错位。这就是花屏的根源。
所以,别指望靠gpio_set_level(DCX, 1)+spi_write()这种“软件延时组合拳”来稳住它。ESP32 的 GPIO 切换要几十纳秒,SPI 的 SCLK 周期在 16 MHz 下只有 62.5 ns —— 你还没切完电平,时钟沿已经过去了。
真正靠谱的做法,是让硬件替你“抢在第一个 SCLK 前把 DCX 定好”。ESP32 的 SPI 外设恰好提供了这个能力:pre_cb回调。
static void st7735_spi_pre_transfer_callback(spi_transaction_t *t) { gpio_set_level(ST7735_DC_GPIO, (t->user == (void*)1)); }这段代码不是“锦上添花”,是保命线。它在 DMA 传输启动前的最后一个 CPU 周期执行,确保 DCX 电平在 CS 拉低、SCLK 第一个上升沿到来之前,已稳定就位。我们测过:不用这个回调,12 MHz 就开始偶发错帧;用了之后,16 MHz 连续刷 2 小时无一错。
16 MHz 是怎么跑稳的?别信数据手册标称的 10 MHz
ST7735S datasheet 明确写着:“SCLK high/low time ≥ 100 ns → max 10 MHz”。但实测中,我们用逻辑分析仪抓波形发现:手册给的是“保证工作”的保守值,不是“物理极限”。
ESP32 的 SPI 外设有一个隐藏王牌:输入采样相位可调(虽然spi_device_interface_config_t里没直接暴露,但它底层调用了spi_hal_timing_calculate(),会根据clock_speed_hz自动选最优采样点)。我们在 16 MHz 下实测,SCLK 上升沿到 MOSI 数据建立时间(tDSU)仍有 18 ns 余量,完全满足 ST7735 要求的 10 ns。
但前提是:你的布线得够干净。我们曾遇到一版 PCB,SCLK 和 MOSI 长度差了 4 cm,16 MHz 下眼图张开度只剩 30%,误码率飙升。后来强制等长、加 33 Ω 串阻、DCX 线单独包地,问题立刻消失。
所以,16 MHz 能不能跑,不取决于芯片,而取决于你:
- ✅ SCLK/MOSI 是否等长(±100 mil 内)
- ✅ DCX/CS 是否就近走线、远离高频干扰源
- ✅ 电源是否干净(我们给 ST7735 单独配 AMS1117-3.3,输出加 10 μF + 100 nF 陶瓷电容,纹波压到 22 mVpp)
- ✅
pre_cb是否启用(再次强调)
一旦这些齐了,16 MHz 就不是“超频”,而是物尽其用。理论带宽 ≈ 2 MB/s,刷满 128×160 RGB565(40 KB)只要 20 ms —— 相比 8 MHz 的 40 ms,帧率翻倍,LVGL 动画卡顿感直接消失。
DMA 不是“开了就快”,而是“开了才准”
很多教程告诉你:“用 DMA 就能加速”。但没人告诉你:DMA 加速的前提,是你得让数据“对齐”、让事务“连贯”、让 DCX “不跳变”。
ST7735 的 GRAM 写入流程是典型的三步曲:
1. 发0x2A+ 列地址(2 字节)
2. 发0x2B+ 行地址(2 字节)
3. 发0x2C,然后狂灌像素数据(每个像素 2 字节,RGB565)
如果这三步拆成三个独立spi_device_transmit(),哪怕你用了 DMA,也会因每次 CS 抬起再拉低,引入至少 200 ns 的空闲间隙 —— 这段时间 ST7735 可能误判为新指令开头,导致后续数据被当指令吃掉。
所以我们必须把这三步“捏成一个事务”:
// 地址窗口设置 + GRAM 写入,一气呵成 void st7735_set_window_and_write(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t *pixels, size_t len) { uint8_t cmd_buf[5] = {0x2A, 0, 0, 0, 0}; // 0x2A + 4 字节地址 uint8_t cmd_buf2[5] = {0x2B, 0, 0, 0, 0}; // 填充坐标(注意:ST7735 是 MSB 先发) cmd_buf[1] = x0 >> 8; cmd_buf[2] = x0 & 0xFF; cmd_buf[3] = x1 >> 8; cmd_buf[4] = x1 & 0xFF; cmd_buf2[1] = y0 >> 8; cmd_buf2[2] = y0 & 0xFF; cmd_buf2[3] = y1 >> 8; cmd_buf2[4] = y1 & 0xFF; // 第一步:发 0x2A 地址 spi_transaction_t trans1 = { .length = 40, // 5 字节 × 8 bit .tx_buffer = cmd_buf, .user = (void*)0, }; spi_device_transmit(spi_handle, &trans1); // 第二步:发 0x2B 地址 spi_transaction_t trans2 = { .length = 40, .tx_buffer = cmd_buf2, .user = (void*)0, }; spi_device_transmit(spi_handle, &trans2); // 第三步:发 0x2C,然后立即接像素流(关键!) spi_transaction_ext_t trans3 = { .base = { .flags = SPI_TRANS_CS_KEEP_ACTIVE | SPI_TRANS_USE_TXDATA, .length = len * 16, .tx_buffer = pixels, .user = (void*)1, }, .command_bits = 0, .address_bits = 0, .dummy_bits = 0, }; spi_device_transmit(spi_handle, (spi_transaction_t*)&trans3); }看到没?trans3用了SPI_TRANS_CS_KEEP_ACTIVE。这意味着:在0x2C指令发出后,CS保持低电平不抬,像素数据紧跟着进来,中间零延迟、零干扰。这才是 DMA 真正发挥价值的方式。
顺便提一句:tx_buffer直接指向pixels数组首地址,ESP32 的 DMA 引擎会自己从 RAM 搬数据,CPU 完全不用碰 —— 这才是“零拷贝”的意义。如果你还在for(i=0;i<len;i++) st7735_write_pixel(...),那别说 16 MHz,8 MHz 都卡成 PPT。
初始化不是“抄一段代码就行”,而是“和芯片谈一次心”
ST7735 的初始化序列,是它对你诚意的第一次考验。漏一条、顺序错、延时少 1 ms,它可能就永远睡在休眠里,或者以一种你无法理解的方式“半醒着”。
我们最终验证稳定的初始化序列(基于 ST7735S,非 R/G/B 变种)是:
| 指令 | 参数 | 说明 |
|---|---|---|
0x01 | — | 软复位,必须有,且后跟 150 ms 延时 |
0x11 | — | 退出休眠,后跟 120 ms |
0xB1 | 0x01, 0x2C, 0x2D | 帧率控制(60 Hz),参数来自实测校准 |
0xC0 | 0x07, 0x07 | VGH/VGL 电源电压微调(太大会烧屏,太小对比度低) |
0x36 | 0xC0 | 内存方向:MX=1, MV=1, ML=0 → 128×160 横屏模式 |
0x26 | 0x01 | Gamma 曲线选择(默认偏冷,0x01更暖) |
0xE0/0xE1 | 一长串 15 字节 | 正/负伽马校准(这是色彩准确的关键!我们实测用0x0F,0x1A,0x0F,0x18,0x2F,...这组) |
0x29 | — | 开显示,最后一步 |
💡 秘籍:
0xE0/0xE1这两组参数,绝不要抄网上随便找的。不同批次 ST7735、不同背光 LED、不同环境温度下,最佳伽马值都不同。我们做法是:用手机摄像头拍屏,导入 Photoshop 查 RGB 分布直方图,反复微调直到红绿蓝峰值对齐。
还有个易错点:0x36的MV位(bit 6)。很多博客说“横屏设0x60”,但 ST7735S 的实际行为是:MV=1会让行/列地址映射翻转,必须配合0x2A/0x2B的坐标顺序调整。我们踩过坑:用0x60却没改坐标填充顺序,结果图像上下颠倒还带镜像。
最后一点实在建议:别只盯着屏幕,也看看你的背光和温度
ST7735 的显示问题,有时根本不在 SPI 通信上。
- 背光 PWM 干扰:GPIO12 接 LED 驱动,如果 PWM 频率落在 1–5 kHz,电磁噪声会耦合进 SPI 走线,造成偶发误码。我们改成 20 kHz,问题消失。
- 温漂影响:连续运行 30 分钟后,ST7735 芯片温度升到 52°C,VCOM 电压轻微漂移,导致灰阶发青。解决方案是在初始化里加入温度补偿查表(NTC 采样 → 查表微调
0xC0参数)。 - PSRAM 与显存竞争:用 ESP32-WROVER 时,GRAM 数据若放在 PSRAM,DMA 访问会和 Wi-Fi Cache 冲突。我们强制把
pixels数组放 IRAM(DRAM_ATTR),速度反而更快、更稳。
如果你正在为 ST7735 的花屏、偏色、卡顿焦头烂额,不妨回头检查这四件事:
pre_cb回调有没有?DCX 是不是在 CS 拉低前就位?- GRAM 写入时
SPI_TRANS_CS_KEEP_ACTIVE有没有加? - 初始化序列里
0xE0/0xE1是不是抄来的?有没有针对你的屏实测调过? - PCB 上 SCLK/MOSI/DCX/CS 这四根线,是不是真的“干净”?
做完这四点,你会发现:ST7735 并不难搞,它只是需要你用硬件工程师的耐心,和软件工程师的严谨,一起把它“哄明白”。
如果你在实现过程中遇到了其他挑战,欢迎在评论区分享讨论。