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();
        }
    }
}

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

Step to UEFI (251)Register PPI Notify

St这个系列是根据 QEMU 执行输出的 Log 来研究代码的。这里继续跟踪研究代码,这次研究的对象是下面这条串口输出:

Register PPI Notify: DCD0BE23-9586-40F4-B643-06522CED4EDE

这句话来自 \MdeModulePkg\Core\Pei\Security\Security.c 文件中的InitializeSecurityServices() 函数:

/**
  Initialize the security services.

  @param PeiServices     An indirect pointer to the EFI_PEI_SERVICES table published by the PEI Foundation.
  @param OldCoreData     Pointer to the old core data.
                         NULL if being run in non-permanent memory mode.

**/
VOID
InitializeSecurityServices (
  IN EFI_PEI_SERVICES  **PeiServices,
  IN PEI_CORE_INSTANCE *OldCoreData
  )
{
  if (OldCoreData == NULL) {
    PeiServicesNotifyPpi (&mNotifyList);
  }
  return;
}

其中 mNotifyList 注册了一个 Ppi Notify 的 Callback 函数:SecurityPpiNotifyCallback()

EFI_PEI_NOTIFY_DESCRIPTOR mNotifyList = {
   EFI_PEI_PPI_DESCRIPTOR_NOTIFY_DISPATCH | EFI_PEI_PPI_DESCRIPTOR_TERMINATE_LIST,
   &gEfiPeiSecurity2PpiGuid,
   SecurityPpiNotifyCallback
};

PeiServicesNotifyPpi() 的定义在 \MdePkg\Library\PeiServicesLib\PeiServicesLib.c:

/**
  This service enables PEIMs to register a given service to be invoked when another service is
  installed or reinstalled.

  @param  NotifyList            A pointer to the list of notification interfaces
                                that the caller shall install.

  @retval EFI_SUCCESS           The interface was successfully installed.
  @retval EFI_INVALID_PARAMETER The NotifyList pointer is NULL.
  @retval EFI_INVALID_PARAMETER Any of the PEI notify descriptors in the list do
                                 not have the EFI_PEI_PPI_DESCRIPTOR_NOTIFY_TYPES
                                 bit set in the Flags field.
  @retval EFI_OUT_OF_RESOURCES  There is no additional space in the PPI database.

**/
EFI_STATUS
EFIAPI
PeiServicesNotifyPpi (
  IN CONST EFI_PEI_NOTIFY_DESCRIPTOR  *NotifyList
  )

这个函数的功能是:注册一个 Callback函数,当给定的 Service 安装(install)或者重安装(re-install)时触发这个 callback函数。

经过检查,代码中没有人安装gEfiPeiSecurity2PpiGuid,所以 CallBack 不会发生。接下来找一个有触发 CallBack 函数的作为例子看一下这个如何动作的。

串口输出” Register PPI Notify: 49EDB1C1-BF21-4761-BB12-EB0031AABB397” 和上面的函数类似,这个 GUID定义在  \MdePkg\MdePkg.dec 中:

## Include/Ppi/FirmwareVolumeInfo.h
  gEfiPeiFirmwareVolumeInfoPpiGuid = { 0x49edb1c1, 0xbf21, 0x4761, { 0xbb, 0x12, 0xeb, 0x0, 0x31, 0xaa, 0xbb, 0x39 } }

1. 首先注册Notify,在\MdeModulePkg\Core\Pei\FwVol\FwVol.c 中:

EFI_PEI_NOTIFY_DESCRIPTOR mNotifyOnFvInfoList[] = {
  {
    EFI_PEI_PPI_DESCRIPTOR_NOTIFY_CALLBACK,
    &gEfiPeiFirmwareVolumeInfoPpiGuid,
    FirmwareVolumeInfoPpiNotifyCallback
  },
  {
    (EFI_PEI_PPI_DESCRIPTOR_NOTIFY_CALLBACK | EFI_PEI_PPI_DESCRIPTOR_TERMINATE_LIST),
    &gEfiPeiFirmwareVolumeInfo2PpiGuid,
    FirmwareVolumeInfoPpiNotifyCallback
  }
};

接下来可以在在串口 Log 中找到如下字样:

Install PPI: 49EDB1C1-BF21-4761-BB12-EB0031AABB39
Notify: PPI Guid: 49EDB1C1-BF21-4761-BB12-EB0031AABB39, Peim notify entry point: 82153C

2. “Install PPI: 49EDB1C1-BF21-4761-BB12-EB0031AABB39”来自:

\MdePkg\Library\PeiServicesLib\PeiServicesLib.c 中的InternalPeiServicesInstallFvInfoPpi () 函数,首先给PpiGuid赋值

    //
    // To install FvInfo Ppi.
    //
    FvInfoPpi = AllocateZeroPool (sizeof (EFI_PEI_FIRMWARE_VOLUME_INFO_PPI));
    ASSERT (FvInfoPpi != NULL);
PpiGuid = &gEfiPeiFirmwareVolumeInfoPpiGuid;

接下来PeiServicesInstallPpi (FvInfoPpiDescriptor) 会安装这个 Ppi:

  FvInfoPpiDescriptor->Guid  = PpiGuid;
  FvInfoPpiDescriptor->Flags = EFI_PEI_PPI_DESCRIPTOR_PPI | EFI_PEI_PPI_DESCRIPTOR_TERMINATE_LIST;
  FvInfoPpiDescriptor->Ppi   = (VOID *) FvInfoPpi;
  Status = PeiServicesInstallPpi (FvInfoPpiDescriptor);

安装过程会出现“Install PPI: 49EDB1C1-BF21-4761-BB12-EB0031AABB39” 的提示。

3. “Notify: PPI Guid: 49EDB1C1-BF21-4761-BB12-EB0031AABB39, Peim notify entry point: 82153C”

因为这里安装了gEfiPeiFirmwareVolumeInfoPpiGuid 所以会执行FirmwareVolumeInfoPpiNotifyCallback() 这个 Callback 函数。在函数中有定义

    DEBUG ((
      EFI_D_INFO,
      "The %dth FV start address is 0x%11p, size is 0x%08x, handle is 0x%p\n",
      (UINT32) CurFvCount,
      (VOID *) FvInfo2Ppi.FvInfo,
      FvInfo2Ppi.FvInfoSize,
      FvHandle
      ));

于是我们在串口上能看到如下 Log :

“The 1th FV start address is 0x00000900000, size is 0x00C00000, handle is 0x900000”

就是说按照我们的预期一样,PeiServicesNotifyPpi 函数是用来注册一个 Callback 函数,当安装给定 GUID  的Service 后,触发这个 Callback 函数。

Step to UEFI (250)PcdPeim 的分析

前文提到PEI阶段加载了7个模块,从Log 中的如下字样,我们知道当前跳入了 PcdPeim.efi 运行

Loading PEIM at 0x0000083D120 EntryPoint=0x0000083D620 PcdPeim.efi

执行输出的 Log 如下:

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

其中的 GUID 可以在 \MdePkg\MdePkg.dec 中查到:

  ## Include/Ppi/Pcd.h
  gPcdPpiGuid = { 0x6e81c58, 0x4ad7, 0x44bc, { 0x83, 0x90, 0xf1, 0x2, 0x65, 0xf7, 0x24, 0x80 } }
  ## Include/Ppi/PiPcd.h
  gEfiPeiPcdPpiGuid                  = { 0x1f34d25, 0x4de2, 0x23ad, { 0x3f, 0xf3, 0x36, 0x35, 0x3f, 0xf3, 0x23, 0xf1 } }
  ## Include/Ppi/PcdInfo.h
  gGetPcdInfoPpiGuid                 = { 0x4d8b155b, 0xc059, 0x4c8f, { 0x89, 0x26,  0x6, 0xfd, 0x43, 0x31, 0xdb, 0x8a } }
  ## Include/Ppi/PiPcdInfo.h
  gEfiGetPcdInfoPpiGuid              = { 0xa60c6b59, 0xe459, 0x425d, { 0x9c, 0x69,  0xb, 0xcc, 0x9c, 0xb2, 0x7d, 0x81 } }

最后的一个 PPI Notify 定义为:

  ## Include/Ppi/EndOfPeiPhase.h
  gEfiEndOfPeiSignalPpiGuid = {0x605EA650, 0xC65C, 0x42e1, {0xBA, 0x80, 0x91, 0xA5, 0x2A, 0xB6, 0x18, 0xC6 } }

继代码位于 MdeModulePkg\Universal\PCD\Pei\Pcd.c 中。

/**
  Main entry for PCD PEIM driver.

  This routine initialize the PCD database for PEI phase and install PCD_PPI/EFI_PEI_PCD_PPI.

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

  @return Status of install PCD_PPI

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

从代码上看,QEMU 模拟了 SPI ROM,使可以看到,这个 PEI Module 主要作用是注册了多个和PCD 相关的 Ppi Service 以便后续使用。

使用 CH567 USB 键盘增加支持 HID_SETREPORT

从插入USB 键盘开始抓取数据,按笔记本键盘上的按键,测试 Num 从灭到亮,再到灭;测试 Caps 从灭到亮,再到灭。其中的 SET_REPORT 有三个:

  1. 是刚插入的时候系统用来通知键盘,告知当前三个 LED 状态的;
  2. 是NUM LED 从灭到亮,再到灭的动作;
  3. 是CAPS LED 从灭到亮,再到灭的动作;

首先刷上之前的键盘固件,使用逻辑分析仪分析,可以看到,我们之前的设备没有处理SET_REPORT所以会用 STALL 回复:

因此,我们需要响应这个命令。下面是正常情况下,对于一个 SET_REPORT 的通讯:

此外,测试了按USB 键盘 CAPS LOCK按键,让灯从灭到亮再灭的过程。可以看出,USB 键盘通知系统当前有 CAPS LOCK按键,然后系统再通过 HID_REPORT通知当USB键盘前应该的灯状态来实现的。

  1. 首先在USB1Dev_EDP0_Setup_Deal函数中运行处理 Setup包的代码

// 处理 HID_SET_REPORT

                        if ((UsbSetupBuf->bRequestType==0x21)&&(SetupReqCode==HID_SET_REPORT))

                        {

                                CurrentRequest=HID_SET_REPORT;

                                printf(“SETUP HID_SET_REPORT”);

                                return;

                        }

  • 接下来在USB1Dev_EDP0_OUT_Deal函数中处理 Endpoint0 OUT 数据

        if (CurrentRequest==HID_SET_REPORT)

        {

                printf(“EDP0_OUT HID_SET_REPORT:”);

                for (i=0; i<len; i++)

                {

                        printf(“%X “,UsbEp1OUTBuf[i]);

                }

                printf(“\n”);

        }

  • 最后在USB1Dev_EDP0_IN_Deal函数中发送0字节 Package

        if (CurrentRequest==HID_SET_REPORT)

        {

                R16_UEP0_T_LEN1 = 0;

                R8_UEP0_TX_CTRL1 = UEP_DATA1 | UEP_T_RES_ACK;

                printf(“EDP0_IN HID_SET_REPORT\n”);

                CurrentRequest=0;

                return;

        }

最终成功的代码:

完整的代码:

Step to UEFI (249)继续在PeiMain 中(8)

第一次进入PeiCore() 的时候,因为环境尚未准备好,所以很快就能执行到下面的代码:

  //
  // Call PEIM dispatcher
  //
  PeiDispatcher (SecCoreData, &PrivateData);

在这里会执行 PEIM的执行:

MdeModulePkg\Core\Pei\Dispatcher\Dispatcher.c
/**
  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
  )

进去这个函数之后,因为刚开始内存并没有准备好,所以 (Private->PeiMemoryInstalled) 这个条件不会满足,会进入下面的do … while 循环。

  //
  // This is the main dispatch loop.  It will search known FVs for PEIMs and
  // attempt to dispatch them.  If any PEIM gets dispatched through a single
  // pass of the dispatcher, it will start over from the BFV again to see
  // if any new PEIMs dependencies got satisfied.  With a well ordered
  // FV where PEIMs are found in the order their dependencies are also
  // satisfied, this dispatcher should run only once.
  //

跳转到DiscoverPeimsAndOrderWithApriori() 函数中

      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);
      }

这个函数的作用是找到所有的 PEIM以及Apriori 文件。

/**

  Discover all PEIMs and optional Apriori file in one FV. There is at most one
  Apriori file in one FV.


  @param Private          Pointer to the private data passed in from caller
  @param CoreFileHandle   The instance of PEI_CORE_FV_HANDLE.

**/
VOID
DiscoverPeimsAndOrderWithApriori (
  IN  PEI_CORE_INSTANCE    *Private,
  IN  PEI_CORE_FV_HANDLE   *CoreFileHandle
  )

