打印
[单片机芯片]

基于RISC-V CH32V103的鼠标键盘摇杆手柄Joystick学习开发

[复制链接]
1624|11
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
本帖最后由 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键也可以组合;




使用特权

评论回复
沙发
Litthins| | 2022-3-30 13:49 | 只看该作者
看起来很不错,我最近也打算做一个。

使用特权

评论回复
板凳
andygirl| | 2022-4-2 17:21 | 只看该作者
这个好啊~

使用特权

评论回复
地板
caigang13| | 2022-4-3 10:44 | 只看该作者
不错,空了试试。

使用特权

评论回复
5
wyz6| | 2022-4-24 16:53 | 只看该作者
楼主,资料怎么下不了 了?我想看看手柄的描述符资料,哪里可以看到?

使用特权

评论回复
6
lilijin1995|  楼主 | 2022-4-26 19:55 | 只看该作者
wyz6 发表于 2022-4-24 16:53
楼主,资料怎么下不了 了?我想看看手柄的描述符资料,哪里可以看到?

链接更新了,可以下了

使用特权

评论回复
7
benjaminka| | 2022-5-28 15:23 | 只看该作者
使用什么通信的呢?

使用特权

评论回复
8
saservice| | 2022-5-28 17:33 | 只看该作者
板子很给力呢。   

使用特权

评论回复
9
isseed| | 2022-5-29 08:45 | 只看该作者
这个板子做的好看。  

使用特权

评论回复
10
lilijin1995|  楼主 | 2022-5-30 09:16 | 只看该作者
benjaminka 发表于 2022-5-28 15:23
使用什么通信的呢?

USB hid

使用特权

评论回复
11
lilijin1995|  楼主 | 2022-5-30 09:17 | 只看该作者
isseed 发表于 2022-5-29 08:45
这个板子做的好看。

谢谢

使用特权

评论回复
12
lilijin1995|  楼主 | 2022-5-30 09:17 | 只看该作者

谢谢

使用特权

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

本版积分规则

54

主题

162

帖子

4

粉丝