共计 9435 个字符,预计需要花费 24 分钟才能阅读完成。
在嵌入式开发、串口通信、屏幕点阵字库、日志解析、旧系统兼容中,经常会碰到中文编码问题。
最常见的几个编码是:
ASCII
GB2312
GBK
UTF-8
它们之间的关系经常容易混淆,尤其是:
GB2312 和 GBK 是什么关系?GBK 和 UTF-8 是什么关系?为什么同一个“中”字,在不同编码下字节不一样?嵌入式里拿到一串字节,应该怎么解析?
本文从编码范围、解析流程和 C 语言示例代码三个角度进行整理。
1. 先理解一个核心概念
Unicode 是字符编号
Unicode 负责给全世界字符分配一个统一编号。
例如:
中 = U+4E2D
国 = U+56FD
A = U+0041
Unicode 本身更像是一个“字符编号表”。
UTF-8 是 Unicode 的一种编码方式
UTF-8 负责把 Unicode 编号转换成具体的字节序列。
例如:
字符:中
Unicode:U+4E2D
UTF-8:E4 B8 AD
GB2312 / GBK 是另一套中文编码体系
GB2312 和 GBK 不是 Unicode 的编码方式,而是历史上的中文编码体系。
例如:
字符:中
GB2312:D6 D0
GBK:D6 D0
UTF-8:E4 B8 AD
可以看到,同一个“中”字,在 GBK 和 UTF-8 中的字节完全不一样。
2. 几种编码的整体关系
全球字符
↓
Unicode:统一字符编号,例如“中”= U+4E2D
↓
UTF-8 / UTF-16 / UTF-32 等编码方式
另一套中文编码体系:GB2312
↓ 扩展
GBK
↓ 扩展
GB18030
简单理解:
Unicode / UTF-8:现代互联网、跨平台系统常用
GB2312 / GBK:中文旧系统、点阵字库、嵌入式屏幕中常见
3. ASCII 编码范围
ASCII 是最基础的单字节编码。
3.1 编码范围
0x00 ~ 0x7F
常见字符:
'A' = 0x41
'0' = 0x30
'' = 0x20'\n' = 0x0A
ASCII 只占 1 个字节。
3.2 ASCII 解析规则
只要当前字节满足:
byte <= 0x7F
就可以认为它是 ASCII 字符。
示例:
41 42 43
解析结果:
0x41 = A
0x42 = B
0x43 = C
4. GB2312 编码范围
GB2312 是早期简体中文编码标准,主要包含常用简体汉字和符号。
它兼容 ASCII。
也就是说:
0x00 ~ 0x7F 仍然表示 ASCII
中文字符使用两个字节表示
4.1 GB2312 双字节范围
GB2312 的常见双字节范围是:
高字节:0xA1 ~ 0xF7
低字节:0xA1 ~ 0xFE
组合范围大致为:
A1A1 ~ F7FE
其中汉字区常见范围:
B0A1 ~ F7FE
4.2 GB2312 分区
GB2312 使用区位码思想。
每个字符由:
区号 + 位号
组成。
计算方式:
区号 = 高字节 - 0xA0
位号 = 低字节 - 0xA0
例如:
“中”= D6 D0
计算:
区号 = 0xD6 - 0xA0 = 0x36 = 54
位号 = 0xD0 - 0xA0 = 0x30 = 48
所以:
“中”在 GB2312 中是第 54 区,第 48 位
4.3 GB2312 示例
中 = D6 D0
国 = B9 FA
你 = C4 E3
好 = BA C3
字符串:
中国
GB2312 编码:
D6 D0 B9 FA
4.4 GB2312 解析流程
GB2312 的解析过程比较简单:
读取一个字节
↓
如果 <= 0x7F
↓
ASCII 字符,单字节解析
↓
如果在 0xA1 ~ 0xF7
↓
继续读取下一个字节
↓
如果第二字节在 0xA1 ~ 0xFE
↓
组成一个 GB2312 双字节字符
流程图:
当前字节 ch
ch <= 0x7F
→ ASCII
0xA1 <= ch <= 0xF7
→ 读取下一个字节
→ 判断是否在 0xA1 ~ 0xFE
→ 是:GB2312 字符
→ 否:非法编码
其他情况
→ 非法编码
4.5 GB2312 解析示例代码
#include <stdio.h>
#include <stdint.h>
#include <stddef.h>
static int is_gb2312_first_byte(uint8_t ch)
{return ch >= 0xA1 && ch <= 0xF7;}
static int is_gb2312_second_byte(uint8_t ch)
{return ch >= 0xA1 && ch <= 0xFE;}
void parse_gb2312(const uint8_t *data, size_t len)
{
size_t i = 0;
while (i < len)
{uint8_t ch = data[i];
if (ch <= 0x7F)
{printf("ASCII: 0x%02X, char = %c\n", ch, ch);
i++;
}
else if (is_gb2312_first_byte(ch))
{if (i + 1 >= len)
{
printf("GB2312 error: incomplete character at offset %lu\n",
(unsigned long)i);
break;
}
uint8_t ch2 = data[i + 1];
if (is_gb2312_second_byte(ch2))
{
int area = ch - 0xA0;
int pos = ch2 - 0xA0;
printf("GB2312: 0x%02X%02X, area = %d, pos = %d\n",
ch, ch2, area, pos);
i += 2;
}
else
{
printf("GB2312 error: invalid second byte 0x%02X at offset %lu\n",
ch2, (unsigned long)(i + 1));
i++;
}
}
else
{
printf("GB2312 error: invalid byte 0x%02X at offset %lu\n",
ch, (unsigned long)i);
i++;
}
}
}
int main(void)
{
/*
"A 中国" 的 GB2312 编码:A = 41
中 = D6 D0
国 = B9 FA
*/
uint8_t text[] = {0x41, 0xD6, 0xD0, 0xB9, 0xFA};
parse_gb2312(text, sizeof(text));
return 0;
}
运行结果类似:
ASCII: 0x41, char = A
GB2312: 0xD6D0, area = 54, pos = 48
GB2312: 0xB9FA, area = 25, pos = 90
5. GBK 编码范围
GBK 是 GB2312 的扩展版本。
它的特点是:
兼容 GB2312
支持更多汉字
支持更多生僻字
仍然兼容 ASCII
也就是说,GB2312 中已有的字符,在 GBK 中编码不变。
例如:
中:GB2312 = D6 D0
GBK = D6 D0
5.1 GBK 双字节范围
GBK 的双字节范围是:
首字节:0x81 ~ 0xFE
尾字节:0x40 ~ 0xFE,排除 0x7F
也就是:
8140 ~ FEFE
但是尾字节不能是:
0x7F
5.2 GBK 和 GB2312 的范围对比
| 编码 | 首字节范围 | 尾字节范围 | 是否兼容 ASCII | 是否兼容 GB2312 |
|---|---|---|---|---|
| GB2312 | A1 ~ F7 | A1 ~ FE | 是 | 本身 |
| GBK | 81 ~ FE | 40 ~ FE,排除 7F | 是 | 是 |
GBK 范围更大。
GB2312 的字节范围基本是 GBK 的一个子集。
5.3 GBK 示例
中 = D6 D0
国 = B9 FA
芃 = C6 4D
其中:
“芃”不在 GB2312 中“芃”在 GBK 中
这也是 GBK 比 GB2312 支持更多汉字的典型例子。
5.4 GBK 解析流程
GBK 的解析流程:
读取一个字节
↓
如果 <= 0x7F
↓
ASCII 字符
↓
如果在 0x81 ~ 0xFE
↓
读取下一个字节
↓
判断第二字节是否在 0x40 ~ 0xFE 且不等于 0x7F
↓
是:GBK 双字节字符
↓
否:非法编码
流程图:
当前字节 ch
ch <= 0x7F
→ ASCII
0x81 <= ch <= 0xFE
→ 读取下一个字节
→ 判断是否满足:0x40 <= ch2 <= 0xFE && ch2 != 0x7F
→ 是:GBK 字符
→ 否:非法编码
其他情况
→ 非法编码
5.5 GBK 解析示例代码
#include <stdio.h>
#include <stdint.h>
#include <stddef.h>
static int is_gbk_first_byte(uint8_t ch)
{return ch >= 0x81 && ch <= 0xFE;}
static int is_gbk_second_byte(uint8_t ch)
{return ch >= 0x40 && ch <= 0xFE && ch != 0x7F;}
void parse_gbk(const uint8_t *data, size_t len)
{
size_t i = 0;
while (i < len)
{uint8_t ch = data[i];
if (ch <= 0x7F)
{printf("ASCII: 0x%02X, char = %c\n", ch, ch);
i++;
}
else if (is_gbk_first_byte(ch))
{if (i + 1 >= len)
{
printf("GBK error: incomplete character at offset %lu\n",
(unsigned long)i);
break;
}
uint8_t ch2 = data[i + 1];
if (is_gbk_second_byte(ch2))
{printf("GBK: 0x%02X%02X\n", ch, ch2);
i += 2;
}
else
{
printf("GBK error: invalid second byte 0x%02X at offset %lu\n",
ch2, (unsigned long)(i + 1));
i++;
}
}
else
{
printf("GBK error: invalid byte 0x%02X at offset %lu\n",
ch, (unsigned long)i);
i++;
}
}
}
int main(void)
{
/*
"A 中芃" 的 GBK 编码:A = 41
中 = D6 D0
芃 = C6 4D
*/
uint8_t text[] = {0x41, 0xD6, 0xD0, 0xC6, 0x4D};
parse_gbk(text, sizeof(text));
return 0;
}
输出结果类似:
ASCII: 0x41, char = A
GBK: 0xD6D0
GBK: 0xC64D
需要注意:
判断字节范围,只能判断它“像不像 GBK 编码”。想要知道具体对应哪个 Unicode 字符,还需要 GBK → Unicode 映射表。
6. UTF-8 编码范围
UTF-8 是目前最常见的文本编码,网页、JSON、Linux 文件、网络通信中大量使用 UTF-8。
UTF-8 的特点是:
兼容 ASCII
变长编码
英文通常 1 字节
中文通常 3 字节
emoji 通常 4 字节
6.1 UTF-8 字节结构
UTF-8 使用字节开头的二进制位来判断字符长度。
| 字节数 | 二进制格式 | 说明 |
|---|---|---|
| 1 字节 | 0xxxxxxx | ASCII |
| 2 字节 | 110xxxxx 10xxxxxx | 常见欧洲字符 |
| 3 字节 | 1110xxxx 10xxxxxx 10xxxxxx | 常见中文 |
| 4 字节 | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx | emoji、生僻字符等 |
6.2 UTF-8 常见范围
1 字节
00 ~ 7F
示例:
A = 41
2 字节
C2 80 ~ DF BF
结构:
110xxxxx 10xxxxxx
3 字节
E0 A0 80 ~ EF BF BF
结构:
1110xxxx 10xxxxxx 10xxxxxx
常见中文大多是 3 字节。
例如:
中 = E4 B8 AD
国 = E5 9B BD
4 字节
F0 90 80 80 ~ F4 8F BF BF
结构:
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
常见 emoji 多数是 4 字节。
例如:
😀 = F0 9F 98 80
6.3 UTF-8 解析流程
UTF-8 的解析比 GBK 复杂一些。
核心判断:
读取第一个字节
↓
0xxxxxxx
→ 1 字节 ASCII
110xxxxx
→ 后面还需要 1 个后续字节
1110xxxx
→ 后面还需要 2 个后续字节
11110xxx
→ 后面还需要 3 个后续字节
后续字节必须满足:
10xxxxxx
也就是:
0x80 ~ 0xBF
6.4 UTF-8 示例
字符:
中
Unicode:
U+4E2D
UTF-8:
E4 B8 AD
二进制:
E4 = 1110 0100
B8 = 1011 1000
AD = 1010 1101
解析器看到:
1110xxxx
就知道这是一个 3 字节字符。
后面的两个字节:
10111000
10101101
都满足:
10xxxxxx
所以这是一个合法 UTF-8 字符。
6.5 UTF-8 解析示例代码
下面代码会解析 UTF-8 字节流,并输出 Unicode 码点。
#include <stdio.h>
#include <stdint.h>
#include <stddef.h>
static int is_utf8_continuation(uint8_t ch)
{return (ch & 0xC0) == 0x80;
}
int utf8_decode_one(const uint8_t *data, size_t len, uint32_t *codepoint, size_t *used)
{
uint8_t b0;
if (data == NULL || len == 0 || codepoint == NULL || used == NULL)
{return -1;}
b0 = data[0];
/*
1 字节:0xxxxxxx
*/
if (b0 <= 0x7F)
{
*codepoint = b0;
*used = 1;
return 0;
}
/*
2 字节:110xxxxx 10xxxxxx
合法首字节范围通常从 C2 开始。C0、C1 会造成过短编码,不建议接受。*/
if (b0 >= 0xC2 && b0 <= 0xDF)
{
uint8_t b1;
if (len < 2)
{return -2;}
b1 = data[1];
if (!is_utf8_continuation(b1))
{return -3;}
*codepoint = ((uint32_t)(b0 & 0x1F) << 6) |
((uint32_t)(b1 & 0x3F));
*used = 2;
return 0;
}
/*
3 字节:1110xxxx 10xxxxxx 10xxxxxx
*/
if (b0 >= 0xE0 && b0 <= 0xEF)
{
uint8_t b1;
uint8_t b2;
if (len < 3)
{return -2;}
b1 = data[1];
b2 = data[2];
if (!is_utf8_continuation(b1) || !is_utf8_continuation(b2))
{return -3;}
/*
防止过短编码:E0 后面的第二字节必须 >= A0
*/
if (b0 == 0xE0 && b1 < 0xA0)
{return -4;}
/*
UTF-16 代理区 U+D800 ~ U+DFFF 不允许出现在 UTF-8 中。ED A0 80 ~ ED BF BF 对应代理区,需要排除。*/
if (b0 == 0xED && b1 >= 0xA0)
{return -5;}
*codepoint = ((uint32_t)(b0 & 0x0F) << 12) |
((uint32_t)(b1 & 0x3F) << 6) |
((uint32_t)(b2 & 0x3F));
*used = 3;
return 0;
}
/*
4 字节:11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
*/
if (b0 >= 0xF0 && b0 <= 0xF4)
{
uint8_t b1;
uint8_t b2;
uint8_t b3;
if (len < 4)
{return -2;}
b1 = data[1];
b2 = data[2];
b3 = data[3];
if (!is_utf8_continuation(b1) ||
!is_utf8_continuation(b2) ||
!is_utf8_continuation(b3))
{return -3;}
/*
F0 后面的第二字节必须 >= 90,防止过短编码。*/
if (b0 == 0xF0 && b1 < 0x90)
{return -4;}
/*
Unicode 最大合法码点是 U+10FFFF。F4 后面的第二字节不能超过 8F。*/
if (b0 == 0xF4 && b1 > 0x8F)
{return -6;}
*codepoint = ((uint32_t)(b0 & 0x07) << 18) |
((uint32_t)(b1 & 0x3F) << 12) |
((uint32_t)(b2 & 0x3F) << 6) |
((uint32_t)(b3 & 0x3F));
*used = 4;
return 0;
}
return -7;
}
void parse_utf8(const uint8_t *data, size_t len)
{
size_t i = 0;
while (i < len)
{
uint32_t codepoint = 0;
size_t used = 0;
int ret;
ret = utf8_decode_one(&data[i], len - i, &codepoint, &used);
if (ret == 0)
{
printf("UTF-8: offset = %lu, used = %lu, Unicode = U+%04lX\n",
(unsigned long)i,
(unsigned long)used,
(unsigned long)codepoint);
i += used;
}
else
{
printf("UTF-8 error: offset = %lu, byte = 0x%02X, ret = %d\n",
(unsigned long)i,
data[i],
ret);
i++;
}
}
}
int main(void)
{
/*
"A 中国" 的 UTF-8 编码:A = 41
中 = E4 B8 AD
国 = E5 9B BD
*/
uint8_t text[] = {
0x41,
0xE4, 0xB8, 0xAD,
0xE5, 0x9B, 0xBD
};
parse_utf8(text, sizeof(text));
return 0;
}
输出结果类似:
UTF-8: offset = 0, used = 1, Unicode = U+0041
UTF-8: offset = 1, used = 3, Unicode = U+4E2D
UTF-8: offset = 4, used = 3, Unicode = U+56FD
7. GB2312、GBK、UTF-8 编码范围总结
| 编码 | ASCII 范围 | 中文字节数 | 主要范围 | 说明 |
|---|---|---|---|---|
| ASCII | 00 ~ 7F | 不支持中文 | 00 ~ 7F | 基础英文字符 |
| GB2312 | 00 ~ 7F | 2 字节 | A1A1 ~ F7FE | 早期简体中文编码 |
| GBK | 00 ~ 7F | 2 字节 | 8140 ~ FEFE,尾字节排除 7F | GB2312 扩展,支持更多汉字 |
| UTF-8 | 00 ~ 7F | 中文常见 3 字节 | 1 ~ 4 字节变长 | 现代互联网主流编码 |
8. 三种编码解析过程对比
8.1 GB2312
byte <= 0x7F
→ ASCII
0xA1 <= byte <= 0xF7
→ 读取下一个字节
→ 判断第二字节是否在 0xA1 ~ 0xFE
→ 组成 GB2312 字符
8.2 GBK
byte <= 0x7F
→ ASCII
0x81 <= byte <= 0xFE
→ 读取下一个字节
→ 判断第二字节是否在 0x40 ~ 0xFE,并且不等于 0x7F
→ 组成 GBK 字符
8.3 UTF-8
0xxxxxxx
→ 1 字节 ASCII
110xxxxx
→ 2 字节 UTF-8
1110xxxx
→ 3 字节 UTF-8
11110xxx
→ 4 字节 UTF-8
10xxxxxx
→ 后续字节,不能单独作为字符开头
9. 同一个字符在不同编码下的表现
以“中”为例:
| 字符 | Unicode | GB2312 | GBK | UTF-8 |
|---|---|---|---|---|
| 中 | U+4E2D | D6 D0 | D6 D0 | E4 B8 AD |
以“芃”为例:
| 字符 | GB2312 | GBK | UTF-8 |
|---|---|---|---|
| 芃 | 不支持 | C6 4D | E8 8A 83 |
所以,如果设备只支持 GB2312 字库,那么“芃”这种字可能无法正常显示。
10. 嵌入式场景中的实际处理方式
在嵌入式项目里,经常会遇到几种情况。
10.1 串口收到 GBK 字符串
如果上位机发的是 GBK,那么 MCU 可以按 GBK 规则解析。
串口数据
↓
判断 ASCII / GBK 双字节
↓
如果是 ASCII,直接显示
↓
如果是 GBK,查 GBK 字库
↓
读取点阵数据
↓
显示到 LCD / OLED
10.2 使用 GB2312 点阵字库
很多老式中文字库,比如 HZK16、HZK24,经常使用 GB2312 区位码方式索引。
以 HZK16 为例:
每个汉字 16x16 点阵
每个点 1 bit
总字节数 = 16 * 16 / 8 = 32 字节
偏移计算:
offset = ((area - 1) * 94 + (pos - 1)) * 32;
其中:
area = high_byte - 0xA0;
pos = low_byte - 0xA0;
示例代码:
#include <stdio.h>
#include <stdint.h>
#define HZK16_BYTES_PER_CHAR 32
long gb2312_hzk16_offset(uint8_t high, uint8_t low)
{
int area;
int pos;
long offset;
if (high < 0xA1 || high > 0xF7)
{return -1;}
if (low < 0xA1 || low > 0xFE)
{return -1;}
area = high - 0xA0;
pos = low - 0xA0;
offset = ((long)(area - 1) * 94 + (pos - 1)) * HZK16_BYTES_PER_CHAR;
return offset;
}
int main(void)
{
/*“中”= D6 D0
*/
long offset = gb2312_hzk16_offset(0xD6, 0xD0);
printf("offset = %ld\n", offset);
return 0;
}
10.3 UTF-8 转 GBK / GB2312
如果网络、JSON、网页传过来的是 UTF-8,而屏幕字库是 GBK 或 GB2312,那么中间需要转换:
UTF-8 字节流
↓
解析成 Unicode 码点
↓
Unicode 转 GBK / GB2312
↓
查 GBK / GB2312 字库
↓
显示
这里有一个重点:
UTF-8 和 GBK 之间不能只靠公式转换。
需要映射表:
Unicode → GBK
GBK → Unicode
因为 GBK 编码和 Unicode 码点不是简单的数学关系。
11. 如何判断一段数据是 GBK 还是 UTF-8?
这个问题比较麻烦。
因为单纯靠字节流,有时不能 100% 判断。
例如:
D6 D0
在 GBK 中是:
中
但在 UTF-8 中:
D6 D0
不是合法的 UTF-8 字符。
所以这种情况可以判断它更像 GBK。
但是有些字节组合可能同时满足某种编码规则,尤其是包含大量 ASCII 时,不一定能准确判断。
实际项目中更推荐:
协议里明确编码方式
文件头或配置里明确编码方式
上位机和下位机约定统一编码
不要靠猜。
12. 实际项目建议
12.1 新项目优先使用 UTF-8
如果是新项目,尤其涉及:
网络通信
HTTP
MQTT
JSON
Web
数据库
跨平台软件
建议统一使用 UTF-8。
12.2 屏幕点阵字库可以继续用 GB2312 / GBK
如果是低资源 MCU,显示中文点阵,尤其是使用已有字库文件:
HZK16
HZK24
GB2312 点阵字库
GBK 点阵字库
那么 GB2312 / GBK 仍然很实用。
12.3 最好在系统边界做一次转换
例如:
网络层:UTF-8
内部逻辑:Unicode / UTF-8
显示层:GB2312 / GBK 字库
可以设计成:
UTF-8 输入
↓
解析为 Unicode
↓
查 Unicode → GBK 映射表
↓
得到 GBK 编码
↓
查 GBK 字库
↓
显示
这样架构会更清晰。
13. 总结
GB2312、GBK、UTF-8 的核心区别可以概括为:
GB2312:早期简体中文编码,范围较小
GBK:GB2312 的扩展,支持更多中文字符
UTF-8:Unicode 的编码方式,现代通用编码,支持全球字符
解析规则可以简单记为:
GB2312:ASCII 单字节
中文双字节:A1~F7 + A1~FE
GBK:ASCII 单字节
中文双字节:81~FE + 40~FE,排除 7F
UTF-8:根据首字节高位判断 1~4 字节长度
后续字节必须是 10xxxxxx
在嵌入式和中文字库场景中,最重要的是:
先确认输入数据是什么编码
再按对应编码规则解析
最后根据字库格式查表显示
不要把:
Unicode
UTF-8
GB2312
GBK
混为一谈。
它们解决的问题不一样:
Unicode:字符编号
UTF-8:Unicode 的字节编码方式
GB2312:早期简体中文编码
GBK:GB2312 的扩展编码
搞清楚这层关系,中文乱码、串口解析、点阵字库显示这些问题就会清晰很多。