调试小故事(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

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,
                     &amp;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 进行实验,在没有外围电容的情况下,这个芯片是能够正常工作的,确认之后再进行其他外围元件的焊接。

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