Step to UEFI (241)SecMain 中的 CONSTRUCTOR

在 SecCoreStartupWithStack() 函数中,有下面这一条调用:

ProcessLibraryConstructorList (NULL, NULL);

具体调用的代码在\Build\OvmfX64\DEBUG_VS2015x86\X64\OvmfPkg\Sec\SecMain\DEBUG\AutoGen.c 中:

VOID
EFIAPI
ProcessLibraryConstructorList (
  VOID
  )
{
  RETURN_STATUS  Status;

  Status = PlatformRomDebugLibIoPortConstructor ();
  ASSERT_RETURN_ERROR (Status);

  Status = AcpiTimerLibConstructor ();
  ASSERT_RETURN_ERROR (Status);

  Status = LzmaDecompressLibConstructor ();
  ASSERT_RETURN_ERROR (Status);
}
  1. PlatformRomDebugLibIoPortConstructor() 在 DebugLibDetectRom.c 中,函数是空的直接 return 了事;
  2. AcpiTimerLibConstructor() 在 BaseRomAcpiTimerLib.c 中,可以看到是取得当前一些 IO Base 的信息
/**
  The constructor function enables ACPI IO space.

  If ACPI I/O space not enabled, this function will enable it.
  It will always return RETURN_SUCCESS.

  @retval EFI_SUCCESS   The constructor always returns RETURN_SUCCESS.

**/
RETURN_STATUS
EFIAPI
AcpiTimerLibConstructor (
  VOID
  )
{
  UINT16 HostBridgeDevId;
  UINTN Pmba;
  UINT32 PmbaAndVal;
  UINT32 PmbaOrVal;
  UINTN AcpiCtlReg;
  UINT8 AcpiEnBit;

  //
  // Query Host Bridge DID to determine platform type
  //
  HostBridgeDevId = PciRead16 (OVMF_HOSTBRIDGE_DID);
  switch (HostBridgeDevId) {
    case INTEL_82441_DEVICE_ID:
      Pmba       = POWER_MGMT_REGISTER_PIIX4 (PIIX4_PMBA);
      PmbaAndVal = ~(UINT32)PIIX4_PMBA_MASK;
      PmbaOrVal  = PIIX4_PMBA_VALUE;
      AcpiCtlReg = POWER_MGMT_REGISTER_PIIX4 (PIIX4_PMREGMISC);
      AcpiEnBit  = PIIX4_PMREGMISC_PMIOSE;
      break;
    case INTEL_Q35_MCH_DEVICE_ID:
      Pmba       = POWER_MGMT_REGISTER_Q35 (ICH9_PMBASE);
      PmbaAndVal = ~(UINT32)ICH9_PMBASE_MASK;
      PmbaOrVal  = ICH9_PMBASE_VALUE;
      AcpiCtlReg = POWER_MGMT_REGISTER_Q35 (ICH9_ACPI_CNTL);
      AcpiEnBit  = ICH9_ACPI_CNTL_ACPI_EN;
      break;
    default:
      DEBUG ((DEBUG_ERROR, "%a: Unknown Host Bridge Device ID: 0x%04x\n",
        __FUNCTION__, HostBridgeDevId));
      ASSERT (FALSE);
      return RETURN_UNSUPPORTED;
  }

  //
  // Check to see if the Power Management Base Address is already enabled
  //
  if ((PciRead8 (AcpiCtlReg) & AcpiEnBit) == 0) {
    //
    // If the Power Management Base Address is not programmed,
    // then program it now.
    //
    PciAndThenOr32 (Pmba, PmbaAndVal, PmbaOrVal);

    //
    // Enable PMBA I/O port decodes
    //
    PciOr8 (AcpiCtlReg, AcpiEnBit);
  }

  return RETURN_SUCCESS;
}

3.LzmaDecompressLibConstructor 函数定义在\MdeModulePkg\Library\LzmaCustomDecompressLib\GuidedSectionExtraction.c 中:

/**
  Register LzmaDecompress and LzmaDecompressGetInfo handlers with LzmaCustomerDecompressGuid.

  @retval  RETURN_SUCCESS            Register successfully.
  @retval  RETURN_OUT_OF_RESOURCES   No enough memory to store this handler.
**/
EFI_STATUS
EFIAPI
LzmaDecompressLibConstructor (
  VOID
  )
{
  return ExtractGuidedSectionRegisterHandlers (
          &gLzmaCustomDecompressGuid,
          LzmaGuidedSectionGetInfo,
          LzmaGuidedSectionExtraction
          );
}

在 OvmfPkgX64.dsc 中[LibraryClasses.common.SEC] 这个Section可以看到如下三个 Lib:

  1. DebugLib|OvmfPkg/Library/PlatformDebugLibIoPort/PlatformRomDebugLibIoPort.inf
  2. TimerLib|OvmfPkg/Library/AcpiTimerLib/BaseRomAcpiTimerLib.inf
  3. <LibraryClasses> NULL|MdeModulePkg/Library/LzmaCustomDecompressLib/LzmaCustomDecompressLib.inf

上述的INF 文件中都有定义   CONSTRUCTOR   ,例如: 

CONSTRUCTOR                    = LzmaDecompressLibConstructor

因此,就是说SecMain.inf给出了引用的 LIB,然后在编译过程中如果发现对应的 LIB 有指定CONSTRUCTOR   , 那么就会构造 ProcessLibraryConstructorList() 。这样做的目的是:保证在调用一些LIB之前,已经完成了对应的初始化。类似的还有ProcessLibraryDestructorList(),可以用来完成一些LIB的收尾动作。

接下来的一个问题是:谁做了这个自动生成的动作?

答案是: build.py

在\BaseTools\Source\Python\AutoGen\GenC.py 有下面的函数:

## Create code for library constructor
#
#   @param      Info        The ModuleAutoGen object
#   @param      AutoGenC    The TemplateString object for C code
#   @param      AutoGenH    The TemplateString object for header file
#
def CreateLibraryConstructorCode(Info, AutoGenC, AutoGenH):

在其中加入如下 代码用于验证:

    for Lib in DependentLibraryList:
        if len(Lib.ConstructorList) <= 0:
            continue
        Dict = {'Function':Lib.ConstructorList}
        if Lib.ModuleType in [SUP_MODULE_BASE, SUP_MODULE_SEC]:
            ConstructorPrototypeString.Append("//www.lab-z.com testing");
            ConstructorPrototypeString.Append(gLibraryStructorPrototype[SUP_MODULE_BASE].Replace(Dict))
            ConstructorCallingString.Append(gLibraryStructorCall[SUP_MODULE_BASE].Replace(Dict))

