見出し画像

2023年にハマったあれこれについて


新しい一年を過ごす上での注意喚起として、昨年中にハマってしまった事案を幾つか挙げていきたいと思います。

マルチゾーン対応ToF測距センサーがESP32では動かない

ハマり度: 中


そもそも、ToF(Time-of-Flight) 距離センサーを利用されているでしょうか?
距離を測ることで検出対象の量や、映像で検出したオブジェクトの大きさを推定するのに便利に使えるため重宝しています。

ある案件で水位を計測することになり、ToF距離センサーの出番となりました。せっかくなので、点ではなく面で計測した方が精度が出るだろうと考え、STのマルチゾーン対応ToF測距センサーを選びました。

Breakout board

作業当時は VL53L5CX が手近なところで欠品となっていたので、Mouser で購入することとなり、であれば新しいもと考えて VL53L7CX, VL53L8CX の二種類を選びました。この判断がハマりへの布石が打たれた瞬間でした。

Software

ST のマイコンである STM32 用の Arduino 環境である STM32duino 用コードは、それぞれ以下となっています。

ESP32 では動作しない

上記リンク先はあくまでも STM32duino 用のものなので ESP32 で動く保証は特段ないのですが、いずれのセンサーも I2C デバイスでしかないので動かない理由はありません。しかし、作業当時のリポジトリにコミットされていたコードは、ESP32 ではピクリとも動きませんでした(現時点では ESP32 でも問題なく動作するように修正がされています)。
STM32 の評価ボードなどが手元になかったので、別件で利用していた Arduino MKR WiFi 1010 を利用したところデモサンプルが以下のように動作したため、ESP32 側の何かがダメなのだという判断になり調査を始めました。

53L7A1 Simple Ranging demo application
--------------------------------------

Use the following keys to control application
 'r' : change resolution
 's' : enable signal and ambient
 'c' : clear screen

Cell Format :

        Distance [mm] :               Status


 ----------------- ----------------- ----------------- -----------------
|                 |                 |                 |                 |
|  1195  :     10 |  2291  :      5 |  2287  :      5 |  2278  :      5 |
 ----------------- ----------------- ----------------- -----------------
|                 |                 |                 |                 |
|  1187  :      5 |  2292  :      5 |  2334  :      5 |   292  :      5 |
 ----------------- ----------------- ----------------- -----------------
|                 |                 |                 |                 |
|  1085  :      9 |   133  :      5 |  2256  :      5 |   246  :      5 |
 ----------------- ----------------- ----------------- -----------------
|                 |                 |                 |                 |
|   925  :      5 |    31  :      5 |    41  :      5 |   220  :      5 |
 ----------------- ----------------- ----------------- -----------------

動作しない原因

結論から言うと、 src/platform.cpp の VL53L{7,8}CX::WrMulti における書き込みでの「endTransmission(false)」が問題でした。

uint8_t VL53L7CX::WrMulti(
  VL53L7CX_Platform *p_platform,
  uint16_t RegisterAddress,
  uint8_t *p_values,
  uint32_t size)
{
  uint32_t i = 0;
  uint8_t buffer[2];

  while (i < size) {
    // If still more than DEFAULT_I2C_BUFFER_LEN bytes to go, DEFAULT_I2C_BUFFER_LEN,
    // else the remaining number of bytes
    size_t current_write_size = (size - i > DEFAULT_I2C_BUFFER_LEN ? DEFAULT_I2C_BUFFER_LEN : size - i);
    p_platform->dev_i2c->beginTransmission((uint8_t)((p_platform->address >> 1) & 0x7F));

    // Target register address for transfer
    buffer[0] = (uint8_t)((RegisterAddress + i) >> 8);
    buffer[1] = (uint8_t)((RegisterAddress + i) & 0xFF);
    p_platform->dev_i2c->write(buffer, 2);
    if (p_platform->dev_i2c->write(p_values + i, current_write_size) == 0) {
      return 1;
    } else {
      i += current_write_size;
      if (size - i) {
        // Flush buffer but do not send stop bit so we can keep going
        p_platform->dev_i2c->endTransmission(false);
      }
    }
  }
  return p_platform->dev_i2c->endTransmission(true);
}

I2Cデバイスである VL53L{7,8}CX にデータを書き込む際、データ長がDEFAULT_I2C_BUFFER_LENより長いと以下のような流れになります。

beginTransmission()
write()
write()
endTransmission(false)
// 以下 beginTransmission(), 書き込み, endTransmission(false) が必要なだけ繰り返される
beginTransmission()
write()
write()
endTransmission(true)

動かない ESP32 の beginTransmission, endTransmission の実装を見ると以下のようになっていて、「endTransmission(false) -> beginTransmission()」という流れでは txLength が都度 0 になりデータが送られません。

void TwoWire::beginTransmission(uint16_t address)
{
    if(is_slave){
        log_e("Bus is in Slave Mode");
        return;
    }
#if !CONFIG_DISABLE_HAL_LOCKS
    if(nonStop && nonStopTask == xTaskGetCurrentTaskHandle()){
        log_e("Unfinished Repeated Start transaction! Expected requestFrom, not beginTransmission! Clearing...");
        //release lock
        xSemaphoreGive(lock);
    }
    //acquire lock
    if(lock == NULL || xSemaphoreTake(lock, portMAX_DELAY) != pdTRUE){
        log_e("could not acquire lock");
        return;
    }
#endif
    nonStop = false;
    txAddress = address;
    txLength = 0;
}
uint8_t TwoWire::endTransmission(bool sendStop)
{
    if(is_slave){
        log_e("Bus is in Slave Mode");
        return 4;
    }
    if (txBuffer == NULL){
        log_e("NULL TX buffer pointer");
        return 4;
    }
    esp_err_t err = ESP_OK;
    if(sendStop){
        err = i2cWrite(num, txAddress, txBuffer, txLength, _timeOutMillis);
#if !CONFIG_DISABLE_HAL_LOCKS
        //release lock
        xSemaphoreGive(lock);
#endif
    } else {
        //mark as non-stop
        nonStop = true;
#if !CONFIG_DISABLE_HAL_LOCKS
        nonStopTask = xTaskGetCurrentTaskHandle();
#endif
    }
    switch(err){
        case ESP_OK: return 0;
        case ESP_FAIL: return 2;
        case ESP_ERR_TIMEOUT: return 5;
        default: break;
    }
    return 4;
}

結局、ESP32 の「endTransmission(false)」はI2C デバイスからの読み出しで利用される以下のようなパターンでの利用しか考慮されていません。

beginTransmission()
write()
endTransmission(false)
requestFrom()
read()

ESP32 以外では動作していること、長いデータをバスを開放せずに書き込むために endTransmission(false) を利用しているはずなので、VL53L{7,8}CX::WrMulti 側を修正するのではなく DEFAULT_I2C_BUFFER_LEN の値を大きくし、かつ、事前に Wire::setBuffer() を呼び出して内部バッファーを長くして回避しました。

ところが、この方法だと VL53L{7,8}CX::RdMulti 側の考慮不足に遭遇して、さらにハマりました。具体的には else 側で requestFrom()で要求したデータ長が常に読み出せる期待している部分です。
DEFAULT_I2C_BUFFER_LEN が小さいと問題がないことがほとんどなのですが、WrMulti 側の対処で DEFAULT_I2C_BUFFER_LEN を大きくしたので else 側コードが動く機会が多く事象の発生頻度が上がった訳です。

uint8_t VL53L7CX::RdMulti(
  VL53L7CX_Platform *p_platform,
  uint16_t RegisterAddress,
  uint8_t *p_values,
  uint32_t size)
{
  ....
  ....
  uint32_t i = 0;
  if (size > DEFAULT_I2C_BUFFER_LEN) {
    while (i < size) {
      // If still more than DEFAULT_I2C_BUFFER_LEN bytes to go, DEFAULT_I2C_BUFFER_LEN,
      // else the remaining number of bytes
      uint8_t current_read_size = (size - i > DEFAULT_I2C_BUFFER_LEN ? DEFAULT_I2C_BUFFER_LEN : size - i);
      p_platform->dev_i2c->requestFrom(((uint8_t)((p_platform->address >> 1) & 0x7F)),
                                       current_read_size);
      while (p_platform->dev_i2c->available()) {
        p_values[i] = p_platform->dev_i2c->read();
        i++;
      }
    }
  } else {
    p_platform->dev_i2c->requestFrom(((uint8_t)((p_platform->address >> 1) & 0x7F)), size);
    while (p_platform->dev_i2c->available()) {
      p_values[i] = p_platform->dev_i2c->read();
      i++;
    }
  }

  return i != size;
}

現在のコード

VL53L{7,8}CX::WrMulti で分割書き込みをする際「endTransmission(false)」ではなく「endTransmission(true)」として対応されています。
データ書き込み中はバスを開放しないという考え方をやめ、ESP32 の問題のある実装に寄せられた処置となってしまったようです。

ESP32のWi-FiがステルスSSIDの環境ではつながらない

ハマり度: 小


テスト中はゲスト向けに SSID が公開されていたアクセスポイントへ接続し、稼働しているコードがありました。

実際の運用では、ゲスト向けアクセスポイントにつなぐことは芳しくないため、件の環境のネットワーク管理者に相談したところ IoT 機器などを接続するための Wi-Fi が別途用意されているとのこと。
ただ敢えて公開する必然性がないという理由でステルスSSIDで運用されていました。

