グリッド楽器igetaを作った

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

JLCPCBとは

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

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

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

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

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

グリッド楽器igetaとは

まぁ世の中によくあるグリッド型の電子楽器的なものを自分も作ってみようという試みです。

有名なところだとTENORI-ONとか Launch Padというものがあったりしますね。

igetaは8x8のスイッチと8x8のLEDを搭載した入力装置です。 音声出力する機能もあり、オーディオアンプやボリュームも搭載しています

仕様

  • Raspberry Pi Pico
  • 8x8=64個 タクトスイッチ
  • 8x8=64個 LED
  • NS8002オーディオアンプモジュール
  • ボリューム

今回はRaspberry Pi Picoの性能に全のっかりで行きます

パーツ紹介 - NS8002オーディオアンプモジュール

AliExpressで発見したモノラルのオーディオアンプモジュールです。 詳しいことはよく知らないですが、今まで使っていたLM386よりも3.3Vで大きな音が出る点が気に入っています。

その他のパーツたち

タクトスイッチはFusionPCBを使って(ほぼ)はんだ付けせずに自作ゲーム機を作った(PCBA) - inajob's blogで紹介した DTSM-62K-S-V-T/R(SN431)です。 楽器としてはカチカチ音が鳴る点が微妙かもしれませんが、軽いタッチ(100gF)で押せるのでなかなか気に入っています。

LEDは以前購入していた1206サイズの赤色LEDを使いました。別に何色でも良いので、次作る時は別の色にしようかなと思います。

ボリュームはオリジナルのマクロパッドを作ってみた - inajob's blogで紹介したB103です。薄型の部品で、側面にちょっとした隙間があれば実装できる点が気に入っています。

Raspberry Pi Picoは言わずと知れた有名なマイコンボードです。ちょっと前まではArduno Nanoをメインに使っていたのですが、最近の半導体不足の影響か価格が高騰しています。またArduino Nanoに搭載されているマイコンであるATmegaはどうも質の悪い偽物が出回っているようで、一見ちゃんと動いているようでも、細かい部分の動作がおかしい個体があるように見えます。

一方でRaspberry Pi Picoは安価で、性能が高く、コアとなるRP2040も新しいマイコンのためか、偽物の情報もあまり聞かないので、最近はこちらをメインに利用しています。

基板の設計

今回の基板のキモは大量のタクトスイッチとLEDの接続です。

タクトスイッチ64個とLED64個をどうやってRaspberry Pi Picoに接続するかという話です。 よくあるのはマトリクスで配線する方法ですが、これは64個の部品を制御するのに16個のGPIOが必要となります。

素朴にLEDとタクトスイッチのそれぞれをマトリクス配線するとなると16+16=32のGPIOが必要となります

Raspberry Pi PicoのGPIOは28個なので、このままではGPIOが足りません。

そこで今回は、Charlieplexingという手法を採用します。この手法により16個のGPIOでこれらの制御ができます。

しかしCharieplexingは配線が複雑で、回路図を描くのが難しいという課題があります。

これについても様々な解決策があるのですが、今回はikkeiplexingという手法を採用しました。

この手法は https://twitter.com/jh3kxmさんが考案されたもので、ikkeiplexing shieldのページのPDFが非常に参考になりました。

回路図を示すと以下のような感じとなりました

左下がタクトスイッチ(とダイオード)、右上がLEDで、左縦一列の16個のGPIOで制御できます。

LEDが抵抗なしでGPIOと接続されていますがRP2040はGPIOの電流制限の機能があるようなので、正しく設定して利用すれば問題ないと理解しています。(https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf のp239 2mA, 4mA, 8mA, 12mAの間で設定可能)

で、後は基板を設計するだけ!

だけ、と書きましたが、意外とこの基板配線が複雑で、配置を工夫する必要がありました。

配線は自動配線ツールのFreeroutingに丸投げなのですが、配置が悪いといくら待っても配線が完了しない感じになります。

Raspberry Pi Picoはスルーホール穴での実装はせずに、端面スルーホールを利用し表面実装することにしました。これで裏面が自由に使えるので配線の複雑さが緩和されます。

オーディオアンプNS8002は、出来合いのモジュールを使う方法と、直接基板に実装する方法の2パターンに対応しました。 (直接基板に実装するパターンは、手元に部品がなくまだ動作が確認できていないですが・・)

JLCPCBに発注

今回も発注はJLCPCBです。今回の基板の色は黒色です。

この基板も10cm×10cmに収まるように設計したので5枚だと $2 + 送料です。

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

実装

今回はとにかく、タクトスイッチ、ダイオード発光ダイオードの数が多いというのが大変なところでした。

ひたすらはんだ付けです・・

