☑️ 首先说明:本项目基于Arduino Micro 开发板开发的,外设只用到了EC11E1534408无定位旋转编码器。
项目来源:【DIY】自制PC外设-媒体控制器,在英国_哔哩哔哩_bilibili
Github:GitHub - xuan25/HIDMediaController-ArduinoMicroPro
Human interface device(HID),人体学接口设备。
HID 一般指 USB-HID 标准。
在USB协议官网上,给出了HID的文档:HID Usage Tables 1.3
关于键盘的报告描述符、鼠标的报告描述符,可以用官方网站提供的HID描述符工具(HID Descriptor tool)生成;还可以使用现成的报告描述符进行修改;HID协议和用途表文档中,也有很多现成的例子。
✔️ 关于HID的设备描述符,查阅了相关资料,做出总结。因为首次接触,难免会有错误!
//Button
0x05, 0x0c, // Usage Page (Consumer Devices)
0x09, 0x01, // Usage (Consumer Control)
0xa1, 0x01, // Collection (Application)
0x85, 0x04, // REPORT_ID (4)
0x09, 0xe9, // Usage (Volume Up)
0x09, 0xea, // Usage (Volume Down)
0x09, 0xe2, // Usage (Mute) //静音
0x09, 0xcd, // Usage (Play) //播放
0x09, 0xb5, // Usage (Next)
0x09, 0xb6, // Usage (Previous)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x75, 0x01, // Report Size (1)
0x95, 0x10, // Report Count (10)
0x81, 0x06, // Input (Data, Variable, Relative)
0xc0, // End Collection
描述符没有固定的长度,没有固定的数据类型,而是由条目(item)组成。一个条目占据一行。
短条目(大部分为短条目)构成:一个字节的前缀 + 可选的数据字节
前缀结构:
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
bTag | bType | bSize |
数值 | 功能 | 二进制 |
---|---|---|
8 | input(输入) | 1000 xxxx |
9 | output(输出) | 1001 xxxx |
b | feature(特性) | 1011 xxxx |
0xa1 |
collection(集合) | 1010 0001 |
0xc0 | End collection(闭集合) | 1100 0000 |
数值 | 功能 |
---|---|
0 | Usage Page(用途页) |
1 | Logical Mini(逻辑最小值) |
2 | Logical Maxi(逻辑最大值) |
3 | Physical Mini(物理量最小值) |
4 | Physical Maxi(物理量最大值) |
7 | Report Size(数据域大小) |
8 | Report ID(报告ID) |
9 | Report Count(数据域数量) |
数值 | 功能 |
---|---|
0 | Usage(用途) |
1 | Usage Mini(用途最小值) |
2 | Usage Maxi(用途最大值) |
分析:
0x05, 0x0c, // Usage Page (Consumer Devices)
/* 0x05: 0000 0101 表示:用途页,全局条目,数据字节量为2
* 0x0c: Consumer Page 表示:用户设备
0x09, 0x01, // Usage (Consumer Control)
/* 0x09: 0000 1001 表示:用途页,局部条目,数据字节量为2
* 0x01:Consumer Control 表示:用户控制
0x85, 0x04, // REPORT_ID (4)
/* 0x85:1000 0101 表示:报告ID,全局条目,数据字节量为2
0x09, 0xea, // Usage (Volume Down)
/* 0x09: 0000 1001 表示:用途页,局部条目,数据字节量为2
* 0xea:Volume Decrement 表示:音量减小
0x81, 0x06, // Input (Data, Variable, Relative)
/* 0x81: 1000 0001 表示:输入,主条目,数据字节量为2
Arduino 将报告 ID 有些值设置为固定的。例如: 0:不可用;1:鼠标;2:键盘。
自己编写 HID 设备时,需要设置为其他的 ID。
设置自己想要的报告描述符,可使用辅助软件生成:HID Descriptor Tool | USB-IF
关于 HID 的部分到此为止。后续如果学习到 USB 设备时再看 HID 相关知识吧!
翻到了 Keyboard 库和 Mouse 库。
读库文件的同时发现了都引用了 HID.h 文件:#include "HID.h"
可以仿照 Mouse 和 Keyboard 库来写一个自己的 HID 库。猜测原作者也是这么想的。
偶然发现 Arduino 社区的一篇文章,才让我发现本项目原作者是怎么写 HID 库的。
分析 Mouse 库的 HID 描述符:
static const uint8_t _hidReportDescriptor[] PROGMEM = {
// Mouse
0x05, 0x01, // USAGE_PAGE (Generic Desktop) //用途页为通用桌面设备
0x09, 0x02, // USAGE (Mouse) //用途为鼠标
0xa1, 0x01, // COLLECTION (Application) //COLLECTION1
0x09, 0x01, // USAGE (Pointer) //指针设备
0xa1, 0x00, // COLLECTION (Physical) //COLLECTION2
0x85, 0x01, // REPORT_ID (1) //报告ID设置为1
0x05, 0x09, // USAGE_PAGE (Button) //按键设备
0x19, 0x01, // USAGE_MINIMUM (Button 1) //用途最小值1
0x29, 0x03, // USAGE_MAXIMUM (Button 3) //用途最大值3:1,左键;2,右键;3,中键
0x15, 0x00, // LOGICAL_MINIMUM (0) //逻辑最小值0
0x25, 0x01, // LOGICAL_MAXIMUM (1) //逻辑最大值1(是否按下,0-1)
0x95, 0x03, // REPORT_COUNT (3) //报告的数量(三个键)
0x75, 0x01, // REPORT_SIZE (1) //报告的大小(1 bit)
0x81, 0x02, // INPUT (Data,Var,Abs) //输入,属性为变量,数值,绝对值
0x95, 0x01, // REPORT_COUNT (1) //报告的数量
0x75, 0x05, // REPORT_SIZE (5) //报告的大小
0x81, 0x03, // INPUT (Cnst,Var,Abs) //输入,属性为常量0
0x05, 0x01, // USAGE_PAGE (Generic Desktop) //用途页为通用桌面
0x09, 0x30, // USAGE (X) //用途为X轴
0x09, 0x31, // USAGE (Y) //用途为Y轴
0x09, 0x38, // USAGE (Wheel) //用途为滚轮
0x15, 0x81, // LOGICAL_MINIMUM (-127) //逻辑最小值-127
0x25, 0x7f, // LOGICAL_MAXIMUM (127) //逻辑最大值 127
0x75, 0x08, // REPORT_SIZE (8) //报告的大小
0x95, 0x03, // REPORT_COUNT (3) //报告的数量
0x81, 0x06, // INPUT (Data,Var,Rel) //输入,属性为变量,数值,相对值
0xc0, // END_COLLECTION //END_COLLECTION2
0xc0, // END_COLLECTION //END_COLLECTION1
};
查看 Mouse.cpp 文件,会发现 HID报告描述符 的声明过程如下(在Keyboard.cpp中几乎相同):
Mouse_::Mouse_(void) : _buttons(0)
{
static HIDSubDescriptor node(_hidReportDescriptor, sizeof(_hidReportDescriptor));
HID().AppendDescriptor(&node);
}
发送报告的函数:
void Mouse_::move(signed char x, signed char y, signed char wheel)
{
uint8_t m[4];
m[0] = _buttons;
m[1] = x;
m[2] = y;
m[3] = wheel;
HID().SendReport(1,m,4);
}
其中,重要的是最后一个代码
HID().SendReport(uint8_tid, const void* data, int len)
//ID 数据 数据长度
首先看 HID 仿照 Mouse 和 Keyboard 库写的 HIDDevice 库。
基本为仿照官方库所写。
#include "HIDDevice.h"
// 自己写的 + Keyboard + Mouse 都用上了
static const uint8_t HIDDevice::_hidReportDescriptor[] PROGMEM = {
// Button
0x05, 0x0c, // Usage Page (Consumer Devices)
0x09, 0x01, // Usage (Consumer Control)
0xa1, 0x01, // Collection (Application)
0x85, 0x04, // REPORT_ID (4)
0x09, 0xe9, // Usage (Volume Up)
0x09, 0xea, // Usage (Volume Down)
0x09, 0xe2, // Usage (Mute)
0x09, 0xcd, // Usage (Play)
0x09, 0xb5, // Usage (Next)
0x09, 0xb6, // Usage (Previous)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x75, 0x01, // Report Size (1)
0x95, 0x10, // Report Count (10)
0x81, 0x06, // Input (Data, Variable, Relative)
0xc0, // End Collection
// Keyboard
0x05, 0x01, // USAGE_PAGE (Generic Desktop) // 47
0x09, 0x06, // USAGE (Keyboard)
0xa1, 0x01, // COLLECTION (Application)
0x85, 0x02, // REPORT_ID (2)
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, 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
// Mouse
0x05, 0x01, // USAGE_PAGE (Generic Desktop) // 54
0x09, 0x02, // USAGE (Mouse)
0xa1, 0x01, // COLLECTION (Application)
0x09, 0x01, // USAGE (Pointer)
0xa1, 0x00, // COLLECTION (Physical)
0x85, 0x01, // REPORT_ID (1)
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
};
HIDDevice::HIDDevice(){
}
// 声明报告描述符
void HIDDevice::begin(){
static HIDSubDescriptor node(_hidReportDescriptor, sizeof(_hidReportDescriptor));
HID().AppendDescriptor(&node);
}
// 媒体控制发送报告
void HIDDevice::mediaControl(uint8_t c){
uint8_t m[2] = {c, 0};
HID().SendReport(4,m,2);
}
// 键盘控制发送报告
void HIDDevice::keyEvent(uint8_t modifiers, uint8_t key1, uint8_t key2, uint8_t key3, uint8_t key4, uint8_t key5, uint8_t key6){
uint8_t m[8] = {modifiers, 0, key1, key2, key3, key4, key5, key6};
HID().SendReport(2,m,8);
}
// 鼠标控制发送报告
void HIDDevice::mouseEvent(uint8_t buttons, uint8_t x, uint8_t y, uint8_t wheel){
uint8_t m[4] = {buttons, x, y, wheel};
HID().SendReport(1,m,4);
}
#ifndef HIDDevice_H
#define HIDDevice_H
#include "arduino.h"
#include
#define VOLUME_INCREMENT 0x01
#define VOLUME_DECREMENT 0x02
#define MUTE 0x04
#define PLAY_PAUSE 0x08
#define NEXT 0x10
#define PREVIOUS 0x20
#define MOUSE_LEFT 0x00
#define MOUSE_RIGHT 0x01
#define MOUSE_MIDDLE 0x04
#define KEY_LEFT_CTRL 1<<0x00
#define KEY_LEFT_SHIFT 1<<0x01
#define KEY_LEFT_ALT 1<<0x02
#define KEY_LEFT_GUI 1<<0x03
#define KEY_RIGHT_CTRL 1<<0x04
#define KEY_RIGHT_SHIFT 1<<0x05
#define KEY_RIGHT_ALT 1<<0x06
#define KEY_RIGHT_GUI 1<<0x07
#define KEY_UP_ARROW 0x52
#define KEY_DOWN_ARROW 0x51
#define KEY_LEFT_ARROW 0x50
#define KEY_RIGHT_ARROW 0x4f
#define KEY_BACKSPACE 0x2a
#define KEY_TAB 0x2b
#define KEY_RETURN 0x28
#define KEY_ESC 0x29
#define KEY_INSERT 0x49
#define KEY_DELETE 0x4c
#define KEY_PAGE_UP 0x4b
#define KEY_PAGE_DOWN 0x4e
#define KEY_HOME 0x4a
#define KEY_END 0x4d
#define KEY_CAPS_LOCK 0x39
#define KEY_F1 0x3a
#define KEY_F2 0x3b
#define KEY_F3 0x3c
#define KEY_F4 0x3d
#define KEY_F5 0x3e
#define KEY_F6 0x3f
#define KEY_F7 0x40
#define KEY_F8 0x41
#define KEY_F9 0x42
#define KEY_F10 0x43
#define KEY_F11 0x44
#define KEY_F12 0x45
#define KEY_A 0x04
#define KEY_B 0x05
#define KEY_C 0x06
#define KEY_D 0x07
#define KEY_E 0x08
#define KEY_F 0x09
#define KEY_G 0x0a
#define KEY_H 0x0b
#define KEY_I 0x0c
#define KEY_J 0x0d
#define KEY_K 0x0e
#define KEY_L 0x0f
#define KEY_M 0x10
#define KEY_N 0x11
#define KEY_O 0x12
#define KEY_P 0x13
#define KEY_Q 0x14
#define KEY_R 0x15
#define KEY_S 0x16
#define KEY_T 0x17
#define KEY_U 0x18
#define KEY_V 0x19
#define KEY_W 0x1a
#define KEY_X 0x1b
#define KEY_Y 0x1c
#define KEY_Z 0x1d
class HIDDevice{
private:static const uint8_t _hidReportDescriptor[] PROGMEM;
public:HIDDevice();
public:void begin();
public:void mediaControl(uint8_t c);
public:void keyEvent(uint8_t modifiers, uint8_t key1, uint8_t key2, uint8_t key3, uint8_t key4, uint8_t key5, uint8_t key6);
public:void mouseEvent(uint8_t buttons, uint8_t x, uint8_t y, uint8_t wheel);
};
#endif
其余就是检测输入的问题了。
A、B、C 用来检测旋钮转动方向。D、E 相当于普通按键。
EC11判断方法:左转:逆时针;右转:顺时针
当C接地时!AB之间的电平状态关系
逆时针(左转) | 11、01、00、10 |
顺时针(右转) | 11、10、00、01 |
判断方法:
A信号位于下降沿时,B是低电平,表示顺时针。B是高电平,表示逆时针。
同理:
B信号位于下降沿时,A是高电平,表示顺时针。A是低电平,表示逆时针。
#include "Encoder.h"
int prePinA = 0;
int prePinB = 0;
int preDire = 0;
int a,b,c;
//注册 ABC引脚
Encoder::Encoder(int aPin, int bPin, int cPin){
a=aPin;
b=bPin;
c=cPin;
}
//初始化C为低电平,A、B为输入端口
void Encoder::begin(){
pinMode(c,OUTPUT);
digitalWrite(c,LOW);
pinMode(a,INPUT_PULLUP);
pinMode(b,INPUT_PULLUP);
prePinA = digitalRead(a); //检测电平
prePinB = digitalRead(b);
}
int Encoder::nextFrame(){
int pinA = digitalRead(a);
int pinB = digitalRead(b);
int result = 0;
//通过下一帧检测正反转
if(pinA > prePinA) //a up
{
if(pinB == 1) result = -1;//b high
else result = 1;//b low
}
else if(pinA < prePinA) //a down
{
if(pinB == 1) result = 1;//b high
else result = -1;//b low
}
else if(pinB > prePinB) //b up
{
if(pinA == 1) result = 1;//a high
else result = -1;//a low
}
else if(pinB < prePinB) //b down
{
if(pinA == 1) result = -1;//a high
else result = 1;//a low
}
prePinA = pinA;
prePinB = pinB;
return result;
}
// 是再确认一次还是两次动作一次操作呢?
int Encoder::refresh(){
int dire = nextFrame();
if(dire == 1)
{
if(preDire != 1)
{
preDire = 1;
return 0;
}
else return 1;
}
else if(dire == -1)
{
if(preDire != -1){
preDire = -1;
return 0;
}
else return -1;
}
return 0;
}
#ifndef Encoder_H
#define Encoder_H
#include "arduino.h"
class Encoder{
private:int prePinA;
private:int prePinB;
private:int preDire;
private:int a,b,c;
private:int nextFrame();
public:Encoder(int aPin, int bPin, int cPin);
public:void begin();
public:int refresh();
};
#endif
#include "MediaButton.h"
int d,e;
// 注册 DE 引脚
MediaButton::MediaButton(int dPin, int ePin){
d = dPin;
e = ePin;
}
// 初始化引脚: d为低电平,e为检测口
void MediaButton::begin(){
pinMode(d, OUTPUT);
digitalWrite(d, LOW);
pinMode(e, INPUT_PULLUP);
}
void MediaButton::refresh(HIDDevice hid1)
{
if(!digitalRead(e)) //判断按键是否按下,如果按下 digitalRead(e) = 0;
{
int i = 0;
/* 按钮长按实现播放上一曲功能 */
while(!digitalRead(e)) //按下循环
{
delay(10);
i++;
if(i>50) //按下超时(上一首)
{
hid1.mediaControl(PREVIOUS); //#按钮长按保持#
hid1.mediaControl(0);
i = 0;
while(!digitalRead(e))
{
if(i>1)
{
hid1.mediaControl(PREVIOUS); //#按钮长按保持循环#
hid1.mediaControl(0);
}
delay(500);
i++;
}
return;
}
}
i = 0;
/* 按钮短按实现暂停播放功能 */
while(digitalRead(e)) //释放循环
{
delay(10);
i++;
if(i>50) //释放超时(暂停/播放)
{
hid1.mediaControl(PLAY_PAUSE);//#按钮短按#
hid1.mediaControl(0);
return;
}
}
i = 0;
/* 按钮双击长按实现播放下一曲功能 */
while(!digitalRead(e)) //按下循环2
{
delay(10);
i++;
if(i>50) //按下超时(下一首)
{
hid1.mediaControl(NEXT); //#按钮双击保持#
hid1.mediaControl(0);
i = 0;
while(!digitalRead(e))
{
if(i>1)
{
hid1.mediaControl(NEXT); //#按钮双击保持循环#
hid1.mediaControl(0);
}
delay(500);
i++;
}
return;
}
}
i = 0;
/* 按钮双击实现播放下一曲功能 */
while(digitalRead(e)) //释放循环2
{
delay(10);
i++;
if(i>50) //释放超时(下一首)
{
hid1.mediaControl(NEXT); //#按钮双击#
hid1.mediaControl(0);
return;
}
}
i = 0;
/* 按钮三击长按实现收藏歌曲功能 */
while(!digitalRead(e)) //按下循环3
{
delay(10);
i++;
if(i>50) //按下超时(喜欢)
{
hid1.keyEvent(KEY_LEFT_CTRL | KEY_LEFT_ALT, KEY_L, 0, 0, 0, 0, 0); //#按钮三击保持#
hid1.keyEvent(0, 0, 0, 0, 0, 0, 0);
while(!digitalRead(e))
{
delay(500); //#按钮三击保持循环#
}
return;
}
}
hid1.keyEvent(KEY_LEFT_CTRL | KEY_LEFT_ALT, KEY_L, 0, 0, 0, 0, 0); //#按钮三击#
hid1.keyEvent(0, 0, 0, 0, 0, 0, 0);
return;
}
}
这个实现方式挺有意思的,画个流程图吧
#ifndef MediaButton_H
#define MediaButton_H
#include "arduino.h"
#include "HIDDevice.h"
class MediaButton{
private:int d,e;
public:MediaButton(int dPin, int ePin);
public:void begin();
public:void refresh(HIDDevice hid1);
};
#endif
#include "Encoder.h"
#include "MediaButton.h"
#include "HIDDevice.h"
static int a=8,b=6,c=7,d=16,e=15; //EC11引脚定义
Encoder enc1(a,b,c); //创建实例
MediaButton btn1(d,e);
HIDDevice hid1;
void setup() //初始化
{
enc1.begin();
btn1.begin();
hid1.begin();
}
int total = 0;
int threshold = 5;
void loop()
{
int re = enc1.refresh(); //检测正反转
total += re; //正反转抵消问题
if(total == threshold)
{
//#编码器正转#
hid1.mediaControl(VOLUME_INCREMENT); //增加音量
hid1.mediaControl(0);
total = 0;
}
else if(total == -threshold)
{
//#编码器反转#
hid1.mediaControl(VOLUME_DECREMENT); //减少音量
hid1.mediaControl(0);
total = 0;
}
btn1.refresh(hid1); //检测按键
}
把项目的资源百度网盘放在这里了,做个备份。
下载项目最好还是访问文章最前方的 GitHub 链接。
链接:https://pan.baidu.com/s/1nXlvSr33GzwCzQ4frGQogg
提取码:o8r7
参考:
HID设备描述符的解析
USB_HID协议基础
利用Arduino制作自己的usb hid设备
日常总结啦 ~
C++ 还是不太熟悉。但是这次学会了 带参数创建实例的方法,也可以啦 ~
Encoder enc1(a,b,c);
MediaButton btn1(d,e);
HIDDevice hid1;
共同进步啊 ~ 小伙伴们 ~