Step to UEFI (164)NT32 环境下的OpenFile研究

根据之前的研究,UDK中带的 NT32 模拟环境里面的很多操作都是直接和 Windows API挂钩来实现的。最近查看了一下 NT32 下面的 EFI_SIMPLE_FILE_SYSTEM_PROTOCOL 的实现。具体代码可以在\Nt32Pkg\WinNtSimpleFileSystemDxe\WinNtSimpleFileSystem.c 下面看到。

首先,在 WinNtSimpleFileSystem.c 中有下面这样的代码:

  Private->SimpleFileSystem.OpenVolume  = WinNtSimpleFileSystemOpenVolume;

 

这样,当我们调用EFI_SIMPLE_FILE_SYSTEM_PROTOCOL OpenVolume 的时候,实际工作的是WinNtSimpleFileSystemOpenVolume 的代码。此外,还有下面这样的代码,都是用Windows API 来替换 Protocol 中的操作。

  PrivateFile->EfiFile.Open         = WinNtSimpleFileSystemOpen;
  PrivateFile->EfiFile.Close        = WinNtSimpleFileSystemClose;
  PrivateFile->EfiFile.Delete       = WinNtSimpleFileSystemDelete;
  PrivateFile->EfiFile.Read         = WinNtSimpleFileSystemRead;
  PrivateFile->EfiFile.Write        = WinNtSimpleFileSystemWrite;
  PrivateFile->EfiFile.GetPosition  = WinNtSimpleFileSystemGetPosition;
  PrivateFile->EfiFile.SetPosition  = WinNtSimpleFileSystemSetPosition;
  PrivateFile->EfiFile.GetInfo      = WinNtSimpleFileSystemGetInfo;
  PrivateFile->EfiFile.SetInfo      = WinNtSimpleFileSystemSetInfo;
  PrivateFile->EfiFile.Flush        = WinNtSimpleFileSystemFlush;
  PrivateFile->IsValidFindBuf       = FALSE;

 

通过这样的赋值,当我们在NT32 模拟环境中调用打开读取等等Protocol 的函数时,实际上是用Windows 对应的API来完成实际操作的。
为了证明这一点,可以在上面的函数中插入输出 Debug 信息的代码【参考1】,比如:修改 WinNTSimpleFileSystemOpenVolume() 代码,插入Debug 信息:

/*++

Routine Description:

  Open the root directory on a volume.

Arguments:

  This  - A pointer to the volume to open.

  Root  - A pointer to storage for the returned opened file handle of the root directory.

Returns:

  EFI_SUCCESS           - The volume was opened.

  EFI_UNSUPPORTED       - The volume does not support the requested file system type.

  EFI_NO_MEDIA          - The device has no media.

  EFI_DEVICE_ERROR      - The device reported an error.

  EFI_VOLUME_CORRUPTED  - The file system structures are corrupted.

  EFI_ACCESS_DENIED     - The service denied access to the file.

  EFI_OUT_OF_RESOURCES  - The file volume could not be opened due to lack of resources.

  EFI_MEDIA_CHANGED     - The device has new media or the media is no longer supported.

--*/
// TODO:    EFI_INVALID_PARAMETER - add return value to function comment
{
  EFI_STATUS                        Status;
  WIN_NT_SIMPLE_FILE_SYSTEM_PRIVATE *Private;
  WIN_NT_EFI_FILE_PRIVATE           *PrivateFile;
  EFI_TPL                           OldTpl;
  CHAR16                            *TempFileName;
  UINTN                             Size;

  //LABZ_Start
  DEBUG ((EFI_D_INFO, "www.lab-z.com\n"));
  //LABZ_End
  
  if (This == NULL || Root == NULL) {
    return EFI_INVALID_PARAMETER;
  }

  OldTpl = gBS->RaiseTPL (TPL_CALLBACK);

  Private     = WIN_NT_SIMPLE_FILE_SYSTEM_PRIVATE_DATA_FROM_THIS (This);

  PrivateFile = AllocatePool (sizeof (WIN_NT_EFI_FILE_PRIVATE));
  if (PrivateFile == NULL) {
    Status = EFI_OUT_OF_RESOURCES;
    goto Done;
  }

  PrivateFile->FileName = AllocatePool (StrSize (Private->FilePath));
  if (PrivateFile->FileName == NULL) {
    Status = EFI_OUT_OF_RESOURCES;
    goto Done;
  }

  PrivateFile->FilePath = AllocatePool (StrSize (Private->FilePath));
  if (PrivateFile->FilePath == NULL) {
    Status = EFI_OUT_OF_RESOURCES;
    goto Done;
  }

  StrCpy (PrivateFile->FilePath, Private->FilePath);
  StrCpy (PrivateFile->FileName, PrivateFile->FilePath);
  PrivateFile->Signature            = WIN_NT_EFI_FILE_PRIVATE_SIGNATURE;
  PrivateFile->WinNtThunk           = Private->WinNtThunk;
  PrivateFile->SimpleFileSystem     = This;
  PrivateFile->IsRootDirectory      = TRUE;
  PrivateFile->IsDirectoryPath      = TRUE;
  PrivateFile->IsOpenedByRead       = TRUE;
  PrivateFile->EfiFile.Revision     = EFI_SIMPLE_FILE_SYSTEM_PROTOCOL_REVISION;
  PrivateFile->EfiFile.Open         = WinNtSimpleFileSystemOpen;
  PrivateFile->EfiFile.Close        = WinNtSimpleFileSystemClose;
  PrivateFile->EfiFile.Delete       = WinNtSimpleFileSystemDelete;
  PrivateFile->EfiFile.Read         = WinNtSimpleFileSystemRead;
  PrivateFile->EfiFile.Write        = WinNtSimpleFileSystemWrite;
  PrivateFile->EfiFile.GetPosition  = WinNtSimpleFileSystemGetPosition;
  PrivateFile->EfiFile.SetPosition  = WinNtSimpleFileSystemSetPosition;
  PrivateFile->EfiFile.GetInfo      = WinNtSimpleFileSystemGetInfo;
  PrivateFile->EfiFile.SetInfo      = WinNtSimpleFileSystemSetInfo;
  PrivateFile->EfiFile.Flush        = WinNtSimpleFileSystemFlush;
  PrivateFile->IsValidFindBuf       = FALSE;

  //
  // Set DirHandle
  //
  PrivateFile->DirHandle = PrivateFile->WinNtThunk->CreateFile (
                                                      PrivateFile->FilePath,
                                                      GENERIC_READ,
                                                      FILE_SHARE_READ | FILE_SHARE_WRITE,
                                                      NULL,
                                                      OPEN_EXISTING,
                                                      FILE_FLAG_BACKUP_SEMANTICS,
                                                      NULL
                                                      );

  if (PrivateFile->DirHandle == INVALID_HANDLE_VALUE) {
    Status = EFI_NOT_FOUND;
    goto Done;
  }

  //
  // Find the first file under it
  //
  Size  = StrSize (PrivateFile->FilePath);
  Size += StrSize (L"\\*");
  Status = gBS->AllocatePool (
                  EfiBootServicesData,
                  Size,
                  (VOID **)&TempFileName
                  );
  if (EFI_ERROR (Status)) {
    goto Done;
  }
  StrCpy (TempFileName, PrivateFile->FilePath);
  StrCat (TempFileName, L"\\*");

  PrivateFile->LHandle = PrivateFile->WinNtThunk->FindFirstFile (TempFileName, &PrivateFile->FindBuf);
  FreePool (TempFileName);

  if (PrivateFile->LHandle == INVALID_HANDLE_VALUE) {
    PrivateFile->IsValidFindBuf = FALSE;
  } else {
    PrivateFile->IsValidFindBuf = TRUE;
  }
  *Root = &PrivateFile->EfiFile;

  Status = EFI_SUCCESS;

Done:

  if (EFI_ERROR (Status)) {
    if (PrivateFile) {
      if (PrivateFile->FileName) {
        FreePool (PrivateFile->FileName);
      }

      if (PrivateFile->FilePath) {
        FreePool (PrivateFile->FilePath);
      }

      FreePool (PrivateFile);
    }
  }

  gBS->RestoreTPL (OldTpl);

  return Status;
}

 

之后在启动的过程中就能够看到如下的 Debug 信息:

参考:
1. http://www.lab-z.com/stu130nt32/ NT32 模拟器中的 Debug Message 输出

Ghost 替代者,新的全盘备份工具

很多时候我们需要制作全盘镜像,通过这样的方法能够们快速安装系统和驱动,另外一些客户问题也可以使用这样的方式将客户环境完整的“搬迁”到我们需要实验的机器上。起初,在legacy的情况下(或者说是在 GPT 分区出现之前),Ghost是无二的选择。但是,在出现UEFI 之后,因为Ghost无法兼容GPT分区它已经无法满足我们的需求。

最近研究了一下这个问题,最终找到了名为Macrium Reflect 的工具软件(官方网站https://www.macrium.com/reflectfree)有如下特点:

1.支持 GPT 分区,可以完美备份和恢复Win10的硬盘;
2.支持分包,这样可以不局限于NTFS分区;
3.有Free版本,并且它提供的功能足以满足需求;
4.自动分卷,意思是如果你用一个16G U盘给 64G 硬盘制作镜像,如果出现容量不足的情况会自动提示让你插入额外的U盘继续操作。

Macrium Reflect的使用示例如下。

备份的过程:

1.启动软件(在启动过程中它会进行环境检查,键盘选择和网络连接,直接 ESC 取消即可)。选择 Backup 然后选中你要做镜像的原盘,然后选择 Image this disk

2.设置存放 Image 文件的目录

3.上面的界面中还有 Advances Options 的设置,打开之后是下面这样的界面,根据我的测试建议选择 High Compression Level,速度也是挺快的

4.File Size 中可以指定备份文件的大小,对于FAT格式,支持的上限是 4G,这里我测试2GB大小

5.选择继续,就开始工作了

6.结束时弹出窗口,告知耗时9分14秒。我的硬盘占用 15.1G (包括虚拟内存等等,这些文件时不会被打包的)

7.生成了3个压缩包,感觉压缩比不错。

恢复的过程:

1.选择 Restore 页面,然后 Browse Image 选择你的镜像文件

2.在弹出来的界面中,Source 是Image中保存的分区信息

3.再选择你要恢复到的目标盘,这里我新安装了一个硬盘,上面没有分区

4.接下来就开始恢复的动作了

5.恢复的速度比制作要快。

最终,我还特地测试了一下新制作的盘是否支持 Modern Standby,毫无问题。此外,我还实验了原盘为 SATA ,恢复到 NVME 的 PCIE SSD 的过程,可以正产启动。

为了更好的让这个软件发挥作用,我特地制作了一个 WinPE 环境,内置了Macrium Reflect和Rw Everything。格式为 ISO,同时支持 UEFI 下的启动和 Legacy 的启动。对于 UEFI 的用户,直接解压到一个FAT32的U盘上即可启动;对于Legacy 的用户,需要用 UltraIso 之类的工作制作 ISO 的启动环境。

链接: https://pan.baidu.com/s/1UiKdHMJ3AGSCIgCd3ust3w 提取码: 62nn 428MB

============================================================
上面就是我这次推荐的Macrium Reflect软件,接下来讲讲我测试过的软件。
国产类的软件
1. 国产傲梅轻松备份 https://www.disktool.cn/
只有中文版,英文版是收费的。无法在Whisky Lake 平台上使用,启动之后无法找到硬盘分区。所以没测试成。我给他们售后写过邮件,但是看起来他们并不想解决这个问题,有可能他们工作重点是国外用户;

2. 易数一键还原 https://www.onekeyrestore.cn/
系出名门,是制作DiskGenius的公司编写的。但不知为什么在备份的时候只能识别2个分区,如果使用Windows 10 安装系统,安装之后可能会出现4个分区,单纯备份有数据的最大的那个,等你满心欢喜的折腾完毕之后会发现系统一直蓝屏无法进入;比如,下面是我安装Window 10 之后的分区:

只能备份上面的2个分区:

后来和他们售后进行了沟通,对方建议我在备份的时候使用命令来额外备份分区。因为操作过于复杂,我并没有试验。

3. Dism++ http://www.chuyu.me/en/index.html

这个软件功能强大,可以清理Windows 垃圾等等。但是,我在使用的时候发现只能备份单个分区。问题和上面的易数一键还原一样,无法做到全盘备份。

国外软件:

1. http://www.easis.com/easis-drive-cloning.html
我碰到的问题是 64G 的硬盘,占用19G空间备份的时候生成的文件达到29G还没有停止的意思…….
2. Acronis True Image 2019 http://www.tieten.cn/acronis/personal/ATI2019/compare/index.html
本打算试试,但是体积是在太大了,占用空间过高不划算。

1.8 寸 5v 数码管模块

七段数码管算是很基础的元件了,从使用的角度来说几乎和控制多个LED完全一致。但是如果想控制比较大的数码管则需要考虑驱动电压等等问题会让问题变得比较麻烦。

前几天入手了一个1.8寸的模块,4个LED在一起的,正面照如下,可以看出尺寸还是蛮大的:

背面照片,左边接口是用于级联的模块输出,右边接口是模块输出。下方的是用于烧写芯片的接口,正常使用中无需连接。

主要特性如下:
1.串口输入, 115200, n, 8, 1
2. 可以级联,然后后面有预留的地址选择跳线,可以设定0-31 个地址。购买之后送一根输入线,3Pin,分别是5V GND 和RXD。上图
3. 可以选择 0-9级别的亮度
下面进行上电测试,使用 Arduino 输出的5V供电,1级别亮度

9级亮度(不知道为什么,当选择这个级别的亮度之后有高频的声音)

最后测试了一下功耗,最低亮度显示4个8的时候,消耗电流 40ma左右。最高亮度显示4个8 的时候,消耗电流在81ma左右。因此,一个Arduino 控制两个级联的是没有问题的。下图是2个级联,其中一个跳线为地址1,另一个没有任何跳线是地址0.

下面是测试代码,包含了一个十进制数值显示和一个十六进制数值显示的函数

#include <SoftwareSerial.h> 

SoftwareSerial MySerial(6, 7);  // 定义软串口 RX(插到D6口), TX(插到D7口)

void digitalHex(unsigned int value)
{
   //这是数码管要求的数据头信息
    MySerial.write(0xff);
    MySerial.write((byte)0x00);
    MySerial.write(0x04);  //显示四位数值
 
    //下面是四位当前值
    MySerial.write((value>>12)%0x10);  //最高位
    MySerial.write((value>>8)%0x10);   
    MySerial.write((value>>4)%0x10);  
    MySerial.write((value)%0x10);      
   //最后一位是亮度
     MySerial.write(1);
}

void setup() {
  //Serial1 receive GPIO uart
  Serial1.begin(9600);
  //SoftSerial.begin(9600);
  Serial.begin(115200);
  MySerial.begin(115200);
}

     int  p80;
     int  len=0;
     boolean mark=false;
void loop() {
  byte c;
     while (Serial1.available()) {
                c=(Serial1.read()&0xFF);
                p80=(p80<<8)+c;
                len=len+1;
     }    
     if (len==2) {
                       Serial.println(p80,HEX);
                       digitalHex(p80);
                       len=0;
                  }
}

 

一个 wave 转 h 文件的工具

Wave 文件转 h 文件工具

有些时候我们需要播放一些音频,这时候就需要用工具将 wave中定义好的音频数据提取出来,

这个工具就是用来实现这个功能的。它能将单声道 Wave 中的声音信息提取出来放到一个文本文件中。

使用方法:

wave.exe 声音文件.wav >> 输出文件名.txt

注意:

本工具只支持单声道 wav文件。

编译工具为 delphi xe2

下载
wave2h

Step to UEFI (162)UEFI Shell 下的俄罗斯方块

1984年,在当时还叫做苏联的那个国家的首都莫斯科,俄罗斯科学院(当然那时它也还叫做苏联科学院)总部计算机中心的一位工程师阿列克谢·帕基特诺夫开始考虑在计算机上自行开发一些简单的游戏,经过一段时间的尝试后,他通过另一款拼图游戏得到灵感,考虑让不同形状的图形依次下落,在矩形底部堆叠起来使之排列成完整的一行后消除,在另外两位同伴的协助下,他最终完成了这款被他命名为“Tetris”(俄语:Тетрис)的游戏,而我们今天更习惯叫它为“俄罗斯方块”。


1984年6月6日,是公认的俄罗斯方块诞生纪念日,游戏最初由苏联科学院计算中心的工程师阿列克谢·帕基特诺夫开发,他当时从另一款拼图游戏得到灵感。

  根据另一位当事人的回忆,“Tetris”这个单词是阿列克谢自己发明并坚持使用的,来自反映俄罗斯方块图案基本结构的“四”(希腊语:tetra)和阿列克谢自己最喜爱的运动“网球”(tennis)的组合。

《俄罗斯方块》的原名“Tetris”(俄语:Тетрис)是发明者阿列克谢·帕基特诺夫自己生造出来的单词,来自反映俄罗斯方块图案基本结构的“四”(希腊语:tetra)和阿列克谢自己最喜爱的运动“网球”(tennis)的组合。

  公认的第一款俄罗斯方块原始程序诞生于1984年6月6日,在当时苏联仿制的Elektronika 60计算机上运行,因为这款计算机不能显示色块图案,原始版本只能用字符串来表示图形,但即便这样它也体现出了游戏本身的魅力,阿列克谢和他身边的朋友们很快都开始为之沉迷。1985年,开发同伴之一的瓦丁·格拉西莫夫在MS-DOS下移植了俄罗斯方块,让更多的个人电脑可以运行,游戏得以迅速的普及。

  阿列克谢起初希望能合法贩卖俄罗斯方块游戏,但在苏联当时的制度下这十分困难,几经尝试都失败后,阿列克谢表示可以考虑把游戏版权交给国家——当时的苏联科学院。而在诞生后的数年时间里,俄罗斯方块一直都以免费拷贝的形式传播,从苏联扩展到了整个欧洲,也引起了更多人的注意。

第一个俄罗斯方块的程序在苏联仿制的Elektronika 60计算机上运行,因为无法显示色块只能用字符来表示图形,之后一年游戏移植了MS-DOS版,而这一版首次呈现出的图案画面也成为之后三十年来的游戏基础,甚至没有太多变化。

  1986年匈牙利的程序员在Apple II和Commodore 64上移植了游戏,英国游戏公司Andromeda的一位经理人罗伯特·斯坦恩注意到了这个商机,他开始联系阿列克谢以及匈牙利的程序员试图购买俄罗斯方块的版权,并在确定到手之前就把它分别卖给了英国的游戏公司Mirrorsoft和美国的游戏公司Spectrum Holobyte,从而导致了接下来整整十余年时间关于俄罗斯方块的版权之争,甚至可以说改变了游戏发展史的一连串事件。”  上述文字节选自《三十年成就经典传奇 <俄罗斯方块>发展史》【参考1】

之前网上有一份开源的 Shell 版本的俄罗斯方块游戏,但是我试验发现做的太糟糕,没有办法玩。然后经过在网上搜索到了一份Windows API 编写的俄罗斯方块游戏【参考2】,放在 VS2015 中很快就可以编译成功,运行起来也没有大问题,于是在这个代码基础上移植到UEFI Shell 下(程序整体非常清晰,移植到其他平台也绝无难度)。
其中比较有意思的是代码中定义的方块有下面六种方块,都是4个格子组成的

在代码头部定义了Blocks[][4] 对应了每种方块的各种变换,比如下面这种 Z 字

定义方法是给出每个黑块的坐标(左上角为 0,0): 0,0, 1,0, 1,1, 2,1
旋转变换后的一个结果如下:2, 0, 1, 1, 2, 1, 1, 2,

看懂了上面就能搞清楚代码。

#include <Uefi.h>
#include <Library/UefiLib.h>
#include <Library/DebugLib.h>
#include <Library/ShellCEntryLib.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/UefiRuntimeServicesTableLib.h>
#include <Library/MemoryAllocationLib.h>
#include <Library/BaseLib.h>
#include <Library/BaseMemoryLib.h>

#define NUMLINEBLOCKS   18  //行数
#define NUMCOLUMNBLOCKS 10  //列数
#define BLOCKSTYLES     (sizeof (Blocks) / sizeof (Blocks[0]))  //方块的种类数

//定时器触发时间,单位是100ns
#define TIMER_PERIOD_INIT       10000000UL

BOOLEAN                pause = FALSE;  //暂停

EFI_EVENT       TimerEvent;

//是否要重绘界面的标志
BOOLEAN         ReDraw=TRUE;

//退出标志
BOOLEAN         quit=FALSE;

struct POINT
{
        int x;
        int y;
};

//游戏区各方格顶点布尔值,代表该方格是否有方块
BOOLEAN        GameClient[NUMCOLUMNBLOCKS][NUMLINEBLOCKS];
int                F, S, cF, cS;        //随机方块图形对应的第一、二纬
int                Score;  //得分
struct POINT Block[4],NextBlock[4];


//定义各方块形状,以点表示
struct {
        struct POINT    pt[4];
}

Blocks[][4] =
{
        //正7
        0, 0, 1, 0, 1, 1, 1, 2,  2, 0, 0, 1, 1, 1, 2, 1,  1, 0, 1, 1, 1, 2, 2, 2,  0, 1, 1, 1, 2, 1, 0, 2,
        //反7
        1, 0, 2, 0, 1, 1, 1, 2,  0, 1, 1, 1, 2, 1, 2, 2,  1, 0, 1, 1, 0, 2, 1, 2,  0, 0, 0, 1, 1, 1, 2, 1,
        //1
        1, 0, 1, 1, 1, 2, 1, 3,  0, 1, 1, 1, 2, 1, 3, 1,  1, 0, 1, 1, 1, 2, 1, 3,  0, 1, 1, 1, 2, 1, 3, 1,
        //Z
        0, 0, 1, 0, 1, 1, 2, 1,  2, 0, 1, 1, 2, 1, 1, 2,  0, 0, 1, 0, 1, 1, 2, 1,  2, 0, 1, 1, 2, 1, 1, 2,
        //反Z
        1, 0, 2, 0, 0, 1, 1, 1,  1, 0, 1, 1, 2, 1, 2, 2,  1, 0, 2, 0, 0, 1, 1, 1,  1, 0, 1, 1, 2, 1, 2, 2,
        //田字
        0, 0, 1, 0, 0, 1, 1, 1,  0, 0, 1, 0, 0, 1, 1, 1,  0, 0, 1, 0, 0, 1, 1, 1,  0, 0, 1, 0, 0, 1, 1, 1,
        //尖头
        1, 0, 0, 1, 1, 1, 2, 1,  0, 0, 0, 1, 1, 1, 0, 2,  0, 0, 1, 0, 2, 0, 1, 1,  1, 0, 0, 1, 1, 1, 1, 2
};

/** Expands to an integer constant expression that is the maximum value
    returned by the rand function.
**/
#define RAND_MAX  0x7fffffff
static UINT32 next = 1;

//判断方块是否可以下落
BOOLEAN CanDown(struct POINT pt[])
{
        BOOLEAN result = TRUE;
        
        //将方块所在格子先假设指定为无方块
        for (int i = 0; i < 4; ++i)
                GameClient[pt[i].x][pt[i].y] = FALSE;
        
        for (int i = 0; i < 4; ++i)
        {
                //假如继续落下超过下底边界,返回false;或者假如该小方块下落一格已经有方块,结果为false
                if (pt[i].y + 1 == NUMLINEBLOCKS || GameClient[pt[i].x][pt[i].y + 1])
                {
                        result = FALSE;
                        break;
                }
        }
        
        //恢复方块所在格子为有方块
        for (int i = 0; i < 4; ++i)
                GameClient[pt[i].x][pt[i].y] = TRUE;
        
        return result;
}

//判断是否可以左移
BOOLEAN CanLeft(struct POINT pt[])
{
        BOOLEAN result = TRUE;
        //将方块所在格子先假设指定为无方块
        for (int i = 0; i < 4; ++i)
                GameClient[pt[i].x][pt[i].y] = FALSE;
        for (int i = 0; i < 4; ++i)
        {
                //假如继续左移超过左边边界,返回false;或者假如该小方块左移一格已经有方块,结果为false
                if (!pt[i].x || GameClient[pt[i].x - 1][pt[i].y])
                {
                        result = FALSE;
                        break;
                }
        }
        //恢复方块所在格子为有方块
        for (int i = 0; i < 4; ++i)
                GameClient[pt[i].x][pt[i].y] = TRUE;
        return result;
}

//判断是否可以右移
BOOLEAN CanRight(struct POINT pt[])
{
        BOOLEAN result = TRUE;
        //将方块所在格子先假设指定为无方块
        for (int i = 0; i < 4; ++i)
                GameClient[pt[i].x][pt[i].y] = FALSE;
        for (int i = 0; i < 4; ++i)
        {
                //假如继续左移超过左边边界,返回false;或者假如该小方块左移一格已经有方块,结果为false
                if (pt[i].x + 1 == NUMCOLUMNBLOCKS || GameClient[pt[i].x + 1][pt[i].y])
                {
                        result = FALSE;
                        break;
                }
        }
        //恢复方块所在格子为有方块
        for (int i = 0; i < 4; ++i)
                GameClient[pt[i].x][pt[i].y] = TRUE;
        return result;
}

//判断是否可以旋转
BOOLEAN CanChange(struct POINT pt[])
{
        BOOLEAN result = TRUE;
        //将方块所在格子先假设指定为无方块
        for (int i = 0; i < 4; ++i)
                GameClient[pt[i].x][pt[i].y] = FALSE;
        int t = (cS + 1) % 4;
        for (int k = 0; k < 4; ++k)
        {
                int x = Blocks[cF][t].pt[k].x - Blocks[cF][cS].pt[k].x,
                        y = Blocks[cF][t].pt[k].y - Blocks[cF][cS].pt[k].y;
                if (GameClient[pt[k].x + x][pt[k].y + y] ||  //该方格已经有方块
                        pt[k].x + x > NUMCOLUMNBLOCKS - 1 ||  //x坐标超越了右边界
                        pt[k].x + x < 0 ||   //x坐标超越了左边界
                        pt[k].y + y > NUMLINEBLOCKS - 1)  //y坐标超越了下底边界
                {
                        result = FALSE;
                        break;
                }
        }

        //恢复方块所在格子为有方块
        for (int i = 0; i < 4; ++i)
                GameClient[pt[i].x][pt[i].y] = TRUE;
        return result;
}

//实现旋转
void Change(struct POINT pt[])
{
        int t = (cS + 1) % 4;
        for (int i = 0; i < 4; ++i)
        {
                int x = Blocks[cF][t].pt[i].x - Blocks[cF][cS].pt[i].x,
                        y = Blocks[cF][t].pt[i].y - Blocks[cF][cS].pt[i].y;
                GameClient[pt[i].x][pt[i].y] = FALSE;
                pt[i].x += x;
                pt[i].y += y;
                GameClient[pt[i].x][pt[i].y] = TRUE;
        }
        cS = t;
}

//实现右移
void Right(struct POINT pt[])
{
        for (int i = 0; i < 4; ++i)
        {
                GameClient[pt[i].x][pt[i].y] = FALSE;
                ++pt[i].x;
        }
        for (int k = 0; k < 4; ++k)
                GameClient[pt[k].x][pt[k].y] = TRUE;
}

//实现左移
void Left(struct POINT pt[])
{
        for (int i = 0; i < 4; ++i)
        {
                GameClient[pt[i].x][pt[i].y] = FALSE;
                --pt[i].x;
        }
        for (int k = 0; k < 4; ++k)
                GameClient[pt[k].x][pt[k].y] = TRUE;
}

//实现方块的下落
void Down(struct POINT pt[])
{
        for (int i = 0; i < 4; ++i)
        {
                GameClient[pt[i].x][pt[i].y] = FALSE;
                ++pt[i].y;
        }
        for (int k = 0; k < 4; ++k)
                GameClient[pt[k].x][pt[k].y] = TRUE;
}

//消行处理以及分数结算
void DelSqure()
{
        int line = 0, temp;
        for (int x = NUMLINEBLOCKS - 1; x >= 0; --x)
        {
                BOOLEAN result = TRUE;
                for (int y = 0; y < NUMCOLUMNBLOCKS; ++y)
                {
                        if (!GameClient[y][x])
                        {
                                result = FALSE;
                                break;
                        }
                }
                //判断是否可以消行
                if (result)
                {
                        temp = x;
                        ++line;
                        while (x > 0)
                        {
                                for (int y = 0; y < NUMCOLUMNBLOCKS; ++y)
                                {
                                        GameClient[y][x] = GameClient[y][x - 1];
                                }
                                --x;
                        }
                        for (int y = 0; y < NUMCOLUMNBLOCKS; ++y)
                                GameClient[y][0] = FALSE;
                        x = temp + 1;
                }
        }
        if (line)
                Score += (line - 1) * 2 + 1;
        //要求重绘
        ReDraw=TRUE;
}


/** Compute a pseudo-random number.
  *
  * Compute x = (7^5 * x) mod (2^31 - 1)
  * without overflowing 31 bits:
  *      (2^31 - 1) = 127773 * (7^5) + 2836
  * From "Random number generators: good ones are hard to find",
  * Park and Miller, Communications of the ACM, vol. 31, no. 10,
  * October 1988, p. 1195.
**/
int
rand()
{
  INT32 hi, lo, x;

  /* Can't be initialized with 0, so use another value. */
  if (next == 0)
    next = 123459876;
  hi = next / 127773;
  lo = next % 127773;
  x = 16807 * lo - 2836 * hi;
  if (x < 0)
    x += 0x7fffffff;
  return ((next = x) % ((UINT32)RAND_MAX + 1));
}

//触发时间中断
VOID TimerCallback( EFI_EVENT Event, VOID *Context )
{
        //如果当前已经暂停,那么直接退出
        if (pause) return ;
        //判断是否可以下落
        if (CanDown(Block))
        {       //可以下落,处理  
                Down(Block);
        }
        //不能下移,需要处理消行判断(结合分数),还需要处理下一个显示,和当前显示的方块
        else
        {
                DelSqure();
                for (int i = 0; i < 4; ++i)
                  {
                     Block[i].x = NextBlock[i].x + 4;
                     Block[i].y = NextBlock[i].y;
                     if (GameClient[Block[i].x][Block[i].y])
                                {
                                   // Stop the Periodic Timer 
                                   gBS->SetTimer(TimerEvent, TimerCancel, TIMER_PERIOD_INIT);   
                                }
                                else
                                   GameClient[Block[i].x][Block[i].y] = TRUE;
                  }
                  
                  cS = S;  cF = F;
                  S = rand()%4;
                  F = rand() % BLOCKSTYLES;
                  for (int i = 0; i < 4; ++i)
                    {
                         NextBlock[i].x = Blocks[F][S].pt[i].x;
                         NextBlock[i].y = Blocks[F][S].pt[i].y;
                    }
        }
        //要求重绘
        ReDraw=TRUE;
}

void ConstructGame()
{
        EFI_STATUS      Status;
        CHAR16          ChrSide[2]={0,0};
            
        gST->ConOut->ClearScreen(gST->ConOut);
        gST->ConOut->EnableCursor(gST->ConOut, FALSE);
        gST->ConOut->SetCursorPosition(gST->ConOut, 0, 0);

        //初始化第一个出现的方块,随机生成
        cS = rand() % 4;
        cF = rand()% BLOCKSTYLES;
        for (int i = 0; i < 4; ++i)
          {
                Block[i].x = Blocks[cF][cS].pt[i].x + 4;
                Block[i].y = Blocks[cF][cS].pt[i].y;
                GameClient[Block[i].x][Block[i].y] = TRUE;
          }
        //生成下一个
        S = rand() % 4;
        F = rand()% BLOCKSTYLES;
        for (int i = 0; i < 4; ++i)
          {
               NextBlock[i].x = Blocks[F][S].pt[i].x;
               NextBlock[i].y = Blocks[F][S].pt[i].y;
          }
        //绘制外围
        ChrSide[0]=  BOXDRAW_DOUBLE_DOWN_RIGHT;        
        Print(L"%S",ChrSide); 
        ChrSide[0]=  BOXDRAW_DOUBLE_HORIZONTAL;
        for (UINT16 i=0;i<NUMCOLUMNBLOCKS;i++) {Print(L"%S",ChrSide);}
        ChrSide[0]=BOXDRAW_DOUBLE_DOWN_LEFT;
        Print(L"%S\n",ChrSide); 

        ChrSide[0]=  BOXDRAW_DOUBLE_VERTICAL;  
      
        for (UINT16 j=0;j<NUMLINEBLOCKS;j++) 
        {
                Print(L"%S",ChrSide);
                for (UINT16 i=0;i<NUMCOLUMNBLOCKS;i++) {Print(L" ");}  
                Print(L"%S\n",ChrSide); 
        }

        ChrSide[0]=  BOXDRAW_DOUBLE_UP_RIGHT;        
        Print(L"%S",ChrSide); 
        ChrSide[0]=  BOXDRAW_DOUBLE_HORIZONTAL;
        for (UINT16 i=0;i<NUMCOLUMNBLOCKS;i++) {Print(L"%S",ChrSide);}
        ChrSide[0]=BOXDRAW_DOUBLE_UP_LEFT;
        Print(L"%S\n",ChrSide); 
      
        Status = gBS->CreateEvent( 
                EVT_TIMER | EVT_NOTIFY_SIGNAL,
                TPL_CALLBACK,
                TimerCallback, 
                NULL,
                &TimerEvent);
        ASSERT_EFI_ERROR(Status);
    
        //设置定时触发 
        Status = gBS->SetTimer(TimerEvent, TimerPeriodic, TIMER_PERIOD_INIT);       
}     

void DestructGame()
{
        gBS->SetTimer(TimerEvent, TimerCancel, TIMER_PERIOD_INIT);  
        gBS->CloseEvent(TimerEvent);   
        gST->ConOut->ClearScreen(gST->ConOut);                
        gST->ConOut->EnableCursor(gST->ConOut, TRUE);
}        

/**
  The user Entry Point for Application. 
  The user code starts with this function as the real entry point for the application.
  @param  UINTN Argc     the arguments amount
  @param  CHAR16 **Argv  arguments list
  @return INTN 
**/
INTN ShellAppMain( UINTN Argc, CHAR16 **Argv )
{
      EFI_STATUS Status;
      EFI_INPUT_KEY Key;
        
      ConstructGame();
  
      // The main loop of the game
      while(quit==FALSE) {
         Status= gST->ConIn->ReadKeyStroke(gST->ConIn,&Key);
         if (Status == EFI_SUCCESS) {
                if (Key.ScanCode==0x17) {quit=TRUE;}
                switch (Key.ScanCode)
                {
                case 0x04: //左键
                        if (CanLeft(Block))
                            Left(Block);
                        ReDraw=TRUE;
                        break;

                case 0x03: //右键
                        if (CanRight(Block))
                                Right(Block);
                        ReDraw=TRUE;
                        break;

                case 0x01: //上键
                        if (CanChange(Block))
                                Change(Block);
                        ReDraw=TRUE;
                        break;

                case 0x02: //下键
                        while (CanDown(Block))
                                Down(Block);
                        ReDraw=TRUE;
                        break;

                default:
                        break;
                } //End of switch
                
                //p键 可以起用来暂停
                if (Key.UnicodeChar == 'p')
                        pause = !pause;
                else if (Key.UnicodeChar == 'r')
                {   // r 键用来重置游戏
                        Score = 0;
                        for (int x = 0; x < NUMCOLUMNBLOCKS; ++x)
                        {
                                for (int y = 0; y < NUMLINEBLOCKS; ++y)
                                        GameClient[x][y] = FALSE;
                        }
                        cS = rand() % 4;
                        cF = rand() % BLOCKSTYLES;
                        for (int i = 0; i < 4; ++i)
                        {
                                Block[i].x = Blocks[cF][cS].pt[i].x + 4;
                                Block[i].y = Blocks[cF][cS].pt[i].y;
                                GameClient[Block[i].x][Block[i].y] = TRUE;
                        }
                        S = rand() % 4;
                        F = rand() % BLOCKSTYLES;
                        for (int i = 0; i < 4; ++i)
                        {
                                NextBlock[i].x = Blocks[F][S].pt[i].x;
                                NextBlock[i].y = Blocks[F][S].pt[i].y;
                        }
                        gBS->SetTimer(TimerEvent, TimerPeriodic, TIMER_PERIOD_INIT); 
                        pause = FALSE;
                        ReDraw=TRUE;
                }                
        }
        
        //绘制界面
        if (ReDraw) {

                //显示游戏区的方块
                for (int y = 0; y < NUMLINEBLOCKS; ++y)
                {
                        gST->ConOut->SetCursorPosition(gST->ConOut, 1, y+1);
                        for (int x = 0; x < NUMCOLUMNBLOCKS; ++x)
                        {
                                if (GameClient[x][y])
                                {       
                                        Print(L"O");
                                }
                                else Print(L" ");
                        }
                        Print(L"\n");
                }
                
                for (int j=0;j<4;j++)
                {
                   gST->ConOut->SetCursorPosition(gST->ConOut, (NUMCOLUMNBLOCKS + 20), 6+j);
                   for (int i=0;i<4;i++) {
                           Print(L" ");
                   }
                }  
                
                //显示下一个方块区域的方块
                for (int i = 0; i < 4; ++i)
                {
                        gST->ConOut->SetCursorPosition(gST->ConOut, (NextBlock[i].x + NUMCOLUMNBLOCKS + 20), NextBlock[i].y +6);
                        Print(L"O");
                }
                ReDraw=FALSE;
        }
        
    }

    DestructGame();  

    return 0;
}

 

运行结果:

这个只是一个简单的框架,没有实现升级也没有华丽的界面,但是已经具备了俄罗斯方块的基本理念,有兴趣的朋友请继续完善它吧。

参考:
1.https://www.gamersky.com/wenku/201406/369340.shtml 三十年成就经典传奇 《俄罗斯方块》发展史
2. https://blog.csdn.net/zxlstudio/article/details/8899776 C语言俄罗斯方块(简易版) 写的非常漂亮,可以作为Windows SDK 编程的典范。

Step to UEFI (161)Print 直接输出 ASCII 的String

之前我在处理 ASCII 的 String 时,都是进行格式化后再输出的,最近偶然间看到了 Print 有一个 %a 参数可以用来直接输出 CHAR8 这样定义的字符串。实验代码如下:

#include  <Uefi.h>
#include  <Library/UefiLib.h>
#include  <Library/ShellCEntryLib.h>
#include  <Library/BaseMemoryLib.h>

int
EFIAPI
main (
  IN int Argc,
  IN CHAR16 **Argv
  )
{

    CHAR8      Buffer[]="This is a simple test\n\r from www.lab-z.com\n\r";

    Print(L"%a", Buffer);
  return EFI_SUCCESS;
}

 

运行结果:

完整的代码下载:
Printa

关于 Print 函数的具体实现,可以在 MdePkg\Library\BasePrintLib\PrintLibInternal.c 中看到。

Step to UEFI (163)替换已经存在Protocol中函数的实验

熟悉汇编语言的朋友都知道,DOS下面有很多INT服务,通过这样的入口能够实现硬件相关的服务。比如:INT 15 中可以支持关闭显示。与之类似,UEFI 里面是通过Protocol来实现这样的功能的。找到需要的 Protocol 即可 Call 到其提供的函数。这样的设计也给我们一个机会,可以用自己编写的函数来替换真实的函数。为此,设计了一个实验替换掉 Simple File System 中提供的 Read函数,这样每次 Application 读取到的都会是我们提供的数据。
代码比较长,从原理的角度解释具体流程:
首先,编写测试用的 Application,用 LocateHandleBuffer 枚举出来所有的 Simple File System Protocol,然后在找到的 Handle 上用 OpenVolume 打开 Root Directory。打开之后再用Open 函数打开文件,最后用 Read 来读取文件内容并且显示出来。实验中读取的是一个 44 Bytes 长的 Test.txt 文件。
实验环境是 NT32模拟器,我们只在 fs0: 上放置了 test.txt,因此,能找到2个SimpleFileSystemProtcol,第一个能够读取并且显示内容,第二个会出现Open File error 的错误。

接下来,编写我们的“驱动”,为了简单起见,这里并不是一个正经的驱动。对于我们来说,只是需要程序常驻内存,结束之后不希望被释放掉,否则会出现其他程序调用而无法找到函数的情况。因此,我们在 MdeModulePkg 中写程序,编译之后使用 Load 来加载。代码流程如下:
1. 在入口 MyEntryPoint 中查找一个 SimpleFileSystemProtocol,先保存OpenVolume 的实际入口,再用自己编写的 MySimpleFileSystemOpenVolume替换掉这个入口;
2. Application 在调用 OpenVolume 函数的时候,实际上是会进入MySimpleFileSystemOpenVolume 中。在这个函数中,使用之前保存的实际OpenVolume完成工作,然后将 OpenVolume返回的EFI_FILE_PROTOCOL替换为 MySimpleFileSystemOpen;
3. Application在调用 Open 函数的时候,实际上是会进入MySimpleFileSystemOpen。我们在这里检查参数判断要打开的是否为我们指定的 test.txt 文件,如果是,那么用 MySimpleFileSystemOpen来替换实际的 Open 函数;
4. 这样,当 Application 要Read test.txt 的时候,我们就可以送一个假的值出去。这样就实现了替换的功能。

完整的代码:
用于测试的 Application

#include  <Uefi.h>
#include  <Library/UefiLib.h>
#include  <Library/ShellCEntryLib.h>
#include  <Library/BaseMemoryLib.h>
#include  <Protocol/SimpleFileSystem.h>
#include  <Library/MemoryAllocationLib.h>
                        
extern EFI_RUNTIME_SERVICES      *gRT;
extern EFI_BOOT_SERVICES         *gBS;

int
EFIAPI
main (
  IN int Argc,
  IN CHAR16 **Argv
  )
{

    EFI_STATUS Status;
    EFI_HANDLE  *HandleBuffer = NULL;
    UINTN       NumHandles;
    UINTN       Index; 
    EFI_FILE_IO_INTERFACE *ioDevice; 
    EFI_FILE_HANDLE handleRoots;
    EFI_FILE_PROTOCOL *TestFile;
    CHAR8      Buffer[4*1024];
    UINTN      BufferSize;
    
    //Find all Simnple File System Protocol Handle
    Status = gBS->LocateHandleBuffer(
        ByProtocol,
        &gEfiSimpleFileSystemProtocolGuid,
        NULL,
        &NumHandles,
        &HandleBuffer);
        
    Print(L"Walk handles %ld\n", NumHandles);
    for(Index =0; Index<NumHandles; Index++){
        //Get one Simple File Protocol from a Handle    
        Status = gBS->HandleProtocol (
                        HandleBuffer[Index],
                        &gEfiSimpleFileSystemProtocolGuid,
                        (VOID **) &ioDevice
                        );
        //Opens the root directory on the volume                
        Status = ioDevice->OpenVolume( ioDevice, &handleRoots );   
        if (EFI_ERROR(Status)) {
                Print(L"OpenVolume error \n");
        }           
        //Open a file
        Status = handleRoots->Open(handleRoots,&TestFile,L"test.txt",EFI_FILE_MODE_READ, 0);
        if (!EFI_ERROR(Status)) {
                //The size of "Test.txt" is 44bytes, I use hard code here
                BufferSize=44;
                TestFile->Read(TestFile,&BufferSize,&Buffer);
                Print(L"%a",Buffer);
                TestFile->Close(TestFile);
        }
        else
        {
                Print(L"Open file error [%r]\n",Status);    
        }
    }
    
  FreePool (HandleBuffer);
  
  return EFI_SUCCESS;
}

 

简单的“驱动” 代码

#include <PiDxe.h>
#include <Library/UefiLib.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/DebugLib.h>
#include <Library/UefiDriverEntryPoint.h>
#include <Library/MemoryAllocationLib.h>
#include <Protocol/SimpleFileSystem.h>
#include <Library/MemoryAllocationLib.h>

extern EFI_SYSTEM_TABLE         *gST;

EFI_GUID        gEfiSimpleFileSystemProtocolGuid ={ 0x964E5B22, 0x6459, 0x11D2, 
                        { 0x8E, 0x39, 0x00, 0xA0, 0xC9, 0x69, 0x72, 0x3B }};

//Backup of Read function                        
EFI_FILE_READ OldSimpleFileSystemRead;
//Backup of Open function
EFI_FILE_OPEN OldSimpleFileSystemOpen;
EFI_SIMPLE_FILE_SYSTEM_PROTOCOL_OPEN_VOLUME         OldSimpleFileSystemOpenVolume;

//This one will replace the Read function of Read in Simple File System
EFI_STATUS
EFIAPI
MySimpleFileSystemRead (
  IN     EFI_FILE_PROTOCOL  *This,
  IN OUT UINTN              *BufferSize,
  OUT    VOID               *Buffer 
)
{
     CHAR8   TestBuffer[]="Replaced buffer\n\r";
     AsciiStrCpyS(Buffer,AsciiStrnLenS(TestBuffer,255)+1,TestBuffer);
     return EFI_SUCCESS;
}

//This one will replace the Open function of Open in Simple File System
EFI_STATUS
EFIAPI
MySimpleFileSystemOpen (
  IN  EFI_FILE_PROTOCOL  *This,
  OUT EFI_FILE_PROTOCOL  **NewHandle,
  IN  CHAR16             *FileName,
  IN  UINT64             OpenMode,
  IN  UINT64             Attributes
  )
{
        EFI_STATUS Status;

        //Call into Open function in the Simple File system
        Status=(*OldSimpleFileSystemOpen)(This,NewHandle,FileName,OpenMode,Attributes);
        if (!EFI_ERROR(Status)) {
                //Check the filename to make sure it's the one we will replace
                if (StrnCmp(FileName,L"test.txt",StrLen(FileName))==0) {
                        if ((**NewHandle).Read!=MySimpleFileSystemRead) {
                                //Backup the Read Function
                                OldSimpleFileSystemRead=(**NewHandle).Read;
                                //Replace the Read Function
                                (**NewHandle).Read=MySimpleFileSystemRead;
                        }
                        
                }
        }
       return Status;
} 


EFI_STATUS
EFIAPI
MySimpleFileSystemOpenVolume (
  IN EFI_SIMPLE_FILE_SYSTEM_PROTOCOL  *This,
  OUT EFI_FILE_PROTOCOL               **Root
  )
{
        EFI_STATUS Status;
        
        Status=(*OldSimpleFileSystemOpenVolume)(This,Root);
         
        if (!EFI_ERROR(Status)) {
                if ((**Root).Open!=MySimpleFileSystemOpen) {
                        OldSimpleFileSystemOpen=(**Root).Open;
                        (**Root).Open=MySimpleFileSystemOpen;
                }        
        }
       return Status;
}  
EFI_STATUS
EFIAPI
MyEntryPoint (
  IN EFI_HANDLE           ImageHandle,
  IN EFI_SYSTEM_TABLE     *SystemTable
  )
{
        EFI_STATUS        Status;
        //EFI_FILE_PROTOCOL *Root;
        EFI_SIMPLE_FILE_SYSTEM_PROTOCOL *SimpleFile;
        
        //Look for one Simple File System Protocol 
        Status = gBS->LocateProtocol (
                        &gEfiSimpleFileSystemProtocolGuid,
                        NULL,
                        &SimpleFile);
        if (EFI_ERROR(Status)) {
           gST->ConOut->OutputString(gST->ConOut,L"Can't find Simple File PROTOCOL\n");
           return Status;
        }
        OldSimpleFileSystemOpenVolume=SimpleFile->OpenVolume;
        SimpleFile->OpenVolume=MySimpleFileSystemOpenVolume;
        
        return Status;
}

 

运行结果:

完整的代码下载:

repfun