この記事を書くきっかけ
今回この記事のメインになるrequire機能に関して,このFWの作者であるはすみきんさんに以前より実装予定である旨を聞いていて,23/11/23に満を持して登場した機能を使って約1週間の突貫工事でOLED機能を実装したのでみんなに見てほしいというのが第一,
もうすこし高尚な理由としてははすみきんさんが昨年のアドベントカレンダーに書いている
prk_firmwareとは?
この記事を読んでいる人は恐らく大半が自作キーボード経験者,あるいは自作キーボードに興味がある方だと思っているので蛇足かもしれませんが,せっかくのアドベントカレンダー記事ですので,もっと広い範囲の方が来るかもしれない想定でできるだけこの記事単独で話の流れがわかるように書いていこうと思います.
「そんなこと知っとるわい!」という方はこの章は飛ばしてください
というわけでprk_firmwareとは何ぞやという話ですが,
「picorubyを用いた自作キーボードFWのフレームワーク」とgitのreadmeに記載があります.
自作キーボードを作成されたことがある方はご存じかと思いますが,自作キーボードはマイコンを搭載して,そこにマトリクス上に配置したスイッチからの入力を入れてあげることで作成します.
こうすると何がよいかというと,「このスイッチが押されたははAを入力したことにする!」という部分をマイコンが仲介してくれるので,キーの割り当て(アサイン)をSWの改変のみで自由に変更することができます.
逆に言えばマイコンにこのアサインを割り当ててあげたりするためのFWをマイコンに焼いてあげないと,ただの文鎮と化してしまうわけです.
現状有名なキーボード用のFWとしては
(qmkとprk以外触ったことがないので正直自分もこの辺詳しくないです.間違ってたら教えてください.)
- QMK Firmware
- ZMK Firmware
があると思います.
この他,これらのFWの使い勝手を向上するためのアプリケーションとしてRemapやVIAやVial(名前がややこしい)があると思っています.(いずれも使ったことない)
prk_firmwareの特徴
まずはざっくり列挙します.
- 言語がpicorubyである
- RP2040(要はraspberrypi pico)をターゲットデバイスとしている
- FWをインストールするとストレージとして認識されるようになる
- ストレージにキーマップが入っており,動的に編集できる
順に見ていきましょう.
言語がpicorubyである
当然と言えば当然です.というのもこのFWの作者はpicorubyの生みの親であるはすみきんさんです.
picorubyって何?って話になるかもしれませんが,あんまり詳しく書くとこの記事の文量がとんでもないことになるので,ここではざっくりと「rubyという開発言語をマイコンでも使用できるように軽量に実装したもの」くらいの説明にとどめておきます.
RP2040(要はraspberrypi pico)をターゲットデバイスとしている
最近?というにはちょっと前すぎるような気もしますが流行りのraspi picoを使ってキーボードを作れるということで,敷居が若干低くなっている効果はあると思います.ただし個人的に参入障壁が一番低くなっている部分は次とその次の要素によるものだと思っています.
FWをインストールするとストレージとして認識されるようになる
これはpicorubyの特徴というよりはraspiもそうなっているので,という話ではありますが,とりあえず購入した状態でUSBに繋いでPCに接続するとストレージとして認識されて,そこにFWを入れると書き込まれるというのが非常に扱いやすいです.
参入障壁がかなり低いと思われるarduinoでもアプリを入れて,そこからCOMポートで接続してプログラムを書き込むというのが必要で,そのような経験が全くない人からすると結構大変なことだと思います.
その点prk_firmwareもraspi picoも既にコンパイルされたFWを焼くだけなら,あるいはキーマップを入れるだけなら全く何も必要としないところは初心者にオススメする十分な要素たりうると思っています.
ストレージにキーマップが入っており,動的に編集できる
これも上記とほぼ同じことですが,QMKなどでは動的にキーマップを変更するのが難しいというのが古参的にはありました.今はVialだのRemapだのがありますのでずいぶん楽になっているのかもしれませんが個人的にはprkの方が扱いやすくない?と思っています.
こればっかりは個人の感想ですのでこれ以上追求するのはやめておきます.
追加された`require`機能について
ようやっと本題にたどり着きそうですが,既に3000近く文字を書いていることに自分自身が一番驚いています.
さておき,今回の記事で最もキーになる追加機能,require機能について説明していきます.
とは言っても詳しい内容はprk_firmwareのwikiに記載がありますのでそちらをご覧いただくのがよいと思います.
キーボードの自作の記事でも書きましたが,要は
というお話です.
まさしくはすみきんさんが昨年おっしゃられていた部分だと思っています.
FWというのは基本的にはいろんなプラットフォームの上で動作するはずで,ここではそれが個々のキーボードにあたるわけです.
共通した部分はFW上に盛り込まれてもいい,むしろ盛り込まれているべきですが,例えばキーボードの場合,トラックボール,トラックパッド,トラックポイント,ジョイスティックなどのポインティングデバイスに始まり,ソレノイド,スピーカーなどの音で伝えるデバイス,振動子,そして今回の話題であるOLEDなど,実に多様なデバイスが載るかもしれないし,載らないかもしれません.
ただでさえリソースが厳しいマイコンの上に,これらの使うかもわからない機能の情報が載っていても大概のものは無駄になると言ってよいでしょう.
それであれば,どのデバイスでも使う低レイヤな機能だけをFWに実装し,各々あとから追加できる方が自由度が高く,無駄のない作り方にできます.
また,このようにしておくとスクリプトの見通しがよくなるので,結果的にバグなどの早期発見につながり,ユーザーみんなハッピーになれるわけです.
余談①
生態系に例えると,上流をきれいにしておけば,下流には多様な生物が棲めるようになるでしょうし,同時に何か毒物等が混入したときに,どこからそれが流れてきたのか追いやすいのです.それと同じです.
余談②
PCのOSやスマホも,後から好みのアプリを入れて自分好みの仕様にすると思います.この世に数えきれないくらいあるアプリが全部あらかじめスマホにインストールされていたら発狂ものです.同じです.
OLED機能の実装
え,4000文字近く書いてるんですが…(ドン引き)
ようやく本題の本題です.
ここではOLEDを制御するためのrequire機能を用いた実装について説明します.
requireするための準備(前提条件)
まず,https://github.com/picoruby/prk_firmware/archive/refs/tags/0.9.23.zip からFWをダウンロードしてきてpicoに書き込むと,(書き込み方はwikiを参照してください.)
ディレクトリ構成としてはこんな感じになっていると思います.
├README.txt
└lib/
├README.txt
├keymap.rb
└lib
└ssd1306.rb
ssd1306.rbの中身
# ssd1306.rb require "i2c" class SSD1306 def initialize(unit_name:, freq:, sda:, scl:) @i2c = I2C.new(unit: unit_name, frequency: freq, sda_pin: sda, scl_pin: scl) # initialize @i2c.write(0x3C, [0b10000000, 0x00]) @i2c.write(0x3C, [0b00000000, 0xAE]) @i2c.write(0x3C, [0b00000000, 0xA8, 0x3F]) @i2c.write(0x3C, [0b10000000, 0x40]) @i2c.write(0x3C, [0b10000000, 0xA1]) @i2c.write(0x3C, [0b10000000, 0xC8]) @i2c.write(0x3C, [0b00000000, 0xDA, 0x12]) @i2c.write(0x3C, [0b00000000, 0x81, 0xFF]) @i2c.write(0x3C, [0b10000000, 0xA4]) @i2c.write(0x3C, [0b00000000, 0xA6]) @i2c.write(0x3C, [0b00000000, 0xD5, 0x80]) @i2c.write(0x3C, [0b00000000, 0x20, 0x10]) @i2c.write(0x3C, [0b00000000, 0x21, 0x00, 0x7F]) @i2c.write(0x3C, [0b00000000, 0x22, 0x00, 0x07]) @i2c.write(0x3C, [0b00000000, 0x8D, 0x14]) @i2c.write(0x3C, [0b10000000, 0xAF]) end def all_clear() i=0 while i<8 do @i2c.write(0x3C, [0b10000000, 0xB0 | i]) j=0 while j<128 do @i2c.write(0x3C, [0x00, 0x21, 0x00 | j, 0x00 | j+1]) @i2c.write(0x3C, [0b01000000, 0x55]) j=j+1 end i=i+1 end end def all_white() i=0 while i<8 do @i2c.write(0x3C, [0b10000000, 0xB0 | i]) j=0 while j<128 do @i2c.write(0x3C, [0x00, 0x21, 0x00 | j, 0x00 | j+1]) @i2c.write(0x3C, [0b01000000, 0xFF]) j=j+1 end i=i+1 end end def draw_all(pic:) k=0 while k<128*8 do i = k / 128 j = k-i*128 @i2c.write(0x3C, [0b10000000, 0xB0 | i]) @i2c.write(0x3C, [0x00, 0x21, 0x00 | j, 0x00 | j+1]) @i2c.write(0x3C, [0b01000000, pic[k].bytes[0]]) k=k+1 end end end
requireという機能自体はrubyという言語に含まれている内容で,picorubyにも元々FW内に組み込まれている関数を使用する場合には利用できていたものですので,仕組みや使い方の詳細に関してはそちらを参考にして頂けますと幸いです.
rubyはオブジェクト指向言語なのでrequire自体はある意味必須機能とも言えます.
というわけでこのスクリプト内ではこのOLEDを制御するための一連の定数や関数を集めたクラスを定義して,それをkeymap.rbで呼び出すというのが筋の良い形になると思います.
実際に上記のスクリプトでもSSD1306というクラスを定義しています.
このスクリプトはまだ試作段階なので,非常にシンプルな関数しか用意しておらず,またその関数ももう少し引数を増やして設定できる項目を増やしてあげるべきです.具体例は後程記載します.
とは言ってももうすでに6400字に到達しているので,ここから当記事内でSSD1306の個別な仕様を細かく説明しだすととんでもないことになります.なのでその辺に関しては私が参考に読んでいたブログとデータシートのリンクでお茶を濁すことにします.
https://cdn-shop.adafruit.com/datasheets/SSD1306.pdf
initialize関数の具体的な中身
さて,さすがに一つくらい具体的な話はしておきたいので,一点.initialize関数の詳細を見てみましょう.
def initialize(unit_name:, freq:, sda:, scl:) @i2c = I2C.new(unit: unit_name, frequency: freq, sda_pin: sda, scl_pin: scl) # initialize @i2c.write(0x3C, [0b10000000, 0x00]) @i2c.write(0x3C, [0b00000000, 0xAE]) @i2c.write(0x3C, [0b00000000, 0xA8, 0x3F]) @i2c.write(0x3C, [0b10000000, 0x40]) @i2c.write(0x3C, [0b10000000, 0xA1]) @i2c.write(0x3C, [0b10000000, 0xC8]) @i2c.write(0x3C, [0b00000000, 0xDA, 0x12]) @i2c.write(0x3C, [0b00000000, 0x81, 0xFF]) @i2c.write(0x3C, [0b10000000, 0xA4]) @i2c.write(0x3C, [0b00000000, 0xA6]) @i2c.write(0x3C, [0b00000000, 0xD5, 0x80]) @i2c.write(0x3C, [0b00000000, 0x20, 0x10]) @i2c.write(0x3C, [0b00000000, 0x21, 0x00, 0x7F]) @i2c.write(0x3C, [0b00000000, 0x22, 0x00, 0x07]) @i2c.write(0x3C, [0b00000000, 0x8D, 0x14]) @i2c.write(0x3C, [0b10000000, 0xAF]) end
ここでは,
- i2cクラスのインスタンス作成
- SSD1306の初期化を行うためのi2c通信
これしかやっていません.
1.は1行目のみで完結しています.これはi2cの低レイヤ部分をクラスとしてまとめてくれているおかげで,実際スクリプト全体の最初の方にrequire i2c の記述があると思います.これは私が実装して外部からrequireしているわけではないので,今回のver以前でも使えていた機能です.
というわけで本題は2の処理です.initialize関数関数の大半の中身はこれで,@i2cから始まる行がそれぞれデータを送信しています.
再掲になりますが,詳細全部に関しては
をご覧頂ければと思います.
先ほど機能が足りていないと話しましたが,具体例としては例えば9行目の
@i2c.write(0x3C, [0b10000000, 0xA1])
は,水平方向の向きを決めることができます.OLEDは設置方向がデバイスによって様々ですので,ここは機種の状況によって動的に変更できるのが望ましいでしょう.
今は反転させている設定になります.反転させたくない場合は0xA1を0xA0にすればよいです.こういわれると実装できそうですよね?
(※わかってるならやれよという話ですが,現状これを記述する意味は私にとってはないのです.ちゃんと他の部分も含めて徐々に整備するつもりですのでご容赦を.)
勿論縦方向の反転も設定があります.その辺は必要に応じてデータシートを読み込みながら,必要な情報をSSD1306に送れるようにスクリプトを作成していくのです.
その他の関数について
もう分量がとんでもないことになっている(8500文字)のと,OLEDの処理の話なのでちょっと話がrequireから逸れてしまう都合もあり最低限にとどめます.
all_clear関数はすべてのドットを黒に(光ってない状態)に塗り替えるということをしています.
all_white関数はその逆ですね.
draw_all関数は引数に取ったstringを1バイトずつ拾ってきて16進数の数字に直して送信しています.
このやり方できれいに狙った画像が出るように,string配列を作ってくれるスクリプトをpythonで別途書いてます.その辺はgitのreadmeで説明する予定ですのでここでは割愛します.
まとめ
とんでもなく長くなってしまいましたが,自作のスクリプトをrequireできることのメリットとその実例について,OLEDを例に挙げて説明してみました.
prk_firmwareを採用する理由として,RP2040が使えるからという理由で使っている方もいらっしゃるかもしれませんが,個人的にはオブジェクト指向言語で記述でき,しかもコンパイル不要(厳密には起動時に動的にコンパイルして動かしているのでユーザーが意識する必要がない)という点が最も大きなメリットだと思っています.
今回の外部requireはその点において最も大きなブレイクスルーと言っても過言ではないと思っていて,これを機にどんなマイナーなHWを採用しても,FW本体のコードを必要以上に荒らすことなく実装でき,またその成果物を必要な人に還元する土壌ができたと思っています.
これを機に変態キーボード自作への第一歩を踏み出してみてはいかがでしょうか?
何度でも再掲しますが,現在個人的には
コレ↓積むとか
CO2センサ積むかとか
ちっこいCO2センサをキーボードに内蔵して、部屋の換気を忘れて作業していてぼーっとしてきた頃にキーボードが換気しなさいって教えてくれるようにしたいと換気のたびに思います🙂https://t.co/opErx0tkh8
— TALPKEYBOARD (@TalpKeyboard) December 1, 2023
非常に期待していますので,この機能を活用して是非新たな扉を一緒に開いていきましょう!
ではでは.
コメント