目录
[1.1 USB简介](#1.1 USB简介)
[2.1 USB设置注册](#2.1 USB设置注册)
[2.1.1 usb_device.c](#2.1.1 usb_device.c)
[2.1.2 usb_device.h](#2.1.2 usb_device.h)
[2.1.3 main.c](#2.1.3 main.c)
[2.1.4 结果说明](#2.1.4 结果说明)
[2.2 CDC-ACM使用](#2.2 CDC-ACM使用)
[2.2.1 usb_device.c](#2.2.1 usb_device.c)
[2.2.2 usb_device.h](#2.2.2 usb_device.h)
[2.2.3 main.c](#2.2.3 main.c)
[2.2.4 结果说明](#2.2.4 结果说明)
[2.2.5 usb_device.c](#2.2.5 usb_device.c)
[2.3 HID设备使用](#2.3 HID设备使用)
[2.3.1 usb_device.c](#2.3.1 usb_device.c)
[2.3.2 usb_device.h](#2.3.2 usb_device.h)
[2.3.3 main.c](#2.3.3 main.c)
[2.3.4 结果说明](#2.3.4 结果说明)
[2.4 MSC的SPI flash应用](#2.4 MSC的SPI flash应用)
[2.4.1 usb_msc.c](#2.4.1 usb_msc.c)
[2.4.2 usb_msc.h](#2.4.2 usb_msc.h)
[2.4.3 main.c](#2.4.3 main.c)
[2.4.4 结果说明](#2.4.4 结果说明)
前言
USB设备种类繁多并且在电子产品中有极大的应用场景。本文就基于乐鑫的官方推荐组件《esp_tinyusb》来实现各种USB设备的驱动。
本文使用的开发板是微雪的ESP32-P4-Module-DEV-KIT。ESP-IDF版本是6.0.1。基于第一讲的工程模板。
一、 USB知识
1.1 USB简介
USB 的全称是 Universal Serial Bus(通用串行总线)。它是目前全球最普及、应用最广泛的外部设备连接标准,用于规范电脑与外部设备(如键盘、鼠标、U盘、手机、打印机等)之间的连接、数据传输和供电。
USB 的版本演进
| 版本 | 发布年份 | 最大理论速率 | 备注 |
|---|---|---|---|
| USB 1.1 | 1998 | 12 Mbps | 早期普及版,淘汰 |
| USB 2.0 | 2000 | 480 Mbps | 至今仍在大量使用(键鼠、低速设备) |
| USB 3.0 / 3.2 Gen 1 | 2008 | 5 Gbps | 蓝色接口标志,高速起点 |
| USB 3.1 / 3.2 Gen 2 | 2013 | 10 Gbps | Type-C 开始普及 |
| USB 3.2 Gen 2×2 | 2017 | 20 Gbps | 双通道传输 |
| USB4 | 2019 | 40 Gbps | 基于 Thunderbolt 3 协议,仅 Type-C |
| USB4 v2.0 | 2022 | 80 Gbps (双向) / 120 Gbps (非对称) | 最新标准,支持 PCIe 隧道 |
USB 不仅仅是数据线,除了传文件和充电,USB 还定义了多种设备类,决定了设备插入后如何工作:
- HID:人机交互设备(键盘、鼠标、手柄)→ 免驱
- MSC / Mass Storage:大容量存储(U盘、移动硬盘)→ 免驱
- CDC-ACM:虚拟串口(开发板、3D打印机)→ 通常免驱
- Audio / Video:USB 声卡、摄像头(UVC/UAC)
- PD (Power Delivery):智能电力协商协议,让充电器和设备"握手"决定电压电流USB 是一个集数据传输、电力供应、视频输出于一体的通用物理层和协议层标准,它通过不断进化(从 12Mbps 到 80Gbps,从 Type-A 到 Type-C),成为了连接数字世界的基石。
二、工程示例
首先在你工程中的ESP终端粘贴《idf.py add-dependency "espressif/esp_tinyusb^2.2.0"》,然后回车,这样这个组件就添加到你的工程中了,编译工程能看到工程目录多了下面两个组件包

同时在main组件的组件管理器描述文件idf_component.yml中声明了esp_tinyusb的依赖,这样main组件内就可以使用该USB组件,如果想在其他组件(自定义组件)中使用USB组件函数,就通常在该组件创建一个idf_component.yml文件,添加mian组件自动生成该文件的内容即可。
下面所以示例所使用的函数功能见ESP32实用API指南3-CSDN博客。
2.1 USB设置注册
在设置中搜索tinyusb,找到下列选项,使能挂起和恢复回调。组件提供了内部回调函数来调度暂停/恢复事件。

2.1.1 usb_device.c
cpp
#include <stdio.h>
#include "usb_device.h"
#include "tinyusb.h" // TinyUSB核心头文件
#include "tinyusb_default_config.h" // TinyUSB默认配置头文件
#include "tinyusb_cdc_acm.h" // TinyUSB CDC ACM类头文件
#include "tinyusb_console.h" // TinyUSB控制台头文件
static const char *TAG = "USB_DEVICE"; // 日志标签
void USB_DEVICE_Init(void)
{
const tinyusb_config_t tusb_cfg = TINYUSB_DEFAULT_CONFIG();
ESP_ERROR_CHECK(tinyusb_driver_install(&tusb_cfg));
tinyusb_config_cdcacm_t cdc_cfg = {
.cdc_port = TINYUSB_CDC_ACM_0,
.callback_rx = NULL,
.callback_rx_wanted_char = NULL,
.callback_line_state_changed = NULL,
.callback_line_coding_changed = NULL
};
ESP_ERROR_CHECK(tinyusb_cdcacm_init(&cdc_cfg));
}
2.1.2 usb_device.h
cpp
#ifndef USB_DEVICE_H
#define USB_DEVICE_H
#include <string.h> // 字符串处理函数
#include "esp_log.h" // ESP32日志函数
#include "FreeRTOS/FreeRTOS.h" // FreeRTOS函数
#include "FreeRTOS/task.h" // FreeRTOS任务管理函数
#include "FreeRTOS/semphr.h" // FreeRTOS信号量管理函数
void USB_DEVICE_Init(void);
#endif // USB_DEVICE_H
2.1.3 main.c
cpp
#include <stdio.h>
#include "user.h"
#include "usb_device.h"
void app_main(void)
{
CONSOLE_REPL_INIT(); // 初始化控制台REPL环境
USB_DEVICE_Init();
while(1)
{
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
2.1.4 结果说明

对于四个回调,当芯片的USB端口插上USB设备时会触发USB设备连接回调,但是拔出USB设备不会触发USB设备断开回调,这是因为:
USB设备连接回调是通过检测 USB D+/D- 数据线上的电平变化来自动触发的。USB设备断开回调必须检测 VBUS (5V) 是否消失。但 ESP32-S3 芯片没有专用的内部 VBUS 检测引脚,它需要通过一个外部 GPIO 来监控 VBUS 的电平状态。而我的开发板没有进行这部分的硬件设计,自然无法检测。
设备挂起和恢复回调没有问题,但是我连接的是电脑,需要睡眠才能模拟,所以这部分没有测试。
2.2 CDC-ACM使用
CDC全称是USB Communications Device Class,这是 USB 国际论坛(USB-IF)定义的一种标准设备类别。 CDC 类专门用于通信和网络设备。只要设备声明自己是 CDC 类,操作系统(Windows、Linux、macOS)就知道这是一个通信设备,并会调用系统内置的标准通信驱动来处理它,而不需要设备厂家提供专用的驱动光盘或安装包。
ACM全称是Abstract Control Model,ACM 是 CDC 类别下的一个子类(Subclass)。CDC 类很大(包含网络、ISDN、调制解调器等),而 ACM 专门用于模拟传统的"串行通信端口"或"调制解调器"。它定义了一套标准的控制命令(如设置波特率、数据位、停止位、奇偶校验等)和数据传输格式。
当设备支持 CDC-ACM 时,当你把这个 USB 设备插到电脑上时,操作系统会识别它,并自动生成一个虚拟串口(即插即用,免驱虚拟串口)。你可以使用任何常见的串口调试软件连接到这个虚拟串口,进行数据收发、打印调试信息或发送控制指令。
与USB转串口芯片区别:
- CDC-ACM:通常是单片机自己通过代码模拟出来的(原生 USB),不需要额外的转换芯片,成本低,且符合国际标准。
- CH340/CP2102 等:是外挂的专用硬件转换芯片。它们把 USB 信号在硬件层面上转换成传统的 TTL 串口信号(TX/RX)。这类芯片通常需要安装厂家专用的驱动程序
下面开始配置,首先在设置中搜索tinyusb,找到下列选项,勾选启动CDC功能,

设置的时候要注意一点,Endpoint buffer不能大于RX FIFO buffer,Endpoint buffer不能小于TX FIFO buffer。这是因为,接收数据流程是把Endpoint buffer内的数据复制到RX FIFO buffer,发送数据是把TX FIFO buffer复制到Endpoint buffer。不满足大小要求会导致数据损坏且无法恢复。最简单就是三者大小保持一致。
为了获得最佳性能可以都设置为8192,这 是 USB 高速传输的典型最佳值(接近 USB 2.0 高速的理论最大传输单元)
下面是一个基于CDC-ACM实现控制台数据流重定向的工程,需要设置中将默认的控制台输出选为一个串口

2.2.1 usb_device.c
cpp
#include <stdio.h>
#include "usb_device.h"
#include "tinyusb.h" // TinyUSB核心头文件
#include "tinyusb_default_config.h" // TinyUSB默认配置头文件
#include "tinyusb_cdc_acm.h" // TinyUSB CDC ACM类头文件
#include "tinyusb_console.h" // TinyUSB控制台头文件
static const char *TAG = "USB_DEVICE"; // 日志标签
void USB_DEVICE_Init(void)
{
const tinyusb_config_t tusb_cfg = TINYUSB_DEFAULT_CONFIG();
ESP_ERROR_CHECK(tinyusb_driver_install(&tusb_cfg));
tinyusb_config_cdcacm_t cdc_cfg = {
.cdc_port = TINYUSB_CDC_ACM_0,
.callback_rx = NULL,
.callback_rx_wanted_char = NULL,
.callback_line_state_changed = NULL,
.callback_line_coding_changed = NULL
};
ESP_ERROR_CHECK(tinyusb_cdcacm_init(&cdc_cfg));
while (1) {
ESP_LOGI(TAG, "log -> UART");
vTaskDelay(1000 / portTICK_PERIOD_MS);
fprintf(stdout, "example: print -> stdout\n");
vTaskDelay(1000 / portTICK_PERIOD_MS);
fprintf(stderr, "example: print -> stderr\n");
vTaskDelay(1000 / portTICK_PERIOD_MS);
ESP_ERROR_CHECK(tinyusb_console_init(TINYUSB_CDC_ACM_0)); // log to usb
ESP_LOGI(TAG, "log -> USB");
vTaskDelay(1000 / portTICK_PERIOD_MS);
fprintf(stdout, "example: print -> stdout\n");
vTaskDelay(1000 / portTICK_PERIOD_MS);
fprintf(stderr, "example: print -> stderr\n");
vTaskDelay(1000 / portTICK_PERIOD_MS);
ESP_ERROR_CHECK(tinyusb_console_deinit(TINYUSB_CDC_ACM_0)); // log to usb
}
}
2.2.2 usb_device.h
cpp
#ifndef USB_DEVICE_H
#define USB_DEVICE_H
#include <string.h> // 字符串处理函数
#include "esp_log.h" // ESP32日志函数
#include "FreeRTOS/FreeRTOS.h" // FreeRTOS函数
#include "FreeRTOS/task.h" // FreeRTOS任务管理函数
#include "FreeRTOS/semphr.h" // FreeRTOS信号量管理函数
void USB_DEVICE_Init(void);
#endif // USB_DEVICE_H
2.2.3 main.c
cpp
#include <stdio.h>
#include "user.h"
#include "usb_device.h"
void app_main(void)
{
CONSOLE_REPL_INIT(); // 初始化控制台REPL环境
USB_DEVICE_Init();
while(1)
{
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
2.2.4 结果说明
输出数据流会在两个串口之间不断地交替。
下面是一个使用USB进行数据收发的示例
2.2.5 usb_device.c
cpp
#include <stdio.h>
#include "usb_device.h"
#include "tinyusb.h" // TinyUSB核心头文件
#include "tinyusb_default_config.h" // TinyUSB默认配置头文件
#include "tinyusb_cdc_acm.h" // TinyUSB CDC ACM类头文件
#include "tinyusb_console.h" // TinyUSB控制台头文件
static const char *TAG = "USB_DEVICE"; // 日志标签
static void device_event_handler(tinyusb_event_t *event, void *arg);
void tinyusb_cdc_callback(int itf, cdcacm_event_t *event);
void USB_DEVICE_Init(void)
{
tinyusb_config_t tusb_cfg = TINYUSB_DEFAULT_CONFIG(device_event_handler);
ESP_ERROR_CHECK(tinyusb_driver_install(&tusb_cfg));
tinyusb_config_cdcacm_t cdc_cfg = {
.cdc_port = TINYUSB_CDC_ACM_0,
.callback_rx = &tinyusb_cdc_callback,
.callback_rx_wanted_char = &tinyusb_cdc_callback,
.callback_line_state_changed = &tinyusb_cdc_callback,
.callback_line_coding_changed = &tinyusb_cdc_callback
};
ESP_ERROR_CHECK(tinyusb_cdcacm_init(&cdc_cfg));
uint8_t tx_data[] = "Hello from ESP32 USB Device!";
while(1)
{
tinyusb_cdcacm_write_queue(TINYUSB_CDC_ACM_0, tx_data, sizeof(tx_data) - 1);
tinyusb_cdcacm_write_flush(TINYUSB_CDC_ACM_0, portMAX_DELAY);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
static void device_event_handler(tinyusb_event_t *event, void *arg)
{
switch (event->id) {
case TINYUSB_EVENT_ATTACHED: // 连接到USB主机的设备
ESP_LOGI(TAG, "USB设备已连接");
break;
case TINYUSB_EVENT_DETACHED: // 设备已从USB主机上分离
ESP_LOGI(TAG, "USB设备已断开");
break;
case TINYUSB_EVENT_SUSPENDED:
ESP_LOGI(TAG, "USB设备已挂起");
break;
case TINYUSB_EVENT_RESUMED:
ESP_LOGI(TAG, "USB设备已恢复");
break;
default:
ESP_LOGW(TAG, "未知USB事件: %d", event->id);
break;
}
}
void tinyusb_cdc_callback(int itf, cdcacm_event_t *event)
{
static uint8_t rx_buffer[64]; // 接收缓冲区
size_t rx_size = 0;
switch (event->type) {
case CDC_EVENT_RX:
ESP_LOGI(TAG, "CDC ACM接口%d收到数据", itf);
tinyusb_cdcacm_read(itf, rx_buffer, sizeof(rx_buffer), &rx_size);
if (rx_size > 0) {
ESP_LOGI(TAG, "收到USB设备数据大小为: %d, 数据内容: %s", rx_size, rx_buffer);
}
memset(rx_buffer, 0, sizeof(rx_buffer));
break;
case CDC_EVENT_RX_WANTED_CHAR:
ESP_LOGI(TAG, "CDC ACM接口%d收到指定字符: %c", itf, event->rx_wanted_char_data.wanted_char);
break;
case CDC_EVENT_LINE_STATE_CHANGED:
ESP_LOGI(TAG, "CDC ACM接口%d的线路状态已改变", itf);
break;
case CDC_EVENT_LINE_CODING_CHANGED:
ESP_LOGI(TAG, "CDC ACM接口%d的线路编码已改变为", itf);
break;
default:
break;
}
}
其他文件内容保持不变


这个代码主要的改变是注册cdcacm时定义了四种类型的回调函数(统一函数名,回调函数内部区分回调类型),各回调触发方式如下:
- callback_rx:当主机(电脑)通过 USB 发送任何数据到 ESP32 时立即触发
- callback_rx_wanted_char:当接收到预设的特殊字符时触发(非所有数据)
- callback_line_state_changed:当主机改变DTR/RTS 信号时触发
- callback_line_coding_changed:当主机修改串口参数时触发
其中接收预设字符回调我这没找到预设字符API,经过测试一些典型示例,也没发现内部预先定义。不过这个回调使用场合很低,没有也不影响。
2.3 HID设备使用
HID 的全称是 Human Interface Device, 它是人类用来和计算机(或手机、平板、游戏主机等)进行直接交互的硬件设备。它是 USB 标准和蓝牙标准中非常重要且最普及的一个设备类别。只要是用来"向电脑输入指令"或"感受电脑反馈"的外部硬件,基本都属于 HID:
- 最基础的:键盘、鼠标、触控板、轨迹球。
- 娱乐类的:游戏手柄(Xbox/PS手柄)、飞行摇杆、赛车方向盘、VR 控制器。
- 专业类的:数位板(手绘板)、触摸屏、多媒体控制台(调音台)。
- 特殊输入类的:条码扫描枪、指纹识别仪、身份证阅读器。
HID 的两大主流连接方式
- USB HID :
最常见的形式。只要是通过 USB 接口(Type-A 或 Type-C)连接的键鼠、手柄,底层走的都是 USB HID 协议。 - Bluetooth HID (BLE HID) :
现在的无线鼠标、无线键盘,大多采用蓝牙 HID 协议(具体叫 HID over GATT)。这也是为什么你的蓝牙键盘可以直接连上 iPad 或安卓手机打字,因为移动操作系统原生支持蓝牙 HID。
那么我们接下来把开发板的usb端口配置成HID设备,用来向电脑输入文字(模拟键盘)和控制光标(模拟鼠标)。
先在设置中开启HID功能,这个数值代表HID接口数量值,选为1就行,只有一个HID设备

2.3.1 usb_device.c
cpp
/**
* @file usb_device.c
* @brief USB CDC ACM 与 HID 复合设备驱动实现
* @note 基于 TinyUSB 协议栈,完成 USB 设备初始化、CDC 数据收发以及 HID 键盘/鼠标报文发送
*/
#include <stdio.h>
#include "usb_device.h"
#include "tinyusb.h" // TinyUSB 核心头文件
#include "tinyusb_default_config.h" // TinyUSB 默认配置
#include "tinyusb_cdc_acm.h" // TinyUSB CDC ACM 类驱动
#include "class/hid/hid_device.h" // TinyUSB HID 类驱动
#include "driver/gpio.h" // ESP-IDF GPIO 头文件
/* ============================================================================
* 宏定义、枚举与静态数据
* ==========================================================================*/
static const char *TAG = "USB_DEVICE"; // ESP-IDF 日志标签
/* ============================================================================
* 自定义 USB 描述符(HID 复合设备)
* TinyUSB 默认配置并不包含 HID 相关描述符,因此需要在这里显式注入
* ==========================================================================*/
enum {
ITF_NUM_HID, // HID 键盘+鼠标接口
ITF_NUM_TOTAL // 接口总数 = 1
};
enum {
STRID_LANGID = 0,
STRID_MANUFACTURER,
STRID_PRODUCT,
STRID_SERIAL,
STRID_CDC,
STRID_HID,
};
enum {
EPNUM_HID = 1, // HID 端点 (IN)
};
/** @brief 全长配置描述符总长度 */
#define CUSTOM_CFG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + CFG_TUD_HID * TUD_HID_DESC_LEN)
/* ============================================================================
* HID 复合报告描述符(键盘 Report ID=1 + 鼠标 Report ID=2)
* ==========================================================================*/
/**
* @brief 复合 HID 报告描述符
* - Report ID 1: 标准 Boot Keyboard
* - Report ID 2: 标准 Mouse
*/
static const uint8_t hid_composite_report_desc[] = {
// ===== Report ID 1: 键盘 =====
TUD_HID_REPORT_DESC_KEYBOARD(HID_REPORT_ID(HID_ITF_PROTOCOL_KEYBOARD)),
// ===== Report ID 2: 鼠标 =====
TUD_HID_REPORT_DESC_MOUSE(HID_REPORT_ID(HID_ITF_PROTOCOL_MOUSE)),
};
/**
* @brief 自定义 Full-Speed 配置描述符(包含 1 个 HID 接口)
* 这里仅设置 HID 端点,实际数据包长度由 TinyUSB 配置宏控制
*/
static const uint8_t custom_fs_config_desc[] = {
TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, CUSTOM_CFG_TOTAL_LEN,
TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, 100),
TUD_HID_DESCRIPTOR(ITF_NUM_HID, STRID_HID, HID_ITF_PROTOCOL_NONE,
sizeof(hid_composite_report_desc),
0x80 | EPNUM_HID, CFG_TUD_HID_EP_BUFSIZE, 10),
};
/**
* @brief 自定义 High-Speed 配置描述符(包含 1 个 HID 接口)
* ESP32-P4 使用高速端口时,若缺少该描述符会导致 HS 枚举失败
*/
static const uint8_t custom_hs_config_desc[] = {
TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, CUSTOM_CFG_TOTAL_LEN,
TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, 100),
TUD_HID_DESCRIPTOR(ITF_NUM_HID, STRID_HID, HID_ITF_PROTOCOL_NONE,
sizeof(hid_composite_report_desc),
0x80 | EPNUM_HID, CFG_TUD_HID_EP_BUFSIZE, 10),
};
/* ============================================================================
* 私有函数前置声明
* ==========================================================================*/
/**
* @brief USB 设备状态事件回调(连接/断开/挂起/恢复)
* @param event USB 事件结构体指针
* @param arg 用户自定义参数(当前未使用)
*/
static void device_event_handler(tinyusb_event_t *event, void *arg);
/**
* @brief CDC ACM 数据及线路状态回调
* @param itf CDC 接口编号
* @param event CDC 事件结构体指针
*/
static void tinyusb_cdc_callback(int itf, cdcacm_event_t *event);
/* ============================================================================
* 公共接口
* ==========================================================================*/
/**
* @brief 初始化 CDC ACM 设备功能
* 1. 安装 TinyUSB 驱动并注册设备事件回调
* 2. 初始化 CDC ACM 接口并绑定收发回调
* 3. 进入无限循环,周期性向主机发送测试字符串
* @note 该函数不会返回,适合放在独立的 FreeRTOS 任务中执行
*/
void USB_DEVICE_CDCACM_Init(void)
{
/* 安装 TinyUSB 驱动,注册设备事件回调 */
tinyusb_config_t tusb_cfg = TINYUSB_DEFAULT_CONFIG(device_event_handler);
ESP_ERROR_CHECK(tinyusb_driver_install(&tusb_cfg));
/* 配置 CDC ACM 接口,绑定各类回调 */
tinyusb_config_cdcacm_t cdc_cfg = {
.cdc_port = TINYUSB_CDC_ACM_0,
.callback_rx = &tinyusb_cdc_callback,
.callback_rx_wanted_char = &tinyusb_cdc_callback,
.callback_line_state_changed = &tinyusb_cdc_callback,
.callback_line_coding_changed = &tinyusb_cdc_callback,
};
ESP_ERROR_CHECK(tinyusb_cdcacm_init(&cdc_cfg));
/* 待发送的测试数据 */
uint8_t tx_data[] = "Hello from ESP32 USB Device!";
/* 主循环:周期性向主机发送数据 */
while (1) {
tinyusb_cdcacm_write_queue(TINYUSB_CDC_ACM_0, tx_data, sizeof(tx_data) - 1);
tinyusb_cdcacm_write_flush(TINYUSB_CDC_ACM_0, portMAX_DELAY);
vTaskDelay(pdMS_TO_TICKS(1000)); // 延时 1 秒
}
}
typedef enum {
MOUSE_DIR_RIGHT,
MOUSE_DIR_DOWN,
MOUSE_DIR_LEFT,
MOUSE_DIR_UP,
MOUSE_DIR_MAX,
} mouse_dir_t;
#define DISTANCE_MAX 125
#define DELTA_SCALAR 5
static void mouse_draw_square_next_delta(int8_t *delta_x_ret, int8_t *delta_y_ret);
void USB_DEVICE_HID_Init(void)
{
// 配置引导按钮对应的 GPIO35 为输入脚,用于控制 HID 发送行为
const gpio_config_t boot_button_config = {
.pin_bit_mask = BIT64(GPIO_NUM_35),
.mode = GPIO_MODE_INPUT,
.intr_type = GPIO_INTR_DISABLE,
.pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
};
ESP_ERROR_CHECK(gpio_config(&boot_button_config));
tinyusb_config_t tusb_cfg = TINYUSB_DEFAULT_CONFIG(device_event_handler);
/* 注入自定义配置描述符与字符串描述符,确保 HID 接口能被正确枚举 */
tusb_cfg.descriptor.full_speed_config = custom_fs_config_desc;
tusb_cfg.descriptor.high_speed_config = custom_hs_config_desc;
ESP_ERROR_CHECK(tinyusb_driver_install(&tusb_cfg));
while(1)
{
if(tud_mounted()) // 检查设备是否已连接并完成配置
{
static bool send_hid_data = false;
if(send_hid_data)
{
ESP_LOGI(TAG, "发送 HID 键盘和鼠标数据");
uint8_t keycode[6] = {HID_KEY_N, HID_KEY_H, HID_KEY_SPACE, HID_KEY_1, HID_KEY_2};
tud_hid_keyboard_report(HID_ITF_PROTOCOL_KEYBOARD, 0, keycode); // 发送按键报告
vTaskDelay(pdMS_TO_TICKS(50));
tud_hid_keyboard_report(HID_ITF_PROTOCOL_KEYBOARD, 0, NULL); // 发送空报告
int8_t delta_x;
int8_t delta_y;
for (int i = 0; i < (DISTANCE_MAX / DELTA_SCALAR) * 4; i++) {
// Get the next x and y delta in the draw square pattern
mouse_draw_square_next_delta(&delta_x, &delta_y);
tud_hid_mouse_report(HID_ITF_PROTOCOL_MOUSE, 0x00, delta_x, delta_y, 0, 0);
vTaskDelay(pdMS_TO_TICKS(20));
}
}
send_hid_data = !gpio_get_level(GPIO_NUM_35);
vTaskDelay(pdMS_TO_TICKS(100));
}
else
{
ESP_LOGI(TAG, "USB 设备未连接,等待连接...");
vTaskDelay(pdMS_TO_TICKS(500));
}
}
}
static void mouse_draw_square_next_delta(int8_t *delta_x_ret, int8_t *delta_y_ret)
{
static mouse_dir_t cur_dir = MOUSE_DIR_RIGHT;
static uint32_t distance = 0;
// 根据当前方向生成下一次鼠标位移增量
if (cur_dir == MOUSE_DIR_RIGHT) {
*delta_x_ret = DELTA_SCALAR;
*delta_y_ret = 0;
} else if (cur_dir == MOUSE_DIR_DOWN) {
*delta_x_ret = 0;
*delta_y_ret = DELTA_SCALAR;
} else if (cur_dir == MOUSE_DIR_LEFT) {
*delta_x_ret = -DELTA_SCALAR;
*delta_y_ret = 0;
} else if (cur_dir == MOUSE_DIR_UP) {
*delta_x_ret = 0;
*delta_y_ret = -DELTA_SCALAR;
}
// 累加当前方向已经移动的距离,达到阈值后切换方向
distance += DELTA_SCALAR;
// 当当前边长走完时,切换到下一条边
if (distance >= DISTANCE_MAX) {
distance = 0;
cur_dir++;
if (cur_dir == MOUSE_DIR_MAX) {
cur_dir = 0;
}
}
}
/* ============================================================================
* 私有函数实现
* ==========================================================================*/
/**
* @brief 处理 TinyUSB 设备级事件
* 记录连接、断开、挂起和恢复等状态变化,便于调试和排查问题
*/
static void device_event_handler(tinyusb_event_t *event, void *arg)
{
(void)arg; // 抑制未使用参数警告
switch (event->id) {
case TINYUSB_EVENT_ATTACHED: // 设备已连接主机
ESP_LOGI(TAG, "USB 设备已连接");
break;
case TINYUSB_EVENT_DETACHED: // 设备已从主机断开
ESP_LOGI(TAG, "USB 设备已断开");
break;
case TINYUSB_EVENT_SUSPENDED: // 设备已挂起
ESP_LOGI(TAG, "USB 设备已挂起");
break;
case TINYUSB_EVENT_RESUMED: // 设备从挂起恢复
ESP_LOGI(TAG, "USB 设备已恢复");
break;
default:
ESP_LOGW(TAG, "未知 USB 事件: %d", event->id);
break;
}
}
/**
* @brief 处理 CDC ACM 接口事件
* 包括收到数据、收到指定字符、线路状态变化以及串口编码变化
* @note 接收缓冲区为 64 字节,超过部分将被截断
*/
static void tinyusb_cdc_callback(int itf, cdcacm_event_t *event)
{
static uint8_t rx_buffer[64]; // 接收缓冲区(静态,跨调用保持)
size_t rx_size = 0;
switch (event->type) {
case CDC_EVENT_RX: // 收到主机发来的数据
ESP_LOGI(TAG, "CDC ACM 接口 %d 收到数据", itf);
tinyusb_cdcacm_read(itf, rx_buffer, sizeof(rx_buffer), &rx_size);
if (rx_size > 0) {
ESP_LOGI(TAG, "数据大小: %d, 内容: %s", rx_size, rx_buffer);
}
memset(rx_buffer, 0, sizeof(rx_buffer)); // 清空缓冲区
break;
case CDC_EVENT_RX_WANTED_CHAR: // 收到指定的特殊字符
ESP_LOGI(TAG, "CDC ACM 接口 %d 收到指定字符: %c",
itf, event->rx_wanted_char_data.wanted_char);
break;
case CDC_EVENT_LINE_STATE_CHANGED: // DTR/RTS 线路状态变化
ESP_LOGI(TAG, "CDC ACM 接口 %d 线路状态已改变", itf);
break;
case CDC_EVENT_LINE_CODING_CHANGED: // 波特率等编码参数变化
ESP_LOGI(TAG, "CDC ACM 接口 %d 线路编码已改变", itf);
break;
default:
break;
}
}
/* ============================================================================
* HID 报告描述符回调(TinyUSB 要求用户实现,无弱定义)
* ==========================================================================*/
/**
* @brief 返回指定 HID 实例对应的报告描述符
* 当前仅使用一个实例,因此返回包含键盘和鼠标的复合描述符
*/
uint8_t const *tud_hid_descriptor_report_cb(uint8_t instance)
{
(void)instance;
return hid_composite_report_desc;
}
/**
* @brief 处理 GET_REPORT 控制请求
* 当前未实现,返回 0 会让主机收到 STALL 响应
*/
uint16_t tud_hid_get_report_cb(uint8_t instance, uint8_t report_id,
hid_report_type_t report_type,
uint8_t *buffer, uint16_t reqlen)
{
(void)instance;
(void)report_id;
(void)report_type;
(void)buffer;
(void)reqlen;
return 0; // STALL
}
/**
* @brief 处理 SET_REPORT 控制请求
* 读取主机下发的键盘 LED 状态并记录日志
*/
void tud_hid_set_report_cb(uint8_t instance, uint8_t report_id,
hid_report_type_t report_type,
uint8_t const *buffer, uint16_t bufsize)
{
(void)instance;
(void)report_id;
if (report_type == HID_REPORT_TYPE_OUTPUT && bufsize >= 1) {
uint8_t const kbd_leds = buffer[0];
// 根据 HID 协议,键盘 LED 状态位定义如下,当前回调只打日志,没有真正驱动硬件 LED:
ESP_LOGI(TAG, "键盘 LED: %s%s%s",
(kbd_leds & 0x01) ? "NumLock " : "",
(kbd_leds & 0x02) ? "CapsLock " : "",
(kbd_leds & 0x04) ? "ScrollLock " : "");
}
}
2.3.2 usb_device.h
cpp
#ifndef USB_DEVICE_H
#define USB_DEVICE_H
#include <string.h> // 字符串处理函数
#include "esp_log.h" // ESP32日志函数
#include "FreeRTOS/FreeRTOS.h" // FreeRTOS函数
#include "FreeRTOS/task.h" // FreeRTOS任务管理函数
#include "FreeRTOS/semphr.h" // FreeRTOS信号量管理函数
#include "esp_heap_caps.h" // ESP32内存分配函数
void USB_DEVICE_CDCACM_Init(void);
void USB_DEVICE_HID_Init(void);
#endif // USB_DEVICE_H
2.3.3 main.c
cpp
#include <stdio.h>
#include "user.h"
#include "usb_device.h"
void app_main(void)
{
CONSOLE_REPL_INIT(); // 初始化控制台REPL环境
USB_DEVICE_HID_Init();
while(1)
{
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
2.3.4 结果说明
在tinyusb组件的usb_descriptors.c文件中的配置描述符定义中(descriptor_fs_cfg_default\[\] 和 descriptor_hs_cfg_default\[\] 里)没有关于HID接口的说明,只有以下接口:
#if CFG_TUD_CDC → CDC 接口
#if CFG_TUD_MSC → MSC 接口
#if CFG_TUD_NCM → NCM 接口
#if CFG_TUD_VENDOR → Vendor 接口
所以我们需要自己去写配置描述符,仿照usb_descriptors.c文件,配置HID的Full-Speed 配置描述符custom_fs_config_desc\[\]和High-Speed 配置描述符custom_hs_config_desc\[\]。
接着对TINYUSB_DEFAULT_CONFIG的默认配置添加修改,其中full_speed_config等于custom_fs_config_desc,high_speed_config等于custom_hs_config_desc。将自定义描述符写入设备配置中。其他字段可以保持默认即可。
接着写主体代码,先用tud_mounted()检查连接情况,接着使用tud_hid_keyboard_report发送一个完整的按键事件,再通过tud_hid_mouse_report发送鼠标事件。这里使用boot按键(35)来触发依次控制。
做完这些还差一点,以下3 个 HID 回调函数没有弱定义,需要我们自行添加,否则编译失败
tud_hid_descriptor_report_cb //返回 HID 报告描述符
tud_hid_get_report_cb //处理主机的 GET_REPORT 请求
tud_hid_set_report_cb //处理主机的 SET_REPORT 请求
以上配置描述符和回调函数这些内容,都是deepseek-v4-pro模型发现并解决的,实在是太厉害了!!!
烧录代码,使用usb线连接开发板和电脑,下面是日志打印

此时按下boot键,会发现屏幕光标处输出"你好12",鼠标顺时针画方框。此时打开设备管理器,可以看到鼠标和其他指针设备下面多了一个HID设备。

2.4 MSC的SPI flash应用
USB MSC (全称 USB Mass Storage Class ,即 USB大容量存储设备类)是USB标准中定义的一种设备类别协议。当你把一个设备插到电脑或手机上,系统把它识别为一个"U盘"或"可移动磁盘",并让你像操作本地硬盘一样直接读写里面的文件时,这个设备使用的就是 MSC 协议。
MSC 设备的核心特点
- 免驱与即插即用(Plug and Play):这是MSC最大的优势。几乎所有现代操作系统(Windows、macOS、Linux、Android、甚至车载系统和智能电视)都原生内置了MSC类驱动。设备插上就能用,不需要用户手动下载或安装任何驱动程序。
- 块级访问(Block-level Access):计算机会把MSC设备当成一块"裸硬盘",直接对其物理扇区(逻辑块地址 LBA)进行读写。文件系统(如FAT32、exFAT、NTFS)是由主机(电脑) 来直接管理和维护的。
- 跨平台兼容性极强:只要宿主设备支持USB和对应的文件系统,就能毫无障碍地读取MSC设备里的数据。
首先打开配置,使能MSC功能,其中FIFO缓冲区越大,读写速度越快,用户可以根据实际需要,灵活调整性能与存储占用的平衡点。/storage表示存储设备访问路径是根目录下的storage文件夹。后续配置成功后,也只能访问这个文件夹。

那接着我们去分区表partitions.csv增加一条分区配置。代表分区标签为storage,分区类型是data,子类型是fat,大小是2M。其中分区类型和子类型不能变,FAT 挂载只能访问这种类型的分区。关于分区表具体详情见往期文章。
storage , data, fat , 0x210000, 2M ,
我新建了两个文件usb_msc.c和usb_msc.h用来存放这部分功能代码。
先把需要的依赖添加好
idf_component_register(SRCS "usb_msc.c" "usb_device.c"
INCLUDE_DIRS "include"
PRIV_REQUIRES
esp_driver_gpio
spi_flash
esp_partition
fatfs
)
2.4.1 usb_msc.c
cpp
#include <stdio.h>
#include <string.h>
#include "esp_flash.h"
#include "esp_partition.h"
#include "esp_vfs_fat.h" // FAT 文件系统 VFS 挂载(esp_vfs_fat_mount_config_t)
#include "tinyusb.h" // TinyUSB 核心头文件
#include "tinyusb_default_config.h" // TinyUSB 默认配置宏
#include "tinyusb_msc.h" // TinyUSB MSC 存储管理 API
#include "usb_msc.h" // 本模块头文件
/* ============================================================================
* 宏定义与常量
* ==========================================================================*/
static const char *TAG = "USB_MSC";
/**
* FAT 文件系统挂载点路径
* 设备侧应用程序通过此路径访问存储分区中的文件
*/
#define MSC_STORAGE_MOUNT_PATH "/storage"
/**
* FAT 分配单元大小(字节)
* 较大的分配单元可提高大文件读写性能,但会浪费空间
* 推荐值:4KB ~ 32KB,这里使用 16KB
*/
#define MSC_ALLOCATION_UNIT_SIZE (16 * 1024)
/* ============================================================================
* 私有函数前置声明
* ==========================================================================*/
/**
* @brief 初始化 SPI Flash 并挂载 FAT 文件系统
*
* @param[out] wl_handle 输出的 Wear Levelling 句柄
* @return esp_err_t
* - ESP_OK: 挂载成功
* - ESP_ERR_NOT_FOUND: 未找到 FAT 分区
* - 其他: 挂载失败
*/
static esp_err_t storage_init_spiflash(wl_handle_t *wl_handle);
/**
* @brief USB 设备级事件处理函数
*
* @param[in] event 事件结构体指针
* @param[in] arg 用户自定义参数(当前未使用)
*/
static void device_event_handler(tinyusb_event_t *event, void *arg);
/* ============================================================================
* SPI Flash 存储初始化
* ==========================================================================*/
/**
* @brief 初始化 SPI Flash 并挂载 FAT 文件系统
*
* 执行流程:
* 1. 检查 Flash 驱动是否已就绪
* 2. 读取 Flash 芯片信息(JEDEC ID、容量等)
* 3. 查找分区表中的 FAT 数据分区
* 4. 挂载 FAT 文件系统的 Wear Levelling 层
*
* @param[out] wl_handle 输出的 Wear Levelling 句柄,供后续 MSC 使用
* @return esp_err_t
* - ESP_OK: 挂载成功
* - ESP_ERR_NOT_FOUND: 未找到 FAT 分区
* - 其他: esp_vfs_fat_spiflash_mount_rw_wl() 返回的错误码
*/
static esp_err_t storage_init_spiflash(wl_handle_t *wl_handle)
{
ESP_LOGI(TAG, "========== SPI Flash 存储初始化开始 ==========");
/* ---- 第 1 步:检查 Flash 驱动状态 ---- */
if (!esp_flash_chip_driver_initialized(esp_flash_default_chip)) {
ESP_LOGE(TAG, "[1/4] Flash 驱动未初始化");
return ESP_ERR_INVALID_STATE;
}
ESP_LOGI(TAG, "[1/4] Flash 驱动已就绪");
/* ---- 第 2 步:读取 Flash 芯片信息 ---- */
uint32_t flash_id;
uint32_t curr_size, phys_size;
// 读取 JEDEC ID(包含厂商、类型、容量信息)
esp_flash_read_id(esp_flash_default_chip, &flash_id);
ESP_LOGI(TAG, "[2/4] Flash JEDEC ID: 0x%06" PRIX32, flash_id);
// 读取当前使用大小和物理芯片容量
esp_flash_get_size(esp_flash_default_chip, &curr_size);
esp_flash_get_physical_size(esp_flash_default_chip, &phys_size);
ESP_LOGI(TAG, "[2/4] Flash 容量信息:");
ESP_LOGI(TAG, " - 当前使用容量 : %lu MB (%lu 字节)",
curr_size / (1024 * 1024), curr_size);
ESP_LOGI(TAG, " - 物理芯片容量 : %lu MB (%lu 字节)",
phys_size / (1024 * 1024), phys_size);
/* ---- 第 3 步:查找 FAT 数据分区 ---- */
const esp_partition_t *data_partition = esp_partition_find_first(
ESP_PARTITION_TYPE_DATA, /* type: data ------ 与挂载 API 要求一致 */
ESP_PARTITION_SUBTYPE_DATA_FAT, /* subtype: fat ------ 必须是 FAT 类型分区 */
NULL
);
if (data_partition == NULL) {
ESP_LOGE(TAG, "[3/4] 未找到 FAT 数据分区");
ESP_LOGE(TAG, " 请检查分区表中是否存在 type=data, subtype=fat 的分区");
return ESP_ERR_NOT_FOUND;
}
ESP_LOGI(TAG, "[3/4] 找到 FAT 分区:");
ESP_LOGI(TAG, " - 分区标签 : %s", data_partition->label);
ESP_LOGI(TAG, " - 分区地址 : 0x%08" PRIX32, data_partition->address);
ESP_LOGI(TAG, " - 分区大小 : %lu KB (%lu 字节)",
data_partition->size / 1024, data_partition->size);
/* ---- 第 4 步:挂载 FAT 文件系统(含 Wear Levelling)---- */
ESP_LOGI(TAG, "[4/4] 正在挂载 FAT 文件系统...");
/* FAT 文件系统挂载配置 */
const esp_vfs_fat_mount_config_t mount_config = {
.max_files = 5, /* 最大同时打开文件数 */
.format_if_mount_failed = true, /* 首次使用无文件系统时自动格式化 */
.allocation_unit_size = MSC_ALLOCATION_UNIT_SIZE, /* FAT 簇分配单元大小 */
.disk_status_check_enable = false, /* 不启用磁盘状态检查(SPI Flash) */
.use_one_fat = false, /* 使用双 FAT 表(更可靠) */
};
esp_err_t ret = esp_vfs_fat_spiflash_mount_rw_wl(
MSC_STORAGE_MOUNT_PATH, /* VFS 挂载点路径 */
data_partition->label, /* 分区标签(动态获取,不硬编码) */
&mount_config, /* FAT 文件系统挂载配置 */
wl_handle /* [输出] Wear Levelling 句柄 */
);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "[4/4] FAT 文件系统挂载失败: %s", esp_err_to_name(ret));
return ret;
}
ESP_LOGI(TAG, "[4/4] FAT 文件系统挂载成功");
ESP_LOGI(TAG, " 挂载点路径: %s", MSC_STORAGE_MOUNT_PATH);
ESP_LOGI(TAG, "========== SPI Flash 存储初始化完成 ==========\n");
return ESP_OK;
}
/* ============================================================================
* MSC 存储事件回调
* ==========================================================================*/
/**
* @brief TinyUSB MSC 存储事件回调函数
*
* 当存储设备的挂载状态发生变化时,由 TinyUSB MSC 驱动调用。
* 用于记录挂载/卸载事件,便于调试和监控。
*
* 事件类型说明:
* - TINYUSB_MSC_EVENT_MOUNT_START : 开始挂载/卸载操作
* - TINYUSB_MSC_EVENT_MOUNT_COMPLETE : 挂载/卸载操作完成
* - TINYUSB_MSC_EVENT_MOUNT_FAILED : 挂载操作失败
* - TINYUSB_MSC_EVENT_FORMAT_REQUIRED: 需要格式化才能使用
*
* @param[in] handle 存储设备句柄
* @param[in] event 事件数据结构体
* @param[in] arg 用户自定义参数(当前未使用)
*/
void storage_mount_changed_cb(tinyusb_msc_storage_handle_t handle,
tinyusb_msc_event_t *event,
void *arg)
{
switch (event->id) {
case TINYUSB_MSC_EVENT_MOUNT_START:
ESP_LOGI(TAG, "[MSC事件] 开始挂载存储设备");
ESP_LOGI(TAG, " 句柄: %p", (void *)handle);
break;
case TINYUSB_MSC_EVENT_MOUNT_COMPLETE:
if (event->mount_point == TINYUSB_MSC_STORAGE_MOUNT_APP) {
ESP_LOGI(TAG, "[MSC事件] 存储已挂载到应用层(设备侧)");
ESP_LOGI(TAG, " 访问路径: %s", MSC_STORAGE_MOUNT_PATH);
} else {
ESP_LOGI(TAG, "[MSC事件] 存储已暴露给 USB 主机");
}
break;
case TINYUSB_MSC_EVENT_MOUNT_FAILED:
ESP_LOGE(TAG, "[MSC事件] 存储设备挂载失败");
ESP_LOGE(TAG, " 句柄: %p", (void *)handle);
break;
case TINYUSB_MSC_EVENT_FORMAT_REQUIRED:
ESP_LOGW(TAG, "[MSC事件] 存储设备需要格式化");
ESP_LOGW(TAG, " 句柄: %p", (void *)handle);
break;
default:
ESP_LOGW(TAG, "[MSC事件] 未知事件类型: %d", event->id);
break;
}
}
/* ============================================================================
* USB 设备事件处理
* ==========================================================================*/
/**
* @brief USB 设备级事件处理函数
*
* 处理 USB 协议层事件,与存储层事件独立。
* 这些事件反映了 USB 设备与主机之间的连接状态变化。
*
* 事件类型说明:
* - TINYUSB_EVENT_ATTACHED : USB 设备连接到主机
* - TINYUSB_EVENT_DETACHED : USB 设备从主机断开
* - TINYUSB_EVENT_SUSPENDED : USB 设备进入挂起状态
* - TINYUSB_EVENT_RESUMED : USB 设备从挂起状态恢复
*
* @param[in] event 事件结构体指针
* @param[in] arg 用户自定义参数(当前未使用)
*/
static void device_event_handler(tinyusb_event_t *event, void *arg)
{
(void)arg; // 抑制未使用参数警告
switch (event->id) {
case TINYUSB_EVENT_ATTACHED:
ESP_LOGI(TAG, "[USB事件] 设备已连接到主机");
break;
case TINYUSB_EVENT_DETACHED:
ESP_LOGI(TAG, "[USB事件] 设备已从主机断开");
break;
case TINYUSB_EVENT_SUSPENDED:
ESP_LOGI(TAG, "[USB事件] 设备进入挂起状态");
break;
case TINYUSB_EVENT_RESUMED:
ESP_LOGI(TAG, "[USB事件] 设备从挂起状态恢复");
break;
default:
ESP_LOGW(TAG, "[USB事件] 未知事件类型: %d", event->id);
break;
}
}
/* ============================================================================
* 公共接口实现
* ==========================================================================*/
/**
* @brief 初始化 USB MSC 大容量存储设备
*
* 完整初始化流程:
* 1. 初始化 SPI Flash 并挂载 FAT 文件系统
* 2. 创建 TinyUSB MSC 存储实例,绑定 WL 句柄
* 3. 注册存储事件回调(监控挂载/卸载状态)
* 4. 安装 TinyUSB USB 驱动(启用 MSC 设备功能)
*
* 调用此函数后,当 USB 连接到主机时:
* - 主机会识别此设备为一个可移动磁盘(U 盘)
* - 主机可以对 /storage 分区进行文件读写操作
* - 设备侧通过 /storage 路径也可以访问同一分区
*
* @note 此函数为阻塞调用,不会返回。建议在 FreeRTOS 任务中运行。
*/
void USB_DEVICE_MSC_Init(void)
{
ESP_LOGI(TAG, "========== USB MSC 设备初始化开始 ==========");
/* ==== 第 1 步:初始化 SPI Flash 存储 ==== */
static wl_handle_t wl_handle;
esp_err_t ret = storage_init_spiflash(&wl_handle);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "SPI Flash 初始化失败: %s", esp_err_to_name(ret));
ESP_LOGE(TAG, "中止 MSC 初始化");
return;
}
/* ==== 第 2 步:配置并创建 MSC 存储实例 ==== */
tinyusb_msc_storage_handle_t storage_hdl = NULL;
/**
* MSC 存储配置结构体说明:
*
* .medium.wl_handle
* Wear Levelling 句柄,指向 SPI Flash 上的 FAT 分区
*
* .mount_point
* 初始挂载点:TINYUSB_MSC_STORAGE_MOUNT_USB 表示初始就暴露给 USB 主机
*
* .fat_fs.base_path
* 应用程序侧的 VFS 挂载路径
*
* .fat_fs.config
* FatFs 配置参数:
* - format_if_mount_failed : 挂载失败时是否自动格式化
* - max_files : 同时打开的最大文件数
* - allocation_unit_size : 簇分配单元大小(影响读写性能和空间利用率)
*/
tinyusb_msc_storage_config_t msc_storage_cfg = {
.medium = {
.wl_handle = wl_handle,
},
.mount_point = TINYUSB_MSC_STORAGE_MOUNT_USB,
.fat_fs = {
.base_path = MSC_STORAGE_MOUNT_PATH,
.config = {
.format_if_mount_failed = true,
.max_files = 5,
.allocation_unit_size = MSC_ALLOCATION_UNIT_SIZE,
},
},
};
ret = tinyusb_msc_new_storage_spiflash(&msc_storage_cfg, &storage_hdl);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "创建 MSC 存储实例失败: %s", esp_err_to_name(ret));
return;
}
ESP_LOGI(TAG, "MSC 存储实例创建成功");
ESP_LOGI(TAG, " 句柄地址: %p", (void *)storage_hdl);
/* ==== 第 3 步:注册存储事件回调 ==== */
ret = tinyusb_msc_set_storage_callback(storage_mount_changed_cb, NULL);
if (ret != ESP_OK) {
ESP_LOGW(TAG, "注册存储事件回调失败: %s", esp_err_to_name(ret));
// 警告但不中止,回调失败不影响基本功能
} else {
ESP_LOGI(TAG, "存储事件回调注册成功");
}
/* ==== 第 4 步:安装 TinyUSB USB 驱动 ==== */
/**
* TINYUSB_DEFAULT_CONFIG 宏说明:
*
* 该宏会自动选择适当的 USB 端口:
* - ESP32-P4 / ESP32-S31: 使用高速 USB 端口 (High Speed)
* - 其他芯片: 使用全速 USB 端口 (Full Speed)
*
* 参数 device_event_handler 是 USB 协议层事件回调,
* 用于处理连接/断开等底层 USB 事件
*/
tinyusb_config_t tusb_cfg = TINYUSB_DEFAULT_CONFIG(device_event_handler);
ret = tinyusb_driver_install(&tusb_cfg);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "安装 TinyUSB 驱动失败: %s", esp_err_to_name(ret));
return;
}
ESP_LOGI(TAG, "TinyUSB USB 驱动安装成功");
ESP_LOGI(TAG, "========== USB MSC 设备初始化完成 ==========");
ESP_LOGI(TAG, "等待主机连接...\n");
}
2.4.2 usb_msc.h
cpp
#ifndef __USB_MSC_H
#define __USB_MSC_H
#include <string.h>
#include "esp_log.h"
#include "esp_err.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "wear_levelling.h"
#include "tinyusb_msc.h"
/* ============================================================================
* 公共接口函数
* ==========================================================================*/
void USB_DEVICE_MSC_Init(void);
#endif /* __USB_MSC_H */
2.4.3 main.c
cpp
#include <stdio.h>
#include "user.h"
#include "usb_device.h"
void app_main(void)
{
CONSOLE_REPL_INIT(); // 初始化控制台REPL环境
USB_DEVICE_MSC_Init();
while(1)
{
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
2.4.4 结果说明
代码我使用ai生成了详细的注释,就不解释了,关于存储器的读写见ESP-IDF+vscode开发ESP32第七讲------存储设备读写_esp32-p4-module-dev-kit-CSDN博客
通过USB线连接主机,发现多出了一个磁盘,这就是开发板的flash磁盘,大家可以自行测试一下读写速度。

日志说明也很丰富。

总结
本章内容较多,可能情节上也有些啰嗦,大家多多包涵。

