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 下载:

双USB串口数据交换器

串口是最常见的接口,因为它足够简单,几乎在所有的调试场合都能看到它的身影。从编程的角度来说,这种接口的代码已经非常成熟,从 C 到 Python 都能够支持这种接口。实际工作生产中最常见的就是USB转串口。美中不足的是,这USB转串口在编程的时候存在着如下缺陷:

  1. 某些USB转串口设备需要额外安装驱动才能使用;
  2. 当同一台电脑存在多个COM Port时,查找特定的设备会比较麻烦;
  3. 打开串口后很难得知串口另外一端的设备是否已经准备好;
  4. 传输速度有限制,最常见的波特率只有115200,意味着一秒只能传输11KB左右的数据。

针对上述问题,这次使用南京沁恒微电子公司出品的 CH32V208 制作一个双USB串口数据交换器(Dual USB SERIAL DATA EXCHANGER, 简称DUSDE)。CH32V208是一款基于32位RISC-V设计的无线型微控制器,配备了硬件堆栈区、快速中断入口,在标准RISC-V基础上大大提高了中断响应速度。搭载V4C内核,加入内存保护单元,同时降低硬件除法周期。除了片上集成2Mbps低功耗蓝牙BLE 通讯模块、10M以太网MAC+PHY模块、CAN控制器等接口之外还带有2个USB2.0全速设备+主机/设备接口。这次的双USB串口数据交换器就是将自身模拟为2个 USB 串口设备分别连接到两台电脑上,从而实现数据传输的功能。

CH32V208 架构

从上面的系统结构图可以看到,CH32V208WBU6支持两个USB2.0 Full Speed设备,其中一个可以作为 HOST或者Device,另外一个只能作为 Device 使用。我们通过编程的方式,让他们都工作在Device 模式下,下图就是我们设备的框图。

双USB串口数据交换器架构

针对前面的需要额外驱动的问题,通过编程在DUSDC上实现USB CDC协议,这样在Windows 8 及其以上的系统无需额外安装驱动。Windows 识别所属的 Class 之后,会自动加载内置驱动。

针对多个串口和需要检测对面设备是否准备好的问题,我们预定义了一些命令,当用户使用2022波特率打开串口后,能够响应如下命令。

方向命令返回值用途
PC->Device?ACVUSB1 或者 USB2设备测试。返回当前的USB接口名称
PC->Device?VER例如:0003查询当前固件版本。返回当前固件版本信息,例如:0003 这种版本号
PC->Device?SER例如:1234查询设备序列号。返回当前设备的序列号,对于同一个设备,从USB1和USB2读取的序列号相同,例如:1234
PC->Device?CFGNRDY或者REDY查询另外一个USB端口是否已经Active。当一个USB Port 收到“?ACV” 命令后就处于 Active 状态了。之后另外一个USB Port 使用这个命令会反馈前一个USB Port的状态

例如,系统中存在 COM1 到 COM4 多个串口,程序枚举每一个串口,打开串口后设定波特率为2022,之后从该串口发送“?ACV”命令,如果能够得到“USB1”或者“USB2”这样的回复,会表明当前为1号或者2号USB端口;

再比如,我们用设备连接两台电脑,USB Port1有程序打开过端口,并且用 “?ACV”查询过当前设备,那么USB Port 2这端程序以2022波特率打开端口后,再发送“?CFG”就能得到“REDY”的回复,表示对面的端口(USB1)已经准备好,反之如果收到“NRDY”则表示另外端口没有收到过“?ACV”命令;

最后,在程序看来数据使用串口传输,但是因为所有的传输都是在USB中进行,USB端口之间的数据也是在内存中进行的交换,相比传统串口速度会有很大的提升。实际测试表明在在使用超级终端程序的zmodem 协议传输文件时,速度可达300KB/S以上。同时因为没有串口的发送和采样过程,传输过程发生错误的概率极低。

上面就是为什么要设计DUSDC,以及它的优点。下面介绍DUSDC的具体实现。

首先是硬件部分。整体设计非常简单,并没有太多的元件:

核心部分就是基于CH32V208的最小系统,其中的S4 是Download 按钮,需要下载Firmware时,先按住按钮然后插入USB接口,之后再抬起按钮,使用 WCHISPTool即可下载。

