简介:51单片机ST7789 320×240驱动包面向嵌入式开发者,针对在51单片机上驱动ST7789控制器TFT屏幕的常见难题,系统覆盖硬件SPI接口连接、初始化命令序列、数据读写协议、显示控制与代码优化等关键技能点。压缩包共23个文件,以C源文件、头文件、Hex固件、Keil工程文件为核心,并包含必要的备份、配置文件,整体仅88KB,轻量紧凑,可直接导入51单片机工程使用。该驱动包已有1445人学习下载,是ST7789与51单片机应用开发中值得参考的实例。资源中不仅提供可调用的驱动源码和工程模板,还通过模拟SPI中文显示、时序控制、帧缓冲管理等实现细节,帮助读者理解从底层寄存器配置到上层显示绘制的完整流程。同时涉及缓冲区管理、亮度调节与电源模式处理等实用内容,对学习嵌入式人机交互界面和快速验证屏幕驱动方案有直接帮助。
1. 51 单片机驱动 ST7789 320×240:瓶颈在显存,不在算力
51 单片机加一块 320×240 的 ST7789 彩色屏,很多人第一反应是“带得动吗”。真实情况是,常见的 STC89C52 也能点亮它,但刷新速度、内存占用、坐标方向这几件事,能把第一次上手的人卡住一两天。问题并不是 51 的算力不够,而是整幅画面 320×240×2 字节,约 150KB 的显存需求,51 内部 RAM 却只有 256 字节到 1KB 级别,缓冲区根本放不下。ST7789 自带 20KB 级别 GRAM,能存下一帧,却没法和 51 共享这块数据。所以这篇驱动说明把整个流程拆成接线、命令初始化、窗口寻址、内存策略和排错五段,一步一步落地,适合刚学会 GPIO 输出想尝试 TFT 屏幕的初学者,也能给想快速拿到 51 驱动 ST7789 方案的嵌入式工程师提供一份可以直接抄改的骨架。
2. 51 最小驱动接线:ST7789 屏引脚、软件 SPI 与硬件 SPI 选型
2.1 320×240 屏的引脚定义与一组能直接用的接线表
ST7789 这个芯片在 320×240 模组上最常见的接口是四线 SPI,也就是 SCL、SDA、CS、DC 四条信号线,加上背光、复位和电源,整块屏一共露出八个引脚。买屏时先确认丝印,很多模组把 DIN 写成 SDA、把 CLK 写成 SCL,名称不一样,含义一样。
| 屏引脚 | 含义 | 接 51 芯片 | 备注 |
|---|---|---|---|
| VCC | 屏供电 | 3.3V | 模组带 LDO 时可接 5V,没有 LDO 时只接 3.3V |
| GND | 地 | 系统共地 | 不共地时 SPI 波形看起来正常但屏幕无响应 |
| DIN | SPI 数据输入 | P1.5 | 四线 SPI 模式下只接 MOSI,MISO 通常悬空 |
| CLK | SPI 时钟 | P1.7 | 时钟空闲电平和采样沿决定 CPOL/CPHA |
| CS | 片选 | P1.4 或任意 IO 口 | 只有一块屏时可以直接接地,但仍建议留 GPIO |
| DC | 数据/命令选择 | P1.3 | 低电平写命令,高电平写数据 |
| RESET | 复位 | P1.2 | 上电拉低 20ms 以上再拉高 |
| BLK | 背光控制 | P1.1 或串电阻接 VCC | 高电平点亮,低电平关背光 |
硬件设计时注意几个细节:VCC 和 GND 之间放一个 10μF 电解电容再加一个 0.1μF 陶瓷电容,位置尽量靠近屏的电源排针;背光电流通常在 20mA 到 40mA 之间,如果把它和 51 的 IO 共用同一个稳压器,刷新屏幕时电流波动可能会让单片机复位。最稳妥的做法是背光单独接一个 3.3V 电源轨,IO 只负责切换高低电平。
2.2 软件模拟 SPI:一根普通 GPIO 就能点亮屏幕
标准 8051 架构,比如 STC89C52、AT89S52,没有 SPI 外设,只能用普通 GPIO 逐位翻转。软件模拟 SPI 的时钟频率取决于主频和循环语句条数,在 12MHz 晶振下,一个字节大约需要 7μs 到 10μs。这个速度对初始化命令完全够用,但对全屏刷新来说偏慢,640×480 的图片就别指望了,320×240 一帧 153600 字节,算下来要一秒多。
// 软件模拟 SPI,工作模式 0:空闲低电平,上升沿采样 void spi_write_byte_sw(uint8_t dat) { uint8_t i; for (i = 0; i < 8; i++) { SCLK = 0; // 把时钟拉低,准备移出数据 if (dat & 0x80) { // MSB 先行,先发 bit7 MOSI = 1; } else { MOSI = 0; } dat <<= 1; SCLK = 1; // 上升沿,ST7789 在此时锁存数据 } }这段代码里的关键点是时钟先拉低再拉高,采样发生在上升沿,对应 SPI 模式 0。如果换成模式 3,空闲电平变成高电平,采样仍在上沿,绝大多数 320×240 的 ST7789 模组也能工作,但为了统一,后续所有寄存器配置都以模式 0 为准。软件模拟 SPI 的好处是任何引脚都能用,不需要查复用寄存器,缺点是主循环被占满,程序没法同时做别的事。
2.3 硬件 SPI:STC12/STC15 上更省心的寄存器写法
STC12C5A60S2、STC15 系列和 STC8 系列都带硬件 SPI 模块。以 STC12C5A60S2 为例,SPI 引脚默认复用在一组 P1 口上:P1.4 是 SS,P1.5 是 MOSI,P1.6 是 MISO,P1.7 是 SCLK。配置成主机模式后,写一个字节只是往 SPDAT 寄存器填数据,然后等 SPIF 标志位即可。
// STC12C5A60S2 硬件 SPI 初始化 void spi_init_hw(void) { // SPCTL 各位含义: // SSIG=1 忽略 SS 引脚,SPEN=1 使能 SPI, // DORD=0 高位先发,MSTR=1 主机模式, // CPOL=0 CPHA=0 对应模式 0, // SPR1=0 SPR0=0 分频系数 4 SPCTL = 0xD0; } // 写一个字节并等待完成 uint8_t spi_rw_hw(uint8_t dat) { SPDAT = dat; while (!(SPSTAT & 0x80)); // 等待 SPIF 置位 SPSTAT = 0x80; // 软件清标志,写 1 才有效 return SPDAT; }分频系数 SPR1/SPR0 有四种组合:00 对应 4 分频,01 对应 16 分频,10 对应 64 分频,11 对应 128 分频。主频 12MHz 时,4 分频得到 3MHz SPI 时钟,ST7789 完全支持这个速度。如果主频是 24MHz,建议先用 16 分频保守起见,跑通后再把分频降到 4 分频。硬件 SPI 相比软件模拟大约快三到五倍,全屏刷新能压到 0.3 秒左右,这是 51 上做动态界面的基础。
2.4 上电复位时序:背光亮了不等于控制器就绪
ST7789 的上电要求比普通 LCD 模块严格,电源电压稳定后,RESET 引脚至少要维持 10ms 低电平,然后释放,释放后还要再等 120ms 以上再发第一条命令。这里的延时不是敷衍用的,芯片内部有模拟电路负责产生偏压,电压没有稳定时收到命令会被直接丢弃,表现就是屏幕有背光但永远黑屏。
void lcd_reset(void) { LCD_RST = 0; delay_ms(50); // 拉低 50ms,确保完成硬件复位 LCD_RST = 1; delay_ms(150); // 释放后等待内部电源稳定 }实际操作里,很多人都栽在复位后的第一个延时上。初始化序列里看到 0x11 退出睡眠后再等 120ms 也不要省略,这条延时和硬件复位延时负责的是同一个环节,只是发生在软件层面。如果外接的 RC 复位电路和 51 的复位电路连在一起,屏幕复位时序会受到单片机复位信号影响,建议 RESET 引脚直接用 GPIO 控制,不依赖外部电路。
提示:屏幕没有反应时,先用万用表量 VCC、GND 和背光引脚电压,再查逻辑分析仪上 SCLK 是否有下降沿。绝大多数“驱动代码死活不对”的情况,根源是接线而不是寄存器配置。
3. ST7789 初始化序列与窗口寻址:从写命令到亮起第一个色块
3.1 DC 引脚怎么区分命令和数据
ST7789 通过 DC 引脚的电平区分当前 SPI 总线上传输的是命令还是数据。DC 为低,下一个字节被解析为命令;DC 为高,下一个字节被解析为命令的参数或显存数据。写驱动的第一步就是封装两个最底层的函数。
// 写命令:DC 拉低,发送命令字节 static void lcd_cmd(uint8_t cmd) { LCD_DC = 0; LCD_CS = 0; spi_write_byte(cmd); LCD_CS = 1; } // 写数据:DC 拉高,发送参数或像素数据 static void lcd_dat(uint8_t dat) { LCD_DC = 1; LCD_CS = 0; spi_write_byte(dat); LCD_CS = 1; }注意这里每次传输都把 CS 拉低再拉高。命令和数据分开传输时可以这么写,连续写大量像素时为了性能要把 CS 一直拉低,只发一次片选信号。每个命令的参数个数不同,比如设置像素格式的命令 0x3A 带一个参数,而设置扫描方向的命令 0x36 同样带一个参数,具体格式以 ST7789 参考手册里的寄存器和时序说明为准。
3.2 完整初始化序列:带注释、带延时的可直接使用版本
初始化序列在不同厂家模组之间有差异,但核心步骤是固定的:硬件复位、退出睡眠、设置像素格式、配置电源与伽马值、打开显示。下面这份序列是我在通用 320×240 模组上经常使用的版本,参数偏向兼容性,不是针对单一屏厂的官方代码。
void st7789_init(void) { lcd_reset(); // 硬件复位:拉低 50ms,释放后延时 150ms lcd_cmd(0x01); // 软件复位 delay_ms(150); lcd_cmd(0x11); // Sleep Out,退出睡眠模式 delay_ms(150); // 这 150ms 不能省,等待内部 DC-DC 启动 lcd_cmd(0x36); lcd_dat(0x00); // MADCTL 扫描方向,先写 0,方向问题稍后调 lcd_cmd(0x3A); lcd_dat(0x05); // 像素格式:RGB565,16 位/像素 // 电源与时序设置 lcd_cmd(0xB2); lcd_dat(0x0C); lcd_dat(0x0C); lcd_dat(0x00); lcd_dat(0x33); lcd_dat(0x33); // Porch 设定 lcd_cmd(0xB7); lcd_dat(0x35); // Gate 控制 lcd_cmd(0xBB); lcd_dat(0x19); // VCOM 电压,3.3V 供电常见值 lcd_cmd(0xC0); lcd_dat(0x2C); // LCM 电压控制 lcd_cmd(0xC2); lcd_dat(0x01); // VDV 使能 lcd_cmd(0xC3); lcd_dat(0x12); // VDV 值 lcd_cmd(0xC4); lcd_dat(0x20); // VDV 响应设置 lcd_cmd(0xC6); lcd_dat(0x0F); // 显示时钟频率 lcd_cmd(0xD0); lcd_dat(0xA4); lcd_dat(0xA1); // 调压设置 // 伽马校正,影响灰阶通透度,不影响能否点亮 lcd_cmd(0xE0); lcd_dat(0xD0); lcd_dat(0x04); lcd_dat(0x0D); lcd_dat(0x11); lcd_dat(0x13); lcd_dat(0x2B); lcd_dat(0x3F); lcd_dat(0x54); lcd_dat(0x4C); lcd_dat(0x18); lcd_dat(0x0D); lcd_dat(0x0B); lcd_dat(0x1F); lcd_dat(0x23); lcd_cmd(0xE1); lcd_dat(0xD0); lcd_dat(0x04); lcd_dat(0x0C); lcd_dat(0x11); lcd_dat(0x13); lcd_dat(0x2C); lcd_dat(0x3F); lcd_dat(0x44); lcd_dat(0x51); lcd_dat(0x2F); lcd_dat(0x1F); lcd_dat(0x1F); lcd_dat(0x20); lcd_dat(0x23); lcd_cmd(0x29); // Display On,显示打开 delay_ms(50); }这段代码里有两个容易误解的参数。0x3A 命令收到 0x05,内部会解析为 16 位的 RGB565 格式;很多库写成 0x55,是因为高字节位保留不校验,写 0x55 和写 0x05 效果相同,但按参考手册原始定义,低三位 101 才是有效配置。0xB2 到 0xD0 这组参数决定显示面板的模拟电路工作点,不同模组可能出现亮度差异,调试时如果发现屏发暗或者底色偏灰,优先检查 0xBB 的 VCOM 值而不是怀疑 SPI 接线。
3.3 窗口寻址三件套:CASET、RASET、RAMWR 与大端字节序
ST7789 的显存操作不是从头到尾顺序写,而是先指定一个矩形窗口,然后向窗口内连续灌入像素数据。三个命令分别是 0x2A 列地址、0x2B 行地址、0x2C 开始写入显存。坐标参数为两个字节,先发高字节再发低字节,顺序颠倒会显示错乱。
void lcd_set_window(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { lcd_cmd(0x2A); // 列地址,对应 X 方向 lcd_dat(x0 >> 8); lcd_dat(x0 & 0xFF); lcd_dat(x1 >> 8); lcd_dat(x1 & 0xFF); lcd_cmd(0x2B); // 行地址,对应 Y 方向 lcd_dat(y0 >> 8); lcd_dat(y0 & 0xFF); lcd_dat(y1 >> 8); lcd_dat(y1 & 0xFF); lcd_cmd(0x2C); // 开始写显存数据 }设置完窗口之后,每写两个字节代表一个像素的 RGB565 颜色:第一个字节是高 5 位红色和高 6 位绿色的组合,第二个字节是低 5 位蓝色和绿色低位。窗口的 start 和 end 都是闭区间,全屏写满时 x1、y1 分别取 319 和 239,而不是 320 和 240。ST7789 的 GRAM 会自动跨行连续填充,不需要手动改窗口边界,这给后面做图片流式刷新埋下了基础。
3.4 用小色块验证驱动是否成立再往下走
初始化代码写完不要急着做全屏清屏或者画字库,先用一个只有 16×16 像素的小色块验证。全屏清屏一次要写 153600 字节,如果 SPI 或者窗口方向有问题,满屏全是花点,很难定位原因;而 16×16 色块只有 512 字节,颜色、位置、边界三件事一次就能验证清楚。
uint16_t i; // 画一个 16x16 的红色块,屏幕左上角 lcd_set_window(0, 0, 15, 15); for (i = 0; i < 16 * 16; i++) { lcd_dat(0xF8); // RGB565 红色高字节 lcd_dat(0x00); // 低字节 }如果左上角出现一个红色方块,说明 SPI、复位、DC、初始化序列全部正确。如果出现的是蓝色或者绿色,代表 RGB565 字节序反了,把两个 lcd_dat 参数交换即可。如果方块出现在其他角落,说明 0x36 的扫描方向不对,接下来进入第四章方向调试的逻辑。
4. 51 显存不够怎么办:条带缓冲、分块刷新与屏内 GRAM 复用
4.1 150KB 全屏图像在 51 上注定不能做整幅缓冲
一帧 320×240 的 RGB565 图像需要 320×240×2,算下来 153600 字节。STC89C52 内部只有 128 字节或 256 字节的 RAM,STC12C5A60S2 内部多一块约 1KB 的扩展 RAM,仍然装不下十分之一幅画面。ST7789 内部有 240×320×18 位的 GRAM,约 20KB,能完整存下 320×240 一帧,但它的 GRAM 只能被屏幕控制器读取,51 没法把它当临时画布来使用。
所以 51 驱动 ST7789 的通用做法是放弃全屏重绘,改成局部刷新。每个 UI 控件,比如进度条、波形区、数字显示区,只重绘自己负责的小窗口。这样每次传输量只有几百到几千字节,16MHz 主频下毛刺完全可接受。
4.2 裸点直写与条带缓冲两种模式的选择
最直接的驱动是裸点直写:任何绘图操作都立刻换算成窗口,通过 SPI 写进屏幕显存。
void lcd_draw_point(uint16_t x, uint16_t y, uint16_t color) { lcd_set_window(x, y, x, y); lcd_dat(color >> 8); lcd_dat(color & 0xFF); }这段代码用来画简单图形是够用的,画一条直线就调用一百次,每次调用都要发五条命令带六个参数,效率很低。要显示字符、波形或滚动列表时,更好的方案是维护一块条带缓冲,把屏幕横向按 320 像素宽、纵向按 32 像素高切成条带,先在内存里画好,再整体提交给屏幕。
#define STRIP_H 32 #define STRIP_SIZE (320 * STRIP_H * 2) // 320*32*2 = 20480 字节 // 将缓冲区内容一次性刷到屏幕指定区域 void flush_strip(uint16_t y0) { uint16_t i; lcd_set_window(0, y0, 319, y0 + STRIP_H - 1); LCD_CS = 0; // 片选只拉低一次,提高连续传输效率 for (i = 0; i < STRIP_SIZE; i++) spi_write_byte(strip_buf[i]); LCD_CS = 1; }裸点直写适合界面元素少、每次重绘范围小的场景,比如温度显示每隔一秒变一次。条带缓冲适合做波形滚动、中文字库混排、多行菜单这类频繁覆盖大区域的场景。20KB 的条带缓冲区如果片内放不下,可以放到外部扩展 RAM,或者在 STC15/STC8 上放宽到 64KB 片内存储的型号。
4.3 从 SPI Flash 或串口流式刷全屏图片的代码骨架
要显示整幅图片,常见做法是外挂一片 SPI Flash 存放图片数据,再按行分块读出来写入屏幕。这个过程不需要把整张图装进内存,每读一块刷一块,内存占用只有临时缓冲。
#define CHUNK_ROWS 32 uint8_t chunk_buf[320 * CHUNK_ROWS * 2]; // 20480 字节 void show_image_from_flash(uint32_t img_offset) { uint16_t y; for (y = 0; y < 240; y += CHUNK_ROWS) { // 从外部 Flash 读取 32 行图片数据到缓冲区 spi_flash_read(img_offset + y * 320 * 2, chunk_buf, sizeof(chunk_buf)); // 设置窗口并写入屏幕 lcd_set_window(0, y, 319, y + CHUNK_ROWS - 1); LCD_CS = 0; for (uint16_t i = 0; i < sizeof(chunk_buf); i++) spi_write_byte(chunk_buf[i]); LCD_CS = 1; } }这个流程里每刷 32 行要用 20KB 缓冲区,STM32 这类平台可以改大,51 上 32 行是一个性价比很好的分块尺寸:缓冲区能放进扩展 RAM,刷新次数也只有八次,每次窗口切换的开销可以忽略。需要显示动态波形或者动画时,把整个显示刷新逻辑放在主循环里,每帧只调一次 show_image_from_flash,就能得到一个低速但可靠的幻灯效果。
4.4 想刷动态界面:STC8 的 SPI+DMA 升级路线
如果项目需要流畅刷动画,STC12 系的软查标志位方式还是不够。STC8H 系列仍然属于 51 架构,指令兼容,但片内带有 SPI 和 DMA 控制器,可以把一整块内存数据自动搬运到 SPI 外设,发送完成后触发中断。这种模式下 51 主循环只需要修改条带缓冲区的数据,然后启动一次 DMA 传输,屏幕就会自动刷新。
用这种方式时,条带缓冲可以再一次扩大,直接把 64KB 以内的图片缓存放在 STC8 的扩展 RAM 中,DMA 每搬完一段就更新窗口地址。实际效果是分辨率 320×240 的静态刷新能降到 100ms 以内,动画界面不再是奢求。这套方案仍然延续 51 的开发方式,不需要切换到 ARM 平台,是 51 系驱动 ST7789 最接近“流畅”的落点。
5. 方向、花屏与刷新速度:ST7789 参数调试的三个必查项
5.1 画面镜像和旋转都由 0x36 控制,不是改坐标
ST7789 的扫描方向由 MADCTL 寄存器,也就是命令 0x36 决定,三个关键位是 MY、MX、MV。MY 控制从上到下还是从下到上扫,MX 控制从左到右还是从右到左扫,MV 控制行和列是否交换。注意,旋转 90° 也许搭配设置 MV 位为 1,同时交换宽度和高度,而不是在代码里把所有 x、y 坐标对调。
建议在驱动里固定一个函数集中管理方向,后续所有绘图接口都基于逻辑坐标。
void lcd_set_rotation(uint8_t rot) { switch (rot) { case 0: lcd_cmd(0x36); lcd_dat(0x00); break; // 纵屏,左上角起点 case 1: lcd_cmd(0x36); lcd_dat(0x60); break; // 横屏,左方旋转 case 2: lcd_cmd(0x36); lcd_dat(0xC0); break; // 纵屏,倒置 case 3: lcd_cmd(0x36); lcd_dat(0xA0); break; // 横屏,右方旋转 } }不同模组的排线方向不一样,这四组值不保证每次装上就是正向。调试方法是:先画一个带文字内容的色块,观察镜像方向,再调整对应位的值,直到文字正向且不镜像。
5.2 花屏的三个区间定位:时序、电源、字节序
花屏出问题时要分段定位,不要上来就怀疑初始化序列。先从最原始的环节排查。
| 现象 | 优先检查项 | 对应位置 |
|---|---|---|
| 黑屏无任何反应 | RESET 延时、VCC 电压 | 上电复位流程 |
| 满屏噪点雪花 | SCLK 与 DIN 接反、模式 0/模式 3 设置错 | SPI 接线与 CPOL/CPHA |
| 显示颜色错乱 | RGB565 高字节低字节颠倒 | 写入显存的数据格式 |
| 底色偏灰、亮度忽闪 | 0xBB 的 VCOM 值,匹配屏厂推荐 | 初始化序列电源参数 |
| 画面残影、局部不刷新 | 窗口边界算错,x1/y1 写成尺寸值 | CASET/RASET 参数 |
字节序的问题最隐蔽。RGB565 红色像素高字节是 0xF8,低字节是 0x00,如果把低字节先发,红色会变成暗蓝色甚至紫黑色。确认方法是定义红色、绿色、蓝色三个纯色宏,分别画三个色块,比对输出。
#define COLOR_RED 0xF800 #define COLOR_GREEN 0x07E0 #define COLOR_BLUE 0x001F5.3 刷新速度的最后一个减法:省掉不必要的重建
51 上最有效的优化不是改 SPI 分频,而是减少写入量。全屏清一次颜色大约写 150KB,即便硬件 SPI 也要几十毫秒;许多界面刷新时并不需要全清,只重绘变化区域是最高性价比的优化。其次是连续传输时保持 CS 低电平,避免每个字节都做一次 CS 下降和上升,这个动作对软件模拟 SPI 影响尤其明显。最后才是修改分频系数,STC12 系把分频从 16 改成 4,刷屏时间大约缩短四倍,但要确认 SPI 时钟不超过 ST7789 手册给出的上限,51 平台上一般跑 4MHz 以内足够稳定。
本文还有配套的精品资源,点击获取