stm32/display/oled-fonts.md

OLED 显示与汉字取模:让"韶华实验室"上屏

知识库 · STM32 📅 初版 2026-09-15

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 修复:放大画布 + 按墨迹居中裁剪

三步走:

  1. 把画布放大到 24×24 渲染宋体 16px——墨迹无论怎么飘都装得下;
  2. 扫描墨迹包围盒:逐像素找所有"亮点"的最小外接矩形;
  3. 开一个 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 敲出一条总线》。

#八、相关阅读

#九、更新日志

📝共 1 条记录
日期类型更新内容
2026-09-15新增首次建档:API 三板斧、字库取模格式、三处改造、缺列根因修复与比对验证