U8g2ライブラリで2つ目のI2Cを使おうとしたところ、write(): NULL TX buffer pointer
ESP32でU8g2ライブラリを使ってSH1106 OLEDディスプレイを使おうとしたところ、表題のエラーが発生してそこで停止してしまうようになりました。いわゆるヌルポですね。
発生条件 espressifのバージョン
どうやらespressifのバージョンを上げたタイミングで発生したようでした。platformio.ini
のplatform指定を下記にすると発生しません。
[env:esp32dev]
platform = espressif32@5.0.0
これを例えば6.8.1などに書き換えると発生します。おそらく5.1.0あたりまではOK、それより新しいバージョンではダメみたいです。確かに、エラーが発生する方のWire.h
ではwrite
やendTransmission
にtxtBuffer
という内部変数のNULLチェックが追加されており、そこで引っかかっているのが分かります。
とは言え、古いバージョンのespressifをずっと使うというのも心配です。
原因 TwoWireオブジェクトの重複
U8g2はArduinoであらゆるディスプレイを扱えるすごいライブラリなのですが初期化方法が非常にユニーク(歪曲表現)で、液晶(チップ)の種類、解像度、バッファリング方式、使用するI2CのチャンネルやSPIなどを組み合わせた百もあるコンストラクタから適切なものを探し出さなければ使えません選んで使います。
SH1106などのI2Cディスプレイの場合は、サフィックスが_SW_I2C
(ソフトウェアI2C)、_HW_I2C
(ハードウェアI2C)、_2ND_HW_I2C
(ハードウェアの2番目のI2C)のコンストラクタを使います。
今回自分はESP-WROOM-32をI2Cスレーブデバイスとして動作させつつ、自身でもI2C接続したSH1106を使いたかったので、スレーブデバイスとしての動作にはメインのハードウェアI2C(ESP32だとディフォルトではGPIO21と22)を使いつつ、セカンダリのI2C(ディフォルトでは)でSH1106を使いたかったので、U8G2_SH1106_128X64_NONAME_F_2ND_HW_I2C
メソッドを使っていました。
このコンストラクタだとU8g2のI2Cのメソッド呼び出しがWire
の代わりにWire1
に対して行われるようになります(U8x8lib.cpp
のu8x8_byte_arduino_2nd_hw_i2c
メソッド内)。
一方、自分の方ではセカンダリのI2Cで使うモジュールが他にもあるため、U8g2の初期化前にセカンダリのI2Cを初期化するコードを書いていました。ざっくり下記のような感じです。
// セカンダリのI2C(=1)を指定
TwoWire subI2C = TwoWire(1);
// SDA=GPIO32, SCL=GPIO33で初期化
subI2C.begin(32, 33, 400000UL);
これによりインスタンスとしては別でありながら、いずれもセカンダリのI2Cを指すTwoWire(1)
が2つ存在することになり、こちらで呼んだbegin
及びU8g2が呼ぶbegin
とTwoWire.begin
が2回呼び出されます。すると2回目のbegin
はすでに2番目のハードウェアI2Cに対しての初期化が済んでいるためエラーになり、バッファの確保等が行われません。
// Slave Begin
bool TwoWire::begin(uint8_t addr, int sdaPin, int sclPin, uint32_t frequency)
{
(中略)
end:
if (!started) freeWireBuffer();
これがそれ以降のWire.write
メソッドなどで表題の
〈NULL TX buffer pointer〉エラーに繋がってしまうようです。
解決方法 同じI2Cバスを示す別のTwoWireインスタンスを生成しない
というわけで原因がわかったので、同じバスに対してbegin
が2度呼ばれてしまわないよう修正します。具体的には先程のコードを、下記のように修正しました。
// Wire.hで用意されている規定の2個目のI2Cを指すTwoWireインスタンスを指すよう変更
TwoWire* subI2C = &Wire1;
// そのインスタンスに対してbeginを呼び出す
subI2C->begin(32, 33, 400000UL);
これでU8g2でのWire1
に対するbegin
が正しく失敗するようになり、バッファが確保されないままのTwoWireインスタンスが存在することがなくなり、ヌルポが出なくなります。
下記のスレッドなんかはおそらく同じ症状で悩んでる人の投稿ですね。
すでに初期化された i2c (wire) オブジェクトを使用した SSD1306 ディスプレイの使用 · olikraus/u8g2 · ディスカッション #1951 · GitHub
https://github.com/olikraus/u8g2/discussions/1951
というわけで、
戒め TwoWireオブジェクトを生成してはいけない
Wire.hで定義済みのWireまたはWire1を使うこと。
それにしてもU8g2のコンストラクタ選択方式はあまりに使いづらい…… なにかしらのメリットがあるんだろうとは思うけれども。
いじょ。
そういえばこれが今年最初の記事かも。もう3月だけどあけおめ!
この記事はここで終わりです。
読んでいただきありがとうございました。
良かったらシェアしてね!
That's all for this article. Thank you for your reading.
Please share this if you like it!