MENU

STM32 USB HID 鼠标键盘 复合设备

• February 16, 2015 • Read: 6019 • 折腾

因为做过空中鼠标,外面有个项目,需要结合鼠标和键盘,在查阅了一定资料后,成功移植了复合设备,可以用一个USB设备同时让电脑识别为一个键盘和一个鼠标,和现在市面上买的鼠键套装功能差不多!

1.更改配置描述符

在usb_desc.c中.这是我使用的配置描述符,第一个接口是键盘功能,第二个接口是鼠标功能。

const u8 Joystick_ConfigDescriptor[JOYSTICK_SIZ_CONFIG_DESC] =
{
 /***************配置描述符***********************/
 0x09,  //bLength字段
 USB_CONFIGURATION_DESCRIPTOR_TYPE,  //bDescriptorType字段
 //wTotalLength字段
 JOYSTICK_SIZ_CONFIG_DESC,
 /* wTotalLength: Bytes returned */
 0x00,
 
 0x02, //bNumInterfaces字段
 0x01, //bConfiguration字段
 0x00, //iConfigurationz字段
 0x80, //bmAttributes字段
 0x32, //bMaxPower字段
 
 /*******************第一个接口描述符*********************/
 0x09, //bLength字段
 0x04, //bDescriptorType字段
 0x00, //bInterfaceNumber字段
 0x00, //bAlternateSetting字段
 0x02, //bNumEndpoints字段
 0x03, //bInterfaceClass字段
 0x01, //bInterfaceSubClass字段
 0x01, //bInterfaceProtocol字段
 0x00, //iConfiguration字段
 
 /******************HID描述符************************/
 0x09, //bLength字段
 0x21, //bDescriptorType字段
 0x10, //bcdHID字段
 0x01,
 0x21, //bCountyCode字段
 0x01, //bNumDescriptors字段
 0x22, //bDescriptorType字段
 
 //bDescriptorLength字段。
 //下级描述符的长度。下级描述符为键盘报告描述符。
 sizeof(KeyboardReportDescriptor)&0xFF,
 (sizeof(KeyboardReportDescriptor)>>8)&0xFF,
 
 /**********************输入端点描述符***********************/
 0x07, //bLength字段
 0x05, //bDescriptorType字段
 0x81, //bEndpointAddress字段
 0x03, //bmAttributes字段
 0x10, //wMaxPacketSize字段
 0x00,
 0x0A, //bInterval字段
 
 /**********************输出端点描述符***********************/
 0x07, //bLength字段
 0x05, //bDescriptorType字段
 0x01, //bEndpointAddress字段
 0x03, //bmAttributes字段
 0x10, //wMaxPacketSize字段
 0x00,
 0x0A, //bInterval字段
 
 /*******************第二个接口描述符*********************/
 0x09, //bLength字段
 0x04, //bDescriptorType字段
 0x01, //bInterfaceNumber字段
 0x00, //bAlternateSetting字段
 0x01, //bNumEndpoints字段
 0x03, //bInterfaceClass字段
 0x01, //bInterfaceSubClass字段
 0x02, //bInterfaceProtocol字段
 0x00, //iConfiguration字段
 
 /******************HID描述符************************/
 0x09, //bLength字段
 0x21, //bDescriptorType字段
 0x10, //bcdHID字段
 0x01,
 0x21, //bCountyCode字段
 0x01, //bNumDescriptors字段
 0x22, //bDescriptorType字段
 sizeof(MouseReportDescriptor)&0xFF,  //bDescriptorLength字段
 (sizeof(MouseReportDescriptor)>>8)&0xFF,
 
 /**********************输入端点描述符***********************/
 0x07, //bLength字段
 0x05, //bDescriptorType字段
 0x82, //bEndpointAddress字段
 0x03, //bmAttributes字段。D1~D0为端点传输类型选择
 0x40, //wMaxPacketSize字段
 0x00,
 0x0A  //bInterval字段
};

2.写键盘和鼠标报告描述符

//USB键盘报告描述符的定义
const u8 KeyboardReportDescriptor[KP_ReportDescriptor_Size]=
{
    0x05, 0x01,                    // USAGE_PAGE (Generic Desktop) //63
    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, 0x65,                    //   LOGICAL_MAXIMUM (101)
    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
//0xc0,
  }; /* Joystick_ReportDescriptor */
///////////////////////////键盘报告描述符完毕////////////////////////////
 
//USB鼠标报告描述符的定义
const u8 MouseReportDescriptor[Mouse_ReportDescriptor_Size]=
{
 0x05, 0x01, // USAGE_PAGE (Generic Desktop)
 0x09, 0x02, // USAGE (Mouse)
 0xa1, 0x01, // COLLECTION (Application)
 0x85, 0x01, //Report ID (1)
 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
};

以上是usb_desc.c中的修改,接着是需要修改usb_desc.h中的宏定义

3.重新定义配置描述符的大小

由于我们用了 新的配置描述符,所以,我们需要重新定义配置描述符的大小

#define JOYSTICK_SIZ_CONFIG_DESC                66

4.定义键盘报告描述符的大小

由于我们用了 键盘鼠标 报告 描述符,所以,我们需要定义键盘报告描述符的大小

    #define KP_ReportDescriptor_Size    63
    #define Mouse_ReportDescriptor_Size    54

5.报告描述符的大小

但是我们还有增加 键盘鼠标报告描述符 的定义

    extern const u8 KeyboardReportDescriptor[KP_ReportDescriptor_Size];
    extern const u8 MouseReportDescriptor[Mouse_ReportDescriptor_Size];

(如果有不用的报告描述符,就给删掉吧)

6.更改prop代码

好,接着让我们更改usb_prop.c中的代码, 我们可以在usb_prop.c中找到 类似一下的代码

ONE_DESCRIPTOR Joystick_Report_Descriptor =  //用于获得报告描述符
  {
(u8 *)Joystick_ReportDescriptor,
    JOYSTICK_SIZ_REPORT_DESC
};
 
ONE_DESCRIPTOR Mouse_Hid_Descriptor =          //用于获得配置描述符中的Hid描述
  {
    (u8*)Joystick_ConfigDescriptor + JOYSTICK_OFF_HID_DESC, //
    JOYSTICK_SIZ_HID_DESC
  };
//JOYSTICK_OFF_HID_DESC是HID描述在配置描述符中的偏移量

由于我们更改了报告描述符 和 配置描述符,所以该处应该修改,并且我们有两个报告描述符(鼠标+键盘),也有两个HID描述(在配置描述符中),所以,这里一共要有4段代码

ONE_DESCRIPTOR KP_Report_Descriptor =           //
  {                 //
    (u8 *)KeyboardReportDescriptor,         //
    KP_ReportDescriptor_Size          //
  };                //
//
ONE_DESCRIPTOR KP_Hid_Descriptor =         //
  {                 //
    (u8*)Joystick_ConfigDescriptor + KP_OFF_HID_DESC,    //
    JOYSTICK_SIZ_HID_DESC           //
  };                //
//
ONE_DESCRIPTOR Mouse_Report_Descriptor =       //
  {                 //
    (u8 *)MouseReportDescriptor,         //
    Mouse_ReportDescriptor_Size          //
  };                //
//
ONE_DESCRIPTOR Mouse_Hid_Descriptor =        //
  {                 //
    (u8*)Joystick_ConfigDescriptor + Mouse_OFF_HID_DESC,   //
    JOYSTICK_SIZ_HID_DESC           //
  };

我们上面Mouse_OFF_HID_DESC 和 KP_OFF_HID_DESC分别是鼠标HID描述符,在配置描述符中的偏移量(也就是在配置描述符中的位置),以及键盘HID描述符在配置描述符中的偏移量,所以,我们回到usb_desc.h中,定义一下

    #define KP_OFF_HID_DESC       18
    #define Mouse_OFF_HID_DESC      50

呵呵,可以数来验证一下这个偏移是否正确

7.更改Reset函数

回到usb_prop.c中继续更改,找到void Joystick_Reset(void)函数

由于配置描述符中,使用到了两个端点,端点1的收发(用于键盘),端点2的输入(用于鼠标,输入输出 是对于PC机的而言, 输入也就是单片机输入电脑)

将函数更改为一下代码

void Joystick_Reset(void)
{
  /* Set Joystick_DEVICE as not configured */
  pInformation->Current_Configuration = 0;
  pInformation->Current_Interface = 0;/*the default Interface*/
 
  /* Current Feature initialization */
  pInformation->Current_Feature = Joystick_ConfigDescriptor[7];
 
  SetBTABLE(BTABLE_ADDRESS);
 
  /* Initialize Endpoint 0 */
  SetEPType(ENDP0, EP_CONTROL);
  SetEPTxStatus(ENDP0, EP_TX_STALL);
  SetEPRxAddr(ENDP0, ENDP0_RXADDR);
  SetEPTxAddr(ENDP0, ENDP0_TXADDR);
  Clear_Status_Out(ENDP0);
  SetEPRxCount(ENDP0, Device_Property.MaxPacketSize);
  SetEPRxValid(ENDP0);
 
  /* Initialize Endpoint In 1 */
  SetEPType(ENDP1, EP_INTERRUPT); //初始化为中断端点类型
  SetEPTxAddr(ENDP1, ENDP1_TXADDR); //设置发送数据的地址
  SetEPTxCount(ENDP1, 8); //设置发送的长度
  SetEPTxStatus(ENDP1, EP_TX_NAK); //设置端点处于忙状态
 
  /* Initialize Endpoint Out 1 */
  SetEPRxAddr(ENDP1, ENDP1_RXADDR); //设置接收数据的地址
  SetEPRxCount(ENDP1, 2);  //设置接收长度
  SetEPRxStatus(ENDP1, EP_RX_VALID); //设置端点有效,可以接收数据
 
  /* Initialize Endpoint In 2 */
  SetEPType(ENDP2, EP_INTERRUPT); //初始化为中断端点类型
  SetEPTxAddr(ENDP2, ENDP2_TXADDR); //设置发送数据的地址
  SetEPTxCount(ENDP2, 5); //设置发送的长度
  SetEPTxStatus(ENDP2, EP_TX_NAK); //设置端点处于忙状态
 
  bDeviceState = ATTACHED;
 
  /* Set this device to response on default address */
  SetDeviceAddress(0);
}

由于使用了端点,我们到usb_conf.h中 定义一下各个端点

    /* EP1  */
    /* tx buffer base address */
    #define ENDP1_TXADDR        (0x100)
    #define ENDP1_RXADDR  (0x110)
    /* EP2  */
    /* tx buffer base address */
    #define ENDP2_TXADDR        (0x120)

8.修改SetConfiguration&SetDeviceAddress

回到usb_prop.c中继续作修改,找到Joystick_SetConfiguration 和 Joystick_SetDeviceAddress

这两个函数不需要,我们把里面的代码给删掉,变成以下

   void Joystick_SetConfiguration(void)
   {
   }
   void Joystick_SetDeviceAddress (void)
   {
   }

9.修改Data_Setup函数

找到RESULT Joystick_Data_Setup(u8 RequestNo)函数,在这个函数中,便是对不同的接口进行区分, USBwIndex0反应出了接口数,不同的接口,返回不同的报告描述符,

根据在配置描述符中的定义,第0个接口,返回键盘的相关描述符,第一个接口,返回鼠标相关描述符。将函数配置为如下

RESULT Joystick_Data_Setup(u8 RequestNo)
{
  u8 *(*CopyRoutine)(u16);
 
  CopyRoutine = NULL;
  if ((RequestNo == GET_DESCRIPTOR)
      && (Type_Recipient == (STANDARD_REQUEST | INTERFACE_RECIPIENT))
      && (pInformation->USBwIndex0 < 2))
  {
 
    if (pInformation->USBwValue1 == REPORT_DESCRIPTOR)
    {
     if (pInformation->USBwIndex0 == 0)
CopyRoutine = KP_GetReportDescriptor;
else
CopyRoutine = Mouse_GetReportDescriptor;
    }
    else if (pInformation->USBwValue1 == HID_DESCRIPTOR_TYPE)
    {
     if (pInformation->USBwIndex0 == 0)
CopyRoutine = KP_GetHIDDescriptor;
else
CopyRoutine = Mouse_GetHIDDescriptor;
    }
 
  } /* End of GET_DESCRIPTOR */
 
  /*** GET_PROTOCOL ***/
  else if ((Type_Recipient == (CLASS_REQUEST | INTERFACE_RECIPIENT))
           && RequestNo == GET_PROTOCOL)
  {
    CopyRoutine = Joystick_GetProtocolValue;
  }
 
 
  if (CopyRoutine == NULL)
  {
    return USB_UNSUPPORT;
  }
 
  pInformation->Ctrl_Info.CopyData = CopyRoutine;
  pInformation->Ctrl_Info.Usb_wOffset = 0;
  (*CopyRoutine)(0);
  return USB_SUCCESS;
}

在以上代码中

    KP_GetReportDescriptor
    Mouse_GetReportDescriptor
    KP_GetHIDDescriptor;
    Mouse_GetHIDDescriptor

都是函数名,这些函数需要定义

CopyRoutine是指针函数,以指示运行哪个函数

我们在usb_prop.h中,对函数进行定义

    /***********************************************/
    u8 *Mouse_GetReportDescriptor(u16 Length);
    u8 *KP_GetReportDescriptor(u16 Length);
    u8 *Mouse_GetHIDDescriptor(u16 Length);
    u8 *KP_GetHIDDescriptor(u16 Length);
    /***********************************************/

把一些没有用的函数删掉,比如

    Joystick_GetReportDescriptor(u16 Length)
    u8 *Joystick_GetHIDDescriptor(u16 Length)

10.修改GetReportDescriptor函数

我们回到usb_prop.c中继续修改

找到u8 *Joystick_GetReportDescriptor(u16 Length) 这种 获得报告描述符的 函数
这个函数没有什么用了,我们有我们自己的,可以删掉

    u8 *KP_GetReportDescriptor(u16 Length)
    {
      return Standard_GetDescriptorData(Length, &KP_Report_Descriptor);
    }
 
    u8 *Mouse_GetReportDescriptor(u16 Length)
    {
      return Standard_GetDescriptorData(Length, &Mouse_Report_Descriptor);
    }

由于有两个报告描述符需要获得,所以该处申明两个函数,根据接口号码,会进入不同的函数,Joystick_Data_Setup函数中,我们可以看到接口不同,进入的函数不同,返回的报告描述符也就不同了。

u8 *Joystick_GetHIDDescriptor(u16 Length) 也没有用,

我们用自己的,删掉它,更改为

    u8 *KP_GetHIDDescriptor(u16 Length)
    {
      return Standard_GetDescriptorData(Length, &KP_Hid_Descriptor);
    }
    u8 *Mouse_GetHIDDescriptor(u16 Length)
    {
      return Standard_GetDescriptorData(Length, &Mouse_Hid_Descriptor);
    }

现在编译~,我们就可以成功啦~!!

下载进入开发板

我们可以看到设备识别成功啦!还不保险?那就看看Bus Hound的监控数据吧。

点CapsLock有数据流动,那就OK啦。

端点输入数据,请参考

void Joystick_Send(u8 buf0, u8 buf1)
{
  u8 Buffer[8] = {0, 0, 0, 0, 0, 0, 0, 0};
  
  Buffer[0] = buf0;
  /* prepare buffer to send */
  Buffer[3] = buf1;
 
  if(Buffer[0]==0) //键盘
  {
  /*copy mouse position info in ENDP1 Tx Packet Memory Area*/
  UserToPMABufferCopy(Buffer, GetEPTxAddr(ENDP1), 8);
  /* enable endpoint for transmission */
  SetEPTxValid(ENDP1);
  }
  else    //鼠标
  {
  UserToPMABufferCopy(Buffer, GetEPTxAddr(ENDP2), 5);
  SetEPTxValid(ENDP2);
  }
 
}

鼠标键盘的具体数据,以及格式,请参考单接口模式下的,这个和那个类似的,用Buffer[0]来区别键盘和鼠标(虽然发送的端点不一样,但是由于报告描述符中,鼠标键盘数据的Buffer[0]的数据不一样,所以用这个方法来区别)

以上教程来自论坛,由于本人白天工作,就大半夜的搞一下,键盘鼠标可以识别,但是鼠标没有数据,不知道为什么,今天终于解决了,所以来分享一下,原因是来自论坛大神的程序还缺少了一步很简单的修改:

要使用ENDP2的话,就要在usb_congf.h里面修改EP_NUM宏

    #define EP_NUM     (3) 

把原来的2改成3 问题解决

顺便提一下这个宏吧。我的理解是这样:

  • 首先它是要把EP0算进去的。所以如果只有EP0工作,那这个宏就是1;
  • 其次对于一个附加的端点,无论是只用IN或OUT,还是都用,EP_NUM宏的值都只加1;
  • 在我印象里,没有使用EP2的时候,我同时开通了EP1IN和EP1OUT,而EP_NUM没有改,还是范例项目里的2。我的EP1 IN和OUT都可以工作。由此作出上述推断。

最后,如果端点并非连续分配的,比如EP1根本没有初始化,也不使用,直接跳到EP2上,那么中间空余的端点也应该算进去。所以EP_NUM的值应该取决于我用到的地址最大的端点,将该地址+1。在我自己的项目里,我用了EP1和EP2的IN和OUT,算起来一共是4个。但我只需要将EP_NUM定义为3。实际这样修改后没有发现问题。

Last Modified: December 8, 2016
Leave a Comment

已有 1 条评论
  1. 德菲尔 德菲尔

    “要使用 ENDP2 的话,就要在 usb_congf.h 里面修改 EP_NUM 宏
    #define EP_NUM (3)
    把原来的 2 改成 3 问题解决”

    同样的问题,找了一天,终于在这篇博客找到答案,多谢~