第四十七章 USB U盘实验
本章我们介绍ESP32-P4的USB HOST应用,即通过USB HOST功能,实现读写U盘/读卡器等大容量USB存储设备。
本章分为如下几个部分:
47.1 U盘简介
47.2 硬件设计
47.3 程序设计
47.4 下载验证
47.1 U盘简介
U盘(USB闪存盘,英文名:USB flash disk)是一种通过USB接口连接主机的微型高容量移动存储设备。它无需物理驱动器,支持即插即用,是最常用的移动存储工具之一。
ESP32-P4的USB OTG HS接口支持U盘功能。Espressif官方在ESP-IDF中提供了针对USB HOST大容量存储设备(MSC)的示例代码,路径为:esp-idf\examples\peripherals\usb\host\msc。本实验将参考官方示例代码,通过ESP32-P4的USB HOST接口实现对U盘或SD卡读卡器等设备的读写操作。
47.2 硬件设计
47.2.1 程序功能
本实验代码,开机后,初始化LCD和USB HOST,并不断轮询。当检测并识别U盘后,在LCD上面显示U盘总容量和连接状态。我们可通过espressif的REPL调用FATFS相关函数,来测试U盘数据的读写了。
47.2.2 硬件资源
1)RGBLCD/MIPILCD(引脚太多,不罗列出来)
2)USB_HOST HS接口
47.2.3 原理图
本开发板的USB HOST接口采用的是贴片USB母座,它和USB SLAVE是共用USB_DM和USB_DP信号的,所以USB HOST和USB SLAVE接口不能同时使用。USB HOST和ESP32-P4的连接原理图,如下图所示:

图47.2.2.1 USB母座与ESP32-P4的连接电路图
从上图可以看出,USB母座(USB_HOST)是直接连接到ESP32-P4上面的,所以硬件上不需要我们做什么操作,可直接使用。
需要注意的是:本实验被测试的U盘是通过USB母座(USB_HOST)连接到ESP32-P4的,此时,USB_SLAVE接口是不能连接USB设备或者主机的。如果需要用到USB转串口,数据线可以通过USB_ UART接口连接到电脑。
下面,看看开发板上的USB母座(USB_HOST)的位置,请将U盘插入这个接口上。

图47.2.2.2 开发板U盘母座
47.3 程序设计
47.3.1 USB HOST MSC的IDF驱动
usb_host_msc组件驱动位于ESP-IDF在线组件注册表中。如果需要将该组件添加到项目工程中,可按照以下步骤操作:
1)打开ESP-IDF注册表。
2)搜索 “usb_host_msc”组件。
3)将组件安装到项目中。
组件安装完成后,系统会自动更新main文件夹中的特殊组件清单文件idf_component.yml在项目编译时,系统会根据清单文件从注册表中下载并集成该组件到工程中。关于上述操作流程,可参考本书籍第八章的内容。
为了使用 usb_host_msc 组件提供的功能,首先需要在代码中导入以下头文件:
#include "usb_hid_msc.h"
接下来,作者将介绍一些常用的usb_host_msc函数,这些函数的描述及其作用如下:
1,安装USB HOST usb_host_install
该函数用于安装USB HOST,其函数原型如下:
esp_err_t usb_host_install(const usb_host_config_t *config);
函数形参:

表47.3.1.1 usb_host_install函数形参描述
返回值:
ESP_OK表示USB Host安装成功。
ESP_ERR_INVALID_ARG表示参数无效。
ESP_ERR_INVALID_STATE表示USB Host Library当前状态不正确。
ESP_ERR_NO_MEM表示内存不足。
config为指向USB Host Library 的配置结构体。接下来,笔者将详细介绍usb_host_config_t结构体中的各个成员变量,如下代码所示:
/**
* @brief USB Host Library 配置
*
* USB Host Library 的配置结构体,提供给 usb_host_install() 函数使用。
*/
typedef struct {
bool skip_phy_setup; /* 如果设置true,USB Hos将不会配置USB PHY */
bool root_port_unpowered; /* 如果设置为true,USB Host在安装时不会为根端口供电 */
int intr_flags; /* USB Host 栈底层中断的标志配置 */
usb_host_enum_filter_cb_t enum_filter_cb; /* 枚举过滤器回调函数 */
} usb_host_config_t;
上述结构体用于传递USB Host Library配置参数,以下对各个成员做简单介绍。
1)skip_phy_setup:
若此字段设置为true,则不会配置USB PHY。
2)root_port_unpowered:
若此字段设置为true,则不会为USB HOST提供电源。
3)intr_flags:
USB Host 栈底层中断的标志配置,一般设置为0。
4)enum_filter_cb:
过滤回调函数。
2,安装USB主机大容量存储类(MSC)驱动 msc_host_install
该函数用于安装USB主机大容量存储类(MSC)驱动,其函数原型如下:
esp_err_t msc_host_install(const msc_host_driver_config_t *config);
函数形参:

表47.3.1.2 msc_host_install函数形参描述
返回值:
ESP_OK表示安装存储类(MSC)驱动安装成功。
其他表示安装存储类(MSC)驱动失败
config为指向MSC(大容量存储类)配置结构体。接下来,笔者将详细介绍msc_host_driver_config_t结构体中的各个成员变量,如下代码所示:
/**
* @brief MSC(大容量存储类)配置结构体
*/
typedef struct {
/* 如果设置为 true,则会创建一个后台任务来处理 USB 事件;
否则用户需要定期调用 msc_host_handle_events 函数来处理事件。 */
bool create_backround_task;
size_t task_priority; /* 创建的后台任务的优先级 */
size_t stack_size; /* 创建的后台任务的堆栈大小 */
BaseType_t core_id; /* 选择后台任务运行的核心 ID */
msc_host_event_cb_t callback; /* 当发生 MSC 事件时调用的回调函数,不能为空 */
void *callback_arg; /* 用户提供的参数,将传递给回调函数 */
} msc_host_driver_config_t;
msc_host_driver_config_t结构体用于传递MSC(大容量存储类)配置参数,以便在调用msc_host_install时进行初始化和设置。
3,处理 USB 主机库事件 usb_host_lib_handle_events
该函数用于处理 USB 主机库事件,其函数原型如下:
esp_err_t usb_host_lib_handle_events(TickType_t timeout_ticks, uint32_t *event_flags_ret);
函数形参:

表47.3.1.3 msc_host_install函数形参描述
返回值:
ESP_OK表示无事件需要处理。
ESP_ERR_INVALID_STATE表示USB主机库尚未安装。
ESP_ERR_TIMEOUT表示等待事件的信号量超时。
4,释放所有设备usb_host_device_free_all
该函数用于释放所有设备,其函数原型如下:
esp_err_t usb_host_device_free_all(void);
函数形参:
无。
返回值:
ESP_OK表示所有设备已被释放(即没有设备需要释放)
ESP_ERR_INVALID_STATE表示客户端必须先注销
ESP_ERR_NOT_FINISHED表示仍有一个或多个设备需要释放,请等待USB_HOST_LIB_EVENT_FLAGS_ALL_FREE事件。
5,卸载USB主机库usb_host_uninstall
该函数用于卸载USB主机库,其函数原型如下:
esp_err_t usb_host_uninstall(void);
函数形参:
无。
返回值:
ESP_OK表示USB主机库成功卸载。
ESP_ERR_INVALID_STATE表示USB主机库未安装,或存在未完成的操作。
6,初始化 MSC 设备msc_host_install_device
该函数用于处理 USB 主机库事件,其函数原型如下:
esp_err_t msc_host_install_device( uint8_t device_address, msc_host_device_handle_t *device);
函数形参:

表47.3.1.4 msc_host_install_device函数形参描述
返回值:
ESP_OK表示初始化成功。
其他错误码表示初始化失败。
7,将MSC设备注册到虚拟文件系统msc_host_vfs_register
该函数用于将MSC设备注册到虚拟文件系统,其函数原型如下:
esp_err_t msc_host_vfs_register(msc_host_device_handle_t device, const char *base_path, const esp_vfs_fat_mount_config_t *mount_config, msc_host_vfs_handle_t *vfs_handle);
函数形参:

表47.3.1.5 msc_host_vfs_register函数形参描述
返回值:
ESP_OK表示注册成功。
其他错误码表示注册失败。
8,获取MSC设备信息msc_host_get_device_info
该函数用于将MSC设备注册到虚拟文件系统,其函数原型如下:
esp_err_t msc_host_get_device_info(msc_host_device_handle_t device, msc_host_device_info_t *info);
函数形参:

表47.3.1.6 msc_host_get_device_info函数形参描述
返回值:
ESP_OK表示成功获取设备信息。
其他错误码表示获取失败。
9,从虚拟文件系统中注销MSC设备msc_host_vfs_unregister
该函数用于从虚拟文件系统中注销MSC设备,其函数原型如下:
esp_err_t msc_host_vfs_unregister(msc_host_vfs_handle_t vfs_handle);
函数形参:

表47.3.1.7 msc_host_get_device_info函数形参描述
返回值:
ESP_OK表示成功获取设备信息。
其他错误码表示获取失败。
10,卸载MSC设备msc_host_uninstall_device
该函数用于卸载MSC设备,其函数原型如下:
esp_err_t msc_host_uninstall_device(msc_host_device_handle_t device);
函数形参:

表47.3.1.8 msc_host_uninstall_device函数形参描述
返回值:
ESP_OK表示成功卸载设备。
其他错误码表示卸载失败。
47.3.2 程序流程图

图47.3.2.1 U盘实验程序流程图
47.3.3 程序解析
本章节的例程是基于13_lcd实验编写的,所以笔者重点讲解有区别的文件。
1,USB HID MSC驱动
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。USB HID MSC驱动源码包括两个文件:usb_hid_msc.c和usb_hid_msc.h。
usb_hid_msc.h主要用于声明usb_hid_msc_init函数和USB消息结构体,以便在其他文件中调用,具体内容不再赘述。
下面我们再解析usb_hid_msc.c的程序,看一下初始化函数usb_hid_msc_init,代码如下:
QueueHandle_t usb_queue = NULL; /* 消息队列 */
/**
* @brief MSC设备回调(连接/断开)
* @param event:MSC事件
* @param arg:传入参数
* @retval 无
*/
static void msc_event_cb(const msc_host_event_t *event, void *arg)
{
/* 连接? */
if (event->event == MSC_DEVICE_CONNECTED)
{
/* 发现usb host 已插入U盘 */
usb_message_t message = {
.id = USB_DEVICE_CONNECTED,
.data.new_dev_address = event->device.address,
};
xQueueSend(usb_queue, &message, portMAX_DELAY);
}/* 断开? */
else if (event->event == MSC_DEVICE_DISCONNECTED)
{
/* 发现usb host 未检测到U盘 */
usb_message_t message = {
.id = USB_DEVICE_DISCONNECTED,
};
xQueueSend(usb_queue, &message, portMAX_DELAY);
}
}
/**
* @brief USB轮询任务
* @param args:未使用
* @retval 无
*/
static void usb_task_fun(void *args)
{
/* usb host配置*/
const usb_host_config_t host_config = {.intr_flags = ESP_INTR_FLAG_LEVEL1 };
/* usb host初始化 */
ESP_ERROR_CHECK(usb_host_install(&host_config));
/* msc host设备配置 */
const msc_host_driver_config_t msc_config = {
.create_backround_task = true, /* 创建回调任务 */
.task_priority = 5, /* 任务优先级 */
.stack_size = 4096, /* 任务堆栈大小 */
.callback = msc_event_cb, /* msc事件回调函数 */
};
/* msc host安装 */
ESP_ERROR_CHECK(msc_host_install(&msc_config));
bool has_clients = true;
while (1)
{
uint32_t event_flags;
/* 处理USB事件处理器 */
usb_host_lib_handle_events(portMAX_DELAY, &event_flags);
/* 所有的客户端已从主机注销了吗? */
if (event_flags & USB_HOST_LIB_EVENT_FLAGS_NO_CLIENTS)
{
has_clients = false;
/* 释放usb host内存 */
if (usb_host_device_free_all() == ESP_OK)
{
break;
};
}
/* 主机已释放所有设备? */
if (event_flags & USB_HOST_LIB_EVENT_FLAGS_ALL_FREE && !has_clients)
{
break;
}
}
vTaskDelay(pdMS_TO_TICKS(10));
/* 注销usb host */
ESP_ERROR_CHECK(usb_host_uninstall());
/* 删除usb轮询任务 */
vTaskDelete(NULL);
}
/**
* @brief USB读取U盘初始化
* @param 无
* @retval ESP_OK:初始化成功
*/
esp_err_t usb_hid_msc_init(void)
{
/* 创建新的消息队列 */
usb_queue = xQueueCreate(5, sizeof(usb_message_t));
assert(usb_queue);
BaseType_t usb_task = xTaskCreate(usb_task_fun,"usb_task",4096,NULL,2,NULL);
assert(usb_task);
return ESP_OK;
}
以上代码实现了基于ESP32-P4的USB主机功能,专注于管理USB存储设备的连接与断开,并通过消息队列将设备状态变化通知其他任务。核心任务usb_task_fun负责USB主机的初始化、事件轮询和资源释放,确保整个系统的稳定运行。回调函数msc_event_cb处理存储设备的连接和断开事件,usb_hid_msc_init则提供了一个简单的初始化入口,整合了消息队列和任务创建,便于扩展和调用。
2,main.c驱动代码
在main.c里面编写如下代码。
/* REPL控制器命令 */
const esp_console_cmd_t cmds[] = {
{
.command = "file",
.help = "file browsing",
.hint = NULL,
.func = &console_file_browsing,
},
{
.command = "read",
.help = "read PATH .eg: read /usb:/README.MD",
.hint = NULL,
.func = &console_read,
},
{
.command = "write",
.help = "write PATH Data .eg: write /usb:/README.MD Hello ALIENTEK",
.hint = NULL,
.func = &console_write,
},
{
.command = "info",
.help = "Usb flash drive information",
.hint = NULL,
.func = &console_info,
},
{
.command = "speed",
.help = "Test read/write speed,eg: speed /usb:/README.MD",
.hint = NULL,
.func = &console_test_speed,
},
{
.command = "device_cfg",
.help = "Device configuration information",
.hint = NULL,
.func = &console_device_cfg,
},
};
/**
* @brief 递归列出目录下的所有文件和文件夹
* @param path 需要列出的目录路径
* @param depth 当前目录层级
* @retval 无
*/
static void list_files(const char *path, int depth)
{
/* 省略代码...... */
}
/**
* @brief 测试读写速度
* @param argc:传入参数的数量
* @param argv:传入参数
* @retval -1:取消挂载失败。0:取消挂载成功
*/
static int console_test_speed(int argc, char **argv)
{
/* 省略代码...... */
return 0;
}
/**
* @brief 文件浏览
* @param argc:传入参数的数量
* @param argv:传入参数
* @retval -1:读取失败。0:读取成功
*/
static int console_file_browsing(int argc, char **argv)
{
/* 省略代码...... */
return 0;
}
/**
* @brief 读取存储设备的文件
* @param argc:传入参数的数量
* @param argv:传入参数
* @retval -1:读取失败。0:读取成功
*/
static int console_read(int argc, char **argv)
{
/* 省略代码...... */
return 0;
}
/**
* @brief 写入存储设备的文件
* @param argc:传入参数的数量
* @param argv:传入参数
* @retval -1:读取失败。0:读取成功
*/
static int console_write(int argc, char **argv)
{
/* 省略代码...... */
return 0;
}
/**
* @brief 获取U盘信息
* @param argc:传入参数的数量
* @param argv:传入参数
* @retval -1:读取失败。0:读取成功
*/
static int console_info(int argc, char **argv)
{
/* 省略代码...... */
return 0;
}
/**
* @brief 获取MSC配置信息
* @param argc:传入参数的数量
* @param argv:传入参数
* @retval -1:读取失败。0:读取成功
*/
static int console_device_cfg(int argc, char **argv)
{
/* 省略代码...... */
return 0;
}
void app_main(void)
{
esp_err_t ret;
uint8_t repl_state = 0xAA;
ret = nvs_flash_init(); /* 初始化NVS */
if(ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
{
ESP_ERROR_CHECK(nvs_flash_erase());
ESP_ERROR_CHECK(nvs_flash_init());
}
led_init(); /* LED初始化 */
lcd_init(); /* LCD初始化 */
usb_hid_msc_init(); /* USB MSC HOST初始化 */
/* 实验信息 */
lcd_show_string(30, 50, 200, 16, 16, "ESP32-P4", RED);
lcd_show_string(30, 70, 200, 16, 16, "USB disk TEST", RED);
lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
lcd_show_string(30, 110, 200, 16, 16, "status:disconnect", RED);
lcd_show_string(30, 130, 200, 16, 16, "Total: MB", RED);
while (1)
{
/* 接收消息 */
xQueueReceive(usb_queue, &msg, portMAX_DELAY);
if (msg.id == USB_DEVICE_CONNECTED)
{
/* 检查到U盘,则安装设备 */
ESP_ERROR_CHECK(msc_host_install_device(msg.data.new_dev_address
, &msc_device));
/* 使用文件系统挂载U盘 */
const esp_vfs_fat_mount_config_t mount_config = {
.format_if_mount_failed = false, /* 挂载子分区? */
.max_files = 3, /* 打开文件最大数量 */
.allocation_unit_size = 8192, /* 扇区大小 */
};
/* 注册文件系统 */
ESP_ERROR_CHECK( msc_host_vfs_register(msc_device, MNT_PATH,
&mount_config, &vfs_handle));
if (msg.id == USB_DEVICE_CONNECTED && repl_state == 0xAA)
{
/* 交互监视器 */
esp_console_repl_t *repl = NULL;
/* 默认配置 */
esp_console_repl_config_t repl_config =
ESP_CONSOLE_REPL_CONFIG_DEFAULT();
repl_config.prompt = CONFIG_IDF_TARGET ">"; /* 提示符名称 */
/* 命令行的最大长度。如果为0,则使用默认值 */
repl_config.max_cmdline_length = 64;
esp_console_register_help_command(); /* 帮助命令 */
/* 交互监视器使用串口那个通道输出 */
esp_console_dev_uart_config_t hw_config =
ESP_CONSOLE_DEV_UART_CONFIG_DEFAULT();
/* 新建REPL控制器 */
ESP_ERROR_CHECK(esp_console_new_repl_uart(&hw_config,
&repl_config, &repl));
for (int count = 0; count < sizeof(cmds)
/ sizeof(esp_console_cmd_t); count++)
{
/* 添加命令 */
ESP_ERROR_CHECK(esp_console_cmd_register(&cmds[count]));
}
/* 开始REPL控制器 */
ESP_ERROR_CHECK(esp_console_start_repl(repl));
repl_state = 0x00;
}
/* 获取磁盘信息 */
msc_host_device_info_t info;
ESP_ERROR_CHECK(msc_host_get_device_info(msc_device, &info));
uint64_t capacity = ((uint64_t)info.sector_size * info.sector_count)
/ (1024 * 1024);
lcd_show_string(84, 110, 200, 16, 16, "connected.", BLUE);
lcd_show_num(80, 130, capacity, 5, 16, BLUE);
}
if ((msg.id == USB_DEVICE_DISCONNECTED))
{
lcd_show_string(84, 110, 200, 16, 16, "disconnect", BLUE);
if (vfs_handle)
{
ESP_ERROR_CHECK(msc_host_vfs_unregister(vfs_handle));
vfs_handle = NULL;
}
if (msc_device)
{
ESP_ERROR_CHECK(msc_host_uninstall_device(msc_device));
msc_device = NULL;
}
}
}
}
上述代码实现了基于ESP32-P4的USB闪存驱动和REPL控制器功能,包含多个控制命令和功能模块。主要功能包括目录浏览、文件读写、USB设备信息获取、读写速度测试以及设备配置打印等。通过定义一系列控制命令(如file、read、write等)和递归函数list_files,支持对USB设备的文件系统操作。并且可实时检测U盘是否处于连接状态。
47.4 下载验证
在代码编译成功之后,我们可以在开发板上的HOST口插入U盘,此时LCD提示U盘连接成功,我们可通过终端来操作U盘中的文件,这些操作命令如下:

图47.4.1 REPL终端命令
从上图可以看到,若我们在终端输入 “file”命令,则终端会列出U盘中的所有文件目录,操作过程如下图所示。

图47.4.2 查询U盘文件目录
当然,我们也可以通过程序的方式添加自定义的操作命令,添加过程请参考本章节的实验。