LoginSignup
7
4

More than 1 year has passed since last update.

回路図~プログラムまで自作するキーボード

Last updated at Posted at 2021-12-04

はじめに

今年の1月末あたりにRaspberryPiPico(RP2040)が販売されました。
そんな年も終わりが近いですね。
発売後にこんな記事を書きました。

タイマー実装するとか言って実際にタイマーは実装したのですが、、記事の内容は修正していません。
 → こんな記事書くなら余裕があるならそっちも修正すればいいのに。
mruby/cの方にはマージされていてタイマー使えるhalがおいてあるのでそれを使ってみてください。
使用したLチカのプロジェクトはpicorubyの方に退避されているのでそれを見てもいいかもしれません。

その後prk_firmwareがリリースされ、実際に触ってみました。

この時もプログラムの改造を行っていて、
104キー?を1つのpromicro(RP2040)で制御できるようにIOエキスパンダをつけたり
LEDはws2812ではなく、スルーホールのLEDをLEDドライバ経由で制御していました。

このキーボード、IOエキスパンダがかなりネックになっていてタイピングが早すぎると入力できない
I2CだったのでSPIの物に変更すれば多少良くなるかもしれませんが、、、
今ではただの飾りです。

それからしばらくしてRubykaigiがあり、時間もあったので見ていたのですが、
prk_firmwareも進化していますね。
Rubyモードとmscが実装されていてびっくりしました。
中にコンパイラがあると一味違いますね

という訳で前回の英字配列キーボードの反省を活かし、
IOエキスパンダ無しでキーボードを作ってみようと思います。

今回のキーボード

回路図やプログラム等こちらに置いておきました。
https://drive.google.com/drive/folders/1GQ7uYivapBeGNvSFt8jt23Yr1eOKiUkD?usp=sharing

  • LEDはDIPが好きなのでws2812ではなくws2811を使う
  • ロータリーエンコーダでキーレイヤを切り替える
  • マウスをも入れる(ジョイスティック?トラックボール?)
  • ジェスチャーセンサ入れたら面白そう
  • 適当にOLEDもつけておく(意味はない)

購入先一覧

LCSC

型番 数量
100Ω 100
33Ω 100
0.1uF 100
EC12E1220301 1
WS2811 41
PCA9306D 1

jlcpcb

 基板

遊舎工房

 トッププレート
 コンスルー×4

amazon

 OLED

モノタロウ

 アルミ板(ボトムプレート)
 ねじ,ナット
 トラックボール

マルツ

 promicro(RP2040)×2

aliexpress

キースイッチ
キーキャップ
ジェスチャーセンサ
RGBLED
 →WS2811を使用するのでアノードコモンのLEDを購入

私が購入したところなので、OLEDはaliexpressで安く買えますしpromicroやトラックボールは遊舎工房でも売ってます。

基板設計・製造

Kicadの勉強をしましょう
こちらのチュートリアルをやっておけばフットプリントを作る部分なども書いてあるのでかなり応用できるはず

まずはどんなキーボードにするか決めなきゃならないです。
おそらくほとんどの方はキーの配置やキーの数にこだわりがあり、
自分に合ったキーボードを作っておられると思います。

しかし、キーの配置について私にはとくにこだわりはありません:frowning2:
とりあえずprk_firmwareが標準で対応しているCrkbd (Corne)を真似して作ってみましょう:wink:

回路図は自作キーボードに良く採用されいるマトリックス方式を採用してキーの検出を行います。
今回、左右で別々の回路図を書いています。
良く左右を1種類の基板で作れるようにしてあるもの(Crkbdやhelixとか)もありますが、
WS2811がデカくてどうせPCBは別々で作ることになると思うので回路図も別々で用意します。

左右の通信はとりあえずUARTでいいと思います。prk_firmwareが標準で対応しているので。
一応パッドを用意してI2Cと選べるようにはしておきます。

基板の方のアートワークはノイズの事を考えながらやってみましょう
私は下記のサイトを基準にしています。

