CH554 USB Host配合 ESP32-C3实现USB键盘转蓝牙

之前使用 Ch9350制作过一个 USB Host Shield 【参考1】,能够读取USB键盘鼠标的输入。最近在研究 Ch554 ,使用Ch554e制作了一个同样功能的Shield,配合ESP32-C3 能够实现USB 键盘转蓝牙的功能。

使用 Ch554 的有优点如下:

  1. 价格较低,相对于Ch9350 10元的价格,最便宜的 Ch554e 只要不到1.5元;
  2. 焊接友好,对于 TSSOP-20/SOP-16或者MSOP-10普通人都能够很好的进行焊接;
  3. 如果你的设计对于体积敏感,可以选择MSOP-10 封装的 Ch554e;
  4. 外围电路简单,只需要2个电容和1个电阻

缺点:

  1. 需要自己使用 keil 编写程序;
  2. 兼容性比不上 Ch9350,可能出现无法驱动的USB设备;

这次带来就是基于 Ch554e的设计。硬件部分设计如下:

下方就是CH554e的最小系统,外部配合2个0.1uf电容,以及1个10K电阻即可工作。下载方法是:上电之前短接 DL 位置,然后再上电使用WCHISPStudio即可。不过在研发阶段建议专门准备一个开发板便于操作。同时,官方的例子都是用第一个UART作为调试输出,而Ch554只有第2个 Uart可供使用。

根据上述电路设计的PCB如下:

这是一个底板,上面直接连接 DFRobot ESP32-C3即可。焊接后的板卡如下:

直接安装在 ESP32-C3上即可使用:

接下来开始代码的设计,首先设计的是 Ch554的代码,这里直接使用官方的代码进行简单修改。

为了便于使用我们使用和Ch9350相同的输出格式:

0-157 AB数据头,固定数值
0288表示有效帧值
03NN后续数据长度,从04开始到最后的校验和
0410  固定值 [7:6]:00 – 保留 [5:4]:01 – 鼠标 [3]  :0  – 保留 [2:1]:00 – 未知 [0]  :0  – 端口1
05-AA BB CC…..MM键盘数据,例如:08 00 00 00 00 00 00 00
XXNum帧序列号
XXCheckSum校验和,从05开始的数据和

例如:实际发送的一个数据:

57 AB 88 0B 10 08 00 00 00 00 00 00 00 00 08

代码是基于WCH 官方修改而来的,基本原理是:比较每一次收到的数据(RxBuffer)是否和上一次(LastBuffer)相同,如果不同,那么进行上报。使用上面介绍的数据报文格式:

                        IsSame=TRUE;
                        for ( i = 0; i < len; i ++ ){
                            if (LastBuffer[i]!=RxBuffer[i]) {
                                IsSame=FALSE;
                                LastBuffer[i]=RxBuffer[i];
                            }
                        }
                        //只有与前一次不同才进行输出
                        if (IsSame==FALSE) {
                            checksum=0x00;
                            CH554UART1SendByte(0x57);CH554UART1SendByte(0xAB);CH554UART1SendByte(0x88);CH554UART1SendByte(len+3);CH554UART1SendByte(0x10);
                            for ( i = 0; i < len; i ++ ){
                                CH554UART1SendByte(RxBuffer[i]);
                                checksum=checksum+RxBuffer[i];
                            }
                            checksum=checksum+counter;
                            CH554UART1SendByte(counter); CH554UART1SendByte(checksum);
                            counter++;
                        }

代码使用 Keil4 编译通过。

ESP32-C3代码如下:

#include <Arduino.h>
#include <BleKeyboard.h>

BleKeyboard bleKeyboard;

#define DEBUGMODE 0

void setup() {
  Serial.begin(115200);
  Serial1.begin(115200, SERIAL_8N1, RX, TX);

  bleKeyboard.begin();

}

void loop() {
  while (Serial.available()) {
    char c = Serial.read();
    if (c == '1') {
      Serial.println("get1");
    }
    if (c == '3') {
      ESP.restart();
    }
  }

  //根据 CH9350 Spec 每次最多输出 72Bytes
  byte Data[72];
  unsigned int CounterLast = Serial1.available();
  unsigned int CounterCurrent = 0;


  // 如果当前串口有数据
  if (CounterLast != 0) {
    // 进行简单测试,如果当前还在传输数据那么持续接收
    while (CounterCurrent != CounterLast) {
      CounterLast = Serial1.available();
      delayMicroseconds(500);
      CounterCurrent = Serial1.available();
    }
  }

  if (CounterCurrent > 0) {
    // 一次性将数据收取下来
    Serial1.readBytes(Data, CounterCurrent);
    unsigned int i = 0;
    unsigned int Length;
    while (i < CounterCurrent) {
      // 识别帧头
      if ((Data[i] == 0x57) && (Data[i + 1] == 0xAB)) {
        // 有效键值帧
        if (Data[i + 2] == 0x88) {
          // 获得数据长度
          Length = Data[i + 3];
          if (DEBUGMODE) {
            //Serial.print("Ln:");Serial.print(Length);
            for (int j = 1; j < Length + 1; j++) {
              if (Data[i + 3  + j] < 16) {
                Serial.print("0");
              }
              Serial.print(Data[i + 3  + j], HEX);
              Serial.print(" ");
            }
            Serial.println(" ");
          }

          //如果是键盘
          if (Data[i + 4] == 0x10) {
            if (DEBUGMODE) {
              Serial.print("Key");
              for (int j = 1; j < Length + 1; j++) {
                Serial.print(Data[i + 3  + j], HEX);
                Serial.print(" ");
              }
              Serial.println(" ");
            }
            
            //判断为Dostyle键盘
            if (Data[i + 3  + 1] == 0x10) { 
              if (bleKeyboard.isConnected() == true) {
                bleKeyboard.sendReport((KeyReport*)(&Data[i + 3  + 2]));
              }
            }
          }
          i = i + 3 + Length;
        } else if (Data[i + 2] == 0x82) {
          i = i + 3; // 跳过
        }
      }
      i++;
    } // while (i < Counter)
  }

}

使用时,只需要给ESP32-C3供电,连接好USB键盘后就可以搜索蓝牙键盘进行连接使用了。工作的测试视频在:

https://www.bilibili.com/video/BV1zi421a7NM/?vd_source=cf6121716e06cb669a27c10276f9c920

 Ch554 的代码:

ESP32 C3 代码:

参考:

1. https://mc.dfrobot.com.cn/thread-316678-1-1.html

又一次研究JY901心得

又一次尝试使用 Jy901 模块,没有成功应用,但是通过实验有一些心得记录如下。

1.模块默认使用输出 9600Hz 波特率通讯,10Hz回报;

2.恢复模块默认配置的方法有两种,一种是短接,另外一种是串口命令

3.一些串口配置的方法:

a.	ff aa 03 03 00  设置回传速率为1Hz
b.	ff aa 02 08 00  设置只输出0x53包(手册上提到JY901无法输出四元数)
c.	ff aa 00 00 00 保存当前设置(比如,进行了上述设定之后,需要保存之后下一次上电才能继续使用)

4.使用的轴如下图所示(不要看模块PCB上的标注,是错的)

5.输出范围:X轴±180;Y轴±90;Z轴±180

6.DataSheet上描述的角度输出如下:

其中的计算方法有问题,按照它的方法不会有正负的区别:

例如: 55 53 A0 A1 B0 B1 C0 C1 T0 T1 SUM

其中的 0xA1A0 输出范围是 0000-7FFF ,对应着 -180~+180;0xB1B0 输出范围是 0000-7FFF ,对应着 -90~+90;0xC1C0 输出范围是 0000-7FFF ,对应着 -180~+180.

因此实际可以选择如下处理方法:

  fX = JY901.stcAngle.Angle[0] - 0x4000;
  fX = fX * 180.0 / 0x4000;

  fY = JY901.stcAngle.Angle[1] - 0x4000;
  fY = fY * 90.0 / 0x4000;

  fZ = JY901.stcAngle.Angle[2] - 0x4000;
  fZ = fZ * 180.0 / 0x4000;

符号和方向满足右手原则:拇指指向轴方向,然后四个手指方向是正,相反是负。

参考:

1. https://wenku.baidu.com/view/13665ba8b307e87100f69630.html?ind=1&fr=wenchuang&_wkts_=1719042668522&bdQuery=jy901+%E6%95%B0%E6%8D%AE%E6%A0%BC%E5%BC%8F

WebSerial 3DModelViewer 四元数处理格式

在下面这个网页可以以3D 模式直接展示当前姿态:

https://adafruit.github.io/Adafruit_WebSerial_3DModelViewer

其中以欧拉角模式来演示的例子可以在【参考1】看到。如果想看到以四元数为参数的姿态演示,可以使用下面的格式输出:

  Serial.print("Quaternion: ");
  dtostrf(W, 1, 3, str);
  Serial.print(W,4);
  Serial.print(", ");
  dtostrf(X, 1, 2, str);
  Serial.print(X,4);
  Serial.print(", ");
  dtostrf(Y, 1, 2, str);
  Serial.print(Y,4);
  Serial.print(", ");
  dtostrf(Z, 1, 2, str);
  Serial.println(Z,4);

参考:

1.https://www.lab-z.com/bmx60rab/

获得物理硬盘 PID 的方法

#include <windows.h>
#include <iostream>

// 功能:获取指定物理驱动器的ProductId
std::string GetStorageDeviceProductId(const std::string& drivePath) {
    // 打开物理驱动器
    HANDLE hDevice = CreateFileA(drivePath.c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);
    if (hDevice == INVALID_HANDLE_VALUE) {
        std::cerr << "Failed to open device: " << drivePath << std::endl;
        return "";
    }

    STORAGE_PROPERTY_QUERY query = {};
    query.PropertyId = StorageDeviceProperty;
    query.QueryType = PropertyStandardQuery;

    // 分配足够大的缓冲区来存储STORAGE_DEVICE_DESCRIPTOR及其附加数据
    BYTE buffer[1024] = {};
    DWORD bytesReturned = 0;

    // 查询存储设备属性
    BOOL result = DeviceIoControl(hDevice, IOCTL_STORAGE_QUERY_PROPERTY, &query, sizeof(query), &buffer, sizeof(buffer), &bytesReturned, NULL);
    if (!result) {
        std::cerr << "Failed to query storage device properties." << std::endl;
        CloseHandle(hDevice);
        return "";
    }

    // 获取STORAGE_DEVICE_DESCRIPTOR结构
    STORAGE_DEVICE_DESCRIPTOR* deviceDescriptor = reinterpret_cast<STORAGE_DEVICE_DESCRIPTOR*>(buffer);

    // ProductId是一个以NULL结尾的字符串,位于STORAGE_DEVICE_DESCRIPTOR之后
    // 确保ProductIdOffset不为0
    std::string productId = "";
    if (deviceDescriptor->ProductIdOffset != 0) {
        productId = reinterpret_cast<const char*>(buffer + deviceDescriptor->ProductIdOffset);
    }

    CloseHandle(hDevice);
    return productId;
}

int main() {
    // 示例:获取PhysicalDrive0的ProductId
    std::string productId = GetStorageDeviceProductId("\\\\.\\PhysicalDrive0");
    std::cout << "ProductId: " << productId << std::endl;
    return 0;
}

检查 PhysicalDrive 类型

具体的类型定义在【参考1】:

typedef enum _STORAGE_BUS_TYPE {
  BusTypeUnknown = 0x00,
  BusTypeScsi,
  BusTypeAtapi,
  BusTypeAta,
  BusType1394,
  BusTypeSsa,
  BusTypeFibre,
  BusTypeUsb,
  BusTypeRAID,
  BusTypeiScsi,
  BusTypeSas,
  BusTypeSata,
  BusTypeSd,
  BusTypeMmc,
  BusTypeVirtual,
  BusTypeFileBackedVirtual,
  BusTypeSpaces,
  BusTypeNvme,
  BusTypeSCM,
  BusTypeUfs,
  BusTypeNvmeof,
  BusTypeMax,
  BusTypeMaxReserved = 0x7F
} STORAGE_BUS_TYPE, *PSTORAGE_BUS_TYPE;

示例代码:

#include <windows.h>
#include <iostream>
#include <winioctl.h>

void QueryDriveInterfaceType(int driveNumber) {
    HANDLE hDrive;
    char drivePath[256];
    sprintf_s(drivePath, "\\\\.\\PhysicalDrive%d", driveNumber);

    // 打开物理驱动器
    hDrive = CreateFileA(drivePath, 0, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);
    if (hDrive == INVALID_HANDLE_VALUE) {
        std::cerr << "Unable to open " << drivePath << std::endl;
        return;
    }

    // 准备查询
    STORAGE_PROPERTY_QUERY query;
    memset(&query, 0, sizeof(query));
    query.PropertyId = StorageDeviceProperty;
    query.QueryType = PropertyStandardQuery;

    // 接收数据的缓冲区
    BYTE buffer[1024];
    DWORD bytesRead;

    // 查询设备属性
    BOOL result = DeviceIoControl(hDrive, IOCTL_STORAGE_QUERY_PROPERTY, &query, sizeof(query),
                                  &buffer, sizeof(buffer), &bytesRead, NULL);
    if (result) {
        STORAGE_DEVICE_DESCRIPTOR* deviceDescriptor = (STORAGE_DEVICE_DESCRIPTOR*)buffer;
        switch (deviceDescriptor->BusType) {
            case BusTypeScsi:
                std::cout << drivePath << " is SCSI" << std::endl;
                break;
            case BusTypeAtapi:
                std::cout << drivePath << " is ATAPI" << std::endl;
                break;
            case BusTypeAta:
                std::cout << drivePath << " is ATA" << std::endl;
                break;
            case BusTypeSata:
                std::cout << drivePath << " is SATA" << std::endl;
                break;
            // 添加其他需要的接口类型
            default:
                std::cout << drivePath << " has an unknown interface type: " << (int)deviceDescriptor->BusType << std::endl;
                break;
        }
    } else {
        std::cerr << "Failed to query storage properties for " << drivePath << std::endl;
    }

    CloseHandle(hDrive);
}

int main() {
    // 示例:查询PhysicalDrive0的接口类型
    QueryDriveInterfaceType(0);
    return 0;
}

参考:

1.https://learn.microsoft.com/en-us/windows/win32/api/winioctl/ne-winioctl-storage_bus_type

Step to UEFI (295)手工给 EFI 文件插入代码的试验 (下)

我们继续之前的话题:在一个已经编译成功的 SimpleTest.EFI 中,加入另外一个 Hello.EFI 程序,最终实现Shell 下输入 SimpleTest.EFI, 实际上运行了SimpleTest.EFI 和 Hello.EFI。

前面的代码我们已经实现了大部分,还有一些细节需要处理。

第一个需要注意的是,我们Hello.EFI 是 NASM 生成的,其中有很多对于寄存器和堆栈的操作,这个操作会破坏SimpleTest.efi 需要的运行环境,导致无法跳入后执行出错。

仔细观察 SimpleTest.EFI 反编译结果,在开始处从 Shell 下收到的参数需是放在 rbx 和 rsi 寄存器中的,我们必须妥善保存这两个寄存器才能保证后续的正确运行。

代码头部修改如下:

_start:
	push rdx
	push rcx
	push rdi
	
    push rbx
	push rsi
	
    push rax    ;ConOut requires a push here. I don't know why
 
    ; reserve space for 4 arguments
    sub rsp, 4 * 8

代码尾部修改如下:
    add rsp, 4 * 8
    pop rax     

	pop rsi
	pop rbx
	
	pop rdi
	pop rcx
	pop rdx

	times 20 nop

再次生成一个新的 hello.efi(注意,这样修改之后的代码无法像之前的 EFI Application一样运行了), 用HXD 打开后拷贝代码区放置到 SimpleText.EFI 中。

此外,还有两个位置需要修改:

1.在拷贝到到 SimpleTest.EFI 的带末尾放上 mov  [rsp+0x08],rbx/mov [rsp+x010],rsi 两个操作;

2.跳转回文件头部的指令:

经过这样的改造,在模拟器中测试可以看到:

执行 SimpleTest.EFI 得到了2个输出,这个说明确实运行了2个EFI 。

修改后的 Hello.ASM 相关程序:

修改后的 SimpleTestM.EFI 文件

本文特别感谢Windows 专家天杀提供帮助。他对于 WinPE 结构的非常了解,帮助解决了修改EFI后, 使用模拟器测试崩溃的问题(Section Header 中 .TEXT 的大小需要更新)。

枚举系统中全部 PhysicalDrive

#include <windows.h>
#include <iostream>

void EnumeratePhysicalDrives() {
    HANDLE hDrive;
    DWORD bytesReturned;
    char driveName[24];
    STORAGE_DEVICE_NUMBER deviceNumber;

    // 尝试打开每个可能的物理驱动器
    for (int i = 0; i < 16; i++) {
        // 构造物理驱动器的名称
        sprintf_s(driveName, "\\\\.\\PhysicalDrive%d", i);

        // 尝试打开物理驱动器
        hDrive = CreateFileA(driveName, 0, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);
        if (hDrive == INVALID_HANDLE_VALUE) {
            // 如果无法打开驱动器,可能是因为驱动器不存在
            continue;
        }

        // 尝试获取设备编号
        if (!DeviceIoControl(hDrive, IOCTL_STORAGE_GET_DEVICE_NUMBER, NULL, 0, &deviceNumber, sizeof(deviceNumber), &bytesReturned, NULL)) {
            std::cout << "Failed to get device number for " << driveName << std::endl;
        }
        else {
            std::cout << "Found Physical Drive: " << driveName << ", Device Type: " << deviceNumber.DeviceType << ", Device Number: " << deviceNumber.DeviceNumber << std::endl;
        }

        CloseHandle(hDrive);
    }
}

int main() {
    EnumeratePhysicalDrives();
    return 0;
}

Step to UEFI (294)手工给 EFI 文件插入代码的试验(上)

编译好的 EFI 文件本质上是一个 WinPE 文件,因此我们有机会在文件开始处加入一些我们需要的代码。这次介绍的就是一个手工在 EFI 入口处插入另外一个EFI 代码的试验。

这次进行一个特别的实验。基本的原理是:

  1. 编写一个在屏幕上输出字符串简单的程序,这样我们能得到一段EFI Shell下对屏幕输出字符串的机器码;
  2. 编写一个宿主程序,这个程序编译后的 EFI 文件使用 4K 对齐,这样话,存放代码的.text段会有足够的空间能够存放下步骤1生成的机器码;
  3. 修改生成的EFI文件的 Section Headers 中给出的.text 大小,保证足够放下我们增加的机器码;
  4. 将步骤1生成的代码,插入在步骤3生成的EFI中。

最终,我们得到一个新的 EFI 程序,运行之后它会先执行步骤1 的代码,然后再执行步骤2的代码。

步骤1:这里使用 NASM 汇编语言来完成。

根据之前的文章【参考1】,编写一个程序实现在屏幕上输出字符串的代码。代码有部分修改,主要是将输出的字符串和代码放在了一起:

bits 64
 
; contains the code that will run
section .text
 
; allows the linker to see this symbol
global _start
 
; see http://www.uefi.org/sites/default/files/resources/UEFI Spec 2_7_A Sept 6.pdf#G8.1001729
struc EFI_TABLE_HEADER
    .Signature    RESQ 1
    .Revision     RESD 1
    .HeaderSize   RESD 1
    .CRC32        RESD 1
    .Reserved     RESD 1
endstruc
 
; see http://www.uefi.org/sites/default/files/resources/UEFI Spec 2_7_A Sept 6.pdf#G8.1001773
struc EFI_SYSTEM_TABLE
    .Hdr                  RESB EFI_TABLE_HEADER_size
    .FirmwareVendor       RESQ 1
    .FirmwareRevision     RESD 1
    .ConsoleInHandle      RESQ 1
    .ConIn                RESQ 1
    .ConsoleOutHandle     RESQ 1
    .ConOut               RESQ 1
    .StandardErrorHandle  RESQ 1
    .StdErr               RESQ 1
    .RuntimeServices      RESQ 1
    .BootServices         RESQ 1
    .NumberOfTableEntries RESQ 1
    .ConfigurationTable   RESQ 1
endstruc
 
; see http://www.uefi.org/sites/default/files/resources/UEFI Spec 2_7_A Sept 6.pdf#G16.1016807
struc EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL
    .Reset             RESQ 1
    .OutputString      RESQ 1
    .TestString        RESQ 1
    .QueryMode         RESQ 1
    .SetMode           RESQ 1
    .SetAttribute      RESQ 1
    .ClearScreen       RESQ 1
    .SetCursorPosition RESQ 1
    .EnableCursor      RESQ 1
    .Mode              RESQ 1
endstruc
 
_start:
 
    push rax    ;ConOut requires a push here. I don't know why
 
    ; reserve space for 4 arguments
    sub rsp, 4 * 8
 
    ; rdx points to the EFI_SYSTEM_TABLE structure
    ; which is the 2nd argument passed to us by the UEFI firmware
    ; adding 64 causes rcx to point to EFI_SYSTEM_TABLE.ConOut
    mov rcx, [rdx + 64]
 
    ; load the address of our string into rdx
    lea rdx, [rel strHello]
 
    ; EFI_SYSTEM_TABLE.ConOut points to EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL
    ; call OutputString on the value in rdx
    call [rcx + EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL.OutputString]
     
    add rsp, 4 * 8
    pop rax     
    ret
	
strHello db __utf16__ `Hello World from LAB-Z.COM!\n\r\0`

codesize equ $ - $$
 
; contains nothing - but it is required by UEFI
section .reloc

编译命令如下:

c:\nasm\nasm -f win64 hello.asm -l hello.lst
link /NODEFAULTLIB /IGNORE:4001 /OPT:REF /OPT:ICF=10 /MAP /ALIGN:32 /SECTION:.xdata,D /SECTION:.pdata,D /Machine:X64  /DLL /ENTRY:_start  /SUBSYSTEM:EFI_APPLICATION /SAFESEH:NO /DRIVER Hello.obj

除了正常生成的 EFI 文件之外,还生成了 lst 文件,在其中能够看到代码生成的机器码用于对照参考:

在模拟器中测试可以正常执行。

根据【参考2】,具体对应如下:

.text:(代码段),可读、可执行
.data:(数据段),存放全局变量、全局常量等
.idata:(数据段),导入函数的代码段,存放外部函数地址。(当然还有 edata ,导出函数代码段,但不常用)
.rdata:(数据段),资源数据段,程序用到什么资源数据都在这里(包括自己打包的,还有开发工具打包的)

使用 CFF Explorer查看,我们需要的机器码就在.text段中:

步骤2:我们根据【参考3】,编写一个简单的代码,功能上只是向屏幕输出字符串,然后使用 EDK2 进行开发。因为默认情况下,.text 空余空调很小,所以在编译完成后我们再打开 下面这个 makefile文件

edk2\Build\AppPkg\DEBUG_VS2019\X64\AppPkg\Applications\SimpleTest\SimpleTest\Makefile

将下面的一行中修改为 /ALIGN:0x1000

DLINK_FLAGS = /NOLOGO /NODEFAULTLIB /IGNORE:4001 /IGNORE:4281 /OPT:REF /OPT:ICF=10 /MAP /ALIGN:32 /SECTION:.xdata,D /SECTION:.pdata,D /Machine:X64 /LTCG /DLL /ENTRY:$(IMAGE_ENTRY_POINT) /SUBSYSTEM:EFI_BOOT_SERVICE_DRIVER /SAFESEH:NO /BASE:0 /DRIVER /DEBUG

然后进入对应目录,输入 nmake 重新编译,这样就得到一个段以4K 对齐的 EFI 文件。

查看 .text段,这里有足够的空间插入我们的代码:

步骤3,修改Section Headers 中的.text Size,这里我们修改为 1600.

步骤4,我们手工插入。

1.SimpleTestM.EFI的入口地址在 0x400处:

开始处对应的是 _ModuleEntryPoint() 函数,我们程序代码是从ShellAppMain() 函数开始的。

打开hello.efi ,拷贝这一段:

插入(Paste Write)到 SimpleTest.efi 中:

然后手工修改SimpleTest.efi 如下:

入口处修改为跳转指令, 这里使用的是一个相对跳转指令【参考4】

运行之后结果如下:

就是说我们通过 SimpleTestM 运行了hello.efi 中的内容。

当然这里和我们的预期还有差别

当然,这样的代码并不是我们期待的最终结果,我们期望两个程序能够同时运行。

这次只做到了在一个A.EFI中插入另外一个B.EFI 文件,然后运行A.EFI 实际执行B.EFI的代码。

参考:

  1. https://www.lab-z.com/stu207/
  2. https://blog.csdn.net/Simon798/article/details/96876910
  3. https://www.lab-z.com/stu260/
  4. https://www.felixcloutier.com/x86/jmp

给 EXE 加入 Resource

最近忽然想起来一个问题:如何给一个做好的 EXE 加入其他的内容?比如,我编写一个 EXE 需要更改内容而又不想重新 Build 代码。

经过研究,可以通过给EXE 添加 Resource 的方法来实现这一目标。在 https://github.com/tc-hib/go-winres 这里有一个从命令行给 EXE 添加Resource 的项目。配合这个项目可以实现前述目标。

首先,编写一个测试代码,使用 VC 编写在 VS2019 下编译通过:

#include <windows.h>
#include <iostream>

// 回调函数用于枚举资源
BOOL CALLBACK EnumResNameProc(HMODULE hModule, LPCWSTR lpszType, LPWSTR lpszName, LONG_PTR lParam) {
    // 每找到一个资源,就增加计数
    (*(int*)lParam)++;
    return TRUE; // 继续枚举
}

int main()
{
    HMODULE hModule = GetModuleHandle(NULL); // 获取当前模块句柄
    int resourceCount = 0; // 用于计数的变量

    // 枚举所有RT_RCDATA类型的资源
    EnumResourceNames(hModule, RT_RCDATA, EnumResNameProc, (LONG_PTR)&resourceCount);

    std::cout << "Number of RT_RCDATA resources: " << resourceCount << std::endl;

    return 0;
}

代码非常简单,单纯的输出当前EXE RT_RCDATA类型的 Resource 数量。

接下来将go-winres.exe放在同一个目录下,然后运行下面的命令

go-winres.exe init

对应的会生成 winres 目录,其中有下面三个文件

前面2个是可以作为EXE 的图标的,winres.json是配置文件。例如,我们对这个目录放置一个 png 文件,然后修改如下,增加 RT_RCDATA的部分:

  "RT_GROUP_ICON": {
    "APP": {
      "0000": [
        "icon.png",
        "icon16.png"
      ]
    }
  },
  "RT_RCDATA": {
    "OTHER": {
      "0000": "2.png"
    }
  },  
  "RT_MANIFEST": {
    "#1": {
      "0409": {
        "identity": {
          "name": "",
          "version": ""

运行如下命令:

go-winres.exe patch ResourceTest.exe

工具会自动给 ResourceTest.exe 添加内容,之后再次运行:

如果使用 CFF 工具还可以看到多了一个 Resource。这样,你可以在代码中先判断Resource数量,然后再进行动作。

官方提供的版本(和 Github上的相同,0.3.3版本)

VC 编写的WAVE测试文件生成器

最近为了调试,写了一个 WAVE 的生成器,能够生成指定采样率,指定格式的 WAVE文件,这样可以在输出端直接查看输出是否是指定的数据。特别注意的是,WAVE 中,使用 int16 (有符号十六进制)来表示当前的信号,更具体来说,负数使用补码来表示。代码如下:

// WaveGenerator.cpp : This file contains the 'main' function. Program execution begins and ends there.
//

#include <Windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <math.h>
#pragma pack(push, 1)
struct Chunk1 {          // 'R', 'I', 'F', 'F'
	char chunk_id[4];
	uint32_t chunk_size;
	char format[4];      // 'W', 'A', 'V', 'E'
};

struct Chunk2 {
	char chunk_id[4];
	uint32_t chunk_size;
	int16_t wFormatTag;
	int16_t nChannels;
	int32_t nSamplesPerSec;
	int32_t nAvgBytesPerSec;
	int16_t nBlockAlign;  // => num_channels * bits_per_sample / 8
	int16_t wBitsPerSample;
};

struct Chunk3 {
	char chunk_id[4];
	uint32_t chunk_size;
};
#pragma pack(pop) // 恢复到之前的对齐设置

#define AUDIODURATION  5  // 音频时长,以秒为单位
#define SAMPLEBITS  16  // 采样位
#define SAMPLERATE  96000  // 采样率
#define CHANNELNUM  2  // Channel 数量
int main()
{
	Chunk1 chunk1;
	chunk1.chunk_id[0] = 'R'; chunk1.chunk_id[1] = 'I'; chunk1.chunk_id[2] = 'F'; chunk1.chunk_id[3] = 'F';
	// 数据长度= Chunk123 总长 + RAW 数据长度(双声道,SAMPLEBITS Bits)
	chunk1.chunk_size = sizeof(Chunk1)-8 + sizeof(Chunk2) + sizeof(Chunk3)+ AUDIODURATION* CHANNELNUM *(SAMPLEBITS/8)* SAMPLERATE;
	chunk1.format[0] = 'W'; chunk1.format[1] = 'A'; chunk1.format[2] = 'V'; chunk1.format[3] = 'E';

	Chunk2 chunk2;

	chunk2.chunk_id[0] = 'f'; chunk2.chunk_id[1] = 'm'; chunk2.chunk_id[2] = 't'; chunk2.chunk_id[3] = ' ';
	chunk2.chunk_size = sizeof(Chunk2)-8;
	chunk2.wFormatTag = 0x0001; //PCM 格式
	chunk2.nChannels = CHANNELNUM;
	chunk2.nSamplesPerSec = SAMPLERATE;
	chunk2.nAvgBytesPerSec = CHANNELNUM * SAMPLERATE * (SAMPLEBITS / 8);
	chunk2.nBlockAlign = CHANNELNUM * (SAMPLEBITS / 8);
	chunk2.wBitsPerSample = SAMPLEBITS;

	Chunk3 chunk3;
	chunk3.chunk_id[0] = 'd'; chunk3.chunk_id[1] = 'a'; chunk3.chunk_id[2] = 't'; chunk3.chunk_id[3] = 'a';
	chunk3.chunk_size = AUDIODURATION * CHANNELNUM * SAMPLERATE * (SAMPLEBITS / 8);

	// 打开文件用于写入,以二进制模式
	FILE* file = fopen("C:\\WaveGenerator\\Debug\\example.wav", "wb");
	fwrite(&chunk1, sizeof(chunk1), 1, file);
	fwrite(&chunk2, sizeof(chunk2), 1, file);
	fwrite(&chunk3, sizeof(chunk3), 1, file);
	short int s=0,e=0;
	for (int i = 0; i < AUDIODURATION* SAMPLERATE; i++) {
		//s = sin(i * 2 * 3.1415 / (SAMPLERATE/1000)) * (65535 / 2);
		// 左声道
		s++;
		fwrite(&s, sizeof(s), 1, file);
		// 右声道
		e = 0; 
		fwrite(&e, sizeof(e), 1, file);
	}

	fclose(file);
	//getchar();
}