本项目基于STM32F103C8T6,使用STM32CUBE MX模拟Xbox手柄,实现基本的控制和震动,LED功能,后续会添加客制功能,代码完全开源,一步步讲解,有问题的可以评论提问,我会答疑&修改文章错误。
参考资料是github大佬的开源链接:Github链接参考资料
此处做原理讲解,并对项目做补全,开源项目仅实现IN端点功能,未实现OUT端点功能,本项目对开源项目有功能补全&优化。
大佬的开源项目不包括硬件,本项目基于STM32F103C8T6做了配套电路板,可以免费打样,后续更新硬件开源信息。

对USB协议和底层原理感兴趣的可以看前序USB协议介绍,不感兴趣的,只注重功能实现的可以直接从这篇文章开始看,博主是纯新手,如有错误请各位大佬指正。

项目主要分为4部分:
1.USB设备枚举为xbox360手柄
2.实现IN端点发送手柄摇杆按键数据
3.实现OUT端点接收LED和电机震动数据
4.按键摇杆LED电机驱动和数据采集传输

1.USB设备枚举为xbox360手柄

1.1 参考下图创建工程(部分字符描述符可以参考第五步)
在这里插入图片描述
在这里插入图片描述
1.2 在usbd_desc.c里面修改Device descriptor,直接从协议分析仪抓出来的包复制数组:

0x12, 0x01, 0x00, 0x02, 0xFF, 0xFF, 0xFF, 0x08, 0x5E, 0x04, 0x8E, 0x02, 0x14, 0x01, 0x01, 0x02, 0x03, 0x01

这里面有一个关联以下宏定义需要修改(usbd_def.h):

#define USB_MAX_EP0_SIZE                                8U

3.在usbd_customhid.c里面修改配置描述符,直接从抓的协议里面复制数组:

0x09, 0x02, 0x99, 0x00, 0x04, 0x01, 0x00, 0xA0, 
0xFA, 0x09, 0x04, 0x00, 0x00, 0x02, 0xFF, 0x5D, 
0x01, 0x00, 0x11, 0x21, 0x00, 0x01, 0x01, 0x25, 
0x81, 0x14, 0x00, 0x00, 0x00, 0x00, 0x13, 0x02, 
0x08, 0x00, 0x00, 0x07, 0x05, 0x81, 0x03, 0x20, 
0x00, 0x04, 0x07, 0x05, 0x02, 0x03, 0x20, 0x00, 
0x08, 0x09, 0x04, 0x01, 0x00, 0x04, 0xFF, 0x5D, 
0x03, 0x00, 0x1B, 0x21, 0x00, 0x01, 0x01, 0x01, 
0x83, 0x40, 0x01, 0x04, 0x20, 0x16, 0x85, 0x00, 
0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x05, 0x00, 
0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x05, 0x83, 
0x03, 0x20, 0x00, 0x02, 0x07, 0x05, 0x04, 0x03, 
0x20, 0x00, 0x04, 0x07, 0x05, 0x85, 0x03, 0x20, 
0x00, 0x40, 0x07, 0x05, 0x05, 0x03, 0x20, 0x00, 
0x10, 0x09, 0x04, 0x02, 0x00, 0x01, 0xFF, 0x5D, 
0x02, 0x00, 0x09, 0x21, 0x00, 0x01, 0x01, 0x22, 
0x86, 0x07, 0x00, 0x07, 0x05, 0x86, 0x03, 0x20, 
0x00, 0x10, 0x09, 0x04, 0x03, 0x00, 0x00, 0xFF, 
0xFD, 0x13, 0x04, 0x06, 0x41, 0x00, 0x01, 0x01, 
0x03

4.此时会报错,因为原数组长度不匹配,选择数组长度宏定义USB_CUSTOM_HID_CONFIG_DESC_SIZ,修改为153,排除报错

5.还有一些字符串描述符没有介绍,在usbd_desc.c里面,基本上就是直接照抄微软设定就好了,

