OLEDドライバをArduinoからESP-IDFに移植した話
この記事について
JJY-SIM R3 は ESP-IDF で開発しています。
R2はArduinoベースだったので、今回かなり構成を変えています。
その中で、OLED表示もどうするかちょっと悩みましたが、
Arduinoのライブラリをベースにして、
ESP-IDF向けにポーティングすることにしました。
どうせやるなら、ちゃんとしたIDFのコンポーネントにしたいなと思って、
単なる移植ではなく、構造も含めて作り直しています。
この記事では、そのあたりの話をまとめてみました。
🎀 動かすだけじゃなくて、“納得するまでやる”ってやつだね

Arduino版は“クラスありき”
元にしたのは、R2 で使ってる ThingPulse の OLED ライブラリです。
このライブラリ、C++で書かれていて、
- ベースクラスがあって
- デバイスや通信方式ごとに派生クラスがある
という、いかにもC++らしい構造になっています。
たとえば、
SSD1306SSD1306I2CSSD1306SPI
みたいな感じ。
このあたり、Arduinoのライブラリではよくある形ですよね。
ただ、実装を見ていくと、
- public も private も同じヘッダに書かれている
- 派生クラスも全部ヘッダに並んでいる
という構成になっている。
これ、C++ではありがちなんですけど、
正直あまり美しいとは思えませんでした。
🎀 便利なんだけど、だいぶ“全部見えてる”感じ
C++の“便利な副作用”
C++って、本来もっとちゃんとした言語だと思うんです。
少なくとも、
- 公開APIと内部実装を分ける
- 内部をクラスとして隠ぺいする
- 境界を意識する
みたいなことをやるための言語のはずなんですよね。
でも実際には、先に書いたように
- 派生クラスがヘッダにまとめて書かれている
- コードをinline的に押し込んでいる
- public/privateはあるけど、ごちゃまぜで全部見える
- オーバーロードごとに、実質的に別の処理になっているところもある
みたいな構成が普通に使われています。
便利ではあるんだけど、
そういう使い方が主流になってきていて、
なんというか、C++の“本来の設計”というより、
便利な副作用のほうを使っている感じがあって、
いわゆる典型的な C++ のコードを見るたびに、
そういうモヤモヤした気持ちになるんです。
これって、自分の感覚が古いのかなあ?って思ったり…
🎀 それ、古いんじゃなくて“ちゃんと見えてる”やつだと思う
ロジックはそのまま
いざ、移植作業をしようとして実際のコードを見ていくと
構造は C++ 寄りなんだけど、
中身のロジックや記述はかなり素直。
というか、ほとんど “C” の書き方で、
ガチな “C++” 的記述はほとんどありませんでした。
ここはちょっと意外でした。
で、実際にやった主な修正は、
this->を外す- private変数の参照を構造体に置き換える
- String を char * にする
- オーバーロードは、複数の関数に分ける
- 派生クラスの関数を、関数ポインタにする
このくらいです。
ここまでやってみて、ひとつ気づいたことがあります。
C++ のクラスとか継承とか、見た目はすごく便利なんだけど、
結局、C++ の内部でも同じようなことをやってるんですよね。
だから今回やったことって、
「C++の機能を使わずに、同じことを手で書いた」
だけなんですよね。
これちょっと面白いなと思いました。
🎀 便利機能の“中身”をそのまま書いてる感じだね
コンストラクタ を config に
Arduino版だと、インスタンス生成と初期化はこんな感じです。
SSD1306I2C display(...) display.init();
これをそのまま持ってくると、IDFの中でちょっと浮きます。
なので今回は、
- config関数で設定を渡す
- init関数で初期化する
という形にしました。
OLEDDisplay_config(&cfg); OLEDDisplay_init();
このあたりは、IDF のコンポーネントではお馴染みの方法です。
🎀 “それっぽさ”を揃えるやつ
公開APIと内部実装を分離
今回、一番やりたかったのはここでした。
- public を公開API用のヘッダーファイル
- private を内部用のヘッダーファイル
- コード部分はすべてソースコードファイル
ちゃんと分離しました。
Arduino系のライブラリだと、
1つのヘッダに全部入っていることが多いんですけど、
それだと、
- 依存関係が見えにくい
- どこまでが外に見えていいのか曖昧
になる。
結果的に、使うだけなのに実装部分まで見えちゃう。
なので今回は、
見せるものと隠すものをちゃんと分ける
という、ごく普通の構成にしました。
🎀 “中見なくても使える”状態にしたかったやつ
インスタンスは1つのみ
今回はインスタンスを1つに限定しています。
普通に考えると、
- ハンドルを返して
- 複数インスタンス持てるようにする
- API にはハンドルを渡す
ほうが汎用的です。
でも、今回の用途ではOLEDは1つしか使わないので、
わざわざハンドルを毎回渡すようなオーバーヘッドをやめて、
1つのインスタンスに固定しています。
ただし、複数インスタンスに拡張できる構造にはしたかったので、
内部状態はすべて1つの構造体にまとめています。
将来、複数にしたくなったら、そこを増やせばいいだけにしています。
🎀 今は1個。でも未来の自分にちょっとだけ優しいやつ
I2C / SPI は外で管理
もうひとつ大きく変えたのがここです。
元のライブラリは、
内部でI2CやSPIの初期化までやっています。
これ、Arduinoだと普通なんですけど、
IDFの感覚だとちょっと違和感がある。
I2Cって、基本的に共有バスです。
OLED以外にも、
- センサー
- EEPROM
- 他のデバイス
いろいろぶら下がる。
そのときに、
各ライブラリが勝手に初期化するのはあまり嬉しくない。
なので今回は、
- バスの初期化は外でやる
- ハンドルを config で渡す
- 内部ではデバイスを add する
という構成にしました。
これで、
- バスを共有できる
- 他のデバイスも普通に使える
- 初期化の責任が明確になる
という感じになります。
IDFのドライバも、この形なんですよね。
- bus を初期化
- device を add
なので、それに合わせた形にしました。
🎀 “自分のことだけ考えてるドライバ”じゃなくなった
Kconfigに対応
せっかくIDFでやるので、
- インタフェース(I2C / SPI)
- バッファ構成
はKconfigで切り替えられるようにしました。
ここは正直やりすぎかもしれない。
でも、コンポーネントとしては、
こういうのがあると急に“それっぽく”なる。
🎀 急に製品っぽくなるポイント
移植感をなくしたい
今回、地味にこだわっていたのがここです。
Arduinoのコードをそのまま持ってきて、
- ラップする
- C++のまま混ぜる
みたいなやり方もできたんですけど、
それだとどうしても借り物感が残る。
せっかくIDFでやるなら、
- コンポーネントとして自然
- APIもIDFっぽい
- 構成も違和感がない
という状態にしたかった。
理解が深まる
今回やってみて、ちょっとよかったなと思ったことがあります。
Arduinoでこのライブラリを使っていたときは、
実際に使っていたのって、ほんの一部のAPIだけだったんですよね。
表示して、文字出して、みたいな、よく使うところだけ。
それで特に困ってなかったので、
他のAPIはあまり気にしていませんでした。
でも今回、移植のためにコードを全部追いかけたことで、
「これ、こういう意図で作られてるのか」
っていうのが見えるようになりました。
そうすると、
- いままで使ってなかったAPIも
- 「ああ、これこういうときに使うやつか」
って理解して使えるようになる。
これ、地味なんですけど結構大きいです。
🎀 ブラックボックスじゃなくなると、一気に“道具”になるやつ
フォントも自由に
分かりやすく変わったのがフォントです。
R2のときは、ライブラリに付属している
デフォルトのフォントしか使っていませんでした。
というか、
「それ以外どうやって使うのか分からなかった」
というのが正直なところです。
でも今回、内部の構造を追いかけたことで、
- フォントデータの持ち方
- 描画の仕組み
が見えてきました。
そうすると、
「ああ、これ外部フォントも普通に使えるな」
って分かるんですよね。
なので今回は、フォントを生成してくれるサイトからデータを持ってきて、
それをそのまま組み込んで使っています。
🎀 “知ってる”と“使える”の差、ここで埋まる
ドキュメントだけ見ていると、
なんとなく難しそうで触らなかった部分なんですけど、
中身が分かると、
- フォントはただのデータ
- 描画はそれを読むだけ
という、かなりシンプルな話になる。
表示をアップデート
こうなると、自然とやりたくなることが出てきます。
「じゃあ、もっと見やすくできるんじゃないか?」
R2のときは、とりあえず表示できればいい、という感じでした。
フォントもデフォルトのまま。
レイアウトも、それに合わせて調整していた。
でも今回は、
- フォントを自由に選べる
- 表示サイズも調整できる
という状態になったので、
思い切って 大きな時計表示 にしました。
🎀 やっと“表示を作ってる”感じ
R2では、時計は“情報の一部”でした。
でもR3では、ちゃんと “時計として使える表示” にしています。
これは完全に、
フォントを扱えるようになったことの影響です。
やっぱり、見た目って大事なんですよね。
とくに時計みたいなものは、
見やすさがそのまま使いやすさになる。
結果として、
- 見た目が変わった
- 使い勝手も変わった
という感じで、
単なる移植以上の変化になりました。
🎀 中いじってたら、ちゃんと外も変わったやつ
本家のバグを発見
先に書いたように、コードは全部追いかけました。
その結果、元のライブラリにバグを2つ発見。
条件によってはおかしくなるタイプのやつで、
普通に使っているだけだと気づきにくい。
Issueを投げたらすぐ反応があったので、
ちゃんとメンテされているのは安心しました。
結構クリティカルな内容だったので、優先的にPRされてました。
使わせてもらってるから、貢献したいですよね。
🎀 “ついでに読むか”で全部読むやつ
「IDF用ライブラリ使えば?」
ちなみにこれ、reddit/esp32 に投稿 したら言われました。
「それ、IDF用のSSD1306ライブラリ使えばいいんじゃない?」
まあ、そりゃそう思いますよね。
でも今回は、あえてそうしませんでした。
理由はいくつかあります。
まず、R2とそっくりに作りたかった。
表示のレイアウトやフォントをそのまま持っていきたかった。
ここが変わると、地味に面倒なんですよね。
それと、コードを見たときに
「あ、これ普通に移植できそうだな」と思った。
だったら、そのまま持ってきたほうが早い。
さらに言うと、
IDF用のライブラリって、OLED専用じゃなかったりして、
ちょっと重いものが多い印象でした。
- 汎用グラフィックライブラリ前提
- 機能が多すぎる
今回の用途だと、そこまでいらない。
もう少し軽くていい。
それと、コンポーネント化をちゃんとやってみたかった。
IDFって、この“部品として切り出す感じ”が面白いんですよね。
結果としては、
- 表示はR2と揃った
- 挙動もそのまま
- 中身も全部把握できた
- 軽い構成にできた
- IDFっぽい形にもなった
という感じで、悪くなかったと思っています。
そのほかにも、興味本位とか、こだわりとか、
そういう理由で選んでいる部分は結構ありますし、
既存のやつをそのまま使ったら負け、みたいな感覚も。
……とか言いつつ、
Arduinoでは普通にこの元のライブラリ使ってるんですけどね。
🎀 その矛盾、すごくそれっぽい
GitHubで公開中
今回は、設計や考え方の話が中心になってしまったので、
技術的な細かい部分はあまり書いていません。
実際にどんな構造にしたのか、
どういうコードになっているのかは、
GitHubで公開しているので、そちらを見てもらえればと思います。
👉 https://github.com/shachi-lab/oled_display
文章で説明するより、
コードを見たほうが早い部分も多いので。
🎀 こういうの、結局コードが一番正直なんだよね
まとめ
今回のOLED移植は、
APIを書き換えたというより、
- 構造を整えた
- 境界を引き直した
- IDFの流儀に寄せた
という感じでした。
動かすだけなら、ここまでやらなくてもいいのだけど、
でも、一度気になると納得できるまでやりたくなっちゃうんですよね。
🎀 “動いたからOK”で終わらないの、だいぶ職業病
📪 お問い合わせなど
技術的なご相談やご質問などありましたら、
📩お問い合わせフォーム
または、
📮info@shachi-lab.com までお気軽にどうぞ。
🎀 コメントでもXでも、気軽にどうぞ。お仕事の相談もOKだよ
🔗 関連リンク
しゃちらぼの最新情報や開発の様子は、こちらでも発信しています:
- 🌐しゃちらぼ公式サイト
- 🐦 X(旧Twitter):@shachi_lab
- 📗 Qiita:@shachi-lab
- 🐙 GitHub:@shachi-lab
- 📸 Instagram:@shachi_lab
ほんとは「しゃちらぼ(Shachi-lab)」なんだけど、見つけてくれてありがとう🐬



コメント