自作キーボードHanamuguriを作った

この記事ははJLCPCBの提供でお送りします。

JLCPCBとは

jlcpcb.com (↑こちらは日本語版のログインページで、お得なクーポンも配布されています。)

JLCPCBとは、プリント基板製造などで有名な香港の企業です。

日本からでもWebページでポチポチするだけでKiCADなどで作成した基板データの製造を依頼できます。

値段もかなりお手頃で、ホビー電子工作ユーザーの間では広く利用されています。

この記事の作例もJLCPCBに基板を発注して実現しました。

自作キーボード「Hanamuguri」とは

Hanamuguriはinajobの2つ目に設計した自作キーボードです。

いわゆる40%キーボードと呼ばれるレイアウトで、コンパクトながら実用的なキー配列となっています。一般的なパソコンのキーボードとしても利用できますが、どちらかというと、Raspberry Piなどを使って作られたコンパクトな自作モバイル端末を実現するための部品の一つとして利用できるものを目指しています。

前作はALLPCB39というキーボードを作りました。こちらもコンパクトなキー配列のキーボードです。

ALLPCB39はオーソリニアと呼ばれる格子状のキーボードだったのですが、今回のHanamuguriはロウスタッガードと呼ばれる、行ごとに少しずつキーがずれている、いわゆる一般的なキーボードの配列のキーボードと同じ配列を選択しました。 加えて、ALLPCB39はCherry MX互換キーボードスイッチを採用しましたが、HanamuguriはGateronのLowProfileを利用できるようにして、より薄くなりました。

これも含めて以下のような特徴があります

  • 49キー(配列はいわゆる40%キーボード的)
  • 独立カーソルキー
  • 固定用のねじ穴がたくさんある
  • ユニバーサル基板っぽい領域がキーボード上部にある
  • 対応キーボードスイッチ
    • Gateron LowProfile
    • Gateron LowProfile Hot swap
    • Cherry MX(not LowProfile)
  • キーボードスイッチはパネルマウント
  • 128*64 OLED
  • 開発ボードはRP2040-Zero
  • キーボードマトリクス、未使用ピンのための拡張ピンヘッダ

Hanamuguriをいきなり作るのは心配だったので、まず オリジナルのマクロパッドを作ってみた - inajob's blogを使ってキーボードスイッチまわりのフットプリントの使い方に問題がないかを検証しています。そのため、キーの数が多いことを除いては、以前作ったマクロパッドと同じような構成となっています。

RP2040-Zero

今回のキーボードのキモともいえるこのマイコンボード。コアとなるMCUはRP2040で、Raspberry Pi Picoと同じものですが、RP2040-Zeroの方が基板がコンパクトであることや、USB Type-C端子であることに魅力を感じ、こちらを選択しました。

コンパクトな分利用できるGPIOは少ない、、と思いきや裏面に細かいピッチでパッドが並んでいるので、この部分をはんだ付けできればより多くのGPIOを利用できます。(Hanamuguriではこの部分のピンは今後の拡張のために引き出しているだけで、利用していません)

www.waveshare.com

基板の設計

基板の設計と書きましたが、以前のALLPCB39を作った時と同様、Keyboard Layout Editorのデータから基板のデータを半自動的に生成します。

ということで、まずキーレイアウトを作成しました。

keyboard-layout-editorのデータから回路図を作るツールについて、KiCADのバージョンを上げたせいか、以前使ったツールはうまく動かなくなっていたので、今回はKiCADのアドオンであるGitHub - adamws/kicad-kbplacer: KiCad plugin for automatic keyboard's key placement and routingを使いました。

使い方はkicad-kbplacerのREADMEに従いましたが、ちょっとわかりにくかったので、ここでも説明します

まず、キーボードマトリクスの論理回路図を作成します。今回はキーの数が44キーなので、Rowが7ピン、Columが7ピンのマトリクスレイアウトを採用します。結果として49キー利用できるので、キーボードの配列とは別に5つのキーを利用できるように設計することとします。

で、この論理回路図の部品に番号を振ります

annotation機能を実行すれば番号を振れるのですが、その際に以下の画像のように設定します。

おそらくここを変な形で設定しても、ファームウェアでうまいこと設定すれば対応できそうですが、今回はおとなしく指示に従いました。 その後、フットプリントを割り当てます。

Gateron LowProfileのキーのフットプリントは以前作成したものがあるので、これを使います。

次にKiCADの基板エディタで、ネットリストを読み込みます。 読み込んだ直後は以下のようになっています。

さて、ここでこの部品たちをkeyboard-layout-editorで設計したキー配列のように並べるのがkicad-kbplacerの役割です。

keyboard-layout-editorで作成したjsonをkle-serial形式に変換する必要があるので、以下のツールを利用しました

adamws.github.io

その後ツールバーにあるkbplacerをクリックし、設定画面を開きます

kle-serial形式のjsonファイルを指定して、OKをクリックすると以下のように、部品がkeyboard-layout-editorで設定したものと同様の配列になりました。

なお、今回は45キーのレイアウトだったので、余っている5キーについては手動で配置します。

ここまででキーボードスイッチとダイオードについてはレイアウトが決定しました。

その他の部品や基板外形はいつもの回路設計と同様に行います。

RP2040-Zeroのフットプリントは GitHub - crides/kleeb: Collection of Kicad 6.0 symbols, footprints and 3D models useful in keyboard creationを利用しました。

キーボードの上部にユニバーサル基板のような領域を作ってみました。

これを作るにあたってはKiCadのレイアウトエディタ(Pcbnew)で半田付けパッドを自由に追加する方法(viaを利用する) - PCB Design Tutorial - PCBwayで紹介されているviaとソルダーマスクを使用する方法を利用しました。 ただしこの方法を愚直にやるとfreeroutingで自動配線するときにviaをfreeroutingが認識せず、自動配線がユニバーサル基板領域と重なってしまう問題が起きました。

そのため、まずはユニバーサル基板領域を配線禁止エリアとし、自動配線後にviaとソルダーマスクを並べる手法でユニバーサル基板風の基板を作りました。

続いて、トッププレートも作成します。作り方は以前のオリジナルのマクロパッドを作ってみた - inajob's blogと同じです。

基板の発注

発注はもちろんJLCPCBです。

jlcpcb.com (↑こちらは日本語版のログインページで、お得なクーポンも配布されています。)

10cm×10cm以内の基板が激安なのは、今まで紹介してきたとおりですが、今回の基板はそれより大きいです、さて、いくらくらいかかるのか・・?

大きさは、本体基板が 242.6 mm × 127 mm、トッププレートが242.6 mm × 92.2 mmです。 注文後にメールが1つきて「配線が含まれない基板があるけどデータが間違っているのでは?」(英語)と確認されました。こちらはキーボードのトッププレートなので、配線なしで合っているので、問題ないので進めてくださいと返しました。

その後さらにAn additional charge will be applied to your order[JLCPCB] というメールが来て、「細かい穴が多いので追加で$7.5支払ってほしい」と連絡がありました。これも問題ないので追加で支払いを行いました。

具体的にはこのあたりが追加料金となった原因とのことでした。(まぁ、確かに細かい穴が多い)

ということで5セット作ってもらうのに、お値段は下記に加えて追加の$7.5、送料の$13.77がかかりました。(別にもう1種類基板を発注していたので、送料はそれも込みのものとなっています)

部品の発注

Gateronのロープロファイルのキーボードスイッチ、これに対応したホットスワップソケット、RP2040-Zero、128*64のI2C接続のOLED、ダイオード、キーキャップが必要でしたが、今回はこれらの部品は家のストック部品を利用しました。

Gateronのロープロファイルキーボードスイッチの2Uのスタビライザーは家になかったのでAliExpressで購入しました。

今回のミス

スペースキーが横に長い(2.75U)ため、スタビライザーを取り付けられるように溝を付けたのですが、そのフットプリントのサイズが誤っており、そのままでは取り付けられないものとなりました。

やはり、新しい部分があると、そこでミスしますね・・ 今回は取り敢えず、スタビライザーなしで組み立てることとしました。

部品実装

RP2040-Zeroは端面スルーホール部品なので、基板を重ねて実装することが出来るのですが、とりあえず後で着脱が出来るようにピンヘッダ・ピンソケットを介して実装しました。OLEDも同様にピンヘッダ・ピンソケットを介して実装しました。

後はダイオードと、ホットスワップソケットです。単純な部品なのですが、何しろ44個もあるので、実装が大変でした。特にホットスワップソケットは、はんだがパッド面ではなくソケットの溝の方にばかり流れ込んでしまい、一見はんだが乗っているように見えるが、パッドと部品が接続されていないという状態になるケースが多発し、苦労しました。

加えて、とても初歩的なミスですが、ダイオードの向きを一部間違えて実装してしまい、これも修正が面倒でした。

ファームウェア実装

ファームウェアKMKfw | KMKを使いました。 これはCircuitPython上に実装された、自作キーボード向けのファームウェアで、非常に簡単に利用することが出来ました。

実装したファームウェアはこんな感じです

  • 日本語キー配列(KMKは日本語キー配列もサポートしていました)
  • レイヤーは2つ
    • 通常レイヤー(キートップに刻印されているアルファベット)
    • 数字レイヤー (一般的なキーボードの1行目にあるキーや、一部の記号)
  • OLEDにレイヤーの状態を表示

これだけの記述でカスタマイズできるのはとても便利だと感じました。またCircuitPythonはインタプリタとして動いているので、修正・確認のフィードバックループを素早く回すことが出来たのも良かったです。(その分実行速度は遅いですが、今回の用途ではその遅さが気になることはなかったです)

import board

from kmk.kmk_keyboard import KMKKeyboard
from kmk.keys import KC
from kmk.scanners import DiodeOrientation

# レイヤーを利用する
from kmk.modules.layers import Layers

# 日本語対応
import kmk.extensions.keymap_extras.keymap_jp

# OLED対応
from kmk.extensions.peg_oled_display import Oled,OledDisplayMode,OledReactionType,OledData


keyboard = KMKKeyboard()

# ピンアサイン
keyboard.col_pins = (board.GP11, board.GP12, board.GP13, board.GP14, board.GP15, board.GP26,board.GP27)
keyboard.row_pins = (board.GP4, board.GP5, board.GP6, board.GP7, board.GP8, board.GP9, board.GP10)
keyboard.diode_orientation = DiodeOrientation.COL2ROW

# レイヤー
keyboard.modules.append(Layers())

# OLED用のピンアサイン
keyboard.SCL=board.GP3
keyboard.SDA=board.GP2

# OLEDの設定
oled_ext = Oled(
    OledData(
        corner_one={0:OledReactionType.STATIC,1:["layer"]},
        corner_two={0:OledReactionType.LAYER,1:["1","2","3","4"]},
        corner_three={0:OledReactionType.LAYER,1:["base","raise","lower","adjust"]},
        corner_four={0:OledReactionType.LAYER,1:["qwerty","nums","shifted","leds"]}
        ),
        toDisplay=OledDisplayMode.TXT,flip=False,oWidth=128, oHeight=64)
keyboard.extensions.append(oled_ext)

# よく使うキー
LOWER = KC.MO(1)
_______ = KC.TRNS

# キーマップ
keyboard.keymap = [
        # layer0
        [
            KC.ESC, KC.Q, KC.W, KC.E, KC.R, KC.T, KC.Y, KC.U, KC.I, KC.O, KC.P, KC.BSPC,
            KC.TAB, KC.A, KC.S, KC.D, KC.F, KC.G, KC.H, KC.J, KC.K, KC.L, KC.ENT,
            KC.LSFT, KC.Z, KC.X, KC.C, KC.V, KC.B, KC.N, KC.M, KC.COMM, KC.UP,LOWER,
            KC.LCTRL, KC.LGUI, KC.LALT, LOWER, KC.MINUS, KC.SPC, KC.DOT, KC.LEFT, KC.DOWN, KC.RIGHT,
            KC.N0, KC.N1, KC.N2, KC.N3, KC.N4
            ],
        # layer1
        [
            KC.GRAVE, KC.N1, KC.N2, KC.N3, KC.N4, KC.N5, KC.N6, KC.N7, KC.N8, KC.N9, KC.N0, KC.BSLASH,
            _______, _______, _______, KC.CIRC, KC.JYEN, KC.AT, KC.SCLN, KC.COLN, KC.LBRC, KC.RBRC, _______,
            _______, _______, _______, _______, _______, _______, KC.SLASH, KC.BSLS, _______, _______,LOWER,
            _______, _______, _______, LOWER, LOWER, _______, _______, _______, _______, _______,
            KC.N0, KC.N1, KC.N2, KC.N3, KC.N4
            ],
]

if __name__ == '__main__':
    keyboard.go()

まとめ

自分としては2つ目の自作キーボードを設計・実装しました。 kicad-kbplacerを使った自動配置、ロープロファイルキーボードスイッチ、KMKなど、以前とは違う手法・構成を使い、多くの学びを得ました。

前回のときも感じましたが、自作キーボードは、趣味の電子工作のジャンルの中でもとてもポピュラーなものとなってきており、周辺ツールや文書が潤沢にあるため、非常に入門しやすく、簡単に作り上げることが出来ると感じました。

Hanamuguriは、まだキーボードとして最低限動作するところまでしかできていないので、今後ケースの作成、キーマップの充実、OLEDやカラーLEDの活用、これを組み込んだモバイル端末の作成など、まだまだやりたいことがたくさんあります。

また進展があれば、ブログ記事などで紹介しようと思います。

ガラケー型開発ボード「PiPoPa」を作った

この記事ははJLCPCBの提供でお送りします。

JLCPCBとは

jlcpcb.com (↑こちらは日本語版のログインページで、お得なクーポンも配布されています。)

JLCPCBとは、プリント基板製造などで有名な香港の企業です。

日本からでもWebページでポチポチするだけでKiCADなどで作成した基板データの製造を依頼できます。

値段もかなりお手頃で、ホビー電子工作ユーザーの間では広く利用されています。

この記事の作例もJLCPCBに基板を発注して実現しました。

ガラケー型開発ボード「PiPoPa」とは

PiPoPaはガラケー型の開発ボードです。Arduinoで開発でき、以下の機能を持ちます

  • 2+3×4キー
  • 5wayキースイッチ
  • USBシリアル(CH340N)によるArduinoでの書き込み
  • I2C接続のOLED
  • RTC DS1307
  • 表面実装スピーカー
  • 単4乾電池2本駆動
  • 5V昇圧(以下から選べる)
    • 5V昇圧モジュール
    • HT7750を使った昇圧回路
  • ICSP書き込みピン
  • I2C接続ポート
  • 余ったピンの接続ポート
  • 乾電池の電圧測定

なお、ガラケーの形はしていますが無線通信機能は搭載していません

さて、次に今回利用した特徴的な部品について紹介していきます。

CH340N

安いArduinoクローンなどで使われているUSBシリアルICであるCH340Gとよく似たICですが、CH340Nの特徴は水晶発振子が不要というところです。

(ピンぼけ画像ですが・・8ピンで比較的はんだ付けしやすいパッケージです)

これにより部品点数を押さえてUSBシリアル機能を持たせることが出来ます。

DS1307

Arduinoで時刻を扱う際によく用いられるRTCです。周辺回路が同梱されたモジュールとなったものをよく見かけますが、今回はICをそのまま取り付けています。 バックアップ電源を用いることで、メイン電源が切れても時を刻み続けることが出来ますが、今回はメイン電源をそのままバックアップ電源として接続しています。 (電源スイッチがOFFでもRTCのバックアップ電源ポートに電気が流れるようにしてあります)

5Wayキースイッチ

上下左右とセンタークリックの5つのスイッチが内部に備えたスイッチです。

この部品は背面に凸が2つあり、基板に位置合わせできるようになっていますが、この穴の位置が似たような部品でも違うパターンのものがあるので注意が必要です。

スイッチの感触は少し硬いように感じますが、そのおかげで誤った操作はしづらくなっていると感じます。

5V昇圧モジュール

これは以前紹介しました。

inajob.hatenablog.jp

上記の記事のArduino Nanoを単4電池で動かすボードと同様に5V昇圧モジュールとHT7750の好きな方を実装できるようにしています。 単4電池2本(3V)から5V昇圧し、標準的なArduinoと同じ電圧で動作するようにしています。

(今回利用している部品だと3Vのままでも一応動くような気がします)

デザイン、発注

この基板は、以前似たようなものを作っていて、それを改良する形で作成しました。

回路的にはArduino Unoとほとんど同じで、それにRTC回路や、昇圧回路、キーボードマトリクスなどを付け足した感じです。 部品の配置は、なるべくスマートになるようにギュっっと詰め込みました。

横幅は33mm以内となっているため、10cmx10cmの中に3枚面付けできるようになっています(今回は面付けしていませんが・・)

基板の発注

JLCPCBに基板を発注します。

jlcpcb.com

今回は何かカッコ良さそうな紫色の基板にしました。

今回のミス

スピーカーに直列して470Ωの抵抗をつける回路にしていたのですが、これはいつもRakuChordで利用している大きめのスピーカーのための抵抗値で、実際はもっと小さい抵抗値にする必要がありました。まぁこれはシルクが間違っているだけなので、適切な部品を使うことで対応できました。

RTC用の水晶のフットプリントが電池ボックスと少し干渉している問題もありました。この部品はスルーホール部品なので、表面からはんだ付けすることで、裏面の電池ボックスと干渉している部分にはんだ付けすることを回避しました。組み立て順を工夫して、先にRTC用の水晶を取り付けるようにしても、この問題を回避できます。

動作確認

動作確認として、まずはOLEDの表示、キーの入力、RTC、電源電圧測定を試してみました。 どれも想定通りに動き、安心しました。

OLEDの操作はu8g2、RTCの操作はRTCLibを利用しました。

github.com

github.com

以下のソースコードで、時計の表示・設定、電源電圧の測定、キーの入力を試すことが出来ました。

#include <Arduino.h>
#include <U8g2lib.h>
#include <Wire.h>
#include <avr/sleep.h>
#include <avr/wdt.h>
#include <RTClib.h>

#define COL1 8
#define COL2 9
#define COL3 10
#define COL4 11
#define COL5 12

#define ROW1 4
#define ROW2 5
#define ROW3 6
#define ROW4 7

#define BAT A0

#define INT 2

#define M_NORMAL 0
#define M_SETTING 1

int mode = M_NORMAL;

RTC_DS1307 RTC;

U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);
enum Button{
  LEFT,
  UP,
  DOWN,
  RIGHT,
  CENTER,
  SS1,
  SS2,
  BTN1,
  BTN2,
  BTN3,
  BTN4,
  BTN5,
  BTN6,
  BTN7,
  BTN8,
  BTN9,
  BTN0,
  BTNS,
  BTNA,
  BTNNUM
};
byte trigger[BTNNUM];
String buf = "";
char timebuf[32];
int count = 0;
bool isCounting = false;

void setup(){
  RTC.begin();

  u8g2.begin();
  u8g2.setFont(u8g2_font_profont17_tf);

  pinMode(COL1, INPUT_PULLUP);
  pinMode(COL2, INPUT_PULLUP);
  pinMode(COL3, INPUT_PULLUP);
  pinMode(COL4, INPUT_PULLUP);
  pinMode(COL5, INPUT_PULLUP);

  pinMode(ROW1, INPUT_PULLUP);
  pinMode(ROW2, INPUT_PULLUP);
  pinMode(ROW3, INPUT_PULLUP);
  pinMode(ROW4, INPUT_PULLUP);

  pinMode(INT, INPUT_PULLUP);

  analogReference(DEFAULT);
  pinMode(BAT, INPUT);
  digitalWrite(BAT, LOW);

  pinMode(3, OUTPUT);
  tone(3, 440, 100);

  for(byte i = 0; i < BTNNUM; i ++){
    trigger[i] = 0;
  }
}
void loop(){
  bool state[BTNNUM];

  state[SS2] = !digitalRead(INT);

  pinMode(ROW1, OUTPUT);
  digitalWrite(ROW1, LOW);
  pinMode(ROW2, INPUT_PULLUP);
  pinMode(ROW3, INPUT_PULLUP);
  pinMode(ROW4, INPUT_PULLUP);

  state[LEFT] = !digitalRead(COL1);
  state[CENTER] = !digitalRead(COL2);
  state[DOWN] = !digitalRead(COL3);
  state[RIGHT] = !digitalRead(COL4);
  state[UP] = !digitalRead(COL5);

  pinMode(ROW1, INPUT_PULLUP);
  pinMode(ROW2, OUTPUT);
  digitalWrite(ROW2, LOW);
  pinMode(ROW3, INPUT_PULLUP);
  pinMode(ROW4, INPUT_PULLUP);

  state[SS1] = !digitalRead(COL5);
  state[BTN1] = !digitalRead(COL4);
  state[BTN4] = !digitalRead(COL3);
  state[BTN7] = !digitalRead(COL2);
  state[BTNA] = !digitalRead(COL1);

  pinMode(ROW1, INPUT_PULLUP);
  pinMode(ROW2, INPUT_PULLUP);
  pinMode(ROW3, OUTPUT);
  digitalWrite(ROW3, LOW);
  pinMode(ROW4, INPUT_PULLUP);

  state[BTN2] = !digitalRead(COL4);
  state[BTN5] = !digitalRead(COL3);
  state[BTN8] = !digitalRead(COL2);
  state[BTN0] = !digitalRead(COL1);

  pinMode(ROW1, INPUT_PULLUP);
  pinMode(ROW2, INPUT_PULLUP);
  pinMode(ROW3, INPUT_PULLUP);
  pinMode(ROW4, OUTPUT);
  digitalWrite(ROW4, LOW);

  state[BTN3] = !digitalRead(COL4);
  state[BTN6] = !digitalRead(COL3);
  state[BTN9] = !digitalRead(COL2);
  state[BTNS] = !digitalRead(COL1);

  for(byte i = 0; i < BTNNUM; i ++){
    if(state[i]){
      trigger[i] ++;
    }else{
      trigger[i] = 0;
    }
  }

  bool dirty = false;

  if(mode == M_NORMAL){
    for(byte i = 0; i < BTNNUM; i ++){
      if(trigger[i] == 1){
        switch(i){
          case SS1:  mode = M_SETTING; break;
          case SS2:  break;
          case BTN1: buf.concat("1"); break;
          case BTN2: buf.concat("2"); break;
          case BTN3: buf.concat("3"); break;
          case BTN4: buf.concat("4"); break;
          case BTN5: buf.concat("5"); break;
          case BTN6: buf.concat("6"); break;
          case BTN7: buf.concat("7"); break;
          case BTN8: buf.concat("8"); break;
          case BTN9: buf.concat("9"); break;
          case BTN0: buf.concat("0"); break;
          case BTNA: buf.concat("*"); break;
          case BTNS: buf.concat("#"); break;

          case LEFT: if(buf.length() > 0){buf.remove(buf.length() - 1);} break;
        }
        if(buf.length() > 14){
          buf.remove(buf.length() - 1);
        }
        dirty = true;
      }
    }

    if (RTC.isrunning()) {
      DateTime now = RTC.now();
      sprintf(timebuf, "%02d:%02d:%02d", now.hour(), now.minute(), now.second());
    }
    float bat = 5.0 * analogRead(BAT)/1024;
    char bbuf[6];
    dtostrf(bat, 3,2, bbuf);
    char batbuf[25];
    sprintf(batbuf, "%sV", bbuf);

    u8g2.clearBuffer();
    u8g2.drawStr(0,12,"HELLO PiPoPa2!");
    u8g2.drawStr(0,24, buf.c_str());
    u8g2.drawStr(0,36, timebuf);
    u8g2.drawStr(0,48, batbuf);
    u8g2.sendBuffer();
  }else if(mode == M_SETTING){
    for(byte i = 0; i < BTNNUM; i ++){
      if(trigger[i] == 1){
        switch(i){
          case SS1:  break;
          case SS2:  mode = M_NORMAL;break;
          case BTN1: buf.concat("1"); break;
          case BTN2: buf.concat("2"); break;
          case BTN3: buf.concat("3"); break;
          case BTN4: buf.concat("4"); break;
          case BTN5: buf.concat("5"); break;
          case BTN6: buf.concat("6"); break;
          case BTN7: buf.concat("7"); break;
          case BTN8: buf.concat("8"); break;
          case BTN9: buf.concat("9"); break;
          case BTN0: buf.concat("0"); break;
          case BTNA: break;
          case BTNS: break;

          case LEFT: if(buf.length() > 0){buf.remove(buf.length() - 1);} break;
        }
        if(buf.length() == 12){
          // complete
          int year = buf.substring(0, 4).toInt();
          int month = buf.substring(4, 6).toInt();
          int day = buf.substring(6, 8).toInt();
          int hour = buf.substring(8, 10).toInt();
          int minute = buf.substring(10, 12).toInt();
          RTC.adjust(DateTime(year, month, day, hour, minute, 0));
          buf = "";
          mode = M_NORMAL;
        }
        dirty = true;
      }
    }

    if (RTC.isrunning()) {
      DateTime now = RTC.now();
      sprintf(timebuf, "%02d:%02d:%02d", now.hour(), now.minute(), now.second());
    }

    u8g2.clearBuffer();
    u8g2.drawStr(0,12,"SETTING");
    u8g2.drawStr(0,24, "YYYYMMDDHHMM");
    u8g2.drawStr(0,24, buf.c_str());
    u8g2.drawBox(buf.length() * 9,12 + 12-2, 9, 2);
    u8g2.drawStr(0,36, timebuf);
    u8g2.sendBuffer();

  }
}

まとめ

ガラケー型開発ボードPiPoPaを作成しました。Arduinoの標準的な構成にすることで、既存の資源をうまく利用した設計が出来ました。 基本機能の確認をしただけですが、キーがたくさんあるので、かつてのガラケーのような操作性を備えたアプリケーションを色々動かすことが出来そうです。 (うまいことやればひらがな入力くらいまではできそうに思います)

また、基板むき出しのこのままでは使いづらいので3Dプリンタでケースを作ることにも挑戦したいなと考えています。

オリジナルのマクロパッドを作ってみた

この記事ははJLCPCBの提供でお送りします。

JLCPCBとは

jlcpcb.com (↑こちらは日本語版のログインページで、お得なクーポンも配布されています。)

JLCPCBとは、プリント基板製造などで有名な香港の企業です。

日本からでもWebページでポチポチするだけでKiCADなどで作成した基板データの製造を依頼できます。

値段もかなりお手頃で、ホビー電子工作ユーザーの間では広く利用されています。

この記事の作例もJLCPCBに基板を発注して実現しました。

オリジナルのマクロパッドとは?

マクロパッドというのはキーボードの補助として用いる、キーボードのような装置です。 一般的にはメインのキーボードよりキーの数が少なく、よく使う機能などを割り当ててパソコンの操作を簡単にするために用いるものです。

今回はこのマクロパッドを自作してみようと思います。

材料

  • マイコンボード
  • キーボードスイッチ
    • Gateron low-profile(hot-swap / direct) / Cherry MX
      • キーボードスイッチも2種類、固定の仕方も選べるようにしました
  • 128x64 OLED
    • なんとなく表示機がついている方が面白そうなのでつけました
  • オーディオジャック/表面実装スピーカー
    • なんとなく音が鳴ると面白そうだったのでつけました

キーボードスイッチ Gateron low-profile

Gateron low-profileはロープロファイルキーボードスイッチの中でも比較的安価なスイッチです。ロープロファイルキーボードスイッチにはKailh Choc V1/V2やCherry MX low-profileなど様々な種類があり、それぞれフットプリントが違うので基板を流用することが出来ない点に注意が必要です。(一方で普通のキーボードスイッチはCherry MX互換スイッチが多く、多くの場合は基板が流用できます)

ホットスワップソケット

ホットスワップソケットはGateron low-profileはフットプリントが独自なので専用のものが必要です。

キーキャップ

ロープロファイルのキーキャップもキーボードスイッチの種類によって形状が違うため、Gateron low-profileに適合したものが必要です。 しかし調べるとGateron low-profile専用のキーキャップはあまり見当たりません、そこで寸法を色々調べたところCherry MX low-profile用のキーキャップが、かなり近い形をしていたので、今回はこれを購入してみました。

結果、問題なくキーキャップとして利用できました。

表面実装スピーカー

表面実装のスピーカーを雑に検索して見つかったものを使用しています。型番的には8530というもののようです(単にサイズを表した数字列です) 特に増幅などもせずRaspberry Pi Picoのハイパスフィルタとしてのコンデンサ経由して接続しています。

スイッチ付きイヤホンジャック

このイヤホンジャック、デスクトップパソコンなどでよく見かけるものと同じものだと思います。 5極のピンがあり、イヤホンジャックが抜かれているときだけ導通するようになっており、この部品だけで、イヤホンを接続しているとイヤホンから、イヤホンを接続していないときはスピーカーから、という制御ができる優れものです。

型番はPJ-307と呼ばれているもののようです。

スイッチの仕組みは仕様書の図だとこんな感じです。

https://www.aitendo.com/product/4432 から転載

ボリューム

2連の薄型のボリュームです。今回は2連である必要はないのですが、手元にこれがあったのでこれの1つのボリュームだけを使っています。 スルーホールの部品ですが、かなり薄型なので、コンパクトなガジェットにも使いやすくてお気に入りです。

