目录
项目介绍
本项目基于ESP32-S2模块制作USB键盘鼠标设备,实现鼠标移动、点击、滚动以及键盘输入字符的功能。
硬件介绍
本项目使用扩展板如下功能:
- 按键、旋转编码器输入 - 以模拟信号的方式
- 双电位计控制输入 - 以数字信号的方式
- Arduino IDE:用于代码编写、调试及上传
- 手柄位置读取:手柄的x、y方向分别为一个电位器,通过改变手柄的位置可改变输出PWM的频率和占空比。程序中,利用外部中断进行捕获,同时判断是上升沿还是下降沿,并记录时间,两上升沿间的时间间隔为PWM周期,上升沿与下降沿间的时间间隔为一个PWM周期中的高电平时间。
- 编码器及按键读取:IO扩展板上的1个按键和旋转编码器的3个输入端口通过R-2R电阻网络的方式连接在一起,生成一个模拟电压量,按下任何一个按键或转动编码器都会改变这个模拟电压量的值。
编码器转动方向会决定AC先导通还是BC先导通,由此可以判断编码器转动的方向及圈数。 - 鼠标控制:通过调用
USB.h
和USBHIDMouse.h
实现鼠标的移动、点击和滚动。 - 键盘控制:通过调用
USB.h
和USBHIDKeyboard.h
实现输入字符功能。
- 鼠标移动:通过手柄摇杆移动鼠标
- 鼠标点击和滚动:通过转动及按下编码器,实现鼠标滚动和点击
- 输入字符:通过按下按键,实现模拟键盘输入一串字符
利用外部中断对上升沿及下降沿进行捕获,同时根据电平状态判断是上升沿还是下降沿,并记录时间,两上升沿间的时间间隔为PWM周期,上升沿与下降沿间的时间间隔为一个PWM周期中的高电平时间。
#define PWM_OUT 2
pinMode(PWM_OUT, INPUT);
//启用外部中断
attachInterrupt(PWM_OUT, pwm_interrupt, CHANGE);
//PWM捕获
void pwm_interrupt() {
if (digitalRead(PWM_OUT)) { //上升沿
pwm.time = micros() - pwm.prev_time;
pwm.prev_time = micros();
}
else { //下降沿
pwm.high_time = micros() - pwm.prev_time;
}
}
编码器及按键的读取判断
在定时器中断(1ms)中,利用ADC采集引脚上的模拟电压,因为扩展板上的按键和编码器通过R-2R电阻网络连接在一起,产生一个模拟电压量,按下任何一个按键或转动编码器都会改变这个模拟电压量的值,所以利用该电压值便可以反推出按键及编码器的状态。
读取到按键状态改变时,会判断改变的时间间隔,实现消抖的目的。
编码器转动方向会决定AC先导通还是BC先导通,由此可以判断编码器转动的方向及圈数。
#define A_OUT 1
hw_timer_t* timer = NULL; //声明一个定时器
/定义一个判断按钮的结构体
typedef struct {
unsigned long counter = 0; //计时器
int prev_value = 0; //前一状态
int now_value; //当前状态
int mode = 0;
} Button;
struct {
int mode = 0; //一个周期总时间
int counter = 0; //一个周期高电平时间
} gun;
struct {
unsigned long prev_time; //前一计时时间
int time; //一个周期总时间
int high_time; //一个周期高电平时间
} pwm;
Button k2, s, ac, bc;
//定时器中断函数
void IRAM_ATTR onTimer() {
int k2_now = k2.now_value;
int s_now = s.now_value;
int ac_now = 0;
int bc_now = 0;
switch (analogRead(A_OUT)) {
case 1112 ... 1368:
ac_now = 1;
bc_now = 1;
case 2176 ... 2328: //k2
k2_now = 1;
break;
case 4952 ... 5208:
ac_now = 1;
bc_now = 1;
case 5942 ... 6210: //s 5942 ... 6198
s_now = 1;
k2_now = 0;
break;
case 6550 ... 6850: //6580 ... 6836
ac_now = 1;
break;
case 6870 ... 7180: //6902 ... 7158
bc_now = 1;
break;
case 6220 ... 6530: //6257 ... 6513
ac_now = 1;
bc_now = 1;
case 7220 ... 7520: //7252 ... 7508
k2_now = 0;
s_now = 0;
break;
}
k2.prev_value = k2.now_value;
k2.now_value = k2_now;
s.prev_value = s.now_value;
s.now_value = s_now;
ac.prev_value = ac.now_value;
ac.now_value = ac_now;
bc.prev_value = bc.now_value;
bc.now_value = bc_now;
if (s.now_value != s.prev_value) {
if (s.now_value) {
s.counter = millis();
}
else if (millis()-s.counter > 50) {
s.mode = 1;
}
}
if (k2.now_value != k2.prev_value) {
if (k2.now_value) {
k2.counter = millis();
}
else {
if (millis()-k2.counter > 50) {
k2.mode = 1;
}
else {
k2.mode = 0;
}
}
}
//判断编码器方向并计数
if (ac.now_value && bc.now_value) {
if (ac.prev_value && !bc.prev_value) {
gun.mode = -1;
gun.counter++;
}
else if (!ac.prev_value && bc.prev_value) {
gun.mode = 1;
gun.counter++;
}
}
else if (!ac.now_value && !bc.now_value) {
if (ac.prev_value && !bc.prev_value) {
gun.mode = 1;
gun.counter++;
}
else if (!ac.prev_value && bc.prev_value) {
gun.mode = -1;
gun.counter++;
}
}
}
//启用定时器中断(每1ms触发)
timer = timerBegin(0, 80, true); //定时器0,80MHz
timerAttachInterrupt(timer, &onTimer, true);
timerAlarmWrite(timer, 1000, true); //us
timerAlarmEnable(timer);
鼠标的控制
PWM的频率和占空比分别反映了手柄X和Y方向上的移动。根据捕获到的PWM频率及占空比与其在原点处的偏差,传入Mouse.move
函数中对鼠标进行移动操作。
根据中断中记录到的编码器转动方向及转动圈数,传入Mouse.move
函数中对鼠标进行滚动操作。
根据中断中记录到的单击标志位,使用Mouse.press
及Mouse.release
函数中对鼠标进行单击操作。
#include "USB.h"
#include "USBHIDMouse.h"
USBHIDMouse Mouse; //声明一个鼠标对象
Mouse.begin();
USB.begin();
//鼠标移动及滚动
Mouse.move(34-(pwm.time-30)/100, pwm.high_time/100-20, -gun.mode*gun.counter);
gun.counter = 0;
//鼠标点击
if(s.mode) {
if (!Mouse.isPressed(MOUSE_LEFT)) {
Mouse.press(MOUSE_LEFT);
}
s.mode = 0;
}
else {
if (Mouse.isPressed(MOUSE_LEFT)) {
Mouse.release(MOUSE_LEFT);
}
}
键盘的控制
根据中断中记录到的键盘输入标志位,使用Keyboard.print
函数中进行字符输入操作。
#include "USB.h"
#include "USBHIDKeyboard.h"
USBHIDKeyboard Keyboard; //声明一个键盘对象
Keyboard.begin();
USB.begin();
//键盘输出文字
if(k2.mode) {
Keyboard.print("eetree.cn");
k2.mode = 0;
}
遇到主要难题及解决方法
打印串口信息过程中,有时会导致板子卡死,使用其他软件无法读取串口信息。
在串口打印过程中增加延时,外接RX1、TX1。有群友说使用CDC时串口初始化函数Serial.begin()
中无需添加波特率,暂时还未尝试。
- 使用串口软件vofa,无法读取到串口发送回来的消息,可能是与该板子使用CDC传输串口信息有关。
- 打印串口信息,有时会导致板子卡死。
- 有时候会误触到背面的模拟引脚,导致误操作。
- 为模块增加外壳,避免手触碰到模拟引脚,导致误操作。
- 尝试使用ESP32C3实现蓝牙鼠标键盘功能。
- 增加简单的左键长按、右键点击的功能。
- 学习LVGL库,实现简单UI界面。
R_2R电阻网络DAC原理分析 - 知乎
ESP32 arduino定时器中断_糖朝的博客-CSDN博客
边沿中断中如何判断上升沿和下降沿? - Kinetis - 恩智浦技术社区
ESP32-S2 PWM输入捕获_李法师_的博客-CSDN博客_esp32 脉冲计数器