lilijin1995 发表于 2024-12-30 22:41

【ch585评估板测评】基于CH585的Xbox 360 for Windows手柄

本帖最后由 lilijin1995 于 2024-12-31 00:20 编辑

# 基于CH585的Xbox 360 for Windows手柄

## 概述

XInput是微软为了方便游戏厂商在PC和Xbox 360之间移植游戏而开发的一个接口标准,也被包含在微软的DirectX中。这一标准能同时应用于Xbox 360和Windows XP SP3以上的系统,具备设置简单、通用性强和支持震动功能等特点。目前,大部分支持手柄的PC游戏都支持微软的XInput标准,而部分游戏则支持较老的DirectInput标准。

基于CH585芯片实现Xbox 360 for Windows手柄的Xinput功能,需要硬件和软件两方面的设计。硬件设计方面,可能包括设计一个基于CH585的评估板,该板应包含必要的摇杆和按键,以模拟Xbox 360手柄的功能。软件设计则主要涉及USB HID(Human Interface Device)相关的描述符修改,以及实现Xinput协议。

在实现过程中,首先需要修改设备描述符,包括设置正确的厂商ID(VID)和产品ID(PID),这两个ID分别用于标识设备和产品,对于Xbox 360手柄,微软的VID是0x045E,而Xinput的PID可能是0x028E(这一信息可能因具体实现而异)。接下来,需要修改配置、接口、HID、端点和厂商描述符,以符合Xinput的要求。

完成硬件和软件设计后,还需要进行下载和验证,以确保手柄能够正确地与PC通信,并模拟Xbox 360手柄的功能。此外,对于非Xinput标准的手柄,可以通过下载Xinput Emulator等模拟软件,对手柄按键进行映射,以实现类似的功能。

请注意,以上信息仅供参考,具体实现过程可能因硬件和软件的不同而有所差异。在实际操作中,建议查阅相关的技术文档和资料,以确保实现的准确性和可靠性。

## 硬件设计:

之前有设计过一款基于CH582的评估板,采用顶板+底板的模式,这次咱们就只需要用到底板,通过杜邦线接线的方式实现;

!(data/attachment/forum/202412/30/224236x8v938bbhmauxb5a.png "image.png")

下图是接好线的硬件;

!(data/attachment/forum/202412/30/224627htmbmsaszsmbypsp.jpg "608b2c9bb0d72468ca824995b421dc5.jpg")

## 软件设计

软件是基于CH585EVT的USB->USBHS ->DEVICE->CompositeKM修改而来

!(data/attachment/forum/202412/30/225336a0b88q1xf8rjt0wm.png "image.png")

### USB描述符

USB HID(Human Interface Devices,人机接口设备)相关描述符是用于描述HID设备的特性和功能的。以下是USB HID相关描述符的简单描述:

#### 一、USB标准描述符

虽然这些不是HID特有的描述符,但它们在HID设备的配置和识别中起着重要作用:

1. **设备描述符**:描述设备的基本信息,如供应商ID、产品ID、版本号等。
2. **配置描述符**:描述设备的配置信息,如功耗、接口数量等。
3. **接口描述符**:描述设备的功能接口,对于HID设备,其bInterfaceClass的值必须为0x03。
4. **端点描述符**:描述设备上用于数据传输的端点信息,包括端点地址、传输类型、最大数据包大小等。
5. **字符串描述符**:提供设备的文字描述信息,如制造商名称、产品名称等。

#### 二、HID设备类特定描述符

1. **HID描述符**
   - **作用**:关联于接口描述符,主要描述HID规范的版本号、HID通信所使用的额外描述符、报表描述符的长度等。
   - **结构**:包括bLength(描述符长度)、bDescriptorType(描述符类型,此处为0x21)、bcdHID(HID规范版本号)、bCountry(硬件目的国家的识别码)、bNumDescriptors(支持的附属描述符数目)、bDescriptorType(hid相关描述符的类型)、wDescriptorLength(报告描述符长度)等字段。如果HID设备有多个下级描述符(如报告描述符和物理描述符),则HID描述符的长度会相应增加。
2. **报告描述符**
   - **作用**:描述HID设备的输入和输出报告格式。报告是HID设备用来传送数据的主要方式,包括输入报告(由设备发送给主机)和输出报告(由主机发送给设备)。报告描述符提供了设备报告的详细信息,如数据域、逻辑范围等,使得主机能够正确解析和处理报告中的数据。
   - **特点**:报告描述符的语法不同于USB标准描述符,它是以项目(items)方式排列而成,无一定的长度。报告描述符已经能够组合出很多种情况,需要PC上的HID驱动程序提供解析器(Parser)来对描述的设备情况进行重新解释,进而组合生成出本HID硬件设备独特的数据流格式。
3. **实体描述符**(可选)
   - **作用**:用来描述设备的行为特性。HID设备可以根据其本体的设备特性选择是否包含实体描述符。

综上所述,USB HID相关描述符共同构成了对HID设备的完整描述,使得主机能够正确识别、配置和使用这些设备。

CH585的USB描述符的代码实现是在usb_desc.c,usb_desc.h中实现,这里我们修改成了我们Xinput对应的

```c
/********************************** (C) COPYRIGHT *******************************
* File Name          : usb_desc.h
* Author             : WCH
* Version            : V1.0.0
* Date               : 2024/07/31
* Description      : All descriptors for the keyboard and mouse composite device.
*********************************************************************************
* Copyright (c) 2024 Nanjing Qinheng Microelectronics Co., Ltd.
* Attention: This software (modified or not) and binary are used for
* microcontroller manufactured by Nanjing Qinheng Microelectronics.
*******************************************************************************/

/*******************************************************************************/
/* Header File */
#include "usb_desc.h"

/*******************************************************************************/
/* Device Descriptor */
const uint8_t MyDevDescr[] =
    {
      0x12,                                              // bLength
      0x01,                                              // bDescriptorType
      0x00, 0x02,                                        // bcdUSB
      0xFF,                                              // bDeviceClass
      0xFF,                                              // bDeviceSubClass
      0xFF,                                              // bDeviceProtocol
      DEF_USBD_UEP0_SIZE,                              // bMaxPacketSize0
      (uint8_t)DEF_USB_VID, (uint8_t)(DEF_USB_VID >> 8), // idVendor
      (uint8_t)DEF_USB_PID, (uint8_t)(DEF_USB_PID >> 8), // idProduct
      0x10, DEF_IC_PRG_VER,                              // bcdDevice
      0x01,                                              // iManufacturer
      0x02,                                              // iProduct
      0x03,                                              // iSerialNumber
      0x01,                                              // bNumConfigurations
};

/* Configuration Descriptor */
const uint8_t MyCfgDescr[] =
    {
      /* Configuration Descriptor */
      0x09,       // bLength
      0x02,       // bDescriptorType
      0x30, 0x00, // wTotalLength
      0x01,       // bNumInterfaces
      0x01,       // bConfigurationValue
      0x00,       // iConfiguration
      0xA0,       // bmAttributes: Bus Powered; Remote Wakeup
      0xFA,       // MaxPower: 100mA

      /* Interface Descriptor (Vendor-Specific) */
      0x09, // bLength
      0x04, // bDescriptorType
      0x00, // bInterfaceNumber
      0x00, // bAlternateSetting
      0x02, // bNumEndpoints
      0xFF, // bInterfaceClass
      0x5D, // bInterfaceSubClass
      0x01, // bInterfaceProtocol: Keyboard
      0x00, // iInterface

      /* Unrecognized Class-Specific Descriptor */
      0x10, // bLength
      0x21, // bDescriptorType
      0x10,
      0x01,
      0x01,
      0x24,
      0x81,
      0x14,
      0x03,
      0x00,
      0x03,
      0x13,
      0x02,
      0x00,
      0x03,
      0x00,
      /* Endpoint Descriptor*/
      0x07,       // bLength
      0x05,       // bDescriptorType
      0x81,       // bEndpointAddress: IN Endpoint 1
      0x03,       // bmAttributes
      0x20, 0x00, // wMaxPacketSize
      0x04,       // bInterval: 4mS

      /* Endpoint Descriptor*/
      0x07,       // bLength
      0x05,       // bDescriptorType
      0x02,       // bEndpointAddress: IN Endpoint 2
      0x03,       // bmAttributes
      0x20, 0x00, // wMaxPacketSize
      0x08      // bInterval: 8mS
};

/* Language Descriptor */
const uint8_t MyLangDescr[] =
    {
      0x04,
      0x03,
      0x09,
      0x04};

/* Manufacturer Descriptor */
const uint8_t MyManuInfo[] =
    {
      0x0C,
      0x03,
      'e',
      0,
      'p',
      0,
      'l',
      0,
      'a',
      0,
      'y',
      0};

/* Product Information */
const uint8_t MyProdInfo[] =
    {
      0x0E,
      0x03,
      'x',
      0,
      'i',
      0,
      'n',
      0,
      'p',
      0,
      'u',
      0,
      't',
      0};

/* Serial Number Information */
const uint8_t MySerNumInfo[] =
    {
      0x16,
      0x03,
      '0',
      0,
      '1',
      0,
      '2',
      0,
      '3',
      0,
      '4',
      0,
      '5',
      0,
      '6',
      0,
      '7',
      0,
      '8',
      0,
      '9',
      0};
```

在USB2_DEVICE_IRQHandler中断函数里面需要修改相关的请求,我们会提供附件代码,这里不再赘述;

### 应用代码实现

#### ADC+DMA+通道切换模式

```c
/*
* adc.c
*
*Created on: Dec 24, 2024
*      Author: Administrator
*/
#include "adc.h"
#include "xinput.h"
#include "stdint.h"
uint16_t adcBuff;
volatile uint8_t DMA_end = 0;
static volatile int16_t X = 0, Y = 0;
volatile int16_t lastX = 0, lastY = 0;
void adc_init(void)
{
    /* DMA单通道采样:选择adc通道0做采样,对应 PA4引脚 */
    PRINT("\n3.Single channel DMA sampling...\n");
    GPIOA_ModeCfg(GPIO_Pin_4, GPIO_ModeIN_Floating);
    GPIOA_ModeCfg(GPIO_Pin_5, GPIO_ModeIN_Floating);
    ADC_ExtSingleChSampInit(SampleFreq_8_or_4, ADC_PGA_1_4);
    ADC_ChannelCfg(0);
    ADC_ExcutSingleConver();// 时间足够时建议再次转换并丢弃首次ADC数据
    ADC_AutoConverCycle(192); // 采样周期�? (256-192)*16个系统时�?
    ADC_DMACfg(ENABLE, (uint32_t)&adcBuff, (uint32_t)&adcBuff, ADC_Mode_Single);
    PFIC_EnableIRQ(ADC_IRQn);
}
/*        Re-maps a number from one range to another
*
*/
int32_t map(int32_t x, int32_t in_min, int32_t in_max, int32_t out_min, int32_t out_max)
{
    return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}
void adc_handler(void)
{

    uint8_t i;
    uint32_t sumx = 0, sumy = 0;
    uint16_t xtemp = 0, ytemp = 0;
    ADC_ChannelCfg(0);
    DMA_end = 0;
    ADC_DMACfg(ENABLE, (uint32_t)&adcBuff, (uint32_t)&adcBuff, ADC_Mode_Single);

    ADC_StartAutoDMA();
    while (!DMA_end)
      ;
    DMA_end = 0;
    ADC_DMACfg(DISABLE, 0, 0, 0);

    for (i = 0; i < 20; i++)
    {
      sumx += adcBuff;
    }
    xtemp = sumx / 20;
    PRINT("xtemp = %d\n", xtemp);
    if (xtemp > X_AD_MAX)
    {
      xtemp = X_AD_MAX;
    }
    else if (xtemp < X_AD_MIN)
    {
      xtemp = X_AD_MIN;
    }

    if (xtemp > (X_AD_CENTER + 5))
    {
      X = map(xtemp, (X_AD_CENTER + 5), X_AD_MAX, 0, INT16_MAX);
    }
    else if (xtemp < (X_AD_CENTER - 5))
    {
      X = map(xtemp, X_AD_MIN, (X_AD_CENTER - 5), INT16_MIN, 0);
    }
    else
    {
      X = 0;
    }

    PRINT("X = %d\n", X);

    ADC_ChannelCfg(1);
    DMA_end = 0;
    ADC_DMACfg(ENABLE, (uint32_t)&adcBuff, (uint32_t)&adcBuff, ADC_Mode_Single);

    ADC_StartAutoDMA();
    while (!DMA_end)
      ;
    DMA_end = 0;
    ADC_DMACfg(DISABLE, 0, 0, 0);

    for (i = 0; i < 20; i++)
    {
      sumy += adcBuff;
    }
    ytemp = sumy / 20;
    PRINT("ytemp = %d\n", ytemp);
    if (ytemp > Y_AD_MAX)
    {
      ytemp = Y_AD_MAX;
    }
    else if (ytemp < Y_AD_MIN)
    {
      ytemp = Y_AD_MIN;
    }
    if (ytemp > (Y_AD_CENTER + 5))
    {
      Y = map(ytemp, (Y_AD_CENTER + 5), Y_AD_MAX, 0,INT16_MIN );
    }
    else if (ytemp < (Y_AD_CENTER - 5))
    {
      Y = map(ytemp, Y_AD_MIN, (Y_AD_CENTER - 5), INT16_MAX, 0);
    }
    else
    {
      Y = 0;
    }
    if (X != lastX || Y != lastY)
    {
      lastX = X;
      lastY = Y;
      set_xinput_report(READY);
    }
    PRINT("Y = %d\n", Y);
}
void get_xy(int16_t *x, int16_t *y)
{
    *x = X;
    *y = Y;
}
__INTERRUPT
__HIGH_CODE
void ADC_IRQHandler(void) // adc中断服务程序
{
    if (ADC_GetDMAStatus())
    {
      ADC_StopAutoDMA();
      R32_ADC_DMA_BEG = ((uint32_t)adcBuff) & 0x1ffff;
      ADC_ClearDMAFlag();
      DMA_end = 1;
    }
    if (ADC_GetITStatus())
    {
      ADC_ClearITFlag();
    }
}
```

adc中断服务程序这里一定要注意ADC_GetITStatus和ADC_ClearITFlag中断一定要清,不然会在中断里面出不来;找wch的FAE解决,给我的方案是叫我清中断,并解释可能是库的处理问题;

#### 按键处理

```c
/*
* button.c
*
*Created on: Dec 26, 2024
*      Author: Administrator
*/
#include "button.h"
#include "xinput.h"
static uint8_t button = 0, last_button = 0;
void button_init(void)
{
    GPIOB_ModeCfg(GPIO_Pin_0, GPIO_ModeIN_PU);
    GPIOB_ModeCfg(GPIO_Pin_1, GPIO_ModeIN_PU);
    GPIOB_ModeCfg(GPIO_Pin_2, GPIO_ModeIN_PU);
    GPIOB_ModeCfg(GPIO_Pin_3, GPIO_ModeIN_PU);
    GPIOB_ModeCfg(GPIO_Pin_4, GPIO_ModeIN_PU);
    GPIOB_ModeCfg(GPIO_Pin_5, GPIO_ModeIN_PU);
    GPIOB_ModeCfg(GPIO_Pin_6, GPIO_ModeIN_PU);
    GPIOB_ModeCfg(GPIO_Pin_7, GPIO_ModeIN_PU);
}

void button_handler(void)
{

    // button1
    if (GET_BUTTON1_STATUS() == 0)
    {
      button |= BIT0;
    }
    else
    {
      button &= ~BIT0;
    }
    // button2
    if (GET_BUTTON2_STATUS() == 0)
    {
      button |= BIT1;
    }
    else
    {
      button &= ~BIT1;
    }
    // button3
    if (GET_BUTTON3_STATUS() == 0)
    {
      button |= BIT2;
    }
    else
    {
      button &= ~BIT2;
    }
    // button4
    if (GET_BUTTON4_STATUS() == 0)
    {
      button |= BIT3;
    }
    else
    {
      button &= ~BIT3;
    }
    // button5
    if (GET_BUTTON5_STATUS() == 0)
    {
      button |= BIT4;
    }
    else
    {
      button &= ~BIT4;
    }
    // button6
    if (GET_BUTTON6_STATUS() == 0)
    {
      button |= BIT5;
    }
    else
    {
      button &= ~BIT5;
    }
    // button7
    if (GET_BUTTON7_STATUS() == 0)
    {
      button |= BIT6;
    }
    else
    {
      button &= ~BIT6;
    }
    // button8
    if (GET_BUTTON8_STATUS() == 0)
    {
      button |= BIT7;
    }
    else
    {
      button &= ~BIT7;
    }
    if (button != last_button)
    {

      set_xinput_report(READY);

      last_button = button;
    }
}

uint8_t get_button_status(void)
{
    return button;
}
```

按键驱动只需要简单的扫描,毕竟按键数量较少;

```c
/*
* xinput.c
*
*Created on: Dec 26, 2024
*      Author: Administrator
*/
#include "xinput.h"
#include "adc.h"
#include "button.h"
#include <ch585_usbhs_device.h>
#define XINPUT_BUFFER_SIZE 20
uint8_t xinput_buffer = {0x00, 0x14, 0x00, 0x00, 0x00,
                                             0x00, 0x00, 0x00, 0x00, 0x00,
                                             0x00, 0x00, 0x00, 0x00, 0x00,
                                             0x00, 0x00, 0x00, 0x00, 0x00}; // Holds USB transmit packet data
static ErrorStatus report_flag = NoREADY;


/**
* @brief Returns the status of xinput report transmission
*
* @return ErrorStatus READY = successful transmission, NoREADY = failed transmission
*/
/******abc32b94-3e7c-4b82-a397-bbb72a854fb0*******/ ErrorStatus get_xinput_report(void)
{
    return report_flag;
}

void set_xinput_report(ErrorStatus status)
{
    report_flag = status;
}

void xinput_handler(void)
{
    uint8_t status, button_sta;
    int16_t x, y;
    if (report_flag)
    {
      // XY
      get_xy(&x, &y);
      // xinput_buffer = get_button_status();
      xinput_buffer = LOBYTE(x); // (CONFERIR)
      xinput_buffer = HIBYTE(x);
      // Left Stick Y Axis
      xinput_buffer = LOBYTE(y);
      xinput_buffer = HIBYTE(y);
      // Clear DPAD
         xinput_buffer &= DPAD_MASK_OFF;
      button_sta = get_button_status();
      if (button_sta & BIT4)
      {
            xinput_buffer |= A_MASK_ON;
      }
      else
      {
            xinput_buffer &= A_MASK_OFF;
      }

      if (button_sta & BIT5)
      {
            xinput_buffer |= B_MASK_ON;
      }
      else
      {
            xinput_buffer &= B_MASK_OFF;
      }
      if (button_sta & BIT6)
      {
            xinput_buffer |= X_MASK_ON;
      }
      else
      {
            xinput_buffer &= X_MASK_OFF;
      }
      if (button_sta & BIT7)
      {
            xinput_buffer |= Y_MASK_ON;
      }
      else
      {
            xinput_buffer &= Y_MASK_OFF;
      }


      // DPAD Up
      if ((button_sta & BIT0) && !(button_sta & BIT3))
      {
            xinput_buffer |= DPAD_UP_MASK_ON;
      }
         // DPAD Down
         if ((button_sta & BIT3) && !(button_sta & BIT0))
         {
             xinput_buffer |= DPAD_DOWN_MASK_ON;
         }
         // DPAD Left
         if ((button_sta & BIT1) && !(button_sta & BIT2))
         {
             xinput_buffer |= DPAD_LEFT_MASK_ON;
         }
         // DPAD Right
         if ((button_sta & BIT2) && !(button_sta & BIT1))
         {
             xinput_buffer |= DPAD_RIGHT_MASK_ON;
         }
      /* Load keyboard data to endpoint 1 */
      status = USBHS_Endp_DataUp(DEF_UEP1, xinput_buffer, sizeof(xinput_buffer), DEF_UEP_CPY_LOAD);

      if (status == READY)
      {
            /* Clear flag after successful loading */
            report_flag = NoREADY;
      }
    }
}
```

Xinput里面主要就是给上报数据给我的PC(USB主机)

#### main函数

```c
/********************************** (C) COPYRIGHT *******************************
* File Name          : main.c
* Author             : WCH
* Version            : V1.0.0
* Date               : 2024/07/31
* Description      : Main program body.
*********************************************************************************
* Copyright (c) 2024 Nanjing Qinheng Microelectronics Co., Ltd.
* Attention: This software (modified or not) and binary are used for
* microcontroller manufactured by Nanjing Qinheng Microelectronics.
*******************************************************************************/

#include <ch585_usbhs_device.h>

#include "adc.h"
#include "timer.h"
#include "button.h"
#include "xinput.h"


/*********************************************************************
* @fn      DebugInit
*
* @brief   ���Գ�ʼ��
*
* @returnnone
*/
void DebugInit(void)
{
    GPIOA_SetBits(GPIO_Pin_14);
    GPIOPinRemap(ENABLE, RB_PIN_UART0);
    GPIOA_ModeCfg(GPIO_Pin_15, GPIO_ModeIN_PU);
    GPIOA_ModeCfg(GPIO_Pin_14, GPIO_ModeOut_PP_5mA);
    UART0_DefInit();
}

/*********************************************************************
* @fn      main
*
* @brief   Main program.
*
* @returnnone
*/
int main(void)
{
    SetSysClock(CLK_SOURCE_HSE_PLL_62_4MHz);
    DebugInit();
    PRINT("Compatibility HID Running On USBHS Controller\n");
    adc_init();
    button_init();
    // timer_init();
    /* Initialize USBHS interface to communicate with the host*/
    USBHS_Device_Init(ENABLE);
    PFIC_EnableIRQ(USB2_DEVICE_IRQn);

    while (1)
    {
      if (USBHS_DevEnumStatus)
      {
            button_handler();
            adc_handler();
            xinput_handler();
      }
      // if (USBHS_DevEnumStatus)
      // {
      //   if (sys_timer_flag.tf.tim_5msflag)
      //   {
      //         sys_timer_flag.tf.tim_5msflag = 0;
      //         button_handler();
      //   }

      //   if (sys_timer_flag.tf.tim_10msflag)
      //   {
      //         sys_timer_flag.tf.tim_10msflag = 0;
      //         adc_handler();
      //   }
      //   if (sys_timer_flag.tf.tim_1msflag)
      //   {
      //         sys_timer_flag.tf.tim_1msflag = 0;
      //         xinput_handler();
      //   }
      // }
    }
}
```

main函数里面主要就是调用相关函数,本来想用时间片轮询,我这里开试过定时器1ms,10ms中断,结果USB都枚举失败,不知道是不是因为定时器中断影响,按理说USB中断优先级最高才对,我不知道它的这个库的优先级咋设置的,这里就直接注释掉,这样ADC和按键扫描完成就发送这样反而响应更快,不太清楚手柄是否可以测试回报率,鼠标是可以设置的;

## 总结

现在学了一点bms的皮毛,下一步打算做个三模的,还打算用wch的蓝牙去采集电压和电流,NTC温度等,估算SOC,SOH,做电池包,不过不能商用,可以做保护板,因为一般锂电池不能快递;而且需要有一定的资质才能卖这个电池包吧,还有各种认证;麻烦,做来玩玩先吧!

纯软件行业一般都是跟两大巨头走,微软或甲骨文,BLE和USB我想跟着wch走,我2018年就接触了wch,从USB、以太网到蓝牙,从有线到无线,从51、ARM到RISC-V;希望WCH越做越强;

## 视频演示


<iframe src="https://player.bilibili.com/player.html?bvid=BV1HV6LYDEAz&page=1&danmaku=0" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"></iframe>



[!(/source/plugin/zhanmishu_markdown/template/editor/images/upload.svg) 附件:Xinput01.zip](forum.html?mod=attachment&aid=2346658 "attachment")

妖妖妖 发表于 2025-4-22 10:30

我从51到RISC-V,32位单片机用的沁恒,希望沁恒在资料方面多用心些。
页: [1]
查看完整版本: 【ch585评估板测评】基于CH585的Xbox 360 for Windows手柄