简介:这份资源面向使用STM32F103C8T6驱动1.8寸ST7735彩屏的嵌入式开发者,尤其适合需要显示中英文的显示类项目。作者在商家代码基础上重写了大部分逻辑,加入字库支持,可同时显示中文与英文,并附有移植提示,方便读者将显示模块迁移到自己的工程中。压缩包共101个文件,约412KB,以49个.h头文件与41个.c源文件为主,涵盖STM32标准外设库的定时器、Flash、RCC、ADC、I2C、CAN、USART、FSMC等模块,另含2个hex烧录文件、2个.s启动文件及uvproj、uvopt等Keil工程文件,目录结构完整,可直接打开编译。目前已有4886人学习下载,说明该方案在彩屏驱动与中文字库应用上具有较高参考价值。读者可借此掌握ST7735初始化、字库取模与中英文混合显示的实现思路,并依据文件内提示完成移植,减少重复调试成本。
1. 一块 1.8 寸 ST7735 屏配 STM32F103C8,中文显示到底卡在哪
很多人第一次把 1.8 寸 ST7735 彩屏接到 STM32F103C8T6 上,英文和数字刷得飞起,一到中文就翻车:要么满屏乱码,要么只显示几个方块,要么干脆白屏。问题往往不在屏幕本身,而在字库、取模方式和驱动初始化这三件事没对齐。这份资源就是围绕 STM32F103C8 驱动 1.8 寸 ST7735 屏显示中文这条链路打包的,适合正在做小尺寸彩屏 UI、想快速验证中文显示方案的嵌入式开发者。它解决的不是“点亮屏幕”这种入门问题,而是把中文字库怎么存、怎么取、怎么刷到 ST7735 的显存窗口里这一整套流程跑通。如果你手里正好有这块屏和这颗 MCU,下面这些参数和踩坑记录能帮你少走几天弯路。
2. ST7735 与 STM32F103C8 的硬件对位:SPI 接线与初始化参数
2.1 为什么 ST7735 更适合 SPI 而不是并口
1.8 寸 ST7735 常见模块有两种接口形态:8 位并口和 4 线 SPI。并口刷屏快,但会吃掉 STM32F103C8T6 大量 IO,这颗 MCU 总共才 37 个可用 IO,留给按键、传感器、串口的余量本来就不多。SPI 方案只占 4 到 5 根线,代价是刷新率下降,但对于 128x160 分辨率、主要显示文字和简单图形的场景,SPI 完全够用。常见做法是 SPI1 跑 18MHz 到 36MHz,ST7735 手册标称写周期最小 66ns,对应约 15MHz,实际很多模块超到 36MHz 也能稳定跑,但这是玄学,批量产品建议留余量。
接线我一般这样分配:
| ST7735 引脚 | STM32F103C8 引脚 | 说明 |
|---|---|---|
| SCL/SCK | PA5 | SPI1 时钟 |
| SDA/MOSI | PA7 | SPI1 数据 |
| RES/DC | PA3 | 数据/命令切换 |
| CS | PA4 | 片选,可接地省 IO |
| BLK | PA2 或 3.3V | 背光,PWM 可调亮度 |
注意:ST7735 模块的 VCC 一定确认是 3.3V 版本,5V 版本直接接 STM32 的 3.3V IO 会通信异常,甚至损伤电平。
2.2 SPI 初始化和 ST7735 寄存器配置的关键参数
初始化分两层:STM32 的 SPI 外设配置,和 ST7735 内部寄存器序列。SPI 这边重点是模式。ST7735 支持 SPI 模式 0 和模式 3,多数模块用模式 0 就能跑,但个别批次必须模式 3,遇到白屏先换模式试。数据位 8 位,MSB 先行,这些是固定的。
// STM32F103 SPI1 初始化,模式0,18MHz void SPI1_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE); // PA5 SCK, PA7 MOSI 复用推挽 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); SPI_InitStructure.SPI_Direction = SPI_Direction_1Line_Tx; // 只发不收 SPI_InitStructure.SPI_Mode = SPI_Mode_Master; SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; // 模式0 SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4; // 72M/4=18M SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; SPI_Init(SPI1, &SPI_InitStructure); SPI_Cmd(SPI1, ENABLE); }逻辑说明:这里把 SPI1 设为主机、只发不收,因为驱动屏幕基本不需要读回数据。SPI_BaudRatePrescaler_4对应 18MHz,想更快改成_2就是 36MHz,但先保证稳定再提速。DC 引脚用普通推挽输出,写命令拉低、写数据拉高,这个切换时序如果搞反,屏幕会一直白屏或花屏。
ST7735 的初始化序列里,0x11是退出睡眠,0x3A设颜色格式,0x36设扫描方向。颜色格式选0x05是 16 位 RGB565,这是最常用的。扫描方向0x36的参数决定横屏还是竖屏,也决定原点在哪个角,中文取模的坐标计算跟这个直接相关,后面会细说。
3. 中文字库落地:取模、存储与 ST7735 显存窗口写入
3.1 中文取模的三种方式与选型
中文显示绕不开字库。常见做法有三种:一是用 PC 端取模软件生成点阵数组,直接编进代码;二是把字库文件放到外部 Flash 或 SD 卡,运行时读取;三是用芯片自带 Flash 剩余空间存部分常用字。这份资源走的是第一种,适合字量不大、UI 固定的场景,比如菜单、状态提示。
取模参数必须和显示代码严格一致,否则就是乱码。关键参数有四个:字体大小、取模方式、字节倒序、阴码阳码。我一般用 16x16 点阵显示正文,12x12 显示小字。取模方式选“逐行式”,阴码(1 表示亮),字节不倒序。这四个参数只要有一个和代码里的解析逻辑对不上,出来的就是雪花点。
// 16x16 汉字点阵示例,"中"字,逐行式,阴码 const unsigned char HZ_ZHONG[32] = { 0x01,0x00,0x01,0x00,0x01,0x00,0x01,0x00, 0x3F,0xF8,0x21,0x08,0x21,0x08,0x21,0x08, 0x21,0x08,0x21,0x08,0x3F,0xF8,0x21,0x08, 0x01,0x00,0x01,0x00,0x01,0x00,0x01,0x00 };这段数组每行 2 字节,共 16 行,合计 32 字节。解析时先取第一个字节的高位到低位,再取第二个字节,依次画 16 个像素,然后换行。如果取模软件设的是“逐列式”,这个解析顺序就要改成先列后行,否则字会转 90 度或者散架。
3.2 把点阵刷进 ST7735 的显存窗口
ST7735 显示中文的本质,是把点阵的每个 bit 映射成 RGB565 颜色,写进指定矩形区域。流程是:设列地址范围、设行地址范围、发写显存命令、连续写颜色数据。这里最容易翻车的是坐标偏移。ST7735 的显存是 132x162,但可见区域是 128x160,不同模块的偏移量不一样,常见的有 (0,0)、(2,1)、(1,0) 几种。偏移没设对,显示内容会整体偏几个像素,中文看起来就像被裁了一角。
// 在指定位置显示一个 16x16 汉字 void LCD_ShowChinese(uint16_t x, uint16_t y, const uint8_t *hz, uint16_t color, uint16_t bg) { uint8_t i, j; uint16_t byteIdx = 0; LCD_SetWindow(x, y, x + 15, y + 15); // 设置 16x16 窗口 for (i = 0; i < 16; i++) { uint8_t high = hz[byteIdx++]; uint8_t low = hz[byteIdx++]; for (j = 0; j < 8; j++) { // 先画高字节的 8 个像素 LCD_WriteData((high & 0x80) ? color : bg); high <<= 1; } for (j = 0; j < 8; j++) { // 再画低字节的 8 个像素 LCD_WriteData((low & 0x80) ? color : bg); low <<= 1; } } }逻辑说明:LCD_SetWindow内部先发0x2A设列地址,再发0x2B设行地址,最后发0x2C准备写显存。LCD_WriteData每次写 16 位颜色,高字节先发。参数color是前景色,bg是背景色,如果背景色和屏幕底色一致,可以只画前景点,省一半数据量,但刷新时会有残影,建议还是全画。
提示:连续写显存时 CS 可以一直拉低,DC 保持高电平,这样比每写一个像素都切换 DC 快很多。ST7735 的写显存是自增地址,窗口设好后不用重复设坐标。
3.3 字库索引与多字显示
单个汉字好办,一串汉字就需要字库索引。常见做法是建一个结构体数组,把汉字编码和点阵指针关联起来。如果字量超过几十个,手工维护容易出错,我一般写个简单的查找函数,用 GB2312 编码做 key。注意 STM32 是小端模式,GB2312 编码在数组里存的时候高低字节顺序要确认,搞反了查不到字。
typedef struct { uint16_t gbCode; // GB2312 编码 const uint8_t *pData; // 点阵数据指针 } HZ_Item; const HZ_Item HZ_Table[] = { {0xD6D0, HZ_ZHONG}, // "中" {0xB9FA, HZ_GUO}, // "国" // ... 其他汉字 }; const uint8_t* HZ_Find(uint16_t gbCode) { for (uint16_t i = 0; i < sizeof(HZ_Table)/sizeof(HZ_Item); i++) { if (HZ_Table[i].gbCode == gbCode) { return HZ_Table[i].pData; } } return 0; // 未找到 }逻辑说明:gbCode用 16 位存 GB2312 编码,比如“中”是 0xD6D0。查找时线性遍历,字量小的时候够用,字量上千建议改成二分查找或哈希。返回 0 表示没找到,显示时可以画个方框占位,方便排查是哪个字缺了。
4. 避坑与排查:中文显示翻车的五条血泪记录
4.1 现象:屏幕全白,连英文都不显示
原因:SPI 模式不对,或者 RES 复位时序太短。ST7735 要求复位低电平至少 10us,很多代码只延时 1us 就拉高,芯片没复位成功。解决:把 RES 拉低后延时 20ms,拉高后再延时 120ms,等芯片内部稳定。同时确认 SPI 模式,模式 0 不行就换模式 3。
4.2 现象:英文正常,中文全是方块或乱码
原因:取模参数和解析代码不一致。最常见的是取模软件设了“字节倒序”,代码里没倒。解决:统一取模设置为逐行式、阴码、不倒序,然后检查解析时是先高字节还是先低字节。16x16 字模每行 2 字节,先发第一个字节的 8 个像素,再发第二个字节,顺序反了字就镜像。
4.3 现象:中文显示位置整体偏移几个像素
原因:ST7735 显存偏移量没设对。不同厂家模块的可见区域起始地址不同。解决:在初始化里改0x2A和0x2B的起始值,常见偏移组合是列偏移 0 或 2,行偏移 0 或 1。可以画一个全屏边框,看哪条边被裁掉,然后调偏移。
4.4 现象:刷新中文时屏幕闪烁或撕裂
原因:没有用窗口写入,而是逐像素设坐标,或者背景色和前景色分两次写。解决:用LCD_SetWindow框定汉字区域,然后连续写 256 个颜色值,中间不切 DC。如果还是闪,检查 SPI 速率是不是太高导致数据丢失,降到 9MHz 试。
4.5 现象:部分汉字显示正常,部分缺笔画
原因:字库数组定义时漏了字节,或者const数组被编译器优化到错误地址。解决:检查每个汉字的点阵数组长度是不是 32 字节(16x16),用sizeof确认。另外,STM32F103C8 的 Flash 只有 64KB,字库太大会挤占代码空间,编译报错region FLASH overflowed就是字库超了,需要精简或外挂存储。
5. 进阶技巧:用 DMA 刷中文和字库裁剪的取舍
5.1 用 DMA 把中文刷新从 CPU 手里解放出来
SPI 逐字节写显存时 CPU 全程忙等,刷一屏中文要几十毫秒,界面卡顿。STM32F103C8 的 SPI1 支持 DMA 发送,把汉字点阵转成 RGB565 缓冲区后,一次 DMA 传输整块数据,CPU 可以去处理按键或串口。配置时注意 DMA 通道要和 SPI1_TX 对应,传输完成中断里再拉高 CS。
// SPI1 TX 使用 DMA1 通道3 void SPI1_DMA_Init(void) { DMA_InitTypeDef DMA_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&SPI1->DR; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)colorBuf; // RGB565 缓冲区 DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; DMA_InitStructure.DMA_BufferSize = 256; // 16x16 像素 DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; DMA_Init(DMA1_Channel3, &DMA_InitStructure); }逻辑说明:DMA_PeripheralDataSize_HalfWord对应 16 位颜色,和 ST7735 的 RGB565 对齐。DMA_BufferSize设成汉字像素数,16x16 就是 256。传输前先调LCD_SetWindow,再使能 DMA,传输完成中断里拉高 CS。这样刷一个汉字的时间从 CPU 忙等的几毫秒降到 DMA 后台搬运,界面流畅度提升明显。
5.2 字库裁剪:只留用到的字,省出 Flash 空间
STM32F103C8 只有 64KB Flash,一个 16x16 汉字 32 字节,一千个字就是 32KB,直接吃掉一半。我一般只保留 UI 里实际出现的字,用脚本从完整字库里筛。比如菜单只有“设置、返回、确认、取消”这些,加上数字和单位,通常不超过 200 个字,6KB 左右就够。如果后续要扩展,再考虑外挂 SPI Flash 存完整字库,运行时按需读取。
| 字库方案 | 占用空间 | 适用场景 | 读取速度 |
|---|---|---|---|
| 全字库编进代码 | 约 260KB | Flash 充足的型号 | 最快 |
| 裁剪字库编进代码 | 6KB 到 32KB | STM32F103C8 等小容量 | 快 |
| 外挂 SPI Flash | 0(片内) | 字量大、需扩展 | 中等,受 SPI 速率限制 |
| SD 卡字库 | 0(片内) | 字量极大、不常刷 | 慢,需文件系统 |
选型建议:如果 UI 文字固定,裁剪字库是最省事的,编译进去直接用,没有额外硬件。如果产品要支持多语言或用户自定义文字,再上外挂 Flash。别为了“以后可能用到”把全字库塞进 64KB 的芯片,编译过不去的时候后悔药可没地方买。
5.3 验证中文显示是否正确的笨办法
刷完一个汉字后,别急着写下一个。我习惯先画一个 16x16 的边框,把汉字刷在边框里,看笔画有没有超出边界、有没有偏移。然后刷一行“中中中中”,看字间距是不是均匀。最后刷一段混合中英文,确认 ASCII 和汉字的基线对齐。这套流程走一遍,基本能排除取模、坐标、窗口设置这三类问题。从那以后我每次换屏幕模块或者换取模软件,都强制走一遍这个验证流程,省得在整屏刷新时才发现字是歪的。希望帮到你。
本文还有配套的精品资源,点击获取