STM32 USB Host库 CDC类 修改程序驱动4G/5G模块的总结
背景
使用STM32的USB接口与无线4G/5G模块通信,驱动无线4G/5G模块上网。
STM32的USB与无线模块通信,STM32做主机,类选择在CDC类,之后在STM32_USB_Host_Library的代码上修改。
参考资料
- 使用stm32cubemx的usb-host-cdc库驱动EC20模块
- 利用STM32CubeMX软件生成USB_HOST_CDC驱动ME909s-821ap(4G通信模块)
- USB协议分析
- USB 配置描述符
- 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做了下面几件事:
- 找通信接口(Communication Interface)
- 找通信接口下的端点0(endpoint),连接端点,作为NotifEp
- 打开管道(pipe)
- 找数据接口(DATA INTERFACE)
- 找数据接口下的端点,根据IN和OUT,连接端点,作为OutEp或InEp
- 打开管道(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)的名称等。
我们自己可以选择使用哪个接口,过程如下:
- 在
USBH_Process()函数中的case HOST_CHECK_CLASS:语句下面打断点停止。
此时主机和设备已经完成了枚举的过程。 - 查看变量
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() 函数发送,
注意传入的指针参数必须是全局变量,不能是局部变量(应该静态局部变量也可以,没试过)。
结尾
有什么错误,欢迎指出。有什么问题,欢迎提问。
谢谢。

浙公网安备 33010602011771号