UEFI TIPS: 在一个程序中启动另外一个程序

这里提供一个在一个EFI程序中启动另外一个EFI 的例子,没有使用 UEFI Shell API ,放置在 ESP 分区后,可以启动当前的 Windows。

#include <Uefi.h>
#include <Library/UefiLib.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/DevicePathLib.h>
#include <Library/MemoryAllocationLib.h>
#include <Library/PrintLib.h>
#include <Protocol/LoadedImage.h>
#include <Protocol/SimpleFileSystem.h>
#include <Guid/FileInfo.h>

/**
 * 启动指定路径的 EFI 应用程序
 */
EFI_STATUS
StartEfiApplication (
  IN EFI_HANDLE        ParentImageHandle,
  IN CHAR16           *ApplicationPath
  )
{
    EFI_STATUS                      Status;
    EFI_HANDLE                      ChildImageHandle;
    EFI_DEVICE_PATH_PROTOCOL        *DevicePath;
    EFI_LOADED_IMAGE_PROTOCOL       *ParentLoadedImage;
    UINTN                           ExitDataSize;
    CHAR16                          *ExitData;

    Print(L"=== Starting EFI Application: %s ===\n", ApplicationPath);

    // 步骤 1: 获取父镜像的 LoadedImage 协议
    Status = gBS->HandleProtocol(
        ParentImageHandle,
        &gEfiLoadedImageProtocolGuid,
        (VOID**)&ParentLoadedImage
    );
    if (EFI_ERROR(Status)) {
        Print(L"ERROR: Failed to get parent LoadedImage protocol: %r\n", Status);
        return Status;
    }
    Print(L"SUCCESS: Got parent LoadedImage protocol\n");

    // 步骤 2: 构建目标应用的设备路径
    DevicePath = FileDevicePath(ParentLoadedImage->DeviceHandle, ApplicationPath);
    if (DevicePath == NULL) {
        Print(L"ERROR: Failed to create device path for %s\n", ApplicationPath);
        return EFI_OUT_OF_RESOURCES;
    }
    Print(L"SUCCESS: Created device path\n");

    // 步骤 3: 加载目标镜像
    Status = gBS->LoadImage(
        FALSE,                  // BootPolicy - FALSE 表示不是启动策略
        ParentImageHandle,      // ParentImageHandle - 父镜像句柄
        DevicePath,             // DevicePath - 目标文件的设备路径
        NULL,                   // SourceBuffer - NULL 表示从设备路径加载
        0,                      // SourceSize - 0 表示从设备路径加载
        &ChildImageHandle       // ImageHandle - 返回的子镜像句柄
    );

    // 释放设备路径内存
    FreePool(DevicePath);

    if (EFI_ERROR(Status)) {
        Print(L"ERROR: LoadImage failed: %r\n", Status);
        return Status;
    }
    Print(L"SUCCESS: Image loaded successfully, Handle = 0x%lx\n", (UINTN)ChildImageHandle);

    // 步骤 4: 启动镜像
    Print(L"Starting image...\n");
    Status = gBS->StartImage(
        ChildImageHandle,       // ImageHandle - 要启动的镜像句柄
        &ExitDataSize,          // ExitDataSize - 返回退出数据大小
        &ExitData               // ExitData - 返回退出数据
    );

    // 步骤 5: 处理启动结果
    if (EFI_ERROR(Status)) {
        Print(L"ERROR: StartImage failed: %r\n", Status);
        
        // 如果有退出数据,显示它
        if (ExitData != NULL && ExitDataSize > 0) {
            Print(L"Exit Data Size: %d bytes\n", ExitDataSize);
            Print(L"Exit Data: %s\n", ExitData);
            
            // 释放退出数据内存
            gBS->FreePool(ExitData);
        }
    } else {
        Print(L"SUCCESS: Image started and returned: %r\n", Status);
        
        // 处理正常退出的数据
        if (ExitData != NULL && ExitDataSize > 0) {
            Print(L"Application returned data: %s\n", ExitData);
            gBS->FreePool(ExitData);
        }
    }

    // 步骤 6: 卸载镜像(如果需要)
    Print(L"Unloading image...\n");
    gBS->UnloadImage(ChildImageHandle);

    Print(L"=== Application execution completed ===\n\n");
    return Status;
}

/**
 * 检查文件是否存在
 */
EFI_STATUS
CheckFileExists (
  IN EFI_HANDLE     DeviceHandle,
  IN CHAR16        *FilePath
  )
{
    EFI_STATUS                      Status;
    EFI_SIMPLE_FILE_SYSTEM_PROTOCOL *FileSystem;
    EFI_FILE_PROTOCOL               *Root;
    EFI_FILE_PROTOCOL               *File;

    // 获取文件系统协议
    Status = gBS->HandleProtocol(
        DeviceHandle,
        &gEfiSimpleFileSystemProtocolGuid,
        (VOID**)&FileSystem
    );
    if (EFI_ERROR(Status)) {
        return Status;
    }

    // 打开根目录
    Status = FileSystem->OpenVolume(FileSystem, &Root);
    if (EFI_ERROR(Status)) {
        return Status;
    }

    // 尝试打开目标文件
    Status = Root->Open(
        Root,
        &File,
        FilePath,
        EFI_FILE_MODE_READ,
        0
    );

    if (!EFI_ERROR(Status)) {
        Print(L"File exists: %s\n", FilePath);
        File->Close(File);
    } else {
        Print(L"File not found: %s (Status: %r)\n", FilePath, Status);
    }

    Root->Close(Root);
    return Status;
}

/**
 * 主入口函数
 */
EFI_STATUS
EFIAPI
UefiMain (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
    EFI_STATUS                Status;
    EFI_LOADED_IMAGE_PROTOCOL *LoadedImage;
    EFI_INPUT_KEY             Key;

    // 清屏
    gST->ConOut->ClearScreen(gST->ConOut);
    
    Print(L"UEFI StartImage Example Application\n");
    Print(L"====================================\n\n");

    // 获取当前镜像信息
    Status = gBS->HandleProtocol(
        ImageHandle,
        &gEfiLoadedImageProtocolGuid,
        (VOID**)&LoadedImage
    );
    if (EFI_ERROR(Status)) {
        Print(L"Failed to get LoadedImage protocol: %r\n", Status);
        return Status;
    }

    // 示例 : 启动 Windows Boot Manager
    Print(L"Example 1: Starting Windows Boot Manager\n");
    CheckFileExists(LoadedImage->DeviceHandle, L"EFI\\Boot\\bootx64.efi");
    Status = StartEfiApplication(ImageHandle, L"EFI\\Boot\\bootx64.efi");
    Print(L"Windows Boot Manager result: %r\n\n", Status);

    // 等待用户按键
    Print(L"Press any key to exit...\n");
    gST->ConIn->Reset(gST->ConIn, FALSE);
    while (gST->ConIn->ReadKeyStroke(gST->ConIn, &Key) == EFI_NOT_READY) {
        gBS->Stall(10000); // 等待 10ms
    }

    return EFI_SUCCESS;
}
[Defines]
  INF_VERSION                    = 0x00010006
  BASE_NAME                      = sat
  FILE_GUID                      = 4ea97c46-7491-2025-1125-747010f3ce5f
  MODULE_TYPE                    = UEFI_APPLICATION
  VERSION_STRING                 = 0.1
  ENTRY_POINT                    = UefiMain

#   
#  VALID_ARCHITECTURES           = IA32 X64 IPF
#

[Sources]
  StartImageTest.c

[Packages]
  MdePkg/MdePkg.dec

[LibraryClasses]
  UefiApplicationEntryPoint
  UefiLib
  UefiBootServicesTableLib
  
[Protocols]
  gEfiLoadedImageProtocolGuid
  gEfiSimpleFileSystemProtocolGuid
  gEfiSimpleTextInProtocolGuid
  gEfiSimpleTextOutProtocolGuid

  
[BuildOptions]

[Guids]

SMBIOS 2.X 和 3.X 区别

今天偶然发现 SMBIOS 2.X 和 3.X 存在一些差别,在处理的时候代码需要不同对待。

1. 入口点结构 (Entry Point Structure)定义的差异:

SMBIOS 2.X Entry Point

typedef struct {
  UINT8   AnchorString[4];           // "_SM_"
  UINT8   EntryPointStructureChecksum;
  UINT8   EntryPointLength;          // 0x1F
  UINT8   MajorVersion;
  UINT8   MinorVersion;
  UINT16  MaxStructureSize;
  UINT8   EntryPointRevision;
  UINT8   FormattedArea[5];
  UINT8   IntermediateAnchorString[5]; // "_DMI_"
  UINT8   IntermediateChecksum;
  UINT16  TableLength;               // 表长度
  UINT32  TableAddress;              // 32位表地址
  UINT16  NumberOfSmbiosStructures;
  UINT8   SmbiosBcdRevision;
} SMBIOS_TABLE_ENTRY_POINT;

SMBIOS 3.X Entry Point

typedef struct {
  UINT8   AnchorString[5];           // "_SM3_"
  UINT8   EntryPointStructureChecksum;
  UINT8   EntryPointLength;          // 0x18
  UINT8   MajorVersion;
  UINT8   MinorVersion;
  UINT8   DocRev;
  UINT8   EntryPointRevision;
  UINT8   Reserved;
  UINT32  TableMaximumSize;          // 表最大长度
  UINT64  TableAddress;              // 64位表地址
} SMBIOS_TABLE_3_0_ENTRY_POINT;

2. 主要技术差异

特性SMBIOS 2.XSMBIOS 3.X
地址空间32位地址64位地址
表大小限制最大 65535 字节最大 4GB
入口点标识SM” + “DMISM3
入口点大小31 字节 (0x1F)24 字节 (0x18)
结构计数明确指定结构数量不指定,需遍历到Type 127
校验和两个校验和一个校验和

实践发现,目前机器有不同的实现方式,比如:声明了 3.0 但是实际上仍然是 2.0 的结构;3.0 和 2.0 同时共存,这种情况下看起来 Windows 更倾向于使用 3.o 提供的信息。

基于 FireBeetle P4 制作一个USB 麦克风

在 FireBeetle P4 板子上,有一个 PDM 的麦克风。

基本实现原理是:通过ESP32  IDF编程使用 TinyUSB 架构将 P4 模拟为 USB UAC 设备,然后通过这个麦克风获得环境声音,这样就得到了一个 USB 麦克风。

需要特别注意的是 PDM 的初始化和 I2S 的会有一些差异,DFRobot 选择的这个数字麦克风资料很少,看起来只支持一个 24K 的采样率:

void init_pdm_rx(void) {
    i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0, I2S_ROLE_MASTER);
    i2s_new_channel(&chan_cfg, NULL, &rx);

    i2s_pdm_rx_config_t pdm_cfg = {
        .clk_cfg = I2S_PDM_RX_CLK_DEFAULT_CONFIG(CONFIG_UAC_SAMPLE_RATE),
        //.slot_cfg = I2S_PDM_RX_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO),
        .slot_cfg = I2S_PDM_RX_SLOT_PCM_FMT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO),
                .gpio_cfg = {
            .clk = MIC_I2S_CLK,      // PDM clock
            // QUESTION - what about the LR clock pin? No longer relevant? Do we ties it high or low?
            .din = MIC_I2S_DATA,     // PDM data
            .invert_flags = { .clk_inv = false },
        },
    };
    pdm_cfg.slot_cfg.slot_mode = I2S_SLOT_MODE_MONO; // single mic

    i2s_channel_init_pdm_rx_mode(rx, &pdm_cfg);
    i2s_channel_enable(rx);
}

上述设置之后,就可以在回调函数中填充需要对主机反馈的数据了:

static esp_err_t usb_uac_device_input_cb(uint8_t *buf, size_t len, size_t *bytes_read, void *arg)
{
    if (!rx) {
        return ESP_FAIL;
    }
        //memcpy(buf,Buff,len);
        //*bytes_read=len;
        //return ESP_OK;
    return i2s_channel_read(rx, buf, len, bytes_read, portMAX_DELAY);
}

完整的代码:

工作的视频

eSPI 综述

这篇文章来自 MicroChip ,原文在【参考1】,标题是“是时候迁移到 eSPI 总线了吗?”(Is It Time to Migrate to the eSPI Bus?)。

大多数计算机用户都知道高速总线的存在,比如 PC 上配备的 PCI Express® (PCIe®) 附加卡或 USB 接口。然而,他们可能不知道所有计算机上都存在低速总线。多年来,这种总线一直用于连接各种设备,例如嵌入式控制器 (EC,笔记本上使用)、基板管理控制器 (BMC,服务器上用于远程管理)、Super I/O (SIO, 台式机) 、用于存储 BIOS 代码的SPINOR以及可信平台模块 (TPM) 到系统核心逻辑。这种低速总线最初被称为低引脚数 (LPC) 总线。 

随着计算行业需求的不断发展,更加灵活高效的增强型串行外设接口 (eSPI) 总线应运而生,以克服 LPC 总线的局限性。这款一体化总线由最新的 PC 计算芯片组支持,旨在取代 LPC 总线以及 SPI 总线、SMbus 和Sideband信号。这种情况下可以通过一个 GPIO 控制这个设备的供电重新给让它工作起来,这个 GPIO 就可以称作 Out band)。对于计算应用设计人员而言,从 LPC 总线迁移到 eSPI 总线具有以下优势:

  • 节省成本:由于 LPC 总线需要大量边带信号来实现电源排序和睡眠模式支持,因此它使用 13 个引脚连接到系统处理器。eSPI 协议使用虚拟线来实现其中一些信号,因此大多数实现中只需要五六个引脚,从而减少了引脚数量和成本。
  • 更低电压:LPC总线需要3.3VI/O信号,而eSPI总线使用1.8V,显著降低系统功耗。
  • 简化电路板布局和设计:LPC 总线需要同步 24 MHz 或 33 MHz 时钟,因此需要仔细的电路板布局,以确保时钟和数据信号长度与所有设备匹配。eSPI 总线使用来自系统处理器的主驱动时钟,从而简化了电路板布局和设计。
  • 低功耗状态:LPC 总线只能在系统处于 S0 状态时运行,而 eSPI 总线则可以在系统处于低功耗 S5 状态时运行。这可以实现许多系统改进,包括:
    • 用于支持电源排序的边带信号可以打包在eSPI 中传输从而变成虚拟线,就无需在硬件上拉出来线路。
    • EC可以在启动时共享系统SPI存储,从而无需在系统中添加额外的SPI芯片,从而降低系统成本。
    • 在 S5 状态下,eSPI 总线可用于核心逻辑与 EC 之间的通信。这样可以移除额外的边带通信总线,例如 I²C 和 PECI,从而减少电路板上的额外信号

以下两个图表显示了基于 LPC 的系统和基于 eSPI 的系统之间的差异。

图-LPC系统图

图 – eSPI 系统图

从上图可以看到,很多总线和功能能够“打包”到 eSPI中。

eSPI 规范指定了几种可通过总线进行通信的模式或通道:

  • 外设通道用于与位于 EC、BMC 和 SIO 中的设备(以前位于 LPC 总线上)进行通信。这些设备包括 UART、邮箱寄存器、端口 80 寄存器、嵌入式内存接口和键盘控制器。外设通道还支持总线主控通道。总线主控功能允许 EC 直接从主系统内存读取/写入数据。
  • 虚拟线通道用于将边带信号信息传输到/接收自 EC、BMC 和 SIO。来自外围设备(例如 UART)的中断也通过虚拟线通道传输。与 LPC 总线相比,该通道大大减少了 eSPI 总线的引脚数量和成本。
  • 带外 (OOB) 消息通道用于通过 eSPI 传输 SMBus 流量。这些消息可以包括系统逻辑和处理器温度值,或 SMBus 管理组件传输协议 (MCTP) 数据包。
  • 闪存访问通道允许系统处理器在 BIOS、管理引擎 (ME)、EC、BMC 和 SIO 之间共享系统 SPI Flash。这通过减少系统中 SPI Flash 芯片的数量来降低系统成本。

如果您准备将您的设计迁移到支持 eSPI 总线,我们的 MEC14xx 和 MEC17xx 嵌入式控制器是绝佳选择。Microchip 是首批支持 eSPI 总线的公司之一,并被英特尔® 选为其 eSPI 开发的验证合作伙伴。这意味着我们的设备已通过英特尔 eSPI 主站的全面验证。英特尔还选择了我们的 EC 作为其参考验证平台,确保它们获得英特尔的全面支持。访问我们的嵌入式控制器设计中心 ,了解更多关于如何将您的计算设计迁移到这项新总线技术的信息。

参考:

1.https://www.microchip.com/en-us/solutions/data-centers-and-computing/computing-solutions/technologies/espi

PY 更改默认值

Py 是 Python 的 Launcher,在安装的时候可以选择:

装好之后,运行 py 可以直接打开 Python。它的作用是让你方便的在不同版本之间切换。比如,你的系统安装了多个 Python,可以使用 py –list 进行查看:

默认情况下运行 py ,会运行 3.14 版本的。

一些资料上说可以通过修改 py.ini 的方法修改上面的列表,但是这个文件安装之后不会自动生成,有需要的话可以手工添加,例如下面的文件保存为 py.ini 然后放在 py.exe 同一个目录下,每次运行 py 会自动调用 Python 3.8:

;
; This is an example of how a Python Launcher .ini file is structured.
; If you want to use it, copy it to py.ini and make your changes there,
; after removing this header comment.
; This file will be removed on launcher uninstallation and overwritten
; when the launcher is installed or upgraded, so don't edit this file
; as your changes will be lost.
;
[defaults]
; Uncomment out the following line to have Python 3 be the default.
python=3.8

[commands]
; Put in any customised commands you want here, in the format
; that's shown in the example line. You only need quotes around the
; executable if the path has spaces in it.
;
; You can then use e.g. #!myprog as your shebang line in scripts, and
; the launcher would invoke e.g.
;
; "c:\Program Files\MyCustom.exe" -a -b -c myscript.py
;
;myprog="c:\Program Files\MyCustom.exe" -a -b -c

此外,还有一种方法是设定一个环境变量,例如:set py_python=3.13,再次运行py 会直接调用 python 3.13

上述方法来自【参考1】。

但是,上述方法并不能完全解决问题,比如,我的编译环境中会调用 py -3 来启动 Python ,测试下来 -3 参数会导致py 自动调用当前系统中最新版本的 Python。我这边研究之后的解决方法是:重新编译 Python Source Code 直接写死:

在Python-3.14.0\Python-3.14.0\PC\launcher2.c 中,首先在开头处定义需要的版本:

static FILE * log_fp = NULL;

wchar_t MyTag[] = L"3.8";

void
debug(wchar_t * format, ...)
{
    va_list va;

然后修改代码

       if (argLen > 0) {
            if (STARTSWITH(L"2") || STARTSWITH(L"3")) {
                
                // All arguments starting with 2 or 3 are assumed to be version tags
                //LAB-Z_Debug search->tag = arg;
                //LAB-Z_Debug search->tagLength = argLen;
                //LAB-Z_Debug_Start
                wchar_t MyTag[] = L"3.8";
                search->tag = MyTag;
                search->tagLength = 3;
                //LAB-Z_Debug_End
                search->oldStyleTag = true;
                search->restOfCmdLine = tail;
            } else if (STARTSWITH(L"V:") || STARTSWITH(L"-version:")) {

编译后的 py.exe 替换之前的 py.exe即可。

修改后的代码下载:

重新编译后的 py.exe 下载(固定调用 3.8)

参考:

1.https://docs.python.org/3/using/windows.html

2.调试时可以设置 set PYLAUNCHER_DEBUG=1 这样会打开 Py.exe 的调试输出,方便研究。

UEFI TIPS: 这样会导致内存泄漏吗?

最近编写代码的时候,忽然提出一个问题,按照下面的写法会导致内存泄漏的问题吗?

  for (UINTN i=0;i<1000;i++) {
	  UINTN x;
	  Print(L"%x\n",x++);
  }

为了验证这个,编写一个完整的 UEFI 代码进行测试:

#include  <Uefi.h>
#include  <Library/UefiLib.h>
#include  <Library/ShellCEntryLib.h>

/***
  Print a welcoming message.

  Establishes the main structure of the application.

  @retval  0         The application exited normally.
  @retval  Other     An error occurred.
***/
INTN
EFIAPI
ShellAppMain (
  IN UINTN Argc,
  IN CHAR16 **Argv
  )
{
  
  for (UINTN i=0;i<1000;i++) {
	  UINTN x;
	  Print(L"%x\n",x++);
  }

  return(0);
}

对应的,在 INF 文件中定义生成汇编代码:

[BuildOptions]
  MSFT:*_*_X64_CC_FLAGS  = /FAsc /Od

查看生成的 cod文件:

$LN6:
  00000	48 89 54 24 10	 mov	 QWORD PTR [rsp+16], rdx
  00005	48 89 4c 24 08	 mov	 QWORD PTR [rsp+8], rcx
  0000a	48 83 ec 48	 sub	 rsp, 72			; 00000048H

; 27   :   
; 28   :   for (UINTN i=0;i&lt;1000;i++) {

  0000e	48 c7 44 24 20
	00 00 00 00	 mov	 QWORD PTR i$1[rsp], 0
  00017	eb 0d		 jmp	 SHORT $LN4@ShellAppMa
$LN2@ShellAppMa:
  00019	48 8b 44 24 20	 mov	 rax, QWORD PTR i$1[rsp]
  0001e	48 ff c0	 inc	 rax
  00021	48 89 44 24 20	 mov	 QWORD PTR i$1[rsp], rax
$LN4@ShellAppMa:
  00026	48 81 7c 24 20
	e8 03 00 00	 cmp	 QWORD PTR i$1[rsp], 1000 ; 000003e8H
  0002f	73 2a		 jae	 SHORT $LN3@ShellAppMa

; 29   : 	  UINTN x;
; 30   : 	  Print(L"%x\n",x++);

  00031	48 8b 44 24 28	 mov	 rax, QWORD PTR x$2[rsp]
  00036	48 89 44 24 30	 mov	 QWORD PTR tv68[rsp], rax
  0003b	48 8b 54 24 30	 mov	 rdx, QWORD PTR tv68[rsp]
  00040	48 8d 0d 00 00
	00 00		 lea	 rcx, OFFSET FLAT:??_C@_17KDIHCDGM@?$AA?$CF?$AAx?$AA?6@
  00047	e8 00 00 00 00	 call	 Print
  0004c	48 8b 44 24 28	 mov	 rax, QWORD PTR x$2[rsp]
  00051	48 ff c0	 inc	 rax
  00054	48 89 44 24 28	 mov	 QWORD PTR x$2[rsp], rax

; 31   :   }

  00059	eb be		 jmp	 SHORT $LN2@ShellAppMa
$LN3@ShellAppMa:

可以看到:变量 X 就是 QWORD PTR i$1[rsp],在循环中并不会每次重新分配内存。因此,无需担心内存泄漏的问题。

查了一下资料:

“在C语言的早期版本中,局部变量的声明必须集中在函数或代码块的开头,位于任何可执行语句之前。这种限制在C89/ANSI C标准中得到了明确的规范。

随着C语言标准的发展,C99标准引入了更灵活的变量声明方式,允许在代码块的任意位置声明局部变量,只要遵循“先定义后使用”的原则即可。这种改进使得程序员可以在需要使用变量的地方才进行声明,从而提高了代码的可读性和编写灵活性。“

Flex Windows 下的简单测试

一般来说,计算机系的毕业生很容易编写出来一个词法分析器,能够将输入的文本解析为 Token 。但是业界已经有了成熟完善的方法和工具,Flex 就是其中的一个

Flex是一种用于生成词法分析器的工具,通过读取包含正则表达式和对应C代码的规则文件,自动生成可识别特定词法模式的C语言源代码。其输入文件由定义区、规则区、用户代码区组成,支持与语法分析器生成工具Bison协同工作。生成的词法分析器可应用于编译器开发、复杂系统建模、教学实验等领域,具有正则表达式语法兼容Lex、错误处理机制完善、自定义函数扩展灵活等技术特性。

Flex通过解析用户定义的正则表达式规则,生成C语言实现的词法分析器源码,可自动将输入文本分解为预定义的词法单元。生成的词法分析器包含默认入口函数,支持通过重定义宏实现自定义输入源。工具保持与Lex语法的高度兼容性,生成的代码可直接嵌入到C/C++工程项目中使用。

从上面也可以看到使用 Flex 的好处是:可以通过定义正则表达式规则来进行此法分析,方便编写C代码,此外生成的结果可以配合 Bison进行语法分析。之前提到过, ACPICA 提供的套件就是基于 Flex和Bison 编写的。

1.根据【参考1】配置环境

2.编写 lexer.l 代码如下:

%{
#include <stdio.h>
#include <stdlib.h>

// 手动定义Token类型(若无Bison)
#define NUMBER 256
#define ID     257
#define PLUS   258
int line_num = 1;
%}

DIGIT    [0-9]
LETTER   [a-zA-Z]
%%
{DIGIT}+    { printf("NUMBER: %s\n", yytext); return NUMBER; }
{LETTER}+   { printf("IDENTIFIER: %s\n", yytext); return ID; }
"+"         { printf("PLUS\n"); return PLUS; }
"\n"        { line_num++; }
[ \t]       ;  // 忽略空白字符
.           { printf("Unknown char: %s\n", yytext); }
%%

int yywrap() { return 1; }

3.Visual C++ 2019 编写 flextest.c 文件(不是 .cpp)如下:

#include <stdio.h>
#pragma warning(disable:4996)
#include "lex.yy.c"  // 包含Flex生成的代码

int main() {
    yyin = fopen("input.txt", "rb");
    if (!yyin) {
        perror("Failed to open input file");
        return 1;
    }


    while (yylex()) {
    }

    fclose(yyin);
    return 0;
}

4.使用时,先运行 flex lexer.l 生成 lex.yy.c(如果需要 debug 可以使用 flex -d lexer.l)。

5.编译flextest.c,然后配合如下测试文件

123
abc
+
xyz 456
@
123123
1222
4dd

6.运行结果如下

可以看到输出了识别到的各种Token

EDK2 202508 来了

今年8月份,EDK2 202508正式发布在:

https://github.com/tianocore/edk2/releases/tag/edk2-stable202508

从 History 来看,增加了一点新的功能

在上面的链接下载代码之后,大小是 25MB 左右,但是无法直接编译(build -a X64 -p EmulatorPkg\EmulatorPkg.dsc -t VS2019),会出现下面的错误

MdePkg.dec(33): error 000E: File/directory not found in workspace
C:\BuildBs\edk2508\MdePkg\Library\MipiSysTLib\mipisyst\library\include

产生的原因是这个目录下缺少文件,可以通过和抓到的完整版进行比较补全:

补全上面的之后,还是会碰到类似问题,也是因为缺少文件导致的,重复上述步骤补全。最终得到一个可以完整编译EmulatorPkg的。

于是,这次提供三个 Pacakge

1.原始的 EDK2 202508 (24.6MB)

edk2-edk2-stable202508_ORG.zip
链接: https://pan.baidu.com/s/1onGGn_4UFN3loVktKNrVGQ?pwd=labz 提取码: labz

2.修改后的EDK2 202508,可以编译 EmulatorPkg (207MB)

edk2508.7z
链接: https://pan.baidu.com/s/1fhPY4Hqqf5jOE6iqJ6Dheg?pwd=labz 提取码: labz

3.完整的 EDK2 202508 , 包括所有的第三方源代码(2.27G)

edk22508Full.7z
链接: https://pan.baidu.com/s/1TSbmW8v7jTtmdsXc40ScQQ?pwd=labz 提取码: labz

ESP32 ESPNOW 功能测试

ESP32 的 ESPNOW 给我们提供了一个方便的让 ESP32 无线互联通讯的方法。这次测试的是:一个 DFRobot 的 FireBeetle ESP32 和 ESP32S3 通过 ESPNOW 互联。

代码要点:

  1. 发送方需要知道接收方的 MAC,每一个ESP32都内置了一个独一无二的MAC;
  2. 通讯成功后,接收方能够得知发送方的 MAC;
  3. 简单起见,通过    esp_wifi_set_mac函数直接指定当前 ESP32 的 MAC,这样就不用专门查询了;
  4. 通过IDPIN区分两块板子,就是说测试的时候,一块板子这个引脚悬空,另外一个拉低
#include &lt;esp_now.h>
#include &lt;WiFi.h>
#include "MacAddress.h"
#include "WiFi.h"
#include "esp_wifi.h"

#define IDPIN 48

//  发射器地址(当前是发射) ESP32 A 的 MAC 地址
uint8_t AddressA[] = {'L', 'A', 'B', '-', 'Z', 'S'};
//  接收器地址 ESP32 B 的 MAC 地址
uint8_t AddressB[] = {'L', 'A', 'B', '-', 'Z', 'R'};

// 数据结构
typedef struct {
  char data[250];
} esp_now_message_t;

esp_now_message_t message;

void onDataReceive(const esp_now_recv_info *mac, const uint8_t *incomingData, int len) {
  esp_now_message_t receivedMessage;
  memcpy(&amp;receivedMessage, incomingData, sizeof(receivedMessage));
  if (digitalRead(IDPIN) == HIGH) {
    Serial.print("Received from ESP32 B: ");
  } else {
    Serial.print("Received from ESP32 A: ");
  }
  Serial.println(receivedMessage.data);
}

void setup() {
  pinMode(IDPIN, INPUT_PULLUP);
  Serial.begin(115200);
  WiFi.mode(WIFI_STA);

  if (digitalRead(IDPIN) == HIGH) {
    // 设定本机 MAC 为 AddressA
    esp_wifi_set_mac(WIFI_IF_STA, &amp;AddressA[0]);
  } else {
    // 设定本机 MAC 为 AddressB
    esp_wifi_set_mac(WIFI_IF_STA, &amp;AddressB[0]);
  }

  if (esp_now_init() != ESP_OK) {
    Serial.println("Error initializing ESP-NOW");
    return;
  }

  esp_now_peer_info_t peerInfo;
  if (digitalRead(IDPIN) == HIGH) {
    memcpy(peerInfo.peer_addr, AddressB, 6);
  } else {
    memcpy(peerInfo.peer_addr, AddressA, 6);
  }
  peerInfo.channel = 1;
  peerInfo.ifidx = WIFI_IF_STA;
  peerInfo.encrypt = false;

  if (esp_now_add_peer(&amp;peerInfo) != ESP_OK) {
    Serial.println("Failed to add peer");
    return;
  }

  esp_now_register_recv_cb(onDataReceive);
}

void loop() {
  if (Serial.available()) {
    int len = Serial.readBytesUntil('\n', message.data, sizeof(message.data));
    message.data[len] = '\0'; // 确保字符串以 null 结尾

    esp_err_t result;
    if (digitalRead(IDPIN) == HIGH) {
       result = esp_now_send(AddressB, (uint8_t *)&amp;message, sizeof(message));
    } else {
       result = esp_now_send(AddressA, (uint8_t *)&amp;message, sizeof(message));
    }

    if (result == ESP_OK) {
      if (digitalRead(IDPIN) == HIGH) {
        Serial.println("Sent to ESP32 B successfully");
      } else {
        Serial.println("Sent to ESP32 A successfully");
      }
    } else {
      Serial.println("Error sending the data");
    }
  }
}

工作的视频在 【ESP32和ESP32S3通过ESPNOW通信演示】

Step to memory 016 Fly-by 拓扑

https://www.bit-tech.net/reviews/tech/memory/the_secrets_of_pc_memory_part_4/3

为了在更高速度等级下获得更佳的信号质量,DDR3 采用了所谓的“Fly-by”架构来传输命令、地址和时钟信号。这有效地减少了 DDR2 T-Branch 架构中的短截线数量和信号长度,使其设计更加简洁、简洁。Fly-by 拓扑通常将内存模块上的 DRAM 芯片串联起来,并在线性连接的末端设置一个接地端接点,用于吸收残余信号,防止其沿总线反射回来。

我们最近采访了镁光公司的应用工程师 Aaron Boehm,了解 Fly-by 拓扑对 DDR3 的重要性。他表示,Fly-by 设计“在 DDR3 中非常重要。采用 Fly-by 拓扑的最大优势之一或许在于:能够实现更快的信号斜率。这为我们提供了更好的眼图数据,这在 DRAM 中非常重要。 ”

Fly-by 架构的实际应用 资料来源:RAMBUS

Boehm 强调:“如果没有 Fly-by 设计,DDR3 将无法发送信号。 ” 实施这种新架构是为了避免 DDR2 在更高速度下出现的 T-Branch 限制,因为“所有寻址命令都必须在一个时钟周期内到达 DRAM。Fly-by 拓扑结构可以解决这个问题。如果开始提高速度,那么在一个周期内将这些信号发送到 DRAM 就相当困难了。 ”

尽管 Fly-by 拓扑结构有诸多优势,但也增加了复杂性;命令-地址-时钟总线与 DRAM 的顺序 Fly-by 连接会导致沿线每个 DRAM 的数据总线时钟偏差增加。简而言之,命令-地址-时钟总线信号沿线传输的延迟会增加。

读写分级

DDR3 内存控制器内置了读写均衡功能,以补偿这些时钟偏差问题。Boehm解释了新拓扑结构的问题:“时钟、命令和地址都以‘Fly-by’方式路由到每个 DRAM,而包含选通信号的 DQ [数据] 总线的路由方式更类似于 T 分支。命令、地址和时钟到达每个 DRAM 的时间略有不同,而 DQ 信号到达的时间大致相同。因此,在采用飞越方式的每个 DRAM 中,DQ 总线和时钟之间存在固有的偏差,这会导致问题,您必须通过(在另一端)进行去偏差来解决,以使一切重新对齐。

这就是 DDR3 中写入均衡的用武之地。您所做的就是稍微延迟 DQ 总线,使其与时钟同时到达每个 DRAM内存控制器会动态地将每个 DRAM 芯片的数据-数据选通 (DQ-DQS) 与时钟 (CK) 对齐。

这通过简单的”问候-应答”(Hello-and-Respond)反馈分析完成。一旦确定了每个 DRAM 芯片的延迟或偏差量,内存控制器就可以轻松地在后续活动中补偿该问题。DDR3使用新的Multi-Purpose Register (多用途寄存器,MPR) 进行信号调平处理;这会在初始化期间生成用于校准的预定数据模式。MPR 通过动态调整数据选通使其尽可能靠近数据眼的中心来帮助内存控制器校准。

当内存控制器处于写入均衡模式时,至少一个数据位必须通过 x4、x8 或 x16 的 DRAM 配置将均衡反馈传送到内存控制器。校准完成后,内存控制器将启动写入均衡禁用序列。

均衡过程也以另一种方式进行;顾名思义,读取均衡会在读取周期内将数据总线与命令-地址-时钟总线对齐。完成内存控制器均衡校准程序后,内存将恢复到标准操作模式。

类比:多个机场接送

想象一下,你需要接八位乘飞机来参加节日团聚的亲戚。每个人到达的时间都不一样。

起初你并不知道每个人的日程安排,所以你必须在他们离开家之前给他们打电话。打电话给每个人确认他们是否到达,正是内存控制器中的多用途寄存器 (MPR) 的功能。

一旦你知道每个人的到达时间,你就可以安排各自的接机时间,最早的在最前,最晚的在最后。你是数据 (DQ),你的车是数据选通 (DQS),机场是内存模块,亲戚们是通过 Fly-by 路径飞来的命令-地址-时钟信号。每个人都会到达不同的登机口,代表 DRAM 芯片。

动态片上终端 (ODT)

DDR3 扩展了 DDR2 的片上终端电阻设计,通过增加额外的灵活性,可以优化不同条件下的终端电阻值,从而管理终端电阻的功耗。

在读写操作期间未被访问的内存模块,可以使用 30 或 40 欧姆的低阻抗值来终止数据总线。在写入操作期间,最佳终端电阻可以更改为约 60 或 120 欧姆的高阻抗值。

镁光公司的 Todd Farrell 表示:“动态 ODT 允许 DDR3 SDRAM 设备在向不同模块发出写入命令之间无缝更改终端电阻值。此功能在 DDR2 SDRAM 系统中不可用,因为在同一设备上更改终端电阻值时需要总线空闲时间。 【这句话含义我不理解】”

动态 ODT 可根据不同情况调整终端电阻值,将干扰噪声降低到更易于管理的水平,从而进一步提升信号完整性。这使得内存能够以比 DDR2 更高的速率运行。

ZQ 驱动器自校准

此功能有时称为“ZQ 校准”,用于增强阻抗值校准并实现更严格的公差。这是一项重要的改进,涉及分配所谓的“ZQ”引脚以实现片上驱动器校准功能。ZQ 引脚位于 DRAM 芯片本身上,因此有时可以将其称为球栅阵列 (BGA) 封装中的“ZQ 球”。ZQ校准在两个级别运行:首先,它在任何主要内存操作之前的启动序列中使用 – 这称为“ZQ 长校准”或 ZQCL。启动校准通常需要更长时间,并且在 DDR3 写入均衡之前。
第二个 ZQ 校准称为“ZQ 短校准”或 ZQCS。它有时被称为“跟踪校准”,因为它发生在整个内存操作周期中,但所需的时间比启动阶段的校准要少。

实施跟踪校准是为了在整个内存操作过程中降低在较高频率下信号时序严重不准确的可能性。通过在正常运行期间随着电压和温度的上下漂移重新校准阻抗值,可以显著减少阻抗不连续的问题。

总而言之,ZQCS 持续跟踪和补偿电压和温度 (PVT) 的变化,以实现最佳数据眼,从而实现最佳数据传输。ZQ 引脚连接到一个高精度外部电阻器,该电阻器用于高清调整输出驱动器的“导通”阻抗和 ODT 阻抗。ZQ校准是 DDR3 的一个重要特性:随着在给定时间内挤入更多数据以实现更快的传输,DDR3 可用的裕度更小。如果没有 ZQ 校准,DDR3 的可靠性将大大降低。

复位功能

DDR3 引入了异步复位功能,该功能由内存控制器从外部启动。复位信号将强制 DRAM 进入明确定义的非操作状态。这是一个低调却又极其关键的功能。

复位功能会清除 DRAM 中的所有状态和数据,而无需单独复位每个 DRAM 或关闭模块电源。当内存控制器尝试将内存恢复到已知状态时,这可以节省时间和功耗。一旦复位被激活,内存将完全重新初始化,并且可以在任何周期的任何时间激活复位。

在内存启动功率上升期间,复位会被激活,直到内存功率达到稳定状态——这确保后续校准不会基于不可靠的功率状态。复位与 ZQ 校准协同工作,在任何周期,一旦复位功能被激活,ZQ 校准长 (ZQCL) 就会发挥作用。这将确保在数据传输开始之前,时序精度和信号质量处于最佳状态。

一些内存设计师在 JEDEC 上多次讨论并要求实现此功能,但直到 DDR3 出现,无需任何特定分配的 DRAM 引脚才使重置功能成为可能。

DDR3 及后续产品面临的挑战

在2007年电子设计大会DesignCon上,Altera公司发表了一篇关于校准技术和DDR内存未来发展状况的论文。论文指出:“尽管每一代内存的性能都会翻一番,但内存的不确定性并不会以同样的速度下降。 ”

我们最近就1600MHz及更高​​频率的DDR3内存设计挑战向镁光科技进行了咨询。应用工程师Aaron Boehm表示:“随着每一代DRAM的推出,设计难度都越来越大,因为我们的芯片尺寸不断缩小,速度不断提高,电压却不断下降。获取DRAM内部和外部的清晰信号变得更加困难。总线架构极具挑战性,信号走线越紧密,串扰问题就越多。 ”

Boehm继续强调,“电压降低后,很难获得良好清晰的信号边沿。因此,我认为随着我们不断进步,满足这些速度要求将变得越来越具有挑战性。 ” 从根本上说,当内存速度提高时,可用的余量也会随之减小。

内存系统由内存模块、主板和 CPU 组成。计算机内存系统不能仅仅被视为单个部件,而应该包含主板上的整个内存子系统。因此,内存性能和超频高度依赖于所有这些组件在越来越严格的容差下完美运行。

这给 DRAM 和内存模块制造商带来了一个新问题:随着内存速度的提升,实现组件兼容性变得越来越困难,“ ……尤其是在市面上存在各种主板设计的情况下。这非常困难,因为你试图在一个很多情况下都无法控制的环境中运行。即使他们想要极快的速度,他们也在试图降低 PCB 或主板的成本, ”Boehm说。

随着每一代 DDR 技术的发展,超频收益都在递减,这是意料之中的事情。

LAB-Z注释:至此,PC内存的秘密系列已经翻译完毕,后面我会继续介绍内存相关知识。