STM32 USB Host库 CDC类 修改程序驱动4G/5G模块的总结

背景

使用STM32的USB接口与无线4G/5G模块通信,驱动无线4G/5G模块上网。
STM32的USB与无线模块通信,STM32做主机,类选择在CDC类,之后在STM32_USB_Host_Library的代码上修改。

参考资料

  1. 使用stm32cubemx的usb-host-cdc库驱动EC20模块
  2. 利用STM32CubeMX软件生成USB_HOST_CDC驱动ME909s-821ap(4G通信模块)
  3. USB协议分析
  4. USB 配置描述符
  5. USB中文网-USB2.0规范-很详细,通俗易懂,推荐

需要用到的知识

USB协议中的:各种描述符,接口(Interface)、端点(Endpoint)的含义和作用。
USB具体的通讯模型不需要,只需要了解传输的类型有:中断传输、批量传输、同步传输、控制传输。

需要用到的USB协议知识的简单介绍(自己的理解)

这部分是之后需要用到USB协议的知识的介绍,详细的可以看看参考资料5。

  • 在一个USB系统中,有一个主机和一个或多个设备,设备端不会主动发起通信,而是主机问设备答。

  • 在USB体系中,数据的传输方向永远是以主机为参考对象的。向主机发送数据叫做IN,从主机发出数据叫OUT.

  • 在USB体系中,一个USB设备端基础的单位是端点(Endpoint),是真实的物理概念,一个或多个端点组成接口(Interface),一个接口就是实现一种功能。一般情况,一个设备有多个接口。

  • 端点和管道的区别
    端点是USB设备端的概念,是真实的物理设备上的概念,是物理存在。
    管道是USB主机端的上软件的概念,可动态销毁和创建,在使用管道进行数据传输前,需要配置。
    USB主机端是通过管道进行数据的收发。USB设备端通过端点进行数据的收发。
    USB端点按功能可分为控制端点和数据端点。
    控制端点在USB设备端就是端点0,每个USB设备都必须包括一个USB控制端点。
    在USB系统中每一个端点都有唯一的地址,这是由设备地址和端点号给出的。
    USB设备的端点按组进行分类,这个组在USB主语中叫做USB接口,通过接口描述符来描述。
    USB主机和设备使用管道进行数据通讯。管道是USB主机在软件层面上的一个抽象.管道可以理解为USB主机端对USB端点的软件抽象,所以它包括USB设备端点的所有信息。

  • 传输的类型有:中断传输、批量传输、同步传输、控制传输。

  • 当一个设备与主机在物理层面开始连接后,双方会进行一系列规定好的通信,使主机认识此设备的情况。其中重要的阶段是枚举的过程。

  • 枚举
    USB主机检测到USB设备插入后,就要对设备进行枚举了。
    枚举的作用就是从设备读取一些信息,知道设备是什么样的设备,然后通信。
    枚举的过程是主机与设备的端点0进行控制传输,获取描述符。

  • 描述符
    一个设备只有一个设备描述符,而一个设备描述符可以包含多个配置描述符,而一个配置描述符可以包含多个接口描述符,一个接口使用了几个端点,就有几个端点描述符。
    image
    这个描述符内部的每个成员的含义,用到的时候可以看参考资料4。

过程

CubeMX配置

接口选择USB的Host_only,
中间件选择USB_Host,
Class选择Communication Host Class (Virtual Port Com)。

STM32 Host Library 调用关系和函数解释

文件夹 STM32_USB_Host_Library 中有两个文件夹,
Core 文件夹是USB核心的程序
Class 文件夹是和某个类相关的程序

void MX_USB_HOST_Process(void)
调用
USBH_StatusTypeDef USBH_Process(USBH_HandleTypeDef *phost)//USB内核背景任务
调用
phost->pUser(),即 USBH_UserProcess() 函数,改变 Appli_state 的值。

USBH_Process() 函数的解释
USBH_Process() 函数中是根据 USBH_HandleTypeDef.gState 的状态机,状态如下,作用是连接USB包括枚举过程
/* Following states are used for gState */
typedef enum
{
  HOST_IDLE = 0U,
  HOST_DEV_WAIT_FOR_ATTACHMENT,
  HOST_DEV_ATTACHED,
  HOST_DEV_DISCONNECTED,
  HOST_DETECT_DEVICE_SPEED,
  HOST_ENUMERATION,        //枚举过程
  HOST_CLASS_REQUEST,
  HOST_INPUT,
  HOST_SET_CONFIGURATION,
  HOST_SET_WAKEUP_FEATURE,
  HOST_CHECK_CLASS,
  HOST_CLASS,
  HOST_SUSPENDED,
  HOST_ABORT_STATE,
} HOST_StateTypeDef;

其中 HOST_ENUMERATION 是枚举过程,调用 USBH_HandleEnum() 进行枚举,
此函数是根据 USBH_HandleTypeDef.EnumState 的状态机,状态如下,
/* Following states are used for EnumerationState */
typedef enum
{
  ENUM_IDLE = 0U,
  ENUM_GET_FULL_DEV_DESC,  //获取设备描述符
  ENUM_SET_ADDR,     //设置地址
  ENUM_GET_CFG_DESC,    //获取配置描述符
  ENUM_GET_FULL_CFG_DESC,    //获取全部配置描述符
  ENUM_GET_MFC_STRING_DESC,    //获取生产商字符串描述符
  ENUM_GET_PRODUCT_STRING_DESC,    //获取产品字符串描述符
  ENUM_GET_SERIALNUM_STRING_DESC,    //获取系列字符串描述符
} ENUM_StateTypeDef; 

枚举完成之后,查看全局变量USBH_HandleTypeDef hUsbHostHS的值,详情如下

USBH_HandleTypeDef 结构体
/* USB Host handle structure */
typedef struct _USBH_HandleTypeDef
{
  __IO HOST_StateTypeDef     gState;       /*  Host State Machine Value */
  ENUM_StateTypeDef     EnumState;    /* Enumeration state Machine */
  CMD_StateTypeDef      RequestState;
  USBH_CtrlTypeDef      Control;
  USBH_DeviceTypeDef    device; //设备的情况,有设备描述符、配置描述符
  USBH_ClassTypeDef    *pClass[USBH_MAX_NUM_SUPPORTED_CLASS]; //类
  USBH_ClassTypeDef    *pActiveClass; //当前的类
  uint32_t              ClassNumber;
  uint32_t              Pipes[16];
  __IO uint32_t         Timer;
  uint32_t              Timeout;
  uint8_t               id;
  void                 *pData;
  void (* pUser)(struct _USBH_HandleTypeDef *pHandle, uint8_t id);
} USBH_HandleTypeDef;

需要关注结构体成员device,是USBH_DeviceTypeDef类型,详情如下。

USBH_DeviceTypeDef 结构体
/* Attached device structure */
typedef struct
{
  uint8_t                           CfgDesc_Raw[USBH_MAX_SIZE_CONFIGURATION];
  uint8_t                           Data[USBH_MAX_DATA_BUFFER];
  uint8_t                           address;
  uint8_t                           speed;
  uint8_t                           EnumCnt;
  uint8_t                           RstCnt;
  __IO uint8_t                      is_connected;
  __IO uint8_t                      is_disconnected;
  __IO uint8_t                      is_ReEnumerated;
  uint8_t                           PortEnabled;
  uint8_t                           current_interface; //选择的设备的接口
  USBH_DevDescTypeDef               DevDesc; //设备描述符
  USBH_CfgDescTypeDef               CfgDesc; //配置描述符,里面有接口描述符、端点描述符
} USBH_DeviceTypeDef;

这个结构体,需要重点关注成员
current_interface选择的设备的接口,之后需要修改
DevDesc设备描述符,里面的classcode之后需要修改
CfgDesc配置描述符,里面包含多个接口描述符,每个接口描述符包含多个端点描述符

开始修改

1. 修改CDC_Class结构体

CDC_Class结构体是USBH_ClassTypeDef类型,将成员ClassCode修改为0xFF

修改原因:根据USB协议规定,ClassCode:0x02代表CDC设备,0xFF为生产商自定义USB设备。
一般通信模块的给自己的ClassCode为0xFF。
这个修改在文件 usbh_core.c 中的USBH_Process函数中用到,详情如下。

点击查看代码
case HOST_CHECK_CLASS:

省略······

for (idx = 0U; idx < USBH_MAX_NUM_SUPPORTED_CLASS; idx++)
{
    if (phost->pClass[idx]->ClassCode == phost->device.CfgDesc.Itf_Desc[0].bInterfaceClass)
    {
        phost->pActiveClass = phost->pClass[idx];
        break;
    }
}
省略······

CDC_Class结构体的成员ClassCode与设备的第0个接口描述符的InterfaceClass成员的值一样,就会把pClass赋值给pActiveClass,我们要确保一样。

2. 修改文件 usbh_cdc.c 中的 USBH_CDC_InterfaceInit 函数

修改方法参考:
使用stm32cubemx的usb-host-cdc库驱动EC20模块

修改原因:
函数USBH_CDC_InterfaceInit做了下面几件事:

  1. 找通信接口(Communication Interface)
  2. 找通信接口下的端点0(endpoint),连接端点,作为NotifEp
  3. 打开管道(pipe)
  4. 找数据接口(DATA INTERFACE)
  5. 找数据接口下的端点,根据IN和OUT,连接端点,作为OutEp或InEp
  6. 打开管道(pipe)
    详情如下。( 端点描述符的bEndpointAddress成员的Bit7位,代表方向,1/0:IN/OUT )
USBH_CDC_InterfaceInit()函数解释
static USBH_StatusTypeDef USBH_CDC_InterfaceInit(USBH_HandleTypeDef *phost)
{
······
// 找通信接口(Communication Interface)
  interface = USBH_FindInterface(phost, COMMUNICATION_INTERFACE_CLASS_CODE,
                                   ABSTRACT_CONTROL_MODEL, COMMON_AT_COMMAND);
······
// 选定这个接口
  status = USBH_SelectInterface(phost, interface);
······
// 找通信接口下的端点0(endpoint),连接端点,作为NotifEp
  /*Collect the notification endpoint address and length*/
  if ((phost->device.CfgDesc.Itf_Desc[interface].Ep_Desc[0].bEndpointAddress & 0x80U) != 0U)
  {
    CDC_Handle->CommItf.NotifEp = phost->device.CfgDesc.Itf_Desc[interface].Ep_Desc[0].bEndpointAddress;
    CDC_Handle->CommItf.NotifEpSize  = phost->device.CfgDesc.Itf_Desc[interface].Ep_Desc[0].wMaxPacketSize;
  }
// 打开管道(pipe)
  /*Allocate the length for host channel number in*/
  CDC_Handle->CommItf.NotifPipe = USBH_AllocPipe(···);
  /* Open pipe for Notification endpoint */
  (void)USBH_OpenPipe(···);
  (void)USBH_LL_SetToggle(···);
// 4. 找数据接口(DATA INTERFACE)
  interface = USBH_FindInterface(phost, DATA_INTERFACE_CLASS_CODE,
                                   RESERVED, NO_CLASS_SPECIFIC_PROTOCOL_CODE);
// 5. 找数据接口下的端点,根据IN和OUT,连接端点,作为OutEp或InEp
// 与0x80做&运算时因为端点描述符的bEndpointAddress成员的Bit7位,代表方向,1/0:IN/OUT
  /*Collect the class specific endpoint address and length*/
  if ((phost->device.CfgDesc.Itf_Desc[interface].Ep_Desc[0].bEndpointAddress & 0x80U) != 0U)
  {
    CDC_Handle->DataItf.InEp = phost->device.CfgDesc.Itf_Desc[interface].Ep_Desc[0].bEndpointAddress;
    CDC_Handle->DataItf.InEpSize  = phost->device.CfgDesc.Itf_Desc[interface].Ep_Desc[0].wMaxPacketSize;
  }
······
  if ((phost->device.CfgDesc.Itf_Desc[interface].Ep_Desc[1].bEndpointAddress & 0x80U) != 0U)
  {······}

// 6. 打开管道(pipe)
  /*Allocate the length for host channel number out*/
  CDC_Handle->DataItf.OutPipe = USBH_AllocPipe(···);
  /*Allocate the length for host channel number in*/
  CDC_Handle->DataItf.InPipe = USBH_AllocPipe(···);
  /* Open channel for OUT endpoint */
  (void)USBH_OpenPipe(···);
  /* Open channel for IN endpoint */
  (void)USBH_OpenPipe(···);
······
}

这就是

cubemx生成的例程中,标准的CDC类设备,1个配置描述符中需要2接口,
其中一个为Communication Interface Class, 该接口需要一个方向为in的Ep,
另外一个为Data Interface Class, 该接口需要一个方向为in的Ep和一个方向为out的Ep,
所以USBH_CDC_InterfaceInit函数,调用了两次USBH_FindInterface函数,查找两个匹配的Interface, 分别进行配置

的原因。

但是,我们不需要在代码中找接口,在通信模块的资料和手册里面会有设备USB的接口(Interface)的说明,比如接口(Interface)的名称等。
我们自己可以选择使用哪个接口,过程如下:

  1. USBH_Process()函数中的case HOST_CHECK_CLASS:语句下面打断点停止。
    此时主机和设备已经完成了枚举的过程。
  2. 查看变量hUsbHostHS结构体,里面的接口描述符,看看该接口有几个端点,端点的方向和传输类型

选择的接口至少需要2个端点,这两个端点,1个是IN,1个是OUT。
(正常应该是3个,Communication Interface需要1个,Data Interface需要2个,一共3个。如果不使用Communication Interface,即CDC_HandleTypeDef中的CommItf,就只需要2个端点,用作Data Interface。)

3. 修改 USBH_CDC_ClassRequest 函数

点击查看代码
static USBH_StatusTypeDef USBH_CDC_ClassRequest(USBH_HandleTypeDef *phost)
{
  USBH_StatusTypeDef status = USBH_OK;
  phost->pUser(phost, HOST_USER_CLASS_ACTIVE);
  return status;
}

4. 添加 USBH_CDC_Receive() 函数开启接收

代码运行到 USBH_CDC_ClassRequest() 函数之后,
USBH_Process()函数中状态会变为 HOST_CLASS 并一直保持直到设备断开,
USBH_UserProcess() 函数中状态会变为 HOST_USER_CLASS_ACTIVE 并一直保持直到设备断开,
Appli_state = APPLICATION_READY 并一直保持直到设备断开。

在合适的位置添加 USBH_CDC_Receive() 函数开启接收。

5. 修改 USBH_CDC_ReceiveCallback() 函数

当USB接收到数据,程序会中断运行到 USBH_CDC_ReceiveCallback() 函数,在 USBH_CDC_ReceiveCallback() 函数中添加用户的缓存接收数据代码。

发送

使用 USBH_CDC_Transmit() 函数发送,
注意传入的指针参数必须是全局变量,不能是局部变量(应该静态局部变量也可以,没试过)。

结尾

有什么错误,欢迎指出。有什么问题,欢迎提问。
谢谢。

posted @ 2025-07-17 17:47  quanshimutou  阅读(825)  评论(0)    收藏  举报