第五十九章 USB U盘(Host)实验
本章我们介绍STM32H562的USB HOST应用,即通过USB HOST功能,实现读写U盘/读卡器等大容量USB存储设备。
本章分为如下几个部分:
59.1 U盘简介
59.2 硬件设计
59.3 程序设计
59.4 下载验证
59.1 U盘简介
U盘,全称USB闪存盘,英文名“USB flash disk”。它是一种使用USB接口的无需物理驱动器的微型高容量移动存储产品,通过USB接口与主机连接,实现即插即用,是最常用的移动存储设备之一。
STM32H562的USB OTG FS支持U盘,并且ST官方提供了USB HOST大容量存储设备(MSC)例程,ST官方例程路径:光盘→8,STM32参考资料 →1,STM32CubeH5 固件包 →STM32Cube_FW_H5_V1.0 →Projects →STM32H573I-DK →Applications →USBX →Ux_Host_MSC。本实验,我们就要移植该例程到开发板上,以通过STM32H562的USB HOST接口,读写U盘或SD卡读卡器等设备。
59.2 硬件设计
1. 例程功能
本实验代码,开机后,检测字库,然后初始化USB HOST,并不断轮询。当检测并识别U盘后,在LCD上面显示U盘总容量和剩余容量,此时便可以通过USMART调用FATFS相关函数,来测试U盘数据的读写了,方法同FATFS实验一模一样。LED0闪烁,提示程序运行。
2. 硬件资源
1)LED灯 LED0 - PB4 LED1 - PB5
2)串口1(PA9/PA10连接在板载USB转串口芯片CH340上面)
3)正点原子2.8/3.5/4.3/7寸TFTLCD模块(仅限MCU屏,16位8080并口驱动)
4)USB_HOST接口(D-/D+连接在PA11/PA12上)
5)TIM6
3. 原理图
本开发板的USB HOST接口采用的是贴片USB母座,它和USB SLAVE是共用USB_DM和USB_DP信号的,所以USB HOST和USB SLAVE接口不能同时使用。USB HOST和STM32的连接原理图,如下图所示:

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

图59.2.2 开发板U盘母座
59.3 程序设计
59.3.1 程序流程图

图59.3.1.1 USB U盘(Host)实验程序流程图
59.3.2 程序解析
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。本实验,在实验34 汉字显示实验基础上,添加USB相关代码到工程中,最终得到如图59.3.2.1所示的工程:

