(请保留-> 作者: 罗冰 https://blog.csdn.net/luobing4365)
粗略地数了数,在博客中起码开发了近50个各类UEFI的演示程序。从理论上来说,大部分的代码,实际上都可以移植到YIE001开发板的Option ROM中。
只不过,YIE001所用的Ch366芯片,只支持窗口容量为32K的程序文件(当然,可以通过CH366的I2C两线串口进行扩展)。在这种情况下,对代码的编写设置了一定的限制。
本篇将以《UEFI编程实践》第6章的示例MyGuiFrame为蓝本,将其移植到YIE001的Option ROM框架代码中。此示例在仓库https://gitee.com/luobing4365/uefi-practical-programming.git的/ RobinPkg\Applications\MyGuiFrame下。(目前仍旧在整理中,还没有上传到仓库中,by robin,20210215)
开发板YIE001主要用来进行Option ROM的开发,由于其本身所用的PCIE芯片CH366的限制,以及UEFI下Option ROM的本身限制,编写代码中还是有不少需要注意的地方。从我的角度出发,有以下点需要注意。
毕竟YIE001的窗口容量只有32K,能够容纳的代码量有限。而且由于代码是用C语言编写的,很难精确地计算生成后文件的大小,必须要注意代码的大小。
因此,第一个规则是尽量使用EDK2提供的库函数。不要使用外部库,比如StdLib库等所提供的函数。使用封装过的库函数,会导致生成文件增大。在EDK2本身提供的库函数中,对内存处理、字符串处理等常用的函数,都有提供,尽量使用这些函数就行。
开发中,最占用空间的是图像、汉字字模等这些资源。一般来说,应该尽可能使用图形函数自己绘制图像,尽量少使用标准的图像进行显示。第二条规则是图形界面尽量简单,汉字库应该采用小字库的技术,也即用哪些汉字,就提取这些汉字的字库。
在之前的博客中,针对汉字的显示,以及图形和图像的显示,已经花了比较多的篇幅进行描述,可以去查看相应的篇章。
当然,也可以用一些无损压缩算法,对资源进行压缩。不过,个人认为在32K的空间内去压缩,也很难容纳较大的文件,有兴趣的技术同好可以试一下。
我接手过印象最深的项目,是自己构建Option ROM程序,并嵌入到BIOS中去。最头疼的是,程序在运行的过程中跑飞了,导致BIOS无法启动磁盘。
YIE001的代码在Flash中,当然不会严重到影响BIOS启动,最多把YIE001板卡从PCIe槽上取下,再开机启动就行。
不过,这两种情况的问题是类似的。如果把Option ROM代码刷入到YIE001的Flash ROM中,如果没有有效的退出机制,很可能导致板卡插在PCIe槽的时候,无法启动U盘或其他启动磁盘,导致没办法重新刷写程序。
因此,应该在将ROM文件刷入到YIE001的Flash ROM中时,先在UEFI Shell下进行完整的测试,确保没有问题后再刷入。
第三条规则是,在UEFI Shell下测试ROM文件,再刷入Flash ROM中。
这是对上面规则的加强,在程序代码中提供退出手段。第四条规则是,编程时始终要有退出手段。
即便在UEFI Shell测试通过了ROM文件,也无法保证程序所调用的Protocol,在Option ROM运行时能够工作很好。
因此,在开始运行Option ROM中的主要程序前,应该允许用户通过某种手段退出Option ROM,比如通过按键判断等。
当然,这些规则都是针对学习Option ROM的程序员而言的,如果是开发商业产品,肯定能够直接用编程器去刷写Flash ROM了,请无视这些规则。
如果还是不小心将问题ROM文件刷入了,无法退出,导致启动不了U盘,YIE001还预留了最后一个手段,也即UP32K#。
这是CH366上切换到另外一个32K窗口代码所提供的机制,CH366支持两套完全独立的 32KB 主程序,由复位时 UP32K#引脚的状态选择。
一般情况下,UP32K#引脚上的拨动开关(在板子上标明了“UP32K#”)是远离“ON”端的。如果由于Option ROM导致无法启动U盘,可以在开机时将拨动开关拨动到“ON”端。
进入到DOS启动盘时(准备刷写Flash ROM),再将UP32K#的拨动开关,拨动到远离“ON”端。此时,就可以再次进行刷写了。
注意,UP32K#在不同的状态,对应的是Flash ROM中不同的32K空间。上述的方法,只是利用了靠近“ON”端的32K,一直没有写入代码而已。所以,在拨动UP32K#的开关时,一定要记住刷写时对应的位置。
以上的规则总结为第五条:记住UP32K#。
总结一下,在YIE001上编写Option ROM,记住五条规则:
(1) 尽量使用EDK2提供的库函数;
(2) 图形界面尽量简单,汉字库应该采用小字库的技术;
(3) 在UEFI Shell下测试ROM文件,再刷入Flash ROM中;
(4) 编程时始终要有退出手段;
(5) 记住UP32K#。
如果还是导致无法重刷YIE001的Flash ROM,那就只能使用编程器去刷写Flash ROM了。
下面将以MyGuiFrame为例,介绍如何将之前编写的UEFI应用,移植到Option ROM框架中。
我所构建的MyGuiFrame,是一个UEFI下的简单GUI框架,主要实现了以下功能:
(1) 建立整体处理事件的机制,将鼠标事件、键盘事件,以及定时检查界面消息的事件等,统一在同一管理机制下;
(2) 处理鼠标初始化,以及相应的鼠标绘制工作;
(3) 处理键盘事件,对键盘按键进行处理;
也就是说,需要处理的事件包括鼠标事件、键盘事件和定时器事件共三个事件。其中,定时器事件是准备用来建立完整的消息机制,将图形控件与处理函数联系起来的。
响应事件的GUI管理框架如下:
EFI_EVENT gTimerEvent;
EFI_EVENT gWaitArray[3];
VOID InitGUI(VOID) //初始化GUI事件及其他初始化工作
{
gBS->CreateEvent(EVT_TIMER,TPL_APPLICATION,(EFI_EVENT_NOTIFY)NULL,
(VOID*)NULL,&gTimerEvent);//创建定时器事件
gBS->SetTimer(gTimerEvent,TimerPeriodic,10*1000*1000);//设置为每秒触发
gWaitArray[EVENT_TIMER]=gTimerEvent; //事件数组元素0为定时器
gWaitArray[EVENT_KEY]=gST->ConIn->WaitForKey; //事件数组元素1为键盘事件
gWaitArray[EVENT_MOUSE]=gMouse->WaitForInput; //事件数组元素2为鼠标事件
initMouseArrow(); //初始化鼠标
}
VOID HanlderGUI(VOID) //各类GUI事件处理
{
UINTN Index;
EFI_INPUT_KEY key={0,0};
EFI_SIMPLE_POINTER_STATE mouseState;
while(1)
{
gBS->WaitForEvent(3, gWaitArray, &Index);
if(Index == EVENT_KEY) //处理键盘事件
{
gST->ConIn->ReadKeyStroke(gST->ConIn,&key);
HandlerKeyboard(&key);
}
else if(Index == EVENT_MOUSE) //处理鼠标事件
{
GetMouseState(&mouseState);
HandlerMouse(&mouseState);
}
else if(Index == EVENT_TIMER) //处理定时器事件
{
HandlerTimer();
}
else{ }//意外错误处理
}
}
在事件处理框架中,设置了每过1秒触发的定时器事件。它连同鼠标事件、键盘事件,共同组成了事件数组。事件管理的框架中,针对这三种事件进行了分别处理。
实际的应用中,定时器事件可以用来遍历GUI控件、对话框等图形元素的实时刷新和消息处理,肯定不能将刷新的时间设置为1秒才触发,这么慢是无法满足需求的。UEFI的定时器事件最小可设置为100ns触发,能满足非常实时的画面刷新。
在实际事件管理框架中,可以设置多个定时器以满足应用需求。示例只设置了一个定时器,它主要用来演示框架功能,其事件处理的函数,每过1秒将在屏幕上显示一段字符串,代码如下:
EFI_STATUS HandlerTimer(VOID)
{
static UINT8 flag=0;
UINT8 *s_text = "Timer Event has triggered.";
if(flag==1)
{
flag=0;
draw_string(s_text, 100, 150, &MyFontArray, &(gColorTable[WHITE]));
}
else
{
flag=1;
rectblock(100,150,400,180,&(gColorTable[DEEPBLUE]));//用背景色消除字符串
}
return EFI_SUCCESS;
}
鼠标是GUI框架中相对比较特殊的部分,它的事件处理涉及到鼠标图像的绘制、鼠标位置获取以及鼠标按键的处理。
为实现鼠标的绘制,示例工程MyGuiFrame中准备了18x25的鼠标图案,是使用PCX图像格式提取并保存的,可直接调用5.1.2节的PCX图像显示函数绘制鼠标。
实现代码如下:
VOID putMouseArrow(UINTN x,UINTN y)
{
EFI_GRAPHICS_OUTPUT_BLT_PIXEL *BltBuffer;
EFI_GRAPHICS_OUTPUT_BLT_PIXEL *BltBuffer1;
UINT32 BltBufferSize;
if(x>=(SY_SCREEN_WIDTH-1-gMouseWidth)) //限制鼠标x坐标不超过屏幕
x=SY_SCREEN_WIDTH-1-gMouseWidth;
if(y>=SY_SCREEN_HEIGHT-1-gMouseHeight) //显示鼠标y坐标不超过屏幕
y=SY_SCREEN_HEIGHT-1-gMouseHeight;
//1 oldZone中包含了上次鼠标显示所覆盖的区域,还原此区域图像
putRectImage(mouse_xres,mouse_yres,gMouseWidth,gMouseHeight,oldZone);
mouse_xres=(UINT16)x; //鼠标x坐标
mouse_yres=(UINT16)y; //鼠标y坐标
getRectImage(x,y,gMouseWidth,gMouseHeight,oldZone); //保存当前鼠标覆盖区域
//2 在当前位置显示鼠标
BltBufferSize = ((UINT32)gMouseWidth * (UINT32)gMouseHeight * (sizeof (EFI_GRAPHICS_OUTPUT_BLT_PIXEL)));
BltBuffer = AllocateZeroPool(BltBufferSize);
BltBuffer1 = AllocateZeroPool(BltBufferSize);
getRectImage(x,y,gMouseWidth,gMouseHeight,BltBuffer);
decompressPCX256_special(gMouseWidth,gMouseHeight,
gMousePicColorTable,gMousePicPicture,BltBuffer1,1);
//透明处理并显示
MaskingTransparent(gMouseWidth,gMouseHeight,BltBuffer1,BltBuffer,10);
putRectImage(x,y,gMouseWidth,gMouseHeight,BltBuffer);
FreePool(BltBuffer);
FreePool(BltBuffer1);
}
鼠标绘制的过程,就是不断地还原上一次鼠标覆盖的内容,保存当前鼠标将要覆盖的内容,并在指定的当前位置上绘制鼠标图案。
在鼠标事件处理的函数中,主要进行了鼠标图案的绘制。而且,主要是针对鼠标移动的事件进行了处理,对于鼠标中键滚动和鼠标左右按键的处理,并没有实现。如有需要,也可以在鼠标事件处理函数中添加代码。
处理函数如下:
EFI_STATUS HandlerMouse(EFI_SIMPLE_POINTER_STATE *State)
{
INT32 i,j;
i=(INT32)mouse_xres;
j=(INT32)mouse_yres;
i += ((State->RelativeMovementX<<MOUSE_SPEED) >> mouse_xScale);
if (i < 0) i = 0; //鼠标位置不超过屏幕
if (i > SY_SCREEN_WIDTH - 1) i = SY_SCREEN_WIDTH - 1;
j += ((State->RelativeMovementY<<MOUSE_SPEED) >> mouse_yScale);
if (j < 0) j = 0;
if (j > SY_SCREEN_HEIGHT - 1) j = SY_SCREEN_HEIGHT - 1;
putMouseArrow(i, j); //绘制鼠标图案
}
UEFI下提供了两种访问键盘的方式。在示例工程MyGuiFrame中,使用了EFI_SIMPLE_TEXT_INPUT_PROTOCOL来处理键盘按键。如果需要处理组合键,或者处理热键,则必须使用EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL。
处理GUI事件的函数HandlerGUI()中,对于键盘事件进行了处理。在处理键盘事件时,所调用的函数为HandlerKeyboard(),实现代码如下:
EFI_STATUS HandlerKeyboard(EFI_INPUT_KEY *key)
{
UINT8 *s_text = "Please Input:";
draw_string(s_text,100,100,&MyFontArray,&(gColorTable[WHITE]));//字符串
rectblock(240,100,270,130,&(gColorTable[DEEPBLUE]));//以背景色清除上次显示
draw_single_char((UINT32)key->UnicodeChar, //显示按键字符
240,100, //显示位置
&MyFontArray, //字模数组
&(gColorTable[RED]));//红色
return EFI_SUCCESS;
}
所准备的键盘处理函数比较简单,它会以背景色清除需要显示的位置,并用红色字体将按键字符显示出来。目前所准备的示例,只能处理字符数字键,对于控制键和切换状态按键的处理,必须使用EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL了。
代码的移植工作比较简单,把MyGuiFrame中的Font.c、Font.h、Mouse.c、Mouse.h、MyGUI.c、MyGUI.h、Pictures.c和Pictures.h拷贝到Option ROM的框架代码文件夹下。其他的支持文件,在原有框架中已经包含了。
本篇的示例名为YIE1GUI,接下来还有一些工作需要完成。
MyGuiFrame示例中,显示了一个相对较大的BMP文件,这会导致生成文件超过32K,必须去除。实际上,在拷贝的过程中,主要文件GUIPIC.c和GUIPIC.h并没有拷贝到框架代码文件夹下。
Pictures.c中提供的函数,只有绘制鼠标的时候用到,而且也只用到pcx的显示函数。因此,只要保留四个与pcx显示相关的函数putPCX256()、putPCX256_fast()、decompressPCX256()和decompressPCX256_special()就行了,其他的函数和相关的数据结构全部可以注释掉。
规则四是编程时始终要有退出手段,这是为了防止无法退出Option ROM时准备的。在HelloMyROM()中添加退出机制,并实现GUI主程序:
VOID HelloMyROM(VOID)
{
UINT64 flag;
EFI_INPUT_KEY key={0,0};
gST->ConOut->OutputString(gST->ConOut,L"YIE1GUI: Press key 1 to continue,key 2 to exit...\n\r");
while(key.ScanCode!=0x17)
{
GetKey(&key);
if(key.UnicodeChar == 0x31)
break;
if(key.UnicodeChar == 0x32)
return;
}
flag = InintGloabalProtocols( GRAPHICS_OUTPUT | SIMPLE_POINTER);
if(flag)
{
Print(L"Init Procotols: flag=%x\n",flag);
WaitKey();
}
else
{
SwitchGraphicsMode(TRUE);
SetBKG(&(gColorTable[DEEPBLUE]));
// ShowBMP24True(L"mygui.bmp",400,100);
// ShowMyGuiPic(400,100);
InitGUI();
HanlderGUI(); //添加了ESC键退出的机制
SetMyMode(OldGraphicsMode);
SwitchGraphicsMode(FALSE);
}
}
为了防止Option ROM陷入死循环,或者因Option ROM运行时所用到的UEFI Protocol无法正常运行,增加了退出机制。在加载时,按‘1’继续运行,按‘2’则退出Option ROM。
把需要编译的文件添加到INF文件的[Sources.common] Section,并修改FILE_GUID的GUID。
至此,完成了代码的移植工作。
编译命令如下:
C:\UEFIWorkspace>build -t VS2015x86 -p RobinPkg\RobinPkg.dsc \
-m RobinPkg\Drivers\YIE1GS\YIE1GS.inf -a X64
按照之前介绍的方法,在实际的机器上进行测试。不过,在我所测试的机器上,Option ROM刷入YIE001后,加载时无法支持Event机制,出现了无法运行的状况。
下面是在UEFI Shell下测试ROM得到的效果:
图1 测试YIE1GUI
YIE1GUI的移植工作完成了,系列博客中的其他UEFI程序,也可以用同样的方法进行移植。
到本篇为止,就介绍完了如何在YIE001上进行Option ROM的编程了。
目前在UEFI下进行PCIe开发的产品较少,我想在这个细分的领域做一些工作和探索,因此才有了开发板YIE001的诞生。
我采用了CH366作为主芯片进行开发,有兴趣的技术同好也可以用其他的PCIe芯片进行相关的开发。在与技术同好交流的过程中,也有人在进行显卡ROM或网卡ROM的开发,现在所介绍的YIE001的系列内容同样适用。
希望大家在UEFI的世界中玩得愉快!
Gitee地址:https://gitee.com/luobing4365/uefi-explorer
项目所用ROM文件位于:/ 80 YIE1GUI下