玩转小彩屏:ST7789 + STM32 驱动实战全解析
你有没有遇到过这样的场景?手里的STM32开发板一切正常,接上一块漂亮的1.3寸彩色TFT屏(标着ST7789),代码一烧录——屏幕要么全黑、要么花屏乱码,甚至完全没反应。反复检查接线无误,初始化序列也照搬例程,可就是点不亮。
别急,这不是你的问题。ST7789虽然号称“易用”,但它的脾气其实挺挑——哪怕SPI时序差一点点、初始化顺序错一步,或者DCX控制稍有疏忽,它就会“罢工”。
今天我们就来彻底拆解ST7789与STM32的接口配置全过程,从硬件连接到软件初始化,再到常见坑点排查,带你一步步把这块小彩屏真正“驯服”。无论你是做DIY项目还是工业HMI界面,这篇文章都能帮你少走弯路。
为什么选 ST7789?
在众多TFT驱动IC中,ST7789近年来越来越受欢迎,尤其是在基于STM32的小型显示应用中。它不是最老的(比如ILI9341),也不是最新的(如RM67162),但它恰好站在了“性能”和“易用性”的平衡点上。
关键优势一览
| 特性 | 说明 |
|---|---|
| 分辨率支持 | 最高 320×240,常见模组为240×240或240×320 |
| 接口方式 | 支持四线SPI(SCK/MOSI/CS/DCX)、三线SPI、8080并行等 |
| 显存管理 | 内置GRAM(无需外挂SRAM),240×320×16bit ≈ 150KB |
| 色深格式 | 原生支持RGB565,65K色显示 |
| 控制灵活性 | 通过MADCTL寄存器轻松实现横竖屏切换、镜像翻转 |
| 功耗表现 | 支持睡眠、深睡模式,适合电池供电设备 |
相比经典的ILI9341,ST7789有几个明显优点:
- 初始化序列更简洁;
- 对SPI Mode 0兼容更好(正是STM32默认配置);
- 默认支持更高的通信速率(官方支持达15MHz);
- 更精细的电源管理和伽马调节能力。
这些特性让它成为STM32平台上的理想搭档。
硬件怎么接?一个都不能错!
再好的软件也架不住接错一根线。我们先来看ST7789模块与STM32之间的典型连接方式。
引脚功能对照表
| 模块引脚 | 功能描述 | 推荐连接 |
|---|---|---|
| VCC | 电源输入(3.3V) | 接稳压LDO输出,勿直连MCU供电 |
| GND | 地 | 共地处理 |
| SCL / SCK | SPI时钟 | 连STM32的SPI_SCK(如PA5) |
| SDA / MOSI | 数据输入 | 连SPI_MOSI(如PA7) |
| CS | 片选信号 | GPIO控制(建议PB6) |
| DCX / D/C | 命令/数据选择 | GPIO控制(建议PB7) |
| RST | 复位引脚 | 可接GPIO复位(建议PB8),也可悬空上拉 |
| BLK / LED /背光 | 背光控制 | 接PWM或直接拉高使能 |
⚠️ 注意事项:
- 所有信号电平必须为3.3V!如果你使用的是5V主控(如Arduino),需要加电平转换。
- RST建议由MCU控制,这样可以在程序中统一软复位,避免冷启动异常。
- BLK引脚通常需要上拉才能点亮背光,否则即使驱动成功也会一片漆黑。
实际布线建议
- SCK与MOSI尽量走短线,避免与其他高频信号平行;
- 电源端务必添加10μF电解电容 + 0.1μF陶瓷电容并靠近模块VCC脚;
- 若使用长排线连接,可在SCK线上串联一个小电阻(22Ω~47Ω)抑制振铃。
一句话总结:电源要干净,走线要短,关键引脚不能悬空。
STM32如何配置SPI?Mode 0是命门!
很多开发者第一次失败的原因,往往出在SPI配置上。你以为开了SPI就能通,其实ST7789对SPI模式非常敏感。
必须满足的关键参数
| 参数 | 正确设置 |
|---|---|
| SPI Mode | Mode 0 (CPOL=0, CPHA=0)—— 空闲低电平,第一个上升沿采样 |
| 数据帧长度 | 8位 |
| MSB First | ✅ 开启(高位先行) |
| NSS片选 | 软件管理(GPIO控制CS),不要用硬件NSS |
| 波特率预分频 | 主频72MHz时建议设为fpclk / 6~/8→ 实际SCK约9~12MHz |
| CRC校验 | ❌ 关闭!ST7789不支持CRC |
为什么强调Mode 0?因为ST7789的数据手册明确指出:
“The SCLK idles at low level and data is latched at the rising edge.”
也就是说,SCK空闲为低,上升沿采样数据 —— 这正是SPI Mode 0的标准行为。
而STM32的HAL库默认SPI初始化就是Mode 0,这反而成了它的加分项,省去了复杂的时序调试。
HAL库配置示例(以STM32F1为例)
hspi1.Instance = SPI1; hspi1.Init.Mode = SPI_MODE_MASTER; hspi1.Init.Direction = SPI_DIRECTION_1LINE; // 单向发送 hspi1.Init.DataSize = SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; hspi1.Init.NSS = SPI_NSS_SOFT; // 软件控制CS hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_6; // 72MHz / 6 = 12MHz hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; hspi1.Init.TIMode = DISABLE; HAL_SPI_Init(&hspi1);注意这里用了SPI_DIRECTION_1LINE,因为我们只发不收,节省资源。
核心机制:DCX 到底起什么作用?
这是最容易被忽视、却最关键的一环:DCX(Data/Command eXtend)引脚决定了每次传输的意义。
简单来说:
- 当
DCX = 0→ 下一条SPI数据被视为命令(比如0x2A表示设置列地址) - 当
DCX = 1→ 下一条SPI数据被视为数据(比如像素值、参数)
你可以把它想象成“开关”:告诉芯片“我现在是要下指令,还是要交作业”。
所以,在软件层面,我们必须严格区分这两个状态:
void ST7789_SendCommand(uint8_t cmd) { HAL_GPIO_WritePin(TFT_DCX_PORT, TFT_DCX_PIN, GPIO_PIN_RESET); // DCX=0 HAL_GPIO_WritePin(TFT_CS_PORT, TFT_CS_PIN, GPIO_PIN_RESET); // CS=0 HAL_SPI_Transmit(&hspi1, &cmd, 1, HAL_MAX_DELAY); HAL_GPIO_WritePin(TFT_CS_PORT, TFT_CS_PIN, GPIO_PIN_SET); // CS=1 } void ST7789_SendData(uint8_t *data, size_t len) { HAL_GPIO_WritePin(TFT_DCX_PORT, TFT_DCX_PIN, GPIO_PIN_SET); // DCX=1 HAL_GPIO_WritePin(TFT_CS_PORT, TFT_CS_PIN, GPIO_PIN_RESET); HAL_SPI_Transmit(&hspi1, data, len, HAL_MAX_DELAY); HAL_GPIO_WritePin(TFT_CS_PORT, TFT_CS_PIN, GPIO_PIN_SET); }🔥 小贴士:有些初学者会尝试用DMA一次性发送“命令+数据”,但一定要注意中间不能让DCX变化失效。稳妥做法是分开调用。
屏幕为何点不亮?初始化序列才是灵魂
很多人以为只要发个Display On(0x29)就能亮屏,结果发现毫无反应。真相是:ST7789需要一套完整的初始化流程来建立工作环境,跳步或顺序错误都会导致失败。
以下是经过验证的精简初始化流程(适用于大多数240×240/240×320模组):
void ST7789_Init(void) { HAL_Delay(10); // Step 1: 退出睡眠模式 ST7789_SendCommand(0x11); HAL_Delay(120); // Step 2: 设置像素格式为16位(RGB565) ST7789_SendCommand(0x3A); uint8_t pf = 0x55; ST7789_SendData(&pf, 1); // Step 3: 前后肩设置(Porch) ST7789_SendCommand(0xB2); uint8_t porch[] = {0x0C, 0x0C, 0x00, 0x33, 0x33}; ST7789_SendData(porch, 5); // Step 4: Gate电压控制 ST7789_SendCommand(0xB7); uint8_t gate = 0x35; ST7789_SendData(&gate, 1); // Step 5: VCOM设置 ST7789_SendCommand(0xBB); uint8_t vcom = 0x19; ST7789_SendData(&vcom, 1); // Step 6: LCM控制 ST7789_SendCommand(0xC0); uint8_t lcm = 0x2C; ST7789_SendData(&lcm, 1); // Step 7: 外部VSL使能 ST7789_SendCommand(0xC2); uint8_t vsl = 0x01; ST7789_SendData(&vsl, 1); // Step 8: VRH设置 ST7789_SendCommand(0xC3); uint8_t vrh = 0x12; ST7789_SendData(&vrh, 1); // Step 9: VDV设置 ST7789_SendCommand(0xC4); uint8_t vdv = 0x20; ST7789_SendData(&vdv, 1); // Step 10: 帧率控制(60Hz) ST7789_SendCommand(0xC6); uint8_t fr_ctl = 0x0F; ST7789_SendData(&fr_ctl, 1); // Step 11: 电源控制 ST7789_SendCommand(0xD0); uint8_t pw_ctl[] = {0xA4, 0xA1}; ST7789_SendData(pw_ctl, 2); // Step 12: 正向伽马校正 ST7789_SendCommand(0xE0); uint8_t pos_gamma[] = {0xD0,0x04,0x0D,0x11,0x13,0x2B,0x3F,0x54,0x4C,0x18,0x0D,0x0B,0x1F,0x23}; ST7789_SendData(pos_gamma, 14); // Step 13: 负向伽马校正 ST7789_SendCommand(0xE1); uint8_t neg_gamma[] = {0xD0,0x04,0x0C,0x11,0x13,0x2C,0x3F,0x44,0x51,0x2F,0x1F,0x1F,0x20,0x23}; ST7789_SendData(neg_gamma, 14); // Step 14: 开启显示反转 & 正常模式 ST7789_SendCommand(0x21); // Display Inversion ON ST7789_SendCommand(0x13); // Normal Display Mode ON // Step 15: 最终点亮屏幕 ST7789_SendCommand(0x29); // Display ON HAL_Delay(10); }📌重点提醒:
- 初始化中的延时不可随意删除,尤其是0x11后的120ms;
- 不同厂商的模组可能略有差异(例如某些便宜模块省略了部分寄存器),若标准序列无效,请查阅具体模块资料;
-0x3A必须设置为0x55以启用16位模式,否则颜色会错乱。
如何画图?GRAM写入全流程揭秘
屏幕点亮只是第一步,真正的挑战在于如何高效绘制内容。
ST7789通过以下三步完成区域写入:
- 设定列地址范围(CASET)
- 设定行地址范围(RASET)
- 触发内存写入(RAMWR)
示例:全屏填充红色
void ST7789_FillScreen(uint16_t color) { ST7789_SetAddressWindow(0, 0, 239, 239); // 240x240屏 ST7789_SendCommand(0x2C); // Memory Write uint8_t data[2]; for (int i = 0; i < 240 * 240; i++) { data[0] = color >> 8; data[1] = color & 0xFF; ST7789_SendData(data, 2); } }其中ST7789_SetAddressWindow实现如下:
void ST7789_SetAddressWindow(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1) { ST7789_SendCommand(0x2A); // CASET: Column Address Set uint8_t col_addr[4] = {0x00, x0 + 0, 0x00, x1 + 0}; // 根据偏移调整 ST7789_SendData(col_addr, 4); ST7789_SendCommand(0x2B); // RASET: Row Address Set uint8_t row_addr[4] = {0x00, y0 + 0, 0x00, y1 + 0}; ST7789_SendData(row_addr, 4); }⚠️ 注意坐标系偏移:有些模组原点不在(0,0),需根据数据手册调整起始地址。
常见故障排查清单
| 故障现象 | 可能原因 | 解决方案 |
|---|---|---|
| 完全无显示(黑屏) | 供电不足、RST未释放、背光未开启 | 测量VCC是否稳定3.3V;确认BLK引脚已拉高 |
| 花屏、雪花、乱码 | SPI速率过高、模式错误、CRC开启 | 降低波特率至6MHz测试;关闭CRC;确认Mode 0 |
| 颜色异常(偏绿/蓝) | RGB565字节顺序错误 | 检查高低字节发送顺序,必要时交换 |
| 只显示部分内容 | 地址窗口越界或设置错误 | 打印x0/x1/y0/y1确认是否超出实际分辨率 |
| 刷新卡顿严重 | 使用轮询发送大量数据 | 启用DMA传输,或将图像压缩缓存 |
| 开机偶尔失灵 | 上电时序不稳定 | 添加独立复位电路或延长初始化延时 |
🔧调试技巧:
- 先用逻辑分析仪抓取SPI波形,确认命令是否发出;
- 单独测试DCX切换是否准确;
- 尝试简化初始化流程,仅保留0x11 → delay → 0x29看能否短暂亮屏。
性能优化进阶思路
当你已经能让屏幕正常工作后,下一步就是提升效率。
1. 启用DMA传输
STM32的SPI支持DMA,可以将CPU从海量像素数据发送中解放出来。
HAL_SPI_Transmit_DMA(&hspi1, pixel_buffer, 240*240*2);配合双缓冲机制,可实现流畅动画。
2. 局部刷新代替全屏重绘
只更新变化区域,大幅减少带宽占用:
ST7789_SetAddressWindow(x, y, x+w-1, y+h-1); // 仅刷新该矩形区域3. 使用Framebuffer + 脏区检测
维护一块内存中的“虚拟屏幕”,每次只对比差异区域进行刷新,特别适合菜单类UI。
4. 方向控制:MADCTL 寄存器详解
通过写入0x36命令,可灵活设置显示方向:
| Bit | 含义 |
|---|---|
| MY(bit7) | 行扫描方向:0=自顶向下,1=自底向上 |
| MX(bit6) | 列扫描方向:0=从左到右,1=从右到左 |
| MV(bit5) | XY轴交换:0=正常,1=横竖互换 |
常用组合:
0x00:默认方向(竖屏)0x60:横屏(适配手表类设计)0xC0:倒置竖屏
写在最后:通往LVGL的第一步
掌握ST7789的基础驱动,其实只是嵌入式图形开发的起点。一旦你能稳定读写GRAM,就可以进一步集成轻量级GUI框架,比如:
- LVGL:开源、功能强大、支持触摸、动画、主题;
- GUIslice:专为小资源系统设计,零依赖;
- emWin(Segger):商业级,但需授权。
而这一切的前提,都是你要先把底层通信打通。屏幕能亮,才有资格谈美观。
所以,下次当你面对一块沉默的小彩屏时,不要再盲目刷例程了。静下心来,从电源、接线、SPI模式、DCX控制、初始化序列这五个环节逐一排查,你会发现,原来点亮它并没有那么难。
如果你正在做一个基于STM32的智能仪表、迷你示波器或DIY游戏机,欢迎在评论区分享你的项目进展!我们一起把更多创意变成现实。