> 技术文档 > STM32 的 USB 驱动代码分析_stm32 usbhal库源码详解

STM32 的 USB 驱动代码分析_stm32 usbhal库源码详解

STM32 的 USB 驱动代码通常基于 STM32 HAL 库(硬件抽象层)或 LL 库(低层库)实现,其核心是对 USB 外设的寄存器操作、USB 协议解析和设备类功能的封装。从架构上可分为硬件抽象层USB 核心层设备类驱动层三个主要部分,下面结合典型代码结构进行分析:

一、整体架构与核心文件

STM32 的 USB 驱动代码通常包含以下关键文件(以 STM32F4 系列 USB FS 为例):

  • stm32f4xx_hal_usb.h/c:HAL 层核心文件,封装 USB 外设的初始化、中断处理、数据收发等硬件操作。
  • usb_core.h/c:USB 协议核心层,处理 USB 枚举、端点管理、 Setup 包解析等协议相关逻辑。
  • usb_xxx.c/h:设备类驱动(如usb_cdc.c for CDC 串口、usb_hid.c for HID 设备),实现特定设备类的功能(如数据透传、报告传输)。
  • usb_desc.h/c:定义 USB 设备描述符(设备描述符、配置描述符、接口描述符等),用于主机枚举识别设备。

二、核心模块解析

1. 硬件抽象层(HAL 层)

HAL 层直接操作 USB 外设寄存器,提供最基础的硬件控制接口,核心功能包括:

  • USB 外设初始化
    通过HAL_USB_Init(USB_HandleTypeDef *husb)函数完成,主要配置:

    • 时钟:USB 需要 48MHz 时钟(通常由 PLL 分频或 HSI48 提供);
    • GPIO:配置 USB_DP/USB_DM 引脚为复用功能;
    • 中断:使能 USB 全局中断,配置中断优先级;
    • 外设参数:设置 USB 模式(设备模式 / 主机模式)、端点数量等。

    示例代码片段:

    c

    运行

    USB_HandleTypeDef husb;void USB_Init(void) { husb.Instance = USB_OTG_FS; // 选择USB外设(FS/HS) husb.Init.Mode = USB_OTG_DEVICE_MODE; // 设备模式 husb.Init.low_power_enable = DISABLE; // 其他配置(如中断优先级、PHY类型等) if (HAL_USB_Init(&husb) != HAL_OK) { Error_Handler(); }}
  • 中断处理
    USB 外设产生的所有事件(如总线复位、Setup 包接收、数据传输完成)都会触发中断,由HAL_USB_IRQHandler(USB_HandleTypeDef *husb)统一处理。该函数会根据中断标志位(如USB_OTG_GINTSTS寄存器)识别事件类型,并调用对应的回调函数(如HAL_USB_ResetCallbackHAL_USB_SetupStageCallback)。

    示例中断处理流程:

    void OTG_FS_IRQHandler(void) { HAL_USB_IRQHandler(&husb); // 调用HAL层中断处理函数}// 总线复位回调(枚举第一步)void HAL_USB_ResetCallback(USB_HandleTypeDef *husb) { // 复位后初始化端点0(控制端点) USB_Endpoint0_Init(husb);}
2. USB 核心层(协议处理)

核心层基于 HAL 层提供的硬件接口,实现 USB 协议的核心逻辑,重点是枚举过程端点管理

  • 枚举过程
    枚举是 USB 设备插入主机后,主机识别设备的过程,核心是通过控制端点(端点 0) 交换描述符。核心层需处理主机发送的 Setup 包(包含请求类型、请求码等),并返回对应的数据(如设备描述符、配置描述符)。

    Setup 包解析示例:

    void USB_ProcessSetupPacket(USB_HandleTypeDef *husb, uint8_t *setup_buf) { USB_SetupTypeDef *setup = (USB_SetupTypeDef*)setup_buf; switch (setup->bmRequestType & USB_REQ_TYPE_MASK) { case USB_REQ_TYPE_STANDARD: // 标准请求(如获取描述符) USB_HandleStandardRequest(husb, setup); break; case USB_REQ_TYPE_CLASS: // 类请求(如CDC的设置波特率) USB_HandleClassRequest(husb, setup); break; // 其他请求类型... }}
  • 端点管理
    STM32 USB 外设通常包含多个端点(如 USB FS 有 4 个双向端点),每个端点对应特定的传输类型(控制、批量、中断、同步)。核心层需管理端点的缓冲区(通过USB_OTG_EP寄存器配置)、监控传输完成状态,并提供数据收发接口(如USB_EP_TransmitUSB_EP_Receive)。

    端点配置示例(批量输出端点):

    void USB_Endpoint1_Init(USB_HandleTypeDef *husb) { USB_OTG_EPTypeDef ep; ep.Num = 1; // 端点号1 ep.IsIN = 0; // 输出端点(主机到设备) ep.Type = USB_OTG_EP_BULK; // 批量传输 ep.MaxPacketSize = 64; // 最大包大小64字节 HAL_USB_EP_Init(husb, &ep); // 初始化端点}
3. 设备类驱动层

USB 协议定义了多种设备类(如 CDC、HID、MSC 等),类驱动层基于核心层实现特定类的功能,以CDC(虚拟串口) 为例:

  • 类描述符:除标准描述符外,CDC 还需定义类特定描述符(如CDC_DescriptorTypeCDC_FunctionalDescriptor),用于主机识别串口功能。
  • 类请求处理:处理主机发送的类特定请求(如设置波特率、流量控制),例如:
    void USB_CDC_HandleClassRequest(USB_HandleTypeDef *husb, USB_SetupTypeDef *setup) { switch (setup->bRequest) { case CDC_SET_LINE_CODING: // 设置波特率 USB_CDC_ReceiveLineCoding(husb, setup->wLength); // 接收主机发送的波特率参数 break; case CDC_SEND_BREAK: // 发送中断信号 USB_CDC_SendBreak(husb); break; // 其他类请求... }}
  • 数据收发:通过批量端点实现数据透传,例如:
    // 发送数据(设备到主机)uint8_t USB_CDC_Transmit(USB_HandleTypeDef *husb, uint8_t *data, uint16_t len) { return HAL_USB_EP_Transmit(husb, CDC_EP_IN, data, len); // 通过IN端点发送}// 接收数据(主机到设备)void USB_CDC_ReceiveCallback(USB_HandleTypeDef *husb) { uint16_t len = HAL_USB_EP_GetRxCount(husb, CDC_EP_OUT); // 获取接收长度 HAL_USB_EP_Receive(husb, CDC_EP_OUT, rx_buf, len); // 读取数据到缓冲区}

三、关键注意事项

  1. 时钟配置:USB 需要严格的 48MHz 时钟(误差≤0.25%),否则会导致枚举失败,需确保时钟源(HSI48 或 PLL)配置正确。
  2. 端点缓冲区:每个端点的缓冲区大小需匹配描述符中定义的wMaxPacketSize,过大或过小会导致数据传输异常。
  3. 中断优先级:USB 中断优先级需高于应用层中断,避免中断延迟导致的数据包丢失。
  4. 枚举失败排查:常见原因包括描述符格式错误、端点配置不匹配、时钟不稳定等,可通过 USB 分析仪抓取枚举过程的数据包定位问题。

总结

STM32 的 USB 驱动代码是硬件操作协议解析设备类功能的分层封装:HAL 层负责硬件控制,核心层处理 USB 协议(枚举、端点管理),类驱动层实现特定设备的功能。理解各层的交互逻辑(如中断触发→核心层解析→类驱动处理)是掌握 USB 驱动的关键。

详细介绍枚举和数据通信过程

在 USB 协议中,枚举过程是设备插入主机后建立通信的基础,而数据通信则是枚举完成后设备与主机通过端点传输数据的过程。下面结合 STM32 USB 驱动代码,详细解析这两个核心过程。

一、USB 枚举过程(Device Enumeration)

枚举是主机识别设备、分配地址并配置设备的过程,核心是通过控制端点(Endpoint 0) 交换信息。整个过程由主机主导,设备被动响应,分为以下 8 个关键步骤:

步骤 1:设备插入与总线复位(Bus Reset)
  • 触发条件:设备插入 USB 接口后,主机检测到 DP/DM 线上的电平变化(全速设备会将 DP 线拉高,低速设备拉低),识别到有新设备接入。

  • 主机动作:主机向设备发送总线复位信号(持续至少 10ms 的低电平),强制设备回到默认状态。

  • 设备响应(STM32 代码)
    设备 USB 外设检测到复位信号后触发中断,HAL 层中断处理函数HAL_USB_IRQHandler调用复位回调HAL_USB_ResetCallback。此时设备会:

    • 复位所有端点(仅保留端点 0 有效);
    • 回到默认地址(0x00,所有新设备的初始地址);
    • 初始化端点 0 的控制传输缓冲区(通常 64 字节)。

    代码示例:

    void HAL_USB_ResetCallback(USB_HandleTypeDef *husb) { // 复位端点配置,仅保留端点0 USB_ResetEndpoints(husb); // 初始化端点0(控制端点,双向,最大包长64字节) USB_Endpoint0_Init(husb, 64); // 重置设备地址为默认地址0x00 husb->dev_addr = 0;}
步骤 2:主机获取设备描述符(Get Device Descriptor)
  • 主机动作:主机通过默认地址(0x00)向设备端点 0 发送Setup 包,请求获取设备描述符(bRequest = USB_REQ_GET_DESCRIPTORwValue = 0x0100表示设备描述符)。
    Setup 包结构(8 字节):

    字段 含义 示例值(获取设备描述符) bmRequestType 请求类型(主机→设备) 0x80(标准请求,设备方向) bRequest 请求码 0x06(获取描述符) wValue 描述符类型 + 索引 0x0100(设备描述符,索引 0) wIndex 语言 ID(设备描述符无效) 0x0000 wLength 请求长度 0x0012(设备描述符固定 18 字节)
  • 设备响应(STM32 代码)
    设备通过HAL_USB_SetupStageCallback回调接收 Setup 包,核心层解析后调用USB_HandleStandardRequest处理标准请求,从usb_desc.c中读取设备描述符并通过端点 0 返回给主机。

    设备描述符示例(usb_desc.c中定义):

    c

    运行

    const uint8_t Device_Descriptor[18] = { 0x12, // bLength:描述符长度18字节 0x01, // bDescriptorType:设备描述符类型(0x01) 0x0200, // bcdUSB:支持USB 2.0 0x00, // bDeviceClass:设备类(0表示由接口描述符定义) 0x00, // bDeviceSubClass 0x00, // bDeviceProtocol 0x40, // bMaxPacketSize0:端点0最大包长64字节 0x1234, // idVendor:厂商ID 0x5678, // idProduct:产品ID 0x0100, // bcdDevice:设备版本 0x01, // iManufacturer:厂商字符串索引 0x02, // iProduct:产品字符串索引 0x03, // iSerialNumber:序列号索引 0x01 // bNumConfigurations:支持的配置数};
步骤 3:主机分配设备地址(Set Address)
  • 主机动作:主机根据设备描述符确认设备合法性后,发送设置地址请求bRequest = USB_REQ_SET_ADDRESS),为设备分配唯一地址(如 0x05)。
    Setup 包关键字段:wValue = 0x0005(新地址)。

  • 设备响应(STM32 代码)
    设备解析请求后,在USB_HandleStandardRequest中保存新地址,并通过状态阶段返回确认。注意:新地址在本次控制传输完成后生效(即状态阶段结束后)。

    代码示例:

    case USB_REQ_SET_ADDRESS: // 保存新地址(wValue低8位) husb->dev_addr = (uint8_t)(setup->wValue & 0xFF); // 发送状态包确认(控制传输的状态阶段) USB_CtlSendStatus(husb); break;
步骤 4:主机再次获取完整设备描述符
  • 主机动作:使用新分配的地址,再次请求完整的设备描述符(确认地址生效,并获取更详细信息)。
  • 设备响应:设备以新地址响应,返回完整的设备描述符。
步骤 5:主机获取配置描述符(Get Configuration Descriptor)
  • 主机动作:发送请求获取配置描述符(wValue = 0x0200),包含配置描述符、接口描述符、端点描述符等组合信息。

  • 设备响应:设备返回配置描述符集合,其中包含:

    • 配置描述符:总长度、接口数量、功耗等;
    • 接口描述符:接口类(如 CDC 类为 0x02)、端点数量等;
    • 端点描述符:端点号、传输类型(批量 / 中断等)、最大包长等。

    例如,CDC 设备的端点描述符(usb_desc.c):

    // 批量IN端点(设备→主机,端点1)const uint8_t EP1_IN_Descriptor[7] = { 0x07, // bLength:7字节 0x05, // bDescriptorType:端点描述符 0x81, // bEndpointAddress:端点1,IN方向(0x80表示IN) 0x02, // bmAttributes:批量传输(0x02) 0x4000, // wMaxPacketSize:64字节(低字节有效) 0x00 // bInterval:批量传输无需轮询间隔};
步骤 6:主机获取字符串描述符(可选)
  • 主机动作:若设备描述符中字符串索引非 0,主机请求字符串描述符(如厂商名称、产品名称),wValue指定字符串索引和语言 ID(如 0x0409 表示英文)。
  • 设备响应:设备返回 UTF-16 编码的字符串(如 \"STM32 USB CDC\")。
步骤 7:主机设置配置(Set Configuration)
  • 主机动作:发送设置配置请求(bRequest = USB_REQ_SET_CONFIGURATION),指定要激活的配置值(如wValue = 0x01表示第一个配置)。

  • 设备响应:设备激活配置,初始化配置中定义的所有端点(如 CDC 的批量 IN/OUT 端点、中断端点),并返回确认。

    STM32 代码中,配置激活后会调用类驱动的初始化函数:

    void USB_HandleSetConfiguration(USB_HandleTypeDef *husb, uint8_t config) { if (config == 1) { // 激活配置1 // 初始化CDC类的端点(批量IN/OUT、中断IN) USB_CDC_InitEndpoints(husb); // 标记配置完成 husb->configured = 1; }}
步骤 8:枚举完成
  • 设备进入配置状态,所有端点按配置描述符生效,可开始数据通信。

二、数据通信过程(Data Transfer)

枚举完成后,设备与主机通过端点(Endpoint) 进行数据传输,USB 定义了 4 种传输类型,对应不同的应用场景:

1. 控制传输(Control Transfer)
  • 用途:用于配置设备、获取状态等(如枚举过程中的请求),仅通过端点 0 实现。
  • 传输阶段:Setup 阶段(8 字节请求)→ Data 阶段(可选,传输数据)→ Status 阶段(确认完成)。
  • STM32 处理:通过HAL_USB_DataInStageCallback(设备→主机)和HAL_USB_DataOutStageCallback(主机→设备)回调处理数据阶段,状态阶段由USB_CtlSendStatusUSB_CtlReceiveStatus完成。
2. 批量传输(Bulk Transfer)
  • 用途:传输大量数据(如文件、串口数据),无固定时序但保证数据正确性(出错重传),典型应用:U 盘、CDC 虚拟串口。

  • STM32 实现

    • 发送(设备→主机):通过HAL_USB_EP_Transmit函数向 IN 端点写入数据,传输完成后触发HAL_USB_EP_TxCallback
    • 接收(主机→设备):通过HAL_USB_EP_Receive函数从 OUT 端点读取数据,接收完成后触发HAL_USB_EP_RxCallback

    代码示例(CDC 设备发送数据):

    // 发送数据到主机(通过批量IN端点1)uint8_t CDC_Transmit(uint8_t *data, uint16_t len) { // 检查端点状态,空闲时发送 if (husb->ep[1].is_in_busy == 0) { return HAL_USB_EP_Transmit(&husb, 1, data, len); } return HAL_BUSY;}// 发送完成回调void HAL_USB_EP_TxCallback(USB_HandleTypeDef *husb, uint8_t epnum) { if (epnum == 1) { // 端点1发送完成 // 标记发送完成,可准备下一次发送 cdc_tx_complete = 1; }}
3. 中断传输(Interrupt Transfer)
  • 用途:传输小批量、低延迟数据(如鼠标 / 键盘事件、设备状态通知),主机按固定间隔(bInterval)轮询设备。
  • STM32 实现
    设备通过中断 IN 端点主动上报数据,主机按bInterval(如 10ms)查询。例如,CDC 设备通过中断端点通知主机串口状态(如是否有数据待发):
    // 向主机发送CDC状态通知(通过中断IN端点2)void CDC_SendStatus(uint8_t status) { uint8_t buf[1] = {status}; HAL_USB_EP_Transmit(&husb, 2, buf, 1); // 端点2为中断IN端点}
4. 同步传输(Isochronous Transfer)
  • 用途:传输实时数据(如音频、视频),保证时序但不保证正确性(出错不重传),STM32 较少使用(需特定外设支持)。

三、关键交互逻辑(STM32 驱动视角)

  1. 中断驱动:所有 USB 事件(复位、Setup 包、数据传输完成)均通过中断触发,核心处理函数为HAL_USB_IRQHandler,再分发到各阶段回调(如SetupStageCallbackDataInStageCallback)。
  2. 端点状态管理:STM32 通过USB_OTG_EP寄存器组管理端点状态(空闲 / 忙碌),每次传输前需检查端点是否空闲,避免数据覆盖。
  3. 类驱动适配:不同设备类(CDC/HID/MSC)的数据通信逻辑封装在类驱动中,例如:
    • HID 设备通过中断端点发送报告(HID_SendReport);
    • MSC 设备通过批量端点传输 SCSI 命令和数据。

总结

  • 枚举过程是设备与主机建立 “身份认知” 的过程,核心是通过端点 0 交换描述符和配置信息,最终设备获得唯一地址并激活配置。
  • 数据通信是枚举后的实际数据传输,依赖枚举过程中配置的端点,按传输类型(控制 / 批量 / 中断 / 同步)通过不同端点完成,由 STM32 的中断回调和端点操作函数驱动。

理解这两个过程的交互逻辑,是调试 USB 设备枚举失败、数据传输异常等问题的关键。