简介一套基于STC单片机与ST7735驱动芯片的1.8寸128x160 TFT图片显示完整工程面向嵌入式入门开发者解决彩色屏幕驱动与图片显示的具体实现问题。资源包共72个文件涵盖Keil工程源码C/H文件、编译中间文件obj、lst、hex及演示视频mp4压缩包整体约1.21MB结构清晰。目前已有1171人学习适合通过实际工程学习SPI通信、RGB565图片转换及GUI字库调用等关键知识。资料内置STC15X库、ST7735S驱动、GUI界面模块和多种字库文件并提供可直接烧录的hex固件与效果演示视频方便读者快速复现和二次开发是开发智能家居、仪表显示等小型嵌入式系统的实用参考。1. 128x160 图片塞进 STC 单片机先破除“没有文件系统”的误区很多人一听到 STC 单片机要显示 128x160 图片第一反应是“容量不够吧要不要加外部Flash”实际答案恰恰相反ST7735 这块 1.8 寸 TFT 彩屏内部自带 128x160 的显存每像素 16bit合 40KB。图片要做的只是以 RGB565 格式准备好 40KB 数据然后通过 SPI 连续写进显存。STC8A8K64S4 这类带 64KB Flash 的芯片完全可以把一整张图直接塞进 code 数组开机就显示不需要文件系统、不需要外挂 Flash、也不需要学虚拟文件系统那套。这篇就按 STC单片机 ST7735 这套组合从初始化命令到图片数组生成再到全屏刷新和排错把 128x160 图片显示这条路走通。2. ST7735 驱动模型初始化命令、SPI 时序与引脚规划2.1 为什么“会SPI”不等于“屏会亮”ST7735 上电后处于 Sleep In 状态内部振荡器未开启背光默认由模块电路控制。要让它出图必须先按固定顺序写一组控制寄存器软复位、唤醒、设置显示方向、色彩格式、帧率和伽马最后开启显示。任何一步没执行到屏幕就可能白屏或花屏。和字符屏、OLED 屏不同ST7735 没有内置字库也没有“图片显示模式”所谓显示图片就是把像素值直接填进它的 GRAM。因此驱动模型可以简化为三件事初始化寄存器、设置窗口、写像素数据。下面代码全部基于 GPIO 模拟 SPI因为 STC 不同系列硬件 SPI 寄存器差异较大模拟 SPI 在这套组合里移植性最好。2.2 最小引脚表与电平匹配TFT 模块引脚接到 STC8A8K64S4说明VCC3.3V模块供电背光消耗约 20-40mAGNDGND共地必须接CSP1.0片选低有效整帧刷新期间保持低RESETP1.1复位低脉冲后拉高DC/RSP1.2命令/数据切换0 命令、1 数据SDAP1.3SPI MOSI模式 0 上升沿采样SCLKP1.4SPI 时钟空闲为低LEDA3.3V 或 PWM背光正极串电阻可调亮度注意 VCC 和 LEDA 在很多红色 PCB 模组上是分开引出的。如果 STC 用 5V 供电而模块没带电平转换芯片建议在 SDA、SCLK、CS、DC、RESET 五根线上各串一个 1kΩ 电阻再进 3.3V 模块背光部分直接由独立 3.3V 供电。我一般会先看模块背面有没有 74LVC 一类的双向转换芯片有就直接接 5V 单片机没有就串电阻避免长时间把 3.3V 芯片的 IO 拉到 5V。2.3 基础 SPI 收发与命令/数据区分sbit TFT_CS P1^0; sbit TFT_RST P1^1; sbit TFT_DC P1^2; sbit TFT_SDA P1^3; sbit TFT_SCLK P1^4; void spi_write_byte(unsigned char dat) { unsigned char i; for (i 0; i 8; i) { TFT_SCLK 0; if (dat 0x80) TFT_SDA 1; else TFT_SDA 0; dat 1; TFT_SCLK 1; } } void st7735_write_cmd(unsigned char cmd) { TFT_DC 0; spi_write_byte(cmd); } void st7735_write_data(unsigned char dat) { TFT_DC 1; spi_write_byte(dat); }逻辑说明SPI mode 0 是时钟空闲低电平、上升沿锁存数据所以先在SCLK0的时候摆好 SDA 电平再拉高时钟。DC 这根线在发送命令和发送数据之间必须切换示波器上最容易看错的就是 DC 切换太早或太晚。spi_write_byte每次都从最高位开始发正好对应 ST7735 先收高字节的要求。参数说明sbit定义的是 STC 的位寻址 IOP1.0 到 P1.4 都可以位寻址。若换成硬件 SPI只需要把spi_write_byte替换成写SPDAT寄存器并等待SPIF标志DC 的处理完全一样。为了让 3.3V 模块能可靠识别高电平SCLK 和 SDA 建议配置为推挽输出不要用默认的准双向口硬拉。2.4 初始化序列里必须先定住的命令参数void st7735_init(void) { TFT_RST 1; delay_ms(10); TFT_RST 0; delay_ms(10); TFT_RST 1; delay_ms(120); st7735_write_cmd(0x11); delay_ms(120); // 退出睡眠 st7735_write_cmd(0xB1); // 帧率控制 st7735_write_data(0x01); st7735_write_data(0x2C); st7735_write_data(0x2D); st7735_write_cmd(0xB2); st7735_write_data(0x01); st7735_write_data(0x2C); st7735_write_data(0x2D); st7735_write_cmd(0xB3); st7735_write_data(0x01); st7735_write_data(0x2C); st7735_write_data(0x2D); st7735_write_data(0x01); st7735_write_data(0x2C); st7735_write_data(0x2D); st7735_write_cmd(0xB4); st7735_write_data(0x07); st7735_write_cmd(0xC0); st7735_write_data(0xA2); st7735_write_data(0x02); st7735_write_data(0x84); st7735_write_cmd(0xC1); st7735_write_data(0xC5); st7735_write_cmd(0xC2); st7735_write_data(0x0A); st7735_write_data(0x00); st7735_write_cmd(0xC3); st7735_write_data(0x8A); st7735_write_data(0x2A); st7735_write_cmd(0xC4); st7735_write_data(0x8A); st7735_write_data(0xEE); st7735_write_cmd(0xC5); st7735_write_data(0x0E); st7735_write_cmd(0x36); st7735_write_data(0xA0); // MADCTL扫描方向RGB st7735_write_cmd(0x3A); st7735_write_data(0x05); // 16bit/pixel st7735_write_cmd(0xE0); // 正伽马 st7735_write_data(0x02); st7735_write_data(0x1C); st7735_write_data(0x07); st7735_write_data(0x12); st7735_write_data(0x37); st7735_write_data(0x32); st7735_write_data(0x29); st7735_write_data(0x2D); st7735_write_data(0x29); st7735_write_data(0x25); st7735_write_data(0x2B); st7735_write_data(0x39); st7735_write_data(0x00); st7735_write_data(0x01); st7735_write_data(0x03); st7735_write_data(0x10); st7735_write_cmd(0xE1); // 负伽马 st7735_write_data(0x03); st7735_write_data(0x1D); st7735_write_data(0x07); st7735_write_data(0x06); st7735_write_data(0x2E); st7735_write_data(0x2C); st7735_write_data(0x29); st7735_write_data(0x2D); st7735_write_data(0x2E); st7735_write_data(0x2E); st7735_write_data(0x37); st7735_write_data(0x3F); st7735_write_data(0x00); st7735_write_data(0x00); st7735_write_data(0x02); st7735_write_data(0x10); st7735_write_cmd(0x13); // 正常显示 st7735_write_cmd(0x29); // 开启显示 delay_ms(20); }逻辑说明这段初始化里最关键的是0x36和0x3A两条命令。0x36是 MADCTL控制行/列扫描方向、RGB/BGR 颜色排列0xA0是常见红色 1.8 寸模组能左上角对齐的一个值你的屏幕如果镜像或旋转后面 5.1 会讲怎么试其他值。0x3A写0x05表示每个像素用 16bit这点必须和图片转换脚本保持一致否则显示颜色会错乱。0xB1~0xC5 是电压和帧率控制不同厂家初始化序列差异不大但直接用公板参数最省事。参数说明0x11退出睡眠后要等 120ms不能省。如果初始化后屏幕直接白屏优先查 RESET 脉冲宽度和0x11后的delay_ms(120)。注意不要发反色命令0x20那个会让整屏红蓝互换很多网上拷贝的序列里残留这一行。2.5 单色填充函数屏幕的“hello world”void st7735_set_window(unsigned char x0, unsigned char y0, unsigned char x1, unsigned char y1) { st7735_write_cmd(0x2A); // CASET列地址 st7735_write_data(0x00); st7735_write_data(x0); st7735_write_data(0x00); st7735_write_data(x1); st7735_write_cmd(0x2B); // PASET行地址 st7735_write_data(0x00); st7735_write_data(y0); st7735_write_data(0x00); st7735_write_data(y1); st7735_write_cmd(0x2C); // RAMWR } void st7735_fill_screen(unsigned int color) { unsigned int i; st7735_set_window(0, 0, 127, 159); for (i 0; i 128 * 160; i) { st7735_write_data(color 8); st7735_write_data(color 0xFF); } }调用st7735_fill_screen(0xF800)后整屏应该是红色0x07E0是绿色0x001F是蓝色。如果红绿蓝三原色都能正确显示说明 0x36 和 0x3A 基本没问题。注意x0, x1, y0, y1是含端点的窗口宽是x1-x01高是y1-y01所以全屏传 127 和 159而不是 128 和 160。写满 128*160 个像素每个像素两字节总共 40960 字节 SPI 数据如果多写字节GRAM 会 wrap 回窗口起点继续覆盖看到的就是尾部内容盖到屏幕顶部。3. 用 Python 把图片转成 STC 的 code 数组BMP/JPEG 到 RGB5653.1 为什么不是“读文件”而是“编译进固件”STC 单片机没有文件系统TF 卡转接又贵又费引脚所以 128x160 图片显示最常见的做法是把图片直接转成 C 数组作为 code 段的一部分烧进 Flash。STC8A8K64S4 有 64KB Flash40KB 图片加几 KB 驱动代码容量正好STC15F2K60S2 的 60KB 也能放下。反过来STC15W408AS 这类 4KB Flash 的芯片就完全不适合做全屏图片。很多新人会把数组写成unsigned char img[128*160*2]放在函数里编译立刻报 address space overflow原因是普通无修饰数组会放在内部 SRAM而 STC8 的 xdata 只有 8KB。正确做法是放在 code 空间C51 会使用 MOVC 指令读取速度并没有许多人想象的那么慢。3.2 一个可复用的图片转换脚本from PIL import Image import sys def rgb565(r, g, b): return ((r 3) 11) | ((g 2) 5) | (b 3) src Image.open(sys.argv[1]).convert(RGB) src src.resize((128, 160), Image.LANCZOS) pixels src.load() with open(image_data.c, w, encodingutf-8) as f: f.write(#include image_data.h\n\n) f.write(unsigned char code img_128x160[128*160*2] {\n) for y in range(160): for x in range(128): r, g, b pixels[x, y] val rgb565(r, g, b) f.write(0x%02X,0x%02X, % (val 8, val 0xFF)) f.write(\n) f.write(};\n) print(输出 image_data.c 完成)逻辑说明rgb565把 8bit 红色取高 5 位成为 R5绿色取高 6 位成为 G6蓝色取高 5 位成为 B5合成一个 16bit 像素。输出时先写高字节再写低字节对应 ST7735 的 RAMWR 数据顺序。脚本按屏幕坐标(0,0)在左上角、先 x 后 y 的顺序遍历和st7735_set_window(0,0,127,159)默认方向一致。参数说明源图可以是 BMP、JPEG、PNGPillow 会自动解码再缩放。resize里写的(128,160)是宽高不要写反LANCZOS适合缩小图片如果原图小于 128x160 可以换Image.BILINEAR。脚本生成的image_data.c是一个 40KB 左右的数组直接用 Keil 打开会有点卡但编译没问题。3.3 Keil 工程里的 code 声明和边界条件// image_data.h #ifndef IMAGE_DATA_H #define IMAGE_DATA_H extern unsigned char code img_128x160[]; #endifextern声明里的code不能丢否则 C51 会把它当成 xdata 指针函数访问时生成完全不同的寻址代码轻则变慢重则取错数据。数组名可以用img_128x160在显示函数里直接当作const unsigned char code *传入。另一个边界条件是单个 code 数组最大 64KB。这张图 40960 字节没有超过但如果你塞进第二张同尺寸图总容量就超了。多图项目我一般分两个方案一是把图片压缩成 RLE 或 LZ4显示时在 RAM 里解压到 40KB 临时区再刷屏缺点是 RAM 不够二是外接 SPI NOR Flash 存图片包STC 只做搬运这是做相册类产品更稳的路。单张开机图场景直接用 code 数组最简单。3.4 旋转方向和数组遍历顺序怎么配合实际显示方向常见 MADCTL 值数组生成顺序竖屏头朝左上角0xA0左上角开始先 x 后 y逆时针旋转 90 度0x60左上角开始先 y 后 x左右镜像0xC0 或 0x00x 方向反向遍历上下翻转0xA0 配合 y 反向y 方向反向遍历不想动 MADCTL 时可以改脚本的遍历顺序效果等价。比如想在 ST7735 上横着显示一张原本竖版的图脚本里把外层循环改为for x in range(128)内层for y in range(159, -1, -1)再按同样的 RGB565 公式输出即可。调试时我习惯先用 3.2 的脚本生成一版如果方向不对就改脚本重新生成而不去改 C 代码里的窗口坐标。这样出问题至少能确定数据本身是按某个固定方向排的。4. 全屏刷新、局部刷新与 STC 的小内存取舍4.1 为什么 STC 不能像大内存单片机那样做双缓存STM32F407 驱动 TFT 时可以在外部 SDRAM 开两个全屏 framebuffer先画后台再切换。STC8A8K64S4 的 xdata 只有 8KB连一个 128x160 的 40KB 缓冲都放不下更别说双缓冲。这套方案里图片数据在 Flash输出目标是 ST7735 内部的 GRAM本质是“ROM 单帧 屏显存”结构。优点是省 RAM缺点是整屏刷新时如果逐行改写 GRAMST7735 同时还在从 GRAM 读数据显示画面可能出现撕裂感。缓解撕裂的常用做法不是去追求双缓冲而是把大部分动态内容设计成局部刷新进度条、数字、曲线这类只在屏幕上占一小块用窗口命令只刷变化区域。全屏图片留给开机 LOGO、菜单页面这种低频切换场景视觉上就完全没有问题。如果非要连续整屏翻页可以用 STC8 的硬件 SPI 缩短每帧时间但不要指望达到流畅视频的效果。4.2 窗口法与全屏图片显示函数void st7735_show_full_image(const unsigned char code *img) { unsigned int idx 0; st7735_set_window(0, 0, 127, 159); for (idx 0; idx 128 * 160 * 2; idx) st7735_write_data(img[idx]); }逻辑说明img形参显式加了codeC51 会直接生成 MOVC 取数的紧凑代码。如果这里不加code编译器会生成通用指针img[idx]每次访问都要先判断指针指向哪个存储空间代码膨胀不说速度也慢一截。循环次数固定为 40960因为数组长度在 C51 里对函数参数不可见不能用sizeof(img)。参数说明全屏函数依赖st7735_set_window(0,0,127,159)。如果屏幕坐标被 MADCTL 旋转过这里的x/y映射也会跟着变。我在实际项目里固定使用竖屏方向让窗口坐标和图片数组保持一致旋转交给脚本处理这样出问题好排查。4.3 局部刷新从图片里裁一块画到任意位置void st7735_show_region(const unsigned char code *img, unsigned char xs, unsigned char ys, unsigned char x, unsigned char y, unsigned char w, unsigned char h) { unsigned char i, j; unsigned int idx; st7735_set_window(x, y, x w - 1, y h - 1); for (j 0; j h; j) { idx ((ys j) * 128 xs) * 2; // 源图行起点 for (i 0; i w * 2; i) { st7735_write_data(img[idx i]); } } }逻辑说明xs, ys是源图里要复制的小块起点x, y是目标屏坐标w, h是块宽高。内层循环直接按w*2字节连续发因为源图是整幅 128x160 的连续数组每一行固定 256 字节。写完一行后idx要加上128*2 256跳到源图的下一行。如果直接从头连续写w*h*2字节截出来的图会斜掉因为中间跳过了其他列的数据。参数说明xs, xsw-1不能超出 0~127ys, ysh-1不能超出 0~159。函数里没有做边界检查这是刻意为之显示函数必须足够短越界等异常在上层调用时解决。做滚动文字时每次把xs加 1重复调用这个函数就能实现图片源里的横向滚动条。4.4 全屏刷新的速度估算和优化方向RGB565 全屏图的数据量是 128×160×2 40960 字节。GPIO 模拟 SPI 下SCLK 受 IO 翻转速度和延时函数影响STC8 在 24MHz 系统时钟时能做到 1~2Mbps 已经很不错。按 1.5Mbps 算40960 字节接近 0.22 秒加上 DC 切换和循环开销肉眼看到的是“刷上去”的效果。如果觉得慢优先优化三处改成 STC8 硬件 SPI模式 0查询 SPIF 标志发送。硬件 SPI 不需要软件翻转 SCLK同样主频下能快一倍以上。连续发数据时不要反复执行st7735_write_cmd(0x2C)。RAMWR命令只需要发一次之后全部发像素数据直到窗口填满。把st7735_write_data写成宏或内联函数避免每个字节都经历函数调用和参数压栈。Keil C51 对小函数会自动 inline 一部分但显式写成宏更可控。// 硬件 SPI 发送一个字节的参考写法 void spi_send_fast(unsigned char dat) { SPDAT dat; while (!(SPSTAT 0x80)); SPSTAT 0x80; }说明SPSTAT的0x80是 SPIF 标志写 1 清标志。不同 STC8 型号的 SPI 寄存器地址一致但分频位定义略有差异工程里以对应头文件为准。开启硬件 SPI 后初始化代码里的spi_write_byte可以直接替换成spi_send_fastDC 逻辑不需要动。5. 用一张测试图把 ST7735 的方向、偏移和字节序一次看清5.1 生成四角有标记的网格测试图正式 LOGO 花屏时很难判断是数组错、方向错还是偏移错。先做一张“方向测试图”再调试每一步都有明确反馈。from PIL import Image img Image.new(RGB, (128, 160)) p img.load() for y in range(160): for x in range(128): if x 8 and y 8: p[x, y] (255, 255, 255) elif x 120 and y 8: p[x, y] (255, 0, 0) elif x 8 and y 152: p[x, y] (0, 255, 0) elif x 120 and y 152: p[x, y] (0, 0, 255) else: p[x, y] ((x * 2) % 256, (y * 2) % 256, 0) img.save(test_grid.bmp)说明四个角分别显示白、红、绿、蓝背景从左到右偏红、从上到下偏绿。用 3.2 的脚本把这 BMP 转成数组烧进去如果屏幕四角颜色和预期一致说明方向和边界都对如果左上角出现了蓝说明 0x36 的 RGB/BGR 位反了如果上下颠倒按 0xA0、0x60、0xC0、0x00 依次试 MADCTL试到四角对齐为止。测试图比直接看 LOGO 有效得多。5.2 白屏、花屏现象反推表现象优先检查全屏白无任何内容RESET 脉冲、0x11后 120ms 延时满屏随机色点SPI 模式、SCLK 极性和 SDA 相位颜色块正确但整体反色初始化序列里是否误发 0x20红蓝互换数组高低字节顺序反了改生成脚本图像整体偏移 2~3 像素修改st7735_set_window起始坐标四角有颜色但中央偏移MADCTL 里 RGB/BGR 位与扫描方向不匹配5.3 用 IO 翻转量直接测刷屏耗时想知道一帧全屏刷新到底花多少毫秒不一定要接仿真器。在显示函数外面包一个引脚翻转用示波器或逻辑分析仪看脉宽sbit FRAME_TICK P3^2; FRAME_TICK 0; st7735_show_full_image(img_128x160); FRAME_TICK 1;P3.2 从低到高的时间就是整帧刷新的实际耗时。这样能同时看出刷新期间是否被中断打乱也能对比软件 SPI 和硬件 SPI 的优化效果。改完代码后再测一次如果时间明显下降说明优化方向正确如果时间没变问题往往不在 SPI 发送而在其他环节。这个办法比在 Keil 的仿真窗口里看周期数准确得多。本文还有配套的精品资源点击获取