USB 使用的是Type-B 母头,选择原因是这种结构非常稳固,最大限度保证连接的可靠。需要注意的是J1是预留的取电接口,在连接两台PC时,为了避免5V电压不同可以将J1断开,这样设备就只能通过USB2进行取电。

通过一颗TLV1117 来实现5V到3.3V的转换,5V来自USB 端口。预留了UART1作为调试接口,基本上所有的问题都能通过输出 Log 来解决。

此外,板子上预留了一个SPI1 出来的SPI NOR 接口,如果有记录数据的需求,可以考虑在该位置焊接SPI NOR芯片或者PSRAM。

因为板子上没有多余的元件,所以布线也比较简单:

3D渲染结果:

成品PCB:

硬件确定之后就可以着手设计软件了。CH32V208的示例程序有两个USB 转UART的例子,一个是USBDB (全速设备控制器)的例子,另外一个是USBFS(全速主机/设备控制器,这里用作全速设备控制器)。因为功能上有差别,这两个控制器名称也有差别,编程比较麻烦。类似CH567的话,一个是USB1另外一个是USB2感觉就会好很多。第一个工作是将两个代码融合在一起通过编译。

例如,当前是USBDB,工作基本流程是:

  1. 报告HOST 当前是 USB CDC 设备;
  2. 在 USBDB OUT的Endpoint收取来自HOST的数据,收下之后该OUT Endpoint设置为 NAK 回复状态,这样HOST不会继续对该OUT Endpoint 发送数据;
  3. 在主循环中轮询,如果有收到数据,那么查询USBFS IN Endpoint 是否 Busy,如果Busy继续等待,否则通过USBFS的IN Endpoint 将数据发送出去。

注意:USB 中描述的 OUT 和IN 是以HOST的CPU 为准的,对于运行着 Windows的x86 来说,OUT 是指从CPU到单片机方向(Write),反之数据是从单片机到CPU(Read)。

上述过程中, 代码位置简述:

  1. 在 usb_desc.c 有添加iSerialNumber 定义,这样当使用同样的设备时,每次插入会维持相同的串口号。例如,第一次使用Windows 分配为为 COM10,如果iSerialNumber为空,那么再次插入可能会被分配为COM11,但是iSerialNumber 不为空,再次插入仍然会是 COM10;
  2. usb_endp.c 处理USBDB 收到的主机OUT的数据,接收到的数据会放在 USB1_Tx_Buf[] 中。这部分代码可以看作是两部分:一部分是处理特别 COMMAND(以2022波特率打开串口之后进入特别 COMMAND 模式);另外一部分是处理转发数据的代码,就是在USB1_Tx_Counter=USB1BufLen 记录收到的数据长度
/*********************************************************************
 * @fn      EP2_OUT_Callback
 *
 * @brief  Endpoint 2 OUT.
 *
 * @return  none
 */
void EP2_OUT_Callback(void) {
 
    //ZivDebug_Start
    uint32_t     Status;
    Status = _GetEPRxStatus(EP2_OUT);
    uint16_t USB1BufLen = GetEPRxCount( EP2_OUT &amp; 0x7F);
    PMAToUserBufferCopy(&amp;USB1_Tx_Buf[USB1_Tx_Counter], GetEPRxAddr( EP2_OUT &amp; 0x7F),
            USB1BufLen);
 
    if (USB1Replay!=0xFF) {
        if ((USB1_Tx_Buf[0] == '?') &amp;&amp; (USB1_Tx_Buf[1] == 'A')
                &amp;&amp; (USB1_Tx_Buf[2] == 'C') &amp;&amp; (USB1_Tx_Buf[3] == 'V')) {
            printf("RCV ACV CMD\r\n");
            USB1Replay = 0x01;
        }
        if ((USB1_Tx_Buf[0] == '?') &amp;&amp; (USB1_Tx_Buf[1] == 'V')
                &amp;&amp; (USB1_Tx_Buf[2] == 'E') &amp;&amp; (USB1_Tx_Buf[3] == 'R')) {
            printf("RCV VER CMD\r\n");
            USB1Replay = 0x02;
        }
        if ((USB1_Tx_Buf[0] == '?') &amp;&amp; (USB1_Tx_Buf[1] == 'S')
                &amp;&amp; (USB1_Tx_Buf[2] == 'E') &amp;&amp; (USB1_Tx_Buf[3] == 'R')) {
            printf("RCV SER CMD\r\n");
            USB1Replay = 0x03;
        }
        if ((USB1_Tx_Buf[0] == '?') &amp;&amp; (USB1_Tx_Buf[1] == 'C')
                &amp;&amp; (USB1_Tx_Buf[2] == 'F') &amp;&amp; (USB1_Tx_Buf[3] == 'G')) {
            printf("RCV CFG CMD\r\n");
            USB1Replay = 0x04;
        }
    } else {
        //printf("EPS1[%d]\r\n",GetEPRxStatus(ENDP2));
        USB1_Tx_Counter=USB1BufLen;
        printf("EP2O[%d]\r\n",USB1_Tx_Counter);
    }
 
}

主机的轮询发送,在main.c中,主要动作是将数据通过 USBFS_Endp_DataUp() 函数转发给USBFS 的 Port 上。

// If USB2 is connected, we will send data to USB2
if (USBFS_DevEnumStatus == 1) {
    if ((USBFS_Endp_Busy[DEF_UEP3]==0)&amp;&amp;(USB1_Tx_Counter!=0)&amp;&amp;(USB1Replay==0xFF)) {
        printf("A[%d]\r\n",USB1_Tx_Counter);
        // Send data to USB2
        USBFS_Endp_DataUp( ENDP3, &amp;USB1_Tx_Buf[0], USB1_Tx_Counter, DEF_UEP_CPY_LOAD);
        // If data length is 64, we should send a null package
        if (USB1_Tx_Counter==DEF_USBD_UEP0_SIZE) {
            // Wait until USB2 is free
            while (USBFS_Endp_Busy[DEF_UEP3]!=0) {
            }
            // Send a NULL package
            USBFS_Endp_DataUp( ENDP3, &amp;USB1_Tx_Buf[0], 0, DEF_UEP_CPY_LOAD);
        }
        USB1_Tx_Counter=0;
        // Enable USB1 Endpoint2
        SetEPRxValid( ENDP2);
    }
} else {
    //If USB2 is NOT connected, data from USB1 would be dropped
    USB1_Tx_Counter=0;
    SetEPRxValid( ENDP2);
}

此外,特别命令模式处理代码如下,通过USBD_ENDPx_DataUp() 将数据返回到USBDB对的USB Port上。

if ((USB1Replay>0)&amp;&amp;(USB1Replay&lt;5)) {
    // Reply USB1 Command
    if (USB1Replay==1) {
        RPYMSG[0]='U';
        RPYMSG[1]='S';
        RPYMSG[2]='B';
        RPYMSG[3]='1';
    }
    if (USB1Replay==2) {
        RPYMSG[0]='0';
        RPYMSG[1]='0';
        RPYMSG[2]='0';
        RPYMSG[3]='3';
    }
    if (USB1Replay==3) {
        RPYMSG[0]='1';
        RPYMSG[1]='2';
        RPYMSG[2]='3';
        RPYMSG[3]='4';
    }
    if (USB1Replay==4) {
        if (USB2Replay!=0xFF) {
            RPYMSG[0]='R';
            RPYMSG[1]='E';
            RPYMSG[2]='D';
            RPYMSG[3]='Y';
        } else {
            RPYMSG[0]='N';
            RPYMSG[1]='R';
            RPYMSG[2]='D';
            RPYMSG[3]='Y';
        }
 
    }
    USBD_ENDPx_DataUp( ENDP3, &amp;RPYMSG, sizeof(RPYMSG)); // Send to USB1
    USB1Replay=0;
    SetEPRxValid( ENDP2);
}

上面就是USBDB 的USB Port 处理流程,USBFS的USB Port处理逻辑相同,但是代码表达上差异很大。下面是实物:

介绍的视频

本文提到的电路图和PCB设计下载:

DualUSBCDC下载

本文提到的完整代码下载:

DualHeaderCDC050下载

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

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

简单的说简单的说问题是这样的,我需要在 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 如下:

做一个低成本的USB LED 变色灯

这次制作的目标是:一个插在USB接口就能不停变化颜色的灯。

为了尽可能的压低成本,使用印刷在PCB上的USB。为了低成本实现不停变化颜色,选择淘宝上“F3圆头5mm草帽七彩慢闪led灯珠雾状装饰3v5v12伏F5七彩闪烁渐变色”这款彩灯。这款LED内置了IC,所以能够实现颜色的不停切换变化。

接下来开始电路设计,非常简单就是将LED引脚接在USB的 VCC 和GND即可。

对应的USB1 封装如下:

最终设计的PCB 如下:

为了尽可能降低成本,特别使用V-Cut拼版,这样我们可以在一块PCB上容纳尽量多的PCB。同时需要注意的是:对于嘉立创来说,如果每一个PCB长度或者宽度小于15mm,那么会加收100元的费用,因此,如果有可能尽量不要过窄或者过短。

PCB制作之后是这样的:

需要注意的是:理论上如果直接制作厚度为2.0mm的PCB 那么直接可以插入USB接口的。但是制作PCB时需要增加费用,因此选择制作1.6mm常规厚度的PCB ,拿到手后在USB线路位置自行粘贴剪裁后的信用卡、SIM卡之类的塑料卡片增加厚度。

我使用502胶水粘贴固定

正面:

工作的视频可以在下面看到:

总结省钱妙招:

  1. PCB印刷USB 公头;
  2. 拼版,并且保证长和宽都大于15mm;
  3. 制作 1.6mm的PCB, 拿到手后自行增厚;

最终花费:47.43 元制作12*6=60个PCB, LED 费用为 18元100个。因此,平均下来每个0.97元。

完整的电路图和PCB 下载(立创EDA)

usbled下载

参考:

1. https://detail.tmall.com/item.htm?_u=dkf8s92d31&id=624355880993&spm=a1z09.2.0.0.26642e8dewUEbx

FireBeetle 制作一个LED矩阵

这次介绍的项目是通过 FireBeetle ESP32 实现一个 8*16的单色LED矩阵,可以在上面实现一些简单的图形和动画效果。

在开始之前,首先介绍LED 的静态驱动和动态驱动的概念。当我们在一个发光二极管两端加上一个电压的时候,发光二极管即可工作。理论上,如果驱动N个共阴极的LED那么需要N个提供正电压。这种带来一个问题,如果需要驱动大量的LED,那么就同样需要同样数量的引脚作为正极。对于单片机来说,会遇到IO引脚不够的问题。

发光二极管/LED符号

这种直接驱动的方式称作“静态显示驱动”。与之相对,还可以通过构成矩阵的方式来进行驱动。以3×3的LED矩阵为例,通过6个IO 引脚可以驱动9个LED。

3×3 LED 矩阵

可以看到这种电路,我们可以一次性点亮一行或者一列上的LED,但是如果要点亮的位于不同的行列就会出现问题。例如:我们希望在矩阵上点亮LED0和LED6, 那么需要Y0、Y2为高,X0 为低,这种情况比较简单;但是如果需要同时点亮LED0和 LED4 问题就变得麻烦。因为 LED0 要求Y0 为高X0为低才能点亮,LED4 要求Y1为高X1为低,但是Y1为高X0为低时LED3也会同时亮起。因此,这里需要引入一个分时点亮的方法,比如,先设置Y0 为高X0为低点亮LED0,再设置Y0为低熄灭LED0,设置Y1为高X1为低点亮LED4,只要点亮速度足够快眼睛无法分辨出他们不是同时点亮的。这就是所谓的“动态扫描”。

如果使用 Arduino 编写代码,通常需要使用一个定时中断:

Void timerInterrupt()
{
熄灭上一次的行,点亮第x行
x=x+1
}

可以看出,这样的方法会使得程序复杂度上升,同样的,对于N 个灯需要根号N 个 IO。经过研究发现一个好玩的 IC: WCH 的 CH423。它是IC I/O 扩展芯片,功能如下【参考1】:

CH423 功能
  • 通过两线串行接口远程扩展出8 个通用输入输出引脚GPIO 和16 个通用输出引脚GPO。
  • 内置电流驱动级,连续驱动电流不小于15mA,OC 引脚输出1/16 脉冲灌电流不小于120mA。
  • 静态显示驱动方式支持24 只发光管LED 或者3 位共阳数码管。
  • 分时动态扫描显示驱动方式支持128 只发光管LED 或者16 位共阴数码管,支持亮度控制。
  • 双向I/O 引脚在输入方式下具有输入电平变化时产生中断的功能,中断输出低电平有效。
  • 16 个通用输出引脚可以选择推挽输出或者开漏输出。
  • 支持3V~5V 电源电压,支持低功耗睡眠,可以被输入电平变化唤醒。
  • 高速2 线串行接口,时钟速度从0 到1MHz,兼容两线I2C 总线,节约引脚。
  • 提供SDIP28 和SOP28 两种无铅封装,兼容RoHS。

这次的试验就使用这个芯片来实现一个 8×16的LED点阵。

首先进行电路设计:

控制电路

芯片是I2C 接口,控制线路非常简单:SCl和SDA就好了。CH423是SOP28封装,为了便于试验我从淘宝购买了一个SOP28转DIP的小PCB,焊接之后将CH423插入到PCB上。接下来是LED矩阵的设计:

LED 矩阵 8×16

其中 SE[N] 信号能够输出高低电平,DEG[M] 只用作吸收电流使用。对CH423 发送命令,告知我现在要做动态扫描使用,然后告知SE[N]和DEG[M] 的组合即可。例如:告知SE1 输出高,DIG0 吸收电流和SE7输出高,DIG15吸收电流,之后芯片本身会动态控制,肉眼看起来就是 L00和L7F 点亮【参考2】。

因为都是低速信号,没有太多限制,摆放好 LED后直接使用立创自动布线走的通即可。

PCB设计

焊接后的样子:

焊接后的实物

编写一个测试代码如下:

#include <Wire.h>
 
// CH423接口定义
#define     CH423_I2C_ADDR1     0x20         // CH423的地址
#define     CH423_I2C_MASK      0x3E         // CH423的高字节命令掩码
 
#define CH423_SYSON1    0x0417    //开启自动扫描显示
 
unsigned char CH423_buf[16];    //定义16个数码管的数据映象缓存区
const unsigned char BCD_decode_tab[ 0x10 ] = { 0X3F, 0X06, 0X5B, 0X4F, 0X66, 0X6D, 0X7D, 0X07, 0X7F, 0X6F, 0X77, 0X7C, 0X58, 0X5E, 0X79, 0X71 };
 
 
void CH423_Write( uint32_t cmd )    // 写命令
{
  Serial.print("Address ");
  Serial.print(( unsigned char )(cmd >> 8), HEX);
  Serial.print("  command  ");
  Serial.print(( unsigned char ) (cmd & 0xff), HEX);
  Wire.beginTransmission (( unsigned char )(cmd >> 8));
  Wire.write( ( unsigned char ) (cmd & 0xff) );  // 发送数据
  // 结束总线
  if (Wire.endTransmission() == 0) {
    Serial.println(" I2C Success!");
  } else {
    Serial.println("I2C error!");
  }
}
 
// 向CH423输出数据或者操作命令,自动建立数据映象
void CH423_buf_write( uint32_t cmd )
{
  if ( cmd & 0x1000 )
  { // 加载数据的命令,需要备份数据到映象缓冲区
    CH423_buf[ (unsigned char)( cmd >> 8 ) & 0x0F ] = (unsigned char)( cmd & 0xFF );    // 备份数据到相应的映象单元
  }
  CH423_Write( cmd );    // 发出
}
 
void setup() {
  Serial.begin (115200);
  Wire.begin (21, 22);   // sda= GPIO_21 /scl= GPIO_22.
  /* INTENS [00-11]
     OD_EN 使能开漏
     X_INT 0x08
     DEC_H 0x04
     DEC_L 0x02
     IO_OE 0x01
  */
  CH423_buf_write( 0x2417 );
  /* OC_L_DAT  OC7-OC0 电平控制
  */
  CH423_buf_write( 0x2200 );
  /* OC_H_DAT  OC15-OC8 电平控制
  */
  CH423_buf_write( 0x2300 );
 
  // 初始化时保持全灭
  uint32_t i;
  for (i = 0; i < 16; i++) {
    CH423_buf_write(((0x30 + i) << 8) + 0x00);
  }
}
 
// 要显示的字符取模, DFRobot 字样
// 来自 https://www.zhetao.com/fontarray.html
const unsigned char bitmap_bit_bytes[] = {
  0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
  0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
  0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
  0b11111000, 0b11111100, 0b11111100, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
  0b01000100, 0b01000010, 0b01000010, 0b00000000, 0b11000000, 0b00000000, 0b00000000,
  0b01000010, 0b01001000, 0b01000010, 0b00000000, 0b01000000, 0b00000000, 0b00010000,
  0b01000010, 0b01001000, 0b01000010, 0b00000000, 0b01000000, 0b00000000, 0b00010000,
  0b01000010, 0b01111000, 0b01111100, 0b00111100, 0b01011000, 0b00111100, 0b01111100,
  0b01000010, 0b01001000, 0b01001000, 0b01000010, 0b01100100, 0b01000010, 0b00010000,
  0b01000010, 0b01001000, 0b01001000, 0b01000010, 0b01000010, 0b01000010, 0b00010000,
  0b01000010, 0b01000000, 0b01000100, 0b01000010, 0b01000010, 0b01000010, 0b00010000,
  0b01000010, 0b01000000, 0b01000100, 0b01000010, 0b01000010, 0b01000010, 0b00010000,
  0b01000100, 0b01000000, 0b01000010, 0b01000010, 0b01100100, 0b01000010, 0b00010010,
  0b11111000, 0b11100000, 0b11100011, 0b00111100, 0b01011000, 0b00111100, 0b00001100,
  0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
  0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
};
 
// 显示一个动画效果
uint16_t buf[8] = {
  0b0100000000000000,
  0b0010000000000000,
  0b0001000000000000,
  0b0000100000000000,
  0b0000010000000000,
  0b0000100000000000,
  0b0001000000000000,
  0b0010000000000000,
};
 
void loop() {
  uint16_t i, j, m;
  char c, v;
  while (Serial.available()) {
    c = Serial.read();
    // 显示卡面定义的字符
    if (c == '1') {
      for (i = 0; i < 7 ; i++) { //一共有7个字符
        for (j = 0; j < 16; j++) { // 每个字符有16个1Byte数据
          CH423_buf_write( ((0x30 + j) << 8) + bitmap_bit_bytes[i + j * 7] );
        }
        delay(500);
      }
    }
    // 随机点亮测试
    if (c == '2') {
      for (i = 0; i < 16; i++) {
        for (j = 0; j < 16; j++) {
          CH423_buf_write( ((0x30 + j) << 8) + random(0, 256) );
        }
        delay(500);
      }
    }
    // 移动的动画效果
    if (c == '3') {
      for (m = 0; m < 32; m++) {
        // 显示 buf 定义的图形
        for (i = 0; i < 16; i++)
        {
          v = 0;
          for (j = 0; j < 8; j++) {
            if ((buf[j] & (1 << i)) == 0) {
              v = v << 1;
            }
            else {
              v = (v << 1) + 1;
            }
          }
          CH423_buf_write( ((0x30 + i) << 8) + v );
        }
        // 移动 buf 字符
        for (i = 0; i < 8; i++) {
            if ((buf[i]&1)!=0) {buf[i]=buf[i]|0x8000;}
            buf[i]=buf[i]>>1;
          }
        delay(100);  
 
         
      }
    }
  }
 
}

根据串口输入,进行不同的测试:

  1. 输入1 会逐个显示 DFRobot 字样
  2. 输入2会随机点亮
  3. 输入3会显示一个方向的简单动画效果。

参考:

  1. http://www.wch.cn/products/CH423.html
  2. 这部分在DataSheet有描述 “8.2. 动态显示驱动 CH423 的动态显示驱动方式用于驱动 128 只 LED 或者 16 只共阴数码管,由 IO7~IO0 引脚分别驱动共阴数码管的各个段引脚(各数码管并联),由 OC15~OC0 引脚分别驱动各个共阴数码管的公共端。单片机在加载完所有字数据后,开启 DEC_L 和 DEC_H 控制位由 CH423 自动地进行分时动态显示扫描。如果只需要驱动 8 只数码管,那么可以只开启 DEC_L 或者 DEC_H 其中的一个控制位,剩余的另CH423 中文手册”

工作的测试视频

MemTest86 Free 最新版

最近在测试 Memory 的时候偶然发现老版本的 MemTest86 在运行时会发生死机,新版本可以正常使用。于是,动手制作了一个精简版:

MemTest2023下载

解压之后得到 MemTest2023.IMG 文件,可以直接放在Ventoy 制作出来的启动盘上,开机启动之后从菜单选择这个文件即可进入 Memtest86。

参考:

1.https://www.memtest86.com/ 官方网站

C# 读取物理硬盘的例子

本质上和VC 写的没有区别,都是使用 CreateFile 打开 PhysicalDrive 然后进行操作。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using System.Runtime.InteropServices;
 
namespace RW_PyhsicalDisk
{
    class Program
    {
        [DllImport("kernel32", SetLastError = true)]
        static extern IntPtr CreateFile(
        string FileName,
        uint DesiredAccess,
        uint ShareMode,
        IntPtr SecurityAttributes,
        uint CreationDisposition,
        int FlagsAndAttributes,
        IntPtr hTemplate
        );
 
        static class DESIREDACCESS
        {
            public const uint GENERIC_READ = 0x80000000;
            public const uint GENERIC_WRITE = 0x40000000;
            public const uint GENERIC_EXECUTE = 0x20000000;
            public const uint GENERIC_ALL = 0x10000000;
        }
        /// <summary>
        /// Sharing mode of the file or object
        ///</summary>
        static class SHAREMODE
        {
            public const uint FILE_SHARE_READ = 0x00000001;
            public const uint FILE_SHARE_WRITE = 0x00000002;
            public const uint FILE_SHARE_DELETE = 0x00000004;
        }
        /// <summary>
        /// Action to take on files that exist, and which action to take when files do not exist.
        /// </summary>
        static class CREATIONDISPOSITION
        {
            public const uint CREATE_NEW = 1;
            public const uint CREATE_ALWAYS = 2;
            public const uint OPEN_EXISTING = 3;
            public const uint OPEN_ALWAYS = 4;
            public const uint TRUNCATE_EXISTING = 5;
        }
        /// <summary>
        /// File attributes and flags for the file.
        /// </summary>
        static class FLAGSANDATTRIBUTES
        {
            public const uint FILE_FLAG_WRITE_THROUGH = 0x80000000;
            public const uint FILE_FLAG_OVERLAPPED = 0x40000000;
            public const uint FILE_FLAG_NO_BUFFERING = 0x20000000;
            public const uint FILE_FLAG_RANDOM_ACCESS = 0x10000000;
            public const uint FILE_FLAG_SEQUENTIAL_SCAN = 0x08000000;
            public const uint FILE_FLAG_DELETE_ON_CLOSE = 0x04000000;
            public const uint FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;
            public const uint FILE_FLAG_POSIX_SEMANTICS = 0x01000000;
            public const uint FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000;
            public const uint FILE_FLAG_OPEN_NO_RECALL = 0x00100000;
            public const uint FILE_FLAG_FIRST_PIPE_INSTANCE = 0x00080000;
        }
 
        static void Main(string[] args)
        {
            IntPtr handle = CreateFile(@"\\.\PHYSICALDRIVE1",
                            DESIREDACCESS.GENERIC_READ | DESIREDACCESS.GENERIC_WRITE,
                            SHAREMODE.FILE_SHARE_READ | SHAREMODE.FILE_SHARE_WRITE,
                            IntPtr.Zero,
                            CREATIONDISPOSITION.OPEN_EXISTING,
                           0,
                IntPtr.Zero);
            FileStream disk = new FileStream(handle, FileAccess.ReadWrite);
            byte[] bt = new byte[512];
            disk.Seek(0, SeekOrigin.Begin);
            disk.Read(bt, 0, 512);
            for (int i = 0; i < bt.Length; i++)
            {
                Console.Write(bt[i].ToString("x2") + " ");
            }
 
            Console.ReadLine();
        }
    }
}

运行结果: