字符串
UEFI 中字符串有两种,一种是 Unicode16 编码 的字符串,另一种是 ASCII 字符串。
Unicode16 字符串是 UEFI 默认使用的字符串;
ASCII 字符串以 \0
结尾,每个字符占用 1 字节。
L"Hello World"
是 Unicode16 字符串,而 “Hello World”
是 ASCII 字符串。
字符串函数
凡是操作 ASCII 字符串的函数其名字中都以 Ascii
为前缀,没有 Ascii
前缀的字符串函数都是 Unicode16 字符串函数。
StrLen
函数用于返回字符串中字符的个数,字符个数不包含结尾的NULL
字符;
StrSize
函数用于返回字符串占用的字节数,包含结尾的NULL
字符;
StrCpy
函数用于将源字符串复制到目的内存中,并返回复制后的目的字符串地址。调用者负责内存的分配与释放;
StrnCpy
函数用于将源字符串复制到目的内存中,但至多复制指定的字节数,并返回复制后的目的字符串地址。调用者负责内存的分配与释放;
StrCmp
函数用于比较两个字符串,返回第一对不匹配的两个字符之间的大小关系;
StrnCmp
函数用于比较两个字符串,但至多比较指定个数(Length
)的字符,返回第一对不匹配的两个字符之间的大小关系,两个字符串前Length
个字符相同则返回 0
;
StrCat
函数用于拼接字符串,即将第二个字符串复制到第一个字符串的末尾。调用者负责内存的管理;
StrnCat
函数用于拼接字符串,即将第二个字符串复制到第一个字符串的末尾,但至多复制指定的字符个数。调用者负责内存的管理;
StrStr
用于在第一个字符串中查找第二个字符串,并返回匹配处的首地址,若没有找到,则返回 NULL;
StrDecimalToUintn
返回十进制数字符串表示的无符号整数(UINTN
);
StrDecimalToUint64
返回十进制数字符串表示的无符号64位整数(UINTN64
);
StrHexToUintn
返回十六进制数字符串表示的无符号整数(UINTN
)。字符串可以以0x
开头,也可以以任何十六进制符号开头;
StrHexToUint64
返回十六进制数字符串表示的无符号64位整数(UINTN64
);
UnicodeStrToAsciiStr
用于将Unicode16
字符串转换为ASCII字符串,转换时Unicode16
字符的低8
位复制到ASCII字符串。调用者负责内存管理。
操作 ASCII 字符串的函数用法与这些操作 Unicode16 字符串的函数相似;
程序开发中经常会用到一些字符串常量,如菜单中的字符串、向用户显示的消息字符串等。这些字符串内容不会改变,并且每个字符串在不同语言下有不同的翻译,对这些字符串最常用的操作是取出并显示。
字符串资源
EDK2 提供了一套方案用于管理这些字符串,被管理起来的这些字符串称为字符串资源,这些字符串资源都是 Unicode16
字符串。Unicode
字符几乎囊括了世界上所有语言用到的字符,每种语言都有自己的编码区间。
字符串资源文件 .uni
,编译时 EDK 会把它转化为 C 语言能够识别的格式:
#langdef en-US "English"
#langdef fr-FR "Francais"
#langdef zh-Hant "繁體中文"
#langdef zh-Hans "简体中文"#string STR_LANGUAGE_SELECT
#language en-US "Select Language"
#langeuge fr-FR "Choisir la Langue"
#language zh-Hant "選擇語言"
#language zh-Hans "选择语言"
#langdef
用于声明本字符串资源文件所支持的语言,第一个参数是语言的名字(字符串代码),第二个参数是该语言的可显示名字字符串。
#string
用于定义字符串。第一个参数是字符串标识符。其后是不同形式语言中的字符串,每个字符串以 #language
开头。#language
的第一个参数是语言的名字,第二个参数是该语言下的字符串。
编译后,EDK2 会将.uni
资源文件编译成头文件和 C 源代码。在头文件里面定义了字符串的编号。C源代码放在 AutoGen.c
文件里。
然后在源程序中通过 HiiAddPackages
注册字符串资源,通过字符串标识符使用对应的字符串。
UEFI 提供了 EFI_HII_STRING_PROTOCOL
以管理字符串资源。EDK2 提供的 HiLib
包含了用于管理字符串资源的函数。
通过 EFI_HII_STIRNG_PROTOCOL
管理字符串:
.uni
文件里的字符串资源最终会注册到系统 HII 数据库中形成 包列表。字符串 Protocol EFI_HII_STRING_PROTOCOL
用于管理 HII 数据库中指定包列表的字符串资源,包括检索添加、更新字符串以及检索字符串资源包列表所支持的语言。EFI_HII_STRING_PROTOCOL
是一种服务型 Protocol,因而 整个系统仅需要一个 EFI_HII_STRING_PROTOCOL
实例。
1)字符串 Protocol 的 NewString
服务,用于添加字符串到指定的 PackageList
中,并返回该字符串的标识符 Stringld
。该标识符在这个 PackageList
中是唯一的。
添加新的字符串资源时,可以同时指定该字符串的字体信息EFI_FONT_INFO
。字体信息是可选参数,如果为NULL
,则使用默认的系统字体、大小和风格。字体信息中包括了字体风格、字符高度和字体名称。
字体信息(EFI_FONT_INFO
)中的字体风格EFI_HII_FONT_STYLE
是32位无符号整数。UEFI定义了8种字体风格。
2)字符串Protocol的GetString
服务用于从PackageList
中找出ID
为Stringld
、语言为Language
的字符串。检索出的字符串将复制到缓冲区String
中,它是 EFI_STRING
类型的变量,而EFI_STRING
是CHAR16*
类型。
*StringSize
作为输入参数,表示缓存区的大小;作为输出参数,表示字符串的长度。如果缓冲区的大小小于检索出的字符串所需的内存大小,则 *StringSize
返回字符串所需内存大小,调用者应重新分配缓冲区后再次调用GetString
服务。
StringFontInfo
是可选参数,若其不为 NULL
,则*StringFontInfo
返回指向EFI_FONT_INFO
的指针,调用者负责释放该内存。
3)字符串Protocol 的 SetString
服务用于在 PackageList
中更新ID
为 Stringld
、语言为 Language
的字符串。StringFontlnfo
是可选参数,若非 NULL
,则字符串使用此字体;若为 NULL
,则保持不变。
4)字符串 Protocol 的 GetLanguages
服务用于返回 PackageList
支持的所有语言。Languages
作为返回值,是一个标准字符串,该字符串包含了所有支持的语言的代码,语言代码以分号分隔。
5)每个字符串包列表都有一个主语言,零个或多个次要语言。字符串Protocol的GetSecondaryLanguages
服务用于查询包列表的次要语言。若所查询包列表的次要语言为空则通过*SecondaryLanguagesSize
返回0
。
通过 HiiLib
使用字符串Protocol
的服务
除了直接使用EFI_HII_STRING_PROTOCOL
,也可以利用HiLib
提供的函数管理字符串资源。HiiLib
是 EDK2 提供的HII
操作库,主要是对 Hii Protocol(如字符串Protocol、字体 Protocol等)的封装,还包括一些辅助函数。
。。。。省略。。。
管理语言
1.系统的当前语言及更改方法: UEFI 系统的当前语言存放在 UEFI 全局系统变量L"PlatformLang"
中。更改全局变量 L"PlatformLang"
即可更改系统的语言。全局变量 L"PlatformLangCodes"
存放了系统所支持的全部语言。全局系统变量要通过运行时服务提供的 GetVariable
和 SetVariable
服务访问读取和设置。
2.操作语言的辅助函数:EKD2提供了几个语言相关的函数,这些函数不在 UEFI 规范范围内,是 EDK2 提供的库函数。这些函数包括 HiiGetSupportedLanguages
、GetNextLanguage
及GetBestLanguage
。
包列表
UEFI 将资源组织在包(也可称为 HII
包)中。几个不同的包组织在一起构成 包列表(Package List)。包列表由 GUID
、包列表大小以及一系列包组成。每个包列表以EFI_HII_PACKAGE_END
类型的包结束。
UEFI 规范定义的包类型:
图像界面显示
在文本模式下,可以使用Print
系列的函数显示字符串。在图形模式下,要使用EFI_GRAPHICS_OUTPUT_PROTOCOL
(简称GraphicsOut
)将图像显示到屏幕上。EFI_GRAPHICS_OUTPUT_PROTOCOL
包含三个成员函数和一个成员变量。
大部分系统只有一个显示设备,可以通过 Boot Service
的 LocateProtocol
获得 GraphicsOut
实例。
显示模式
显示模式包括分辨率、颜色深度等。可以通过QueryMode(…)
查询显示模式,通过 SetMode(…)
设置显示模式,通过指针 Mode
读取当前的显示模式。
Mode
是指向 EFI_GRAPHICS_OUTPUT_PROTOCOL_MODE
的指针:
一般而言,系统已经初始化,系统帧缓冲区的物理地址及大小就不会再发生改变,而其他属性(包括分辨率、像素颜色深度)是可以改变的,因而这些会发生改变的信息(称为“显示模式信息”)放在 Info
指向的内存区域。
显示模式信息的像素格式 EFI_GRAPHICS_PIXEL_FORMAT
的四个类型:
当 PixelFormat
是PixelBitMask
时,像素的格式由显示模式信息的PixelInformation
决定。PixelInformation
是EFI_PIXEL_BITMASK
类型的变量:
RedMask
共32bit
,其中为1
的位是红色分量的有效位。
而像素格式为 PixelBltOnly
,则意味着帧缓冲区的线性物理地址无效,不能通过向g_GraphicsOutput->FrameBuferBase
中写内容而改变屏幕。在这种格式下,只能用gGraphicsOutput->Blt
读写屏幕。
QueryMode
的函数原型:
QueryMode(…)
用于获得模式 ModeNumber
的模式信息。模式信息由Info
返回,Info
指向的内存由 QueryMode
负责分配,大小由 SizeOfInfo
返回。Info
的大小之所以由 SizeOfInfo
决定,是因为EFI_GRAPHICS_OUTPUT_MODE_INFORMATION
在不同的 UEFI 版本中的格式会有所不同,不能由 sizeof(EFI_GRAPHICS_OUTPUT_MODE_INFORMATION)
确定大小。调用者负责释放Info
指向的内存。
SetMode
的函数原型:
如果设置系统显示模式采用编号的ModeNumber
的模式,那么ModeNumber
取值必须在[0,This->MaxMode-1]
。若指定的模式不存在,则返回 EFI_UNSUPPORTED
。
Block Transfer(Blt)传输图像
GraphicsOut
可以让我们操作显卡中的帧缓冲(FrameBuffer)。其主要操作是通过 GraphicsOut
的 Blt
服务实现的。
通过 Blt 可以执行如下四种操作:
1.将整个屏幕填充为某个单一颜色;
2.将图像显示到屏幕;
3.将屏幕区域复制到图像;
4.复制屏幕区域到屏幕另一片区域。
EFI_GRAPHICS_OUTPUT_BLT_PIXEL
定义了像素的格式,在一些老旧的显卡的帧缓冲中,一个像素占3
字节,而现在的显卡帧缓冲都是4
字节了。读取BMP图像的时候要小心,因为 BMP 中每个像素占3
字节。从 BMP 文件读取的图像要转换成EFI_GRAPHICS_OUTPUT_BLT_PXEL
格式。
EFI_GRAPHICS_OUTPUT_BLT_OPERATION
定义了 GraphicsOut
允许的操作。
在图形界面下显示字符串
若要在图形界面下显示字符串,则需要 字体(Font) 及字体 Protocol 的支持。UEFI 中的 HiiDatabase
有两种类型的字体,这两种字体分别是EFI_HII_PACKAGE_SIMPLE_FONTS
和EFI_HII_PACKAGE_FONTS
。
其中,EFI_HII_PACKAGE_SIMPLE_FONTS
结构简单,但性能较差,适合在显示少量字符时使用;EFI_HII_PACKAGE_FONTS
复杂,但查找字符的性能较好。
字体 Protocol EFI_HII_FONT_PROTOCOL
可以把字符串转化为位图,然后可以用 Blt 把字符串位图显示到屏幕上。
StringTolmage
函数将字符串根据 StringInfo
给定的格式渲染到目的位图Blt
中,如果参数Blt
指向屏幕并且Flags
中包含EFI_HII_DIRECT_TO_SCREEN
,则字符串位图输出到屏幕。
StringInfo
是EFI_FONT_DISPLAY_INFO
类型的变量,是可选参数,包含了前景和背景色,以及字体的大小、名字等。若StringInfo
为 NULL
则使用系统默认值。EFI_FONT_DISPLAY_INFO
的 FontInfoMask
定义了使用Fontlnfo
的方式,通常可以设为EFI_FONT_INFO_ANY_FONT
,这样 UEFI 会首先查找 HiiDatabase
中有没有 Fontlnfo
指定的 Font
,如果没有,则使用SimpleFont
。
FontInfoMaSk
是EFI_FONT_INFO_MASK
(定义为32为无符号整数)类型的变量,其有效预定义值:
参数BIt
是 EFI_IMAGE_OUTPUT
类型的变量,它定义了目的位图的大小及位图地址。如果目的位图地址为NULL
,则该StringToImage
服务创建一个新的位图作为目的位图。如果目的位图指向屏幕,则字符串位图输出到屏幕。
用 SimpleFont 显示中文
字符串的显示还需要字体的支持。然而 EDK2 仅提供了英语和法语的字体库(或称字库),其他语言的字库需要开发者来实现。
SimpleFont 格式
SimpleFont 是一种点阵字体,有两种字体格式,一种是窄字符,另一种是宽字符。窄字符用 8(宽度) x 19(高度)
个 bit
的点阵表示。宽字符用16x19
个bit
的点阵表示,前8x19
个bit
表示左半部分,后8x19
个bit
表示右半部分。每个bit
表示一个像素(1
表示显示该像素;0
表示不显示)。
字体资源也要注册到 HII
数据库中形成包才能使用。SimpleFont 包的包头数据结构:
由EFI_HII_SIMPLE_FONT_PACKAGE_HDR
结构体可以看出,SimpleFont
包中的字符数据是以数组的形式附在EFI_HII_SIMPLE_FONT_PACKAGE_HDR
后面。每个 SimpleFont
包包含一个EFI_NARROW_GLYPH
数组及一个EFI_WIDE_GLYPH
数组。
SimpleFont 格式:
生成字体文件:
1)建立画布,取得画布上下文 context
;
2)依次处理 Unicode16
编码从0x4E00
到0x9FA5
的每一个字符;
3)将字符写到 context
区域;
4)从context
区域取出16x19
位图;
5)将该位图转化为EFI_WIDE_GLYPH
格式。
注册字体文件:
注册字体文件,即将字体点阵数据注册到HI数据库中,生成包列表。首先生成字体包组合,然后调用HiiAddPackages
注册到系统内。字体包组合由包组合头、SimpleFont包头和点阵数组组成。
字体 Font
SimpleFont
的优点是格式简单,制作和使用容易,但缺点也很明显是性能较差,渲染任一个字符都需要在SimpleFontPackage
中从头依次检索;二是功能有限,只有Narrow(8x19)
和Wide(16x19)
两种格式的字符。
为了获得更好的性能或者渲染复杂的字符,UEFI提供了Font
格式的字体。
Font 的格式
同 SimpleFont
相比,Font
提供了更加复杂和灵活的点阵格式。它不再限制字符点阵大小,同时增加了边距(OffsetX
和OffsetY
)及步长。
Font 点阵信息 EFI_HII_GLYPH_INFO
的结构体:
内框(实线框)表示字符的宽度和高度,外框(虚线框)的宽度表示AdanceX
,内外框之间的距离为OffsetX
和 OffsetY
。
字体包的格式
SimpleFont
点阵列表没有索引,Font
则采用带索引性质的点阵块列表。
Font
包由两部分组成,首先是Font
包头EFI_HII_FONT_PACKAGE_HDR
,然后是点阵块(Glyph Block)列表。
在Font
包头中,定义了该包中默认的字体信息。点阵块列表中的每个点阵块定义了一个连续区域内字符的字体信息和点阵数据,因为其连续性,所以访问点阵块中的某个字符时就可以用访问数组的方式,从而加快了速度。
EFI_HII_FONT_PACKAGE_HDR
结构体:
Font 包的结构:
GlYPH Block 点阵块列表,由一个或多个 EFI_HII_GLYPH_*_BLOCK
按顺序(字符 Unicode 编码)排列而成,每个块都继承自 EFI_HII_GLYPH_BLOCK
。
Font 性能高于 SimpleFont
对比 SimpleFont
和 Font
的数据结构可以看出,要想检索某个字符的 GLYPH
数据,
对于 SimpleFont
,需要依次取出Simple Package
中的每个EFI_NARROW_GLYPH
或EFI_WIDE_GLYPH
,与待检索字符的 Unicode码做比较,直到取出的 GLYPH
的 Unicode
码与待检索字符的 Unicode
码相同;
对于Font Package
而言,只需依次查询 EFI_HI_GIBT_*_BLOCK
,这些EFI_HI_GIBT_*_BLOCK
相当于为Package的字符设置了索引。
UEFI 事件处理
- CreateEvent:用于生成一个事件对象;
- CloseEvent:用于关闭事件对象;
- SignalEvent:用于触发事件对象;
- WaitForEvent:用于等待事件数组中的任一事件被触发;
- CheckEvent:用于检查 Event 状态;
- SetTimer:用于设置定时器事件属性。
键盘事件
用户按键方式分为两种,一种是单个按键,另一种是组合键。
处理单个按键:如果只是处理单个按键,则可以使用EFI_SIMPLE_TEXT_INPUT_PROTOCOL
(简称 ConIn
),
WaitForKey
是一个普通类型的事件,在有按键时触发。事实上,UEFI 内核为键盘设备生成了一个定时器,每隔一段时间会检查键盘设备,如果发现有按键,则会触发 WaitForEvent
事件。
ReadKeyStroke
用于读取键盘设备的下一个击键。读取击键后会重置 WaitForEvent
事件。如果键盘设备没有击键,则该函数返回EFI_NOT_READY
。
得到系统 EFI_SIMPLE_TEXT_INPUT_PROTOCOL
的实例有两种方法,一种是使用 BS 的 OpenProtocol/LocateProtocal/HandleProtocol
服务打开该Protocol
;另一种是直接使用系统表提供的该Protocol
实例ConIn
。
UINTN Index;
Status = gBS->WaitForEvent(1, &gST->WaitForKey, &Index);EFI_INPUT_KEY Key;
Status = gST->ConIn->ReadKeyStroke(gST->ConIn, &Key);
读取按键一般应遵循以下这两个步骤,首先用 WaitForEvent
服务等待按键的发生,然后用ReadKeyStroke
读取这个按键。得到的Key
是EFI_INPUT_KEY
类型的变量。
常见的按键字符:
处理组合键,需要使用 EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL
,
比起单个按键的 protocol,多了三个函数,即SetState
、RegisterKeyNotify
和 UnregisterKeyNotify
。
EFI_KEY_DATA
包含了按键的编码 EFI_INPUT_KEY
,以及按键时键盘的状态EFI_KEY_STATE
。通过EFI_PKEY_STATE
,可以知道按键的同时有哪些特殊按键被同时按下,以及 <Num Lock>、<Caps Lock> 键功能是否打开。
KeyShiftState
最高位为1
时,KeyShitState
有效,其中某个位为1
表示对应的按键被按下。
KeyToggleState
最高位为1
时,KeyToggleState
有效,某个位为1
表示对应的按键处于打开状态。
读键盘函数 ReadKeyStrokeEx
:
ReadKeyStrokeEx
返回键盘设备输入队列中下一个按键,如果键盘设备输入队列为空,则返回EFI_NOT_READY
。
RegisterKeyNotify
用于注册热键,通过该服务注册的热键回调函数将在用户按下热键后由系统自动执行:
注册成功后,系统为这个热键分配一个 NotifyHandle
,这个 Handle
可以用来访问这个热键。
热键使用完毕后,需调用 UnregisterKeyNotify
注销热键。
鼠标事件
UEFI 提供了 EFI_SIMPLE_POINTER_PROTOCOL
用于获取鼠标事件。
通过 Mode
可以获得鼠标设备的属性。鼠标设备属性是 EFI_SIMPLE_POINTER_MODE
类型的变量。
GetState
函数用于获取鼠标当前状态,鼠标状态是 EFI_SIMPLE_POINTER_STATE
类型的变量。鼠标状态包含了 X、Y、Z 三个方向的位移以及左键和右键是否按下,鼠标状态中 X、Y、Z 仅仅记录了鼠标的(从上次位置到当前位置的)位移,鼠标的位置要由上层应用程序管理和维护。
定时器事件
定时器可分为以下三大类:
1.不带 Notification 函数,其事件类型为EVT_TIMER
。
2.带 Notification 函数,并且 Notification 在定时器到期时执行,其类型为 EVT_TIMER | EVT_NOTIFY_SIGNAL
;
3.带 Notification 函数,并且 Notification 在定时器等待时执行,其类型为 EVT_TIMER | EVT_NOTIFY_WAIT
。
生成定时器事件需要以下两步:
第一步通过BS
的CreateEvent
服务生成一个类型为EVT_TIMER
的定时器事件;
第二步通过BS
的SetTimer
务设置触发时间。
UI 事件服务类
在 UIEvents
类中,键盘事件(UI_KEY
)在最前面,其次为鼠标事件(UI_MOUSE
),最后为定时器事件(UI_TIMER
)。
UIEvents
有两个核心函数:Init
和 Wait
:Init
主要用于初始化事件数组中的三个事件;Wait 用于等待事件的发生。UIEvents还提供了读取键盘、读写鼠标位置及控制定时器的服务。
事件处理框架
事件处理框架包括两大部分,一是事件的侦听,二是事件的响应(或派遣)。事件的侦听和派遣都在事件处理循环中。
事件处理循环是事件处理框架的核心部分,也是事件驱动的核心,任何一个GUI系统都有这样一个循环。
1.事件处理循环
GUI 程序只响应 3 种事件,即鼠标、键盘和定时器事件,控件的绘制和更新在事件处理循环中实现,而不是作为事件被派遣。
a.重绘需要更新的控件,重绘鼠标;
b.等待事件发生;
c.派遣事件至活动控件,并由活动控件响应该事件;
d.如果当前窗口有事件处理函数,则派遣事件至窗口的事件处理函数中。
2.派遣和响应键盘事件
当键盘事件发生时,会由当前活动控件响应按键。
按键分为5类:
a.热键;
b.Tab 键:大部分情况下按键 Tab 键会导致切换焦点;由当前控件的 OneTab() 函数响应;
c.Enter 键:大部分情况下按下 Enter 键时会执行当前控件中的事件处理函数;由当前事件的 OnEnter() 响应;
d.可打印字符键:由当前控件的 OnKey(UINT16 Key)响应;
e.不可打印字符键:ASCII 码为 0 的按键,如方向键。由当前控件的 OnKeyExt(UINT16 Key)响应。
3.派遣和响应鼠标事件
当鼠标事件发生时,首先需要根据鼠标位置查找目标控件,然后由目标控件响应该事件。
4.派遣定时器事件
定时器事件属于全局事件,不隶属于某个特定的控件,因而其派遣函数相比鼠标和键盘事件比较简单。定时器事件首先派发给全局定时器事件处理函数(pUiEvent->HandleTimer())然后派发给窗口处理函数。
5.窗口的事件处理函数
在事件处理循环中,每发生一个事件,首先由全局派遣函数将事件派遣到相应的处理函数,然后将事件派遣到当前窗口的窗口处理函数。
《UEFI 原理与编程》。。。。这些东西的原理挺简单的,但是得用的时候才能理解,这里大概了解一下。。。。。。