ESP32でBME680を使ってみた

昨日の修正でBME680が使えるようになったので、温度と湿度と気圧を測って電圧と一緒にWebサーバーにPOSTするプログラムを書いて放置してみたところ、突然データが送信されないバグに遭遇した。

調査してみたところ、BME680のライブラリがバグってた。

症状

突然プログラムが一切のログも吐かずに動かなくなった。カーネルパニックとかのログもなく。一切動かなくなる。

原因調査

デバッグするための機器をもってないので、昔ながらのprintfデバッグ。片っ端からログ仕込んで、どこまで動いてるのか、どこで止まってるのかを確認。

最終的にたどり着いたのが

static void bme680_delay_ms(uint32_t period)
{
    uint32_t start_time = sdk_system_get_time () / 1000;

    vTaskDelay((period + portTICK_PERIOD_MS-1) / portTICK_PERIOD_MS);

    while (sdk_system_get_time()/1000 - start_time < period)
        vTaskDelay (1);
}

bmc680.cにあったこの関数。指定時間より若干短めにdelayしたあと、時刻確認しながらdelayを微調整してるっぽい。

ここのwhileにログ仕込んでstart_timeとsdk_system_get_time()/1000の値を出してみたところ

時間が進まない

sdk_system_get_time()/1000の値が4294967で固まったまま動かず、ここで無限ループになってた。(Exception云々は、idf.py monitorを止めたときのもの)

続いてsdk_system_get_time()を確認

uint32_t sdk_system_get_time ()
{
    struct timeval time;
    gettimeofday(&time,0);
    return time.tv_sec*1e6 + time.tv_usec;
}

これはesp8266_wrapper.cにあった。gettimeofdayで秒とマイクロ秒を取得し、秒を106倍してそれとマイクロ秒を足し算したuint32_tを返してる。

で、それを1000で割ってミリ秒に変換して待ち時間比較に使っている。

ここでちょっと気になってuint32_tの最大値を見てみたら「4294967295」。1000で割ったら「4294967」で固まったときの値と一致する。

「多分これ、オーバーフローして計算できなくなったんかな?」と思い、修正

修正

マイクロ秒を足した値を使おうにも使い物にならないので、方針変更。

gettimeofday()で取得できるtimeval構造体にはそれを計算をする関数があるので、「timeval同士で計算して経過時間を取得してから変換して比較」する方針で修正。

static void bme680_delay_ms(uint32_t period)
{
    struct timeval start_timeval, now_timeval, difference_timeval;
    gettimeofday(&start_timeval, NULL);

    vTaskDelay((period + portTICK_PERIOD_MS-1) / portTICK_PERIOD_MS);

    gettimeofday(&now_timeval, NULL);
    timersub(&now_timeval, &start_timeval, &difference_timeval);
    uint32_t difference_time = (difference_timeval.tv_sec * 1e6 + difference_timeval.tv_usec) / 1000;
    while (difference_time < period)
    {
        vTaskDelay (1);

        gettimeofday(&now_timeval, NULL);
        timersub(&now_timeval, &start_timeval, &difference_timeval);
        difference_time = (difference_timeval.tv_sec * 1e6 + difference_timeval.tv_usec) / 1000;
    }
}

いまのところこれで2時間は動いた。

余談

秒を106倍してuint32_tの最大値に届くってことは、最大で4294秒までで約1時間11分ちょっと。最初に友人に愚痴ったときは「1時間ちょっとで止まる」だったので、ちゃんと測ってたわけでもないのに意外とあってた。

gettimeofday()は途中で時計が変わると影響を受けたり、そもそもPOSIXで非推奨になったらしい。つまりこれ、NTP実装するとかして動作中にESP32の時計を修正すると、タイミングによってはまたバグる可能性がある。gettimeofdayが非推奨の件は、clock_gettime()で取得するのが推奨されてるらしい。

「オーバーフローして計算できなくなった」と思っているけど、本当にそうかはわからない。アセンブラ読めないし、CPUの仕様も知らないし。ただ、値が進まなくて無限ループしてるのは確かなようだ。

Diff

diff -cr a/bme680/bme680.c b/bme680/bme680.c
*** a/bme680/bme680.c	2023-09-10 17:24:28.864249600 +0900
--- b/bme680/bme680.c	2023-09-11 19:04:30.342671456 +0900
***************
*** 50,55 ****
--- 50,56 ----

  #include <string.h>
  #include <stdlib.h>
+ #include <sys/time.h>

  #include "bme680_platform.h"
  #include "bme680.h"
***************
*** 1214,1225 ****

  static void bme680_delay_ms(uint32_t period)
  {
!     uint32_t start_time = sdk_system_get_time () / 1000;

      vTaskDelay((period + portTICK_PERIOD_MS-1) / portTICK_PERIOD_MS);

!     while (sdk_system_get_time()/1000 - start_time < period)
          vTaskDelay (1);
  }


--- 1215,1236 ----

  static void bme680_delay_ms(uint32_t period)
  {
!     struct timeval start_timeval, now_timeval, difference_timeval;
!     gettimeofday(&start_timeval, NULL);

      vTaskDelay((period + portTICK_PERIOD_MS-1) / portTICK_PERIOD_MS);

!     gettimeofday(&now_timeval, NULL);
!     timersub(&now_timeval, &start_timeval, &difference_timeval);
!     uint32_t difference_time = (difference_timeval.tv_sec * 1e6 + difference_timeval.tv_usec) / 1000;
!     while (difference_time < period)
!     {
          vTaskDelay (1);
+
+         gettimeofday(&now_timeval, NULL);
+         timersub(&now_timeval, &start_timeval, &difference_timeval);
+         difference_time = (difference_timeval.tv_sec * 1e6 + difference_timeval.tv_usec) / 1000;
+     }
  }

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です