函数中有一个 DEBUG 输出如下:

  DEBUG ((
    DEBUG_INFO,
    "%a(): Found 0x%x PEI FFS files in the %dth FV\n",
    __FUNCTION__,
    PeimCount,
    Private->CurrentPeimFvCount
));

对应的串口 Log 中有如下字样,就是说在找到了7个 PEIM

DiscoverPeimsAndOrderWithApriori(): Found 0x7 PEI FFS files in the 0th FV

为了更好的观测,加入代码,输出它找到的 PEIM 的 GUID:

      for (Index = 0; Index < PeimCount; Index++) {
        //
        // Make an array of file name GUIDs that matches the FileHandle array so we can convert
        // quickly from file name to file handle
        //
        Status = FvPpi->GetFileInfo (FvPpi, TempFileHandles[Index], &FileInfo);
        ASSERT_EFI_ERROR (Status);
        CopyMem (&TempFileGuid[Index], &FileInfo.FileName, sizeof(EFI_GUID));
		DEBUG ((DEBUG_INFO , "%g\n", FileInfo.FileName));
      }

然后可以在串口输出中看到多了如下的输出:

9B3ADA4F-AE56-4C24-8DEA-F03B7558AE50
A3610442-E69F-4DF3-82CA-2360C4031A23
9D225237-FA01-464C-A949-BAABC02D31D0
222C386D-5ABC-4FB4-B124-FBB82488ACF4
86D70125-BAA3-4296-A62F-602BEBBB9081
89E549B0-7CFE-449D-9BA3-10D8B2312D71
EDADEB9D-DDBA-48BD-9D22-C1C169C8C5C6

使用工具继续查看,可以将上面的GUID 和文件名对应起来:

GUID文件名
9B3ADA4F-AE56-4C24-8DEA-F03B7558AE50PcdPeim.efi
A3610442-E69F-4DF3-82CA-2360C4031A23ReportStatusCodeRouterPei.efi
9D225237-FA01-464C-A949-BAABC02D31D0StatusCodeHandlePei.efi
222C386D-5ABC-4FB4-B124-FBB82488ACF4PlatformPei.efi
86D70125-BAA3-4296-A62F-602BEBBB9081DxeIpl.efi
89E549B0-7CFE-449D-9BA3-10D8B2312D71S3Resume2Pei.efi
EDADEB9D-DDBA-48BD-9D22-C1C169C8C5C6CpuMpPei.efi

特别注意,用工具我们能看到9个文件(模块),但是这里只提示找到7个。

接下来退出      DiscoverPeimsAndOrderWithApriori() 函数继续在PeiDispatcher 中执行

     //
      // Start to dispatch all modules within the current FV.
      //

对于每一个 FFS文件,需要用DepexSatisfied()检查是否满足条件,如果满足会使用PeiLoadImage()

              //
              // For PEIM driver, Load its entry point
              //
              Status = PeiLoadImage (
                         PeiServices,
                         PeimFileHandle,
                         PEIM_STATE_NOT_DISPATCHED,
                         &EntryPoint,
                         &AuthenticationState
                         );

可以通过加入 Log 的方法证明这里调用的是位于\MdeModulePkg\Core\Pei\Image\Image.c 中的函数

/**
  Routine to load image file for subsequent execution by LoadFile Ppi.
  If any LoadFile Ppi is not found, the build-in support function for the PE32+/TE
  XIP image format is used.

  @param PeiServices     - An indirect pointer to the EFI_PEI_SERVICES table published by the PEI Foundation
  @param FileHandle      - Pointer to the FFS file header of the image.
  @param PeimState       - The dispatch state of the input PEIM handle.
  @param EntryPoint      - Pointer to entry point of specified image file for output.
  @param AuthenticationState - Pointer to attestation authentication state of image.

  @retval EFI_SUCCESS    - Image is successfully loaded.
  @retval EFI_NOT_FOUND  - Fail to locate necessary PPI
  @retval Others         - Fail to load file.

**/
EFI_STATUS
PeiLoadImage (
  IN     CONST EFI_PEI_SERVICES       **PeiServices,
  IN     EFI_PEI_FILE_HANDLE          FileHandle,
  IN     UINT8                        PeimState,
  OUT    EFI_PHYSICAL_ADDRESS         *EntryPoint,
  OUT    UINT32                       *AuthenticationState
  )