#define USBD_VID     1118
#define USBD_LANGID_STRING     1033
#define USBD_MANUFACTURER_STRING     "@Microsoft"
#define USBD_PID_FS     654
#define USBD_PRODUCT_STRING_FS     "Controller"
#define USBD_CONFIGURATION_STRING_FS     "Custom HID Config"
#define USBD_INTERFACE_STRING_FS     "\xB2\x03\x58\x00\x62\x00\x6F\x00\x78\x00\x20\x00\x53\x00\x65\x00\x63\x00\x75\x00\x72\x00\x69\x00\x74\x00\x79\x00\x20\x00\x4D\x00\x65\x00\x74\x00\x68\x00\x6F\x00\x64\x00\x20\x00\x33\x00\x2C\x00\x20\x00\x56\x00\x65\x00\x72\x00\x73\x00\x69\x00\x6F\x00\x6E\x00\x20\x00\x31\x00\x2E\x00\x30\x00\x30\x00\x2C\x00\x20\x00\xA9\x00\x20\x00\x32\x00\x30\x00\x30\x00\x35\x00\x20\x00\x4D\x00\x69\x00\x63\x00\x72\x00\x6F\x00\x73\x00\x6F\x00\x66\x00\x74\x00\x20\x00\x43\x00\x6F\x00\x72\x00\x70\x00\x6F\x00\x72\x00\x61\x00\x74\x00\x69\x00\x6F\x00\x6E\x00\x2E\x00\x20\x00\x41\x00\x6C\x00\x6C\x00\x20\x00\x72\x00\x69\x00\x67\x00\x68\x00\x74\x00\x73\x00\x20\x00\x72\x00\x65\x00\x73\x00\x65\x00\x72\x00\x76\x00\x65\x00\x64\x00\x2E\x00"

编译烧录,此时就设备插到电脑上就可以识别为手柄了
在这里插入图片描述

在这里插入图片描述

2.实现IN端点发送手柄摇杆按键数据

首先,IN和OUT端点都是中断传输端点,IN传输一般是硬件中断触发,然后直接把FIFO里面的数据发走,FIFO里面的数据由USBD_CUSTOM_HID_SendReport函数 传入。
采集数据目前想到三种思路:
第一中参考Github链接的大佬逻辑是直接while(1)循环,然后里面采集ADC和按键数据,无限往FIFO里面填,FIFO只会保留最新的一次数据,好处是逻辑简单,坏处是循环更新浪费CPU资源,可能在一帧内采集多次CPU数据,优先功能实现先参考这个。
还有另外一个逻辑是在中断过程中采集数据,采集到数据后再回复Host,好处是数据是实时最新的,坏处是中断过程中操作过多,这是比较忌讳的事情,把所有东西都放到USB中断里面执行,也有采集函数时间过长,导致USB中断得不到回复,通讯出错的问题(比如超过1ms,无法在1帧内完成一次IN包传送)。
还有第三种逻辑是引入轮询时间参数,并内部计时参数采集时间,动态调整计时器参数触发信息采集,在USB中断前完成信息采集,并留部分时间冗余,可以最大化降低无效CPU占用,降低延迟,提升USB通信稳定性。缺点是逻辑复杂,实施困难。

补充一些前置基础信息,这段可以看,辅助理解,也可以不看,不影响按部就班实现功能:
USB协议的固件层大概分为6层。最底下一层算第一层是由硬件实现的,FIFO,SOF,Token的识别和硬件中断,这层是纯硬件,不太用管,知道有什么就行了;第二层是PCD层(Peripheral Controller Driver),这层是硬件中断的第一层回调函数,里面也有最底层的EP打开关闭和读取写入函数,这层在stm32f1xx_hal_pcd.c里面,感兴趣可以看看,不建议直接调用或者修改,有一些弱定义的函数如果需要可以重定义;第三层Device low level interface层,衔接从最底层的PCD到协议栈层的回调函数,官方注释叫LL Driver Callbacks,第四层是协议栈核心USB Core,这层在协议层实现Setup和IN、OUT任务的分流,调用不同的协议栈函数,第五层是Class类的实现,这层是实现类的具体功能的,一些类的函数逻辑处理可以在这层找回调函数, 修改回调函数来实现,这层在本项目是HID类,第六次就是应用层,这层获取用户数据,并把用户数据通过第五层函数USBD_CUSTOM_HID_SendReport发送出去。
主要有两层需要修改,第五层和第六层,修改这两层就可以满足本项目的功能实现。

先实施第一步,最简单的,通过IN端点把数据发出去。

思路是先把ENDP的参数配置正确,然后依据协议分析仪里面的数据和网上的逆向工程,定义20位的发送数据数组,然后通过USBD_CUSTOM_HID_SendReport函数发送数组。

首先需要解决一个问题,把usbd_customhid.h里面的两个宏定义修改如下,修正EP1和EP2的地址和数据长度,需要参考依据可以在前序2去看EP的描述符注释,EPOUT_SIZE有点出入,Descriptor里面是32,实际只用到8.
一些可以不看的注释解释:一个Endpoint可以既做输入,又做输出,固件内部通过唯一的地址来确定命令属于哪个Endpoint,是输入还是输出功能,Xbox这里是EP1只做IN,EP2只做OUT,逻辑比较简单。

#define CUSTOM_HID_EPIN_SIZE                 0x20U
#define CUSTOM_HID_EPOUT_ADDR                0x02
#define CUSTOM_HID_EPOUT_SIZE                0x08U
	```
一并更改usbd_conf.h里面的

```c
#define USBD_CUSTOMHID_OUTREPORT_BUF_SIZE     32
#define USBD_CUSTOM_HID_REPORT_DESC_SIZE     32

下一步在usbd_custom_hid_if.c文件中添加如下指针结构体定义,在初始化中,将它指向hUsbDeviceFS,方便调用USB句柄,且部分函数需要指针传参

USBD_HandleTypeDef  *hUsbDevice_0;

static int8_t CUSTOM_HID_Init_FS(void)
{
  /* USER CODE BEGIN 4 */
  hUsbDevice_0 = &hUsbDeviceFS;
  return (USBD_OK);
  /* USER CODE END 4 */
}

并在main.c中引用

extern USBD_HandleTypeDef  *hUsbDevice_0;
extern uint8_t USBD_CUSTOM_HID_SendReport     (USBD_HandleTypeDef  *pdev, 
                                 uint8_t *report,
                                 uint16_t len);

在main.c里面定义以下两个发送数组做实验用,数组格式由网络开源逆向工程得出,也可以通过协议分析仪抓到,第一个数组是无任何触发的数组,第二个数组触发方向键的上键

uint8_t TXData[20] = {0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
uint8_t TXData1[20] = {0x00, 0x14, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};

TXData数组参数含义参见如下,有一些固定位保留没有用到:

//Byte location Definitions
#define BUTTON_PACKET_1 2
#define BUTTON_PACKET_2 3
#define LEFT_TRIGGER_PACKET 4
#define RIGHT_TRIGGER_PACKET 5
#define LEFT_STICK_X_PACKET_LSB 6
#define LEFT_STICK_X_PACKET_MSB 7
#define LEFT_STICK_Y_PACKET_LSB 8
#define LEFT_STICK_Y_PACKET_MSB 9
#define RIGHT_STICK_X_PACKET_LSB 10
#define RIGHT_STICK_X_PACKET_MSB 11
#define RIGHT_STICK_Y_PACKET_LSB 12
#define RIGHT_STICK_Y_PACKET_MSB 13

此时直接在主程序调用USBD_CUSTOM_HID_SendReport发送摇杆和按键数据就可以传到Host了,这里调用上面定义的句柄和TXData示例

  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
      USBD_CUSTOM_HID_SendReport( hUsbDevice_0, TXData, 20 );
      HAL_Delay(1000);
      USBD_CUSTOM_HID_SendReport( hUsbDevice_0, TXData1, 20 );
      HAL_Delay(1000);
  }

