打印
[活动]

【APM32F107VC MINI开发板测评】基于APM32F107实现客制化USB游戏手柄过程分享

[复制链接]
420|1
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
本帖最后由 lilijin1995 于 2023-2-17 09:23 编辑

一、开发背景与环境
APM32F107VC MINI开发板到手了,已经迫不及待想要开始搞事情了!板子到手后,唯一比较感慨的是,Mini板没有网口,就没有评测以太网,后续可能会购买一个以太网模块用以评测以太网功能。
今天已经完成了评测,基本实现了一个具有XYZ轴和Rx(X旋转)、Ry(Y旋转),以及带有Button1~Button10共10颗按钮以及视觉头盔的USB游戏手柄。
如下图:



由于是评估板,而不是自己设计的PCB,所以这里我们通过飞线解决,接了许多的杜邦线,硬件环境如下图



最后我们来看一下引脚配置图:
这里使用了HSE外部晶振,APM32F107VC MINI开发板使用的是25Mhz的晶振,
ADCIN10~ADCIN14共5个通道,分别作为XYZ轴和Rx(X旋转)、Ry(Y旋转)的模拟输入。
KEY1~KEY10则是对应按钮Button1~Button10;
PE7~PE10其实也可以认为是按钮,它对应是视觉头盔Hat。
还有通信接口是PA11--DM,PA12--DP,UART4则是Log日志的打印,
最后是开启SWD口方便程序下载和硬件调试。
二、软件开发:
1、USB HID驱动:
Usb HID我们直接用APM32F107_EVAL_SDK_v1.0里面的HID实例;
设备描述符我们直接用Geehy的,因为涉及厂商ID和产品ID的定义我们这里不修改,包括字符串描述符,也不改了,另外还有配置描述符集合(里面有配置描述符、接口描述符、HID描述符、端点描述符)以及报表描述符,都是在usbd_descriptor.c可以找到。
这里我们就修改配置描述符集合和报表描述符,其他描述符都不修改,修改后的配置描述符集合和报表描述符如下代码:
/**
* [url=home.php?mod=space&uid=247401]@brief[/url]   Configuration descriptor
*/
uint8_t g_usbConfigDescriptor[USB_CONFIG_DESCRIPTOR_SIZE] =
{
    /* bLength */
    0x09,
    /* bDescriptorType */
    USBD_DESC_CONFIGURATION,
    /* wTotalLength */
    USB_CONFIG_DESCRIPTOR_SIZE & 0XFF, USB_CONFIG_DESCRIPTOR_SIZE >> 8,

    /* bNumInterfaces */
    0X01,
    /* bConfigurationValue */
    0X01,
    /* iConfiguration */
    0X00,
    /* bmAttributes */
    0XE0,
    /* MaxPower */
    0X32,

    /* bLength */
    0X09,
    /* bDescriptorType */
    USBD_DESC_INTERFACE,
    /* bInterfaceNumber */
    0X00,
    /* bAlternateSetting */
    0X00,
    /* bNumEndpoints */
    0X01,
    /* bInterfaceClass */
    0X03,
    /* bInterfaceSubClass */
    0X01,
    /* bInterfaceProtocol */
    0X02,
    /* iInterface */
    0X00,

    /* bLength */
    0X09,
    /* Functional Descriptor */
    0x21,
    /* bcdHID */
    0X00, 0X01,
    /* bCountryCode */
    0X00,
    /* bNumDescriptors */
    0X01,
    /* bDescriptorType */
    0X22,
    /* wItemLength */
    120 & 0xFF, 120 >> 8,

    /* bLength */
    0X07,
    /* bDescriptorType */
    USBD_DESC_ENDPOINT,
    /* bEndpointAddress */
    HID_IN_EP,
    /* bmAttributes */
    0X03,
    /* wMaxPacketSize */
    0X40, 0X00,
    /* bInterval */
    0X05
};

/**
* [url=home.php?mod=space&uid=247401]@brief[/url]   HID report descriptor
*/
uint8_t g_hidMouseReportDescriptor[120] =
{
        0x05, 0x01,                    // USAGE_PAGE (Generic Desktop)
        0x09, 0x05,                    // USAGE (Game Pad)
        0xa1, 0x01,                    // COLLECTION (Application)
        0xa1, 0x00,                    //   COLLECTION (Physical)
        0x09, 0x30,                    //     USAGE (X)
        0x09, 0x31,                    //     USAGE (Y)
        0x15, 0x00,                    //     LOGICAL_MINIMUM (0)
        0x26, 0xff, 0x00,                    //     LOGICAL_MAXIMUM (255)
        0x35, 0x00,                    //     PHYSICAL_MINIMUM (0)
        0x46, 0xff, 0x00,                    //     PHYSICAL_MAXIMUM (255)
        0x95, 0x02,                    //     REPORT_COUNT (2)
        0x75, 0x08,                    //     REPORT_SIZE (8)
        0x81, 0x02,                    //     INPUT (Data,Var,Abs)
        0xc0,                          //     END_COLLECTION
        0xa1, 0x00,                    //   COLLECTION (Physical)
        0x09, 0x33,                    //     USAGE (Rx)
        0x09, 0x34,                    //     USAGE (Ry)
        0x15, 0x00,                    //     LOGICAL_MINIMUM (0)
        0x26, 0xff, 0x00,                    //     LOGICAL_MAXIMUM (255)
        0x35, 0x00,                    //     PHYSICAL_MINIMUM (0)
        0x46, 0xff, 0x00,                    //     PHYSICAL_MAXIMUM (255)
        0x95, 0x02,                    //     REPORT_COUNT (2)
        0x75, 0x08,                    //     REPORT_SIZE (8)
        0x81, 0x02,                    //     INPUT (Data,Var,Abs)
        0xc0,                          // END_COLLECTION
        0xa1, 0x00,                    //   COLLECTION (Physical)
        0x09, 0x32,                    //     USAGE (Z)
        0x15, 0x00,                    //     LOGICAL_MINIMUM (0)
        0x26, 0xff, 0x00,                    //     LOGICAL_MAXIMUM (255)
        0x35, 0x00,                    //     PHYSICAL_MINIMUM (0)
        0x46, 0xff, 0x00,                    //     PHYSICAL_MAXIMUM (255)
        0x95, 0x01,                    //     REPORT_COUNT (1)
        0x75, 0x08,                    //     REPORT_SIZE (8)
        0x81, 0x02,                    //     INPUT (Data,Var,Abs)
        0xc0,                          // END_COLLECTION
        0x05, 0x09,                    //   USAGE_PAGE (Button)
        0x19, 0x01,                    //   USAGE_MINIMUM (Button 1)
        0x29, 0x0a,                    //   USAGE_MAXIMUM (Button 10)
        0x95, 0x0a,                    //   REPORT_COUNT (10)
        0x75, 0x01,                    //   REPORT_SIZE (1)
        0x81, 0x02,                    //   INPUT (Data,Var,Abs)
        0x05, 0x01,                    //   USAGE_PAGE (Generic Desktop)
        0x09, 0x39,                    //   USAGE (Hat switch)
        0x15, 0x01,                    //   LOGICAL_MINIMUM (1)
        0x25, 0x08,                    //   LOGICAL_MAXIMUM (8)
        0x35, 0x00,                    //   PHYSICAL_MINIMUM (0)
        0x46, 0x3b, 0x10,              //   PHYSICAL_MAXIMUM (4155)
        0x66, 0x0e, 0x00,                    //   UNIT (None)
        0x75, 0x04,                    //   REPORT_SIZE (4)
        0x95, 0x01,                    //   REPORT_COUNT (1)
        0x81, 0x42,                    //   INPUT (Data,Var,Abs,Null)
        0x75, 0x02,                    //   REPORT_SIZE (2)
        0x95, 0x01,                    //   REPORT_COUNT (1)
        0x81, 0x03,                    //   INPUT (Cnst,Var,Abs)
        0x75, 0x08,                    //   REPORT_SIZE (8)
        0x95, 0x02,                    //   REPORT_COUNT (2)
        0x81, 0x03,                     //   INPUT (Cnst,Var,Abs)
      /* USER CODE END 0 */
      0xC0    /*     END_COLLECTION              */
};
我们定义的是9个字节的报文:在标准请求stdReqCallback的setConfiguration Callback Fuction里面需要修改一下,将最后一个参数改为9如下代码:
/*!
* [url=home.php?mod=space&uid=247401]@brief[/url]       Standard request set configuration call back
*
* @param       None
*
* @retval      None
*/
static void USBD_HID_SetConfigCallBack(void)
{
    USBD_OpenInEP(HID_IN_EP & 0x7F, USB_EP_TYPE_INTERRUPT, 9);
}
2、ADC DMA驱动:我们XYZ轴和Rx(X旋转)、Ry(Y旋转)的模拟输入采样ADC之DMA模式,关于ADC DMA的配置,我主要是参考了APM32F103的,因为都是M3内核,都是Geehy的,所以代码比较通用,这里参考了这篇帖子
https://bbs.21ic.com/icview-3217292-1-1.html
在此表示感谢!最后代码如下:
#define ADC1_DR_Address ((uint32_t)0x40012400+0x4c) /* ADC1数据寄存器地址(ADC基地址+偏移) */


uint16_t dma_buffer[5] = {0};   /* 存储DMA传输ADC数据的buffer */
//uint8_t dma_data_done_flag = 0; /* DMA传输完成中断标志 */
//uint8_t adc_int_eoc_flag = 0;   /* ADC转换完成中断标志 */

void RCM_Configuration(void)
{
    RCM_ConfigADCCLK(RCM_PCLK2_DIV_6); /* 6分频 72/6=12MHZ ADCCLK不能超过14MHZ*/
        
    RCM_EnableAPB2PeriphClock( RCM_APB2_PERIPH_GPIOC); /* 使能GPIO时钟 */
        RCM_EnableAHBPeriphClock(RCM_AHB_PERIPH_DMA1);     /* 使能DMA1时钟 */
    RCM_EnableAPB2PeriphClock(RCM_APB2_PERIPH_ADC1);   /* 使能ADC1时钟 */
}

void ADC_GPIO_Init(void)
{
    GPIO_Config_T GPIO_ConfigStruct;

        /* ADC_GPIO初始化 */
    GPIO_ConfigStruct.pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3|GPIO_PIN_4; /* 选择端口 分别对应ADC通道10 11 12 13 14*/
    GPIO_ConfigStruct.mode = GPIO_MODE_ANALOG;  /* IO工作方式 模拟输入*/
    GPIO_Config(GPIOC, &GPIO_ConfigStruct);
}
void ADC_Init(void)
{
    ADC_Config_T ADC_configStruct;
        
        ADC_Reset(ADC1); /* 复位ADC1 */
        /** ADC1 Configuration */
    ADC_configStruct.mode = ADC_MODE_INDEPENDENT;               /* ADC1工作在独立模式 */
    ADC_configStruct.scanConvMode = ENABLE;                     /* 使能扫描 */
        ADC_configStruct.continuosConvMode = ENABLE;                /* 使能ADC连续转换模式 轮询方式使用*/
//        ADC_configStruct.continuosConvMode = DISABLE;               /* 不使能ADC连续转换模式 中断方式使用*/
    ADC_configStruct.externalTrigConv = ADC_EXT_TRIG_CONV_None; /* 软件控制转换 */
    ADC_configStruct.dataAlign = ADC_DATA_ALIGN_RIGHT;          /* 转换数据右对齐 */
    ADC_configStruct.nbrOfChannel = 5;                          /* 顺序进行规则转换的ADC通道的数目 */
    ADC_Config(ADC1, &ADC_configStruct);                        /* 初始化ADC1寄存器 */

        /* 设置指定ADC的规则组通道,设置它们的转化顺序和采样时间 */
    ADC_ConfigRegularChannel(ADC1, ADC_CHANNEL_10, 1, ADC_SAMPLETIME_239CYCLES5); /* ADC1选择通道10 采样顺序1 采样时间13.5个周期 */
    ADC_ConfigRegularChannel(ADC1, ADC_CHANNEL_11, 2, ADC_SAMPLETIME_239CYCLES5); /* ADC1选择通道11 采样顺序2 采样时间13.5个周期 */
    ADC_ConfigRegularChannel(ADC1, ADC_CHANNEL_12, 3, ADC_SAMPLETIME_239CYCLES5); /* ADC1选择通道12 采样顺序3 采样时间13.5个周期 */
    ADC_ConfigRegularChannel(ADC1, ADC_CHANNEL_13, 4, ADC_SAMPLETIME_239CYCLES5); /* ADC1选择通道13 采样顺序4 采样时间13.5个周期 */
        ADC_ConfigRegularChannel(ADC1, ADC_CHANNEL_14, 5, ADC_SAMPLETIME_239CYCLES5); /* ADC1选择通道14 采样顺序5 采样时间13.5个周期 */

//        ADC_EnableInterrupt(ADC1, ADC_INT_EOC); /* 使能ADC转换完成中断 */
    ADC_EnableDMA(ADC1); /* 使能ADC的DMA支持 */
        ADC_Enable(ADC1);    /* 使能ADC1 */

        ADC_ResetCalibration(ADC1);                  /* 复位ADC1的校准寄存器 */
    while(ADC_ReadResetCalibrationStatus(ADC1)); /* 等待ADC1复位校准完成 */
    ADC_StartCalibration(ADC1);                  /* 开始ADC1校准 */
    while(ADC_ReadCalibrationStartFlag(ADC1));   /* 等待ADC1校准完成 */

    ADC_EnableSoftwareStartConv(ADC1); /* 启动ADC1转换 */
}

void DMA_Init(void)
{
    DMA_Config_T    DMA_ConfigStruct;

        DMA_Reset(DMA1_Channel1); /* 复位DMA1通道1 */

    DMA_ConfigStruct.peripheralBaseAddr = ADC1_DR_Address;         /* DMA通道外设基地址 */
    DMA_ConfigStruct.memoryBaseAddr = (uint32_t)dma_buffer;        /* DMA通道ADC数据存储器 */
    DMA_ConfigStruct.dir = DMA_DIR_PERIPHERAL_SRC;                 /* 指定外设为源地址 */
    DMA_ConfigStruct.bufferSize = 5;                               /* DMA缓冲区大小(根据ADC采集通道数量修改) */
    DMA_ConfigStruct.peripheralInc = DMA_PERIPHERAL_INC_DISABLE;   /* 当前外设寄存器地址不变(即不自增) */  
    DMA_ConfigStruct.memoryInc = DMA_MEMORY_INC_ENABLE;            /*  当前存储器地址:Disable不变,Enable递增(用于多通道采集) */
    DMA_ConfigStruct.peripheralDataSize = DMA_PERIPHERAL_DATA_SIZE_HALFWORD; /* 外设数据宽度16位 */
    DMA_ConfigStruct.memoryDataSize = DMA_MEMORY_DATA_SIZE_HALFWORD;         /* 存储器数据宽度16位 */
    DMA_ConfigStruct.loopMode = DMA_MODE_CIRCULAR; /* DMA通道操作模式位环形缓冲模式 */
    DMA_ConfigStruct.priority = DMA_PRIORITY_HIGH; /* DMA通道优先级高 */
    DMA_ConfigStruct.M2M = DMA_M2MEN_DISABLE;      /* 禁止DMA通道存储器到存储器传输 */
    DMA_Config(DMA1_Channel1, &DMA_ConfigStruct);

        //DMA_EnableInterrupt(DMA1_Channel1, DMA_INT_TC);
        DMA_Enable(DMA1_Channel1);
}


void MyADC_DMA_Init(void)
{
        RCM_Configuration();
        NVIC_EnableIRQRequest(ADC1_2_IRQn, 0, 0);
        ADC_GPIO_Init();
        DMA_Init();
        ADC_Init();
        
}

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

u8 X=128,Y=128,Z=128,Rx=128,Ry=128;

void MyADC_Handle(void)
{
        while(!ADC_ReadStatusFlag(ADC1, ADC_FLAG_EOC))
        {
                X=map(dma_buffer[0],0,4095,0,255);
                Y=map(dma_buffer[1],0,4095,0,255);
                Z=map(dma_buffer[2],0,4095,0,255);
                Rx=map(dma_buffer[3],0,4095,0,255);
                Ry=map(dma_buffer[4],0,4095,0,255);               
        }
        

//        
//        printf("ADC1采样数据:\r\n");
//        for (uint8_t i = 0; i < 5; i++) {
//                printf("ADC_CHANNEL_%d:%d\r\n", 10+i, dma_buffer[i]);
//        }
//        printf("\r\n");        
}

void GetAnalogValue(u8* x,u8* y,u8* z,u8* rx,u8* ry)
{
        x[0]=X;
        y[0]=Y;
        z[0]=Z;
        rx[0]=Rx;
        ry[0]=Ry;
}

3、开源软件的移植:
我们的单片机的系统架构移植了一个MultiTimer,这就是多个软件定时器,然后在软件定时器回调函数里面处理我们的
ADC DMA采集、按键扫描、以及Usb HID数据的处理,另外一个是MultiButton,这两个开源软件都是同一作者,并且都是在GitHub上
开源了:可以通过这两个链接获取:
https://github.com/0x1abin/MultiButton
https://github.com/0x1abin/MultiTimer
移植MultiTimer需要配置一个时钟滴答,这里和大多数RTOS一样选择Systick,最后移植代码如下:
#include <stdio.h>

MultiTimer timer1;
MultiTimer timer2;
MultiTimer timer3;
volatile uint32_t tick = 0;

uint64_t PlatformTicksGetFunc(void)
{

    return (uint64_t)tick;
}



void ButtonTimerCallback(MultiTimer* timer, void *userData)
{
        button_ticks();        
    MultiTimerStart(timer, 5, ButtonTimerCallback, userData);
}

void ADCTimerCallback(MultiTimer* timer, void *userData)
{
    MyADC_Handle();               
    MultiTimerStart(timer, 5, ADCTimerCallback, userData);
}

void ReportTimerCallback(MultiTimer* timer, void *userData)
{
    USBD_HID_Proc();               
    MultiTimerStart(timer, 10, ReportTimerCallback, userData);
}

void MyBSP_Init(void)
{
        MyADC_DMA_Init();

        MyButton_Init();

    SysTick_Config(SystemCoreClock / 1000);
        MultiTimerInstall(PlatformTicksGetFunc);
    MultiTimerStart(&timer1, 5, ButtonTimerCallback, NULL);
        MultiTimerStart(&timer2, 5, ADCTimerCallback, NULL);
        MultiTimerStart(&timer3, 10, ReportTimerCallback, NULL);
}
还有就是MultiButton,这是一个事件型驱动的软件模块,而它同样需要一个5ms的按键扫描,这里直接使用MultiTimer中的Timer1的回调函数ButtonTimerCallback,如上代码,然后关于怎样使用MultiButton,,由于篇幅原因,这里不看代码了,因为作者的使用说明写的非常详细,这里就看一下我们在按键动作的回调函数中做了什么动作:
void BTN1_PRESS_DOWN_Handler(void* btn)
{
        button|=BIT0;
        printf("BTN1_PRESS_DOWN_Handler\r\n");
}

void BTN1_PRESS_UP_Handler(void* btn)
{
        button&=(~BIT0);
        printf("BTN1_PRESS_UP_Handler\r\n");
}
如上代码,我们只是在按下和弹起的时候对按键置位了,这真是对应报表描述符中对应的button1~10。不过我们还定义了PE7~PE10也作为按键(视觉头盔)输入,我们一起扫描了,所以这里定义button是uint16类型的,这里用了14bit表示这些按键的扫描动作。


4、USB HID的数据解析:

timer3回调函数中处理的正是ADC数据、按键数据转成XYZ轴和Rx(X旋转)、Ry(Y旋转),以及带有Button1~Button10共10颗按钮以及视觉头盔的过程,以及如何将数据上报给主机的,我们直接看USBD_HID_Proc:
void USBD_HID_Proc(void)
{
        uint16_t button=0;
        uint16_t Hat=0;
        
        uint8_t Buf[9]={128,128,128,128,128,0,0,0,0};
        static uint8_t lastBuf[9]={0,0,0,0,0,0,0,0};

    /* Check the usb device configured state  */
    if(g_usbDev.devState != USBD_DEVICE_STATE_CONFIGURED)
    {
        return;
    }

        GetAnalogValue(&Buf[0],&Buf[1],&Buf[2],&Buf[3],&Buf[4]);
        
    button = GetButtonValue();
        
        Buf[5]=button&0xFF;
        Buf[6]|=((button>>8)&0x03);
        
    //Hat
        Hat=(button>>10)&0x0F;
        if((Hat==0x01)||(Hat==0x0D))
        {
                Buf[6]|=HATSW1;        
        }else if(Hat==0x09)
        {
                Buf[6]|=HATSW2;        
        }else if((Hat==0x08)||(Hat==0x0B))
        {
                Buf[6]|=HATSW3;        
        }else if(Hat==0x0A)
        {
                Buf[6]|=HATSW4;        
        }else if((Hat==0x02)||(Hat==0x0E))
        {
                Buf[6]|=HATSW5;        
        }else if(Hat==0x06)
        {
                Buf[6]|=HATSW6;        
        }else if((Hat==0x04)||(Hat==0x07))
        {
                Buf[6]|=HATSW7;        
        }else if(Hat==0x05)
        {
                Buf[6]|=HATSW8;        
        }else{
                Buf[6]&=(~0x3C);
        }
        
        


        
    if(memcmp(Buf,lastBuf,9)!=0)
    {
                if(s_statusEP)
                {
                        s_statusEP = 0;
                        USBD_TxData(USB_EP_1, Buf, 9);
                }
    }

    memcpy(lastBuf,Buf,9);
        
}


三、实现的效果:
效果就是第一张图,

不过我们有录制视频了,接下来一起看一下最后DIY的效果

使用特权

评论回复
沙发
forgot| | 2023-6-28 17:06 | 只看该作者
感谢楼主的分享,很全面,学习一下,期待更多好的内容

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

54

主题

162

帖子

4

粉丝