在【UEFI实战】UEFI图形显示(显示驱动)中已经介绍了如何使用显卡驱动安装的GOP来进行像素级别的显示,本文介绍的内容是对像素的包装,最终变成普通字符的输出。
本模块将原本的GOP包装成了字符输出的显示模块。GOP输出的最小单元是像素,而经过包装之后,文本模式输出的最小单元变成了一个个的字符。
本模块也是一个UEFI Driver Model,对应的EFI_DRIVER_BINDING_PROTOCOL
:
EFI_DRIVER_BINDING_PROTOCOL gGraphicsConsoleDriverBinding = {
GraphicsConsoleControllerDriverSupported,
GraphicsConsoleControllerDriverStart,
GraphicsConsoleControllerDriverStop,
0xa,
NULL,
NULL
};
Supported函数就是一系列Protocol的判断,包括:
gEfiGraphicsOutputProtocolGuid
或gEfiUgaDrawProtocolGuid
,不过前面一节已经介绍过,gEfiGraphicsOutputProtocolGuid
会被安装,它会被优先使用。gEfiDevicePathProtocolGuid
,对于一个PCI的显卡,这个也是会安装的。gEfiHiiDatabaseProtocolGuid
和gEfiHiiFontProtocolGuid
,两者是UEFI用户界面的基本接口,要使用显示输出,它们也是必须的。尤其是gEfiHiiFontProtocolGuid
,它是像素和字符之间的桥梁,负责完成两者的转换。Start函数的一个主要工作是初始化如下的结构体:
//
// Graphics Console Device Private Data template
//
GRAPHICS_CONSOLE_DEV mGraphicsConsoleDevTemplate = {
GRAPHICS_CONSOLE_DEV_SIGNATURE, // 一个标识,SIGNATURE_32 ('g', 's', 't', 'o')
(EFI_GRAPHICS_OUTPUT_PROTOCOL *)NULL, // 显卡初始化模块安装的Protocol,实际绘制字体的接口
(EFI_UGA_DRAW_PROTOCOL *)NULL, // 如果上一个存在,这个就可以不需要了
{ // EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL基本接口,也是后续UEFI代码操作的接口,完成字符输出及相关操作
GraphicsConsoleConOutReset,
GraphicsConsoleConOutOutputString,
GraphicsConsoleConOutTestString,
GraphicsConsoleConOutQueryMode,
GraphicsConsoleConOutSetMode,
GraphicsConsoleConOutSetAttribute,
GraphicsConsoleConOutClearScreen,
GraphicsConsoleConOutSetCursorPosition,
GraphicsConsoleConOutEnableCursor,
(EFI_SIMPLE_TEXT_OUTPUT_MODE *)NULL // 它指向的就是下面的结构体
},
{ // EFI_SIMPLE_TEXT_OUTPUT_MODE
0, // QueryMode()和SetMode()支持的模式数,由于没有初始化,所以现在默认是0
-1, // 当前的模式,-1表示的是无效的模式
EFI_TEXT_ATTR (EFI_LIGHTGRAY, EFI_BLACK), // 当前字体输出属性,包括前景色和背景色
0, // 光标列位置
0, // 光标行位置
FALSE // 光标是否可见
},
(GRAPHICS_CONSOLE_MODE_DATA *)NULL, // 这个看上去是一个指针,但是实际上是一个数组,表示当前文本显示支持的模式
(EFI_GRAPHICS_OUTPUT_BLT_PIXEL *)NULL // 像素点的属性,其实是一个蓝绿红表示的值
};
对应的Start函数的流程:
整个流程主要是以下的几个步骤:
跟GOP类似,文本显示也有它自己的模式(这里以模式表示GOP的模式,以文本模式表示SimpleTextOutProtocol的模式),不过相比前者这个要简单许多:
/**
@par Data Structure Description:
Mode Structure pointed to by Simple Text Out protocol.
**/
typedef struct {
///
/// The number of modes supported by QueryMode () and SetMode ().
///
INT32 MaxMode;
//
// current settings
//
///
/// The text mode of the output device(s).
///
INT32 Mode;
///
/// The current character output attribute.
///
INT32 Attribute;
///
/// The cursor's column.
///
INT32 CursorColumn;
///
/// The cursor's row.
///
INT32 CursorRow;
///
/// The cursor is currently visible or not.
///
BOOLEAN CursorVisible;
} EFI_SIMPLE_TEXT_OUTPUT_MODE;
具体参数的意义可以看英文说明或者前面的注释。GOP中的显示是以像素为单位的,而文本模式中显示是以一个小的矩形为单位的,每个矩形包含一个字符,这里就通过Column和Row来指定位置。前面的两个结构体成员MaxMode
和Mode
跟GOP中的模式类似,也表示有多个,并指定了其中的一个,这也说明还存在另外的一个数组来表示所有支持的文本模式,它们描述了像素到字符矩形之间的关系,其结构体表示如下:
typedef struct {
UINTN Columns; // 表示文本模式对应的列数
UINTN Rows; // 表示文本模式对应的行数
INTN DeltaX; // 文本显示相对于GOP显示的水平偏移
INTN DeltaY; // 文本显示相对于GOP显示的垂直偏移
UINT32 GopWidth; // GOP显示的宽度,即水平像素个数
UINT32 GopHeight; // GOP显示的高度,即垂直像素个数
UINT32 GopModeNumber; // 对应GOP模式的Index
} GRAPHICS_CONSOLE_MODE_DATA;
这对应到前文提到的数组:
//
// Graphics Console Device Private Data template
//
GRAPHICS_CONSOLE_DEV mGraphicsConsoleDevTemplate = {
// 略
(GRAPHICS_CONSOLE_MODE_DATA *)NULL, // 这个看上去是一个指针,但是实际上是一个数组,表示当前文本显示支持的模式
// 略
};
通过上述两个结构体就构成了完整的文本模式,并与底层GOP模式绑定。
下面的示例代码显示了当前支持的所有文本模式:
VOID
ShowTextMode (
IN EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *Stp
)
{
EFI_STATUS Status = EFI_ABORTED;
UINTN Index = 0;
UINTN Col = 0;
UINTN Row = 0;
Print (L"Current Text Mode:\r\n");
Print (L" MaxMode : %d\r\n", Stp->Mode->MaxMode);
Print (L" Mode : %d\r\n", Stp->Mode->Mode);
Print (L" Attribute : 0x%x\r\n", Stp->Mode->Attribute);
Print (L" CursorColumn : %d\r\n", Stp->Mode->CursorColumn);
Print (L" CursorRow : %d\r\n", Stp->Mode->CursorRow);
Print (L" CursorVisible : %d\r\n", Stp->Mode->CursorVisible);
Print (L"Supported Text Mode:\r\n");
for (Index = 0; Index < Stp->Mode->MaxMode; Index++) {
Status = Stp->QueryMode (Stp, Index, &Col, &Row);
if (EFI_ERROR (Status)) {
Print (L"%d. Not supported.\r\n", Index);
continue;
}
Print (L"%d. Column: %d, Row: %d\r\n", Index, Col, Row);
}
}
得到的结果:
这里有几点需要说明:
QueryMode()
函数,文本模式下的QueryMode()
只能查看行和列两个参数,而底层的GRAPHICS_CONSOLE_MODE_DATA
并不能直接查看,如果想要获取这些信息,可以通过在本模块增加DEBUG信息来查看:ModeData 0
Columns : 80
Rows : 25
DeltaX : 320
DeltaY : 162
GopWidth : 1280
GopHeight : 800
GopModeNumber : 0
ModeData 1
Columns : 0
Rows : 0
DeltaX : 0
DeltaY : 0
GopWidth : 1280
GopHeight : 800
GopModeNumber : 0
ModeData 2
Columns : 100
Rows : 31
DeltaX : 240
DeltaY : 105
GopWidth : 1280
GopHeight : 800
GopModeNumber : 0
ModeData 3
Columns : 128
Rows : 40
DeltaX : 128
DeltaY : 20
GopWidth : 1280
GopHeight : 800
GopModeNumber : 0
ModeData 4
Columns : 160
Rows : 42
DeltaX : 0
DeltaY : 1
GopWidth : 1280
GopHeight : 800
GopModeNumber : 0
mode
命令是查看当前支持的文本模式,可以看到少了几个,这个的原因之后再分析。最后再说明一下文本模式中的Attribute
这个结构体成员,它的值可以分为两类,一类是颜色,跟GOP的像素颜色对应;另一类表示文本是窄体还是宽体,它对应的一个示例就是英文是窄体而中文是宽体,它们的不同导致了绘制一个字体需要的像素的不同。后面一节会进一步介绍。Attribute
目前的取值:
//
// EFI Console Colours
//
#define EFI_BLACK 0x00
#define EFI_BLUE 0x01
#define EFI_GREEN 0x02
#define EFI_CYAN (EFI_BLUE | EFI_GREEN)
#define EFI_RED 0x04
#define EFI_MAGENTA (EFI_BLUE | EFI_RED)
#define EFI_BROWN (EFI_GREEN | EFI_RED)
#define EFI_LIGHTGRAY (EFI_BLUE | EFI_GREEN | EFI_RED)
#define EFI_BRIGHT 0x08
#define EFI_DARKGRAY (EFI_BLACK | EFI_BRIGHT)
#define EFI_LIGHTBLUE (EFI_BLUE | EFI_BRIGHT)
#define EFI_LIGHTGREEN (EFI_GREEN | EFI_BRIGHT)
#define EFI_LIGHTCYAN (EFI_CYAN | EFI_BRIGHT)
#define EFI_LIGHTRED (EFI_RED | EFI_BRIGHT)
#define EFI_LIGHTMAGENTA (EFI_MAGENTA | EFI_BRIGHT)
#define EFI_YELLOW (EFI_BROWN | EFI_BRIGHT)
#define EFI_WHITE (EFI_BLUE | EFI_GREEN | EFI_RED | EFI_BRIGHT)
//
// Macro to accept color values in their raw form to create
// a value that represents both a foreground and background
// color in a single byte.
// For Foreground, and EFI_* value is valid from EFI_BLACK(0x00) to
// EFI_WHITE (0x0F).
// For Background, only EFI_BLACK, EFI_BLUE, EFI_GREEN, EFI_CYAN,
// EFI_RED, EFI_MAGENTA, EFI_BROWN, and EFI_LIGHTGRAY are acceptable
//
// Do not use EFI_BACKGROUND_xxx values with this macro.
//
#define EFI_TEXT_ATTR(Foreground, Background) ((Foreground) | ((Background) << 4))
#define EFI_BACKGROUND_BLACK 0x00
#define EFI_BACKGROUND_BLUE 0x10
#define EFI_BACKGROUND_GREEN 0x20
#define EFI_BACKGROUND_CYAN (EFI_BACKGROUND_BLUE | EFI_BACKGROUND_GREEN)
#define EFI_BACKGROUND_RED 0x40
#define EFI_BACKGROUND_MAGENTA (EFI_BACKGROUND_BLUE | EFI_BACKGROUND_RED)
#define EFI_BACKGROUND_BROWN (EFI_BACKGROUND_GREEN | EFI_BACKGROUND_RED)
#define EFI_BACKGROUND_LIGHTGRAY (EFI_BACKGROUND_BLUE | EFI_BACKGROUND_GREEN | EFI_BACKGROUND_RED)
//
// We currently define attributes from 0 - 7F for color manipulations
// To internally handle the local display characteristics for a particular character,
// Bit 7 signifies the local glyph representation for a character. If turned on, glyphs will be
// pulled from the wide glyph database and will display locally as a wide character (16 X 19 versus 8 X 19)
// If bit 7 is off, the narrow glyph database will be used. This does NOT affect information that is sent to
// non-local displays, such as serial or LAN consoles.
//
#define EFI_WIDE_ATTRIBUTE 0x80
这里详细说明了当前支持的字体类型和颜色。
从GOP模式到文本模式,首先需要解决的就是前面章节中提到的像素转换成行列的问题,它主要在InitializeGraphicsConsoleTextMode()
函数中完成:
EFI_STATUS
InitializeGraphicsConsoleTextMode (
IN UINT32 HorizontalResolution,
IN UINT32 VerticalResolution,
IN UINT32 GopModeNumber,
OUT UINTN *TextModeCount,
OUT GRAPHICS_CONSOLE_MODE_DATA **TextModeData
)
该函数的入参是像素构成的长和宽以及对应的GOP模式,出参是对应的可适配的文本模式。下面简单介绍该函数的实现,以此来了解像素到行列的转换关系。
MaxColumns = HorizontalResolution / EFI_GLYPH_WIDTH; // 8
MaxRows = VerticalResolution / EFI_GLYPH_HEIGHT; // 19
这里的8和19的来源是UEFI规范中定义的窄体字,后面会进一步说明。按照UEFI规范的要求,最小支持的行列必须要满足80x25的要求,所以紧接着有以下的判断:
//
// According to UEFI spec, all output devices support at least 80x25 text mode.
//
ASSERT ((MaxColumns >= 80) && (MaxRows >= 25));
以当前OVMF的示例,像素是1280x800,所以MaxColumns = 160,MaxRows = 42,它会被作为支持全屏的行列,因此会放到mGraphicsConsoleModeData
中,这样实际代码中支持的所有行列是这样的:
GRAPHICS_CONSOLE_MODE_DATA mGraphicsConsoleModeData[] = {
{ 100, 31 }, // 800 x 600
{ 128, 40 }, // 1024 x 768
{ 160, 42 }, // 1280 x 800
{ 240, 56 }, // 1920 x 1080
// 上面的都是硬编码的
//
// New modes can be added here.
// The last entry is specific for full screen mode.
//
{ 160, 42 } // 代码根据像素实际生成的
};
但是mGraphicsConsoleModeData
并不是最终文本模式能够支持的行列(显然上表有重复),这里还需要进行一些处理。
//
// Mode 0 and mode 1 is for 80x25, 80x50 according to UEFI spec.
//
ValidCount = 0;
NewModeBuffer[ValidCount].Columns = 80;
NewModeBuffer[ValidCount].Rows = 25;
NewModeBuffer[ValidCount].GopWidth = HorizontalResolution;
NewModeBuffer[ValidCount].GopHeight = VerticalResolution;
NewModeBuffer[ValidCount].GopModeNumber = GopModeNumber;
NewModeBuffer[ValidCount].DeltaX = (HorizontalResolution - (NewModeBuffer[ValidCount].Columns * EFI_GLYPH_WIDTH)) >> 1;
NewModeBuffer[ValidCount].DeltaY = (VerticalResolution - (NewModeBuffer[ValidCount].Rows * EFI_GLYPH_HEIGHT)) >> 1;
ValidCount++;
if ((MaxColumns >= 80) && (MaxRows >= 50)) {
NewModeBuffer[ValidCount].Columns = 80;
NewModeBuffer[ValidCount].Rows = 50;
NewModeBuffer[ValidCount].DeltaX = (HorizontalResolution - (80 * EFI_GLYPH_WIDTH)) >> 1;
NewModeBuffer[ValidCount].DeltaY = (VerticalResolution - (50 * EFI_GLYPH_HEIGHT)) >> 1;
}
NewModeBuffer[ValidCount].GopWidth = HorizontalResolution;
NewModeBuffer[ValidCount].GopHeight = VerticalResolution;
NewModeBuffer[ValidCount].GopModeNumber = GopModeNumber;
ValidCount++;
这里需要关注DeltaX
和DeltaY
的值,这样做是为了保证即使文本模式不能全屏,但是也能够占据在屏幕的正中间。
还需要注意那个if
判断,显然这个条件在现在的OVMF环境下是满足要求的,这就导致了第二项中的行列都不会被赋值,都是默认的0。这比较奇怪,目前还不确定原因。
mGraphicsConsoleModeData
中的文本模式,判断是否有效的点有:a)行列不能超过最大值,所以{ 240, 56 }
这一项就不满足要求,b)不能有重复项,这里主要是防止最后一项{MaxColumns,MaxRows}
跟前面的冲突,其它的都是代码写死的,它们之间应该不存在重复的可能。最终可用的行列在DEBUG信息中显示如下:
Graphics - Mode 0, Column = 80, Row = 25
Graphics - Mode 1, Column = 0, Row = 0
Graphics - Mode 2, Column = 100, Row = 31
Graphics - Mode 3, Column = 128, Row = 40
Graphics - Mode 4, Column = 160, Row = 42
这也跟前面的分析是一致的。
到这里已经将像素都分割成了行列,并且可以看到最适配的是160x42的情况,但是从前面的例子以及mode
命令打印的情况来看,实际使用的却是100x31,这个由于不影响本节内容的说明,所以暂时不关注。
后面需要关注的是,当像素分割成一个个矩形之后(窄体对应矩形是8x19个像素),该如何在这个矩形中表示一个字符,这个可以通过下图很明显地看出来:
图中的最小的矩形就是一个个的像素,而其中的圆形可以通过不同的颜色来描述(注意不是真的有圆形),这样就可以勾勒出一个字。通过观察上图,就可以通过GOP来“写”出一个字符,这里以A这个字符为例,只需要将上图中有圆形的那些位置对应的像素改成其它颜色,就可以显示出来,它们对应的位置是:
UINT8 BltIndex[NARROW_HEIGHT * NARROW_WIDTH] = {
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 1, 0, 0, 0, 0,
0, 0, 1, 1, 1, 0, 0, 0,
0, 1, 1, 0, 1, 1, 0, 0,
1, 1, 0, 0, 0, 1, 1, 0,
1, 1, 0, 0, 0, 1, 1, 0,
1, 1, 0, 0, 0, 1, 1, 0,
1, 1, 0, 0, 0, 1, 1, 0,
1, 1, 1, 1, 1, 1, 1, 0,
1, 1, 0, 0, 0, 1, 1, 0,
1, 1, 0, 0, 0, 1, 1, 0,
1, 1, 0, 0, 0, 1, 1, 0,
1, 1, 0, 0, 0, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0
};
这里用数组模拟一个8x19的像素,只要值为1,就用其它颜色表示,这样就构造出了一个A字,下面是剩余的代码:
for (Index = 0; Index < NARROW_HEIGHT * NARROW_WIDTH; Index++) {
if (BltIndex[Index]) {
Blt[Index].Red = 0xFF;
}
}
Gop->Blt (
Gop,
Blt,
EfiBltBufferToVideo,
0,
0,
0,
0,
Width,
Height,
0
);
最终得到的结果:
可以看到左上角就显示了一个A。不过这只是一个简单的例子,如何输出字符可以直接调用EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL
的OutputString()
函数即可,它的实现是:
EFI_STATUS
EFIAPI
GraphicsConsoleConOutOutputString (
IN EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *This,
IN CHAR16 *WString
)
除了一些特殊字符和特殊情况(比如回车键意味着换行,而换行时刚好已经在显示的最后一行则要全局地向上平移,这个时候参数EfiBltVideoToVideo
就能派上用处)的处理有所不同,其它的处理都在如下的函数:
/**
Draw Unicode string on the Graphics Console device's screen.
@param This Protocol instance pointer.
@param UnicodeWeight One Unicode string to be displayed.
@param Count The count of Unicode string.
@retval EFI_OUT_OF_RESOURCES If no memory resource to use.
@retval EFI_UNSUPPORTED If no Graphics Output protocol and UGA Draw
protocol exist.
@retval EFI_SUCCESS Drawing Unicode string implemented successfully.
**/
EFI_STATUS
DrawUnicodeWeightAtCursorN (
IN EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *This,
IN CHAR16 *UnicodeWeight,
IN UINTN Count
)
而将字符转换成像素,则依赖于EFI_HII_FONT_PROTOCOL
:
Status = mHiiFont->StringToImage (
mHiiFont,
EFI_HII_IGNORE_IF_NO_GLYPH | EFI_HII_DIRECT_TO_SCREEN | EFI_HII_IGNORE_LINE_BREAK,
String,
FontInfo,
&Blt,
This->Mode->CursorColumn * EFI_GLYPH_WIDTH + Private->ModeData[This->Mode->Mode].DeltaX,
This->Mode->CursorRow * EFI_GLYPH_HEIGHT + Private->ModeData[This->Mode->Mode].DeltaY,
NULL,
NULL,
NULL
);
不仅仅是转换,该函数也完成了最终的输出。关于HII Font的实现,将在后续进一步介绍。