基于ESP32+IO扩展板实现的USB键盘鼠标设备
本项目是基于2023寒假一起练平台(5)- 基于ESP32 WiFi 的综合应用完成的项目5- 实现一个USB键盘鼠标设备,摇动游戏手柄实现鼠标的移动,一个按键实现左键点击,另一个按键按下实现键盘敲入一串字符"eetree.cn"
标签
嵌入式系统
USB
ESP32
2023寒假在家练
MALossov
更新2023-03-27
电子科技大学
1408

内容介绍

项目介绍

本项目是基于2023寒假一起练平台(5)- 基于ESP32 WiFi 的综合应用完成的项目5- 实现一个USB键盘鼠标设备,使用IO扩展板上的一个用X、Y二轴电位计制作的游戏手柄,并且此芯片支持USB通信。实现一个USB鼠标&键盘复合设备,摇动游戏手柄实现鼠标的移动,一个按键实现左键点击,另一个按键按下实现键盘敲入一串字符"eetree.cn"

简单的硬件介绍 板卡详情

  1. ESP32-S2-MINI-1模块:ESP32 S2 开发板除了ESP32wifi模组之外还集成了USB TYPE -C接口,两个按键,一个电源指示灯,一个用户LED灯,2排10pin的排针,将重要IO引出。使用USB供电或通过排针3.3V供电。ESP32-S2 是一款高度集成、高性价比、低功耗、主打安全的单核 Wi-Fi SoC,具备强大的功能和丰富的 IO 接口。

  2. 输入、输出扩展板:由于本次做的项目只需要使用旋转编码器/按键、摇杆两处IO扩展板外设,所以简单列一下这里的外设:

    1. 按键、旋转编码器输入 - 以模拟信号的方式

    2. 双电位计控制输入/摇杆 - 以数字信号的方式

功能分析

由于本次做的项目只需要使用旋转编码器/按键、摇杆两处IO扩展板外设,所以简单分析一下这两处的电路:

  1. IO扩展板卡上的旋转编码器和按键被接入了一组电阻网络:FmSE9o6gxci-po5etRQT-tqsvmtm
    1. 在这部分电阻网络当中,将旋转编码器顺时针和逆时针转动时,A、B脚产生的相位不同脉冲信号引入到电阻网络当中,通过电阻分压,实现了将PWN信号转换为模拟信号的功能。

    2. 当转动旋转编码器时,可以将A、B两脚发出的PWM波,视为加载R16和R17电阻上的电压在不同时刻交替变化,产生的A_Out使用ESP32采样之后,可以看到电平锯齿状变化,其中下降后保持的电平不同,此时使用状态机可以进行判断。

    3. 同理,按下旋转编码器后,S2产生低电平,被加载到C7附近,A_Out被降低至一个更低的值。此时,A_Out为一个稳定的,较低的电压值。

    4. 同样,当K1被按下时,电压被降低至最低值。

  2. IO扩展板卡上的摇杆器件FJ08K与一个四运放LMV324的网络相连如图所示:FhTxQgm4oLI5PVv1qBl4UHYpiFBv

    1. 此时,将4运放分开来看:(这部分看的不是很细,可能说错了一些)

      1. U2D的作用为将电压分压之后,实现为X+的输出提供积分(借助C2),以及为U2A的V+以及X-进行电压传递的任务。——总体来说起一个提供电压基准的工作

      2. U2A的作用为实现一个R-C迟滞-震荡电路。U2A的输出端会产生一个震荡信号。

      3. U2B的V+与Y的输出相连,Y的输出为一个在Vcc_GND之间的分压;U2B的V-与震荡信号相连。可以看出,V2B被当做一个比较器使用,产生的信号为VCC-GND的PWM信号。

      4. U2C没有被使用。

      5. 总结一下: 在该电路当中,将2组分压信号输入转化为一个PWM信号。

设计思路

在前文已经分析过了两处重要的外设电路,所以在这里不再分析电路,直接说代码思路。

    1. 总体思路:

      1. 使用ARDUINO-IDE进行开发:在ESP-IDF当中,下载/调试使用的USB-CDC和键盘外设存在一定冲突(堆栈问题),而ARDUINO-IDE对于USB处理有着另外的方法,因此这里使用ARDUINO-IDE进行项目的开发,以方便调试值的打印和USB外设功能能够两全其美。Ft1KqE6E0MEQ44JH-b7WYz2oVORW

      2. 使用定时器完成外设扫描、更新任务:由于本次项目需要频繁采集模拟值、PWM占空比等值,因此在获取信号时,如果全部加入loop当中,对于模拟量和数字量的采集也不用那么频繁,必然添加延时语句,会造成处理逻辑的阻塞等不良情况,因此采用ESP32S2的硬件定时器完成轮询功能。

        hw_timer_t* timer_analog = NULL;
        hw_timer_t* timer_joystick = NULL;
        hw_timer_t* timer_led_fade = NULL;
      3. 使用USB库: ARDUINO的ESP库提供了良好的硬件外设支持,键盘、鼠标等只需要导入HID库即可:

        #include "USB.h"
        #include "USBHIDKeyboard.h"
        #include "USBHIDMouse.h"
      4. 使用状态机判断模拟按键状态: 由于模拟电压不一定保持稳定,读取时有出现噪声和毛刺的可能,因此在轮询模拟电压时,采用状态机的方式再三确认按键被按下,同时也避免了枯燥的count

        typedef enum key_status {
            IDLE,
            PrePressed,
            OnPressed,
            Pressed,
            AfterPressed,
            Released
        }key_status;
      5. 在主程序中才进行一些外设的调用: 可能是ARDUINO外设支持的问题,extern的变量可能不会在定时任务中被正确更新(比如TFT屏幕),因此在外设支持当中,只传递值,而较少直接调用第三方外设防止出现意外问题。主循环例子如下:

          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);
          }

所有模拟值和PWM的值都是在特定环境下测试的,如果需要迁移代码,请自行重新打表!!!!

  1. 旋转编码器和按钮部分:由于这里主要使用模拟采样,因此使用结构体保存每次采样的结果,同时加入按键状态机和判断函数,对于获取的电压值进行处理、判断按键状态。
    1. 因为需要获取模拟量,因此直接使用ARDUINO的内置函数实现:akeys.analogValue = analogRead(1);其中,akeys是所有按键的结构体,analogRead为获取一个0-4095之间的模拟值(此时的模拟方案设置为:analogReadResolution(12);,1为连接IO扩展板的引脚编号)

    2. 在定时器中,每次判断按键是否合理后,即进入更新状态的阶段,只有当按键被确认为是Pressed的状态之后,才在主程序当中进行更新。(实例程序如上)

    3. 为了防止模拟电压的噪声,在状态机中采用冗余设计,通过反复采集模拟量再更新状态,代码实现如:

      /**
       *@brief 更新传入的状态
       * 
       * @param status_old 上一次的状态
       * @return key_status 本次的状态
       */
      key_status ModifyKey(key_status status_old) {
          switch (status_old) {
          case PrePressed:
              return OnPressed;
              break;
          //省略一些状态判定,作为冗余设计,最后才判定为Pressed
              case Released:  //Release不单独更新
              return Released;
              break;
          default:
              return IDLE;
              break;
          }
      }
    4. 为保证每次状态更新的正确性,在“打表”算出每个按键的模拟量之余,采用宏来设置一个合理的阈值,防止采入其他量,打表和阈值(ACCURACY宏)如图:

      #define CLOCKWISE_V 3400
      #define ANTICLOCKWISE_V 3300
      #define ENCODER_PRESSED_V 2800
      #define KEY_V 1000
      #define ACCURACY 1000
    5. 由于两种旋转当中的“电压台阶”过于相近,因此最后的代码当中,我选择放宽电压阈值,忽略这两种状态——并未使用正转和反转这两种行为,而是仅仅将:

      1. 旋转编码器按下: 设置为左键

      2. 按钮按下: 设置为打印项目要求的“eetree.cn”字符串。

  2. 摇杆部分:

    1. 使用Arduino的内置函数pulseIN(<pin>,<level>)进行采集,采集到的信号为电平为<level>时在<pin>引脚上产生的脉冲计数。因为摇杆位置不同时,X-Y之间产生的脉冲高电平时长不同,这里暂且使用这个高电平的值作为判断摇杆方向的依据。打表如下,被封装为一个枚举类型:

      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;
    2. 但是其实相当难按出对应打表的值,因此添加一个容差,让我们更方便按出这些值,同时,斜上方的值更难按出,因此,添加一个容差系数,代码如下:

      // 定义容差和容差系数OBLIQUE
      #define RANGE 1 //每次鼠标的运动范围
      #define ACCURACY 40
      #define OBLIQUE 4
      
      //省略调试和过程代码,进入扫描函数
      
      //pulsein被保存为duration变量
        if (duration > Up_Left - ACCURACY * OBLIQUE && duration < Up_Left + ACCURACY * OBLIQUE) {
          return Up_Left;
        } else if (duration > Up - ACCURACY && duration < Up + ACCURACY) {
          return Up;
        } else if
            //之后的代码省略
    3.  此时再对方向进行值更新的封装(多层封装是因为为了测试能否在.h之中就操作外设,结果是不行,但是保留了这个利于解耦合的分层结构):
      //解耦函数
      void MoveMouse(joyStickDest joystick, int *x_dis, int *y_dis) {
        switch (joystick) {
          case Up:
            *x_dis = 0;
            *y_dis = -RANGE;
            break;
          case Up_Right:
            *x_dis = RANGE;
            *y_dis = -RANGE;
            break;
        //省略其他case
                
      //主程序业务代码
      if (x_dis != 0 || y_dis != 0) {
          digitalWrite(LED2_PIN,HIGH);
          Mouse.move(x_dis, y_dis, 0);
        }
  3. 屏幕显示和其他杂项:

    1. 屏幕显示一开始想做的,也引入了#include <Adafruit_ST7735.h>这个库,但是发现,由于没有连接ESP32S2的SPI外设,而采用软件SPI的刷新速度特别慢,因此这个方案被放弃了。本来预期的显示效果如下:Fj2FvWCXFMenq8NyNu87_4A6JThj

      1. 将屏幕取而代之的是使用指示灯来表示是否有操作,同时在程序开开始将屏幕刷新为黑屏 来判断是否正确进入主程序(部分时候,电脑并不会识别外设,因此屏幕默认是白色的)。

    2. 指示灯:使用定时器操作ESP32S2模组上的指示灯(即LED_GREEN,被分配在40脚),定时器更新时关闭指示灯,有操作时开启指示灯。(本来预计用扩展板上的灯,结果发现被硬件串口占了,就没使用)

      void IRAM_ATTR led_fade_callback(){
        digitalWrite(LED1_PIN,LOW);
        digitalWrite(LED2_PIN,LOW);   //后来未使用串口灯,两个灯的宏被定义为同一个
      }
      1. 具体效果为:有按键操作时,指示灯轻微亮一下;当操作摇杆时,灯常亮Fi_gsJnVvBIekR4hGlTkH2_wmI78
      2. 引入DEBUG宏: 串口发送是相当占时间的,同时由于在Linux下开发,缺乏良好的串口绘图软件,因此希望一次只检测一个串口值,使用DEBUG宏控制各个文件当中监测量的输出,如:

        #define DEBUG_JOYSTICK
        
        //在功能函数当中
        #ifdef DEBUG_JOYSTICK
          Serial.printf("%ld\n", duration); //打印当前获取的脉冲数
        #endif

框图和软件流程图 框图

ForUvha01uMsP0QArHicO1dngoBB

软件流程图

 

FuQuXpIdSuNUdlhvOuDP5uij8Kr3

 

实现的功能及图片展示 实现功能

  1. 可以根据摇杆输入实现鼠标的8相移动(斜上方和斜下方可能比较难按,但是正方向绝对没问题)。

  2. 旋转编码器按下与鼠标左键的功能等同,每次按下只持续一次点击。

  3. 按键按下可以在光标选定区域打印:eetree.cn

图片展示

详情请看视频,图片静态效果比较差

Ft_t60rVrHXOiWDFpmgammHU63h6FvPLNINZ6AaB05kFeZO4g4toMzHVFmg-m7KGZCDS3LlZ_TbYqV0--mtH

鼠标残影拍的比较差,抱歉抱歉

Fkm-zV_01nx2fcFJJ6wyTVnWQZY1

打印EETREE

 

主要代码片段及说明

项目主程序

#include "AnalogKeys.h"
#include "JoyStick.h"
#include "displayStatus.h"

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

#include <Adafruit_GFX.h>     // Core graphics library
#include <Adafruit_ST7735.h>  // Hardware-specific library for ST7735

#define ARDUINO_FEATHER_ESP32

#define TFT_CS 13
#define TFT_RST 18
#define TFT_DC 17
#define TFT_MOSI 21  // Data out
#define TFT_SCLK 41  // Clock out

Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_MOSI, TFT_SCLK, TFT_RST);


#define LED1_PIN 40
#define LED2_PIN 40
void IRAM_ATTR led_fade_callback();

USBHIDMouse Mouse;

#define DEBUG_MAIN

USBHIDKeyboard Keyboard;
hw_timer_t* timer_analog = NULL;
hw_timer_t* timer_joystick = NULL;
hw_timer_t* timer_led_fade = NULL;

int count = 0;

AnalogReadKeys akeys;
void IRAM_ATTR analog_key_read();

int x_dis, y_dis;
void ARDUINO_ISR_ATTR joystick_read();
extern unsigned long duration;
joyStickDest joystickdst;

void setup() {
#ifdef DEBUG_MAIN
  Serial.begin(115200);
#endif
  //set the resolution to 12 bits (0-4096)
  analogReadResolution(12);
  Mouse.begin();
  Keyboard.begin();
  USB.begin();

  pinMode(JOY_STICK_PIN, INPUT);
  //调试灯
  pinMode(LED1_PIN,OUTPUT);
  pinMode(LED2_PIN,OUTPUT);

  tft.initR(INITR_144GREENTAB);  // Init ST7735R chip, green tab
  tft.fillScreen(ST77XX_BLACK);

  timer_analog = timerBegin(0, 80, true);                      //设置定时器0,80分频,定时器是否上下计数
  timerAttachInterrupt(timer_analog, &analog_key_read, true);  //定时器地址指针,中断函数名称,中断边沿触发类型
  timerAlarmWrite(timer_analog, 4000, true);                   //操作那个定时器,定时时长单位us,是否自动重装载
  timerAlarmEnable(timer_analog);                              //打开那个定时器

  //摇杆控制函数
  
  timer_joystick = timerBegin(1, 80, true);
  timerAttachInterrupt(timer_joystick, &joystick_read, true);
  timerAlarmWrite(timer_joystick, 7000, true);
  timerAlarmEnable(timer_joystick);

  timer_led_fade = timerBegin(2,80,true);
  timerAttachInterrupt(timer_led_fade,&led_fade_callback,true);
  timerAlarmWrite(timer_led_fade,10000,true);
  timerAlarmEnable(timer_led_fade);
  
  
}

void loop() {


  
  if (akeys.key == KEY && akeys.status == Pressed) {
    count++;
    // type out a message
    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() {
  joystickdst = scanJoyStick();
  MoveMouse(joystickdst, &x_dis, &y_dis);
}

void IRAM_ATTR led_fade_callback(){
  digitalWrite(LED1_PIN,LOW);
  digitalWrite(LED2_PIN,LOW);  
}

按键输入控制

/*
 * @Description:
 * @Author: MALossov
 * @Date: 2023-03-10 23:04:41
 * @LastEditTime: 2023-03-12 19:58:18
 * @LastEditors: MALossov
 * @Reference:
 */
#ifndef _ANALOGKEYS_H_
#define _ANALOGKEYS_H_

// #define DEBUG_ANALOGKEYS


#define CLOCKWISE_V 3400
#define ANTICLOCKWISE_V 3300
#define ENCODER_PRESSED_V 2800
#define KEY_V 1000
#define ACCURACY 1000

typedef enum keys {
    ClockWise,
    AntiClockWise,
    EncoderPressed,
    KEY
}keys;

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);
void ScanKeys(AnalogReadKeys* akey);

/**
 *@brief 更新传入的状态
 * 
 * @param status_old 上一次的状态
 * @return key_status 本次的状态
 */
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 > 3600 && (akey->status == AfterPressed || akey->status == Pressed))
    {
        akey->status = Released;
    }

    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;
        }
    }
    else if (akey->analogValue > ANTICLOCKWISE_V && akey->analogValue < ANTICLOCKWISE_V + ACCURACY) {
        if (akey->key == AntiClockWise)
            akey->status = ModifyKey(akey->status);
        else
        {
            akey->key = AntiClockWise;
            akey->status = PrePressed;
        }
    }
    else if (akey->analogValue > ENCODER_PRESSED_V && akey->analogValue < ENCODER_PRESSED_V + ACCURACY) {
        if (akey->key == EncoderPressed)
            akey->status = ModifyKey(akey->status);
        else
        {
            akey->key = EncoderPressed;
            akey->status = PrePressed;
        }
    }
    else if (akey->analogValue > KEY_V && akey->analogValue < KEY_V + ACCURACY) {
        if (akey->key == KEY)
            akey->status = ModifyKey(akey->status);
        else
        {
            akey->key = KEY;
            akey->status = PrePressed;
        }
    }
#ifdef DEBUG_ANALOGKEYS
    Serial.println(akey->analogValue);
    // if (akey->status == Pressed) {
    //     Serial.println(akey->key);
    // }
#endif // DEBUG
}


#endif
 

摇杆输入控制

#include <Arduino.h>

#ifndef _JOYSTICK_H_
#define _JOYSTICK_H_

#define JOY_STICK_PIN 2

#define RANGE 1

#define ACCURACY 40
#define OBLIQUE 4
#define DEBUG_JOYSTICK

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;

static unsigned long duration;

joyStickDest scanJoyStick() {

  duration = pulseIn(JOY_STICK_PIN, HIGH);
#ifdef DEBUG_JOYSTICK
  // if (joystick != CENTER) {
  // Serial.print("joystick: ");
  Serial.printf("%ld\n", duration);
  // }
#endif
  //判断duration的值与joyStickDest的值(误差允许为ACCURACY)
  if (duration > Up_Left - ACCURACY * OBLIQUE && duration < Up_Left + ACCURACY * OBLIQUE) {
    return Up_Left;
  } else if (duration > Up - ACCURACY && duration < Up + ACCURACY) {
    return Up;
  } else if (duration > Up_Right - ACCURACY * OBLIQUE && duration < Up_Right + ACCURACY * OBLIQUE) {
    return Up_Right;
  } else if (duration > Right - ACCURACY && duration < Right + ACCURACY) {
    return Right;
  } else if (duration > Down_Right - ACCURACY * OBLIQUE && duration < Down_Right + ACCURACY * OBLIQUE) {
    return Down_Right;
  } else if (duration > Down - ACCURACY && duration < Down + ACCURACY) {
    return Down;
  } else if (duration > Down_Left - ACCURACY * OBLIQUE && duration < Down_Left + ACCURACY * OBLIQUE) {
    return Down_Left;
  } else if (duration > Left - ACCURACY && duration < Left + ACCURACY) {
    return Left;
  } else {
    return CENTER;
  }
}

void MoveMouse(joyStickDest joystick, int *x_dis, int *y_dis) {
  switch (joystick) {
    case Up:
      *x_dis = 0;
      *y_dis = -RANGE;
      break;
    case Up_Right:
      *x_dis = RANGE;
      *y_dis = -RANGE;
      break;
    case Right:
      *x_dis = RANGE;
      *y_dis = 0;
      break;
    case Down_Right:
      *x_dis = RANGE;
      *y_dis = RANGE;
      break;
    case Down:
      *x_dis = 0;
      *y_dis = RANGE;
      break;
    case Down_Left:
      *x_dis = -RANGE;
      *y_dis = RANGE;
      break;
    case Left:
      *x_dis = -RANGE;
      *y_dis = 0;
      break;
    case Up_Left:
      *x_dis = -RANGE;
      *y_dis = -RANGE;
      break;
    default:
      *x_dis = 0;
      *y_dis = 0;
      break;
  }
}

#endif
遇到的主要难题及解决方法
  1. 模拟量的获取和PWM脉冲的捕获都需要打表,打表后结果不准。

    1. 解决方法:重新打表,添加允许的阈值。

  2. 使用ESP-IDF开发发现,CDC和USB-HID库难以共存。

    1. 解决方法:换ARDUINO,技术能力暂时无法驾驭ESP-IDF。

  3. 调试多文件时串口结果难以判断是谁发出的。

    1. 解决方法:引入DEBUG宏,分别观看每个文件的情况。

  4. 模拟量按键容易出现一次按下,被判定按下多次的情况:

    1. 解决方法:引入状态机,定时任务获取模拟量。

未来的计划或建议

本次开发当中,主要的困难和挑战在于技术路线的选择、开发工具的选择、校准一些需要打表的值。极度增进了我对于结构体、枚举类型的认知以及串口调试的能力。

当然,目前的打表肯定是一个折中的方案,妥协的产物,在未来,肯定需要继续精进对于模拟电路的认知,为摇杆的PWM方案给出更为科学合理的计算方案,而不是只能针对8个方向打表(还打的不准);同时在ANALOG-KEY当中也舍弃了正转和反转的输出,这些需要算法的加持。

未来的规划便是等能力足够之后,使用ESP-IDF重新开发本项目——精进PWM量的获取与摇杆关系的对应,不再使用打表而是使用计算,实现更好的鼠标移动效果;通过算法能够判断旋转编码器的正转与反转,而不是舍弃这一功能;同时解决一下USB-CDC和USB-HID无法公用的问题(自己创建USB协议栈)。

可以说,本次项目仅仅是完成了基础的功能而已,而没有对于这块板卡的潜力有着更好的开发,同时也照见了自己的很多不足——模拟电路分析能力差,算法功底不行,对于单片机内部存储结构理解有所欠缺。

未来,希望能够一一补上这些遗憾!

附件下载

JoyStickKey-MouseKeyboard.zip
源代码文件
JoyStickKey-MouseKeyboard.ino.elf
烧录文件,编译环境是UBUNTU,不知道WINDOWS下是否能够烧录
esp32s2.pin.xlsx
引脚对应关系分享以及打表,如果需要烧录固件可以考虑重新打表保证你的板子也能正确判断摇杆

团队介绍

个人任务 姓名:李书扬 学校:电子科技大学 大二 网络工程专业
团队成员
MALossov
好菜好菜的小辣鸡!

评论

0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号