Step to UEFI (277)QEMU 增加自定义的 FFS和读取

这次实验的是在 OVMF 生成的BIOS中插入一个Binary ,然后在代码中将这个Binary 读取出来。

第一个目标:在 OVMF 中插入 Binary。

1.我们准备一个 message.txt,其中内容是简单的字符串:

This is a test message comes from 
www.lab-z.com

2.在\OvmfPkg\OvmfPkgX64.fdf 文件中,加入下面的代码

!if $(E1000_ENABLE)
  FILE DRIVER = 5D695E11-9B3F-4b83-B25F-4A8D5D69BE07 {
    SECTION PE32 = Intel3.5/EFIX64/E3522X2.EFI
  }
!endif

#LABZDebug_Start
FILE FREEFORM = C3E36D09-2023-0829-A857-D5288FE33E28 Align=4K {
  SECTION RAW = OvmfPkg/LabzBin/message.txt
}
#LABZDebug_End

!include NetworkPkg/Network.fdf.inc
  INF  OvmfPkg/VirtioNetDxe/VirtioNet.inf

3.使用工具查看放置的FFS,可以看到正确的增加到 BIOS 中

这样,第一个目标已经完成,我们成功的生成了一个FFS文件。

第二个目标,将这个 FFS文件从FV中读取出来。之前我们做过类似的实验,在【参考1】中有介绍。这次我们编写一个 UEFI Shell Application ,显示前面插入的 FFS文件的内容。测试代码如下:

#include  <Uefi.h>
#include  <Library/UefiLib.h>
#include  <Library/ShellCEntryLib.h>
#include <Library/UefiBootServicesTableLib.h>
#include  "PiFirmwareVolume.h"
#include  "PiFirmwareFile.h"
#include  "FirmwareVolume2.h"

INTN
EFIAPI
ShellAppMain (
    IN UINTN Argc,
    IN CHAR16 **Argv
)
{
	CONST EFI_GUID    NameGuid= { 0xC3E36D09, 0x2023, 0x0829,
		{ 0xA8, 0x57, 0xD5, 0x28, 0x8F, 0xE3, 0x3E, 0x28 }
	};
	EFI_SECTION_TYPE  SectionType=EFI_SECTION_RAW;

	VOID             *Buffer=NULL;
	UINTN             Size=0;
	UINT32            AuthenticationStatus=0;
	EFI_STATUS  	  Status;
	EFI_FIRMWARE_VOLUME2_PROTOCOL *Fv;

	Status = gBS->LocateProtocol (
	             &gEfiFirmwareVolume2ProtocolGuid,
	             NULL,
	             (VOID **) &Fv
	         );
	if (EFI_ERROR (Status))
	{
		Print(L"[EFI_FIRMWARE_VOLUME2_PROTOCOL not found]\n");
		return EFI_NOT_FOUND;
	}
	//
	// Read desired section content in NameGuid file
	//
	Status      = Fv->ReadSection (
	                  Fv,
	                  &NameGuid,
	                  SectionType,
	                  0,
	                  &Buffer,
	                  &Size,
	                  &AuthenticationStatus);
	UINT8 *P=(UINT8 *)Buffer;
	Print(L"[EFI_FIRMWARE_VOLUME2_PROTOCOL %r]\n",Status);

	for (UINTN i=0; i<Size; i++)
	{
		Print(L"%c",P[i]);
	}
	Print(L"\n");

	return(0);
}

运行的结果如下图所示,可以看到正确的读取出我们存放的内容:

完整的代码下载:

参考:

1. https://www.lab-z.com/getffs/  代码读取一个 FFS

Step to UEFI (276)宏和结构体初始化表格

在 EDK2 中有一种比较有趣的定义和初始化Table 的方法,主要是基于 __VA_ARGS__ 这个宏。

“__VA_ARGS__是一个预处理宏,用于表示可变数量的参数。当在宏定义中使用__VA_ARGS__,它会自动展开为传递给宏的实际参数。以下是一个示例使用__VA_ARGS__的宏定义代码:
#include &lt;stdio.h>
 
#define PRINT_ARGS(...) printf(__VA_ARGS__)
 
int main() {
    PRINT_ARGS("Hello, %s!\n", "World");
    return 0;
}
上述代码中,宏定义PRINT_ARGS使用__VA_ARGS__来表示可变数量的参数,并通过printf函数打印参数。在main函数中,我们调用PRINT_ARGS宏来打印字符串"Hello, World!"。运行结果为输出"Hello, World!"。
总结:__VA_ARGS__是一个用于表示可变数量参数的预处理宏,在宏定义中使用它可以方便地处理不定数量的参数。“----来自百度

很多时候,我们定义一个 Table 用来传递一些常量,Table需要给出具体的长度,通过这个宏可以实现自动给出Table 的长度,避免用户手工计数的麻烦。

下面是一个示例代码:

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

#define MY_TABLE_INIT(Vid,Did,...) \
{ \
  { Vid, Did, (sizeof((UINT32[]){__VA_ARGS__})/sizeof(UINT32)) }, \
  { __VA_ARGS__ } \
}

typedef struct {
  UINT16  VendorId;
  UINT16  DeviceId;
  UINT16  DataDwords;
} MY_TABLE_HEADER;

typedef struct  {
  MY_TABLE_HEADER  Header;
  UINT32 Data[];
} ONE_TABLE;

ONE_TABLE Table = MY_TABLE_INIT (
  0x1234, 0x5678,
  
  // Raw Data
  0x01234567,
  0x89ABCDEF,
  0xFEDCBA98,
  0x76543210
);

INTN
EFIAPI
ShellAppMain (
  IN UINTN Argc,
  IN CHAR16 **Argv
  )
{
  Print(L"Table:\n");
  Print(L"   VendorId:[%X]\n",Table.Header.VendorId);
  Print(L"   DeviceId:[%X]\n",Table.Header.DeviceId);
  Print(L"   Size    :[%X]\n",Table.Header.DataDwords);  
  
  for (int i=0;i<Table.Header.DataDwords;i++) {
	  Print(L"[%04X]",Table.Data[i]);  
  }
  Print(L"\n");
  return(0);
}

运行结果如下:

上面代码的解释如下:

1.首先我们定义一个 ONE_TABLE 结构体用来“携带”数据。

typedef struct  {
  MY_TABLE_HEADER  Header;
  UINT32 Data[];
} ONE_TABLE;

从定义可以看到,这个结构体包含了一个头,还有一个变长的数据段。头可以实现用于识别判断这个Table 是否为我们需要的目的,例如,其中有DID和VID 信息。具体定义如下,特别注意 DataDwords 给出了后面变长数据段的长度:

typedef struct {
  UINT16  VendorId;
  UINT16  DeviceId;
  UINT16  DataDwords;
} MY_TABLE_HEADER;

对于DataDwords 就是我们前面提到的“Table需要给出具体的长度”的问题。

2.为了解决上述问题,通过下面的宏来解决:

#define MY_TABLE_INIT(Vid,Did,...) \
{ \
  { Vid, Did, (sizeof((UINT32[]){__VA_ARGS__})/sizeof(UINT32)) }, \
  { __VA_ARGS__ } \
}

其中(sizeof((UINT32[]){__VA_ARGS__})/sizeof(UINT32)) 就是计算长度的代码。最终的结果是以 UINT32(DWORD)给出的。

3.初始化定义如下,可以看到DataDwords的计算是宏直接完成的,并不需要我们直接提供

ONE_TABLE Table = MY_TABLE_INIT (
  0x1234, 0x5678,
  
  // Raw Data
  0x01234567,
  0x89ABCDEF,
  0xFEDCBA98,
  0x76543210
);

可以看到,通过上面的方法可以帮助我们方便的实现可变数据的长度定义,有兴趣的朋友不妨尝试一下。

完整的代码下载:

批处理延时和计算经过时间

首先介绍一下批处理中延时的实现:下面代码实现延时3秒

CHOICE /T 3 /C ync /CS /D y

计算经过时间,以秒为单位:

@echo off
set "t=%time%"
::You code start here

::You code end here
set "t1=%time%"

if "%t1:~,2%" lss "%t:~,2%" set "add=+24"
set /a "times=(%t1:~,2%-%t:~,2%%add%)*3600+(1%t1:~3,2%%%100-1%t:~3,2%%%100)*60+(1%t1:~6,2%%%100-1%t:~6,2%%%100)" 
echo Time Used %times% Seconds
pause

上述代码合在一起进行测试:

@echo off
set "t=%time%"
::You code start here
CHOICE /T 3 /C ync /CS /D y
::You code end here
set "t1=%time%"

if "%t1:~,2%" lss "%t:~,2%" set "add=+24"
set /a "times=(%t1:~,2%-%t:~,2%%add%)*3600+(1%t1:~3,2%%%100-1%t:~3,2%%%100)*60+(1%t1:~6,2%%%100-1%t:~6,2%%%100)" 
echo Time Used %times% Seconds
pause

Step to UEFI (275)UEFI 创建内存虚拟盘

前面介绍过在 UEFI 下创建内存虚拟盘的操作,这次介绍如何创建包含需要内容的内存虚拟盘。生成的虚拟盘可以在没有硬盘的情况下充当临时文件的存放位置。当然如果关机或者断电后,盘中内容会消失。

