USB-全速设备接口
更新于 2026年9月8日
本篇总结
一根 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 缓冲区,不能同时使用。
前置工具准备
开始本例程前,请确认以下工具已安装:
- WIZ UartTool V1.0:串口调试工具:点击下载
硬件连接
W55MH32 的 USB 全速设备接口通过 PA11(USB_DM) 和 PA12(USB_DP) 连接板载 USB 收发器到 USB TypeB 接口;调试串口 USART1 通过 PA9/PA10 走 DEBUG USB 口输出日志。使用时需两根线:一根 Micro USB 接 DEBUG USB 口供电并观察串口日志,一根 USB 线接 USB Device 口做 USB 数据通信。
| 引脚 | 功能 | 方向 | 模式 |
|---|---|---|---|
| PA9 | USART1_TX(调试) | 开发板 → PC | 复用推挽 (AF_PP) |
| PA10 | USART1_RX(调试) | PC → 开发板 | 浮空输入 (IN_FLOATING) |
| PA11 | USB_DM | 双向 | USB 复用功能 |
| PA12 | USB_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 出现即表示枚举成功。)

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_27,HSE_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)