☰
STM32驱动ST7735显示中文:SPI接线、字模与排查全解析
2026/10/17 11:06:19 网站建设 项目流程

简介:这套面向 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寸ST7735:SPI接线、初始化序列与驱动函数

2.1 先看模块丝印再接线:同样是1.8寸屏,引脚顺序可能不同

市面上1.8寸ST7735模块常见为8针:VCC、GND、CS、RESET、A0(也叫DC/RS)、SDA(MOSI)、SCK、LED(背光)。红板黑板的引脚顺序不完全一样,接线前先拍张照片对照丝印。

屏模块引脚作用推荐接法(F103C8T6)备注
SCK/SCLSPI时钟PA5SPI1_SCK,空闲默认低电平
SDA/MOSI主机数据输出PA7SPI1_MOSI
CS片选PA4软件控制即可,不占硬件NSS
A0/DC/RS数据/命令选择PB0别接到SPI1的MISO脚
RESET/RST复位PB1低电平有效
LED/BLK背光控制PA2或3.3V常亮可直接接VCC,PWM可调亮度
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,对应硬件SPI1;CS用PA4软件控制,不启动硬件NSS;DC和RST放PB0/PB1是为了避开PA6(MISO复用脚),免得全双工模式下把DC误配成输入。

SPI基础配置要落在Mode 0,即CPOL=Low、CPHA=1Edge。时钟分频先用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); // 0x05=RGB565,16位色,高字节先发 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不是字符型屏幕,送一个字符不会直接出字。所有内容先写进GRAM,GRAM通过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行,每行左右两个字节,共16x2=32字节,每字节高位在前。

区位码检索公式就一句话:

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取左字节,row2+1取右字节。0x80向右移动用来逐位判断当前像素是前景还是背景。设定窗口后连续写数据,屏幕GRAM地址自动从左到右、从上到下移动,不需要再设置坐标。

参数说明:fg和bg都是RGB565格式,常见颜色直接写宏,例如红色0xF800、绿色0x07E0、白色0xFFFF、黑色0x0000。前景背景分离的意义是显示带底色的菜单时,字符区域直接用背景色覆盖,不需要先擦除整块矩形。

3.4 字模数组放哪:片内Flash还是外部字库

最直白的边界是存储空间:STM32F103C8T6的Flash只有64KB,RAM只有20KB,HZK16全量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-20=108,可以整除16。CHAR_SPACE和LINE_SPACE控制字距与行距,做菜单时统一放宏里,方便整体调整间距。

5.2 局部刷新与帧缓冲取舍

STM32F103C8T6的RAM只有20KB,RGB565整帧需要128x160x2约40KB,装不下。所以别想着做整帧缓冲再一次性刷屏,应该按“变化区域”刷新:菜单选中态变化时,只调用LCD_SetWindow限定那一行矩形,重绘局部区域。这样既避免全屏闪烁,又能把刷新时间控制在毫秒级。从那以后我每次拿到一块新屏,第一件事不是抄初始化序列,而是把分辨率、MADCTL方向、偏移量三个参数先试出来,再决定字模用什么取模方向,整套流程走下来基本不会再翻车。希望帮到你,这套工程包里的驱动和脚本可以直接上手复现。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询