全部例程

USB-全速设备接口

W55MH32

更新于 2026年9月8日

本篇总结

一根 USB 线枚举出 VCP 虚拟串口——串口工具发什么设备原样回传,实现数据回显功能。

🔑 KEIL MDK、UART调试、USB、VCP虚拟串口

硬件平台:本例程基于 W55MH32L-EVB 开发板运行。

采购推荐:点击前往 WIZnet 官方淘宝店铺 选购 W55MH32L-EVB

本示例演示 W55MH32 的 USB 2.0 全速设备接口(Full-Speed,12Mbps),实现一个 USB 虚拟串口(VCP,Virtual COM Port)设备。主机插入 USB 线后,设备管理器会枚举出一个虚拟串口(COMx)。用户通过串口工具向该虚拟串口发送数据,设备会将数据原样回传,实现回显功能。
设备 VID=0x0483 / PID=0x5740,USB 与 CAN 共用 512 字节 SRAM 缓冲区,不能同时使用。

前置工具准备

开始本例程前,请确认以下工具已安装:

硬件连接

W55MH32 的 USB 全速设备接口通过 PA11(USB_DM) 和 PA12(USB_DP) 连接板载 USB 收发器到 USB TypeB 接口;调试串口 USART1 通过 PA9/PA10 走 DEBUG USB 口输出日志。使用时需两根线:一根 Micro USB 接 DEBUG USB 口供电并观察串口日志,一根 USB 线接 USB Device 口做 USB 数据通信。

引脚功能方向模式
PA9USART1_TX(调试)开发板 → PC复用推挽 (AF_PP)
PA10USART1_RX(调试)PC → 开发板浮空输入 (IN_FLOATING)
PA11USB_DM双向USB 复用功能
PA12USB_DP双向USB 复用功能(内部上拉)

工程结构

工程位于 SDK 内 1.SDK\ModuleDemo\USB\VirtualCOMPort\,核心文件如下:

VirtualCOMPort/
├── USER/
│   ├── main.c            ← 主流程:时钟初始化 + USB 初始化 + 主循环
│   ├── w55mh32_conf.h    ← 外设头文件集中包含
│   ├── w55mh32_it.c/h    ← 中断服务函数
│   ├── system_w55mh32.c/h← 系统初始化(时钟配置)
│   └── startup_w55mh32.s ← 启动文件
├── USB/
│   ├── CONFIG/
│   │   ├── hw_config.c/h ← USB 时钟/中断/GPIO 配置
│   │   ├── usb_conf.h    ← 端点数目、PMA 缓冲区地址
│   │   ├── usb_desc.c/h  ← 设备/配置/接口/端点描述符
│   │   ├── usb_endp.c    ← 端点回调(VCP 收发)
│   │   ├── usb_istr.c/h  ← USB 中断分发器
│   │   └── usb_prop.c/h  ← 设备属性(标准请求处理)
│   └── W55MH32_USB-FS-Device_Driver/  ← USB 全速设备库
└── SYSTEM/
    └── delay/
        └── delay.c/h      ← SysTick 延时函数

端点分配:EP0 控制端点(枚举),EP1_IN VCP 发送端点,EP3_OUT VCP 接收端点。PMA 缓冲区共 512 字节,由 usb_conf.h 中的 ENDPx_RXADDR / ENDPx_TXADDR 宏分配。

步骤1:USART 初始化(调试输出)

文件:USER/main.c | 函数:UART_Configuration() — 配置 USART1 PA9/PA10 为 115200 / 8N1,用于打印系统时钟与 USB 连接状态日志。

USART_TypeDef *USART_TEST = USART1;

void UART_Configuration(uint32_t bound)
{
    GPIO_InitTypeDef  GPIO_InitStructure;
    USART_InitTypeDef USART_InitStructure;

    /* 开启外设时钟 */
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

    /* PA9: USART1_TX → 复用推挽输出 */
    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_9;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_AF_PP;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    /* PA10: USART1_RX → 浮空输入 */
    GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_10;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    /* USART 参数:115200 / 8N1 / 全双工 */
    USART_InitStructure.USART_BaudRate            = bound;
    USART_InitStructure.USART_WordLength          = USART_WordLength_8b;
    USART_InitStructure.USART_StopBits            = USART_StopBits_1;
    USART_InitStructure.USART_Parity              = USART_Parity_No;
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    USART_InitStructure.USART_Mode                = USART_Mode_Rx | USART_Mode_Tx;

    USART_Init(USART_TEST, &USART_InitStructure);
    USART_Cmd(USART_TEST, ENABLE);
}

调试串口仅用于日志输出,与 USB 通信彼此独立。系统启动后会先打印 SYSCLK / HCLK / PCLK 时钟信息,便于核对 USB 时钟是否为 48MHz。

步骤2:系统时钟与主函数初始化

文件:USER/main.c — 系统时钟配置、USB 重枚举与主循环。RCC_ClkConfiguration() 配置 PLL 得到 216MHz 系统主频;USB_Port_Set(0/1) 实现软件重枚举,替代物理插拔 USB 线;主循环中轮询 bDeviceState 打印连接状态,并检测 USB_USART_RX_STA 标志判断是否收到完整数据帧。

void RCC_ClkConfiguration(void)
{
    RCC_DeInit();

    /* 使能 HSE(外部高速时钟) */
    RCC_HSEConfig(RCC_HSE_ON);
    while (RCC_GetFlagStatus(RCC_FLAG_HSERDY) == RESET);

    /* 配置 PLL:8MHz × 27 = 216MHz */
    RCC_PLLCmd(DISABLE);
    WIZ_RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_27, 1);
    RCC_PLLCmd(ENABLE);
    while (RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET);

    /* 选择 PLL 作为系统时钟 */
    RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK);

    /* 配置总线分频 */
    RCC_HCLKConfig(RCC_SYSCLK_Div1);   /* HCLK = SYSCLK = 216MHz */
    RCC_PCLK1Config(RCC_HCLK_Div2);    /* PCLK1 = 108MHz */
    RCC_PCLK2Config(RCC_HCLK_Div1);    /* PCLK2 = 216MHz */

    RCC_LSICmd(ENABLE);
    while (RCC_GetFlagStatus(RCC_FLAG_LSIRDY) == RESET);
    RCC_HSICmd(ENABLE);
    while (RCC_GetFlagStatus(RCC_FLAG_HSIRDY) == RESET);
}

void USB_Port_Set(u8 enable)
{
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    if (enable)
        _SetCNTR(_GetCNTR() & (~(1 << 1)));    /* 退出掉电模式 */
    else
    {
        _SetCNTR(_GetCNTR() | (1 << 1));       /* 进入掉电模式 */
        GPIOA->CRH &= 0XFFF00FFF;
        GPIOA->CRH |= 0X00033000;
        PAout(12) = 0;
    }
}

int main(void)
{
    uint16_t t, len, times = 0;
    uint8_t usbstatus = 0;
    RCC_ClocksTypeDef clocks;

    RCC_ClkConfiguration();
    delay_init();
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    UART_Configuration(115200);
    RCC_GetClocksFreq(&clocks);

    printf("\n");
    printf("USB Virtual COM Port\n");
    printf("SYSCLK: %3.1fMhz, HCLK: %3.1fMhz, PCLK1: %3.1fMhz, PCLK2: %3.1fMhz, ADCCLK: %3.1fMhz\n",
           (float)clocks.SYSCLK_Frequency / 1000000,
           (float)clocks.HCLK_Frequency / 1000000,
           (float)clocks.PCLK1_Frequency / 1000000,
           (float)clocks.PCLK2_Frequency / 1000000,
           (float)clocks.ADCCLK_Frequency / 1000000);

    delay_ms(1800);
    USB_Port_Set(0);    /* 先断开 USB */
    delay_ms(700);
    USB_Port_Set(1);    /* 重新连接 USB */
    Set_USBClock();
    USB_Interrupts_Config();
    USB_Init();
    DP_PUUP = 1;        /* 拉高 D+ 上拉,向主机宣告全速设备接入 */

    while (1)
    {
        /* 检测 USB 连接状态变化 */
        if (usbstatus != bDeviceState)
        {
            usbstatus = bDeviceState;
            if (usbstatus == CONFIGURED)
                printf("USB connection successful\n");
            else
                printf("USB disconnected\n");
        }

        /* 如果收到完整一帧数据(以回车换行结束) */
        if (USB_USART_RX_STA & 0x8000)
        {
            len = USB_USART_RX_STA & 0x3FFF;
            usb_printf("\r\nThe length of the message you sent is: %d. The message content is:\r\n", len);
            for (t = 0; t < len; t++)
                USB_USART_SendData(USB_USART_RX_BUF[t]);
            usb_printf("\r\n\r\n");
            USB_USART_RX_STA = 0;
        }
        else
        {
            times++;
            if (times % 200 == 0)
                usb_printf("Please enter data and end with the enter key.\r\n");
            delay_ms(10);
        }
    }
}

步骤3:USB 时钟、中断与设备初始化