基板の製作に失敗はつきもの
1度目の試作:
・左側のpromicro用ピン穴径を0.8mmにしてなくてコンスルーがスカスカだった
・I2CがSCLとSDA逆になってる
・チップ部品1005ははんだ付けめんどくさいから次は1608にしておこう
・トラックボールはレベル変換しないといけない
2度目の試作:
・トラックボールモジュールが動かない。呪いにかかっているようだ。
  → データの信号が0の時GNDまで下がりきっていない。
    ノイズ対策のサイトを↑で紹介しました、そこの5番ですね。
    信号ラインは平行に配線しない!!(差動でない限り)
    思いっきり3.3Vの電源ラインとクロック・データの線が平行に配線されていました。
    リターンパスも気にしたほうがいいと思います。
・トラックボール用に準備してたレギュレーターが2.7Vを出力しない。
 と思ったら出力できるのもある。これだから中華のレギュレーターICは…:rage:
 できなかったものは入力と出力のピンが逆になってました。同じレギュレータなのになぜ:thinking:
・左右の電源ラインに3.3Vが供給されている事が今更判明。片側だけLEDの光が若干暗い。
 5Vへ変更するべき

今回は2度目の試作基板で進めていきます。
トラックボールと左右の電源ラインはパターンを切断して外付けでケーブルをはんだ付けしました。
※左右の電源ラインを5Vにしたらジェスチャーセンサが動かなくなった
 原因調査中

ガーバーデータ出力

ガーバーデータの出力方法についてはfusionPCBにある内容で基本的に大丈夫だと思います。

基板発注

基板の注文先はfusionPCBまたはjlcpcbをよく利用します。
日本語のサポートが欲しい場合はチップワンストップがおすすめ。
allpcbでは無料で注文できるという話を聞いたので、次は試してみたい。

一応jlcpcbの注文画面を載せときます。
ちなみに値段は
基板製造費:\$19.66
送料: \$18.14
合計費: \$37.80 となりました。
Screenshot 2021-11-28 at 20-13-05 PCB Order Online PCB Quote SMT Assembly Quote - JLCPCB.png

キーボード組み立て

アクリルの天板を自分で加工しようと思ったのですが、
加工中バッキバキに割れてしまったので遊舎工房に頼みました。
(卓上CNCあるのに使ったことがない…)
2mmの穴が1か所レーザーでは難しそうだったので自分で穴あけしましたが、
割れちゃいました:wink: 割れたまま使います

アルミの加工は、
1. アルミ板にPCBを押し付けながら外周をけがき針で印付けておきます。
2. 印より外側をジグソーで適当に切って。
3. ハンドニブラーで余分な部分も切断して
4. どこで買ったか忘れたやすりでバリを取っておけばいい感じになります。
5. ドリルドライバドリルビットを付けて穴あけもします。今回はM2ねじなので2.5mm
穴あけするときもPCBをあてて適当に穴あけしておきます。
スケールやノギスなどで計測を一切行わない荒い作業。

2mmのアクリル板と1.6mmの基板。
背面は1.5mmのアルミ板でスペーサーはそれぞれ3mmとしましたが、ねじの首下12mmじゃ足りないですね。
でも頑張って締めました。
PCBとアルミ板の間は2mmのスペーサーでもよかったかもしれませんね。
寸法.png

prk_firmwareビルド環境構築

必要ないかもしれないけどホストOS(私はwindows)にVScodeをインストールします。

wslにて環境を構築します
下記commandでwslをインストールしますubuntuもついてくる

cmd.exe(管理者)
wsl --install

再起動後、wslのubuntuが立ち上がってユーザー名やパスワード入れてと言ってくるので適当に入れた後、
↓のコマンドを実行していくと環境が出来上がると思います。

# pipico sdk setup
sudo apt update
sudo apt install cmake gcc-arm-none-eabi libnewlib-arm-none-eabi libstdc++-arm-none-eabi-newlib
git clone https://github.com/raspberrypi/pico-sdk.git
cd pico-sdk
git submodule update --init
cd ../
echo 'export PICO_SDK_PATH=$HOME/pico-sdk' >> ~/.bashrc

# ruby install
git clone https://github.com/sstephenson/rbenv.git
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(rbenv init -)"' >> ~/.bashrc
source ~/.bashrc
git clone https://github.com/rbenv/ruby-build.git
sudo apt install -y libssl-dev zlib1g-dev
rbenv install 3.0.0
rbenv global 3.0.0

# prk_firmware install
git clone --recursive https://github.com/picoruby/prk_firmware.git
cd prk_firmware
sudo apt install build-essential
sudo apt install g++
sudo apt install -y zip
./setup.sh
cd build
make
cd ../keyboards
git clone https://github.com/picoruby/prk_meishi2.git
cd prk_meishi2/build
cmake -DPRK_NO_MSC=1 ../../..
make

プログラムの変更

本家prk_firmwareから変更した内容を記載します。
主に.rbや.cの機能的な部分について説明するので、
.rbsや.hなど他の変更が気になる方はgithubのコードを見てみてください:disappointed_relieved:

githubはこちら。今回の作業ブランチはdev_jsです。
https://github.com/aikawaYO/prk_firmware/tree/dev_js
ジェスチャーセンサの開発用ブランチでしたが、ジェスチャーセンサなんて本家prk_firmwareに必要ない機能で、気が付いたらすべてここに集まってました。一人開発は恐ろしいですね。
jsはジェスチャーのつもりですが、ジェスチャーはgestureですね。jじゃない

prk_firmwareビルド環境構築
windowsのエクスプローラーから見るとこんな位置にprk_firmwareがある状態だと思います。
\\wsl$\ubuntu\home\ユーザー名\prk_firmware

OLED

本家へプルリク出してます。

参考にしたのはこのあたりだった気がする
SSD1306は設定項目が多く、どこで何を設定するのか日本語で説明しているこの記事は日本人の私にとって最高です。

今回のプログラムでは一応QMK_Firmwareのglcdfont.cを使えるようにしているので個人的に頑張ったと思ってる
QMK_Firmwareを使ったことないので使用できるメソッドは適当だけど…
文字列の出力とロゴ表示、初期化だけ。

QMK_Firmwareでは1行目?が下記のようになっているが

glcdfont.c
#include "progmem.h"

今回のプルリクではこんな感じにすればQMK_Firmwareで使用していたglcdfont.cも使えます。

glcdfont.c
#include "oled.h"

ロータリーエンコーダ

今回はロータリーエンコーダにてレイヤーの切り替えを行います。
prk_firmwareの標準機能にあるロータリーエンコーダは正転と逆転を検知して紐づけしたRubyコードを実行するようになっています。
これはレイヤーの切り替えができるのか:thinking:

とりあえず標準機能を書き換えて実現させています。
かなりコードの改変をしているのでkeymap.rbのみ説明します。
基本的には元と同じですが、encoder_left.clockwiseを複数回実行しています。
実行した回数だけ配列の要素が増えるようになっていて、
ロータリーエンコーダを回した際にこの中身が順番に実行されるようになる。
(ことが目標ですがあまりうまく動いていない、)

keymap.rb
encoder_left = RotaryEncoder.new(26, 27)
encoder_left.configure :left
encoder_left.clockwise do
  $rgb.fill(hsv2rgb(150, 100, 12.5))
  kbd.lock_layer :default
end
encoder_left.clockwise do
  $rgb.fill(hsv2rgb(10, 100, 12.5))
  kbd.lock_layer :raise
end
encoder_left.clockwise do
  $rgb.fill(hsv2rgb(225, 100, 12.5))
  kbd.lock_layer :lower
end
encoder_left.clockwise do
  $rgb.fill(hsv2rgb(80, 100, 12.5))
  kbd.lock_layer :al
end
kbd.append encoder_left

ジェスチャーセンサ

キーボードへ触れずに操作出来たらカッコいいかもしれません。
私はスナック菓子を手掴みで食べながらキーボード触りたくないので実装します。

ジェスチャーセンサにはいろいろなものがありますが、今回はAPDS-9960を使用します
APDS-9960はジェスチャーの検知以外にも距離を計測したり、色を判別したりと要らない便利な機能が盛りだくさんです。
今回はジェスチャー(上下左右のみ)なので下記記事を参考にします。

内容的には↑の記事そのままなので説明はしません。
それぞれ対応する方向へgestureが検知された際はsend_key()でその方向のキーを出力しています。

keyboard.rb
class Keyboard
  def initialize
……………………………………………………
    $gesture_mode = false
  end

  def gesture_init()
    i2c_write(0x39,[0x80,0x45])   #POWER ON, GESTURE ENABLE, PROXIMITY DETECT ENALBE,AEN=0
    i2c_write(0x39,[0xA3,0xC1])   #Gain x8, LED Drive 25mA, Wait Time 2.8mS
    i2c_write(0x39,[0xAB,0x00])   #GIEN off(INTERRUPT DISABLE), GMODE OFF
    i2c_write(0x39,[0xA0,40])     # Enter Threshold
    i2c_write(0x39,[0xA1,40])     # Exit Threshold
  end

  def af_check()
    check = i2c_read(0x39, 0xAE, 1)
    return check[0]
  end

  def ag_check()
    check = i2c_read(0x39, 0xAB, 1)
    return check[0]
  end

  def start!
    i2c_init()
    gesture_init()
    g_end = 0
    i = 0
    datas_old = [0,0,0,0]
    datas_peak = [0,0,0,0]
    upe_flag = 0
    cnt = 0

    while true
……………………………………………………
      if($gesture_mode && @anchor)
        if(af_check() != 0)
          datas = i2c_read(0x39, 0xFC, 4)
          if(g_end == 0)
            cnt += 1
            i = 0
            while(i<4)
              if(datas[i] > datas_old[i])
                datas_old[i] = datas[i]
                datas_peak[i] = cnt
              else
                upe_flag = (upe_flag & ~(1 << i)) + (1 << i)
              end
              i += 1
            end
            if(upe_flag == 0b1111)
              if((datas_peak[0] > datas_peak[1]) && (datas_peak[0] >= datas_peak[2]) && (datas_peak[0] >= datas_peak[3]))
                send_key(:KC_DOWN)
              elsif((datas_peak[1] > datas_peak[0]) && (datas_peak[1] >= datas_peak[2]) && (datas_peak[1] >= datas_peak[3]))
                send_key(:KC_UP)
              elsif((datas_peak[2] > datas_peak[3]) && (datas_peak[2] >= datas_peak[0]) && (datas_peak[2] >= datas_peak[1]))
                send_key(:KC_RIGHT)
              elsif((datas_peak[3] > datas_peak[2]) && (datas_peak[3] >= datas_peak[0]) && (datas_peak[3] >= datas_peak[1]))
                send_key(:KC_LEFT)
              else
                send_key(:KC_)
              end
              datas_old = [0,0,0,0]
              upe_flag = 0
              cnt = 0
              g_end = 1
            end
          end
        elsif((ag_check() == 0) && (g_end == 1))
          g_end = 0
        end
      end

keymap.rb
class Gesture
  def set_mode()
    if $gesture_mode
      $gesture_mode = false
      oled_clear
      oled_puts_logo()
    else
      $gesture_mode = true
      oled_clear
      oled_puts_txt(1, "Gesture ON")
    end
    return ""
  end
end

kbd.define_mode_key :G_MODE,      [ nil, Proc.new{ kbd.macro Gesture.set_mode(),[]}, nil, nil ]

ジェスチャーセンサは通常邪魔なので、ONOFFできるよう$gesture_modeで切り替えます。
それをキースイッチで指定できるようにkbd.define_mode_keyでキーコードを追加します。
本当にONしているかわからないので、ONしているときはOLEDに文字列を表示させています。
こんなときにOLEDは便利ですね。

マウス

本家で対応予定になってるけど待てなかったので入れました。

cの部分はtinyusbでほぼ実装されているのであまり難しくはないです。
ボタンとXYの移動のみ実装します。
スクロールは今後。

usb_descriptors.c
void
c_report_hid_mouse(mrb_vm *vm, mrb_value *v, int argc)
{
  int8_t x_axis = GET_INT_ARG(1);
  int8_t y_axis = GET_INT_ARG(2);
  int8_t button = GET_INT_ARG(3);

  /*------------- Mouse -------------*/
  if (tud_hid_ready()) {
    tud_hid_mouse_report(REPORT_ID_MOUSE, button, x_axis, y_axis, 0, 0);
  }
}
keyboard.rb
# マウスのボタンを初期化
def initialize
……………………………………………………
  $mouse_button = 0
end

def start!
  mouse_reset = true

……………………………………………………

  @switches.clear
  @modifier = 0
  # modifierを初期化している下でマウスのボタンも初期化
  $mouse_button = 0

……………………………………………………

  # マウスの変化があった場合にセット
  if(0!=x||0!=y||0!=$mouse_button)
    report_hid_mouse(x, y, $mouse_button)
    mouse_reset = true
  # マウスの変化がない場合1度のみリセット
  elsif(mouse_reset)
    report_hid_mouse(0, 0, 0)
    mouse_reset = false
  end

  # report_hidより上に実装
  report_hid(@modifier, @keycodes.join)

XYの変化を求める部分についてはジョイスティックとトラックボールを今回は実装します。詳細は下階層。
左右のクリックはキーマップ側で実装しました。
本当はkeyboard.rbでいい感じにできると理想的でいいのですが…

keymap.rb
class Mouse
  def button(b)
    $mouse_button += b
    return ""
  end
end

kbd.define_mode_key :MOUSE_LEFT, [ nil, Proc.new{ kbd.macro Mouse.button(1),[]}, nil, nil ]
kbd.define_mode_key :MOUSE_RIGHT, [ nil, Proc.new{ kbd.macro Mouse.button(2),[]}, nil, nil ]

マウス-ジョイスティック

実装しやすい
ジョイスティックは可変抵抗が2つついているので、変化値をADCで読み取ります。
そのために必要なrubyのadcメソッドを準備します。

adc.c
#include "adc.h"

#include "hardware/adc.h"

void
c_adc_init(mrb_vm *vm, mrb_value *v, int argc)
{
  adc_gpio_init(GET_INT_ARG(1));
}

void
c_adc_set_dir(mrb_vm *vm, mrb_value *v, int argc)
{
  adc_select_input(GET_INT_ARG(1));
}

void
c_adc_read(mrb_vm *vm, mrb_value *v, int argc)
{
  SET_INT_RETURN(adc_read());
}

void
c_adc_read_v(mrb_vm *vm, mrb_value *v, int argc)
{
  SET_FLOAT_RETURN(3.3 * adc_read() / 0x1000);
}

マウス-トラックボール

PAW3204で動くADTB7Mというモジュールがありました。
購入後に通信がI2Cと異なること、電圧が最大2.9Vであることが発覚し焦る。

RP2040で動かすサンプルを探していたところ下記を発見しました。

KermiteはGUIでキーマップなどを一式ファームウェアとして書き出せる?様です。
 → 申し訳ないですが使ってないので憶測です。
こちらがなんとRP2040とATMega32u4に対応しています!
PAW3204との通信に関してはこちらをベースに実装します。

ということで、i2cではなくgpioを使用して通信するため、
hardware/gpio.hが宣言されているgpio.cに実装します。
(paw3204.cなど作るべきです:angry:

gpio.c
# トラックボール用GPIO初期化
void
c_init_paw3204(mrb_vm *vm, mrb_value *v, int argc)
{
  gpio_init(PAW3204_SCLK);
  gpio_set_dir(PAW3204_SCLK, GPIO_OUT);
  gpio_put(PAW3204_SCLK, 1);


  gpio_init(PAW3204_DATA);
  gpio_pull_up(PAW3204_DATA);
  gpio_set_dir(PAW3204_DATA, 0);

  sleep_ms(2);
  gpio_put(PAW3204_SCLK, 0);
  busy_wait_us_32(100);
  gpio_put(PAW3204_SCLK, 1);
  sleep_ms(2);
}

# トラックボールからデータ取得
void
c_read_track_ball(mrb_vm *vm, mrb_value *v, int argc)
{
  gpio_set_dir(PAW3204_DATA, GPIO_OUT);
  gpio_disable_pulls(PAW3204_DATA);
  uint8_t tx_data = GET_INT_ARG(1);
  for (int i = 7; i >= 0; i--) {
    uint8_t bit = (tx_data >> i) & 0x01;
    gpio_put(PAW3204_SCLK, 0);
    busy_wait_us_32(5);
    gpio_put(PAW3204_DATA, bit);
    busy_wait_us_32(5);
    gpio_put(PAW3204_SCLK, 1);
    busy_wait_us_32(5);
  }
  busy_wait_us_32(5);
  gpio_set_dir(PAW3204_DATA, GPIO_IN);
  gpio_pull_up(PAW3204_DATA);
  int8_t rx_data = 0;
  for (int i = 7; i >= 0; i--) {
    gpio_put(PAW3204_SCLK, 0);
    busy_wait_us_32(5);
    gpio_put(PAW3204_SCLK, 1);
    busy_wait_us_32(5);
    uint8_t bit = gpio_get(PAW3204_DATA);
    busy_wait_us_32(5);
    rx_data += (bit << i);
  }
  busy_wait_us_32(5);
  SET_INT_RETURN(rx_data);
}

で、X,Yの値を取得するのですが、
PAW3204のドキュメントにはMotion_Status(0x02)にこんな記載があります。

Read this register before reading the Delta_X and Delta_Y registers.

X,Yのデータ取得する前にこのステータスを確認してくださいってことですね。

keyboard.rb
# トラックボールから値を取得
read_track_ball(0x02)
x = read_track_ball(0x03)
y = read_track_ball(0x04)

# マウスの情報をセット
if(0!=x||0!=y||0!=$mouse_button)
  report_hid_mouse(x, y, $mouse_button)
  mouse_reset = true
elsif(mouse_reset)
  report_hid_mouse(0, 0, 0)
  mouse_reset = false
end

# report_hidより上に実装
report_hid(@modifier, @keycodes.join)

その他自作したもの

ロータリーエンコーダを回すためのつまみの部分を作りました!
このキーボードで一番の推しポイントです:bangbang:
IMG_20211204_224543.jpg

これはしゅ〇キャラをイメージして作ったものです。
今回のキーボードはこちらを搭載する事により、レイヤー切替の事をキャラチェンジと呼びます。

作り方
1. モデルを3Dプリンターで印刷
2. やすりがけ
3. サーフェイサーで滑らかに
4. 上と下を瞬間接着剤で結合
5. 黄金塗装
6. 🍀部分にレジンを流し込んで固める

私が使用している3Dプリンターはこんな感じのRepRap Prusa i3互換機です。
メーカー製よりカスタマイズしやすいのと交換部品も入手が楽なのでお勧めです。
組み立て大変なのと精度が微妙だったりなど欠点もいくつかありますが…

M2×3mmのスペーサもこいつで作りました。

デモンストレーション動画

動画の編集でキースイッチの動作と画面が若干ずれているかもしれません

感想

IMG_20211204_223643.jpg

元々はprk_firmwareをそのまま使う予定でしたが、いろいろ追加してしまいました。
ジェスチャーセンサ以外はqmk_firmwareの方にもありそう?な機能なのでprkの方にも今後実装されるといいですね。

まだまだ未完成なので開発を続けないといけない(´;ω;`)

7
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
4