Step to UEFI (255)PlatformPei之后

当BIOS执行完 PlatformPei.efi 之后,不会和其他PeiM 一样继续执行而是要进行一些特别的处理。

在\MdeModulePkg\Core\Pei\Dispatcher\Dispatcher.c 文件 PeiDispatcher函数中会 invoke 每一个PEIM。

/**
  Conduct PEIM dispatch.

  @param SecCoreData     Points to a data structure containing information about the PEI core's operating
                         environment, such as the size and location of temporary RAM, the stack location and
                         the BFV location.
  @param Private         Pointer to the private data passed in from caller

**/
VOID
PeiDispatcher (
  IN CONST EFI_SEC_PEI_HAND_OFF  *SecCoreData,
  IN PEI_CORE_INSTANCE           *Private
  )

这里会调用每个 PEIM 的 EntryPoint, 有兴趣的朋友可以在此加入 Debug Message.

                  //
                  // Call the PEIM entry point for PEIM driver
                  //
                  PeimEntryPoint = (EFI_PEIM_ENTRY_POINT2)(UINTN)EntryPoint;
                  PeimEntryPoint (PeimFileHandle, (const EFI_PEI_SERVICES **) PeiServices);
                  Private->PeimDispatchOnThisPass = TRUE;

特别提一句,在\mdemodulepkg\core\pei\dispatcher\Dispatcher.c这个文件中,大多数 DEBUG message是通过下面的方式输出的:

DEBUG ((DEBUG_DISPATCH, "Evaluate PEI DEPEX for FFS(Unknown)\n"));

但是如果你也用这种方式,会发现无法在Log 文件中看到消息的。这是因为在\OvmfPkg\OvmfPkgX64.dsc 中定义如下:

  # DEBUG_INIT      0x00000001  // Initialization
  # DEBUG_WARN      0x00000002  // Warnings
  # DEBUG_LOAD      0x00000004  // Load events
  # DEBUG_FS        0x00000008  // EFI File system
  # DEBUG_POOL      0x00000010  // Alloc & Free (pool)
  # DEBUG_PAGE      0x00000020  // Alloc & Free (page)
  # DEBUG_INFO      0x00000040  // Informational debug messages
  # DEBUG_DISPATCH  0x00000080  // PEI/DXE/SMM Dispatchers
  # DEBUG_VARIABLE  0x00000100  // Variable
  # DEBUG_BM        0x00000400  // Boot Manager
  # DEBUG_BLKIO     0x00001000  // BlkIo Driver
  # DEBUG_NET       0x00004000  // SNP Driver
  # DEBUG_UNDI      0x00010000  // UNDI Driver
  # DEBUG_LOADFILE  0x00020000  // LoadFile
  # DEBUG_EVENT     0x00080000  // Event messages
  # DEBUG_GCD       0x00100000  // Global Coherency Database changes
  # DEBUG_CACHE     0x00200000  // Memory range cachability changes
  # DEBUG_VERBOSE   0x00400000  // Detailed debug messages that may
  #                             // significantly impact boot performance
  # DEBUG_ERROR     0x80000000  // Error
gEfiMdePkgTokenSpaceGuid.PcdDebugPrintErrorLevel|0x8000004F

默认是没有DEBUG_DISPATCH  的,所以要么将 PcdDebugPrintErrorLevel 设置为0x800000CF,要么在 DEBUG 宏中使用DEBUG_INFO参数。

从PlatformPei.efi出来之后,因为内存初始化已完成,进入下面的函数之后 PeiCheckAndSwitchStack (SecCoreData, Private);会检查

Private->SwitchStackSignal是否已经Enable。 具体的是在\mdemodulepkg\core\pei\memory\MemoryServices.c 代码中设置起来的。

/**

  This function registers the found memory configuration with the PEI Foundation.

  The usage model is that the PEIM that discovers the permanent memory shall invoke this service.
  This routine will hold discoveried memory information into PeiCore's private data,
  and set SwitchStackSignal flag. After PEIM who discovery memory is dispatched,
  PeiDispatcher will migrate temporary memory to permanent memory.

  @param PeiServices        An indirect pointer to the EFI_PEI_SERVICES table published by the PEI Foundation.
  @param MemoryBegin        Start of memory address.
  @param MemoryLength       Length of memory.

  @return EFI_SUCCESS Always success.

**/
EFI_STATUS
EFIAPI
PeiInstallPeiMemory (
  IN CONST EFI_PEI_SERVICES  **PeiServices,
  IN EFI_PHYSICAL_ADDRESS    MemoryBegin,
  IN UINT64                  MemoryLength
  )

一句话:当BIOS执行完 PlatformPei.efi 之后内存可用。

Temp Stack : BaseAddress=0x818000 Length=0x8000
Temp Heap  : BaseAddress=0x810000 Length=0x8000
Total temporary memory:    65536 bytes.
  temporary memory stack ever used:       29192 bytes.
  temporary memory heap used for HobList: 6608 bytes.
  temporary memory heap occupied by memory pages: 0 bytes.
Memory Allocation 0x0000000A 0x7F78000 - 0x7FFFFFF
Memory Allocation 0x0000000A 0x810000 - 0x81FFFF
Memory Allocation 0x0000000A 0x807000 - 0x807FFF
Memory Allocation 0x0000000A 0x800000 - 0x805FFF
Memory Allocation 0x0000000A 0x806000 - 0x806FFF
Memory Allocation 0x00000006 0x7EF4000 - 0x7F77FFF
Memory Allocation 0x0000000A 0x820000 - 0x8FFFFF
Memory Allocation 0x00000004 0x900000 - 0x14FFFFF
Old Stack size 32768, New stack size 131072
Stack Hob: BaseAddress=0x3F36000 Length=0x20000
Heap Offset = 0x3746000 Stack Offset = 0x3736000
TemporaryRamMigration(0x810000, 0x3F4E000, 0x10000)

在运行PeiCheckAndSwitchStack() 函数的过程中输出上述Log。之后,再次跳入 PeiCore 进行执行,此时内存已经 Ready。

      //
      // Entry PEI Phase 2
      //
      PeiCore (SecCoreData, NULL, Private);

我们能够看到的Log 如下:

Loading PEIM 52C05B14-0B98-496C-BC3B-04B50211D680
Loading PEIM at 0x00007EE7000 EntryPoint=0x00007EE7538 PeiCore.efi
Reinstall PPI: 8C8CE578-8A3D-4F1C-9935-896185C32DD3
Reinstall PPI: 5473C07A-3DCB-4DCA-BD6F-1E9689E7349A
Reinstall PPI: B9E0ABFE-5979-4914-977F-6DEE78C278A6
Install PPI: F894643D-C449-42D1-8EA8-85BDD8C65BDE
Loading PEIM 9B3ADA4F-AE56-4C24-8DEA-F03B7558AE50
Loading PEIM at 0x00007EE1000 EntryPoint=0x00007EE14C8 PcdPeim.efi
Reinstall PPI: 06E81C58-4AD7-44BC-8390-F10265F72480
Reinstall PPI: 4D8B155B-C059-4C8F-8926-06FD4331DB8A
Reinstall PPI: 01F34D25-4DE2-23AD-3FF3-36353FF323F1
Reinstall PPI: A60C6B59-E459-425D-9C69-0BCC9CB27D81

就是说再次进入 Peicore 之后重新执行了 PcdPeim.efi。

Step to UEFI (254)PlatformPei 分析

继续跟着Log 分析,加载了PlatformPei.efi,它代码位于 \OvmfPkg\PlatformPei 目录中。

Loading PEIM 222C386D-5ABC-4FB4-B124-FBB82488ACF4
Loading PEIM at 0x00000851920 EntryPoint=0x00000851F70 PlatformPei.efi

从Log 中可以看到,刚进入这个 Module 之后,调用到位于\OvmfPkg\Library\QemuFwCfgLib\QemuFwCfgPei.c 的QemuFwCfgInitialize() 函数中。QemuFwCfgPeiLib.inf 中指定了这个函数作为这个 PEIM 的构造函数。

  CONSTRUCTOR                    = QemuFwCfgInitialize

接下来的 Log 是:

Select Item: 0x0
FW CFG Signature: 0x554D4551
Select Item: 0x1
FW CFG Revision: 0x3
QemuFwCfg interface (DMA) is supported.

对应 \OvmfPkg\PlatformPei\Platform.c 中的如下函数:

/**
  Perform Platform PEI initialization.

  @param  FileHandle      Handle of the file being invoked.
  @param  PeiServices     Describes the list of possible PEI Services.

  @return EFI_SUCCESS     The PEIM initialized successfully.

**/
EFI_STATUS
EFIAPI
InitializePlatform (
  IN       EFI_PEI_FILE_HANDLE  FileHandle,
  IN CONST EFI_PEI_SERVICES     **PeiServices
  )
Platform PEIM Loaded
CMOS:
00: 44 00 54 00 06 00 02 13 12 21 26 02 10 80 00 00
10: 50 00 00 00 07 80 02 FF FF 00 00 00 00 00 00 00
20: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
30: FF FF 20 00 00 07 00 20 30 00 00 00 00 12 00 00
40: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
50: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
60: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
70: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

这里会调用到\OvmfPkg\Library\QemuFwCfgS3Lib\QemuFwCfgS3PeiDxe.c中的QemuFwCfgS3Enabled()

/**
  Determine if S3 support is explicitly enabled.

  @retval  TRUE   If S3 support is explicitly enabled. Other functions in this
                  library may be called (subject to their individual
                  restrictions).

           FALSE  Otherwise. This includes unavailability of the firmware
                  configuration interface. No other function in this library
                  must be called.
**/
BOOLEAN
EFIAPI
QemuFwCfgS3Enabled (
  VOID
  ) 

QEMU 模拟了 SPI ROM,使得BIOS能够访问到一些设置。

Select Item: 0x19
Select Item: 0x27
S3 support was detected on QEMU
  if (QemuFwCfgS3Enabled ()) {
    DEBUG ((DEBUG_INFO, "S3 support was detected on QEMU\n"));
    mS3Supported = TRUE;
    Status = PcdSetBoolS (PcdAcpiS3Enable, TRUE);
    ASSERT_EFI_ERROR (Status);
  }
Install PPI: 7408D748-FC8C-4EE6-9288-C4BEC092A410

BootModeInitialization() 中的Status = PeiServicesInstallPpi (mPpiBootMode); 输出

Select Item: 0x19
Select Item: 0x25
ScanOrAdd64BitE820Ram: Base=0x0 Length=0x8000000 Type=1
Select Item: 0x19
Select Item: 0x19
GetFirstNonAddress: Pci64Base=0x800000000 Pci64Size=0x800000000
Select Item: 0x5

对应代码在 \ovmfpkg\platformpei\MemDetect.c

/**
  Iterate over the RAM entries in QEMU's fw_cfg E820 RAM map that start outside
  of the 32-bit address range.

  Find the highest exclusive >=4GB RAM address, or produce memory resource
  descriptor HOBs for RAM entries that start at or above 4GB.

  @param[out] MaxAddress  If MaxAddress is NULL, then ScanOrAdd64BitE820Ram()
                          produces memory resource descriptor HOBs for RAM
                          entries that start at or above 4GB.

                          Otherwise, MaxAddress holds the highest exclusive
                          >=4GB RAM address on output. If QEMU's fw_cfg E820
                          RAM map contains no RAM entry that starts outside of
                          the 32-bit address range, then MaxAddress is exactly
                          4GB on output.

  @retval EFI_SUCCESS         The fw_cfg E820 RAM map was found and processed.

  @retval EFI_PROTOCOL_ERROR  The RAM map was found, but its size wasn't a
                              whole multiple of sizeof(EFI_E820_ENTRY64). No
                              RAM entry was processed.

  @return                     Error codes from QemuFwCfgFindFile(). No RAM
                              entry was processed.
**/
STATIC
EFI_STATUS
ScanOrAdd64BitE820Ram (
  OUT UINT64 *MaxAddress OPTIONAL
  )
MaxCpuCountInitialization: CmdData2=0x0
MaxCpuCountInitialization: QEMU v2.7 reset bug: BootCpuCount=1 Present=0
MaxCpuCountInitialization: BootCpuCount=0 mMaxCpuCount=1

PublishPeiMemory: mPhysMemAddressWidth=36 PeiMemoryCap=65800 KB
PeiInstallPeiMemory MemoryBegin 0x3F36000, MemoryLength 0x4042000

QEMU 的内存初始化在  \ovmfpkg\platformpei\MemDetect.c

/**
  Publish PEI core memory

  @return EFI_SUCCESS     The PEIM initialized successfully.

**/
EFI_STATUS
PublishPeiMemory (
  VOID
  )
  PublishPeiMemory ();

QemuUc32BaseInitialization: rounded UC32 base from 0x8000000 up to 0x80000000, for an UC32 size of 0x80000000

VOID
QemuUc32BaseInitialization (
  VOID
  )
QemuInitializeRam called

具体实现在 \ovmfpkg\platformpei\MemDetect.c 文件中

/**
  Publish system RAM and reserve memory regions

**/
VOID
InitializeRamRegions (
  VOID
  )
QemuInitializeRam ();
/**
  Peform Memory Detection for QEMU / KVM

**/
STATIC
VOID
QemuInitializeRam (
  VOID
  )
Platform PEI Firmware Volume Initialization
Install PPI: 49EDB1C1-BF21-4761-BB12-EB0031AABB39
Notify: PPI Guid: 49EDB1C1-BF21-4761-BB12-EB0031AABB39, Peim notify entry point: 82153C
The 1th FV start address is 0x00000900000, size is 0x00C00000, handle is 0x900000
Select Item: 0x19
Register PPI Notify: EE16160A-E8BE-47A6-820A-C6900DB0250A
Select Item: 0x19
\MdePkg\MdePkg.dec
  ## Include/Ppi/MpServices.h
  gEfiPeiMpServicesPpiGuid           = { 0xee16160a, 0xe8be, 0x47a6, { 0x82, 0xa, 0xc6, 0x90, 0xd, 0xb0, 0x25, 0xa } }

PlatformPei.efi 中有大量和“硬件”相关的内容,最重要的部分是内存的初始化,运行完成后,内存可用。

参考:

1. https://www.lab-z.com/stu238/ OVMF 从第一条指令到 SecMain

英文版 Windows Python 窗口显示汉字

这次介绍一下如何在英文版 Windows的 CMD 窗口中显示中文的方法。例如,有如下代码,保存在 test.py 中:

print ("Hello,world!")
print (u"学习")

直接运行显示如下:

这时候我们使用命令置活动代码页编号:

再进行测试即可显示:

同样的,我们可以直接在修改代码自动切换:

import os
os.system('chcp 936')
print ("Hello,world!")
print (u"学习")

Step to UEFI (253)PEIM 调用顺序研究

当EFI 刚出现的时候,模块没有固定的加载顺序是作为优点介绍的,但是运行过程确实需要加载顺序,这一切是通过源代码中每个模块 INF 中[Depex] Section 决定的。前面的文章中提到了PEIM 加载了一些模块,根据 Log 输出的信息对其运行顺序进行排序结果如下:

顺序文件名INF 文件位置Depex
1PcdPeim.efi\MdeModulePkg\Universal\PCD\Pei\Pcd.infTRUE
2ReportStatusCodeRouterPei.efi\MdeModulePkg\Universal\ReportStatusCodeRouter\Pei\ReportStatusCodeRouterPei.infTRUE
3StatusCodeHandlePei.efi\MdeModulePkg\Universal\StatusCodeHandler\Pei\StatusCodeHandlerPei.infTRUE
4PlatformPei.efi\OvmfPkg\PlatformPei\PlatformPei.infTRUE
 (PeiCore.efi) N/A*
