最近想查一下 USB Host Shield 的资料,惊奇的发现网站怎么都上不去。起初以为是网络的问题,后来换了几个地方都上不去,恍然大悟应该是 GFW 的问题。
科学上网,下载资料 https://www.circuitsathome.com/downloads/
很多时候我们需要制作全盘镜像,通过这样的方法能够们快速安装系统和驱动,另外一些客户问题也可以使用这样的方式将客户环境完整的“搬迁”到我们需要实验的机器上。起初,在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

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

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

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

恢复的过程:
1.选择 Restore 页面,然后 Browse Image 选择你的镜像文件

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

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

最终,我还特地测试了一下新制作的盘是否支持 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 之后的分区:

后来和他们售后进行了沟通,对方建议我在备份的时候使用命令来额外备份分区。因为操作过于复杂,我并没有试验。
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
本打算试试,但是体积是在太大了,占用空间过高不划算。
七段数码管算是很基础的元件了,从使用的角度来说几乎和控制多个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中定义好的音频数据提取出来,
这个工具就是用来实现这个功能的。它能将单声道 Wave 中的声音信息提取出来放到一个文本文件中。
使用方法:
wave.exe 声音文件.wav >> 输出文件名.txt
注意:
本工具只支持单声道 wav文件。
编译工具为 delphi xe2
下载
wave2h
“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 编程的典范。
之前我在处理 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 中看到。
熟悉汇编语言的朋友都知道,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;
}
完整的代码下载:
在我们使用 Eagle 绘制电路板的的过程中,有时候需要取消当前已经布好的线路,比如使用自动布线,但是发现选择的线宽太窄,需要重新自动布线。
这时候可以使用 ripup * 命令即可取消之前的布线。
参考:
1.http://web.mit.edu/xavid/arch/i386_rhel4/help/81.htm
最近需要使用 DEVCON 又不想下载庞大的 WDK ,网上搜索了一下,找到如下的方法:
Windows 10 version 1803 Redstone 4
(April 2018 Update)
Windows Build: 10.0.17134
Driver Kit Build: 10.0.17134
下载之后,用7Z 之类的工具进行解压,在解压之后的文件中查找 filbad6e2cce5ebc45a401e19c613d0a28f 文件,改名为 devcon.exe 即可。
参考:
1.http://www.cnblogs.com/litifeng/p/9211792.html 如何安全的下载Devcon.exe文件
2.https://superuser.com/questions/1002950/quick-method-to-install-devcon-exe