基于ESP32-S2模块的USB键盘鼠标设备
基于ESP32-S2模块的USB键盘鼠标设备,实现鼠标移动、点击、滚动以及键盘输入字符的功能。
标签
嵌入式系统
ESP32
2023寒假在家练
136ytr
更新2023-03-27
汕头大学
678

目录

 

项目介绍

本项目基于ESP32-S2模块制作USB键盘鼠标设备,实现鼠标移动、点击、滚动以及键盘输入字符的功能。

硬件介绍

硬件介绍图
本项目使用扩展板如下功能:

  • 按键、旋转编码器输入 - 以模拟信号的方式
  • 双电位计控制输入 - 以数字信号的方式
软件工具
  • Arduino IDE:用于代码编写、调试及上传
设计思路
  • 手柄位置读取:手柄的x、y方向分别为一个电位器,通过改变手柄的位置可改变输出PWM的频率和占空比。程序中,利用外部中断进行捕获,同时判断是上升沿还是下降沿,并记录时间,两上升沿间的时间间隔为PWM周期,上升沿与下降沿间的时间间隔为一个PWM周期中的高电平时间。
    FtP8y71930WiXMUCwEbHMXtdv6v-
  • 编码器及按键读取:IO扩展板上的1个按键和旋转编码器的3个输入端口通过R-2R电阻网络的方式连接在一起,生成一个模拟电压量,按下任何一个按键或转动编码器都会改变这个模拟电压量的值。
    Fmb1dxG3IaI4UlELhVQnnnmiaR_d
    编码器转动方向会决定AC先导通还是BC先导通,由此可以判断编码器转动的方向及圈数。
    FpSvo8qMPcc61fbRxjuycRwP71zt
  • 鼠标控制:通过调用USB.hUSBHIDMouse.h实现鼠标的移动、点击和滚动。
  • 键盘控制:通过调用USB.hUSBHIDKeyboard.h实现输入字符功能。

流程图

实现功能
  • 鼠标移动:通过手柄摇杆移动鼠标
  • 鼠标点击和滚动:通过转动及按下编码器,实现鼠标滚动和点击
  • 输入字符:通过按下按键,实现模拟键盘输入一串字符
    FgejnqCefdx7zwv1K4Lc-UaE84sB
主要代码片段
手柄的读取判断

利用外部中断对上升沿及下降沿进行捕获,同时根据电平状态判断是上升沿还是下降沿,并记录时间,两上升沿间的时间间隔为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.pressMouse.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()中无需添加波特率,暂时还未尝试。

存在问题
  1. 使用串口软件vofa,无法读取到串口发送回来的消息,可能是与该板子使用CDC传输串口信息有关。
  2. 打印串口信息,有时会导致板子卡死。
  3. 有时候会误触到背面的模拟引脚,导致误操作。
未来计划
  1. 为模块增加外壳,避免手触碰到模拟引脚,导致误操作。
  2. 尝试使用ESP32C3实现蓝牙鼠标键盘功能。
  3. 增加简单的左键长按、右键点击的功能。
  4. 学习LVGL库,实现简单UI界面。
参考资料

R_2R电阻网络DAC原理分析 - 知乎
ESP32 arduino定时器中断_糖朝的博客-CSDN博客
边沿中断中如何判断上升沿和下降沿? - Kinetis - 恩智浦技术社区
ESP32-S2 PWM输入捕获_李法师_的博客-CSDN博客_esp32 脉冲计数器

附件下载
esp32_mouse_keyboard.zip
团队介绍
汕头大学 姚罗然
团队成员
136ytr
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号