2026年2月更新,Step to UEFI 文章索引:
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 编码允许发送方在数据通道上发送额外信息,而不会与常规数据混淆。协议通常利用此功能来帮助接收方与发送方的数据流同步。
参考:
- https://www.cnblogs.com/zxdplay/p/19080208 8b/10b 编码的工作原理
- https://zhuanlan.zhihu.com/p/560350350 高速串行通信编码8b/10b(一)
- https://blog.csdn.net/Luckiers/article/details/130470493 8b/10b编码方式(详细)总结附实例快速理解
- https://blog.csdn.net/neufeifatonju/article/details/120548871 详解FPGA实现8b10b编码原理(含VHDL及verilog源码)
- https://www.01signal.com/using-ip/mgt/encodings/ A brief introduction to 8b/10b encoding, 64b/66b, 128b/130b etc.
- https://en.wikipedia.org/wiki/8b/10b_encoding
Step to UEFI (301)先于 Windows 启动的 UEFI APP
简单介绍一下 Windows 启动的原理:
- UEFI 会查找 FAT32 分区上 \EFI\BOOT\BOOTX64.EFI 然后启动
- Windows 安装完成后会创建一个启动变量,启动 \EFI\Microsoft\Bootmgrfw.efi
- 安装好后上面两个会共存,但是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.<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功能):
- 在 VMWARE 中安装好 Windows 虚拟机
- 使用 DiskGuinus 打开 ESP 分区,找到\EFI\Microsoft\Bootmgrfw.efi将它改名为 BootBK.efi
- 将编译好的Bootmgrfw.efi放在 \EFI\Microsoft\ 目录下
- 重新启动即可看到
完整代码下载:
完整代码下载
工作的完整视频
可设置小夜灯
基于 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:
工作的视频: