ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

STC15驱动128x160 ST7735小屏:SPI时序、字库与刷新率优化

STC15驱动128x160 ST7735小屏:SPI时序、字库与刷新率优化 简介基于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 引脚示例说明SCLKP1.7SPI 时钟空闲电平由初始化时序决定MOSIP1.6屏的 SDA 数据输入注意 ST7735 没有 MISODCP2.0数据/命令选择高电平写数据低电平写命令CSP2.1片选低电平有效RSTP2.2复位低电平复位复位后拉高BLP2.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_tw 等于 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(f0x{x:02X} for x in out[i:i16]) ,\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 字节到 linebufST7735_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 的图上最大值是 40958uint16_t 还能扛住但如果改成显示两张横向拼接的大图就很容易超出 65535。ST7735_WrDataMany 内部不做任何地址窗口操作只把 DC 拉高后连续写指定字节数中间保持 CS 为低。参数说明这个函数适合整区域更新但如果要显示背景图上的半透明文字行缓冲还要额外做一次“先读原背景再混合”的操作ST7735 没有读回显存的功能所以 51 上做半透明通常是用两张图片数据提前合成好再一次性输出。这也解释了为什么很多 STC 项目里界面是“整块贴图 简单文字”而不是复杂图层叠加。4.4 双缓冲的误用为什么 STC15 上完整双缓冲不现实双缓冲能消除闪烁但完整双缓冲需要两个 32KB 帧缓冲一共 64KB 内存这在片内只有 2KB XDATA 的 STC15 上完全不现实外扩 32KB SRAM 等于把简单项目复杂化。因此 51 工程里说的双缓冲大多是指“局部双缓冲”把界面切成背景层和前景层背景图只在切换页面时更新前景菜单文字落入一个很小的重绘区域。我改动这套工程时会维护一个 dirty_rect记录上一次文字位置和当前文字位置的并集只对这块矩形做清屏和重画。这样一帧需要写的像素从 40960 字节降到几个汉字区域闪烁自然消失。下图这个表格是不同重绘策略的量化对比方案XDATA 占用整屏写入量闪烁程度适用场景全屏逐点重绘040960 字节明显演示、开机动画行缓冲整屏刷新256 B40960 字节轻微全屏图片切换局部脏矩形刷新64~512 B远小于全屏几乎无菜单、仪表盘完整帧缓冲双缓冲64KB40960 字节无外扩 RAM 或换 MCU局部刷新最重要的一点是地址窗口不能越界尤其是清除文字背景时如果只清文字区域而忘了右侧没写满的半个汉字像素就会出现残影。我看过不少 GUI 库的 LCD_Clear 实现是整屏刷白在 51 上这是最消耗性能的操作。正确做法是把清除范围限制在控件尺寸范围内用 GUI_FillRect 对指定矩形填充背景色而不是调全屏清屏。5. 用示波器和秒表把刷新率从 8fps 拉到 14fps5.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 是 MYBIT6 是 MXBIT5 是 MVBIT3 是 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 引脚不是虚高很多屏上电后内置复位不可靠程序里主动拉低再拉高这一下是不能省的。本文还有配套的精品资源点击获取
返回列表