0
点赞
收藏
分享

微信扫一扫

UEFI中的界面设计(一)


本文目录!
界面设计概论
BDS引导到UIAPP
HII驱动
UNI文件
VFR文件
FORMSET
FORM
VARSTORE
TEXT
LABEL
其他
最近
我从显示驱动搞到显示LOGO和显示界面、再到做界面、改框架,一路走来很是坎坷,但或许会有意义所在吧?
一直想写点东西,然而真没空懒 ,三月末疫情哐的一下给我锁家里了,好了,可算是有空了。
废话说的太多,直接上笔记好了,为了不违规用的都是EDK2官方的代码,可以直接找对应的文件对应着看一下!

界面设计概论
最新的EDK2代码,构造界面框架大概如下

稍微解释一下
HiiDriver:最上层的界面实现,由.uni、.vfr和.c组成,这里我只画了一个,其实可以有很多个,以lib的形式被UiApp集成。
SetupBrowser: 控件实现,简单的说,这个模块做了很多工具,在HiiDriver中你可以使用某些工具、根据需求去完成这个界面。界面嘛,不同厂商可能想做不同的,但又有些逻辑可以复用,为了更好的完成以上需求,这个模块把自己分成了三部分。除自己外的另两个都可以由厂商自行实现——它甚至给自行实现的部分也分了级,CustomizedDisplayLib基本都要自己实现,Text Display Engine可以根据情况看看,它真的好温柔,我哭死(
Text Display Engine:对于EDK2界面框架,我对它的理解类似于一种界面编码,通过编码传递信息和解释界面,这个模块就主要针对这部分,后面细写的时候再说吧。
CustomizedDisplayLib:这是各个厂商基本都要重新实现的,通过它可以改变颜色和修改控件布局。
UiApp:这个模块是页面的集成,BDS通过找到它进入界面,这个详细流程后面会讲。

看完上面这些,相信你已经胸(一)有(脸)成(懵)竹(B)了,所以我们从最上层开始说。

BDS引导到UIAPP
EDK2的BDS模块从IntelFrameworkModulePkg升级到MdePkg中现有的BdsEntry,其实在启动项方面变化不大,更多的在界面这里(个人认为)。
打开UiApp的INF文件,你会发现这个文件的类型是UEFI_APPLICATION

[Defines]
 INF_VERSION = 0x00010005
 BASE_NAME = UiApp
 MODULE_UNI_FILE = UiApp.uni
 FILE_GUID = 462CAA21-7614-4503-836E-8AB6F4662331
 MODULE_TYPE = UEFI_APPLICATION
 VERSION_STRING = 1.0
 ENTRY_POINT = InitializeUserInterface

对于UEFI_APPLICATION类型的模块,我们无法像驱动一样通过FDF中的INF去引导。正确的方法是首先,把它放在DSC中,让它被编译成二进制。通过句柄在FV中找到对应文件,然后通过StartImage去执行。
从底层来看,BDS也是这么做的,但它用了比较巧妙的方式——要知道,BIOS的主要功能是引导进GRUB(或者其他的),GRUB也是一个二进制文件。既然都是引导二进制文件,那它们能不能复用逻辑呢?巧了,EDK2就是这么想的。它调用BmRegisterBootManagerMenu函数把界面二进制文件当成启动项来注册,打开这个函数,你可以看到寻找二进制文件的两种方式

gBS->LocateHandleBuffer (
 ByProtocol,
 &gEfiLoadFileProtocolGuid,
 NULL,
 &HandleCount,
 &Handles
 );
 for (Index = 0; Index < HandleCount; Index++) {
 if (BmIsBootManagerMenuFilePath (DevicePathFromHandle (Handles[Index]))) {
 DevicePath = DuplicateDevicePath (DevicePathFromHandle (Handles[Index]));
 Description = BmGetBootDescription (Handles[Index]);
 break;
 }
 }

这是第一种,寻找现有句柄中所有打开gEfiLoadFileProtocolGuid这一协议的,遍历对应句柄的设备路径,如果是BootMenu类型的就返回找到了
那么怎么判断BootMenu类型呢?答案是

CompareGuid (NameGuid, PcdGetPtr (PcdBootManagerMenuFile));
1
这个Pcd的内容可以在DEC文件里面查看

OvmfPkg/AmdSev/AmdSevX64.dsc:510:
 gEfiMdeModulePkgTokenSpaceGuid.PcdBootManagerMenuFile|
 { 0x21, 0xaa, 0x2c, 0x46, 0x14, 0x76, 0x03, 0x45, 0x83, 0x6e, 0x8a, 0xb6, 0xf4, 0x66, 0x23, 0x31 }

眼熟吗,这一串拼出来其实是上面那个INF文件中的FILE_GUID,怎么样,对于INF文件是不是有了更深刻的认识。

第二种方式和上面的大同小异

if (DevicePath == NULL) {
 Status = GetFileDevicePathFromAnyFv (
 PcdGetPtr (PcdBootManagerMenuFile),
 EFI_SECTION_PE32,
 0,
 &DevicePath
 );
 if (EFI_ERROR (Status)) {
 DEBUG ((DEBUG_WARN, “[Bds]BootManagerMenu FFS section can not be found, skip its boot option registration\n”));
 return EFI_NOT_FOUND;
 }


理解到这里,引导界面的方法呼之欲出,

EFI_STATUS Status;
 EFI_BOOT_MANAGER_LOAD_OPTION BootManagerMenu;Status = EfiBootManagerGetBootManagerMenu (&BootManagerMenu);
 EfiBootManagerBoot (&BootManagerMenu);

两个函数都是官方LIB提供的,其中,EfiBootManagerBoot就是启动项启动的函数,也就是说,我们可以把界面当成一个特殊的启动项来理解。

那么,现在我们从BDS找到了UiApp,加载了里面的内容。现在压力来到了后者。

HII驱动
UiApp其实就是Hii驱动的集合,所以我们先来说HII驱动好啦。
HII,中文译名人机交互接口(我翻译的……),我们能看见、操作界面、能更换语言、能使用之前预存的字符,都是拜这个模块所赐。
之前已经说过了,比较健全的HII驱动包括.uni、.vfr、.c和.h四种文件。先说比较简单的.uni吧。

UNI文件
很多文件都有.uni,用来保存模块名称和模块作用。这里引用的是官方代码BootManager的uni文件,直观一些。

#langdef en-US “English”
 #langdef fr-FR “Français”#string STR_BM_BANNER #language en-US “Boot Manager”
 #language fr-FR “Boot Manager”
 #string STR_BOOT_MANAGER_HELP #language en-US “This selection will take you to the Boot Manager”
 #language fr-FR “This selection will take you to the Boot Manager”
 #string STR_HELP_FOOTER #language en-US “Use the <↑> and <↓> keys to choose a boot option, the key to select a boot option, and the key to exit the Boot Manager Menu.”
 #language fr-FR “<↑> pour <↓> changer l’option, choisir une option, pour sortir”
 #string STR_AND #language en-US " and "
 #language fr-FR " et "
 #string STR_BOOT_OPTION_BANNER #language en-US “Boot Manager Menu”
 #language fr-FR “le Menu d’Option de Botte”
 #string STR_ANY_KEY_CONTINUE #language en-US “Press any key to continue…”
 #language fr-FR “Appuie n’importe quelle pour continuer…”
 #string STR_LAST_STRING #language en-US “”
 #language fr-FR “”

简单的说,这个就是字符串资源文件,左边是字符串的TOKEN,右边是对应TOKEN的、不同语言下的字符串。
这个文件是在预处理前被autogen工具处理的,所以,它不支持宏、ifdef之类的修饰符。还有一点比较有趣的是并不是所有字符串都会被编译,只有TOKEN在其他文件中被引用时才会被编译打包,这是一种减少不必要数据的机制。
引用它的方式也很简单

STRING_TOKEN(STR_BM_BANNER)
1
这样就可以了。
顺便一提,界面的文字转换主要就是运用这个文件,通过指定对应全局变量修改所有TOKEN引用的语言。

VFR文件
如果说.uni是字符串的资源文件,vfr就是窗体的资源文件,里面记录着各个控件的位置和使用方式。
刚接到做界面任务时,我是非常有信心的,我好歹也是QT选手欸!设计过很复杂的界面的!然后就被这玩意制裁了,真的,写界面时没有一刻不怀念QT上万页的说明文档。
这里记一下我用过的控件们

FORMSET
 formset
 guid = FORMSET_GUID,
 title = STRING_TOKEN(STR_BM_BANNER),
 help = STRING_TOKEN(STR_BOOT_MANAGER_HELP),
 classguid = gEfiIfrFrontPageGuid,
 ……
 endformset;

主窗体,这个应该都见过哦。
guid:不提了,不要重复就行。
title:显示在屏幕最上方(这个可以在Browser里面改)
help:显示在下面(也可以改)
classguid:窗体的GUID,可以用来判断窗体是否是同一级的,比如说这个函数

UiListThirdPartyDrivers (HiiHandle, &gEfiIfrFrontPageGuid, NULL, StartOpCodeHandle);
1
就是通过classguid判断系统里有没有这个级的HII驱动,如果有,就创建一个摁纽作为跳转到这个驱动的入口——官方代码中的菜单界面,就是这么构成的。这玩意是不能写死的,总不能你这个项目不想要启动管理界面,那个写死的界面就空着吧。

主窗体是界面设计里面的最大单位,一切内容都要在窗体内部,即formset与endformset中间。

FORM
普通窗体,一个主窗体可以有很多普通窗体,至少一个。

form formid = BOOT_MANAGER_FORM_ID,
 title = STRING_TOKEN(STR_BM_BANNER);
 ……
 endform;

formid:顾名思义,可以用于标识不同FORM
title:显示在Formset下的第一行
窗体内部就是界面的内容了

VARSTORE
 varstore LEGACY_BOOT_NV_DATA,
 varid = VARSTORE_ID_LEGACY_BOOT,
 name = LegacyBootData,
 guid = LEGACY_BOOT_OPTION_FORMSET_GUID;

非常重要的一个控件,基本需要用户交互的都有
从本质上讲,资源文件与代码相互传递信息用的是全局变量。这个结构就是存储这个窗体需要的所有全局变量的地方。其中,varstore后面跟着的是全局变量的成员,自己定义的,例如上文的这个

typedef struct {
 UINT16 LegacyFD[MAX_MENU_NUMBER];
 UINT16 LegacyHD[MAX_MENU_NUMBER];
 UINT16 LegacyCD[MAX_MENU_NUMBER];
 UINT16 LegacyNET[MAX_MENU_NUMBER];
 UINT16 LegacyBEV[MAX_MENU_NUMBER];
 } LEGACY_BOOT_NV_DATA;

值得注意的是,vfr只能识别UINTN/8/16/32/64五种类型,所以结构的成员只能是这些。还有一点是一定要记得,vfr文件需要包含拥有这个类型声明的头文件,不然会给你报一个很诡异的编译错。我查这个编译错查了整整一天,呵呵!
对了,头文件的本质是链接,所以包含这个声明的头文件里,除了它自己,其他声明里也不能有其他数据结构,你最好为了它单建一个头文件……
name:这个全局变量的名字,在后面的控件中有应用。
guid:身份证号,没有什么好说的。

与这个控件功能类似的是efivarstore,写法也类似,会多一个和全局变量一样的attribute

TEXT
虽然它叫TEXT,但从某种程度上来说,可以分成文字和摁纽两种控件。

subtitle text = STRING_TOKEN(STR_DEVICES_LIST);
1
这是第一种用法,就是普通的一行一列文字,后面通过TOKEN提取UNI中的字符串

suppressif TRUE;
 text
 help = STRING_TOKEN(STR_LAST_STRING ),
 text = STRING_TOKEN(STR_LAST_STRING ),
 text = STRING_TOKEN(STR_LAST_STRING ),
 endif;

这是第二种用法,一行两列文字,可以用来显示固件信息之类的。光标不会出现在上面,我一般叫它“死文字”。
当然,它的内容其实是可以修改的,不过不是用户来改。通过HII封装好的SetString可以修改UNI中对应TOKEN保存的内容,这样可以改变界面上显示的文字——所以我会用它显示固件信息,可以在初始化时调用函数获取,然后SetString,这样在引导进界面时,信息就会出现在屏幕上。
哦,顺便讲一下suppressif,其实就是VFR中的ifdef,后面可以接判断条件,如果满足就加载包含内容,不满足就跳过。比如,后面的条件是VARSTORE的某一成员是否为1,这样,可以在代码中修改这个值来修改界面元素。

text
 help = STRING_TOKEN(STR_LAST_STRING ),
 text = STRING_TOKEN(STR_LAST_STRING ),
 flags = INTERACTIVE,
 key = 0x1212;

这种写法更像摁纽,选中后点击会回调代码中的函数
text:摁纽上面的内容
help:光标移到摁纽时右侧(可以改)的内容
flag:性质,这里这个表示这个摁纽可以触发回调函数
key:用于在回调函数中判断是哪个摁纽被触发了——因为回调函数是公用的,该界面所有有回调FLAG的控件都会调回去,得有标志区分它们。

LABEL
标签,用于在代码中生成控件

label LABEL_FRANTPAGE_INFORMATION;
 label LABEL_END;
 1
 2


有些时候,界面的内容需要根据现状更改。比如启动项界面,在设计时我们并不知道现在有几个启动项,这就需要这个控件去占位,然后在代码里替换掉这些占位符。
使用方法可以参照官方代码的UpdateFrontPageForm函数,注意两点,一是写控件的参数,二是写完控件后要调用HiiUpdateForm更新界面信息。

其他
STRING:可以在界面显示一个可选中的文本信息,点击后出现一个框可以让用户输入,输入后点击回车可以保存
ONEOF:可以在界面显示一个可选中的文本信息,点击后出现一个框,里面是写好的选择,用户可以选中任一,类似于主界面选择语言的
NUMERIC:可以在界面显示一个可修改的数字信息
……
这些都是交互类控件,大同小异。举STRING为例

oneof varid = BootDiscoveryPolicy.BootDiscoveryPolicy,
 prompt = STRING_TOKEN(STR_FORM_BDP_MAIN_TITLE),
 help = STRING_TOKEN(STR_FORM_BDP_MAIN_TITLE),
 flags = NUMERIC_SIZE_4 | INTERACTIVE | RESET_REQUIRED,
 option text = STRING_TOKEN(STR_FORM_BDP_CONN_MIN), value = BDP_CONNECT_MINIMAL, flags = DEFAULT;
 option text = STRING_TOKEN(STR_FORM_BDP_CONN_NET), value = BDP_CONNECT_NET, flags = 0;
 option text = STRING_TOKEN(STR_FORM_BDP_CONN_ALL), value = BDP_CONNECT_ALL, flags = 0;
 endoneof;

varid:后面跟着varstore里存储的数据名称.对应成员
prompt:显示在左边的文字
help:选中后出现在右侧提示栏的文字
flag:属性
这些都是这些交互类控件公用的,其他的根据自己的性质有不同,比如这里后面是oneof里面的选项,default说明这是默认选项。STRING中有最短输入字符和最长之类的。
可以在代码中调用HiiGetBrowserData获取当前界面中用户输入的内容


举报

相关推荐

0 条评论