5PcdPeim.efi\MdeModulePkg\Universal\PCD\Pei\Pcd.infTRUE
6DxeIpl.efi\MdeModulePkg\Core\DxeIplPeim\DxeIpl.infgEfiPeiLoadFilePpiGuid AND gEfiPeiMasterBootModePpiGuid
7S3Resume2Pei.efi\UefiCpuPkg\Universal\Acpi\S3Resume2Pei\S3Resume2Pei.infTRUE
8CpuMpPei.efi\UefiCpuPkg\CpuMpPei\CpuMpPei.infTRUE
9DxeCore.efi这里已经进入 DXE 了N/A*
*PeiCore和DxeCore 不是 PEIM 所以没有 Depex。

(有兴趣的朋友可以按照上面给出的 INF 文件中的 ENTRY_POINT 处添加 DEBUG)

可以看到,上述的 PEIM 中大部分 DEPEX 都是 TRUE 就是不依赖任何条件。对于这种,猜测是根据压缩打包的前后顺序来决定,前面使用过 UEFITool NE 检查过生成的文件,可以看到大致顺序:

接下来来研究如何改变这个顺序。在 \OvmfPkg\OvmfPkgX64.fdf 文件中有如下代码:

#
#  PEI Phase modules
#
INF  MdeModulePkg/Core/Pei/PeiMain.inf
INF  MdeModulePkg/Universal/PCD/Pei/Pcd.inf
INF  MdeModulePkg/Universal/ReportStatusCodeRouter/Pei/ReportStatusCodeRouterPei.inf
INF  MdeModulePkg/Universal/StatusCodeHandler/Pei/StatusCodeHandlerPei.inf
INF  OvmfPkg/PlatformPei/PlatformPei.inf
INF  MdeModulePkg/Core/DxeIplPeim/DxeIpl.inf
INF  UefiCpuPkg/Universal/Acpi/S3Resume2Pei/S3Resume2Pei.inf
!if $(SMM_REQUIRE) == TRUE
INF  MdeModulePkg/Universal/FaultTolerantWritePei/FaultTolerantWritePei.inf
INF  MdeModulePkg/Universal/Variable/Pei/VariablePei.inf
INF  OvmfPkg/SmmAccess/SmmAccessPei.inf
!endif
INF  UefiCpuPkg/CpuMpPei/CpuMpPei.inf

我们进行一下修改,将 CpuMpPei.inf 放在最前面。

#
#  PEI Phase modules
#
INF  UefiCpuPkg/CpuMpPei/CpuMpPei.inf
INF  MdeModulePkg/Core/Pei/PeiMain.inf
NF  MdeModulePkg/Universal/PCD/Pei/Pcd.inf
INF  MdeModulePkg/Universal/ReportStatusCodeRouter/Pei/ReportStatusCodeRouterPei.inf
INF  MdeModulePkg/Universal/StatusCodeHandler/Pei/StatusCodeHandlerPei.inf
INF  OvmfPkg/PlatformPei/PlatformPei.inf
INF  MdeModulePkg/Core/DxeIplPeim/DxeIpl.inf
INF  UefiCpuPkg/Universal/Acpi/S3Resume2Pei/S3Resume2Pei.inf
!if $(SMM_REQUIRE) == TRUE
INF  MdeModulePkg/Universal/FaultTolerantWritePei/FaultTolerantWritePei.inf
INF  MdeModulePkg/Universal/Variable/Pei/VariablePei.inf
INF  OvmfPkg/SmmAccess/SmmAccessPei.inf
!endif

这样修改之后再次使用工具查看,可以看到 CpuMpPei确实排在第一位:

但是从 Log 上来看CpuMpPei加载位置提前了,但是加载的第一个 PEIM 仍然是 PcdPeim:

The 0th FV start address is 0x00000820000, size is 0x000E0000, handle is 0x820000
Register PPI Notify: 49EDB1C1-BF21-4761-BB12-EB0031AABB39
Register PPI Notify: EA7CA24B-DED5-4DAD-A389-BF827E8F9B38
Install PPI: B9E0ABFE-5979-4914-977F-6DEE78C278A6
Install PPI: DBE23AA9-A345-4B97-85B6-B226F1617389
DiscoverPeimsAndOrderWithApriori(): Found 0x7 PEI FFS files in the 0th FV
Loading PEIM 9B3ADA4F-AE56-4C24-8DEA-F03B7558AE50
Loading PEIM at 0x0000083A7A0 EntryPoint=0x0000083AC68 PcdPeim.efi
Install PPI: 06E81C58-4AD7-44BC-8390-F10265F72480
Install PPI: 01F34D25-4DE2-23AD-3FF3-36353FF323F1
Install PPI: 4D8B155B-C059-4C8F-8926-06FD4331DB8A
Install PPI: A60C6B59-E459-425D-9C69-0BCC9CB27D81
Register PPI Notify: 605EA650-C65C-42E1-BA80-91A52AB618C6
Loading PEIM EDADEB9D-DDBA-48BD-9D22-C1C169C8C5C6
Loading PEIM at 0x00000820120 EntryPoint=0x00000821364 CpuMpPei.efi
Register PPI Notify: F894643D-C449-42D1-8EA8-85BDD8C65BDE
Loading PEIM A3610442-E69F-4DF3-82CA-2360C4031A23
Loading PEIM at 0x00000840220 EntryPoint=0x000008406E8 ReportStatusCodeRouterPei.efi
Install PPI: 0065D394-9951-4144-82A3-0AFC8579C251
Install PPI: 229832D3-7A30-4B36-B827-F40CB7D45436
Loading PEIM 9D225237-FA01-464C-A949-BAABC02D31D0
Loading PEIM at 0x00000842D20 EntryPoint=0x000008431A8 StatusCodeHandlerPei.efi

进一步研究,在【参考1】有提到:

“在PEIM Dispatcher寻找可启动的PEIM时,会先在每一个FV上定位Apriori文件,然后读取文件内容来查找PEIM的GUID,确保Apriori文件中的PEIM被首先调用。在OvmfPkgX64.fdf中我们可以看到FV.PEIFV中有以下内容:

APRIORI PEI {

INF  MdeModulePkg/Universal/PCD/Pei/Pcd.inf

}“

就是说 PeiDispatcher() 函数会先分析 Apriori 文件,然后首先执行其中指定的PEIM,对应的 OvmfPkgX64.fdf 中指定了 PcdPeim.efi,所以这个文件会首先被执行到。

      if (Private->CurrentPeimCount == 0) {
        //
        // When going through each FV, at first, search Apriori file to
        // reorder all PEIMs to ensure the PEIMs in Apriori file to get
        // dispatch at first.
        //
        DiscoverPeimsAndOrderWithApriori (Private, CoreFvHandle);
      }

接下来编写一个最简单的 PEI Driver, 其中只有输出信息的动作:

  DEBUG ((DEBUG_INFO, "LABZ Test PEIM\n"));

修改如下2个文件加入OVMF 的编译:

1.OvmfPkgX64.dsc

!if $(SMM_REQUIRE) == TRUE
      LockBoxLib|MdeModulePkg/Library/SmmLockBoxLib/SmmLockBoxPeiLib.inf
!endif
  }
  
  OvmfPkg/TestPei/TestPei.inf
  
!if $(SMM_REQUIRE) == TRUE
  MdeModulePkg/Universal/FaultTolerantWritePei/FaultTolerantWritePei.inf
  MdeModulePkg/Universal/Variable/Pei/VariablePei.inf
  OvmfPkg/SmmAccess/SmmAccessPei.inf
!endif

2.OvmfPkgX64.fdf

INF  MdeModulePkg/Core/DxeIplPeim/DxeIpl.inf
INF  UefiCpuPkg/Universal/Acpi/S3Resume2Pei/S3Resume2Pei.inf
!if $(SMM_REQUIRE) == TRUE
INF  MdeModulePkg/Universal/FaultTolerantWritePei/FaultTolerantWritePei.inf
INF  MdeModulePkg/Universal/Variable/Pei/VariablePei.inf
INF  OvmfPkg/SmmAccess/SmmAccessPei.inf
!endif
INF  OvmfPkg/TestPei/TestPei.inf

