字符编码基础:ASCII、GB2312、Unicode、UTF-8 编码原理与区别

做字体驱动,说白了就是在跟「字符编码」打交道。你想想看,屏幕上一个「中」字,从键盘敲进去到像素点亮,中间要经过多少层编码转换?我早年做一款嵌入式点阵屏项目时,就因为在编码上栽了个跟头——GB2312 和 Unicode 混用,结果日文字符全显示成乱码。嗯,从那以后,我花了不少功夫把编码这块彻底啃透了。

这一章,我们就从最基础的 ASCII 开始,一路讲到 UTF-8。你理解了这些,后面做字体索引、字库裁剪、多语言适配,都会顺手很多。

1. ASCII:一切的开端

ASCII(American Standard Code for Information Interchange)诞生于 1960 年代。它用 7 个比特表示一个字符,总共 128 个编码点。

范围(十六进制) 类别 示例
0x00 - 0x1F 控制字符 NUL、LF、CR
0x20 - 0x7E 可打印字符 数字、字母、标点
0x7F 删除控制符 DEL

为什么是 7 位?因为当时电传打字机只用了 7 根数据线。后来计算机普遍用 8 位字节,就把最高位空出来,或者用来做奇偶校验。

我的习惯:在嵌入式驱动里,ASCII 字符的字体数据我通常直接按 8x16 点阵硬编码。因为字符少、结构固定,查表快,不占额外存储。

2. GB2312:中文的第一次标准化

ASCII 只能处理英文。中文怎么办?1980 年,中国发布了 GB2312 标准。它用两个字节表示一个汉字,每个字节都使用 0xA1-0xF7 这个范围(高位为 1)。

GB2312 收录了 6763 个汉字,分成两级:

  • 一级汉字(3755 个):按拼音排序,覆盖最常用字
  • 二级汉字(3008 个):按部首排序,覆盖次常用字

编码结构是这样的:

区码(94 区) + 位码(94 位)
每个区/位取值范围:0xA1 - 0xFE

举例:「中」字
区码 = 0xD6(第 54 区)
位码 = 0xD0(第 48 位)
编码 = 0xD6 0xD0
我曾经踩过的坑:GB2312 的第二个字节范围是 0xA1-0xFE,但有些芯片的串口驱动会把 0xFE 当成帧结束符。结果每次发「啊」字(0xB0 0xA1)没问题,发「齄」字(0xF7 0xFE)就丢数据。后来我在驱动层加了转义处理才解决。

3. Unicode:一统天下的野心

GB2312 只解决了简体中文。那繁体中文、日文、韩文、阿拉伯文呢?每个国家搞一套编码,乱成一锅粥。Unicode 的目标就是:给全世界每个字符一个唯一的编号。

Unicode 的编号范围是 0x0000 - 0x10FFFF,总共 1,114,112 个码点。它分为 17 个平面(Plane),每个平面 65536 个码点:

平面 范围 内容
BMP(基本多文种平面) U+0000 - U+FFFF 常用字符:拉丁、中日韩统一表意文字(CJK)等
SMP(多文种补充平面) U+10000 - U+1FFFF 古文字、音乐符号、Emoji
SIP(表意文字补充平面) U+20000 - U+2FFFF CJK 扩展 B/C/D/E/F 区汉字
SSP(专用平面) U+E0000 - U+EFFFF 标签字符、变体选择符

注意:Unicode 只是「编号」,不是「存储方式」。同一个编号,可以用不同的编码方案存到内存或文件里。这就是 UTF-8、UTF-16、UTF-32 的由来。

4. UTF-8:互联网的事实标准

UTF-8 是 Unicode 的一种变长编码方案。它用 1 到 4 个字节表示一个字符,而且完全兼容 ASCII。

编码规则很简单:

  • 单字节:0xxxxxxx(兼容 ASCII,0x00-0x7F)
  • 双字节:110xxxxx 10xxxxxx
  • 三字节:1110xxxx 10xxxxxx 10xxxxxx
  • 四字节:11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

举个例子,汉字「中」的 Unicode 码点是 U+4E2D,落在三字节范围:

U+4E2D = 0100 1110 0010 1101(二进制)

按三字节模板填充:
1110 0100  10 111000  10 101101
= 0xE4 0xB8 0xAD

所以「中」的 UTF-8 编码是:E4 B8 AD
为什么 UTF-8 这么流行?
  • 兼容 ASCII:英文文档完全不变
  • 自同步:从任意位置开始,通过首字节就能判断当前字符长度
  • 无字节序问题:不像 UTF-16 需要 BOM
  • 存储效率高:英文 1 字节,中文 3 字节,比 UTF-32 省空间

5. 四种编码的核心区别

我把它们放在一张表里,你一眼就能看明白:

特性 ASCII GB2312 Unicode(码点) UTF-8
字符集范围 128 个 7445 个(含符号) 1,114,112 个 同 Unicode
编码长度 固定 1 字节 固定 2 字节 无(仅编号) 1-4 字节变长
兼容 ASCII — 不兼容 前 128 个相同 完全兼容
中文存储 不支持 2 字节/字 — 3 字节/字
字节序问题 无 无 — 无
典型应用 英文终端、协议头 国内早期系统 内存处理、API 内部 网页、文件、网络传输

6. 编码转换:驱动开发者的必修课

在嵌入式字体驱动里,你经常需要做编码转换。比如:

  • 用户输入的是 GB2312 编码(来自老旧串口设备)
  • 字库索引用的是 Unicode 码点
  • 最终输出到 LCD 的是点阵数据

转换流程大致是这样:

输入字节流(GB2312)
    ↓
查 GB2312 → Unicode 映射表
    ↓
得到 Unicode 码点
    ↓
查字库索引表(Unicode → 字模偏移)
    ↓
读取点阵数据
    ↓
输出到显示缓冲区
我建议:在资源受限的 MCU 上,不要放完整的 Unicode 字库。你可以只保留 GB2312 范围内的汉字字模,然后用 GB2312 编码直接索引。这样省掉一层映射,速度更快。

7. 一张图看懂编码体系

下面这张 SVG 图,把四种编码的关系和转换路径画清楚了:

字符编码体系与转换关系 ASCII 7 位 / 128 字符 GB2312 双字节 / 6763 汉字 Unicode 码点 U+0000 ~ U+10FFFF UTF-8 1~4 字节变长 UTF-16 2 或 4 字节 直接映射 查映射表 编码算法 编码算法 💡 关键理解: • ASCII 是基础,GB2312 是中文特化,Unicode 是统一编号,UTF-8/16 是存储方案 • 驱动开发中,输入可能是 GB2312 或 UTF-8,内部处理用 Unicode,字库索引用 Unicode

8. 驱动开发中的编码选择建议

根据我多年的嵌入式开发经验,给你几条实用建议:

  1. 内部统一用 Unicode 码点:所有字符处理逻辑都基于码点,不要混用编码。这样字库索引、字符宽度计算、排版逻辑都清晰。
  2. 输入输出层做编码转换:在驱动的最外层(比如串口接收、文件读取)做一次编码检测和转换,内部全用 Unicode。
  3. 字库裁剪时按需选择:如果只显示 GB2312 范围内的汉字,就用 GB2312 直接索引,省掉映射表。如果要做多语言,必须用 Unicode 字库。
  4. 注意存储空间:UTF-8 的中文占 3 字节,GB2312 占 2 字节。在 Flash 紧张的项目里,GB2312 字库能省 1/3 空间。
  5. 测试边界字符:比如 U+FFFE(非字符)、U+FFFF(非字符)、U+10FFFF(最大码点),确保你的编码转换函数不会崩溃。
我曾经踩过的另一个坑:某次做国际版固件,客户要求支持阿拉伯语。我以为 UTF-8 能搞定一切,结果发现阿拉伯语是双向文本(BiDi),需要特殊排版引擎。编码只是第一步,文字渲染还有更多坑等着你。

好了,字符编码这块就讲到这里。你把这些概念理清了,后面做字库索引、点阵提取、多语言适配,都会轻松很多。记住:编码是基础,基础不牢,地动山摇。