【Keil 】工程转换为 UTF-8 编码
背景与问题
Keil 的默认编码
Keil MDK作为嵌入式开发的主流 IDE,默认使用系统本地编码(中文 Windows 为 GBK)。这一设计主要面向使用中文的开发环境,在早期项目中能够很好地支持中文字符串的显示和编辑。
编码不一致带来的问题
1. 第三方库和驱动的兼容性问题
项目开发中经常需要引入第三方库或从网络获取的驱动代码。这些代码大多采用 UTF-8 编码,特别是来自 GitHub、GitLab 等平台的代码。当使用 GBK 编码的编辑器打开这些文件时,中文字符会出现乱码,影响代码的可读性和维护性。
例如,一个 UTF-8 编码的驱动文件中包含中文注释:
// 初始化传感器
void sensor_init(void);
在 GBK 编辑器中可能显示为:
// 鍒濆鍖栦紶鎰熷櫒
void sensor_init(void);
2. 串口调试输出问题
在调试过程中,经常使用 printf 函数通过串口输出调试信息。如果代码中的中文字符串是 GBK 编码,而一般的串口调试助手默认使用 UTF-8 编码显示,会导致中文显示异常。
例如,代码中输出:
printf("系统初始化完成\r\n");
串口助手可能显示为:
绯荤粺鍒濆鍖栧畬鎴?
3. 云平台数据传输问题
当需要将设备数据上传到云平台时,如果数据中包含中文且使用 GBK 编码,而云平台接口通常期望 UTF-8 编码,会导致数据解析错误或显示异常。这在物联网项目中尤为常见。
4. 版本控制系统的困扰
使用 Git 等版本控制系统时,如果项目中同时存在 GBK 和 UTF-8 编码的文件,Git 可能无法正确识别文件编码的变化,导致合并冲突或历史记录混乱。
为什么选择 UTF-8
UTF-8 编码具有以下优势:
- 广泛兼容性:现代开发工具、调试软件、云平台大多默认支持 UTF-8
- 国际化标准:UTF-8 是 Unicode 的实现方式之一,支持全球所有语言的字符
- 向后兼容:UTF-8 对 ASCII 字符完全兼容,不会影响英文代码
- 网络传输友好:HTTP、JSON 等协议默认使用 UTF-8 编码
转换步骤
第一步:批量转换文件编码
在修改 Keil 编辑器设置之前,需要先将工程中的所有源文件从 GBK 编码转换为 UTF-8 编码。
使用 Python 脚本转换
项目中提供了一个 Python 脚本 convert_to_utf8.py,可以自动检测并转换目录下所有文本文件的编码格式。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
将当前目录下所有文本文件转换为UTF-8编码格式
功能:
- 递归遍历当前目录及所有子目录
- 识别文本文件(.c, .h, .ignore, .md, .txt等)
- 检测并跳过二进制文件
- 将文件内容转换为UTF-8编码并保存
"""
import os
import sys
from pathlib import Path
# 延迟导入chardet,在main函数中检查
try:
import chardet
except ImportError:
chardet = None
# 常见的文本文件扩展名列表
TEXT_FILE_EXTENSIONS = {
# 源代码文件
'.c', '.h', '.cpp', '.cc', '.cxx', '.hpp', '.hxx',
# 脚本文件
'.py', '.js', '.ts', '.sh', '.bat', '.cmd', '.ps1',
# 配置文件
'.json', '.xml', '.yaml', '.yml', '.toml', '.ini', '.cfg', '.conf',
# 文档文件
'.md', '.txt', '.rst', '.adoc',
# 其他文本文件
'.ignore', '.gitignore', '.gitattributes', '.editorconfig',
'.cmake', '.make', '.mk', '.s', '.asm', '.sx', '.src',
'.css', '.html', '.htm', '.scss', '.less',
'.sql', '.lua', '.rb', '.go', '.rs', '.java', '.kt',
'.properties', '.log', '.csv', '.tsv',
'.uvprojx', '.uvoptx', '.ewp', '.eww', '.uvguix',
'.clang-format', '.clang-tidy', '.clangd'
}
# 需要跳过的目录
SKIP_DIRS = {
'.git', '.svn', '.hg', '__pycache__', '.vs', '.vscode',
'node_modules', 'build', 'dist', 'bin', 'obj', 'OBJ',
'.idea', '.settings', 'DebugConfig', 'Listings'
}
# 需要跳过的文件模式
SKIP_FILES = {
'.exe', '.dll', '.so', '.dylib', '.a', '.lib', '.o', '.obj',
'.bin', '.hex', '.elf', '.map', '.lst', '.bak'
}
def is_binary_file(file_path):
"""
检测文件是否为二进制文件
通过检查文件是否包含null字节来判断
"""
try:
with open(file_path, 'rb') as f:
# 读取前1KB内容检查
chunk = f.read(1024)
if b'\x00' in chunk:
return True
return False
except Exception:
return True # 读取失败,假设是二进制文件
def detect_encoding(file_path):
"""
检测文件编码
返回编码名称和置信度
"""
if chardet is None:
return 'utf-8', 0
try:
with open(file_path, 'rb') as f:
raw_data = f.read()
result = chardet.detect(raw_data)
encoding = result.get('encoding', 'utf-8')
confidence = result.get('confidence', 0)
return encoding, confidence
except Exception:
return 'utf-8', 0
def convert_file_to_utf8(file_path):
"""
将单个文件转换为UTF-8编码
返回:(success, message)
"""
try:
# 检测当前编码
detected_encoding, confidence = detect_encoding(file_path)
# 尝试读取文件
encodings_to_try = []
if detected_encoding and confidence > 0.7:
encodings_to_try.append(detected_encoding)
# 添加常见编码
encodings_to_try.extend(['utf-8', 'gbk', 'gb2312', 'gb18030',
'latin1', 'cp1252', 'iso-8859-1'])
content = None
used_encoding = None
for enc in encodings_to_try:
try:
with open(file_path, 'r', encoding=enc, errors='replace') as f:
content = f.read()
used_encoding = enc
break
except (UnicodeDecodeError, LookupError):
continue
if content is None:
return False, f"无法确定文件编码"
# 检查内容是否已经是UTF-8(简单检查)
try:
content.encode('utf-8')
except UnicodeEncodeError:
return False, f"内容包含无法编码为UTF-8的字符"
# 如果编码已经是UTF-8,检查是否需要更新BOM
# 读取原始字节检查BOM
with open(file_path, 'rb') as f:
first_bytes = f.read(3)
has_utf8_bom = first_bytes == b'\xef\xbb\xbf'
# 如果已经是UTF-8且没有BOM问题,可以跳过(可选)
# 这里为了确保一致性,总是保存
# 保存为UTF-8(无BOM)
with open(file_path, 'w', encoding='utf-8', newline='', errors='replace') as f:
f.write(content)
if used_encoding and used_encoding.lower() not in ('utf-8', 'utf8'):
return True, f"已转换: {used_encoding} -> UTF-8"
else:
return True, f"已确认: UTF-8"
except Exception as e:
return False, f"错误: {str(e)}"
def should_process_file(file_path):
"""
判断是否应该处理该文件
"""
path_obj = Path(file_path)
file_name = path_obj.name.lower()
ext = path_obj.suffix.lower()
# 检查文件扩展名(跳过二进制文件扩展名)
if ext in SKIP_FILES:
return False
# 检查是否为以点开头的特殊文件(如.gitignore, .ignore等)
if file_name.startswith('.'):
# 检查是否在文本文件列表中(去除开头的点)
if file_name in ['.gitignore', '.gitattributes', '.editorconfig', '.ignore', '.clang-format', '.clang-tidy', '.clangd']:
# 这些文件需要进一步检查是否为二进制
if is_binary_file(file_path):
return False
return True
# 如果有扩展名,检查是否在文本文件列表中
if ext and ext in TEXT_FILE_EXTENSIONS:
# 检查是否为二进制文件
if is_binary_file(file_path):
return False
return True
# 对于没有扩展名或未知扩展名的文件,跳过
return False
def process_directory(root_dir='.'):
"""
递归处理目录中的所有文件
"""
root_path = Path(root_dir).resolve()
processed_count = 0
skipped_count = 0
error_count = 0
print(f"开始处理目录: {root_path}")
print(f"文本文件扩展名: {', '.join(sorted(TEXT_FILE_EXTENSIONS))}")
print("-" * 80)
# 遍历所有文件
for file_path in root_path.rglob('*'):
# 跳过目录
if not file_path.is_file():
continue
# 跳过指定目录中的文件
if any(skip_dir in file_path.parts for skip_dir in SKIP_DIRS):
continue
# 检查是否应该处理
if not should_process_file(file_path):
skipped_count += 1
continue
# 获取相对路径用于显示
try:
rel_path = file_path.relative_to(root_path)
except ValueError:
rel_path = file_path
# 转换文件
success, message = convert_file_to_utf8(file_path)
if success:
processed_count += 1
print(f"[✓] {rel_path}: {message}")
else:
error_count += 1
print(f"[✗] {rel_path}: {message}")
print("-" * 80)
print(f"处理完成!")
print(f" 成功处理: {processed_count} 个文件")
print(f" 跳过文件: {skipped_count} 个文件")
print(f" 错误文件: {error_count} 个文件")
print(f" 总计文件: {processed_count + skipped_count + error_count} 个文件")
def main():
"""
主函数
"""
# 检查chardet库
if chardet is None:
print("错误: 需要安装 chardet 库")
print("请运行: pip install chardet")
sys.exit(1)
# 获取工作目录
work_dir = os.getcwd()
# 确认操作
print("=" * 80)
print("文件编码转换工具 - 转换为UTF-8")
print("=" * 80)
print(f"工作目录: {work_dir}")
print()
print("警告: 此操作将直接修改文件,请确保已备份重要文件!")
print()
response = input("是否继续? (y/n): ").strip().lower()
if response not in ('y', 'yes', '是'):
print("操作已取消")
return
print()
process_directory(work_dir)
if __name__ == '__main__':
main()
注意事项:
- 转换前建议先备份整个工程
- 脚本会跳过二进制文件(.o, .hex, .elf 等)和构建目录
第二步:修改 Keil 编辑器编码设置
文件编码转换完成后,需要修改 Keil IDE 的编辑器设置,使其能够正确显示 UTF-8 编码的文件。
操作步骤:
- 打开 Keil MDK
- 进入
Edit->Configuration->Editor - 在
Encoding选项中选择UTF-8 - 点击
OK保存设置
设置后的效果:
- 编辑器能够正确显示 UTF-8 编码的中文字符
- 新创建的文件默认使用 UTF-8 编码
- 打开现有文件时,Keil 会按照 UTF-8 编码解析
第三步:添加编译选项
完成前两步后,虽然编辑器能够正确显示中文,但直接编译可能会遇到错误。这是因为 ARM 编译器在处理多字节字符时的行为与编辑器不同。
编译错误示例
如果不添加编译选项,编译时可能出现如下错误:
main.c(217): error: #18: expected a ")"
oled_draw_text(0, 32, (uint8_t *)"1.鏄? 2.鍚?", 1);
main.c(217): error: #8: missing closing quote
oled_draw_text(0, 32, (uint8_t *)"1.鏄? 2.鍚?", 1);
这个错误的原因是:ARM 编译器在解析字符串字面量时,会将 UTF-8 编码的多字节字符序列(如中文字符的 3 个字节)尝试识别为单个字符单元。当遇到无法识别的字节序列时,编译器会报错。
解决方案:添加 --no-multibyte-chars 选项
操作步骤:
-
在 Keil 中打开工程
-
右键点击
Target,选择Options for Target... -
进入
C/C++标签页 -
在
Misc Controls输入框中添加:--no-multibyte-chars -
点击
OK保存
编译选项的作用:
--no-multibyte-chars 选项告诉编译器禁用多字节字符的特殊处理:
- 禁用多字节字符识别:编译器不会将 UTF-8 的多字节序列(如中文字符的 3 个字节)识别为单个字符,而是按字节逐个处理
- 字符串字面量处理:字符串中的多字节字符会被当作多个独立的字节,而不是一个字符单元
- 编译行为影响:字符串长度计算、字符比较等操作会基于字节数而非字符数
这样设置后,编译器会将字符串中的 UTF-8 编码的中文字符当作普通的字节序列处理,不会尝试解析其字符含义,从而避免编译错误。
注意事项:
- 添加此选项后,
strlen()等函数返回的是字节数,而不是字符数 - 如果代码中有基于字符数的逻辑,需要相应调整
第四步:检查并修改字符串处理代码
转换编码后,如果项目中使用了 OLED 等显示设备来显示中文,需要特别注意字符串处理的代码。
UTF-8 与 GBK 的字节数差异
- GBK 编码:中文字符占用 2 个字节
- UTF-8 编码:中文字符占用 3 个字节
OLED 显示代码示例
在 ssd1306.c 的 oled_draw_text 函数中,需要根据编码格式调整字节跳转:
void oled_draw_chinese_char(uint8_t chXpos, uint8_t chYpos,const uint8_t *pchChar, uint8_t mode, uint8_t char_len);
void oled_draw_text(uint8_t chXpos, uint8_t chYpos, const uint8_t *pchText,uint8_t chMode)
{
uint8_t size = 16;
while (*pchText != '\0')
{
if (*pchText & 0x80) // 判断是否为中文字符
{
oled_draw_chinese_char(chXpos, chYpos, pchText, chMode,3);
pchText += 3; // UTF-8 编码:跳过 3 个字节
// pchText += 2; // GBK/GB2312 编码:跳过 2 个字节(已注释)
chXpos += size; // 移动到下一个字符位置
}
else
{
// 绘制 ASCII 字符
oled_draw_char(chXpos, chYpos, *pchText, size, chMode);
pchText += 1;
chXpos += size / 2;
}
// ...
}
}
关键修改点:
- 字节跳转数:从
pchText += 2改为pchText += 3- 原因:UTF-8 编码的中文字符由 3 个字节组成(例如 “中” 的 UTF-8 编码是
0xE4 0xB8 0xAD) - 如果仍使用
+= 2,会导致字符解析错位,显示乱码
- 原因:UTF-8 编码的中文字符由 3 个字节组成(例如 “中” 的 UTF-8 编码是
- 字符判断逻辑:
*pchText & 0x80的判断仍然有效- UTF-8 编码的中文字符首字节最高位为 1(大于 0x80)
- 但需要注意,UTF-8 的后续字节也可能大于 0x80,需要更精确的判断
更精确的 UTF-8 字符检测
如果需要更准确地判断 UTF-8 字符,可以使用以下逻辑:
// 判断是否为 UTF-8 中文字符的首字节
// UTF-8 中文字符范围:0xE4-0xE9 开头
if ((*pchText & 0xE0) == 0xE0 && (*pchText & 0xF0) != 0xF0)
{
// 这是一个 3 字节的 UTF-8 字符(中文字符通常在此范围)
oled_draw_utf8_char(chXpos, chYpos, pchText, chMode);
pchText += 3;
chXpos += size;
}
参考
更多推荐



所有评论(0)