图59.3.2.1 USB U盘实验工程分组
1. USB驱动代码
app_usbx_host.c提供了USB主机库的回调及MSP初始化函数等,当USB状态机处理完不同事务的时候,会调用这些回调函数,我们通过这些回调函数,就可以知道USB当前状态,比如:是否连接上了?是否断开了?等,根据这些状态,用户应用程序可以执行不同操作,完成特定功能。该.c文件我们重点介绍3个函数,首先是初始化HCD MSP函数,其定义如下:
/**
* @brief HAL库USB HCD初始化MSP回调函数
* @param hpcd: USB HCD句柄
* @retval 无
*/
void HAL_HCD_MspInit(HCD_HandleTypeDef* hhcd)
{
RCC_OscInitTypeDef rcc_osc_init_struct = {0};
RCC_PeriphCLKInitTypeDef rcc_periph_clk_init_struct = {0};
GPIO_InitTypeDef gpio_init_struct = {0};
if (hhcd->Instance == USB_DRD_FS)
{
/* 配置时钟 */
rcc_osc_init_struct.OscillatorType = RCC_OSCILLATORTYPE_HSI48;
rcc_osc_init_struct.HSI48State = RCC_HSI48_ON;
HAL_RCC_OscConfig(&rcc_osc_init_struct);
rcc_periph_clk_init_struct.PeriphClockSelection = RCC_PERIPHCLK_USB;
rcc_periph_clk_init_struct.UsbClockSelection = RCC_USBCLKSOURCE_HSI48;
HAL_RCCEx_PeriphCLKConfig(&rcc_periph_clk_init_struct);
/* 使能USB电源检测器 */
HAL_PWREx_EnableVddUSB();
HAL_PWREx_EnableUSBVoltageDetector();
/* 使能时钟 */
__HAL_RCC_USB_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
/* 初始化DM、DP引脚 */
gpio_init_struct.Pin = GPIO_PIN_11 | GPIO_PIN_12;
gpio_init_struct.Mode = GPIO_MODE_AF_PP;
gpio_init_struct.Pull = GPIO_NOPULL;
gpio_init_struct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
gpio_init_struct.Alternate = GPIO_AF10_USB;
HAL_GPIO_Init(GPIOA, &gpio_init_struct);
/* 配置中断 */
HAL_NVIC_SetPriority(USB_DRD_FS_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(USB_DRD_FS_IRQn);
}
}
HAL_HCD_MspInit函数,用于使能USB时钟,选择内核时钟源,初始化IO口,设置中断等。该函数在HAL_HCD_Init函数里面被调用。
接下来介绍的是USB OTG 中断服务函数,其定义如下:
/**
* @brief USB中断服务函数
* @param 无
* @retval 无
*/
void USB_DRD_FS_IRQHandler(void)
{
HAL_HCD_IRQHandler(&g_hcd_handle);
}
USB_DRD_FS_IRQHandler()是USB的中断服务函数,通过调用HAL_HCD_IRQHandler函数,实现对USB各种事务的处理。
最后介绍的是USB主机初始化函数,其定义如下:
/**
* @brief USB主机初始化
* @param 无
* @retval 初始化结果
* @arg 0: 成功
* @arg 1: 失败
*/
uint8_t app_usbx_host_init(void)
{
/* 初始化USBX内存资源 */
if (ux_system_initialize((UCHAR *)ux_device_memory_pool,
USBX_DEVICE_MEMORY_POOL_SIZE, UX_NULL, 0) != UX_SUCCESS)
{
return 1;
}
/* 初始化USB主机堆栈 */
if (ux_host_stack_initialize(app_usbx_host_event_callback) != UX_SUCCESS)
{
return 1;
}
/* 注册USB主机类 */
if (ux_host_stack_class_register(_ux_system_host_class_storage_name,
ux_host_class_storage_entry) != UX_SUCCESS)
{
return 1;
}
/* 初始化USB HCD */
g_hcd_handle.Instance = USB_DRD_FS;
g_hcd_handle.Init.dev_endpoints = 8;
g_hcd_handle.Init.Host_channels = 8;
g_hcd_handle.Init.speed = USBD_FS_SPEED;
g_hcd_handle.Init.phy_itface = HCD_PHY_EMBEDDED;
g_hcd_handle.Init.Sof_enable = DISABLE;
g_hcd_handle.Init.low_power_enable = DISABLE;
g_hcd_handle.Init.vbus_sensing_enable = DISABLE;
g_hcd_handle.Init.bulk_doublebuffer_enable = DISABLE;
g_hcd_handle.Init.iso_singlebuffer_enable = DISABLE;
if (HAL_HCD_Init(&g_hcd_handle) != HAL_OK)
{
return 1;
}
ux_host_stack_hcd_register(_ux_system_host_hcd_stm32_name,
_ux_hcd_stm32_initialize,
(ULONG)USB_DRD_FS,
(ULONG)&g_hcd_handle);
HAL_HCD_Start(&g_hcd_handle);
return 0;
}
app_usbx_host_init()函数,用于初始化USB底层设置,比如:使用内部PHY,使用全速模式,不使能VBUS检测等,详见以上代码。
ux_host_msc.c提供U盘和FATFS文件系统之间的输入/输出接口函数,这里总共有7个函数,首先是MSC磁盘初始化相关函数,其定义如下:
/**
* @brief MSC读写磁盘初始化
* @param storage_instance: 磁盘实例
* @retval 无
*/
VOID ux_host_msc_read_write_init(UX_HOST_CLASS_STORAGE *storage_instance)
{
g_storage = storage_instance;
}
/**
* @brief MSC读写磁盘反初始化
* @param 无
* @retval 无
*/
VOID ux_host_msc_read_write_deinit(VOID)
{
g_storage = UX_NULL;
}
/**
* @brief MSC磁盘初始化
* @param 无
* @retval 磁盘初始化结果
* @arg 0: 成功
* @arg 1: 失败
*/
uint8_t ux_host_msc_disk_init(void)
{
if (g_storage == UX_NULL)
{
return 1;
}
return 0;
}
下面介绍的是MSC获取磁盘扇区大小函数,其定义如下:
/**
* @brief MSC获取磁盘扇区大小
* @param 无
* @retval 磁盘扇区大小
*/
uint32_t ux_host_msc_disk_get_sector_size(void)
{
if (g_storage == UX_NULL)
{
return 0;
}
return (uint32_t)g_storage->ux_host_class_storage_sector_size;
}
ux_host_msc_disk_get_sector_size函数,用于获取U盘扇区大小。
下面介绍的是U盘读扇区操作函数,其定义如下:
/**
* @brief MSC读磁盘
* @param buf: 数据
* @param sector: 起始扇区
* @param cnt: 扇区数量
* @retval 读磁盘结果
* @arg 0: 成功
* @arg 1: 失败
*/
uint8_t ux_host_msc_disk_read(uint8_t *buf, uint32_t sector, uint32_t cnt)
{
if (g_storage == UX_NULL)
{
return 1;
}
if (ux_host_class_storage_media_read(g_storage, (ULONG)sector, (ULONG)cnt,
(UCHAR *)buf) != UX_SUCCESS)
{
return 1;
}
return 0;
}
ux_host_msc_disk_read函数,用于从U盘指定位置,读取指定长度的数据。
下面介绍的是U盘写扇区操作函数,其定义如下:
/**
* @brief MSC写磁盘
* @param buf: 数据
* @param sector: 起始扇区
* @param cnt: 扇区数量
* @retval 写磁盘结果
* @arg 0: 成功
* @arg 1: 失败
*/
uint8_t ux_host_msc_disk_write(uint8_t *buf, uint32_t sector, uint32_t cnt)
{
if (g_storage == UX_NULL)
{
return 1;
}
if (ux_host_class_storage_media_write(g_storage, (ULONG)sector, (ULONG)cnt,
(UCHAR *)buf) != UX_SUCCESS)
{
return 1;
}
return 0;
}
ux_host_msc_disk_write函数,用于往U盘指定位置,写入指定长度的数据。
最后介绍的是MSC获取磁盘扇区数量函数,其定义如下:
/**
* @brief MSC获取磁盘扇区数量
* @param 无
* @retval 磁盘扇区数量
*/
uint32_t ux_host_msc_disk_get_sector_number(void)
{
if (g_storage == UX_NULL)
{
return 0;
}
return (uint32_t)g_storage->ux_host_class_storage_last_sector_number;
}
ux_host_msc_disk_get_sector_number函数,可以用于获取U盘扇区数量。
另外,我们需要修改FATFS相关文件来支持U盘,在diskio.c里面和FATFS完成对接,同时需要设置FATFS支持3个磁盘(SD卡、QSPI FLASH和U盘),在ffconf.h文件里面将FF_VOLUMES的宏定义值改成3,以支持FATFS操作U盘。
2. main.c代码
下面是main.c的代码,具体如下:
/**
* @brief USB主机设备连接回调函数
* @param 无
* @retval 无
*/
void app_usbx_host_device_connect_callback(void)
{
uint32_t total;
uint32_t free;
LED1(0);
f_mount(fs[2], "2:", 1);
text_show_string(30, 110, 200, 16, "U盘连接成功! ", 16, 0, BLUE);
if (exfuns_get_free((uint8_t *)"2:", &total, &free) == 0)
{
text_show_string(30, 130, 200, 16, "U盘容量: MB", 16, 0, BLUE);
text_show_string(30, 150, 200, 16, "U盘可用空间: MB", 16, 0, BLUE);
lcd_show_num(102, 130, total >> 10, 5, 16, BLUE);
lcd_show_num(134, 150, free >> 10, 5, 16, BLUE);
}
else
{
text_show_string(30, 130, 200, 16, "获取U盘容量失败", 16, 0, BLUE);
}
}
/**
* @brief USB主机设备断开回调函数
* @param 无
* @retval 无
*/
void app_usbx_host_device_disconnect_callback(void)
{
LED1(1);
f_unmount("2:");
text_show_string(30, 110, 200, 16, "等待U盘插入...", 16, 0, BLUE);
lcd_fill(30, 130, 240, 166, WHITE);
}
int main(void)
{
uint8_t t = 0;
uint8_t res;
HAL_Init(); /* HAL库初始化 */
HAL_ICACHE_Enable(); /* 使能指令缓存 */
sys_stm32_clock_init(250, 4, 2, 2); /* 配置时钟,250MHz */
delay_init(250); /* 初始化延时 */
usart_init(115200); /* 初始化串口 */
led_init(); /* 初始化LED */
lcd_init(); /* 初始化LCD */
usmart_dev.init(250); /* 初始化USMART */
my_mem_init(SRAMIN); /* 初始化内部内存池 */
exfuns_init(); /* 为exfuns申请内存 */
f_mount(fs[0], "0:", 1); /* 挂载SD卡 */
f_mount(fs[1], "1:", 1); /* 挂载NOR Flash */
/* 检查字库 */
while (fonts_init() != 0)
{
lcd_show_string(30, 30, 200, 16, 16, "Font Error! ", RED);
delay_ms(500);
lcd_show_string(30, 30, 200, 16, 16, "Please Check!", RED);
delay_ms(500);
}
lcd_fill(30, 30, 30 + 200, 30 + 16, WHITE);
text_show_string(30, 50, 200, 16, "STM32", 16, 0, RED);
text_show_string(30, 70, 200, 16, "USB U盘(Host)实验", 16, 0, RED);
text_show_string(30, 90, 200, 16, "ATOM@ALIENTEK", 16, 0, RED);
text_show_string(30, 110, 200, 16, "等待U盘插入...", 16, 0, BLUE);
/* USB主机初始化 */
res = app_usbx_host_init();
while (res != 0)
{
lcd_show_string(30, 130, 200, 16, 16, "USB Host Init Failed!", RED);
delay_ms(500);
lcd_show_string(30, 130, 200, 16, 16, "Please Check! ", RED);
delay_ms(500);
}
/* 每1毫秒运行一次独立模式下的USBX系统任务 */
btim_timx_int_init(1000 - 1, 250 - 1);
while (1)
{
if (++t == 20)
{
t = 0;
LED0_TOGGLE();
}
delay_ms(10);
}
}
app_usbx_host_device_connect_callback函数,其作用是:当U盘连接成功,会挂载U盘、显示U盘连接成功/U盘容量等信息。
app_usbx_host_device_disconnect_callback函数,其作用是:当U盘断开连接,会取消挂载U盘、显示"等待U盘插入..."。
main函数中,先进行系统和外设初始化,检查字库,显示实验信息、初始化USB等,然后每1毫秒运行一次独立模式下的USBX系统任务。
59.4 下载验证
将程序下载到开发板后,然后在USB_HOST端子插入U盘/读卡器(带卡),注意:此时USB SLAVE口不要插USB线到电脑,否则会干扰!
等U盘成功识别后,便可以看到LCD显示U盘容量等信息,如图59.4.1所示:

图59.4.1 U盘识别成功
此时,我们便可以通过USMART来测试U盘读写功能等,如图59.4.2所示。
框图右边的序号对应的是下方调用的函数序号。按照1、2、3、4、1顺序调用对应的函数,便可以得到图59.4.2的测试结果。下面列出0~4序号对应的函数:
0:list
1:mf_scan_files("2:")
2:mf_open("2:test u disk.txt",7)
3:mf_write("这是一个测试,写入文件",22)
4:mf_close()
这里的测试主要是通过发送:mf_open("2:test u disk.txt",7),在U盘根目录创建test u disk.txt这个文件,然后发送:mf_write("这是一个测试,写入文件",22),写入“这是一个测试,写入文件”到这个文件里面,然后发送:mf_close(),关闭文件,完成一次文件创建。最后,发送:mf_scan_files("2:"),扫描U盘根目录文件,发现U盘根目录下多出了一个test u disk.txt的文件,说明U盘写入成功。
这样,就完成了本实验的设计目的:实现U盘的读写操作。最后,大家还可以调用其它函数,实现相关功能测试,这里就不给大家一一演示了。

图59.4.2 测试U盘
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70040860/viewspace-3107744/ ,如需转载,请注明出处,否则将追究法律责任。