UEFI开发探索80- YIE001PCIe开发板(终篇 移植杂谈)

请保留-> 【原文:  https://blog.csdn.net/luobing4365 和 http://yiiyee.cn/blog/author/luobing/】

粗略地数了数,在博客中起码开发了近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)

1 Option ROM开发注意点

开发板YIE001主要用来进行Option ROM的开发,由于其本身所用的PCIE芯片CH366的限制,以及UEFI下Option ROM的本身限制,编写代码中还是有不少需要注意的地方。从我的角度出发,有以下点需要注意。

1) 减少生成文件的尺寸

毕竟YIE001的窗口容量只有32K,能够容纳的代码量有限。而且由于代码是用C语言编写的,很难精确地计算生成后文件的大小,必须要注意代码的大小。

因此,第一个规则是尽量使用EDK2提供的库函数。不要使用外部库,比如StdLib库等所提供的函数。使用封装过的库函数,会导致生成文件增大。在EDK2本身提供的库函数中,对内存处理、字符串处理等常用的函数,都有提供,尽量使用这些函数就行。

开发中,最占用空间的是图像、汉字字模等这些资源。一般来说,应该尽可能使用图形函数自己绘制图像,尽量少使用标准的图像进行显示。第二条规则是图形界面尽量简单,汉字库应该采用小字库的技术,也即用哪些汉字,就提取这些汉字的字库。

在之前的博客中,针对汉字的显示,以及图形和图像的显示,已经花了比较多的篇幅进行描述,可以去查看相应的篇章。

当然,也可以用一些无损压缩算法,对资源进行压缩。不过,个人认为在32K的空间内去压缩,也很难容纳较大的文件,有兴趣的技术同好可以试一下。

2) 尽量使用load或loadpcirom

我接手过印象最深的项目,是自己构建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中。

3) 始终要有退出手段

这是对上面规则的加强,在程序代码中提供退出手段。第四条规则是,编程时始终要有退出手段。

即便在UEFI Shell测试通过了ROM文件,也无法保证程序所调用的Protocol,在Option ROM运行时能够工作很好。

因此,在开始运行Option ROM中的主要程序前,应该允许用户通过某种手段退出Option ROM,比如通过按键判断等。

当然,这些规则都是针对学习Option ROM的程序员而言的,如果是开发商业产品,肯定能够直接用编程器去刷写Flash ROM了,请无视这些规则。

4) 记住UP32K#

如果还是不小心将问题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了。

2 MyGuiFrame的简介

下面将以MyGuiFrame为例,介绍如何将之前编写的UEFI应用,移植到Option ROM框架中。

我所构建的MyGuiFrame,是一个UEFI下的简单GUI框架,主要实现了以下功能:
(1) 建立整体处理事件的机制,将鼠标事件、键盘事件,以及定时检查界面消息的事件等,统一在同一管理机制下;
(2) 处理鼠标初始化,以及相应的鼠标绘制工作;
(3) 处理键盘事件,对键盘按键进行处理;

也就是说,需要处理的事件包括鼠标事件、键盘事件和定时器事件共三个事件。其中,定时器事件是准备用来建立完整的消息机制,将图形控件与处理函数联系起来的。

2.1 GUI事件管理

响应事件的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;
}

2.2 鼠标事件处理

鼠标是GUI框架中相对比较特殊的部分,它的事件处理涉及到鼠标图像的绘制、鼠标位置获取以及鼠标按键的处理。

为实现鼠标的绘制,示例工程MyGuiFrame中准备了18×25的鼠标图案,是使用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);  //绘制鼠标图案
}

2.3 键盘事件处理

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了。

3 代码移植

代码的移植工作比较简单,把MyGuiFrame中的Font.c、Font.h、Mouse.c、Mouse.h、MyGUI.c、MyGUI.h、Pictures.c和Pictures.h拷贝到Option ROM的框架代码文件夹下。其他的支持文件,在原有框架中已经包含了。

本篇的示例名为YIE1GUI,接下来还有一些工作需要完成。

3.1 去除非必要代码

MyGuiFrame示例中,显示了一个相对较大的BMP文件,这会导致生成文件超过32K,必须去除。实际上,在拷贝的过程中,主要文件GUIPIC.c和GUIPIC.h并没有拷贝到框架代码文件夹下。

Pictures.c中提供的函数,只有绘制鼠标的时候用到,而且也只用到pcx的显示函数。因此,只要保留四个与pcx显示相关的函数putPCX256()、putPCX256_fast()、decompressPCX256()和decompressPCX256_special()就行了,其他的函数和相关的数据结构全部可以注释掉。

3.2 增加退出机制

规则四是编程时始终要有退出手段,这是为了防止无法退出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。

3.3 修改INF文件

把需要编译的文件添加到INF文件的[Sources.common] Section,并修改FILE_GUID的GUID。

至此,完成了代码的移植工作。

4 编译及测试

编译命令如下:

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下

1,476 total views, 1 views today

发表评论

电子邮件地址不会被公开。