第一步,根据【参考1】制作一个磁盘镜像VHD,这里出于体积考虑,制作了一个 64MB 的FAT32 空盘;

第二步,上面制作磁盘镜像大部分内容都是 0x00(可以看作是以512Bytes为单位的稀疏矩阵),因此我们还需要一个工具提取文件中的非零内容保存起来,这样能够有效降低镜像尺寸。使用 C# 编写代码如下。简单的说就是以512Bytes为单位读取文件,然后检查512 bytes 中是否为全0.如果不是就记录下来保存到文件中。每一个项目是由 4 Bytes 记录加一个 512Byte的数组构成。特别注意 VHD 镜像文件末尾包含了一个文件头,我们这里会对文件大小取整,对于末尾的文件头不会进行处理。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;

namespace ConsoleApp7
{
    class Program
    {
        static void Main(string[] args)
        {
            if (args.Count() == 0) {
                Console.WriteLine("Please input file name!");
                Console.ReadKey();
                Environment.Exit(0);
            }

            if (File.Exists((args[0] + ".raw"))) {
                File.Delete(args[0] + ".raw");
            }
            FileStream fs = new FileStream(args[0], FileMode.Open);
            FileStream RawFs = new FileStream(args[0]+".raw", FileMode.CreateNew);
            byte[] array = new byte[512];
            UInt32 Data;
            Boolean EmptyMark;

            // 写入 Disk Image 的Size (以 1MB 为单位)
            Data = (UInt32) fs.Length / 1024 / 1024;
            RawFs.Write(BitConverter.GetBytes(Data), 0, 4);
            
            // 处理整数以内的磁盘,例如:扫描 64MB 以内的内容生成及镜像
            while (fs.Position < fs.Length / 1024 / 1024 * 1024 * 1024) {
                EmptyMark = true;
                fs.Read(array, 0, array.Length);
                for (int i = 0; i < array.Length; i++)
                {
                    if (array[i] != 0x00)
                    {
                        EmptyMark = false;
                        break;
                    }
                }
                if (EmptyMark==false)
                {
                    Data = (UInt32)(fs.Position / 512 - 1);
                    RawFs.Write(BitConverter.GetBytes(Data),0,4);
                    RawFs.Write(array, 0, array.Length);
                    Console.WriteLine("{0}", Data);
                }
            }
            RawFs.Close();
            Console.WriteLine("Done!");

        }
    }
}

例如:64MB的FAT32 空盘,经过上述操作后会变成6K 大小。

第三步,编写Shell 下的 UEFI Application,创建内存虚拟盘,然后读取前述生成的文件,将内容放在虚拟盘中。这样就得到了一个带有文件系统的内存虚拟盘。代码如下:

#include <Library/BaseLib.h>
#include <Uefi.h>
#include <Library/UefiLib.h>
#include <Library/PrintLib.h>
#include <Library/ShellCEntryLib.h>
#include <Protocol/RamDisk.h>
#include <Protocol/DevicePathToText.h>
#include <Protocol/HiiDatabase.h>
#include <Protocol/HiiPackageList.h>
#include <Protocol/HiiImageEx.h>
#include <Protocol/PlatformLogo.h>
#include <Protocol/GraphicsOutput.h>
#include <Library/UefiApplicationEntryPoint.h>
#include <Library/ShellLib.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/BaseMemoryLib.h>
#include <Library/MemoryAllocationLib.h>

//DO NOT REMOVE IMAGE_TOKEN (IMG_LOGO)

extern EFI_BOOT_SERVICES         *gBS;

EFI_STATUS
EFIAPI
UefiMain (
    IN EFI_HANDLE        ImageHandle,
    IN EFI_SYSTEM_TABLE  *SystemTable
)
{
	EFI_STATUS               Status;
	EFI_RAM_DISK_PROTOCOL    *MyRamDisk;
	EFI_FILE_HANDLE   		FileHandle;
	UINTN	tmp, DiskSize,SectorIndex;
	UINT64                   *StartingAddr,Position;
	UINT8                    Sector[512];
	UINTN					 ImageFileSize;
	EFI_DEVICE_PATH_PROTOCOL *DevicePath;

	// 磁盘镜像文件
	CHAR16	*DiskImage=L"DiskImage.BIN";
	// 如果磁盘镜像不存在,报错退出
	if (ShellFileExists(DiskImage)!=EFI_SUCCESS)
	{
		Print(L"Couldn't find 'DiskImage.bin'\n");
		return EFI_INVALID_PARAMETER;
	}

	// 打开磁盘镜像文件
	Status = ShellOpenFileByName(
	             DiskImage,
	             (SHELL_FILE_HANDLE *)&FileHandle,
	             EFI_FILE_MODE_READ,
	             0);
	if(Status != RETURN_SUCCESS)
	{
		Print(L"OpenFile failed!\n");
		return EFI_INVALID_PARAMETER;
	}


	// Look for Ram Disk Protocol
	Status = gBS->LocateProtocol (
	             &gEfiRamDiskProtocolGuid,
	             NULL,
	             &MyRamDisk
	         );
	if (EFI_ERROR (Status))
	{
		Print(L"Couldn't find RamDiskProtocol\n");
		return EFI_ALREADY_STARTED;
	}

	tmp=4;
	Status = ShellReadFile(FileHandle,&tmp,&DiskSize);
	Print(L"Disk size %dMB\n",DiskSize);
	DiskSize=DiskSize*1024*1024;

	//Allocate a memory for Image
	StartingAddr = AllocateReservedZeroPool	((UINTN)DiskSize);	 
	if(StartingAddr==0)
	{
		Print(L"Allocate Memory failed!\n");
		return EFI_SUCCESS;
	}

	ShellGetFileSize(FileHandle,&ImageFileSize);
	Position=0;
	Print(L"File size %d\n",ImageFileSize);

	while (Position<ImageFileSize)
	{
		tmp=4;
		Status = ShellReadFile(FileHandle,&tmp,&SectorIndex);
		if (Status!=EFI_SUCCESS)
		{
			break;
		}
		Print(L"Sector index %d\n",SectorIndex);
		tmp=512;
		Status = ShellReadFile(FileHandle,&tmp,&Sector);
		if (Status==EFI_SUCCESS)
		{
			//Print(L"Read success %d\n",(UINT8*)StartingAddr+SectorIndex*512);
			CopyMem((UINT8*)StartingAddr+SectorIndex*512,&Sector,tmp);
		}
		ShellGetFilePosition(FileHandle,&Position);
		//Print(L"postion %d\n",Position);
	}

	//
	// Register the newly created RAM disk.
	//
	Status = MyRamDisk->Register (
	             ((UINT64)(UINTN) StartingAddr),
	             DiskSize,
	             &gEfiVirtualDiskGuid,
	             NULL,
	             &DevicePath
	         );
	if (EFI_ERROR (Status))
	{
		Print(L"Can't create RAM Disk!\n");
		return EFI_SUCCESS;
	}
	ShellCloseFile(&FileHandle);

	return EFI_SUCCESS;
}

基本思路就是:打开镜像文件读取4 Bytes,然后创建这个大小的 Memory Disk。接下来读取 4 Bytes的扇区位置,然后再读取512字节的扇区内容,将这个内容存放在内存中的对应位置。

在Shell 下执行这个程序后,使用 map -r 即可看到新生成的内存盘。

UEFI 代码和编译后的EFI 文件,同时内置了一个64MB的磁盘镜像。

前面提到的 Windows 下的磁盘镜像扫描工具。内置了一个 64MB的空磁盘镜像。

参考:

1. https://www.lab-z.com/vt/

Step to UEFI (274)EFI Application MEMTEST86 自启动研究

前面提到了如何将 MemTest86 打包为一个 EFI 文件,美中不足的是运行这个 EFI 之后无法自动跳转到 Memtest86中执行,还需要手工运行map 和执行。针对这个问题增加代码进行实验。

新增的代码如下:

1.代码从创建 RAM Disk 开始

	//
	// Register the newly created RAM disk.
	//
	Status = MyRamDisk->Register (
	             ((UINT64)(UINTN) StartingAddr),
	             FileSize,
	             &gEfiVirtualDiskGuid,
	             NULL,
	             &DevicePath
	         );
	if (EFI_ERROR (Status))
	{
		Print(L"Can't create RAM Disk!\n");
		return EFI_SUCCESS;
	}

2.使用 ShellExecute 来运行 map -r 命令

	Print(L"Running map -r command!\n");
	CHAR16	  *MapCommmand=L"map -r";
	EFI_STATUS  CmdStat;
	Status = ShellExecute( &gImageHandle, MapCommmand, FALSE, NULL, &CmdStat);
	if (EFI_ERROR (Status))
	{
		Print(L"Can't run MAP command\n");
		return EFI_SUCCESS;
	}
	Print(L"%r\n",CmdStat);

3.接下来扫描所有的 FSx: 。如果存在某个FSx::\efi\boot\mt86.png,那就说明是 MemTest86 的盘。

	UINTN i;
	CHAR16 StrBuffer[80];
	BOOLEAN Found=FALSE;

	for (i=0; i&lt;9; i++)
	{
		UnicodeSPrint(StrBuffer,sizeof(StrBuffer),L"fs%d:\\efi\\boot\\mt86.png",i);
		Print(L"%s\n",StrBuffer);
		if (!EFI_ERROR(ShellFileExists(StrBuffer)))
		{
			UnicodeSPrint(StrBuffer,sizeof(StrBuffer),L"fs%d:\\efi\\boot\\BootX64.EFI",i);
			Found=TRUE;
			break;
		}
	}
	if (Found)
	{
		ShellExecute( &amp;gImageHandle, StrBuffer, FALSE, NULL, NULL);
	}

	return 0;

