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 设备枚举失败、数据传输异常等问题的关键。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