找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

394

积分

0

好友

52

主题
发表于 昨天 08:05 | 查看: 6| 回复: 0

本项目基于ESP32-S2,实现了一个USB键盘与鼠标的复合设备。利用IO扩展板上的双轴电位计摇杆控制鼠标移动,旋转编码器按键模拟鼠标左键点击,独立按键用于触发键盘输入一串预置字符“eetree.cn”。

硬件介绍

1. ESP32-S2-MINI-1模块

该开发板集成ESP32-S2 Wi-Fi SoC、USB Type-C接口、用户按键及LED指示灯,并通过排针引出关键IO。它支持USB供电或3.3V排针供电,是一款功能丰富且主打安全的单核Wi-Fi芯片。

2. 输入、输出扩展板

本项目主要使用了扩展板上的两处外设:

  • 旋转编码器及按键 - 以模拟信号方式输入
  • 双轴电位计摇杆 - 以数字信号(PWM)方式输入

功能分析与设计思路

外设电路分析

1. 旋转编码器与按键电路
这部分外设连接至一个电阻网络,将旋转编码器产生的PWM相位信号和按键开关状态转换为不同的模拟电压值,供ESP32-S2的ADC采样。

旋转编码器与按键的电阻网络电路图

其工作原理如下:

  • 旋转编码器转动时,A、B脚产生的PWM波在电阻网络分压下,使输出端 A_Out 产生具有特定台阶的锯齿状电平,通过状态机可判断转动方向。
  • 按下旋转编码器或独立按键(K1)时,会分别将 A_Out 电压拉低至不同的稳定值,从而区分不同按键动作。

2. 摇杆电路
摇杆(FJ08K)连接到一个由四运放LMV324构成的电路,将X、Y轴的两组分压信号转化为一个PWM信号输出。

摇杆与四运放LMV324电路图

电路简析:

  • U2D为后续电路提供电压基准。
  • U2A与外围RC构成振荡电路,产生震荡信号。
  • U2B作为比较器,将摇杆的分压信号与震荡信号比较,最终输出PWM信号(PWM_OUT)。
  • 总结:该电路将摇杆的模拟位置信息转换为脉宽可变的数字PWM信号。

软件设计思路

1. 总体思路

  • 开发环境选择:选用Arduino IDE而非ESP-IDF。主要因为Arduino对USB HID的支持更友好,且便于通过串口(USB-CDC)打印调试信息,避免了ESP-IDF中USB-CDC与HID可能存在的堆栈冲突问题。
    Arduino core for ESP32 系列支持页面
  • 定时器轮询:为了避免在loop主循环中使用延时导致阻塞,采用ESP32-S2的硬件定时器来定期执行模拟量采样和PWM捕获任务,保证系统响应及时。
    hw_timer_t* timer_analog = NULL;
    hw_timer_t* timer_joystick = NULL;
    hw_timer_t* timer_led_fade = NULL;
  • USB HID库:直接使用Arduino-ESP32核心库提供的USB HID功能,简化开发。
    #include "USB.h"
    #include "USBHIDKeyboard.h"
    #include "USBHIDMouse.h"
  • 状态机防抖:针对模拟电压读取可能存在的噪声,为按键设计了状态机,确保只有稳定且确认的按压动作才会被识别,提高了可靠性。
    typedef enum key_status {
        IDLE,
        PrePressed,
        OnPressed,
        Pressed,
        AfterPressed,
        Released
    }key_status;
  • 主循环驱动:为避免在中断服务程序(ISR)中直接调用复杂外设可能引发的问题,定时器中断仅负责更新状态和数值,所有HID设备(鼠标、键盘)的最终操作都在loop()主循环中执行。

⚠️ 重要提示:所有模拟量和PWM的阈值都是在特定硬件环境下测试得出的。如果迁移代码,请务必根据实际硬件重新校准!

核心功能实现详解

1. 旋转编码器与按键处理

这部分通过ADC采样模拟电压来识别动作。

