ARTICLE DETAIL

资讯详情

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

0.96寸OLED显示汉字乱码?PCtoLCD2002取模与SSD1306驱动详解

0.96寸OLED显示汉字乱码?PCtoLCD2002取模与SSD1306驱动详解 最近好几个做小项目的朋友问我同一个问题0.96寸OLED显示英文、数字都挺顺怎么一到汉字屏幕就花成一团要么上下两截错位要么文字被揉碎成斜向条纹要么干脆整屏不亮。我把他们发来的字模文件打开一看问题十有八九都出在PCtoLCD2002的取模设置上。这期就把这件事从头到尾讲透先用一张图讲清楚SSD1306的显存结构再说PCtoLCD2002到底怎么设置才能导出适合OLED的字模然后给一套软件I2C驱动和显示汉字的完整代码。代码基于STM32 HAL库但思路完全通用你在什么平台上都能照搬。先说结论0.96寸OLED显示汉字用到的字模格式跟传统TFT屏、点阵屏用的逐行式字模不一样。一个字选错取模方向后面全乱。1. 取模方式不对OLED汉字必然乱码先搞清楚SSD1306的显存布局1.1 显存不是一行一行排列的而是8页×128列0.96寸OLED分辨率是128×64内置控制器SSD1306板上有一块1KB的GDDRAM对应屏幕上128×64个像素点。这听上去很常规但SSD1306在页地址模式下的组织方式和常见的LCD屏完全不同SSD1306把64行像素拆成了8个Page页每个Page覆盖连续的8行。1字节数据不再代表某个像素而代表某一列上垂直方向的8个点。所以你可以把显存想象成一张便签本8页横带每页128列。我们发送一个字节到某个Page的某个Column位置实际控制的是这一列从上往下8个点各亮不亮。字节的最低位(bit0)对应最上面的那根扫描线最高位(bit7)对应最下面的那根。关键命令是0xB0 page指定页地址0~7分别对应行0~7、8~15……56~63列地址分两次设置先写低4位再写高4位之后每写一个字节列地址自动加1这个自动加1特性让页地址模式非常适合逐列写入图形数据——你只需要设置好起始页和起始列一口气发一串字节进去就画出了一条横向的8像素高、指定宽度的横带。1.2 为什么逐行式字模放到OLED上会撕裂普通TFT屏、LCD12864屏显示汉字时习惯用的是逐行式取模。16×16汉字拆成16行每行16bit正好2字节整个字32字节数据排列是第1行的2字节、第2行的2字节……一直到第16行。如果把这种数据直接喂给SSD1306问题就出现了页地址模式下你写进Page 0的第一个字节不是第1行的左8个像素而是第0列从上到下8个像素。你要写16行它却按8行为单位切页等于把原来的行信息强行转成了列信息结果当然是花屏、撕裂、斜向错位。很多人问是不是OLED坏了其实屏幕好好的只是数据组织的坐标系完全对不上。1.3 16×16汉字在页地址模式下的正确摆法一个16×16汉字在SSD1306上正确占位是横向16列Column x ~ x15纵向16行正好占2个PagePage N 负责汉字上半部分8行Page N1 负责汉字下半部分8行所以最适合OLED的字模格式是逐列式每一列从上往下取8个点组成1字节先取完上半部分16列Page N的数据再取下半部分16列Page N1的数据。这样一来一个汉字在代码里就是32字节前16字节对应上半页后16字节对应下半页。OLED_ShowCH16函数只需要先SetPos(PageN, col)发前16字节再SetPos(PageN1, col)发后16字节汉字就完整显示出来了。这个结构贯穿整篇后面所有代码都是围绕它写的。2. PCtoLCD2002选项怎么选一套能直接出32字节字模的设置2.1 软件里的几个关键选项逐个说人话PCtoLCD2002是一款很老的取模软件界面简陋但功能确实够用。打开后最常用的是字模选项这个弹窗里面有几个选项直接影响OLED显示效果我按重要性讲。点阵格式选阴码。OLED是黑底发光我们把笔画对应的像素写成1点亮屏幕其余位置是0正好和清屏后的全灭状态一致。选阳码的话文字笔画会变成挖空的形状背景反而全亮基本等于反色显示。取模方向选逐列式。这是整个设置里最关键的一项。逐列式从左上角开始先取第0列的8个点组成一个字节再取第1列一列一列往右走。16列取完正好是汉字上半部分16字节接着往下走再取下半部分16字节。最终顺序就是Page N数据在前、Page N1数据在后正好贴合SSD1306页地址模式。每行显示数一般填16。这只会影响输出文本的排版不改变取模结果。填16的话32字节分两行显示比较直观方便和上/下半部分对应。输出数制选十六进制。这是默认项不用动。自定义格式推荐选上并设置成类似{0x00,0x00}的形式。这样生成出来的字模直接就是C语言数组的样式复制到代码里改个变量名就能用省去手工补逗号括号的麻烦。2.2 完整操作步骤跟着做一遍打开PCtoLCD2002点击顶部菜单字模选项。弹窗里按上面的设置选好点阵格式阴码取模方向逐列式每行显示数16输出数制十六进制自定义格式打勾。确定后回到主界面在左侧小格子区域或底部输入框里输入你要显示的汉字。一次输入一个字最好也可以输入多个字生成的字模会自动按顺序排列但建议初学阶段一次一个字容易核对。点击工具栏上的生成字模按钮。下方文本区域会出现类似这样的数据{0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x04, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}全选复制粘贴到你的C代码里。上面这组数据是一字的逐列式字模——上半部分16个0x04说明第2根扫描线bit2在16列上全亮形成一条横线下半部分全0。如果显示正常屏幕上应该出现一条位于汉字区域约三分之一高度处的横线这个特征非常适合做验证。2.3 软件版本不同、选项叫法不同怎么办PCtoLCD2002有很多修改版、汉化版、定制版选项命名经常不一样。有的叫纵向取模有的把逐列式写成列行式还有的取模走向叫顺向正向或者逆向反向。不管名字怎么变记住一个判断标准生成的32字节数据前16字节看起来像原汉字的上半部分后16字节像下半部分就是对的。如果你点开取模选项发现没有逐列式就用纵向取模或者垂直取模这类词做参考。实在拿不准就用一字做试验显示成横线就正常显示成竖线或乱线就换一个方向参数重取。2.4 取模走向的顺向/逆向会影响什么PCtoLCD2002里取模走向决定取模时是往上扫还是往下扫以及从左往右还是从右往左。实际使用中OLED屏幕坐标原点一般在左上角所以建议选顺向/正向。这个选项对最终显示的影响是文字可能上下颠倒或左右镜像不在根本性的花屏问题范畴选错了后面能靠初始化命令补救不用太焦虑。3. 软件I2C驱动与SSD1306初始化点亮屏幕前的基础代码3.1 接线与硬件准备0.96寸OLED模块一般有4个引脚VCC、GND、SCL、SDA。接线如下模块引脚单片机引脚说明VCC3.3V大多数模块支持3.3V不建议直接接5VGNDGND共地SCLPB6I2C时钟线SDAPB7I2C数据线I2C地址方面SSD1306有SA0地址引脚大多数成品模块默认接地I2C地址是0x3C。如果模块背面SA0接了VCC地址就是0x3D。代码里写死0x3C如果点不亮先检查这个。还有一个容易被忽视的点I2C总线需要上拉电阻。多数OLED模块板载了上拉电阻直接用没问题。如果你用的是长杜邦线连接或者自制的转接板建议在SCL、SDA上各加一个4.7kΩ上拉到3.3V不然信号边沿不够陡偶尔会出现白屏或显示错乱。3.2 为什么用软件I2C而不是硬件I2COLED显示数据量不大而且SSD1306的I2C速率最高到400kHz软件模拟完全足够。软件I2C的好处非常明显不依赖具体单片机的外设配置GPIO翻转速度也够换平台只要改引脚宏剩下的代码原样搬走就能用。很多人卡在硬件I2C的各种状态机、错误标志、超时处理上其实完全没必要。GPIO在STM32CubeMX里配成推挽输出即可速度选High。如果你用的是开漏输出就必须确保外部有上拉电阻否则总线上不了高电平。3.3 SSD1306初始化命令逐条解释SSD1306上电后默认是关闭显示状态必须按一定顺序初始化。下面是常用的初始化序列关键命令我标了作用。void OLED_Init(void) { HAL_Delay(100); // 上电稳定等SSD1306内部复位完成 OLED_WriteCmd(0xAE); // 关闭显示 OLED_WriteCmd(0xD5); OLED_WriteCmd(0x80); // 时钟分频/振荡器频率 OLED_WriteCmd(0xA8); OLED_WriteCmd(0x3F); // 多路复用比64行 OLED_WriteCmd(0x40); // 显示起始行为0 OLED_WriteCmd(0x8D); OLED_WriteCmd(0x14); // 开启电荷泵不开不亮 OLED_WriteCmd(0x20); OLED_WriteCmd(0x02); // 页地址模式 OLED_WriteCmd(0xA1); // 段重映射取消左右镜像 OLED_WriteCmd(0xC8); // COM扫描方向反转取消上下镜像 OLED_WriteCmd(0xDA); OLED_WriteCmd(0x12); // COM引脚硬件配置 OLED_WriteCmd(0x81); OLED_WriteCmd(0x7F); // 对比度 OLED_WriteCmd(0xD9); OLED_WriteCmd(0xF1); // 预充电周期 OLED_WriteCmd(0xDB); OLED_WriteCmd(0x40); // VCOMH电压 OLED_WriteCmd(0xA4); // 显示内容跟随RAM OLED_WriteCmd(0xA6); // 正常显示非反显 OLED_WriteCmd(0xAF); // 打开显示 OLED_Clear(); // 清屏 }这里重点说两个命令0x8D 0x14电荷泵开关。0.96寸OLED内部需要升压驱动OLED像素电荷泵不开的话屏幕永远是黑的命令写再多也没用。这是白屏问题最常见的原因。0xA1和0xC8控制扫描方向。如果屏幕上的内容左右镜像把0xA1改成0xA0上下颠倒把0xC8改成0xC0。这两个命令是用来调整屏幕物理安装方向带来的显示方向问题。3.4 软件I2C底层函数底层核心就是模拟I2C时序起始信号、停止信号、发送一个字节。这部分代码网上有很多版本我写一个尽量简洁能用的static void OLED_I2C_Delay(void) { for (volatile uint8_t i 0; i 5; i) {} } static void OLED_I2C_Start(void) { OLED_SDA_H(); OLED_SCL_H(); OLED_I2C_Delay(); OLED_SDA_L(); OLED_I2C_Delay(); OLED_SCL_L(); OLED_I2C_Delay(); } static void OLED_I2C_Stop(void) { OLED_SDA_L(); OLED_SCL_H(); OLED_I2C_Delay(); OLED_SDA_H(); OLED_I2C_Delay(); } static void OLED_I2C_WriteByte(uint8_t dat) { for (uint8_t i 0; i 8; i) { if (dat 0x80) OLED_SDA_H(); else OLED_SDA_L(); OLED_SCL_H(); OLED_I2C_Delay(); OLED_SCL_L(); OLED_I2C_Delay(); dat 1; } // 释放SDA接收从机的ACK应答 OLED_SDA_H(); OLED_SCL_H(); OLED_I2C_Delay(); OLED_SCL_L(); OLED_I2C_Delay(); }写命令和写数据时控制字节不同。SSD1306在I2C通信时从机地址后紧跟一个控制字节0x00表示后面是命令0x40表示后面是数据static void OLED_WriteCmd(uint8_t cmd) { OLED_I2C_Start(); OLED_I2C_WriteByte(OLED_ADDR 1); // 0x3C左移一位加上写位0 OLED_I2C_WriteByte(0x00); // Co0, D/C#0后面这是命令 OLED_I2C_WriteByte(cmd); OLED_I2C_Stop(); } static void OLED_WriteData(uint8_t dat) { OLED_I2C_Start(); OLED_I2C_WriteByte(OLED_ADDR 1); OLED_I2C_WriteByte(0x40); // Co0, D/C#1后面这是数据 OLED_I2C_WriteByte(dat); OLED_I2C_Stop(); }到这里屏幕点亮的基础已经齐了。接下来进入正题汉字怎么显示。4. 显示汉字的核心代码字模表、坐标定位与完整工程4.1 页坐标和像素坐标怎么换算SSD1306页地址模式下屏幕地址由页号和列号组成页号范围0~7对应64行列号范围0~127对应128列。如果要把一个汉字显示在像素坐标(x, y)换算关系是列地址col x页地址page y / 816×16汉字占两页和一个16列宽的区间所以显示一个汉字前要确认page 1 8且col 15 128否则内容会跨出屏幕边界显示残缺。设置坐标的函数void OLED_SetPos(uint8_t page, uint8_t col) { OLED_WriteCmd(0xB0 page); // 页地址 0xB0~0xB7 OLED_WriteCmd(col 0x0F); // 列地址低4位 OLED_WriteCmd(0x10 | (col 4)); // 列地址高4位 }这里有个容易忽略的细节列地址分两次写入低4位在前高4位在后高4位命令是0x10 | (col 4)。如果只写了低4位没写高4位或者顺序颠倒数据就会写到意想不到的列去屏幕内容像被随机打散了一样。4.2 在指定位置显示一个16×16汉字核心函数就是前面说的上半页16字节下半页16字节。void OLED_ShowCH16(uint8_t page, uint8_t col, const uint8_t *dat) { // 上半部分Page 0-7行 OLED_SetPos(page, col); for (uint8_t i 0; i 16; i) { OLED_WriteData(dat[i]); } // 下半部分Page 1-8~15行 OLED_SetPos(page 1, col); for (uint8_t i 16; i 32; i) { OLED_WriteData(dat[i]); } }你从PCtoLCD2002里复制出来的逐列式字模前16字节就是Page N的数据后16字节是Page N1的数据直接传给这个函数即可不需要做任何额外转换。这也是前面花大篇幅讲取模方向的原因——取模方向对了驱动代码能省一半。4.3 字模查找表与字符串显示项目中一般不会只显示一个汉字需要做一个查找表。我习惯的结构体里同时存汉字的GB2312内码和32字节字模像这样typedef struct { uint8_t code[2]; // 汉字的GB2312内码 uint8_t bitData[32]; // 16x16逐列式字模 } CN16x16_Typedef;这里必须说明一个编码问题GB2312/GBK编码下一个汉字占2字节。如果你的代码里直接写字符串你好编译器按什么编码处理取决于源文件的保存格式。Keil MDK里如果源文件是ANSI/GB2312那没问题但如果用STM32CubeIDE或者VS CodeGCC源文件很可能保存为UTF-8一个汉字是3字节直接查表会匹配失败屏幕上一个字都显示不出来。最稳妥的显示字符串方式是用GB2312内码构成一个字节数组再传给查询函数完全绕开源文件编码问题。这样无论你在什么环境下编译都能得到一致结果。void OLED_ShowString16(uint8_t page, uint8_t col, const char *str) { while (*str ! 0) { // 只处理汉字ASCII字符跳过 if ((uint8_t)(*str) 0x80) { str; continue; } uint8_t code0 (uint8_t)str[0]; uint8_t code1 (uint8_t)str[1]; for (uint8_t i 0; i OLED_CHN_NUM; i) { if (CN16x16_Table[i].code[0] code0 CN16x16_Table[i].code[1] code1) { OLED_ShowCH16(page, col, CN16x16_Table[i].bitData); break; } } col 16; str 2; if (col 128 - 16) { return; } } }调用示例// 一的GB2312内码是0xD2 0xBB字节数组方式最保险 uint8_t show_str[] {0xD2, 0xBB, 0x00}; OLED_ShowString16(2, 32, (char *)show_str);如果一定要直接用你好这种字符串请确保你的源文件保存为GB2312/GBK编码再把字模表的code字段和编码保持一致。不同工具链的坑不一样我建议在工程里明确统一编码策略否则换台电脑编译就可能出现明明代码一样字却显示不出来的诡异问题。4.4 完整可直接使用的OLED驱动代码整理一份完整的oled.c把上面的函数全部串起来。引脚宏定义按你自己的工程改。/* oled.c * 适用于0.96寸SSD1306 128x64 OLED * 通信方式软件I2C * 依赖STM32 HAL库PB6SCL, PB7SDA */ #include main.h #define OLED_ADDR 0x3C /* I2C地址SA0接地为0x3C接VCC为0x3D */ #define OLED_PORT GPIOB #define OLED_SCL_PIN GPIO_PIN_6 #define OLED_SDA_PIN GPIO_PIN_7 #define OLED_SCL_H() HAL_GPIO_WritePin(OLED_PORT, OLED_SCL_PIN, GPIO_PIN_SET) #define OLED_SCL_L() HAL_GPIO_WritePin(OLED_PORT, OLED_SCL_PIN, GPIO_PIN_RESET) #define OLED_SDA_H() HAL_GPIO_WritePin(OLED_PORT, OLED_SDA_PIN, GPIO_PIN_SET) #define OLED_SDA_L() HAL_GPIO_WritePin(OLED_PORT, OLED_SDA_PIN, GPIO_PIN_RESET) static void OLED_I2C_Delay(void) { for (volatile uint8_t i 0; i 5; i) {} } static void OLED_I2C_Start(void) { OLED_SDA_H(); OLED_SCL_H(); OLED_I2C_Delay(); OLED_SDA_L(); OLED_I2C_Delay(); OLED_SCL_L(); OLED_I2C_Delay(); } static void OLED_I2C_Stop(void) { OLED_SDA_L(); OLED_SCL_H(); OLED_I2C_Delay(); OLED_SDA_H(); OLED_I2C_Delay(); } static void OLED_I2C_WriteByte(uint8_t dat) { for (uint8_t i 0; i 8; i) { if (dat 0x80) OLED_SDA_H(); else OLED_SDA_L(); OLED_SCL_H(); OLED_I2C_Delay(); OLED_SCL_L(); OLED_I2C_Delay(); dat 1; } OLED_SDA_H(); OLED_SCL_H(); OLED_I2C_Delay(); OLED_SCL_L(); OLED_I2C_Delay(); } static void OLED_WriteCmd(uint8_t cmd) { OLED_I2C_Start(); OLED_I2C_WriteByte(OLED_ADDR 1); OLED_I2C_WriteByte(0x00); OLED_I2C_WriteByte(cmd); OLED_I2C_Stop(); } static void OLED_WriteData(uint8_t dat) { OLED_I2C_Start(); OLED_I2C_WriteByte(OLED_ADDR 1); OLED_I2C_WriteByte(0x40); OLED_I2C_WriteByte(dat); OLED_I2C_Stop(); } void OLED_SetPos(uint8_t page, uint8_t col) { OLED_WriteCmd(0xB0 page); OLED_WriteCmd(col 0x0F); OLED_WriteCmd(0x10 | (col 4)); } void OLED_Clear(void) { for (uint8_t page 0; page 8; page) { OLED_SetPos(page, 0); for (uint8_t col 0; col 128; col) { OLED_WriteData(0x00); } } } void OLED_Init(void) { HAL_Delay(100); OLED_WriteCmd(0xAE); OLED_WriteCmd(0xD5); OLED_WriteCmd(0x80); OLED_WriteCmd(0xA8); OLED_WriteCmd(0x3F); OLED_WriteCmd(0x40); OLED_WriteCmd(0x8D); OLED_WriteCmd(0x14); OLED_WriteCmd(0x20); OLED_WriteCmd(0x02); OLED_WriteCmd(0xA1); OLED_WriteCmd(0xC8); OLED_WriteCmd(0xDA); OLED_WriteCmd(0x12); OLED_WriteCmd(0x81); OLED_WriteCmd(0x7F); OLED_WriteCmd(0xD9); OLED_WriteCmd(0xF1); OLED_WriteCmd(0xDB); OLED_WriteCmd(0x40); OLED_WriteCmd(0xA4); OLED_WriteCmd(0xA6); OLED_WriteCmd(0xAF); OLED_Clear(); } void OLED_ShowCH16(uint8_t page, uint8_t col, const uint8_t *dat) { OLED_SetPos(page, col); for (uint8_t i 0; i 16; i) { OLED_WriteData(dat[i]); } OLED_SetPos(page 1, col); for (uint8_t i 16; i 32; i) { OLED_WriteData(dat[i]); } } typedef struct { uint8_t code[2]; uint8_t bitData[32]; } CN16x16_Typedef; #define OLED_CHN_NUM (sizeof(CN16x16_Table) / sizeof(CN16x16_Table[0])) const CN16x16_Typedef CN16x16_Table[] { /* 一 GB2312: D2 BB * 这里是从PCtoLCD2002逐列式取出的字模 * 实际项目里把你要用的汉字字模都填进来 */ {0xD2, 0xBB, {0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x04, 0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x04, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}}, }; void OLED_ShowString16(uint8_t page, uint8_t col, const char *str) { while (*str ! 0) { if ((uint8_t)(*str) 0x80) { str; continue; } uint8_t code0 (uint8_t)str[0]; uint8_t code1 (uint8_t)str[1]; for (uint8_t i 0; i OLED_CHN_NUM; i) { if (CN16x16_Table[i].code[0] code0 CN16x16_Table[i].code[1] code1) { OLED_ShowCH16(page, col, CN16x16_Table[i].bitData); break; } } col 16; str 2; if (col 128 - 16) { return; } } }main函数里的调用流程int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); /* PB6/PB7配成推挽输出 */ OLED_Init(); OLED_Clear(); /* x32, y16换算得到page2, col32 */ uint8_t str1[] {0xD2, 0xBB, 0x00}; /* 一 */ OLED_ShowString16(2, 32, (char *)str1); while (1) { /* 主循环里做你自己的事 */ } }GPIO的初始化在CubeMX里做就行PB6、PB7配置为GPIO_Output推挽输出速度设为High不需要开复用功能因为这里完全是用GPIO模拟I2C。5. 实测中的五个坑与对应解法5.1 文字上下两截错位像拉链一样咬合不上这是很典型的现象一个字的上半部分和下半部分错开了像两条锯齿咬在一起。排查方向是字模数据排列顺序。虽然PCtoLCD2002选逐列式时输出的通常是前16字节上半页、后16字节下半页但部分修改版软件或不同选项组合下输出会变成第0列上半、第0列下半、第1列上半、第1列下半……交替排列。这时候直接传给OLED_ShowCH16就会错位。解决办法是写一个重排函数把交替排列转换为前16/后16格式void OLED_CH16_Convert(const uint8_t *src, uint8_t *dst) { for (uint8_t i 0; i 16; i) { dst[i] src[2 * i]; /* 第i列的上半字节 */ dst[16 i] src[2 * i 1]; /* 第i列的下半字节 */ } }先用这个函数转换一下再显示。注意只有确认字模是交替排列时才需要转换如果字模本来就是前16/后16格式转换反而会弄乱。判断方法很简单显示一个一字如果上半部分是16个0x04而下半部分全0说明已经是正确格式如果数据里0x04和0x00交替出现就是交替格式。5.2 汉字左右镜像或上下颠倒先说结论这种问题大概率不是字模的锅是SSD1306初始化命令里的扫描方向不匹配。文字左右反了像照镜子把初始化里的0xA1改成0xA0文字上下反了倒过来显示把初始化里的0xC8改成0xC0注意一点0.96寸OLED模块的出厂焊接方向不统一同一批模块也可能因为PCB版本差异出现不同扫描方向。所以这两条命令没有绝对的正确值以你实测的画面为准。如果你取模软件里选的走向本身就反了也可以通过这两个命令把画面正回来效果上是一样的。5.3 全屏白屏、点不亮白屏和点不亮是两个不同的表现分开说。全屏发光但没有内容大概率是清屏后一直在显示0xFF或者反显模式打开了。检查初始化里有没有写0xA6正常显示。如果写了0xA6还白屏看看代码里有没有误把数据命令当成写0xFF。完全不亮先检查电源和I2C地址0x3C还是0x3D再检查电荷泵命令0x8D 0x14有没有执行。很多人把这条命令漏掉屏幕怎么初始化都是黑的。如果模块是通过长时间高温焊接的批量点不亮还可能是因为焊接时间过长导致OLED排线损坏——这种属于硬件问题换一块再试。我的建议是先用同一个程序点亮两到三块屏排除个体差异。5.4 显示汉字时屏幕闪烁OLED本身刷新率不低如果闪烁大多数是因为你每次刷新都做了全屏清屏。清屏再重绘会有明显的闪一下感觉尤其是16×16汉字这种数据量不算小的图形全屏清一次要发1024字节数据软件I2C速度本来就不算快。正确做法是局部更新只往需要变化的页和列写入数据不清屏。比如状态栏某一项要更新先把那一个汉字区域用0x00覆盖再在同一个位置写入新字模这样视觉上几乎没有闪烁。OLED_ShowCH16天然支持局部更新不要为了省事每次调OLED_Clear。5.5 汉字在代码里查不到字模代码跑起来了但屏幕显示空白或者显示的汉字不对。这种问题九成是编码问题。如果你在GCC工具链下直接用你好字符串匹配GB2312内码而源文件是UTF-8编码那字符串在内存里是3字节一组和字模表的2字节code永远对不上。我建议统一用内码字节数组的方式/* 你 的GB2312内码是0xC4 0xE3 */ uint8_t ni[] {0xC4, 0xE3, 0x00}; /* 好 的GB2312内码是0xBA 0xC3 */ uint8_t hao[] {0xBA, 0xC3, 0x00}; OLED_ShowString16(2, 0, (char *)ni); OLED_ShowString16(2, 16, (char *)hao);内码字节数组和源文件编码无关任何编译器下行为都一致。如果你需要显示整句话就用内码组数组拼起来别直接依赖编译器的字符串编码。关于扩展的一点个人经验这篇文章到代码部分其实已经闭环了不过有个小技巧我想多说一句16×16汉字字模占32字节如果你打算做菜单、做中文界面把PCtoLCD2002生成的字模一个个手工粘贴到数组里非常痛苦。我习惯把常用汉字整理成一个独立cn_font.h头文件每个汉字一个宏注释按GB2312内码排好哪次要加字就去PCtoLCD2002里生成一次数据粘进对应位置。这样项目做到后面字库表越来越全新界面开发就快了。成批显示大量文本时可以考虑直接在SD卡或外部Flash里放HZK16字库文件运行时按内码偏移读取字模就不用几十个汉字手动维护了。不过那个方案会引入文件系统依赖一般小项目用查表法就够了。最后再分享一个实测经验取模完成后先用一字验证显示通路再用电这种笔画较多的字验证点阵密度这两个字都正常其他汉字基本不会有问题。真遇到奇怪的花屏不要急着调代码先回PCtoLCD2002里重新生成一版逐列式字模往往比改驱动的效率高得多。
返回列表