上述设计逻辑没有问题,但是测试发现无法达到预期的目标:MemTest86 不会自动运行起来。执行之后,仍然需要手工运行 Map -r ,然后找到新生成的 FsX:再进入执行。

于是进行调试,从ShellExecute()函数入手。

这个函数定义在 \ShellPkg\Library\UefiShellLib\UefiShellLib.c 这个文件中。

/**
  Cause the shell to parse and execute a command line.

  This function creates a nested instance of the shell and executes the specified
  command (CommandLine) with the specified environment (Environment). Upon return,
  the status code returned by the specified command is placed in StatusCode.
  If Environment is NULL, then the current environment is used and all changes made
  by the commands executed will be reflected in the current environment. If the
  Environment is non-NULL, then the changes made will be discarded.
  The CommandLine is executed from the current working directory on the current
  device.

  The EnvironmentVariables pararemeter is ignored in a pre-UEFI Shell 2.0
  environment.  The values pointed to by the parameters will be unchanged by the
  ShellExecute() function.  The Output parameter has no effect in a
  UEFI Shell 2.0 environment.

  @param[in] ParentHandle         The parent image starting the operation.
  @param[in] CommandLine          The pointer to a NULL terminated command line.
  @param[in] Output               True to display debug output.  False to hide it.
  @param[in] EnvironmentVariables Optional pointer to array of environment variables
                                  in the form "x=y".  If NULL, the current set is used.
  @param[out] Status              The status of the run command line.

  @retval EFI_SUCCESS             The operation completed sucessfully.  Status
                                  contains the status code returned.
  @retval EFI_INVALID_PARAMETER   A parameter contains an invalid value.
  @retval EFI_OUT_OF_RESOURCES    Out of resources.
  @retval EFI_UNSUPPORTED         The operation is not allowed.
**/
EFI_STATUS
EFIAPI
ShellExecute (
  IN EFI_HANDLE   *ParentHandle,
  IN CHAR16       *CommandLine OPTIONAL,
  IN BOOLEAN      Output OPTIONAL,
  IN CHAR16       **EnvironmentVariables OPTIONAL,
  OUT EFI_STATUS  *Status OPTIONAL
  )

从Log可以看到执行的是下面这段代码:

  //
  // Check for UEFI Shell 2.0 protocols
  //
  if (gEfiShellProtocol != NULL) {
    //
    // Call UEFI Shell 2.0 version (not using Output parameter)
    //
    return (gEfiShellProtocol->Execute (
                                 ParentHandle,
                                 CommandLine,
                                 EnvironmentVariables,
                                 Status
                                 ));
  }

进一步追踪,执行的是 \ShellPkg\Application\Shell\ShellProtocol.c定义的如下:

// Pure FILE_HANDLE operations are passed to FileHandleLib
// these functions are indicated by the *
EFI_SHELL_PROTOCOL  mShellProtocol = {
  EfiShellExecute,
  EfiShellGetEnv,
……
};

具体实现在 \ShellPkg\Application\Shell\ShellProtocol.c文件中:

EFI_STATUS
EFIAPI
EfiShellExecute (
  IN EFI_HANDLE   *ParentImageHandle,
  IN CHAR16       *CommandLine OPTIONAL,
  IN CHAR16       **Environment OPTIONAL,
  OUT EFI_STATUS  *StatusCode OPTIONAL
  )

运行方式如下:

    Temp = NULL;
    Size = 0;
    ASSERT ((Temp == NULL && Size == 0) || (Temp != NULL));
    StrnCatGrow (&Temp, &Size, L"Shell.efi -exit ", 0);
    StrnCatGrow (&Temp, &Size, CommandLine, 0);

    Status = InternalShellExecuteDevicePath (
               ParentImageHandle,
               DevPath,
               Temp,
               (CONST CHAR16 **)Environment,
               StatusCode
               );

    Status = InternalShellExecute (
               (CONST CHAR16 *)CommandLine,
               (CONST CHAR16 **)Environment,
               StatusCode
               );

从代码上看,出现问题的原因是:默认情况下(允许 嵌套/NEST),ShellExecute()运行的 MAP 命令会通过 “shell.efi map -r ”的方式运行,这样相当于重新启动了一个 Shell ,在新启动的 Shell 中执行了 map -r,运行完成后会返回调用者处于的 shell 中,之前的 map 加载出现的 Fsx:盘符失效,所以无法看到新添加的 fsx:。

