ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

STM32F103驱动OLED显示汉字:从字模提取到混合显示实战

2026/9/4 6:36:15 拓冰建站 浏览量
STM32F103驱动OLED显示汉字:从字模提取到混合显示实战 简介本资源是一套基于STM32F103微控制器的OLED汉字显示完整实验工程面向嵌入式初学者与课程实践者解决汉字在单色OLED屏如SSD1306/SH1106驱动上可靠显示的核心问题涵盖硬件接口、点阵字模处理与底层驱动集成等关键环节。压缩包共78个文件含35个头文件.h定义OLED寄存器、汉字库索引及API接口、33个C源文件.c实现SPI/I2C通信、GB2312编码解析、字模查表与显存刷新等核心逻辑以及hex可执行文件、Keil工程配置.uvprojx/.uvoptx、启动代码.s和系统级支持文件整体仅334KB结构清晰、模块解耦便于逐层理解与调试。已有5865人学习下载提供从GPIO初始化、OLED底层驱动到汉字字符串渲染的一站式参考特别包含text.h/text.c汉字处理模块、keilkilll.bat一键清理脚本及README.TXT说明文档显著降低汉字显示开发门槛。1. 项目缘起从点阵到汉字OLED屏显的“第一课”刚拿到一块0.96寸的OLED屏点亮它、显示几个英文字符对于玩STM32的朋友来说可能半小时就搞定了。但当你兴致勃勃地想显示一行中文比如“你好世界”时大概率会卡住——屏幕上要么一片空白要么显示出一堆乱码。这个“OLED显示汉字”的实验可以说是嵌入式显示开发从入门到实用的一个关键分水岭。它不像点亮一个LED那样直观背后涉及字符编码、字模提取、驱动适配等一系列“脏活累活”但一旦打通你的设备就拥有了与用户进行更丰富信息交互的能力。我最初在STM32F103C8T6也就是常说的“蓝桥杯”或“最小系统板”上做这个实验时也踩了不少坑。网上教程很多但往往只给代码不讲原理或者字库工具版本老旧生成的数组格式对不上。这次我就结合最新的工具和常见的SSD1306驱动把整个流程掰开揉碎了讲清楚重点不仅是“怎么做”更是“为什么这么做”以及那些教程里不会写的“坑在哪里”。简单来说这个实验的核心目标是在STM32F103微控制器上驱动I2C或SPI接口的0.96寸OLED屏幕通常指SSD1306驱动芯片实现任意中文字符的稳定、清晰显示。它适用于所有需要本地显示中文菜单、状态提示、数据标签的嵌入式项目比如智能家居终端、工业仪表盘、小型手持设备等。2. 核心原理拆解汉字在单片机里是如何“画”出来的要显示汉字首先得明白单片机“眼里”的汉字是什么。它不像电脑有庞大的操作系统和字体文件支持。在资源有限的STM32F103通常只有几十KB RAM里汉字是以“位图”的形式存在的。2.1 字模汉字的“基因图谱”所谓字模就是把一个汉字看成一张由许多小点像素组成的黑白图片。每个点对应一个二进制位1表示这个点亮显示0表示这个点灭不显示。对于常见的16x16像素的汉字它就有16行每行16个点总共256个点需要256位也就是32个字节因为1字节8位来存储。例如一个“中”字的字模数据就是一个长度为32的字节数组。单片机显示时就是把这个数组里的数据按照顺序一个字节一个字节地“画”到OLED屏幕对应的像素位置上。这就是最基础的“取模显示”原理。2.2 编码与索引如何找到正确的“中”字有了字模数据下一个问题是怎么找到它。我们电脑上输入“中”用的是GB2312、GBK或者Unicode编码。在单片机领域为了节省空间和简化查找通常采用一种更直接的方式自定义索引。最常用的方法是只把你项目中需要用到的汉字提取出它们的字模然后按照你定义的顺序比如拼音首字母顺序存成一个大的数组这就是部分字库。同时你还需要维护一张“索引表”记录每个汉字在这个大字库数组中的起始位置。举个例子如果你的字库里只存了“中”、“国”、“芯”、“片”四个字你可以定义“中” - 索引0 字模数据位于数组第0~31字节“国” - 索引1 字模数据位于数组第32~63字节以此类推...在程序里当你需要显示“中国”时你就先查找“中”的索引是0然后从大字库的第0字节开始连续读取32字节发送给OLED绘制再查找“国”的索引是1从第32字节开始读32字节绘制。这种方式避免了在单片机内实现完整的编码转换效率极高。2.3 OLED驱动基础页寻址模式与数据写入0.96寸OLEDSSD1306的显存结构是理解显示的关键。它的显存通常被视为一个“页”式结构比如128x64的分辨率可以看作8页Page每页8行Row每页有128列Column。在显示英文字符通常是8x16像素时我们经常按“列”写入数据。但显示16x16的汉字时“页寻址模式”更为合适。在此模式下我们可以设置起始页地址和列地址然后连续写入数据这些数据会自动填充当前页的当前列并向下一个列移动。写入128字节后会自动换到下一行的起始列这取决于具体设置。显示一个16x16汉字可以理解为先显示它的上半部分前16字节对应页0和页1再显示它的下半部分后16字节对应页2和页3。驱动函数需要能灵活设置起始显示位置X坐标和页Y坐标。3. 实战准备工具链与工程搭建理论清楚了我们开始动手。这里以STM32F103C8T6核心板、I2C接口的0.96寸OLEDSCL-PB6, SDA-PB7、使用标准库或HAL库为例。使用Keil MDK或STM32CubeIDE均可。3.1 硬件连接与驱动层代码首先确保OLED的硬件连接正确这是所有工作的基础。OLED引脚STM32F103引脚功能VCC3.3V电源切勿接5VGNDGND地SCLPB6 (或其它I2C1_SCL引脚)时钟线SDAPB7 (或其它I2C1_SDA引脚)数据线注意市面上多数0.96寸OLED模块工作电压是3.3V直接接5V可能会烧毁。务必确认驱动层代码需要完成OLED的初始化、清屏、设置坐标、写命令、写数据等基本操作。网上有大量成熟的“OLED驱动代码”如江协科技、正点原子等提供的通常包含oled.c和oled.h。你需要做的是根据你的连接引脚I2C或SPI修改oled.c中的引脚初始化函数OLED_Init。确认I2C地址。SSD1306的I2C地址通常是0x78写地址或0x7A具体看模块背面电阻。驱动代码里会有类似#define OLED_ADDRESS 0x78的定义不对应则无法通信。将驱动文件加入你的工程并调用OLED_Init()进行初始化。一个常见的初始化后测试是调用OLED_ShowString(0, 0, Hello, 16);来显示英文字符串确保基础驱动是通的。3.2 字模提取选择趁手的工具这是显示汉字的核心准备工作。我们需要一个工具把需要的汉字转换成C语言数组格式的字模数据。这里强烈推荐“PCtoLCD2002”完美版或“PctoLCD2002_zimo”这类工具。它们比一些在线工具更稳定功能也更强大。操作步骤与关键配置打开软件选择“字符模式”。设置字模格式这是最容易出错的一步取模方式选择“逐列式”或“逐行式”。对于SSD1306“逐列式、顺向高位在前”是兼容性最好的选择。这意味着软件会从左到右取每一列的数据每一列从上到下组成一个字节高位在上。每行显示数据点16*16汉字这里通常填16表示每行/每列16个点。输出数制选择“十六进制”。自定义格式在“选项”中设置输出格式为C语言数组。通常格式为{0xXX, ...},。确保没有多余的分号或格式错误。输入汉字并生成在文字输入框输入你需要的汉字比如“温度湿度监测”点击“生成字模”。软件会生成对应的字节数组。验证字模可以将生成的数据复制到一个简单的测试程序中或利用软件自带的预览看看生成的图案是否正确。一个技巧是一个16x16汉字的字模数组长度一定是32。实操心得不同版本的PCtoLCD2002默认设置可能不同。生成数组后务必先取一个简单的汉字如“一”或“口”在OLED上测试。如果显示镜像、旋转或错位基本就是取模方式设置不对需要调整“取模走向”顺向/逆向列行式/行列式重新生成。3.3 字库数组与索引表的构建假设我们只需要显示“温”、“度”、“湿”、“监”、“测”这五个字。我们用PCtoLCD2002生成它们的字模。1. 创建字库数组 (myfont.c/.h)在myfont.c中我们创建一个二维数组或者一个包含多个一维数组的结构体来存储所有字模。// myfont.c #include “myfont.h” // “温” 的字模数据 (示例非真实数据) const unsigned char wen_16x16[] { 0x00,0x00,0x00,0x00,0x7F,0xFC,0x40,0x04, 0x5F,0xF4,0x41,0x04,0x41,0x04,0x5F,0xF4, 0x41,0x04,0x41,0x04,0x5F,0xF4,0x40,0x04, 0x7F,0xFC,0x00,0x00,0x00,0x00,0x00,0x00 }; // “度” 的字模数据 const unsigned char du_16x16[] { // ... 32个字节数据 }; // ... 其他汉字2. 创建索引表与查找函数在myfont.h中我们定义一个枚举或宏作为索引并声明一个查找函数。// myfont.h #ifndef __MYFONT_H #define __MYFONT_H #include “stm32f10x.h” // 汉字索引定义 typedef enum { FONT_INDEX_WEN 0, // 温 FONT_INDEX_DU, // 度 FONT_INDEX_SHI, // 湿 FONT_INDEX_JIAN, // 监 FONT_INDEX_CE // 测 } FONT_INDEX_t; // 根据汉字索引返回对应的字模数组首地址 const unsigned char *Get_Font_Data(FONT_INDEX_t index); #endif在myfont.c中实现这个函数const unsigned char *Get_Font_Data(FONT_INDEX_t index) { switch(index) { case FONT_INDEX_WEN: return wen_16x16; case FONT_INDEX_DU: return du_16x16; case FONT_INDEX_SHI: return shi_16x16; case FONT_INDEX_JIAN: return jian_16x16; case FONT_INDEX_CE: return ce_16x16; default: return NULL; } }这种方式虽然每次增加新字都要修改代码但对于固定界面的项目来说是最简单、最节省内存的方法。如果你的汉字需要动态变化且数量多可以考虑将索引与GBK编码关联并存储完整的字库文件到外部Flash这里暂不展开。4. 核心实现汉字显示函数的设计与优化有了字库和驱动接下来就是编写汉字显示函数。这个函数需要接收显示位置x, y和汉字索引然后将对应的32字节字模数据正确地“画”到OLED上。4.1 基础显示函数编写一个最直接的实现如下假设OLED驱动提供了设置坐标OLED_Set_Pos和写数据OLED_Write_Data的函数/** * brief 在指定位置显示一个16x16汉字 * param x: 列起始位置 (0~127) * param y: 页起始位置 (0~7) 一个汉字占2页16像素高 * param index: 汉字索引 * retval None */ void OLED_Show_Chinese(uint8_t x, uint8_t y, FONT_INDEX_t index) { const unsigned char *font_data Get_Font_Data(index); if(font_data NULL) return; uint8_t i, j; // 汉字上半部分 (8行对应页y) OLED_Set_Pos(x, y); for(j0; j16; j) { // 遍历16列 OLED_Write_Data(font_data[j]); // 写入上半部分一列的数据一个字节 } // 汉字下半部分 (8行对应页y1) OLED_Set_Pos(x, y1); for(j0; j16; j) { OLED_Write_Data(font_data[j16]); // 写入下半部分一列的数据 } }这个函数的工作原理是根据索引获取字模数组指针。设置光标到起始位置(x, y)这里y是页地址。一个16像素高的汉字占据两页因为一页8行。循环16次每次写入一个字节对应一列的8个点即上半部分。将页地址y加1移动到下一页再循环16次写入字模数组的后16个字节下半部分。4.2 处理显示位置边界与优化上面的基础函数存在一些问题我们需要优化问题1跨页与列地址重置在页寻址模式下当我们写完一页的128列后列地址会自动回到0但页地址会加1吗这取决于芯片的“页地址递增模式”设置。为了通用性最好在显示每个汉字时都显式地设置两次坐标上半页和下半页就像上面代码做的那样这是最稳妥的。问题2显示位置超出屏幕边界如果x列设置过大比如x120一个16宽的汉字需要显示到x136这超出了128列的范围。我们需要在函数内部进行裁剪或报错。优化后的函数示例void OLED_Show_Chinese(uint8_t x, uint8_t y, FONT_INDEX_t index) { const unsigned char *font_data Get_Font_Data(index); if(font_data NULL || y 6) return; // y不能大于6因为汉字占2页 // 简单边界检查如果汉字显示区域超出屏幕右边界则不显示 if(x 112) return; // 128 - 16 112 uint8_t j; // 显示上半部分前16字节 OLED_Set_Pos(x, y); for(j0; j16; j) { OLED_Write_Data(font_data[j]); } // 显示下半部分后16字节 OLED_Set_Pos(x, y1); for(j0; j16; j) { OLED_Write_Data(font_data[j16]); } }4.3 显示字符串函数组合汉字与ASCII字符实际项目中我们经常需要显示混合字符串如“温度25.6℃”。这就需要我们能同时处理ASCII字符8x16或6x8和汉字16x16。思路是遍历字符串判断每个字符的类型。如果是ASCII字符 0x80调用原有的OLED_ShowChar函数并前进一个字符宽度如8列。如果是汉字通常用两个连续的、大于0xA0的字节表示GBK编码但我们用的是索引所以需要另一种策略。我们可以约定一个特殊的“转义序列”或直接使用索引号。这里给出一个更实用的思路我们创建一个“显示内容”的缓冲区或结构体直接指定每个位置要显示的“元素”是ASCII还是汉字索引。但对于简单的固定字符串可以这样实现/** * brief 显示混合字符串需提前将汉字部分转换为索引并存入数组 * param x, y: 起始坐标 * param ascii_str: ASCII字符串 * param chi_index_arr: 汉字索引数组 * param chi_count: 汉字个数 * retval None */ void OLED_Show_Mixed_String(uint8_t x, uint8_t y, char *ascii_str, FONT_INDEX_t *chi_index_arr, uint8_t chi_count) { uint8_t x_offset x; uint8_t chi_idx 0; // 假设ASCII字符宽度为8汉字宽度为16 while(*ascii_str) { // 这里可以加入一个特殊标记来判断当前位置是ASCII还是汉字 // 例如在ascii_str中用‘\xFF’作为占位符表示后面跟一个汉字索引 if(*ascii_str ‘\xFF’) { ascii_str; // 跳过占位符 if(chi_idx chi_count) { OLED_Show_Chinese(x_offset, y, chi_index_arr[chi_idx]); chi_idx; x_offset 16; } } else { OLED_ShowChar(x_offset, y, *ascii_str, 16); // 显示ASCII字号16 x_offset 8; } ascii_str; } }在主程序中你可以这样调用FONT_INDEX_t my_chi[] {FONT_INDEX_WEN, FONT_INDEX_DU}; OLED_Show_Mixed_String(0, 0, “\xFF\xFF: 25.6C”, my_chi, 2);这个例子中字符串里的两个\xFF占位符会被替换成my_chi数组里的“温”、“度”两个汉字索引。实操心得混合显示是项目中最常见的需求。上面的方法虽然有点“土”但非常直观且易于调试。更高级的做法是设计一个完整的显示引擎解析包含中文的UTF-8或GBK字符串并动态从字库中查找。但对于STM32F103这类资源紧张的单片机“空间换时间”和“预先规划”往往是更优解。在项目初期就确定好所有需要显示的汉字制作一个专属的小字库是最稳定高效的方案。5. 深度优化与常见问题排查功能实现了但要做得稳定、高效、显示效果好还需要考虑以下问题。5.1 内存优化字库到底放哪里对于STM32F103字库存储位置直接影响启动速度和内存占用。默认Flashconst数组默认存储在内部Flash中。这是最常用的方式不占用RAM。但F103的Flash写入寿命有限频繁擦写如更新字库不合适。读取速度完全满足显示要求。内置SRAM如果字库很小且需要极速读取通常没必要可以复制到SRAM。但这会占用宝贵的运行内存。外置SPI Flash如果汉字数量庞大比如整个GB2312字库约200KB就必须存放在外置Flash中。此时需要编写SPI Flash的驱动并实现从Flash中按地址读取字模数据的函数。这会增加代码复杂度和读取时间微秒级但仍在可接受范围。建议对于大多数项目将几十到几百个汉字的字库以const数组形式放在内部Flash是最佳选择。5.2 显示效果优化解决字体发虚与彩边问题这个问题在网络上如“mactype怎么配置能解决oled屏幕字体彩边和发虚的问题”很热但那是针对PC端OLED屏幕的字体渲染。在单片机OLED上“彩边”和“发虚”通常不是软件问题而是硬件和物理特性。彩边尤其是白色文字边缘有红/蓝边这是OLED像素排列Pentile排列的固有特性。每个像素点由红、绿、蓝子像素组成但并非独立。显示白色时需要相邻像素共同点亮在边缘处由于子像素共用可能产生色偏。在单片机级别我们无法通过算法完全消除。只能尝试避免单像素细线设计UI时尽量使用至少2像素宽的线条或字体。使用反色显示在深色背景上显示亮色文字比在亮背景上显示深色文字彩边感更弱。微调对比度通过OLED_Set_Contrast函数降低对比度有时能减轻视觉上的不适感。发虚字体边缘模糊、不锐利这主要与取模质量和显示驱动有关。确保取模正确再次检查PCtoLCD2002的设置。“阴码”、“逐列式”、“顺向”是最通用的。用“口”、“国”这类有边框的字测试看四个角是否清晰。检查驱动时序I2C通信速度过快可能导致数据不稳定。尝试在OLED_Write_Byte函数中的I2C_Stop后加一个微秒级的延时delay_us(1)。电源干扰确保给OLED模块的3.3V电源干净、稳定。可以在VCC和GND之间并联一个10uF和0.1uF的电容。5.3 典型问题排查流程当汉字显示不出来或显示异常时可以按以下步骤排查第一步确认基础通信用万用表测量OLED的VCC是否为稳定的3.3V。用逻辑分析仪或示波器抓取I2C的SCL和SDA波形看是否有起始信号、地址应答、数据信号。这是最直接的硬件排查手段。第二步确认英文字符显示正常先运行一个仅显示英文“Hello World”的程序。如果不正常问题在OLED基础驱动初始化序列、I2C地址、引脚配置。第三步确认字模数据正确将生成的字模数组用printf通过串口打印出来与PCtoLCD2002软件预览的二进制点阵对比。或者写一个简单的测试函数在OLED上固定位置反复绘制同一个字模数组看图案是否稳定。第四步单步调试显示函数在OLED_Show_Chinese函数中设置断点观察获取到的font_data指针是否有效x, y坐标值是否正确传入。跟踪进入OLED_Write_Data确认写入的数据是否与字模数组中的数据一致。第五步检查屏幕坐标系统确认你的OLED_Set_Pos(x, y)函数中x代表列0~127y代表页0~7。一个16像素高的汉字需要占用y和y1两页。如果汉字显示为上下颠倒的两半可能是取模的“顺向/逆向”设置错误或者显示函数里上下半部分的数据顺序写反了。6. 项目进阶从显示到GUI的思考当你能稳定显示单个汉字和字符串后就可以尝试更复杂的应用了。6.1 显示图片与图标显示图片的原理和汉字一模一样只是图片的“字模”更大。你可以用Img2Lcd等工具将单色BMP图片转换为C数组。显示函数需要能处理更大的数据块并正确分割到多个“页”。例如一张32x32的图标需要占用4页32/84高度宽度为32列。你需要写一个四重循环或者更通用的OLED_DrawBMP函数。6.2 简单菜单系统的实现结合按键就可以实现中文菜单。核心是维护一个“当前菜单项索引”和对应的“显示内容数组”。例如typedef struct { FONT_INDEX_t icon_index; // 图标索引 FONT_INDEX_t text_index[2]; // 菜单文字索引可能两个汉字 void (*func)(void); // 菜单项对应的功能函数指针 } MenuItem_t; MenuItem_t main_menu[] { {ICON_HOME, {FONT_INDEX_SHE, FONT_INDEX_BEI}, menu_setting}, {ICON_FILE, {FONT_INDEX_WEN, FONT_INDEX_DU}, menu_temp}, // ... };通过上下键改变索引左键进入右键返回再调用OLED_Show_Chinese刷新显示对应的菜单文字和图标。6.3 移植到HAL库与其它平台很多朋友在从标准库转向HAL库时会遇到驱动移植问题。江协科技的OLED驱动代码有标准库和HAL库两个版本。移植的关键在于替换底层通信函数找到原来标准库中OLED_WR_Byte写一个字节函数里关于GPIO_WriteBit或软件模拟I2C的部分替换为HAL库的HAL_I2C_Master_Transmit函数调用。调整延时HAL库的HAL_Delay是毫秒级而OLED初始化序列可能需要微秒级延时。你可能需要保留原有的delay_us函数基于SysTick实现或者使用HAL库的HAL_Delay并确认时序是否满足。检查引脚宏定义确保oled.h中关于SCL、SDA引脚的宏定义与HAL库中MX_I2C1_Init里配置的引脚一致。至于移植到ESP8266/ESP32如ESP-01S OLED模块或Linux驱动原理完全相通只是底层通信从STM32的硬件I2C变成了ESP的Wire库或Linux的I2C设备文件/dev/i2c-N。字模数据的组织和显示逻辑可以完全复用。走到这一步你已经不仅仅是在“显示汉字”而是在构建一个嵌入式设备的人机交互界面基础。这个实验积累的经验——从字模处理、内存管理到底层驱动调试——是后续进行更复杂图形显示、甚至移植轻量级GUI如LVGL、uGFX的宝贵基石。本文还有配套的精品资源点击获取