ESP32 Arduino HTTPS 的研究

在物联网获得数据的过程中,HTTP 和 HTTPS 还是有蛮大区别的。

  1. 可以使用下面的Curl 命令测试
curl -X POST https://httpbin.org/post -H "Content-Type: application/json" -d '{"name": "test", "value": "123"}'

返回值

{
  "args": {},
  "data": "{name: test, value: 123}",
  "files": {},
  "form": {},
  "headers": {
    "Accept": "*/*",
    "Content-Length": "24",
    "Content-Type": "application/json",
    "Host": "httpbin.org",
    "User-Agent": "curl/8.7.0",
    "X-Amzn-Trace-Id": "Root=1-69ddf67f-75016f90629ad2556d1387f1"
  },
  "json": null,
  "origin": "117.143.53.223",
  "url": "https://httpbin.org/post"
}

2.编写 ESP32 Arduino 代码

#include <WiFi.h>
#include <WiFiClientSecure.h>

// 设置 Wi-Fi 凭据, 
const char* ssid = "你的 WIFI 名称";
const char* password = "对应的密码";

// 目标服务器信息
const char* server = "httpbin.org"; // 域名
const int port = 443; // HTTPS 默认端口
const char* path = "/post"; // 请求路径

// 创建安全客户端对象
WiFiClientSecure client;

void setup() {
  Serial.begin(115200);
  delay(1000);

  // 连接 Wi-Fi
  Serial.println("正在连接 Wi-Fi...");
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nWi-Fi 连接成功!");
  Serial.print("IP 地址: ");
  Serial.println(WiFi.localIP());

  // 设置 HTTPS 客户端(跳过证书验证,仅用于测试)
  client.setInsecure(); // 注意:生产环境中应使用证书验证

  // 发送 POST 请求
  sendPostRequest();
}

void loop() {
  // 主循环为空,仅执行一次请求
}

void sendPostRequest() {
  Serial.println("正在连接服务器...");
  if (!client.connect(server, port)) {
    Serial.println("连接服务器失败!");
    return;
  }
  Serial.println("服务器连接成功!");

  // 构建 POST 请求数据(JSON 格式)
  String postData = "{\"name\":\"test\",\"value\":\"123\"}";

  // 构建 HTTP 请求头
  String request = "POST " + String(path) + " HTTP/1.1\r\n";
  request += "Host: " + String(server) + "\r\n";
  request += "Content-Type: application/json\r\n";
  request += "Content-Length: " + String(postData.length()) + "\r\n";
  request += "Connection: close\r\n\r\n"; // 关闭连接
  request += postData;

  // 发送请求
  Serial.println("发送 POST 请求...");
  client.print(request);
  Serial.println("请求已发送!");

  // 等待并读取响应
  Serial.println("服务器响应:");
  unsigned long timeout = millis();
  while (client.connected() || client.available()) {
    if (client.available()) {
      String line = client.readStringUntil('\n');
      Serial.println(line);
    }
    // 超时处理
    if (millis() - timeout > 5000) {
      Serial.println("响应超时!");
      break;
    }
  }

  // 断开连接
  client.stop();
  Serial.println("连接已关闭。");
}

运行结果串口输出如下:

请求已发送!
服务器响应:
HTTP/1.1 200 OK

Date: Tue, 14 Apr 2026 08:21:05 GMT

Content-Type: application/json

Content-Length: 412

Connection: close

Server: gunicorn/19.9.0

Access-Control-Allow-Origin: *

Access-Control-Allow-Credentials: true



{
  "args": {}, 
  "data": "{\"name\":\"test\",\"value\":\"123\"}", 
  "files": {}, 
  "form": {}, 
  "headers": {
    "Content-Length": "29", 
    "Content-Type": "application/json", 
    "Host": "httpbin.org", 
    "X-Amzn-Trace-Id": "Root=1-69ddf8f1-007fb8cc07ce50765cddb2f1"
  }, 
  "json": {
    "name": "test", 
    "value": "123"
  }, 
  "origin": "117.143.53.223", 
  "url": "https://httpbin.org/post"
}
[  7009][E][ssl_client.cp

另外一个例子,看起来更容易理解一些:

#include <WiFi.h>
#include <HTTPClient.h>
#include <WiFiClientSecure.h>
#include <ArduinoJson.h>

// WiFi凭据
const char* ssid = "your_wifi_ssid";
const char* password = "your_wifi_password";

void setup() {
  Serial.begin(115200);
  
  // 连接WiFi
  WiFi.begin(ssid, password);
  Serial.print("连接WiFi");
  
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  
  Serial.println();
  Serial.println("WiFi连接成功!");
  Serial.print("IP地址: ");
  Serial.println(WiFi.localIP());
  
  // 发送HTTPS POST请求
  sendHttpsPostRequest();
}

void loop() {
  // 空循环
}

void sendHttpsPostRequest() {
  // 创建WiFiClientSecure对象用于HTTPS
  WiFiClientSecure *client = new WiFiClientSecure;
  
  if(client) {
    // 设置为不验证SSL证书(用于测试,生产环境建议验证证书)
    client->setInsecure();
    
    // 或者如果你想验证证书,可以设置根CA证书
    // client->setCACert(rootCACertificate);
    
    HTTPClient https;
    
    Serial.println("[HTTPS] 开始连接...");
    
    if (https.begin(*client, "https://httpbin.org/post")) {
      Serial.println("[HTTPS] 连接成功");
      
      // 设置HTTP头
      https.addHeader("Content-Type", "application/json");
      https.addHeader("Token", "abc");
      
      // 准备JSON数据
      String jsonPayload = "{\"cityCode\":\"310100\",\"isGetStopArrive\":\"1\",\"lon\":\"121.3747863\",\"lat\":\"31.11027359\",\"limit\":20,\"offset\":0,\"coordinateType\":1}";
      
      Serial.println("发送HTTPS POST请求到: https://httpbin.org/post");
      Serial.println("请求头: Token=abc");
      Serial.println("请求体: " + jsonPayload);
      
      // 发送POST请求
      int httpResponseCode = https.POST(jsonPayload);
      
      if (httpResponseCode > 0) {
        String response = https.getString();
        Serial.println("HTTPS响应代码: " + String(httpResponseCode));
        Serial.println("响应内容:");
        Serial.println(response);
      } else {
        Serial.println("HTTPS请求失败");
        Serial.println("错误代码: " + String(httpResponseCode));
        Serial.println("错误信息: " + https.errorToString(httpResponseCode));
      }
      
      // 关闭连接
      https.end();
    } else {
      Serial.println("[HTTPS] 无法连接");
    }
    
    // 删除客户端对象
    delete client;
  } else {
    Serial.println("无法创建WiFiClientSecure客户端");
  }
}

Step to UEFI (303)更改 Memmap

之前提到过,UEFI 上没有 E820 ,而是通过gBS->GetMemoryMap() 来获得当前内存分配情况,这里给出了一个驱动代码,可以在UEFI Shell 下加载,他会替换之前的GetMemoryMap() 函数,基于之前的返回值增加一个条目:0xFED00000-0xFED1FFFF ,报告为 available 。

HookMemoryMapDxe.c

/** @file
  Memory Map Hook DXE Driver
  Hooks GetMemoryMap to add a fake memory region
**/

#include <Uefi.h>
#include <Library/UefiDriverEntryPoint.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/MemoryAllocationLib.h>
#include <Library/BaseMemoryLib.h>
#include <Library/DebugLib.h>

// Hook Protocol GUID
#define MEMORY_MAP_HOOK_PROTOCOL_GUID \
  { 0x87654321, 0x4321, 0x8765, { 0xab, 0xcd, 0xef, 0x12, 0x34, 0x56, 0x78, 0x90 } }

EFI_GUID gMemoryMapHookProtocolGuid = MEMORY_MAP_HOOK_PROTOCOL_GUID;

// Hook数据结构
typedef struct {
  UINT32                Signature;
  EFI_GET_MEMORY_MAP    OriginalGetMemoryMap;
  EFI_PHYSICAL_ADDRESS  FakeMemoryStart;
  UINT64                FakeMemoryPages;
  BOOLEAN               HookEnabled;
} MEMORY_MAP_HOOK_DATA;

#define HOOK_SIGNATURE  SIGNATURE_32('H','M','A','P')

// 全局变量
STATIC MEMORY_MAP_HOOK_DATA *gHookData = NULL;
STATIC EFI_HANDLE gProtocolHandle = NULL;
STATIC EFI_EVENT gExitBootServicesEvent = NULL;

/**
  Hooked GetMemoryMap function
**/
EFI_STATUS
EFIAPI
HookedGetMemoryMap (
  IN OUT UINTN                  *MemoryMapSize,
  IN OUT EFI_MEMORY_DESCRIPTOR  *MemoryMap,
  OUT UINTN                     *MapKey,
  OUT UINTN                     *DescriptorSize,
  OUT UINT32                    *DescriptorVersion
  )
{
  EFI_STATUS                Status;
  UINTN                     OriginalMapSize;
  EFI_MEMORY_DESCRIPTOR     *OriginalMap;
  UINTN                     NewMapSize;
  UINTN                     EntryCount;
  UINTN                     Index;
  BOOLEAN                   NeedToAddEntry = TRUE;
  
  // 检查Hook是否启用
  if (gHookData == NULL || !gHookData->HookEnabled || gHookData->OriginalGetMemoryMap == NULL) {
    return EFI_UNSUPPORTED;
  }
  
  // 如果MemoryMap为NULL,只是查询大小
  if (MemoryMap == NULL) {
    Status = gHookData->OriginalGetMemoryMap(MemoryMapSize, MemoryMap, MapKey, DescriptorSize, DescriptorVersion);
    if (Status == EFI_BUFFER_TOO_SMALL) {
      // 为新增的条目预留空间
      *MemoryMapSize += *DescriptorSize;
    }
    return Status;
  }
  
  // 分配临时缓冲区
  OriginalMapSize = *MemoryMapSize + *DescriptorSize;
  OriginalMap = AllocatePool(OriginalMapSize);
  if (OriginalMap == NULL) {
    return EFI_OUT_OF_RESOURCES;
  }
  
  // 调用原始的GetMemoryMap
  Status = gHookData->OriginalGetMemoryMap(&OriginalMapSize, OriginalMap, MapKey, DescriptorSize, DescriptorVersion);
  if (EFI_ERROR(Status)) {
    FreePool(OriginalMap);
    return Status;
  }
  
  EntryCount = OriginalMapSize / *DescriptorSize;
  
  // 检查是否已经存在重叠的内存区域
  for (Index = 0; Index < EntryCount; Index++) {
    EFI_MEMORY_DESCRIPTOR *Entry = (EFI_MEMORY_DESCRIPTOR *)((UINT8 *)OriginalMap + Index * (*DescriptorSize));
    EFI_PHYSICAL_ADDRESS EntryStart = Entry->PhysicalStart;
    EFI_PHYSICAL_ADDRESS EntryEnd = EntryStart + (Entry->NumberOfPages * EFI_PAGE_SIZE) - 1;
    
    // 检查是否与新条目重叠
    if ((gHookData->FakeMemoryStart >= EntryStart && gHookData->FakeMemoryStart <= EntryEnd) ||
        (gHookData->FakeMemoryStart + (gHookData->FakeMemoryPages * EFI_PAGE_SIZE) - 1 >= EntryStart && 
         gHookData->FakeMemoryStart + (gHookData->FakeMemoryPages * EFI_PAGE_SIZE) - 1 <= EntryEnd)) {
      NeedToAddEntry = FALSE;
      break;
    }
  }
  
  // 计算新的内存映射大小
  if (NeedToAddEntry) {
    NewMapSize = OriginalMapSize + *DescriptorSize;
  } else {
    NewMapSize = OriginalMapSize;
  }
  
  // 检查提供的缓冲区是否足够大
  if (*MemoryMapSize < NewMapSize) {
    *MemoryMapSize = NewMapSize;
    FreePool(OriginalMap);
    return EFI_BUFFER_TOO_SMALL;
  }
  
  // 复制原始内存映射
  CopyMem(MemoryMap, OriginalMap, OriginalMapSize);
  
  // 如果需要,添加新的内存条目
  if (NeedToAddEntry) {
    EFI_MEMORY_DESCRIPTOR *NewEntry = (EFI_MEMORY_DESCRIPTOR *)((UINT8 *)MemoryMap + OriginalMapSize);
    
    NewEntry->Type = EfiConventionalMemory;  // Available memory
    NewEntry->PhysicalStart = gHookData->FakeMemoryStart;
    NewEntry->VirtualStart = 0;
    NewEntry->NumberOfPages = gHookData->FakeMemoryPages;
    NewEntry->Attribute = EFI_MEMORY_WB;  // Write-back cacheable
  }
  
  *MemoryMapSize = NewMapSize;
  FreePool(OriginalMap);
  
  return EFI_SUCCESS;
}

/**
  ExitBootServices event handler
**/
VOID
EFIAPI
ExitBootServicesNotify (
  IN EFI_EVENT  Event,
  IN VOID       *Context
  )
{
  // 在ExitBootServices之前恢复原始函数指针
  if (gHookData != NULL && gHookData->HookEnabled && gHookData->OriginalGetMemoryMap != NULL) {
    gBS->GetMemoryMap = gHookData->OriginalGetMemoryMap;
    
    // 更新CRC32
    gBS->Hdr.CRC32 = 0;
    gBS->CalculateCrc32((UINT8 *)gBS, gBS->Hdr.HeaderSize, &gBS->Hdr.CRC32);
    
    gHookData->HookEnabled = FALSE;
  }
}

/**
  Driver entry point
  
  @param ImageHandle     The image handle
  @param SystemTable     The system table
  
  @retval EFI_SUCCESS    Driver loaded successfully
**/
EFI_STATUS
EFIAPI
HookMemoryMapDxeEntry (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
  EFI_STATUS Status;
  
  DEBUG((DEBUG_INFO, "HookMemoryMapDxe: Driver starting...\n"));
  
  // 分配Hook数据结构
  gHookData = AllocateRuntimeZeroPool(sizeof(MEMORY_MAP_HOOK_DATA));
  if (gHookData == NULL) {
    DEBUG((DEBUG_ERROR, "HookMemoryMapDxe: Failed to allocate hook data\n"));
    return EFI_OUT_OF_RESOURCES;
  }
  
  // 初始化Hook数据
  gHookData->Signature = HOOK_SIGNATURE;
  gHookData->OriginalGetMemoryMap = gBS->GetMemoryMap;
  gHookData->FakeMemoryStart = 0xFED00000;  // 0xFED00000-0xFED1FFFF
  gHookData->FakeMemoryPages = 0x20000 / EFI_PAGE_SIZE; // 128KB = 32 pages
  gHookData->HookEnabled = FALSE;
  
  // 安装Protocol (用于标识和查找)
  Status = gBS->InstallProtocolInterface(
    &gProtocolHandle,
    &gMemoryMapHookProtocolGuid,
    EFI_NATIVE_INTERFACE,
    gHookData
  );
  
  if (EFI_ERROR(Status)) {
    DEBUG((DEBUG_ERROR, "HookMemoryMapDxe: Failed to install protocol: %r\n", Status));
    FreePool(gHookData);
    gHookData = NULL;
    return Status;
  }
  
  // 创建ExitBootServices事件
  Status = gBS->CreateEvent(
    EVT_SIGNAL_EXIT_BOOT_SERVICES,
    TPL_NOTIFY,
    ExitBootServicesNotify,
    NULL,
    &gExitBootServicesEvent
  );
  
  if (EFI_ERROR(Status)) {
    DEBUG((DEBUG_WARN, "HookMemoryMapDxe: Failed to create ExitBootServices event: %r\n", Status));
    // 不是致命错误,继续执行
  }
  
  // 安装Hook
  gBS->GetMemoryMap = HookedGetMemoryMap;
  
  // 更新CRC32
  gBS->Hdr.CRC32 = 0;
  gBS->CalculateCrc32((UINT8 *)gBS, gBS->Hdr.HeaderSize, &gBS->Hdr.CRC32);
  
  // 启用Hook
  gHookData->HookEnabled = TRUE;
  
  DEBUG((DEBUG_INFO, "HookMemoryMapDxe: Driver loaded successfully\n"));
  DEBUG((DEBUG_INFO, "HookMemoryMapDxe: Hook installed and enabled\n"));
  DEBUG((DEBUG_INFO, "HookMemoryMapDxe: Fake memory region: 0x%lx-0x%lx\n", 
        gHookData->FakeMemoryStart,
        gHookData->FakeMemoryStart + (gHookData->FakeMemoryPages * EFI_PAGE_SIZE) - 1));
  
  return EFI_SUCCESS;
}

HookMemoryMapDxe.inf


这个代码需要放置在MdeModulePkg下面,然后在MdeModulePkg.dsc添加:

[PcdsDynamicExDefault]
  gEfiMdeModulePkgTokenSpaceGuid.PcdRecoveryFileName|L"FVMAIN.FV"

[Components]
  MdeModulePkg/HookMemoryMapDxe/HookMemoryMapDxe.inf
  MdeModulePkg/Application/HelloWorld/HelloWorld.inf
  MdeModulePkg/Application/DumpDynPcd/DumpDynPcd.inf
  MdeModulePkg/Application/MemoryProfileInfo/MemoryProfileInfo.inf

之后,使用下面的命令进行编译即可:

build -a X64 -p MdeModulePkg/MdeModulePkg.dsc -t VS2019

完整的代码下载:

使用时,只需要在 UEFI Shell 下 load HookMemoryMapDxe.efi 即可,之后可以使用 memmap 命令看到改动结果。

特别注意:这里只是一个Demo ,实际使用时需要根据你的需求进行修改。

pbatard 的轻量级UEFI 编译环境

pbatard 在GitHub 上提供了一个轻量级UEFI 编译环境(https://github.com/pbatard/uefi-simple),用户可以直接在 Visual Studio 中直接创建项目,然后使用 GNU-EFI 来进行编译。

下载之后可以看到项目提供了一个 VC 的工程文件作为例子,可以使用 VS2019 直接打开。

打开之后编译会发生错误,因为这个项目是 VS2022 的。修改的方法是在项目中搜索所有的 V143 字样,修改为 V142

比如,uefi-simple.vs\msvc\uefi-simple.vcxproj 中的下面<PlatformToolset> 中给出的

  &lt;PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
    &lt;ConfigurationType>Application&lt;/ConfigurationType>
    &lt;UseDebugLibraries>true&lt;/UseDebugLibraries>
    &lt;PlatformToolset>v143&lt;/PlatformToolset>
    &lt;CharacterSet>Unicode&lt;/CharacterSet>
  &lt;/PropertyGroup>

另外还要修改项目的属性中, Platform Toolset 为 v142

修改之后就可以直接 Build 出来需要的 EFI 文件了(main.c)

在模拟器中运行结果如下:

修改后的,能直接在 VS2019 下编译的可以在这里下载

DS3231 中断引脚问题

仍然使用之前提到的模块 https://www.lab-z.com/ds3231165/ 。这个模块已经有内部上拉了,因此中断 Pin 无需再额外上拉。

这次遇到的问题是中断经常触发,打开Alarm1 后就立即触发。经过研究发现这是因为没有清除A1F 导致的,之前触发过Alarm1 中断后这个Flag 就会一直处于 1(触发状态)。当再次Enable Alarm1 后,就会继续拉低 Interrupt

解决方法是在关闭 Alarm 1的时候多做一个动作:

  // 特别注意必须用下面函数清除 Interrupt Flag 否则只要 Enable Alarm 就会触发
      myRTC.checkIfAlarm(1);
  // 关闭 Alarm1
      myRTC.turnOffAlarm(1);

完整的测试代码如下:

#include <Wire.h>
#include <Arduino.h>

#include <WiFi.h>
#include <WiFiMulti.h>

#include <HTTPClient.h>
#include <ArduinoJson.h>

#include <DS3231.h>

DS3231 myRTC;
RTClib CurrentTime;

WiFiMulti wifiMulti;
JsonDocument doc;

const char* ssid = "CMCC-TSR6739";
const char* password = "!!1783az";

void showdate() {
  bool century;
  bool h12Flag;
  bool pmFlag;
  Serial.print(myRTC.getYear(), DEC);
  Serial.print("-");
  Serial.print(myRTC.getMonth(century), DEC);
  Serial.print("-");
  Serial.print(myRTC.getDate(), DEC);
  Serial.print(" ");
  Serial.print(myRTC.getDoW(), DEC);
  Serial.print(" ");
  Serial.print(myRTC.getHour(h12Flag, pmFlag), DEC); //24-hr
  Serial.print(":");
  Serial.print(myRTC.getMinute(), DEC);
  Serial.print(":");
  Serial.println(myRTC.getSecond(), DEC);
}

void setup() {
  wifiMulti.addAP(ssid, password);

  Serial.begin(115200);
  Serial.println("Start");
  Wire.begin();
  delay(100);

  myRTC.setClockMode(false);  // set to 24h

  pinMode(14, INPUT_PULLUP);
}

// 设置 seconds 秒后的闹钟
void SetAlart(uint32_t seconds) {
  DateTime Current = CurrentTime.now();

  printf("%d\n", Current.unixtime());
  printf("%d %d %d %d %d %d \n",
         Current.year(), Current.month(), Current.day(),
         Current.hour(), Current.minute(), Current.second());

  Current = DateTime(Current.unixtime() + seconds);
  printf("%d\n", Current.unixtime());

  printf("%d %d %d %d %d %d \n",
         Current.year(), Current.month(), Current.day(),
         Current.hour(), Current.minute(), Current.second());

  // 1分钟后触发
  myRTC.setA1Time(Current.day(), Current.hour(), Current.minute(), Current.second(), 0, false, false, false);
  // myRTC.setA1Time(Current.day(), Current.hour(), Current.minute(), Current.second()+20, 0, false, false, false);
  // 打开闹钟
  myRTC.turnOnAlarm(1);

}

uint8_t geti2c(uint8_t reg) {
  Wire.beginTransmission(0x68);
  Wire.write(reg);
  Wire.endTransmission();

  Wire.requestFrom(0x68, 1);
  return Wire.read();
}


unsigned long char_to_uint32(const char* t) {
  unsigned long result = 0;
  for (int i = 0; i < 14; i++) {
    if (t[i] >= '0' && t[i] <= '9') {
      result = result * 10 + (t[i] - '0');
    }
  }
  result = result + 8 * 3600;
  return result;
}

void loop() {



  while (Serial.available()) {
    char c = Serial.read();
    if (c == '1') {
      // 从互联网取得当前时间
      Serial.print("测试:从互联网获得当前时间, 并且设置给 RTC:");
      // 从互联网取得时间
      if ((wifiMulti.run() == WL_CONNECTED)) {
        HTTPClient http;
        //http.begin("http://quan.suning.com/getSysTime.do"); //HTTP
        http.begin("http://api.k780.com/?app=life.time&appkey=10003&sign=b59bc3ef6191eb9f747dd4e83c99f2a4&format=json");
        int httpCode = http.GET();

        // httpCode will be negative on error
        if (httpCode > 0) {
          String payload = http.getString();
          // 解析JSON数据
          DeserializationError error = deserializeJson(doc, payload);

          Serial.println(doc["result"]["timestamp"].as<const char*>());
          myRTC.adjust(char_to_uint32(doc["result"]["timestamp"].as<const char*>()));

        } else {
          Serial.println("Http 访问出错");
        }
        http.end();
      } else {
        Serial.println("WIFI 未连接");
      }
      Serial.print("测试结束");
    }

    if (c == '2') {
      // 显示当前时间
      showdate();
    }
    if (c == '3') {
      // 设置 20秒后触发中断,对应引脚会变低
      SetAlart(20);
    }
    if (c == '4') {
      // 关闭闹钟
      Serial.println("TurnOff Alarm");
      // 特别注意必须用下面函数清除 Interrupt Flag 否则只要 Enable Alarm 就会触发
      myRTC.checkIfAlarm(1);
      // 关闭 Alarm1
      myRTC.turnOffAlarm(1);
    }

    if (c == '5') {
      // 检查闹钟状态
      if (myRTC.checkAlarmEnabled(1)) {
        Serial.println("闹钟 Enabled");
        if (myRTC.checkIfAlarm(1, false)) {
          Serial.println("闹钟已触发");
        } else {
          Serial.println("闹钟未触发");
        }
      } else {
        Serial.println("闹钟 Disabled");
      }
    }

    if (c == '6') {
      // 输出当前闹钟设定的时间
      byte alarmDay, alarmHour, alarmMinute, alarmSecond, alarmBits;
      bool alarmDy, alarmH12Flag, alarmPmFlag;
      // Display Alarm 1 information
      myRTC.getA1Time(alarmDay, alarmHour, alarmMinute, alarmSecond, alarmBits, alarmDy, alarmH12Flag, alarmPmFlag);
      Serial.print(alarmDay, DEC);
      if (alarmDy) {
        Serial.print(" DoW");
      } else {
        Serial.print(" Date");
      }
      Serial.print(' ');
      Serial.print(alarmHour, DEC);
      Serial.print(' ');
      Serial.print(alarmMinute, DEC);
      Serial.print(' ');
      Serial.print(alarmSecond, DEC);
      Serial.print(' ');
      if (alarmH12Flag) {
        if (alarmPmFlag) {
          Serial.print("pm ");
        } else {
          Serial.print("am ");
        }
      }
      Serial.printf("alarmBits:%x %x \n", alarmBits, alarmDy);
      Serial.println(' ');
    }

    if (c == '7') {
      // 寄存器检测
      printf("0x07: %d\n", geti2c(0x07));
      printf("0x08: %d\n", geti2c(0x08));
      printf("0x09: %d\n", geti2c(0x09));
      printf("0x0A: %d\n", geti2c(0x0A));
      printf("0x0E: %d\n", geti2c(0x0E));
      printf("0x0F: %d\n", geti2c(0x0F));
    }

  }

}

Ch398:USB千兆网络方案的新选择

作为BIOS工程师,单纯的 BIOS问题不多,更多的时间是在处理各种兼容性问题。BIOS最开始的目标就是用于统一硬件接口,以便软件和操作系统控制硬件。自然而然,客户遇到问题时首先会想到找BIOS工程师。

前一段时间,客户提出一个关于USB 千兆网卡的性能问题,为此我特地研究了网卡传输性能评估方法。正好最近听说WCH 推出了一款USB千兆网卡芯片:Ch398,于是申请样品进行简单的测试。这款产品和 Realtek 的 RTL8153 是Pin2Pin兼容的。

根据官网介绍,这款方案的特点如下:

  • 单芯片USB3.0转10M/100M/1000M以太网,集成USB PHY和以太网PHY
  • 支持CDC-NCM协议和CDC-ECM协议及RNDIS协议,免安装驱动程序或可选厂商驱动程序
  • 支持10Mbps、100Mbps和1000Mbps速率,兼容IEEE 802.3、IEEE 802.3u和IEEE 802.3ab
  • 支持10BASE-T、100BASE-TX、1000BASE-T及自动协商
  • 支持1000BASE-T标准CAT5E、CAT6双绞线超百米传输距离
  • 支持IEEE 802.3az节能协议
  • 支持USB3.2 Gen1,向前兼容USB3.1、USB3.0、USB2.1、USB2.0、USB1.1协议规范
  • 支持LPM(Link Power Management),支持符合USB3.2 Gen1协议规范的U1/U2/U3以及符合USB2.1协议规范的L1/L2电源管理模式
  • 内置TX/RX封包缓冲
  • 支持IPv4/IPv6封包校验,支持IPv4 TCP/UDP/HEAD和IPv6 TCP/UDP封包校验生成和检查
  • 支持IPV4/IPV6的TCP大包分片
  • 支持IEEE 802.3x流量控制和半双工冲突压力回退流量控制
  • 支持IEEE 802.3Q VLAN标记
  • 支持巨型帧传输
  • 支持休眠模式和低功耗的睡眠模式,支持网络低功耗配置,支持动态电源管理
  • 支持通过魔术包和网络唤醒包等事件进行远程唤醒
  • 内置以太网收发器支持Auto-MDIX自动交换TX/RX,自动识别正负信号线
  • 支持LED闪烁频率和占空比配置
  • 内置LDO调压器和DC-DC降压器,支持单一5V或者单一3.3V供电,外围精简
  • 支持25MHz无源晶振或外置时钟输入
  • 工业级温度范围:-40~85℃
  • 提供QFN40封装形式

和官方申请了一块Ch398开发板,上面的芯片远比我想象中要小,外围零件也非常少。

拿到手之后插入电脑即可使用,Win11已经内置了驱动无需额外安装,这点有点让我出乎预料。

接下来对这个网卡的传输性能通过简单的试验进行评估,测试环境是单网线双机互联,测试使用的是 Intel 最新一代平台 Panther Lake Ultra 325)。

1.共享目录文件拷贝测试

一端设置了共享目录,其中使用工具生成了一个 100G 的文件从另外的机器进行访问。这是最简单的,不用第三方工具即可进行的测速方法。

下面是测试的视频可以看到传输速度能够始终维持在 110MB/S以上,同时芯片摸起来并不是特别烫。

下面是一个时长16分钟的视频,展示了完整的传输过程:

比较有意思的是,使用共享文件测试的网速会受到系统硬盘性能的影响,比如,视频后期可以从Task Manager中看到,因为硬盘占用率出现波动,传输速度也受到了影响。我猜测这是因为SSD的缓冲耗尽,所以前期硬盘工作正常,传输后期出现写入性能的下降。如果你在工作中遇到这样的问题,不妨考虑一下硬盘的因素。此外,传输速度的稳定性还可能受到 Power Mode 策略和CPU性能的影响。

2.第三方网络测试工具

使用  iperf3(官网 https://iperf.fr/)

第一步,在服务器端运行 iperf3.exe -s

服务器端工作的截图

第二步,在被测机端运行 iperf3.exe -c 192.168.0.4 -b -1000m -u

被测试机器端运行后的截图

通常测试中,第一个数据包并不能准确反映实际速度,这里可以通过 -O 1 参数强制忽略,例如:

这里可以看到测试得到的速度会比之前的测试有所提升。

完整测试的工作视频如下:

可以看到,这种测试方法传输速度上比前面的共享文件会快一些,同时因为不依赖硬盘性能会使得测试更加单纯,同时这款软件能够生成简单的报告方便进一步评估。

作为对比,我同样测试了一下RTL8153性能,在Windows 文件共享拷贝环节,偶然会发生速度掉到900Mbps 下的情况,整体来看传输速度似乎对硬盘的性能更加依赖。

同样的使用 iperf 测试, RTL8153性能能够达到 Ch398 水平。

这个测试更加证明了Windows 共享拷贝文件测试网速,会受到网速之外的其他因素。如果有条件,尽量使用 iperf 进行测试。

这里可以看到 Ch398作为国产USB千兆网络方案,性能完全可以满足传输需求。此外,国产芯片更容易得到技术支持,双方在沟通上不会存在障碍,这也是选择国产芯片的充足理由。

如果在设计上有USB扩展千兆网络的需求,不妨考虑Ch398这款国产芯片。

推荐一个 PDF 处理工具

很多时候,我们需要对 PDF 文件进行一些简单的处理,诸如:拆分合并加水印等等。通常情况下,在线的 PDF 工具已经足够应付。但是如果考虑到隐私保密和速度的问题,本地工具会更合适一些。

最近我有一个将扫描后的 PDF 合并的需求,最后使用了PDFtk 这个工具,网站是 https://www.pdflabs.com/tools/pdftk-the-pdf-toolkit/

有兴趣的朋友可以试试。

BCDEDIT 创建一个 WinDBG 选项

本文介绍通过 BCDEDIT 创建一个 Windows 启动选项,这个选项中打开 WinDBG 功能。

根据当前已有启动项拷贝出来一个名为 “WinDBG”的启动项, 这个命令运行之后会生成一个 GUID,需要记下来

bcdedit /copy {current} /d "windbg"

设置启动超时 45秒

bcdedit /timeout 45

设置打开 Debug 功能

bcdedit /debug {前面的 GUID} on

设置使用 USB Debug

bcedit /dbgsettings usb targetname:labz

指定 XHCI

bcdedit /set "{dbgssettings}" busparams 0.20.0

这样,每次启动的时候都会出现一个菜单,选择第一项会正常启动Windows,选择第二项即可启动能够通过 WinDBG 调试的 Windows

TIPS:一种C语言奇怪的数组引用方式

最近看到了一种奇怪的写法,例如下面的代码中:Print(L”%d “,i[segM]); 这个语句,实际上是Print(L”%d “,segM[i]) 的意思。

#include  &lt;Uefi.h>
#include  &lt;Library/UefiLib.h>
#include  &lt;Library/ShellCEntryLib.h>

UINT8 segM[]={
	0xAA,0xBB,0xCC,0xDD,0xEE,0xFF
};

INTN
EFIAPI
ShellAppMain (
  IN UINTN Argc,
  IN CHAR16 **Argv
  )
{
	;
	for (UINT8 i=0;i&lt;6;i++) {
		Print(L"%d ",i[segM]);
	}
  return(0);
}

解释说数组访问操作符 [] 是‌对称的‌,即:

a[b] == b[a]

当然了,我个人是非常不建议你在代码中使用上面的方法的。

好用的C语言分析工具 CTags

CTags 是一款开源的 C语言分析工具,项目地址是:

https://github.com/universal-ctags/ctags

使用这个工具可以分析C语言,例如:

ctags C:/BuildBs/edk2202302/edk2/OvmfPkg/Bhyve/PlatformPei/MemDetect.c

可以得到下面的结果;

!_TAG_EXTRA_DESCRIPTION	anonymous	/Include tags for non-named objects like lambda/
!_TAG_EXTRA_DESCRIPTION	fileScope	/Include tags of file scope/
!_TAG_EXTRA_DESCRIPTION	pseudo	/Include pseudo tags/
!_TAG_EXTRA_DESCRIPTION	subparser	/Include tags generated by subparsers/
!_TAG_FIELD_DESCRIPTION	epoch	/the last modified time of the input file (only for F\/file kind tag)/
!_TAG_FIELD_DESCRIPTION	file	/File-restricted scoping/
!_TAG_FIELD_DESCRIPTION	input	/input file/
!_TAG_FIELD_DESCRIPTION	name	/tag name/
!_TAG_FIELD_DESCRIPTION	pattern	/pattern/
!_TAG_FIELD_DESCRIPTION	typeref	/Type and name of a variable or typedef/
!_TAG_FILE_FORMAT	2	/extended format; --format=1 will not append ;" to lines/
!_TAG_FILE_SORTED	1	/0=unsorted, 1=sorted, 2=foldcase/
!_TAG_KIND_DESCRIPTION!C	d,macro	/macro definitions/
!_TAG_KIND_DESCRIPTION!C	e,enumerator	/enumerators (values inside an enumeration)/
!_TAG_KIND_DESCRIPTION!C	f,function	/function definitions/
!_TAG_KIND_DESCRIPTION!C	g,enum	/enumeration names/
!_TAG_KIND_DESCRIPTION!C	h,header	/included header files/
!_TAG_KIND_DESCRIPTION!C	m,member	/struct, and union members/
!_TAG_KIND_DESCRIPTION!C	s,struct	/structure names/
!_TAG_KIND_DESCRIPTION!C	t,typedef	/typedefs/
!_TAG_KIND_DESCRIPTION!C	u,union	/union names/
!_TAG_KIND_DESCRIPTION!C	v,variable	/variable definitions/
!_TAG_OUTPUT_EXCMD	mixed	/number, pattern, mixed, or combineV2/
!_TAG_OUTPUT_FILESEP	slash	/slash or backslash/
!_TAG_OUTPUT_MODE	u-ctags	/u-ctags or e-ctags/
!_TAG_OUTPUT_VERSION	1.1	/current.age/
!_TAG_PARSER_VERSION!C	2.2	/current.age/
!_TAG_PATTERN_LENGTH_LIMIT	96	/0 for no limit/
!_TAG_PROC_CWD	C:/Users/yanbwang/Downloads/ctags/	//
!_TAG_PROGRAM_AUTHOR	Universal Ctags Team	//
!_TAG_PROGRAM_NAME	Universal Ctags	/Derived from Exuberant Ctags/
!_TAG_PROGRAM_URL	https://ctags.io/	/official site/
!_TAG_PROGRAM_VERSION	6.2.0	/p6.2.20260125.0/
!_TAG_ROLE_DESCRIPTION!C!function	foreigncall	/called in foreign languages/
!_TAG_ROLE_DESCRIPTION!C!function	foreigndecl	/declared in foreign languages/
!_TAG_ROLE_DESCRIPTION!C!header	local	/local header/
!_TAG_ROLE_DESCRIPTION!C!header	system	/system header/
!_TAG_ROLE_DESCRIPTION!C!macro	undef	/undefined/
!_TAG_ROLE_DESCRIPTION!C!struct	foreigndecl	/declared in foreign languages/
AddressWidthInitialization	C:/BuildBs/edk2202302/edk2/OvmfPkg/Bhyve/PlatformPei/MemDetect.c	/^AddressWidthInitialization ($/;"	f	typeref:typename:VOID
GetFirstNonAddress	C:/BuildBs/edk2202302/edk2/OvmfPkg/Bhyve/PlatformPei/MemDetect.c	/^GetFirstNonAddress ($/;"	f	typeref:typename:STATIC UINT64
GetPeiMemoryCap	C:/BuildBs/edk2202302/edk2/OvmfPkg/Bhyve/PlatformPei/MemDetect.c	/^GetPeiMemoryCap ($/;"	f	typeref:typename:STATIC UINT32
GetSystemMemorySizeAbove4gb	C:/BuildBs/edk2202302/edk2/OvmfPkg/Bhyve/PlatformPei/MemDetect.c	/^GetSystemMemorySizeAbove4gb ($/;"	f	typeref:typename:STATIC UINT64
GetSystemMemorySizeBelow4gb	C:/BuildBs/edk2202302/edk2/OvmfPkg/Bhyve/PlatformPei/MemDetect.c	/^GetSystemMemorySizeBelow4gb ($/;"	f	typeref:typename:UINT32
InitializeRamRegions	C:/BuildBs/edk2202302/edk2/OvmfPkg/Bhyve/PlatformPei/MemDetect.c	/^InitializeRamRegions ($/;"	f	typeref:typename:VOID
PublishPeiMemory	C:/BuildBs/edk2202302/edk2/OvmfPkg/Bhyve/PlatformPei/MemDetect.c	/^PublishPeiMemory ($/;"	f	typeref:typename:EFI_STATUS
Q35TsegMbytesInitialization	C:/BuildBs/edk2202302/edk2/OvmfPkg/Bhyve/PlatformPei/MemDetect.c	/^Q35TsegMbytesInitialization ($/;"	f	typeref:typename:VOID
QemuInitializeRam	C:/BuildBs/edk2202302/edk2/OvmfPkg/Bhyve/PlatformPei/MemDetect.c	/^QemuInitializeRam ($/;"	f	typeref:typename:STATIC VOID
mPhysMemAddressWidth	C:/BuildBs/edk2202302/edk2/OvmfPkg/Bhyve/PlatformPei/MemDetect.c	/^UINT8  mPhysMemAddressWidth;$/;"	v	typeref:typename:UINT8
mQ35SmramAtDefaultSmbase	C:/BuildBs/edk2202302/edk2/OvmfPkg/Bhyve/PlatformPei/MemDetect.c	/^BOOLEAN  mQ35SmramAtDefaultSmbase = FALSE;$/;"	v	typeref:typename:BOOLEAN
mQ35TsegMbytes	C:/BuildBs/edk2202302/edk2/OvmfPkg/Bhyve/PlatformPei/MemDetect.c	/^STATIC UINT16  mQ35TsegMbytes;$/;"	v	typeref:typename:STATIC UINT16
mS3AcpiReservedMemoryBase	C:/BuildBs/edk2202302/edk2/OvmfPkg/Bhyve/PlatformPei/MemDetect.c	/^STATIC UINT32  mS3AcpiReservedMemoryBase;$/;"	v	typeref:typename:STATIC UINT32
mS3AcpiReservedMemorySize	C:/BuildBs/edk2202302/edk2/OvmfPkg/Bhyve/PlatformPei/MemDetect.c	/^STATIC UINT32  mS3AcpiReservedMemorySize;$/;"	v	typeref:typename:STATIC UINT32

类似的,可以直接分析取得C语言中的函数名,命令是:

ctags --c-kinds=f C:/BuildBs/edk2202302/edk2/OvmfPkg/Bhyve/PlatformPei/MemDetect.c

输出结果:

AddressWidthInitialization	C:/BuildBs/edk2202302/edk2/OvmfPkg/Bhyve/PlatformPei/MemDetect.c	/^AddressWidthInitialization ($/;"	f	typeref:typename:VOID
GetFirstNonAddress	C:/BuildBs/edk2202302/edk2/OvmfPkg/Bhyve/PlatformPei/MemDetect.c	/^GetFirstNonAddress ($/;"	f	typeref:typename:STATIC UINT64
GetPeiMemoryCap	C:/BuildBs/edk2202302/edk2/OvmfPkg/Bhyve/PlatformPei/MemDetect.c	/^GetPeiMemoryCap ($/;"	f	typeref:typename:STATIC UINT32
GetSystemMemorySizeAbove4gb	C:/BuildBs/edk2202302/edk2/OvmfPkg/Bhyve/PlatformPei/MemDetect.c	/^GetSystemMemorySizeAbove4gb ($/;"	f	typeref:typename:STATIC UINT64
GetSystemMemorySizeBelow4gb	C:/BuildBs/edk2202302/edk2/OvmfPkg/Bhyve/PlatformPei/MemDetect.c	/^GetSystemMemorySizeBelow4gb ($/;"	f	typeref:typename:UINT32
InitializeRamRegions	C:/BuildBs/edk2202302/edk2/OvmfPkg/Bhyve/PlatformPei/MemDetect.c	/^InitializeRamRegions ($/;"	f	typeref:typename:VOID
PublishPeiMemory	C:/BuildBs/edk2202302/edk2/OvmfPkg/Bhyve/PlatformPei/MemDetect.c	/^PublishPeiMemory ($/;"	f	typeref:typename:EFI_STATUS
Q35TsegMbytesInitialization	C:/BuildBs/edk2202302/edk2/OvmfPkg/Bhyve/PlatformPei/MemDetect.c	/^Q35TsegMbytesInitialization ($/;"	f	typeref:typename:VOID
QemuInitializeRam	C:/BuildBs/edk2202302/edk2/OvmfPkg/Bhyve/PlatformPei/MemDetect.c	/^QemuInitializeRam ($/;"	f	typeref:typename:STATIC VOID