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

}

Memtest86 显示异常的解决方法

MemTest86 是一款很好用的 UEFI 的内存测试软件。但是最近在使用中碰到了开始测试之后显示异常的问题,具体的说是显示内容偏于一侧无法看到大部分内容。这是由于你的显示器或者屏幕分辨率过高导致的软件错误。解决方法是,打开EFI\BOOT 下面的 MemTest86.log 文件,查找 Product: 后面的字样。例如,下面需要找到的字符串是”LABZ Product”。

2021-09-01 05:49:27 - =============================================
2021-09-01 05:49:27 - MemTest86 V9.2 Free Build: 2000 (64-bit)
2021-09-01 05:49:27 - =============================================
2021-09-01 05:49:27 - SMBIOS BIOS INFO Vendor: "LABZ", Version: "1.65280.9", Release Date: "06/11/2021"
2021-09-01 05:49:27 - SMBIOS SYSTEM INFO Manufacturer: "LABZ", Product: "LABZ Product", Version: "1114I:00074T:000M:0300000D:0B:04F:6C:09P:38S:01E:0Y:0L:0", S/N: "1322423123", SKU: "", Family: ""
2021-09-01 05:49:28 - SMBIOS: Found SMBIOS BaseboardInformation (pbLinAddr=0x8BE000F8, FormattedLen=15, iTotalLen=46)
2021-09-01 05:49:28 - SMBIOS BASEBOARD INFO Manufacturer: "OEMAL", Product: "OEMAL Product", Version: "", S/N: "456456456", AssetTag: "", LocationInChassis: ""
2021-09-01 05:49:28 - EFI Specifications: 2.70
2021-09-01 05:49:28 - Found blacklist file

之后,将这个字符串添加到 blacklist.cfg 文件末尾,例如:

"Surface Pro",ALL,PARTIAL,FIXED_SCREENRES
"MACH-WX9-PCB",ALL,EXACT,FIXED_SCREENRES
"MACH-WX9",ALL,EXACT,FIXED_SCREENRES
"8873",ALL,EXACT,FIXED_SCREENRES
"LABZ Product",ALL,PARTIAL,FIXED_SCREENRES

再次运行的时候,MemTest86 会在这个文件中查找你的机型,如果找到的话会对你的机器做针对性的调整避免这个问题。

调试小故事(1)

本文来自如下链接, 是一个关于调试的小故事。无论你是否懂得编程,相信看完之后都能有所感悟。

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

那还是80年代初期,我爸爸在一家存储设备公司工作,这个公司现在已经不存在了,它生产磁带机和驱动这些磁带高速运转的气动系统 —— 这是那个时代的产物。

他们技术改造了磁带驱动器,使得你可以只有一个中心驱动器 —— “A”盘 —— 由它连接着数个“B”盘,在跟A盘连接的内存里驻留这一个小型的操作系统,负责代理所有B盘的数据的读写操作。

每次当你启动A驱动器,你需要在外围驱动器里插入一张软盘,操作系统会把A盘加载到内存里。这个操作系统简单的出奇 —— 它的处理能力全部从一个8字节的微型控制器产生。

这种设备的目标用户是拥有大量数据的企业 —— 银行,杂志等等 —— 他们需要打印大量的地址簿或银行帐目。

有个客户出现了一个问题。在打印的过程中,有个别的驱动器会停止工作,导致整个打印过程终止。为了重载驱动器,值班人员必须重启所有驱动 —— 如果这种事情发生在一个6小时的打印任务中,大量宝贵的计算机使用时间都会浪费,整个任务将不能按时间完成。

公司派出了技术人员。技术人员尽了他最大的努力也不能在测试环境复制出这个问题:这个问题似乎只会出现在打印大量任务的过程中。尽管问题出在硬件上可能性微乎其微,他还是更换了所有的设备 —— 内存,微处理器,磁盘驱动,所有跟磁带机相关的部件 —— 但问题仍然出现。

于是技术人员打电话给总部叫来了一位专家。

专家要了一把椅子和一杯咖啡,坐在了计算机房 —— 那个时候他们已经专门为计算机提供了机房 —— 值班人员准备了一大堆的打印任务,他就在旁边看着。他等着,一直到机器崩溃。机器果真崩溃了,所有人都看着专家 —— 专家没有发现任何的线索。他命令把打印任务重新执行一次,所有的值班人员和技术人员都回各自岗位工作。

专家又在椅子上坐下来,等着机器崩溃。这一等就是六小时,但真的又发生了。专家仍然没有弄清是什么导致了崩溃 —— 除了有一点他注意到,崩溃总是发生在屋内人比较多的时候。他命令再打印一次,重新坐下,等着。

当第三次崩溃时,他发现了一件事情。崩溃总是在值班人员更换其他没有关联的启动盘时发生的。进一步研究,他意识到当一个值班人员走过某块地板时崩溃就会发生。

地板是由铝制的板块拼成,下面有6 到 8 英寸高的隔空层,计算机所使用的大量的电缆都走地板下,这样可以避免值班人员无意间踢到它们。地板块间拼合的很紧密,这是为了保证垃圾不掉进电缆通过的空间。

专家说有一块地板变形了。当值班人员踩着这块变形的地板的一角时,地板块的边缘相互摩擦,这就会跟连接各地板的塑料之间产生静电,进而造成电磁干扰。

如今所有的RAM都有防电磁干扰功能。但当时并没有这种技术。专家指出,电磁干扰破坏的RAM的工作,操作系统也就崩溃了。

专家打电话给维护部门,拿来了一块新地板,他自己把它装上,问题就这样解决了。

译文出处:外刊IT评论

译文地址:http://www.vaikan.com/the-best-debugging-story-i-ve-ever-heard/

原文地址:http://patrickthomson.tumblr.com/post/2499755681/the-best-debugging-story-ive-ever-heard

Step to UEFI (236)UEFI Shell 下浮点运算测试

UEFI的 StdLib 中内置了一些数学运算操作,在Math.h中可以找到tan, sin 等等。这次测试一下如何调用sin() 函数。

首先,要保证 \AppPkg\AppPkg.dsc 末尾有如下定义:

!include StdLib/StdLib.inc

接下来在编写的 UEFI程序对应的 inf 文件中LibraryClasses下面有引用 LibMath:

[LibraryClasses]
  LibC
  LibStdio
  ShellCEntryLib   
  ShellLib
  BaseLib
  BaseMemoryLib
  UefiLib
  LibMath

最后,代码中 include math.h 即可。

示例代码:

1. MathTest.c 如下:

#include  <Uefi.h>
#include  <Library/UefiLib.h>
#include  <Library/ShellCEntryLib.h>
#include  <stdio.h>
#include  <stdlib.h>
#include  <math.h>

extern EFI_BOOT_SERVICES         *gBS;

int
EFIAPI
main (                                         
  IN int Argc,
  IN char **Argv
  )
{
  printf("Pi=%f\n",M_PI);
  printf("sin(Pi/2)=%f\n",sin(M_PI /2));
  printf("sin(Pi/4)=%f\n",sin(M_PI /4));
  printf("sin(Pi/6)=%f\n",sin(M_PI /6));
  return EFI_SUCCESS;
}

2. MathTest.inf 如下:

[Sources]
  MathTest.c

[Packages]
  StdLib/StdLib.dec   
  MdePkg/MdePkg.dec
  ShellPkg/ShellPkg.dec 


[LibraryClasses]
  LibC
  LibStdio
  ShellCEntryLib   
  ShellLib
  BaseLib
  BaseMemoryLib
  UefiLib
  LibMath

代码运行结果:

计算时间间隔的批处理

来自stackoverflow:

@echo off
setlocal

rem The format of %TIME% is HH:MM:SS,CS for example 23:59:59,99
set STARTTIME=%TIME%

rem 这里开始
rem 你要计算的部分
rem 这里结束

set ENDTIME=%TIME%

rem output as time
echo STARTTIME: %STARTTIME%
echo ENDTIME: %ENDTIME%

rem convert STARTTIME and ENDTIME to centiseconds
set /A STARTTIME=(1%STARTTIME:~0,2%-100)*360000 + (1%STARTTIME:~3,2%-100)*6000 + (1%STARTTIME:~6,2%-100)*100 + (1%STARTTIME:~9,2%-100)
set /A ENDTIME=(1%ENDTIME:~0,2%-100)*360000 + (1%ENDTIME:~3,2%-100)*6000 + (1%ENDTIME:~6,2%-100)*100 + (1%ENDTIME:~9,2%-100)

rem calculating the duratyion is easy
set /A DURATION=%ENDTIME%-%STARTTIME%

rem we might have measured the time inbetween days
if %ENDTIME% LSS %STARTTIME% set set /A DURATION=%STARTTIME%-%ENDTIME%

rem now break the centiseconds down to hors, minutes, seconds and the remaining centiseconds
set /A DURATIONH=%DURATION% / 360000
set /A DURATIONM=(%DURATION% - %DURATIONH%*360000) / 6000
set /A DURATIONS=(%DURATION% - %DURATIONH%*360000 - %DURATIONM%*6000) / 100
set /A DURATIONHS=(%DURATION% - %DURATIONH%*360000 - %DURATIONM%*6000 - %DURATIONS%*100)

rem some formatting
if %DURATIONH% LSS 10 set DURATIONH=0%DURATIONH%
if %DURATIONM% LSS 10 set DURATIONM=0%DURATIONM%
if %DURATIONS% LSS 10 set DURATIONS=0%DURATIONS%
if %DURATIONHS% LSS 10 set DURATIONHS=0%DURATIONHS%

rem outputing
echo STARTTIME: %STARTTIME% centiseconds
echo ENDTIME: %ENDTIME% centiseconds
echo DURATION: %DURATION% in centiseconds
echo %DURATIONH%:%DURATIONM%:%DURATIONS%,%DURATIONHS%

endlocal
goto :EOF

参考:

1.https://stackoverflow.com/questions/9922498/calculate-time-difference-in-windows-batch-file

Step to UEFI (235)UEFI Shell 下的变幻线

CRT(阴极射线显像管)显示器的显像原理主要是由灯丝加热阴极,阴极发射电子,然后在加速极电场的作用下,经聚焦极聚成很细的电子束,在阳极高压作用下,获得巨大的能量,以极高的速度去轰击荧光粉层。这些电子束轰击的目标就是荧光屏上的三原色。为此,电子枪发射的电子束不是一束,而是三束,它们分别受电脑显卡R、 G、 B三个基色视频信号电压的控制,去轰击各自的荧光粉单元,从而在显示屏上显示出完整的图像。

在图形界面的操作系统下,显示屏上显示的色彩多种多样,当用户停止对电脑进行操作时,屏幕显示就会始终固定在同一个画面上,即电子束长期轰击荧光层的相同区域,长时间下去,会因为显示屏荧光层的疲劳效应导致屏幕老化,甚至是显像管被击穿。因此从Windows 3.X时代至今,屏幕保护程序一直作为保护CRT显示屏的最佳帮手,通过不断变化的图形显示使荧光层上的固定点不会被长时间轰击,从而避免了屏幕的损坏。【参考2】

比如,下面这个照片就是游戏《吃豆人》的烧屏,《吃豆人》历史久远,画面对比度高且单一,又是热门游戏,所以很多老机台烧屏都十分严重。【参考3】

吃豆人烧屏

随着时代的发展,屏保除了展示信息已经没有太多意义了。Windows XP 开始,内置了一个非常经典的“变换线”屏保。这次的实验就是在 UEFI Shell 下实现这个功能。

关键部分参考了《模拟经典屏保“变幻线”》【参考1】,主要是绘制直线(采用打点组成直线的方法,效率不高)。

代码如下:

#include  <Uefi.h>
#include  <Library/UefiLib.h>
#include  <Library/ShellCEntryLib.h>
#include  <stdio.h>
#include  <stdlib.h>
#include  <math.h>

#include <Protocol/EfiShell.h>
#include <Library/ShellLib.h>
#include <Protocol/GraphicsOutput.h>


extern EFI_BOOT_SERVICES         *gBS;
extern EFI_SYSTEM_TABLE          *gST;

EFI_GUID GraphicsOutputProtocolGuid = EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID;
EFI_GRAPHICS_OUTPUT_PROTOCOL          *GraphicsOutput = NULL;
EFI_GRAPHICS_OUTPUT_BLT_PIXEL         PixelColor= {230,17,219,0};
        
#define HIGH 600
#define WIDTH 800
#define RADIUS 20
#define PN 5

typedef struct Point
{
        int x;
        int y;
        int velocity_x;
        int velocity_y;
} VPOINT;

static VPOINT vpoint[PN];

int
abs(int j)
{
        return(j < 0 ? -j : j);
}

EFI_STATUS DrawPoint(
        IN EFI_GRAPHICS_OUTPUT_PROTOCOL  *GraphicsOutput,
        IN UINTN x,
        IN UINTN y,
        IN UINTN Width,
        IN EFI_GRAPHICS_OUTPUT_BLT_PIXEL *PixelColor
)
{
        EFI_STATUS Status;

        Status = GraphicsOutput->Blt(
                         GraphicsOutput,
                         PixelColor,
                         EfiBltVideoFill,
                         0, 0,
                         x, y,
                         Width, Width,
                         0
                 );

        return EFI_SUCCESS;
}

EFI_STATUS DrawLine(
        IN EFI_GRAPHICS_OUTPUT_PROTOCOL  *GraphicsOutput,
        IN UINTN x0, UINTN y0, UINTN x1, UINTN y1,
        IN UINTN BorderWidth,
        IN EFI_GRAPHICS_OUTPUT_BLT_PIXEL *BorderColor
)
{
        INT32 dx  = abs((int)(x1 - x0));
        INT32 sx  = x0 < x1 ? 1 : -1;
        INT32 dy  = abs((int)(y1-y0)), sy = y0 < y1 ? 1 : -1;
        INT32 err = ( dx > dy ? dx : -dy) / 2, e2;

        for(;;)
        {
                DrawPoint(GraphicsOutput, x0, y0, BorderWidth, BorderColor);

                if (x0==x1 && y0==y1) break;

                e2 = err;

                if (e2 > -dx)
                {
                        err -= dy;
                        x0 += sx;
                }
                if (e2 <  dy)
                {
                        err += dx;
                        y0 += sy;
                }
        }
        return EFI_SUCCESS;
}

void show()
{
        gST -> ConOut -> ClearScreen(gST->ConOut);

        for (int i = 0; i < PN; i++)
        {
                if ((vpoint[i].x <= 0) || (vpoint[i].x >= WIDTH))
                        vpoint[i].velocity_x = -vpoint[i].velocity_x;
                if ((vpoint[i].y  <= 0) || (vpoint[i].y  >= HIGH))
                        vpoint[i].velocity_y = -vpoint[i].velocity_y;

                vpoint[i].x += vpoint[i].velocity_x;
                vpoint[i].y += vpoint[i].velocity_y;

                for (int j = 0; j < PN - 1; j++)
                {
                        DrawLine(
                                GraphicsOutput,
                                vpoint[j].x,vpoint[j].y,
                                vpoint[j + 1].x,vpoint[j + 1].y,
                                1,
                                &PixelColor);
                }
                DrawLine(
                        GraphicsOutput,
                        vpoint[0].x, vpoint[0].y,
                        vpoint[PN - 1].x, vpoint[PN - 1].y,
                        1,
                        &PixelColor);
        }
}

int
EFIAPI
main (
        IN int Argc,
        IN char **Argv
)
{
        EFI_STATUS      Status;
        EFI_INPUT_KEY   Key;
        int             a;
        
        Status = gBS->LocateProtocol(
                                &GraphicsOutputProtocolGuid, 
                                NULL, 
                                (VOID **) &GraphicsOutput);
        if (EFI_ERROR(Status))
        {
                GraphicsOutput = NULL;
                Print(L"Loading Graphics_Output_Protocol error!\n");
                return EFI_SUCCESS;
        }

        for (int i = 0; i < PN; i++)
        {
                vpoint[i].x = rand() % (WIDTH - RADIUS) + RADIUS;
                vpoint[i].y = rand() % (HIGH - RADIUS) + RADIUS;
                a = rand() % 361;
                vpoint[i].velocity_x = (int)((RADIUS / 5) * cos(a));
                vpoint[i].velocity_y = (int)((RADIUS / 5) * sin(a));
        }

        for (int i = 0; i < PN - 1; i++)
        {
                DrawLine(
                        GraphicsOutput,
                        vpoint[i].x,vpoint[i].y,
                        vpoint[i + 1].x,vpoint[i + 1].y,
                        1,
                        &PixelColor);
        }
        DrawLine(
                GraphicsOutput,
                vpoint[0].x, vpoint[0].y,
                vpoint[PN - 1].x, vpoint[PN - 1].y,
                1,
                &PixelColor);


        Key.ScanCode=SCAN_NULL;
        while (SCAN_DOWN!=Key.ScanCode)
        {
                show();
                Status= gST -> ConIn -> ReadKeyStroke(gST->ConIn,&Key);
        }
        
        return EFI_SUCCESS;

}

运行之后按向下的方向键退出代码。运行结果:

UEFI Shell 下面的变换线

源代码和编译后的 X64程序可以在这里下载:

参考:

  1. https://blog.csdn.net/weixin_48758682/article/details/108680035
  2. https://baike.baidu.com/item/%E5%B1%8F%E5%B9%95%E4%BF%9D%E6%8A%A4/6124893?fr=aladdin#2
  3. https://tieba.baidu.com/p/5041549602?red_tag=3176677362

Win11 下 RW_Everything 无法运行的解决方法

最近在 Win11 下发现 RW 无法运行,会弹出下面这个错误提示:

RW_Everything 无法运行的提示信息

经过实验可以用下面的方法解决,将注册表中下面这个值从 1修改为0【参考1】:

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\DeviceGuard\Scenarios\HypervisorEnforcedCodeIntegrity

Enabled DWORD

0 = Off
1 = On

个人感觉,Win11 中有很多安全方面的设置会影响 RW 导致其无法运行。上面只是其中的一个。后面如果再遇到其他原因我会在本文持续更新。

参考:

1.https://www.cnblogs.com/chucklu/p/13130267.html Memory integrity 导致 A driver can’t load on this device

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

2022年6月8日 RW 无法在 Windows 11 22H2 下运行的解决方法:

Intel Shenzhen 招聘 BIOS(Nov 2021)

Job Description: 

BIOS engineer is responsible for working with customers and architects to drive firmware requirements into clientbased platform designs Responsibilities include triage and replication resolving and tracking incoming customer issues related to the firmware on customer designs involving BIOS ME TBT and other platform components defining compliance requirements for our customers and developingsupporting documents and application notesResponsibilities also include issue management and collaborating with other organizations such as firmware development System validation and other hardware teams to resolve issues together In addition the individual will coordinate with other global organizations A strong technical background is required with the ability to lead taskforces and initiatives supporting customer designs to match launch plans

Qualifications:

You must possess a Bachelor of Science degree in Electrical Engineering Computer Engineering or equivalent with at least four years of industry experience in BIOS with Intel platform development debug and engineering An advanced degree is preferred Additional qualifications include
Excellent problemsolving skills comfortable with the use of software tools to diagnose and debug BIOS firmware and software issues
Excellent knowledge of UEFI kernel architecture PEIDXEBDS dispatch flow EFI driver framework
Strong coding skill knowledge of MASM and C languages
Familiar with PC standards such as PCI SMBUS ACPI USB SATA PCI Express especially ACPI
Deep knowledge of PC architecture hardware software firmware and Windows OS
Excellent communication including spoken and written English and customer support skills
Strong interpersonal skills with proven track record of cross group collaboration
Presentation skills and the ability understand specify and communicate requirements to marketing and engineering teams
Ability to work in a highly ambiguous environment and to achieve high quality results
Ability to international and domestic travel and travel to customer facilities

The following qualifications would be added advantages
Experience in Mobile UEFI BIOS with Intel platform
Experience in EC firmware development
Work experience in BIOS vendor
Experience in UEFI source level debug
Knowledge of Python languages

Primary Location:

PRC, Shenzhen

有兴趣的朋友可以给我发邮件 2925059@qq.com

从 Core 的设计来看CPU 执行指令的流程

最近在看一些资料,这里从 Core 架构入手,研究CPU 是如何读取执行一条指令的。从下面可以看到 Core 架构是很老的设计了:

Intel P6以来核心架构及对应型号、芯片组一览表 【参考1】
Core 架构的指令流程【参考2】

之所以选择 Core 来进行查看最主要的原因是整体流程非常清晰。

首先是“Instruction Fetch and PreDecode” 取得指令,“这个阶段指令预解码单元会接收到从instruction cache或者instruction prefetch buffer发出的16字节,并执行以下任务:”

  • 解码出指令的长度
  • 解码出指令的所有前缀
  • 为指令打上标记(如“是分支 / is branch”)

之后放入 Instruction Queue 中。“Instruction Queue(IQ)最大的作用就是侦测并存储循环指令。IQ内提供了能存储小于18条指令的loop cache,如果loop stream detector(LSD)侦测到IQ内的指令为循环内的指令,则会把这些指令锁定下来,那么在循环期间就可以直接从IQ中获取指令到解码器,从而省去前面fetch以及predecode等工作。如此一来能很好地提高效率以及降低功耗。”

接下来是 Decode,这个阶段会将指令解释为微指令(μop),之前在 CISC 和 RISC 的介绍有提到过,Intel CPU 虽然是 CISC 架构,但是内部会将 CISC指令解释成为类似 RISC 的指令再进行执行。

Allocator/Renamer 是之前介绍过的寄存器重命名,Allocator 应该是为一些微指令(比如: load 和 store)创建 buffer。

Retirement (Reorder buffer, ROB) ,”主要用于记录μops的状态以及存储EU执行完成后返回的结果,然后按照in-order的顺序把执行结果写回寄存器,使得在用户层看来指令在以in-order的顺序执行,in-order写回的这一步被称为retirement。Core微处理器一共可以容纳96项μops。”

“Scheduler(RS)的目的是把μops分配到相应的execution port。为了实现这个目的,RS必须具备识别μop是否ready的能力。当μop的所有的源(source)都就位时则表明该μop为ready,可以让RS调度到execution unit执行。RS会根据Issue Port的可用情况以及已就绪μops的优先级来对μop进行调度。”【参考3】

接下来的指令会进入不同的 ALU 中进行执行,从图中可以看到这些ALU 相对独立,再在运行时可以并行进行,也正是因为这样的设计,保证了CPU的运行效率。

最终的运算结果放在L1D Cache 中。

上面的介绍大部分来自【参考2】,强烈推荐有兴趣的朋友阅读原文。最后再看一下最新的2个架构框图。分别是 Sandy Bridge 和 Icelake 的架构:

Sandy Bridge 的架构【参考4】
IceLake 的架构 【参考4】

有了前面的知识理解这两个并不困难。

参考:

1.https://www.sohu.com/a/245702002_467792 Intel P6以来核心架构及对应型号、芯片组一览表

2.http://www.qdpma.com/systemarchitecture/IntelMicroArchitectureDiagrams.html

3.https://www.cnblogs.com/TaigaCon/p/7678394.html Intel Core Microarchitecture Pipeline

4.Intel® 64 and IA-32 Architectures Optimization Reference Manual