[i=s] 本帖最后由 lilijin1995 于 2024-12-31 00:20 编辑 [/i]<br />
<br />
基于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的评估板,采用顶板+底板的模式,这次咱们就只需要用到底板,通过杜邦线接线的方式实现;

下图是接好线的硬件;

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

USB描述符
USB HID(Human Interface Devices,人机接口设备)相关描述符是用于描述HID设备的特性和功能的。以下是USB HID相关描述符的简单描述:
一、USB标准描述符
虽然这些不是HID特有的描述符,但它们在HID设备的配置和识别中起着重要作用:
- 设备描述符:描述设备的基本信息,如供应商ID、产品ID、版本号等。
- 配置描述符:描述设备的配置信息,如功耗、接口数量等。
- 接口描述符:描述设备的功能接口,对于HID设备,其bInterfaceClass的值必须为0x03。
- 端点描述符:描述设备上用于数据传输的端点信息,包括端点地址、传输类型、最大数据包大小等。
- 字符串描述符:提供设备的文字描述信息,如制造商名称、产品名称等。
二、HID设备类特定描述符
- HID描述符
- 作用:关联于接口描述符,主要描述HID规范的版本号、HID通信所使用的额外描述符、报表描述符的长度等。
- 结构:包括bLength(描述符长度)、bDescriptorType(描述符类型,此处为0x21)、bcdHID(HID规范版本号)、bCountry(硬件目的国家的识别码)、bNumDescriptors(支持的附属描述符数目)、bDescriptorType(hid相关描述符的类型)、wDescriptorLength(报告描述符长度)等字段。如果HID设备有多个下级描述符(如报告描述符和物理描述符),则HID描述符的长度会相应增加。
- 报告描述符
- 作用:描述HID设备的输入和输出报告格式。报告是HID设备用来传送数据的主要方式,包括输入报告(由设备发送给主机)和输出报告(由主机发送给设备)。报告描述符提供了设备报告的详细信息,如数据域、逻辑范围等,使得主机能够正确解析和处理报告中的数据。
- 特点:报告描述符的语法不同于USB标准描述符,它是以项目(items)方式排列而成,无一定的长度。报告描述符已经能够组合出很多种情况,需要PC上的HID驱动程序提供解析器(Parser)来对描述的设备情况进行重新解释,进而组合生成出本HID硬件设备独特的数据流格式。
- 实体描述符(可选)
- 作用:用来描述设备的行为特性。HID设备可以根据其本体的设备特性选择是否包含实体描述符。
综上所述,USB HID相关描述符共同构成了对HID设备的完整描述,使得主机能够正确识别、配置和使用这些设备。
CH585的USB描述符的代码实现是在usb_desc.c,usb_desc.h中实现,这里我们修改成了我们Xinput对应的
/********************************** (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+通道切换模式
/*
* adc.c
*
* Created on: Dec 24, 2024
* Author: Administrator
*/
#include "adc.h"
#include "xinput.h"
#include "stdint.h"
uint16_t adcBuff[20];
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[0], (uint32_t)&adcBuff[20], 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[0], (uint32_t)&adcBuff[20], 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[i];
}
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[0], (uint32_t)&adcBuff[20], 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[i];
}
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解决,给我的方案是叫我清中断,并解释可能是库的处理问题;
按键处理
/*
* 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;
}
按键驱动只需要简单的扫描,毕竟按键数量较少;
/*
* 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[XINPUT_BUFFER_SIZE] = {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](home.php?mod=space&uid=247401) Returns the status of xinput report transmission
*
* [@return](home.php?mod=space&uid=266161) 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[2] = get_button_status();
xinput_buffer[LEFT_STICK_X_PACKET_LSB] = LOBYTE(x); // (CONFERIR)
xinput_buffer[LEFT_STICK_X_PACKET_MSB] = HIBYTE(x);
// Left Stick Y Axis
xinput_buffer[LEFT_STICK_Y_PACKET_LSB] = LOBYTE(y);
xinput_buffer[LEFT_STICK_Y_PACKET_MSB] = HIBYTE(y);
// Clear DPAD
xinput_buffer[BUTTON_PACKET_1] &= DPAD_MASK_OFF;
button_sta = get_button_status();
if (button_sta & BIT4)
{
xinput_buffer[BUTTON_PACKET_2] |= A_MASK_ON;
}
else
{
xinput_buffer[BUTTON_PACKET_2] &= A_MASK_OFF;
}
if (button_sta & BIT5)
{
xinput_buffer[BUTTON_PACKET_2] |= B_MASK_ON;
}
else
{
xinput_buffer[BUTTON_PACKET_2] &= B_MASK_OFF;
}
if (button_sta & BIT6)
{
xinput_buffer[BUTTON_PACKET_2] |= X_MASK_ON;
}
else
{
xinput_buffer[BUTTON_PACKET_2] &= X_MASK_OFF;
}
if (button_sta & BIT7)
{
xinput_buffer[BUTTON_PACKET_2] |= Y_MASK_ON;
}
else
{
xinput_buffer[BUTTON_PACKET_2] &= Y_MASK_OFF;
}
// DPAD Up
if ((button_sta & BIT0) && !(button_sta & BIT3))
{
xinput_buffer[BUTTON_PACKET_1] |= DPAD_UP_MASK_ON;
}
// DPAD Down
if ((button_sta & BIT3) && !(button_sta & BIT0))
{
xinput_buffer[BUTTON_PACKET_1] |= DPAD_DOWN_MASK_ON;
}
// DPAD Left
if ((button_sta & BIT1) && !(button_sta & BIT2))
{
xinput_buffer[BUTTON_PACKET_1] |= DPAD_LEFT_MASK_ON;
}
// DPAD Right
if ((button_sta & BIT2) && !(button_sta & BIT1))
{
xinput_buffer[BUTTON_PACKET_1] |= 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) 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](home.php?mod=space&uid=247401) ���Գ�ʼ��
*
* [@return](home.php?mod=space&uid=266161) none
*/
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](home.php?mod=space&uid=247401) Main program.
*
* [@return](home.php?mod=space&uid=266161) none
*/
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>
附件:Xinput01.zip