英文版 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

Step to UEFI (252)ReportStatusCodeRouter 和 StatusCodeHandlerPei

继续我们的研究。从Log中可以看到,加载了ReportStatusCodeRouterPei.efi:

Loading PEIM A3610442-E69F-4DF3-82CA-2360C4031A23
Loading PEIM at 0x000008470A0 EntryPoint=0x000008475A0 ReportStatusCodeRouterPei.efi

对应代码位于\MdeModulePkg\Universal\ReportStatusCodeRouter\Pei目录下:

/**
  Entry point of Status Code PEIM.

  This function is the entry point of this Status Code Router PEIM.
  It produces Report Stataus Code Handler PPI and Status Code PPI.

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

  @retval EFI_SUCESS  The entry point of DXE IPL PEIM executes successfully.

**/
EFI_STATUS
EFIAPI
GenericStatusCodePeiEntry (
  IN       EFI_PEI_FILE_HANDLE  FileHandle,
  IN CONST EFI_PEI_SERVICES     **PeiServices
  )

接下来Log 如下:

Install PPI: 0065D394-9951-4144-82A3-0AFC8579C251
Install PPI: 229832D3-7A30-4B36-B827-F40CB7D45436

这两个 GUID 在 \MdePkg\MdePkg.dec 有定义:

  ## Include/Ppi/ReportStatusCodeHandler.h
  gEfiPeiRscHandlerPpiGuid           = { 0x65d394, 0x9951, 0x4144, {0x82, 0xa3, 0xa, 0xfc, 0x85, 0x79, 0xc2, 0x51 }}
  ## Include/Ppi/StatusCode.h
  gEfiPeiStatusCodePpiGuid = { 0x229832d3, 0x7a30, 0x4b36, {0xb8, 0x27, 0xf4, 0xc, 0xb7, 0xd4, 0x54, 0x36 } }

这个里面比较有意思的是这里面注册了2个Ppi,其中一个是 ReportStatusCode的Ppi,另外一个是用来给ReportStatusCode 注册实际工作的函数,接下来的模块会调用这个Ppi能够让我们理解实际动作。

继续查看 Log 有如下:

Loading PEIM 9D225237-FA01-464C-A949-BAABC02D31D0
Loading PEIM at 0x0000084BFA0 EntryPoint=0x0000084C4A0 StatusCodeHandlerPei.efi

StatusCodeHandlerPei对应的代码位于 \MdeModulePkg\Universal\StatusCodeHandler\Pei目录

/**
  Entry point of Status Code PEIM.

  This function is the entry point of this Status Code PEIM.
  It initializes supported status code devices according to PCD settings,
  and installs Status Code PPI.

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

  @retval EFI_SUCESS  The entry point of DXE IPL PEIM executes successfully.

**/
EFI_STATUS
EFIAPI
StatusCodeHandlerPeiEntry (
  IN       EFI_PEI_FILE_HANDLE  FileHandle,
  IN CONST EFI_PEI_SERVICES     **PeiServices
  )
{
  EFI_STATUS                  Status;
  EFI_PEI_RSC_HANDLER_PPI     *RscHandlerPpi;

  Status = PeiServicesLocatePpi (
             &gEfiPeiRscHandlerPpiGuid,
             0,
             NULL,
             (VOID **) &RscHandlerPpi
             );
  ASSERT_EFI_ERROR (Status);

  //
  // Dispatch initialization request to sub-statuscode-devices.
  // If enable UseSerial, then initialize serial port.
  // if enable UseMemory, then initialize memory status code worker.
  //
  if (PcdGetBool (PcdStatusCodeUseSerial)) {
    Status = SerialPortInitialize();
    ASSERT_EFI_ERROR (Status);
    Status = RscHandlerPpi->Register (SerialStatusCodeReportWorker);
    ASSERT_EFI_ERROR (Status);
  }
  if (PcdGetBool (PcdStatusCodeUseMemory)) {
    Status = MemoryStatusCodeInitializeWorker ();
    ASSERT_EFI_ERROR (Status);
    Status = RscHandlerPpi->Register (MemoryStatusCodeReportWorker);
    ASSERT_EFI_ERROR (Status);
  }

  return EFI_SUCCESS;
}

就是说通过 RscHandlerPpi 注册了2个Callback函数SerialStatusCodeReportWorker 和MemoryStatusCodeReportWorker。当有人使用 ReportStatusCode时,会先后进入 SerialStatusCodeReportWorker() 和MemoryStatusCodeReportWorker()。

再进一步检查,在\OvmfPkg\OvmfPkgX64.dsc 有如下定义:

  gEfiMdeModulePkgTokenSpaceGuid.PcdStatusCodeUseSerial|FALSE 
  gEfiMdeModulePkgTokenSpaceGuid.PcdStatusCodeUseMemory|TRUE

从 PCD可以看到在 OVMF 中,没有使用 SerialStatusCodeReportWorker。当代码使用了ReportStatusCode时,会执行MemoryStatusCodeReportWorker()。

这是一种非常巧妙的实现,有兴趣的朋友可以研究 \MdeModulePkg\Universal\ReportStatusCodeRouter\Pei\ReportStatusCodeRouterPei.c 这个文件。

日常生活中的数学:保险公司推销的理财产品

数学,在日常生活中出了买菜之外,还能发挥很大的作用。笔者的一个朋友在朋友圈发布了一条保险的广告:

简单的说:连续三年每年存3万,然后第八年就能够取出来 111,659元。根据他们的说法,第八年的收益是 21,659元,因此利息是:(21659/90000)=24.1%,这算得每年利息 24.1%/7=3.45% (我也不知道为什么他们要除以7 )。从直觉上看我感觉这个非常可疑,按照复利计算得年利率应该不高。于是动笔进行计算。

假设年利率为x,那么一年之后本金加利率为  p=1+x。

第一年末尾一共有: 3*p

第二年末尾一共有: (3*p+3)*p

第三年末尾一共有: ((3*p+3)*p+3)*p

第四年末尾一共有: ((3*p+3)*p+3)*(p^2)

……………………………………….

第八年末尾一共有: ((3*p+3)*p+3)*(p^6)

第N年末尾一共有: ((3*p+3)*p+3)*(p^(N-2))

针对第八年进行研究,展开算式一共有 3*(P^8) +3*(P^7) +3*(P^6), 对照上面表格有方程

3*(P^8) +3*(P^7) +3*(P^6)= 111659

这是一元八次方程,我没有办法直接从数学角度解开。于是编写代码来解。因为我们知道这是一个单调递增函数,所以我们可以给定一个初始值Start,然后给出一个步长 Step,不断尝试计算f(Start+Step)的值,如果它小于目标,那么 Start=Start+Step,否则 Step=Step/2 继续尝试。最终得到一个值: 1.03123863220215

换句话说,以复利计算年利率 3.123%。

之后,我们再使用20年的收益168703进行计算,结果是:1.03360308647156。以复利计算年利率 3.36%。

C#编写的完整代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Diagnostics;
namespace ConsoleApplication31
{
    class Program
    {
        static double f(double value,int year)
        {
            return Math.Pow(value, year) + Math.Pow(value, year-1) + Math.Pow(value, year-2);
        }
        static void Main(string[] args)
        {

            const double ESP = 1e-6;
            const int YEAR = 8;
            double start = 1.00;
            double step = 0.01;
            double target = 111659.00 / 30000;
            for (int i = 0; i < 1000; i++)
            {
                if (Math.Abs(f(start, YEAR) - target) <ESP)
                {
                    Console.WriteLine("Target: {0}", start);
                    break;
                }
                // 增加之后 f(x) 大于 target
                if (f(start + step, YEAR) > target)
                {
                    step = step / 2;
                }
                else {
                    start += step;
                }
                Console.WriteLine("{0}:{1} {2}", i, f(start, YEAR), start);
            }
            Console.ReadKey();
        }
    }
}

从上面可以看出:在查看这种收益表格时,因为计算复杂,消费者很容易被误导。另外,这种产品周期太长,风险很大,是另外的成本。