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 代码

工作的测试视频:

USB 安全麦克风

基于 ESP32-S3 开发的 USB 安全麦克风。让使用者完全避免开会中声音泄露的尴尬。

1.主控是 ESP32-S3,内置USB Device 支持,将自身模拟为一个 USB麦克风

2.使用MSM261S4030H0麦克风,这是一个高灵敏度的单麦克,通过I2S接口直接输出音频信息。

通过国产的 Cherry USB 架构实现了一个简单的 UAC.

关键代码如下:

1.初始化麦克风的 I2S ,特别注意是单声道

// 标准模式配置
i2s_std_config_t std_cfg =
{
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(SAMPLE_RATE),
.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(
    I2S_DATA_BIT_WIDTH_32BIT,
    I2S_SLOT_MODE_MONO
),
//.slot_cfg.slot_mask = I2S_STD_SLOT_LEFT,
.gpio_cfg = {
.bclk = I2S_BCK_GPIO,
.ws = I2S_WS_GPIO,
.din = I2S_DATA_GPIO,
.invert_flags = {
.bclk_inv = false, //时钟空闲时为 High
.ws_inv = false
}
}
};
std_cfg.slot_cfg.slot_mode=I2S_SLOT_MODE_MONO;
std_cfg.slot_cfg.slot_mask=I2S_STD_SLOT_LEFT;

2.Cherry USB架构下,下面的函数中完成 UAC 的初始化,同时创建一个队列用于接收音频数据

void audio_v1_init(uint8_t busid, uintptr_t reg_base)
{
    //  创建同步信号
    sign_tx = xSemaphoreCreateBinary();
    //  数据队列
s_receive_queue = xQueueCreate(10, sizeof(i2c_mic_rx_data_t));
//创建接收任务
xTaskCreatePinnedToCore(task_func, "task", 4096, NULL, 10, NULL, tskNO_AFFINITY);
 
 
    ESP_ERROR_CHECK(esp_task_wdt_add_user("usb", &twdt_usb));
 
    usbd_desc_register(busid, audio_v1_descriptor);
    usbd_add_interface(busid, usbd_audio_init_intf(busid, &intf0, 0x0100, audio_entity_table, 1));
    usbd_add_interface(busid, usbd_audio_init_intf(busid, &intf1, 0x0100, audio_entity_table, 1));
    usbd_add_endpoint(busid, &audio_in_ep);
 
    usbd_initialize(busid, reg_base, usbd_event_handler);
}

3.收到的音频数据在如下回调函数中

void usbd_audio_iso_callback(uint8_t busid, uint8_t ep, uint32_t nbytes)
{
    //USB_LOG_RAW("actual in len:%d\r\n", nbytes);
    ep_tx_busy_flag = false;
    if (0 == tx_flag)
    {
        printf("usbd_audio_iso_callback tx_flag = 0\n");
    }
    
    //  释放信号,让Main那边可以发送了
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    xSemaphoreGiveFromISR(sign_tx, &xHigherPriorityTaskWoken);
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

4. 最终,在将音频数据发送给PC时,做一个简单的判断,只有指定的 GPIO 拉低才会将数据发送出去,否则送出空数据包

// 如果当前没有发送
if (ep_tx_busy_flag != true)
{
ep_tx_busy_flag = true;
 
                xSemaphoreTake(sign_tx, 0);
 
if(gpio_get_level(GPIO_NUM_9) == 0) {  // 低电平触发
// 发送数据到 USB 
usbd_ep_start_write(0, AUDIO_IN_EP, rx_data.buffer, rx_data.size);
} else {
usbd_ep_start_write(0, AUDIO_IN_EP, NullBuffer, AUDIO_IN_PACKET);
}
 
 
                
                xSemaphoreTake(sign_tx, 10);
                
while (ep_tx_busy_flag)
{
if (tx_flag == false)
{
break;
}
}
//发送完成,释放缓冲区
rx_data.size = 0;
free(rx_data.buffer);

电路图设计如下:

电路图如下:

最终PCB 如下:

MSM261S4040H0 DataSheet 下载

源代码下载(IDF):

电路图和 PCB 下载:

工作的测试视频:

钥匙密码输入器

在日常工作中,经常遇到需要输入密码的地方。为此,设计了这个设备,可以通过开锁的动作来完成密码的输入。

基本的原理是:使用CH554模拟一个 USB键盘设备,上面还有一个USB CDC。首次使用时,通过一个网页的 WebSerial 功能将要输入的密码设置存储在CH554中。然后通过判断卓朗齐的钥匙开关(ZLQ9Y)电平来判断是否有开锁的动作。如果有开锁动作,那么读取存储的密码然后从USB键盘输入到电脑中。这样就模拟了人手工输入密码的过程。

电路图:

PCB:

3D 外壳设计图:

Arduino代码

SCH 和 PCB (立创EDA专业版)

用于对设备设置密码的网页

工作的视频

ESP32S3 无线双机文件传输器

在日常工作中,经常会遇到需要在测试机和主机之间传输文件的需求。通常WIFI 是非常好的方法,但是安全规则限制,主机和测试机无法接入同一个网络中,如果能用无线将他们连接起来能够提升效率。

基本思路是:ESP32 S3 将自身模拟为 USB CDC 设备,这样插入系统后就会出现一个串口。我们使用超级终端来进行文本和文件的传输。

收到的数据会放置在 USB Buffer 中,这些数据我们通过 ESP32 的 ESPNOW 发送出去。接收到之后,再通过串口传入系统中,同样又超级终端来接收。

需要注意的地方是:

1. Arduino 中需要修改如下2个位置

a.C:\Users\USERNAME\AppData\Local\Arduino15\packages\esp32\hardware\esp32\3.2.1\cores\esp32\USBCDC.cpp 这里是USB缓冲区的大小。太小了影响效率。

void USBCDC::begin(unsigned long baud) {
  if (itf >= CFG_TUD_CDC) {
    return;
  }
  if (tx_lock == NULL) {
    tx_lock = xSemaphoreCreateMutex();
  }
  // if rx_queue was set before begin(), keep it
  if (!rx_queue) {
    //ZivDebug setRxBufferSize(256);  //default if not preset
setRxBufferSize(64*1024);  //ZivDebug 64K Buffer
  }
  devices[itf] = this;
}

b.C:\Users\USERNAME\AppData\Local\Arduino15\packages\esp32\hardware\esp32\3.2.1\cores\esp32\USB.h 这里是USB CDC Task 的栈大小,原值过小,导致使用时会重启

class ESPUSB {
public:
  //ZivDebug  ESPUSB(size_t event_task_stack_size = 2048, uint8_t event_task_priority = 5);
  ESPUSB(size_t event_task_stack_size = 20480, uint8_t event_task_priority = 5);
  ~ESPUSB();
  void onEvent(esp_event_handler_t callback);
  void onEvent(arduino_usb_event_t event, esp_event_handler_t callback);

2.代码烧录需要设置如下

3. 代码中 ESPNOW 的发送和接收写在了一起,具体使用时读取 Io10 的状态来决定自身的 Mac 地址,换句话说成对使用时,一块板子的Io10 悬空,另外一块Io10 接地就可以了。

3.Arduino 版本  1.8.16 ,  ESP32 Package 是3.2.1

源代码:

电路图和PCB下载:

工作的测试视频:

电路图和PCB:

工作的视频:

【翻译】USB 2.0 简述

本文翻译自 https://embeddedinn.com/articles/tutorial/usb-2-0/#history-of-revisions

USB(Universal Serial Bus,通用串行总线)是一种以主机为中心的 4 线总线协议,旨在提供标准、低成本的接口,具有自识别、动态连接的外围设备,可自动将功能映射到驱动程序和配置,并具有低协议开销、保证带宽和低延迟,适用于电话、音频、视频等要求苛刻的应用。

USB规范由包括惠普、英特尔、LSI、微软、瑞萨和意法爱立信在内的多家主要厂商联合制定。该规范及其认证由USB开发者论坛(USBIF)负责维护。

版本历史

版本 1.0(1996 年 1 月 15 日)

  • 低速传输速率为 1.5 Mbps
  • 全速传输速率为 12 Mbits/s。

修订版 1.1(1998 年 9 月 23 日)

  • 改进了规格,是第一个被广泛使用的USB版本。

修订版 2.0(2000 年 4 月 27 日)

  • 高速传输速率为 480 Mbits/s。

修订版 3.0(2008 年 11 月 17 日)

  • 超高速 USB(原始数据吞吐量高达 5.0 Gbit/s)

本文将重点介绍高速 USB 协议修订版,俗称 USB 2.0。

基础知识

总线拓扑结构

基本 USB 遵循如下图所示的层级星型拓扑结构。

主机位于层级结构的顶端,所有“功能”最终都连接到主机。“hub”用于扩展“root hub”(主机的最终端点)的连接性。hub是每一层级的中心,每一层级都以hub为中心构建星型拓扑结构。

USB协议中每个操作都有一个最大延时。为此,最大延时限制了层级数量为7层——包括顶层(根/主机)。包含Hub和其他功能的组合称为“复合设备”。由于不允许再增加层级,因此第7层不应包含Hub。

系统组件

USB系统由三个主要部分组成:USB设备、USB主机和USB接口。

USB 设备可以是提供额外 USB 连接点的Hub集线器,也可以是为系统提供额外功能的设备。

任何USB系统中都只有一个主机。与主机系统连接的USB接口称为主机控制器。主机控制器可以用硬件、固件或软件实现。主机系统内部集成了一个根Hub,用于提供一个或多个连接点。

USB接口是总线的物理接口,由总线的电气和机械规范进行描述。

USB互连和信号传输

互连

USB 通过铜线使用 4 线物理接口实现规定的协议速度。该总线还为设备供电。

电缆的最大长度取决于信号延迟和信号完整性。标准电缆符合如图所示的颜色编码。

信号传导

USB协议采用电缆D+和D-线上的差分信号传输。数据使用NRZI协议编码,并以J(电流流入D+线)和K(电流流入D-线)表示。

在NRZI编码中,0流通过每比特时间翻转一次JK键来表示,而1流则保持D+和D-线的先前状态。单端零(SE0)信号通过同时驱动D+和D-线来发出。

主机和设备之间的同步由两端的数字锁相环 (DPLL) 维持。然而,长时间的 1 序列会导致 DPLL 失去同步,因此,每连续 6 个 1 之后会“填充”一个 0。这称为比特填充。

“比特时间”取决于函数运行的速度模式。例如,低速运行的设备的数据速率为 1.5Mbps,因此一个比特时间就是 (1/1.5 微秒)。

速度模式是通过 D+ 和 D- 线中的终端电阻来区分的。

对于低速器件,D- 线末端会连接一个 1.5K 欧姆的上拉电阻;而对于全速/高速器件,D+ 线也会连接类似的上拉电阻。因此,高速器件最初会像全速器件一样工作。该器件会生成一个“high speed chirp”,即一组 15 个 JK 对,用于向主机表明其为高速器件(480Mb/s)。

一旦检测到速度模式,主机便会启动“枚举”过程,这实际上就是配置该功能以供使用。但在深入了解枚举细节之前,我们需要了解一些基本术语。这将在下一节中进行解释。

协议基础知识

基本术语

  • 信号模式(Signalling mode),USB2.0 支持下面三种模式
    • 低速:1.5Mb/s
    • 全速:12Mb/s
    • 高速:480Mb/s
  • 主机(Host)
    是指连接 USB 设备的计算机系统(也可以是嵌入式系统)。主机发起所有通信,并且是总线的主控端。在总线拓扑结构中,只能有一个 USB 主机。
  • 设备(Device)
    是指连接到主机的 USB 功能设备。一个设备可能包含多种功能,这些功能可能同时可用,也可能不可用。这将在后续章节中详细讨论。
  • 设备地址(Device Address)
    主机在设备连接时为其分配的唯一 7 位地址。主机使用此地址将所有通信定向到目标设备。设备首次连接到主机时,其地址为 0(默认地址)。
  • 端点(End Point)
    是设备中主机可以与之通信的最细粒度的分类。端点通常实现为一个缓冲区,主机向该缓冲区发送数据(无论是否与特定协议相关)。

每个 USB 设备都应该有一个默认端点 0 (EP0),主机使用该端点来识别设备的功能并向其发送配置信息。

最多可以有 31 个端点,包括 EP0。除 EP0 外,所有端点均为单向的,每个端点都由一个唯一的 4 位端点编号及其方向标识。端点方向由 8 位端点地址的低半字节表示。因此,EP1 IN 的地址为 0x10,EP1 Out 的地址为 0x18。

  • 管道(Pipe)
    主机上某一点与设备上某一点之间的逻辑连接。一个设备最多可以有 31 个管道,包括终止于 EP0 的默认管道。

USB 管道分为两种类型:单向流管道(可以传输任何格式的数据)和流管道(可以传输没有指定格式的数据)。

EP0 默认始终是消息管道,而到其他端点的管道都是流管道。

  • 接口和配置(Interface and configuration)
    接口和配置是功能内部各项能力的两级层次结构。一个功能(设备)可以有多个配置,每个配置又可以关联多个接口。每个接口可以关联多个端点。在任何给定时间点,主机只能加载一个配置。接口对端点进行逻辑分组,从而简化了设备驱动程序的加载。

主机通过设备描述符、接口描述符和端点描述符读取设备的配置和接口功能。这些信息是在主机向设备发出某些标准命令时传递给主机的。

设备运行期间,主机可以加载或卸载特定的接口/配置。

  • 设备类别(Device Class)
    USB 设备被分为不同的类别,以便于驱动程序的开发和分类。但是,并没有规定设备必须属于某个类别规范。经过认证的设备仍然可以使用完全专有的驱动程序。然而,该设备必须符合 SUB 2.0 规范第 9 章中规定的基本协议行为,并满足电气和机械兼容性要求。
  • 帧(Frame)
    无论目标设备如何,USB总线上的所有通信都以帧为单位进行分段传输。对于低速/全速设备,帧宽为1毫秒;而对于高速设备,帧宽为125微秒。每个帧都由一个11位帧号标识,该帧号以称为帧起始信息(SOF)的特殊数据包的形式发送。帧内可以包含多个数据包,这些数据包将被发送到不同的设备。数据包的实际传输过程将在以下章节中进行详细说明。
  • 挂起(Stall)

当终端设备未准备好发送或接收数据时,它会发送 NAK响应
。而如果终端设备收到错误的命令或由于某些内部错误而无法响应,则设备会发送 STALL 握手响应。收到 STALL 握手响应后,主机必须采取纠正措施来恢复终端设备的功能。

  • 超时(Timeout)
    连接到拓扑结构最后一层(第 7 层)的高速设备最多需要 736 Bit时间才能响应任何主机请求。如果在 816 bit时间内未检测到信号,主机将超时。超时后,主机将发出重置指令并尝试重新枚举该设备。

USB基本通信流程

USB总线上的所有数据通信都以数据包的形式进行。通信由主机发起,主机向设备和端点发送一个令牌数据包,后续数据包将指向该令牌数据包。令牌数据包包含一个8位数据包标识符(PID),用于标识即将发生的事务类型。

PID 实际上为 4 位宽。低半字节是高半字节的补码,用于进行有效性检查。

令牌包之后会有一个方向与令牌包中指定的方向相同的数据包。对于不同类型的令牌,这是一个可选阶段,相关说明请参见本文档的相应部分。

交易完成后,会向其发送握手包。

主机每毫秒(LS 或 FS 设备)或每 125 微秒( HS 设备)广播帧开始 (SoF) 数据包。

因此,每笔传输(transfer )都包含多笔事务(transactions)。每笔事务分为三个阶段。

  • 令牌阶段(token stage:):主机向目标设备发出令牌数据包(token packet)以便进行后续通信。该令牌数据包还指示传输方向。
  • 数据阶段(Data Stage):实际数据传输通过数据包(data packet)进行。
  • 状态阶段(status stage):数据传输通过握手得到确认。

枚举

识别 USB 设备的功能并将其映射到主机系统中相应的驱动程序的过程称为“枚举”。设备枚举完成后,主机就能了解其速度模式、可用端点数量及其属性和类别等信息。具体步骤如下。

这里我们假设只有一个设备直接插入主机的根集线器。

  • 设备插入集线器端口后,总线将复位,SE0(D+ 和 D- 低电平),然后总线上电。
  • 接下来,通过数据线上拉电阻检测设备的速度模式。
  • 低速器件的 D- 线会连接一个 1.5 K欧姆的电阻,而全速/高速器件的 D+ 线则会连接一个 1.5 K欧姆的电阻。
  • 高速设备启动时以全速运行。主机重置总线后,设备会发送一个低频啁啾信号(16个JK对),向主机表明设备已具备高速运行能力。主机会以反向啁啾信号确认此信号。
  • 一旦检测到速度模式,主机就会向 EP0 发出名为“GetDescriptor (device)”的“标准请求”,以读取连接设备的功能。
  • 描述符的解读是多层次的,因为描述符根据其所传达的信息类型而按层次进行区分,正如上一节所述。
  • 首次读取设备描述符后,如果主机支持该设备(由各种因素决定),则总线再次重置,并为该设备提供一个唯一的 7 位地址。
  • 将设备置于目标状态后,即可通过其他描述符读取设备的更多详细信息,如下图所示。
  • 当读取完设备的所有可用配置后,可以使用设置配置命令。该命令使用索引来指定要选择的设备配置。
  • 配置完成后,相应的类驱动程序(稍后会详细介绍类)将被加载,并生成类规范。

设备枚举完毕且端点配置完成后,逻辑通信安排如下图所示。

系统软件主要是根集线器驱动程序,它负责跟踪所有连接到它的设备。任何与配置相关的通信都通过默认管道 EP0 进行。

特定于端点的驱动程序可以通过相应的管道进行通信。

传输模式

如前所述,一个USB设备可以包含多个端点。每个端点可以是输入端点(IN)或输出端点(OUT),具体取决于数据传输方向。另一种分类方法是基于端点上发生的数据传输类型。

USB总线上发生的四种传输类型是:

  1. 控制权传输(Control Transfer)
  2. 批量传输(Bulk Transfer)
  3. 同步传输(Isochronous transfer)
  4. 中断传输(Interrupt Transfer)

在枚举过程中,处于工作状态的端点(比如,一个USB设备可能总共有16个端点,但是实际只使用了6个端点)将会分配为上面传输类型的一种。比如,端点1 用于 批量传输,端点2使用中断传输。之后的过程中并不会改变传输类型。

控制传输

控制传输用于主机和 EP0 功能(设备)之间的通信。例如,第 9 章中的命令事务,如 SetAddress、Getdescriptor、SetDescriptor 等。

控制传输分三个阶段进行,即

  1. 建立阶段 setup stage
  2. 数据阶段 data stage
  3. 握手阶段handshake stage

建立阶段从主机发送的建立令牌(setup token )开始,然后是包含实际命令信息的数据包,随后是确认阶段。

数据阶段是可选的。接下来会有一个可选的数据阶段,具体取决于所发出的命令类型。例如,`set configuration` 命令将要设置的配置索引嵌入到命令中,因此不需要单独的数据阶段;而 `Getdescriptor` 命令则需要多个数据阶段来传输所有必需的信息。

即使多个设备连接到主机,控制传输始终保证占用总线带宽的 10%。也就是说,在 1 毫秒内(对于高速设备,则为 125 微秒微帧的 20%),始终有 10% 的时间用于控制传输。这是因为如果其他端点出现数据错误,控制传输会用于对这些功能执行纠正措施。

同步传输

这种数据传输模式可以保证访问 USB 总线带宽。

总线带宽

USB 通信以帧的形式进行。每个帧可以包含发往多个设备的传输。带宽是指在 1 毫秒/125 微秒的帧中分配给特定设备的时间。考虑到传输速度模式(1.5/12/480 Mb/s),带宽可以转化为数据量。

中断传输

中断传输提供了一种机制,可以确保设备和主机之间在周期性的时间间隔内进行通信。

当枚举中断端点时,可以指定轮询间隔,范围从每帧一次到每 255 帧一次。

如果在轮询时设备没有数据要传输,则下一次重试将在下一个服务帧中进行。

每个事务的最大数据包大小为 8 字节(LS)、64 字节(FS)或 1024 字节(HS)。

下图显示了光电鼠标中断端点的端点描述符以及端点的周期性轮询。

关于“保持连接”的说明:

所有连接低速设备的集线器端口都必须生成一个低速保活选通信号,该信号在帧的开头生成,包含一个有效的低速 EOP。在接收到 SOF 的每个帧中,该选通信号必须至少生成一次。此选通信号用于防止总线上没有其他低速流量时,低速设备挂起。

批量传输

当需要在终端和主机之间传输大量非时效性数据时,会使用批量传输。使用批量数据传输机制的设备包括闪存盘(大容量存储设备)、打印机、扫描仪等。对于这些设备而言,数据内容的可靠性比数据传输速率更为重要。因此,批量传输的可用带宽会因连接到主机的其他设备的配置而逐帧变化。

USB可靠性

错误 (Errors

由于各种原因,USB总线通信可能会出现错误。本节简要概述常见错误以及USB框架实现的相应恢复机制。

错误类型(Error Types

  • PID 错误
    8 位 PID 实际上由一个 4 位 PID 和一个 4 位 PID 补码构成,如下图所示。如果出现不匹配,设备将不会响应,并且主机端会发生超时(参考:USB 2.0 规范的 8.3.1 节)。

对所有设备进行 PID 完全解码是强制性的。

  • PID 之后的CRC 错误
    字段用于计算循环冗余码,该码用于验证接收数据的完整性。任何 CRC 校验错误的数据都将返回 NAK(无效确认)。

令牌包和SOF包的CRC校验码为5位,数据包的CRC校验码为16位。握手包没有CRC校验码。

  • 无效命令:
    当端点收到“完整但逻辑错误”的数据包(例如错误命令)时,或者设备内部出现故障导致端点无法使用时,端点将停止工作,并且任何发送到该端点的请求都会返回 STALL 握手作为响应。

错误处理(Error Handling)

  • NAK
    • 当主机收到设备返回的NAK时,前三次重试将由硬件触发。之后,将由软件介入。
  • STALL
    • 当某个端点发生阻塞( STALL)时,主机必须发送一个 ClearFeature 命令,其中包含指向要清除接口的“功能选择器值”。该命令会发送到控制端点 (EP0)。因此,即使某个端点停止工作,ClearFeature 也会应用于该接口下的所有端点。
  • Timeout超时
    • 当设备在指定的时间间隔内没有向主机发送任何信号时,主机将通过施加复位信号(在数据线上驱动 SE0 超过 2.5us)来复位设备。

用于同步的标志交替

USB 采用一种称为标志交替的简单机制,以确保主机和设备之间传输的数据同步。

在进行数据传输时,主机使用两种类型的数据令牌,分别是 Data0 和 Data1。初始数据传输使用 Data0 令牌。收到设备的 ACK 确认后,下一条数据将使用 Data1 令牌发送。设备还维护一个序列位,该序列位在成功确认传输后会被翻转。

但是,如果出现问题导致数据不被接受(发送 NAK),则主机和设备会保持序列位状态,并使用前一个数据令牌类型立即进行下一次传输。

假设总线上丢失了 ACK,主机未收到该 ACK,则主机使用相同的令牌(DATA0)重新发送数据。然而,在设备端,由于之前的事务,序列位已被翻转。因此,设备会忽略接收到的数据并发送 ACK。

USB

枚举完成后,实际的设备通信由主机内部的驱动程序发起,该驱动程序了解设备的行为。USB 协议非常灵活,可以同时使用现有驱动程序和新驱动程序,因此 USB 既可以作为接口,也可以作为通信媒介。这是通过将 USB 设备划分为不同的类,并使用过滤器类型的驱动程序层次结构来实现的。

我们将通过考虑众所周知的存储设备(即 USB 闪存驱动器)的例子来尝试理解 USB 类的概念。

USB闪存盘主要由大容量存储器和连接到USB控制器的存储器控​​制器组成。

典型的U盘会有一个遵循SCSI协议的内存控制器接口。SCSI协议与USB类似,用于在逻辑块地址级别对内存进行完全控制。因此,枚举完成后,设备(U盘)会作为从设备响应主机发出的SCSI命令。

SCSI 的每笔交易都有 3 个阶段,即

  • 命令阶段(Command stage)
  • 数据阶段(Data Stage)
  • 状态阶段(Status Stage)

文件表和所有逻辑内存处理都由驻留在主机上的 SCSI 驱动程序完成。

SCSI 驱动程序是一个通用驱动程序,与 USB 协议没有任何关系。USB 作为一种总线协议,充当主机和设备之间传输 SCSI 协议数据单元的媒介。这通过在接口级别将设备识别为大容量存储设备 (MSC 设备) 来实现。一旦设备被识别为具有 MSC 接口,主机就会加载 MSC 驱动程序栈。任何与被指定为 MSC 接口的接口之间的通信都将由 MSC 驱动程序发起,它会向根集线器驱动程序发送请求。根集线器驱动程序 (RHD) 会收集来自不同驱动程序的多个请求并形成帧。

MSC接口驱动程序的主要部分是对实际SCSI驱动程序的封装。SCSI驱动程序会向MSC驱动程序发送请求,这些请求随后由根集线器驱动程序(RHD)通过USB传输。然而,SCSI驱动程序无需了解USB协议或结构。它可以调用MSC驱动程序API,进而向RHD发送请求。另一方面,RHD也无需了解任何SCSI或MSC特有的实现细节。

下图清晰地展示了SCSI命令是如何嵌入到USB传输中的。

因此,基于类的架构使得USB设备能够通过现有的驱动程序进行控制,而该驱动程序的实现与USB协议无关(重用现有驱动程序)。

主机工作流程

到目前为止,我们一直专注于单个设备连接到根中心的情况。然而,该协议的设计能够处理以分层星型拓扑结构连接的多达 126 个设备。在本节中,我们将讨论主机如何调度事务到同时连接到它的多个不同速度模式的设备。

从拓扑图中可以看出,所有设备都通过称为集线器的端口扩展单元连接到根集线器上的一个节点。集线器有一个连接到主机的上游端口和一个下游端口,下游端口为设备提供连接点。

因此,集线器在USB总线拓扑结构中扮演着重要角色,它承担了主机的部分“职责”,例如频繁发送“保持连接”信号。本教程不讨论集线器协议。

一旦多个设备连接到拓扑结构,主机就会给每个设备分配一个唯一的 7 位地址。为此,初始通信将使用设备地址 0(设备在获得实际设备地址之前的默认地址)。主机和集线器之间会协作防止使用地址 0 进行设备寻址。

一旦确定了设备,主机就会根据前面章节中描述的描述符读取设备的“功能”和“要求”,从而对设备进行实际枚举。

如果设备请求的资源超过主机可用资源,则该设备不会被枚举。为了详细说明此用例,假设有三个同步端点(函数)已被枚举,并占用了所有可用带宽。当新连接的设备请求一些不可用的带宽(因为该带宽已被已枚举的设备占用)时,主机将不会枚举该设备。

设备枚举完成后,主机将根据端点类型和从客户端软件收到的请求创建其内部数据结构。以下将对此进行简要说明。

设备枚举完成后,主机上会为该设备生成一个端点数据描述符。这可以看作是链表的头节点,链表中包含要传输到特定设备端点的数据。附加到该端点的是传输描述符,其中包含要发送到端点的数据。客户端软件(设备驱动程序)请求中包含的数据将作为传输描述符附加。

向多个端点发送传输请求的调度是基于帧号进行的。例如,一个中断端点可以请求每 10 帧轮询一次,那么主机就会调度每 10 帧发送一次相应的传输描述符。如图所示,传输描述符可以包含一个 IN 令牌。

本文数据取自Ellisys USB 分析工具和 USB 2.0 规范本身提供的插图。

可编程降压芯片MP8869S测试

降压芯片/方案能够帮助我们将一个高电压的 DC 降低成合适的低电压。市面上也有很多降压模块可供选择,通常上面有一个可调电阻,通过旋转调整电阻实现期望的输出电压:

但是能够实现可编程的降压模块非常罕见。这次基于 MP8869 来制作一个可编程降压模块。

MP8869是一款集成I2C接口的高频同步降压转换器,主要特性如下:

核心参数

  • 输入电压:2.85-18V
  • 输出电流:12A连续/15A峰值
  • 输出电压:0.6- 5.5V
  • 封装:QFN-14(3mm×4mm)‌

首先进行电路图设计:

电路图中需要注意的位置有:

1.L1 的选择,如果你有大电流的需求,那么需要特别注意 L1 的参数是否能够承载你的要求,这里选择的是 14A 的

2.R1 和 R2 决定芯片开始工作后的电压,公式如下:

当前使用 R1=82K,R2=75K, 默认 Vref 是 0.72V, 上电后的输出电压为 0.72*(82+75)/75=1.5072V

3.编程控制 Vref 的输出,从上面的公式可以看到 R1 R2 是固定的, Vref 决定最终的输出

4.RA0_UP 和 RA0_DOWN 决定芯片的 I2C 地址,默认不上件地址为 0x61

5.Vout 是通过SW Pin输出的

6.EN 拉高开始工作,实际使用时可以将这个引脚直接连接到 Vin 这样上电即可工作

PCB 设计如下:

最终成品如下:

下面就可以进行测试了。

编写一个库,提供如下功能:

  1. 输出当前芯片版本
  2. 输出当前电流和电压
  3. 设置输出电压

工作的测试视频,测试中 R1=75K R2=82K ,此外,还测试了  R1=39K,R2=82K 的情况。根据公式可以得知这两种组合提供的电压范围不同。

https://www.bilibili.com/video/BV1jG2sB5ERc/der=”no” framespacing=”0″ allowfullscreen=”true”> </iframe>

最后芯片还提供了编程控制 EN 功能,电压斜率控制,过流保护,软停止,最大电流限制,过流指示,温度超限提示等功能,有需要的朋友可以继续研究。

特别注意:这次使用的是 MP8869S 型号,此外还有一款MP8869(不带后缀),最大的却别在于后者的参考电压调节范围为 0.6V 至 1.87V, 这样的话,可以直接覆盖  0.6×3=1.8V 到 1.87×3=5.61V。

电路图和PCB下载(立创EDA):

Arduino 库下载:

特别注意:在测试中发生了芯片烧毁的问题,猜测可能是因为测试时有过一次Vin先断电的情况,这样会导致 Arduino 从 I2C 灌入5V 电流导致的。

【翻译】USB线缆的最大长度

原文在 https://knowledge.cambrionix.com/Content/Articles/USB/Maximum-Length-of-a-USB-Cable.htm?TocPath=Articles%7CUSB%7C_____4

USB线缆的最大长度

USB 线缆的长度不仅仅关乎便利性,它还会直接影响线缆的性能。负责制定和维护 USB 标准的 USB-IF(USB 开发者论坛)制定了相关指南,以确保最佳功能和信号完整性。然而,这些建议会因 USB 版本的不同而有所差异。

早期 USB 版本 – 定义长度

对于早期的 USB 标准,USB-IF 设定了特定的最大长度,以确保可靠的性能:

USB 1.0:最大长度为 3 米(约 10 英尺)

USB 2.0:最大长度为 5 米(约 16 英尺)

选择这些长度是为了最大限度地减少长距离传输过程中信号丢失和延迟等问题。

USB 3.x 及更高版本——聚焦性能标准

随着 USB 3.0(现也称为 USB 3.2 Gen 1)及后续版本的推出,USB-IF 改变了其策略。该组织不再规定最大线缆长度,而是强调性能标准,例如:

信号传播延迟(信号沿电缆传输所需的时间)

衰减(信号强度随距离的损失)

因此,USB线缆的实际最大长度取决于其满足这些性能要求的能力。以下是现代USB标准典型长度的细分:

USB 3.2 Gen 1(USB 3.0、USB 3.1 Gen 1):最远可达 2 米(约 6 英尺)

USB 3.2 Gen 2(USB 3.1 Gen 2):最远可达 1 米(约 3 英尺)

USB 3.2 Gen 2×2:最远可达 1 米(约 3 英尺)

USB4:最大传输距离限制为0.8米(约2.6英尺)

为什么新标准需要更短的电缆?

较新的USB标准线缆长度更短,这反映了其更高的数据传输速率和更严格的性能要求。例如:

USB 2.0:支持高达 480 Mbps 的传输速度,允许使用更长的电缆而不会出现明显的信号衰减。

USB 3.2 Gen 2 和 USB4:运行速度更快(USB4 最高可达 40 Gbps),因此长距离传输时的信号丢失和干扰是一个关键问题。

为了克服这些限制,用户可以选择有源USB线缆或带有信号放大器的USB集线器来延长传输距离。这些设备可以放大信号,从而实现更远距离的连接,且不会影响性能。

确保USB线缆的兼容性和性能

为确保 USB 线缆可靠运行,请选择 USB-IF 认证的线缆,因为它可以保证符合性能和质量标准。认证线缆经过兼容性测试,让您安心使用;而未经认证的线缆则可能导致性能不稳定或故障。此外,选择与 USB 版本相匹配的线缆对于实现最佳功能也至关重要。