字符编码基础: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 位字节,就把最高位空出来,或者用来做奇偶校验。
2. GB2312:中文的第一次标准化
ASCII 只能处理英文。中文怎么办?1980 年,中国发布了 GB2312 标准。它用两个字节表示一个汉字,每个字节都使用 0xA1-0xF7 这个范围(高位为 1)。
GB2312 收录了 6763 个汉字,分成两级:
- 一级汉字(3755 个):按拼音排序,覆盖最常用字
- 二级汉字(3008 个):按部首排序,覆盖次常用字
编码结构是这样的:
区码(94 区) + 位码(94 位)
每个区/位取值范围:0xA1 - 0xFE
举例:「中」字
区码 = 0xD6(第 54 区)
位码 = 0xD0(第 48 位)
编码 = 0xD6 0xD0
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
- 兼容 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 → 字模偏移)
↓
读取点阵数据
↓
输出到显示缓冲区
7. 一张图看懂编码体系
下面这张 SVG 图,把四种编码的关系和转换路径画清楚了:
8. 驱动开发中的编码选择建议
根据我多年的嵌入式开发经验,给你几条实用建议:
- 内部统一用 Unicode 码点:所有字符处理逻辑都基于码点,不要混用编码。这样字库索引、字符宽度计算、排版逻辑都清晰。
- 输入输出层做编码转换:在驱动的最外层(比如串口接收、文件读取)做一次编码检测和转换,内部全用 Unicode。
- 字库裁剪时按需选择:如果只显示 GB2312 范围内的汉字,就用 GB2312 直接索引,省掉映射表。如果要做多语言,必须用 Unicode 字库。
- 注意存储空间:UTF-8 的中文占 3 字节,GB2312 占 2 字节。在 Flash 紧张的项目里,GB2312 字库能省 1/3 空间。
- 测试边界字符:比如 U+FFFE(非字符)、U+FFFF(非字符)、U+10FFFF(最大码点),确保你的编码转换函数不会崩溃。
好了,字符编码这块就讲到这里。你把这些概念理清了,后面做字库索引、点阵提取、多语言适配,都会轻松很多。记住:编码是基础,基础不牢,地动山摇。