はじめに
私は普段、業務では主に Web 開発をしています。
その一方で、プログラミングコミュニティー piyopiyo.ex では、AtomVM や ESP32 を使った組み込み開発にも取り組んでいます。
今回、AtomVM入門会に向けた準備として、AtomVM 上で Elixir から LovyanGFX を扱えるようにするためのライブラリ atomlgfx を書きました。
作り始めた当初、「色には RGB565 や RGB888 がある」「バイトオーダーにはビッグエンディアンとリトルエンディアンがある」くらいの理解はありましたが、正直に言うと、それぞれをきっちり理解できていたかというとそうではありませんでした。
その時に有耶無耶にしていたことが、あとで見事に色化けとして返ってきました。![]()
つくりたかったもの
今回やりたかったのは、Elixir から ESP32 に接続された液晶画面を操作することでした。
友人から、C++ で書かれた LovyanGFX が非常に高機能で良いと聞いていましたが、そのままでは Elixir から直接使えません。そこで、AtomVM から呼び出せる形に包んだものを用意し、Elixir 側から描画命令を送れるようにしました。
構成としては、だいたいこんな感じです。
Elixir コード
↓
AtomVMから呼び出すための橋渡し (C/C++)
↓
LovyanGFX (C++)
↓
ILI9488 LCD
C/C++の実装に慣れていないので簡単ではありませんでしたが、AIに相談しながら進めていくことができました。
ここまではよかったのですが、色の扱いを軽く見ていたのがよくありませんでした。
序盤の判断ミス
当初の私は、Elixir 側の利用者にとってやさしい API にすればよいのではないかと考え、AI に勧められるまま、一見使いやすそうな形に寄せて実装を進めていました。
たとえば、
「Elixir 側では RGB888 のような分かりやすい形だけを見せておけばよいのではないか」
「RGB565 やバイトオーダーのことは内部で何とか吸収すればよいのではないか」
という発想です。
しかし、その結果、システム全体で色の扱いに少しずつずれが生じ、実装が元の LovyanGFX の作法からも徐々に離れていきました。
そのため、コードベース全体の一貫性を保つのが難しくなり、利用者の側でも本家のサンプルコードの考え方をそのまま踏襲しにくくなっていました。
色化け
問題がはっきり見えたのは、友人が書いたスタックチャン風の C++ 実装を、自作の Elixir ラッパーを使って移植し始めたときでした。
私は C++ の LovyanGFX のコードを、ほぼそのまま Elixir に置き換えていきました。見た目は単純な描画なのですが、期待した色になりませんでした。
たとえば次のような具合です。
- 顔は黄色のはずなのに、シアン寄りに見える
- 汗は青のはずなのに、黒っぽく見える
- ハートはピンクのはずなのに、シアン寄りに見える
このときは、「どこかで色定数を間違えたのだろうか」くらいに思っていました。
しかし実際には、もっと根本のところで、
- 色をどういう形式で表しているか
- その値をどういうバイト列として扱っているか
- 元の LovyanGFX と自分の Elixir 側で前提が揃っているか
が噛み合っていませんでした。
まさかと思い、確認してみたら、コード上は問題ないのに画面上は想定していない色ばかりでした。
つまり、単なる色指定のミスではなく、色の意味と表現形式とバイト列の扱いがずれていた、ということです。
この体験が、自分の中で「色形式とエンディアンをちゃんと理解しないといけない」と思った決定的なきっかけでした。
しっかり考えながら構築したら、想定通りの色がでるようになりました。
色の話を 4 つの層に分けて整理
ここでの色の話は、次の 4 つの層に分けて考えてみるとわかりやすい氣がしています。
何色を表したいのか
↓
画素を何ビットで表すか
↓
その値をメモリ上でどう並べるか
↓
液晶へどの形式で送るか
何色を表したいのか
まずは「何色を表したいのか」という話です。
たとえば黄色なら、
R = 255, G = 255, B = 0
という意味です。
Web 開発では、普段はほぼこの層だけを意識していれば十分です。#ffff00 と書けば、あとは環境側がうまく処理してくれます。
画素をどう表すか
次は、その色を 画素として何ビットで表すか、という話です。
代表的なものとしては、次のような形式があります。
- RGB888
- RGB565
- RGB332
たとえば同じ黄色でも、
- RGB888 なら
0xFFFF00 - RGB565 なら
0xFFE0
のように表し方が変わります。
ここで大事なのは、同じ色でも表現形式が変われば値も変わる、ということです。
その値をメモリ上でどう並べるか
さらにその下の層では、画素値が実際にどのようなバイト列として並ぶかが問題になります。
RGB565 の 0xFFE0 という 16 ビット値を考えると、画素としての意味は同じでも、生のバイト列としては次のような違いがありえます。
FF E0
E0 FF
この違いが、pushImage のように生の画素列を扱う場面で効いてきます。
表示器へどう送るか
最後は、そのバイト列を液晶制御器へどのように送るか、という話です。
- 何ビット単位で送るのか
- どういう順で送るのか
- その表示器が何を期待しているのか
ここまで来ると、単なる「色の意味」の話ではなく、表示器との取り決めの話になります。
私が混同していたこと
今回の私の混乱は、次の 2 つを混同していたことにありました。
- 色の意味や画素形式の話
- 生のバイト列や転送順の話
たとえば「このデータは RGB565 です」と言っても、それだけでは十分ではありません。
それは、
- 16 ビットの画素形式が RGB565 である
という意味にすぎません。
一方で、生の画素列を扱うのであれば、
- その 16 ビット値を上位バイトから並べるのか
- 下位バイトから並べるのか
まで含めて考える必要があります。
この区別が曖昧だと、色化けにつながります。
最終的にどう設計を変えたか
最終的には、Elixir 側の API を元の LovyanGFX の考え方にできるだけ合わせる方針に戻しました。
理由は単純です。
独自に「分かりやすい API」を足していくと、一見便利でも次の問題が起きやすくなります。
- 元の C++ の例をそのまま移しにくい
- 利用者が別の學習をしなければならない
- 内部変換が増えて、前提の不一致が起きやすい
- 不具合の切り分けが難しくなる
元の API に寄せれば、少なくとも「LovyanGFX ではこうなっている」という共通の土台に乗れます。
結果として、その方が利用者にとっても分かりやすいと考えるようになりました。
今回學び直したこと
今回の経験で、私の中では次の点がはっきりしました。
- 色の話では、まず文脈を確認する必要がある
- RGB565 と RGB888 は、同じ色を別の形式で表したものとして捉える
- 生の画素列を扱うときは、バイト順まで確認する
- 元のライブラリの約束事から離れすぎると、翻訳しにくくなる
- 便利そうな抽象化が、かえって混乱を増やすことがある
おわりに
今回、AtomVM/Elixir から LovyanGFX を包む中で、色形式やエンディアンを學び直すことになりました。
最初は色の値だけ合わせればよいと思っていましたが、実際には、色の意味、画素の表現形式、メモリ上での並び、表示器への送り方を分けて考える必要がありました。
そこを曖昧にすると色化けし、整理すると LovyanGFX の設計も見えやすくなります。Web 開発の感覚だけでは見えにくかったことを、今回の組み込み開発で學べました。
![]()