ステルスSSIDは未対応なのか

早速、それようのSSIDにを変えてみたところ、つながりません。ググってもステルスSSIDは未対応というような情報は一切ありませんでしたので、調査です。

接続できないのは当たりまえ

利用していたコードは複数のアクセスポイントに対応できるようにと
WiFi クラスではなく WiFiMulti クラス側を使っていたのですが、これの run メソッドの実装を確認です。
分かる人であれば、WiFiMulti という時点でダメなのはすぐに気がつくと思います。

 何のことはありません、WiFiMulti::run は複数のアクセスポイント(SSID) に対応するため内部ではスキャンを行い、指定された SSID が見つかれば接続を行うという実装なのです。

uint8_t WiFiMulti::run(uint32_t connectTimeout)
{
    int8_t scanResult;
    ....
    ....

    scanResult = WiFi.scanNetworks();
    if(scanResult == WIFI_SCAN_RUNNING) {
        // scan is running
        return WL_NO_SSID_AVAIL;
    } else if(scanResult >= 0) {
        // scan done analyze
        WifiAPlist_t bestNetwork { NULL, NULL };
        int bestNetworkDb = INT_MIN;
        uint8_t bestBSSID[6];
        int32_t bestChannel = 0;

        log_i("[WIFI] scan done");

        ....
        ....
    }
    ....
    ....
}

ステルス SSID ですからスキャンしても見つかるわけはなく、接続できなかったというオチでした。

WiFiMulti ではなく WiFi クラス側を使ったところ、ステルス SSID でも問題なく接続し利用できました。

ESP32C3のDeepSleepがきちんと機能しない

ハマり度: 大


Breakout board

ESP32 C3 はおなじみの Tensilica の Xtensa LX6 ではなく RISC-V  です。
入手しやすい Breakout board は何種類かありますが、必要最小限でコンパクトな Seeed Studio の XIAO シリーズのものを利用する機会が増えています。

何が機能しないのか

冒頭では雑に書いてしまいましたが、DeepSleep 自体は行え消費電流がきちんと減少します。問題となったのは以下の二つです。

  • esp_sleep_get_wakeup_cause() が常に0(ESP_SLEEP_WAKEUP_UNDEFINED)

  • RTC_DATA_ATTR を指定した変数が常に0

DeepSleep からの復帰などは、タイマーでの時間指定、指定したGPIOの変化などいずれでも問題なく動作します。ただ、何をどうやっても復帰理由と変数値の保持が行われませんでした。利用法の問題かもと考え、esp-idf の DeepSleep のサンプルでも確認しましたが、問題は解決しませんでした。

なぜか動く環境がある

行き詰っていたのですが、開発機として利用していた Windows のノートPCの USB ポートが別作業のためにふさがってしまっていたことと、Windows からのアップロードが失敗する率が高く作業効率が悪いといおうこともあり、脇にあった Khadas Vim4 (Raspberry Pi のような所謂 SBC で、Linux で利用しています)に環境を作って書き込んでみると、なぜか動きます。
※ESP32 C3 は、Full-speed USB シリアルコントローラーを内包しているので DeepSleep にはいるとシリアルポートが認識されなくなります。

おまけに、その状態の ESP32 C3 を開発機の Windows ノートPCにつないだら動きました。ホスト環境が Windows, Linux と違っても、ターゲット側である ESP32 C3は同じなので、何で?となりました。

また、Windows ノートPCでも、つないで動く場合と動かない場合があり、これもまた謎を深めました。

省電力の罠

あれこれと切り分けてみると、ESP32 C3 を Windows ノートPCにつなぐ際にThunderbolt 4 対応 USB-Cを使うと、件の機能が動作しません。

DeepSleep に入るとシリアルポートとして認識されなくなるのは Windows, Linux (VIM4 だけではなく Raspberry Pi なども)のいずれも同じ挙動でした。
しかし、Windows ノートPC側の USB コントローラーは DeepSleep に入って流れる電流少なくなると、途中から完全に電源供給をカットしてしまうのでした。
いくら省電力だといっても、電源を切られたら ESP32 C3側としては再起動になってしまうわけで、結果として復帰理由と変数の初期化がされていたということです。

ESP32 シリーズに限らず MCU をモバイルバッテリーで運用する際、DeepSleep のような省電力モードを利用すると、電流が少ないため電源供給を止めるものが多いのですが、それと同じ状況となっていました。

最後に

最初のToFセンサーについては元コード側の問題なので致し方がないのですが、残りの二つは明らかに自分の不注意です。
原因がわかればどちらもいたって当たり前で、トホホな恥ずかしい内容ですが、同じような状況に遭遇した際の参考になればと考え書いてみました。
この記事が皆様のハマり回避の一助となれば幸いです。


この記事が気に入ったらサポートをしてみませんか?