37浏览
查看: 37|回复: 1

[项目] 做一个真实的虚拟键盘鼠标

[复制链接]
广大工矿企业在日常生产生活中,经常会遇到需要虚拟键盘和鼠标的场景。通常的解决方法是使用软件进行模拟。但是软件模拟经常会遇到安全软件误杀等等情况。为了解决这种问题,这次带来的制作是一个于CH552开发的虚拟键盘鼠标项目。它是基于CH554Arduino 环境开发的设备,插上之后,系统 中会出现一个 USB串口,一个USB 键盘,一个USB 鼠标,我们将数据从串口送进设备,然后设备将收到的串口数据直接转发到键盘鼠标对应的端口上,从而实现鼠标键盘操作。
首先进行硬件的设计,电路图如下:
做一个真实的虚拟键盘鼠标图1
图片1 电路图
图片中上方是一个CH554 的最小电路图,他是WCH出品的一款兼容MCS51指令集的增强型E8051内核单片机。带有256 字节内部iRAM,可以用于快速数据暂存以及堆栈;1KB 片内xRAM,可以用于大量数据暂存以及DMA直接内存存取。16KB 容量的可多次编程的非易失存储器ROM,可以全部用于程序存储空间;或者可以分为14KB 程序存储区和2KB引导代码BootLoader/ISP程序区。更特别的是其内嵌USB 控制器和USB 收发器,支持USB-Host 主机模式和USB-Device 设备模式,支持USB type-C主从检测,支持USB 2.0全速12Mbps或者低速1.5Mbps。支持最大64字节数据包,内置FIFO,支持DMA
这里使用的型号为CH554E MSOP-10 封装,体积非常小便于整体设备小型化。
为了调试方便,预留了P2UART输出。此外,还有一个 WS2812B LED 可以实现多种颜色的灯效。
PCB 设计如下:
做一个真实的虚拟键盘鼠标图2
图片2 PCB 设计
3D 预览如下
做一个真实的虚拟键盘鼠标图3
图片3 正面预览

做一个真实的虚拟键盘鼠标图4
图片4 背面预览

在实际使用中,只需要焊接CH554最小系统部分即可实现USB 串口转键盘鼠标功能,其余部分可以不上件。
这次的设计尺寸是根据透明U盘外壳来的,在制作时选择 0.8mm PCB, 刚好能够放入外壳中。
硬件设计完成之后就可以进行代码的编写了。主要代码如下:

  1. #include <WS2812.h>
  2. #include "src/CdcHidCombo/USBCDC.h"
  3. #include "src/CdcHidCombo/USBHIDKeyboardMouse.h"
  4. #define NUM_LEDS 1
  5. #define COLOR_PER_LEDS 3
  6. #define NUM_BYTES (NUM_LEDS*COLOR_PER_LEDS)
  7. __xdata uint8_t ledData[NUM_BYTES];
  8. #define KeyboardReportID 0x01
  9. #define MouseReportID    0x02
  10. #define OnBoardLED       0x03
  11. // Data format
  12. // Keyboard(Total 9 bytes): 01(ReportID 01) + Keyboard data (8 Bytes)
  13. // Mouse(Total 5 bytes): 02(ReportID 02) + Mouse Data (4 Bytes)
  14. uint8_t recvStr[9];
  15. uint8_t recvStrPtr = 0;
  16. unsigned long Elsp;
  17. void setup() {
  18.   USBInit();
  19.   Serial0_begin(115200);
  20.   delay(1000);
  21.   Serial0_print("start");
  22.   Elsp=0;
  23. }
  24. void loop() {
  25.   while (USBSerial_available()) {
  26.     uint8_t serialChar = USBSerial_read();
  27.     recvStr[recvStrPtr++] = serialChar;
  28.     if (recvStrPtr == 10) {
  29.       for (uint8_t i = 0; i < 9; i++) {
  30.         Serial0_write(recvStr[i]);
  31.       }      
  32.       if (recvStr[0] == KeyboardReportID) { // Keyboard
  33.         USB_EP3_send(recvStr, 9);
  34.       }
  35.       if (recvStr[0] == MouseReportID) {
  36.         USB_EP3_send(recvStr, 5); // Mouse
  37.       }
  38.       if (recvStr[0] == OnBoardLED) {
  39.         set_pixel_for_GRB_LED(ledData, 0, recvStr[0], recvStr[1], recvStr[2]);
  40.         neopixel_show_P1_5(ledData, NUM_BYTES);  
  41.       }
  42.       recvStrPtr = 0;
  43.     }
  44.     Elsp=millis();
  45.   }
  46.   // If there is no data in 100ms, clear the receive buffer
  47.   if (millis()-Elsp>100) {
  48.       recvStrPtr = 0;
  49.       Elsp=millis();
  50.     }
  51. }
复制代码
每次收取10字节串口数据,如果第一字节为KeyboardReportID,那么直接将9个字节从端点3发送给主机;如果第一字节是MouseReportID,那么直接将5个字节从端点3发送给主机;如果第一字节为 OnBoardLED,那么将后续3个字节合成为一个颜色信息,然后通过neopixel_show_P1_5() 函数将数据从 P1.5 引脚发送给WS2812
USB 设备信息在USBconstant.c文件中有描述。其中包含了USB CDC 设备/USB 键盘/USB鼠标的描述符。其中的USB 键盘/USB鼠标使用端点3 OUTPUT
对于键盘鼠标的HID 描述符,在USBconstant.c文件的ReportDescriptor[]的结构体中。使用的是标准的键盘鼠标描述符。
键盘数据为8字节长:
  
Byte0
  
Byte1
Byte2
Byte3
Byte4
Byte5
Byte6
Byte7
特殊按键
NA
数据0
数据1
数据2
数据3
数据4
数据5

鼠标数据为4字节长:
  
Byte0
  
Byte1
Byte2
Byte3
左右中键
X轴数据
Y轴数据
滚轮数据

需要特别注意的是:键盘鼠标都是通过端点3发送给主机的,他们使用 Report ID 进行区分,在 ReportDescriptor[]中有如下定义:
  1.     0x09, 0x06,       // USAGE (Keyboard)
  2.     0xa1, 0x01,       // COLLECTION (Application)
  3.     0x85, 0x01,       //   REPORT_ID (1)
  4.     0x05, 0x07,       //   USAGE_PAGE (Keyboard)
  5. 0x19, 0xe0,       //   USAGE_MINIMUM (Keyboard LeftControl)
  6. …….
  7.     0x09, 0x02,       // USAGE (Mouse)
  8.     0xa1, 0x01,       // COLLECTION (Application)
  9.     0x09, 0x01,       //   USAGE (Pointer)
  10.     0xa1, 0x00,       //   COLLECTION (Physical)
  11.     0x85, 0x02,       //   REPORT_ID (2)
  12.     0x05, 0x09,       //     USAGE_PAGE (Button)
复制代码
就是说,设备通过端点3发送给主机的数据,如果是 0x01+ 8个字节的数据,主机会当作键盘数据来处理;如果是0x02+4个字节的数据,主机则会当作鼠标数据来处理。
上面的代码中,USB串口,收到数据之后,将9字节和5字节发送给主机即可实现键盘和鼠标的操作了。
端点3的发送代码在 USB_EP3_send()函数中,可以看到对于CH554 来说,USB的发送操作非常明确简单,要发送的数据直接填充到Ep3Buffer[]中,然后设定 UEP3_T_LEN 寄存器要发送的数据长度,再设定 UEP3_CTRL 寄存器就完成了了发送。这对于USB初学者非常友好,代码直接对应硬件动作,通俗易懂。

  1.   // 将所有数据放入 EP3 准备发送
  2.   for (__data uint8_t i = 0; i < Len; i++) { // load data for upload
  3.     Ep3Buffer[i] = Data[i];
  4.   }
  5.   UEP3_T_LEN = Len; // data length
  6.   UpPoint3_Busy = 1;
  7.   UEP3_CTRL = UEP3_CTRL & ~MASK_UEP_T_RES |
  8.               UEP_T_RES_ACK; // upload data and respond ACK
复制代码
做一个真实的虚拟键盘鼠标图5

上位机代码:
  1. // CDC_vKBMSTest.cpp : This file contains the 'main' function. Program execution begins and ends there.
  2. //
  3. #include <windows.h>
  4. #include <SetupAPI.h>
  5. #include <tchar.h>
  6. #include <iostream>
  7. #include <cstring>
  8. #include <atlstr.h>
  9. #pragma comment(lib, "Setupapi.lib")
  10. #define MY_USB_PID_VID        _T("VID_1209&PID_C55C")
  11. class ComPortException : public std::exception {
  12. public:
  13.         ComPortException(DWORD errorCode) : errorCode(errorCode) {}
  14.         DWORD getErrorCode() const {
  15.                 return errorCode;
  16.         }
  17. private:
  18.         DWORD errorCode;
  19. };
  20. // 对串口portName 发送数据
  21. // 无法打开串口返回 1
  22. // 无法设置串口参数 2
  23. void SendToComPort(const int port, const char* data) {
  24.         int Result = 0;
  25.         HANDLE hCom = INVALID_HANDLE_VALUE;
  26.         TCHAR portName[10]; // 用于存储 "COMp" 字符串
  27. // 将整数 p 转换为 "COMp" 字符串
  28. #ifdef UNICODE
  29.         swprintf(portName, 10, _T("\\\\.\\COM%d"), port);
  30. #else
  31.         sprintf(portName, "COM%d", port);
  32. #endif
  33.         try {
  34.                 // 打开串口
  35.                 hCom = CreateFile(portName,
  36.                         GENERIC_READ | GENERIC_WRITE,
  37.                         0,
  38.                         NULL,
  39.                         OPEN_EXISTING,
  40.                         0,
  41.                         NULL);
  42.                 if (hCom == INVALID_HANDLE_VALUE) {
  43.                         throw ComPortException(GetLastError());
  44.                 }
  45.                 // 设置串口参数
  46.                 DCB dcb;
  47.                 SecureZeroMemory(&dcb, sizeof(DCB));
  48.                 dcb.DCBlength = sizeof(DCB);
  49.                 if (!GetCommState(hCom, &dcb)) {
  50.                         throw ComPortException(GetLastError());
  51.                 }
  52.                 dcb.BaudRate = CBR_115200;  // 波特率
  53.                 dcb.ByteSize = 8;         // 数据位
  54.                 dcb.StopBits = ONESTOPBIT; // 停止位
  55.                 dcb.Parity = NOPARITY;    // 校验位
  56.                 if (!SetCommState(hCom, &dcb)) {
  57.                         throw ComPortException(GetLastError());
  58.                 }
  59.                 // 设置超时参数
  60.                 COMMTIMEOUTS timeouts;
  61.                 timeouts.ReadIntervalTimeout = 50;
  62.                 timeouts.ReadTotalTimeoutConstant = 50;
  63.                 timeouts.ReadTotalTimeoutMultiplier = 10;
  64.                 timeouts.WriteTotalTimeoutConstant = 50;
  65.                 timeouts.WriteTotalTimeoutMultiplier = 10;
  66.                 if (!SetCommTimeouts(hCom, &timeouts)) {
  67.                         throw ComPortException(GetLastError());
  68.                 }
  69.                 // 发送数据
  70.                 DWORD bytesWritten;
  71.                 if (!WriteFile(hCom, data, 10, &bytesWritten, NULL)) {
  72.                         std::cerr << "Failed to write to COM port." << std::endl;
  73.                 }
  74.                 else {
  75.                         std::cout << "Successfully sent data to COM port: [" << port << "]" << std::endl;
  76.                 }
  77.                 // 关闭串口
  78.                 if (hCom != INVALID_HANDLE_VALUE) {
  79.                         CloseHandle(hCom);
  80.                 }
  81.         }
  82.         catch (const ComPortException& ex) {
  83.                 std::cerr << "Error: " << ex.getErrorCode() << std::endl;
  84.                 if (hCom != INVALID_HANDLE_VALUE) {
  85.                         CloseHandle(hCom);
  86.                 }
  87.         }
  88. }
  89. /************************************************************************/
  90. /* 根据USB描述信息字符串中读取
  91. /************************************************************************/
  92. int MTGetPortFromVidPid(CString strVidPid)
  93. {
  94.         // 获取当前系统所有使用的设备
  95.         int                                        nPort = -1;
  96.         int                                        nStart = -1;
  97.         int                                        nEnd = -1;
  98.         int                                        i = 0;
  99.         CString                                strTemp, strName;
  100.         DWORD                                dwFlag = (DIGCF_ALLCLASSES | DIGCF_PRESENT);
  101.         HDEVINFO                        hDevInfo = INVALID_HANDLE_VALUE;
  102.         SP_DEVINFO_DATA                sDevInfoData;
  103.         TCHAR                                szDis[2048] = { 0x00 };// 存储设备实例ID
  104.         TCHAR                                szFN[MAX_PATH] = { 0x00 };// 存储设备实例属性
  105.         DWORD                                nSize = 0;
  106.         // 准备遍历所有设备查找USB
  107.         hDevInfo = SetupDiGetClassDevs(NULL, L"USB", NULL, dwFlag);
  108.         if (INVALID_HANDLE_VALUE == hDevInfo)
  109.                 goto STEP_END;
  110.         // 开始遍历所有设备
  111.         memset(&sDevInfoData, 0x00, sizeof(SP_DEVICE_INTERFACE_DATA));
  112.         sDevInfoData.cbSize = sizeof(SP_DEVINFO_DATA);
  113.         for (i = 0; SetupDiEnumDeviceInfo(hDevInfo, i, &sDevInfoData); i++)
  114.         {
  115.                 nSize = 0;
  116.                 // 无效设备
  117.                 if (!SetupDiGetDeviceInstanceId(hDevInfo, &sDevInfoData, szDis, sizeof(szDis), &nSize))
  118.                         goto STEP_END;
  119.                 // 根据设备信息寻找VID PID一致的设备
  120.                 strTemp.Format(_T("%s"), szDis);
  121.                 strTemp.MakeUpper();
  122.                 if (strTemp.Find(strVidPid, 0) == -1)
  123.                         continue;
  124.                 // 查找设备属性
  125.                 nSize = 0;
  126.                 SetupDiGetDeviceRegistryProperty(hDevInfo, &sDevInfoData,
  127.                         SPDRP_FRIENDLYNAME,
  128.                         0, (PBYTE)szFN,
  129.                         sizeof(szFN),
  130.                         &nSize);
  131.                 // "XXX Virtual Com Port (COM7)"
  132.                 strName.Format(_T("%s"), szFN);
  133.                 if (strName.IsEmpty())
  134.                         //goto STEP_END;
  135.                         continue;
  136.                 // 寻找串口信息
  137.                 nStart = strName.Find(_T("(COM"), 0);
  138.                 nEnd = strName.Find(_T(")"), 0);
  139.                 if (nStart == -1 || nEnd == -1)
  140.                         //goto STEP_END;
  141.                         continue;
  142.                 strTemp = strName.Mid(nStart + 4, nEnd - nStart - 2);
  143.                 nPort = _ttoi(strTemp);
  144.         }
  145. STEP_END:
  146.         // 关闭设备信息集句柄
  147.         if (hDevInfo != INVALID_HANDLE_VALUE)
  148.         {
  149.                 SetupDiDestroyDeviceInfoList(hDevInfo);
  150.                 hDevInfo = INVALID_HANDLE_VALUE;
  151.         }
  152.         return nPort;
  153. }
  154. int main()
  155. {
  156.         int Port;
  157.         char Data[10];
  158.         printf("Virutal KB MS Test\n");
  159.         Port = MTGetPortFromVidPid(MY_USB_PID_VID);
  160.         if (Port == -1) {
  161.                 printf("No device is found\n");
  162.                 goto EndProgram;
  163.         }
  164.         else {
  165.                 printf("Found COM%d\n",Port);
  166.         }
  167.         Sleep(5000);
  168.         // 测试鼠标移动
  169.         memset(Data,0,sizeof(Data));
  170.         Data[0] = 2; // 鼠标
  171.         Data[2] = 50;
  172.         SendToComPort(Port,(char *)&Data);
  173.         Sleep(300);
  174.         Data[2] = 0; Data[3] = 50;
  175.         SendToComPort(Port, (char*)&Data);
  176.         Sleep(300);
  177.         Data[2] = -50; Data[3] = 0x00;
  178.         SendToComPort(Port, (char*)&Data);
  179.         Sleep(300);
  180.         Data[2] = 0; Data[3] = -50;
  181.         SendToComPort(Port, (char*)&Data);
  182.         Sleep(300);
  183.         // 测试键盘数据
  184.         memset(Data, 0, sizeof(Data));
  185.         Data[0] = 1; // 键盘
  186.         // 发送GUI信息
  187.         Data[1] = 0x08;
  188.         SendToComPort(Port, (char*)&Data);
  189.         Sleep(1000);
  190.         memset(Data, 0, sizeof(Data));
  191.         Data[0] = 1; // 键盘
  192.         SendToComPort(Port, (char*)&Data);
  193.         Sleep(300);
  194.         // 测试键盘数据
  195.         memset(Data, 0, sizeof(Data));
  196.         Data[0] = 1; // 键盘
  197.         // 发送按键信息
  198.         Data[2] = 0x04;
  199.         Data[3] = 0x0F; //'l'
  200.         Data[4] = 0x04; //'a '
  201.         Data[5] = 0x05; //'b'
  202.         Data[6] = 0x1d; //'z'
  203.         SendToComPort(Port, (char*)&Data);
  204.         Sleep(300);
  205.         memset(Data, 0, sizeof(Data));
  206.         Data[0] = 1; // 键盘
  207.         // 抬起按键
  208.         SendToComPort(Port, (char*)&Data);
  209.         Sleep(300);
  210.         // 测试LED
  211.         memset(Data, 0, sizeof(Data));
  212.         Data[0] = 3; // LED
  213.         Data[1] = 0xFF;
  214.         SendToComPort(Port, (char*)&Data);
  215.         Sleep(500);
  216.         memset(Data, 0, sizeof(Data));
  217.         Data[0] = 3; // LED
  218.         Data[2] = 0xFF;
  219.         SendToComPort(Port, (char*)&Data);
  220.         Sleep(500);
  221.         memset(Data, 0, sizeof(Data));
  222.         Data[0] = 3; // LED
  223.         Data[3] = 0xFF;
  224.         SendToComPort(Port, (char*)&Data);
  225.         Sleep(500);
  226.         memset(Data, 0, sizeof(Data));
  227.         Data[0] = 3; // LED
  228.         SendToComPort(Port, (char*)&Data);
  229.         Sleep(500);
  230. EndProgram:
  231.         printf("End\n");
  232. }
复制代码
工作的测试视频在:

https://www.bilibili.com/video/BV1k8k4YxE8s/



zoologist  高级技匠
 楼主|

发表于 昨天 09:30

本文提到的电路图
下载附件CH554VRKBMS.zip

本文提到的 Arduino 代码:
下载附件CdcVKBMS.zip

本问题到的VS2019 代码:
下载附件CDC_vKBMSTest.zip
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

为本项目制作心愿单
购买心愿单
心愿单 编辑
[[wsData.name]]

硬件清单

  • [[d.name]]
btnicon
我也要做!
点击进入购买页面
上海智位机器人股份有限公司 沪ICP备09038501号-4

© 2013-2024 Comsenz Inc. Powered by Discuz! X3.4 Licensed

mail