!if $(TPM_ENABLE) == TRUE
INF  OvmfPkg/Tcg/TpmMmioSevDecryptPei/TpmMmioSevDecryptPei.inf
INF  OvmfPkg/Tcg/Tcg2Config/Tcg2ConfigPei.inf
INF  SecurityPkg/Tcg/TcgPei/TcgPei.inf
INF  SecurityPkg/Tcg/Tcg2Pei/Tcg2Pei.inf
!endif

运行结果如下:

Loading PEIM 89E549B0-7CFE-449D-9BA3-10D8B2312D71
Loading PEIM at 0x00007ECD000 EntryPoint=0x00007ECD5C8 S3Resume2Pei.efi
Install PPI: 6D582DBC-DB85-4514-8FCC-5ADF6227B147
Loading PEIM 122C386D-5ABC-4FB4-B124-FBB82488ACF4
Loading PEIM at 0x00007ECA000 EntryPoint=0x00007ECA470 TestPei.efi
DiscoverPeimsAndOrderWithApriori(): Found 0x0 PEI FFS files in the 1th FV
DXE IPL Entry
Loading PEIM D6A2CB7F-6A18-4E2F-B43B-9920A733700A
Loading PEIM at 0x00007EA3000 EntryPoint=0x00007EA3D78 DxeCore.efi
Loading DXE CORE at 0x00007EA3000 EntryPoint=0x00007EA3D78

之后,尝试将这个PEIM放在APRIORI 中最前面:

APRIORI PEI {
  INF  OvmfPkg/TestPei/TestPei.inf
  INF  MdeModulePkg/Universal/PCD/Pei/Pcd.inf
}

运行结果如下:

The 0th FV start address is 0x00000820000, size is 0x000E0000, handle is 0x820000
Register PPI Notify: 49EDB1C1-BF21-4761-BB12-EB0031AABB39
Register PPI Notify: EA7CA24B-DED5-4DAD-A389-BF827E8F9B38
Install PPI: B9E0ABFE-5979-4914-977F-6DEE78C278A6
Install PPI: DBE23AA9-A345-4B97-85B6-B226F1617389
DiscoverPeimsAndOrderWithApriori(): Found 0x8 PEI FFS files in the 0th FV
Loading PEIM 122C386D-5ABC-4FB4-B124-FBB82488ACF4
Loading PEIM at 0x0000085A820 EntryPoint=0x0000085AC90 TestPei.efi
Loading PEIM 9B3ADA4F-AE56-4C24-8DEA-F03B7558AE50
Loading PEIM at 0x0000083A7A0 EntryPoint=0x0000083AC68 PcdPeim.efi
Install PPI: 06E81C58-4AD7-44BC-8390-F10265F72480
Install PPI: 01F34D25-4DE2-23AD-3FF3-36353FF323F1
Install PPI: 4D8B155B-C059-4C8F-8926-06FD4331DB8A
Install PPI: A60C6B59-E459-425D-9C69-0BCC9CB27D81
Register PPI Notify: 605EA650-C65C-42E1-BA80-91A52AB618C6

可以看到 TestPei 先于PcdPeim运行。

总结:

  1. fdf 文件中 APRIORI PEI  指定的优先级最高;
  2. 模块中 INF 文件 Depex Section 可以指定依赖关系;
  3. 同等优先级的由fdf中的先后顺序决定。

特别强调上述只是为了实验学习之用,在实际工作中请勿轻易调整 PEIM 顺序避免导致奇怪的问题。另外,对于PEIM 调用顺序还可以参考 PI Spec 中 “PEI Dispatcher Introduction”章节描述。

本文使用的自己编写的 PEIM 如下:

参考:

1. https://blog.csdn.net/weixin_39945816/article/details/111506524 uefi下的开机顺序_UEFI启动过程与协议加载顺序

ESP32 S2 上实现假U盘(ESP32 2.0.1 )

这次介绍的是在 Arduino 1.8.16 + ESP32 2.0.1 下实现假U盘的例子。特别强调:ESP32 Arduino 环境必须是 2.0.1, 更高的版本反倒有兼容问题。

实现的方法和之前的相同【参考1】:

  1. 首先在本机做一个虚拟的256MB硬盘,然后在上面放置一个 200MB 的内容为全0x00的文件(太大的话,处理起来非常麻烦);
  2. 将这个硬盘使用HXD 做成镜像文件,然后使用工具扫描这个文件,将所有的不为0的内容写入.h文件中(这就是256MBDisk.bin.h文件的来源);
  3. 这个文件中0x804 到 0x8CC扇区内容,和0x902 到 0x9CA扇区内容相同,因此在数组中删除后者内容,当有到这个扇区的请求时,用前者的内容替代,这样可以大大缩减编译后文件的体积;
  4. 在代码中响应 onRead(),如果请求的LBA在.h文件中,就进行Buffer 的填充;
  5. 插入过程 Windows 有写入操作,因此必须响应 onWrite

完整代码如下:

#include "USB.h"
#include "USBMSC.h"
#include "256MBDisk.h"

#if ARDUINO_USB_CDC_ON_BOOT
#define HWSerial Serial0
#define USBSerial Serial
#else
#define HWSerial Serial
//USBCDC USBSerial;
#endif

USBMSC MSC;

uint32_t findLBA(uint32_t LBA) {
  for (uint32_t i=0; i<415;i++) {
      if (Index[i]==LBA) {
          if (i>=205) {i=i-205+4;}
          return i;
        }
    }
  return 0xFFFFFF;  
}
static const uint32_t DISK_SECTOR_COUNT = 520192; 

static int32_t onRead(uint32_t lba, uint32_t offset, void* buffer, uint32_t bufsize){
  //HWSerial.printf("MSC READ: lba: %u, offset: %u, bufsize: %u\n", lba, offset, bufsize);

  uint32_t getLBA=0;
  uint8_t *p;
  p=(uint8_t *)buffer;
  //HWSerial.printf("Buffer1 %X\n", buffer);
  for (uint32_t i=0;i<bufsize/DISK_SECTOR_SIZE;i++) {
     //HWSerial.printf("Check %u\n", lba+i);
     getLBA=findLBA(lba+i);
     //HWSerial.printf("getLBA %u %u\n", getLBA,lba+i);
     if (getLBA!=0xFFFFFF) { //如果找到了
        memcpy((void *)&p[i*DISK_SECTOR_SIZE], &msc_disk[getLBA][0], DISK_SECTOR_SIZE);
        //HWSerial.printf("%u %u %u %u\n", msc_disk[getLBA][0],msc_disk[getLBA][1],msc_disk[getLBA][2],msc_disk[getLBA][3]);
        //HWSerial.printf("Send %u\n", getLBA);
        //HWSerial.printf("Buffer2 %X\n", (void *)&p[i*DISK_SECTOR_SIZE]);
      } else {
       //HWSerial.printf("Send all zero to %u\n", getLBA);
        for (uint16_t j=0;j<DISK_SECTOR_SIZE;j++) {
              p[i*DISK_SECTOR_SIZE+j]=0;
          }
          
      }
    }
  return bufsize;
}

static bool onStartStop(uint8_t power_condition, bool start, bool load_eject){
  HWSerial.printf("MSC START/STOP: power: %u, start: %u, eject: %u\n", power_condition, start, load_eject);
  return true;
}

static void usbEventCallback(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data){
  if(event_base == ARDUINO_USB_EVENTS){
    arduino_usb_event_data_t * data = (arduino_usb_event_data_t*)event_data;
    switch (event_id){
      case ARDUINO_USB_STARTED_EVENT:
        HWSerial.println("USB PLUGGED");
        break;
      case ARDUINO_USB_STOPPED_EVENT:
        HWSerial.println("USB UNPLUGGED");
        break;
      case ARDUINO_USB_SUSPEND_EVENT:
        HWSerial.printf("USB SUSPENDED: remote_wakeup_en: %u\n", data->suspend.remote_wakeup_en);
        break;
      case ARDUINO_USB_RESUME_EVENT:
        HWSerial.println("USB RESUMED");
        break;
      
      default:
        break;
    }
  }
}

