基于小华例程3.2版本USB之usb_dev_cdc工程深入代码详解USB过程

打开工程首先看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 在正确加载驱动程序后,便可以进行各种配置操作以及数据传输操作等。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值