打开工程首先看readme文件:(官方的说明中,串口3和4不明确,根据代码和实测,确定CDC测试使用的是串口3,后文我会讲到为什么是串口3).
================================================================================
样例使用说明
================================================================================
Date Author IAR MDK GCC
2022-03-31 CDT 7.70 5.36 gcc-arm-none-eabi-10.3-2021.10-win32
================================================================================
平台说明
================================================================================
GCC工程,由Eclipse IDE外挂GNU-ARM Toolchain,再结合pyOCD GDB Server实现工程的编译、
链接和调试。在用Eclipse导入工程后,请将xxxx_PyOCDDebug中pyocd-gdbserver和SVD文件
设置为正确的路径;请将xxxx_PyOCDDownload中pyocd设置为正确的路径。注意,这些路径不
能包含非英文字符。
================================================================================
功能描述
================================================================================
本样例主要展示通过MCU的USB虚拟出1路CDC的功能,上位机向所虚拟的CDC接口发送数据,MCU接收到以后通过串口USART3输出;
USART3接收到数据时,通过USB所虚拟的CDC向上位机输出:
USART3
----------->PC <-------------->MCU<---------
| |
|__________________USB_______________________|
================================================================================
测试环境
================================================================================
测试用板:
---------------------
EV_F460_LQ100_Rev2.0
辅助工具:
---------------------
无
辅助软件:
---------------------
串口调试助手软件(115200bps,8N1)
================================================================================
使用步骤
================================================================================
1)使用USB连接线通过测试版的J14连接到PC,
2)打开工程编译并全速运行(可使用自带的虚拟串口观察调试信息);
3)安装CDC VCP driver
4)第三步成功后,windows设备管理器会出现虚拟COM口
5)串口数据可用过EVB上的J2收发
================================================================================
注意
================================================================================
功能很简单,使用官方V2评估版,插上电脑,尝试OK。下一步深入代码:
(1)先看main函数:
/**
* 主函数
* 本函数为程序的入口点,主要完成USB设备的初始化。
* @param None 无参数
* @retval None 无返回值
*/
int32_t main(void)
{
// 初始化USB端口识别结构体
stc_usb_port_identify stcPortIdentify;
stcPortIdentify.u8CoreID = USBFS_CORE_ID; // 设置USB核心ID
// 初始化USB设备
usb_dev_init(&usb_dev, &stcPortIdentify, &user_desc, &class_cdc_cbk, &user_cb);
// 进入无限循环,等待USB事件处理
for (;;) {
}
}
看上去很简单,短短几行,就实现了USB功能??USB协议那么多,main函数里面没有体现,看来在其他部分体现了。上面的结构就是典型的前后系统,初始化之后进入死循环,前台靠中断,后台什么也不干。我们继续深入usb_dev_init看一下:
/**
* 初始化设备堆栈并加载类驱动
*
* 该函数负责初始化USB设备控制器的堆栈,选择合适的USB核心和PHY,并加载类驱动。
* 完成堆栈初始化后,会调用用户定义的初始化回调函数。
*
* @param [in] pdev 设备实例指针,指向USB设备控制器的实例。
* @param [in] pstcPortIdentify USB核心和PHY的选择标识。
* @param [in] pdesc 设备描述符函数指针,用于提供设备描述符。
* @param [in] class_cb 类回调结构体地址,用于设备类的回调函数。
* @param [in] usr_cb 用户回调结构体地址,用于用户定义的回调函数。
* @retval None
*/
void usb_dev_init(usb_core_instance *pdev,
stc_usb_port_identify *pstcPortIdentify,
usb_dev_desc_func *pdesc,
usb_dev_class_func *class_cb,
usb_dev_user_func *usr_cb)
{
// 初始化USB BSP(板级支持包),为设备控制器和PHY进行硬件初始化。
usb_bsp_init(pdev, pstcPortIdentify);
// 设置设备类回调和用户回调。
pdev->dev.class_callback = class_cb;
pdev->dev.user_callback = usr_cb;
pdev->dev.desc_callback = pdesc;
// 初始化USB设备控制器的软件状态。
usb_initdev(pdev, pstcPortIdentify);
// 调用用户定义的初始化回调函数。
pdev->dev.user_callback->user_init();
// 初始化设备状态为EP0空闲状态,准备进行后续的设备配置。
pdev->dev.device_state = USB_EP0_IDLE;
// 配置NVIC(Nested Vector Interrupt Controller),使能USB中断。
usb_bsp_nvicconfig(pdev);
}
根据注释,函数的第一个参数是一个核心,pdev这个变量是一个全局变量,所有USB的操作都需要这个变量,函数的第3、4、5参数都是入参赋值给了pdev这个核心。函数里面先实现了bsp初始化,这个跟硬件相关,虽然跟硬件相关,但是小华芯片的USB控制器和IO基本是确定的。接下来通过usb_initdev初始化了USB核心,最后开启中断。
我们先来看usb_bsp_init(pdev, pstcPortIdentify);
/**
* @brief initialize configurations for the BSP
* @param [in] pdev device instance
* @param [in] pstcPortIdentify usb core and phy select
* @retval None
*/
void usb_bsp_init(usb_core_instance *pdev, stc_usb_port_identify *pstcPortIdentify)
{
stc_gpio_init_t stcGpioCfg;
/* Unlock peripherals or registers */
LL_PERIPH_WE(EXAMPLE_PERIPH_WE);
BSP_CLK_Init();
BSP_LED_Init();
#if (LL_PRINT_ENABLE == DDL_ON)
DDL_PrintfInit(BSP_PRINTF_DEVICE, BSP_PRINTF_BAUDRATE, BSP_PRINTF_Preinit);
#endif
/* USB clock source configure */
UsbClockInit();
#if (LL_PRINT_ENABLE == DDL_ON)
DDL_Printf("USBFS start !!\r\n");
#endif
(void)GPIO_StructInit(&stcGpioCfg);
stcGpioCfg.u16PinAttr = PIN_ATTR_ANALOG;
(void)GPIO_Init(USB_DM_PORT, USB_DM_PIN, &stcGpioCfg);
(void)GPIO_Init(USB_DP_PORT, USB_DP_PIN, &stcGpioCfg);
GPIO_SetFunc(USB_VBUS_PORT, USB_VBUS_PIN, GPIO_FUNC_10); /* VBUS */
FCG_Fcg1PeriphClockCmd(FCG1_PERIPH_USBFS, ENABLE);
}
里面实现了系统初始化,LED初始化(这个跟本例程无关),打印初始化(这个用了串口4,而且只用了输出功能),USB时钟初始化,只能设定为48M,这个外部晶振是8M的,如果遇到了其他频率的外部晶振,需要修改这部分。然后初始化了USB的三个引脚Vbus,DM,DP,DM和DP就是差分数据线,VBUS引脚我理解就是USB检测连接,从电路图上面看到Vbus直接和USB的VCC相连。最后开启USB时钟。
需要注意的是,以上操作并没有完成USB模块的初始化,根据芯片的参考手册,相关的寄存器都没有进行操作,当然没有完成初始化了。同时,这里产生疑问,说好的用串口3进行通信呢,串口3的初始化为什么不在这里呢?
下面继续看函数usb_initdev:
/**
* 初始化USB设备
*
* 该函数负责初始化USB设备控制器,设置设备的基本配置,并启用全局中断。
*
* @param [in] pdev 指向USB核心实例的指针。
* @param [in] pstcPortIdentify 指向USB端口识别结构体的指针,用于选择USB核心和PHY。
* @retval None
*/
void usb_initdev(usb_core_instance *pdev, stc_usb_port_identify *pstcPortIdentify)
{
uint32_t tmp_1;
USB_DEV_EP *iep, *oep;
/* 设置寄存器地址、USB端口识别信息和基本配置。 */
usb_setregaddr(&pdev->regs, pstcPortIdentify, &pdev->basic_cfgs);
/* 初始化设备状态、地址和临时变量。 */
pdev->dev.device_cur_status = (uint8_t)USB_DEV_DEFAULT;
pdev->dev.device_address = 0U;
tmp_1 = 0UL;
/* 遍历所有端点,初始化输入和输出端点。 */
do {
iep = &pdev->dev.in_ep[tmp_1];
oep = &pdev->dev.out_ep[tmp_1];
iep->ep_dir = 1U; /* 设置输入端点方向 */
oep->ep_dir = 0U; /* 设置输出端点方向 */
iep->epidx = (uint8_t)tmp_1;
oep->epidx = iep->epidx;
iep->tx_fifo_num = (uint16_t)tmp_1;
oep->tx_fifo_num = iep->tx_fifo_num;
iep->trans_type = EP_TYPE_CTRL; /* 设置传输类型为控制类型 */
oep->trans_type = iep->trans_type;
iep->maxpacket = USB_MAX_EP0_SIZE; /* 设置最大包大小 */
oep->maxpacket = iep->maxpacket;
iep->xfer_buff = 0U; /* 设置传输缓冲区地址为0 */
oep->xfer_buff = iep->xfer_buff;
iep->xfer_len = 0UL; /* 设置传输长度为0 */
oep->xfer_len = iep->xfer_len;
tmp_1++;
} while (tmp_1 < pdev->basic_cfgs.dev_epnum);
/* 禁用全局中断。 */
usb_gintdis(&pdev->regs);
/* 初始化USB核心(通用初始化)。 */
usb_initusbcore(&pdev->regs, &pdev->basic_cfgs);
/* 强制设置设备模式。 */
usb_modeset(&pdev->regs, DEVICE_MODE);
/* 初始化设备。 */
usb_devmodeinit(&pdev->regs, &pdev->basic_cfgs);
/* 启用USB全局中断。 */
usb_ginten(&pdev->regs);
}
前面提到USB模块初始化并没有在BSP中执行,那么这个函数就是USB的初始化了,这部分需要对照手册来看,不过例程中,好像都是一样的,除了主从模式之外。
接下来一句是:
// 调用用户定义的初始化回调函数。
pdev->dev.user_callback->user_init();
那么用户自定义回调干了什么事情呢?这些函数定义在usb_dev_user.c中,其实就是将USB的各种状态打印出来,方便我们用户看到和调试。在城里里面看到user_cb或者dev.user_callback大家就知道了,可以不用管。
/*******************************************************************************
* Global variable definitions (declared in header file with 'extern')
******************************************************************************/
usb_dev_user_func user_cb = {
&usb_dev_user_init,
&usb_dev_user_rst,
&usb_dev_user_devcfg,
&usb_dev_user_devsusp,
&usb_dev_user_devresume,
&usb_dev_user_conn,
&usb_dev_user_disconn,
};
/*******************************************************************************
* Local variable definitions ('static')
******************************************************************************/
/*******************************************************************************
* Function implementation - global ('extern') and local ('static')
******************************************************************************/
/**
* @brief usb_dev_user_init
* @param None
* @retval None
*/
void usb_dev_user_init(void)
{
/* Add initial code here */
}
/**
* @brief usb_dev_user_rst
* @param None
* @retval None
*/
void usb_dev_user_rst(void)
{
#if (LL_PRINT_ENABLE == DDL_ON)
DDL_Printf(">>USB Device has reset.\r\n");
#endif
}
/**
* @brief usb_dev_user_devcfg
* @param None
* @retval None
*/
void usb_dev_user_devcfg(void)
{
#if (LL_PRINT_ENABLE == DDL_ON)
DDL_Printf(">>CDC interface starts.\r\n");
#endif
}
/**
* @brief usb_dev_user_conn
* @param None
* @retval None
*/
void usb_dev_user_conn(void)
{
#if (LL_PRINT_ENABLE == DDL_ON)
DDL_Printf(">>USB device connects.\r\n");
#endif
}
/**
* @brief USBD_USR_DeviceDisonnected
* @param None
* @retval None
*/
void usb_dev_user_disconn(void)
{
#if (LL_PRINT_ENABLE == DDL_ON)
DDL_Printf(">>USB device disconnected.\r\n");
#endif
}
/**
* @brief usb_dev_user_devsusp
* @param None
* @retval None
*/
void usb_dev_user_devsusp(void)
{
#if (LL_PRINT_ENABLE == DDL_ON)
DDL_Printf(">>USB device in suspend status.\r\n");
#endif
}
/**
* @brief usb_dev_user_devresume
* @param None
* @retval None
*/
void usb_dev_user_devresume(void)
{
#if (LL_PRINT_ENABLE == DDL_ON)
DDL_Printf(">>USB device resumes.\r\n");
#endif
}
继续看最后一个函数: usb_bsp_nvicconfig(pdev);
/**
* @brief 配置USB BSP NVIC(Nested Vector Interrupt Controller)
*
* 本函数用于配置USB控制器的NVIC中断,包括注册中断、设置中断优先级以及使能中断。
*
* @param pdev 指向USB核心实例的指针
*/
void usb_bsp_nvicconfig(usb_core_instance *pdev)
{
stc_irq_signin_config_t stcIrqRegiConf;
/* 配置中断注册信息 */
stcIrqRegiConf.enIRQn = INT030_IRQn; /* 将USBFS全局中断注册到向量号030 */
stcIrqRegiConf.enIntSrc = INT_SRC_USBFS_GLB; /* 选择中断源为USBFS全局中断 */
stcIrqRegiConf.pfnCallback = &USB_IRQ_Handler; /* 设置中断服务函数回调地址 */
/* 注册中断 */
(void)INTC_IrqSignIn(&stcIrqRegiConf);
/* 清除中断挂起状态 */
NVIC_ClearPendingIRQ(stcIrqRegiConf.enIRQn);
/* 设置中断优先级 */
NVIC_SetPriority(stcIrqRegiConf.enIRQn, DDL_IRQ_PRIO_15);
/* 使能NVIC中断 */
NVIC_EnableIRQ(stcIrqRegiConf.enIRQn);
}
这个函数功能十分简单,就是开启中断,这个是标准格式,后面移植可以直接复制。里面最重要的就是USB_IRQ_Handler,这个中断函数怎么实现呢?
/**
* 处理USB中断
* 本函数无参数传入,也不返回任何值。
* 它主要负责调用usb_isr_handler函数来处理USB设备的中断。
*/
static void USB_IRQ_Handler(void)
{
// 调用USB中断服务例程处理中断
usb_isr_handler(&usb_dev);
}
那么重点就变成了:usb_isr_handler(&usb_dev);
其中的usb_dev就是USB核心,在代码中只有一个USB核心,所有USB操作都是使用这个核心。让我们继续看看这个中断怎么实现的?
/**
* @brief 处理所有USB中断
*
* @param [in] pdev 设备实例
* @retval None
*/
void usb_isr_handler(usb_core_instance *pdev)
{
uint32_t u32gintsts;
// 如果当前USB模式为0(设备模式),则处理中断
if (0U == usb_getcurmod(&pdev->regs)) {
// 获取当前中断状态
u32gintsts = usb_getcoreintr(&pdev->regs);
// 如果没有中断发生,则直接返回
if (u32gintsts == 0UL) {
return;
}
// 处理OUT端点中断
if ((u32gintsts & OUTEP_INT) != 0UL) {
usb_outep_isr(pdev);
}
// 处理IN端点中断
if ((u32gintsts & INEP_INT) != 0UL) {
usb_inep_isr(pdev);
}
// 处理模态中断
if ((u32gintsts & MODEMIS_INT) != 0UL) {
WRITE_REG32(pdev->regs.GREGS->GINTSTS, MODEMIS_INT);
}
// 处理唤醒中断
if ((u32gintsts & WAKEUP_INT) != 0UL) {
usb_resume_isr(pdev);
}
// 处理USB暂停中断
if ((u32gintsts & USBSUSP_INT) != 0UL) {
usb_susp_isr(pdev);
}
// 处理SOF中断
if ((u32gintsts & SOF_INT) != 0UL) {
usb_sof_isr(pdev);
}
// 处理接收FIFO非空中断
if ((u32gintsts & RXFLVL_INT) != 0UL) {
usb_rxstsqlvl_isr(pdev);
}
// 处理USB重置中断
if ((u32gintsts & USBRST_INT) != 0UL) {
usb_reset_isr(pdev);
}
// 处理枚举完成中断
if ((u32gintsts & ENUMDONE_INT) != 0UL) {
usb_enumfinish_isr(pdev);
}
// 处理非完整的IN ISO传输中断
if ((u32gintsts & INCOMPLSOIN) != 0UL) {
usb_isoinincomplt_isr(pdev);
}
// 处理非完整的OUT ISO传输中断
if ((u32gintsts & INCOMPLSOOUT) != 0UL) {
usb_isooutincomplt_isr(pdev);
}
// 处理VBUS电压中断(如果启用)
#ifdef VBUS_SENSING_ENABLED
if ((u32gintsts & VBUSV_INT) != 0UL) {
usb_sessionrequest_isr(pdev);
}
#endif
}
}
这个中断处理首先读取USBFS全局中断寄存器(USBFS_GINTSYS)和USBFS全局中断屏蔽寄存器(USBFS_GINTMSK),基于小华的特殊设计,这里必须两个寄存器都要读取,因为参考手册里面说了:如果将某一个中断位屏蔽,则不会产生与该位相关的中断,但是,与该位相关的模块中断寄存器仍会置1. 所有USB中断都用一个中断号,那么必须判断发生了什么中断。
初始化完成,我们就明白了大概:程序一上电,完成必要的初始化,配置中断,然后所有的事情都交给中断处理。但是问题来了,串口3到目前为止,还没有进行初始化呢?
因为从main函数开始,看到这里,就结束了。但是我们脑海里面仍有很多疑问。于是我们再看看,肯定遗憾了什么?回头看:
pdev->dev.class_callback = class_cb;
pdev->dev.user_callback = usr_cb;
pdev->dev.desc_callback = pdesc;
这三个参数分别是什么呢?usr_cb已经知道是用户自定义的函数了。经过查找,另外两个分别是:
/* USB设备CDC类回调函数结构体定义 */
usb_dev_class_func class_cdc_cbk = {
/* 初始化CDC类 */
&usb_dev_cdc_init,
/* 销毁CDC类 */
&usb_dev_cdc_deinit,
/* 处理CDC类设置请求 */
&usb_dev_cdc_setup,
/* NULL占位,保留供未来使用 */
NULL,
/* 处理CDC类控制端点RXReady事件 */
&usb_dev_cdc_ctrlep_rxready,
/* 获取CDC类配置描述符 */
&usb_dev_cdc_getcfgdesc,
/* SOF(帧)事件处理函数 */
&usb_dev_cdc_sof,
/* 数据IN传输处理函数 */
&usb_dev_cdc_datain,
/* 数据OUT传输处理函数 */
&usb_dev_cdc_dataout,
/* NULL占位,保留供未来使用 */
NULL,
/* NULL占位,保留供未来使用 */
NULL,
};
// 定义USB设备描述符函数结构体
usb_dev_desc_func user_desc = {
&usb_dev_desc, // USB设备描述符
&usb_dev_langiddesc, // USB设备语言ID描述符
&usb_dev_manufacturerstr, // USB设备制造商字符串描述符
&usb_dev_productdesc, // USB设备产品字符串描述符
&usb_dev_serialstr, // USB设备序列号字符串描述符
&usb_dev_configstrdesc, // USB设备配置字符串描述符
&usb_dev_intfstrdesc, // USB设备接口字符串描述符
};
pdesc获取设备的各种信息的,这个信息怎么获得呢,从源码中看到,这些信息都是自己定义的。这里也提供了一个思路,如果我们要定义自己的USB设备,就需要修改这个文件usb_dev_desc.c,这个里面内容是比较简单的,作用就是用来给USB主机一些信息。
重点看class_cdc_cbk这个函数结构体:里面提供了USBCDC的操作,先看第一个usb_dev_cdc_init
/**
* 初始化CDC(通信设备类)应用
* @param [in] pdev 设备实例指针,用于指向当前USB设备
* @retval None 该函数没有返回值
*/
void usb_dev_cdc_init(void *pdev)
{
// 打开CDC的输入端点,配置端点号、最大包大小和类型
usb_opendevep(pdev, CDC_IN_EP, MAX_CDC_IN_PACKET_SIZE, EP_TYPE_BULK);
// 打开CDC的输出端点,配置端点号、最大包大小和类型
usb_opendevep(pdev, CDC_OUT_EP, MAX_CDC_OUT_PACKET_SIZE, EP_TYPE_BULK);
// 打开CDC的命令端点,配置端点号、最大包大小和类型
usb_opendevep(pdev, CDC_CMD_EP, CDC_CMD_PACKET_SIZE, EP_TYPE_INTR);
// 初始化虚拟控制台通信(VCP),其实就是串口
vcp_init();
// 准备接收数据,配置USB设备、端点号、接收缓冲区地址和最大包大小
usb_readytorx(pdev, CDC_OUT_EP, (uint8_t *)(usb_rx_buffer), MAX_CDC_OUT_PACKET_SIZE);
}
这个函数一共调用了5个函数,其中1、2、3是基本操作,4是初始化串口,5是配置USB设备,主要有端点号,缓存区和最大包大小,要记得这个缓冲区,这个就是CDC接收数据之后存放的位置,其他函数也有对这个缓冲区进行读取操作。vcp_init打开一看,就是初始化了串口3,我们之前的疑问得到解决,但是新的问题又来了,这里初始化了串口,那么在什么地方调用呢?在函数初始化中,只是进行了函数指针的赋值,并没有执行函数啊?
我们继续查找,由于使用了函数指针的复制,直接按照本函数名称查找找不到,所以我们需要查找pdev->dev.class_callback->class_init 这个名字。找到如下:
/**
* @brief 设置当前配置或清除当前配置
* @param [in] pdev 设备实例
* @param [in] cfgidx 配置索引
* @param [in] action USB_DEV_CONFIG_SET 或 USB_DEV_CONFIG_CLEAR
* @retval None
*/
void usb_dev_ctrlconfig(usb_core_instance *pdev, uint8_t cfgidx, uint8_t action)
{
__IO uint8_t tmp_1;
(void)(cfgidx); // 弃用参数,防止编译器警告
tmp_1 = action; // 存储操作动作
if (tmp_1 == USB_DEV_CONFIG_SET) { /* 设置配置 */
pdev->dev.class_callback->class_init(pdev); // 调用类初始化回调
pdev->dev.user_callback->user_devconfig(); // 调用用户配置回调
} else if (tmp_1 == USB_DEV_CONFIG_CLEAR) { /* 清除配置 */
pdev->dev.class_callback->class_deinit(pdev); // 调用类去初始化回调
} else {
; // 如果action既不是设置也不是清除,什么也不做
}
}
那么这个usb_dev_ctrlconfig又在哪被调用了呢?继续搜索:
/**
* @brief Handle Set device configuration request
* @param [in] pdev device instance
* @param [in] req usb request
* @retval None
*/
void usb_setconfig(usb_core_instance *pdev, const USB_SETUP_REQ *req)
{
static uint8_t tmp_cfgidx;
tmp_cfgidx = (uint8_t)(req->wValue);
if (tmp_cfgidx > DEV_MAX_CFG_NUM) {
usb_ctrlerr(pdev);
} else {
switch (pdev->dev.device_cur_status) {
case USB_DEV_ADDRESSED:
if (0U != tmp_cfgidx) {
pdev->dev.device_config = tmp_cfgidx;
pdev->dev.device_cur_status = USB_DEV_CONFIGURED;
usb_dev_ctrlconfig(pdev, tmp_cfgidx, USB_DEV_CONFIG_SET);
usb_ctrlstatustx(pdev);
} else {
usb_ctrlstatustx(pdev);
}
break;
case USB_DEV_CONFIGURED:
if (tmp_cfgidx == 0U) {
pdev->dev.device_cur_status = USB_DEV_ADDRESSED;
pdev->dev.device_config = tmp_cfgidx;
usb_dev_ctrlconfig(pdev, tmp_cfgidx, USB_DEV_CONFIG_CLEAR);
usb_ctrlstatustx(pdev);
} else if (tmp_cfgidx != pdev->dev.device_config) {
/* Clear old configuration */
usb_dev_ctrlconfig(pdev, pdev->dev.device_config, USB_DEV_CONFIG_CLEAR);
/* set new configuration */
pdev->dev.device_config = tmp_cfgidx;
usb_dev_ctrlconfig(pdev, tmp_cfgidx, USB_DEV_CONFIG_SET);
usb_ctrlstatustx(pdev);
} else {
usb_ctrlstatustx(pdev);
}
break;
case USB_DEV_SUSPENDED:
break;
default:
usb_ctrlerr(pdev);
break;
}
}
}
原来是在这个函数usb_setconfig,按图索骥,继续查询:在usb_standarddevreq函数中,找到了。
/**
* @brief 处理标准USB设备请求
*
* @param pdev 指向设备实例的指针
* @param req 指向USB请求的指针
* @retval None
*/
void usb_standarddevreq(usb_core_instance *pdev, USB_SETUP_REQ *req)
{
// 根据请求的类型,调用相应的处理函数
if (req->bRequest == USB_REQ_GET_DESCRIPTOR) {
usb_getdesc(pdev, req) ; // 获取描述符
} else if (req->bRequest == USB_REQ_SET_ADDRESS) {
usb_setaddr(pdev, req); // 设置设备地址
} else if (req->bRequest == USB_REQ_SET_CONFIGURATION) {
usb_setconfig(pdev, req); // 设置配置
} else if (req->bRequest == USB_REQ_GET_CONFIGURATION) {
usb_getconfig(pdev, req); // 获取配置
} else if (req->bRequest == USB_REQ_GET_STATUS) {
usb_getstatus(pdev, req); // 获取状态
} else if (req->bRequest == USB_REQ_SET_FEATURE) {
usb_setfeature(pdev, req); // 设置特性
} else if (req->bRequest == USB_REQ_CLEAR_FEATURE) {
usb_clrfeature(pdev, req); // 清除特性
} else {
// 如果不是上述标准请求,则调用类回调函数处理,处理失败则报错
if (0U != pdev->dev.class_callback->ep0_setup(pdev, req)) {
usb_ctrlerr(pdev);
}
}
}
这个函数是用来实现主机对设备的标准请求的,那么这个函数如何被调用呢?
在这里:USB的设置处理函数usb_setup_process。
/**
* 处理设置阶段
* @param [in] pdev 设备实例指针
* @retval None
*/
void usb_setup_process(usb_core_instance *pdev)
{
USB_SETUP_REQ req;
// 解析设置请求
usb_parsesetupreq(pdev, &req);
// 根据请求的类型执行相应的标准请求处理
switch (req.bmRequest & 0x1FU) {
case USB_REQ_RECIPIENT_DEVICE:
// 设备接收的标准请求处理
usb_standarddevreq(pdev, &req);
break;
case USB_REQ_RECIPIENT_INTERFACE:
// 接口接收的标准请求处理
usb_standarditfreq(pdev, &req);
break;
case USB_REQ_RECIPIENT_ENDPOINT:
// 端点接收的标准请求处理
usb_standardepreq(pdev, &req);
break;
default:
// 对于不支持的请求,使设备端点处于挂起状态
usb_stalldevep(pdev, req.bmRequest & 0x80U);
break;
}
}
继续查找,找到这个结构体:
// 静态定义USB设备中断回调结构体
static usb_dev_int_cbk_typedef dev_int_cbk = {
// USB设备重置回调函数
&usb_dev_rst,
// USB控制连接回调函数
&usb_ctrlconn,
// USB设备挂起状态回调函数
&usb_dev_susp,
// USB设备恢复状态回调函数
&usb_dev_resume,
// USB帧开始处理回调函数
&usb_sof_process,
// USB设置过程回调函数
&usb_setup_process,
// USB数据输出过程回调函数
&usb_dataout_process,
// USB数据输入过程回调函数
&usb_datain_process,
// USB异步传输输入不完整处理回调函数
&usb_isoinincomplt_process,
// USB异步传输输出不完整处理回调函数
&usb_isooutincomplt_process
};
这个结构体赋值给了usb_dev_int_cbk_typedef *dev_int_cbkpr = &dev_int_cbk;,然后我们继续查找dev_int_cbkpr指针,查找之前,先看一下usb_dev_int_cbk_typedef 结构体定义。我们要查的函数对应的是dev_int_cbkpr->SetupStage,于是我们又搜索到:
/**
* @brief 此函数用于指示一个OUT端点有待处理的中断
* @param [in] pdev 设备实例
* @retval None
*/
static void usb_outep_isr(usb_core_instance *pdev)
{
uint32_t u32EpIntr; // 用于存储所有OUT端点的中断状态
uint32_t u32doepint; // 用于存储当前处理端点的中断状态
uint8_t u8epnum = 0U; // 当前处理的端点编号
uint32_t u8Xfer; // 传输长度
uint32_t u32ReadEpSize; // 已读取的数据大小
// 获取所有OUT端点的中断状态
u32EpIntr = usb_getalloepintr(&pdev->regs);
// 遍历所有端点检查中断
while ((u32EpIntr != 0UL) && (u8epnum < USB_MAX_TX_FIFOS)) {
// 检查端点是否有中断
if ((u32EpIntr & 0x1UL) != 0UL) {
// 获取当前端点的中断状态
u32doepint = usb_getoepintbit(&pdev->regs, u8epnum);
// 处理传输完成的中断
if ((u32doepint & XFER_COMPL) != 0UL) {
// 清除传输完成的中断状态
WRITE_REG32(pdev->regs.OUTEP_REGS[u8epnum]->DOEPINT, XFER_COMPL);
// 如果DMA启用,处理数据传输长度
if (pdev->basic_cfgs.dmaen == 1U) {
u32ReadEpSize = (READ_REG32(pdev->regs.OUTEP_REGS[u8epnum]->DOEPTSIZ) & USBFS_DOEPTSIZ_XFRSIZ);
u8Xfer = LL_MIN(pdev->dev.out_ep[u8epnum].maxpacket, pdev->dev.out_ep[u8epnum].xfer_len);
pdev->dev.out_ep[u8epnum].xfer_count = u8Xfer - u32ReadEpSize;
if (u8epnum != 0U) {
pdev->dev.out_ep[u8epnum].xfer_count = pdev->dev.out_ep[u8epnum].xfer_len - u32ReadEpSize;
}
}
// 调用数据输出阶段的回调函数
dev_int_cbkpr->DataOutStage(pdev, u8epnum);
// 如果DMA启用,并且是EP0的状态输出阶段,进行特殊处理
if (pdev->basic_cfgs.dmaen == 1U) {
if ((pdev->dev.device_state == USB_EP0_STATUS_OUT) && (u8epnum == 0U)) {
pdev->dev.out_ep[0].xfer_len = 64U;
pdev->dev.out_ep[0].rem_data_len = 64U;
pdev->dev.out_ep[0].total_data_len = 64U;
usb_ep0revcfg(&pdev->regs, pdev->basic_cfgs.dmaen, pdev->dev.setup_pkt_buf);
pdev->dev.device_state = USB_EP0_IDLE;
}
}
}
// 处理端点禁用的中断
if ((u32doepint & EPDISABLED) != 0UL) {
// 清除端点禁用的中断状态
WRITE_REG32(pdev->regs.OUTEP_REGS[u8epnum]->DOEPINT, EPDISABLED);
}
// 如果是端点0,处理设置阶段的中断
if (u8epnum == 0U) {
u32doepint = usb_getoepintbit(&pdev->regs, u8epnum);
if ((u32doepint & SETUP_BIT) != 0UL) {
// 调用设置阶段的回调函数
dev_int_cbkpr->SetupStage(pdev);
// 清除设置阶段的中断状态
WRITE_REG32(pdev->regs.OUTEP_REGS[u8epnum]->DOEPINT, SETUP_BIT);
}
}
}
u8epnum++; // 移动到下一个端点
u32EpIntr >>= 1U; // 清除当前端点的中断状态
}
}
看到这个函数,结尾isr知道了是一个中断,于是跟之前的中断就联系上了。
查找到这里,我们基本是了解了USB的编程,BSP设置+寄存器设置+中断设置,其中中断函数调用栈比较深,但是所有的处理都是在中断完成的。掌握了基本结构之后,回到本例程的功能中,既然是串口发送和接收,那么数据在哪儿处理呢?
还记得上文中提到的usb_rx_buffer这个数组吗?既然这个接收缓存,那么数据处理肯定跟这个有关。找到这个函数:
/**
* @brief 在非控制Out端点接收到数据时调用的函数
* @param [in] pdev 设备实例指针
* @param [in] epnum 端点索引
* @retval None
*/
void usb_dev_cdc_dataout(void *pdev, uint8_t epnum)
{
uint16_t usb_rx_cnt;
// 从指定的非控制Out端点获取已接收的数据字节数
usb_rx_cnt = (uint16_t)((usb_core_instance *)pdev)->dev.out_ep[epnum].xfer_count;
// 将数据通过串口发送,如果修改程序不用串口,则注释掉这一行
vcp_rxdata(usb_rx_buffer, usb_rx_cnt);
// jinyuhang 增加一个发送函数测试一下
if (USB_Tx_State != 1U) {
// usb_deveptx(pdev,
// CDC_IN_EP,
// (uint8_t *)"HELLo WORLD!\r\n",
// (uint32_t)sizeof("HELLo WORLD!\r\n"));
usb_deveptx(pdev,
CDC_IN_EP,
(uint8_t *)usb_rx_buffer,
(uint32_t)usb_rx_cnt);
}
// 上面的测试有效,说明这个函数usb_deveptx就是通过USB的CDC发送函数
// 准备接收新的数据,设置接收缓冲区和最大包大小
usb_readytorx(pdev, CDC_OUT_EP, (uint8_t *)(usb_rx_buffer), MAX_CDC_OUT_PACKET_SIZE);
}
其中,vcp_rxdata(usb_rx_buffer, usb_rx_cnt);表示通过串口3发送数据,usb_readytorx这个是CDC串口接收数据;usb_deveptx函数是我自己加上的,这个表示CDC串口发送数据。
程序看到这里,基本就明白了功能是如何实现的,如果换一个板子,那么移植功能的话,也大概知道修改哪些部分了。
=============================================================
补充一下USB的连接过程:(了解这个过程,才能明白为什么程序会那么写!)
在USB功能设备连接到USB总线时,USB主机通过默认的控制传输管道向设备发出标准USB设备请求。整个USB设备连接过程分为如下几个步骤:
STEP 1 当USB功能设备连接到USB主机或者USB集线器的下行USB端口后,USB总线立即为该设备提供电源。
STEP 2 USB主机检测D-/D+线上的电压,确认其下行USB端口有USB功能设备连接。
STEP 3 USB集线器通过中断IN管道,向USB主机报告下行USB端口有USB功能设备连接。
STEP 4 USB主机接到USB集线器的通知后,通过USB集线器设备类请求GetPortStatus获得关于USB功能设备的更多信息。
STEP 5 USB主机等待100ms,以确保USB功能设备稳定连接。
STEP 6 USB主机发送USB集线器设备类请求SetPortStatus,对连接的USB功能设备执行复位操作。
STEP 7 USB功能设备复位结束后,USB功能设备进入默认状态,从USB总线获取小于100mA的电流,用于使用默认地址对管道0的控制事务进行响应。
STEP 8 USB主机向USB功能设备发送GetDescriptor请求,获取默认控制管道的最大数据包长度。
STEP 9 USB主机发出SetAddress请求,为连接的USB功能设备分配一个唯一的USB设备地址。
STEP 10 USB主机使用新的USB设备地址向USB功能设备发送GetDescriptor请求,并读取其设备描述符的全部字段,包括产品PID、供应商VID等。
STEP 11 USB主机循环发送GetDescriptor请求,获取完整的USB设备配置信息,包括配置描述符、接口描述符、端点描述符以及各种设备类定义描述符和供应商自定义描述符等。
STEP 12 USB主机根据USB功能设备的配置信息,如产品PID、供应商VID等,为其选择并加载一个合适的主机驱动程序。
STEP 13 在正确加载驱动程序后,便可以进行各种配置操作以及数据传输操作等。