其中通过如下调用来实现真正的加载:

      Status = LoadFile->LoadFile (
                          LoadFile,
                          FileHandle,
                          &ImageAddress,
                          &ImageSize,
                          EntryPoint,
                          AuthenticationState
                          );

实际的 Load动作是通过处于同一个文件中的PeiLoadImageLoadImageWrapper() 来实现的,而这个函数中实际干活的是PeiLoadImageLoadImage() 函数:

/**
  Loads a PEIM into memory for subsequent execution. If there are compressed
  images or images that need to be relocated into memory for performance reasons,
  this service performs that transformation.

  @param PeiServices      An indirect pointer to the EFI_PEI_SERVICES table published by the PEI Foundation
  @param FileHandle       Pointer to the FFS file header of the image.
  @param ImageAddressArg  Pointer to PE/TE image.
  @param ImageSizeArg     Size of PE/TE image.
  @param EntryPoint       Pointer to entry point of specified image file for output.
  @param AuthenticationState - Pointer to attestation authentication state of image.

  @retval EFI_SUCCESS      Image is successfully loaded.
  @retval EFI_NOT_FOUND    Fail to locate necessary PPI.
  @retval EFI_UNSUPPORTED  Image Machine Type is not supported.
  @retval EFI_WARN_BUFFER_TOO_SMALL 
                           There is not enough heap to allocate the requested size.
                           This will not prevent the XIP image from being invoked.

**/
EFI_STATUS
PeiLoadImageLoadImage (
  IN     CONST EFI_PEI_SERVICES       **PeiServices,
  IN     EFI_PEI_FILE_HANDLE          FileHandle,
  OUT    EFI_PHYSICAL_ADDRESS         *ImageAddressArg,  OPTIONAL
  OUT    UINT64                       *ImageSizeArg,     OPTIONAL
  OUT    EFI_PHYSICAL_ADDRESS         *EntryPoint,
  OUT    UINT32                       *AuthenticationState
  )

  其中有2处使用 DEBUG 宏进行串口输出的代码(这里研究对象是 EDK2 202108,不同版本之间可能有差别)

  DEBUG ((DEBUG_INFO, "Loading PEIM %g\n", FileHandle));
   DEBUG ((EFI_D_INFO | EFI_D_LOAD, "Loading PEIM at 0x%11p EntryPoint=0x%11p ", (VOID *)(UINTN)ImageAddress, (VOID *)(UINTN)*EntryPoint));

因此串口 Log 中看到的如下字样:

Loading PEIM 9B3ADA4F-AE56-4C24-8DEA-F03B7558AE50
Loading PEIM at 0x0000083D120 EntryPoint=0x0000083D620 PcdPeim.efi

接下的部分串口 Log就是来自PcdPeim.efi。

USB 键盘支持 HID_SET_IDLE

之前的代码中,没有对 SET_IDLE 进行处理,所以会使用 STALL 进行回复。这次的目标是在代码中添加处理的代码。

作为比对,正常情况下应该使用下面的方式处理:

                /* 分析并处理当前的SETUP包 */
                len = 0;                                                                      // 默认为成功并且上传0长度
                status = 0;
                if (( UsbSetupBuf->bRequestType &amp; USB_REQ_TYP_MASK ) != USB_REQ_TYP_STANDARD )  /* 非标准请求 */
                {
                        // 处理 HID_SET_IDLE
                        if ((UsbSetupBuf->bRequestType==0x21)&amp;&amp;(SetupReqCode==HID_SET_IDLE)) {
                                printf("SETUP HID_SET_IDLE\n");
                                R16_UEP0_T_LEN1 = 0;
                                R8_UEP0_TX_CTRL1 = UEP_DATA1 | UEP_T_RES_ACK;                    // 默认数据包是DATA1
                                return ;
                        }
                        status = 0xFF;  // 操作失败
                }

修改之后的运行结果:

完整代码如下: