STM32驱动ST7735显示中文:SPI接线、字模与排查全解析

发布时间:2026/10/17 11:06:24
STM32驱动ST7735显示中文:SPI接线、字模与排查全解析
简介这套面向 STM32F103C8T6 与 1.8 寸 ST7735 彩屏的中文显示程序包是嵌入式入门者学习彩屏驱动和汉字显示的较好样例也适合电子竞赛、课程设计和快速原型验证。作者在商家驱动基础上重写了大部分代码内置字库后可直接显示中英文和常用符号并给出注释明确的移植提示更换引脚或板卡时能够较快完成适配。压缩包共有 101 个文件整体大小约 412KB其中 49 个 C 源文件与 41 个 H 头文件构成完整驱动逻辑另有 Keil 工程、HEX 固件和清理脚本打开工程即可编译烧录。目前已有 4886 人学习下载。基于标准外设库的源码不仅演示了屏幕初始化、画点、汉字定位等步骤也覆盖定时器、串口、ADC 等外设的调用方式能够帮助新手减少踩坑是小屏中文界面开发中值得保留的参考资料。1. 在STM32F103C8T6上点亮ST7735中文屏先搞清楚这三件事用STM32F103C8T6驱动一块1.8寸ST7735彩屏显示中文是很多桌面仪表、温湿度计、迷你菜单项目绕不开的关卡。这块屏便宜SPI两根线就能点亮官方例程里跑英文毫无压力可一旦换成中文白屏、花屏、乱码三件事经常在同一次上电里一起出现。这套资源解决的就是“在1.8寸ST7735上稳定显示中文”这组问题包含完整工程、ST7735驱动初始化代码、16x16中文点阵字模生成脚本以及现成字模数组。核心思路不依赖中文字库芯片而是自己把GB2312编码转成点阵再送给屏幕显存。适合两类人一是刚接触这块屏、想跳过“英文能亮、中文乱码”怪圈的新手二是要给设备界面加中文菜单但不想在64KB Flash里塞整套HZK16的开发老手。读完你会清楚三件事中文显示的本质是什么、取模方向怎么和驱动配合、哪些初始化参数不能照抄别人的。2. 点亮1.8寸ST7735SPI接线、初始化序列与驱动函数2.1 先看模块丝印再接线同样是1.8寸屏引脚顺序可能不同市面上1.8寸ST7735模块常见为8针VCC、GND、CS、RESET、A0也叫DC/RS、SDAMOSI、SCK、LED背光。红板黑板的引脚顺序不完全一样接线前先拍张照片对照丝印。屏模块引脚作用推荐接法F103C8T6备注SCK/SCLSPI时钟PA5SPI1_SCK空闲默认低电平SDA/MOSI主机数据输出PA7SPI1_MOSICS片选PA4软件控制即可不占硬件NSSA0/DC/RS数据/命令选择PB0别接到SPI1的MISO脚RESET/RST复位PB1低电平有效LED/BLK背光控制PA2或3.3V常亮可直接接VCCPWM可调亮度VCC供电3.3V或5V看模块部分模块板载稳压能适应5V接线只有一条铁律线越短越好。杜邦线超过10cm后SPI信号反射会导致花屏后面排查章节会详细讲。2.2 写命令/写数据函数CS不要夹在每个字节里ST7735一条指令分两步DC拉低发命令字节DC拉高发数据字节。CS由调用方统一管理不要在WriteCmd和WriteData内部频繁拉高拉低CS否则批量送像素时速度会掉一大截。#define LCD_CS_PIN GPIO_Pin_4 #define LCD_DC_PIN GPIO_Pin_0 #define LCD_RST_PIN GPIO_Pin_1 #define LCD_BLK_PIN GPIO_Pin_2 static void LCD_CS_Enable(void) { GPIO_WriteBit(GPIOA, LCD_CS_PIN, Bit_RESET); } static void LCD_CS_Disable(void) { GPIO_WriteBit(GPIOA, LCD_CS_PIN, Bit_SET); } static void LCD_DC_Cmd(void) { GPIO_WriteBit(GPIOB, LCD_DC_PIN, Bit_RESET); } static void LCD_DC_Data(void) { GPIO_WriteBit(GPIOB, LCD_DC_PIN, Bit_SET); } static uint8_t LCD_SPI_TxByte(uint8_t byte) { // 等待发送缓冲空否则新数据会覆盖还没移出去的旧字节 while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) RESET); SPI_I2S_SendData(SPI1, byte); // 等一个字节收完顺便读走RXNE避免长时间发送后OVR错误标志堆积 while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) RESET); return SPI_I2S_ReceiveData(SPI1); } static void LCD_WriteCmd(uint8_t cmd) { LCD_DC_Cmd(); LCD_SPI_TxByte(cmd); } static void LCD_WriteData(uint8_t dat) { LCD_DC_Data(); LCD_SPI_TxByte(dat); }逻辑说明DC引脚的电平决定当前字节是命令还是数据ST7735内部靠这个脚切换指令解码器状态。SPI外设的发送缓冲只有一级不查TXE标志直接连续写会发生覆盖这是很多“第一次能亮第二次就乱”的根源。参数说明SCK接PA5、MOSI接PA7对应硬件SPI1CS用PA4软件控制不启动硬件NSSDC和RST放PB0/PB1是为了避开PA6MISO复用脚免得全双工模式下把DC误配成输入。SPI基础配置要落在Mode 0即CPOLLow、CPHA1Edge。时钟分频先用SPI_BaudRatePrescaler_16在72MHz的APB2上得到4.5MHz。这个速度对于10cm内杜邦线比较稳。分频改8能到9MHz前提是飞线足够短。提示模块不带MISO脚时主机SPI1的MISO输入悬空每发一个字节RXNE仍会置位读走即可不影响显示。2.3 初始化序列最小可跑集不是越长越好网上很多ST7735初始化代码又长又玄学里面塞了大量Gamma、电源参数真正让屏幕工作起来的最小序列其实很短软件复位、退出睡眠、设置颜色格式、设置扫描方向、开显示。其余那些参数针对不同批次面板有差异不需要逐条照搬。void LCD_ST7735_Init(void) { // 硬件复位先拉低至少10ms再拉高 GPIO_WriteBit(GPIOB, LCD_RST_PIN, Bit_RESET); delay_ms(10); GPIO_WriteBit(GPIOB, LCD_RST_PIN, Bit_SET); delay_ms(120); LCD_CS_Enable(); LCD_WriteCmd(0x01); // SWRESET软件复位 delay_ms(150); LCD_WriteCmd(0x11); // SLPOUT退出睡眠模式 delay_ms(120); // 等内部DCDC稳定不能省 LCD_WriteCmd(0x36); // MADCTL扫描方向与RGB序 LCD_WriteData(0x00); // 默认横屏方向镜像和偏移见第4章 LCD_WriteCmd(0x3A); // COLMOD像素格式 LCD_WriteData(0x05); // 0x05RGB56516位色高字节先发 LCD_WriteCmd(0x29); // DISPON打开显示 delay_ms(20); LCD_CS_Disable(); LCD_Clear(0x07E0); // 全屏填绿色验证屏有没有点亮 }这段代码里最容易翻车的三个点0x11之后必须有120ms左右延时屏退出睡眠后内部电压要时间稳定等不够就发0x29会白屏0x36的数据与屏幕物理方向强相关不同批次屏幕的默认值可能不一样0x3A设成0x05后所有像素数据必须高字节在前。这个最小序列省掉了原厂手册里0xB1、0xB3、0xC0那些电源和Gamma寄存器因为它们不影响“能不能出字”只影响“颜色饱和度对不对”。把屏点亮后再回来调那些参数才不容易把自己绕晕。2.4 显示窗口忘记发0x2C数据进不了显存ST7735不是字符型屏幕送一个字符不会直接出字。所有内容先写进GRAMGRAM通过0x2A和0x2B两条命令划定写入窗口最后用0x2C让数据连续灌进去。窗口范围是闭区间x和y最大值一个是128、一个是160。void LCD_SetWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { LCD_WriteCmd(0x2A); // 列地址 LCD_WriteData(x0 8); LCD_WriteData(x0 0xFF); LCD_WriteData(x1 8); LCD_WriteData(x1 0xFF); LCD_WriteCmd(0x2B); // 行地址 LCD_WriteData(y0 8); LCD_WriteData(y0 0xFF); LCD_WriteData(y1 8); LCD_WriteData(y1 0xFF); LCD_WriteCmd(0x2C); // RAMWR开始写显存 } void LCD_Clear(uint16_t color) { LCD_SetWindow(0, 0, 127, 159); // 128x160分辨率 for (uint32_t i 0; i 128 * 160; i) { LCD_WriteData(color 8); LCD_WriteData(color 0xFF); } }如果SetWindow之后少了0x2C或者把窗口边界写成128、160而不是127、159屏幕会表现出“局部区域没反应”或“边缘多一条残影”。全屏填色的意义不只是验证还能帮你确认这块屏的GRAM坐标是否从(0,0)开始这一点直接影响后面中文坐标计算。3. 中文显示关键一步取模格式、字模生成与16x16点阵绘制3.1 中文显示原理屏幕只认点阵不认汉字编码ST7735内部没有任何汉字字库它只认GRAM里的颜色点。要让屏幕显示“博”这个字本质是把这个字的16x16点阵按“某一点亮/灭”翻译成像素颜色。点阵里的1画前景色0画背景色。汉字在计算机里有多种编码典型工程里用GB2312。一个汉字在GB2312中占两个字节分别对应区位码通过区位码可以在HZK16字库文件里定位到32字节点阵数据。标准HZK16的字模排列规则是从上到下16行每行左右两个字节共16x232字节每字节高位在前。区位码检索公式就一句话offset ((区号 - 1) * 94 (位号 - 1)) * 32这个公式和文件系统无关HZK16文件里从第0字节开始排列按区号、位号线性排布。搞清楚这个公式后面写提取脚本才不会被“偏移总是差一点”折磨。3.2 用Python脚本从HZK16提取字模直接生成C数组工程里不需要为每个汉字手抄点阵写一个几行的Python脚本从HZK16里按GB2312编码把汉字点阵提出来输出成C数组复制进工程即可。# extract_hzk16.py # 用法python extract_hzk16.py 博 import sys HZK16 HZK16 # 与脚本同目录 def get_bitmap_bytes(hanzi): if len(hanzi) ! 1: raise ValueError(只支持单个汉字) gb hanzi.encode(gb2312) # 转成GB2312机内码 qu gb[0] - 0xA0 # 区号 wei gb[1] - 0xA0 # 位号 offset ((qu - 1) * 94 (wei - 1)) * 32 with open(HZK16, rb) as f: f.seek(offset) return f.read(32) def format_c_array(data): print(const unsigned char hanzi_bitmap[32] {) for i, b in enumerate(data): if i % 16 0: print( , end) print(0x%02X, % b, end) if i % 16 15: print() print(};) if __name__ __main__: format_c_array(get_bitmap_bytes(sys.argv[1]))逻辑说明脚本先取汉字字符串的GB2312两个字节减去0xA0得到区和位再用公式算出在HZK16文件里的字节偏移。HZK16文件总共约256KB所有常用汉字按区位顺序排列偏移计算准确就能一次读对32字节。参数说明gb[0]是机内码高字节gb[1]是低字节例如“博”的GB2312编码是B2A9则区号为0x12位号为0x09。HZK16文件需要对位如果从网上下载的变体版本行数有差异可以先打印offset和文件大小判断。注意Python 3的encode(gb2312)是标准编码不需要额外装库。输出的是ASCII字符直接重定向到文本文件也不会乱码。3.3 16x16点阵绘制窗口连续写避免逐点开窗初学者最常见的写法是逐像素调用“设置坐标写颜色”一个汉字16x16共256个点每个点都开一次窗口刷一屏文字能慢到肉眼可见。正确做法是先把窗口一次设好然后连续送512字节屏幕内部自动递增地址。void LCD_DrawChar16(uint16_t x, uint16_t y, const uint8_t *font, uint16_t fg, uint16_t bg) { LCD_SetWindow(x, y, x 15, y 15); for (uint8_t row 0; row 16; row) { uint8_t lb font[row * 2]; // 左字节高位在前 uint8_t rb font[row * 2 1]; // 右字节高位在前 for (uint8_t bit 0; bit 8; bit) { uint16_t pixel (lb (0x80 bit)) ? fg : bg; LCD_WriteData(pixel 8); LCD_WriteData(pixel 0xFF); pixel (rb (0x80 bit)) ? fg : bg; LCD_WriteData(pixel 8); LCD_WriteData(pixel 0xFF); } } }逻辑说明字模数组按“逐行式、高位在前”读取row2取左字节row21取右字节。0x80向右移动用来逐位判断当前像素是前景还是背景。设定窗口后连续写数据屏幕GRAM地址自动从左到右、从上到下移动不需要再设置坐标。参数说明fg和bg都是RGB565格式常见颜色直接写宏例如红色0xF800、绿色0x07E0、白色0xFFFF、黑色0x0000。前景背景分离的意义是显示带底色的菜单时字符区域直接用背景色覆盖不需要先擦除整块矩形。3.4 字模数组放哪片内Flash还是外部字库最直白的边界是存储空间STM32F103C8T6的Flash只有64KBRAM只有20KBHZK16全量256KB根本塞不下。所以中文方案天然分两条路。路径一是这个资源包采用的固定字模数组。把界面里真正会用到的汉字用脚本提取成const数组编译进Flash。一个汉字32字节200个汉字只占6.4KB一段常规仪表菜单完全够用。缺点也很明确以后想显示新汉字要重新跑脚本、重新编译烧录。路径二是把HZK16放进外部SPI Flash或SD卡通过FATFS按偏移读取。这适合词库大、动态内容多、要显示任意用户输入文字的场景。代价是要额外引入文件系统、Flash驱动和内存管理工程复杂度上一个台阶。对绝大多数固定界面项目我宁愿选路径一。别为了“以后可能用到”预生成几千个汉字字模数组膨胀到几十KB之后C8T6的Flash空间会被压缩得很难受后续加功能都没地方。4. ST7735中文显示排查实录白屏、花屏、镜像、偏移、编码五个坑4.1 上电白屏背光亮但屏幕没有内容现象背光正常屏幕白茫茫一片初始化代码执行完没有任何显示。原因排查按顺序来第一RESET引脚时序不对或引脚悬空ST7735一直处于复位状态第二VCC供电不稳屏幕内部DCDC没起来第三0x11退出睡眠后的120ms延时被优化掉或延时函数根本不工作第四发送0x3A时数据错写成0x06之类的非法值像素格式没配成565。解决硬件上确认RST引脚有10ms以上低电平再拉高的完整复位过程用万用表量VCC在点亮瞬间有没有跌落。软件上先把初始化后的验证动作固定成一句话——直接全屏填固定色比如LCD_Clear(0x07E0)填绿色不要上来就画中文或者写字。纯色能出来说明底层链路通纯色出不来问题一定在电源、复位、SPI时序这三者之间。4.2 花屏或偶发花屏刷新越快越明显现象全屏填充颜色时出现横条纹、噪点位置不固定把SPI分频调高后更明显。原因杜邦线太长加上面包板寄生电容SPI时钟沿变形或者SPI速率超过面板能稳定接收的极限。很多模块在演示视频里看起来很流畅但那是短排线加9MHz甚至18MHz驱动的效果自己桌面飞线一长信号反射立刻翻车。解决把SPI_BaudRatePrescaler从8改成16或32强迫自己在4.5MHz以下工作。线长超过10cm时别硬顶高频再快也只是数字好看。电源脚上加一个0.1uF瓷片电容到GND能滤掉清屏瞬间的大电流毛刺。提示花屏问题修到怀疑硬件时先试试把CPOL改成High、CPHA保持1Edge也就是切到Mode 3。ST7735虽然多数模块在Mode 0工作但个别转接板的电平转换电路会反向导致采样沿变化。4.3 英文字正常中文字体镜像或颠倒现象同样的初始化代码下英文数字显示方向正常一旦调用16x16中文函数汉字要么左右镜像要么上下颠倒有时还是反转90度。原因字模点阵的出厂扫描方向与MADCTL设置的GRAM扫描方向不一致。屏幕GRAM的访问顺序受0x36寄存器影响而取模软件生成点阵时按“屏幕从左到右、从上到下”假设来排列。两边不匹配数据流没变但物理上映射错位了。解决全部统一成一套约定——驱动侧MADCTL写0x00取模侧选“逐行式、顺向、高位在前、阴码”代码里用第3章的row*2读取方式。只要换一套字模工具先检查工具导出设置里的“横向取模/纵向取模”和“高位在前/低位在前”再决定要不要改驱动代码。取模方向错了靠改MADCTL转回来能救一时但新屏再往另一个方向偏又得重调。4.4 显示区域偏移上下有黑边或边缘残留像素现象全屏填充白色时屏幕上方或左侧出现明显黑边内容整体偏移或者某个方向填不满。原因不同厂家玻璃面板的GRAM起始坐标不一样ST7735的列偏移和行偏移寄存器默认值不一定适合当前面板。有些屏幕的显示区物理起点是(2,1)有些是(0,2)硬编码从(0,0)开始就会有一整条边不对。解决把坐标偏移抽成两个宏在LCD_SetWindow入口统一加上#define LCD_X_OFFSET 2 // 实测偏移可能是0/1/2 #define LCD_Y_OFFSET 1 // 不同屏批次不一样 // SetWindow内部第一行改成 // LCD_WriteData((x0 LCD_X_OFFSET) 8);调试方法很朴素全屏填红色再全屏填蓝色观察那条异常亮带的位置每次改偏移宏后重新填充几分钟就能试出正确值。不要高估这块屏的批次一致性同一家店不同批次的1.8寸屏偏移都可能不一样。4.5 中文显示成乱码编码不匹配不是显示函数的问题现象字模数组和绘制函数都对菜单字符串传入后屏幕上出现的是错字、杂点或空白。原因Keil工程源码文件被VS Code或其它编辑器保存成了UTF-8编码中文字符串在编译后内存里是UTF-8的字节序列而GetFontBitmap按GB2312的两字节去查表字节流对不上自然找不到正确字模。这种问题在代码里看一切正常因为编辑器能按UTF-8正确显示但编译结果已经错了。解决在Keil里把源文件编码统一成ANSI也就是GB2312或者把中文字符串直接写成转义字节数组。查字模时按GB2312索引的设计一旦定下来源码编码就必须跟它一致这是最容易被忽略的隐藏依赖。5. 进阶把中文显示封装成自动换行的小文本框架5.1 带换行逻辑的字符串绘制接口固定坐标画单个汉字只能应付简单标题菜单场景里通常需要在限定宽度内显示多行文本。封装一个支持中文和ASCII混排、能自动换行的函数是让界面进入可用状态的关键一步。#define CN_W 16 #define CN_H 16 #define CHAR_SPACE 2 #define LINE_SPACE 2 void LCD_DrawStringCN(uint16_t x0, uint16_t y0, const char *str, uint16_t fg, uint16_t bg, uint16_t max_width) { uint16_t x x0, y y0; while (*str) { if ((uint8_t)*str 0x80) // ASCII分支 { x 8; // 半角字符按8像素宽推进 str; } else // 汉字分支 { uint16_t gb ((uint8_t)str[0] 8) | (uint8_t)str[1]; const uint8_t *bm GetFontBitmap(gb); if (bm) LCD_DrawChar16(x, y, bm, fg, bg); str 2; x CN_W CHAR_SPACE; if (x CN_W - x0 max_width) // 超过限宽就换行 { x x0; y CN_H LINE_SPACE; } } } }逻辑说明函数按字节流逐个字符推进ASCII按单字节处理并占用8像素宽度汉字按双字节处理并占用16像素宽度。每画完一个汉字就检查当前x坐标加上一个字的宽度是否超过max_width超过则换行。查表函数GetFontBitmap在固定数组中线性匹配GB2312编码这套资源包里已生成好。参数说明max_width建议传屏幕宽度减去左右边距比如128-20108可以整除16。CHAR_SPACE和LINE_SPACE控制字距与行距做菜单时统一放宏里方便整体调整间距。5.2 局部刷新与帧缓冲取舍STM32F103C8T6的RAM只有20KBRGB565整帧需要128x160x2约40KB装不下。所以别想着做整帧缓冲再一次性刷屏应该按“变化区域”刷新菜单选中态变化时只调用LCD_SetWindow限定那一行矩形重绘局部区域。这样既避免全屏闪烁又能把刷新时间控制在毫秒级。从那以后我每次拿到一块新屏第一件事不是抄初始化序列而是把分辨率、MADCTL方向、偏移量三个参数先试出来再决定字模用什么取模方向整套流程走下来基本不会再翻车。希望帮到你这套工程包里的驱动和脚本可以直接上手复现。本文还有配套的精品资源点击获取