Basic AML Debugging

The AMLI Debugger supports two types of specialized commands: AMLI Debugger extensions and AMLI Debugger commands.

When you are performing AML debugging, you should carefully distinguish between two different kinds of prompts that will appear in the Debugger Command window:

  • When you see the kd> prompt, you are controlling the kernel debugger. All the standard kernel debugger commands and extensions are available. In addition, the AMLI Debugger extensions are also available. In Windows 2000, these extensions have a syntax of !acpikd.amli command. In Windows XP and later versions of Windows, these extensions have a syntax of !amli command. The AMLI Debugger commands are not available in this mode.
  • When you see the AMLI(? for help)-> prompt, you are controlling the AMLI Debugger. (When you are using WinDbg, this prompt will appear in the top pane of the Debugger Command window, and an Input> prompt will appear in the bottom pane.) From this prompt, you can enter any AMLI Debugger command. You can also enter any AMLI Debugger extension; these extensions should not be prefixed with !amli. The standard kernel debugging commands are not available in this mode.
  • When you see no prompt at all, the target computer is running.

At the beginning of any debugging session, you should set your AMLI Debugger options with the !amli set extension. The verboseontraceon, and errbrkon options are also very useful. When your target computer is running Windows XP or later, you should always activate the spewon option. See the extension reference page for details.

There are several ways for the AMLI Debugger to become active:

  • If a breakpoint in AML code is encountered, ACPI will break into the AMLI Debugger.
  • If a serious error or exception occurs within AML code (such as an int 3), ACPI will break into the AMLI Debugger.
  • If the errbrkon option has been set, any AML error will cause ACPI to break into the AMLI Debugger.
  • If you want to deliberately break into the AMLI Debugger, use the !amli debugger extension and then the g (Go) command. The next time any AML code is executed by the interpreter, the AMLI Debugger will take over.

When you are at the AMLI Debugger prompt, you can type q to return to the kernel debugger, or type g to resume normal execution.

The following extensions are especially useful for AML debugging:

  • The !amli dns extension displays the ACPI namespace for a particular object, the namespace tree subordinate to that object, or even the entire namespace tree. This command is especially useful in determining what a particular namespace object is — whether it is a method, a fieldunit, a device, or another type of object.
  • The !amli find extension takes the name of any namespace object and returns its full path.
  • The !amli u extension unassembles AML code.
  • The !amli lc extension displays brief information about all active ACPI contexts.
  • The !amli r extension displays detailed information about the current context of the interpreter. This is useful when the AMLI Debugger prompt appears after an error is detected.
  • Breakpoints can be set and controlled within AML code. Use !amli bp to set a breakpoint, !amli bc to clear a breakpoint, !amli bd to disable a breakpoint, !amli be to re-enable a breakpoint, and !amli bl to list all breakpoints.
  • The AMLI Debugger is able to run, step, and trace through AML code. Use the runp, and t commands to perform these actions.

For a full list of extensions and commands, see Using AMLI Debugger Extensions and Using AMLI Debugger Commands .

CH397 的 IPXE 启动

最近测试了一下 WCH 出品的 CH397 USB 网卡(百兆)网络启动功能,通过网络成功的启动到WinPE 环境。通过这样的方式用户可以方便的进行系统安装和维护。

除此之外,该IPXE启动方案支持 WCH USB网卡全系列产品:CH398(USB3.0千兆网卡) 、CH397、CH339(七端口路多功能HUB)、CH336(四端口HUB)。

本文记录一下操作过程。测试环境是2台电脑(网线直连,没有通过路由器之类),本次实验使用到的软件可以在文章后面下载到,解压在主机端的 CH397PXE 目录下即可

1.一台作为主机,主机端需要手工指定一个 IP地址,这里使用  192.168.50.2

2.PXE Server选用的是 Tiny PXE Server,来自【参考1】,除了TFTP、HTTP服务它还能提供 DHCP 功能。

3.PXE Server 配置如下,Boot File 选择C:\CH397PXE\Menu.ipxe

上述配置完成后,点击Tiny PXE Server 的 Online 按钮即可启动

1.目标机上使用一个 FAT32 U盘作为启动盘,将C:\CH397PXE\ Ch397_ipxe.efi 改名为 BootX64.efi 后放在这个U盘的 EFI\Boot\ 目录下

2.在目标机上启动这个U盘,自动进入 IPXE

3.进入一个选择菜单,其中的内容是我们上面提到的 menu.ipxe 中定义的

4.开始加载Win10 的 WinPE 镜像

5.最终启动进入 WinPE 环境,进去之后加载 CH397的驱动,还可以看到通过 DHCP自动给当前设备分配了 IP 地址。

本文提到的 Ch397_ipxe.efi(官方 IPXE没有CH397支持,这是WCH官方定制的) 可以在这里下载。

测试使用的 Tiny PXE Server 和修改后的 Win10 可以在这里下载。

http://a78231029.gs2.cosfiles.com/d/ty-2501/LABZ/ch397PXE.7z

参考:

  1. https://github.com/erwan2212/tinypxeserver
  2. https://zhuanlan.zhihu.com/p/343569176  UEFI开发探索50 – UEFI与网络2
  3. https://www.lab-z.com/vt/

搜集到的老版本 IASL

在一些情况下,我们需要老版本的 iasl.exe 来处理ACPI,这里整理了一下,有需要的朋友可以下载:

iasl20051117.exe
iasl20100121.exe
iasl20100806.exe
iasl20120111-32.exe
iasl20121220-32.exe
iasl20130117-32.exe
iasl20160729-32.exe
iasl20200717.exe
iasl20210331.exe
iasl20210930.exe
iasl20230628.exe
待补充

在这里可以下载上述集合,同时欢迎提供你手上的老版本方便他人使用。

ESP32 P4 Arduino GPIO 最快翻转速度测试

测试代码如下:

#include <arduino.h>
#include "soc/gpio_struct.h" // GPIO

void setup() {
  pinMode(20,OUTPUT);
}

void loop() {
  GPIO.out_w1ts.val = 1<<20;
  GPIO.out_w1tc.val = 1<<20;
  GPIO.out_w1ts.val = 1<<20;
  GPIO.out_w1tc.val = 1<<20;
  GPIO.out_w1ts.val = 1<<20;  
  delay(100);
}

可以看到翻转以 100ms 为间隔

放大可以看到从低->高或者高->低,最少需要 250ns

ESP32S3 制作的 ESP32S3 烧写器

很多年前开始玩Arduino 的时候使用的是 Arduino Uno,它使用 Atmel 328P 的主控。当时有一个有趣的项目是使用Uno 给另外一个设备刷写 BootLoader。这个项目能够极大的方便使用 Arduino。

这次的项目是一个使用 ESP32-S3 实现的 ESP32 下载器。

硬件部分非常简单,可以看做是一个 ESP32S3 的最小系统。

电路图:

PCB:

软件部分

代码使用 IDF 编写,首先实现基于 TinyUSB 的 USB CDC 功能。

1.TinyUSB 是 IDF 内置的原生 USB 库,通过下面的代码就可以实现 USB CDC 功能

ESP_LOGI(TAG, "USB initialization");
  const tinyusb_config_t tusb_cfg = {
    .device_descriptor = NULL,
    .string_descriptor = NULL,
    .external_phy = false,
    .configuration_descriptor = NULL,
  };
  ESP_ERROR_CHECK(tinyusb_driver_install(&tusb_cfg));
  tinyusb_config_cdcacm_t acm_cfg = {
    .usb_dev = TINYUSB_USBDEV_0,
    .cdc_port = TINYUSB_CDC_ACM_0,
    .rx_unread_buf_sz = 64,
    .callback_rx = &tinyusb_cdc_rx_callback, // the first way to register a callback
    .callback_rx_wanted_char = NULL,
    .callback_line_state_changed = &tinyusb_cdc_line_state_changed_callback,
    .callback_line_coding_changed = &tinyusb_cdc_line_coding_changed_callback
  };
  ESP_ERROR_CHECK(tusb_cdc_acm_init(&acm_cfg));
  ESP_LOGI(TAG, "USB initialization DONE");

2.之后, USB CDC收到的数据会出现在下面再合格回调函数中

void tinyusb_cdc_rx_callback(int itf, cdcacm_event_t *event)
{
  // USB CDC 接收的处理
  uint8_t rx_buf[CONFIG_TINYUSB_CDC_RX_BUFSIZE];
  size_t rx_size = 0;
  // 读取放入 rx_buf 缓冲区
  esp_err_t ret = tinyusb_cdcacm_read(itf, rx_buf, CONFIG_TINYUSB_CDC_RX_BUFSIZE, &rx_size);
  if (ret == ESP_OK) {
    // 根据资料,如果缓冲区有足够的空间,那么不会阻塞
    uart_write_bytes(UART_NUM_1,rx_buf,rx_size);
  } else {
    ESP_LOGE(TAG, "Read Error");
  }
}

3.Arduino ESP32 的烧写工具是 ESPTool,它通过将串口波特率从 9600切换到 115200 来通知 ESP32S3进入下载模式(这部分代码可以在 ESP32 的库中看到)

对饮的,我们在代码中做一个判断,如果出现了这样的切换,那么通过2个 GPIO 拉被刷机进入下载模式,然后通过串口通讯完成下载

// 设置波特率的回调函数
void tinyusb_cdc_line_coding_changed_callback(int itf, cdcacm_event_t *event)
{
  cdc_line_coding_t *line_coding = event->line_coding_changed_data.p_line_coding;
  Baudrate = line_coding->bit_rate;
  if (previous_Baudrate != Baudrate) {
    ESP_LOGI(TAG, "Change Baudrate from %lu to %lu", previous_Baudrate, Baudrate);
    
    // 如果出现从 9600 波特率到 115200 的切换,那么说明是要进入下载模式
    if ((previous_Baudrate==9600)&&(Baudrate==115200)) {
        DownloadMode=true;
    }
    if ((Baudrate==115200)||(Baudrate==921600)) {
        uart_set_baudrate(UART_NUM_1, Baudrate);
    }
    previous_Baudrate=Baudrate;
  }
}

最终烧写代码后,可以通过下面这个命令进行简单测试,它会让ESP32 S3进入下载模式,然后通过命令读取MAC地址,基本上这个命令如果可以跑过,那么烧写也没有问题

Esptool5 --trace -c esp32s3 -p com19 read-mac

完整代码下载:

电路图和PCB:

工作的测试视频

8b/10b 编码概述

本文介绍了一下8b/10b 编码, 主要是从定性的角度来介绍。因为对于我们来说,读取翻译物理信号是逻辑分析仪的工作。

8b/10b是一种广泛应用于高速数据传输领域的线路编码技术,其核心是将 8 位二进制数据(1 字节)映射为 10 位二进制符号后再传输,通过 “增加冗余”解决高速传输中的直流分量偏移、时钟同步丢失等关键问题,常见于 PCI Express、SATA、以太网(部分速率)等接口标准。

这种编码的目的是:

  • 在数据中嵌入时钟

8b/10b编码确保数据流中具有足够的边沿让接收端恢复时钟,从而不再需要分配时钟(传输过程不需要通用参考时钟,与之对比的是 SPI 总线,数据线必须在时钟线的帮助下才能得到期望的数据),让传输实现更高的速率(串行)。避免了并行总线的一些缺点,比如飞行时间的限制,时钟偏斜的影响。同样避免了分配高频时钟可能带来的EMI和布线困难的影响。

  • 保持DC平衡

以PCIe为例,链路使用AC耦合,链路中放置电容,当频率越高,阻抗越低,反之频率越低,阻抗越高,当码型的0和1交替频繁,那么信号很容易传输过去,但如果出现连续的0或者1,意味着频率降低,可能无法识别0和1。

高速串行总线通常会使用AC耦合电容,而通过编码技术使得DC平衡的原理可以从电容“隔直流、通交流”的角度理解。 如下图所示,DC平衡时,位流中的1和0交替出现,可认为是交流信号,可以顺利的通过电容;DC不平衡时,位流中出现多个连续的1或者0,可认为该时间段内的信号是直流,通过电容时会因为放电导致传输后的编码错误。高速串行总线采用编码技术的目的是平衡位流中的1和0,从而达到DC平衡。大多数串行电路都是ac coupling,就是会在tx端有串电容。电容是隔直通交的,如果不做dc balance,会把直流信号滤除,信号会畸变。但并不是所有的串行电路标准都是ac coupling,比如HDMI就是dc coupling,也就是说HDMI标准电气编码并不是dc balance的。

  • 加强错误检测

8b/10b编码方案同样加强了错误检测机制,8bit数据有256个编码,而10bit数据有1024个编码,如果1对1进行映射,那么这1024个编码中只需要找出256个编码来对应原始的8bit数据。由于数据的极性偏差要变化来保持DC平衡,所以一些数据映射到10位数据是存在两个数值的,即一个8位数据对应2个10bit的编码后值,分别为为正极性偏差和负极性偏差(无偏差也映射2个),那么数据应该映射了512个编码,即使加上控制符号编码,这个数字也是远小于1024个编码的,那么哪些不被映射的数据,就属于非法字符,接收端也可以依靠判断数据是否在合法来检测错误。

总之,这种编码对于高速信号有很大好处,所以很多高速通讯使用这种编码方式。

这种编码的设计目标是:让最后生成的编码达到 0 和 1 数量相同,并且不会出现超过5个的连续 0 或者 1。

这里介绍一下对于一个数值如何进行编码

1.将需要编码的 8Bit拆分为高 5 Bits 和 低3 Bits,前者是用  EDCBA 表示,后者用 HGF 表示。最后编码后的结果可以记为 Dx.y 或者 Kx.y。 其中的 x就是EDCBA的十进制值(0-31),y是HGF 的十进制值(0-7)。

2. 对高5Bit 的编码

5 Bits变成 6Bits需要在最高位插入一个 bit, 就是说 EDCBA变成  iEDCBA。插入的结果可能是0 也可能是1,于是有下面的表格。可以看到编码后的数字有三种情况:0 和 1 一样多,0比1多2个,0比1少两个。RD:Running Disparity 直译“运行不一致性”,也翻译成“极性偏差”,RD是对编码后的数据流Disparity的一个统计,+1用来表示1比0多,-1用来表示0比1多,-1是它的初始化状态,编码中“1”和“0”数量相等的码字称为“完美平衡码”。图片种会出现 D和K 使用相同的编码情况,比如 D/K 23 27 29 30,但是我猜测在实际使用中不会使用和D定义相同的K值,比如,不会出现 K.23.0 这种,因为会导致通讯时无法分发送的是 D.23.0 还是 K.23.0。

3.对 低3位的编码

3 Bits变成 4Bits同样需要在最高位插入一个 bit, 就是说 HGF变成  jHGF。同样也是使用原始值查表。同样的编码后的结果要么 0 1 一样多,要么0比1多2个,要么0比1少两个。

具体使用时,先假设  RD=0, 然后对数据流进行编码。

例如:原始数据0000 0000 ,拆分为  00000 000, 前面5个Bits可以编码为100111,这样 1比0 多2个,RD=+2; 然后计算 000 的编码,因为 RD=+2,所以要选择0多的编码方式,于是 000->0100。 最终结果时 100111 0100 , 可以看到 0 1 数量相同,平衡了。

例如:原始数据 0001 1111,拆分为  00011 111, 前面5个Bits只能编码为110011,0和1一样多RD=0; 然后计算 111 的编码,因为 RD=0,所以可以选择2种,但是选择之后 0 1 仍然不平衡,初始条件变成了 RD=-1或者 RD=+1 ,等待下一个数据进入之后作为初始值继续计算。

K 符号

8位/10位编码器每次编码8位数据,生成10位数据,这意味着编码后的字有1024种可能的组合,而原始字只有256种可能的组合。即使我们假设每个原始字有两种可能的编码字,也只有512种可能的组合。如前所述,有些8位字只有一个对应的10位字,因此10位字的组合少于512种。由此得出结论:至少有512种10位字的组合没有对应的8位字。

因此,8b/10b 编码能够检测物理链路上的比特错误,其原理是检测非法的 10 位字。然而,这种错误检测机制的价值有限,因为它无法检测到所有错误。

8b/10b 编码真正有价值的特性在于 K 符号。K 符号代替8 位字进行编码和传输。解码器能够区分普通数据字和 K 符号,并且始终有方法在收到 K 符号时通知应用程序逻辑。

因此,8b/10b 编码允许发送方在数据通道上发送额外信息,而不会与常规数据混淆。协议通常利用此功能来帮助接收方与发送方的数据流同步。

参考:

  1. https://www.cnblogs.com/zxdplay/p/19080208 8b/10b 编码的工作原理
  2. https://zhuanlan.zhihu.com/p/560350350 高速串行通信编码8b/10b(一)
  3. https://blog.csdn.net/Luckiers/article/details/130470493 8b/10b编码方式(详细)总结附实例快速理解
  4. https://blog.csdn.net/neufeifatonju/article/details/120548871  详解FPGA实现8b10b编码原理(含VHDL及verilog源码)
  5. https://www.01signal.com/using-ip/mgt/encodings/ A brief introduction to 8b/10b encoding, 64b/66b, 128b/130b etc.
  6. https://en.wikipedia.org/wiki/8b/10b_encoding

Step to UEFI (301)先于 Windows 启动的 UEFI APP

简单介绍一下 Windows 启动的原理:

  1. UEFI 会查找 FAT32 分区上  \EFI\BOOT\BOOTX64.EFI 然后启动
  2. Windows 安装完成后会创建一个启动变量,启动 \EFI\Microsoft\Bootmgrfw.efi
  3. 安装好后上面两个会共存,但是2被设置为每次默认的启动项

因此,我们可以编写一个文件替换Bootmgrfw.efi 这个文件,在完成我们代码中自定义的操作后,再启动原版的 Bootmgrfw.efi 完成 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>

/**
 * 基本的_getch()函数 - 等待并获取一个字符
 * @return 返回按下的字符,如果是特殊键则返回扩展码
 */
CHAR16 _getch(VOID)
{
    EFI_INPUT_KEY Key;
    EFI_STATUS Status;
    
    // 等待按键事件
    Status = gST->ConIn->ReadKeyStroke(gST->ConIn, &Key);
    
    // 如果没有按键,等待按键事件
    while (Status == EFI_NOT_READY) {
        gBS->WaitForEvent(1, &gST->ConIn->WaitForKey, NULL);
        Status = gST->ConIn->ReadKeyStroke(gST->ConIn, &Key);
    }
    
    if (EFI_ERROR(Status)) {
        return 0;
    }
    
    // 如果是普通字符,直接返回
    if (Key.UnicodeChar != 0) {
        return Key.UnicodeChar;
    }
    
    // 如果是特殊键,返回扫描码
    return (CHAR16)(0x100 + Key.ScanCode);
}

/**
 * 启动指定路径的 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);

    // 等待用户按键
    Print(L"Press any key to exit...\n");
    _getch();
	
    // 步骤 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;

    // 清屏
    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\\Microsoft\\boot\\bootbk.efi");
    Status = StartEfiApplication(ImageHandle, L"EFI\\Microsoft\\boot\\bootbk.efi");
    Print(L"Windows Boot Manager result: %r\n\n", Status);
    return EFI_SUCCESS;
}

对应的 INF 文件如下:

## @file
#   A simple, basic, application showing how the Hello application could be
#   built using the "Standard C Libraries" from StdLib.
#
#  Copyright (c) 2010 - 2011, Intel Corporation. All rights reserved.&lt;BR>
#  This program and the accompanying materials
#  are licensed and made available under the terms and conditions of the BSD License
#  which accompanies this distribution. The full text of the license may be found at
#  http://opensource.org/licenses/bsd-license.
#
#  THE PROGRAM IS DISTRIBUTED UNDER THE BSD LICENSE ON AN "AS IS" BASIS,
#  WITHOUT WARRANTIES OR REPRESENTATIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED.
##

[Defines]
  INF_VERSION                    = 0x00010006
  BASE_NAME                      = bootmgfw
  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]

具体的实验方法(在VMWARE 中完成,如果在实体机上运行,无比关闭SecureBoot功能):

  1. 在 VMWARE 中安装好  Windows 虚拟机
  2. 使用 DiskGuinus 打开 ESP 分区,找到\EFI\Microsoft\Bootmgrfw.efi将它改名为 BootBK.efi
  3. 将编译好的Bootmgrfw.efi放在 \EFI\Microsoft\ 目录下
  4. 重新启动即可看到

完整代码下载:

完整代码下载

工作的完整视频

可设置小夜灯

基于 CH554 实现一个小夜灯。

当下的小夜灯普遍存在着痛点:

1.待机时间短

2.颜色不可调,夜间太亮光线刺眼

3.点亮时间不可调,不方便使用

为此,制作了这样一个小夜灯:使用 18650 电池,同时外壳设计上预留了最够的空间,可以根据用户需要自行扩展加大电池通量。颜色和点亮时间可以用过串口自行设置。

核心部件有2个,一个是 HC-SR602 人体红外感应模块;另外一个是CH554 单片机芯片。此外,外部还有TP4056充电模块,18650电池,XT1861B502MR-G升压芯片,5V开关芯片和SN74AHC1G32DBVR或门芯片。

基本原理是 18650和TP4056充电模块配合工作,负责充放电管理。TP4056充电模块自带一个TypeC接口可以用于充电。当18650放电到2.4V时,TP4056充电模块自动停止工作防止过放。然后XT1861芯片负责将2.4-4.2V电压升压到5V 提供给HC-SR602 人体红外感应模块使用。当这个有人触发红外感应模块后,模块输出到或门芯片,经过运算后用于触发SY6280AAC进行供电。之后,CH554 根据存储的颜色控制 WS2812 LED 发光。同时根据设定的时间控制前面提到的或门。这样就可以实现即便人体红外感应模块输出停止工作之后,仍然输出5V。

HC-SR602模块主要参数(在底板上)

  • 工作电压:3.3V-15V;
  • 静态电流:20uA;
  • 感应距离:最大5M;建议0-3.5M;
  • 信号电平输出:H=3.3V(检测到周围有人体);L=0V(检测周围无人体);

XT1861产品特点(在底板上)

·       最高效率:94%

·       最高工作频率:300KHz

·       低静态电流:15µA

·       输出电压:1.8V~5.0V(步进 0.1V)

·       输入电压:0.9V~6.5V

·       低纹波,低噪声 小体积封装

这里设计的是主控部分,如果想整体工作起来需要配合底板。具体项目在 https://oshwhub.com/zoologist/ch554-xiao-ye-deng-20250510

这里主控部分完整的主要功能是:

1.接收来自串口的,LED 颜色和时长的设定;

2.工作之后负责控制LED 颜色

电路图:

PCB 设计:

 代码使用 Arduino 完成:

#ifndef USER_USB_RAM
#error "This example needs to be compiled with a USER USB setting"
#endif
#include "src/userUsbCdc/USBCDC.h"
#include
#include "DataFlash.H"
#include "include/ch5xx.h"
 
#define NUM_LEDS 2
#define COLOR_PER_LEDS 3
#define NUM_BYTES (NUM_LEDS*COLOR_PER_LEDS)
__xdata uint8_t ledData[NUM_BYTES];
 
#define BIT1 2
 
// USB 串口 Buffer
uint8_t recvStr[6];
uint8_t recvStrPtr = 0;
// 之前保存的颜色值
uint8_t rValue, gValue, bValue;
uint16_t TimeLighting;
 
// 定义电源控制引脚
#define POWERCTRL 15
// 定义LED信号线
#define LEDCOLOR 14
#define NEOPIXELSHOW neopixel_show_P1_4
 
unsigned long ElspLighten = 0;
unsigned long Elsp = 0;
 
void SetLEDColor(uint8_t r, uint8_t g, uint8_t b) {
  for (uint8_t i = 0; i   {
    set_pixel_for_GRB_LED(ledData, i, r,g,b);
    NEOPIXELSHOW(ledData, NUM_BYTES);
    delay(10);
  }
}
void setup() {
  // 供电引脚接管电源
  pinMode(POWERCTRL, OUTPUT);
  digitalWrite(POWERCTRL, HIGH);
 
  // LED 颜色控制
  pinMode(LEDCOLOR, OUTPUT);
 

  USBInit();
 
  // 读取颜色信息
  Flash_Op_Check_Byte1 = 0x00;
  Flash_Op_Check_Byte2 = 0x00;
  ReadDataFlash(0, 1, &rValue);
  ReadDataFlash(1, 1, &gValue);
  ReadDataFlash(2, 1, &bValue);
 
  // 读取时长
  ReadDataFlash(3, 2, &TimeLighting);
 
  // 这里需要写成这样,避免上电亮一下的问题
  delay(10);
  //set_pixel_for_GRB_LED(ledData, 0, 0, 0, 0);
  //NEOPIXELSHOW(ledData, NUM_BYTES);
  SetLEDColor(0,0,0);
  delay(10);
 
  // 读取之前保存的灯颜色
  //set_pixel_for_GRB_LED(ledData, 0, rValue, gValue, bValue);
  //NEOPIXELSHOW(ledData, NUM_BYTES);
  SetLEDColor(rValue, gValue, bValue);
  delay(100);
 
  ElspLighten = millis();
}
 
void Enter_DeepSleep(void)
{
  // 第一步:关闭所有外设模块
  SAFE_MOD = 0x55;       // 进入安全模式
  SAFE_MOD = 0xAA;       // 解锁寄存器写保护
  PCON &= ~BIT1;         // 确保PD位初始为0
  IE_EX = 0x00;          // 关闭扩展中断
  IE = 0x00;             // 关闭所有中断
  TCON = 0x00;           // 关闭定时器控制
  TMOD = 0x00;           // 关闭定时器模式
  SAFE_MOD = 0x00;       // 恢复安全模式
 
  // 第二步:设置IO口为低功耗状态
  P1_DIR_PU = 0x00;         // 所有IO设为输入模式
  P3_DIR_PU = 0x00;         // 所有IO设为输入模式
 
  // 第三步:进入停机模式
  SAFE_MOD = 0x55;       // 二次确认安全模式
  SAFE_MOD = 0xAA;
  PCON |= BIT1;           // 置位PD位进入停机模式
  PCON |= BIT1;           // 推荐重复写入确保执行
 
  while (1);
}
 

void loop() {
  while (USBSerial_available()) {
    char serialChar = USBSerial_read();
    recvStr[recvStrPtr++] = serialChar;
 
    if (recvStrPtr == 5) {
      // 测试命令
      if ((recvStr[0] == 0x55) && (recvStr[1] == 0xCC)) {
        USBSerial_print(rValue);
        USBSerial_flush();
        USBSerial_print(gValue);
        USBSerial_flush();
        USBSerial_print(bValue);
        USBSerial_flush();
        USBSerial_println(TimeLighting);
        USBSerial_flush();
      }
 
      // 设置颜色
      if ((recvStr[0] == 0x55) && (recvStr[1] == 0xAA)) {
        // 记录收到的颜色信息
        rValue = recvStr[2];
        gValue = recvStr[3];
        bValue = recvStr[4];
 
        // 将颜色信息写入 eeprom
        Flash_Op_Check_Byte1 = DEF_FLASH_OP_CHECK1;
        Flash_Op_Check_Byte2 = DEF_FLASH_OP_CHECK2;
        uint8_t result = WriteDataFlash(0, &recvStr[2], 3);
        if (result == 0) {
          // 写入成功
          USBSerial_println(result);
          USBSerial_flush();
        } else {
          // 写入失败
          USBSerial_println(result);
          USBSerial_println("f1");
          USBSerial_flush();
        }
 
        //set_pixel_for_GRB_LED(ledData, 0, rValue, gValue, bValue);
        //NEOPIXELSHOW(ledData, NUM_BYTES);
        SetLEDColor(rValue, gValue, bValue);
 
      }
 
      // 设定时长的命令
      if ((recvStr[0] == 0x55) && (recvStr[1] == 0xBB)) {
        // 记录收到的颜色信息
        TimeLighting = (recvStr[2]) + (recvStr[3] 200) {
    recvStrPtr = 0;
    Elsp = millis();
  }
 
  // 到达点亮的时间后关闭,如果是插在电脑上则不关闭
  if ((millis() - ElspLighten > TimeLighting * 1000UL) && (USBConfiged == 0)) {
    // 关灯
    //set_pixel_for_GRB_LED(ledData, 0, 0, 0, 0);
    //NEOPIXELSHOW(ledData, NUM_BYTES);
    SetLEDColor(0, 0, 0);
 
    digitalWrite(POWERCTRL, LOW);
    // 进入省电模式
    Enter_DeepSleep();
  }
 

}

焊接后的实物:

安装后的照片

3D外壳设计图:

完整代码:

完整电路图和PCB:

工作的测试视频

CH554小夜灯底板设计

这是一个小夜灯底板,使用TP4056充电模块对一个 18650电池进行充放电管理。

这个模块通过Type-C 可以输入 5V 充电,充电截止电压 4.2V, 最大充电电流1A. 电池过放保护为2.5V.

就是说当电池电压小于2.5V时自动截止输出,实现电池的保护功能。

电池通过上述模块的输出经过XT1861B502MR 芯片升压到5V 提供给后端使用。

人体感应模块是 SR602 基本参数如下,有人输出 3.3V电平,无人时输出 0V.

SR602 信号连接到SN74或门,同时还有一个信号一同参与运算,这样可以实现控制。

比如,SR602模块当前输出是 10s, 但是我们期望20s后才切断,因此用单片机输出另外一个信号参与运算,

这样就保证了20s都不会切断电源。

电路图设计:

PCB设计:

完整的电路图和 PCB下载(立创专业版):

ESP32S3 制作便携扬声器

这是一个基于 ESP32 的编写的便携式扬声器,通过数字麦克风获得音频数据,然后通过数字功放 HT513 从喇叭播放出去。

通过 Arduino 基于 AudioTools 库完成。

1. 音频数据通过MSM261S4030H0来获得

2.使用国产的 HT513作为功放。这款芯片支持通过寄存器直接调整音量,使用起来非常方便。外部有一个旋转按钮,通过 ADC 来得到当前需要的音量。

完整代码如下:

/**
   @file streams-i2s-i2s-2.ino
   @brief Copy audio from I2S to I2S: We use 2 different i2s ports!
   @author Phil Schatzmann
   @copyright GPLv3
*/
#include
#include "AudioTools.h"
AudioInfo IN_info(16000, 1, 32);
AudioInfo OUT_info(16000, 1, 32);
I2SStream in;
I2SStream out;
VolumeStream vol(in);
StreamCopy copier(out, vol); // copies sound into i2s
//FormatConverterStream converter(in);  // or use converter(out)
//StreamCopy copier(out, converter);       //        copier(converter, sound);
// HT513 音量
uint16_t Volume;
// 最小音量
#define LOWESTVOLUME 1
#define TOLENCE 16
#define HT513_ADDR_L 0x6c
/**
   @brief  ht513写寄存器
   @param  addr 寄存器地址
   @param  val 要写的值
   @retval None
*/
void HT513_WriteOneByte(uint8_t addr, uint8_t val)
{
  Wire.beginTransmission(HT513_ADDR_L);
  Wire.write(addr);
  Wire.write(val);
  int ack = Wire.endTransmission(true);
  Serial.print("Ack ");
  Serial.println(ack, HEX);
}
/**
   @brief  ht513读寄存器
   @param  addr 寄存器地址
   @retval 读取到的寄存器值
*/
uint8_t HT513_ReadOneByte(uint8_t addr)
{
  uint8_t temp = 0;
  Wire.beginTransmission(HT513_ADDR_L);
  Wire.write(addr);
  Wire.endTransmission(false);
  uint8_t bytesReceived = 0;
  bytesReceived = Wire.requestFrom(HT513_ADDR_L, (uint8_t)1, true);
  if (bytesReceived == 1) {
    temp = Wire.read();
  }
  else {
    Serial.println("Read Error ");
  }
  return temp;
}
// Arduino Setup
void setup(void) {
  delay(5000);
  // HT513 SD Pin 需要设置为 High
  pinMode(8, OUTPUT);
  digitalWrite(8, HIGH);
  analogReadResolution(9);
  // Open Serial
  Serial.begin(115200);
  // change to Warning to improve the quality
  //AudioToolsLogger.begin(Serial, AudioToolsLogLevel::Info);
  Wire.begin(18, 17);
  int nDevices;
  byte error, address;
  Serial.println("Scanning...");
  nDevices = 0;
  for ( address = 1; address     Wire.beginTransmission(address);
    error = Wire.endTransmission();
    if (error == 0) {
      Serial.print("I2C device found at address 0x");
      if (address         Serial.print("0");
      }
      Serial.println(address, HEX);
      nDevices++;
    }
    else if (error == 4) {
      Serial.print("Unknow error at address 0x");
      if (address         Serial.print("0");
      }
      Serial.println(address, HEX);
    }
  }
  if (nDevices == 0) {
    Serial.println("No I2C devices found\n");
  }
  else {
    Serial.println("done\n");
  }
  //  设置 SD 为LOW
  HT513_WriteOneByte(0x12, 0b11110000);
  // 设置数据格式为 I2S, 32Bits
  HT513_WriteOneByte(0x13, 0b00000000);
  // 读取音量设置
  Volume = analogRead(3);
  uint8_t Vol=(Volume,0,511,0x07,0xff);
  HT513_WriteOneByte(0x16, Vol);
  HT513_WriteOneByte(0x15, Vol);
  Serial.println(Volume, HEX);
  // 调整声道
  HT513_WriteOneByte(0x17, 0b10110000);
  Serial.println("++++++++++++++++");
  //  设置 SD 为HIGH
  HT513_WriteOneByte(0x12, 0b11110100);
  uint8_t Value = HT513_ReadOneByte(0x12);
  Serial.println(Value, HEX);
  Value = HT513_ReadOneByte(0x13);
  Serial.println(Value, HEX);
  Value = HT513_ReadOneByte(0x16);
  Serial.println(Value, HEX);
  Value = HT513_ReadOneByte(0x17);
  Serial.println(Value, HEX);
  // Define Converter
  //converter.begin(IN_info, OUT_info);
  // start I2S in
  Serial.println("starting I2S...");
  auto config_in = in.defaultConfig(RX_MODE);
  config_in.copyFrom(IN_info);
  config_in.i2s_format = I2S_STD_FORMAT;
  config_in.is_master = true;
  config_in.port_no = 1;
  config_in.pin_bck = 37;
  config_in.pin_ws = 38;
  config_in.pin_data = 36;
  // config_in.fixed_mclk = sample_rate * 256
  // config_in.pin_mck = 2
  in.begin(config_in);
  // start I2S out
  auto config_out = out.defaultConfig(TX_MODE);
  config_out.copyFrom(OUT_info);
  config_out.i2s_format = I2S_STD_FORMAT;
  config_out.is_master = true;
  config_out.port_no = 0;
  config_out.pin_bck = 15;
  config_out.pin_ws = 6;
  config_out.pin_data = 7;
  config_out.pin_mck = 16;
  out.begin(config_out);
  // set initial volume
  vol.begin(IN_info); // we need to provide the bits_per_sample and channels
  vol.setVolume(0.3);
  
  Serial.println("I2S started...");

}
// Arduino loop - copy sound to out
void loop() {
  copier.copy();
  if (abs(analogRead(3) - Volume) > TOLENCE) {
    // 读取音量设置
    Volume = analogRead(3);
    //  设置 SD 为LOW
    HT513_WriteOneByte(0x12, 0b11110000);
    uint8_t Vol=map(Volume,0,511,0x07,0xff);
    HT513_WriteOneByte(0x16, Vol);
    //  设置 SD 为HIGH
    HT513_WriteOneByte(0x12, 0b11110100);
    Serial.print(analogRead(3), HEX);
    Serial.print("  ");
    Serial.print(Volume, HEX);
    Serial.print("  ");
    Serial.println(Vol, HEX);
  }

}

电路图设计如下:

PCB 设计如下:

电路图和PCB 下载:

完整Arduino 代码

工作的测试视频: