本帖最后由 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的效果
|