此时在网上随便找个手柄测试网站,可以看到方向键上键每隔一秒定时触发,这就实现了最基础的信息输入。
在这里插入图片描述

接下来就是需要把按键和摇杆信息送入数组中发送,分为三部分,按键采集,ADC采集,数据传入,可以把前两部分归结为外设接口层,第三部分归为数据组织和传输层,层与层之间通过接口调用,切换不同外设接口输入时,只需要保持数据接口一致就可以不用动数据层的代码。
这部分放到下一章,不然这章显得很臃肿。

3. 实现OUT端点接收LED和电机震动数据

首先定义一个结构体来储存OUT端点接收到的LED和电机震动状态数据:

typedef struct {
    uint8_t led_state;     // LED状态位,
    uint8_t motor_left;    // 左电机强度 (0-255)
    uint8_t motor_right;   // 右电机强度 (0-255)
} XboxCtrlState_t;

volatile XboxCtrlState_t xbox_periph_state; //因为在USB中断中更新数据,所以每次都要从内存中取实际值

OUT端点接收数据需要找到类里面处理out的函数,可以看到如下函数

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;
}

USBD_LL_PrepareReceive是在接收完成后调用的函数,表示准备好下一次接收
函数功能主要由中间这句实现

((USBD_CUSTOM_HID_ItfTypeDef *)pdev->pUserData)->OutEvent(hhid->Report_buf[0],
                                                            hhid->Report_buf[1]);

它调用的是usbd_customhid_if.c里面的接口函数CUSTOM_HID_OutEvent_FS来处理数据,我们在这个函数里面把RX数据传入,根据USB协议分析仪抓取到的数据及网上逆向数据可知,RX数据第一位为0x00时传递的是电机数据,第二位为数据长度8Byte,后面第三位是左电机数据,第四位是右电机数据;RX数据第一位为0x01时传递的是LED数据,第二位为数据长度长度3Byte,后面第三位是LED状态, 网上查到的XBOX逆向工程数据是有问题的,根据抓到的USB协议分析可以确定的数据如下:
0x01:4灯闪烁
0x02:LED1亮
0x03:LED2亮
0x04:LED3亮
0x05:LED4亮

根据以上信息,OUTEVENT数据处理函数修改如下:

static int8_t CUSTOM_HID_OutEvent_FS(uint8_t event_idx, uint8_t state)
{
  /* USER CODE BEGIN 6 */
        USBD_CUSTOM_HID_HandleTypeDef *hhid = (USBD_CUSTOM_HID_HandleTypeDef*)hUsbDevice_0->pClassData;

    for (uint8_t i = 0 ; i < 8 ; i++)
    {
        RXData[i] = hhid->Report_buf[i];
    }

    if (event_idx==0x01 && state== 0x03)
    {
        xbox_periph_state.led_state=RXData[2];
        
    }
    else if (event_idx==0x00 && state== 0x08)
    {
        xbox_periph_state.motor_left=RXData[3];
        xbox_periph_state.motor_right=RXData[4];
    }
    else if (event_idx==0x02)
    {
        return (0);
    }
  return (USBD_OK);
  /* USER CODE END 6 */
}

这里已经把参数传入结构体了,下一步就是在外设控制函数中调用结构体里面的数据,根据结构体里面最新的数据更新外设状态,这部分放到下一章。

三步写完了,
第一步主要是模拟设备描述符,配置描述符,根据这两个描述符里面的配置修改部分设置,修改固定的字符串描述符;
第二步配置ENDP的参数,创建USB句柄,通过USBD_CUSTOM_HID_SendReport发送符合XBOX格式的数据;
第三步配置OUT端点的时间回调处理函数,把参数传入准备好的结构体数组,后续外设控制函数直接调用来控制外设状态。

下一部分主要是IN端点上传数组的生成,和根据OUT端点数据更新外设状态的函数。

Logo

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

更多推荐