static int32_t onWrite(uint32_t lba, uint32_t offset, uint8_t* buffer, uint32_t bufsize){
  HWSerial.printf("MSC WRITE: lba: %u, offset: %u, bufsize: %u\n", lba, offset, bufsize);
  return bufsize;
}
void setup() {
  HWSerial.begin(115200);
  HWSerial.setDebugOutput(true);

  USB.onEvent(usbEventCallback);
  MSC.vendorID("ESP32");//max 8 chars
  MSC.productID("USB_MSC");//max 16 chars
  MSC.productRevision("1.02");//max 4 chars
  MSC.onStartStop(onStartStop);
  MSC.onRead(onRead);
  MSC.onWrite(onWrite);
  MSC.mediaPresent(true);
  MSC.begin(DISK_SECTOR_COUNT, DISK_SECTOR_SIZE);
  //USBSerial.begin();
  USB.begin();
}

void loop() {
  // put your main code here, to run repeatedly:
}

速度测试:

本文提到的用于测试的 U盘镜像:

代码1,这个是完整的分区:

代码2,这个是压缩后的分区(分区的FAT 表有部分是相同的,所以可以对这部分进行压缩)

参考:

  1. https://mc.dfrobot.com.cn/thread-309859-1-1.html ESP32 S2 做一个假U盘

Intel GOP 简介

GOP是 Grapshics Output Protocol 的缩写,这是 UEFI 定义的 Pre-OS 显示接口,目标就是提供显示的基本功能。 无论你使用何种显卡,如果想让屏幕在开机过程有所显示,必须遵从这个协议。这次简单介绍一下 Intel 的 GOP。

Intel 的GOP 会放在BIOS 中,可以看作 BIOS 的一部分。Intel Release 的 GOP有三部分:

1.IntelGopDriver: 这是DXE 的GOP Driver

2.IntelGraphicsPeim: 这是 PEI 阶段的 GOP。用户按下开机键后会希望尽快点亮屏幕看到提示信息,因此在 PEI 阶段点亮屏幕是非常必要的。如果希望上电就能显示,还可以去找屏幕厂商进行定制。

3.VBT:GOP 的配置文件,比如:要求某个端口输出eDP 还是HDMI信号。

BIOS 会在 PEI 阶段 调用IntelGraphicsPeim,然后 DXE 阶段调用IntelGopDriver.efi。和其他的 UEFI 下的Driver一样,这两个文件是C语言编写的,如果由需要可以联系Intel FAE 索要 DEBUG 版本的 GOP。

在OS启动过程中,OS Loader会通过 ExitBootervices()通知 Driver 退出,这时 GOP 会在从系统中卸载。但是 Intel GFX Driver会继续使用 VBT 提供的配置信息(之前的文章提到过如何在 Windows 下查找VBT)。

在 S4 和 S5 的阶段,会调用 GOP。但是 S3 和 ModernStandby 不会调用 GOP。特别是后者,如果遇到问题通常都是 Graphics Driver的问题。

目前用于修改 VBT 进行 GOP 配置的软件是: DisCon (Display Conigureation Tool),上一代的工具是 BMP (至少用了十年以上)。个人感觉这两种没有什么差别。使用的方法都是一个 Binary File 配合一个解释文件(BMP 用的是 BSF ,  DisCon 用的是 XML文件)来使用。特别注意,我感觉目前 DisCon 似乎还不稳定,有时候在解析 XML 配置文件的时候会遇到问题。出现这种问题请检查 DisCon 的版本和 VBT/Json 文件是否匹配。

特别提一下 VBT 一个新功能:LFP PnP ID。使用场景是:当你打算使用一个 VBT 支持多个LFP(Local Front Panel,内置屏幕),比如一个型号的笔记本有多个 SKU,使用了几种不同的屏幕。之前的解决方法是在 BIOS 中放置多个 VBT,然后通过 GPIO 之类的作为 BoardID,在POST过程中Load不同的 VBT。显而易见的是这样会比较麻烦,BIOS 改动较大(作为BIOS工程师,最好的设计就是不要BIOS修改)。另外,还有直接在C 代码中,通过结构体来更改 VBT 数值的方法,这种方法会让接手的人一头雾水:放在BIOS 中的 VBT 和最终 OS 下 Dump 出来的结果不同。因此,在新版的 VBT 中增加了使用 LFP PnP ID 来区分不同Panel 的方法。

1.在Select Panel Type 中选择Panel #FF

2.在 Panel #0X 的 PnP ID 中填写你屏幕的 PnP ID

3.在开机过程中 GOP ,会从 Panel #01 开始扫描,如果发现有匹配的 PnP ID ,那就会使用对应的 Panel 参数

4.对于这个功能,如果 GOP 扫描中没有发现,那么会使用 Panel #01 的设定值;如果Select Panel Type 中没有使用Panel #FF,那么这个功能不会开启。

ESP32S2 一个设备多个键盘的实现

最近在看《圈圈教你玩USB(第一版)》,我手上的这本是作者签名版,十多年前买的。

书中提到了一个USB设备同时实现键盘鼠标功能的方案,其中的一种是:在 HID 描述符中分别报告鼠标和键盘,然后通过Report ID 对数据进行区分。于是手工编写一个代码,实现了一个USB设备下有3个键盘的功能。

代码是基于CustomHIDDevice编写的,对于 HID 设备来说,彼此之间主要差别就是 HID 描述符。这里定义了三个键盘的HID描述符:

//报告ID,(这里定义键盘报告的ID为1报告ID 0是保留的)
 0x85, 0x01, //Report ID (1)
….
//报告ID,这里定义键盘报告的ID为2(报告ID 0是保留的)
 0x85, 0x02, //Report ID (2)
….
//报告ID,这里定义键盘报告的ID为3(报告ID 0是保留的)
 0x85, 0x03, //Report ID (3)
之后,主循环中有三个发送数据的部分,其中axis[0] 给出每隔Report 的ID,之后的8Bytes就是键盘的数据。
  //键盘1 输出一个 a
  //其中 axis[0] 是 report ID 这里为 1
  axis[0]=0x01;axis[3]=0x04;
  Device.send(axis);
  delay(20);
  axis[0]=0x01;axis[3]=0x00;
  Device.send(axis);

发送数据部分:

  delay(20);
  //键盘2 输出一个 b
  //其中 axis[0] 是 report ID 这里为 2
  axis[0]=0x02;axis[3]=0x05;
  Device.send(axis);
  delay(20);
  axis[0]=0x02;axis[3]=0x00;
  Device.send(axis);

  delay(20);
  //键盘3 输出一个 c
  //其中 axis[0] 是 report ID 这里为 3
  axis[0]=0x03;axis[3]=0x06;
  Device.send(axis);
  delay(20);
  axis[0]=0x03;axis[3]=0x00;
  Device.send(axis);

此外,代码中还修改了每一次发送的数据从8个改为9个(就是ReportID+8Byte 键盘数据):

  bool send(uint8_t * value){
    return HID.SendReport(0, value, 9);
  }

设备管理器中可以看到:

每隔10秒,电脑会收到输入的 abc 三个字符.

#include "USB.h"
#include "USBHID.h"
USBHID HID;

static const uint8_t report_descriptor[] = { // 8 axis
 //每行开始的第一字节为该条目的前缀,前缀的格式为:
 //D7~D4:bTag。D3~D2:bType;D1~D0:bSize。以下分别对每个条目注释。
 
/************************USB键盘部分报告描述符**********************/
/*******************************************************************/
 //这是一个全局(bType为1)条目,将用途页选择为普通桌面Generic Desktop Page(0x01)
 //后面跟一字节数据(bSize为1),后面的字节数就不注释了,
 //自己根据bSize来判断。
 0x05, 0x01, // USAGE_PAGE (Generic Desktop)
 
 //这是一个局部(bType为2)条目,说明接下来的集合用途用于键盘
 0x09, 0x06, // USAGE (Keyboard)
 
 //这是一个主条目(bType为0)条目,开集合,后面跟的数据0x01表示
 //该集合是一个应用集合。它的性质在前面由用途页和用途定义为
 //普通桌面用的键盘。
 0xa1, 0x01, // COLLECTION (Application)
 
 //报告ID,(这里定义键盘报告的ID为1报告ID 0是保留的)
 0x85, 0x01, //Report ID (1)
 
 //这是一个全局条目,选择用途页为键盘(Keyboard/Keypad(0x07))
 0x05, 0x07, //     USAGE_PAGE (Keyboard/Keypad)

 //这是一个局部条目,说明用途的最小值为0xe0。实际上是键盘左Ctrl键。
 //具体的用途值可在HID用途表中查看。
 0x19, 0xe0, //     USAGE_MINIMUM (Keyboard LeftControl)
 
 //这是一个局部条目,说明用途的最大值为0xe7。实际上是键盘右GUI键。
 0x29, 0xe7, //     USAGE_MAXIMUM (Keyboard Right GUI)
 
 //这是一个全局条目,说明返回的数据的逻辑值(就是我们返回的数据域的值)
 //最小为0。因为我们这里用Bit来表示一个数据域,因此最小为0,最大为1。
 0x15, 0x00, //     LOGICAL_MINIMUM (0)
 
 //这是一个全局条目,说明逻辑值最大为1。
 0x25, 0x01, //     LOGICAL_MAXIMUM (1)
 
 //这是一个全局条目,说明数据域的数量为八个。
 0x95, 0x08, //     REPORT_COUNT (8)
 
 //这是一个全局条目,说明每个数据域的长度为1个bit。
 0x75, 0x01, //     REPORT_SIZE (1)
 
 //这是一个主条目,说明有8个长度为1bit的数据域(数量和长度
 //由前面的两个全局条目所定义)用来做为输入,
 //属性为:Data,Var,Abs。Data表示这些数据可以变动,Var表示
 //这些数据域是独立的,每个域表示一个意思。Abs表示绝对值。
 //这样定义的结果就是,当某个域的值为1时,就表示对应的键按下。
 //bit0就对应着用途最小值0xe0,bit7对应着用途最大值0xe7。
 0x81, 0x02, //     INPUT (Data,Var,Abs)
 
 //这是一个全局条目,说明数据域数量为1个
 0x95, 0x01, //     REPORT_COUNT (1)
 
 //这是一个全局条目,说明每个数据域的长度为8bit。
 0x75, 0x08, //     REPORT_SIZE (8)
 
 //这是一个主条目,输入用,由前面两个全局条目可知,长度为8bit,
 //数量为1个。它的属性为常量(即返回的数据一直是0)。
 //该字节是保留字节(保留给OEM使用)。
 0x81, 0x03, //     INPUT (Cnst,Var,Abs)
 
 //这是一个全局条目。定义位域数量为6个。
 0x95, 0x06, //   REPORT_COUNT (6)
 
 //这是一个全局条目。定义每个位域长度为8bit。
 //其实这里这个条目不要也是可以的,因为在前面已经有一个定义
 //长度为8bit的全局条目了。
 0x75, 0x08, //   REPORT_SIZE (8)
 
 //这是一个全局条目,定义逻辑最小值为0。
 //同上,这里这个全局条目也是可以不要的,因为前面已经有一个
 //定义逻辑最小值为0的全局条目了。
 0x15, 0x00, //   LOGICAL_MINIMUM (0)
 
 //这是一个全局条目,定义逻辑最大值为255。
 0x25, 0xFF, //   LOGICAL_MAXIMUM (255)
 
 //这是一个全局条目,选择用途页为键盘。
 //前面已经选择过用途页为键盘了,所以该条目不要也可以。
 0x05, 0x07, //   USAGE_PAGE (Keyboard/Keypad)
 
 //这是一个局部条目,定义用途最小值为0(0表示没有键按下)
 0x19, 0x00, //   USAGE_MINIMUM (Reserved (no event indicated))
 
 //这是一个局部条目,定义用途最大值为0x65
 0x29, 0x65, //   USAGE_MAXIMUM (Keyboard Application)
 
 //这是一个主条目。它说明这六个8bit的数据域是输入用的,
 //属性为:Data,Ary,Abs。Data说明数据是可以变的,Ary说明
 //这些数据域是一个数组,即每个8bit都可以表示某个键值,
 //如果按下的键太多(例如超过这里定义的长度或者键盘本身无法
 //扫描出按键情况时),则这些数据返回全1(二进制),表示按键无效。
 //Abs表示这些值是绝对值。
 0x81, 0x00, //     INPUT (Data,Ary,Abs)

 //以下为输出报告的描述
 //逻辑最小值前面已经有定义为0了,这里可以省略。 
 //这是一个全局条目,说明逻辑值最大为1。
 0x25, 0x01, //     LOGICAL_MAXIMUM (1)
 
 //这是一个全局条目,说明数据域数量为5个。 
 0x95, 0x05, //   REPORT_COUNT (5)
 
 //这是一个全局条目,说明数据域的长度为1bit。
 0x75, 0x01, //   REPORT_SIZE (1)
 
 //这是一个全局条目,说明使用的用途页为指示灯(LED)
 0x05, 0x08, //   USAGE_PAGE (LEDs)
 
 //这是一个局部条目,说明用途最小值为数字键盘灯。
 0x19, 0x01, //   USAGE_MINIMUM (Num Lock)
 
 //这是一个局部条目,说明用途最大值为Kana灯。
 0x29, 0x05, //   USAGE_MAXIMUM (Kana)
 
 //这是一个主条目。定义输出数据,即前面定义的5个LED。
 0x91, 0x02, //   OUTPUT (Data,Var,Abs)
 
 //这是一个全局条目。定义位域数量为1个。
 0x95, 0x01, //   REPORT_COUNT (1)
 
 //这是一个全局条目。定义位域长度为3bit。
 0x75, 0x03, //   REPORT_SIZE (3)
 
 //这是一个主条目,定义输出常量,前面用了5bit,所以这里需要
 //3个bit来凑成一字节。
 0x91, 0x03, //   OUTPUT (Cnst,Var,Abs)
 
 //下面这个主条目用来关闭前面的集合。bSize为0,所以后面没数据。
 0xc0,        // END_COLLECTION

/************************USB键盘部分报告描述符**********************/
/*******************************************************************/
 //这是一个全局(bType为1)条目,将用途页选择为普通桌面Generic Desktop Page(0x01)
 //后面跟一字节数据(bSize为1),后面的字节数就不注释了,
 //自己根据bSize来判断。
 0x05, 0x01, // USAGE_PAGE (Generic Desktop)
 
 //这是一个局部(bType为2)条目,说明接下来的集合用途用于键盘
 0x09, 0x06, // USAGE (Keyboard)
 
 //这是一个主条目(bType为0)条目,开集合,后面跟的数据0x01表示
 //该集合是一个应用集合。它的性质在前面由用途页和用途定义为
 //普通桌面用的键盘。
 0xa1, 0x01, // COLLECTION (Application)
 
 //报告ID,这里定义键盘报告的ID为2(报告ID 0是保留的)
 0x85, 0x02, //Report ID (2)
 
 //这是一个全局条目,选择用途页为键盘(Keyboard/Keypad(0x07))
 0x05, 0x07, //     USAGE_PAGE (Keyboard/Keypad)

 //这是一个局部条目,说明用途的最小值为0xe0。实际上是键盘左Ctrl键。
 //具体的用途值可在HID用途表中查看。
 0x19, 0xe0, //     USAGE_MINIMUM (Keyboard LeftControl)
 
 //这是一个局部条目,说明用途的最大值为0xe7。实际上是键盘右GUI键。
 0x29, 0xe7, //     USAGE_MAXIMUM (Keyboard Right GUI)
 
 //这是一个全局条目,说明返回的数据的逻辑值(就是我们返回的数据域的值)
 //最小为0。因为我们这里用Bit来表示一个数据域,因此最小为0,最大为1。
 0x15, 0x00, //     LOGICAL_MINIMUM (0)
 
 //这是一个全局条目,说明逻辑值最大为1。
 0x25, 0x01, //     LOGICAL_MAXIMUM (1)
 
 //这是一个全局条目,说明数据域的数量为八个。
 0x95, 0x08, //     REPORT_COUNT (8)
 
 //这是一个全局条目,说明每个数据域的长度为1个bit。
 0x75, 0x01, //     REPORT_SIZE (1)
 
 //这是一个主条目,说明有8个长度为1bit的数据域(数量和长度
 //由前面的两个全局条目所定义)用来做为输入,
 //属性为:Data,Var,Abs。Data表示这些数据可以变动,Var表示
 //这些数据域是独立的,每个域表示一个意思。Abs表示绝对值。
 //这样定义的结果就是,当某个域的值为1时,就表示对应的键按下。
 //bit0就对应着用途最小值0xe0,bit7对应着用途最大值0xe7。
 0x81, 0x02, //     INPUT (Data,Var,Abs)
 
 //这是一个全局条目,说明数据域数量为1个
 0x95, 0x01, //     REPORT_COUNT (1)
 
 //这是一个全局条目,说明每个数据域的长度为8bit。
 0x75, 0x08, //     REPORT_SIZE (8)
 
 //这是一个主条目,输入用,由前面两个全局条目可知,长度为8bit,
 //数量为1个。它的属性为常量(即返回的数据一直是0)。
 //该字节是保留字节(保留给OEM使用)。
 0x81, 0x03, //     INPUT (Cnst,Var,Abs)
 
 //这是一个全局条目。定义位域数量为6个。
 0x95, 0x06, //   REPORT_COUNT (6)
 
 //这是一个全局条目。定义每个位域长度为8bit。
 //其实这里这个条目不要也是可以的,因为在前面已经有一个定义
 //长度为8bit的全局条目了。
 0x75, 0x08, //   REPORT_SIZE (8)
 
 //这是一个全局条目,定义逻辑最小值为0。
 //同上,这里这个全局条目也是可以不要的,因为前面已经有一个
 //定义逻辑最小值为0的全局条目了。
 0x15, 0x00, //   LOGICAL_MINIMUM (0)
 
 //这是一个全局条目,定义逻辑最大值为255。
 0x25, 0xFF, //   LOGICAL_MAXIMUM (255)
 
 //这是一个全局条目,选择用途页为键盘。
 //前面已经选择过用途页为键盘了,所以该条目不要也可以。
 0x05, 0x07, //   USAGE_PAGE (Keyboard/Keypad)
 
 //这是一个局部条目,定义用途最小值为0(0表示没有键按下)
 0x19, 0x00, //   USAGE_MINIMUM (Reserved (no event indicated))
 
 //这是一个局部条目,定义用途最大值为0x65
 0x29, 0x65, //   USAGE_MAXIMUM (Keyboard Application)
 
 //这是一个主条目。它说明这六个8bit的数据域是输入用的,
 //属性为:Data,Ary,Abs。Data说明数据是可以变的,Ary说明
 //这些数据域是一个数组,即每个8bit都可以表示某个键值,
 //如果按下的键太多(例如超过这里定义的长度或者键盘本身无法
 //扫描出按键情况时),则这些数据返回全1(二进制),表示按键无效。
 //Abs表示这些值是绝对值。
 0x81, 0x00, //     INPUT (Data,Ary,Abs)

 //以下为输出报告的描述
 //逻辑最小值前面已经有定义为0了,这里可以省略。 
 //这是一个全局条目,说明逻辑值最大为1。
 0x25, 0x01, //     LOGICAL_MAXIMUM (1)
 
 //这是一个全局条目,说明数据域数量为5个。 
 0x95, 0x05, //   REPORT_COUNT (5)
 
 //这是一个全局条目,说明数据域的长度为1bit。
 0x75, 0x01, //   REPORT_SIZE (1)
 
 //这是一个全局条目,说明使用的用途页为指示灯(LED)
 0x05, 0x08, //   USAGE_PAGE (LEDs)
 
 //这是一个局部条目,说明用途最小值为数字键盘灯。
 0x19, 0x01, //   USAGE_MINIMUM (Num Lock)
 
 //这是一个局部条目,说明用途最大值为Kana灯。
 0x29, 0x05, //   USAGE_MAXIMUM (Kana)
 
 //这是一个主条目。定义输出数据,即前面定义的5个LED。
 0x91, 0x02, //   OUTPUT (Data,Var,Abs)
 
 //这是一个全局条目。定义位域数量为1个。
 0x95, 0x01, //   REPORT_COUNT (1)
 
 //这是一个全局条目。定义位域长度为3bit。
 0x75, 0x03, //   REPORT_SIZE (3)
 
 //这是一个主条目,定义输出常量,前面用了5bit,所以这里需要
 //3个bit来凑成一字节。
 0x91, 0x03, //   OUTPUT (Cnst,Var,Abs)
 
 //下面这个主条目用来关闭前面的集合。bSize为0,所以后面没数据。
 0xc0,        // END_COLLECTION

/************************USB键盘部分报告描述符**********************/
/*******************************************************************/
 //这是一个全局(bType为1)条目,将用途页选择为普通桌面Generic Desktop Page(0x01)
 //后面跟一字节数据(bSize为1),后面的字节数就不注释了,
 //自己根据bSize来判断。
 0x05, 0x01, // USAGE_PAGE (Generic Desktop)
 
 //这是一个局部(bType为2)条目,说明接下来的集合用途用于键盘
 0x09, 0x06, // USAGE (Keyboard)
 
 //这是一个主条目(bType为0)条目,开集合,后面跟的数据0x01表示
 //该集合是一个应用集合。它的性质在前面由用途页和用途定义为
 //普通桌面用的键盘。
 0xa1, 0x01, // COLLECTION (Application)
 
 //报告ID,这里定义键盘报告的ID为3(报告ID 0是保留的)
 0x85, 0x03, //Report ID (3)
 
 //这是一个全局条目,选择用途页为键盘(Keyboard/Keypad(0x07))
 0x05, 0x07, //     USAGE_PAGE (Keyboard/Keypad)

 //这是一个局部条目,说明用途的最小值为0xe0。实际上是键盘左Ctrl键。
 //具体的用途值可在HID用途表中查看。
 0x19, 0xe0, //     USAGE_MINIMUM (Keyboard LeftControl)
 
 //这是一个局部条目,说明用途的最大值为0xe7。实际上是键盘右GUI键。
 0x29, 0xe7, //     USAGE_MAXIMUM (Keyboard Right GUI)
 
 //这是一个全局条目,说明返回的数据的逻辑值(就是我们返回的数据域的值)
 //最小为0。因为我们这里用Bit来表示一个数据域,因此最小为0,最大为1。
 0x15, 0x00, //     LOGICAL_MINIMUM (0)
 
 //这是一个全局条目,说明逻辑值最大为1。
 0x25, 0x01, //     LOGICAL_MAXIMUM (1)
 
 //这是一个全局条目,说明数据域的数量为八个。
 0x95, 0x08, //     REPORT_COUNT (8)
 
 //这是一个全局条目,说明每个数据域的长度为1个bit。
 0x75, 0x01, //     REPORT_SIZE (1)
 
 //这是一个主条目,说明有8个长度为1bit的数据域(数量和长度
 //由前面的两个全局条目所定义)用来做为输入,
 //属性为:Data,Var,Abs。Data表示这些数据可以变动,Var表示
 //这些数据域是独立的,每个域表示一个意思。Abs表示绝对值。
 //这样定义的结果就是,当某个域的值为1时,就表示对应的键按下。
 //bit0就对应着用途最小值0xe0,bit7对应着用途最大值0xe7。
 0x81, 0x02, //     INPUT (Data,Var,Abs)
 
 //这是一个全局条目,说明数据域数量为1个
 0x95, 0x01, //     REPORT_COUNT (1)
 
 //这是一个全局条目,说明每个数据域的长度为8bit。
 0x75, 0x08, //     REPORT_SIZE (8)
 
 //这是一个主条目,输入用,由前面两个全局条目可知,长度为8bit,
 //数量为1个。它的属性为常量(即返回的数据一直是0)。
 //该字节是保留字节(保留给OEM使用)。
 0x81, 0x03, //     INPUT (Cnst,Var,Abs)
 
 //这是一个全局条目。定义位域数量为6个。
 0x95, 0x06, //   REPORT_COUNT (6)
 
 //这是一个全局条目。定义每个位域长度为8bit。
 //其实这里这个条目不要也是可以的,因为在前面已经有一个定义
 //长度为8bit的全局条目了。
 0x75, 0x08, //   REPORT_SIZE (8)
 
 //这是一个全局条目,定义逻辑最小值为0。
 //同上,这里这个全局条目也是可以不要的,因为前面已经有一个
 //定义逻辑最小值为0的全局条目了。
 0x15, 0x00, //   LOGICAL_MINIMUM (0)
 
 //这是一个全局条目,定义逻辑最大值为255。
 0x25, 0xFF, //   LOGICAL_MAXIMUM (255)
 
 //这是一个全局条目,选择用途页为键盘。
 //前面已经选择过用途页为键盘了,所以该条目不要也可以。
 0x05, 0x07, //   USAGE_PAGE (Keyboard/Keypad)
 
 //这是一个局部条目,定义用途最小值为0(0表示没有键按下)
 0x19, 0x00, //   USAGE_MINIMUM (Reserved (no event indicated))
 
 //这是一个局部条目,定义用途最大值为0x65
 0x29, 0x65, //   USAGE_MAXIMUM (Keyboard Application)
 
 //这是一个主条目。它说明这六个8bit的数据域是输入用的,
 //属性为:Data,Ary,Abs。Data说明数据是可以变的,Ary说明
 //这些数据域是一个数组,即每个8bit都可以表示某个键值,
 //如果按下的键太多(例如超过这里定义的长度或者键盘本身无法
 //扫描出按键情况时),则这些数据返回全1(二进制),表示按键无效。
 //Abs表示这些值是绝对值。
 0x81, 0x00, //     INPUT (Data,Ary,Abs)

 //以下为输出报告的描述
 //逻辑最小值前面已经有定义为0了,这里可以省略。 
 //这是一个全局条目,说明逻辑值最大为1。
 0x25, 0x01, //     LOGICAL_MAXIMUM (1)
 
 //这是一个全局条目,说明数据域数量为5个。 
 0x95, 0x05, //   REPORT_COUNT (5)
 
 //这是一个全局条目,说明数据域的长度为1bit。
 0x75, 0x01, //   REPORT_SIZE (1)
 
 //这是一个全局条目,说明使用的用途页为指示灯(LED)
 0x05, 0x08, //   USAGE_PAGE (LEDs)
 
 //这是一个局部条目,说明用途最小值为数字键盘灯。
 0x19, 0x01, //   USAGE_MINIMUM (Num Lock)
 
 //这是一个局部条目,说明用途最大值为Kana灯。
 0x29, 0x05, //   USAGE_MAXIMUM (Kana)
 
 //这是一个主条目。定义输出数据,即前面定义的5个LED。
 0x91, 0x02, //   OUTPUT (Data,Var,Abs)
 
 //这是一个全局条目。定义位域数量为1个。
 0x95, 0x01, //   REPORT_COUNT (1)
 
 //这是一个全局条目。定义位域长度为3bit。
 0x75, 0x03, //   REPORT_SIZE (3)
 
 //这是一个主条目,定义输出常量,前面用了5bit,所以这里需要
 //3个bit来凑成一字节。
 0x91, 0x03, //   OUTPUT (Cnst,Var,Abs)
 
 //下面这个主条目用来关闭前面的集合。bSize为0,所以后面没数据。
 0xc0,        // END_COLLECTION 
};

class CustomHIDDevice: public USBHIDDevice {
public:
  CustomHIDDevice(void){
    static bool initialized = false;
    if(!initialized){
      initialized = true;
      HID.addDevice(this, sizeof(report_descriptor));
    }
  }
  
  void begin(void){
    HID.begin();
  }
    
  uint16_t _onGetDescriptor(uint8_t* buffer){
    memcpy(buffer, report_descriptor, sizeof(report_descriptor));
    return sizeof(report_descriptor);
  }

  bool send(uint8_t * value){
    return HID.SendReport(0, value, 9);
  }
};

CustomHIDDevice Device;

const int buttonPin = 0;
int previousButtonState = HIGH;
uint8_t axis[9];

void setup() {
  Serial.begin(115200);
  Serial.setDebugOutput(true);
  pinMode(buttonPin, INPUT_PULLUP);
  Device.begin();
  USB.begin();
}

void loop() {
  int buttonState = digitalRead(buttonPin);
  if (HID.ready() && buttonState != previousButtonState) {
    previousButtonState = buttonState;
    if (buttonState == LOW) {
      Serial.println("Button Pressed");
      axis[0] = random() & 0xFF;
      Device.send(axis);
    } else {
      Serial.println("Button Released");
    }
    delay(100);
  }
  //每隔10秒
  delay(10000);
  //键盘1 输出一个 a
  //其中 axis[0] 是 report ID 这里为 1
  axis[0]=0x01;axis[3]=0x04;
  Device.send(axis);
  delay(20);
  axis[0]=0x01;axis[3]=0x00;
  Device.send(axis);

  
  delay(20);
  //键盘2 输出一个 b
  //其中 axis[0] 是 report ID 这里为 2
  axis[0]=0x02;axis[3]=0x05;
  Device.send(axis);
  delay(20);
  axis[0]=0x02;axis[3]=0x00;
  Device.send(axis);

  delay(20);
  //键盘3 输出一个 c
  //其中 axis[0] 是 report ID 这里为 3
  axis[0]=0x03;axis[3]=0x06;
  Device.send(axis);
  delay(20);
  axis[0]=0x03;axis[3]=0x00;
  Device.send(axis);
}

对于 PS2 键盘来说是“全键无冲突的”,意思是可以按下任意多的按键;对于标准的USB 键盘来说,最多只能同时按下6个按键。这是因为 PS2 是分开发送按下和抬起消息的;而标准USB键盘,有8Bytes的数据,其中第一个byte 用来指示 alt ctrl 等等,第二个Byte 始终为0,接下来剩下6Byte,需要放置按下键的信息,如果抬起还需要用0来指示。这样只能支持同时按下6个键。上面提到的这个方法可以用来扩展USB键盘,比如,声明3个键盘就可以支持 6*3=18键无冲突。

ESP32 对于 SDIO 的支持

1.最原始的 ESP32 支持 SDIO

SDIO/SPI 从机控制器 ESP32 集成了符合工业标准 SDIO 2.0 规格的 SD 设备接口,并允许主机控制器使用 SDIO 总线协议访问 SoC 设备。ESP32 用作 SDIO 总线上的从机。主机可以直接访问 SDIO 接口的寄存器并通过使用 DMA 引擎访问设备 中的共享内存,从而不需要处理器内核即可使性能最优化。 SDIO/SPI 从机控制器具有以下特性:

• 时钟范围为 0 至 50 MHz,支持 SPI、1-bit SDIO 和 4-bit SDIO 的传输模式

• 采样和驱动的时钟边沿可配置

• 主机可直接访问的专用寄存器

• 可中断主机,启动数据传输

• 支持自动填充 SDIO 总线上的发送数据,同样支持自动丢弃 SDIO 总线上的填充数据

• 字节块大小可达 512 字节

• 主机与从机间有中断向量可以相互中断对方

• 用于数据传输的 DMA 详细信息请参考 《ESP32 技术参考手册》中的 SDIO 从机控制器章节。

2.ESP32 S2 不支持 SDIO, 如果想用 SD 卡只能走 SPI

3.ESP32 S3 支持 SDIO

支持 SDIO 3.0 版本

上述来自各自的技术规格书。

  1. https://www.espressif.com.cn/sites/default/files/documentation/esp32_datasheet_cn.pdf
  2. https://www.espressif.com.cn/sites/default/files/documentation/esp32-s3_datasheet_cn.pdf