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

工作的测试视频:

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:

工作的视频: