verylowfreqのアイコン画像
verylowfreq 2024年06月16日作成 (2024年06月17日更新)
製作品 製作品 閲覧数 135
verylowfreq 2024年06月16日作成 (2024年06月17日更新) 製作品 製作品 閲覧数 135

マクロパッド基板を作る/Arduinoシールド基板の設計

マクロパッド基板を作る/Arduinoシールド基板の設計

Arduino UNOサイズでマクロパッドが作りたくなったので、シールド基板を設計しました。Arduino LeonardoとSuzuduino UNO (自作のCH32Vボード) でコードを書きます。
自作キーボードの文脈でのマクロパッドだとArduino Pro Microを利用するケースが多いと思いますが、個人的に最近Arduino UNOのフォームファクターにはまっているので、あえてのUNOシールド基板での製作にトライです。

この記事は動画の補足記事です。三峰スズはものづくりや電子工作を楽しむVTuberとして活動しています。
ぜひ動画のほうも見てね!

ここに動画が表示されます

コード全体はGitHubにも置いてあります。
https://github.com/verylowfreq/switchnobshield_samples

基板の設計

スイッチが4つ、ロータリーエンコーダーが2つ(押し込み操作あり)を接続するだけなので、回路としてはシンプルです。
ピン割り当ては本家Arduinoだけを考えるならば制約はないのですが、Suzuduino UNO / Suzuno32RVでの利用を想定して、複雑な事情を抱えるピンを避けた結果、このような割り当てになっています。
回路図

基板はArduino UNOシールドサイズです。さほど広い面積ではないので、スイッチやノブ同士の隙間があまり確保できませんでした。
スイッチはCherry MX互換、ロータリーエンコーダーはEC11互換です。よく使われるものですね。

基板

できあがった基板

この規模のはんだづけは気楽にできていいですね。まったりと無心にはんだづけしたいときにも好適かもしれません。はんだづけの様子はぜひ動画にて!

組み立て例

コード例

Arduino Leonardo (本家Arduino系)

Arduino UNOサイズのボードでUSB機能を持っているものは、Arduino LeonardoやArduino UNO R4があります。これらにはUSBキーボード・マウスとして振舞うための "Keyboard" / "Mouse" ライブラリがあるので、かんたんに実装できます。
ロータリーエンコーダーもライブラリがありますが割り込みを利用しないものを選定し、Suzuduino UNOでも利用できるようにしました。

// RotaryEncoder by Matthias Hertel
#include <RotaryEncoder.h>
#include <Keyboard.h>

constexpr int PIN_SWITCH[6] = { 4,5,6,7,10,11 };

RotaryEncoder encoder1(A2, A3, RotaryEncoder::LatchMode::FOUR3);
RotaryEncoder encoder2(A4, A5, RotaryEncoder::LatchMode::FOUR3);

void setup() {
  for (int i = 0; i < 6; i++) {
    pinMode(PIN_SWITCH[i], INPUT_PULLUP);
  }

  Keyboard.begin();
}

void loop() {
  encoder1.tick();
  encoder2.tick();

  int dir1 = (int)encoder1.getDirection();
  if (dir1 > 0) {
    Keyboard.press('l');
    Keyboard.release('l');
  } else if (dir1 < 0) {
    Keyboard.press('r');
    Keyboard.release('r');
  }
  int dir2 = (int)encoder2.getDirection();
  if (dir2 > 0) {
    Keyboard.press('L');
    Keyboard.release('L');
  } else if (dir2 < 0) {
    Keyboard.press('R');
    Keyboard.release('R');
  }

  if (digitalRead(PIN_SWITCH[0]) == LOW) {
    Keyboard.press('1');
  } else {
    Keyboard.release('1');
  }
  if (digitalRead(PIN_SWITCH[1]) == LOW) {
    Keyboard.press('2');
  } else {
    Keyboard.release('2');
  }
  if (digitalRead(PIN_SWITCH[2]) == LOW) {
    Keyboard.press('3');
  } else {
    Keyboard.release('3');
  }
  if (digitalRead(PIN_SWITCH[3]) == LOW) {
    Keyboard.press('4');
  } else {
    Keyboard.release('4');
  }
  if (digitalRead(PIN_SWITCH[4]) == LOW) {
    Keyboard.press('5');
  } else {
    Keyboard.release('5');
  }
  if (digitalRead(PIN_SWITCH[5]) == LOW) {
    Keyboard.press('6');
  } else {
    Keyboard.release('6');
  }
}

Suzuduino UNO / Suzuno32RV (WCH CH32V CH32V203)

CH32VにはArduino環境で利用できるUSB機能のライブラリがまだありません。そのため、WCH提供のサンプルコードをもとに改変していきます。
このコードでは送信するHIDレポートを直接書き換えることで、入力を設定しています。

コード全体はGitHubで公開しています

constexpr int PIN_LED_BUILTIN = PA5;
constexpr int PIN_BTN_0 = PA15;
constexpr int PIN_BTN_1 = PB3;
constexpr int PIN_BTN_2 = PB4;
constexpr int PIN_BTN_3 = PB5;
constexpr int PIN_BTN_4 = PA4;
constexpr int PIN_BTN_5 = PA7;
constexpr int PIN_RE1A = PA2;
constexpr int PIN_RE1B = PA3;
constexpr int PIN_RE2A = PB0;
constexpr int PIN_RE2B= PB1;

// RotaryEncoder by Matthias Hertel
#include <RotaryEncoder.h>

#include "src/USB-Driver/inc/usb_lib.h"
#include "src/CONFIG/usb_desc.h"
#include "src/CONFIG/usb_pwr.h"
#include "src/CONFIG/usb_prop.h"
#include "src/CONFIG/hw_config.h"

// WORKAROUND: These function declaration exists in hw_config.h but no effect.
extern "C" {
    void Set_USBConfig(void);
    void USB_Interrupts_Config(void);
    uint8_t USBD_ENDPx_DataUp( uint8_t endp, uint8_t *pbuf, uint16_t len );
    void MCU_Sleep_Wakeup_Operate(void);
}

RotaryEncoder re1(PIN_RE1A, PIN_RE1B, RotaryEncoder::LatchMode::FOUR3);
RotaryEncoder re2(PIN_RE2A, PIN_RE2B, RotaryEncoder::LatchMode::FOUR3);


void setup() {
    Serial.begin(115200);
    Serial.println("Initializing...");
    Serial.println("USB-HID composite, Keyboard and Consumer control");
    Serial.printf("SystemCoreClock=%d\n", SystemCoreClock);

    Set_USBConfig();
    USB_Init();
    USB_Interrupts_Config();
    
    Serial.println("Ready.");
}

void led_blink(unsigned int interval_ms) {
    static unsigned long timer = 0;
    static bool led_on = false;

    if (timer == 0) {
        pinMode(PIN_LED_BUILTIN, OUTPUT);
    }

    if (millis() - timer > interval_ms) {
        timer = millis();
        
        digitalWrite(PIN_LED_BUILTIN, led_on ? LOW : HIGH);
        led_on = !led_on;
    }
}


// Keyboard's LED state (updated on USB callbacks)
volatile uint8_t KB_LED_Cur_Status;

uint8_t usbd_kbd_report[8];
uint8_t USBD_KBD_REPORT_BYTES = sizeof(usbd_kbd_report) / sizeof(usbd_kbd_report[0]);

void usbd_kbd_update(void) {
    static unsigned long timer = 0;

    if (timer == 0) {
        pinMode(PIN_BTN_0, INPUT_PULLUP);
        pinMode(PIN_BTN_1, INPUT_PULLUP);
        pinMode(PIN_BTN_2, INPUT_PULLUP);
        pinMode(PIN_BTN_3, INPUT_PULLUP);
    }

    if (millis() - timer >= 10) {
        timer = millis();

        bool btn_w = digitalRead(PIN_BTN_0) == LOW;
        bool btn_a = digitalRead(PIN_BTN_1) == LOW;
        bool btn_s = digitalRead(PIN_BTN_2) == LOW;
        bool btn_d = digitalRead(PIN_BTN_3) == LOW;

        if (btn_w) {
            usbd_kbd_report[3] = 0x1a;
        }
        if (btn_a) {
            usbd_kbd_report[4] = 0x04;
        }
        if (btn_s) {
            usbd_kbd_report[5] = 0x16;
        }
        if (btn_d) {
            usbd_kbd_report[6] = 0x07;
        }

        bool success = USBD_ENDPx_DataUp(1, usbd_kbd_report, USBD_KBD_REPORT_BYTES);
        if (success) {
          memset(usbd_kbd_report, 0x00, USBD_KBD_REPORT_BYTES);
        }
    }
}

/// HID Report for Mouse
/// Layout:
///   - Buttons: bitfield [ 0,0,0,0,0, Button3, Button2, Button1 ]
///   - Cursor X: signed 1 byte integer
///   - Cursor Y: signed 1 byte integer
///   - Wheel vertical: signed 1 byte integer
///   - Wheel horizontal: signed 1 byte integer
uint8_t usbd_mouse_report[5];
uint8_t USBD_MOUSE_REPORT_BYTES = sizeof(usbd_mouse_report) / sizeof(usbd_mouse_report[0]);

void usbd_mouse_update(void) {
    static unsigned long timer = 0;
    static long prevPosition = 0;

    if (timer == 0) {
        memset(usbd_mouse_report, 0x00, USBD_MOUSE_REPORT_BYTES);
    }

    unsigned long current_time = millis();
    if (timer - current_time >= 10) {
        timer = current_time;

        bool btn_left = re1.getPosition() - prevPosition > 0;
        bool btn_right = re1.getPosition() - prevPosition < 0;
        prevPosition = re1.getPosition();

        if (btn_left) {
            usbd_mouse_report[4] = (uint8_t)-1;
        } else if (btn_right) {
            usbd_mouse_report[4] = 1;
        }

        bool success = USBD_ENDPx_DataUp(2, usbd_mouse_report, USBD_MOUSE_REPORT_BYTES);
        if (success) {
            memset(usbd_mouse_report, 0x00, USBD_MOUSE_REPORT_BYTES);
        }
    }
}

uint8_t usbd_cc_report[1];
uint8_t USBD_CC_REPORT_BYTES = sizeof(usbd_cc_report) / sizeof(usbd_cc_report[0]);

void usbd_cc_update(void) {
    static unsigned long timer = 0;
    static long prevPosition = 0;
    static bool muteButtonSent = false;

    if (timer == 0) {
        pinMode(PIN_BTN_5, INPUT_PULLUP);
        memset(usbd_cc_report, 0x00, USBD_CC_REPORT_BYTES);
    }

    if (millis() - timer >= 50) {
        timer = millis();
        bool btn_down = (re2.getPosition() - prevPosition) < 0;
        bool btn_up = (re2.getPosition() - prevPosition) > 0;
        prevPosition = re2.getPosition();
        bool btn_mute = false;
        if (digitalRead(PIN_BTN_5) == LOW) {
          if (!muteButtonSent) {
            btn_mute = true;
            muteButtonSent = true;
          }
        } else {
          muteButtonSent = false;
        }
        
        if (btn_mute) {
            usbd_cc_report[0] = 0x01;
        } else if (btn_down) {
            usbd_cc_report[0] = 0x04;
        } else if (btn_up) {
            usbd_cc_report[0] = 0x08;
        } else {
            usbd_cc_report[0] = 0;
        }

        bool success = USBD_ENDPx_DataUp(3, usbd_cc_report, USBD_CC_REPORT_BYTES);
        if (success) {
          memset(usbd_cc_report, 0x00, USBD_CC_REPORT_BYTES);
        }
    }
}


void loop() {

    if( bDeviceState == CONFIGURED )
    {
        led_blink(500);

        re1.tick();
        re2.tick();

        usbd_kbd_update();
        usbd_mouse_update();
        usbd_cc_update();

    } else {
        led_blink(1000);
    }
}



/** MCUをスリープ状態にする。復帰後の再初期化をする。
*/
void MCU_Sleep_Wakeup_Operate(void) {
    Serial.printf( "Enter to Sleep\r\n" );
    __disable_irq();

    PWR_EnterSTOPMode(PWR_Regulator_LowPower,PWR_STOPEntry_WFE);
    
    SystemInit();
    SystemCoreClockUpdate();
    Set_USBConfig();
    
    __enable_irq( );
    Serial.printf( "Wake\r\n" );
}

雑記

USBデバイスを作るときに大切なこと:
USB機能を停止し、ファームウェア書き込み待機状態にできる方法を確保しておきましょう。Arduino Leonardoではリセットボタン2回押しで、書き込み待機になります。Suzuduino UNO / Suzuno32RV (CH32V)では、BOOTボタンを押しながらリセットで書き込み待機です。もし書き込み待機モードがないボードの場合は、特定のスイッチを押しながらリセットでUSB機能停止、などを仕込んでおくと便利です。
あるいは、動作テスト用のパソコンと開発・書き込み用のパソコンを分けるというのもアリですね。

基板むき出しでは取り扱いがこわいので、ケースを作りたいです。ただシールド基板側にネジ穴や固定のとっかかりがないので、どう固定するかはちょっと悩みそうです。

スイッチとノブはピンヘッダで支えているだけなので、長期的な耐久性は期待できません。あくまでも「かんたんに作るための基板」ということで。その点でも、シールド基板にネジ穴は用意したほうがよかったですね。

CH32VのUSBライブラリが欲しいです。記事執筆時点でTinyUSBの移植が進んでいるようなので、そちらにも期待ですが、Arduino環境から気軽に利用できるものもほしいです。(いずれ自分で書くことになると思います)

verylowfreqのアイコン画像
"verylowfreq" あるいは 「三峰スズ」(VTuber 2023年2月より) です。趣味で電子工作や3Dプリンターを楽しんでいます!
ログインしてコメントを投稿する