DS3231 2165/165/165 165:165:85 问题分析

最近制作的一个ESP32项目需要使用RTC,最先选中的是一个DS1307,经过实验发现有奇怪的问题:设定好时间之后经过一段时间会出现时间错乱的情况;无奈之下更换为 DS3231。

DS3231 模块

卖家关于这个模块介绍如下:

DS3231是低成本、高精度I2C实时时钟(RTC),具有集成的温补晶振(TCXO)和晶体。该器件包含电池输入端,断开主电源时仍可保持精确的计时。集成晶振提高了器件的长期精确度,并减少了生产线的元件数量。DS3231提供商用级和工业级温度范围,采用16引脚300mil的SO封装。

 RTC保存秒、分、时、星期、日期、月和年信息。少于31天的月份,将自动调整月末的日期,包括闰年的修正。时钟的工作格式可以是24小时或带/AM/PM指示的12小时格式。提供两个可设置的日历闹钟和一个可设置的方波输出。地址与数据通过I2C双向总线串行传输。

精密的、经过温度补偿的电压基准和比较器电路用来监视VCC状态,检测电源故障,提供复位输出,并在必要时自动切换到备份电源。另外,/RST监视引脚可以作为产生μP复位的手动输入。

除计时精度高之外,DS3231还具有一些其它功能,这些功能扩展了系统主机的附加功能和选择范围。该器件内部集成了一个非常精确的数字温度传感器,可通过I2C*接口对其进行访问(如同时间一样)。这个温度传感器的精度为±3°C。片上控制电路可实现自动电源检测,并管理主电源和备用电源(即低压电池)之间的电源切换。如果主电源掉电,该器件仍可继续提供精确的计时和温度,性能不受影响。当主电源重新加电或电压值返回到容许范围内时,片上复位功能可用来重新启动系统微处理器。

模块参数:

1.尺寸:38mm(长)*22mm(宽)*14mm(高)

2.重量:8g

3.工作电压:3.3–5.5V

4.时钟芯片:高精度时钟芯片DS3231

5.时钟精度:0-40℃范围内,精度2ppm,年误差约1分钟

6.带2个日历闹钟

7.可编程方波输出

8.实时时钟产生秒、分、时、星期、日期、月和年计时,并提供有效期到2100年的闰年补偿

9.芯片内部自带温度传感器,精度为±3℃

10.存储芯片:AT24C32(存储容量32K)

11.IIC总线接口,最高传输速度400KHz(工作电压为5V时)

12.可级联其它IIC设备,24C32地址可通过短路A0/A1/A2修改,默认地址为0x57

13.带CR2032电池,保证系统断电后,时钟任然正常走动

14.包装方式:单个防静电包装

接线说明(以Arduino uno r3为例):

SCL→A5

SDA→A4

VCC→5V

GND→GND

从网上看这个比前面那个口碑好很多,但是惊奇的发现经过一段时间运行同样会出现问题。典型问题如下,可以看到前一个时间尚且正常,随后就变成了 2165/165/165 165:165:85 这样的错误时间。

58:2d:34:3a:81:cf,2021/8/26 20:0:48,74.9,31.8

58:2d:34:3a:81:cf,2021/8/26 20:0:50,74.9,31.8

58:2d:34:3a:e6:d1,2021/8/26 20:0:51,77.1,31.8

58:2d:34:3a:e6:d1,2165/165/165 165:165:85,76.3,31.7

58:2d:34:3a:81:cf,2021/8/45 78:79:41,74.8,31.8

58:2d:34:3a:e6:d1,2021/8/45 78:79:41,75.7,31.7

58:2d:34:3a:81:cf,2021/8/45 78:79:41,74.8,31.7

58:2d:34:3a:81:cf,2021/8/45 78:79:43,74.8,31.8

58:2d:34:3a:81:cf,2021/8/45 78:79:45,74.8,31.8

连续试验了2个库都有同样的问题。想来应该不是软件的问题。另外,可以观察到出现错误之后 RTC 仍然会按照错误的时间继续输出,这个说明芯片仍然在工作。最终,我将目光投向了模块本身。前面提到过DS1307电池的问题,这次仍然从这里下手。在工作状态下,首先测量了一下电池电压,在3.3V 说明是电力充足的;之后,拆下来电池再次测量电池位(具体可以从正面下面的位置测量到)。惊奇的发现电压是 4.2V 左右。再接上电池之后发现这个位置虽然是 3.3V 左右,但是以每秒0.01v的速度在升高。经过一段时间的观察发现它能升高到 3.48V,仍然在不停的升高。这个看起来是模块在试图对电池充电。

DS3231 电池电压测量点

在英文网站上搜索发现,有人指出这个模块必须使用可充电纽扣电池:LIR2032,而这种电池的电压是 3.6v。我购买的模块也确实带有了一个这个型号的电池,但是目前已经鼓包了。查看模块的电路如下:

DS3231 电池充电电路

可以看到上面的 VCC 经过 R5 和一个二极管直接给电池供电,这样的话,如果使用3.3V对模块供电,那么出现在电池上的电压是3.3-0.7=2.6V 左右(电池低于2.6V时可以进行充电);但是如果使用5V 对模块供电的话,出现在电池上的压降将会时 5-0.7=4.3V 左右,这个电压远超过电池的最高电压。这就是为什么我看到电池两端电压缓慢上升的原因,也许是原配电池鼓包的原因。

确定这一点后,去掉下面 R5 处的电阻即可断开充电电路:

Rework R5

但是,测试结果仍然会出现错误。但是,如果硬件上有一个问题,那么很可能还有其他的问题。从上面的信息来看,问题发生很可能和 IIC 有关系。从 DS3231 上看过去 IIC 被拉到了 VCC 上。但是,我们的 VCC 是 5V。换句话说,这里出现在 IIC 上的是5V,而ESP32端是 3.3V 的引脚。因此,这里会出现电平不匹配的问题(5V供电的设备可以将 3.3V判定为1, 但是3.3V供电的设备无法正确判断5V信号)。

I2C 上拉

最终的解决方法是:使用 3.3V 作为RTC模块的供电。当然,如果使用3.3V进行供电的话,前面的 rework 也不是必须的动作了。

总结:这个模块的卖家说明是错误的,如果你使用GPIO 是 3.3V的单片机/SOC 作为主控,那么必须使用3.3V对DS3231模块供电才能保证工作。

Step to UEFI (233)屏幕分辨率研究

最近测试 (USH)UEFI Shell Helper 的时候发现一个奇怪的现象:直接启动内置的 Shell 时,会全屏打字显示;而启动 USH 上面的 Shell 后,显示字体非常小。具体照片如下,使用 HDMI 显示器,下图是正常情况:

显示器中分辨率正常,字体足够大

下面是从 USH 启动后的照片,可以看到居中,字体非常小:

分辨率很高,字很小

于是针对这个现象进行一番研究。首先,重新编译 BIOS ,替换它内置的 Shell.efi 为和 USH 相同的版本,测试显示仍然有这样的现象。之后,编译 DEBUG 版本的BIOS, 在串口 Log 中看到如下字样:

GraphicsConsole video resolution 1920 x 1200
Graphics - Mode 0, Column = 80, Row = 25
Graphics - Mode 1, Column = 80, Row = 50
Graphics - Mode 2, Column = 100, Row = 31
Graphics - Mode 3, Column = 240, Row = 63
……………..
GraphicsConsole video resolution 800 x 600
Graphics - Mode 0, Column = 80, Row = 25
Graphics - Mode 1, Column = 0, Row = 0
Graphics - Mode 2, Column = 100, Row = 31

虽然没有找到直接证据,但是感觉上问题和当前屏幕分辨率有关。从代码上上,上面的Log 来自于 edk2\MdeModulePkg\Universal\Console\GraphicsConsoleDxe 代码中。于是,首先编写一个查看当前系统 GRAPHICS_CONSOLE_DEV 的代码,这个结构体定义如下:

typedef struct
{
        UINTN                            Signature;
        EFI_GRAPHICS_OUTPUT_PROTOCOL     *GraphicsOutput;
        EFI_UGA_DRAW_PROTOCOL            *UgaDraw;
        EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL  SimpleTextOutput;
        EFI_SIMPLE_TEXT_OUTPUT_MODE      SimpleTextOutputMode;
        GRAPHICS_CONSOLE_MODE_DATA       *ModeData;
        EFI_GRAPHICS_OUTPUT_BLT_PIXEL    *LineBuffer;
} GRAPHICS_CONSOLE_DEV;

对于我们来说,首先枚举系统中的 EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL ,之后使用下面的 CR 定义即可找到GRAPHICS_CONSOLE_DEV 结构体,这些都是定义在 edk2\MdeModulePkg\Universal\Console\GraphicsConsoleDxe\GraphicsConsole.h 中的代码:

#define GRAPHICS_CONSOLE_CON_OUT_DEV_FROM_THIS(a) \
  CR (a, GRAPHICS_CONSOLE_DEV, SimpleTextOutput, GRAPHICS_CONSOLE_DEV_SIGNATURE)

具体代码如下:

Private = GRAPHICS_CONSOLE_CON_OUT_DEV_FROM_THIS (SimpleTextOutput);
                
if (Private->Signature != GRAPHICS_CONSOLE_DEV_SIGNATURE) {
                        continue;
                }

Print(L"Show ModeData:\n");
Print(L" Col      : %d\n",Private->ModeData->Columns);
Print(L" Row      : %d\n",Private->ModeData->Rows);
Print(L" DeltaX   : %d\n",Private->ModeData->DeltaX);
Print(L" DeltaY   : %d\n",Private->ModeData->DeltaY);
Print(L" GopWidth : %d\n",Private->ModeData->GopWidth);
Print(L" GopHeight: %d\n",Private->ModeData->GopHeight);
Print(L" GopMode  : %d\n",Private->ModeData->GopModeNumber);

这里可以看到内置 Shell 和启动USH 上的 Shell 会有一些差别,前者运行结果:

Show ModeData:
 Col      : 80
 Row      : 25
 DeltaX   : 80
 DeltaY   : 62
 GopWidth : 800
 GopHeight: 600
 GopMode  : 2

后者运行结果:

Show ModeData:
 Col      : 80
 Row      : 25
 DeltaX   : 640
 DeltaY   : 362
 GopWidth : 1920
 GopHeight: 1200
 GopMode  : 0

两者运行在不同的分辨率下。接下来的问题就是:当运行在 1920×1200 时,是否有机会再切成 800×600 的分辨率呢?

这里还要从 GraphicsConsole 代码入手。在 CheckModeSupported() 函数中,我们可以看到代码使用 GraphicsOutput->SetMode 进行分辨率的切换,于是我们照搬这个代码到我们的 Application 中,切换为 800×600:

        //
        // if not supporting current mode, try 800x600 which is required by UEFI/EFI spec
        //
        HorizontalResolution = 800;
        VerticalResolution   = 600;
        Status = CheckModeSupported (
                     Private->GraphicsOutput,
                     HorizontalResolution,
                     VerticalResolution,
                     &ModeNumber
                     );

运行结果分辨率看起来是正确的,但是内容偏于一隅:

都在一边

这时候我注意到前面还有两个参数DeltaX  和 DeltaY   ,屏幕内容显示的位置应该是这里决定的。查看代码,在InitializeGraphicsConsoleTextMode() 函数中,有计算这两个参数的代码如下:

  NewModeBuffer[ValidCount].DeltaX        = (HorizontalResolution - (NewModeBuffer[ValidCount].Columns * EFI_GLYPH_WIDTH)) >> 1;
  NewModeBuffer[ValidCount].DeltaY        = (VerticalResolution - (NewModeBuffer[ValidCount].Rows * EFI_GLYPH_HEIGHT)) >> 1;   

其中EFI_GLYPH_WIDTH 定义为 8,EFI_GLYPH_HEIGHT定义为 19。例如,当前如果是 800×600分辨率,那么

DeltaX = (800-(80*8))/2=80 就是前面 Private->ModeData->DeltaX 中给出的值。这里我猜测这样的设计是为了保证屏幕内容居中所以进行了这样的设定。但是DeltaX和DeltaY并不会因为切换分辨率而有所不同(屏幕分辨率是GraphicsOutput负责,字符显示由GRAPHICS_CONSOLE_DEV 负责)。所以,我们应该需要手工设定 DeltaX 和 DeltaY,然后再对  Protocol 进行 Reset。完整代码如下:

#include  <Uefi.h>
#include  <Library/UefiLib.h>
#include  <Library/ShellCEntryLib.h>
#include  <Protocol/SimpleFileSystem.h>
#include  <Library/UefiBootServicesTableLib.h> //global gST gBS gImageHandle
#include  <Library/ShellLib.h>
#include  <Library/MemoryAllocationLib.h>
#include  <Library/BaseMemoryLib.h>
#include  <Protocol/UgaDraw.h>
#include  <Library/DebugLib.h>
#include  <stdio.h>
#include  <stdlib.h>

//
// Device Structure
//
#define GRAPHICS_CONSOLE_DEV_SIGNATURE  SIGNATURE_32 ('g', 's', 't', 'o')

typedef struct
{
        UINTN   Columns;
        UINTN   Rows;
        INTN    DeltaX;
        INTN    DeltaY;
        UINT32  GopWidth;
        UINT32  GopHeight;
        UINT32  GopModeNumber;
} GRAPHICS_CONSOLE_MODE_DATA;

typedef struct
{
        UINTN                            Signature;
        EFI_GRAPHICS_OUTPUT_PROTOCOL     *GraphicsOutput;
        EFI_UGA_DRAW_PROTOCOL            *UgaDraw;
        EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL  SimpleTextOutput;
        EFI_SIMPLE_TEXT_OUTPUT_MODE      SimpleTextOutputMode;
        GRAPHICS_CONSOLE_MODE_DATA       *ModeData;
        EFI_GRAPHICS_OUTPUT_BLT_PIXEL    *LineBuffer;
} GRAPHICS_CONSOLE_DEV;

#define GRAPHICS_CONSOLE_CON_OUT_DEV_FROM_THIS(a) \
  CR (a, GRAPHICS_CONSOLE_DEV, SimpleTextOutput, GRAPHICS_CONSOLE_DEV_SIGNATURE)

// Include/Protocol/SimpleTextOut.h
EFI_GUID        gEfiSimpleTextOutProtocolGuid  = { 0x387477C2, 0x69C7, 0x11D2,
        { 0x8E, 0x39, 0x00, 0xA0, 0xC9, 0x69, 0x72, 0x3B }
};

/**
  Check if the current specific mode supported the user defined resolution
  for the Graphics Console device based on Graphics Output Protocol.

  If yes, set the graphic devcice's current mode to this specific mode.

  @param  GraphicsOutput        Graphics Output Protocol instance pointer.
  @param  HorizontalResolution  User defined horizontal resolution
  @param  VerticalResolution    User defined vertical resolution.
  @param  CurrentModeNumber     Current specific mode to be check.

  @retval EFI_SUCCESS       The mode is supported.
  @retval EFI_UNSUPPORTED   The specific mode is out of range of graphics
                            device supported.
  @retval other             The specific mode does not support user defined
                            resolution or failed to set the current mode to the
                            specific mode on graphics device.

**/
EFI_STATUS
CheckModeSupported (
  EFI_GRAPHICS_OUTPUT_PROTOCOL  *GraphicsOutput,
  IN  UINT32                    HorizontalResolution,
  IN  UINT32                    VerticalResolution,
  OUT UINT32                    *CurrentModeNumber
  )
{
  UINT32     ModeNumber;
  EFI_STATUS Status;
  UINTN      SizeOfInfo;
  EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *Info;
  UINT32     MaxMode;

  Status  = EFI_SUCCESS;
  MaxMode = GraphicsOutput->Mode->MaxMode;

  for (ModeNumber = 0; ModeNumber < MaxMode; ModeNumber++) {
    Status = GraphicsOutput->QueryMode (
                       GraphicsOutput,
                       ModeNumber,
                       &SizeOfInfo,
                       &Info
                       );
    if (!EFI_ERROR (Status)) {
      if ((Info->HorizontalResolution == HorizontalResolution) &&
          (Info->VerticalResolution == VerticalResolution)) {
        if ((GraphicsOutput->Mode->Info->HorizontalResolution == HorizontalResolution) &&
            (GraphicsOutput->Mode->Info->VerticalResolution == VerticalResolution)) {
          //
          // If video device has been set to this mode, we do not need to SetMode again
          //
          FreePool (Info);
          break;
        } else {
          Status = GraphicsOutput->SetMode (GraphicsOutput, ModeNumber);
          if (!EFI_ERROR (Status)) {
            FreePool (Info);
            break;
          }
        }
      }
      FreePool (Info);
    }
  }

  if (ModeNumber == GraphicsOutput->Mode->MaxMode) {
    Status = EFI_UNSUPPORTED;
  }

  *CurrentModeNumber = ModeNumber;
  return Status;
}

INTN
EFIAPI
main (
        IN UINTN Argc,
        IN CHAR16 **Argv
)
{
        UINTN                            NumHandles;
        EFI_STATUS                       Status;
        EFI_HANDLE                      *HandleBuffer;
        GRAPHICS_CONSOLE_DEV             *Private;
        UINTN                            Index;
        EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL  *SimpleTextOutput;
        UINT32                          HorizontalResolution;
        UINT32                          VerticalResolution;
        UINT32                               ModeNumber;
        EFI_GRAPHICS_OUTPUT_PROTOCOL_MODE    *Mode;        
        //
        // Locate all handles that are using the SFS protocol.
        //
        Status = gBS->LocateHandleBuffer(
                        ByProtocol,
                        &gEfiSimpleTextOutProtocolGuid,
                        NULL,
                        &NumHandles,
                        &HandleBuffer);
        if (EFI_ERROR(Status) != FALSE)
        {
                Print(L"failed to locate any handles using the EfiSimpleTextOutProtocol\n");
                goto CleanUp;
        }

        for (Index = 0; (Index < NumHandles); Index += 1)
        {
                Status = gBS->HandleProtocol(
                                HandleBuffer[Index],
                                &gEfiSimpleTextOutProtocolGuid,
                                (VOID**)&SimpleTextOutput);

                if (EFI_ERROR(Status))
                {
                        Print(L"Failed to locate SimpleTextOutProtocol.\n");
                        continue;
                }
                
                Private = GRAPHICS_CONSOLE_CON_OUT_DEV_FROM_THIS (SimpleTextOutput);
                
                if (Private->Signature != GRAPHICS_CONSOLE_DEV_SIGNATURE) {
                        continue;
                }
                
                Print(L"Show ModeData:\n");
                Print(L" Col      : %d\n",Private->ModeData->Columns);
                Print(L" Row      : %d\n",Private->ModeData->Rows);
                Print(L" DeltaX   : %d\n",Private->ModeData->DeltaX);
                Print(L" DeltaY   : %d\n",Private->ModeData->DeltaY);
                Print(L" GopWidth : %d\n",Private->ModeData->GopWidth);
                Print(L" GopHeight: %d\n",Private->ModeData->GopHeight);
                Print(L" GopMode  : %d\n",Private->ModeData->GopModeNumber);

                // Set new screen offset
                Private->ModeData->DeltaX=80;
                Private->ModeData->DeltaY=62;
                //
                // if not supporting current mode, try 800x600 which is required by UEFI/EFI spec
                //
                HorizontalResolution = 800;
                VerticalResolution   = 600;
                Status = CheckModeSupported (
                             Private->GraphicsOutput,
                             HorizontalResolution,
                             VerticalResolution,
                             &ModeNumber
                             );
                
                Mode = Private->GraphicsOutput->Mode;
                
                if (EFI_ERROR (Status) && Mode->MaxMode != 0) {
                  //
                  // Set default mode failed or device don't support default mode, then get the current mode information
                  //
                  HorizontalResolution = Mode->Info->HorizontalResolution;
                  VerticalResolution = Mode->Info->VerticalResolution;
                  ModeNumber = Mode->Mode;
                  Print(L" CheckModeSupported failed\n");
                } else {
                  Print(L" CheckModeSupported passed\n");
                }
                Private->SimpleTextOutput.Reset(&Private->SimpleTextOutput,TRUE);
        }

CleanUp:
        if (HandleBuffer != NULL)
        {
                FreePool(HandleBuffer);
        }

        return(0);
}

经过上述操作之后,可以满足要求,不过有时候并不太稳定需要多次执行。遇到同样问题的朋友可以试试。

完整代码和编译后的EFI 程序:

虽然没有找到从内置Shell 和U盘上 Shell 启动之后分辨率不同的原因,但是我们有了一个可以在 Shell 下切换分辨率的工具,这个问题也算有一个解决方法。

CH340 的替代者:CH343

南京沁恒的 CH340 是非常好用的USB 转串口芯片,在日常使用中完全可以替代FT232R。美中不足的是CH340 虽然在 Datasheet中标明可以支持2 000 000的高速波特率,但是在实际测试中这个波特率会有丢失数据的问题(该问题可以使用 LoopBack 的方式看到)。CH340C 和CH340B 同样都是内置晶振的,但是C 表现会比B 的好一点。我用逻辑分析仪确认过,问题发生在接收的时候,概率性丢失数据。理论上这种问题可以通过通讯协议来克服,但这样做会导致代码复杂性增高可靠性降低。

经过和沁恒的工程师交流,更新的 CH343(貌似是21年6月新出品的)能够支持2000000的波特率,于是尝试之。新型号基本特性和CH340 一样:


●全速 USB 设备接口,兼容 USB V2.0。
●内置固件,仿真标准串口,用于升级原串口外围设备,或者通过 USB 增加额外串口。
● 计算机端 Windows 操作系统下的串口应用程序完全兼容,无需修改。
● 支持免安装的操作系统内置 CDC 类驱动程序或者多功能高速率的 VCP 厂商驱动程序。
● 硬件全双工串口,内置独立的收发缓冲区,支持通讯波特率 50bps~6Mbps。
● 可选自动识别和动态自适应在 115200bps 及以下的常用通讯波特率。
● 串口支持 5、6、7 或者 8 个数据位,支持奇校验、偶校验、空白、标志以及无校验。
● 支持常用的 MODEM 联络信号 RTS、DTR、DCD、RI、DSR、CTS。
● 支持 CTS 和 RTS 硬件自动流控。
● 支持半双工,提供正在发送状态 TNOW 支持 RS485 切换。
● 通过外加电平转换器件,支持 RS232 接口。
● USB 端支持 5V 电源电压和 3.3V 电源电压。
● 串口 I/O 独立供电,支持 5V、3.3V、2.5V、1.8V 电源电压。
● 内置上电复位,内置时钟,无需外部晶振。
● CH343P 内置 EEPROM,可配置芯片 VID、PID、最大电流值、厂商和产品信息字符串等参数。
● 芯片内置 Unique ID(USB Serial Number)。
● 提供 SOP16 和 ESSOP10 及 QFN16 无铅封装,兼容 RoHS。

CH343 这个型号有三种封装,基本功能相同。其中的 CH343P 还提供了修改定制VID,PID 以及其他信息的功能(对标 CH340B)。

CH343 三种型号

这次尝试自己制作了一个  CH343P 的开发板,PCB 如下:

设计的 PCB 验证板

焊接之后发现无法工作,经过了3天的调试最终成功。总结如下:

  1. 务必准备热风枪,电烙铁焊接可靠性不强;
  2. 要想使这款芯片工作,只需要下面4个电设置正确即可
芯片的4个电

首先 VBUS, 需要接到USB接口上面的 VCC(5V);其次,VDD 是芯片供电输入位置,需要输入5V;接下来V3 是芯片内部将5V转为3.3V输出的Pin;最后 VIO 是用来决定UART 信号电平的输入Pin,如果这里是 3.3V 那么 TXD RXD 将会是3.3V,如果是5V 那么TXD RXD 将会是5V 电平。在DetaSheet中有如下描述:

特别注意:如果VIO给的是 3.3V ,而其他 VUART 送入了 5V,那么你的芯片就会损坏(我因为这个原因损坏了2个芯片)。

TXD RXD 工作电平为3.3V 最稳妥的电路如下:

  1. VBUS 和 VDD5 都使用 USB接口上的5V 供电
  2. V3 和 VIO 在一起,这样TXD RXD都是3.3V
电路

当然更稳妥的是跟着参考电路设计(官网可以下载)

官方电路

焊接时建议先焊接 USB 接口,然后焊接上这个芯片,焊接完成后即可插入PC 进行实验,在没有外围电容的情况下,这个芯片是能够正常工作的,确认之后再进行其他外围元件的焊接。

最终调试成功的开发板(上面有一粒大米用于比较尺寸)

CATERR 介绍

CATERR# 是 CPU 上的一个引脚,当CPU 有严重错误发生时,这个引脚会拉低(#表示低有效)。它在所有的Intel CPU 上都有。特别注意:这里是 OD 输出,是没法输出高电平,想要输出高电平,必须外部再接一个上拉电阻(pull-up resistor)。换句话说,如果测量这里为高或者低,务必记得在外面连接一个上拉电阻才能得到正确值【参考3】.

CATERR# 来自【参考1】

Intel 错误分类

首先是两大类:可以检测到的(Detected) 和 不可以检测到的(Undetected)。其中的 Undetected 是非常重要的,因为这种错误无法检测到的错误是没有办法捕捉到和处理的。进一步分为影响不大的 (Benign)和Critical(严重的,这种又被称作 Silent Data Corruption缩写 SDC)。作为系统设计者,必须努力降低这种情况的发生率。

更多的,我们需要关注Detected 这一类。其中又分作可纠正(Corrected)错误(例如,ECC 内存发现了错误,然后可以纠正为正确值)和不可纠正(Uncorrected)错误。例如,我们经常看到的蓝屏就是可以检测不可纠正错误。再进一步,不可纠正(Uncorrected)错误又分作可检测但是不可修正错误(DUE)和可检测不可纠正但可恢复错误(UCR,比如在从U盘Copy 数据到硬盘时,发生了错误,这个错误就是可以检测不可纠正,但是再次尝试读取还可以继续Copy,就是 UCR错误)。

显而易见,我们最大的敌人是DUE。

Intel 内置了 MCA(Machine Check Architecture)来帮助诊断DUE。这也是为什么在碰到稀奇古怪的问题时需要使用 CCA/DCI 的原因:CPU 死翘翘的,只能从不依赖CPU 的路径取得当前的错误。MCA提供了检测和记录:系统总线错误,内存错误,奇偶校验错误,Cache错误和TLB 错误等等。它是通过CPU内部的一组专用的MSR寄存器来实现的。例如:下面就是一组MCA 的 MSR 寄存器:

这里有一个MCA 应用的典型例子【参考4】。当问题发生的时候CATERR#会拉低,进一步检查出现的错误是ROB Timeout(这个有时候也被称作  “three-strike timeout”。3 strike 翻译为“三振出局” ,通常出现问题的时候CPU会进行多次尝试,尝试都失败后就放弃之)。这里提到的 ROB 在前面有介绍过,作用是:“Retirement (Reorder buffer, ROB) ,”主要用于记录μops的状态以及存储EU执行完成后返回的结果,然后按照in-order的顺序把执行结果写回寄存器”。ROB Timeout 的意思是有正在执行的指令超时。所有的指令发送给下一层的 Scheduler 来分配执行的时候会进行记录,如果15秒之后无法得到结果,会报告这个错误。

很明显这样的错误更容易出现在内存读写,IO读写等等和外围设备打交道的情景中。【参考4】提到的错误是发生在内存读写中,发生问题的内存地址是一个 PCIE设备映射的位置,最终配合PCIE 逻辑分析仪找到了原因。

作为BIOS工程师,大部分工作是在诊断定位问题,真正BIOS本身的问题少之又少,类似上面这种问题,如果能确定是某个PICE 的问题,下面直接交给对应工程师或者联系厂商就可以了。

参考:

  1. https://www.intel.com/content/dam/www/public/us/en/documents/datasheets/10th-gen-core-families-datasheet-vol-1-datasheet.pdf
  2. https://www.intel.com/content/dam/www/public/us/en/documents/research/2012-vol16-iss-2-intel-technology-journal.pdf
  3. https://blog.csdn.net/zwl1584671413/article/details/83095044?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522163100098916780265447231%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=163100098916780265447231&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~first_rank_v2~rank_v29-1-83095044.pc_v2_rank_blog_default&utm_term=OD&spm=1018.2226.3001.4450 单片机I/O口推挽输出与开漏输出的区别(open-drain与push-pull)
  4. https://www.intel.com/content/dam/www/public/us/en/documents/white-papers/rob-timeout-debug-guide-paper.pdf

EDK2 202108 来了

edk2-stable202108 在  https://github.com/tianocore/edk2/releases/tag/edk2-stable202108

下载解压 stable202108.tar.gz 之后,尝试编译自带的模拟环境:

  1. edksetup.bat
  2. build -a X64 -p EmulatorPkg\EmulatorPkg.dsc

会遇到下面的错误

stable202108 报错1

错误的原因是无法找到 Brotli相关的内容。解决方法是下载 submodule-MdeModulePkg-Library-BrotliCustomDecompressLib-brotli.zip 将解压后的内容放到\MdeModulePkg\Library 目录下。

再次编译遇到下面的错误:

        "C:\Program Files (x86)\Microsoft Visual Studio 14.0\Vc\bin\x86_amd64\cl.exe" /showIncludes /nologo /E /TC /DVFRCOMPILE /FIFileExplorerLibStrDefs.h /Ic:\buildbs\stable202108\MdeModulePkg\Library\FileExplorerLib  /Ic:\buildbs\stable202108\Build\EmulatorX64\DEBUG_VS2015x86\X64\MdeModulePkg\Library\FileExplorerLib\FileExplorerLib\DEBUG  /Ic:\buildbs\stable202108\MdePkg  /Ic:\buildbs\stable202108\MdePkg\Include  /Ic:\buildbs\stable202108\MdePkg\Test\UnitTest\Include  /Ic:\buildbs\stable202108\MdePkg\Include\X64  /Ic:\buildbs\stable202108\MdeModulePkg  /Ic:\buildbs\stable202108\MdeModulePkg\Include  /Ic:\buildbs\stable202108\MdeModulePkg\Library\BrotliCustomDecompressLib\brotli\c\include c:\buildbs\stable202108\MdeModulePkg\Library\FileExplorerLib\FileExplorerVfr.vfr > c:\buildbs\stable202108\Build\EmulatorX64\DEBUG_VS2015x86\X64\MdeModulePkg\Library\FileExplorerLib\FileExplorerLib\OUTPUT\FileExplorerVfr.i
BootManagerVfr.Vfr
DeviceManagerVfr.Vfr
'VfrCompile' is not recognized as an internal or external command,
operable program or batch file.

查了一下,这个错误应该是 VfrCompile 这个工具没有编译为 EXE 导致的。所以我们需要先编译准备好 Windows 下的Build工具。命令:

edksetup.bat Rebuild

错误如下:

Microsoft (R) Program Maintenance Utility Version 14.00.24210.0
Copyright (C) Microsoft Corporation.  All rights reserved.

        cl.exe -c  /nologo /Zi /c /O2 /MT /W4 /WX /D _CRT_SECURE_NO_DEPRECATE /D _CRT_NONSTDC_NO_DEPRECATE /W2 -I .\brotli\c\include  -I . -I C:\BuildBs\stable202108\BaseTools\Source\C\Include -I C:\BuildBs\stable202108\BaseTools\Source\C\Include\Ia32 -I C:\BuildBs\stable202108\BaseTools\Source\C\Common BrotliCompress.c -FoBrotliCompress.obj
cl : Command line warning D9025 : overriding '/W4' with '/W2'
BrotliCompress.c
BrotliCompress.c(20): fatal error C1083: Cannot open include file: './brotli/c/common/constants.h': No such file or directory
NMAKE : fatal error U1077: '"C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\BIN\cl.exe"' : return code '0x2'
Stop.

NMAKE : fatal error U1077: 'if' : return code '0x1'
Stop.
NMAKE : fatal error U1077: 'if' : return code '0x1'
Stop.

应该是缺少BrotliCompress的代码导致的,下载 submodule-BaseTools-Source-C-BrotliCompress-brotli.zip,将内容解压到 \BaseTools\Source\C\BrotliCompress\brotli 中。

再次编译能够正常生成build工具:

stable202108 编译 BaseTools

此外,再次强调必须使用 x86 编译窗口,我是用的是VS2015。

接下来再次编译模拟工具,成功:

stable202108 编译成功

运行  \Build\EmulatorX64\DEBUG_VS2015x86\X64\WinHost.exe

stable202108 模拟器

之后测试编译支持SecureBoot 的OVMF BIOS:

build -a X64 -p OvmfPkg\OvmfPkgx64.dsc -D SECURE_BOOT_ENABLE=TRUE

收到错误提示如下:

Processing meta-data .
Architecture(s)  = X64
Build target     = DEBUG
Toolchain        = VS2015x86

Active Platform          = c:\buildbs\stable202108\OvmfPkg\OvmfPkgX64.dsc
...

build.py...
c:\buildbs\stable202108\CryptoPkg\Library\OpensslLib\OpensslLibCrypto.inf(26): error 000E: File/directory not found in workspace
        c:\buildbs\stable202108\CryptoPkg\Library\OpensslLib\openssl\e_os.h

和前面的方法一样,补充 submodule-CryptoPkg-Library-OpensslLib-openssl.zip 文件到

\CryptoPkg\Library\OpensslLib\openssl 目录下。,

之后即可正常编译。

本问题到的完整编译环境已经打包,可以在 https://pan.baidu.com/s/1pqD3XYAzrZbExRQALxs4OQ 提取码: tuqy  下载。

寄存器重命名(register renaming)

寄存器重命名是计算机CPU的微体系结构(Microarchitecture)中的一种技术,避免了机器指令或者微指令(μop)不必要的顺序化执行,从而提高了处理器的指令级并行的能力。【参考1】

例如:

mov eax, [mem1]
imul eax, 6
mov [mem2], eax
mov eax, [mem3]
add eax, 2
mov [mem4], eax

这段代码看起来是依赖于 EAX 的计算结果,但是如果将最后三条指令中的 EAX 替换为其他寄存器,就可以清楚的看到代码是在执行两个独立的操作:[MEM1]乘以6结果保存在[MEM2], 以及[MEM3]加上2之后存在[MEM4]中。就是说,这两个操作完全可以并行执行。处理器会自动给最后三条指令分配一个另外的寄存器,这样两个运算可以同时进行。

参考:

1.https://baike.baidu.com/item/%E5%AF%84%E5%AD%98%E5%99%A8%E9%87%8D%E5%91%BD%E5%90%8D/10927257?fr=aladdin

好用的 ESP32 DS1307 库

最近需要实验使用 DS1307 作为 RTC,惊奇的发现很多库都是为标准 Arduino 开发的,因此可能遇到使用 ESP32 不支持的寄存器,或者 TimeLib.h 没有定义的情况。

经过一段时间的探索,找到了来自下面这个网址的库

https://www.elecrow.com/wiki/index.php?title=Tiny_RTC

经过实验(在 DFRobot FireBeetle上),可以正常工作。

代码如下:
#include <Wire.h>
#include "RTClib.h"
RTC_DS1307 RTC;

void setup () {
    Serial.begin(9600);
    Wire.begin();
    RTC.begin();
  if (! RTC.isrunning()) {
    Serial.println("RTC is NOT running!");
    // following line sets the RTC to the date & time this sketch was compiled
    RTC.adjust(DateTime(__DATE__, __TIME__));
  }
}
void loop () {
    DateTime now = RTC.now(); 
    Serial.print(now.year(), DEC);
    Serial.print('/');
    Serial.print(now.month(), DEC);
    Serial.print('/');
    Serial.print(now.day(), DEC);
    Serial.print(' ');
    Serial.print(now.hour(), DEC);
    Serial.print(':');
    Serial.print(now.minute(), DEC);
    Serial.print(':');
    Serial.print(now.second(), DEC);
    Serial.println(); 
    delay(1000);
}
DS1307 取得时间

有需要的朋友可以在这里:

乱序执行 (out-of-order execution)

乱序执行(out-of-order execution)是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理的技术。比方Core乱序执行引擎说程序某一段有7条指令,此时CPU将根据各单元电路的空闲状态和各指令能否提前执行的具体情况分析后,将能提前执行的指令立即发送给相应电路执行。

这好比请A、B、C三个名人为晚会题写横幅“春节联欢晚会”六个大字,每人各写两个字。如果这时在一张大纸上按顺序由A写好”春节”后再交给B写”联欢”,然后再由C写”晚会”,那么这样在A写的时候,B和C必须等待,而在B写的时候C仍然要等待而A已经没事了。但如果采用三个人分别用三张纸同时写的做法, 那么B和C都不必须等待就可以同时各写各的了,甚至C和B还可以比A先写好也没关系(就象乱序执行),但当他们都写完后就必须重新在横幅上(自然可以由别人做,就象CPU中乱序执行后的重新排列单元)按”春节联欢晚会”的顺序排好才能挂出去。【参考1】

从Intel 第六代 CPU(Pentuim Pro) 开始, 引入了乱序执行的功能。通过前面的介绍也可以看出来 CPU 需要确定后面要执行的指令并不依赖于前面的指令。比如:计算 a=1+2, b= a+1, 这种情况是无法先进行 b=a+1 运算的。这种依赖被称作依赖链。

首先,处理器将指令转为微指令(μop)。比如: ADD EAX,EBX 这样的只对应一个μop;但是像 ADD EAX,[MEM1] 会生成2个μop:一条指令是从内存读取数值到寄存器,另外一条是将保存内存值的寄存器和 EAX 相加;再例如 ADD [MEM1],EAX 会生成3个μop:第一个是从内存取值,一个是进行相加,最后一个是将结果传输到内存中。这样做的好处是,拆分之后的μop可以进行乱序执行:

例如:

mov eax,[mem1]
imul eax,5
add eax[mem2]
mov [mem3],eax

这里 ADD EAX,[MEM2] 拆分为2条μop。这样计算 imul eax, 5 的时候可以同时到内存中读取[MEM2]。如果Cache 中没有数据,那么处理器读取[MEM1]之后会马上进行读取[MEM2]的操作。

再比如堆栈操作指令拆分为μop之后运行会更有效率。例如:

push eax
call func

如果没有将 PUSH 拆分为2条μop的话,因为 CALL 操作需要依赖于 ESP ,所以 CALL 需要等待 PUSH EAX 之后才能执行。如果将 PUSH eax 指令拆分为2个μop: SUB ESP,4 和 MOV [ESP],EAX。 那么 SUB ESP,4 可以在 EAX 还没有赋值之前执行。这样就解除了CALL 指令的路径依赖。可见在μop的帮助下,堆栈的操作能够节省时间。【参考2】

参考:

1.https://baike.baidu.com/item/%E4%B9%B1%E5%BA%8F%E6%89%A7%E8%A1%8C/4944129?fr=aladdin

2.https://www.agner.org/optimize/microarchitecture.pdf

CISC 和 RISC

计算机指令就是指挥机器工作的指示和命令,程序就是一系列按一定顺序排列的指令,执行程序的过程就是计算机的工作过程。指令集,就是CPU中用来计算和控制计算机系统的一套指令的集合,而每一种新型的CPU在设计时就规定了一系列与其他硬件电路相配合的指令系统。比如:日常生活中,“去吃饭”这样的命令可以看作是简单的指令。根据我的观察,男人和女人在指令集上是存在差别的。男人理解的指令集通常只是女人指令集的子集。譬如说,女人对于“洗衣服”指令的理解是包括“将衣物放入洗衣机启动洗涤”,“洗衣完成后晾衣服”以及“晾干后收起来”;但是对于男人来说“洗衣服”指令只是 “将衣物放入洗衣机启动洗涤” 的含义。

指令集的先进与否,也关系到CPU的性能发挥,它也是CPU性能体现的一个重要标志。每款CPU在设计时就规定了一系列与其硬件电路相配合的指令系统。指令的强弱也是CPU的重要指标,指令集是提高微处理器效率的最有效的工具之一。从现阶段的主流体系结构讲,指令集可分为CISC和RISC两种。

CISC是“复杂指令集计算机”的缩写:Complex Instruction Set Computer。我们日常使用的 X86 就是 CISC 的典型代表。

计算机处理器包含有实现各种功能的指令或微指令,指令集越丰富,为微处理器编写程序就越容易,但是丰富的微指令集会影响其性能。复杂指令集计算机(CISC)体系结构的设计策略是使用大量的指令,包括复杂指令。与其他设计相比,在CISC中进行程序设计要比在其他设计中容易,因为每一项简单或复杂的任务都有一条对应的指令。程序设计者不需要写一大堆指令去完成一项复杂的任务。 但指令集的复杂性使得CPU和控制单元的电路非常复杂。[1]

RISC是“精简指令集计算机”的缩写:Reduced Instruction Set Computing RISC)。RISC的指令系统相对简单,它只要求硬件执行很有限且最常用的那部分指令,大部分复杂的操作则使用成熟的编译技术,由简单指令合成。RISC结构采用精简的,长短划一的指令集,使大多数的操作获得了尽可能高的效率。某些在传统结构中要用多周期指令实现的操作,在RISC结构中,通过机器语言编程,就代之以多条单周期指令了。【参考1】

接下来用一个例子来说明二者的区别。

假设我们有一个这样的计算机:内存是一个 6×4 的数组, 有A-F 6个寄存器。

对于 CISC 来说,如果想完成内存中2个数值的相乘的操作,可以直接使用下面的指令:

MULT 2:3,5:2

这里的 MULT 就是一个“复杂指令”。 进一步,如果设定变量 x 为内存 2:3 中的值。 y 为内存 5:2 的值,上面的指令就可以理解为C语言的 x=x*y。

如果用 RISC 来完成,那么需要写成如下的指令:

LOAD A, 2:3
LOAD B, 5:2
PROD A, B
STORE 2:3, A

从这个结果上来看,RISC 效率更低,因为需要执行更多的代码,使用更多的内存。但是实际上 RISC 的指令通常只需要1个机器周期来完成,所以整体上消耗时间和CISC 的 MULT 指令相同,另外因为 RISC 机器码指令长度相同,执行时只需要一个机器周期所以更适合流水线方式执行(我的理解是方便硬件预读取和判断)。【参考2】

虽然 Intel X86 CPU 是 CISC 的典型代表,但是它的内部也存在RISC 的设计概念。所有的操作都会被分解为微指令再执行。微指令是典型的 CISC 的指令。

微指令(英语:microcode,μop),还有翻译为微码或者微操作,为了避免歧义,后面全部使用 μop,是在CISC结构下,运行一些功能复杂的指令时,所分解一系列相对简单的指令。相关的概念最早在1947年开始出现。
微指令的作用是将机器指令与相关的电路实现分离,这样一来机器指令可以更自由的进行设计与修改,而不用考虑到实际的电路架构。与其他方式比较起来,使用微指令架构可以在降低电路复杂度的同时,建构出复杂的多步骤机器指令。撰写微指令一般称为微程序设计(microprogramming),而特定架构下的处理器实做中微指令有时会称为微程序(microprogram)。


现代的微指令通常由CPU工程师在设计阶段编写,并且存储在只读内存(ROM, read-only-memory)或可编程逻辑数组(PLA, programmable logic array)中。然而有些机器会将微指令存储在静态随机存取内存(SRAM)或是闪存(flash memory)中。它通常对普通程序员甚至是汇编语言程序员来说是不可见的,也是无法修改的。与机器指令不同的是,机器指令必须在一系列不同的处理器之间维持兼容性,而微指令只设计成在特定的电路架构下运行,成为特定处理器设计的一部分。【参考3】

参考:
1.https://baike.baidu.com/item/%E7%B2%BE%E7%AE%80%E6%8C%87%E4%BB%A4%E9%9B%86%E8%AE%A1%E7%AE%97%E6%9C%BA/661859?fromtitle=risc&fromid=62696#viewPageContent

2.https://cs.stanford.edu/people/eroberts/courses/soco/projects/risc/risccisc/

3.https://baike.baidu.com/item/%E5%BE%AE%E7%A0%81/10708310?fr=aladdin

3.http://www.enroo.com/support/category1/dpjrmzs/78378500.html 什么是RISC架构?RISC架构的优点与缺点

4.http://www.enroo.com/support/category1/dpjrmzs/38700314.html 什么是CISC体系结构?CISC架构的优点与缺点

5.https://teachcomputerscience.com/risc-and-cisc-processors/ RISC and CISC Processors

6.https://www.zhihu.com/question/404743266/answer/1321179351 这里还有一个 X86 为什么选择 CISC 的历史。

CISC诞生的原因也很简单,第一,当时的主流看法是设计硬件比设计编译器简单;第二,而且当时的内存很贵,以8086开始设计的1976年来看,当时4KB内存需要159美元,8KB内存需要250美元;第三,当时的内存速度很慢,而寄存器价格上天,8086只有约20000个晶体管,而寄存器为了存储16bit数据就花费了至少256个晶体管。

8086的祖宗8008只有3500个晶体管,配合0.5Mhz的时钟频率,连寄存器-寄存器的复制都需要20us,更别说当时的内存速度了。

8008时期的DRAM每字节高达2.52美元,实现一个完整的系统就需要几千字节,基于各种原因,微架构工程师只能让处理器在使用同样的资源下尽可能的多干一些活,让一个指令干完全部工作,在RISC出来之后这种方式就被叫做复杂指令集了(CISC)

在70年代后期,随着编译器的普及和汇编的减少,正交寻址几乎被程序员忽略,虽然当时的编译器不能完全利用CISC处理器的优势,但是显然历史的车轮是不会停下的,于是乎正交寻址变得更加没用了。

实际上随着集成电路的发展,一些复杂指令集相比一系列简单的指令集更慢,因为芯片越来越复杂,但是设计者显然没有时间针对几百条指令的每一条都进行优化。几乎就在同时,得益于半导体工艺的进步,微处理器的运行速度变得比内存更快,而且可以预见的是在未来这个差距会变得越来越大,因此就需要腾出空间去安排更多的寄存器和缓存。