简介基于STM32F103C8T6与1.8寸ST7735彩屏的中文显示程序面向嵌入式入门及显示驱动开发者。作者在商家原代码基础上重写大部分逻辑支持中英文字库显示并预留清晰移植接口适合快速实现中文菜单、状态信息或简单GUI。压缩包共101个文件大小412KB含49个H头文件、41个C源文件覆盖STM32外设库与屏幕驱动另有HEX固件可直接烧录以及Keil工程文件与编译脚本便于二次修改。目前已有4886人学习下载。通过该资源可获得一套可运行的彩屏中文工程掌握ST7735驱动及汉字字库调用思路对后续移植到其他STM32型号或屏幕极具参考价值。1. 不解决字库问题1.8寸 ST7735 永远只能显示英文和色块某开发者从抽屉里翻出几块吃灰的 1.8 寸彩屏主控是 STM32F103C8T6。例程里满屏跑马灯、色条、数字都能正常显示唯独中文全是乱码和方块。折腾一晚后他发现问题根本不在屏幕驱动而在于字模的提取方式和编码链路。这个项目解决的就是这件事F103C8T6 驱动 1.8 寸 ST7735 彩屏把“显示中文”这条路完整打通从 SPI 初始化、控制器初始化到 GBK 编码、字模取模、描点函数每一层都是可直接编译的 C 工程。这个项目适合两类人一类是刚入手 STM32 彩屏驱动、想跑通一个能显示中文菜单的板子另一类是在做仪表、手持设备、小家电交互界面需要快速把中文字符串落到屏幕上的人。它不涉及复杂 OS 和 GUI 库代码平铺直叙改起来不费劲。2. 工程全景与技术选型为什么还要一款中文显示例程2.1 硬件链路和引脚分配F103C8T6 与 1.8 寸屏的典型接法STM32F103C8T6 是 64 脚封装Flash 只有 64KBRAM 20KB。1.8 寸 ST7735 屏幕的分辨率通常是 128x160颜色格式为 RGB565通过 SPI 接口通信。驱动这块屏用到的信号线只有五根SCK、SDAMOSI、CS、DC数据/命令选择、RST加上背光控制和电源。用硬件 SPI 还是软件 SPI 影响的是刷屏速度不影响显示逻辑这也是项目里值得注意的设计取舍。常见的引脚映射如下和网上多数最小系统板例程一致屏幕信号STM32F103C8T6 引脚说明SCKPA5SPI1_SCK复用推挽输出SDA/MOSIPA7SPI1_MOSI复用推挽输出CSPA2片选软件控制拉低DCPA10 为命令1 为数据RSTPA0复位信号低电平复位后拉高BLPA3背光控制可接 3.3V 或 PWM 控制这块屏的 IO 电平是 3.3VF103 的 GPIO 也是 3.3V可以直接互连不需要电平转换。只有背光部分要注意有些模块的背光引脚直接接在 5V 上也行但如果板子上有稳压电路建议统一用 3.3V避免个别批次模块漏电导致发热。2.2 ST7735 驱动层初始化序列和画点是整个工程的底层地基ST7735 是一个需要主机发送大量初始化命令的显示控制器。出厂后屏幕内部寄存器是默认状态必须先执行一段厂商规定的初始化序列屏幕才会进入可用的显示模式。这段序列在不同厂家的模块上几乎一模一样基本来自 ST7735V 的数据手册只是开窗函数和颜色格式略有差异。下面是初始化序列的关键片段按 1.8 寸屏常规流程裁剪void ST7735_Init(void) { ST7735_RST_LOW(); // 硬件复位 delay_ms(20); ST7735_RST_HIGH(); // 拉高释放复位 delay_ms(50); // 可选软件复位写 0x01 命令 ST7735_WriteCmd(0x01); delay_ms(120); // 退出睡眠模式 ST7735_WriteCmd(0x11); delay_ms(120); // 设置帧率正常模式 60Hz ST7735_WriteCmd(0xB1); ST7735_WriteData(0x05); ST7735_WriteData(0x3A); ST7735_WriteData(0x3A); // 像素格式16bit/pixelRGB565 ST7735_WriteCmd(0x3A); ST7735_WriteData(0x05); // MADCTL: 控制扫描方向和 RGB/BGR 顺序 ST7735_WriteCmd(0x36); ST7735_WriteData(0xC8); // 开启显示 ST7735_WriteCmd(0x29); }逻辑说明ST7735_WriteCmd会把 DC 引脚拉低然后通过 SPI 发送一个字节ST7735_WriteData会把 DC 拉高再发送数据。RST 引脚用普通 GPIO 控制是因为硬件复位时序必须精确到毫秒级而 SPI 外设无法直接控制这种时序。0x36 寄存器里的 0xC8 表示 MX1、MY1此时屏幕的左上角坐标原点和模块丝印方向对齐如果你的屏幕显示方向转了改这个值就能旋转画面。画点函数则是所有图形和文字的地基所有中文显示最终都会落到画点上void ST7735_DrawPixel(uint16_t x, uint16_t y, uint16_t color) { if (x SCREEN_WIDTH || y SCREEN_HEIGHT) return; // 边界裁剪 // 设置显示窗口为一个像素 ST7735_WriteCmd(0x2A); // 列地址设置 ST7735_WriteData(0x00); ST7735_WriteData(x 2); // 修正偏移量 ST7735_WriteData(0x00); ST7735_WriteData(x 2); ST7735_WriteCmd(0x2B); // 行地址设置 ST7735_WriteData(0x00); ST7735_WriteData(y 1); ST7735_WriteData(0x00); ST7735_WriteData(y 1); ST7735_WriteCmd(0x2C); // 写显存 ST7735_WriteData(color 8); ST7735_WriteData(color 0xFF); }参数说明SCREEN_WIDTH和SCREEN_HEIGHT在工程里定义为 128 和 160。x2、y1 是 ST7735 控制器的显存偏移量修正出厂时模块的可见区域往往不是从坐标 0,0 开始这部分偏移量需要在实物上微调。颜色值是 RGB565 格式比如红色是 0xF800绿色是 0x07E0蓝色是 0x001F。因为 ST7735 的显存接口是 16bit发送颜色时要把高字节和低字节分开发送。2.3 中文显示链路字符串到像素的完整传递路径英文和数字字符大多可以用 8x16 或 6x12 点阵直接给出 ASCII 索引而中文需要知道字符的内码才能在字模数组里找到对应的点阵数据。这个项目的中文显示链路是中文字符串先经过编码转换得到 GBK 内码的高位字节和低位字节再根据这两个字节计算字模数组的偏移量然后逐字节读取点阵数据最后通过画点循环将数据写入屏幕。uint16_t text_index 0; uint8_t high_byte text_utf8[text_index]; uint8_t low_byte text_utf8[text_index]; uint32_t offset 0; // 按 GB2312 编码计算字模偏移 if (high_byte 0xB0 low_byte 0xA1) { offset (high_byte - 0xB0) * 94 (low_byte - 0xA1); offset offset * FONT_BYTES; // FONT_BYTES 32即 16x16 点阵 } // 取得字模起始地址 const uint8_t *font_addr Chinese_Font_Table offset;逻辑说明F103C8T6 里存中文字模通常用数组方式把 GB2312 的常用汉字按区位码顺序排布显示时直接按内码索引。high_byte是 GBK 编码的高位落在 0xB0~0xF7 之间low_byte落在 0xA1~0xFE 之间。每 94 个字一个区区号从汉字区开始算所以要先减去 0xB0。FONT_BYTES对于 16x16 点阵是 32 字节因为一行两个字节共 16 行。这一层的关键是字节序和数组布局必须一致。如果字模数组是按“列行式”排列的而画点函数是按“逐行式”读取的那么显示出来的文字就是一块噪声这也是许多人下载了例程之后中文依旧花屏的最常见原因。3. 核心机制拆解一个中文字符从内码到屏幕的完整旅程3.1 GBK 编码和字模表布局区位码不是玄学是能算出来的偏移GB2312 编码中共有 6763 个汉字按拼音顺序分成了 72 个区每区 94 个字符。汉字区从第 16 区开始即区位码 16~55 是一级汉字按拼音排序56~87 是二级汉字按部首排序。在 C 语言里一个 GBK/GB2312 双字节字符的高位字节落在 0xB0~0xF7低位字节落在 0xA1~0xFE这就是计算数组索引的全部依据。计算字模位置的实际公式如下// 假定字模表只包含常用汉字区 uint32_t GetChineseFontOffset(uint8_t high, uint8_t low) { uint16_t index; uint32_t offset; if (high 0xB0 || high 0xF7) return 0; // 非汉字区返回0 if (low 0xA1 || low 0xFE) return 0; index (high - 0xB0) * 94 (low - 0xA1); offset (uint32_t)index * FONT_BYTES; return offset; }参数说明94 是每区的字符容量这个数在任何一本 GB2312 编码文档里都是固定的。如果你的字模表包含的是 16x16 点阵每个字 32 字节那偏移量就是索引乘以 32。用uint32_t类型计算偏移是为了避免 16 位乘法溢出因为最大偏移量接近 7000 字 * 32 字节 224KB打表工程里一般只放常用字或自制小字库。3.2 取模软件参数设置错误参数会导致整片文字糊成噪声中文点阵字模一般不是手写的而是从取模软件生成。取模软件里最关键的设置有三项取模方式逐行式 / 逐列式、每行显示的点数、取模走向高位在前还是低位在前。用 16x16 点阵为例逐行式取模后得到的数据排列是第一行左半字节、第一行右半字节、第二行左半字节……推荐的一组参数是点阵格式选“阴码”1 表示亮取模走向选“高位在前”取模方式选“逐行式”每行点数选 16。把这组参数生成的数组放到工程里配套的描点函数就很好写void DrawChar16(uint16_t x, uint16_t y, const uint8_t *font_data, uint16_t color, uint16_t bg_color) { for (uint8_t row 0; row 16; row) { uint8_t byte_l font_data[row * 2]; // 行内左字节 uint8_t byte_h font_data[row * 2 1]; // 行内右字节 for (uint8_t bit 0; bit 8; bit) { if (byte_l (0x80 bit)) { ST7735_DrawPixel(x bit, y row, color); } else { ST7735_DrawPixel(x bit, y row, bg_color); } if (byte_h (0x80 bit)) { ST7735_DrawPixel(x 8 bit, y row, color); } else { ST7735_DrawPixel(x 8 bit, y row, bg_color); } } } }逻辑说明font_data指向 32 字节的字模数据row 循环 0~15 对应点阵的横向行每一行用两个字节表示 16 个点左字节表示 x 坐标 0~7 的点右字节表示 x 坐标 8~15 的点。测试位时按0x80 bit从高位往低位取如果显示的文字左右颠倒把这里改成0x01 bit即可但屏幕上的文字方向会反转。这段代码还有一层价值它为后续扩展各种字体大小铺平了路。只要把循环上限从 16 改成任意数值再调整字节拆分逻辑就能适配 24x24、32x32 甚至更大的字库而不影响上层调用接口。3.3 显示函数的分层设计转码、取模、描点三层各司其职工程里把显示函数拆成了三层textUTF8ToGBK负责编码转换GetChineseFontOffset负责字模寻址DrawChar16负责把字模数据变成像素。用户层只调用一个统一入口void LCD_ShowChinese(uint16_t x, uint16_t y, const char *str, uint16_t color) { uint8_t high, low; uint32_t offset; uint16_t pos_x x; while (*str) { // 从 UTF-8 字符串中提取一个完整汉字的 GBK 内码 high *str; low *str; // 英文字符直接跳过避免解析错位按需求可另走 ASCII 路径 if (high 0x80) continue; offset GetChineseFontOffset(high, low); if (offset ! 0) { DrawChar16(pos_x, y, Chinese_Font_Table offset, color, 0x0000); pos_x 16; // 16x16 点阵所以列坐标加 16 } } }参数说明pos_x是当前写入的横向坐标每个汉字占 16 个像素宽度写完一个就自增 16这样才能让连续的汉字水平排列。颜色参数color控制的是前景色背景色固定为黑色。如果你做的是深色主题界面背景色还可以通过另一个参数传入。这里的 UTF-8 到 GBK 转换依赖编译器的默认编码MDK 默认将源文件保存为 UTF-8 时“中”字在内存里是 E4 B8 AD 三个字节不能直接当 GBK 处理所以工程里通常会有一个转换函数或者直接把源文件设成 GB2312 编码二选一即可。4. 工程代码走读从 main 函数看一个典型的中文显示工程4.1 主循环的工作方式初始化占大头循环里只刷内容这个工程的主函数结构清晰系统时钟初始化之后立刻进入外设初始化然后是一个无休止的显示循环。它没有嵌入 RTOS所以整个循环是裸机轮询方式。对于静态菜单这类界面这种结构最稳定且易读。int main(void) { SystemInit(); // 时钟配置 72MHz Delay_Init(); GPIO_Init(); SPI1_Init(); // 硬件 SPI1 初始化 ST7735_Init(); // 屏幕控制器初始化 LCD_Fill(0, 0, 128, 160, 0x0000); // 清屏全黑 LCD_ShowStr(10, 20, STM32F103, 0xFFFF); // 英文数字 LCD_ShowChinese(10, 40, 温度传感器, 0x07E0); // 中文显示 LCD_ShowNum(10, 60, 25, 3, 0x001F); // 数值显示 while (1) { // 静态显示不需要反复刷可在此处放按键扫描或采样 Delay_ms(10); } }逻辑说明SystemInit复位时钟到 72MHzSPI1_Init配置 PA5/PA7 的复用功能、分频系数和数据格式ST7735_Init唤醒屏幕。清屏用LCD_Fill它内部会设置整个屏幕的开窗区域然后连续发送 128*160 个像素的颜色数据。整个主循环空转时不重复刷屏因为 ST7735 没有显存回读机制静态内容不需要持续刷新。这个结构的好处是后续加功能不打架。你要加按键扫描、传感器采样、串口打印直接塞进while(1)即可。要加多级菜单只需要把显示函数按菜单状态拆分循环里根据状态跳转。虽然裸机写法看起来不够高级但在 F103C8T6 这种小内存芯片上反而是最不容易翻车的做法。4.2 SPI 写入与硬件/软件 SPI 的取舍数据手写还是外设自动搬运工程默认使用的是硬件 SPI1发送一个字节的代码很简洁void SPI1_WriteByte(uint8_t data) { while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) RESET); SPI_I2S_SendData(SPI1, data); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) RESET); // 读 DR 寄存器清除 RXNE 标志避免连续发送时卡死 SPI_I2S_ReceiveData(SPI1); }参数说明SPI_I2S_FLAG_TXE是发送缓冲区空标志只有上一个字节被硬件移出后才能往 DR 寄存器写入新数据。如果在数据手册里看到 SPI 硬件有 8 字节 FIFO也建议逐字节发送中文显示的数据量不大速度不是瓶颈如果刷全屏图片再改用 DMA 方式更合适。软件 SPI 的用途在于灵活性。硬件 SPI 的 MOSI 引脚是固定的 PA7但某些项目因为 PCB 布线必须把 MOSI 挪到别的引脚这时就需要软件模拟。软件 SPI 只用两个 GPIO 就可以在任意引脚上模拟标准 8 位时序代价是重复的GPIO_SetBits和GPIO_ResetBits调用实测刷 128x160 全屏大约比硬件 SPI 慢 3~4 倍。这个速度差异在文字界面上感知不明显所以很多老例程干脆用软件 SPI 规避引脚冲突。4.3 关键配置参数SPI 分频、GPIO 复用和屏幕偏移量的实践经验SPI 时钟分频是新手最容易忽略的一处。F103 的 SPI1 挂在 APB2 上APB2 时钟默认 72MHz如果 SPI1 不分频SCK 高达 36MHz多数 1.8 寸 ST7735 模块跟不上会导致屏幕不稳定、雪花点或者完全白屏。工程里常见的做法是把 SPI1 分频设为 8得到 9MHz 的 SCK这样的速度对 ST7735 是一种稳妥的平衡void SPI1_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE); 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); // 片选、DC、RST 全部设为普通推挽输出 GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_Init(GPIOA, GPIO_InitStructure); SPI_InitStructure.SPI_Direction SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_Mode SPI_Mode_Master; SPI_InitStructure.SPI_DataSize SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL SPI_CPOL_Low; SPI_InitStructure.SPI_CPHA SPI_CPHA_1Edge; SPI_InitStructure.SPI_NSS SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_8; // 9MHz SPI_InitStructure.SPI_FirstBit SPI_FirstBit_MSB; SPI_Init(SPI1, SPI_InitStructure); SPI_Cmd(SPI1, ENABLE); }参数说明SPI_CPOL_Low和SPI_CPHA_1Edge是 ST7735 手册要求的模式 0 时序。如果这里配置成模式 3屏幕通常白屏不显示SPI_NSS_Soft表示使用软件管理片选此时 NSS 引脚不作为硬件控制CS 的高低电平完全由普通 GPIO 掌握方便灵活拉低。SPI_FirstBit_MSB对应取模时“高位在前”的设置取模软件里如果选了低位在前这里改成SPI_FirstBit_LSB配合也行但一般建议改取模参数而不是改 SPI。5. 常见问题排查与避坑1.8 寸屏显示中文的典型翻车场景5.1 中文全是方块、乱码或者花屏现象英文字符正常中文字符区域显示为密集的方块或静态噪声。 原因字模偏移计算错误或者取模参数与描点函数不匹配。最常见的是取模软件选了“逐列式”而代码按“逐行式”解析两边的数据语义完全对不上。 解决先确认取模软件的三个关键设置是“阴码、逐行式、高位在前”再检查工程里FONT_BYTES是否等于 3216x16 点阵。如果还是不对打印一个字模的原始字节和屏幕上的点阵对照肉眼核对前两行数据是否一致。5.2 屏幕有背光但全白或全黑现象背光亮但屏幕上什么都没有或者整个屏幕只有一种颜色。 原因SPI 极性配置错误屏幕没有完成初始化。具体检查点是 SPI_CPOL/CPHA 是否为模式 0RST 时序是否给出足够的低电平持续时间至少 10ms。 解决先单独测试ST7735_WriteCmd(0x29)能否打开显示。如果打开后依然全黑观察是否因为 GPIO 的 AF 配置错误PA5/PA7 必须设为GPIO_Mode_AF_PP如果误设成普通推挽输出SPI 外设的信号根本到不了引脚上。5.3 中文能显示但英文和数字变成随机符号现象中文正常显示但 ASCII 字符乱掉甚至显示成半个中文字模。 原因字符串指针一次性按两个字节处理一个字符把英文字符和它的下一个字节误认为一个完整中文字符。 解决在解析函数里先判断当前字节是否小于 0x80小于则按 ASCII 单字节处理对应的显示函数另走一套 8x16 点阵表。如果 LCD_ShowChinese 遇到了英文跳过去不显示也可以用统一封装让中英文混排自动切换核心是判断分支必须放在指针自增之前。5.4 文字方向不对或镜像现象汉字整体上下颠倒或者左右反过来了。 原因ST7735 初始化里 MADCTL 寄存器的 MX/MY 位设置与实际模块排布不一致。 解决工程里 MADCTL 写的是 0xC8如果你想调整成横屏或者换一边起始扫描改成 0xA8、0x68、0x28 中的某个值并观察效果。常见组合关系如下表MADCTL 值效果0xC8竖屏左上角为坐标原点默认0xA8竖屏X 轴翻转0x68横屏0x28横屏Y 轴翻转提示调整 MADCTL 后ST7735_DrawPixel里的坐标偏移量也要跟着重新校准否则画面边缘会出现偏移或显示错位。5.5 刷中文时屏幕边缘出现残影或者颜色残留现象文字刷新后旧文字的边缘还有淡淡痕迹。 原因ST7735 的显存写入没有清背景或者开窗位置计算留下了 1~2 个像素的间隔区域。 解决DrawChar16里已经写了背景色填充如果背景色用0x0000需确认该操作真的覆盖到整个字符区域。一个简单验证方法是把背景色改成红色或绿色看文字周围是否出现对应色块。如果边缘依然有残留检查开窗函数0x2A/0x2B是否重复写入相同坐标有些例程写画点时默认先增量坐标再写入导致最后一个像素落在窗口之外这种情况要回到开窗时序上修。6. 二次开发思路让显示从“能显示”到“好用”的小改造一旦中文显示链路跑通接下来的改造空间就集中在接口封装、交互体验和存储优化上。把显示函数从“画字符串”升级到“画可变信息”是这个阶段最实用的演进方向。typedef struct { uint16_t x; uint16_t y; uint16_t fg_color; uint16_t bg_color; } DispContext; void DispFormatString(DispContext *ctx, const char *fmt, ...) { char buffer[64]; va_list args; uint16_t cur_x ctx-x; va_start(args, fmt); vsnprintf(buffer, sizeof(buffer), fmt, args); va_end(args); const char *p buffer; while (*p) { if (*p 0x80) { // 英文字符走 8x16 点阵 DrawASCIIChar(cur_x, ctx-y, *p, ctx-fg_color, ctx-bg_color); cur_x 8; p; } else { // 中文字符双字节解析 LCD_ShowChinese(cur_x, ctx-y, p, ctx-fg_color); cur_x 16; p 2; } } }这段封装的核心改动是用vsnprintf把温度、电压、计数值这类运行时数据格式化进缓冲再统一走显示函数。这样界面代码里就不需要反复调LCD_ShowNum、LCD_ShowStr而是一口气输出“温度:25.3°C”这样的完整信息显示和改版成本都会低很多。紧接着可以做一个翻页和反色高亮状态。维护一个当前菜单索引按一下按键让索引加一同时记录旧位置把背景色恢复新位置用反色重绘void Menu_Refresh(uint8_t old_index, uint8_t new_index) { DrawChar16(10, 20 old_index * 20, Menu_Text[old_index], 0xFFFF, 0x0000); // 把反色区恢复为正常显示 DrawChar16(10, 20 new_index * 20, Menu_Text[new_index], 0x0000, 0xFFFF); }高亮通过前景色背景色互换实现。反色思路简单清晰速度也快比在 GUI 库层面做焦点管理节省大量时间。存储扩展方面如果要做成产品级界面16x16 点阵的几千个汉字会把 F103C8T6 的 64KB Flash 耗干。常见做法是给屏幕接一颗 SPI NOR Flash如 W25Q32字库固化成 Bin 文件烧到 Flash 里F103 启动时按偏移地址读取16MB 空间能容纳几十套字体。如果只显示固定几屏内容用代码生成工具把界面文本里出现的字符自动裁剪成小字库嵌入工程后整机资源占用会低很多。每次拿到一块新屏幕我的习惯是不管三七二十一先把ST7735_DrawPixel这几个核心函数跑通再去纠结字库和中文编码。基础调稳定了后续加 UI、加动画都只是在画点函数上的堆叠。尤其是取模参数和 MADCTL 这类配置一句话就能改变显示方向但排查时却容易消耗大半天。把这些问题提前理清后面整套流程几乎不会再卡。希望这篇笔记对你有帮助直接对照工程代码跑一遍中文显示收获比看十遍文档大得多。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?