そんな中で、今回もいくつかのトラブルが起きました。

配線が足りてない!

今回のミスです。単純にCharieplexingの配線を間違えており、必要な部分が結線されていませんでした。 いくつかのパターンをカットし、8本の配線を後から追加で足すことで、正しい回路に修正しましたが、表面はもじゃもじゃのケーブルが這うことになりました。

USBケーブルが基板と干渉する

Raspberry Pi Picoを基板の端からやや内側に配置したことと、端面スルーホールを利用して表面実装したために、Raspberry Pi PicoのUSBポートが基板にかなり近くなってしまいました。

その影響で、USBケーブルを指そうとすると基板と干渉してしまいうまく接続できない問題が起きました。

これはどうしようもないか・・と思ったのですが、手元にあるちょっと変わった形のUSBケーブルを使うと、何とか干渉せずに接続することが出来ました。

とはいえ、一般的なUSBケーブルが刺さらないのは問題なので、次回は直したいところです。

完成

何とか動作するものが出来ました。

ソフトウェアの実装

今回はCircuitPythonで実装しました。

今回の回路設計に用いたCharieplexingという手法の難しい所は配線だけではなく、ソフトウェアの実装についてもです。 適切な順番でGPIOを制御しないと、意図しないLEDが光ったり、正しくキー入力を検知できない問題が起きます。

CircuiyPythonはプログラムを変更してから実行するまでのサイクルがC/C++と比べると早いので(特にコンパイルが不要というのが大きい)こういう試行錯誤には向いていると感じました。

一応「楽器」ということで、音を鳴らす機能も付けてみることにしました。CircuitPythonには音を鳴らすためのSynthioというライブラリがデフォルトで同梱されているのでこれを使うことにしました。

Synthioはとてもシンプルな制御で、和音も鳴らすことが出来るのでちょっとした音の鳴るおもちゃを作るのに非常に便利だと感じました。

ただCircuitPyhton自体がネイティブと比べると処理が遅いので、凝ったことをしようとすると音が途切れてしまったり、期待した速度で動かないことなどがあるので、そういう場合はおとなしくC/C++を使うのがよさそうです。

ということで、できました。

押したキーに対応するLEDが点灯し、かつそのキーに対応した音が鳴るプログラムです。 同時押しにも対応しており、同時押しすると和音を演奏できます。

"""
igeta-pico test

Copyright (c) 2023 inajob

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

import digitalio
from board import *
import time
import synthio
import audiopwmio
audio = audiopwmio.PWMAudioOut(GP18)

synth = synthio.Synthesizer(sample_rate=22050)
audio.play(synth)
#synth.press((65,69,72))

p1 = GP2
p2 = GP3
p3 = GP4
p4 = GP5
p5 = GP6
p6 = GP7
p7 = GP8
p8 = GP9

p9 = GP10
p10 = GP11
p11 = GP12
p12 = GP13
p13 = GP14
p14 = GP15
p15 = GP16
p16 = GP17

ports = [p1,p2,p3,p4,p5,p6,p7,p8,p9,p10,p11,p12,p13,p14,p15,p16]
pports = []
for p in ports:
    pports.append(digitalio.DigitalInOut(p))
    
sw = [
    [True, True, True, True, True, True, True, False],
    [True, True, True, True, True, True, True, True],
    [False, False, False, False, False, False, False, False],
    [False, False, False, False, False, False, False, False],
    [False, False, False, False, False, False, False, False],
    [False, False, False, False, False, False, False, False],
    [False, False, False, False, False, False, False, False],
    [False, False, False, False, False, False, False, False],
    ]

def scanKey(n):
    for i in range(8):
        pports[i].switch_to_input(digitalio.Pull.UP)
    pports[n + 8].switch_to_output(False) # active
    for i in range(8):
        if pports[i].value:
            if sw[i][n]:
                pass
            else:
                synth.release(i*8+n+64)
        else:
            if sw[i][n]:
                synth.press(i*8+n + 64)
            else:
                pass
        sw[i][n] = pports[i].value
    pports[n + 8].switch_to_input(None) # deactive
def scanLed(n):
    for i in range(8):
        if sw[n][i]:
            pports[i].switch_to_input(None)
        else:
            pports[i].switch_to_output(False)
    #time.sleep(0.1)
    pports[8 + n].switch_to_output(True)
    # flash
    time.sleep(0.001)
    pports[8 + n].switch_to_input(None) # Hi-Z

while True:
    # scan switch
    for i in range(8):
        scanKey(i)
    
    # LED
    for i in range(8):
        pports[i].switch_to_output(False)
        pports[i+8].switch_to_input(None)
    #time.sleep(1)
    for i in range(8):
        scanLed(i)

まとめ

グリッド楽器igetaを作ることが出来ました。

この過程で以下を学ぶことが出来ました

  • Charieplexingを使い多数のスイッチとLEDを制御
  • Raspberry Pi Pico + CircuitPythonを使ってCharieplexingを実現
  • Synthioを使った簡易的な楽器の作成

あとやりたいこととしては、筐体の作成、楽器としての機能の拡充、乾電池駆動でモバイル楽器化などを考えています。まぁボチボチやります。

おまけ 過去のigeta

実はこのグリッド楽器igetaは過去に別のアプローチで試作したことがありました。

その時は Arduino NanoとCH451を使った構成でした。

CH451というのが、この時のキモで、8×8入力、8×8出力を一気に扱えるインターフェースICです。

しかし、作ってから気づいたのですがこのCH451はキーの同時押しに対応しておらず、楽器として利用するには機能が足りないという点でお蔵入りとなりました。

また、この時は小型のスイッチと3Dプリンタで作成したスイッチのカバープレートを組み合わせて、スイッチ自体が光るような見た目にできないかと模索していました。

この試みは一定の成果があり、キーが光るような見た目の筐体を作ることはできたのですが、手元の3Dプリンタの精度ではどうしてもキーが引っかかったりとか、押し心地にムラが出る、という結果となりました。

ということで今回は素朴に6mm角のタクトスイッチをそのまま使う設計としました。

この過去のigetaでも音を鳴らすところまではプログラムを作り、それなりに遊んだりもしました。

こういう過去があっての今回のigetaの製作につながっています。

ってこれやってたの2021年5月とかですね、、意外と歴史があります。

表面実装部品版RakuChordの改良版を作った

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

JLCPCBとは

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

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

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

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

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

表面実装版RakuChordとは?

RakuChordというのはinajobが開発している電子楽器です。

inajob.github.io

ワンタッチで和音を演奏できるボタンがあり、だれでも比較的簡単に演奏ができるという電子楽器で、少しずつバージョンアップをさせながら、電子工作キットとしての販売をしています。

以前RakuChordの表面実装版を作ったことがありましたが、今回はこれの更なる改良版です。

inajob.hatenablog.jp

今回の表面実装版RakuChordの設計

基本的な設計は前回の表面実装版RakuChordと同じです

前回は6mmのスイッチで、修飾キーは基板裏面に配置したのですが、どうにも使い勝手が悪かったので、今回の改良で12mmタクトスイッチを採用し、従来のRakuChordの操作性を再現することと、修飾キーを上側に並べることを試すことにしました。

  • タクトスイッチは12mmスルーホール
    • 表面実装版と謳っているいるが、家にこの部品がたくさんあるのでスルーホールを使うことにする
  • 昇圧回路は安価な昇圧モジュールも利用できるようにする
  • 7つの修飾キーは基板裏面ではなく、90度の角度が付いたものを上辺に並べる
  • 相変わらず10cm×10cmに収める(安いので)
  • 表面実装部品のサイズを0603に変更(以前は1206)

90度の角度が付いたタクトスイッチ

以前、AliExpressでタクトスイッチのアソートを買ったのですが、その中に90度の角度が付いたものがあり、これを使うことにしました。

タクトスイッチのアソートは100種類のタクトスイッチがそれぞれ5個ずつ入っているような商品で、スイッチ好きの人にはたまらないものとなっています。お勧めです。

商品リンク

基板の設計

今回は以前作ったデータを改良する形で設計を行いました。そのため比較的簡単に設計が完了しました。 (前回の作例を記事にしていたおかげで、修正が必要なバグが探しやすかったです)

実際論理回路図の変更部分は非常に少なく、フットプリントの付け替えや、拡張端子の引き出し程度でした。

実体配線図は、さすがに流用することが難しいので新たに作っています。 タクトスイッチが6mmのものから12mmのものに変更となったので、基板の横幅を少し広げました。

JLCPCBに発注

今回も発注はJLCPCBです。今回の基板の色は紫色です。

10cm×10cm以内で5枚だと $2 + 送料です。お安い・・

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

実装

さてさて、今回もいくつかのトラブルに見舞われました

我が家のICがやはりおかしい

今回もどうもATmega328の様子がおかしく、シリアル通信の速度を遅くしないと書き込みができませんでした。 以前の記事ではUSBシリアル変換ICであるCH340Nがおかしいかも、と書きましたが、色々実験をしているとATmega328の方がおかしいようでした。

我が家にストックしているATmega328が偽物という可能性が出てきています・・・どうしたものか・・

ともあれ、前回と同様にだましだまし書き込むことで一応期待した動作をしています。

格安の昇圧モジュールから電源を供給すると動作が不安定

今回は、HT7750を使った昇圧回路とは別に、格安の昇圧モジュール(名称不明)を使って乾電池2本から5Vへの昇圧が出来るように設計していました。 しかし、こちらの昇圧モジュールを使った場合、どうも動作が不安定になるという事象が発生しました。

おそらく、音を鳴らす際に多くの電流を取り出そうとするために、電圧低下を起こしてしまっているのだと思います。 おとなしく前回と同じHT7750を用いた昇圧回路の方を使うことにしました。

昇圧方法を両方用意しておいてよかったです。

この格安の昇圧モジュールについては Arduino Nanoを単4電池で動かすボードを作った - inajob's blog で紹介しました

ダイオードのフットプリントが1つだけ逆向き

この基板にはスイッチの同時押しを実現するために、多数のダイオードが存在しています。 ダイオードには極性があるため、実装時には注意が必要ですが、実装を簡単にするため、同じような位置に存在するダイオードの向きは揃えておいたつもりだったのですが・・・

1つだけ並んでいるなかで向きが逆のものが残ってしまっていました。 案の定実装するときに、ほかと同じ向きに実装してしまい、キーが認識されないという問題が発生しました。

基板上には正しい向きが指定されているので、バグではないのですが、実装ミスを誘う良くない設計だと感じました。

格安の昇圧モジュールの向きが逆

昇圧モジュールは単純に2.54mm間隔ピンが3つ出ているだけなので、専用のフットプリントを作らず、汎用的なコネクタ部品とを配置し、格安の昇圧モジュールの配置を示す四角形を基板上に描画していたのですが、この四角形の配置を間違えており、実際の格安の昇圧モジュールの配置と食い違っていました。

そのままでは乾電池ケースと干渉してしまい、格安の昇圧モジュールが実装できない、という感じだったのですが、想定していた基板の面とは逆の表面に実装することで対応しました。 (上に書いたようにこのモジュールはそもそも性能不足で利用できなかったのですが・・)

電源スイッチの配線が間違っている

電源スイッチ周りの配線は、格安の昇圧モジュールからくるもの、HT7750の昇圧モジュールからくるものの両方を考慮する必要があったのですが、格安の昇圧モジュールからくるものを見落としており、こちらは電源スイッチとは関係なく導通した状態になってしまっていました。

これは一部の回路のパターンを切断し、導線を使ってジャンパすることで、修正しました。

(これも上に書いたように、このモジュールの性能不足により、利用できないのですが・・・)

ダイオードが破損

これは実装が終わってしばらく遊んでいる最中に起きたことなのですが、1つのキーが反応しなくなりました。 原因がわからずいろいろ試していたところ、キーの同時押しを実現するためのダイオードの1つが壊れていることが判明しました。

ダイオードは通常片方向からのみ電気を通す素子ですが、なぜかどちらからも電気を通さない状態になっていました。

部品を交換することで、この問題を解消しました。 ダイオードって壊れやすいんですかね?

ケースの作成

演奏しやすいように3Dプリンタでケースを作るというのもやってみました。

利用したツールはOpenSCADとLibreCADです。

モデリングを簡単にするために、KiCADから基板データをdxf形式エクスポートしたものを積極的に利用するスタイルで設計しました。

まず基板外形を使いベースとなる形を整え、それをくりぬいていくという手法です。 くりぬく範囲もKiCADのUserレイヤーを使いGUIで作成しました。

くりぬきは貫通、micro USBコネクタの高さ、筐体側2mm残しの3段階とし、それぞれのレイヤーをdxfでエクスポートし、OpenSCADを使って切り抜きました。

(この画像で黄色、薄緑、灰の3色の塗りつぶしがくりぬきを表しています)

この手法は、OpenSCADのプログラミングによる手軽な設計の良さと、KiCADで部品を見ながらくりぬき範囲を描画できるGUIの良さを両立させる手法だと感じています。

指をひっかけるための突起は、LibreCADやOpenSCADで頑張って設計しています。(ここはもうちょっとどうにかしたい)

まとめ

以前とほぼ同じ構成ですが、表面実装RakuChordの改良版を作りました。

やはり新しく作った個所で問題が起こりやすいことがわかりました。前回の物より演奏しやすくなって満足です。

格安昇圧モジュールは試してみたものの、今回の用途では性能不足であることが分かったのも収穫で、かつそのような問題があってもHT7750を使った昇圧回路を利用できるようにした「こんなこともあろうかと」の設計が役立って良かったです。

自作キーボード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つの世界を作るような楽しさがあり、うまくコンセプトにあったボードを設計できると何とも言えない達成感がありました。

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