重新编译代码,再次查看\Build\OvmfX64\DEBUG_VS2015x86\X64\OvmfPkg\Sec\SecMain\DEBUG\AutoGen.c可以看到我们测试的注释代码已经写入 AutoGen.c 了:

GLOBAL_REMOVE_IF_UNREFERENCED const BOOLEAN _gPcd_FixedAtBuild_PcdCpuSmmStackGuard = _PCD_VALUE_PcdCpuSmmStackGuard;
extern const  BOOLEAN  _gPcd_FixedAtBuild_PcdCpuSmmStackGuard;
#define _PCD_GET_MODE_BOOL_PcdCpuSmmStackGuard  _gPcd_FixedAtBuild_PcdCpuSmmStackGuard
//#define _PCD_SET_MODE_BOOL_PcdCpuSmmStackGuard  ASSERT(FALSE)  // It is not allowed to set value for a FIXED_AT_BUILD PCD

//www.lab-z.com testing
RETURN_STATUS
EFIAPI
PlatformRomDebugLibIoPortConstructor (
  VOID
  );
//www.lab-z.com testing
RETURN_STATUS
EFIAPI
AcpiTimerLibConstructor (
  VOID
  );
//www.lab-z.com testing
RETURN_STATUS
EFIAPI
LzmaDecompressLibConstructor (
  VOID
  );


VOID
EFIAPI
ProcessLibraryConstructorList (
  VOID
  )
{
  RETURN_STATUS  Status;

  Status = PlatformRomDebugLibIoPortConstructor ();
  ASSERT_RETURN_ERROR (Status);

  Status = AcpiTimerLibConstructor ();
  ASSERT_RETURN_ERROR (Status);

  Status = LzmaDecompressLibConstructor ();
  ASSERT_RETURN_ERROR (Status);

}

上述实验是基于 edk202108, 如果你在其他版本实验,有可能调用的是 build.exe,实验无法成功。

能在 ADL 上使用的 RU

今天忽然发现之前的RU 无法在 Intel 最新的 ADL 平台上工作(现象是运行之后黑屏)。我忽然也意识到在后台留言“密码“二字的朋友,他们需要 RU 压缩包的解压密码。

特此声明: RU 系列的作者是: AMI 的 James。

正式的下载网站是https://github.com/JamesAmiTw/ru-uefi , 其中的压缩包密码可以在

他个人网站 http://ruexe.blogspot.com/ 看到(该网站需要科学上网)。。

为了方便大家,这里放上一个最新的无密码 RU (截至2021年12月27日)。有需要的朋友可以下载。

=============================================================

2024年1月5日

更新的版本,解压密码 cc92d99a-ab87-43c3-99df-a5957ea53be9

=============================================================

最新版本更新在 https://www.lab-z.com/intru/ 页面

Step to UEFI (240)分析第一条 Debug Log 的输出方法

前面提到,Debug Log 中的第一条信息位于 SecMain.c 中的 SecCoreStartupWithStack() 函数中:

  DEBUG ((DEBUG_INFO,
    "SecCoreStartupWithStack(0x%x, 0x%x)\n",
    (UINT32)(UINTN)BootFv,
    (UINT32)(UINTN)TopOfCurrentStack
));

这次就分析一下 OVMF 这段代码的具体实现方式。DEBUG 这个宏,之前我们有分析过,有兴趣的读者可以在【参考1】看到之前的文章。

1.这里的 DEBUG 宏可以在  \MdePkg\Include\Library/DebugLib.h 看到,这个和我们之前遇到的是完全相同的定义:

/**
  Macro that calls DebugPrint().

  If MDEPKG_NDEBUG is not defined and the DEBUG_PROPERTY_DEBUG_PRINT_ENABLED
  bit of PcdDebugProperyMask is set, then this macro passes Expression to
  DebugPrint().

  @param  Expression  Expression containing an error level, a format string,
                      and a variable argument list based on the format string.


**/
#if !defined(MDEPKG_NDEBUG)
  #define DEBUG(Expression)        \
    do {                           \
      if (DebugPrintEnabled ()) {  \
        _DEBUG (Expression);       \
      }                            \
    } while (FALSE)
#else
  #define DEBUG(Expression)
#endif

1.1 其中的 DebugPrintEnabled () 定义在 \OvmfPkg\Library\PlatformDebugLibIoPort\DebugLib.c 文件中,OVMF 这个函数是检查编译期定义

/**
  Returns TRUE if DEBUG() macros are enabled.

  This function returns TRUE if the DEBUG_PROPERTY_DEBUG_PRINT_ENABLED bit of
  PcdDebugProperyMask is set.  Otherwise FALSE is returned.

  @retval  TRUE    The DEBUG_PROPERTY_DEBUG_PRINT_ENABLED bit of PcdDebugProperyMask is set.
  @retval  FALSE   The DEBUG_PROPERTY_DEBUG_PRINT_ENABLED bit of PcdDebugProperyMask is clear.

**/
BOOLEAN
EFIAPI
DebugPrintEnabled (
  VOID
  )
{
  return (BOOLEAN) ((PcdGet8(PcdDebugPropertyMask) &amp; DEBUG_PROPERTY_DEBUG_PRINT_ENABLED) != 0);
}

1.2 _DEBUG 的定义在 DebugLib.h 文件中,其中 DebugPrint() 是真正实现功能的函数

/**
  Internal worker macro that calls DebugPrint().

  This macro calls DebugPrint() passing in the debug error level, a format
  string, and a variable argument list.
  __VA_ARGS__ is not supported by EBC compiler, Microsoft Visual Studio .NET 2003
  and Microsoft Windows Server 2003 Driver Development Kit (Microsoft WINDDK) version 3790.1830.

  @param  Expression  Expression containing an error level, a format string,
                      and a variable argument list based on the format string.

**/

#if !defined(MDE_CPU_EBC) &amp;&amp; (!defined (_MSC_VER) || _MSC_VER > 1400)
  #define _DEBUG_PRINT(PrintLevel, ...)              \
    do {                                             \
      if (DebugPrintLevelEnabled (PrintLevel)) {     \
        DebugPrint (PrintLevel, ##__VA_ARGS__);      \
      }                                              \
    } while (FALSE)
  #define _DEBUG(Expression)   _DEBUG_PRINT Expression
#else
#define _DEBUG(Expression)   DebugPrint Expression
#endif

1.2.1 \OvmfPkg\Library\PlatformDebugLibIoPort\DebugLib.c 中定义了 DebugPrint() 函数:

/**
  Prints a debug message to the debug output device if the specified error level is enabled.

  If any bit in ErrorLevel is also set in DebugPrintErrorLevelLib function
  GetDebugPrintErrorLevel (), then print the message specified by Format and the
  associated variable argument list to the debug output device.

  If Format is NULL, then ASSERT().

  @param  ErrorLevel  The error level of the debug message.
  @param  Format      Format string for the debug message to print.
  @param  ...         Variable argument list whose contents are accessed
                      based on the format string specified by Format.

**/
VOID
EFIAPI
DebugPrint (
  IN  UINTN        ErrorLevel,
  IN  CONST CHAR8  *Format,
  ...
  )
{
  VA_LIST         Marker;

  VA_START (Marker, Format);
  DebugVPrint (ErrorLevel, Format, Marker);
  VA_END (Marker);
}

1.2.2 同一个文件中,定义了函数DebugVPrint()

/**
  Prints a debug message to the debug output device if the specified
  error level is enabled.

  If any bit in ErrorLevel is also set in DebugPrintErrorLevelLib function
  GetDebugPrintErrorLevel (), then print the message specified by Format and
  the associated variable argument list to the debug output device.

  If Format is NULL, then ASSERT().

  @param  ErrorLevel    The error level of the debug message.
  @param  Format        Format string for the debug message to print.
  @param  VaListMarker  VA_LIST marker for the variable argument list.

**/
VOID
EFIAPI
DebugVPrint (
  IN  UINTN         ErrorLevel,
  IN  CONST CHAR8   *Format,
  IN  VA_LIST       VaListMarker
  )
{
  DebugPrintMarker (ErrorLevel, Format, VaListMarker, NULL);
}

1.2.3 同一个文件中 DebugPrintMarker() 代码如下:

/**
  Prints a debug message to the debug output device if the specified
  error level is enabled base on Null-terminated format string and a
  VA_LIST argument list or a BASE_LIST argument list.

  If any bit in ErrorLevel is also set in DebugPrintErrorLevelLib function
  GetDebugPrintErrorLevel (), then print the message specified by Format and
  the associated variable argument list to the debug output device.

  If Format is NULL, then ASSERT().

  @param  ErrorLevel      The error level of the debug message.
  @param  Format          Format string for the debug message to print.
  @param  VaListMarker    VA_LIST marker for the variable argument list.
  @param  BaseListMarker  BASE_LIST marker for the variable argument list.

**/
VOID
DebugPrintMarker (
  IN  UINTN         ErrorLevel,
  IN  CONST CHAR8   *Format,
  IN  VA_LIST       VaListMarker,
  IN  BASE_LIST     BaseListMarker
  )
{
  CHAR8    Buffer[MAX_DEBUG_MESSAGE_LENGTH];
  UINTN    Length;

  //
  // If Format is NULL, then ASSERT().
  //
  ASSERT (Format != NULL);

  //
  // Check if the global mask disables this message or the device is inactive
  //
  if ((ErrorLevel &amp; GetDebugPrintErrorLevel ()) == 0 ||
      !PlatformDebugLibIoPortFound ()) {
    return;
  }

  //
  // Convert the DEBUG() message to an ASCII String
  //
  if (BaseListMarker == NULL) {
    Length = AsciiVSPrint (Buffer, sizeof (Buffer), Format, VaListMarker);
  } else {
    Length = AsciiBSPrint (Buffer, sizeof (Buffer), Format, BaseListMarker);
  }

  //
  // Send the print string to the debug I/O port
  //
  IoWriteFifo8 (PcdGet16 (PcdDebugIoPort), Length, Buffer);
}

其中PcdDebugIoPort定义在 \OvmfPkg\OvmfPkg.dec文件中:

  ## This flag is used to control the destination port for PlatformDebugLibIoPort
  gUefiOvmfPkgTokenSpaceGuid.PcdDebugIoPort|0x402|UINT16|4

因此,就是说这里的DEBUG 是通过向 0x402 直接写入 ASCII 来实现的。

参考:

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

注册表关闭 ModernStandby 的方法

在注册表 HKLM \ System \ CurrentControlSet \ Control \ Power 创建 PlatformAoAcOverride 属性为 DWORD32 值为0.

上述方法来自:

1.https://zhuanlan.zhihu.com/p/339761713 在Win10 v2004以上版本启用S3睡眠并禁用Modern Standby待机的终极解决方案

2.http://blog.sinovale.com/3867.html 戴尔dell笔记本无法进入睡眠的解决办法

3.https://www.reddit.com/r/Dell/comments/h0r56s/getting_back_s3_sleep_and_disabling_modern/

Step to UEFI (239)MainAsm跳转到SecEntry的证明

前面提到了 Main.asm 中的 Main16 最后会跳转到 SecEntry.nasm 中,这次用实验来证明这一点。

在Main.asm跳转代码如下,前面找到的地址放在 rsi 中,然后直接跳转到该地址:

BITS    64

    ;
    ; Some values were calculated in 32-bit mode.  Make sure the upper
    ; 32-bits of 64-bit registers are zero for these values.
    ;
    mov     rax, 0x00000000ffffffff
    and     rsi, rax
    and     rbp, rax
    and     rsp, rax

    ;
    ; RSI - SEC Core entry point
    ; RBP - Start of BFV
    ;

    ;
    ; Restore initial EAX value into the RAX register
    ;
    mov     rax, rsp

    ;
    ; Jump to the 64-bit SEC entry point
    ;
    jmp     rsi

这里加入代码,从0x402 Port输出 rsi的值:

    ;
    ; Restore initial EAX value into the RAX register
    ;
    mov     rax, rsp

    mov     rax,rsi
    mov     dx,0x402
	
	; 7-0 Bits
    out     dx,al       

    ; 15-8 Bits
	shr     rax,8
    out     dx,al       
	
	; 23-16 Bits
	shr     rax,8
    out     dx,al       

	; 31-24 Bits
	shr     rax,8
    out     dx,al       

	; 39-32 Bits
	shr     rax,8
    out     dx,al       

	; 47-40 Bits
	shr     rax,8
    out     dx,al       

	; 55-48 Bits
	shr     rax,8
    out     dx,al       

	; 63-56 Bits
	shr     rax,8
    out     dx,al       

接下来在SecEntry.nasm添加代码,输出当前 rip 的值:

;
; SecCore Entry Point
;
; Processor is in flat protected mode
;
; @param[in]  RAX   Initial value of the EAX register (BIST: Built-in Self Test)
; @param[in]  DI    'BP': boot-strap processor, or 'AP': application processor
; @param[in]  RBP   Pointer to the start of the Boot Firmware Volume
; @param[in]  DS    Selector allowing flat access to all addresses
; @param[in]  ES    Selector allowing flat access to all addresses
; @param[in]  FS    Selector allowing flat access to all addresses
; @param[in]  GS    Selector allowing flat access to all addresses
; @param[in]  SS    Selector allowing flat access to all addresses
;
; @return     None  This routine does not return
;
global ASM_PFX(_ModuleEntryPoint)
ASM_PFX(_ModuleEntryPoint):
    lea     rax,[$]
    mov     dx,0x402
	
	; 7-0 Bits
    out     dx,al       

    ; 15-8 Bits
	shr     rax,8
    out     dx,al       
	
	; 23-16 Bits
	shr     rax,8
    out     dx,al       

	; 31-24 Bits
	shr     rax,8
    out     dx,al       

	; 39-32 Bits
	shr     rax,8
    out     dx,al       

	; 47-40 Bits
	shr     rax,8
    out     dx,al       

	; 55-48 Bits
	shr     rax,8
    out     dx,al       

	; 63-56 Bits
	shr     rax,8
    out     dx,al     

    ;
    ; Fill the temporary RAM with the initial stack value.
    ; The loop below will seed the heap as well, but that's harmless.
    ;
    mov     rax, (FixedPcdGet32 (PcdInitValueInTempStack) << 32) | FixedPcdGet32 (PcdInitValueInTempStack)
                                                              ; qword to store
    mov     rdi, FixedPcdGet32 (PcdOvmfSecPeiTempRamBase)     ; base address,
                                                              ;   relative to
                                                              ;   ES
    mov     rcx, FixedPcdGet32 (PcdOvmfSecPeiTempRamSize) / 8 ; qword count
    cld                                                       ; store from base
                                                              ;   up
    rep stosq

在 QEMU 上运行后,用十六进制工具查看 debug.log:

可以看到输出的 0xFFFC C374, 这样可以证明跳转到 SecEntry.nasm 继续执行的。

记录一个“诡异”的矩阵键盘问题

最近用船型开关做了一个矩阵键盘,电路图如下:

简单的说就是 KEY_ROW1-3 轮流为 HIGH 然后通过读取 KEY_COL1 引脚的电平就能得到当前开关状态。编写的取得按键信息的代码如下:

int GetPressed() {
  int result = 0;
  
  digitalWrite(KEY_ROW1, HIGH);
  digitalWrite(KEY_ROW2, LOW);
  digitalWrite(KEY_ROW3, LOW);

  // Upper line
  result = (digitalRead(KEY_COL1) << 3) +
           (digitalRead(KEY_COL2) << 2) +
           (digitalRead(KEY_COL3) << 1) +
           digitalRead(KEY_COL4);

  // Middle line
  digitalWrite(KEY_ROW1, LOW);
  digitalWrite(KEY_ROW2, HIGH);
  digitalWrite(KEY_ROW3, LOW);

  result = ((digitalRead(KEY_COL1) << 3) +
            (digitalRead(KEY_COL2) << 2) +
            (digitalRead(KEY_COL3) << 1) +
            (digitalRead(KEY_COL4))) +
           result * 10;
  
  
  // Buttom line
  digitalWrite(KEY_ROW1, LOW);
  digitalWrite(KEY_ROW2, LOW);
  digitalWrite(KEY_ROW3, HIGH);
  result = (digitalRead(KEY_COL1) << 3) +
           (digitalRead(KEY_COL2) << 2) +
           (digitalRead(KEY_COL3) << 1) +
           (digitalRead(KEY_COL4) << 0) +
           result * 10;
  return result;
}

调试中我惊奇的发现每次读取KEY_COL1的值竟然不同,示意如下:

即使我将 digitalRead直接写成(GPIO.in >> KEY_COL1) & 0x1 也有同样的现象。

于是针对这个问题做了如下实验:

  1. 直接在 FireBeetle上跑,不插入在矩阵键盘中,无现象;
  2. 将 x 定义为全局变量,同样现象;
  3. 试图关掉ESP32 的编译优化功能,没有找到对应项目

从1的结果判断这个问题和我的硬件是有关的,最终拿出了示波器,果真,上了之后能够看到如下波形(黄色是KEY_COL1,绿色是KEY_ROW1):可以看到当拉高之后接收端马上变高,但是拉低之后过了很久才会降低。于是怀疑的点就是读取太快,上一个拉高的影响还没有完全结束,导致了误判。

有了上面的猜想就编写一个代码, 不断拉高ROW1 和 ROW3 ,然后看看是否会出现这样的问题,结果显示:如果一个 ROW上有一个按键是闭合的,那么另外一个 ROW的读取很大概率产生误判。

  digitalWrite(KEY_ROW1, HIGH);
  digitalWrite(KEY_ROW2, LOW);
  digitalWrite(KEY_ROW3,  LOW);

c1=(digitalRead(KEY_COL1) &lt;&lt; 3) +
           (digitalRead(KEY_COL2) &lt;&lt; 2) +
           (digitalRead(KEY_COL3) &lt;&lt; 1) +
           digitalRead(KEY_COL4);

  digitalWrite(KEY_ROW1, LOW);
  digitalWrite(KEY_ROW2, LOW);
  digitalWrite(KEY_ROW3,  HIGH);

c2=(digitalRead(KEY_COL1) &lt;&lt; 3) +
           (digitalRead(KEY_COL2) &lt;&lt; 2) +
           (digitalRead(KEY_COL3) &lt;&lt; 1) +
           digitalRead(KEY_COL4);

下面黄色是KEY_COL1 Pin (读取)和KEY_ROW3(发送)的波形图(我的示波器只有两个通道),可以看到ROW1拉高的影响持续了到KEY_ROW3的发生,因此再次读取 KEY_COL1的结果是错误的。

找到了原因问题就好解决了,每次 digitalRead() 之后加一个一个 delay 让它完成放电就能保证结果的正确性了。

所谓

矩阵键盘惹人愁,

现象诡异结果谬。

I/O 若水不停歇,

CPU更快似光流。

Step to UEFI (238)OVMF 从第一条指令到 SecMain

最近抽空研究了一下 OVMF 的代码。它是为 QEMU虚拟机设计的UEFI BIOS内置在 EDK2 的代码,对于研究 UEFI EDK2架构非常有意义。分析代码是非常枯燥的事情,相信读者也会有这样的感觉。但是“所有的答案都在代码中”,通过这样的过程能够让我们对EDK2有着更深入的理解。

这次从“上电”开始,就是下图的最左侧 “SEC”阶段,研究一下 OVMF 是如何运行的。

图片来自【参考1】

使用的代码是edk202108,可以在【参考2】看到介绍。

首先使用 build -a X64 -p OvmfPkg\OvmfPkgX64.dsc 编译生成 ovmf.fd ,之后使用下面的命令启动 QEMU:

qemu-system-x86_64 -bios "ovmf.fd" -debugcon file:debug.log -global isa-debugcon.iobase=0x402

运行之后,即可在QEMU的目录下看到 debug.log,其中的前几条记录如下:

SecCoreStartupWithStack(0xFFFCC000, 0x820000)
Register PPI Notify: DCD0BE23-9586-40F4-B643-06522CED4EDE
Install PPI: 8C8CE578-8A3D-4F1C-9935-896185C32DD3
Install PPI: 5473C07A-3DCB-4DCA-BD6F-1E9689E7349A
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

很容易可以在 \OvmfPkg\Sec\SecMain.c 的SecCoreStartupWithStack()函数中找到下面的语句:

  DEBUG ((DEBUG_INFO,
    "SecCoreStartupWithStack(0x%x, 0x%x)\n",
    (UINT32)(UINTN)BootFv,
    (UINT32)(UINTN)TopOfCurrentStack
));

这就是我们 Log 中看到的第一条。但是很明显,这里并不是上电的第一条语句,这里作为本次研究的终点。

使用 HxD 打开 OVMF.FD, 在最后可以看到如下字样,这里是CPU 上电之后运行的第一条指令:

这里是 QEMU 执行的第一条语句

使用UEFITool NE 打开 OVMF.FD查看,这个位置处于 Volume Top File 中。

工具查看它所在的 VTF

在\Build\OvmfX64\DEBUG_VS2015x86\FV\SECFV.inf 中可以看到定义了下面2个 FFS, 换句话说 SecMain + Pad-File + Volume Top File = 第一个FV (上图中 GUID 是 753BE…. 的这个)

EFI_READ_LOCK_CAP = TRUE
EFI_READ_LOCK_STATUS = TRUE
EFI_FVB2_ALIGNMENT_16 = TRUE
EFI_FV_EXT_HEADER_FILE_NAME = d:\stable202108\Build\OvmfX64\DEBUG_VS2015x86\FV\SECFV.ext
[files]
EFI_FILE_NAME = d:\stable202108\Build\OvmfX64\DEBUG_VS2015x86\FV\Ffs\df1ccef6-f301-4a63-9661-fc6030dcc880SecMain\df1ccef6-f301-4a63-9661-fc6030dcc880.ffs
EFI_FILE_NAME = d:\stable202108\Build\OvmfX64\DEBUG_VS2015x86\FV\Ffs\1BA0062E-C779-4582-8566-336AE8F78F09ResetVector\1BA0062E-C779-4582-8566-336AE8F78F09.ffs

对于这部分生成方法感兴趣的朋友可以在【参考3】中看到介绍。

接下来我们查看1BA0062E-C779-4582-8566-336AE8F78F09.ffs的代码,在\OvmfPkg\ResetVector\Ia16\ResetVectorVtf0.asm 中:

;
; The VTF signature
;
; VTF-0 means that the VTF (Volume Top File) code does not require
; any fixups.
;
vtfSignature:
    DB      'V', 'T', 'F', 0

ALIGN   16

resetVector:
;
; Reset Vector
;
; This is where the processor will begin execution
;
    nop
    nop
    jmp     EarlyBspInitReal16
ALIGN   16
fourGigabytes:

为了证明代码确实来自这里,在    jmp     EarlyBspInitReal16 语句后面添加2条作为标记

	db 0x12
	db 0x34

重新编译之后,可以在 OVMF.FD中看到多出了 12 34 (如果你在IBV 的代码中使用这个方法验证第一条语句的话可能无法成功,原因是IBV 的工具在写入跳转地址的时候会完全覆盖这一行的其他内容,所以你代码中修改之后,生成打包过程中你的标记会被覆盖)。

可以在生成的 OVMF.FD 中看到设置的标记

其中 90 90 E9 53 FF 是跳转指令,会跳转到 0xFFFF FF48

反编译结果

代码在\UefiCpuPkg\ResetVector\Vtf0\Ia16\Init16.asm 中(这里同样可以用上面加标志的方法进行验证,有兴趣的朋友可以自行尝试):

;
; @param[out] DI    'BP' to indicate boot-strap processor
;
EarlyBspInitReal16:
    mov     di, 'BP'
    jmp     short Main16

;
; @param[out] DI    'AP' to indicate application processor
;
EarlyApInitReal16:
    mov     di, 'AP'
jmp     short Main16

接下来跳转到 \stable202108\UefiCpuPkg\ResetVector\Vtf0\Main.asm的Main16 处继续执行。

;
; Modified:  EBX, ECX, EDX, EBP
;
; @param[in,out]  RAX/EAX  Initial value of the EAX register
;                          (BIST: Built-in Self Test)
; @param[in,out]  DI       'BP': boot-strap processor, or
;                          'AP': application processor
; @param[out]     RBP/EBP  Address of Boot Firmware Volume (BFV)
; @param[out]     DS       Selector allowing flat access to all addresses
; @param[out]     ES       Selector allowing flat access to all addresses
; @param[out]     FS       Selector allowing flat access to all addresses
; @param[out]     GS       Selector allowing flat access to all addresses
; @param[out]     SS       Selector allowing flat access to all addresses
;
; @return         None  This routine jumps to SEC and does not return
;
Main16:
OneTimeCall EarlyInit16

其中 EarlyInit16在Init16.asm 中:

;
; Modified:  EAX
;
; @param[in]  EAX   Initial value of the EAX register (BIST: Built-in Self Test)
; @param[out] ESP   Initial value of the EAX register (BIST: Built-in Self Test)
;
EarlyInit16:
    ;
    ; ESP -  Initial value of the EAX register (BIST: Built-in Self Test)
    ;
    mov     esp, eax

    debugInitialize   //这是空的宏

OneTimeCallRet EarlyInit16

返回 EarlyInit16 后继续:

    ;
    ; Transition the processor from 16-bit real mode to 32-bit flat mode
    ;
OneTimeCall TransitionFromReal16To32BitFlat
    ;
    ; Search for the Boot Firmware Volume (BFV)
    ;
OneTimeCall Flat32SearchForBfvBase

这个函数是在查找EFI_FIRMWARE_FILE_SYSTEM2_GUID的FV:

;#define EFI_FIRMWARE_FILE_SYSTEM2_GUID \
;  { 0x8c8ce578, 0x8a3d, 0x4f1c, { 0x99, 0x35, 0x89, 0x61, 0x85, 0xc3, 0x2d, 0xd3 } }
%define FFS_GUID_DWORD0 0x8c8ce578
%define FFS_GUID_DWORD1 0x4f1c8a3d
%define FFS_GUID_DWORD2 0x61893599
%define FFS_GUID_DWORD3 0xd32dc385

找到之后继续查找 SEC 的入口:

    ;
    ; EBP - Start of BFV
    ;

    ;
    ; Search for the SEC entry point
    ;
OneTimeCall Flat32SearchForSecEntryPoint

最后,跳转到前面找到的 SEC Entry Point(放在 RSI寄存器中)

    ;
    ; Jump to the 64-bit SEC entry point
    ;
jmp     rsi

接下来代码就跳转到 SecEntry.nasm 的代码中,主要动作是用于指定 Stack的内存:

    ;
    ; Load temporary RAM stack based on PCDs
    ;
    %define SEC_TOP_OF_STACK (FixedPcdGet32 (PcdOvmfSecPeiTempRamBase) + \
                          FixedPcdGet32 (PcdOvmfSecPeiTempRamSize))
    mov     rsp, SEC_TOP_OF_STACK
    nop

    ;
    ; Setup parameters and call SecCoreStartupWithStack
    ;   rcx: BootFirmwareVolumePtr
    ;   rdx: TopOfCurrentStack
    ;
    mov     rcx, rbp
    mov     rdx, rsp
    sub     rsp, 0x20
call    ASM_PFX(SecCoreStartupWithStack)

接下来就是C语言的代码了,位于 \OvmfPkg\Sec\SecMain.c 的 SecCoreStartupWithStack() 函数中。

参考:

  1. https://raw.githubusercontent.com/tianocore/tianocore.github.io/master/images/PI_Boot_Phases.JPG
  2. https://www.lab-z.com/edk202108/
  3. https://www.lab-z.com/ovmffv/
  4. https://onlinedisassembler.com/odaweb/OAs8VKE4/0

调试小故事(2)

作者:王家CFA

链接:https://www.zhihu.com/question/30835865/answer/114446127
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

有一天,美国通用汽车公司的庞帝雅克(Pontiac)部门收到一封客户抱怨信,上面是这样写的:这是我为了同一件事第二次写信给你们,我不会怪你们为什么没有回信给我,因为我也觉得这样别人会认为我疯了,但这的确是一个事实。


   我们家有一个传统的习惯,就是我们每天在吃完晚餐后,都会以冰激凌来当我们的饭后甜点。由于冰激凌的口味很多,所以我们家每天在饭后才投票决定要吃哪一种口味,等大家决定后我就会开车去买。


   但自从最近我买了一部新的庞帝雅克后,在我去买冰激凌的这段路程中问题就发生了。
你也许不相信,每当我买的冰激凌是香草口味时,我从店里出来车子就发不动。但如果我买的是其他的口味,车子发动就顺得很。我要让你知道,我对这件事情是非常认真的,尽管这个问题听起来不可思议。为什么当我买了香草冰激凌时,这部庞帝雅克就发不动?而我不管什么时候买其他口味的冰激凌,它就没事?



   庞帝雅克客户部很奇怪这个事件,立即将信件转给了总经理,总经理对这封信还真的感到迷惑不解,于是他派了一位工程师去查看究竟。工程师安排与这位客户的见面时间刚好是在用完晚餐的时间,两人于是一个箭步跃上车,往冰激凌店开去。那个晚上投票结果是香草口味,当买好香草冰激凌回到车上后,车子又发不动了。



   这位工程师之后又依约来了三个晚上。

   第一晚,巧克力冰激凌,车子没事。

   第二晚,草莓冰激凌,车子也没事。

   第三晚,香草冰激凌,车子发不动。



   这位工程师,到目前还是死不相信这位客户的车子对香草冰激凌“过敏”。因此,他仍然不放弃继续安排相同的行程,希望能够将这个问题解决。工程师开始记下从开始到现在所发生的种种详细资料,如时间、车子使用油的种类、车子开出及开回的时间等。



   最后他发现了线索:车主买香草冰激凌比买其他冰激凌所花的时间要短。因为香草冰激凌很受欢迎,故分箱摆在货架前面,很易取到。

   现在,工程师所要知道的疑问是,为什么这部车会因为从熄火到重新激活的时间较短时就会发不动?原因很清楚,绝对不是因为香草冰激凌的关系,工程师很快就想到,答案应该是“蒸气锁”。因为当这位客户买其他口味冰激凌时,由于时间较久,引擎有足够的时间散热,重新发动时就没有太大的问题。但是买香草口味时,由于花的时间较短,引擎太热以至于还无法让“蒸气锁”有足够的散热时间。

Step to UEFI (237)从 QEMU 看显卡的初始化

经常在面试的时候,会有人提问:如何编写一个代码来实现在没有操作系统的情况下在屏幕上显示一个字符。十年或者二十年之前,这个问题的答案是:调用 BIOS中断或者直接对0xB000:0000内存位置写入数值。但是在UEFI大行其道的今天,答案则是调用UEFI 提供的Service。但是听起来这个答案似乎并不能让人完全满意。对于带有集显的 Intel 平台,BIOS工程师将 GOP Driver 放置在BIOS中,启动过程中执行之,就有了显示的Service,对于独立显卡,也是调用了 GOP Driver 一切就会准备好。更具体来说,是如何实现通过显卡的显示呢?带着这个问题,我通过 QEMU一探究竟。

QEMU 通过模拟Cirrus CLGD 5446 PCI VGA card来实现的显示【参考1】,这款显卡的DataSheet 中有如下描述【参考2】:

就是说,这亏啊显卡符合 IBM VGA 规范,而这份规范年代久远,资料比较难以找到【参考3】.大概是说显卡的一些基本显示参数(例如:分辨率)是通过IO和显卡沟通的。首先是定义了几个基本的文本显示模式,处于这种文本显示的模式下,直接对下面给出来的内存写入字符和参数(比如颜色,闪烁等等)即可显示出来,这也是很早之前我们使用的DOS 的显示方式。

apping of Display Memory into CPU Address Space
        The first element that defines this mapping is whether or not the VGA decodes accesses from the CPU. This is controlled by the RAM Enable field. If display memory decoding is disabled, then the VGA hardware ignores writes to its address space. The address range that the VGA hardware decodes is based upon the Memory Map Select field. The following table shows the address ranges in absolute 32-bit form decoded for each value of this field:

  • 00 — A0000h-BFFFFh — 128K
  • 01 — A0000h-AFFFFh — 64K
  • 10 — B0000h-B7FFFh — 32K
  • 11 — B8000h-BFFFFh — 32K

但是,我们的 UEFI 是工作在图形模式下。因此,BIOS需要先通过IO Port (例如:3CEh和3CFh,这些都是 VGA Spec 规定好的端口号)使得显卡切换到图形模式下。例如,下面就是Cirrus CLGD 5446显卡支持的显示模式:

当处于图形模式下之后,就无法通过 B0000 这样的地址写入数据了。同样在 Cirrus CLGD 5446 Data Sheet 上有如下描述,就是说 PCI 空间上会给出显卡内存的地址:

在 QEMU 下用 RU 查看,在VGA PCI 配置空间的配置空间中,可以看到PCI Display Memory Base Address 是 0x8000 0000。

接下来到EDK2代码中查看OVMF 部分的代码:

1.前面提到的通过IO Port 对VGA 进行初始化的操作,在 \OvmfPkg\QemuVideoDxe\Gop.c文件中:

EFI_STATUS
EFIAPI
QemuVideoGraphicsOutputSetMode (
  IN  EFI_GRAPHICS_OUTPUT_PROTOCOL *This,
  IN  UINT32                       ModeNumber
  )
/*++

Routine Description:

  Graphics Output protocol interface to set video mode

  Arguments:
    This             - Protocol instance pointer.
    ModeNumber       - The mode number to be set.

  Returns:
    EFI_SUCCESS      - Graphics mode was changed.
    EFI_DEVICE_ERROR - The device had an error and could not complete the request.
    EFI_UNSUPPORTED  - ModeNumber is not supported by this device.

--*/

2.通过写入 MMIO实现显示的代码,同样在 \OvmfPkg\QemuVideoDxe\Gop.c文件中:

EFI_STATUS
EFIAPI
QemuVideoGraphicsOutputBlt (
  IN  EFI_GRAPHICS_OUTPUT_PROTOCOL          *This,
  IN  EFI_GRAPHICS_OUTPUT_BLT_PIXEL         *BltBuffer, OPTIONAL
  IN  EFI_GRAPHICS_OUTPUT_BLT_OPERATION     BltOperation,
  IN  UINTN                                 SourceX,
  IN  UINTN                                 SourceY,
  IN  UINTN                                 DestinationX,
  IN  UINTN                                 DestinationY,
  IN  UINTN                                 Width,
  IN  UINTN                                 Height,
  IN  UINTN                                 Delta
  )

其中调用了 FrameBufferBlt()

  switch (BltOperation) {
  case EfiBltVideoToBltBuffer:
  case EfiBltBufferToVideo:
  case EfiBltVideoFill:
  case EfiBltVideoToVideo:
    Status = FrameBufferBlt (
      Private->FrameBufferBltConfigure,
      BltBuffer,
      BltOperation,
      SourceX,
      SourceY,
      DestinationX,
      DestinationY,
      Width,
      Height,
      Delta
      );
break;

FrameBufferBlt函数在 \MdeModulePkg\Library\FrameBufferBltLib\FrameBufferBltLib.c文件中:

/**
  Performs a UEFI Graphics Output Protocol Blt operation.

  @param[in]     Configure    Pointer to a configuration which was successfully
                              created by FrameBufferBltConfigure ().
  @param[in,out] BltBuffer    The data to transfer to screen.
  @param[in]     BltOperation The operation to perform.
  @param[in]     SourceX      The X coordinate of the source for BltOperation.
  @param[in]     SourceY      The Y coordinate of the source for BltOperation.
  @param[in]     DestinationX The X coordinate of the destination for
                              BltOperation.
  @param[in]     DestinationY The Y coordinate of the destination for
                              BltOperation.
  @param[in]     Width        The width of a rectangle in the blt rectangle
                              in pixels.
  @param[in]     Height       The height of a rectangle in the blt rectangle
                              in pixels.
  @param[in]     Delta        Not used for EfiBltVideoFill and
                              EfiBltVideoToVideo operation. If a Delta of 0
                              is used, the entire BltBuffer will be operated
                              on. If a subrectangle of the BltBuffer is
                              used, then Delta represents the number of
                              bytes in a row of the BltBuffer.

  @retval RETURN_INVALID_PARAMETER Invalid parameter were passed in.
  @retval RETURN_SUCCESS           The Blt operation was performed successfully.
**/
RETURN_STATUS
EFIAPI
FrameBufferBlt (
  IN     FRAME_BUFFER_CONFIGURE                *Configure,
  IN OUT EFI_GRAPHICS_OUTPUT_BLT_PIXEL         *BltBuffer, OPTIONAL
  IN     EFI_GRAPHICS_OUTPUT_BLT_OPERATION     BltOperation,
  IN     UINTN                                 SourceX,
  IN     UINTN                                 SourceY,
  IN     UINTN                                 DestinationX,
  IN     UINTN                                 DestinationY,
  IN     UINTN                                 Width,
  IN     UINTN                                 Height,
  IN     UINTN                                 Delta
  )

简单起见我们只是研究一下填充函数:

  case EfiBltVideoFill:
    return FrameBufferBltLibVideoFill (
             Configure,
             BltBuffer,
             DestinationX,
             DestinationY,
             Width,
             Height
             );

FrameBufferBltLibVideoFill()函数在\MdeModulePkg\Library\FrameBufferBltLib\FrameBufferBltLib.c代码如下:

/**
  Performs a UEFI Graphics Output Protocol Blt Video Fill.

  @param[in]  Configure     Pointer to a configuration which was successfully
                            created by FrameBufferBltConfigure ().
  @param[in]  Color         Color to fill the region with.
  @param[in]  DestinationX  X location to start fill operation.
  @param[in]  DestinationY  Y location to start fill operation.
  @param[in]  Width         Width (in pixels) to fill.
  @param[in]  Height        Height to fill.

  @retval  RETURN_INVALID_PARAMETER Invalid parameter was passed in.
  @retval  RETURN_SUCCESS           The video was filled successfully.

**/
EFI_STATUS
FrameBufferBltLibVideoFill (
  IN  FRAME_BUFFER_CONFIGURE        *Configure,
  IN  EFI_GRAPHICS_OUTPUT_BLT_PIXEL *Color,
  IN  UINTN                         DestinationX,
  IN  UINTN                         DestinationY,
  IN  UINTN                         Width,
  IN  UINTN                         Height
  )
{}

我们可以通过在其中加入DEBUG Message 的方法来判定对显卡的写入操作。经过实验发现,cls 命令之后会在 0x8000 001c 写入0x00。因此,这里实验从这个位置开始写入 0xFF, 可以看到屏幕上出现了一个白色的线条:

总结:UEFI Spec 定义了显卡应该提供什么样的接口给用户调用,但是具体实现由各家自己完成。相比大多数的应该都是简单设定都是 IO 来完成,具体要显示的内容则是通过 MMIO 来完成。

参考:

  1. https://stuff.mit.edu/afs/sipb/project/phone-project/OldFiles/share/doc/qemu/qemu-doc.html
  2. CL-GD5446 Datasheet.pdf
  3. http://www.osdever.net/FreeVGA/vga/vgamem.htm

ESP32 S2 的 SPI

打开ESP32-S2 技术参考手册 (“esp32-s2_technical_reference_manual_cn”),可以看到下图:

ESP32 S2的Arduino 环境对于 SPI 的定义是有问题的。

ESP32 S2 SPI框图

对于 S2 这个芯片来说,有四个 SPI,其中“SPI0 和 SPI1 仅供内部使用,通过仲裁器共享 SPI 信号总线”。因此,对于用户来说,只能使用 FSPI(GP-SPI2)和SPI3(GP-SPI3)。对比之前的 ESP32:

ESP32 SPI 框图

同样的有4个 SPI,其中“SPI0控制器作为 cache 访问外部存储单元接口使用” ,因此用户可以使用 SPI1-3,其中 SPI2 又称作 HSPI ,SPI3又称作 VPSI。其中的 HSPI 和 VSPI 只是一个代号,并不表示 High Speed SPI 值了的。

对比二者,再次强调S2 只有 FSPI 和 SPI3。但是在\Arduino15\packages\esp32\hardware\esp32\2.0.1\cores\esp32\esp32-hal-spi.h 中有如下定义:

#if CONFIG_IDF_TARGET_ESP32C3
#define FSPI  0
#define HSPI  1
#else
#define FSPI  1 //SPI bus attached to the flash (can use the same data lines but different SS)
#define HSPI  2 //SPI bus normally mapped to pins 12 - 15, but can be matrixed to any pins
#if CONFIG_IDF_TARGET_ESP32
#define VSPI  3 //SPI bus normally attached to pins 5, 18, 19 and 23, but can be matrixed to any pins
#endif
#endif

这样导致在你编写的 ESP32 S2 代码中 FSPI=1 HSPI=2, 欢聚话说 FSPI 是 SPI1, HSPI 才是 GP-SPI2。这会导致之前能够正常运行在ESP32的代码移植到 S2 之后SPI 无法工作(想确认这一点最简单的方法是示波器测量 SPI SCLK 信号)。

此外,SPI.cpp定义的 SPI实际上是 SPI1,这个在 S2 上根本无法工作。

#if CONFIG_IDF_TARGET_ESP32
SPIClass SPI(VSPI);
#else
SPIClass SPI(FSPI);
#endif

如果你的代码直接使用 SPI,那么无比将上面的代码修改为 SPIClass SPI(HSPI);。

第二个坑是关于 SPI 引脚分配的,同样在 SPI.cpp 中有如下定义,如果你没有给定 SPI 的引脚,那么默认分配的都是 -1 Pin:

    if(sck == -1 && miso == -1 && mosi == -1 && ss == -1) {
#if CONFIG_IDF_TARGET_ESP32S2
        _sck = (_spi_num == FSPI) ? SCK : -1;
        _miso = (_spi_num == FSPI) ? MISO : -1;
        _mosi = (_spi_num == FSPI) ? MOSI : -1;
        _ss = (_spi_num == FSPI) ? SS : -1;

这个在代码中可以使用 Serial.print(hspi->pinSS()); 进行检查。最后是一个我这边测试过,ESP32 S2 工作正常的 SPI 例子。使用 GP-SPI2 ,定义SCLK = 14, MISO = 12, MOSI = 13, SS = 15.

#include &lt;SPI.h>

static const int spiClk = 40000000; 

SPIClass * hspi = NULL;

void setup() {
  Serial.begin(115200);
  //initialise two instances of the SPIClass attached to VSPI and HSPI respectively
  hspi = new SPIClass(HSPI);
  
  //initialise hspi with default pins
  //SCLK = 14, MISO = 12, MOSI = 13, SS = 15
  hspi->begin(10,12,11,13);

  //set up slave select pins as outputs as the Arduino API
  //doesn't handle automatically pulling SS low
  pinMode(hspi->pinSS(), OUTPUT); //HSPI SS

}

// the loop function runs over and over again until power down or reset
void loop() {

  spiCommand(hspi, 0b11001100);

  Serial.print(MISO);Serial.print(" ");
  Serial.print(MOSI);Serial.print(" ");
  Serial.print(SCK);Serial.print(" ");
  Serial.print(SS);Serial.print(" ");
  Serial.print(HSPI);Serial.print(" ");
  Serial.print(FSPI);Serial.print(" ");  
  Serial.print(hspi->pinSS());Serial.println(" ");
  
  delay(2000);
}

void spiCommand(SPIClass *spi, byte data) {
  //use it as you would the regular arduino SPI API
  spi->beginTransaction(SPISettings(spiClk, MSBFIRST, SPI_MODE0));
  digitalWrite(spi->pinSS(), LOW); //pull SS slow to prep other end for transfer
  spi->transfer(data);
  digitalWrite(spi->pinSS(), HIGH); //pull ss high to signify end of data transfer
  spi->endTransaction();
}

=============================================================

2022年5月10日更新,在 2.0.1 上实验完全不修改 Arduino 相关文件,完全使用默认配置,下面的代码将使用IO34/35/37/36 分别作为 SS/MOSI/MISO/SCK (示波器验证过)

static const uint8_t SS    = 34;
static const uint8_t MOSI  = 35;
static const uint8_t MISO  = 37;
static const uint8_t SCK   = 36;

上述定义来自C:\Users\NAME\AppData\Local\Arduino15\packages\esp32\hardware\esp32\2.0.1\variants\esp32s2\pins_arduino.h

#include <SPI.h>
static const int spiClk = 40000000;
void setup() {
  Serial.begin(115200);
  SPI.begin();
}

void loop() {
  spiCommand(&SPI, 0b11001100);
  Serial.print(MISO);Serial.print(" ");
  Serial.print(MOSI);Serial.print(" ");
  Serial.print(SCK);Serial.print(" ");
  Serial.print(SS);Serial.print(" ");
  Serial.print(HSPI);Serial.print(" ");
  Serial.print(FSPI);Serial.println(" "); 

  delay(200);

}

 

void spiCommand(SPIClass *spi, byte data) {

  //use it as you would the regular arduino SPI API

  spi->beginTransaction(SPISettings(spiClk, MSBFIRST, SPI_MODE0));

  digitalWrite(spi->pinSS(), LOW); //pull SS slow to prep other end for transfer

  spi->transfer(data);

  digitalWrite(spi->pinSS(), HIGH); //pull ss high to signify end of data transfer

  spi->endTransaction();

}