STM32F4xx USB 库源码详解(custom HID)
首先我列出几个非常棒的参考文档:
Keil USB documentation
http://www.keil.com/pack/doc/mw/USB/html/index.html
USB_in_a_Nutshell
https://www.beyondlogic.org/usbnutshell/usb1.shtml
USB基础知识概论
https://www.crifan.com/files/doc/docbook/usb_basic/release/htmls/
http://www.crifan.com/files/doc/docbook/usb_basic/release/pdf/usb_basic.pdf.7z
一些常见的疑问
EndPoints到底有多少个
STM32F407/417/27/37/29/39等系列芯片,包括EP0在内的双向EP数目只有4个,而不是16个。这是要特别注意的地方,只不过程序为了其通用性,HAL_PCD_IRQHandler中对16个端点进行了轮询。
更具体详细的信息,建议参考《STM-AN4879》,
https://www.st.com/resource/en/application_note/dm00296349.pdf
传输方向的问题
这里特别列出来,是因为传输永远是相对HOST的,对于Device,IN是指输出到HOST(对HOST来说IN是输入,对DEVICE来说就是输出)OUT才是接收。
进入正题
然后我们从初始化开始讲解,源码库函数直接到ST.com上去下载,其中有关于customHID的例程,也可以到这里下载,
https://github.com/ErakhtinB/USB_Custom_HID
我试了下,这位老兄的代码能直接跑通(只有DM,DP两根线的那种USB,没有SOF,ID, VBUS, st.com的反而要改的地方更多),各个例程大同小异。重要的是,要结合手册和规范理解这些东西。
在HID源码中,初始化的代码如下,
/**
* Init USB device Library, add supported class and start the library
* @retval None
*/
void MX_USB_DEVICE_Init(void){
/* Init Device Library, add supported class and start the library. */
USBD_Init(&hUsbDeviceFS, &FS_Desc, DEVICE_FS);
USBD_RegisterClass(&hUsbDeviceFS, &USBD_CUSTOM_HID);
USBD_CUSTOM_HID_RegisterInterface(&hUsbDeviceFS, &USBD_CustomHID_fops_FS);
USBD_Start(&hUsbDeviceFS);
}
hUsbDeviceFS
该结构变量的类型为USBD_HandleTypeDef ,
/* USB Device Core handle declaration. */
USBD_HandleTypeDef hUsbDeviceFS;
注意这个是我们USB用到的全局变量,保存了USB生命周期的全部信息。
USBD_HandleTypeDef
/* USB Device handle structure */
typedef struct _USBD_HandleTypeDef
{
uint8_t id;
uint32_t dev_config;
uint32_t dev_default_config;
uint32_t dev_config_status;
USBD_SpeedTypeDef dev_speed;
USBD_EndpointTypeDef ep_in[15];
USBD_EndpointTypeDef ep_out[15];
uint32_t ep0_state;
uint32_t ep0_data_len;
uint8_t dev_state;
uint8_t dev_old_state;
uint8_t dev_address;
uint8_t dev_connection_status;
uint8_t dev_test_mode;
uint32_t dev_remote_wakeup;
USBD_SetupReqTypedef request;
USBD_DescriptorsTypeDef *pDesc;
USBD_ClassTypeDef *pClass;
void *pClassData;
void *pUserData;
void *pData;
} USBD_HandleTypeDef;
每个端点(ep_in/ep_out)都有自己的配置,类型为USBD_EndpointTypeDef,
USBD_EndpointTypeDef(ep_in/ep_out)
/* USB Device handle structure */
typedef struct
{
uint32_t status;
uint32_t is_used;
uint32_t total_length;
uint32_t rem_length;
uint32_t maxpacket;
} USBD_EndpointTypeDef;
USBD_CUSTOM_HID
该结构变量的类型是USBD_ClassTypeDef ,其定义如下,
/** @defgroup USBD_CUSTOM_HID_Private_Variables
* @{
*/
USBD_ClassTypeDef USBD_CUSTOM_HID =
{
USBD_CUSTOM_HID_Init,
USBD_CUSTOM_HID_DeInit,
USBD_CUSTOM_HID_Setup,
NULL, /*EP0_TxSent*/
USBD_CUSTOM_HID_EP0_RxReady, /*EP0_RxReady*/ /* STATUS STAGE IN */
USBD_CUSTOM_HID_DataIn, /*DataIn*/
USBD_CUSTOM_HID_DataOut,
NULL, /*SOF */
NULL,
NULL,
USBD_CUSTOM_HID_GetCfgDesc,
USBD_CUSTOM_HID_GetCfgDesc,
USBD_CUSTOM_HID_GetCfgDesc,
USBD_CUSTOM_HID_GetDeviceQualifierDesc,
};
注册路线
这个结构变量是通过
USBD_RegisterClass(&hUsbDeviceFS, &USBD_CUSTOM_HID);
注册到全局变量
USBD_HandleTypeDef hUsbDeviceFS;
中的,也就是经常调用的
hUsbDeviceFS->pClass (pdev->pClass)
USBD_ClassTypeDef
结构类型的定义为USBD_ClassTypeDef:
typedef struct _Device_cb
{
uint8_t (*Init) (struct _USBD_HandleTypeDef *pdev , uint8_t cfgidx);
uint8_t (*DeInit) (struct _USBD_HandleTypeDef *pdev , uint8_t cfgidx);
/* Control Endpoints*/
uint8_t (*Setup) (struct _USBD_HandleTypeDef *pdev , USBD_SetupReqTypedef *req);
uint8_t (*EP0_TxSent) (struct _USBD_HandleTypeDef *pdev );
uint8_t (*EP0_RxReady) (struct _USBD_HandleTypeDef *pdev );
/* Class Specific Endpoints*/
uint8_t (*DataIn) (struct _USBD_HandleTypeDef *pdev , uint8_t epnum);
uint8_t (*DataOut) (struct _USBD_HandleTypeDef *pdev , uint8_t epnum);
uint8_t (*SOF) (struct _USBD_HandleTypeDef *pdev);
uint8_t (*IsoINIncomplete) (struct _USBD_HandleTypeDef *pdev , uint8_t epnum);
uint8_t (*IsoOUTIncomplete) (struct _USBD_HandleTypeDef *pdev , uint8_t epnum);
uint8_t *(*GetHSConfigDescriptor)(uint16_t *length);
uint8_t *(*GetFSConfigDescriptor)(uint16_t *length);
uint8_t *(*GetOtherSpeedConfigDescriptor)(uint16_t *length);
uint8_t *(*GetDeviceQualifierDescriptor)(uint16_t *length);
#if (USBD_SUPPORT_USER_STRING == 1)
uint8_t *(*GetUsrStrDescriptor)(struct _USBD_HandleTypeDef *pdev ,uint8_t index, uint16_t *length);
#endif
} USBD_ClassTypeDef;
DataIn、DataOut – 数据传输函数
从这两个变量的定义可见,其输入输出函数对应起来就是
DataIn = USBD_CUSTOM_HID_DataIn
DataOut = USBD_CUSTOM_HID_DataOut
主要调用路线:
HAL_PCD_IRQHandler–>HAL_PCD_DataInStageCallback–>USBD_LL_DataInStage --> DataIn
HAL_PCD_IRQHandler–>HAL_PCD_DataOutStageCallback --> USBD_LL_DataOutStage–>DataOut
也可看到,其发送和接收,都是在中断函数HAL_PCD_IRQHandler中进行处理的。
hpcd_USB_OTG_FS
该结构变量的类型是
PCD_HandleTypeDef hpcd_USB_OTG_FS;
其中PCD就是硬件控制器的意思:peripheral controller device/driver。
注册路线
这个结构变量的注册路线是:
USBD_Init(&hUsbDeviceFS, &FS_Desc, DEVICE_FS);
==>
USBD_LL_Init(pdev); 这里pdev就是hUsbDeviceFS
其中,
hpcd_USB_OTG_FS.pData = pdev;
pdev->pData = &hpcd_USB_OTG_FS;
所以,
hpcd_USB_OTG_FS.pData = &hUsbDeviceFS;
hUsbDeviceFS->pData = &hpcd_USB_OTG_FS;
PCD_HandleTypeDef
usb_conf.c中定义的全局变量hpcd_USB_OTG_FS,该变量在USBD_LL_Init中完成初始化
/**
* @brief PCD Handle Structure definition
*/
typedef struct
{
PCD_TypeDef *Instance; /*!< Register base address */
PCD_InitTypeDef Init; /*!< PCD required parameters */
PCD_EPTypeDef IN_ep[16U]; /*!< IN endpoint parameters */
PCD_EPTypeDef OUT_ep[16U]; /*!< OUT endpoint parameters */
HAL_LockTypeDef Lock; /*!< PCD peripheral status */
__IO PCD_StateTypeDef State; /*!< PCD communication state */
uint32_t Setup[12U]; /*!< Setup packet buffer */
#ifdef USB_OTG_GLPMCFG_LPMEN
PCD_LPM_StateTypeDef LPM_State; /*!< LPM State */
uint32_t BESL;
uint32_t lpm_active; /*!< Enable or disable the Link Power Management .
This parameter can be set to ENABLE or DISABLE */
#endif /* USB_OTG_GLPMCFG_LPMEN */
#ifdef USB_OTG_GCCFG_BCDEN
uint32_t battery_charging_active; /*!< Enable or disable Battery charging.
This parameter can be set to ENABLE or DISABLE */
#endif /* USB_OTG_GCCFG_BCDEN */
void *pData; /*!< Pointer to upper stack Handler */
} PCD_HandleTypeDef;
相应地,每个端点IN_ep/OUT_ep也都有自己的配置,类型为PCD_EPTypeDef,
PCD_EPTypeDef (IN_ep/OUT_ep)
typedef USB_OTG_EPTypeDef PCD_EPTypeDef ;
/**
* @brief OTG End Point Initialization Structure definition
*/
typedef struct
{
uint8_t num; /*!< Endpoint number Min_Data = 1 and Max_Data = 15*/
uint8_t is_in; /*!< Endpoint direction Min_Data = 0 and Max_Data = 1*/
uint8_t is_stall; /*!< Endpoint stall condition Min_Data=0 and Max_Data=1*/
uint8_t type; /*!< Endpoint type any value of @ref USB_EP_Type_*/
uint8_t data_pid_start; /*!< Initial data PID Min_Data = 0 and Max_Data = 1*/
uint8_t even_odd_frame; /*!< IFrame parity Min_Data = 0 and Max_Data = 1*/
uint16_t tx_fifo_num; /*!< Transmission FIFO number Min_Data=1 and Max_Data=15*/
uint32_t maxpacket; /*!< Endpoint Max packet size Min_Data=0 and Max_Data=64KB*/
uint8_t *xfer_buff; /*!< Pointer to transfer buffer*/
uint32_t dma_addr; /*!< 32 bits aligned transfer buffer address*/
uint32_t xfer_len; /*!< Current transfer length*/
uint32_t xfer_count; /*!< Partial transfer length in multi packet transfer*/
}USB_OTG_EPTypeDef;
USBx数据传输中的几个概念
库中大量用到的USBx是指FS和HS传输方式配置寄存器的基地址USB_OTG_FS和USB_OTG_HS。
注意,STM32F4xx的寄存器地址USB_OTG_FS和USB_OTG_HS具体可参考STM32F4xx datasheet Table 10 register boundary addresses ,实际物理地址定义如下,
#define USB_OTG_HS_PERIPH_BASE 0x40040000U
#define USB_OTG_FS_PERIPH_BASE 0x50000000U
#define USB_OTG_FS ((USB_OTG_GlobalTypeDef *) USB_OTG_FS_PERIPH_BASE)
#define USB_OTG_HS ((USB_OTG_GlobalTypeDef *) USB_OTG_HS_PERIPH_BASE)
选择EndPoint并进行数据传输
USBx_INEP(i)
这里都以USB_OTG_FS为例进行讲解,
#define USBx_DEVICE ((USB_OTG_DeviceTypeDef *)((uint32_t )USBx + USB_OTG_DEVICE_BASE))
#define USBx_INEP(i) ((USB_OTG_INEndpointTypeDef *)((uint32_t)USBx + USB_OTG_IN_ENDPOINT_BASE + (i)*USB_OTG_EP_REG_SIZE))
#define USBx_OUTEP(i) ((USB_OTG_OUTEndpointTypeDef *)((uint32_t)USBx + USB_OTG_OUT_ENDPOINT_BASE + (i)*USB_OTG_EP_REG_SIZE))
#define USBx_DFIFO(i) *(__IO uint32_t *)((uint32_t)USBx + USB_OTG_FIFO_BASE + (i) * USB_OTG_FIFO_SIZE)
以USBx_INEP(i)为例,在FS模式下,USBx是指
#define USB_OTG_FS_PERIPH_BASE 0x50000000U
#define USB_OTG_IN_ENDPOINT_BASE 0x900U
#define USB_OTG_EP_REG_SIZE 0x20U
所以USBx_INEP(0)就是OTG_FS_DIEPCTL0,0x50000900
USBx_OUTEP(i)
同样,我们计算一下USBx_OUTEP到底是指哪一个,假设这里x=3,
#define USB_OTG_FS_PERIPH_BASE 0x50000000U // 这个就是USBx
#define USB_OTG_OUT_ENDPOINT_BASE 0xB00U
#define USB_OTG_EP_REG_SIZE 0x20U
#define USBx_OUTEP(i) ((USB_OTG_OUTEndpointTypeDef *)((uint32_t)USBx + USB_OTG_OUT_ENDPOINT_BASE + (i)*USB_OTG_EP_REG_SIZE))
因此,USBx_OUTEP(15) = 0x50000B00U + 3*0x20
在接收发送中断函数HAL_PCD_IRQHandler中,先要通过
USB_ReadDevOutEPInterrupt(或USB_ReadDevInEPInterrupt)
确定中断到底来自哪一个EndPoint(共计16个),
USB_ReadDevOutEPInterrupt或USB_ReadDevInEPInterrupt进行确认的代码如下,
epint = USB_ReadDevOutEPInterrupt(hpcd->Instance, epnum);
epint = USB_ReadDevInEPInterrupt(hpcd->Instance, epnum);
具体实现代码如下,
/**
* @brief Returns Device OUT EP Interrupt register
* @param USBx Selected device
* @param epnum endpoint number
* This parameter can be a value from 0 to 15
* @retval Device OUT EP Interrupt register
*/
uint32_t USB_ReadDevOutEPInterrupt (USB_OTG_GlobalTypeDef *USBx , uint8_t epnum)
{
uint32_t v;
v = USBx_OUTEP(epnum)->DOEPINT;
v &= USBx_DEVICE->DOEPMSK;
return v;
}
/**
* @brief Returns Device IN EP Interrupt register
* @param USBx Selected device
* @param epnum endpoint number
* This parameter can be a value from 0 to 15
* @retval Device IN EP Interrupt register
*/
uint32_t USB_ReadDevInEPInterrupt (USB_OTG_GlobalTypeDef *USBx , uint8_t epnum)
{
uint32_t v, msk, emp;
msk = USBx_DEVICE->DIEPMSK;
emp = USBx_DEVICE->DIEPEMPMSK;
msk |= ((emp >> epnum) & 0x1U) << 7U;
v = USBx_INEP(epnum)->DIEPINT & msk;
return v;
}
传输设定
调用路线
传输模式与数据量是在USB初始化的时候设定的,其函数调用路线如下,
OTG_FS_IRQHandler --> HAL_PCD_IRQHandler --> HAL_PCD_SetupStageCallback --> USBD_LL_SetupStage --> USBD_StdDevReq --> USBD_SetConfig --> USBD_SetClassConfig --> USBD_CUSTOM_HID_Init --> USBD_LL_OpenEP --> HAL_PCD_EP_Open
要注意:USBD_SetClassConfig中的这个pClass前面讲过,if(pdev->pClass->Init(pdev, cfgidx)调用的就是USBD_CUSTOM_HID_Init 。
数据传输大小
USBD_LL_OpenEP设定了端点号如CUSTOM_HID_EPIN_ADDR,传输模式如USBD_EP_TYPE_INTR数据传输量的大小如USB_FS_MAX_PACKET_SIZE(自定义)。另外注意,在VCP/CDC模式下,传输模式往往被设定为USBD_EP_TYPE_BULK。
USBD_CUSTOM_HID_Init
static uint8_t USBD_CUSTOM_HID_Init (USBD_HandleTypeDef *pdev,
uint8_t cfgidx)
{
uint8_t ret = 0;
USBD_CUSTOM_HID_HandleTypeDef *hhid;
/* Open EP IN */
USBD_LL_OpenEP(pdev,
CUSTOM_HID_EPIN_ADDR,
USBD_EP_TYPE_INTR,
CUSTOM_HID_EPIN_SIZE); // USB_FS_MAX_PACKET_SIZE
/* Open EP OUT */
USBD_LL_OpenEP(pdev,
CUSTOM_HID_EPOUT_ADDR,
USBD_EP_TYPE_INTR,
CUSTOM_HID_EPOUT_SIZE); // USB_FS_MAX_PACKET_SIZE
pdev->pClassData = USBD_malloc(sizeof (USBD_CUSTOM_HID_HandleTypeDef));
if(pdev->pClassData == NULL)
{
ret = 1;
}
else
{
hhid = (USBD_CUSTOM_HID_HandleTypeDef*) pdev->pClassData;
hhid->state = CUSTOM_HID_IDLE;
((USBD_CUSTOM_HID_ItfTypeDef *)pdev->pUserData)->Init();
/* Prepare Out endpoint to receive 1st packet */
USBD_LL_PrepareReceive(pdev, CUSTOM_HID_EPOUT_ADDR, hhid->Report_buf,
USBD_CUSTOMHID_OUTREPORT_BUF_SIZE);
}
return ret;
}
这个USBD_LL_OpenEP函数中,有用的只有一句,
hal_status = HAL_PCD_EP_Open(pdev->pData, ep_addr, ep_mps, ep_type);
所以真正干活的只有HAL_PCD_EP_Open函数。
HAL_PCD_EP_Open
具体的设定由HAL_PCD_EP_Open负责,其中PCD_HandleTypeDef中的IN_ep/OUT_ep中的maxpacket就是最大允许传输量,mps就是max packet size 的意思,
注意:STM原来的例程中,ep->maxpacket = CUSTOM_HID_EPIN_SIZE = 2k; 也就是每个packet最大数据量为2k。
HAL_StatusTypeDef HAL_PCD_EP_Open(PCD_HandleTypeDef* hpcd, uint8_t ep_addr, uint16_t ep_mps, uint8_t ep_type)
{
HAL_StatusTypeDef ret = HAL_OK;
USB_OTG_EPTypeDef* ep;
if ((ep_addr & 0x80) == 0x80){
ep = &hpcd->IN_ep[ep_addr & 0x7F];
}
else{
ep = &hpcd->OUT_ep[ep_addr & 0x7F];
}
ep->num = ep_addr & 0x7F;
ep->is_in = (0x80 & ep_addr) != 0;
ep->maxpacket = ep_mps;
ep->type = ep_type;
if (ep->is_in){
/* Assign a Tx FIFO */
ep->tx_fifo_num = ep->num;
}
/* Set initial data PID. */
if (ep_type == EP_TYPE_BULK){
ep->data_pid_start = 0U;
}
__HAL_LOCK(hpcd);
USB_ActivateEndpoint(hpcd->Instance, ep);
__HAL_UNLOCK(hpcd);
return ret;
}
传输函数
USBD_CUSTOM_HID_DataIn
这里再次提醒,IN相对于Device来说,就是发送的意思。前面讲到,发送函数是USBD_CUSTOM_HID_DataIn,
static uint8_t USBD_CUSTOM_HID_DataIn (USBD_HandleTypeDef *pdev, uint8_t epnum){
/* Ensure that the FIFO is empty before a new transfer, this condition could
be caused by a new transfer before the end of the previous transfer */
((USBD_CUSTOM_HID_HandleTypeDef *)pdev->pClassData)->state = CUSTOM_HID_IDLE;
return USBD_OK;
}
但是,注意这个USBD_CUSTOM_HID_DataIn貌似什么都没做,到这里再思考一下:那么数据是放在哪里,又是怎么发送出去了呢?
其实,这个函数是中断HAL_PCD_IRQHandler发送过程中调用的,我们后面会看到
真实的发送路线(准备数据阶段)
真实的发送路线如下,
USBD_CUSTOM_HID_SendReport -->USBD_LL_Transmit–>HAL_PCD_EP_Transmit -->USB_EPStartXfer
先看一下真正的发送函数(该函数是用户直接在有需要发送数据到上位机的地方调用的函数),
USBD_CUSTOM_HID_SendReport
uint8_t USBD_CUSTOM_HID_SendReport(USBD_HandleTypeDef *pdev,
uint8_t *report,
uint16_t len)
{
USBD_CUSTOM_HID_HandleTypeDef* hhid=(USBD_CUSTOM_HID_HandleTypeDef*)pdev->pClassData;
if (pdev->dev_state == USBD_STATE_CONFIGURED ){
if(hhid->state == CUSTOM_HID_IDLE){
hhid->state = CUSTOM_HID_BUSY;
USBD_LL_Transmit (pdev, CUSTOM_HID_EPIN_ADDR, report, len);
}
}
return USBD_OK;
}
其调用的这个USBD_LL_Transmit函数,有用的也只有一句
hal_status = HAL_PCD_EP_Transmit(pdev->pData, ep_addr, pbuf, size);
HAL_PCD_EP_Transmit
HAL_StatusTypeDef HAL_PCD_EP_Transmit(PCD_HandleTypeDef* hpcd, uint8_t ep_addr, uint8_t* pBuf, uint32_t len){
USB_OTG_EPTypeDef* ep;
ep = &hpcd->IN_ep[ep_addr & 0x7F];
/*setup and start the Xfer */
ep->xfer_buff = pBuf;
ep->xfer_len = len;
ep->xfer_count = 0U;
ep->is_in = 1U;
ep->num = ep_addr & 0x7F;
if (hpcd->Init.dma_enable == 1U){
ep->dma_addr = (uint32_t)pBuf;
}
if ((ep_addr & 0x7F) == 0){
USB_EP0StartXfer(hpcd->Instance, ep, hpcd->Init.dma_enable);
}
else{
USB_EPStartXfer(hpcd->Instance, ep, hpcd->Init.dma_enable);
}
return HAL_OK;
}
USB_EPStartXfer
HAL_StatusTypeDef USB_EPStartXfer(USB_OTG_GlobalTypeDef *USBx , USB_OTG_EPTypeDef *ep, uint8_t dma){
uint16_t pktcnt = 0U;
/* IN endpoint */
if (ep->is_in == 1U){
/* Zero Length Packet? */
if (ep->xfer_len == 0U){
USBx_INEP(ep->num)->DIEPTSIZ &= ~(USB_OTG_DIEPTSIZ_PKTCNT);
USBx_INEP(ep->num)->DIEPTSIZ |= (USB_OTG_DIEPTSIZ_PKTCNT & (1U << 19U)) ;
USBx_INEP(ep->num)->DIEPTSIZ &= ~(USB_OTG_DIEPTSIZ_XFRSIZ);
}
else{
/* Program the transfer size and packet count
* as follows: xfersize = N * maxpacket +
* short_packet pktcnt = N + (short_packet
* exist ? 1 : 0)
*/
USBx_INEP(ep->num)->DIEPTSIZ &= ~(USB_OTG_DIEPTSIZ_XFRSIZ);
USBx_INEP(ep->num)->DIEPTSIZ &= ~(USB_OTG_DIEPTSIZ_PKTCNT);
USBx_INEP(ep->num)->DIEPTSIZ |= (USB_OTG_DIEPTSIZ_PKTCNT & (((ep->xfer_len + ep->maxpacket -1U)/ ep->maxpacket) << 19U)) ;
USBx_INEP(ep->num)->DIEPTSIZ |= (USB_OTG_DIEPTSIZ_XFRSIZ & ep->xfer_len);
if (ep->type == EP_TYPE_ISOC)
{
USBx_INEP(ep->num)->DIEPTSIZ &= ~(USB_OTG_DIEPTSIZ_MULCNT);
USBx_INEP(ep->num)->DIEPTSIZ |= (USB_OTG_DIEPTSIZ_MULCNT & (1U << 29U));
}
}
if (dma == 1U){
USBx_INEP(ep->num)->DIEPDMA = (uint32_t)(ep->dma_addr);
}
else{
if (ep->type != EP_TYPE_ISOC){
/* Enable the Tx FIFO Empty Interrupt for this EP */
if (ep->xfer_len > 0U){
USBx_DEVICE->DIEPEMPMSK |= 1U << ep->num;
}
}
}
if (ep->type == EP_TYPE_ISOC){
if ((USBx_DEVICE->DSTS & ( 1U << 8U )) == 0U){
USBx_INEP(ep->num)->DIEPCTL |= USB_OTG_DIEPCTL_SODDFRM;
}
else{
USBx_INEP(ep->num)->DIEPCTL |= USB_OTG_DIEPCTL_SD0PID_SEVNFRM;
}
}
/* EP enable, IN data in FIFO */
USBx_INEP(ep->num)->DIEPCTL |= (USB_OTG_DIEPCTL_CNAK | USB_OTG_DIEPCTL_EPENA);
if (ep->type == EP_TYPE_ISOC){
USB_WritePacket(USBx, ep->xfer_buff, ep->num, ep->xfer_len, dma);
}
}
else {// OUT endpoint
/* Program the transfer size and packet count as follows:
* pktcnt = N
* xfersize = N * maxpacket
*/
USBx_OUTEP(ep->num)->DOEPTSIZ &= ~(USB_OTG_DOEPTSIZ_XFRSIZ);
USBx_OUTEP(ep->num)->DOEPTSIZ &= ~(USB_OTG_DOEPTSIZ_PKTCNT);
if (ep->xfer_len == 0U){
USBx_OUTEP(ep->num)->DOEPTSIZ |= (USB_OTG_DOEPTSIZ_XFRSIZ & ep->maxpacket);
USBx_OUTEP(ep->num)->DOEPTSIZ |= (USB_OTG_DOEPTSIZ_PKTCNT & (1U << 19U));
}
else{
pktcnt = (ep->xfer_len + ep->maxpacket -1U)/ ep->maxpacket;
USBx_OUTEP(ep->num)->DOEPTSIZ |= (USB_OTG_DOEPTSIZ_PKTCNT & (pktcnt << 19U));
USBx_OUTEP(ep->num)->DOEPTSIZ |= (USB_OTG_DOEPTSIZ_XFRSIZ & (ep->maxpacket * pktcnt));
}
if (dma == 1U){
USBx_OUTEP(ep->num)->DOEPDMA = (uint32_t)ep->xfer_buff;
}
if (ep->type == EP_TYPE_ISOC){
if ((USBx_DEVICE->DSTS & ( 1U << 8U )) == 0U){
USBx_OUTEP(ep->num)->DOEPCTL |= USB_OTG_DOEPCTL_SODDFRM;
}
else{
USBx_OUTEP(ep->num)->DOEPCTL |= USB_OTG_DOEPCTL_SD0PID_SEVNFRM;
}
}
/* EP enable */
USBx_OUTEP(ep->num)->DOEPCTL |= (USB_OTG_DOEPCTL_CNAK | USB_OTG_DOEPCTL_EPENA);
}
return HAL_OK;
}
该函数中,主要对USB_OTG_DOEPTSIZ_XFRSIZ和USB_OTG_DOEPTSIZ_PKTCNT进行了设置,
USBx_INEP(ep->num)->DIEPTSIZ |= (USB_OTG_DIEPTSIZ_PKTCNT & (((ep->xfer_len + ep->maxpacket -1U)/ ep->maxpacket) << 19U)) ;
以前面的例子,maxpacket = CUSTOM_HID_EPIN_SIZE = 2,表示可传输最大2K,例程中xfer_len = 5 = XFRSIZ 个字节,
PKCNT = (ep->xfer_len + ep->maxpacket -1U) / ep->maxpacket = 6/2 = 3
最后调用中断
USBx_INEP(ep->num)->DIEPCTL |= (USB_OTG_DIEPCTL_CNAK | USB_OTG_DIEPCTL_EPENA);
将数据发送出去,需要理解的是,具体什么时候发送是用户控制不了的,用抓包工具就可以看出来,这是和主机的具体通信情况相关的。
发送传输过程(采用中断发送阶段)
Step-by-step
HAL_PCD_IRQHandler(&hpcd_USB_OTG_FS);
==>
HAL_PCD_DataInStageCallback(hpcd, epnum);
这里,hpcd就是hpcd_USB_OTG_FS,
==>
USBD_LL_DataInStage((USBD_HandleTypeDef*)hpcd->pData, epnum, hpcd->IN_ep[epnum].xfer_buff);
这相当于
USBD_LL_DataInStage(&hUsbDeviceFS, epnum, hpcd_USB_OTG_FS.IN_ep[epnum].xfer_buff);
其中有
pdev->pClass->DataIn(pdev, epnum);
==>
USBD_CUSTOM_HID_DataIn(&hUsbDeviceFS, epnum);
到此为止,一切准备就绪,然后,中断函数HAL_PCD_IRQHandler会调用PCD_WriteEmptyTxFifo进行数据发送,
if ((epint & USB_OTG_DIEPINT_TXFE) == USB_OTG_DIEPINT_TXFE){
PCD_WriteEmptyTxFifo(hpcd, epnum);
}
而其中PCD_WriteEmptyTxFifo最后调用的,是USB_WritePacket函数。
==>
最后,
USB_WritePacket(USBx, ep->xfer_buff, epnum, len, hpcd->Init.dma_enable);
把结果发送出去。
PCD_WriteEmptyTxFifo
特别注意,这里len32b是对32位对齐进行处理,在参考手册34.16 OTG_FS control and status registers中,对所有寄存器,包括DFIFO,都要求 32位对齐(32-bit block aligned)。
static HAL_StatusTypeDef PCD_WriteEmptyTxFifo(PCD_HandleTypeDef* hpcd, uint32_t epnum)
{
USB_OTG_GlobalTypeDef* USBx = hpcd->Instance;
USB_OTG_EPTypeDef* ep;
int32_t len = 0U;
uint32_t len32b;
uint32_t fifoemptymsk = 0U;
ep = &hpcd->IN_ep[epnum];
//假设我有5个字节要发送,那么这里xfer_len=5,xfer_count=0表示还没有数据写到FIFO中,len = 5;
len = ep->xfer_len - ep->xfer_count;
// 这里,我们已经设置了maxpacket=2
if (len > ep->maxpacket){
len = ep->maxpacket;
}
// 这个是发送的辅助变量,意思是32bit长的数据能有多少个。也就是对齐到32bit(4个BYTE)的意思。
// 注意,为了避免负数,这里都用的是uint32_t类型
// 参考:https://github.com/ARMmbed/mbed-os/pull/6609
// 这里的计算结果是 len32b = (5+3)/4 = 2
len32b = (len + 3U) / 4U;
// ep->xfer_count < ep->xfer_len 表示已经发送的字节数 < 全部需要发送的字节数
while (((USBx_INEP(epnum)->DTXFSTS & USB_OTG_DTXFSTS_INEPTFSAV) > len32b) &&
(ep->xfer_count < ep->xfer_len) &&
(ep->xfer_len != 0U))
{
/* Write the FIFO */
len = ep->xfer_len - ep->xfer_count; // 还有多少个字节没有发送
if (len > ep->maxpacket){ // maxpacket这里是2,或64
len = ep->maxpacket; // 所以这里len是2 (如果maxpacket=64,len就是5)
}
len32b = (len + 3U) / 4U; // 剩下的还能凑成多少个4字节
// 发送一个len长度的字节数
USB_WritePacket(USBx, ep->xfer_buff, epnum, len, hpcd->Init.dma_enable);
ep->xfer_buff += len;
ep->xfer_count += len;
}
if (len <= 0U){ // 送来无意义的0(即没有字节要发送)所以屏蔽掉这个中断即可
fifoemptymsk = 0x1U << epnum;
USBx_DEVICE->DIEPEMPMSK &= ~fifoemptymsk;
}
return HAL_OK;
}
最后的发送 – USB_WritePacket
和前面的函数一样,这里也同样对32位对齐进行了处理,每次USBx_DFIFO发送4个BYTE的数据,后面不足4个Byte的也凑齐4个BYTE发送出去。从PCD_WriteEmptyTxFifo和USB_WritePacket这两个函数可以看出,maxpacket设置得越大,发送速度就越快,因为可以省掉很多数据大小检测的时间。
另外一点,一次发送的数据量和maxpacket没有关系(实际上能否正确发送与接收,与HID Report Descriptor的关系更大),maxpacket的作用仅仅是检测每次往DFIFO写入的数据大小,没发完的还是照样会一个劲地发,直到发完。至于写到FIFO里后,芯片什么时候发,发送多少,这些都和硬件的设计有关,也是用户控制不了的。
本质上,DFIFO中还能一次性写多少数据是由USB_OTG_DTXFSTS_INEPTFSAV决定的,所以程序中要不停地对其进行检测,while (((USBx_INEP(epnum)->DTXFSTS & USB_OTG_DTXFSTS_INEPTFSAV) > len32b)
,
HAL_StatusTypeDef USB_WritePacket(USB_OTG_GlobalTypeDef *USBx, uint8_t *src, uint8_t ch_ep_num, uint16_t len, uint8_t dma)
{
uint32_t count32b = 0U , i = 0U;
if (dma == 0U){
count32b = (len + 3U) / 4U;
for (i = 0U; i < count32b; i++, src += 4U){
USBx_DFIFO(ch_ep_num) = *((__packed uint32_t *)src);
}
}
return HAL_OK;
}
USBx_DFIFO(i) – 数据传输寄存器
同样,我们计算一下USBx_DFIFO到底是指哪一个,假设这里i=1,
#define USB_OTG_FS_PERIPH_BASE 0x50000000U // 这个就是USBx
#define USB_OTG_FIFO_BASE 0x1000U
#define USB_OTG_FIFO_SIZE 0x1000U
#define USBx_DFIFO(i) *(__IO uint32_t *)((uint32_t)USBx + USB_OTG_FIFO_BASE + (i) * USB_OTG_FIFO_SIZE)
因此,USBx_DFIFO(1) = 0x50001000U + 1*0x1000
这个,正是手册34.6.1中讲到的数据传输寄存器Data FIFO (DFIFO) access register map。
接收函数
USBD_CUSTOM_HID_DataOut
接收相对简单一些,就不再细述。源码也很容易懂。
static uint8_t USBD_CUSTOM_HID_DataOut (USBD_HandleTypeDef *pdev, uint8_t epnum){
USBD_CUSTOM_HID_HandleTypeDef *hhid = (USBD_CUSTOM_HID_HandleTypeDef*)pdev->pClassData; ((USBD_CUSTOM_HID_ItfTypeDef *)pdev->pUserData)->OutEvent(hhid->Report_buf[0],
hhid->Report_buf[1]);
USBD_LL_PrepareReceive(pdev, CUSTOM_HID_EPOUT_ADDR , hhid->Report_buf,
USBD_CUSTOMHID_OUTREPORT_BUF_SIZE);
return USBD_OK;
}
理解配置字Descriptors
USBD_FS_DeviceDesc
<usbd_desc.c>
/** SPEC2.0 Tab.9-8 USB standard device descriptor. */
__ALIGN_BEGIN uint8_t USBD_FS_DeviceDesc[USB_LEN_DEV_DESC] __ALIGN_END =
{
0x12, /*bLength */
USB_DESC_TYPE_DEVICE, /*bDescriptorType SPEC2.0 Tab.9-5 */
#if (USBD_LPM_ENABLED == 1)
0x01, /*bcdUSB */ /* changed to USB version 2.01
in order to support LPM L1 suspend
resume test of USBCV3.0*/
#else
0x00, /*bcdUSB bcd coded version number */
#endif /* (USBD_LPM_ENABLED == 1) */
0x02, /* bcdUSB = 0x0200*/
0x00, /*bDeviceClass, ref. https://www.usb.org/defined-class-codes */
0x00, /*bDeviceSubClass, as above*/
0x00, /*bDeviceProtocol, as above*/
USB_MAX_EP0_SIZE, /*bMaxPacketSize*/
LOBYTE(USBD_VID), /*idVendor*/
HIBYTE(USBD_VID), /*idVendor*/
LOBYTE(USBD_PID_FS), /*idProduct*/
HIBYTE(USBD_PID_FS), /*idProduct*/
0x00, /*bcdDevice rel. 2.00*/
0x02,
USBD_IDX_MFC_STR, /*Index of manufacturer string*/
USBD_IDX_PRODUCT_STR, /*Index of product string*/
USBD_IDX_SERIAL_STR, /*Index of serial number string*/
USBD_MAX_NUM_CONFIGURATION /*bNumConfigurations*/
};
CUSTOM_HID_ReportDesc_FS
report descriptor 的详解解释可参考:
https://eleccelerator.com/tutorial-about-usb-hid-report-descriptors/
官方USB-HID文档与解析工具可参考:
https://www.usb.org/hid
https://usb.org/sites/default/files/documents/hid1_11.pdf
https://www.usb.org/sites/default/files/documents/dt2_4.zip
注意事项:
- 每个transaction中,对Full speed最大packet只有64字节,Low speed最大8个字节;每毫秒(per millisecond)发起一个transaction,所以每秒传输字节数,FullSpeed大约是64K, LowSpeed大约是8K。
- 只有Input才可以由Interrupt In管道发送,Output和Feature则必须由Host通过控制管道发起。(HID1.11chapter5.6)
- 如果在报告描述符中使用了 REPORT_ID 则 USB 发送数据缓冲区第一个字节必须为 REPORT_ID 以告知Windows/Linux系统该数据属于哪个 ID,否则系统无法正常接收数据;
- 下位机在上传数据时要按照报告描述符中规定的字节个数进行传输。假如已经规定为64字节,下位机某次只有10个字节要发送给主机,那么实际发送的缓冲区中的字节数也应该是64个字节,不能只发10个字节。至于为了补全这64字节,后续到底是什么内容这些都不重要。只有这样上位机Readfile时才能正确收到数据。
源码,
/** Usb HID report descriptor. */
__ALIGN_BEGIN static uint8_t CUSTOM_HID_ReportDesc_FS[USBD_CUSTOM_HID_REPORT_DESC_SIZE] __ALIGN_END =
{
// 7BYTES
0x06, 0x00, 0xFF, // Usage Page (Vendor Defined 0xFF00)
0x09, 0x01, // Usage (0x01)
0xA1, 0x01, // Collection (Application)
// LED1 20BYTES
0x85, 0x01, // Report ID (1)
0x09, 0x01, // Usage (0x01)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x75, 0x08, // Report Size (8)
0x95, 0x01, // Report Count (1)
0xB1, 0x82, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Volatile)
0x85, 0x01, // Report ID (1)
0x09, 0x01, // Usage (0x01)
0x91, 0x82, // Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Volatile)
// LED2 20BYTES
0x85, 0x02, // Report ID (2)
0x09, 0x02, // Usage (0x02)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x75, 0x08, // Report Size (8)
0x95, 0x01, // Report Count (1)
0xB1, 0x82, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Volatile)
0x85, 0x02, // Report ID (2)
0x09, 0x02, // Usage (0x02)
0x91, 0x82, // Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Volatile)
// LED3 20BYTES
0x85, 0x03, // Report ID (3)
0x09, 0x03, // Usage (0x03)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x75, 0x08, // Report Size (8)
0x95, 0x01, // Report Count (1)
0xB1, 0x82, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Volatile)
0x85, 0x03, // Report ID (3)
0x09, 0x03, // Usage (0x03)
0x91, 0x82, // Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Volatile)
// KEY1 20BYTES
0x85, 0x04, // Report ID (4)
0x09, 0x03, // Usage (0x04)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x75, 0x08, // Report Size (8)
0x95, 0x01, // Report Count (1)
0xB1, 0x82, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Volatile)
0x85, 0x04, // Report ID (4)
0x09, 0x03, // Usage (0x04)
0x81, 0x82, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
// 15BYTES
0x85, 0x05, // Report ID (5)
0x09, 0x05, // Usage (0x05)
0x15, 0x00, // Logical Minimum (0)
0x26, 0xFF, 0x00, // Logical Maximum (255)
0x75, 0x08, // Report Size (8)
0x95, 0x3F, // Report Count (63)
0x91, 0x82, // Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Volatile)
// 15BYTES
0x85, 0x06, // Report ID (6)
0x09, 0x05, // Usage (0x06)
0x15, 0x00, // Logical Minimum (0)
0x26, 0xFF, 0x00, // Logical Maximum (255)
0x75, 0x08, // Report Size (8)
0x95, 0x3F, // Report Count (63)
0x81, 0x82, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
// 15BYTES
0x85, 0x07, // Report ID (7)
0x09, 0x06, // Usage (0x07)
0x15, 0x00, // Logical Minimum (0)
0x26, 0xFF, 0x00, // Logical Maximum (255)
0x75, 0x08, // Report Size (8)
0x95, 0x3F, // Report Count (63)
0x81, 0x82, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
// 1BYTES
0xC0, // End Collection
// 133 bytes
};
在上面这个源码中,我们配置了3个LED,即通过上位机(USB HID Demonstrator, ST官网提供)控制,另外定义了一个开发板上按键,然后定义了3个63(包括report_ID,实际有64个)字节的通信通道。
在Windows中是通过下面这个结构获取这些信息的,比如有多少个通道(不知道怎么翻译好,英文是capabilities),最大通信字节数等。
typedef struct _HIDP_CAPS {
USAGE UsagePage;
USAGE Usage;
USHORT InputReportByteLength;
USHORT OutputReportByteLength;
USHORT FeatureReportByteLength;
USHORT Reserved[17];
USHORT NumberLinkCollectionNodes;
USHORT NumberInputButtonCaps;
USHORT NumberInputValueCaps;
USHORT NumberInputDataIndices;
USHORT NumberOutputButtonCaps;
USHORT NumberOutputValueCaps;
USHORT NumberOutputDataIndices;
USHORT NumberFeatureButtonCaps;
USHORT NumberFeatureValueCaps;
USHORT NumberFeatureDataIndices;
} HIDP_CAPS, *PHIDP_CAPS;
后面的基本上没有难度,估且贴一下源码吧!
USBD_CUSTOM_HID_CfgDesc
<usbd_customhid.h>
#define USB_CUSTOM_HID_CONFIG_DESC_SIZ 41
#define USB_CUSTOM_HID_DESC_SIZ 9
#define CUSTOM_HID_DESCRIPTOR_TYPE 0x21
#define CUSTOM_HID_REPORT_DESC 0x22
SPEC2.0 Tab9-5
// spec Tab.9-5
#define USB_DESC_TYPE_DEVICE 1
#define USB_DESC_TYPE_CONFIGURATION 2
#define USB_DESC_TYPE_STRING 3
#define USB_DESC_TYPE_INTERFACE 4
#define USB_DESC_TYPE_ENDPOINT 5
#define USB_DESC_TYPE_DEVICE_QUALIFIER 6
#define USB_DESC_TYPE_OTHER_SPEED_CONFIGURATION 7
#define USB_DESC_TYPE_BOS 0x0F
<usbd_customhid.c>
/* USB CUSTOM_HID device Configuration Descriptor */
__ALIGN_BEGIN static uint8_t USBD_CUSTOM_HID_CfgDesc[USB_CUSTOM_HID_CONFIG_DESC_SIZ] __ALIGN_END =
{
0x09, /* bLength: Configuration Descriptor size */
USB_DESC_TYPE_CONFIGURATION, /* bDescriptorType: Configuration SPEC2.0 Tab.9-5 */
USB_CUSTOM_HID_CONFIG_DESC_SIZ, /* wTotalLength: Bytes returned */
0x00,
0x01, /*bNumInterfaces: 1 interface*/
0x01, /*bConfigurationValue: Configuration value*/
0x00, /*iConfiguration: Index of string descriptor describing the configuration*/
0xC0, /*bmAttributes: bus powered */
0x32, /*MaxPower 100 mA: this current is used for detecting Vbus*/
/************** Descriptor of CUSTOM HID interface ****************/
/* 09 SPEC2.0 Tab9-12, interface descriptor */
0x09, /*bLength: Interface Descriptor size*/
USB_DESC_TYPE_INTERFACE,/*bDescriptorType: Interface descriptor type */
0x00, /*bInterfaceNumber: Number of Interface*/
0x00, /*bAlternateSetting: Alternate setting*/
0x02, /*bNumEndpoints*/
0x03, /*bInterfaceClass: CUSTOM_HID*/
//re. https://www.usb.org/defined-class-codes, INTERFACE CLASS = 03
0x00, /*bInterfaceSubClass : 1=BOOT, 0=no boot*/
0x00, /*nInterfaceProtocol : 0=none, 1=keyboard, 2=mouse*/
0, /*iInterface: Index of string descriptor*/
/******************** Descriptor of CUSTOM_HID *************************/
/* 18 */
0x09, /*bLength: CUSTOM_HID Descriptor size*/
CUSTOM_HID_DESCRIPTOR_TYPE, /*bDescriptorType: CUSTOM_HID*/
0x11, /*bCUSTOM_HIDUSTOM_HID: CUSTOM_HID Class Spec release number*/
0x01,
0x00, /*bCountryCode: Hardware target country*/
0x01, /*bNumDescriptors: Number of CUSTOM_HID class descriptors to follow*/
0x22, /*bDescriptorType*/
USBD_CUSTOM_HID_REPORT_DESC_SIZE,/*wItemLength: Total length of Report descriptor*/
0x00,
/******************** Descriptor of Custom HID endpoints ********************/
/* 27 */
0x07, /*bLength: Endpoint Descriptor size*/
USB_DESC_TYPE_ENDPOINT, /*bDescriptorType:*/
CUSTOM_HID_EPIN_ADDR, /*bEndpointAddress: Endpoint Address (IN)*/
0x03, /*bmAttributes: Interrupt endpoint*/
CUSTOM_HID_EPIN_SIZE, /*wMaxPacketSize: 2 Byte max */
0x00,
0x20, /*bInterval: Polling Interval (20 ms)*/
/* 34 */
0x07, /* bLength: Endpoint Descriptor size */
USB_DESC_TYPE_ENDPOINT, /* bDescriptorType: */
CUSTOM_HID_EPOUT_ADDR, /*bEndpointAddress: Endpoint Address (OUT)*/
0x03, /* bmAttributes: Interrupt endpoint */
CUSTOM_HID_EPOUT_SIZE, /* wMaxPacketSize: 2 Bytes max */
0x00,
0x20, /* bInterval: Polling Interval (20 ms) */
/* 41 */
} ;
其回调函数为,
static uint8_t *USBD_CUSTOM_HID_GetCfgDesc (uint16_t *length)
{
*length = sizeof (USBD_CUSTOM_HID_CfgDesc);
return USBD_CUSTOM_HID_CfgDesc;
}
USBD_CUSTOM_HID_Desc
<usbd_customhid.c>
/* USB CUSTOM_HID device Configuration Descriptor */
__ALIGN_BEGIN static uint8_t USBD_CUSTOM_HID_Desc[USB_CUSTOM_HID_DESC_SIZ] __ALIGN_END =
{
/* 18 */
0x09, /*bLength: CUSTOM_HID Descriptor size*/
CUSTOM_HID_DESCRIPTOR_TYPE, /*bDescriptorType: CUSTOM_HID*/
0x11, /*bCUSTOM_HIDUSTOM_HID: CUSTOM_HID Class Spec release number*/
0x01,
0x00, /*bCountryCode: Hardware target country*/
0x01, /*bNumDescriptors: Number of CUSTOM_HID class descriptors to follow*/
0x22, /*bDescriptorType*/
USBD_CUSTOM_HID_REPORT_DESC_SIZE,/*wItemLength: Total length of Report descriptor*/
0x00,
};
USBD_CUSTOM_HID_DeviceQualifierDesc
<usbd_customhid.c>
/* USB Standard Device Descriptor */
__ALIGN_BEGIN static uint8_t USBD_CUSTOM_HID_DeviceQualifierDesc[USB_LEN_DEV_QUALIFIER_DESC] __ALIGN_END =
{
USB_LEN_DEV_QUALIFIER_DESC,
USB_DESC_TYPE_DEVICE_QUALIFIER,
0x00,
0x02,
0x00,
0x00,
0x00,
0x40,
0x01,
0x00,
};
来源:CSDN
作者:丝贝视像-高精度计算机视觉
链接:https://blog.csdn.net/tanmx219/article/details/104033816