
简介一套完整的基于STC单片机与ST7735驱动芯片的1.8寸TFT彩屏中文显示工程适用于嵌入式入门及小型彩屏界面开发场景帮助开发者绕过繁琐的底层驱动配置直接掌握SPI通信、LCD初始化、帧缓冲与汉字点阵显示等核心环节。压缩包共72个文件包含12个C源码、21个头文件、Keil工程配置与编译输出文件uvproj、hex、lst、map等另附TFT彩屏显示中文效果的演示视频整体约1.1MB结构清晰便于按模块查阅。目前已有1852人学习下载。资源内按功能模块组织提供STC15系列基础库、GUI图形接口以及多尺寸汉字字库8x16、16x16、16x32、32x32并配有.h头文件声明整体代码风格简洁可快速适配到实际项目中。通过阅读代码和观看演示读者能够理清单片机与ST7735之间的数据交互流程解决中文取模、颜色填充与文本排版等常见问题适合作为毕业设计、课程实验或产品原型的基础参考。1. 一块 128×160 的屏难点不在画点而在字形映射STC 单片机上点亮 ST7735 几乎是两天就能做完的事SPI 时序对着数据手册调几次屏上就能画出彩条和色块。真正让项目停下来的是「中文显示」这四个字——ST7735 并不知道“中文”是什么它只认坐标和像素颜色而单片机里也没有现成的字形。于是问题变成三条线字符集用 GB2312 还是 UTF-8、16×16 点阵字模从哪里来、点阵数据如何按区位码映射到屏幕坐标。网上大量“tftlcd显示中文”“显示不出中文”的求助帖基本都卡在字模的获取和地址换算上而不是 SPI 驱动本身。下文以 STC8/STC15 为参照按驱动点亮、字模提取、字符串解析、全字库扩展的顺序讲一遍实际工程里最稳的做法。2. ST7735 驱动点亮SPI 时序、初始化序列与底层画图函数2.1 引脚、电平与 SPI 模式先排除两类“点不亮”STC 单片机常见 5V 和 3.3V 两种供电而市面上 1.8 寸 ST7735 模组几乎都是 3.3V 逻辑。直接拿 5V GPIO 去推模组的 SDA、SCL轻则电平阈值不匹配导致读不到数据重则烧坏驱动芯片。我一般在这几根线上做 1KΩ2KΩ 分压或者直接加一块 3.3V 电平转换模块背光 LED 引脚可以经一个三极管由 5V 控制避免从单片机引脚取太大电流。模组引脚单片机 GPIO说明VCC3.3V注意模组上是否已带稳压GNDGND共地必须接CSP1.2片选低有效RSTP1.3复位低有效DC/RSP1.4命令/数据选择SDA/MOSIP1.5SPI 数据线SCL/SCKP1.6SPI 时钟线ST7735 支持的标准 SPI Mode 0CPOL0、CPHA0即空闲时钟为低电平数据在上升沿被采样8 位数据 MSB 先发。STC8 系列自带硬件 SPI 模块但调试初期建议先用软件模拟 SPI方便用逻辑分析仪抓时序。等字符显示稳定后再切到硬件 SPI速度能提升一个量级。2.2 软件 SPI 收发与命令/数据切换sbit LCD_CS P1^2; sbit LCD_DC P1^3; sbit LCD_SDA P1^4; sbit LCD_SCL P1^5; sbit LCD_RST P1^6; void SPI_WriteByte(uint8_t dat) { uint8_t i; for (i 0; i 8; i) { LCD_SCL 0; if (dat 0x80) LCD_SDA 1; else LCD_SDA 0; LCD_SCL 1; dat 1; } } void LCD_WriteCmd(uint8_t cmd) { LCD_DC 0; // DC 拉低表示传输的是命令 LCD_CS 0; SPI_WriteByte(cmd); LCD_CS 1; } void LCD_WriteData(uint8_t dat) { LCD_DC 1; // DC 拉高表示传输的是数据 LCD_CS 0; SPI_WriteByte(dat); LCD_CS 1; }逻辑说明ST7735 通过 DC 引脚区分当前 SPI 字节是命令还是数据CS 片选在每次传输前拉低、结束后拉高。SPI_WriteByte从最高位开始逐位输出数据在 SCL 上升沿之前的低电平阶段被放到 SDA 上正好满足 Mode 0。参数说明dat是要发送的 8 位数据如果需要读取屏幕状态需要额外实现SPI_ReadByte但显示类项目基本用不到读操作。注意每发送一个字节后要有一小段延时或至少保持 SCL 高电平一个机器周期否则在 12T 模式的 STC89 系列上容易丢位。2.3 ST7735 最小初始化序列void LCD_Init(void) { LCD_RST 0; delay_ms(50); LCD_RST 1; delay_ms(50); LCD_WriteCmd(0x01); // SWRESET 软件复位 delay_ms(120); LCD_WriteCmd(0x11); // SLPOUT 退出休眠模式 delay_ms(120); LCD_WriteCmd(0x36); // MADCTL 数据访问控制 LCD_WriteData(0x00); // RGB 顺序正常扫描方向 LCD_WriteCmd(0x3A); // COLMOD 像素格式 LCD_WriteData(0x05); // 0x05 RGB56516 位色 LCD_WriteCmd(0x29); // DISPON 打开显示 delay_ms(20); }这段初始化省掉了部分模组需要的帧率、伽马等寄存器配置在不少 ST7735 模组上能直接点亮。如果显示偏色、对比度异常再按商家提供的完整寄存器序列补齐一般集中在 FRMCTR1、PWCTR1、GMCTRP1 这些地址。调试时优先确认的是0x3A寄存器0x05表示 16 位 RGB5650x03表示 12 位 RGB444。如果这里设错画出来的颜色会明显偏色甚至花屏。2.4 开窗函数与画点影响后续字模速度的基础操作ST7735 的显存是按窗口写入的先设置列地址范围再设置行地址范围然后连续写入像素数据。画单个像素就是连续三次命令加两字节 RGB565 数据。void LCD_SetWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { LCD_WriteCmd(0x2A); // CASET 列地址 LCD_WriteData(x0 8); LCD_WriteData(x0 0xFF); LCD_WriteData(x1 8); LCD_WriteData(x1 0xFF); LCD_WriteCmd(0x2B); // RASET 行地址 LCD_WriteData(y0 8); LCD_WriteData(y0 0xFF); LCD_WriteData(y1 8); LCD_WriteData(y1 0xFF); LCD_WriteCmd(0x2C); // RAMWR 开始写显存 } void LCD_DrawPixel(uint16_t x, uint16_t y, uint16_t color) { if (x 128 || y 160) return; LCD_SetWindow(x, y, x, y); LCD_WriteData(color 8); // RGB565 先发高字节 LCD_WriteData(color 0xFF); }逻辑说明LCD_SetWindow将后续的 RAM 写入限定在矩形区域内写入指针会按从左到右、从上到下的顺序自动递增直到到达(x1, y1)才会停止。这里color 8和color 0xFF表示 RGB565 的大端发送顺序具体字节序取决于模组兼容的是哪种控制器多数 ST7735 用这个顺序没有错误。参数说明分辨率 128×160坐标原点在左上角MADCTL寄存器可以翻转方向和交换 RGB/BGR 顺序如果画面上的红色和蓝色互换了把LCD_WriteData(0x00)改成0x08或0xC8试一下。3. 中文字模原理GB2312 区位码、16×16 点阵与取模参数3.1 为什么 ST7735 显示中文容易“乱码”ASCII 字符一个字节就能表示显示时直接查一张 8×16 点阵表即可。而汉字在 GB2312/GBK 编码里占两个字节例如“中”的机内码是0xD6 0xD0两个字节都大于 0x80。如果把这两个字节当作两个 ASCII 字符直接查表屏幕上就会出现两个无法识别的符号这就是“乱码”的根源。所以真正要解决的是把汉字机内码映射到字模数据所在地址。GB2312 把汉字按区、位排列一个汉字对应一个区位号。一级汉字 3755 个按拼音排序放在 1655 区二级汉字 3008 个按部首排序放在 5687 区。机内码与区位码的关系是机内码高字节 区号 0xA0机内码低字节 位号 0xA0。反过来已知机内码hi和lo区号是hi - 0xA0位号是lo - 0xA0。3.2 16×16 点阵字模的排列与 32 字节存储16×16 点阵就是把每个汉字画在一个 16 行、16 列的网格里每一行用 2 个字节表示高字节对应左 8 点低字节对应右 8 点字节内的每一位对应一个像素点。整个汉字占16 行 × 2 字节 32 字节这是嵌入式中文显示最常用的一种规格。如果想省存储还有 12×1224 字节和 8×1616 字节点阵但 16×16 是阅读最舒服、取模工具支持最好的选择。全量 GB2312 汉字字模文件如 HZK16大小约为6763 × 32 ≈ 216KB再加上符号区内容接近 230KB。STC8 系列内部 Flash 最大不过 64KB 左右所以“全字库”在 STC 单片机上几乎不可能直接塞进内部 Flash必须用外挂存储或内嵌少量字模。这决定了整套方案的走向。方案存储占用适用场景主要限制内嵌常用字模数组每字 32B菜单、仪表界面汉字数量固定换字需要重新编译外挂 SPI Flash 全字库约 220KB需要显示任意文本需要额外芯片和烧录流程SD 卡 文件系统字库文件独立频繁更换字库内容文件系统占用资源多3.3 取模软件参数与字模字节顺序常用工具是 PCtoLCD2002取模参数直接决定显示函数能不能正确还原字形。我的固定配置是阴码、逐行式、顺向、16×16。“阴码”表示二进制位为 1 时点亮像素“逐行式”表示按从上到下的顺序输出每一行“顺向”表示每行从左到右输出高字节在前。取模结果类似const uint8_t code zhong_16[] { /* “中” 16×16 点阵逐行式阴码 */ // 数据由取模工具生成这里省略实际 32 字节 };取模工具生成的数据顺序必须与显示函数一致。如果你生成的数组在屏幕上左右颠倒多半是字节内位序选成了“逆向”如果上下颠倒则是取模方向选成了“列行式”或“逐列式”。解决方法是固定一套参数然后显示函数永远按这一套参数解析排查时只改取模工具配置不动代码。3.4 区位码换算成字模文件偏移当字库文件按区位码连续存放时一个汉字在文件中的偏移地址是uint32_t get_gb2312_offset(uint8_t hi, uint8_t lo) { uint16_t qu hi - 0xA1; // 区号从 0 开始 uint16_t wei lo - 0xA1; // 位号从 0 开始 return ((uint32_t)(qu * 94 wei)) * 32; }逻辑说明GB2312 每个区最多 94 个字符所以先算出汉字在字库文件中的第几个字符位置再乘以 32 字节得到文件偏移。注意这里hi - 0xA1和lo - 0xA1是基于区位从0xA1A1开始的算法和前面机内码0xA0的公式只差 1原因在于 GB2312 没有使用0xA0A0前面的编码空间字库文件里第一个字符是从0xA1A1开始算的。参数说明hi、lo是字符串中连续两个字节的汉字编码如果工程里同时存在 UTF-8 编码源文件必须先把 UTF-8 转成 GB2312 再交给这个函数否则偏移会落到错误位置。4. 内嵌字模的中文显示字符串解析、字库表与显示函数封装4.1 字库表设计机内码 32 字节点阵STM32 或 Linux 上可以直接打开字库文件按偏移读取但 STC 内部 Flash 装不下全字库所以最直接的办法是把项目里实际用到的汉字提取出来编译进代码。每个汉字由两部分组成机内码和 32 字节点阵数据。用结构体组织成一张表#define FONT_MAX 64 struct ch_gb { uint8_t hi; // 机内码高字节 uint8_t lo; // 机内码低字节 const uint8_t code *matrix; // 指向 32 字节点阵 }; const uint8_t code zhong_16[] { /* “中” 16×16 点阵数据 */ }; const struct ch_gb code font_table[] { { 0xD6, 0xD0, zhong_16 }, // “中” // 项目里用到的其它汉字按同样方式加入 }; const uint8_t *find_font(uint8_t hi, uint8_t lo) { uint8_t i; for (i 0; i FONT_MAX; i) { if (font_table[i].hi hi font_table[i].lo lo) return font_table[i].matrix; } return (const uint8_t code *)0; }逻辑说明find_font是“线性查表”函数遍历整张字库表找到匹配的机内码返回点阵地址。菜单项目里几十个汉字线性查找几十次对 STC8 的 1T 模式来说完全不是负担不必引入哈希或二分。参数说明FONT_MAX要大于实际汉字数量code是 Keil C51 的关键字把数据放到程序 Flash 而不是 RAM。如果换用 SDCC 或标准 C 编译器把code去掉改成const即可。4.2 从汉字机内码到屏幕像素逐行逐点映射拿到了 32 字节点阵显示函数要按取模参数还原字形。下面这段是配合“阴码、逐行式、顺向”配置的显示函数void LCD_ShowChinese16(uint16_t x, uint16_t y, uint8_t hi, uint8_t lo, uint16_t fg, uint16_t bg, uint8_t transparent) { const uint8_t *font find_font(hi, lo); uint8_t i, j, hb, lb; if (font (const uint8_t code *)0) return; for (i 0; i 16; i) { hb font[i * 2]; // 当前行左 8 个点 lb font[i * 2 1]; // 当前行右 8 个点 for (j 0; j 16; j) { uint8_t mask (j 8) ? (0x80 j) : (0x80 (j - 8)); uint8_t bit (j 8) ? (hb mask) : (lb mask); if (transparent !bit) continue; // 透明模式跳过背景 if (bit) LCD_DrawPixel(x j, y i, fg); else if (!transparent) LCD_DrawPixel(x j, y i, bg); } } }逻辑说明外层循环遍历 16 行每行取两个字节hb对应左半边lb对应右半边。内层循环遍历 16 列当列号j 8时从hb取位否则从lb取位位值为 1 画前景色为 0 画背景色。取位顺序是0x80 j正好对应“顺向、高位在前”的取模配置。参数说明fg和bg是 RGB565 颜色值transparent为 0 时每个像素不是前景就是背景效果是汉字带底色为 1 时只画前景适合在图片或色块上叠加文字。4.3 混合字符串解析把“中英混排”当成扫描问题处理实际界面几乎不会只有汉字更常见的是“温度 25℃”这种中英混排。处理规则很简单读取一个字节如果最高位为 0按 ASCII 处理如果最高位为 1说明是汉字机内码的第一个字节再读下一个字节组成完整的 16 位机内码。void LCD_ShowGBString(uint16_t x, uint16_t y, const char *str, uint16_t fg, uint16_t bg, uint8_t transparent) { while (*str) { if (*str 0x80) { // 汉字双字节 uint8_t hi *str; uint8_t lo *str; if (x (128 - 16)) { x 0; y 16; } LCD_ShowChinese16(x, y, hi, lo, fg, bg, transparent); x 16; } else { // ASCII 单字节 if (x (128 - 8)) { x 0; y 16; } LCD_ShowChar8x16(x, y, *str, fg, bg, transparent); x 8; } if (y (160 - 16)) break; } }逻辑说明这里有三个关键判断*str 0x80判断中英文汉字宽度 16、ASCII 宽度 8所以换行阈值分别是128 - 16和128 - 8超出屏幕高度后直接 break 防止野指针。参数说明str必须是 GB2312/GBK 编码的字符串。Keil C51 默认按本地代码页处理中文字符串字面量所以直接写LCD_ShowGBString(0, 0, 中文字符串, RED, WHITE, 0);在 Windows 上编译没问题。如果你在 Linux 或 VSCode 里编辑源文件文件保存成 UTF-8 编码那字符串在内存里是 UTF-8 字节流显示前先转成 GB2312否则就会出现“vscode输出中文显示乱码”类似的现象。4.4 内嵌方案的边界与改造方向内嵌方案最大的问题是字库表会随项目膨胀。一个 20 字的界面占 640 字节 Flash看起来不多但当页面增加到十几屏、需要“中英文名称”那种长列表时手写结构体数组就变得难以维护而且不同页面重复汉字会白白浪费空间。遇到这种情况常见的改造方向是把字模数据按区位顺序连续存放把“线性查表”改成“按编码区间计算”这一步做完其实已经是在向全字库方案靠拢了。5. 全字库显示方案SPI Flash 字库、偏移计算与缓存设计5.1 为什么 STC 上可以考虑外挂 SPI Flash很多做产品的人问“STC 能不能显示任意汉字”答案是可以前提是字库必须放在外部。常见做法是用 W25Q162MB或 W25Q324MB存 HZK16 字库字库文件占 220KB 左右剩余空间放图片或其他配置数据。STC8 控制器的 SPI 硬件接口可以直接连接 SPI Flash读速度比模拟 SPI 高很多2MB 容量对于字库来说绰绰有余。5.2 从 SPI Flash 读取 32 字节字模外挂思路下不再需要内嵌字库表而是在收到汉字机内码后先算出在字库文件中的偏移然后向 W25Q16 发送读取命令#define HZK16_BASE 0x000000 // 字库在 Flash 中的偏移 uint8_t spi_flash_read_byte(uint32_t addr) { uint8_t dat; FLASH_CS 0; SPI_WriteByte(0x03); // W25Q 系列读数据命令 SPI_WriteByte((addr 16) 0xFF); SPI_WriteByte((addr 8) 0xFF); SPI_WriteByte(addr 0xFF); dat SPI_ReadByte(); // 同时产生时钟 FLASH_CS 1; return dat; }逻辑说明W25Q 系列的读命令是0x03其后跟 3 字节地址高字节在前。发送完地址后主控通过继续产生时钟读取数据每读一个字节Flash 内部地址自动加 1。参数说明HZK16_BASE表示字库烧录到 Flash 的起始地址如果字库和其他数据共用 Flash必须保证这个地址是 4KB 对齐的因为 W25Q 的扇区擦除以 4KB 为单位。读取字模时连续调用 32 次spi_flash_read_byte即可注意调用之间不要拉高 CS否则地址会复位。读取 32 字节并显示汉字的完整流程void LCD_ShowChineseFromFlash(uint16_t x, uint16_t y, uint8_t hi, uint8_t lo, uint16_t fg, uint16_t bg, uint8_t transparent) { uint32_t offset get_gb2312_offset(hi, lo) HZK16_BASE; uint8_t i, j, hb, lb; for (i 0; i 16; i) { hb spi_flash_read_byte(offset i * 2); lb spi_flash_read_byte(offset i * 2 1); for (j 0; j 16; j) { uint8_t mask (j 8) ? (0x80 j) : (0x80 (j - 8)); uint8_t bit (j 8) ? (hb mask) : (lb mask); if (transparent !bit) continue; LCD_DrawPixel(x j, y i, bit ? fg : bg); } } }这种方案的代价是每次显示一个汉字都要发送几十条 SPI 命令如果整屏刷新SPI 总线上会有大量读操作。在 STC8 上打开硬件 SPI 后一个汉字从读取到显示大约需要 13ms基本能满足菜单切换的需求。5.3 字库烧录把 HZK16 写进 W25Q16我一般用串口配合一个简单的 Bootloader 完成字库烧录流程是用取字软件导出 HZK16 二进制文件注意文件内从0xA1A1开始连续排布单片机侧运行一个“接收串口数据写入 Flash”的烧录程序串口每次接收 256 字节先擦除目标扇区再写入分配好的 Flash 页全部写完回读前 32 字节做校验和源文件逐位比较。如果不想自己写烧录器也可以用 USB 转 SPI 工具直接在电脑上把 HZK16 写到 W25Q16然后把 Flash 芯片焊到板子上。两种方式本质都是把字库文件按偏移 0 放入 Flash运行时再通过HZK16_BASE调整起始位置。5.4 给全字库方案加一个“常用字缓存”从 Flash 读字模每次至少 32 字节看上去不慢但菜单切换时翻页会连续读几百个汉字SPI Flash 的连续读速度和 STC 的 SPI 外设会成为瓶颈。常见的优化是加一个简单缓存在 RAM 里开辟一个 8 条目的缓存表每条记录“机内码 32 字节点阵”。显示汉字先查缓存命中就直接用未命中才去 Flash 读同时把最旧的一条替换掉。#define CACHE_NUM 8 struct cache_item { uint8_t hi; uint8_t lo; uint8_t matrix[32]; }; struct cache_item code cache_table[CACHE_NUM]; // Keil 下需要定位到 XRAM逻辑说明这个缓存表不需要 LRU 那么复杂的算法因为界面文字通常按顺序刷新直接用“最简单的 FIFO 替换”就能获得不错的命中率。RAM 占用是8 × 34 272字节STC8 的 XRAM 一般有 1KB 以上可以接受。参数说明CACHE_NUM越大命中率越高但 STC 的 RAM 有限8 条已经是常见折中。如果你的界面是按页面切换缓存 8 个常用字足够覆盖一屏上的大部分汉字。6. 验证与优化自检点、画字速度与常见显示问题6.1 用一组自检点快速定位“显示不出中文”的环节遇到中文显示异常不要先调字库按下面的顺序逐级确认现象检查点处理方式全屏白屏或黑屏背光、RST、CS 时序确认背光电压和复位延时花屏、彩条混乱SPI 极性、时钟频率改 Mode 0降低分频系数红蓝色反了MADCTL 寄存器的 BGR 位0x00改0x08能显示图形但不显示汉字字符串编码和字库编码不一致确认源文件是 GB2312 保存汉字左右颠倒取模方向PCtoLCD2002 改“顺向”汉字上下颠倒行扫描顺序取模方式从“逐行式”改“逐列式”再改回来最容易忽略的是第二行STC8 硬件 SPI 频率过高时部分 ST7735 模组会花屏把 SPI 时钟分频调低一档往往就好了。软件模拟 SPI 时则要注意每一拍之间加_nop_()避免信号太短。6.2 一次 RAMWR 写入整行替代逐点画像素画一个 16×16 汉字逐点调用LCD_DrawPixel意味着触发 256 次LCD_SetWindow每次都要发送 12 个字节效率很低。正确的做法是先把汉字按点阵展开成 RGB565 像素缓冲区然后一次性设置窗口连续发送全部 512 字节void LCD_ShowChineseBlock(uint16_t x, uint16_t y, const uint8_t *matrix, uint16_t fg, uint16_t bg) { uint8_t i, j, hb, lb; uint8_t pixel; LCD_SetWindow(x, y, x 15, y 15); // 固定 16×16 窗口 for (i 0; i 16; i) { hb matrix[i * 2]; lb matrix[i * 2 1]; for (j 0; j 16; j) { pixel (j 8) ? (hb (0x80 j)) : (lb (0x80 (j - 8))); LCD_WriteData((pixel ? fg : bg) 8); LCD_WriteData((pixel ? fg : bg) 0xFF); } } }逻辑说明这个函数把“判断像素是否透明”和“写像素”拆开整个汉字只调用一次LCD_SetWindow后续 256 个像素全部通过连续 RAMWR 发送。ST7735 的显存写入指针会自动递增不需要每写一个像素都重新设置坐标。实测在 STC8 上用 12MHz SPI 时钟这种方式比逐点画快 8 倍以上。参数说明matrix指向 32 字节字模fg、bg必须是完整 RGB565 值函数内部不再做任何坐标换算。6.3 透明文字与反白显示的实用写法中文菜单里高亮选中项一般用反白前景色和背景色互换。利用上面的块写入函数只需要交换fg和bg参数不需要改任何逻辑。透明文字则要在块写入前先把目标区域背景读回来——但 ST7735 一般不提供读显存功能所以更常见的做法是在应用程序里维护一个 128×160 的 RGB565 Shadow Buffer所有绘制操作先画到缓冲区再一次性刷到屏幕。这个方案对 RAM 要求高128×160×2 40KBSTC8 系列部分型号有 32KB 以上 XRAM 才能支持没有这么大 RAM 时就退回原来的“画点不画背景”透明模式。最后一招是控制显示更新区域只重绘文字附近的 16×16 小窗口而不是全屏刷新十六个汉字同时变化时画面依然能保持流畅。本文还有配套的精品资源点击获取