注意点としてこのボリューム、同じ見た目でピン配置が異なるものがあるようです。

型番的にはB103と呼ばれているもののようです。(103はおそらく抵抗値だろうから、104とかもあると思います)

基板の設計

今回はキーボードスイッチを固定するためのトッププレートも基板として発注することにします。 つまりメインボードと、トッププレートの2枚を設計しJLCPCBに発注します。

基板の設計にあたってはいくつか工夫した点があります。

2種類のマイコンボードをサポート

Raspberry Pi Picoと Pro Microという自作キーボード界ではメジャーなマイコンボード両方に対応するため、2つのフットプリントを少しずらして重ねて配置しています。もちろん配線は両方のマイコンに接続させています

3種類のキーボードスイッチをサポート

自作キーボードで用いるキーボードスイッチにはいくつか種類があり、その種類によりフットプリントの形状が異なります。 今回試したかったのはGateronのlow-profile(ロープロファイル)のキーボードスイッチです。

ロープロファイルというのは、通常のキーボードスイッチよりも高さの低いキーボードスイッチの種類です。

このスイッチのフットプリントは一般的なCherry MXのものとは違っていたので、フットプリントから自作することにしました。 (後にすでにあるフットプリントの存在を教えてもらいましたが、、まぁ勉強ということで・・)

せっかく作るので、通常のGateronのlow-profile、Gateron low-profileのホットスワップソケット(キーボードスイッチを取り外しできるようにソケットを介してつなげる方式)、そしてキーボード界のデファクトスタンダードであるCherry MXの3種類に対応したフットプリントを作りました。

トッププレートの設計

トッププレートはキーボードスイッチを固定するための四角い穴の開いた板です。 基板のキーボードスイッチの固定位置とぴったり合わせて、穴の大きさもキーボードスイッチぴったりに作る必要があるので、気を付けて設計します。

具体的には、まずフットプリントとして、キーボードスイッチを固定するための四角穴を作ります。この際、部品の原点がキーボードスイッチのフットプリントと同一になるように注意が必要です。

その後、完成したキーボード基板のデータをコピーし、不要な部品を削除したうえで、キーボードスイッチのフットプリントをキーボードスイッチの四角穴のフットプリントに置換します。

最後にディスプレイのための窓などもくりぬくように指定をして完成です。

この後、基板に修正を加える場合は、部品の位置をずらさないように注意しました。(よくわからなくなった場合は、トッププレートを再度作り直しました)

基板の発注

発注はもちろんJLCPCBです。

jlcpcb.com

通常の基板は1.6mmなのですが、トッププレートはGateronのlow-profileの仕様に合わせて1mmにしました。 (比較用で1.2mmも作ってみましたが、どちらでも問題なかったです、強いて言えば1.2mmのほうがしっかり固定できている感じがします)

基板は100mm*100mm以内に収めるようにしたため、5枚で$4(はじめの1つは$2)+送料で発注できました。 色はなんとなくかっこいい黒!

今回のミス

懲りずにまたミスをしてしまいました・・

雑に設計したため、Raspbery Pi Picoの3V3につなげるべき配線を3V3_ENに繋いでしまっていました。

今回も配線がシンプルだったので1か所パターンをカットして、正しい配線をジャンパ線でつなげて修正完了です。

動作確認

Raspberry Pi PicoとGateron low-profileのキーボードスイッチをhot-swapソケットでマウントする方式で組み立ててみました。

Raspberry Pi PicoとOLEDはピンソケット経由で取り付けましたが、基板に直接はんだ付けした方が薄く済ませることが出来ます。

実はこのマクロパッドの配線は、以前作ったRaspberry Pi Picoを使った携帯ゲーム機と同じような配線になっているため、同じプログラムが動作します。

inajob.hatenablog.jp

マクロパッドとしての動作確認

さて、ここからはキーボードとしての振る舞いを実装していきます。

調べると、MicroPythonではなくCircuitPythonを使うと簡単にキーボードやマウスが作れるとのこと・・

circuitpython.org

CircuitPythonのuf2ファイルを取得し、Raspberry Pi Picoに書き込みます(RUNを操作するための、スイッチを付けて置けばよかったと後悔・・BOOTスイッチを押しながら、USBケーブルを抜き差しします)

CircuitPythonはMicroPythonとは違い実行中もUSBマスストレージとして認識され、ライブラリなどをPCから直接コピーすることができます。

まずはOLEDの制御用に Adafruit_CircuitPython_SSD1306 をインストール(libディレクトリにコピーする)

github.com

文字を表示するには以下のフォントファイルが必要です(ルートディレクトリにコピーする)

github.com

さらにマウスやキーボード制御のために Adafruit_CircuitPython_HID をインストールします。

github.com

後は初期化コードを少し書いて、スイッチの状態に合わせてキーコードを送信すればマクロパッドの基礎は完成です。

ここでは、十字キーとA,Bというそのままの機能を割り付けましたが、もっと自由に、1キー打鍵すると複数の文字を入力できるようにしたり、OLEDに表示されたメニューから選択した文字を入力するなどといった複雑な機能を作ることもできます。

工夫した点としては、キーが押され始めるとカウントアップする変数を用意して、キーが押されてから2フレーム目にキー入力を発行するようにした点です。(もっとちゃんとするなら、キーの押し下げ、押し上げなども送信するのが良いと思います。)

"""
Raspberry Pi Pico Keyboard test

Copyright (c) 2023 inajob

This software is released under the MIT License.
http://opensource.org/licenses/mit-license.php
"""

import board
import busio
import digitalio
import time

# OLED
import adafruit_ssd1306

i2c = busio.I2C(board.GP5, board.GP4)  # (SCL, SDA)17,16
display = adafruit_ssd1306.SSD1306_I2C(128, 64, i2c, addr=0x3C)
display.fill(0)
display.text("Hello",0,0,1)
display.show()

# USB HID
import usb_hid
from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keycode import Keycode

right = digitalio.DigitalInOut(board.GP16)
right.switch_to_input(pull=digitalio.Pull.UP)
left = digitalio.DigitalInOut(board.GP18)
left.switch_to_input(pull=digitalio.Pull.UP)
up = digitalio.DigitalInOut(board.GP19)
up.switch_to_input(pull=digitalio.Pull.UP)
down = digitalio.DigitalInOut(board.GP17)
down.switch_to_input(pull=digitalio.Pull.UP)

btna = digitalio.DigitalInOut(board.GP15)
btna.switch_to_input(pull=digitalio.Pull.UP)
btnb = digitalio.DigitalInOut(board.GP14)
btnb.switch_to_input(pull=digitalio.Pull.UP)


kbd = Keyboard(usb_hid.devices)
trigger = [0,0,0,0,0,0]

while True:
    if not(left.value):
        trigger[0] = trigger[0] + 1
    else:
        trigger[0] = -1
    if not(right.value):
        trigger[1] = trigger[1] + 1
    else:
        trigger[1] = -1
    if not(up.value):
        trigger[2] = trigger[2] + 1
    else:
        trigger[2] = -1
    if not(down.value):
        trigger[3] = trigger[3] + 1
    else:
        trigger[3] = -1
    if not(btna.value):
        trigger[4] = trigger[4] + 1
    else:
        trigger[4] = -1
    if not(btnb.value):
        trigger[5] = trigger[5] + 1
    else:
        trigger[5] = -1

    if trigger[0] == 2:
        kbd.send(Keycode.LEFT_ARROW)
        display.fill(0)
        display.text("LEFT",0,0,1)
        display.show()
    if trigger[1] == 2:
        kbd.send(Keycode.RIGHT_ARROW)
        display.fill(0)
        display.text("RIGHT",0,0,1)
        display.show()
    if trigger[2] == 2:
        kbd.send(Keycode.UP_ARROW)
        display.fill(0)
        display.text("UP",0,0,1)
        display.show()
    if trigger[3] == 2:
        kbd.send(Keycode.DOWN_ARROW)
        display.fill(0)
        display.text("DOWN",0,0,1)
        display.show()
    if trigger[4] == 2:
        kbd.send(Keycode.A)
        display.fill(0)
        display.text("A",0,0,1)
        display.show()
    if trigger[5] == 2:
        kbd.send(Keycode.B)
        display.fill(0)
        display.text("B",0,0,1)
        display.show()

まとめ

Gateronのlow-profileのキーボードスイッチを使ったマクロキーパッドを作ってみました。 キーボードパネルも基板として発注することで、手軽にしっかりしたつくりのマクロパッドを作成できました。

Raspberry Pi Picoでこのようなマクロパッドを使う際はCircuitPythonが便利であることも知りました。

次はもっとキーの数の多いキーボードの作成にも挑戦してみたいと思いました。

参考

以下の記事を参考にしました。

https://hf-labo.net/ew-pico-mouse-keyboard/hf-labo.net

logikara.blog

Raspberry Pi Picoを使った携帯ゲーム機

この記事ははJLCPCBの提供でお送りします。

JLCPCBとは

jlcpcb.com (↑こちらは日本語版のログインページで、お得なクーポンも配布されています。)

JLCPCBとは、プリント基板製造などで有名な香港の企業です。

日本からでもWebページでポチポチするだけでKiCADなどで作成した基板データの製造を依頼できます。

値段もかなりお手頃で、ホビー電子工作ユーザーの間では広く利用されています。

この記事の作例もJLCPCBに基板を発注して実現しました。

Raspberry Pi Picoを使った携帯ゲーム機とは?

文字通り、Raspberry Pi Picoを搭載した携帯ゲーム機です。 1枚の基板で色々なことが試せるように工夫しました。

  • ディプレイは以下から1つ
    • I2C接続OLED
    • SPI接続OLED
    • SPI接続カラーTFT(128px*128px)
  • スピーカー
  • 3.5mmオーディオジャック端子
  • ボリューム
  • 電源は以下から1つ
    • 単4電池2つ
    • LiPOバッテリー(充電モジュールは既製品を想定)
  • MicroSDカードソケット
  • 十字ボタン、A,Bボタン
  • 電源用スライドスイッチ
  • Raspberry Pi Picoのすべての端子を引き出した拡張端子

タクトスイッチは6mm角の表面実装の物、コンデンサは0603(1608M)を使っています

複数のディスプレイ、複数の電源への対応

まぁ単純にフットプリントを重ねて配置しただけです。どれか1つしか使わない想定なので、フットプリント自体が重なることは問題がないため、スルーホール穴が重ならないように配置しました。

電源も、乾電池ホルダーに重ねてLiPoバッテリーの充電モジュールを配置し、どちらか一方を利用できるようにしました。

発注

今回は基板の色は黒で発注しました。JLCPCBの黒の基板はツヤ消しマットのような質感で、高級感があり良いものだと思いました。

基板サイズは10cm*10cm以内なので、JLCPCBの最安料金で発注できました。 (1注文当たり5枚の基板が1つだけ割引になるため、1注文なら5枚で$2+送料)

今回の失敗

今回も凡ミスをしてしまいました・・

Raspberry Pi PicoのGNDが他のGNDと繋がっておらず、電源を入れても、Raspberry Pi Picoに電源が供給されない状態でした。

まぁ、修正は簡単で、適当なGNDの来ているピンからRaspberry Pi PicoのGNDピンにジャンパ線を取り付けて修正しました。

毎回ミスはありますね・・

単4電池2つでRaspberry Pi Pico?!

Raspberry Pi Picoは3.3V駆動なので単4電池2つでは少し電圧が足りないと思って、DC-DCコンバータを別途取り付けようと思っていたのですが、よく仕様を見るとRaspberry Pi PicoにはDC-DCコンバータが搭載されており、1.8~5.5Vを印加すると、いい感じに3.3Vにしてくれるようで、外部部品が不要であることに気づきました。

https://datasheets.raspberrypi.com/pico/pico-datasheet.pdf より(CC BY-ND)

MicroPythonでのプログラミング

とりあえず I2C接続OLED、単4電池2つの構成で組み立てました。

動作確認がてらMicroPythonでプログラミングしてみました。 以下のソースコードで、キー入力を取得し、画面に文字を表示し、音を鳴らすことができます。

"""
Raspberry Pi Pico Sequencer test

Copyright (c) 2023 inajob

This software is released under the MIT License.
http://opensource.org/licenses/mit-license.php
"""

from machine import Pin, I2C, PWM, Timer
import ssd1306
import time
buzzer = PWM(Pin(9))
freqLis = [262, 294, 330, 349, 392, 440, 494, 523]
idx = 0
i2c = I2C(0, sda=Pin(4), scl=Pin(5) , freq=400000)
BTNA = Pin(15, Pin.IN, Pin.PULL_UP)
BTNB = Pin(14, Pin.IN, Pin.PULL_UP)
BTNU = Pin(19, Pin.IN, Pin.PULL_UP)
BTND = Pin(17, Pin.IN, Pin.PULL_UP)
BTNL = Pin(18, Pin.IN, Pin.PULL_UP)
BTNR = Pin(16, Pin.IN, Pin.PULL_UP)

display = ssd1306.SSD1306_I2C(128, 64, i2c)
count = 0
pos = [0,0]
MAX_STEP = int(128/4)
MAX_TONE = int(64/4)
data = [[False] * MAX_TONE for _ in range(MAX_STEP)]
playPos = 0;

def draw():
    display.fill_rect(0,0,128,64,0)
    for step in range(128/4):
        for tone in range(64/4):
            if data[step][tone]:
                display.fill_rect(step*4, tone*4, 4, 4, 1)
    display.fill_rect(pos[0]*4, pos[1]*4, 4, 4, 1)
    display.fill_rect(playPos*4, 0, 1, 64, 1)

def tick(timer):
    global idx
    global count
    global pos
    global playPos
    draw()
    count = count + 1
    if count > 2:
        count = 0
        playPos = playPos + 1
        if playPos >= 8:
            playPos = 0
    display.show()
    
    if(BTNA.value() == 0):
        data[pos[0]][pos[1]] = True
    if(BTNB.value() == 0):
        data[pos[0]][pos[1]] = False
    if(BTNU.value() == 0):
        pos[1] = pos[1] - 1
        if pos[1] < 0:
            pos[1] = 0
    if(BTND.value() == 0):
        pos[1] = pos[1] + 1
    if(BTNL.value() == 0):
        pos[0] = pos[0] - 1
        if pos[0] < 0:
            pos[0] = 0
    if(BTNR.value() == 0):
        pos[0] = pos[0] + 1
    
    f = False
    for i in range(MAX_TONE):
        if data[playPos][i]:
            buzzer.freq(freqLis[i])
            buzzer.duty_u16(256*16)
            f = True
    if not(f):
        buzzer.duty_u16(0)
tim = Timer()
tim.init(period=100, mode=Timer.PERIODIC, callback=tick)

MicroPythonの開発にはThony( https://thonny.org/ )を利用しました。

MicroPythonはコンパイルすることなく、パソコン上のIDEからプログラムを転送し、実行できるので、Arduinoなどに比べるとトライ&エラーがやりやすいと感じました。

Arduinoでのプログラミング

MicroPythonで作曲ソフトを作りましたが、これでは和音に対応できません。 ということでArduinoを使って和音も利用できる作曲ソフトを作ってみました。

OLEDの制御はArduino UNOでよく利用しているu8g2というライブラリがそのまま動いたので、簡単でした。

github.com

ソースコードは雑ですが、、こんな感じです。

/**
Raspberry Pi Pico Sequencer test

Copyright (c) 2023 inajob

This software is released under the MIT License.
http://opensource.org/licenses/mit-license.php
*/

#define NOTE_B0 31
#define NOTE_C1 33
#define NOTE_CS1 35
#define NOTE_D1 37
#define NOTE_DS1 39
#define NOTE_E1 41
#define NOTE_F1 44
#define NOTE_FS1 46
#define NOTE_G1 49
#define NOTE_GS1 52
#define NOTE_A1 55
#define NOTE_AS1 58
#define NOTE_B1 62
#define NOTE_C2 65
#define NOTE_CS2 69
#define NOTE_D2 73
#define NOTE_DS2 78
#define NOTE_E2 82
#define NOTE_F2 87
#define NOTE_FS2 93
#define NOTE_G2 98
#define NOTE_GS2 104
#define NOTE_A2 110
#define NOTE_AS2 117
#define NOTE_B2 123
#define NOTE_C3 131
#define NOTE_CS3 139
#define NOTE_D3 147
#define NOTE_DS3 156
#define NOTE_E3 165
#define NOTE_F3 175
#define NOTE_FS3 185
#define NOTE_G3 196
#define NOTE_GS3 208
#define NOTE_A3 220
#define NOTE_AS3 233
#define NOTE_B3 247
#define NOTE_C4 262
#define NOTE_CS4 277
#define NOTE_D4 294
#define NOTE_DS4 311
#define NOTE_E4 330
#define NOTE_F4 349
#define NOTE_FS4 370
#define NOTE_G4 392
#define NOTE_GS4 415
#define NOTE_A4 440
#define NOTE_AS4 466
#define NOTE_B4 494
#define NOTE_C5 523
#define NOTE_CS5 554
#define NOTE_D5 587
#define NOTE_DS5 622
#define NOTE_E5 659
#define NOTE_F5 698
#define NOTE_FS5 740
#define NOTE_G5 784
#define NOTE_GS5 831
#define NOTE_A5 880
#define NOTE_AS5 932
#define NOTE_B5 988
#define NOTE_C6 1047
#define NOTE_CS6 1109
#define NOTE_D6 1175
#define NOTE_DS6 1245
#define NOTE_E6 1319
#define NOTE_F6 1397
#define NOTE_FS6 1480
#define NOTE_G6 1568
#define NOTE_GS6 1661
#define NOTE_A6 1760
#define NOTE_AS6 1865
#define NOTE_B6 1976
#define NOTE_C7 2093
#define NOTE_CS7 2217
#define NOTE_D7 2349
#define NOTE_DS7 2489
#define NOTE_E7 2637
#define NOTE_F7 2794
#define NOTE_FS7 2960
#define NOTE_G7 3136
#define NOTE_GS7 3322
#define NOTE_A7 3520
#define NOTE_AS7 3729
#define NOTE_B7 3951
#define NOTE_C8 4186
#define NOTE_CS8 4435
#define NOTE_D8 4699
#define NOTE_DS8 4978

int tones[] = {
  NOTE_C4, NOTE_D4, NOTE_E4, NOTE_F4, NOTE_G4, NOTE_A4, NOTE_B4,
  NOTE_C5, NOTE_D5, NOTE_E5, NOTE_F5, NOTE_G5, NOTE_A5, NOTE_B5,
  NOTE_C6, NOTE_D6, NOTE_E6, NOTE_F6, NOTE_G6, NOTE_A6, NOTE_B6
};

#include "hardware/pwm.h"
#include <U8g2lib.h>
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);

#define PWM_AUDIO_L    (9)
//#define PWM_AUDIO_R    (28)
#define RANDOMBIT      (*((uint *)(ROSC_BASE + ROSC_RANDOMBIT_OFFSET)) & 1)

#define PWM_RANGE_BITS (10)
#define PWM_RANGE      (1<<PWM_RANGE_BITS)
#define NUM_PSG        (4)
#define PSG_VOL_MAX    (PWM_RANGE / NUM_PSG - 1)
#define SAMPLE_RATE    (125000000 / PWM_RANGE)
#define OMEGA_UNIT     (FIXED_1_0 / SAMPLE_RATE)

#define FIXED_0_5      (0x40000000)
#define FIXED_1_0      (0x7fffffff)
typedef uint32_t fixed;       // 1.0=0x7fffffff, 0.0=0x0

enum psg_type {OSC_SQUARE, OSC_SAW, OSC_TRI, OSC_NOISE};

struct psg_t {
    volatile fixed phi;       // 0..FIXED_1_0
    fixed step;               // 0..FIXED_1_0
    volatile int sound_vol;   // 0..PSG_VOL_MAX
    enum psg_type type;
};

static struct psg_t psg[NUM_PSG];

void psg_freq(int i, float freq) {
    assert(i < NUM_PSG);
    psg[i].step = freq * OMEGA_UNIT;
}

void psg_vol(int i, int value) {
    assert(i < NUM_PSG);
    if (value < 0) {
        value = 0;
    }
    psg[i].sound_vol = value % (PSG_VOL_MAX + 1);
}

void psg_type(int i, enum psg_type type) {
    assert(i < NUM_PSG);
    psg[i].type = type;
}

static inline uint psg_value(int i) {
    assert(i < NUM_PSG);
    uint result;
    if (psg[i].type == OSC_SQUARE) {
        result = (psg[i].phi > FIXED_0_5) ? psg[i].sound_vol : 0;
    } else if (psg[i].type == OSC_SAW) {
        result = ((psg[i].phi >> (31 - PWM_RANGE_BITS)) * psg[i].sound_vol) >> PWM_RANGE_BITS;
    } else if (psg[i].type == OSC_TRI) {
        result = ((psg[i].phi >> (30 - PWM_RANGE_BITS)) * psg[i].sound_vol) >> PWM_RANGE_BITS;
        result = (result < psg[i].sound_vol) ? result : psg[i].sound_vol * 2 - result;
    } else { // OSC_NOISE
        result = RANDOMBIT * psg[i].sound_vol;
    }
    return result;
}

static inline void psg_next() {
    for(int i = 0; i < NUM_PSG; i++) {
        psg[i].phi += psg[i].step;
        if (psg[i].phi > FIXED_1_0) {
            psg[i].phi -= FIXED_1_0;
        }
    }
}

void on_pwm_wrap() {
    uint sum = 0;

    pwm_clear_irq(pwm_gpio_to_slice_num(PWM_AUDIO_L));
#ifdef PWM_AUDIO_R
    pwm_clear_irq(pwm_gpio_to_slice_num(PWM_AUDIO_R));
#endif
    psg_next();
    for(int i = 0; i < NUM_PSG; i++) {
        sum += psg_value(i);
    }
    pwm_set_gpio_level(PWM_AUDIO_L, sum);
#ifdef PWM_AUDIO_R
    pwm_set_gpio_level(PWM_AUDIO_R, sum);
#endif
}

void psg_pwm_config() {
    gpio_set_function(PWM_AUDIO_L, GPIO_FUNC_PWM);
#ifdef PWM_AUDIO_R
    gpio_set_function(PWM_AUDIO_R, GPIO_FUNC_PWM);
#endif
    uint slice_num = pwm_gpio_to_slice_num(PWM_AUDIO_L);
    pwm_clear_irq(slice_num);
    pwm_set_irq_enabled(slice_num, true);
    irq_set_exclusive_handler(PWM_IRQ_WRAP, on_pwm_wrap);
    irq_set_enabled(PWM_IRQ_WRAP, true);

    pwm_config config = pwm_get_default_config();
    pwm_config_set_clkdiv_int(&config, 1);
    pwm_config_set_wrap(&config, PWM_RANGE);
    pwm_init(slice_num, &config, true);
#ifdef PWM_AUDIO_R
    slice_num = pwm_gpio_to_slice_num(PWM_AUDIO_R);
    pwm_init(slice_num, &config, true);
#endif
}

void psg_init() {
    for(int i = 0; i < NUM_PSG; i++) {
        psg[i].phi = 0;
        psg[i].step = 0;
        psg[i].sound_vol = PSG_VOL_MAX / 4;
        psg[i].type = OSC_SQUARE;
    }
    psg_pwm_config();
}

void psg_all_vol(int value) {
    for(int i = 0 ; i < NUM_PSG; i++) {
        psg_vol(i, value);
    }
}

int channel = 0;
int cursorX = 0;
int cursorY = 0;
int pos = 0;
#define SIZE 16
bool scoreData[SIZE][32];
int trigger[6];

void setup() {
  psg_init();

  psg_type(0, OSC_SQUARE);
  psg_type(1, OSC_SQUARE);
  psg_all_vol(0);
  psg_vol(channel, PSG_VOL_MAX / 4);
  psg_vol(1, PSG_VOL_MAX / 4);
  psg_freq(channel, 220);
  psg_freq(1, 220);

  pinMode(14, INPUT_PULLUP);
  pinMode(15, INPUT_PULLUP);
  pinMode(16, INPUT_PULLUP);
  pinMode(17, INPUT_PULLUP);
  pinMode(18, INPUT_PULLUP);
  pinMode(19, INPUT_PULLUP);
  
  u8g2.begin();
  u8g2.setFont(u8g2_font_ncenB08_tr);

  for(int i = 0; i < 128/SIZE; i ++){
    for(int j = 0; j < 32; j ++){
      scoreData[i][j] = false;
    }
  }
  for(int i = 0; i < 6; i ++){
    trigger[i] = 0;
  }
}

void draw(){
  u8g2.drawLine(pos*128/SIZE, 0, pos*128/SIZE, 63);
  for(int i = 0; i < SIZE; i ++){
    for(int j = 0; j < 32; j ++){
      if(scoreData[i][j]){
        u8g2.drawBox(i*128/SIZE, j*2, 128/SIZE, 2);
      }
    }
  }
  u8g2.drawBox(cursorX * 128/SIZE, cursorY*2, 128/SIZE, 2);
}

int count = 0;

void loop() {
  count ++;
  if(count > 3){
    count = 0;
    pos ++;
    if(pos > SIZE - 1){pos = 0;}
    for(int i = 0; i < NUM_PSG; i ++){
      psg_freq(i, 0);
      psg_vol(i, 0);
    }
    int freeChan = 0;
    for(int j = 0; j < 32; j ++){
      if(scoreData[pos][j]){
        psg_vol(freeChan, PSG_VOL_MAX);
        psg_freq(freeChan, tones[j]);
        freeChan ++;
        if(freeChan >= NUM_PSG){
          freeChan = 0;
        }
      }
    }
  }

  if(!digitalRead(16)){trigger[0] ++;}else{trigger[0] = -1;}
  if(!digitalRead(17)){trigger[1] ++;}else{trigger[1] = -1;}
  if(!digitalRead(18)){trigger[2] ++;}else{trigger[2] = -1;}
  if(!digitalRead(19)){trigger[3] ++;}else{trigger[3] = -1;}
  if(!digitalRead(15)){trigger[4] ++;}else{trigger[4] = -1;}
  if(!digitalRead(14)){trigger[5] ++;}else{trigger[5] = -1;}


  if(trigger[0] == 3){cursorX ++;}
  if(trigger[1] == 3){cursorY ++;}
  if(trigger[2] == 3){cursorX --;}
  if(trigger[3] == 3){cursorY --;}
  if(trigger[4] == 3){scoreData[cursorX][cursorY] = true;}
  if(trigger[5] == 3){scoreData[cursorX][cursorY] = false;}

  for(int i = 0; i < 6; i ++){
    if(trigger[i] == -1)trigger[i] = 0;
  }

  if(cursorX < 0)cursorX = 0;
  if(cursorY < 0)cursorY = 0;
  if(cursorX > SIZE - 1)cursorX = SIZE - 1;
  if(cursorY > 64/2 - 1)cursorY = 64/2 - 1;

  sleep_ms(3);
  
  u8g2.clearBuffer();
  draw();
  u8g2.sendBuffer();
}

参考にしたもの

以下のRP2040を使ったゲーム機の回路を参考にしており、SPIのOLEDを搭載した場合は、同じように動作するように設計しました(試していないですが・・)

github.com

和音を使った演奏については以下の記事を参考にしました

blog.boochow.com

まとめ

この手のゲーム機といえば、Arduboy( https://www.arduboy.com/ )が有名で、自分も本物を持っていますし、互換機を作ったこともあります、それに比べるとこのRaspberry Pi Picoを使ったゲーム機は、性能がケタ違いに高いものとなっており、様々なゲームを動かすポテンシャルがあります。

また、半導体不足の影響もあり、おそらくRaspberry Pi Picoを使ったこのゲーム機の方が安上がりで作れそうです。

まぁ、Arduboyほどのスタイリッシュなデザインにするのは、かなり大変で、Arduboyの価値はそちらの方にあると思うのですが、自作ゲーム機界隈も日進月歩だなと感じています。

最後に、この基板が数枚余っていますので、もしほしい人がいたら連絡ください。 (可能なら、ボツ基板交換的なノリで、何か自作の面白い基板と交換してもらえると嬉しいです。 私が提供できるボツ基板はこちらにまとめています。

Arduino Nanoを単4電池で動かすボードを作った

この記事ははJLCPCBの提供でお送りします。

JLCPCBとは

jlcpcb.com (↑こちらは日本語版のログインページで、お得なクーポンも配布されています。)

JLCPCBとは、プリント基板製造などで有名な香港の企業です。

日本からでもWebページでポチポチするだけでKiCADなどで作成した基板データの製造を依頼できます。

値段もかなりお手頃で、ホビー電子工作ユーザーの間では広く利用されています。

この記事の作例もJLCPCBに基板を発注して実現しました。

どういうもの?

Arduino NanoはArduino UNOと同じATMega328を搭載した、小型の開発ボードです。 小型ゆえそのまま製品に組み込みやすく、例えば私の作っている電子楽器のRakuChordでも、このボードをそのまま組み込んでいます。

Arduino Nanoの電源はUSBコネクタを経由して供給するのが普通ですが、携帯できるガジェットを作る場合は電池で動かせると便利です。

電池で動かす場合は、USBと同じ5Vを作る必要があるので、例えば単四電池を4本の電池で6Vを作り、レギュレータで降圧するような構成が必要です。

しかし、携帯できるガジェットとなると大きさの制約などもあり単四電池4本だとかさばってしまうこともあり、もう少しコンパクトな電源が利用できると便利です。

ということで、単4電池1本1.5Vを昇圧して5Vを作り出すような開発ボードが欲しくなりました。

機能紹介

  • 単4電池x1ホルダー
  • I2C接続のOLED
  • Arduino Nano
  • 電源用スライドスイッチ
  • 5V昇圧

細かくそれぞれの機能について紹介します。

単4電池x1ホルダー

単4電池を1つ固定するための電池ホルダーを利用します。 単4電池2本にするか、1本にするかは動作時間に影響するので、迷ったのですが、今回は省スペースを意識して1本にしました。

この電池ホルダーは、AliExpressで出回っているものですが、ほかの種類のものはあまり見当たりません。 安いのは良いのですが、以下のような問題点もあるので注意です

  • 電池を取り外すのが大変
    • 電池を取り出さないようにするためのガードがきつすぎて、逆に外すのが大変
  • ピンが太すぎて、ニッパーで切るのが大変

以上の問題はありますが、比較的安価($0.3くらい)で基板に直接固定できるため、コンパクトな開発ボードを作るのにはかなり適した部品であると思い、今回はこれを使いました。

I2C接続のOLED

開発ボードには何かしらの表示機がついていると便利だと思い定番の128x64のOLEDを搭載することにしました。

(手元には2種類のOLEDボードがありました、ピン配置、コントローラICは同じですが、若干ボードのサイズが違います)

I2C接続のもの、SPI接続の物、I2C接続の中でもピンの並びが違うもの、などいろいろなバリエーションがあるので注意が必要ですが、今回はI2C接続でピンがGND, VCC, SCL, SDAの並びのものを選びました。

Arduino Nano

Arduino UNOと同じATMega328を搭載した開発ボードです。家にあったのはこれの互換品で、USB端子がMicro-Aのものです。 純正のものと機能は変わらず、ボードの大きさやピンの配列も同じです。

電源用スライドスイッチ

これも我が家の定番部品。基板の端に取り付ける基板表面に対して垂直な向きのスライドスイッチです。

5V昇圧

5V昇圧については2種類の回路を用意しました。

1つは5V昇圧用の3ピンのモジュールを取り付ける方法、もう一つはHT7750という昇圧用のICとその周辺部品を取り付ける方法です。

今まで5Vに昇圧するときはHT7750というICを使っていたのですが、AliExpressで3ピンの5V昇圧用モジュールをみつけ、便利そうだと思ったので、今回はどちらか1つを実装できるように基板を設計しました。

これで、仮に3ピンの5V昇圧モジュールに問題があった場合は、実績のあるHT7750を使うこともできます。このように基板上に複数の同じ役割のパターンを作るのは「こんなこともあろうかと」という感じで、有用なのですが、あまりやりすぎると複雑になりすぎて、設計を間違えてしまい、結果としてどちらも動かない役立たずの基板になったりすることもあるので注意が必要です。

デザイン、発注

今回の基板は、なるべくコンパクトにしたかったので、部品の配置に気を遣いました。 また、開発ボードということで、自由に部品を配置できるようなユニバーサル基板的なゾーンも用意しました。

Arduino Nanoのピンはすべて引き出して、Arduino Nanoの機能はそのまま利用できるようにしました。

また、ADCポートでバッテリーの残量も確認できるようにしました。

大きさは名刺サイズ程度にし、JLCPCBの最安料金で発注できるようにしました。 (1注文当たり5枚の基板が1つだけ割引になるため、1注文なら5枚で$2+送料)

組み立てと今回の設計ミス

ICを直接マウントしていないため、このボードの組み立ては比較的簡単です。 電池ケース、Arduino Nano、5V昇圧モジュール、スライドスイッチ、OLEDをはんだするだけです。

なのですが、、

今回も設計をミスってしまいました・・、とても初歩的な間違いで恥ずかしいのですが、まぁこれも誰かの役に立つかもしれないので、紹介します。

Arduino Nanoから引き出したピンの順番がおかしい

今回の動作テストではあまり関係なかったですが、Arduino Nanoのピンをそのまま引き出したつもりが、一部のピンの配置が入れ替わっており、紛らわしい仕様となっていました。 まぁ、これはこれらのピンを利用する際に気を付ければ問題は起きないのですが、不可解な仕様となってしまいました。

Arduino NanoのGNDが未結線

Arduino NanoのGNDの端子をバッテリーのGNDの端子がつながっていませんでした。そのため、組み立てて電源を入れてもArduino Nanoが起動しない問題が発生しました。

これは単純に回路図上のArduino NanoのGNDのピンが未結線だったためで、デザインルールチェックを実行するとエラーが出るのですが、未結線のまま発注してしまいました。

ワークアラウンドとして、Arduino NanoのGNDのピンと近くのGNDが来ているパッドを導線ではんだ付けしました。

5V昇圧モジュールのVINが未結線

こちらも凡ミスです。電源のラインに「BAT」というラベルを付けていたのですが、「BAT+」という表記と混ざってしまい、意図した結線が行われていませんでした。

こちらもワークアラウンドとして電源の来ているピンと5V昇圧モジュールのVINを導線ではんだ付けしました。

動作確認

OLEDのライブラリであるu8g2ライブラリのデモプログラムを書き込んで動作を確認しました。 電池駆動でOLEDを制御できることを確認しました。

まとめと感想

Arduino Nanoを単四電池1本で動作させるためのOLED付きの開発ボードを設計しました。 Arduinoを使ったモバイルゲーム機や、電子楽器などのプロトタイピングに役立ちそうだと感じました。

また5V昇圧モジュールも初めて使い、問題なく1.5Vを5Vに昇圧できることを確認しました。

開発ボードを設計してみて、開発ボードに乗せる機能の選別について、かなりのデザインセンスが必要だということを感じました。あれもこれも機能を乗せたいという気持ちもありつつ、色々乗せれば乗せるほど基板が大きくなったり、部品点数が増えてしまったりとトレードオフがあります。

また、開発ボードの設計は1つの世界を作るような楽しさがあり、うまくコンセプトにあったボードを設計できると何とも言えない達成感がありました。

皆さんも、自分の考える最強の開発ボード、作ってみませんか?

小型のフルカラーLED「WS2812C-2020」テスト基板を作った

この記事ははJLCPCBの提供でお送りします。

JLCPCBとは

jlcpcb.com (↑こちらは日本語版のログインページで、お得なクーポンも配布されています。)

JLCPCBとは、プリント基板製造などで有名な香港の企業です。

日本からでもWebページでポチポチするだけでKiCADなどで作成した基板データの製造を依頼できます。

値段もかなりお手頃で、ホビー電子工作ユーザーの間では広く利用されています。

この記事の作例もJLCPCBに基板を発注して実現しました。

WS2812とは?

WS2812はシリアル接続のフルカラーLEDとしてはデファクトスタンダードとなった部品ですが、よく見られるものは5mm角のものが一般的ですが、2mm角の「WS2812C-2020」というものがあることを発見しました。

自分はAliExpressで購入しましたが、秋月電子で購入することもできます。 マイコン内蔵RGBLED WS2812C-2020: オプトエレクトロニクス 秋月電子通商-電子部品・ネット通販

2mm角ということで、非常にコンパクトで手のひらサイズのガジェットに搭載するのにぴったりです。

テスト基板の設計

今回はテスト基板ということで、非常にシンプルな設計にします。

「WS2812C-2020」を8個並べただけの細長い基板です。注意点としては「WS2812C-2020」は部品1つ毎に100nFのコンデンサを並べる必要があるということです。(データシートのTypical Application Circuitに記載があります)

まぁよく見かける5mm角のWS2812Bなどでも、たくさん並べる際にはコンデンサを挟んだ方が良い気がします。(いわゆるおまじないコンデンサというやつ)

折角フルカラーLEDを小型のものを使っているので、このコンデンサは、今回から0603(1608M)サイズにしました。(今までは1206(3216M)サイズを使っていました)

基板の両端にVCC, GND, DINとVCC, GND, DOUTのピンを配置し、この基板を数珠繋ぎに接続できるようにしました。

長辺は85mm程度なので、JLCPCBの安く製造できる100m角に収めました。

基板の発注

JLCPCBに発注します。10枚で$5 + 送料でした。 色によって値段は変わらないので、白色の基板にしてみました。(緑に比べると製造が遅くなるようです)

合わせて0603(1608M)サイズの100nFのコンデンサもAliExpressに注文しました。

設計ミス、発覚

単純な基板だったのですが、数珠繋ぎ用の配線を間違えてしまい、VCC, GND, DOUTのところがVCC, VCC, DOUTとなってしまいました・・ まぁ数珠つなぎにしなければ問題ないし、数珠繋ぎする場合もGND配線を引き回せばこの問題は回避できます。

部品の実装

2mm角のWS2812C-2020は、はんだ付けが難しいかなと思い、はんだペーストとホットエアーガンを使い実装しました。 が、小さいですが4ピンのWS2812C-2020は、4隅をはんだ付けするだけなので同じピッチの6ピンのICなどと比べるとはんだ付けは比較的簡単に感じました。

0603(1608M)サイズのコンデンサも実装できるか心配でしたが、ピンセットを使いつつはんだ付けすれば特に問題なく実装できました。

制御

WS2812はデファクトスタンダードなフルカラーLEDなので、Arduino向けのライブラリは充実しています。 今回はその中でも間違いなさそうなAdafruitのものを利用しました。

github.com

#include <Adafruit_NeoPixel.h>
#define PIN 2
#define NUMPIXELS 8
Adafruit_NeoPixel pixels(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800);

void setup() {
  pixels.begin();
  pixels.setBrightness(16);
}

int count = 0;
int number = 0;
void loop() {
  pixels.clear();
  for(byte i = 0; i < 8; i ++){
    pixels.setPixelColor(i, pixels.Color(((8-i)*30 + count+128)%256, ((8-i)*30 + count)%256, (i*30 + count)%256));
  }
  pixels.show();
  delay(1);
  count ++;
}

ちなみに100nFのコンデンサは無くても問題なく動作しました。まぁもっと光らせて電源が不安定になった場合などにはあったほうが良い気がします。

まとめ

単純なテスト基板でしたが「WS2812C-2020」の動作を確認できました。

制御も簡単、2mm角で小回りの利くフルカラーLEDは、手のひらサイズのガジェットなどに搭載するのはぴったりだと感じました。

ESP32の静電容量タッチでフリック入力を実現してみた

この記事ははJLCPCBの提供でお送りします。

JLCPCBとは

jlcpcb.com (↑こちらは日本語版のログインページで、お得なクーポンも配布されています。)

JLCPCBとは、プリント基板製造などで有名な香港の企業です。

日本からでもWebページでポチポチするだけでKiCADなどで作成した基板データの製造を依頼できます。

値段もかなりお手頃で、ホビー電子工作ユーザーの間では広く利用されています。

この記事の作例もJLCPCBに基板を発注して実現しました。

ESP32の静電容量タッチ機能とフリック入力への応用

ESP32には静電容量タッチ機能が搭載されており、ESP-WROOM-32には10個のピンを静電容量タッチセンサーとして利用できます。

よくある作例としては、10個のパッドを搭載し、静電容量タッチでテンキーなどを作っているものがあります。

ピンが10個しかないので、10個のパッドしか用意できない、というのは、ある意味その通りなのですが、もっとたくさんの入力を得るための工夫の余地はいろいろあります。

今回はこれらの静電容量タッチセンサーの2つのピンの組み合わせを1つのスイッチのように扱うことで5×5=25個のスイッチとして扱うような基板をデザインしてみました。

静電容量タッチ周りの設計

静電容量タッチの基板を作る際は、通常の回路の設計よりも気にすべきことが多いです。 ぱっとわかりやすいところとしては、タッチ検知に関する配線を変に迂回させたり、高周波の信号線と平行に並べない、などという制限があったりします。

github.com

まぁ、すべてのルールを守って設計するのは、なかなか大変なので、今回はそこまで厳密にルールに従わず、静電容量タッチまわりの配線をなるべく迂回させずに配置する、程度にしています。

基板全体の設計

静電容量タッチ以外の部分については、なるべくシンプルになるように余計な機能をつけず、既存のモジュールを並べるだけの構成にしました。

  • ESP32-DevKitC用ピンヘッダ
    • 定番の開発ボード
  • 128x64のI2C接続のOLEDモジュール用ピンヘッダ
  • 128x128のSPI接続のTFT液晶モジュール用ピンヘッダ
    • 上記のOLEDとどちらかを利用できる
  • SDカードモジュール用ピンヘッダ
  • スピーカー接続用ピンヘッダ
  • 外部静電容量タッチセンサー用ピンヘッダ

Amazonのリンクだと以下のような部品を想定しています。

静電容量タッチのフットプリントのデザイン

静電容量タッチのフットプリントのデザインは、以前の300円テトリスのスイッチ部分と同じような方式で用意しました。

KiCADの自動生成ツールの「Mutualcap Touch Button」というツールを使います。 このツールで作成するフットプリントはソルダーレジストがついた状態のものですが、今回はタッチ部分はソルダーレジストなしに編集しました。

静電容量タッチのほかのボードを見るとソルダーレジストありの物を多く見かけるので、それでもおそらく動作すると思うのですが、電極が露出していた方が、より感度が高くなるのでは?という雑な考えの元このように変更してみました。 (結局比べていないので、どちらが良かったのかはまだ不明です。)

基板の発注

今回もJLCPCBに発注しました。基板サイズが10cm×10cm以内だったので、非常に安くで作ることが出来ました。 5枚発注だと送料を抜くと$2でした。送料も$1程度なので、ワンコイン以内ですね。

組み立て

今回は、モジュールを組み合わせるだけなので、組み立ては簡単です。 念のためソケットを介して各モジュールを実装しました。

動作確認とファームウェア開発

まずはI2C OLEDの動作確認をします。といってもこれは素直にESP32のI2Cのポートに取り付けただけなのでu8g2ライブラリを使えば、かんたん・・

と思っていたのですが、手元のOLEDが不良品だったようで妙に動作確認に手間取りました、まぁこれは本質と関係ないので割愛します。

u8g2ライブラリは日本語フォントにも対応しており、今回の用途にはぴったりでした。

github.com

次に静電容量タッチの動作確認をしたところ、どうもT1のポートが反応していないことに気が付きました。 詳しく確認してみると、このT1はGPIO0に使われているピンで、BOOTという起動時の動作を設定するためのピンでもあることがわかりました。 そのためESP32 DevkitCのモジュールでは自動リセットの回路の一部としてこのピンが利用されていました。

おそらくこの影響でT1が意図しない状況になっており、うまくタッチセンサーとして機能しない状況になっているようでした。

T1についてはあきらめてしまおうかと思っていたのですが、色々試行錯誤していると、どうもパソコンとのシリアル接続がつながっている状態だと、問題なく静電容量タッチが動作することに気付きました。

これは自動リセットにかかわるDTR、RTSの信号が、うまい具合にシリアル通信の際に設定されることで、T1のポートが静電容量タッチセンサーとしてうまく機能する状態になるようでした。

(おそらくシリアル通信中はRTSが大半の期間LOWとなるため、IO0がハイインピーダンスになるのではないかと思います。)

https://dl.espressif.com/dl/schematics/esp32_devkitc_v4-sch.pdf

上記PDFより

まぁ、こんな具合でこの基板は何とか動作しましたが、シリアル通信をしていないときはT1ポートが使えないのは、かなり問題なので、ESP32DevKitCなど、自動リセット回路を搭載している回路構成ではT1ポートを静電容量タッチセンサーとして使うのはお勧めしません。

タッチセンサーの入力はアナログ値として取り出せます。この値は絶対値ではなく、パッドの位置や環境によって毎回異なるもののようなので、起動時の初期値を覚えておいて、その値からの変化を見ることで、タッチを検出するのが定番のようです。

ここまでで、25個のキーのどこに触れたかを検出できるようになり、その結果をOLEDに表示できるようになりました。

25キーのキーパッドが作りたいだけなら、ここで完成です。

フリック入力のロジックの実装

25個のキーパッドの入力が検出できるようになったので、次は本命のフリック入力のロジックを実装します。

様々な実現方法がありますが、今回は比較的単純な実装とします。 それでも、ちょっと複雑なので、まずはフリック入力の状態遷移について考えました。

まずは、スワイプではなくタップについて考えます。これは、あるキーの入力が検知され、その後一定期間経過後にどのキーの入力も検出されなくなる、と定義できそうです。

一定期間待つのは、タップ開始中や、タップしている最中に静電容量センサーの状態が不規則にON/OFFされる、いわゆるチャタリング的な現象が発生するからです。

次にスワイプについてです。これはタップのような現象が短い間隔で2度連続して発生することと定義できそうです。厳密には2つの入力の間にどのパッドにも指が触れない瞬間があるか、続けて2つのキーが検出されるかは場合によるので、初回タップのリリースが検出されない場合もありそうです。

などと考えると、結構ややこしいです。

状態遷移図はこのような問題を考えるときに良い道具となります。

まだまだ改善点はありますが、現時点でのソースコードを公開しておきます。

github.com

フリック入力のデモ

そんなこんなで、一応フリック入力のプログラムが完成しました。 スマートフォンのようにスムーズに、というわけにはいかないですが、一応フリック入力でひらがなを入力できました。

現時点では実用は難しそうですが、ソフトウェア的に改良できる点は多いので、ESP32で少ない部品点数で日本語を入力する手法として可能性があると感じました。

さらなる改良案

今回は、キーの検出を静電容量タッチセンサーの値を2値化して、処理しましたが、アナログ値のままで、最も大きな値のものを選択するなどの実装にすると、スワイプの途中に少し隣のキーをかすってしまうような場合もうまく処理ができるようになるのではと考えています。

また、今回は5x5のキーパッドの形状に配線しましたが、X軸,Y軸に5本の細長いパッドを配置し、各パッドの検出値の重みを考慮して、タッチされたX,Y座標を計算するような方式の方が、より応用例の多いタッチセンサーになるかなと感じました。この方式であれば、ノートパソコンに搭載されているトラックパッドのような操作にも対応できそうです。(というかトラックパッド自体がそのような仕組みなのだと思いますが・・)

www.dush.co.jp

上記サイトより

まとめ

以下のことがわかりました

  • ESP32を使うと10個の静電容量タッチが利用できる
    • うちT1については自動書き込み回路の影響で、利用は推奨できない
    • タッチセンサーは部品なしにスイッチを作ることが出来るため、コスト削減にもつながる
  • 10個の静電容量タッチは、組み合わせることで、より多くのタッチ入力センサーとして利用できる
  • 5x5のタッチパッドの構成において、フリック入力が実装可能であること
    • ただし、ソフトウェアをもう少し工夫しないと実用的とはいいがたい
  • 静電容量タッチのアナログ値を利用することで、より精度の高いタッチセンサーの製作が可能であること

簡単な基板でしたが、多くのことを学ぶことが出来ました。 静電容量タッチの仕組みがなんとなく理解できたので、今後のガジェット作成で活用していきたいです。

最後に、この基板が数枚余っていますので、もしほしい人がいたら連絡ください。 (可能なら、ボツ基板交換的なノリで、何か自作の面白い基板と交換してもらえると嬉しいです。 私が提供できるボツ基板はこちらにまとめています。)