解决方法:通过 shell.efi -nonest 参数启动 Shell ,这个 Shell 禁止了 Nest ,再次运行 mrd3 调用的 map -r 就是对于当前shell。

完整代码下载:

编译后的 EFI Application 下载:

“又不是不能用”逻辑引发的问题

最近碰到了一个困扰我很久的奇怪问题问题,最终研究发现这应该算是设计上“又不是不能用”引发的悲剧。

简单的说简单的说问题是这样的,我需要在 DSC 文件中修改一个PCD 定义:问题是这样的,我需要在 DSC 文件中修改一个PCD 定义:

gChipsetPkgTokenSpaceGuid.PcdLABZBuild|FALSE

于是将代码修改为:

#LABZDebug gChipsetPkgTokenSpaceGuid.PcdLABZBuild|FALSE
gChipsetPkgTokenSpaceGuid.PcdLABZBuild|TRUE

实际测试下来问题依旧存在,检查 Build 目录生成的中间代码最终确定是上述的修改没有起效。最终翻来覆去看代码,终于发现批处理使用如下代码来识别赋值:

findstr /C:"gChipsetPkgTokenSpaceGuid.PcdLABZBuild|FALSE" %WORKSPACE%\%PROJECT_REL_PATH%\%PROJECT_PKG%\Project.dsc >nul

因为被注释掉的代码在文件位置靠前,这个语句会把它当作正式的设定,所以导致了问题。

通常来说,本科计算机系毕业生足以编写能够实现 Parser 的代码,但是如果仅仅以“又不是不能用”作为标准来编写代码将会给使用者带来极大的麻烦。当然,如果使用者仍然遵循“又不是不能用”的逻辑来处理,还可以将代码写为如下形式,只是不知道下一个接手者是否会再撞上奇怪的问题:

gChipsetPkgTokenSpaceGuid.PcdLABZBuild|TRUE
#LABZDebug gChipsetPkgTokenSpaceGuid.PcdLABZBuild|FALSE

Step to UEFI (273打包为 EFI Application 的 MEMTEST86

前面介绍了最新的 MemTest86 ,美中不足的是这个版本需要制作启动盘,这次介绍一种将它打包为一个 EFI 的方法。

基本的思路是:将完整的 MemTest86 磁盘镜像按照资源打包到一个 EFI 文件中,然后再配合之前的RamDisk 知识将这个镜像加载到内存中。这样就相当于制作的镜像文件,跳进去就可以执行了。

代码要点:

1.将之前介绍的 MemTest86 制作成一个硬件镜像,然后将这个镜像命令为MemTest2023.png

2.MyRamDisk2.inf 中给出用到的文件如下

[Sources]
  MyRamDisk2.c
  MyRamDisk2.idf
  MemTest2023.png

3. MyRamDisk2.idf 文件内容如下

#image IMG_LOGO MemTest2023.png

4.主程序

a.首先找到当前 EFI 中的资源

//
        // Retrieve HII package list from ImageHandle
        //
        Status = gBS->OpenProtocol (
                        gImageHandle,
                        &gEfiHiiPackageListProtocolGuid,
                        (VOID **) &PackageListHeader,
                        gImageHandle,
                        NULL,
                        EFI_OPEN_PROTOCOL_GET_PROTOCOL
                        );
        if (EFI_ERROR (Status)) {
          Print(L"HII Image Package with logo not found in PE/COFF resource section\n");
          return Status;
        }

b.取得资源

//Step2. Parser HII Image
        ImageHeader=(EFI_HII_IMAGE_PACKAGE_HDR*)(PackageListHeader+1);
		ImageData=(UINT8 *)(ImageHeader+1);

c.解析资源之后拷贝到分配的内存中

	// Look for Ram Disk Protocol
        Status = gBS->LocateProtocol (
                        &gEfiRamDiskProtocolGuid,
                        NULL,
                        &MyRamDisk
                 );
        if (EFI_ERROR (Status)) {
            Print(L"Couldn't find RamDiskProtocol\n");
            return EFI_ALREADY_STARTED;
        }
					
        //Allocate a memory for Image
        Status = gBS->AllocatePool (
                    EfiReservedMemoryType,
                    (UINTN)FileSize,
                    (VOID**)&StartingAddr
                    ); 
        if(EFI_ERROR (Status)) {
                Print(L"Allocate Memory failed!\n");
                return EFI_SUCCESS;
        } 
		
		CopyMem(StartingAddr,&ImageData[5],FileSize);
        
        //
        // Register the newly created RAM disk.
        //
        Status = MyRamDisk->Register (
             ((UINT64)(UINTN) StartingAddr),
             FileSize,
             &gEfiVirtualDiskGuid,
             NULL,
             &DevicePath
             );
        if (EFI_ERROR (Status)) {
                Print(L"Can't create RAM Disk!\n");
                return EFI_SUCCESS;
        }

上述代码编译运行之后,会在当前系统中生成一个新的盘符,进入这个盘符之后就可以运行 MemTest86 进行内存测试了。

文本完整代码如下:

生成的 EFI 如下:

Step2FPGA(1) 环境的搭建

一直有学习 FPGA的想法,这次下定决心花时间来学习FPGA。因为 FPGA 相关知识能够帮助更好的理解硬件知识,同时可以使用FPGA来实现验证自己的想法。我选择的开发板和教材是 “至芯携手特权同学Altera Cyclone IV EP4CE6 FPGA开发板NIOSII”:

勇敢的心 伴你玩转 Altera FPGA 书籍和开发板

教材是《勇敢的芯伴你玩转 Altera FPGA》《例说FPGA》《FPGA设计 实战演练(逻辑篇)》,作者都是吴厚航先生(特权同学)。

勇敢的心 伴你玩转 Altera FPGA 和其余两本内容上有所重复

这套书籍和套件是我在 2018年购买的,但是一直没有坚持下去。和学习单片机一样,最大的敌人并不是内容的难度,而是自己是否能够不断坚持学习。因此,这次开始新的系列。

教材上使用的软件比较老(Quartus 13),在新的操作系统上有问题必须升级,这次选择 Quartus 18 的版本。安装文件是QuartusLiteSetup-18.1.0.625-windows.exe

Quartus 18 安装界面

 推荐使用默认路径进行安装,一路Next即可

安装占用大约7G 的空间。

安装选项,推荐默认选项即可

点击 “Finish” 按钮后会自动继续安装 USB-Blaster(下载器):

下载器安装,我购买开发板也选购了配套的下载器

接下来可以从 Windows 菜单中启动 Quartus:

安装后从 Start Menu 上启动

启动后会提示目前没有安装任何 Devices ,需要安装Device Package(这个类似于 Keil 安装好之后,需要安装某一个型号的单片机的支持文件;又好比在 Arduino IDE 上使用 ESP32 需要先从 Board Manger上安装 ESP32支持包):

提示需要安装 Device

关闭上面的软件再回到 Start Menu 选择“Device Installer”

Start menu 启动 “Device Installer”

在这个页面选择存放着 Devices 支持文件的路径(文件后缀是 .qdz)

接下来需要选择要安装的功能如下:

  1. Cyclone IV (其中有开发板的FPGA型号)
  2.  ModelSim (模拟仿真工具,Starter Edition 是免费的,下面的是需要额外购买的收费版本)
这个两个功能会占用硬盘 4.5G 的空间。

再次启动 Quartus,选择 File->Open  Project 打开开发板例子 cy4ex2 项目 cy4.qpf 文件。项目中的各种设置都已经准备好了,使用 Processing -> Start Compilation 即可直接编译:

开始编译
编译成功

接下来使用 “Programmer” 功能将编译后的结果下载到开发板中:

选择这里的 programmer
第一次使用需要用 Setup进行设置
在弹出的界面下拉菜单选择 USB-Blaster
点击 Start 即可下载,右上角会提示当前进度

特别注意,烧写时需要给开发板上电。成功之后,板子上的蜂鸣器一直会发出Beep声。

至此,已经踏出了第一步开始了 FPGA之旅。

实现 Ch567 USB0 串口

上次我们在 CH567 的 USB1 上实现了 USB CDC 的功能,这一次尝试在 USB0上实现同样的同能。相比之前的程序,需要修改的位置有:

  1. \src\sys\CH56X_irq.c 中使用USB0DevIntDeal() 响应 USB 0 的中断
__attribute__( ( interrupt ( "id="XSTR(INT_ID_SATA) ) ) )void SATA_Handler(void)
{
	USB0DevIntDeal( );
}

2. \src\main\main.c 中打开 USB0 的中断

	Interrupt_init( 1&lt;&lt;INT_ID_USB0 );     /* 系统总中断开启 */

	USB0DeviceInit();			/* USB0Device Init */
	printf("USB0 Device Init!\n");

        while(1)
        {
                printf("Run\n");
                mDelaymS(5000);
                if (UsbConfig!=0)
                {
                        memcpy( UsbEp3INBuf, &amp;Msg[0], sizeof( Msg ));
                        R16_UEP3_T_LEN1 =  sizeof( Msg );
                        R8_UEP3_TX_CTRL1 = (R8_UEP3_TX_CTRL1 &amp; ~ MASK_UEP_T_RES) | UEP_T_RES_ACK;
                        while (R8_USB0_MIS_ST&amp;bUMS_SIE_FREE==0) {}
                }
        };

3. ch56x_usb0dev372.h 中全部 USB1 替换为 USB0

4. ch56x_usb0dev372.c 中全部 USB1 替换为 USB0

CopperCube 配合 FireBeetle 改变球体颜色

这次使用 CopperCube 制作2个球体,然后可以通过 FireBeetle 控制这两个球体的颜色。
1.创建一个新的场景,删除场景中自带的立方体,然后创建一个球体(Sphere)

2.新建的球体是自带贴图的,这个贴图来自前面立方

3.选中球体,在Textures 中选择第一个空贴图,然后在属性的 Materials 中点击更换贴图

4.之后球体上面的贴图就为空了

5.为了便于观察,我们给图赋予一个颜色,选中物体后右键,在弹出菜单中 选择 “Modify Selection”->”Set vertex Colors”。 在弹出的调色板上选择你喜欢的颜色

6.球体变成了红色,选中球体后再使用右键调出菜单进行: clone

7.现在场景中有2个红色球体了,为了便于观察,改成动态光照,在Materials 中选择 Dynamic

8.在场景中创景一个光源

9.让光源动起来,具体方法在上次的文章中介绍过

10.之后保存场景为FBTest.ccb文件

11.编写一个响应键盘的JavaScripe 文档,当收到不同的按键时,改变球体的颜色。文件命名为 FBTest.js 放到和上面 FBTest.ccb 同一个目录下

// register key events
ccbRegisterKeyDownEvent("keyPressedDown");

function keyPressedDown(keyCode)
{
	//z
	if (keyCode == 90)
	{
		var sN = ccbGetSceneNodeFromName("sphereMesh1");
		print(ccbGetSceneNodeMeshBufferCount(sN) );
		for (var x=0; x<ccbGetMeshBufferVertexCount(sN,0); ++x) {
		ccbSetMeshBufferVertexColor(sN, 0, x, 0x00ff0000);
		} 
	}

	//x
	if (keyCode == 88)
	{
		var sN = ccbGetSceneNodeFromName("sphereMesh1");
		print(ccbGetSceneNodeMeshBufferCount(sN) );
		for (var x=0; x<ccbGetMeshBufferVertexCount(sN,0); ++x) {
		ccbSetMeshBufferVertexColor(sN, 0, x, 0x0000FF00);
		} 

	}


	//c
	if (keyCode == 67)
	{
		var sN = ccbGetSceneNodeFromName("sphereMesh2");
		print(ccbGetSceneNodeMeshBufferCount(sN) );
		for (var x=0; x<ccbGetMeshBufferVertexCount(sN,0); ++x) {
		ccbSetMeshBufferVertexColor(sN, 0, x, 0x0000FF00);
		} 
	}


	//v
	if (keyCode == 86)
	{
		var sN = ccbGetSceneNodeFromName("sphereMesh2");
		print(ccbGetSceneNodeMeshBufferCount(sN) );
		for (var x=0; x<ccbGetMeshBufferVertexCount(sN,0); ++x) {
		ccbSetMeshBufferVertexColor(sN, 0, x, 0x000000FF);
		} 

	}
	
	print(keyCode );
}
  1. 编写FireBeetle 代码,我们需要使用 FireBeetle 的蓝牙功能,将其模拟为一个蓝牙键盘,当有不同按键按下后,发送按键信息。这样当CopperCube 生成的 EXE 收到后,会改变颜色
  2. 编译之后就能看到最终的结果了。
/**
   This example turns the ESP32 into a Bluetooth LE keyboard that writes the words, presses Enter, presses a media key and then Ctrl+Alt+Delete
*/
#include <BleKeyboard.h>
#define PINA 12
#define PINB  4
#define PINC 16
#define PIND 17

BleKeyboard bleKeyboard;

void setup() {
  Serial.begin(115200);
  Serial.println("Starting BLE work!");
  pinMode(PINA, INPUT_PULLUP);
  pinMode(PINB, INPUT_PULLUP);
  pinMode(PINC, INPUT_PULLUP);
  pinMode(PIND, INPUT_PULLUP);
  bleKeyboard.begin();
}

void loop() {
  if (bleKeyboard.isConnected()) {
    if (digitalRead(PINA) == LOW) {
      Serial.println("Sending 'z'");
      bleKeyboard.print("z");
      delay(200);
    }
    if (digitalRead(PINB) == LOW) {
      Serial.println("Sending 'x'");
      bleKeyboard.print("x");
      delay(200);
    }

    if (digitalRead(PINC) == LOW) {
      Serial.println("Sending 'c'");
      bleKeyboard.print("c");
      delay(200);
    }
    if (digitalRead(PIND) == LOW) {
      Serial.println("Sending 'v'");
      bleKeyboard.print("v");
      delay(200);
    }
  }
}