Step to UEFI (131)gBS 的 Stall 探究

gBS 提供的 Stall 函数是我们经常用来做延时的过程。下面就介绍一下这个函数在NT32Pkg 中的具体实现。因为涉及到了具体的实现代码,所以列出来篇幅很长,对于大多数朋友来说直接看中文部分介绍就足够了。

首先,找到原型的定义,在 \MdeModulePkg\Core\Dxe\DxeMain\DxeMain.c

//
// DXE Core Module Variables
//
EFI_BOOT_SERVICES mBootServices = {
.........
  (EFI_EXIT_BOOT_SERVICES)                      CoreExitBootServices,                     // ExitBootServices
  (EFI_GET_NEXT_MONOTONIC_COUNT)                CoreEfiNotAvailableYetArg1,               // GetNextMonotonicCount
  (EFI_STALL)                                   CoreStall,                                // Stall
  (EFI_SET_WATCHDOG_TIMER)                      CoreSetWatchdogTimer,                     // SetWatchdogTimer
  (EFI_CONNECT_CONTROLLER)                      CoreConnectController,                    // ConnectController
.........

 

CoreStall 定义在 \MdeModulePkg\Core\Dxe\Misc\Stall.c 文件中。他接收的参数是暂停多少微秒,在这个函数中,防止计算溢出分别处理了休眠的事件很短和很长的情况,实际完成每一个单位的暂停是同一个函数:CoreInternalWaitForTick

EFI_STATUS
EFIAPI
CoreStall (
  IN UINTN            Microseconds
  )
{
  UINT64  Counter;
  UINT32  Remainder;
  UINTN   Index;

  if (gMetronome == NULL) {
    return EFI_NOT_AVAILABLE_YET;
  }

  //
  // Counter = Microseconds * 10 / gMetronome->TickPeriod
  // 0x1999999999999999 = (2^64 - 1) / 10
  //
  if ((UINT64) Microseconds > 0x1999999999999999ULL) {
    //
    // Microseconds is too large to multiple by 10 first.  Perform the divide 
    // operation first and loop 10 times to avoid 64-bit math overflow.
    //
    Counter = DivU64x32Remainder (
                Microseconds,
                gMetronome->TickPeriod,
                &Remainder
                );
    for (Index = 0; Index < 10; Index++) {
      CoreInternalWaitForTick (Counter);
    }      

    if (Remainder != 0) {
      //
      // If Remainder was not zero, then normally, Counter would be rounded 
      // up by 1 tick.  In this case, since a loop for 10 counts was used
      // to emulate the multiply by 10 operation, Counter needs to be rounded
      // up by 10 counts.
      //
      CoreInternalWaitForTick (10);
    }
  } else {
    //
    // Calculate the number of ticks by dividing the number of microseconds by
    // the TickPeriod.  Calculation is based on 100ns unit.
    //
    Counter = DivU64x32Remainder (
                MultU64x32 (Microseconds, 10),
                gMetronome->TickPeriod,
                &Remainder
                );
    if (Remainder != 0) {
      //
      // If Remainder is not zero, then round Counter up by one tick.
      //
      Counter++;
    }
    CoreInternalWaitForTick (Counter);
  }

  return EFI_SUCCESS;
}

 

做每一个单位休眠的函数是CoreInternalWaitForTick,他调用的是 Metronome Architectural Protocol。

/**
  Internal worker function to call the Metronome Architectural Protocol for 
  the number of ticks specified by the UINT64 Counter value.  WaitForTick() 
  service of the Metronome Architectural Protocol uses a UINT32 for the number
  of ticks to wait, so this function loops when Counter is larger than 0xffffffff.

  @param  Counter           Number of ticks to wait.

**/
VOID
CoreInternalWaitForTick (
  IN UINT64  Counter
  )
{
  while (RShiftU64 (Counter, 32) > 0) {
    gMetronome->WaitForTick (gMetronome, 0xffffffff);
    Counter -= 0xffffffff;
  }
  gMetronome->WaitForTick (gMetronome, (UINT32)Counter);
}

 

gMetronome 是EFI_METRONOME_ARCH_PROTOCOL ,在 \MdeModulePkg\Core\Dxe\DxeMain\DxeMain.c 中

EFI_METRONOME_ARCH_PROTOCOL       *gMetronome     = NULL;

 

具体这个 Protocol 可以在 PI Specification 中找到:
ma1

他的成员包括一个函数和一个变量, WaitForTick 是用来做实际延时的, TickPeriod 是用来说明WaitForTick函数的单位的。TickPeriod的单位是100ns,最长不能超过 200us。为了实验,编写下面的Application:

/** @file
    A simple, basic, application showing how the Hello application could be
    built using the "Standard C Libraries" from StdLib.

    Copyright (c) 2010 - 2011, Intel Corporation. All rights reserved.<BR>
    This program and the accompanying materials
    are licensed and made available under the terms and conditions of the BSD License
    which accompanies this distribution. The full text of the license may be found at
    http://opensource.org/licenses/bsd-license.

    THE PROGRAM IS DISTRIBUTED UNDER THE BSD LICENSE ON AN "AS IS" BASIS,
    WITHOUT WARRANTIES OR REPRESENTATIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED.
**/
#include <Library/BaseLib.h>
#include <Uefi.h>
#include <Library/UefiLib.h>
#include <Library/PrintLib.h>
#include <Library/ShellCEntryLib.h>

#include <Protocol/Metronome.h>

EFI_GUID gEfiMetronomeArchProtocolGuid  = 
                { 0x26BACCB2, 0x6F42, 0x11D4, 
                        { 0xBC, 0xE7, 0x00, 0x80, 0xC7, 0x3C, 0x88, 0x81 }};
                        
extern EFI_BOOT_SERVICES         *gBS;

/***
  Demonstrates basic workings of the main() function by displaying a
  welcoming message.

  Note that the UEFI command line is composed of 16-bit UCS2 wide characters.
  The easiest way to access the command line parameters is to cast Argv as:
      wchar_t **wArgv = (wchar_t **)Argv;

  @param[in]  Argc    Number of argument tokens pointed to by Argv.
  @param[in]  Argv    Array of Argc pointers to command line tokens.

  @retval  0         The application exited normally.
  @retval  Other     An error occurred.
***/
int
main (
  IN int Argc,
  IN char **Argv
  )
{
  EFI_METRONOME_ARCH_PROTOCOL   *Metronome;
  EFI_STATUS                    Status;
  
  //
  // Locate the Cpu Arch Protocol.
  //
  Status = gBS->LocateProtocol (&gEfiMetronomeArchProtocolGuid, NULL, &Metronome);
  if (EFI_ERROR (Status)) {
    Print(L"Can't find EFI_METRONOME_ARCH_PROTOCOL\n");
    return Status;
  }

  Print(L"TickPeriod=%d\n",Metronome->TickPeriod);
  
  return 0;
}

 

运行之后输出结果如下:
ma2
意思是,在 NT32 模拟环境中,一个Tick的时长是 2000*100ns=200us。在Code中,这个值是固定的,在\Nt32Pkg\MetronomeDxe\Metronome.c 定了下面这个结构体:

//
// Global Variables
//
EFI_METRONOME_ARCH_PROTOCOL mMetronome = {
  WinNtMetronomeDriverWaitForTick,
  TICK_PERIOD
};

 

在 \Nt32Pkg\MetronomeDxe\Metronome.h 中给定了TICK_PERIOD 的值,如果把这个定义修改为 2001,那么再次运行 Application ,输出值也会变化

//
// Period of on tick in 100 nanosecond units
//
#define TICK_PERIOD 2000

 

同样的,处理NT32 “硬件”延时相关的代码就是WinNtMetronomeDriverWaitForTick,在\Nt32Pkg\MetronomeDxe\Metronome.c 文件中:

EFI_STATUS
EFIAPI
WinNtMetronomeDriverWaitForTick (
  IN EFI_METRONOME_ARCH_PROTOCOL  *This,
  IN UINT32                       TickNumber
  )
/*++

Routine Description:

  The WaitForTick() function waits for the number of ticks specified by
  TickNumber from a known time source in the platform.  If TickNumber of
  ticks are detected, then EFI_SUCCESS is returned.  The actual time passed
  between entry of this function and the first tick is between 0 and
  TickPeriod 100 nS units.  If you want to guarantee that at least TickPeriod
  time has elapsed, wait for two ticks.  This function waits for a hardware
  event to determine when a tick occurs.  It is possible for interrupt
  processing, or exception processing to interrupt the execution of the
  WaitForTick() function.  Depending on the hardware source for the ticks, it
  is possible for a tick to be missed.  This function cannot guarantee that
  ticks will not be missed.  If a timeout occurs waiting for the specified
  number of ticks, then EFI_TIMEOUT is returned.

Arguments:

  This       - The EFI_METRONOME_ARCH_PROTOCOL instance.
  TickNumber - Number of ticks to wait.

Returns:

  EFI_SUCCESS - The wait for the number of ticks specified by TickNumber
                succeeded.

--*/
{
  UINT64  SleepTime;

  //
  // Calculate the time to sleep.  Win API smallest unit to sleep is 1 millisec
  // Tick Period is in 100ns units, divide by 10000 to convert to ms
  //
  SleepTime = DivU64x32 (MultU64x32 ((UINT64) TickNumber, TICK_PERIOD) + 9999, 10000);
  gWinNt->Sleep ((UINT32) SleepTime);

  return EFI_SUCCESS;
}

 

可以看到,Nt32 虚拟机中最终是使用 Sleep 来实现的,这个Api 最小取值为 1毫秒(千分之一),在编写一些在 NT32模拟环境下运行的程序时,也要特别注意他延时的最小单位只有1毫秒。

对应在具体的实体机上,会有更加精确的延时,有兴趣的朋友不妨追踪一下手中的代码,看看具体是如何实现的。

本文提到的 Application 下载:

METRONOMEARCHTest

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注