ESP-IDF+vscode开发ESP32第十七讲——USB设备栈

目录

前言

一、USB知识

[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 的两大主流连接方式

  1. USB HID
    最常见的形式。只要是通过 USB 接口(Type-A 或 Type-C)连接的键鼠、手柄,底层走的都是 USB HID 协议。
  2. 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磁盘,大家可以自行测试一下读写速度。

日志说明也很丰富。


总结

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

相关推荐
爱就是恒久忍耐18 天前
VSCode里如何比较2个branch
ide·vscode·编辑器
意法半导体STM3218 天前
【官方原创】如何为STM32CubeMX2配置Visual Studio Code配置方案
vscode·stm32·单片机·嵌入式硬件·策略模式·stm32cubemx·嵌入式开发
bloglin9999918 天前
vscode中可视化的合并分支,在“合并编辑器中解析”中“与基线进行比较”是什么意思
ide·vscode·编辑器
sinat_3990102718 天前
snps usb ip及 vip 使用
usb
欢乐熊嵌入式编程18 天前
选型避坑:ESP32 vs STM32+模组 vs NB-IoT,不同场景怎么选
stm32·单片机·嵌入式硬件·物联网·esp32·嵌入式iot
天疆说18 天前
在 Ubuntu 的 VSCode 中配置 MATLAB
vscode·ubuntu·matlab
春日见19 天前
vscode的AI编程插件推荐:
大数据·ide·vscode·算法·机器学习·编辑器·ai编程
jieshenai19 天前
VScode sys.path,并使CTRL+左键可访问源码
ide·vscode·编辑器
qq_4480111619 天前
VSCode环境搭建
ide·vscode·编辑器