关键实现:

  1. ADC采样:使用analogReadResolution(12)设置12位分辨率,并通过akeys.analogValue = analogRead(1);读取电压值(0-4095)。
  2. 状态判断:在定时器中断中,将采样到的电压值与预设阈值进行比较,并通过状态机ModifyKey()函数更新按键状态。只有状态确认为Pressed时,主程序才会执行相应操作。
  3. 阈值与冗余设计:为每个按键动作定义了中心电压值,并设置一个较大的容差ACCURACY。状态机中的冗余状态(如PrePressed, OnPressed, AfterPressed)确保了按键动作被准确、唯一地识别。
    #define CLOCKWISE_V 3400
    #define ANTICLOCKWISE_V 3300
    #define ENCODER_PRESSED_V 2800
    #define KEY_V 1000
    #define ACCURACY 1000
  4. 功能映射:由于旋转编码器顺时针与逆时针的电压阈值非常接近,难以稳定区分,最终项目简化了设计:
    • 旋转编码器按下:映射为鼠标左键单击。
    • 独立按键(K1)按下:触发键盘打印字符串 eetree.cn

2. 摇杆处理

通过捕获PWM信号的高电平脉宽来判断摇杆方向。

关键实现:

  1. PWM捕获:使用Arduino的pulseIn(JOY_STICK_PIN, HIGH)函数读取指定引脚上高电平脉冲的持续时间(微秒)。
  2. 方向打表:通过实测,为八个方向及中心点定义了对应的脉宽值。
    typedef enum joyStickDest {
      Up = 1178,
      Up_Right = 1111,
      Right = 1384,
      Down_Right = 1815,
      Down = 2850,
      Down_Left = 3000,
      Left = 2640,
      Up_Left = 1650,
      CENTER = 2000
    } joyStickDest;
  3. 容差处理:考虑到很难精确达到理论值,为每个方向添加了容差范围ACCURACY。对于更难触达的斜方向(如Up_Left),则使用更大的容差(ACCURACY * OBLIQUE)。
    #define RANGE 1 //鼠标单次移动像素
    #define ACCURACY 40
    #define OBLIQUE 4
  4. 鼠标移动:根据识别到的方向,通过MoveMouse()函数计算x_disy_dis位移量,最终在主循环中调用Mouse.move(x_dis, y_dis, 0)实现鼠标移动。

3. 其他功能

  • 显示方案:最初计划使用TFT屏幕显示状态,但因软件SPI刷新慢而放弃。改为用板载LED指示灯提示操作状态:按键按下时闪烁,摇杆移动时常亮。
  • 调试宏:为方便调试,在各个模块中定义了DEBUG宏(如DEBUG_JOYSTICK),可以灵活控制串口信息的输出,避免大量打印影响性能。

系统架构与流程

1. 系统框图
展示了硬件模块与软件功能之间的信号流关系。
基于ESP32S2的USB HID复合设备系统架构图

2. 软件流程图
概括了程序从初始化到主循环处理的基本流程。
程序软件流程图

实现效果

  • 鼠标控制:摇杆可实现八个方向的鼠标移动(正方向稳定,斜方向因容差设置可能需轻微调整)。
  • 左键点击:按下旋转编码器,触发一次鼠标左键点击。
  • 键盘输入:按下独立按键K1,在当前光标位置输入字符串“eetree.cn”。

功能展示图
摇杆控制鼠标移动效果演示
按键触发键盘输入字符串演示

核心代码摘要

1. 主程序框架 (main.cpp)

#include "AnalogKeys.h"
#include "JoyStick.h"
#include "USB.h"
#include "USBHIDKeyboard.h"
#include "USBHIDMouse.h"

#define LED1_PIN 40
#define LED2_PIN 40

USBHIDMouse Mouse;
USBHIDKeyboard Keyboard;
hw_timer_t* timer_analog = NULL;
hw_timer_t* timer_joystick = NULL;
AnalogReadKeys akeys;
int x_dis, y_dis;

void setup(){
  analogReadResolution(12);
  Mouse.begin();
  Keyboard.begin();
  USB.begin();
  pinMode(JOY_STICK_PIN, INPUT);
  pinMode(LED1_PIN,OUTPUT);
  pinMode(LED2_PIN,OUTPUT);

  // 初始化并启动定时器
  timer_analog = timerBegin(0, 80, true);
  timerAttachInterrupt(timer_analog, &analog_key_read, true);
  timerAlarmWrite(timer_analog, 4000, true);
  timerAlarmEnable(timer_analog);

  timer_joystick = timerBegin(1, 80, true);
  timerAttachInterrupt(timer_joystick, &joystick_read, true);
  timerAlarmWrite(timer_joystick, 7000, true);
  timerAlarmEnable(timer_joystick);
}

void loop(){
  // 处理按键打印字符串
  if (akeys.key == KEY && akeys.status == Pressed) {
    digitalWrite(LED1_PIN,HIGH);
    Keyboard.print("eetree.cn");
  }
  // 处理编码器按下作为鼠标左键
  if (akeys.key == EncoderPressed && akeys.status == Pressed) {
    if (!Mouse.isPressed(MOUSE_LEFT)) {
      Mouse.press(MOUSE_LEFT);
      digitalWrite(LED1_PIN,HIGH);
    }
  } else {
    Mouse.release(MOUSE_LEFT);
  }
  // 处理摇杆移动鼠标
  if (x_dis != 0 || y_dis != 0) {
    digitalWrite(LED2_PIN,HIGH);
    Mouse.move(x_dis, y_dis, 0);
  }
}

// 定时器中断服务程序
void IRAM_ATTR analog_key_read(){
  akeys.analogValue = analogRead(1);
  ScanKeys(&akeys);
}
void IRAM_ATTR joystick_read(){
  joyStickDest joystickdst = scanJoyStick();
  MoveMouse(joystickdst, &x_dis, &y_dis);
}

2. 按键状态机模块 (AnalogKeys.h 节选)

typedef enum key_status { IDLE, PrePressed, OnPressed, Pressed, AfterPressed, Released } key_status;
typedef struct AnalogReadKeys {
    int analogValue;
    keys key;
    key_status status;
}AnalogReadKeys;

key_status ModifyKey(key_status status_old){
    switch (status_old) {
    case PrePressed: return OnPressed; break;
    case OnPressed: return Pressed; break;
    case Pressed: return AfterPressed; break;
    case AfterPressed: return AfterPressed; break;
    case Released: return Released; break;
    default: return IDLE; break;
    }
}

void ScanKeys(AnalogReadKeys* akey){
    // 根据模拟电压值判断按键,并更新状态
    if (akey->analogValue > CLOCKWISE_V && akey->analogValue < CLOCKWISE_V + ACCURACY) {
        if (akey->key == ClockWise)
            akey->status = ModifyKey(akey->status);
        else {
            akey->key = ClockWise;
            akey->status = PrePressed;
        }
    }
    // ... 其他按键判断逻辑(ANTICLOCKWISE_V, ENCODER_PRESSED_V, KEY_V)
}

遇到的问题与解决方案

  1. 阈值不准:模拟电压和PWM脉宽的实测值与理论偏差大。

    • 解决:重新测量并校准阈值,引入容差机制。
  2. 开发环境冲突:在ESP-IDF中难以同时使用USB-CDC(串口调试)和USB-HID功能。

    • 解决:切换到对USB HID支持更完善的Arduino IDE进行开发,这涉及到开发工具和调试方法的选择,你可以在 云栈社区 的基础技术板块找到更多关于环境配置和调试技巧的讨论。
  3. 调试信息混杂:多文件编程时,串口输出难以溯源。

    • 解决:为每个模块定义独立的DEBUG宏,分段启用调试输出。
  4. 按键误触发:模拟电压波动导致一次按压被多次识别。

    • 解决:引入多状态的状态机,在定时器中多次采样确认后才判定为有效按压。

总结与展望

本项目成功利用ESP32-S2的USB和ADC、PWM捕获功能,实现了一个功能完整的HID复合设备。整个过程加深了对状态机编程、定时器使用以及USB HID协议的理解。

当然,目前的实现仍有改进空间:

  1. 摇杆控制:目前依赖经验“打表”,未来可通过分析电路原理,建立PWM脉宽与摇杆坐标的数学模型,实现更平滑、线性的鼠标控制。
  2. 编码器功能:当前放弃了旋转方向识别。未来可通过改进算法,区分正转与反转,并将其映射为鼠标滚轮或其他键盘功能。
  3. 开发框架:最终希望能在ESP-IDF环境下实现,并解决USB复合设备驱动的更深层次问题。

这只是一个起点,通过对硬件更深入的理解和软件算法的优化,还能挖掘出该硬件平台的更多潜力。如果你对嵌入式开发和USB协议栈感兴趣,欢迎在技术社区进行更深入的交流与探讨。




上一篇:渗透测试侦察实战:利用多工具组合进行子域名枚举与资产发现
下一篇:Golang项目文档生成:从godoc到工程级API文档的完整指南与实践
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-18 19:47 , Processed in 0.256563 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表