文件:USB/CONFIG/hw_config.c — USB 时钟与中断配置。Set_USBClock() 配置 USB 时钟分频为 48MHz;USB_Interrupts_Config() 使能 USB 低优先级中断和唤醒中断;Enter_LowPowerMode()Leave_LowPowerMode() 处理 USB 挂起/唤醒时的状态切换(本例仅做日志打印)。

/* USB 时钟配置:SYSCLK=216MHz,216 ÷ 4.5 = 48MHz */
void Set_USBClock(void)
{
    RCC_USBCLKConfig(RCC_USBCLKSource_PLLCLK_4Div5);
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_USB, ENABLE);
}

/* USB 中断配置 */
void USB_Interrupts_Config(void)
{
    NVIC_InitTypeDef NVIC_InitStructure;
    EXTI_InitTypeDef EXTI_InitStructure;

    /* 配置 EXTI Line18(USB 唤醒事件内部连接) */
    EXTI_ClearITPendingBit(EXTI_Line18);
    EXTI_InitStructure.EXTI_Line    = EXTI_Line18;
    EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising;
    EXTI_InitStructure.EXTI_LineCmd = ENABLE;
    EXTI_InitStructure.EXTI_Mode    = EXTI_Mode_Interrupt;
    EXTI_Init(&EXTI_InitStructure);

    /* USB 低优先级中断:处理所有 USB 事件(CTR/RESET/SUSP 等) */
    NVIC_InitStructure.NVIC_IRQChannel                   = USB_LP_CAN1_RX0_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority        = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd                = ENABLE;
    NVIC_Init(&NVIC_InitStructure);

    /* USB 唤醒中断 */
    NVIC_InitStructure.NVIC_IRQChannel                   = USBWakeUp_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd                = ENABLE;
    NVIC_Init(&NVIC_InitStructure);
}

/* USB 低功耗模式 */
void Enter_LowPowerMode(void)
{
    printf("usb enter low power mode\r\n");
    bDeviceState = SUSPENDED;
}

/* USB 退出低功耗模式 */
void Leave_LowPowerMode(void)
{
    DEVICE_INFO *pInfo = &Device_Info;
    printf("leave low power mode\r\n");
    if (pInfo->Current_Configuration != 0)
        bDeviceState = CONFIGURED;
    else
        bDeviceState = ATTACHED;
}

48MHz 时钟是 USB 全速的硬性要求

USB 全速总线时钟固定为 48MHz,必须由 PLL 分频得到。本例 SYSCLK=216MHz,选用 RCC_USBCLKSource_PLLCLK_4Div5(216÷4.5=48MHz)。DP_PUUP=1 是 W55MH32 专有寄存器位,拉高 D+ 内部上拉电阻即可让主机识别为全速设备,无需外部上拉。

步骤4:端点回调(VCP 收发)

文件:USB/CONFIG/usb_endp.c — VCP 数据收发核心。接收方向:EP3_OUT_Callback 将主机发来的数据经 USB_To_USART_Send_Data 存入 USB_USART_RX_BUF,并检测 0x0d 0x0a 作为帧结束标志置位 USB_USART_RX_STA。发送方向:USB_USART_SendData 将数据写入 FIFO,SOF_Callback 每 5ms 触发 EP1_IN_Callback 从 FIFO 取数经 UserToPMABufferCopy 写入 PMA 后触发发送。

/* FIFO 结构体:用于 USB 发送数据缓存 */
_usb_usart_fifo uu_txfifo;

/* 数据接收缓冲区与状态 */
u8  USB_USART_RX_BUF[USB_USART_REC_LEN];
u16 USB_USART_RX_STA = 0;  /* bit15: 接收完成标志,bit14: 收到0x0d,bit13~0: 有效字节数 */

uint8_t USB_Rx_Buffer[VIRTUAL_COM_PORT_DATA_SIZE];

/* 处理从 USB 虚拟串口接收到的数据(以 0x0d 0x0a 作为帧结束标志) */
void USB_To_USART_Send_Data(u8 *data_buffer, u8 Nb_bytes)
{
    u8 i, res;
    for (i = 0; i < Nb_bytes; i++)
    {
        res = data_buffer[i];
        if ((USB_USART_RX_STA & 0x8000) == 0)
        {
            if (USB_USART_RX_STA & 0x4000)    /* 已收到 0x0d */
            {
                if (res != 0x0a)
                    USB_USART_RX_STA = 0;     /* 错误,重新开始 */
                else
                    USB_USART_RX_STA |= 0x8000; /* 接收完成 */
            }
            else
            {
                if (res == 0x0d)
                    USB_USART_RX_STA |= 0x4000;
                else
                {
                    USB_USART_RX_BUF[USB_USART_RX_STA & 0x3FFF] = res;
                    USB_USART_RX_STA++;
                    if (USB_USART_RX_STA > (USB_USART_REC_LEN - 1))
                        USB_USART_RX_STA = 0;
                }
            }
        }
    }
}

/* EP3_OUT 回调:主机向虚拟串口发数据,存入接收缓冲区 */
void EP3_OUT_Callback(void)
{
    u16 USB_Rx_Cnt;
    USB_Rx_Cnt = USB_SIL_Read(EP3_OUT, USB_Rx_Buffer);
    USB_To_USART_Send_Data(USB_Rx_Buffer, USB_Rx_Cnt);
    SetEPRxValid(ENDP3);
}

/* 向 USB 虚拟串口发送一个字节(写入 FIFO) */
void USB_USART_SendData(u8 data)
{
    uu_txfifo.buffer[uu_txfifo.writeptr] = data;
    uu_txfifo.writeptr++;
    if (uu_txfifo.writeptr == USB_USART_TXFIFO_SIZE)
        uu_txfifo.writeptr = 0;
}

/* USB 虚拟串口 printf 函数(每次发送不超过 USB_USART_REC_LEN 字节) */
void usb_printf(char *fmt, ...)
{
    u16 i, j;
    va_list ap;
    va_start(ap, fmt);
    vsprintf((char *)USART_PRINTF_Buffer, fmt, ap);
    va_end(ap);
    i = strlen((const char *)USART_PRINTF_Buffer);
    for (j = 0; j < i; j++)
        USB_USART_SendData(USART_PRINTF_Buffer[j]);
}

/* EP1_IN 回调:USB 发送完成,继续从 FIFO 取数发送 */
void EP1_IN_Callback(void)
{
    u16 USB_Tx_ptr;
    static u16 USB_Tx_length = 0;

    if (uu_txfifo.readptr == uu_txfifo.writeptr)
    {
        if (USB_Tx_length == VIRTUAL_COM_PORT_DATA_SIZE)
        {
            USB_Tx_length = 0;
            SetEPTxCount(ENDP1, USB_Tx_length);
            SetEPTxValid(ENDP1);
        }
        return;
    }

    /* 计算要发送的数据长度 */
    if (uu_txfifo.readptr < uu_txfifo.writeptr)
        USB_Tx_length = uu_txfifo.writeptr - uu_txfifo.readptr;
    else
        USB_Tx_length = USB_USART_TXFIFO_SIZE - uu_txfifo.readptr;

    if (USB_Tx_length > VIRTUAL_COM_PORT_DATA_SIZE)
        USB_Tx_length = VIRTUAL_COM_PORT_DATA_SIZE;

    USB_Tx_ptr = uu_txfifo.readptr;
    uu_txfifo.readptr += USB_Tx_length;
    if (uu_txfifo.readptr >= USB_USART_TXFIFO_SIZE)
        uu_txfifo.readptr = 0;

    /* 将数据从用户缓冲区拷贝到 PMA 并触发发送 */
    UserToPMABufferCopy(&uu_txfifo.buffer[USB_Tx_ptr], ENDP1_TXADDR, USB_Tx_length);
    SetEPTxCount(ENDP1, USB_Tx_length);
    SetEPTxValid(ENDP1);
}

/* SOF 回调:每 5ms 触发一次,检查 FIFO 是否有数据待发送 */
void SOF_Callback(void)
{
    static uint32_t FrameCount = 0;

    if (bDeviceState == CONFIGURED)
    {
        if (FrameCount++ == 5)
        {
            FrameCount = 0;
            EP1_IN_Callback();
        }
    }
}

关键点:① 接收回调必须重新使能端点(SetEPRxValid(ENDP3)),否则下次主机发送会被 NAK;② 应用代码不能直接读写 PMA(Packet Memory Area,512 字节专用 SRAM),必须用 UserToPMABufferCopy / PMAToUserBufferCopy 在 PMA 与用户缓冲区之间搬运;③ 发送采用 FIFO 缓冲 + SOF 定时触发机制,保证数据不会丢失。

步骤5:中断服务函数

文件:USER/w55mh32_it.c — USB 低优先级中断调用 USB_Istr() 分发事件;唤醒中断清 EXTI18。

/* USB 唤醒中断:清 EXTI Line18(USB 唤醒事件内部连到 EXTI18) */
void USBWakeUp_IRQHandler(void)
{
    EXTI_ClearITPendingBit(EXTI_Line18);
}

/* USB 低优先级中断:调用协议栈分发器,由它根据 ISTR 寄存器调用对应回调 */
void USB_LP_CAN1_RX0_IRQHandler(void)
{
    USB_Istr();
}

步骤6:printf 重定向

文件:USER/main.c — 重写 fputc() 将 printf 输出重定向到 USART1,并自动在 \n 前补 \r 确保串口工具正确换行。

int SER_PutChar(int ch)
{
    while (!USART_GetFlagStatus(USART_TEST, USART_FLAG_TC));
    USART_SendData(USART_TEST, (uint8_t)ch);
    return ch;
}

int fputc(int c, FILE *f)
{
    if (c == '\n')
        SER_PutChar('\r');
    return (SER_PutChar(c));
}

运行验证

按 RESET 启动后,DEBUG 串口(WIZ UartTool V1.0,115200 / 8N1)预期输出:

USB Virtual COM Port
SYSCLK: 216.0Mhz, HCLK: 216.0Mhz, PCLK1: 108.0Mhz, PCLK2: 216.0Mhz, ADCCLK: 108.0Mhz
usb pull up enable
leave low power mode
usb enter low power mode
USB disconnected
leave low power mode
USB disconnected
USB connection successful
linecoding.format:0
linecoding.paritytype:0
linecoding.datatype:8
linecoding.bitrate:115200

(以上为设备枚举过程中的正常状态切换,USB connection successful 出现即表示枚举成功。)

WIZ UartTool 串口输出 — USB 复合设备 HID&VCP 连接测试

Windows 设备管理器中应出现:「端口(COM 和 LPT)」下多出一个 USB Serial Device (COMx)。用串口工具打开该 COMx,向虚拟串口发送任意字符串(以回车换行结束),设备会立即回显相同内容。

常见问题

Q: 插入 USB 线后设备管理器完全不识别

按顺序排查:① DP_PUUP=1 是否执行(用调试串口日志确认程序跑到 USB_Init);② Set_USBClock() 分频是否正确(216MHz 必须选 RCC_USBCLKSource_PLLCLK_4Div5);③ PA11/PA12 引脚是否被其他外设占用;④ USB Device 口线缆是否为数据线(部分线仅供电)。

Q: 设备识别但带黄色感叹号(代码 10 / 43)

通常是描述符与端点配置不一致。检查 usb_desc.c 中端点描述符的地址/方向与 usb_conf.h 中的 ENDPx_RXADDR/TXADDR 是否对应;检查 usb_desc.c 中接口描述符的 bInterfaceClass 是否为 0x02(CDC 类)。

Q: VCP 虚拟串口能识别,但发数据无回显

关键检查:① bDeviceState == CONFIGURED 是否成立(未配置时端点不工作);② EP3_OUT_Callback 内是否漏写 SetEPRxValid(ENDP3) 重新使能接收;③ 主循环中 USB_USART_RX_STA & 0x8000 判断是否触发(检查发送数据是否以 \r\n 结尾)。

Q: 调试串口打印的 SYSCLK 不是 216MHz

USB 48MHz 时钟由 SYSCLK 经分频得到,SYSCLK 异常会导致 USB 时钟偏差、枚举失败。检查 RCC_ClkConfiguration() 中 PLL 倍频是否为 RCC_PLLMul_27HSE_VALUE 是否与板载晶振(8MHz)一致。

总结

本文基于 W55MH32 的 USB 2.0 全速设备接口实现了一个虚拟串口(VCP)回显设备:

  • USB 时钟:SYSCLK=216MHz 经 RCC_USBCLKSource_PLLCLK_4Div5 分频得到 48MHz;DP_PUUP=1 拉高 D+ 内部上拉宣告全速设备接入
  • 端点分配:EP0 控制端点,EP1_IN 发送端点,EP3_OUT 接收端点
  • 接收流程:主机发数据 → EP3_OUT 回调搬到用户缓冲区 → 主循环检测到 0x0d 0x0a 帧结束标志 → 调用 usb_printf / USB_USART_SendData 回传
  • 发送流程USB_USART_SendData 写入 FIFO → SOF 定时触发 EP1_IN_Callback → 从 FIFO 取数经 UserToPMABufferCopy 写入 PMA → 触发发送
  • 关键细节:应用代码不能直接访问 PMA,必须用 UserToPMABufferCopy / PMAToUserBufferCopy 搬运;接收回调必须重新使能端点(SetEPRxValid

下载本章例程

我们提供完整的工程文件以及配套开发板,方便你随时测试,快速完成产品开发:

开发环境: Keil MDK5 配套开发板