《ESP32-P4开发指南—V1.0》第四十七章 USB U盘实验

第四十七章 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盘文件目录


       当然,我们也可以通过程序的方式添加自定义的操作命令,添加过程请参考本章节的实验。


请使用浏览器的分享功能分享到微信等