【翻译】解码 USB:型号和版本

原文在  https://www.tomshardware.com/features/usb-decoded-all-the-specs-and-version-numbers

从USB Hub到充电器、个人电脑、家用电器和树莓派,通用串行总线 (USB) 的应用可谓无处不在,名副其实。但由于 USB 拥有十多种不同的版本、大量的接口以及各种传输速率和充电功能,因此它也是一套庞大的标准,如果您不清楚自己的需求,很容易感到困惑。 

如果你查看一款顶级游戏笔记本电脑游戏台式机的规格表,可能会看到它配备一个 USB 4 接口、一个 USB 3.2 Gen 2 接口和两个 USB 2 接口。但这些接口到底是什么意思?它们各自又能提供哪些功能?这里为你准备了答案,帮助你了解每种 USB 接口的功能。

USB 版本、版本名称和速度

如果您使用 USB 接口传输数据而非仅充电,那么最重要的是了解连接的最高速度。因为要以高达 10 Gbps 的速度将文件复制到外部 SSD,您需要确保连接链上的每个环节都支持该速度:主机、数据线和硬盘本身。

如果每个 USB 端口和产品都能在其上或至少在规格表上标明最高传输速度,那就太好了。事实上,USB-IF(USB 开发者论坛)——USB 标准的维护者——也希望如此。该组织最近推出了一种新的命名规则,将所有速度达到或超过 5 Gbps 的端口分别命名为 USB 5 Gbps、USB 10 Gbps、USB 20 Gbps 或 USB 40 Gbps,不再使用版本号。获得 USB-IF 认证的 USB 产品制造商可以获得带有 USB 传输速度标识的徽标,并将其用于产品包装上。

(图片来源:USB.org)
然而,大多数USB设备并未经过官方认证流程。即使某款产品通过了认证,您仍然很可能会看到它以数字版本号(例如USB 3.2)而非速度标识进行宣传。您可能还会看到USB产品被标注为高速USB、超高速USB、超高速USB 10 Gbps或超高速USB 20 Gbps。虽然“超高速”名称已被弃用,但仍出现在一些包装和营销材料中。

版本工作速度SuperSpeed NameSpeed Name 接口形式
1.1 / 1.012 MbpsType-A, Type-B
2.0480 MbpsHi-Speed USBType-A, Type-B, Type-C,Mini, Micro
3.0 / 3.1 Gen 1 / 3.2 Gen 15 GbpsSuperSpeed USBUSB 5 GpbsType-A, Type-B, Type-C,Micro
3.1 Gen 2 / 3.2 Gen 210 GbpsSuperSpeed USB 10 GbpsUSB 10 GbpsType-A, Type-C
3.2 Gen 2×220 GbpsSuperSpeed USB 20 GbpsUSB 20 GbpsType-C
USB420 / 40 Gbps USB 20 Gbps / USB 40 GbpsType-C
USB4 Version 280 Gbps / 120 Gbps (asymmetric)  Type-C

USB 3.x 的版本众多,我们专门制作了一篇关于USB 3.2 和 USB 3.1 的详细解释文章,帮助您更好地了解它们。不过,需要注意的是,USB 3、3.1 和 3.2 都提供 5 Gbps 的连接速度,版本号后带有“Gen 2”表示速度提升至 10 Gbps,而带有“Gen 2×2”则表示速度提升至 20 Gbps。

USB接口

你可能会在设备上看到六种以上的常见USB接口。其中一些接口在特定类型的设备上更为常见。

USB Type-A

USB-A接口的历史可以追溯到20世纪90年代,它是最早的USB接口,至今仍是应用最广泛的USB接口。它呈矩形扁平状,只能以一个方向插入电脑。大多数外设和电脑都配备USB Type-A接口,只有一些超极本由于追求厚度而无法使用这种接口。USB Type-A接口可能支持USB 1.1、USB 2或USB 3.x,传输速度可以是5Gbps或10Gbps,但这取决于设备或线缆的具体支持情况。USB 3.x接口有时是蓝色的,而支持10Gbps传输速度的接口有时是红色的。

USB Type-B

USB-B 主要用于打印机等体积较大的外设。它也常见于 USB 集线器或内置 USB 集线器的显示器上,不过 B 型端口始终是连接集线器/显示器和电脑的线缆的一部分。【LABZ: 这种接口最大的特点是足够牢固,因此很多测试设备也会使用这种形式的接口】

Mini USB (迷你 USB)

Mini USB接口最早出现于世纪之交,与USB 2.0规范一同问世,如今已基本被Micro USB取代,成为历史。不过,你或许还能在一些老式游戏手柄或数码相机上看到迷你USB接口。严格来说,Mini USB既有A型也有B型,但市面上常见的通常只有B型。

Micro USB(微型USB

虽然 USB-C 是一种更新、更好的接口,尺寸也差不多,但你仍然会在许多设备上看到 micro USB 接口,从入门级安卓平板电脑到树莓派、微控制器、数码相机、移动电源和智能家居设备,不一而足。严格来说,micro USB 同时存在 A 型和 B 型,但你通常只会看到 B 型。

USB Type-C

如今市面上大多数新电脑都至少配备一个 USB-C 接口(有些甚至更多)。这些线缆的插头呈长方形,与其他 USB 接口不同,正反面都可以插入使用。

USB-C 线缆可以传输数据、视频和电力(最高可达 240 瓦),但并非所有线缆都支持所有这些功能,因此您需要查看规格说明。许多新款键盘、鼠标以及几乎所有新款智能手机和平板电脑都配备了 USB-C 接口。

USB电源标准

除了苹果的iPhone之外,几乎所有移动设备都可以通过USB充电。你也可以通过USB连接为任何一款顶级超极本或便携式显示器供电。但并非所有USB端口和线缆都有相同的供电能力。

标准接口类型最大功率最大工作电流伏特
USB 3.2 / 3.1 / 3.0USB-A、USB-B4.5瓦900ma5伏
USB 电源传输 (PD)仅限 USB-C240瓦5A5V、9V、15V、20V、28V、36V、48V
USB电池充电(BC)USB-A、USB-C7.5瓦1.5A5伏
USB-C(非PD)USB-C15瓦3A5伏
USB 2.0USB-A、USB-B、micro USB2.5瓦500ma5伏

一些最新的USB设备包装上可能会印有输出功率的标识,但这种情况目前并不常见。大多数现代手机充电器和所有笔记本电脑充电器都使用某种USB PD协议,但并非所有USB PD设备的最大功率都相同,因此务必检查充电器和线缆的功率。对于笔记本电脑而言,60W是最常见的最低功率,而超过100W的则较为少见,因为这是一个较新的标准。

通过 USB 传输视频

DisplayPort 替代模式(Alt Mode)是简​​化现代 PC(尤其是笔记本电脑)线缆连接的另一种方式。借助 DisplayPort 替代模式,USB-C 线缆可以传输非 USB 信号。也就是说,计算机可以通过 USB-C 线缆传输 DisplayPort 信号。 

虽然这对于台式机来说可能算不上什么问题,但对于笔记本电脑来说却是一大优势。USB-C 接口的物理尺寸比 HDMI 或 DisplayPort 接口要小,因此能够在保持笔记本电脑尺寸小巧的同时传输 DisplayPort 信号,对 PC 厂商来说无疑是一大福音。 

遗憾的是,通常无法仅凭外观判断笔记本电脑的 USB-C 端口是否支持视频输出。您需要查阅电脑的手册/规格书,或者自行尝试。

笔记本电脑上最新的 USB 4 端口支持 DisplayPort Alt Mode 2.0,最高可支持 8K 分辨率、60Hz 刷新率和 HDR10 色彩。虽然 USB 4 本身的数据传输速率只有 40Gbps,但由于 DisplayPort 只需单向传输数据,因此可以占用全部 8 条通道,从而实现 80Gbps 的数据传输速率。

你也可以通过 USB 3.x 甚至 2.0 Type-A 端口输出视频,虽然功能比较有限。尽管 Type-A 端口不支持 USB Alt Mode(USB 替代模式),但市面上有很多扩展坞和便携式显示器都采用了 DisplayLink 技术。安装正确的驱动程序后,你的电脑会将视频压缩并通过标准的 USB 信号发送到扩展坞。

USB 线缆和向下兼容性

USB 最棒的优点之一就是它的向下兼容性。你可以把一块全新的 20Gbps USB 固态硬盘插到 1999 年的 USB 1.1 接口上,它很可能也能正常工作。但是,任何连接的速度都取决于其中速度最慢的部分。所以,如果你把一块 40Gbps 的 USB 硬盘连接到电脑的 40Gbps USB 接口上,但你用的线缆只支持 5Gbps,那么你最终只能达到 5Gbps 的速度。

选购USB数据线时,务必查看其最大额定速度。市面上有很多数据线两端都采用USB-C接口,支持60瓦充电,但数据传输速度却只有USB 2.0的水平(480 Mbps)。

USB的历史

现在很难相信,通用串行总线(USB)已经存在超过二十年了。USB 的起源可以追溯到 20 世纪 90 年代中期,最初是 1996 年推出的 USB 1.0 规范。随后,1998 年推出了 USB 1.1,2001 年推出了 USB 2.0,2008 年推出了 USB 3.0。在 USB 2.0 发布二十年后,USB4 版本 2.0于 2022 年底发布,进一步提升了连接外设的传输速度。

USB 1.0/1.1(1996-1998 年)

USB 的出现正值电脑背面各种接口争夺用户注意力的时期。当时不仅有多个串行接口,还有并行接口(用于连接打印机)和 PS/2 接口(用于连接鼠标和键盘)。如果你玩游戏,就需要一个游戏接口来连接游戏手柄;而在商业环境中,用于连接外部存储设备的SCSI 接口也十分常见。 

USB 的开发初衷是作为所有这些端口的“通用”替代品,通过采用通用连接器简化我们连接设备的方式。

USB 1.0 于 1996 年问世,采用我们熟悉的 A 型接口,这种接口沿用至今。USB 1.0 标准提供 1.5 Mbps 的“低速”传输速率和 12 Mbps 的“全速”传输速率。该标准的第一个修订版 USB 1.1 于 1998 年发布。

USB 2.0(2000 年)

(图片来源:USB.org)

随着 2000 年 USB 2.0 标准的推出,USB 的普及真正开始加速,该标准将最大信号传输速率大幅提高到 480 Mbps,并获得了“高速”的称号。 

我们还见证了USB 2.0引入的USB On-The-Go规范,它允许带有USB接口的智能手机和平板电脑连接其他USB设备。例如,您可以将鼠标、U盘或数码相机插入三星平板电脑或谷歌Pixel手机的USB端口。

USB 3.0 / 3.1 / 3.2(2008 / 2013 / 2017

2008年,USB-IF推出了USB 3.0,其传输速度高达5Gbps,比之前的USB 2.0提升了约10倍。即使在今天,这仍然是大多数设备支持的最高速度。事实上,大多数外设只需要USB 2.0的速度即可。 

然而,USB-IF在2013年推出了USB 3.1,将速度提升到了一个全新的水平,信号传输速率再次翻倍至10 Gbps。最初的5 Gbps速度被重新命名为USB 3.1,而USB 3.1 Gen 2则指的是10 Gbps的速度。 

USB-C 于 2014 年推出,但该连接器版本多样,有的 USB-C 端口仅以 2.0 速度运行,有的则以 20 Gbps 甚至 40 Gbps 的速度运行。

随着 2017 年 USB 3.2 的推出,我们看到了 10 Gbps 和 20 Gbps 两个速度级别,两者都具备双通道功能。正是在那时,USB-IF 决定将所有 3.x 规范统一归入 USB 3.2,因此 USB 3.2(不区分 Gen 或 Gen 1)的速度为 5 Gbps,USB 3.2 Gen 2 的速度为 10 Gbps,而 USB 3.2 Gen 2×2 的速度为 20 Gbps。之所以称为 Gen 2×2,是因为它使用了两条 10 Gbps 的通道。

USB4 / USB4 v2.0

图片来源:USB.org)

USB4 于 2019 年发布,是 USB 标准的下一代产品,它仅使用 USB-C 接口(而非传统的 USB-A 接口)。USB4 的传输速度可选 20Gbps 或 40Gbps,其中 40Gbps 的速度相当于 Thunderbolt 3 或 Thunderbolt 4,因为 USB4 与 Thunderbolt 兼容。

数据传输采用两组四条双向通道。

DisplayPort Alt Mode 2.0 新增了对 8K 分辨率 60Hz 刷新率和 HDR10 色彩的支持。DisplayPort 2.0 最高可使用 80Gbps 的传输速度,因为所有八条数据通道都可用于单向向显示器发送数据。

USB4 v2.0于 2022 年夏季首次发布。与之前的几代升级一样,USB4 v2.0 将最大对称带宽翻了一番,这次从 40 Gbps 提升至 80 Gbps。但这还不是全部;USB4 v2.0 还提供了一种非对称模式(使用三个发送通道和一个接收通道),可将单向最大带宽提升至 120 Gbps。

USB4 v2.0 相较于 USB4 的另一项改进是支持 PAM-3 信号,这是对 NRZ 的升级。使用现有线缆,PAM-3 可以每个时钟周期传输更多比特。USB4 v2.0 还可以支持 PCIe 4.0 和 DisplayPort 2.1(相比 PCIe 3.0 和 DisplayPort 1.4a 有所提升),并且使用认证线缆时,支持最高 240 瓦的 Power Delivery 3.1 供电。

英特尔和苹果公司已经推出了控制器产品,其他一些公司的产品也正在研发或即将上市(例如 ASMedia、Via Labs)。

Step to UEFI (300)改写SMBIOS

通过 SMBIOS 识别当前系统型号是最简单的方法。

通常情况下 SMBIOS是直接写在BIOS中的,可以使用一些工具重新写入,但是因为 IBV 的差别,没有通用工具。

最近我忽然意识到,对于 UEFI 来说,这个信息是存储在内存中的,如果在启动之前直接修改内存,那么就可以实现修改的目的。

代码如下:

#include  <Uefi.h>
#include  <Library/UefiLib.h>
#include  <Library/ShellCEntryLib.h>
#include <Library/BaseMemoryLib.h>
#include <Protocol/Smbios.h>
#include <IndustryStandard/SmBios.h>
#include <Guid/SmBios.h>
#include <string.h>

extern EFI_BOOT_SERVICES         *gBS;
extern EFI_SYSTEM_TABLE			 *gST;
extern EFI_RUNTIME_SERVICES 	 *gRT;

/**
 * 获取 SMBIOS 字符串
 */
CHAR8*
GetSmbiosStringByOffset (
  IN UINT8  *StructureStart,
  IN UINT8  StructureLength,
  IN UINT8  StringNumber
  )
{
  CHAR8  *String;
  UINTN  Index;

  if (StringNumber == 0) {
    return NULL;
  }

  // 跳过结构体,到达字符串区域
  String = (CHAR8 *)(StructureStart + StructureLength);
  
  // 查找指定编号的字符串
  for (Index = 1; Index < StringNumber; Index++) {
    while (*String != 0) {
      String++;
    }
    String++;
    if (*String == 0) {
      return NULL; // 没有更多字符串
    }
  }

  return String;
}

/**
 * 获取下一个 SMBIOS 结构的地址
 */
UINT8*
GetNextSmbiosStructure (
  IN UINT8 *CurrentStructure
  )
{
  SMBIOS_STRUCTURE *Header;
  UINT8            *StringPtr;

  Header = (SMBIOS_STRUCTURE *)CurrentStructure;
  
  // 跳过结构体到字符串区域
  StringPtr = CurrentStructure + Header->Length;
  
  // 跳过所有字符串,直到遇到双 NULL 终止符
  while (!(StringPtr[0] == 0 && StringPtr[1] == 0)) {
    StringPtr++;
  }
  
  // 跳过双 NULL 终止符
  return StringPtr + 2;
}

/**
 * 处理 SMBIOS Type 1 (System Information)
 */
VOID
ProcessType1SystemInfo (
  IN UINT8 *Structure
  )
{
  SMBIOS_TABLE_TYPE1 *Type1;
  CHAR8              *Manufacturer;
  CHAR8              *ProductName;
  CHAR8              *Version;
  CHAR8              *SerialNumber;

  Type1 = (SMBIOS_TABLE_TYPE1 *)Structure;

  Print(L"=== System Information (Type 1) ===\n");
  
  Manufacturer = GetSmbiosStringByOffset(Structure, Type1->Hdr.Length, Type1->Manufacturer);
  if (Manufacturer != NULL) {
  }
  
  ProductName = GetSmbiosStringByOffset(Structure, Type1->Hdr.Length, Type1->ProductName);
  if (ProductName != NULL) {
    Print(L"Product Name: %a\n", ProductName);
	strcpy(ProductName, "lab-z.com");
  }

  Version = GetSmbiosStringByOffset(Structure, Type1->Hdr.Length, Type1->Version);
  if (Version != NULL) {
  }

  SerialNumber = GetSmbiosStringByOffset(Structure, Type1->Hdr.Length, Type1->SerialNumber);
  if (SerialNumber != NULL) {
  }

  Print(L"UUID: %g\n", &Type1->Uuid);
  Print(L"\n");

}

/**
 * 解析 SMBIOS 2.1 表
 */
EFI_STATUS
ParseSmbios21Table (
  IN SMBIOS_TABLE_ENTRY_POINT *Smbios21Entry
  )
{
  UINT8             *TableAddress;
  UINT8             *CurrentStructure;
  UINT8             *TableEnd;
  SMBIOS_STRUCTURE  *Header;
  UINTN             StructureCount = 0;

  Print(L"=== SMBIOS 2.1 Entry Point ===\n");
  Print(L"Anchor String: %.4a\n", Smbios21Entry->AnchorString);
  Print(L"Major Version: %d\n", Smbios21Entry->MajorVersion);
  Print(L"Minor Version: %d\n", Smbios21Entry->MinorVersion);
  Print(L"Table Length: %d bytes\n", Smbios21Entry->TableLength);
  Print(L"Table Address: 0x%x\n", Smbios21Entry->TableAddress);
  Print(L"Number of Structures: %d\n", Smbios21Entry->NumberOfSmbiosStructures);
  Print(L"\n");

  TableAddress = (UINT8 *)(UINTN)Smbios21Entry->TableAddress;
  TableEnd = TableAddress + Smbios21Entry->TableLength;
  CurrentStructure = TableAddress;

  // 遍历所有 SMBIOS 结构
  while (CurrentStructure < TableEnd && 
         StructureCount < Smbios21Entry->NumberOfSmbiosStructures) {
    Header = (SMBIOS_STRUCTURE *)CurrentStructure;
    
    // 检查是否到达表尾 (Type 127)
    if (Header->Type == 127) {
      break;
    }

    StructureCount++;
    
    //Print(L"Structure %d: Type %d, Length %d, Handle 0x%04x\n", 
    //      StructureCount, Header->Type, Header->Length, Header->Handle);

    // 处理特定类型的结构
    switch (Header->Type) {
       
      case 1:  // System Information
        ProcessType1SystemInfo(CurrentStructure);

        break;
        
      default:
        // 其他类型暂时只显示基本信息
        break;
    }

    // 移动到下一个结构
    CurrentStructure = GetNextSmbiosStructure(CurrentStructure);
  }

  Print(L"Total structures processed: %d\n", StructureCount);
  return EFI_SUCCESS;
}
int
EFIAPI
main (
  IN int Argc,
  IN CHAR16 **Argv
  )
{
	EFI_STATUS                  Status;
	SMBIOS_TABLE_ENTRY_POINT   	*SmbiosTable = NULL;


    Status = EfiGetSystemConfigurationTable (
               &gEfiSmbiosTableGuid,
               (VOID **)&SmbiosTable
               );
    if (!EFI_ERROR (Status)) {
		Print (L"SmbiosTable Version %d.%d\n",
			SmbiosTable->MajorVersion,
			SmbiosTable->MinorVersion);
			Print (L"SmbiosTable Address: %x \n",
						SmbiosTable->TableAddress);
			ParseSmbios21Table(SmbiosTable);
	} else {
		Print (L"Can't read SMBIOS\n");
	}
	  
  
  return EFI_SUCCESS;
}

基本流程:

  1. ParseSmbios21Table() 找到 SMBIOS Table
  2. switch (Header->Type) case 1: 确认是System Information ,交给ProcessType1SystemInfo()函数
  3. 这里面找到ProductName,用我们的字符串替代之前的

实体机上测试结果如下:

原本是 Lenovo 的小新:

修改为 lab-z.com 的

在使用的时候需要特别注意可能发生覆盖之前字符串的问题,不要溢出,不要破坏之前的结构。

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