12

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

 3 years ago
source link: http://yiiyee.cn/blog/2021/02/16/2147/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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-exolorer
项目所用ROM文件位于:/ 80 YIE1GUI下

245 total views, 2 views today


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK