UEFI-edk2源码中默认只有英文和法文的字库,在UI界面上或者shell终端打印中文字符串,则无法显示。例如,上一篇博客中的TestoneApp.cpp中,增加一行带中文字符串的打印:
Print(L"Hello, world!\r\n");
Print(L"UEFI-dek 你好..!\r\n");
实际上运行的效果如下图所示。
也就是中文没有显示出来,这是因为UEFI采用位图(点阵图像)的方式进行字符显示,而edk2源码中默认只有英文和法文字库的位图数据。
一、字符编码介绍
UEFI中采用UCS-2的编码方式,即固定两个字节存一个字符的编码,需要显示该字符到显示器时,会通过字符编码来查询它的位图数据,从而实现字符显示。edk2源码中默认采用SimpleFont格式的点阵字体,字体点阵数据位于
MdeModulePkg\Universal\Console\GraphicsConsoleDxe\ LaffStd.c
SimpleFont是一种点阵字体,有两种格式,窄体(8x19)和宽体(16x19),英文字符串用窄体显示,如下图就是英文字符的字体点阵数据存放的数组。
从点阵数据中可以看出,字符编码为0x0020的点阵数据为全0,因为0x20是空格的ascii码,空格什么也不用显示,所以为全0。字符编码为0x0021对应为感叹号的ascii字符,如下图所示,将它的数据展开,由1组成的位图显示正好是个感叹号。
二、增加中文字库
中文需要用到16x19的宽体才能够容纳显示,如何获取中文字库的点阵数据呢?这里我们参考《UEFI原理与编程》原著作者戴正华写的代码,源码链接:https://github.com/zhenghuadai/uefi-programming/tree/master/book/GUIbasics/font/SimpleFont。
源码中example.data文件中包含了字符编码从0x4e00到0x9fa4的所有点阵数据,基本囊括了我们后续要用到的显示中文字库。接下需要做的就是这个中文字库数据注册到UEFI的SimpleFontPackageList中。
源码main.c文件中写了完整的实现过程,基本与edk2源码目录下"MdeModulePkg/Universal/Console/GraphicsConsoleDxe/GraphicsConsole.c"文件写的过程一致,先新建一个SimplifiedFont的结构体,将结构体成员NumberOfWideGlyphs指向一段新申请的空间,然后将点阵数组中的数据拷贝到申请的空间中,最后将SimplifiedFont结构体添加到SimpleFontPackageList中。如下所示是戴正华所写的源码目录下main.c文件中的一段程序。
EFI_STATUS CreatesimpleFontPkg(EFI_WIDE_GLYPH* WideGlyph, UINT32 SizeInBytes)
{
EFI_STATUS Status;
EFI_HII_SIMPLE_FONT_PACKAGE_HDR *simpleFont;
UINT8 *Package;
// Locate HII Database Protocol
EFI_HII_DATABASE_PROTOCOL *HiiDatabase = 0;
Status = gBS->LocateProtocol ( &gEfiHiiDatabaseProtocolGuid, NULL, (VOID **) &HiiDatabase );
UINT32 packageLen = sizeof (EFI_HII_SIMPLE_FONT_PACKAGE_HDR) + SizeInBytes + 4;
Package = (UINT8*)AllocateZeroPool (packageLen);
WriteUnaligned32((UINT32 *) Package,packageLen);
simpleFont = (EFI_HII_SIMPLE_FONT_PACKAGE_HDR *) (Package + 4);
simpleFont->Header.Length = (UINT32) (packageLen - 4);
simpleFont->Header.Type = EFI_HII_PACKAGE_SIMPLE_FONTS;
simpleFont->NumberOfNarrowGlyphs = 0;
simpleFont->NumberOfWideGlyphs = (UINT16) (SizeInBytes / sizeof (EFI_WIDE_GLYPH));
UINT8 * Location = (UINT8 *) (&simpleFont->NumberOfWideGlyphs + 1);
CopyMem (Location, WideGlyph, SizeInBytes);
EFI_HII_HANDLE simpleFontHiiHandle = HiiAddPackages (
&gSimpleFontPackageListGuid,
NULL,
Package,
NULL
);
if(simpleFontHiiHandle == NULL) {
return (EFI_STATUS)-1;
}
FreePool (Package);
return EFI_SUCCESS;
}
三、UEFI SHELL中显示中文
首先我们参考MdeModulePkg/Universal/Console/GraphicsConsoleDxe/GraphicsConsole.c中添加英文字库的方式,先在MdeModulePkg/Universal/Console/目录下新建一个HansFontDxe目录,这里取名HansFont表示它是一个中文字库。在该目录下新建一个HansFontDxe.inf描述文件,同时把戴正华写的源码目录下的main.c程序文件和example.data字库数据文件拷贝过来。HansFontDxe.inf文件内容如下
[Defines]
INF_VERSION = 0x00010006
BASE_NAME = HansFontDxe
FILE_GUID = 4ea97c46-7491-4dfd-b442-74798713ce5f
VERSION_STRING = 0.1
MODULE_TYPE = UEFI_DRIVER
ENTRY_POINT = LoadFont
[Sources]
main.c
[Packages]
MdePkg/MdePkg.dec
MdeModulePkg/MdeModulePkg.dec
[LibraryClasses]
UefiDriverEntryPoint
UefiLib
HiiLib
BaseMemoryLib
UefiHiiServicesLib
[Protocols]
gEfiPciIoProtocolGuid
gEfiGraphicsOutputProtocolGuid
gEfiHiiDatabaseProtocolGuid
gEfiHiiImageProtocolGuid
gEfiHiiConfigRoutingProtocolGuid
gEfiHiiStringProtocolGuid
gEfiSimplePointerProtocolGuid
[BuildOptions]
GCC:*_*_IA32_CC_FLAGS = -O2
MSFT:*_*_*_CC_FLAGS = -DNEFI_SHELL_FILE_PROTOCOL -DUSE_SIMPLE_STDIO /GL- /wd4804 /wd4201 /Oi- /FAs
"MODULE_TYPE = UEFI_DRIVER "指明了这个目录下的工程为一个UEFI驱动模块。
"ENTRY_POINT = LoadFont"指明了驱动的入口函数,该函数位于main.c中,它会调用CreatesimpleFontPkg()函数,从而注册中文字库。
接下来参考"MdeModulePkg/Universal/Console/GraphicsConsoleDxe/GraphicsConsoleDxe.inf"文件的引用方式,将"MdeModulePkg/Universal/Console/HansFontDxe/HansFontDxe.inf"文件应用添加到EmulatorPkg/EmulatorPkg.dsc和EmulatorPkg/EmulatorPkg.fdf中,如下图所示:
其中添加到Emulator.fdf文件中的*.inf模块文件描述,编译时会把该模块编译的最终的efi固件中,如果该模块是驱动类型的话,在固件初始化的时候,会自动调用驱动模块的ENTRY_POINT指定的函数。
现在重新回到TestoneApp.cpp程序中来,重新编译运行Emulator工程,进入到shell后,再次运行TestoneApp.efi程序的话,就能显示中文字符了,结果如下图所示:
四、UEFI设置界面中显示中文
默认状态下UEFI的设置界面如下图所示:
搜索对应的英文字符串可以在源码中找出主界面下显示字符串的定义在"MdeModulePkg/Application/UiApp/FrontPageStrings.uni"文件中,里面由英文和法文的字符串定义显示,我们可以把里面的法文字符串删掉,替换成中文的(不删掉法文,直接添加中文定义也可以)。更改比较简单,如下图所示:
添加完中文字符串后,需要到EmulatorPkg/EmulatorPkg.dsc工程文件中添加语言设置,设置下可选语言与默认语言,我们这里只保留中文和英文语言的选择即可。
更改完成后,再次编译运行EmulatorPkg工程,进入到UEFI配置界面,显示效果如下图所示:
可以看到,中文显示并不完整,只显示了左边一半,同时,部分菜单栏也没有翻译过来。中文字符显示不完整的问题,应该与UiApp中计算显示位置与显示长度的计算问题有关,这里最快解决办法就是在中文字符串后面加同等数量的空格就行,比如STR_RESET_STRING,把它对应的中文字符串从"复位“改为"复位 ",比较长的字符串需要换行显示的也是这样更改,示例程序如下:
...
#string STR_LANGUAGE_SELECT_HELP #language en-US "This is the option one adjusts to change the language for the current system"
#language zh-Hans "这是一个更改 当前系统语言 的选项 "
#string STR_MISSING_STRING #language en-US "Missing String"
#language zh-Hans "字符串缺失 "
#string STR_EMPTY_STRING #language en-US ""
#language zh-Hans ""
#string STR_RESET_STRING #language en-US "Reset"
#language zh-Hans "复位 "
#string STR_RESET_STRING_HELP #language en-US "Reset the current setting."
#language zh-Hans "复位当前的设置 "
...
部分菜单栏没有显示出中文,是英文它们的翻译对应字符串并不在FrontPageString.uni,继续在edk2工程中搜索对应的英文字符串,然后添加它的中文字符串即可,比如"Device Manager"字符串就位于"MdeModulePkg/Library/DeviceManagerUiLib/DeviceManagerStrings.uni"文件中,更改示例如下:
中文字符串添加完成后,再次编译运行EmulatorPkg工程,最终UEFI配置界面显示如下图所示:
可以看到,UEFI配置界面增加了中文显示。