OLED 显示与汉字取模:让"韶华实验室"上屏
OLED 显示与汉字取模:让"韶华实验室"上屏
2026-09-15 实测验证:洋桃 1 号开发板(STM32F103C8T6),21_OLED_PIC 模块改造版。片头图停留 3 秒 → ASCII 标题居中 → "韶华实验室"五字 16×16 汉字上屏居中。汉字缺列问题已修复(24×24 画布渲染 + 墨迹包围盒居中裁剪),修边后与官方字库逐位比对约 68% 匹配(方向校验全部通过)。
#一、实验平台与目标
本次实验基于洋桃 1 号开发板的 21_OLED_PIC 模块:SSD1306 控制器的 128×64 点阵 OLED,走软件 I2C 与主控通信,板载 OLED0561 驱动封装已经把底层时序全部包好,应用层只管"往哪一页、哪个位置、画什么"。
目标有三步:把片头图多停 3 秒、把英文标题换成"韶华实验室"的标识居中、再让"韶华实验室"五个汉字以 16×16 点阵居中上屏。前两步改字符串就行,第三步必须自己建汉字字库——这才是本次的重点。
#二、显示 API 三板斧
OLED0561 封装对上层只暴露三个常用函数,按"初始化 → ASCII → 汉字"分工:
| API | 用途 | 关键参数 |
|---|---|---|
OLED0561_Init() | 初始化:上电配置 SSD1306、清屏、开显示 | 无 |
OLED_DISPLAY_8x16_BUFFER(页号, "字符串") | 显示 8×16 ASCII 字符串 | 每字符 8px 宽,一行(128px)最多 16 字符;"页号"是纵向 8 行一个页的基本单位 |
OLED_DISPLAY_16x16(页, x像素, 字库索引) | 显示 16×16 汉字 | x 是像素坐标(不是字符格);字库索引对应字库数组里第几个字(从 0 数) |
两个坐标系要分清:ASCII 的 API 按"页 + 整串"操作,汉字的 API 按"页 + 像素 x + 数组下标"逐字摆放——这也决定了汉字的"居中"要自己算像素。
#三、字库原理:16×16 纵向取模
汉字字库放在 Hardware\OLED0561\CHS_16x16.h,核心是一个数组:
cuc8 GB_16[] = { /* 每个汉字 32 字节,按索引顺序排列 */ };
格式约定(取模软件与显示驱动必须严格一致):
- 每字 32 字节:16×16 点阵 = 256 个点 = 32 字节;
- 纵向取模:每字节竖着管 8 个点,bit0 是顶行(LSB 在上);
- 页序从上到下:先取上半页 16 字节,再取下半页 16 字节。
换句话说,一个 16×16 汉字在内存里是"上 16 字节 + 下 16 字节"两页拼起来的竖条,显示驱动按同样的顺序把字节铺回屏幕,字就出来了。取模方向只要有一处对不上(横向/纵向、bit0 顶/底、页序上下、是否字节镜像),字就会碎成雪花或左右翻转。
#四、三处改造
#4.1 改造一:片头图停留 3 秒
原工程片头图一闪而过。在片头图显示之后补一句延时,让它站稳 3 秒再进主界面:
cOLED_DISPLAY_PIC(); /* 片头图(函数名以工程为准) */
delay_s(3); /* 新增:停留 3 秒 */
#4.2 改造二:ASCII 标题居中
把 " YoungTalk " 换成 " ShaoHua-lab "。账这么算:一行 128px ÷ 每字符 8px = 16 字符宽;ShaoHua-lab 共 11 个字符,剩 5 个空格,左边补 2、右边补 3,视觉重心居中:
cOLED_DISPLAY_8x16_BUFFER(0, " ShaoHua-lab ");
#4.3 改造三:"韶华实验室"五个汉字居中
五个汉字各 16px 宽,共 5×16 = 80px;居中起点 x = (128?80)/2 = 24。在字库中依次录入五个字(索引 0~4),然后五次调用逐字上屏:
cOLED_DISPLAY_16x16(2, 24, 0); /* 韶 */
OLED_DISPLAY_16x16(2, 40, 1); /* 华 */
OLED_DISPLAY_16x16(2, 56, 2); /* 实 */
OLED_DISPLAY_16x16(2, 72, 3); /* 验 */
OLED_DISPLAY_16x16(2, 88, 4); /* 室 */
相邻字间距正好 16px(24→40→56→72→88),无缝拼成一行。
#五、重点踩坑:每个字最右侧缺一两列
#5.1 现象
上屏后用户反馈:五个汉字每个字最右侧都缺一两列——笔画收尾的竖钩、封口全都缺了一角,字"瘦"了一圈。
#5.2 根因:GDI 的墨迹越界
取模脚本用 Windows GDI 在 16×16 画布上渲染宋体 16px 的字。问题在于:宋体 16px 的墨迹实际占的是列 3~17——最右两列(16、17)落在了 16px 画布之外。脚本按 0~15 裁剪,越界的 16~17 两列直接被切掉,于是每个字统一缺右侧一两列。
这不是取模方向错了,而是画布给小了:16px 字号的墨迹在 GDI 里并不保证装进 16×16。
#5.3 修复:放大画布 + 按墨迹居中裁剪
三步走:
- 把画布放大到 24×24 渲染宋体 16px——墨迹无论怎么飘都装得下;
- 扫描墨迹包围盒:逐像素找所有"亮点"的最小外接矩形;
- 开一个 16×16 的窗口,对墨迹包围盒居中裁剪:窗口起点约为 x=6~7、y=4(随每字墨迹位置微调),保证字的重心落在 16×16 正中,而不是死板地从 (0,0) 开始切。
修完之后,最右列笔画回来了,五个字笔画完整、居中端正。
#5.4 修边后与官方字库逐位比对
为了确认取模方向没修歪,把自己生成的字库与洋桃官方字库逐位比对(每字 32 字节 × 8 位 = 256 位),结果约 68% 匹配。
这个数字听起来不高,但它是合理的:宋体各版本/渲染引擎的像素风格本身就有差异,点阵字不存在"唯一正确答案"。真正可靠的是方向校验——比对的错位模式如果来自取模方向错误,会呈现整字节翻转/行列颠倒的强规律;实测差异是均匀的像素风格差异,且四项方向约定全部确认无误:
- ? 纵向取模(每字节 8 点竖排)
- ? bit0 = 顶行
- ? 页序从上到下(上半页 16 字节在前)
- ? 不镜像
结论:方向正确,68% 的差异来自字体像素风格,可以放心用。
#六、工具坑:PowerShell 的二维数组不可靠
比对脚本的第一个版本用 New-Object 'byte[,]' 建 32 字节的二维数组,结果行为不可靠(赋值/索引偶发不生效)。换成一维数组后问题消失:
powershell$buf = New-Object 'byte[]' 32 # 一维 [byte[]]
# 行主序手工映射:字节索引 = y * 16 + x(x:0~15,y:0~1 两页)
$idx = $page * 16 + $col
另一个教训是匹配率的分子分母必须统一单位:分母用"每字 256 位"(32 字节 × 8),分子就得数"不一致的位数",而不是拿字节数去比位数——单位混了,匹配率会凭空翻倍或归零。
#七、小结
- ASCII 用
OLED_DISPLAY_8x16_BUFFER(页 + 字符串),汉字用OLED_DISPLAY_16x16(页 + 像素 x + 字库索引),坐标系不同,居中自己算像素; - 16×16 汉字 = 32 字节,纵向取模、bit0 顶行、页序从上到下——取模软件、字库数组、显示驱动三方必须严丝合缝;
- GDI 渲染小字号字体会"墨迹越界",16×16 画布装不下宋体 16px:放大画布渲染、按墨迹包围盒居中裁剪是通用修法;
- 字库比对别迷信匹配率数字,看差异的规律:方向错误呈强规律,字体风格差异呈均匀分布;
- 本实验的 OLED 走的是软件 I2C,底层时序如何用 GPIO"敲"出来,见姊妹篇《软件 I2C:用 GPIO 敲出一条总线》。
#八、相关阅读
- 软件 I2C:用 GPIO 敲出一条总线——本实验 OLED 的底层总线
- 定时器与中断:1 秒心跳与五阶段状态机
- 硬件 CRC 单元:一次实验看懂循环冗余校验
#九、更新日志
| 日期 | 类型 | 更新内容 |
|---|---|---|
| 2026-09-15 | 新增 | 首次建档:API 三板斧、字库取模格式、三处改造、缺列根因修复与比对验证 |