ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

0.96寸OLED显示汉字全攻略:PCtoLCD2002取模与STM32 HAL库驱动实战

0.96寸OLED显示汉字全攻略:PCtoLCD2002取模与STM32 HAL库驱动实战 很多人第一次拿到0.96寸OLED第一件事就是点亮它、显示几行英文和数字。这个不难驱动库里自带的ASCII字符集足够你玩半天。但等你想把项目名、菜单项、提示语换成汉字的时候屏幕上一排排的乱码和方块马上就能让你冷静下来。原因很简单OLED本身不认识汉字它只认像素点阵而汉字点阵并没有内置在SSD1306里得靠你自己把它“翻译”成一串数组这个翻译过程就是取模。这篇文章要解决的就是这个环节——用PCtoLCD2002把汉字转换成0.96寸OLED能直接显示的取模数据再配合一套HAL库的驱动代码让你从取模配置到屏幕显示一次跑通。我默认你手头是一块常见的128x64分辨率、SSD1306控制器的0.96寸OLED接口是I2CMCU这边用STM32F103C8T6和HAL库硬件I2C和软件I2C我们都会提到。如果你用的是其他MCU平台也没关系取模部分是通用的驱动代码的逻辑也完全照搬。1. 先搞懂你的屏幕0.96寸OLED到底怎么“画”出来的1.1 SSD1306控制器的基本工作方式0.96寸OLED的屏幕分辨率是128x64意思是横向128列、纵向64行一共128*648192个像素点。每个像素点只有亮和灭两种状态就像一张棋盘你要显示什么内容本质上就是把对应位置的“棋子”放上去——点亮或不点亮。这块屏幕的所有像素点都归SSD1306这颗驱动芯片管。它内部有一块1KB的显存叫做GDDRAM或者说是一块“画布”。你往画布上写1对应的像素就亮写0就灭。注意这块画布的排列方式不是我们习惯的从左到右、从上到下的连续二维数组而是被分成了8个“页”Page每一页8个像素点高。64行像素正好分成64/88页也就是Page0到Page7。每一页里一个字节控制8个像素点这个字节的每一位对应屏幕上一列里的8个纵向像素。一个字节有8位一页横向有128列所以一页就是128字节。这也是为什么OLED驱动里最常见的操作是“先选页再选列然后连续写数据”——你是在按页、按列填这块画布而不是按行填。1.2 页、列与坐标为什么16x16汉字要占“两页”搞清楚页的概念之后汉字的显示逻辑就清晰了。一个16x16汉字也就是宽16像素、高16像素。屏幕每页只有8像素高所以一个16像素高的汉字天然就要被拆成上下两段上半段放在某一页下半段放在下一页。这就是为什么PCtoLCD2002取模时一个16x16汉字的32字节数据通常是按“第一行8个点一个字节、第二行再一个字节……一直数到第16行”来排布的也就是前16个字节是上半部分后16个字节是下半部分。你在代码里显示汉字的时候程序逻辑往往就是先定位到第n页把前16字节写进去再定位到第n1页把后16字节写进去。理解这一点之后你以后再换24x24、32x32的汉字或者图形计算思路完全一样高除以8就知道占多少页。这个换算关系是所有OLED显示汉字的基础。1.3 I2C接口与常见接线0.96寸OLED有I2C和SPI两种接口版本引脚数量不太一样。I2C版一般只引出4个引脚VCC、GND、SCL、SDA部分模块还会带RES和DC但I2C模式下这两个脚一般可以悬空或者接高电平。在STM32F103C8T6上我的习惯是把SCL接到PB8、SDA接到PB9也就是I2C1的默认引脚。如果你用的是硬件I2C在CubeMX里把I2C1配成Standard Mode、100kHz或者400kHz都行。如果用的是软件I2C模拟那这两个GPIO随便选只要代码里对应改一下引脚号即可。这里有个特别坑的细节I2C从机地址。SSD1306的7位I2C地址默认是0x3C但很多模块的地址引脚SA0状态不同实际地址可能是0x3D。更坑的是不同库和不同HAL代码对地址的写法不统一有的直接写0x3C有的写0x78含读写位的8位地址。我的建议是先按0x3C试屏幕没反应就把地址改成0x3D或者0x78再试这几个取值对初学者来说属于“必踩一脚”的经典坑。2. 字模是怎么算出来的PCtoLCD2002核心配置2.1 从像素到字节16x16汉字为什么是32字节取模工具做的事本质就是把一个字在屏幕上显示出来的像素点转成一串二进制数字。汉字以16x16点阵为例横着16个点、竖着16个点。因为OLED是按字节扫描的8个点为一个字节所以16个点宽一行就需要2个字节。16行乘以2字节一共32字节。这就是“32字节”这个数字的来源。放到PCtoLCD2002的预览界面里看更直观你在左侧输入一个汉字右侧就是它的像素放大图黑色点就是要点亮的像素。取模就是把这张像素图按你设定的规则从上到下、从左到右扫一遍用1和0记录下每个位置“亮”还是“灭”最后再按每8个点一组拼成一个字节。这里有一个初学者最容易误解的点你以为取模工具扫出来的顺序和SSD1306画布自动匹配。实际上不是你必须保证取模工具的扫描方向和你在代码里填充GDDRAM的方向一致否则就会出现镜像、颠倒、花屏。2.2 四个关键选项阴码/阳码、顺向/逆向、逐行/逐列、每行点数打开PCtoLCD2002你会看到一堆选项新手很容易懵。其实真正影响显示效果的核心选项就四个我逐个讲明白。第一点阵格式选“阴码”。对SSD1306这种屏幕来说写入1对应像素点亮所以要选阴码也就是“有笔画的地方是1没笔画的地方是0”。如果选了阳码结果正好反过来汉字会变成黑底白字的反显效果很多时候也是能用的但第一次尝试不要自己加戏就用阴码。第二取模走向选“顺向”。这个选项决定你从像素图的哪个方向开始扫描。我的经验是SSD1306配合“顺向”基本能直接显示正常但如果你发现汉字整体左右镜像了不用改代码回到PCtoLCD2002把“顺向”切换成“逆向”重新取模即可。这就好比你把一张透明的字帖翻了个面字就反了。第三取模方式选“逐行式”。这个选项决定一个字节里的8个点是按行排列还是按列排列。对于OLED我强烈建议选逐行式因为SSD1306的GDDRAM本身一列就是8个纵向像素点你按“行”逐行扫描出来的字节排列正好和屏幕的页结构一一对应代码写起来最顺手。选“逐列式”也有对应的玩法但发送数据时的逻辑要跟着变初学者没必要在这里自己为难自己。第四每行显示点数选16。这个选项的意思是生成的字模每“行”数据按16个点来切割。因为16x16汉字一行正好16个点宽所以选16。如果你做的是8x16的ASCII字符这里就选8如果是32x32的图片这里就选32。它决定了生成数组分组的视觉格式对数据内容没有本质影响但选对了数组检查起来舒服很多。2.3 一套稳定配置怎么设置才能让OLED和代码正好对上针对0.96寸OLED我长期使用且已验证无数次的PCtoLCD2002配置组合如下字体宋体16号也就是16x16点阵字宽/字高16、16点阵格式阴码取模走向顺向取模方式逐行式每行显示点数16自定义格式勾选“C51格式”去掉其他不必要的前后缀这套配置产出的数组就是标准的{0x00,0x00,...}形式直接粘贴到C代码里就能用。如果你的屏幕显示出来镜像先把“顺向”改成“逆向”如果显示出来是上下颠倒检查一下你代码里定位页的顺序如果显示出来是一堆杂乱的点阵十有八九是你“逐行式”和“逐列式”搞混了或者代码里按页填数据的顺序和取模顺序不一致。记住这套组合拳以后不管取多少字都不会出大问题。这也是为什么我建议初学阶段不要乱调参数先原样照抄这套配置跑通一遍再考虑个性化。3. 手把手取模实操从输入汉字到生成C数组3.1 打开软件、输入汉字、调整字体的完整流程PCtoLCD2002这个软件界面比较复古但功能打磨得很成熟。第一步打开软件后在左侧输入区域输入你要显示的汉字。一次可以输入多个软件会逐个取模但我建议第一次操作一次只输入一个“中”字方便核对数据。输入完成后点击上方菜单里的“选项”进入设置界面。先把字体设置成宋体大小选择16。注意这里有个隐蔽的问题如果你用系统字体选“宋体”同一个16点阵在不同电脑上的渲染结果可能略有差异但一般不影响使用。如果你需要标准国标点阵可以在“选项”里选用内置的HZK16字库文件。这个方案生成的字模更接近传统汉字库效果但我个人在实际项目里更喜欢系统字体因为它能直接看到预览效果所见即所得。字体设置好之后右侧预览窗口会显示放大后的像素点阵。这时候你要检查一下这个字有没有超出16x16的方框范围。如果笔画明显顶格或者超出后面取出来的模就是残缺的。3.2 自定义输出格式生成可以直接粘贴的数组接下来是输出格式的设置。在“选项”菜单里找到“自定义格式”这地方决定生成的数据长什么样非常关键。推荐设置格式为前缀空后缀用,或者,加换行每个字节用0x%02X输出。最常用的格式是{0x%02X},生成效果就是{0x10,0x08,...},这种标准C数组风格复制到代码里直接用。设置里还有个“索引”选项如果你勾选了生成的数据前面会出现{0}这种占位编号一般不用。我习惯把所有额外信息都去掉只保留纯数据这样粘贴进C文件最干净。设置好之后回到主界面点击“生成字模”或者“取模”按钮下方就会输出一行行十六进制数据。以“中”字为例取模出来是32个字节正好排成两行也行一行排开也行因为每行显示点数设成16所以通常是前16个字节一行、后16个字节一行方便你对照检查。3.3 取模过程中最容易翻车的三个细节第一个细节是字体带锯齿。如果你在系统字体里选的不是16号而是其他带抗锯齿效果的字体取出来的模边缘会有大量低频杂点显示在OLED上就是“毛刺”。解决办法是在字体设置里尽量选择不带抗锯齿的宋体或者关闭平滑效果。第二个细节是输入法状态。某些输入法在输入汉字时会自动带上全角空格或者拼音候选状态下的特殊字符你看着是一个汉字实际可能混入了不可见字符取模出来的数组长度就不对。我的习惯是在输入区只打纯汉字生成前数一眼“字高”和“字宽”是不是16输出字节数是不是32的整数倍。第三个细节是标点符号和数字。如果你要显示“年月日”这种中间夹着数字和汉字的字符串中文取模和ASCII取模是两套流程。ASCII字符可以用8x16点阵直接查库函数里自带的字符表不占用你的字模空间。但数字如果和汉字混排尺寸比例要自己调试。这个没有捷径只能多试几次。4. HAL库工程里的完整OLED驱动代码4.1 工程配置I2C引脚、时钟与地址选择在STM32CubeMX里新建一个STM32F103C8T6工程把SDA配到PB9SCL配到PB8选择I2C1Mode选I2CSpeed Mode选Standard Mode100kHz或者Fast Mode400kHz都行。400kHz在短线上通常没问题但如果你的杜邦线很长我建议先跑100kHz稳一点再提速。生成代码之前记一下I2C1的从机地址宏。在main.h或者i2c.h里加一行#define OLED_I2C_ADDR 0x3C // 根据你模块实际地址调整备用0x3D或0x78这部分要注意HAL库的HAL_I2C_Mem_Write函数第2个参数DevAddress用的是7位地址而很多旧示例代码用的是8位地址0x78。如果你用的是HAL库写0x3C通常是对的如果你在某份代码里看到0x78那不是错了而是那套代码已经帮你把移位处理好了。这两者混用是移植OLED代码最常见的错误来源。4.2 底层函数写命令、写数据、设置坐标先看底层发送函数。SSD1306通过一字节的控制前缀来区分后续字节是命令还是数据所以封装两个函数void OLED_WrCmd(uint8_t cmd) { HAL_I2C_Mem_Write(hi2c1, OLED_I2C_ADDR, 0x00, I2C_MEMADD_SIZE_8BIT, cmd, 1, 100); } void OLED_WrDat(uint8_t dat) { HAL_I2C_Mem_Write(hi2c1, OLED_I2C_ADDR, 0x40, I2C_MEMADD_SIZE_8BIT, dat, 1, 100); }这里核心就是Mem_Write里的第三个参数0x00代表写命令0x40代表写数据。有人会把控制字节一起打包成2字节的数组发送效果一样。接下来是设置坐标。SSD1306页地址模式下要先选页再选列列地址是分两次发送的低位和高位void OLED_SetPos(uint8_t x, uint8_t page) { OLED_WrCmd(0xB0 page); // 页地址0~7 OLED_WrCmd(x 0x0F); // 列地址低4位 OLED_WrCmd(0x10 | (x 4)); // 列地址高4位 }这里的page是页号不是像素行号。如果你想在屏幕上第16行开始显示因为每页8行所以page应该是2。x是列号取值范围0到127。4.3 显示汉字把字模数据填进GDDRAM的完整函数下面这个是核心函数。它接收起始列x、起始页page、以及字模数组的指针void OLED_ShowChinese(uint8_t x, uint8_t page, const uint8_t *data) { uint8_t i; OLED_SetPos(x, page); for (i 0; i 16; i) // 写上半部分16字节 { OLED_WrDat(data[i]); } OLED_SetPos(x, page 1); for (i 16; i 32; i) // 写下半部分16字节 { OLED_WrDat(data[i]); } }这个函数之所以能这么简单完全依赖前面选定的“逐行式”取模方式数组前16字节是汉字上半页的16列点阵后16字节是下半页的。如果你在PCtoLCD2002里换成了“逐列式”这里的发送逻辑就得整个反过来变成先发上半页第1字节、下半页第1字节、上半页第2字节……那种循环方式。注意位置和边界的组合如果汉字放在x120这个位置它实际要占16列最后一列会超出屏幕边界超出去的部分会被SSD1306丢弃显示出来就是半个字。所以x最大不要超过128-16112。4.4 一个完整的最小测试工程示例把上面这些函数组合起来再加一个清屏函数和初始化函数就能组成最小工程。初始化序列网上到处都是我贴一段稳定版本void OLED_Init(void) { HAL_Delay(100); OLED_WrCmd(0xAE); // 关闭显示 OLED_WrCmd(0x20); // 设置内存寻址模式 OLED_WrCmd(0x02); // 页寻址模式 OLED_WrCmd(0xB0); // 设置起始页 OLED_WrCmd(0xC8); // 从上到下扫描 OLED_WrCmd(0x00); // 列地址低四位 OLED_WrCmd(0x10); // 列地址高四位 OLED_WrCmd(0x40); // 起始行 OLED_WrCmd(0x81); // 对比度设置 OLED_WrCmd(0x7F); OLED_WrCmd(0xA1); // 段重映射 OLED_WrCmd(0xA6); // 正常显示 OLED_WrCmd(0xA8); // 多路复用比 OLED_WrCmd(0x3F); OLED_WrCmd(0xD3); // 显示偏移 OLED_WrCmd(0x00); OLED_WrCmd(0xD5); // 时钟分频 OLED_WrCmd(0x80); OLED_WrCmd(0xD9); // 预充电周期 OLED_WrCmd(0xF1); OLED_WrCmd(0xDA); // COM硬件配置 OLED_WrCmd(0x12); OLED_WrCmd(0xDB); // VCOMH OLED_WrCmd(0x40); OLED_WrCmd(0x8D); // 电荷泵 OLED_WrCmd(0x14); OLED_WrCmd(0xAF); // 开启显示 }这段初始化和网上流传的版本差不太多只是我删掉了一些对0.96寸屏幕来说可有可无的指令。如果你是从别的工程移植过来的初始化序列只要没乱改通常也能点亮。测试代码只需要三行OLED_Init(); OLED_Clear(); OLED_ShowChinese(0, 0, HZ_ZHONG); // HZ_ZHONG 是“中”字的字模数组名main函数的while(1)里什么都不用做。屏幕左上角应该出现一个完整的16x16“中”字。5. 常见问题与排查实录5.1 屏幕全黑或无反应全黑是最常见也最让人崩溃的问题。排查顺序我建议按这个来先测供电0.96寸OLED模块供电3.3V和5V都行但VCC和GND接反会直接烧模块所以第一步就是确认引脚没接错。第二步确认I2C地址0x3C和0x3D来回换着试。第三步确认SCL和SDA有没有接反。这里要提一个容易被忽略的情况硬件I2C有时候初始化有问题HAL库的HAL_I2C_Mem_Write会一直返回超时但代码没有报错机制看起来就是屏幕全黑。这时候你在OLED_WrCmd里加一个返回值判断打印错误码或者干脆换软件I2C模拟。我本身并不排斥硬件I2C但在STM32F103上它偶尔会有Busy位卡死的小毛病尤其是在调试器反复复位的场景下。如果你遇到这个情况最简单的办法是给I2C外设加一个DeInit再Init或者直接改成软件I2C。5.2 字形镜像、倒置、花屏字形左右镜像几乎可以肯定是“顺向”和“逆向”的问题回到PCtoLCD2002切一下重新取模就行。注意取模方向同时会影响高低位的排列有时候你切换“顺向”之后不仅字正了连字节的位序也正了。字形上下倒置通常是初始化序列里扫描方向和扫描起始行设置不匹配。修改0xC8为0xC0试试或者查一下0x40起始行指令是否和你的COM输出方向一致。不过多数情况下0.96寸OLED用了0xC8A1这套组合就是正确的你最好先检查页定位逻辑而不是急着改初始化。花屏这种问题最头疼因为成因太多。最常见的一种是你用的是逐列式取模代码却按逐行式发送或者反过来。这类问题只能对比字模数据和实际点阵来排查没有捷径。5.3 显示位置错乱与上下半字分离显示出来的汉字上半部分在屏幕顶部、下半部分在屏幕中间中间隔着一行空白或者上下两部分错开。这种问题的根源是上下两页的起始列没对齐。看OLED_ShowChinese函数第一次OLED_SetPos(x, page)和第二次OLED_SetPos(x, page1)两次的x必须是同一个值而且page1的范围不能超过7。还有一个容易踩的坑是有些人在不同函数之间共享了一个全局的当前坐标变量显示完一个汉字后坐标自动加16但页面坐标没有跟着更新导致下一个汉字显示位置错乱。我的建议是坐标参数全部显式传递不要用隐式全局状态排查起来省力很多。5.4 批量点不亮与供电问题如果你是一次点亮十块屏幕发现有部分屏幕不亮或者亮度明显偏低大概率是供电问题而不是代码问题。SSD1306的电荷泵需要一定的电流余量几块屏堆在一起稳压模块输出能力不够就出现“批量点不亮”的现象。解决思路很简单单独给屏幕供电或者加大电源容量。另外I2C总线上如果挂了多块OLED地址又相同通信会冲突需要给每块屏幕配不同的SA0地址或者在硬件上做I2C地址扩展。这个属于工程化的问题做样品阶段很多教程不会提但批量制作的时候几乎必现。6. 还能怎么玩图片、动画和更多扩展6.1 用PCtoLCD2002给图片取模PCtoLCD2002不仅能给汉字取模也能给图片取模。操作方式是准备一张BMP格式的图片大小建议控制在128x64以内然后在PCtoLCD2002里选择导入图片软件会把图片转换成像素点阵。取模参数和汉字一样依然是阴码、顺向、逐行式这样生成出来的数组可以直接用OLED的整屏填充函数刷上去。显示图片的重点是取模前对图片做预处理。0.96寸OLED是单色屏所以图片在导入前最好先在画图软件里转成黑白两色不然取模出来会有大面积的噪声点。我的习惯是用Photoshop或者画图把图片改成128x64像素然后灰度模式转位图再保存成BMP。纯黑背景、白色前景的图片显示效果最好。6.2 多帧动画与动态显示既然能做到整屏图片显示动画的思路就很顺了。把动画拆成多帧图片每一帧单独取模生成多个数组然后在主循环里按一定的时间间隔切换显示。比如做一个简单的移动方块动画你可以准备4帧图片每帧里方块位置不同。代码逻辑就是每200ms切换一帧用OLED_ShowImage整屏刷新。这里的坑在于刷新速度SSD1306的I2C带宽有限如果每帧128x64的满屏数据在400kHz I2C下大约需要10多毫秒动画帧率不用期望太高做简单UI动效完全够用。还有一种常见做法是只更新局部区域比如一个16x16的图标移动你只需要重绘它经过的那一小块区域不刷新整屏这样动效流畅度会好很多。PCtoLCD2002取模多帧图片的工作量是最烦人的部分建议写个小脚本批量处理或者直接在代码里偏移数组指针实现简单动画。6.3 字库整合与更高分辨率屏的迁移思路当你要显示大量汉字时一个一个取模显然不现实。这时候的思路是做一个完整的字库文件把常用汉字按GB2312编码顺序取模生成一个数组索引文件。显示函数先查拼音或汉字内码再定位到对应字模数据这其实就是迷你版字库引擎。迁移到1.3寸、1.54寸等更大尺寸OLED时核心原理完全没变。分辨率高了比如128x64变成128x128页数从8页变成16页坐标范围扩大其他都一样。取模工具、取模配置、发送逻辑完全可以复用。如果你一开始就把驱动函数写得足够抽象换屏幕时只需要改分辨率和初始化参数代码框架几乎不动。最后分享一个我自己的习惯每次新建OLED工程我都会先做一个自检函数在屏幕上依次显示全0、全1、横线、竖线、棋盘格确认基础显示正常之后再开始跑汉字和图片。这个自检过程看起来多花一分钟但能帮你把“取模问题”和“硬件/驱动问题”快速分开省下的排查时间远不止一分钟。玩OLED显示这块耐心比技巧重要配置比代码重要方向比速度重要——希望你也能一次点亮。
返回列表