简介:基于STC单片机与ST7735驱动的128x160 TFT彩屏显示方案,面向嵌入式开发者和电子爱好者,解决图片显示从数据转换到屏幕驱动的完整实现问题。压缩包共72个文件,包含C语言源程序、头文件、Keil工程文件、Hex固件、链接映射文件及一段TFT显示效果演示视频,整体仅1.21MB。已有1171人学习下载,适合希望快速上手STC15系列配合SPI接口点亮小尺寸彩屏的读者。资源内除了完整的main.c、ST7735驱动和GUI图形库外,还提供延迟、串口、EEPROM、ADC、PCA等外设驱动模块,方便直接移植和二次开发;视频演示可直观核对运行效果,hex文件可直接烧录验证。对于想深入理解RGB565格式转换、双缓冲显示和代码优化的人群,也能从源码和工程结构中直接获得参考。
1. STC15 直接刷 ST7735:为什么 128x160 这种小屏反而最考优化
STC 单片机和 ST7735 驱动的 1.8 寸 TFT 屏在电子工程里算是“性价比搭档”:一颗几块钱的 STC15F、基本不用外扩 RAM,就能点亮 128x160 像素的彩屏。很多人照着网上的初始化代码一跑就花屏,或者能静态显示 logo 却一刷动态画面就闪烁,问题多半出在没搞清 ST7735 的 SPI 时序和 128x160 的整帧数据量。128x160 全彩 RGB565 意味着 32KB 帧缓冲,而 STC15 片内 RAM 只有几 KB,这决定了代码不能走“先在内存里拼好整帧再刷新”的老路。这套包含 ST7735S 驱动、GUI、字库和软串口的工程正好把我平时代码里最花时间的几块一次补齐,适合刚做显示模块以及想把固定界面做成菜单的开发者。
2. ST7735 的 SPI 时序与初始化序列:先让 1.8 寸屏告别花屏
2.1 STC15 的 IO 选型:硬件 SPI 还是普通 IO 模拟
驱动 1.8 寸 ST7735 屏,通信线最少只要四根:SCLK、MOSI、DC 和 CS,再加上控制用的 RST 和可选的背光 BL。STC15 系列很多型号片内自带硬件 SPI,但工程里更常见的做法是把普通 IO 配成模拟 SPI,原因是 STC15 的硬件 SPI 引脚往往和 P1.5/P1.6/P1.7 绑定,而这几根脚在不同封装里可能已经被按键、串口或外部中断占用。下面这组引脚连接是我拆类似工程时最常用的默认配置,具体映射最终以 config.h 里的宏定义为准。
| 功能 | STC15 引脚示例 | 说明 |
|---|---|---|
| SCLK | P1.7 | SPI 时钟,空闲电平由初始化时序决定 |
| MOSI | P1.6 | 屏的 SDA 数据输入,注意 ST7735 没有 MISO |
| DC | P2.0 | 数据/命令选择,高电平写数据,低电平写命令 |
| CS | P2.1 | 片选,低电平有效 |
| RST | P2.2 | 复位,低电平复位,复位后拉高 |
| BL | P2.3 | 背光,可以接 PWM 调亮度 |
选择模拟 SPI 还有个好处:下载程序时用到的 P3.0/P3.1 不会被误碰,调试时想换引脚只需要改宏定义。代价是 GPIO 翻转速度不如硬件 SPI,对 128x160 全屏刷新来说,模拟 SPI 单字节翻转和数据移位总共耗时更长。不过 STC15 是 1T 单片机,时钟跑到 24MHz 以上时,模拟 SPI 配 4MHz 的 SCK 问题不大。真正限制刷新率的不是 SPI 速度,而是后面要讲到的存储系统和逐点计算。
2.2 写命令和写数据的底层函数:DC 线要稳,CS 不要乱跳
ST7735 的写时序很简单:SCK 上升沿采样 MOSI 上的电平,DC 决定写入的是命令还是数据。底层函数拆成只做一件事:
void ST7735_WrCmd(uint8_t cmd) { DC_PIN = 0; // 命令模式 CS_PIN = 0; SpiSendByte(cmd); CS_PIN = 1; } void ST7735_WrData(uint8_t dat) { DC_PIN = 1; // 数据模式 CS_PIN = 0; SpiSendByte(dat); CS_PIN = 1; }逻辑说明:SpiSendByte 是在 ST7735S.c 里实现的模拟 SPI 函数,循环 8 次,按位拉高或拉低 MOSI,再翻转 SCK。命令和数据在硬件层面没有区别,区别只在 DC 引脚状态,因此两个函数必须保证 DC 在 SCK 边沿之前稳定,不能在同一拍里一边改 DC 一边发时钟。参数说明:CS 每字节都拉高一次适合配置阶段,因为寄存器写入之间留出间隔反而更稳定;但后面写大批量像素数据时不能每字节拉高,否则会产生额外的 CS 无效间隙,部分批次屏会把这些间隙当成传输结束,导致画面出现随机细线。
延时也是容易翻车的地方。STC15 用软件延时函数 DelayMs,如果主频从 11.0592MHz 改成 24MHz 但没有同步改延时参数,整个初始化时序会快一倍。ST7735 对寄存器写入间隔多数没有严格限制,只有软件复位到 SLPOUT 这段必须有足够时间,至少在代码里留出 120ms 的可靠延时。
2.3 初始化序列:SLPOUT 之后不等够 120ms,屏就会白得理直气壮
ST7735 的初始化序列看着像一长串寄存器数值,实际拆开只有四个阶段:软件复位、退出睡眠、设置访问方式、开显示。最常见的失败是复位后立刻写显示命令,或者 SLPOUT 后没有延时就直接 DISPON。下面这段是去掉伽马调节后的最小可用初始化:
void ST7735_Init(void) { ST7735_RST_Clr(); DelayMs(10); ST7735_RST_Set(); DelayMs(120); // 等待内部振荡器稳定 ST7735_WrCmd(0x11); // SLPOUT DelayMs(120); // 必须等到 120ms,否则白屏 ST7735_WrCmd(0x36); // MADCTL 扫描方向 ST7735_WrData(0xC0); ST7735_WrCmd(0x3A); // COLMOD 颜色模式 ST7735_WrData(0x05); // RGB565,每像素 2 字节 ST7735_WrCmd(0x20); // INVOFF ST7735_WrCmd(0x29); // DISPON DelayMs(50); }逻辑说明:0x11 是 SLPOUT,退出睡眠模式后内部电源和时钟才完全启动,所以必须等待。0x36 的 MADCTL 控制行列扫描方向和 RGB/BGR 顺序,0x3A 写入 0x05 代表 16 位色深,这两个寄存器不匹配就会出“颜色通道互换”和“文字镜像”问题。参数说明:0x20 是 INVOFF,如果屏买回来显示反色,可以把这一句改成 INVON,也就是 0x21;这条命令只反转像素极性,不动 RGB 顺序,和 MADCTL 里的 BGR 位是两回事。工程里 ST7735S.c 提供的初始化序列比我这里更长,主要是补了伽马曲线和电源设置,那些参数对色彩观感和温度稳定性有影响,建议保留原值。
2.4 CASET/RASET 地址窗口:160 被 uint8_t 截断是第一个坑
ST7735 写像素前必须告诉它“接下来要更新屏幕的哪一块矩形区域”,这就是 CASET(列地址)和 RASET(行地址)。128x160 全屏更新时,列范围是 0~127,行范围是 0~159。地址以 16 位方式发送,高字节在前。
void ST7735_SetAddress(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { ST7735_WrCmd(0x2A); // CASET 列地址 ST7735_WrData(x0 >> 8); ST7735_WrData(x0 & 0xFF); ST7735_WrData(x1 >> 8); ST7735_WrData(x1 & 0xFF); ST7735_WrCmd(0x2B); // RASET 行地址 ST7735_WrData(y0 >> 8); ST7735_WrData(y0 & 0xFF); ST7735_WrData(y1 >> 8); ST7735_WrData(y1 & 0xFF); ST7735_WrCmd(0x2C); // RAMWR 开始写像素 }这里有两个典型坑。第一个是参数类型,x1、y1 看起来没超过 255,但如果中间计算 x0 + w 时用了 uint8_t,w 等于 160 就会溢出变成 0,屏幕会只刷新一部分。所以地址窗口函数参数必须声明为 uint16_t。第二个坑是 ST7735 的地址窗口是包含终点的,也就是说 CASET 传 0 和 127 表示从第 0 列到第 127 列,恰好 128 列;如果照着 GraphLCD 那种“终点不含在内”的习惯写,每行会少画一个像素,画面就会斜向错位。写完地址窗口后,每写两个字节代表一个像素,RGB565 低字节先发还是高字节先发取决于你在初始化里怎么约定,一般屏幕厂商示例代码用低字节在前,和后面图片转换脚本保持一致即可。
3. 图片转 RGB565 数组:128x160 的 40960 字节怎么塞进 STC 的资源
3.1 图片不是直接烧进去的:先说清 JPEG 和 BMP 为什么不能用
ST7735 只认像素流,不认文件格式。无论你手头是 JPEG、PNG 还是 24 位 BMP,最终都要转换成 RGB565 的原始字节流:每个像素 16 位,其中红色取 5 位、绿色取 6 位、蓝色取 5 位。转换前还要把图片裁剪或缩放到 128x160,否则屏只显示图片左上角的一部分。很多工程直接用取模软件生成数组,但取模软件处理大图时界面卡顿,而且不方便批量替换图片。更好的方式是用脚本离线转换,让程序只负责“忠实搬砖”。下面这段 Python 脚本可以在 PC 上把任意图片直接变成能放进 Keil 工程的 C 头文件。
3.2 Python 脚本批量转码:输出 image_data.h 并控制字节序
from PIL import Image import sys W, H = 128, 160 src = Image.open(sys.argv[1]).convert('RGB').resize((W, H)) out = bytearray() for y in range(H): for x in range(W): r, g, b = src.getpixel((x, y)) rgb565 = ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3) out += bytes([rgb565 & 0xFF, rgb565 >> 8]) with open('image_data.h', 'w') as f: f.write("const unsigned char image_data[%d] = {\n" % len(out)) for i in range(0, len(out), 16): f.write(" " + ",".join(f"0x{x:02X}" for x in out[i:i+16]) + ",\n") f.write("};\n")逻辑说明:PIL 先把输入图片统一转成 RGB,再 resize 到 128x160。逐像素取出 8 位 R、G、B,然后分别右移 3、2、3 位得到 5/6/5 位分量,拼成一个 16 位整数。写出时先写低字节,再写高字节,这样和 ST7735_WrData 发送顺序一致。参数说明:resize 默认用双三次插值,适合照片类图片;如果原图是卡通或仪表盘界面,建议显式加上Image.Resampling.NEAREST,否则边缘会出现一圈灰色过渡,在对比度高的 UI 上很难看。转换后的数组一共 40960 字节。
如果屏幕设置了 RGB 顺序而不是 BGR,图片红蓝会互换,人像皮肤偏紫偏青。处理办法有两个:一是改初始化 MADCTL 里的 BGR 位,二是把脚本里的 r 和 b 互换后再转换。我一般倾向改初始化寄存器,因为同一张图片数据可以在不同屏之间通用,不用重新生成数组。
3.3 内部 code 还是外部 EEPROM:按 Flash 容量分三种做法
128x160 整屏图片占 40960 字节,加上 hz16x16.h、hz32x32.h 这类字库,KEIL C51 一次编译出来的 hex 很容易接近或超过芯片 Flash。STC15F2K60S2 这类型号 Flash 大约 60KB,看起来刚够放一张全屏图加一个 16x16 汉字库,但实际工程还要放串口、定时器、GUI 代码,链接时常见报错是L105或L107,意思是 CODE 段溢出。所以实际项目要分场景选择存储位置。
| 存储方案 | 额外硬件 | 读速度 | 典型用途 |
|---|---|---|---|
| const code 数组 | 无 | 最快,类似查表 | 图标、开机 logo、小尺寸菜单图片 |
| I2C EEPROM AT24C256 | 芯片成本低 | 页写慢,读取一般 | 可远程更新的图片、配置参数 |
| SPI NOR Flash | 容量大数量少 | 比 I2C 快 | 多张全屏图或字库外置 |
| 压缩后解码 | 占用 Flash 小 | CPU 开销大 | STC8A 等高主频系列 |
这套工程带上 EEPROM.c 和 Soft_UART.c,正好说明作者考虑了“图片不常驻 code 区”的方案。我一般复用时会把图片用脚本转成裸 bin 数据,通过软串口下发到外置 EEPROM,代码区只保留 EEPROM 页写函数和显示函数。这样产品换图不需要重新烧录单片机,只要上位机发一包数据过去就行。
3.4 从外部存储送显:一次读一行,而不是一次读一屏
从 EEPROM 显示整幅图时,最错误的做法是定义一个大数组unsigned char buf[40960]把图全部读进 RAM,因为 STC15F2K 系列的 XDATA 通常只有 2048 字节,直接溢出不说,还会拖慢整个程序。正确做法是配合 ST7735 的 RAMWR 连续写特性,一次只搬一小段数据到 RAM,再推到 SPI。代码骨架如下:
uint8_t linebuf[256]; void ShowPicFromEeprom(uint16_t base_addr) { uint16_t i, size = 128 * 160 * 2; ST7735_SetAddress(0, 0, 127, 159); // 整屏窗口 for (i = 0; i < size; i += 256) { EEPROM_Read(base_addr + i, linebuf, 256); ST7735_WrDataMany(linebuf, 256); // CS 全程拉低,连续发 } }逻辑说明:EEPROM_Read 从外部存储偏移位置读 256 字节到 linebuf,ST7735_WrDataMany 把 linebuf 里的数据按顺序发送,中途不拉高 CS。为什么选 256 字节而不是 512?因为 128 像素一行正好是 256 字节,按行切分逻辑最简单。参数说明:EEPROM 读地址 base_addr 表示图片数据在外部存储中的起始偏移。AT24C256 一页通常 64 字节,跨页连续读没问题,但写入时必须按 64 字节页边界对齐,否则同一个页内写地址会回卷,导致覆盖前面几个字节。
4. 把焦点从“显示图片”挪到“显示界面”:GUI 库和汉字字形
4.1 工程里的 GUI.c 不只是画点:像素操作是其他控件的地基
很多资源包里的 GUI 层,看起来函数很多,实际上底层都是从一个画点函数长出来的。GUI_DrawPoint 负责把单个像素写进 ST7735 指定坐标,其他画线、画矩形、显示字符最终都要调用它。画点本身很简单,但要注意 ST7735 没有“只写一个像素”的快捷命令,每画一个点都要重新设置一次 CASET/RASET。如果直接用最朴素的画点函数刷整屏,一万个点就要重复两万次命令,速度极慢。所以在 GUI 层我会先做区域合并:能一次设置地址窗口的地方,绝不逐点调用。
void GUI_DrawPoint(uint16_t x, uint16_t y, uint16_t color) { if (x >= 128 || y >= 160) return; // 越界直接丢弃,避免地址错位 ST7735_SetAddress(x, y, x, y); ST7735_WrData(color & 0xFF); ST7735_WrData(color >> 8); }参数说明:color 是 RGB565 原始值,比如纯红是 0xF800,纯绿是 0x07E0,纯蓝是 0x001F。这里先发低字节是为了和图片数组保持一致,如果初始化里另选了高字节在前,要统一改。越界判断必须保留,因为菜单滚动时坐标可能为负,一旦传入 ST7735_SetAddress 的 x 是负数,无符号整数会变成一个极大的数,轻则画错位置,重则让地址窗口无效。画点函数适合文字、图表这类稀疏像素,不适合大面积色块。大面积填充应该调用类似 GUI_FillRect 的函数,它把整个矩形设置成一次地址窗口,再循环发送相同颜色字节,效率能提升几十倍。
4.2 汉字显示:从 hz16x16.h 里取字模,按行拼像素
工程里有 hz16x16.h、hz32x32.h、zifu8x16.h,说明至少内置了 16x16 汉字、32x32 汉字和 8x16 数字字母。16x16 汉字最常用:每个汉字显示区域是 16 宽 16 高,按“逐行式”取模,一行 16 个点分成两个字节,高位在左。这样一个字的字模就是 32 个字节。取模软件里如果选成了“列行式”,字会转动 90 度,所以显示代码和取模方向必须配套。
void GUI_ShowHz16(uint16_t x, uint16_t y, uint16_t fc, uint16_t bc, const unsigned char *str) { while (str[0] && str[1]) { uint16_t qh = str[0] - 0xA0; uint16_t ql = str[1] - 0xA0; uint16_t idx = qh * 94 + ql; // GB2312 区位码转字库索引 const unsigned char *font = &hz16_font[idx * 32]; for (uint16_t row = 0; row < 16; row++) { for (uint16_t col = 0; col < 16; col++) { uint8_t byte = font[row * 2 + col / 8]; if (byte & (0x80 >> (col % 8))) { GUI_DrawPoint(x + col, y + row, fc); } else { GUI_DrawPoint(x + col, y + row, bc); } } } x += 16; str += 2; } }逻辑说明:GB2312 编码的汉字两个字节都大于 0xA0,减去 0xA0 后得区位码,再乘以每字 32 字节得到字库偏移。这个公式只对完整 GB2312 字符集有效。如果工程里用的字库只包含常用汉字,就必须改成查表而不是直接算索引。参数说明:fc 是前景色,bc 是背景色。菜单场景中背景色通常设为 0x0000,但 GUI_DrawPoint 每画一个点都要计算一次颜色值,所以这个函数跑一次 16x16 汉字要调用 256 次画点,耗时非常可观。想要提高汉字刷新速度,就在初始化里把背景色相等的情况分支出去:背景为黑时不画 0 像素,直接跳过 GUI_DrawPoint,减少一半以上的 SPI 写入。
4.3 行缓冲法显示整幅图:RAM 占用压到 256 字节
前面说过帧缓冲太大,但逐点写入又慢,中间路线就是行缓冲。每一行 128 像素在 RGB565 下正好 256 字节,准备一行发一行,不需要把整帧数据放在 RAM 里。资源里的 main.c 最后烧录的 hex 能跑通全屏图片,本质上也是这类思路。我一般会在 GUI_C 里单独开一个 256 字节的linebuf[],既做图片显示缓冲,也做文字点阵拼接缓冲。
uint8_t linebuf[256]; void GUI_ShowImage565(const unsigned char *img, uint16_t x, uint16_t y, uint16_t w, uint16_t h) { ST7735_SetAddress(x, y, x + w - 1, y + h - 1); for (uint16_t row = 0; row < h; row++) { uint32_t offset = (uint32_t)(row * w) * 2; for (uint16_t col = 0; col < w * 2; col++) { linebuf[col] = img[offset + col]; } ST7735_WrDataMany(linebuf, w * 2); } }运算逻辑:offset 必须以 uint32_t 计算,因为row * w * 2在 128x160 的图上最大值是 40958,uint16_t 还能扛住,但如果改成显示两张横向拼接的大图,就很容易超出 65535。ST7735_WrDataMany 内部不做任何地址窗口操作,只把 DC 拉高后连续写指定字节数,中间保持 CS 为低。参数说明:这个函数适合整区域更新,但如果要显示背景图上的半透明文字,行缓冲还要额外做一次“先读原背景再混合”的操作,ST7735 没有读回显存的功能,所以 51 上做半透明通常是用两张图片数据提前合成好,再一次性输出。这也解释了为什么很多 STC 项目里界面是“整块贴图 + 简单文字”而不是复杂图层叠加。
4.4 双缓冲的误用:为什么 STC15 上完整双缓冲不现实
双缓冲能消除闪烁,但完整双缓冲需要两个 32KB 帧缓冲,一共 64KB 内存,这在片内只有 2KB XDATA 的 STC15 上完全不现实,外扩 32KB SRAM 等于把简单项目复杂化。因此 51 工程里说的双缓冲,大多是指“局部双缓冲”:把界面切成背景层和前景层,背景图只在切换页面时更新,前景菜单文字落入一个很小的重绘区域。我改动这套工程时会维护一个 dirty_rect,记录上一次文字位置和当前文字位置的并集,只对这块矩形做清屏和重画。这样一帧需要写的像素从 40960 字节降到几个汉字区域,闪烁自然消失。下图这个表格是不同重绘策略的量化对比:
| 方案 | XDATA 占用 | 整屏写入量 | 闪烁程度 | 适用场景 |
|---|---|---|---|---|
| 全屏逐点重绘 | 0 | 40960 字节 | 明显 | 演示、开机动画 |
| 行缓冲整屏刷新 | 256 B | 40960 字节 | 轻微 | 全屏图片切换 |
| 局部脏矩形刷新 | 64~512 B | 远小于全屏 | 几乎无 | 菜单、仪表盘 |
| 完整帧缓冲双缓冲 | 64KB+ | 40960 字节 | 无 | 外扩 RAM 或换 MCU |
局部刷新最重要的一点是地址窗口不能越界,尤其是清除文字背景时,如果只清文字区域而忘了右侧没写满的半个汉字像素,就会出现残影。我看过不少 GUI 库的 LCD_Clear 实现是整屏刷白,在 51 上这是最消耗性能的操作。正确做法是把清除范围限制在控件尺寸范围内,用 GUI_FillRect 对指定矩形填充背景色,而不是调全屏清屏。
5. 用示波器和秒表把刷新率从 8fps 拉到 14fps
5.1 先测瓶颈:一帧耗时拆成三段
优化前不要凭感觉改随机参数。先在 main.c 里找显示图片循环,用定时器记录一帧开始和结束的时间,再通过串口打印毫秒数。STC15 的定时器和 Soft_UART 在这套工程里都是现成的。
uint16_t t0, elapse; while (1) { t0 = Timer2_GetMs(); GUI_ShowImage565(frameA, 0, 0, 128, 160); elapse = Timer2_GetMs() - t0; UART1_SendHex(elapse); }把得到的一帧耗时分三段看:内存拷贝到 linebuf、SPI 数据搬移、以及地址窗口命令开销。128x160 全屏 40960 字节,在 4MHz SPI 下理论传输就要 82ms,再去掉地址计算和函数调用,帧率大约 10fps 左右,不算离谱。如果菌疒只有 8fps,说明 SPI 空转太多,检查 SCK 是否夹带了多余延迟或每字节都做了函数调用。
5.2 提参数:SPI 分频、中断和编译器优化
常见做法是把模拟 SPI 的字节发送循环展开,或者改用硬件 SPI。STC15 的硬件 SPI 时钟源来自主时钟分频,选择 CLK 分频系数后,SCK 速率能到 10MHz 以上,但要注意杜邦线超过 10cm 时信号边沿会变圆,反而增加误码。搬送大数据时临时关闭不必要的中断也可以减少抖动,但串口收图期间别关中断,否则下位机一包数据只能掉几个字节。这里列几个我验证过效果的改动:
| 改动 | 预期收益 | 风险 |
|---|---|---|
| SPI 时钟从 2MHz 提到 8MHz | 小于 1ms 提高到约 5ms/帧 | 长线干扰、花点 |
| Keil C51 优化级别从 0 改成 9 | 循环和地址计算明显缩小 | 位操作时序可能出问题需重测 |
| 显示前用 linebuf 拼一行再发 | 减少逐点 SetAddress 开销 | 多占 256B XDATA |
| 大面积纯色用 FillRect | 少发一半像素数据 | 无 |
这四项里,row fill 是最容易见效的。比如一个蓝色背景占比 80% 的界面,用 FillRect 先填背景,再在上面贴图标,整屏写入量从 40960 字节降到几千字节,刷新率会翻倍不止。
5.3 SCAN 方向与色序对照:换屏不换逻辑
ST7735 的 MADCTL 寄存器是白屏和镜像问题的重灾区。0x36 里 BIT7 是 MY,BIT6 是 MX,BIT5 是 MV,BIT3 是 BGR。MY 置位会让行扫描反向,MX 置位会让列扫描反向,MV 置位则交换行列,相当于横竖屏切换。如果你的字是镜像的,翻转 MY 或 MX 中对应的那一位即可;如果红色和蓝色互换,翻转 BGR 位。不同批次的 1.8 寸屏出厂初始化值不完全一样,ST7735R 和 ST7735S 在同一块位置显示可能一个起点在左上角,一个起点在右上角。我一般把 MADCTL 的初始值单独定义成宏放在 config.h:
#define ST7735_MADCTL_SET 0xC0 #define ST7735_COLMOD_SET 0x05这样换屏时只用改宏,而不用去翻初始化函数。如果改完仍然错位,就在 SET 值上逐个切 MY、MX、MV,每次只动一位,观察哪个方向变化符合预期。注意,MV 位切换横竖屏后,原来逻辑上的 x 和 y 坐标也要交换读写,否则触摸坐标和显示内容方向是拧着的。最后留一个经验:焊接好屏后先用万用表确认 RST 引脚不是虚高,很多屏上电后内置复位不可靠,程序里主动拉低再拉高这一下是不能省的。
本文还有配套的精品资源,点击获取