从零点亮一块彩屏:手把手教你用ST7789V显示第一行文字
你有没有试过,把一块崭新的TFT彩屏接到开发板上,通电后却是一片漆黑?明明代码烧进去了,引脚也接对了,可屏幕就是“装睡不醒”。别急——这几乎是每个嵌入式开发者在踏入显示世界时都会踩的坑。
今天我们就来干一件简单但极具仪式感的事:让ST7789V驱动的彩屏,显示出属于你的第一行“Hello, World!”。不是跑例程,不是调库函数,而是真正理解每一步发生了什么,从硬件通信到像素生成,一步步亲手点亮它。
为什么是 ST7789V?
市面上能见到的小尺寸彩色屏模组里,ST7789V 几乎无处不在。1.3 英寸、1.54 英寸、甚至圆形表盘屏,背后多半都藏着这颗芯片。它由思立微(Sitronix)设计,专为资源受限的MCU平台优化,支持 SPI 和 MIPI DSI 接口,分辨率常见为240×320,色彩深度达到65K 色(RGB565),足够清晰又不会太吃主控性能。
更重要的是,它内建升压电路和振荡器,不需要外挂晶振或复杂电源管理,非常适合 ESP32、STM32 等主流单片机直接驱动。
但它的“友好”仅限于硬件层面。软件上,如果你不了解其初始化流程与寄存器逻辑,很容易陷入“黑屏—改参数—再黑屏”的死循环。
所以,我们不跳步,先搞懂核心机制。
屏幕是怎么被“唤醒”的?看懂初始化序列
LCD 不像 LED 那样通电就亮。它需要一个“开机仪式”,也就是一系列精确顺序的命令,模拟面板的物理启动过程。这个过程叫初始化序列(Initialization Sequence)。
你可以把它想象成叫醒一个人:
- 先拍两下肩膀(复位);
- 再喊一声“起床啦!”(退出睡眠);
- 然后告诉他今天穿什么衣服(设置颜色格式)、面向哪个方向(旋转角度);
- 最后说:“现在可以开始工作了。”
对应到 ST7789V 上,这些动作就是一条条写入的命令。
关键命令一览(人话版)
| 命令 | 功能说明 |
|---|---|
0x01(Software Reset) | 软件复位,清空内部状态 |
0x11(Sleep Out) | 结束休眠模式,准备干活 |
0x3A(Pixel Format Set) | 设置每像素用 16 位表示(即 RGB565) |
0x36(MADCTL) | 控制屏幕旋转方向 |
0x2A,0x2B(CASET/RASET) | 定义绘图区域边界 |
0x29(Display On) | 开灯!正式显示内容 |
其中任何一个环节出错,屏幕可能就不理你了。
比如你忘了发0x11,芯片还躺在“睡眠模式”里做梦;或者没设0x3A成0x05,那它就不知道你是要用 16 位色还是 18 位色,数据自然对不上号。
SPI 是怎么跟屏幕“对话”的?
ST7789V 支持多种接口,但我们最常用的还是四线 SPI 模式:SCLK、MOSI、CS、DC,加上一个可选的 RST 引脚。
这里最关键的一个点是:如何区分“命令”和“数据”?
答案就在DC 引脚(Data/Command Select):
- DC = 0 → 接下来传的是命令(比如“我要开始画画了”)
- DC = 1 → 接下来传的是数据(比如“画个红色像素”)
举个例子:你想设置列地址范围为 0~239,得这么做:
st7789v_write_command(0x2A); // 命令:我要设列地址 st7789v_write_data(0x00); // 数据:起始高位 XSH st7789v_write_data(0x00); // 数据:起始低位 XSL st7789v_write_data(0x00); // 数据:结束高位 XEH st7789v_write_data(0xEF); // 数据:结束低位 XEL (239 = 0xEF)整个过程就像打电话点餐:
“喂,客服吗?”(CS拉低 + DC=0 发命令)
“我要修改订单。”(发送命令码)
“加一份薯条。”(DC=1 发数据)
每一步都不能乱。
SPI 模式通常使用Mode 0(CPOL=0, CPHA=0),即时钟空闲为低电平,上升沿采样。ESP32 和 STM32 默认都支持这种模式,只要配置正确,通信基本稳。
初始化代码实战:别再复制粘贴了
下面这段初始化函数,是你能否点亮屏幕的关键。每一行都有意义,不能随便删。
void st7789v_init(void) { HAL_Delay(50); // 上电延迟,等电压稳定 TFT_RST_LOW(); // 拉低复位脚 HAL_Delay(10); TFT_RST_HIGH(); // 释放复位 HAL_Delay(150); // 至少等待 120ms 才能发命令 st7789v_write_command(0x11); // 退出睡眠模式 HAL_Delay(120); st7789v_write_command(0x3A); // 设置像素格式 st7789v_write_data(0x05); // 0x05 表示 16-bit/pixel (RGB565) st7789v_write_command(0x36); // MADCTL: 显示控制 st7789v_write_data(0xC0); // 旋转 270° —— 很多模组出厂就是这样! // 设置列地址范围 (X轴): 0 ~ 239 st7789v_write_command(0x2A); st7789v_write_data(0x00); st7789v_write_data(0x00); st7789v_write_data(0x00); st7789v_write_data(0xEF); // 设置页地址范围 (Y轴): 0 ~ 319 st7789v_write_command(0x2B); st7789v_write_data(0x00); st7789v_write_data(0x00); st7789v_write_data(0x01); st7789v_write_data(0x3F); // 319 = 0x013F st7789v_write_command(0x21); // 开启显示反相(可选,改善某些屏观感) st7789v_write_command(0x29); // 开启显示 HAL_Delay(10); }⚠️ 注意事项:
- 所有延时都不能省!尤其是0x11后必须 ≥120ms。
-0x36的值因模组而异,有的是0x60(90°),有的是0xA0(180°),请查你买的模块手册。
- 如果你的屏幕是 240×240 圆形屏,记得调整 CASET/RASET 范围。
运行完这个函数,你应该能看到屏幕从全黑变为深灰色或轻微背光亮起——恭喜,已经成功一半了!
如何往屏幕上“画”东西?GRAM 是关键
ST7789V 内部有一块图形 RAM(GRAM),大小为 240×320×16bit ≈ 150KB。所有你要显示的内容,最终都要写进这块内存。
要写入 GRAM,分三步走:
- 设定地址窗口(前面已做)
- 发出“开始写 GRAM”命令:
0x2C - 连续发送颜色数据
每个像素用两个字节表示,采用RGB565 格式:
Bit: 15-------------------------------0 RRRRR GGGGGG BBBBB RRRRR GGGGGG ... 5-bit Red 6-bit Green 5-bit Blue例如红色是0xF800,绿色是0x07E0,蓝色是0x001F。
我们可以封装一个写像素函数:
void draw_pixel(uint16_t x, uint16_t y, uint16_t color) { // 设置单像素地址窗口 st7789v_write_command(0x2A); st7789v_write_data(x >> 8); st7789v_write_data(x & 0xFF); st7789v_write_data(x >> 8); st7789v_write_data(x & 0xFF); st7789v_write_command(0x2B); st7789v_write_data(y >> 8); st7789v_write_data(y & 0xFF); st7789v_write_data(y >> 8); st7789v_write_data(y & 0xFF); // 开始写数据 st7789v_write_command(0x2C); st7789v_write_data(color >> 8); // 高字节 st7789v_write_data(color & 0xFF); // 低字节 }虽然效率不高(每次都要设窗口),但对于调试初期完全够用。
让文字出现在屏幕上:字体渲染入门
现在我们有了画点的能力,就可以拼字符了。
最简单的办法是使用8x16 固定宽度 ASCII 字体,把每个字符做成一个 16 字节的位图数组。每一位代表一个像素:1 是前景色,0 是背景色。
// 示例:空格字符的位图(全0) const uint8_t font8x16[95][16] = { {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // ' ' {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // '!' // ... 其他字符 };然后实现字符绘制函数:
void st7789v_draw_char(uint16_t x, uint16_t y, char c, uint16_t color, uint16_t bgcolor) { if (c < ' ' || c > '~') return; // 只处理可见ASCII uint8_t idx = c - ' '; for (int row = 0; row < 16; row++) { uint8_t bits = font8x16[idx][row]; for (int col = 0; col < 8; col++) { uint16_t pixel_color = (bits << col) & 0x80 ? color : bgcolor; draw_pixel(x + col, y + row, pixel_color); } } }接着就能打印字符串了:
void st7789v_print_string(uint16_t x, uint16_t y, const char* str, uint16_t color, uint16_t bgcolor) { while (*str && x <= 232) { st7789v_draw_char(x, y, *str++, color, bgcolor); x += 8; // 字符宽8像素 } }最后,在主程序中调用:
int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_SPI1_Init(); st7789v_init(); st7789v_print_string(10, 10, "Hello, World!", 0xFFFF, 0x0000); while (1) {} }如果一切顺利,你会看到左上角出现一行白色文字,背景为黑色——你完成了嵌入式显示开发的第一个里程碑。
常见问题排查清单
别灰心,大多数人都会在以下几点卡住:
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| 完全黑屏 | 未退出睡眠、RST未释放、供电不足 | 检查0x11是否发送、RST是否高电平、VCC是否3.3V |
| 花屏/雪花 | SPI速率过高、时序错误、初始化不完整 | 降低SPI频率至10MHz试试,确认所有命令都发了 |
| 文字倒置/横着 | MADCTL 设置错误 | 尝试改为0x60或0xA0,观察效果 |
| 只显示半边 | CASET/RASET 地址设置错误 | 确认 X/Y 范围是否匹配实际分辨率 |
| 刷新闪烁严重 | 每次重绘全屏 | 后续可引入局部刷新或双缓冲机制 |
建议配合逻辑分析仪抓一下 SCLK/MOSI/CS/DC 波形,看看有没有明显异常。
后续还能做什么?
点亮第一行文字只是起点。接下来你可以:
- ✅ 移植成熟的驱动库(如 TFT_eSPI )
- ✅ 添加触摸屏支持(XPT2046 + SPI)
- ✅ 集成 LVGL 图形库,构建按钮、滑块、仪表盘
- ✅ 实现动态刷新,显示时间、温度、传感器数据
- ✅ 做一个迷你天气站、MP3播放器界面、游戏机前端……
你会发现,一旦掌握了底层原理,调库不再是“魔法”,而是工具的选择。
写在最后:理解比复制更重要
很多初学者习惯直接拿别人的工程编译下载,一旦换块屏就束手无策。其实每一个成功的显示项目背后,都是对通信协议、寄存器配置、内存模型的深刻理解。
今天我们亲手走了这一遭:从 SPI 通信到命令解析,从 GRAM 写入到字体渲染,没有隐藏的黑箱,只有清晰的步骤。
当你下次面对 ILI9341、SSD1351 或其他驱动芯片时,会发现它们的套路大同小异——掌握方法论,远胜于记住代码。
所以,请珍惜你的第一行“Hello, World!”。它不只是文字,更是你进入嵌入式图形世界的入场券。
如果你在实现过程中遇到了具体问题,欢迎留言讨论。我们一起把这块屏,彻底点亮。