[应用相关] STM32 的 USB 驱动代码分析

[复制链接]
554|0
Puchou 发表于 2025-9-5 19:08 | 显示全部楼层 |阅读模式
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_ResetCallback、HAL_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_Transmit、USB_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_DescriptorType、CDC_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);  // 读取数据到缓冲区
}



三、关键注意事项
时钟配置:USB 需要严格的 48MHz 时钟(误差≤0.25%),否则会导致枚举失败,需确保时钟源(HSI48 或 PLL)配置正确。
端点缓冲区:每个端点的缓冲区大小需匹配描述符中定义的wMaxPacketSize,过大或过小会导致数据传输异常。
中断优先级:USB 中断优先级需高于应用层中断,避免中断延迟导致的数据包丢失。
枚举失败排查:常见原因包括描述符格式错误、端点配置不匹配、时钟不稳定等,可通过 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_DESCRIPTOR,wValue = 0x0100表示设备描述符)。
Setup 包结构(8 字节):

1358968ba9875ac77f.png


设备响应(STM32 代码):
设备通过HAL_USB_SetupStageCallback回调接收 Setup 包,核心层解析后调用USB_HandleStandardRequest处理标准请求,从usb_desc.c中读取设备描述符并通过端点 0 返回给主机。

设备描述符示例(usb_desc.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_CtlSendStatus或USB_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 驱动视角)
中断驱动:所有 USB 事件(复位、Setup 包、数据传输完成)均通过中断触发,核心处理函数为HAL_USB_IRQHandler,再分发到各阶段回调(如SetupStageCallback、DataInStageCallback)。
端点状态管理:STM32 通过USB_OTG_EP寄存器组管理端点状态(空闲 / 忙碌),每次传输前需检查端点是否空闲,避免数据覆盖。
类驱动适配:不同设备类(CDC/HID/MSC)的数据通信逻辑封装在类驱动中,例如:
HID 设备通过中断端点发送报告(HID_SendReport);
MSC 设备通过批量端点传输 SCSI 命令和数据。
总结
枚举过程是设备与主机建立 “身份认知” 的过程,核心是通过端点 0 交换描述符和配置信息,最终设备获得唯一地址并激活配置。
数据通信是枚举后的实际数据传输,依赖枚举过程中配置的端点,按传输类型(控制 / 批量 / 中断 / 同步)通过不同端点完成,由 STM32 的中断回调和端点操作函数驱动。
理解这两个过程的交互逻辑,是调试 USB 设备枚举失败、数据传输异常等问题的关键。
————————————————
版权声明:本文为CSDN博主「飞吧~皮卡丘」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/shanstellar/article/details/150775719

您需要登录后才可以回帖 登录 | 注册

本版积分规则

77

主题

240

帖子

0

粉丝
快速回复 在线客服 返回列表 返回顶部