本帖最后由 lilijin1995 于 2022-6-7 16:29 编辑
#申请原创# @21小跑堂 @21小跑堂 @21小跑堂
基于RISC-V CH32V103的鼠标键盘摇杆手柄Joystick学习开发
资料还在持续更新中,评估学习板已经上架,欢迎入手体验,
链接:https://pan.baidu.com/s/1418gLVJf8eAh52CYGSNUaQ?pwd=fkr6 提取码:fkr6 入手链接:https://item.taobao.com/item.htm?ft=t&id=632428000614
在线文档:https://www.cnblogs.com/Li-Share/p/15898572.html
B站视频教程:https://www.bilibili.com/video/BV17r4y1z758/ 第一部分、硬件概述
1.1 实物概图如上图所示,配置了8个6*6轻触按键,一个摇杆(Joystick),搭载一颗WS2812B灯珠; 并将UART1串口,编程接口(SWD),外接Joystick接口,microUSB接口引出; 左边是RKJXV1224005摇杆电位器,右边和下方是8颗66的轻触按键,右上方是5050封装的WS2812B灯珠,中间是microusb母座, H3是SWD烧录接口,烧录程序接口,H2是串口,H1是外接摇杆模块的接口;
1.2 Gamepad原理图
Gamepad原理图如图1.2所示,如看不清可打开Doc目录下的PDF文档查阅 第二部分、软件工具2.1 软件概述
在 /Software 目录下是常用的工具软件: 1. Dt2_4:配置USB设备Report描述符的工具;
2. USBHID调试助手/呀呀USB: USB调试工具,相当于串口调试助手功能;
3. BUSHound:总线调试工具;
4. USBlyzer:一款专业的USB协议分析软件
5. MounRiver: 编译器;2.2 MounRiver软件入门大家访问以下链接: http://mounriver.com/help
第三部分、实战训练3.1
实例Eg1_GamePad本节我们目标是实现GamePad的功能,枚举成XY轴的平面坐标和8个按键的USB HID类设备。 3.1.1硬件设计如上图是Joystick原理图,其中VRX1与VRY1是摇杆的电位器输出的电压信号(ADC检测); SW1则是按键,右侧H1是外接的Joystick口,供接joystick模块使用; 如上图是KEY原理图,我们只要配置8个GPIO作为输入去检测按键信号;
3.1.2 软件设计
首先是工程树,我们打开工程,可以看到Project Explorer下Gamepad目录如下图 其中 Binaries: 二进制文件; Includes: 包含的头文件; Core:内核文件,存放core_riscv内核文件; Debug: 存放串口打印和延迟函数相关的文件; myBSP: 我们自己编写的驱动文件; obj: 编译的生成的obj文件; Peripheral: 这是MCU厂商提供外设相关驱动; Startup: ch32v103的启动文件; User: ch32v103的配置文件,中断相关文件,main函数等;
工程目录这里只做一次介绍,后面的样例目录大同小异。我们先打开startup_ch32v10x.S启动文件,我们看到如下代码 jal SystemInit
la t0, main定位到SystemInit <font face="仿宋, 仿宋_GB2312">void SystemInit (void)
{
RCC->CTLR |= (uint32_t)0x00000001;
RCC->CFGR0 &= (uint32_t)0xF8FF0000;
RCC->CTLR &= (uint32_t)0xFEF6FFFF;
RCC->CTLR &= (uint32_t)0xFFFBFFFF;
RCC->CFGR0 &= (uint32_t)0xFF80FFFF;
RCC->INTR = 0x009F0000;
SetSysClock();
}</font>
关于RCC寄存器的配置,请各位自行查阅用户手册;我们接着打开SetSysClock函数 static void SetSysClock(void)
{
#ifdef SYSCLK_FREQ_HSE
SetSysClockToHSE();
#elif defined SYSCLK_FREQ_24MHz
SetSysClockTo24();
#elif defined SYSCLK_FREQ_48MHz
SetSysClockTo48();
#elif defined SYSCLK_FREQ_56MHz
SetSysClockTo56();
#elif defined SYSCLK_FREQ_72MHz
SetSysClockTo72();
#endif
/* If none of the define above is enabled, the HSI is used as System clock
* source (default after reset)
*/
}</font>
由于我们定义了SYSCLK_FREQ_72MHz,SetSysClockTo72这个函数设置了系统时钟为72M; 接下来我们来看看main函数,如下 int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
Delay_Init();
USART_Printf_Init(115200);
printf("SystemClk:%d\r\n",SystemCoreClock);
printf("USBHD Device Test\r\n");
pEP0_RAM_Addr = EP0_Databuf;
pEP1_RAM_Addr = EP1_Databuf;
pEP2_RAM_Addr = EP2_Databuf;
USBHD_ClockCmd(RCC_USBCLKSource_PLLCLK_1Div5,ENABLE);
USB_DeviceInit();
NVIC_EnableIRQ( USBHD_IRQn );
ADC_DMA_CONF();
KEY_INIT();
while(1)
{
printf("X=%d,Y=%d\r\n",ADC_ConvertedValue[0],ADC_ConvertedValue[1]);
if(Ready)
{
Gp_SendReport();
}
}
}</font>
NVIC_PriorityGroupConfig是配置优先级分组的,Delay_Init初始化延迟函数;USART2_Printf_Init初始化串口打印, pEP0_RAM_Addr = EP0_Databuf; pEP1_RAM_Addr = EP1_Databuf; pEP2_RAM_Addr = EP2_Databuf; 主要配置端点0~2的缓存Ram; USBHD_ClockCmd(RCC_USBCLKSource_PLLCLK_1Div5,ENABLE);配置系统时钟1.5分频,即48M; USB_DeviceInit(),是对usb设备进行初始化; ADC_DMA_CONF主要对ADC DMA进行配置; KEY_INIT是对按键所对应的GPIO进行初始化; 接着是Gp_SendReport,主要是讲处理并上报坐标和按键数据; void Gp_SendReport(void)
{
memset(Joystick_Buf,0,3);
Ytemp=ADC_ConvertedValue[0];
Xtemp=ADC_ConvertedValue[1];
if(Xtemp>Xmax)
Xtemp=Xmax;
if(Xtemp<Xmin)
Xtemp=Xmin;
if(Ytemp>=Ymax)
Ytemp=Ymax;
if(Ytemp<=Ymin)
Ytemp=Ymin;
printf("Xmax=%x,Xcen=%x,Xmin=%x\r\n",Xmax,Xtemp,Xmin);
printf("Ymax=%x,Ycen=%x,Ymin=%x\r\n",Ymax,Ytemp,Ymin);
//根据坐标极点确定坐标(两点直线方程)
X=((Xtemp-Xmin)*255)/(Xmax-Xmin);
Y=((Ytemp-Ymin)*255)/(Ymax-Ymin);
Joystick_Buf[0]=X;
Joystick_Buf[1]=Y;
Joystick_Buf[2]=Key_Scan();
Delay_Ms(10);
while( Endp1Busy )//如果忙(上一包数据没有传上去),则等待。
{ ; }
Endp1Busy = 1; //设置为忙状态
memcpy(pEP1_IN_DataBuf, Joystick_Buf, 3);
DevEP1_IN_Deal(3);
} </font>
最后我们再来看看USBHD_IRQHandler,我们在这个函数值调用了USB_DevTransProcess。 3.1.3 下载验证
我们把固件程序下载进去可以,打开“设备与打印机”可以看到USB设备枚举成了一个Gamepad,如下图。 右键打开游戏控制器后,点击属性得到下图所示界面 我们可以摇Joystick和按按键可以发现上图游戏控制器界面也跟着响应。
3.2 实例Eg2_Mouse
本节我们目标是实现模拟鼠标的功能,枚举一个具有XY,左右中键以及滚轮上下的功能; 3.2.1硬件设计
同上一章节
3.2.2 软件设计
在上一章节的基础上,我们在USB_DevTransProcess中找到报告描述符的获取,并修改为如下内容 case USB_DESCR_TYP_REPORT:
if(((pSetupReqPak->wIndex)&0xff) == 0) //接口0报表描述符
{
pDescr = MouseRepDesc; //数据准备上传
len = sizeof(MouseRepDesc);
Ready = 1; //如果有更多接口,该标准位应该在最后一个接口配置完成后有效
}
else len = 0xff; //本程序只有2个接口,这句话正常不可能执行
break;</font>
另外鼠标的报告描述符MouseRepDesc如下 const UINT8 MouseRepDesc[]=
{
0x05, 0x01, // USAGE_PAGE (Generic Desktop)
0x09, 0x02, // USAGE (Mouse)
0xa1, 0x01, // COLLECTION (Application)
0x09, 0x01, // USAGE (Pointer)
0xa1, 0x00, // COLLECTION (Physical)
0x05, 0x09, // USAGE_PAGE (Button)
0x19, 0x01, // USAGE_MINIMUM (Button 1)
0x29, 0x03, // USAGE_MAXIMUM (Button 3)
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x25, 0x01, // LOGICAL_MAXIMUM (1)
0x95, 0x03, // REPORT_COUNT (3)
0x75, 0x01, // REPORT_SIZE (1)
0x81, 0x02, // INPUT (Data,Var,Abs)
0x95, 0x01, // REPORT_COUNT (1)
0x75, 0x05, // REPORT_SIZE (5)
0x81, 0x03, // INPUT (Cnst,Var,Abs)
0x05, 0x01, // USAGE_PAGE (Generic Desktop)
0x09, 0x30, // USAGE (X)
0x09, 0x31, // USAGE (Y)
0x09, 0x38, // USAGE (Wheel)
0x15, 0x81, // LOGICAL_MINIMUM (-127)
0x25, 0x7f, // LOGICAL_MAXIMUM (127)
0x75, 0x08, // REPORT_SIZE (8)
0x95, 0x03, // REPORT_COUNT (3)
0x81, 0x06, // INPUT (Data,Var,Rel)
0xc0, // END_COLLECTION
0xc0 // END_COLLECTION
};</font>
然后是报文数据的处理如下: //处理并上报数据
void Gp_SendReport(void)
{
memset(Joystick_Buf,0,4);
Ytemp=ADC_ConvertedValue[0];
Xtemp=ADC_ConvertedValue[1];
if(Xtemp>Xmax)
Xtemp=Xmax;
if(Xtemp<Xmin)
Xtemp=Xmin;
if(Ytemp>=Ymax)
Ytemp=Ymax;
if(Ytemp<=Ymin)
Ytemp=Ymin;
printf("Xmax=%x,Xcen=%x,Xmin=%x\r\n",Xmax,Xtemp,Xmin);
printf("Ymax=%x,Ycen=%x,Ymin=%x\r\n",Ymax,Ytemp,Ymin);
//根据坐标极点确定坐标(两点直线方程)
X=((Xtemp-Xmin)*255)/(Xmax-Xmin);
Y=((Ytemp-Ymin)*255)/(Ymax-Ymin);
if(X>(X_BASE+20))
{
Joystick_Buf[1]=((X-X_BASE)>>DIV)+1;
}
if(X<(X_BASE-20))
{
Joystick_Buf[1]=(u8)-(((X_BASE-X)>>DIV)+1);
}
if(Y>(Y_BASE+20))
{
Joystick_Buf[2]=((Y-Y_BASE)>>DIV)+1;;;
}
if(Y<(Y_BASE-20))
{
Joystick_Buf[2]=(u8)-(((Y_BASE-Y)>>DIV)+1);
}
Key_Handle(Joystick_Buf);
Delay_Ms(5);
while( Endp1Busy )
{
; //如果忙(上一包数据没有传上去),则等待。
}
Endp1Busy = 1; //设置为忙状态
memcpy(pEP1_IN_DataBuf, Joystick_Buf, 4);
DevEP1_IN_Deal(4);
} </font>
其中X_BASE为摇杆中点, if(X>(X_BASE+20))就是摇杆左摇动;故而((X-X_BASE)>>DIV)+1计算赋值给我们我们X+坐标;其他方向同理,另外Key_Handle的代码如下,主要是为了处理按键与滚轮值 void Key_Handle(uint8_t* kv)
{
if((LFKEY)==Bit_RESET)
{
kv[0]|=0x01;
}
if((RGKEY)==Bit_RESET)
{
kv[0]|=0x02;
}
if(SW1!=Bit_RESET)
{
kv[0]|=0x04;
}
if((UPKEY)==Bit_RESET)
{
if(c_tick++>5)
{
kv[3]=1;
c_tick=0;
}
}
if((DNKEY)==Bit_RESET)
{
if(c_tick++>5)
{
kv[3]=(u8)-1;
c_tick=0;
}
}
}</font>
3.2.3 下载验证我们把固件程序下载进去可以,打开“设备与打印机”可以看到USB设备枚举成了一个“LD Mouse”,如下图。 我们打开一个网页,摇动摇杆鼠标指针跟着动;右边上下左右键,左右代表鼠标左右键,上下代表滚轮;然后摇杆中键代表鼠标中键;
3.3 实例Eg3_KeyBoard
本节我们目标是实现模拟键盘的功能,枚举一个具有Shift键+1~8键的模拟键盘功能; 3.3.1硬件设计同第一章节
3.3.2 软件设计在上一章节的基础上,我们在USB_DevTransProcess中找到报告描述符的获取,并修改为如下内容 case USB_DESCR_TYP_REPORT:
if(((pSetupReqPak->wIndex)&0xff) == 0) //接口0报表描述符
{
pDescr = KeyboardRepDesc; //数据准备上传
len = sizeof(KeyboardRepDesc);
Ready = 1; //如果有更多接口,该标准位应该在最后一个接口配置完成后有效
}
else len = 0xff; //本程序只有2个接口,这句话正常不可能执行
break;</font>
另外Keyboard的报告描述符KeyboardRepDesc如下 const UINT8 MouseRepDesc[]=
{
0x05, 0x01, // USAGE_PAGE (Generic Desktop)
0x09, 0x06, // USAGE (Keyboard)
0xa1, 0x01, // COLLECTION (Application)
0x05, 0x07, // USAGE_PAGE (Keyboard)
0x19, 0xe0, // USAGE_MINIMUM (Keyboard LeftControl)
0x29, 0xe7, // USAGE_MAXIMUM (Keyboard Right GUI)
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x25, 0x01, // LOGICAL_MAXIMUM (1)
0x75, 0x01, // REPORT_SIZE (1)
0x95, 0x08, // REPORT_COUNT (8)
0x81, 0x02, // INPUT (Data,Var,Abs)
0x95, 0x01, // REPORT_COUNT (1)
0x75, 0x08, // REPORT_SIZE (8)
0x81, 0x03, // INPUT (Cnst,Var,Abs)
0x95, 0x05, // REPORT_COUNT (5)
0x75, 0x01, // REPORT_SIZE (1)
0x05, 0x08, // USAGE_PAGE (LEDs)
0x19, 0x01, // USAGE_MINIMUM (Num Lock)
0x29, 0x05, // USAGE_MAXIMUM (Kana)
0x91, 0x02, // OUTPUT (Data,Var,Abs)
0x95, 0x01, // REPORT_COUNT (1)
0x75, 0x03, // REPORT_SIZE (3)
0x91, 0x03, // OUTPUT (Cnst,Var,Abs)
0x95, 0x06, // REPORT_COUNT (6)
0x75, 0x08, // REPORT_SIZE (8)
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x25, 0xFF, // LOGICAL_MAXIMUM (255)
0x05, 0x07, // USAGE_PAGE (Keyboard)
0x19, 0x00, // USAGE_MINIMUM (Reserved (no event indicated))
0x29, 0x65, // USAGE_MAXIMUM (Keyboard Application)
0x81, 0x00, // INPUT (Data,Ary,Abs)
0xC0 // END_COLLECTION
};</font>
然后是报文数据的处理如下: uint8_t Keyboad_Buf[8]={0};
uint8_t lastshift=0,currentshift=0;
uint8_t lastkeycode[8]={0},currentkeycode[8]={0};
static uint8_t KdataFL=0;
//处理并上报数据
void Keyboard_Handle(void)
{
memset(Keyboad_Buf,0,8);
uint8_t i=0;uint8_t idx=2;
if(SW1==1)
{
currentshift|=0x02;
Keyboad_Buf[0]=currentshift;
}else{
currentshift&=(~0x02);
Keyboad_Buf[0]=currentshift;
}
if(UPKEY==0)
{
currentkeycode[0]=CODE1;
}else{
currentkeycode[0]=0x00;
}
if(DNKEY==0)
{
currentkeycode[1]=CODE2;
}else{
currentkeycode[1]=0x00;
}
if(LFKEY==0)
{
currentkeycode[2]=CODE3;
}else{
currentkeycode[2]=0x00;
}
if(RGKEY==0)
{
currentkeycode[3]=CODE4;
}else{
currentkeycode[3]=0x00;
}
if(BKKEY==0)
{
currentkeycode[4]=CODE5;
}else{
currentkeycode[4]=0x00;
}
if(MDKEY==0)
{
currentkeycode[5]=CODE6;
}else{
currentkeycode[5]=0x00;
}
if(STKEY==0)
{
currentkeycode[6]=CODE7;
}else{
currentkeycode[6]=0x00;
}
if(TBKEY==0)
{
currentkeycode[7]=CODE8;
}else{
currentkeycode[7]=0x00;
}
for(i=0;i<8;i++)
{
if(currentkeycode!=lastkeycode)
{
Keyboad_Buf[idx]=currentkeycode;
if(++idx>=8)
{
idx=2;
}
KdataFL=1;
}else{
Keyboad_Buf[idx]=0x00;
}
}
if(currentshift!=lastshift)
{
KdataFL=1;
}
if(KdataFL!=0)
{
KdataFL=0;
while( Endp1Busy )
{
; //如果忙(上一包数据没有传上去),则等待。
}
Endp1Busy = 1; //设置为忙状态
memcpy(pEP1_IN_DataBuf, Keyboad_Buf, 8);
DevEP1_IN_Deal(8);
}
Delay_Ms(5);
memcpy(lastkeycode,currentkeycode,8);
lastshift=currentshift;
}</font>
最后是main函数,只改了while中的Keyboard_Handle(); /*******************************************************************************
* Function Name : main
* Description : Main program.
* Input : None
* Return : None
*******************************************************************************/
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
Delay_Init();
USART_Printf_Init(115200);
printf("SystemClk:%d\r\n",SystemCoreClock);
printf("USBHD Device Test\r\n");
pEP0_RAM_Addr = EP0_Databuf;
pEP1_RAM_Addr = EP1_Databuf;
pEP2_RAM_Addr = EP2_Databuf;
USBHD_ClockCmd(RCC_USBCLKSource_PLLCLK_1Div5,ENABLE);
USB_DeviceInit();
NVIC_EnableIRQ( USBHD_IRQn );
ADC_DMA_CONF();
KEY_INIT();
while(1)
{
printf("X=%d,Y=%d\r\n",ADC_ConvertedValue[0],ADC_ConvertedValue[1]);
if(Ready)
{
Keyboard_Handle();
}
}
}
3.3.3 下载验证我们把固件程序下载进去可以,打开“设备与打印机”可以看到USB设备枚举成了一个“LD Keyboard”,如下图。 我们打开一个键盘测试网页,地址如下: https://keyboard.bmcx.com/ 按摇杆按键SW1即为shift键按下,其他键分别对应主键盘的1~8;shift+1-8键也可以组合;
|