「えだまめ」しているラズパイ

30年ぶりに半田ごて握ってラズパイ勉強中。

Wroom-02とDS1307で時計を作りRTCの制御ノウハウを得る

◯やりたいこと

前回RX8025を利用してSSD1306を表示器にした時計を作りRTCの制御ノウハウを得たのですが、

アマゾンを見ていたら送料共10個で千円のDS1307を発見。

おっ、こりゃ安くていいなという事でDS1307でもその制御ノウハウを得るため同様の時計を作って見ることにしました。

◯やったこと

・DS1302でスケッチ開発

DS1307が中国から届くまでの間DS1302で実験機を作り時計を動作させていました。ブレッドボードにWR02の書き込み機も載せDSシリーズ用のスケッチを開発。

DS1302はデータ通信が3wire接続のためシンプルサイズのWR02ではどうしても端子不足になってしまいコンパクトサイズのWR02を使用。


(写真はコンパクトサイズのWR02で組み上げたものです)

これでDS1302用のスケッチを開発した上で

DS1307に転用するという手順を踏みました。

・製作

前回RX8025で作ったブレッドボードをそのまま転用します。

これに32.767KHzの水晶や容量負荷を追加したものを作成し

チップを乗せると実験機の完成です。



・スケッチ

スケッチは以下の通りです。

/**
*
*    WR02_DS07_OLED v0.0.1 (ok)
*
*/

// BSP(Board Support Package)
//   ESP8266 Boards v3.1.2
//     FlushSize ESP01:1MB Wroom-02D:2MB -02:4MB

// Built-in Library (Arduino IDE v1.8.19)
#include            <Arduino.h>
#include            <time.h>
#include            <Wire.h>

// BSP Built-in Library
#include            <ESP8266WiFi.h>

// Installed Library
#include            <Adafruit_GFX.h>      // v1.11.3
#include            <Adafruit_SSD1306.h>  // v2.5.7

// Global Variable
/* WiFi */
#define WIFI_SSID   "YOUR_SSID"
#define WIFI_PASS   "YOUR_PASS"

/* TCP/IP */
IPAddress ip        (YOUR_IP);            // ex. (192,168,0,2)
IPAddress gw        (YOUR_GW);
IPAddress dns       (YOUR_DNS);           // Google:(8,8,8,8)
IPAddress mask      (YOUR_MASK);          // ex. (255,255,255,0)

/* UART */
#define UART_TX     1
#define UART_RX     3

/* I2C */
#define I2C_SDA     2
#define I2C_SCK     0
#define I2C_CLK     100000                // fSCL=100KHz

/* NTP DATA */
/* NTP DATA */
#define NTP_SRV1    "ntp.nict.jp"
#define NTP_SRV2    "ntp.jst.mfeed.ad.jp"

/* DS1307 */
#define RTC_ADR     0x68                  // I2Cアドレス
#define RTC_SEC     0x00                  // レジスタ先頭アドレス 0-6
#define RTC_CTR     0x07                  // SQW/OUT制御用アドレス
#define RTC_RAM     0x08                  // RTC RAM トップアドレス(08-3F)

struct rtcTime {
  uint8_t sec;                            // 0-59
  uint8_t min;                            // 0-59
  uint8_t hour;                           // 0-23
  uint8_t day;                            // 1-31
  uint8_t mon;                            // 0-11
  uint8_t year;                           // 00-99
  uint8_t wday;                           // 0(Sun)-6(Sat)
};
rtcTime RT =        {0,0,0,1,1,0,0};

/* OLED */
#define OLED_ADR    0x3C                  // I2Cアドレス
#define OLED_RESET  -1
#define OLED_WIDTH  128                   // OLED width(pixels)
#define OLED_HEIGHT 64                    // OLED height

Adafruit_SSD1306    dsp(OLED_WIDTH, OLED_HEIGHT, &Wire, OLED_RESET);
static const char   *WDay[] = {"Sun","Mon","Tue","Wed","Thu","Fri","Sat"};

/* Time */
time_t   UXTM =     0;                    // UnixTime
uint32_t STTM =     0;                    // スタート時間 milli()

///// ハードウエア 初期設定
void setup() {
  // Start time
  STTM = millis();

  // WiFi
  //WiFi.setOutputPower(10); // [dBm] max 20.5dBm

  // I2C
  Wire.begin(I2C_SDA, I2C_SCK);
  Wire.setClock(I2C_CLK);
  delayy(10);

  // Pin Mode
  digitalWrite(UART_TX, HIGH);
  pinMode     (UART_TX, OUTPUT);

  // スタート表示
  dsp_start();

  // DS1307 安定化待ち
  while ((millis()-STTM)<1000) delayy(1);

  // RTC CHビットチェック
  /* CH on */
  if (rtc_chkCH()) {
    dsp.setCursor( 0, 8);
    dsp.print("CH: Syncing NTP time");
    dsp.display();
    // DS1307 初期化
    rtc_init();

    // NTP時刻をRTCにセット
    rtc_setNTP();
  }

  /* CH off */
  else {
    dsp.setCursor( 0, 8);
    dsp.print("CH: off ");
    dsp.display();
  }

  /* RTC時刻->RT */
  rtc_getRT();
  dsp.clearDisplay();
}

///// メインルーチン
void loop() {
  // 毎日時刻合わせ
  if(RT.hour==1 && RT.min==3 && (RT.sec/5)==3) rtc_setNTP();

  // 時刻表示
  dsp_datetime();

  // LED点滅
  byte led = digitalRead(UART_TX);
  led = (led==LOW) ? HIGH : LOW;
  digitalWrite(UART_TX, led);
  delay(1000);
}

///// I2C Control /////
// i2cのデータが確定するまで待つ
inline void i2c_wait() { while(Wire.available()<1); }

///// DS1307 Control /////
// CH(Clock Halt)ビット読込み 1:クロック停止中
bool rtc_chkCH() { return (rtc_regRD(RTC_SEC) & 0x80); }

// RTC 初期化
void rtc_init() {
  // SQW制御レジスタ(0x07)に1Hz出力設定
  // (bit4) 1:出力on (bit3) 1:SQWon (bit2-0) 00:1Hz
  rtc_regWR(RTC_CTR, 0x18);
}

// RT構造からRTCのレジスタへ時刻をセット
void rtc_setRT() {
  // RT構造上の時刻データ(DEC)をBCDに変換して書込み
  uint8_t buf[7];
  buf[0] = dec2bcd(RT.sec & 0x7f);        // CHビットクリア(時計スタート)
  buf[1] = dec2bcd(RT.min);
  buf[2] = dec2bcd(RT.hour);
  buf[3] = dec2bcd(RT.wday + 1);          // DS1307は1=日曜
  buf[4] = dec2bcd(RT.day);
  buf[5] = dec2bcd(RT.mon + 1);           // DS1307は1-12
  buf[6] = dec2bcd(RT.year % 100);        // 下2桁のみ

  rtc_regSET(RTC_SEC, 7, buf);            // RTC_SEC=0x00から7バイト書き込み
}

// RTCのレジスタから時刻を読出しRT構造にセット
void rtc_getRT() {
  // RTCから日時データ読込
  uint8_t data[7];
  rtc_regGET(RTC_SEC, 7, data);

  // 日時データ(BCD)をDECに変換しRT構造にセット
  RT.sec  = bcd2dec(data[0] & 0x7f);      // CHビット無視
  RT.min  = bcd2dec(data[1]);
  RT.hour = bcd2dec(data[2]);
  RT.wday = bcd2dec(data[3]) - 1;         // DS1307は1=日曜 -> 0=日曜
  RT.day  = bcd2dec(data[4]);
  RT.mon  = bcd2dec(data[5]) - 1;         // DS1307は1-12 -> 0-11
  RT.year = bcd2dec(data[6]);
}

// NTPから時刻を取得しRTCにセット
boolean rtc_setNTP() {
  /* WiFi接続:未接続はRTC時刻を更新しない */
  if (wifi_connect()) {
    if (ntp_connect()) {
      /* NTP時刻->UXTM->RT構造->RTC */
      UXTM = time(NULL)+1;
      uxt2rt(UXTM);
      rtc_setRT();
      wifi_stop();
      /* RTC カウント開始 */
      return true;
    }
  }
  wifi_stop();
  return false;
}

///// RTC RAM Control /////
//  RTC RAM に1byte書込む
void rtc_ramWR(uint8_t adr, uint8_t val) {
  if (adr < RTC_RAM || adr > 0x3F) return;
  rtc_regWR(adr, val);
}

// RTC RAM から1byte読込む
uint8_t rtc_ramRD(uint8_t adr) {
  if (adr < RTC_RAM || adr > 0x3F) return 0;
  return rtc_regRD(adr);
}

///// RTC レジスタ Control /////
// RTCの指定レジスタから1byteデータを読込む
uint8_t rtc_regRD(uint8_t reg) {
  Wire.beginTransmission(RTC_ADR);
  Wire.write(reg);
  Wire.endTransmission();
  Wire.requestFrom(RTC_ADR, 1);
  i2c_wait();
  return Wire.read();
}

// RTCの指定レジスタへ1byteデータを書込む
void rtc_regWR(uint8_t reg, uint8_t val) {
  Wire.beginTransmission(RTC_ADR);
  Wire.write(reg);
  Wire.write(val);
  Wire.endTransmission();
}

// RTCの指定レジスタから連続でデータを書込む
void rtc_regSET(uint8_t reg, int num, uint8_t *data) {
  Wire.beginTransmission(RTC_ADR);
  Wire.write(reg);
  Wire.write(data, num);
  Wire.endTransmission();
}
 
// RTCの指定レジスタから連続でデータを読込む
void rtc_regGET(uint8_t reg, int num, uint8_t *data) {
  Wire.beginTransmission(RTC_ADR);
  Wire.write(reg);
  Wire.endTransmission();

  Wire.requestFrom(RTC_ADR, num);
  for (int i = 0; i < num; i++) {
    i2c_wait();
    data[i] = Wire.read();
  }
}
 
///// Display Control /////
/* OLED 起動処理 */
void dsp_start() {
  // 初期化
  dsp.begin(SSD1306_SWITCHCAPVCC, OLED_ADR);

  // 初期表示
  /* タイトル表示 */
  dsp.clearDisplay();
  dsp.setTextColor(WHITE);
  dsp.setTextSize(1);
  dsp.setCursor( 0, 0);
  dsp.print("Start ESP8266/DS1307");

  dsp.display();
  //dsp.clearDisplay();
}

/* OLED 時刻表示 */
void dsp_datetime() {
  // RTC 現在時刻の取得
  rtc_getRT();

  // DATE 文字列作成
  char fmtDate[11];
  sprintf( fmtDate, "%02d/%02d(%s)",
           RT.mon+1,
           RT.day,
           WDay[RT.wday] );
  // TIME 文字列作成
  char fmtTime[6];
  sprintf( fmtTime, "%02d:%02d",
           RT.hour,
           RT.min );
  // 表示
  dsp.clearDisplay();
  //dsp.fillRect(  0,  0,128, 16,BLACK);
  dsp.setTextSize(2);
  dsp.setCursor(  4, 4);
  dsp.println(fmtDate);
  //dsp.setTextSize(2);
  //dsp.setCursor( 68, 0);
  //dsp.println(fmtTime);
  dsp.setTextSize(3);
  dsp.setCursor( 20, 32);
  dsp.println(fmtTime);
  dsp.display();
}

///// WiFi Control /////
/* WiFi接続開始処理(ノンブロッキング風)
 *   <引数> Timeout: 接続制限時間(0 or 省略は接続するまで無限ループ)
 *   <戻値> 接続:true 未接続:False
 */
boolean wifi_connect() { return wifi_connect(0); }
boolean wifi_connect(int timeout) {
  // 初期化
  unsigned long start = 0;
  unsigned long last  = 0;                // ノンブロッキング用時間変数
  unsigned long now   = 0;
  unsigned long itvl  = 500;              // クライアント再接続時のインターバル時間

  // WiFi 初期化
  wifi_init();

  // WiFiが未接続の場合のみbeginする
  if (WiFi.status() != WL_CONNECTED) {
    WiFi.begin(WIFI_SSID, WIFI_PASS);
    delayy(100);
  }

  // WiFi接続(指定時間内) 接続:true 接続失敗:false
  start = millis();
  /* タイムアウトチェック */
  while (timeout==0 || millis() - start < timeout) {
    // WiFi接続は非同期(ノンブロッキング)風で
    now = millis();
    if (now-last >= itvl) {
      last = now;
      // 接続チェック
      if (WiFi.status() == WL_CONNECTED) {
        return true;
      }
      // 未接続の場合,0.3-0.5sec おきに再接続
      itvl = random(300, 500);
    }
    yield();
  }
  // 接続不可 エラーリターン
  return false;
}

/* WiFi 初期化処理 */
boolean wifi_init() {
  // WiFi初期化 (順番大事)
  //   disconnect(true)されて再接続の場合 mode,configの再設定が必須
  WiFi.mode(WIFI_STA);                    // 1.WiFi モード設定
  WiFi.persistent(false);                 // 2.WiFi設定情報 保存しない
  if (!WiFi.config(ip, gw, mask, dns)) {  // 3.固定ip 設定
    /* DHCPフォールバック(DHCP切替え処理) */
    if (!WiFi.config(0U, 0U, 0U)) {
      return false;
    }
  }
  delayy(100);
  return true;  
}

/* WiFi stop処理 */
void wifi_stop() {
  // WiFiが接続の場合のみdisconnectする
  if (WiFi.status() == WL_CONNECTED) {
    WiFi.disconnect(true);
  }
}

/* 終了処理 */
void wifi_end() {
  // WiFi切断
  wifi_stop();

  // sleep
  ESP.deepSleep(60*1000*1000);
  delay(100);
}

///// NTP Control /////
/* NTP 同期処理(ノンブロッキング風)
 *   <引数> Timeout[ms]:同期制限時間(0 or 省略は同期するまで無限ループ)
 *   <戻値> Unixtime or 0(エラー)
 */
time_t ntp_connect() { return ntp_connect(0); }
time_t ntp_connect(int timeout) {
  // WiFi接続している時のみ時間取得する
  if (WiFi.status() != WL_CONNECTED) {
    return 0;
  }

  // 初期化
  unsigned long start;
  unsigned long itvl = 250; // NTP再接続時のインターバル時間
  unsigned long last = 0;   // ノンブロッキング用時間変数

  // タイムゾーン,NTPサーバー設定
  configTzTime("JST-9", NTP_SRV1, NTP_SRV2);
  /* 最初に一定時間待機すると同期時にループが省かれる(短時間化) */
  delayy(50);

  // NTPサーバー接続 (timeout秒間 接続確認)
  start = millis();
  while (timeout==0 || millis()-start < timeout) {
    // NTP接続は非同期(ノンブロッキング)風で
    if (millis()-last >= itvl) {
      last = millis();
      // 現在時刻の取得
      time_t uxtm  = time(NULL);          // unix時間
 
      // NPT同期チェック (RTC初期値=1,000,000,000)
      if(uxtm>1000000000) {
        return uxtm;
      }
      // 未接続の場合,0.2-0.3sec おきに再接続
      itvl = random(200, 300);
    }
    yield();
  }

  // 同期不可 エラーリターン
  return 0;
}

///// Time Controle /////
// UnixTimeからRTC時刻(RT構造)を得る (UnixTime->tm構造->RT構造)
void uxt2rt(time_t uxtm) {
  struct tm *t = localtime(&uxtm);
  RT.year = (t->tm_year + 1900) % 100;    // tm_year: 1900年から
  RT.mon  = t->tm_mon;                    // tm_mon : 0-11
  RT.day  = t->tm_mday;
  RT.wday = t->tm_wday;                   // 曜日: 0(日)-6(土)
  RT.hour = t->tm_hour;
  RT.min  = t->tm_min;
  RT.sec  = t->tm_sec;

}

// RTC時刻(RT構造)からUnixTimeを得る (RT構造->tm構造->UnixTime)
time_t rt2uxt() {
  struct tm t;
  t.tm_year = RT.year + 100 ;             // tm_year: 1900年から
  t.tm_mon  = RT.mon;                     // tm_mon : 0-11
  t.tm_mday = RT.day;
  t.tm_wday = RT.wday;                    // 曜日: 0(日)-6(土)
  t.tm_hour = RT.hour;
  t.tm_min  = RT.min;
  t.tm_sec  = RT.sec;
  t.tm_isdst= -1;                         // 夏時間設定を無効化

  return mktime(&t);
}

///// Other Control /////
// BCD Control
/* DEC(10進)->BCD(2進化10進) */
inline int dec2bcd(int dec) {
  return (((dec / 10) << 4) | (dec % 10));
}

/* BCD->DEC */
inline int bcd2dec(int bcd) {
  return ((bcd >> 4) * 10 + (bcd & 0x0f));
}

// yield delay(ノンブロッキング ディレイ)
inline void delayy(unsigned long tm) {
  unsigned long now = millis();
  while (millis()-now < tm) { yield();}
}

(相変わらずの我流スケッチ、読みにくさご勘弁です)

◯やってみて

実験機は無事一発動作してくれました。

よかった、よかった。

でも1個100円のRTCならモーションセンサーの長寿命化用に思い切って大量に使いまくることが出来そうで、なんか一安心です。
さっそく

greensoybean.hatenablog.com

をDS1307用に改造していくことにしましょう。

WROOM02でRTC8025を使えるようになる(2) RX8025で時計作り

〇やりたいこと

(1)で作成した時計において、RTCをRTC8564からRX8025に変更して時計を動作させRX8025をコントロールするノウハウを得る。


〇やったこと

・製作

まずはブレッドボード上に回路を組みます。秋月さんのモジュールはRTC8564とRX8025が同じピン配置になっているのでブレッドボードの配線は(1)のものをそのまま使えます。

ただRTC8564ではNCだった端子部分にGND配線を持ってきているためRX8025のINTB部分だけは競合します。そのため今回の時計はBME280を外し時刻・日付だけの時計に変更しました。


・スケッチ

スケッチでRTC8564と違いえらくはまった点は以下の二つ。
・I2CでRX8025のレジスタにアクセスする場合レジスタ番号は上位4bitにセット・下位4bitは0固定
・requestFromでレジスタを連続読み込みする場合、読み込み先頭1byteは読み捨てが必要

この二つに気が付くまで大いにてこずりました。
完成したスケッチは以下の通りです。

/**
*    WR02_RX_OLED v0.0.4
*/

// BSP(Board Support Package)
//   ESP8266 Boards v3.1.2
//     FlushSize ESP01:1MB Wroom-02D:2MB -02:4MB

// Built-in Library (Arduino IDE v1.8.19)
#include            <Arduino.h>
#include            <time.h>
#include            <Wire.h>

// BSP Built-in Library
#include            <ESP8266WiFi.h>       // v1.0

// Installed Library
#include            <Adafruit_GFX.h>      // v1.11.3
#include            <Adafruit_SSD1306.h>  // v2.5.7

// Global Variable
/* WiFi */
#define WIFI_SSID   "YOUR_SSID"
#define WIFI_PASS   "YOUR_PASS"

/* TCP/IP */
IPAddress ip        (YOUR_IP);            // ex. (192,168,0,2)
IPAddress gw        (YOUR_GW);
IPAddress dns       (YOUR_DNS);           // Google:(8,8,8,8)
IPAddress mask      (YOUR_MASK);          // ex. (255,255,255,0)

/* UART */
#define UART_TX     1
#define UART_RX     3

/* I2C */
#define I2C_SDA     2
#define I2C_SCK     0
#define I2C_CLK     100000                // fSCL=100KHz

/* NTP DATA */
#define NTP_SRV1    "ntp.nict.jp"
#define NTP_SRV2    "ntp.jst.mfeed.ad.jp"

/* RTC8025 */
#define RTC_ADR     0x32                  // I2Cアドレス
#define RTC_SEC     0x00                  // レジスタ先頭アドレス
#define RTC_CTR1    0xE0
#define RTC_CTR2    0xF0                  // bit4:VDET(電圧低下検出)

struct rtcTime {
  uint8_t sec;                            // 0-59
  uint8_t min;                            // 0-59
  uint8_t hour;                           // 0-23
  uint8_t day;                            // 1-31
  uint8_t mon;                            // 1-12
  uint8_t year;                           // 00-199
  uint8_t wday;                           // 0(Sun)-6(Sat)
};
rtcTime RT =        {0,0,0,1,1,0,0};

/* OLED */
#define OLED_ADR    0x3C                  // I2Cアドレス
#define OLED_RESET  -1
#define OLED_WIDTH  128                   // OLED width(pixels)
#define OLED_HEIGHT 64                    // OLED height

Adafruit_SSD1306    dsp(OLED_WIDTH, OLED_HEIGHT, &Wire, OLED_RESET);
static const char   *WDay[] = {"Sun","Mon","Tue","Wed","Thu","Fri","Sat"};

/* Time */
time_t   UXTM =     0;                    // UnixTime
uint32_t STTM =     0;                    // スタート時間 milli()

///// ハードウエア 初期設定
void setup() {
  // Start time
  STTM = millis();

  // WiFi
  //WiFi.setOutputPower(10); // [dBm] max 20.5dBm

  // I2C
  Wire.begin(I2C_SDA, I2C_SCK);
  Wire.setClock(I2C_CLK);
  delayy(10);

  // Pin Mode
  digitalWrite(UART_TX, HIGH);
  pinMode     (UART_TX, OUTPUT);

  // スタート表示
  dsp_start();

  // RTC8025 安定化待ち
  while ((millis()-STTM)<1000) delayy(1);

  // RTC PONビットチェック
  /* PON on */
  if (rtc_chkpon()) {
    dsp.setCursor( 0, 8);
    dsp.print("PON: Syncing NTP time");
    dsp.display();
    // RTC8025 初期化
    rtc_init();

    // NTP時刻をRTCにセット (NTP->RT,RTC)
    rtc_setNTP();
  }

  /* PON off */
  else {
    dsp.setCursor( 0, 8);
    dsp.print("PON: off ");
    dsp.display();
  }

  /* RTC時刻->RT */
  rtc_getRT();
  dsp.clearDisplay();
}

///// メインルーチン
void loop() {
  // 月1時刻合わせ
  if(RT.day==1 && RT.hour==1 && RT.min==3 && (RT.sec/5)==3) rtc_setNTP();

  // 時刻表示
  dsp_datetime();

  // LED点滅
  byte led = digitalRead(UART_TX);
  led = (led==LOW) ? HIGH : LOW;
  digitalWrite(UART_TX, led);
  delay(1000);
}

///// I2C Control /////
// i2cのデータが確定するまで待つ
inline void i2c_wait() { while(Wire.available()<1); }

///// RTC8025 Control /////
// PONビット読込み
bool rtc_chkpon() { return (rtc_regRD(RTC_CTR2) & 0x10); }

// RTC 初期化
void rtc_init() {
  uint8_t data[2];
  /* RTC_CTR1 初期化 */
  /* (bit7)0:アラームW無効 (bit6)0:アラームD無効 (bit5)1:24h (bit4)0:FOUTon */
  /* (bit3)0:n/a (bit2-0)2:パルス2Hz 3:パルス1Hz 4:レベル1sec */
  data[0] = 0x13;  // 24h,パルス1Hz
  /* RTC_CTR2 初期化 */
  /* (bit7)0:2.1V (bit6,5,4)000:VDET,XST,PONクリア (bit3)0:FOUTon */
  /* (bit2)1:タイマー_INTAon (bit1)0:アラームW_INTBoff (bit0)0:アラームD_INTAoff */
  data[1] = 0x04;

  // 2レジスタ 一気に書込む
  rtc_regSET(RTC_CTR1, 2, data);
}

// RT構造からRTCのレジスタへ時刻をセット
void rtc_setRT() {
  // RT構造上の時刻データ(DEC)をBCDに変換して変数にセット
  uint8_t data[7];
  data[0] = dec2bcd(RT.sec);
  data[1] = dec2bcd(RT.min);
  data[2] = dec2bcd(RT.hour);
  data[3] = dec2bcd(RT.wday);
  data[4] = dec2bcd(RT.day);
  data[5] = dec2bcd(RT.mon);

  // yearが100年を超えた場合CENTURYビットを立てる
  if (RT.year > 100) {
    data[6] = dec2bcd(RT.year - 100);
    data[5] |= 0x80;  // CENTURY bit
  } else {
    data[6] = dec2bcd(RT.year);
  }

  // 7変数 一気に書込む
  rtc_regSET(RTC_SEC, 7, data);
}

// RTCのレジスタから時刻を読出しRT構造にセット
void rtc_getRT() {
  // RTCから日時データ読込
  uint8_t data[7];
  rtc_regGET(RTC_SEC, 7, data);

  // 日時データ(BCD)をDECに変換しRT構造にセット
  RT.sec     = bcd2dec(data[0] & 0x7f);
  RT.min     = bcd2dec(data[1] & 0x7f);
  RT.hour    = bcd2dec(data[2] & 0x3f);
  RT.wday    = bcd2dec(data[3] & 0x07);
  RT.day     = bcd2dec(data[4] & 0x3f);
  RT.mon     = bcd2dec(data[5] & 0x1f);
  RT.year    = bcd2dec(data[6]);
   
  // CENTURY bit check
  if (data[5] & 0x80)  RT.year += 100;
}

// NTPから時刻を取得しRTCにセット
boolean rtc_setNTP() {
  /* WiFi接続:未接続はRTC時刻を更新しない */
  if (wifi_connect()) {
    if (ntp_connect()) {
      /* NTP時刻->UXTM->RT構造->RTC */
      UXTM = time(NULL);
      uxt2rt(UXTM);
      rtc_setRT();
      wifi_stop();
      return true;
    }
  }
  wifi_stop();
  return false;
}
 
// RTCの指定レジスタからデータを読込む
uint8_t rtc_regRD(uint8_t reg) {
  Wire.beginTransmission(RTC_ADR);
  Wire.write(reg);
  Wire.endTransmission();
  Wire.requestFrom(RTC_ADR, 1);
  i2c_wait();
  return Wire.read();
}

// RTCのアクセスするレジスタを書込む
void rtc_regWR(uint8_t reg, uint8_t value) {
  Wire.beginTransmission(RTC_ADR);
  Wire.write(reg);
  Wire.write(value);
  Wire.endTransmission();
}

void rtc_regSET(uint8_t reg, int num, uint8_t *data) {
  Wire.beginTransmission(RTC_ADR);
  Wire.write(reg);
  Wire.write(data, num);
  Wire.endTransmission();
}
 
void rtc_regGET(uint8_t reg, int num, uint8_t *data) {
  Wire.beginTransmission(RTC_ADR);
  Wire.write(reg);
  Wire.endTransmission();

  Wire.requestFrom(RTC_ADR, num+1);
  Wire.read();
  for (int i = 0; i < num; i++) {
    i2c_wait();
    data[i] = Wire.read();
  }
}
 
// BCD Control
/* DEC(10進)->BCD(2進化10進) */
inline int dec2bcd(int dec) {
  return (((dec / 10) << 4) | (dec % 10));
}

/* BCD->DEC */
inline int bcd2dec(int bcd) {
  return ((bcd >> 4) * 10 + (bcd & 0x0f));
}

///// Time Controle /////
// UnixTimeからRTC時刻(RT構造)を得る (UnixTime->tm構造->RT構造)
void uxt2rt(time_t uxtm) {
  struct tm *t = localtime(&uxtm);
  RT.year = (t->tm_year + 1900) % 100; // tm_year: 1900年から
  RT.mon  = t->tm_mon + 1;             // tm_mon : 0$301C11
  RT.day  = t->tm_mday;
  RT.wday = t->tm_wday;                // 曜日: 0(日)-6(土)
  RT.hour = t->tm_hour;
  RT.min  = t->tm_min;
  RT.sec  = t->tm_sec;
}

// RTC時刻(RT構造)からUnixTimeを得る (RT構造->tm構造->UnixTime)
time_t rt2uxt() {
  struct tm t;
  t.tm_year = RT.year + 100 ;  // tm_year: 1900年から
  t.tm_mon  = RT.mon- 1;       // tm_mon : 0$301C11
  t.tm_mday = RT.day;
  t.tm_wday = RT.wday;         // 曜日: 0(日)-6(土)
  t.tm_hour = RT.hour;
  t.tm_min  = RT.min;
  t.tm_sec  = RT.sec;
  t.tm_isdst = -1;             // 夏時間設定を無効化

  return mktime(&t);
}

// 日時文字列"yyyy-mm-dd hh:mm:ss"から unixtimeを得る
time_t dat2uxt(String str) {
  // String->char 変換
  int len = str.length()+1;
  char chr[len];
  str.toCharArray(chr, len);

  // 日時文字列を tm構造に格納
  struct tm t;
  sscanf(chr, "%d-%d-%d %d:%d:%d",
    &t.tm_year, &t.tm_mon, &t.tm_mday,
    &t.tm_hour, &t.tm_min, &t.tm_sec);
  t.tm_year -= 1900;
  t.tm_mon -= 1;

  // 上記 tm構造から unixtimeを求めてリターン
  return mktime(&t);
}

// unixtimeから日時文字列"yyyy-mm-dd hh:mm:dd"を得る
String uxt2dat(time_t t) {
  char date[64];
  // フォーマット作成
  size_t strftime(char *s, size_t max, const char *format, const struct tm *tm);

  // unixtime->日時文字列
  strftime(date, sizeof(date), "%Y-%m-%d %H:%M:%S", localtime(&t));
  return date;
}

///// Display Control /////
/* OLED 起動処理 */
void dsp_start() {
  // 初期化
  dsp.begin(SSD1306_SWITCHCAPVCC, OLED_ADR);

  // 初期表示
  /* タイトル表示 */
  dsp.clearDisplay();
  dsp.setTextColor(WHITE);
  dsp.setTextSize(1);
  dsp.setCursor( 0, 0);
  dsp.print("Start ESP8266");
  dsp.display();
}

/* OLED 時刻表示 */
void dsp_datetime() {
  // RTC 現在時刻の取得
  rtc_getRT();

  // DATE 文字列作成
  char fmtDate[11];
  sprintf( fmtDate, "%02d/%02d(%s)",
           RT.mon,
           RT.day,
           WDay[RT.wday] );
  // TIME 文字列作成
  char fmtTime[6];
  sprintf( fmtTime, "%02d:%02d",
           RT.hour,
           RT.min );
  // 表示
  dsp.clearDisplay();
  //dsp.fillRect(  0,  0,128, 16,BLACK);
  dsp.setTextSize(2);
  dsp.setCursor(  4, 4);
  dsp.println(fmtDate);
  dsp.setTextSize(3);
  dsp.setCursor( 20, 32);
  dsp.println(fmtTime);
  dsp.display();
}

///// WiFi Control /////
/* WiFi接続開始処理(ノンブロッキング風)
 *   <引数> Timeout: 接続制限時間(0 or 省略は接続するまで無限ループ)
 *   <戻値> 接続:true 未接続:False
 */
boolean wifi_connect() { return wifi_connect(0); }
boolean wifi_connect(int timeout) {
  // 初期化
  unsigned long start = 0;
  unsigned long last  = 0;   // ノンブロッキング用時間変数
  unsigned long now   = 0;
  unsigned long itvl  = 500; // クライアント再接続時のインターバル時間

  // WiFi 初期化
  wifi_init();

  // WiFiが未接続の場合のみbeginする
  if (WiFi.status() != WL_CONNECTED) {
    WiFi.begin(WIFI_SSID, WIFI_PASS);
    delayy(100);
  }

  // WiFi接続(指定時間内) 接続:true 接続失敗:false
  start = millis();
  /* タイムアウトチェック */
  while (timeout==0 || millis() - start < timeout) {
    // WiFi接続は非同期(ノンブロッキング)風で
    now = millis();
    if (now-last >= itvl) {
      last = now;
      // 接続チェック
      if (WiFi.status() == WL_CONNECTED) {
        return true;
      }
      // 未接続の場合,0.3-0.5sec おきに再接続
      itvl = random(300, 500);
    }
    yield();
  }
  // 接続不可 エラーリターン
  return false;
}

/* WiFi 初期化処理 */
boolean wifi_init() {
  // WiFi初期化 (順番大事)
  //   disconnect(true)されて再接続の場合 mode,configの再設定が必須
  WiFi.mode(WIFI_STA);                      // 1.WiFi モード設定
  WiFi.persistent(false);                   // 2.WiFi設定情報 保存しない
  if (!WiFi.config(ip, gw, mask, dns)) {    // 3.固定ip 設定
    /* DHCPフォールバック(DHCP切替え処理) */
    if (!WiFi.config(0U, 0U, 0U)) {
      return false;
    }
  }
  delayy(100);
  return true;  
}

/* WiFi stop処理 */
void wifi_stop() {
  // WiFiが接続の場合のみdisconnectする
  if (WiFi.status() == WL_CONNECTED) {
    WiFi.disconnect(true);
  }
}

/* 終了処理 */
void wifi_end() {
  // WiFi切断
  wifi_stop();

  // sleep
  ESP.deepSleep(60*1000*1000);
  delay(100);
}

///// NTP Control /////
/* NTP 同期処理(ノンブロッキング風)
 *   <引数> Timeout[ms]:同期制限時間(0 or 省略は同期するまで無限ループ)
 *   <戻値> Unixtime or 0(エラー)
 */
time_t ntp_connect() { return ntp_connect(0); }
time_t ntp_connect(int timeout) {
  // WiFi接続している時のみ時間取得する
  if (WiFi.status() != WL_CONNECTED) {
    return 0;
  }

  // 初期化
  unsigned long start;
  unsigned long itvl = 250; // NTP再接続時のインターバル時間
  unsigned long last = 0;   // ノンブロッキング用時間変数

  // タイムゾーン,NTPサーバー設定
  configTzTime("JST-9", NTP_SRV1, NTP_SRV2, NTP_SRV3);
  /* 最初に一定時間待機すると同期時にループが省かれる(短時間化) */
  delayy(50);

  // NTPサーバー接続 (timeout秒間 接続確認)
  start = millis();
  while (timeout==0 || millis()-start < timeout) {
    // NTP接続は非同期(ノンブロッキング)風で
    if (millis()-last >= itvl) {
      last = millis();
      // 現在時刻の取得
      time_t uxtm  = time(NULL);        // unix時間
 
      // NPT同期チェック (RTC初期値=1,000,000,000)
      if(uxtm>1000000000) {
        return uxtm;
      }
      // 未接続の場合,0.2-0.3sec おきに再接続
      itvl = random(200, 300);
    }
    yield();
  }

  // 同期不可 エラーリターン
  return 0;
}

///// Other Control /////
/* yield delay(ノンブロッキング ディレイ) */
inline void delayy(unsigned long tm) {
  unsigned long now = millis();
  while (millis()-now < tm) { yield();}
}

(2025.10.25 v0.0.4 曜日表示のバグ修正)

〇やってみて

同じメーカーの製品だしレジスタテーブルを見るとあちこちそっくりなのでこりゃ簡単に移行できるだろうとルンルン気分でやっていたら思わず大きな沼にはまってしまいました。
とはいえ無事動作を確認。

これでRTCを
・タイマーの時間指定が必要な温度計ロガーは値段高めのRTC8564
・時間がわかればよいだけのモーションセンサーはちょっとお安めRX8025
と使い分けることができるようになりました。ひと段落。
次はもっと安いDS1302に挑戦してみようかと考えています。

WROOM02でRTC8025を使えるようになる(1) まずはRTC8564で時計作り

◯やりたいこと

値段が爆上がり(秋月:800円)してしまったRTC8564、数をそろえるには少々お高くなってしまったので少し安いRX8025(同:450円)を使いこなせるようになりたいという要求が。そこでRTC8564とSSD1306を利用した時計(BME280:温湿度・気圧計付き)を作り必要な部分のみを変更する事によってRX8025へすんなり移行することを目指します。


◯やったこと

・製作

まずはWROOM02でRTC8564とBME280を使いOLED(SSD1306)にデータを表示する時計を作ります。RTC8564バージョンは過去に温湿度計ロガーやモーションセンサーで必要な関数が出来上がっていますので比較的簡単に作ることができます。

回路的にはWROOM02のi2cラインにSSD1306とBME280をぶら下げるだけですのでそれほど難しい回路ではありません。ブレッドボードにササっと組み上げていきます。

配線をし

WROOM02,SSD1306,BME280,RTC8564をセットしたら

もう完成です。ブレッドボードは手間がかからないのでホント助かります。

・スケッチ

スケッチは以下の通りとなります。

/*
*    WR02_RTC_OLED v0.0.4
*/

// BSP(Board Support Package)
//   ESP8266 Boards v3.1.2
//     FlushSize ESP01:1MB Wroom-02D:2MB -02:4MB

// Built-in Library (Arduino IDE v1.8.19)
#include            <Arduino.h>
#include            <time.h>
#include            <Wire.h>

// BSP Built-in Library
#include            <ESP8266WiFi.h>       // v1.0

// Installed Library
#include            <Adafruit_GFX.h>      // v1.11.3
#include            <Adafruit_SSD1306.h>  // v2.5.7
#include            <Adafruit_BME280.h>   // v2.2.2

// Global Variable
/* WiFi */
#define WIFI_SSID   "YOUR_SSID"
#define WIFI_PASS   "YOUR_PASS"

/* TCP/IP */
IPAddress ip        (YOUR_IP);            // ex. (192,168,0,2)
IPAddress gw        (YOUR_GW);
IPAddress dns       (YOUR_DNS);           // Google:(8,8,8,8)
IPAddress mask      (YOUR_MASK);          // ex. (255,255,255,0)

/* I2C */
#define I2C_SDA     2
#define I2C_SCK     0
#define I2C_CLK     100000                // fSCL=100KHz

/* NTP DATA */
#define NTP_SRV1    "ntp.nict.jp"
#define NTP_SRV2    "ntp.jst.mfeed.ad.jp"

/* RTC8564 */
#define RTC_ADR     0x51                  // I2Cアドレス
#define RTC_CTR1    0x00                  // RTC CTRL1レジスタ
#define RTC_CTR2    0x01
#define RTC_SEC     0x02
#define RTC_TMRC    0x0E
#define RTC_TMR     0x0F

struct rtcTime {
  uint8_t sec;                            // 0-59
  uint8_t min;                            // 0-59
  uint8_t hour;                           // 0-23
  uint8_t day;                            // 1-31
  uint8_t mon;                            // 1-12
  uint8_t year;                           // 00-199
  uint8_t wday;                           // 0(Sun)-6(Sat)
};
rtcTime RT =        {0,0,0,1,1,0,0};

/* OLED */
#define OLED_ADR    0x3C                  // I2Cアドレス
#define OLED_RESET  -1
#define OLED_WIDTH  128                   // OLED width(pixels)
#define OLED_HEIGHT 64                    // OLED height

Adafruit_SSD1306    dsp(OLED_WIDTH, OLED_HEIGHT, &Wire, OLED_RESET);
static const char   *WDay[] = {"Sun","Mon","Tue","Wed","Thu","Fri","Sat"};

/* BME280 */
#define BME_ADR     0x76
Adafruit_BME280     bme;
float               tmp, hum, prs;

/* Time */
time_t   UXTM =     0;                    // UnixTime
uint32_t STTM =     0;                    // スタート時間 milli()

///// ハードウエア 初期設定
void setup() {
  // Start time
  STTM = millis();

  // WiFi
  //WiFi.setOutputPower(10); // [dBm] max 20.5dBm

  // I2C
  Wire.begin(I2C_SDA, I2C_SCK);
  Wire.setClock(I2C_CLK);
  delayy(10);

  // Pin Mode
  digitalWrite(UART_TX, HIGH);
  pinMode     (UART_TX, OUTPUT);

  // BME280 
  bme.begin(BME_ADR, &Wire);
 
  // スタート表示
  dsp_start();

  // RTC VLビットチェック
  /* VL on */
  if (rtc_chkvl()) {
    // RTC8564 安定化待ち,初期化
    while ((millis()-STTM)<1000) delayy(1);
    rtc_init();

    // NTP時刻をRTCにセット WiFi->NTP->UXTM->RT->RTC
    rtc_setNTP();
  }

  /* VL off */
  else {
    /* RTC時刻->dt, RTCタイマーセット */
    rtc_init();
    rtc_getRT();

    /* 定時時刻合わせチェック */
    if(RT.day==1 && RT.hour==1 && (RT.min/10)==3) {
      // NTP時刻をRTCにセット WiFi->NTP->UXTM->RT->RTC
      rtc_setNTP();
    }
  }
}

///// メインルーチン
void loop() {
  // BME280 温湿度・気圧測定 temperature,humidity,press
  tmp = bme.readTemperature();
  hum = bme.readHumidity();
  prs = bme.readPressure()/100;

  // 時刻・温湿度気圧表示
  dsp_datetime();
  dsp_data();

  // LED点滅
  byte led = digitalRead(UART_TX);
  led = (led==LOW) ? HIGH : LOW;
  digitalWrite(UART_TX, led);
  delay(1000);
}

///// I2C Control /////
// i2cのデータが確定するまで待つ
inline void i2c_wait() { while(Wire.available()<1); }

///// Display Control /////
/* OLED 起動処理 */
void dsp_start() {
  // 初期化
  dsp.begin(SSD1306_SWITCHCAPVCC, OLED_ADR);

  // 初期表示
  dsp.clearDisplay();
  dsp.setTextColor(WHITE);
  dsp.setTextSize(1);
  dsp.setCursor( 0, 20);
  dsp.print("Syncing NTP time");
  dsp.display();
  dsp.clearDisplay();
}

/* OLED 時刻表示 */
void dsp_datetime() {
  // RTC 現在時刻の取得
  rtc_getRT();

  // DATE 文字列作成
  char fmtDate[11];
  sprintf( fmtDate, "%02d/%02d(%s)",
           RT.mon,
           RT.day,
           WDay[RT.wday] );
  // TIME 文字列作成
  char fmtTime[6];
  sprintf( fmtTime, "%02d:%02d",
           RT.hour,
           RT.min );
  // 表示
  //dsp.clearDisplay();
  dsp.fillRect(  0,  0,128, 16,BLACK);
  dsp.setTextSize(1);
  dsp.setCursor(  4, 4);
  dsp.println(fmtDate);
  dsp.setTextSize(2);
  dsp.setCursor( 68, 0);
  dsp.println(fmtTime);
  dsp.display();
}

/* OLED 測定結果表示 */
void dsp_data() {
   // 初期化
  char buf1[9], buf2[21];

  // 測定結果表示
  dsp.fillRect( 30, 19,128, 53,BLACK);
  dsp.setTextColor(WHITE);
  dsp.setTextSize(1);
  /* Temp 表示 */
  dsp.setCursor(  0, 19);
  dtostrf      (tmp,4,1,buf1);
  sprintf      (buf2, "Tmp: %sC", buf1);
  dsp.print    (buf2);
  /* Humi 表示 */
  dsp.setCursor(  0, 28);
  dtostrf      (hum,4,1,buf1);
  sprintf      (buf2, "Hum: %s", buf1);
  dsp.print    (buf2);
  dsp.print    ('%');
  /* Press 表示 */
  dsp.setCursor(  0, 37);
  dtostrf      (prs,4,0,buf1);
  sprintf      (buf2, "Prs: %sHp", buf1);
  dsp.print    (buf2);

  dsp.display();
}

///// RTC8564 Control /////
// RTC 初期化
/* 時刻再設定なし,タイマー初期化 */
void rtc_init() {
  rtc_setTMR(1);
}

/* 指定時刻でRTC設定,タイマー初期化 */
void rtc_init(time_t uxtm) {
  /* UnixTimeを RTC時刻(RT構造)にセット */
  uxt2rt(uxtm);

  /* RTCへ指定時刻を設定,タイマー初期化 */
  rtc_setRT();
  rtc_setTMR(1);    // [sec] タイマーセット
}

// RTCのレジスタへRT構造から時刻をセット
void rtc_setRT() {
  // stop ビットセット
  rtc_regWR(RTC_CTR1, 0x20);

  // RT構造上の時刻データをBCDに変換してセット
  uint8_t data[7];
  data[0] = dec2bcd(RT.sec);
  data[1] = dec2bcd(RT.min);
  data[2] = dec2bcd(RT.hour);
  data[3] = dec2bcd(RT.day);
  data[4] = dec2bcd(RT.wday);
  data[5] = dec2bcd(RT.mon);

  // yearが100年を超えた場合CENTURYビットを立てる
  if (RT.year > 100) {
    data[6] = dec2bcd(RT.year - 100);
    data[5] |= 0x80;  // RTCS8564_CAL_CENTURY
  } else {
    data[6] = dec2bcd(RT.year);
  }

  // 7レジスタ 一気に書込む
  rtc_regSET(RTC_SEC, 7, data);

  // stop ビットクリア
  rtc_regWR(RTC_CTR1, 0x00);
}

// RTCのレジスタから時刻を読出しRT構造にセット
int rtc_getRT() {
  uint8_t data[7];

  rtc_regGET(RTC_SEC, 7, data);
  RT.sec     = bcd2dec(data[0] & 0x7f);
  RT.min     = bcd2dec(data[1] & 0x7f);
  RT.hour    = bcd2dec(data[2] & 0x3f);
  RT.day     = bcd2dec(data[3] & 0x3f);
  RT.wday    = bcd2dec(data[4] & 0x07);
  RT.mon     = bcd2dec(data[5] & 0x1f);
  RT.year    = bcd2dec(data[6]);
   
  if (data[5] & 0x80) {  // RTCS8564_CAL_CENTURY
    RT.year += 100;
  }
   
  return 0;
}

// 指定時間でタイマー初期化
void rtc_setTMR(int tmr) {
  rtc_regWR(RTC_CTR2, 0x11);  // bit4 1:割込繰返し bit0 1:INT発生
  rtc_regWR(RTC_TMRC, 0x82);  // bit7 1:タイマー有効 bit1,0 3:min 2:sec
  rtc_regWR(RTC_TMR,   tmr);  // カウント値
}

// VLビット読込み
bool rtc_chkvl() { return (rtc_regRD(RTC_SEC) & 0x80); }

// RTCのタイマー値を読込む
int rtc_gettmr() { return rtc_regRD(RTC_TMR); }

// RTCのストップビットクリア
void rtc_stpclr() { rtc_regWR(RTC_CTR1, 0x00); }

// RTCの指定レジスタからデータを読込む
uint8_t rtc_regRD(uint8_t reg) {
  Wire.beginTransmission(RTC_ADR);
  Wire.write(reg);
  Wire.endTransmission();
  Wire.requestFrom(RTC_ADR, 1);
  return Wire.read();
}

// RTCのアクセスするレジスタを書込む
void rtc_regWR(uint8_t reg, uint8_t value) {
  Wire.beginTransmission(RTC_ADR);
  Wire.write(reg);
  Wire.write(value);
  Wire.endTransmission();
}

void rtc_regSET(uint8_t reg, int num, uint8_t *data) {
  Wire.beginTransmission(RTC_ADR);
  Wire.write(reg);
  Wire.write(data, num);
  Wire.endTransmission();
}
 
void rtc_regGET(uint8_t reg, int num, uint8_t *data) {
  Wire.beginTransmission(RTC_ADR);
  Wire.write(reg);
  Wire.endTransmission();
  Wire.requestFrom(RTC_ADR, num);
   
  for (int i = 0; i < num; i++) {
    i2c_wait();
    data[i] = Wire.read();
  }
}
 
// RTCから時刻(RT構造)を読込みUnixTimeを得る
time_t rtc_getUXTM() {
  rtc_getRT();
  return rt2uxt();
}

// NTPから時刻を取得しRTCにセット
boolean rtc_setNTP() {
  /* WiFi接続:未接続はRTC時刻を更新しない */
  if (wifi_connect(60000)) {
    if (ntp_connect(10000)) {
      /* NTP時刻 -> UXTM,RTC */
      UXTM = time(NULL);
      rtc_init(UXTM);
      return true;
    }
  }
  //wifi_stop();  // stopしない方が稼働時間短縮
  return false;
}

// UnixTimeからRTC時刻(RT構造)を得る (UnixTime->tm構造->RT構造)
void uxt2rt(time_t uxtm) {
  struct tm *t = localtime(&uxtm);
  RT.year = (t->tm_year + 1900) % 100; // tm_year: 1900年から
  RT.mon  = t->tm_mon + 1;             // tm_mon : 0-11
  RT.wday = t->tm_wday;
  RT.day  = t->tm_mday;
  RT.hour = t->tm_hour;
  RT.min  = t->tm_min;
  RT.sec  = t->tm_sec;
}

// RTC時刻(RT構造)からUnixTimeを得る (RT構造->tm構造->UnixTime)
time_t rt2uxt() {
  struct tm t;
  t.tm_year = RT.year + 100 ;  // tm_year: 1900年から
  t.tm_mon  = RT.mon- 1;       // tm_mon : 0-11
  t.tm_wday = RT.wday;
  t.tm_mday = RT.day;
  t.tm_hour = RT.hour;
  t.tm_min  = RT.min;
  t.tm_sec  = RT.sec;
  t.tm_isdst = -1;             // 夏時間設定を無効化

  return mktime(&t);
}

///// WiFi Control /////
/* WiFi接続開始処理(ノンブロッキング風)
 *   <引数> Timeout: 接続制限時間(0 or 省略は接続するまで無限ループ)
 *   <戻値> 接続:true 未接続:False
 */
boolean wifi_connect() { return wifi_connect(0); }
boolean wifi_connect(int timeout) {
  // 初期化
  unsigned long start = 0;
  unsigned long last  = 0;   // ノンブロッキング用時間変数
  unsigned long now   = 0;
  unsigned long itvl  = 500; // クライアント再接続時のインターバル時間

  // WiFi 初期化
  wifi_init();

  // WiFiが未接続の場合のみbeginする
  if (WiFi.status() != WL_CONNECTED) {
    WiFi.begin(WIFI_SSID, WIFI_PASS);
    delayy(100);
  }

  // WiFi接続(指定時間内) 接続:true 接続失敗:false
  start = millis();
  /* タイムアウトチェック */
  while (timeout==0 || millis() - start < timeout) {
    // WiFi接続は非同期(ノンブロッキング)風で
    now = millis();
    if (now-last >= itvl) {
      last = now;
      // 接続チェック
      if (WiFi.status() == WL_CONNECTED) {
        return true;
      }
      // 未接続の場合,0.3-0.5sec おきに再接続
      itvl = random(300, 500);
    }
    yield();
  }
  // 接続不可 エラーリターン
  return false;
}

/* WiFi 初期化処理 */
boolean wifi_init() {
  // WiFi初期化 (順番大事)
  //   disconnect(true)されて再接続の場合 mode,configの再設定が必須
  WiFi.mode(WIFI_STA);                      // 1.WiFi モード設定
  WiFi.persistent(false);                   // 2.WiFi設定情報 保存しない
  if (!WiFi.config(ip, gw, mask, dns)) {    // 3.固定ip 設定
    /* DHCPフォールバック(DHCP切替え処理) */
    if (!WiFi.config(0U, 0U, 0U)) {
      return false;
    }
  }
  delayy(100);
  return true;  
}

/* WiFi stop処理 */
void wifi_stop() {
  // WiFiが接続の場合のみdisconnectする
  if (WiFi.status() == WL_CONNECTED) {
    WiFi.disconnect(true);
  }
}

///// NTP Control /////
/* NTP 同期処理(ノンブロッキング風)
 *   <引数> Timeout[ms]:同期制限時間(0 or 省略は同期するまで無限ループ)
 *   <戻値> Unixtime or 0(エラー)
 */
time_t ntp_connect() { return ntp_connect(0); }
time_t ntp_connect(int timeout) {
  // WiFi接続している時のみ時間取得する
  if (WiFi.status() != WL_CONNECTED) {
    return 0;
  }

  // 初期化
  unsigned long start;
  unsigned long itvl = 250; // NTP再接続時のインターバル時間
  unsigned long last = 0;   // ノンブロッキング用時間変数

  // タイムゾーン,NTPサーバー設定
  configTzTime("JST-9", NTP_SRV1, NTP_SRV2, NTP_SRV3);
  /* 最初に一定時間待機すると同期時にループが省かれる(短時間化) */
  delayy(50);

  // NTPサーバー接続 (timeout秒間 接続確認)
  start = millis();
  while (timeout==0 || millis()-start < timeout) {
    // NTP接続は非同期(ノンブロッキング)風で
    if (millis()-last >= itvl) {
      last = millis();
      // 現在時刻の取得
      time_t uxtm  = time(NULL);        // unix時間
 
      // NPT同期チェック (RTC初期値=1,000,000,000)
      if(uxtm>1000000000) {
        return uxtm;
      }
      // 未接続の場合,0.2-0.3sec おきに再接続
      itvl = random(200, 300);
    }
    yield();
  }

  // 同期不可 エラーリターン
  return 0;
}

///// Other Control /////
/* BCD Control */
inline int dec2bcd(int dec) {
  return (((dec / 10) << 4) | (dec % 10));
}
 
inline int bcd2dec(int bcd) {
  return ((bcd >> 4) * 10 + (bcd & 0x0f));
}

/* yield delay(ノンブロッキング ディレイ) */
inline void delayy(unsigned long tm) {
  unsigned long now = millis();
  while (millis()-now < tm) { yield();}
}

(2025.10.25 v0.0.4 曜日表示のバグ修正)

<初期設定>
インクルードファイルや変数の初期化を行なっています。インストールすべきライブラリはInstalled Libraryにまとめてあります。時刻を操作するrtcTime構造はグローバルな変数RTとして定義し各関数でポイント渡しをしなくて済むようにしています。

<setup>
各デバイスの初期化終了後タイトルを表示、RTC8564のVLビットをチェックして初回起動ならばWiFiに接続しNTPから時刻を取得してRTC8564にセットしています。

<loop>
温湿度、気圧を測定しRTC8564から読み込んだ時刻と測定データをOLEDに表示、LEDを反転したら1秒待機、を繰り返します。

<関数>
・i2c関連
・OLED関連
・RTC8564関連
WiFi関連
・NTP関連
・その他
でそれぞれまとめてあります。

・動作確認

ハード、ソフトともに出来上がった時計へ自家製電源で電源を供給してみると...

やり〜。一発動作です♪
センサーを指で温めると温湿度が変わりますのでうまい具合に動いてくれているようです。

順調、順調。
電流値は平時標準的な20mA〜80mAでの推移でした。


◯やってみて

まずは第一ステップ完了です。
RTC8564とRX8025はどちらもエプソン製のせいかピン配列が同じなので(って秋月電子さんの温かい配慮のおかげですね)そのまま差し替えがききます。現状のハードウェアのままRX8025だけを差し替えてRX8025専用スケッチの製作に入っていきたいと思います。

モーションセンサー(人感センサー)の電池長寿命化 (2) Wroom02スケッチ編

◯やりたいこと

(1)で作成したモーションセンサーのWroom02(WR02)用のスケッチです。


◯やったこと

以下がWR02用のスケッチです。
ATtiny85(AT85)と同様にデータ保存にリングバッファ(RB)を取り入れWR02特有のものとしてエンキュー・デキュー用のアドレスポインタにRTCメモリを使っています。RTCメモリは通電さえしていればディープスリープしても内容が保持されるRAMなので書込み制限を気にしなくて済み気が楽です。またWiFi周りでノンブロッキング化を少々進めてWiFi接続性能の向上を図っています。

/**
*
*    WR02_MOS v0.1.2 (ok)
*      AT85からUARTで送られてくるデータをWiFiでSQLに送信する
*
**/
// BSP(Board Support Package)
//   ESP8266 Boards v3.1.2
//     FlushSize ESP01:1MB Wroom023D:2MB Wroom02:4MB

// Built-in Library (Arduino IDE v1.8.19)
#include            <Arduino.h>
#include            <time.h>
#include            <EEPROM.h>
#include            <HardwareSerial.h>

// BSP Built-in Library
#include            <ESP8266WiFi.h>
extern "C" {
  #include          "user_interface.h"
}

// Installed Library

// Global Variable
/* WiFi */
#define WIFI_SSID   "YOUR_SSID"
#define WIFI_PASS   "YOUR_PASS"

/* TCP/IP */
IPAddress ip        (YOUR_IP);            // ex. (192,168,0,2)
IPAddress gw        (YOUR_GW);
IPAddress dns       (YOUR_DNS);           // Google:(8,8,8,8)
IPAddress mask      (YOUR_MASK);          // ex. (255,255,255,0)

/* UART */
#define UART_TX     1
#define UART_RX     3
HardwareSerial      hs(0);

/* Reset */
#define RES_EN      2

/* SQL Server IP */
#define SQL_HOST    "SQL_IP"
#define SQL_PORT    SQL_PORT

/* NTP DATA */
#define NTP_SRV1    "133.243.238.244"     // "ntp.nict.jp"
#define NTP_SRV2    "ntp.jst.mfeed.ad.jp"

/* RTC Memory */
#define RTC_CODE    1234
#define RTC_TOP     64                    // 
#define RTC_END     192                   // 
#define RTC_SIZE    RTC_END-RTC_TOP       // 128個(512byte)

#define ADR_CODE    RTC_TOP
#define ADR_RBT     RTC_TOP+1
#define ADR_RBE     RTC_TOP+2

/* EEPROM */
#define EEP_SIZE    2048                  // EEPROM size
#define RB_MAX      EEP_SIZE/MD_SIZE      // RB(リングバッファ)最大値

/* Measurement DATA */
int     MD_SIZE =   sizeof(time_t);       // time_t(4byte)
#define MD_MAX      125                   // UART受信可能なデータ数
time_t  MDATA       [MD_MAX];

/* Time */
time_t   UXTM =     0;                    // UnixTime
uint32_t STTM =     0;                    // スタート時間 milli()

/* Function Prototype */

///// ハードウエア 初期設定
void setup() {
  // Start time
  STTM = millis();

  // pinモード設定
  digitalWrite(RES_EN, LOW);
  pinMode     (RES_EN, OUTPUT);

  // IRセンサー リセット禁止
  ir_resDE();

  // UART
  /* シリアル起動, ESP8266起動時レポートの送信待ち */
  hs.begin(19200); delay(200);

  /* ESP8266起動時レポート用の改行送信 */
  hs.print('\n'); delay(5);

  // EEPROM
  EEPROM.begin(EEP_SIZE);

  // RTC memory
  rtcm_chk();
}

void loop() {
  // ESP8266起動時レポートのATtiny側での処理待ち
  delay(1000);

  // 初期値
  String  line = "";

  // Xon送信後 ATからの NTP,SOH待ち
  //while (hs.available()>0) {char d = hs.read();}
  unsigned long tout = millis();
  while (millis() - tout < 3000) {
    hs.print("Xon\n");
    line = read_line(2000);

    /* データ転送 mode */
    if (line.equals("SOH")) break;

    /* 時刻転送 mode */
    if (line.equals("NTP")) {
      /* WiFi接続 */;
      if (wifi_connect(60000)) {
        if (ntp_connect(10000)) {
          /* NTP時刻 ->UART ->AT85 */
          UXTM = time(NULL);
        }
      }
      hs.println(UXTM);    // NTP->AT85
      delay(5);      wifi_end();
    }
    delay(100);
  }

  // 返信なしはスリープ
  if (line.length()<=0) wifi_end();

  // 受信開始(受信データはメモリに格納->高速処理)
  int mdcnt = 0;
  unsigned long timeout = millis();
  while (millis() - timeout < 5000) {
    line = read_line(1500);

    /* 受信タイムアウト */
    if      (line.length()<=0)   break;

    /* 全データ受信終了 */
    else if (line.equals("EOT")) break;

    /* 受信データ格納 */
    else MDATA[mdcnt] = line.toInt();
    mdcnt ++;
  }

  // MD->RB保存(EEPROMにゆっくり格納)
  for (int i=0; i<mdcnt; i++) rb_eque(MDATA[i]);

  // RBにデータがあればデータ送信へ
  int num = rb_num();
  if (num>0) {
    // WiFi接続チェック(接続成功は データ送信)
    if (wifi_connect(60000)) {
      /* リングバッファにデータがあれば送信 */
      while (true) {
        /* データがなければ終了 */
        UXTM = rb_peek();
        if (!UXTM)          break;
        /* 送信失敗は終了 */
        if (!md_send(UXTM)) break;
        /* 送信成功でキューポインタをinc */
        rb_dque();
        delayy(100);
     }
    }
  }
  // 送信終了
  wifi_end();
}

///// IR Control ////
// IRセンサー許可
inline void ir_resEN() { digitalWrite(RES_EN, HIGH);}

// IRセンサー禁止
inline void ir_resDE() { digitalWrite(RES_EN,  LOW);}

///// UART Control /////
// 1行読込(タイムアウト付)
String read_line(int timeout) {
  String line = "";
  unsigned long tout = millis();
  while (millis() - tout < timeout) {
    if (hs.available() > 0) {
      line = hs.readStringUntil('\n');
      line.trim();
      return line;
    }
  }
  return "";
}

///// WiFi Control /////
/* WiFi接続開始処理
 *   <引数> Timeout: 接続制限時間(0 or 省略は接続するまで無限ループ)
 *   <戻値> 接続:true 未接続:False
 */
boolean wifi_connect() { return wifi_connect(0); }
boolean wifi_connect(int timeout) {
  unsigned long start = millis();
  unsigned long now   = start;
  //int retry = 0;

  // WiFi 初期化
  wifi_init();

  // WiFiが未接続の場合のみbeginする
  if (WiFi.status() != WL_CONNECTED) {
    WiFi.begin(WIFI_SSID, WIFI_PASS);
    delayy(500);
  }

  // WiFi接続(指定時間内) 接続:true 接続失敗:false
  while (WiFi.status() != WL_CONNECTED) {
    if (timeout>0 && millis()-start >= timeout) return false;
    delayy(500);
  }
  return true;
}

/* WiFi 初期化処理 */
boolean wifi_init() {
  // WiFi初期化 (順番大事)
  //   disconnect(true)されて再接続の場合 mode,configの再設定が必須
  WiFi.mode(WIFI_STA);                      // 1.WiFi モード設定
  WiFi.persistent(false);                   // 2.WiFi設定情報 保存しない
  if (!WiFi.config(ip, gw, mask, dns)) {    // 3.固定ip 設定
    delayy(100);
    return false;
  }
  delayy(100);
  return true;  
}

/* WiFi stop処理 */
void wifi_stop() {
  // WiFiが接続の場合のみdisconnectする
  if (WiFi.status() != WL_CONNECTED) {
    WiFi.disconnect(true);
  }
}

/* 終了処理 */
void wifi_end() {
  // WiFi切断
  wifi_stop();
  // Uptime 表示
//@@  hs.print("WR02(");
//@@  hs.print((millis()-STTM)/1000.0); 
//@@  hs.println("sec)");

  // Reset 許可
  ir_resEN();
  
  // sleep
  ESP.deepSleep(0);
  //ESP.deepSleep(60*1000*1000);
  delay(100);
}

boolean md_send(time_t uxtm) {

  ~ ここでデータを加工し送信する

}

///// NTP Control /////
/* NTP 同期処理(ノンブロッキング風)
 *   <引数> Timeout[ms]:同期制限時間(0 or 省略は同期するまで無限ループ)
 *   <戻値> Unixtime or 0(エラー)
 */
time_t ntp_connect() { return ntp_connect(0); }
time_t ntp_connect(int timeout) {
  // WiFi接続している時のみ時間取得する
  if (WiFi.status() != WL_CONNECTED) {
    return 0;
  }

  // 初期化
  unsigned long now;
  unsigned long start;
  unsigned long itvl = 250; // NTP再接続時のインターバル時間
  unsigned long last = 0;   // ノンブロッキング用時間変数

  // タイムゾーン,NTPサーバー設定
  configTzTime("JST-9", NTP_SRV1, NTP_SRV2, NTP_SRV3);
  /* 最初に一定時間待機すると同期時にループが省かれる(短時間化) */
  delayy(50);

  // NTPサーバー接続 (timeout秒間 接続確認)
  start = millis();
  while (millis()-start < timeout) {
    // NTP接続はノンブロッキング風(非同期)で
    now = millis();
    if (now-last >= itvl) {
      last = now;
      // 現在時刻の取得
      time_t uxtm  = time(NULL);        // unix時間
 
      // NPT同期チェック
      if(uxtm>1755000000) {
        return uxtm;
      }
      // 未接続の場合,0.3-0.5sec おきに再接続
      itvl = random(300, 500);
    }
    delayy(1);
  }

  // 同期不可 エラーリターン
//@@  hs.println("Connection failed");
  return 0;
}

///// Time Controle /////
// 日時文字列"yyyy-mm-dd hh:mm:ss"を unixtimeに変換
time_t dat2uxt(String str) {
  // String->char 変換
  int len = str.length()+1;
  char chr[len];
  str.toCharArray(chr, len);

  // 日時文字列を tm構造に格納
  struct tm t;
  sscanf(chr, "%d-%d-%d %d:%d:%d",
    &t.tm_year, &t.tm_mon, &t.tm_mday,
    &t.tm_hour, &t.tm_min, &t.tm_sec);
  t.tm_year -= 1900;
  t.tm_mon -= 1;

  // 上記 tm構造から unixtimeを求めてリターン
  return mktime(&t);
}

// unixtimeを日時文字列"yyyy-mm-dd hh:mm:dd"に変換
String uxt2dat(time_t t) {
  char date[64];
  // フォーマット作成
  size_t strftime(char *s, size_t max, const char *format, const struct tm *tm);

  // unixtime->日時文字列
  strftime(date, sizeof(date), "%Y-%m-%d %H:%M:%S", localtime(&t));
  return date;
}

///// Ringbuffer Control /////
// RBポインタ inc
inline int rb_inc(int idx) {
  if (++idx >= RB_MAX) return 0;
  else return idx;
}

// RB内のデータ数を求める
int rb_num() {
  /* RTC->RBtop,RBend */
  int rbt = rtcm_get(ADR_RBT);
  int rbe = rtcm_get(ADR_RBE);
  if (rbt > rbe) rbe += RB_MAX;
  return rbe - rbt;
}

// RBポインタから EEPROMのアドレスを求める
inline int rb_idx2adr(int idx) {return idx * MD_SIZE;}

// エンキュー(enqueue) val->EEP
void rb_eque(time_t val) {
  /* RTC->RBend */
  int rbe = rtcm_get(ADR_RBE);

  /* val->EEP */
  int adr = rb_idx2adr(rbe);
  EEPROM.put(adr, val);
  EEPROM.commit();
  /* RBend inc */
  rbe = rb_inc(rbe);
  rtcm_put(ADR_RBE, rbe);
}

// デキュー(dequeue) EEP->val
time_t rb_dque() {
  /* RTC->RBtop,RBend */
  int rbt = rtcm_get(ADR_RBT);
  int rbe = rtcm_get(ADR_RBE);
  if (rbt==rbe) return false;

  /* EEPROM->val */
  time_t val;
  int adr = rb_idx2adr(rbt);
  EEPROM.get(adr, val);
  /* RBtop inc */
  rbt = rb_inc(rbt);
  rtcm_put(ADR_RBT, rbt);
  return val;
}

// peek(非破壊読み出し)
time_t rb_peek() {
 /* RTC->RBtop,RBend */
  int rbt = rtcm_get(ADR_RBT);
  int rbe = rtcm_get(ADR_RBE);
  if (rbt==rbe) return false;

  /* EEPROM->val */
  time_t val;
  int adr = rb_idx2adr(rbt);
  EEPROM.get(adr, val);
  return val;
}

///// RTCmemory(ESP8266) Control /////
// RTCメモリ 初期化チェック
void rtcm_chk() {
  int code = rtcm_get(ADR_CODE);
  if (code!=RTC_CODE) {
//@@    ss.println("RTC"); delay(2);
    /* RTCメモリ ヘッダクリア */
    rtcm_put(ADR_CODE, RTC_CODE);
    rtcm_put(ADR_RBT , 0);
    rtcm_put(ADR_RBE , 0);
  }
}

// RTCメモリに書込む
bool rtcm_put(uint32_t addr, uint32_t val) {
  return system_rtc_mem_write(addr, &val, sizeof(val));
}

// RTCメモリから読出す
uint32_t rtcm_get(uint32_t addr) {
  uint32_t val = 0;
  system_rtc_mem_read(addr, &val, sizeof(val));
  return val;
}

///// Other Control /////
/* yield delay(ノンブロッキング ディレイ) */
inline void delayy(unsigned long tm) {
  unsigned long now = millis();
  while (millis()-now < tm) { yield();}
}

<初期定義>
WR02の起動はAT85のデータFullによるリセット信号により行われます。
初期定義では必要なモジュールのインクルードや各種定数・グローバル変数を定義、別途インストールが必要なモジュールは<Installed Library>にまとめてあります。ちなみにボードの指定が間違っていると<BSP Built-in Library>でエラーが発生します。

<setup>
WR02、シリアル、EEPROM、RTCメモリの設定を行います。シリアル設定では厄介なESP8266の起動メッセージを送信し終わるまで200msほど無駄に待機させています。さらにその起動メッセージをAT85が処理し新規に1行待ち状態にするために追加で"\n"を送信しています。実を言うとこれに気がつくまでどうしてもハンドシェイクがうまくいかず結構ハマってしまっていました。

<loop>
loopは
・ハンドシェイク用にAT85にXonを送信、一定時間返信がなければスリープ
・AT85からの返信がNTPならばNTPから得たUnixtimeを送信しスリープ
・ 〃 SOHならばデータ受信モードに入る
・受信ロストを避けるためEOTまでのデータを一旦メモリに格納
・受信したデータをRBに格納
WiFiに接続しデータを送信
・データ送信に失敗した場合は次回送信用にRBデータは未消去のまま
・スリープ
という流れになっています。 モーターや高周波ノイズなどで一時的にWiFi通信ができなかった場合の再送信に備えデータは非破壊読み出しで取得しています。RBに残っていれば次回WiFi接続時に全送信されます。もちろん送信に成功すればデキューされデータは削除されます。RBはこの辺の処理が簡単なので助かります。
実はこの方式にしてからロガーからのデータ欠損率が劇的に減少していて、データの取得率はほぼ100%に上昇しています。ノンブロッキング化との相乗効果かもしれません。なんだもっと早く気がついていればよかったと少々後悔していたところです。

<関数>
関数は以下のグループでまとめてあります。
・IR(赤外線センサー)
・UART
WiFi
・NTP
・Datatime
・RB(リングバッファ)
・RTCメモリ
・その他
何だかんだいってスケッチがかなり長文化。関数は他のスケッチと共用できるものも多くライブラリ化すればいいのでしょうがIDEのライブラリ化はいまだ挑戦したことがありません。サブルーチンの感覚で本体に付随させているのでダラダラと長いスケッチになっています。スケッチの読みにくさ、素人ということで御勘弁です。

〇やってみて

yield()を使ったノンブロッキング化は結構いい感じで動いてくれています。RBもZ80時代を思い出しながら楽しくスケッチの組換えに取り組むことができました。何十年たっても若いころ取り組んでいたものはちょっとやるとすぐに思い出します。やはり若い頃は無理してでもいろいろやっておいた方がいいもんだといまさらながら感じてます。

モーションセンサー(人感センサー)の電池長寿命化 (2) AT85スケッチ編

◯やりたいこと

(1)で作成したモーションセンサーのAT85用のスケッチです。


◯やったこと

以下がAT85 用のスケッチです。AT85用だけでも結構のサイズになりました。
特徴としては測定データの格納方法にリングバッファ方式を取り入れた事でしょうか。EEPROM全域を使ってROMの長寿命化に多少役立てています。ただリングバッファのポインタは毎回同じところに書き込まれるのであまり意味なさそうではあるのですが...
ちなみにリングバッファのデータ出し入れのことをエンキュー、デキューと言うという事を今回初めて知りました。💦

/*
*
*    AT85_MOS_RTC v0.1.1
*      AT85版モーションセンサー 発生時刻をrtc8564から取得しEEPROMに記録
*                      指定回数測定後 Wroom02にUARTで送信
*
*      CPU  Wroom-02   ATtiny85     RTC8564,mos
*           Vcc        (pin8)Vcc    Vcc
*                      (pin7)SCL -> SCL
*           RES     <- (pin6)PB1
*                      (pin5)SDA <> SDA
*           GND        (pin4)GND    GND
*           TX      -> (pin3)RX
*           RX      <- (pin2)TX
*       IRセンサー&&IO2 -> (pin1)RES
*
*/

// BSP(Board Support Package)
//   ATtiny25/45/85(No bootloader) v1.5.2

// Built-in Library (Arduino IDE v1.8.19)
#include            <Arduino.h>
#include            <EEPROM.h>

// BSP Built-in Library
#include            <avr/sleep.h>

// Installed Library
#include            <TinyWireM.h>
#include            <SoftwareSerial.h>

// User defined
/* ATtiny */
typedef uint32_t    time_t;               // time_t(4byte)定義

// Global Variable
/* RESET */
#define RES         1                     // PB1(pin6) リセット要求ピン

/* UART */
#define UART_TX     3                     // PB3(pin2)
#define UART_RX     4                     // PB4(pin3)
SoftwareSerial      ss(UART_RX, UART_TX);

/* Measurement DATA */
#define MD_MAX      10                    // [回]  何回分をまとめて送信するか
#define MD_ITVL     10                    // [sec] 測定間隔を最低何秒あけるか
time_t  MDATA       [MD_MAX];
int     MD_SIZE =   sizeof(MDATA);        // time_t(4byte)

/* EEPROM */
#define EEP_CODE    1234                  // EEPROM 初期化確認code
#define EEP_SIZE    512                   // EEPROM サイズ

#define ADR_CODE    EEP_SIZE- 4
#define ADR_RBT     EEP_SIZE- 8           // RB(リングバッファ)の読出し先ポインタ
#define ADR_RBE     EEP_SIZE-12           // RBの書出し先ポインタ

#define RB_MAX      (EEP_SIZE-12)/MD_SIZE // RBの最大値

/* RTC8564 */
#define RTC_ADR     0x51                  // I2Cアドレス
#define RTC_CTR1    0x00                  // RTC CTRL1レジスタ
#define RTC_CTR2    0x01
#define RTC_SEC     0x02
#define RTC_TMRC    0x0E
#define RTC_TMR     0x0F

struct rtcTime {                          // RTC用データ構造
  uint8_t second;                         //  0-59 sec
  uint8_t minute;                         //  0-59 min
  uint8_t hour;                           //  0-23 hour
  uint8_t day;                            //  1-31 day
  uint8_t month;                          //  1-12 mon
  uint8_t year;                           //  00-199
  uint8_t week;                           //  0(Sun)-6(Sat)
};
rtcTime RT =        {0,0,0,1,1,0,0};      // 時間操作用 アキュームレータ

/* Time */
time_t   UXTM =     0;                    // UnixTime
uint32_t STTM =     0;                    // スタート時間 milli()

/* Function Prototype */

///// ハードウエア 初期設定
void setup() {
  // Start time
  STTM = millis();

  // ATtiny85
  /* 8MHzモードに設定 */
  CLKPR   =  0x80;
  CLKPR   =  0x00;        // ÷1 8MHz

  /* 内部オシレータ微調整 */
  //   UARTで文字化けしない値を設定 
  //OSCCAL=  74;          // 初期値: 74(0x4A)
  //OSCCAL=  64;          // 正常動作値 5870

  /* WDTを停止 */
  WDTCR  |=  (1<<WDCE) | (1<<WDE); // WDT変更モード
  WDTCR   =  0x00;        // WDT 無効化

  /* ADCを停止 */
  ACSR   |=  (1<<ACD);    // アナログコンパレータ 無効化 (ACSR |=_BV(ACD);)
  ADCSRA &= ~(1<<ADEN);   // ADC 無効化 (ADCSRA &=~_BV(ADEN);) 
  PRR    |=  (1<<PRADC);  // ADC 電力カット

  /* USIを停止 */
  USICR   =  0x00;        // USIのすべての機能を無効化

  /* タイマー1のクロック停止 */
  PRR    |=  (1<<PRTIM1); // Timer1 無効化 PWM,各種割込み

  /* sleep mode */
  set_sleep_mode(SLEEP_MODE_PWR_DOWN);

  // UART
  /* シリアル起動, ESP8266起動時レポートの送信待ち */
  ss.begin(19200);
  delay(200);

  // タイトル,OSC 表示
//@@  ss.print("\nAT85 start(");
//@@  ss.print("OSC 0x");
//@@  ss.print(OSCCAL, HEX);
//@@  ss.print(')\n');

  // pinモード
  digitalWrite(RES, HIGH);
  pinMode(RES, OUTPUT);

  // I2C
  TinyWireM.begin();
  delay(200);

  // EEPROM 初期化チェック
  int code;
  EEPROM.get(ADR_CODE, code);
  if (code!=EEP_CODE) {
//@@    ss.println("EEP"); delay(2);
    /* EEPROM ヘッダクリア */
    EEPROM.put(ADR_CODE, EEP_CODE);
    EEPROM.put(ADR_RBT , 0);
    EEPROM.put(ADR_RBE , 0);
  }

  // RTC VLビットチェック, 時刻合わせチェック
  /* VL on */
  if (rtc_chkvl()) {
//@@    ss.print("VL on\n"); delay(3);

    /* ESP8266 リセット要求(min 100ns) */
    digitalWrite(RES, LOW);
    delay(1);
    digitalWrite(RES, HIGH);

    /* ESP8266 起動待ち */
    urcv_xon();

    /* 時刻要求コマンド送信 */
    ss.print("NTP\n");
    delay(2);

    /* 時刻受信待ち */
    String line = urcv_str(60000);
    if (line.length()<=0) mos_stop();
    UXTM = line.toInt();

    /* VL安定待ち(1sec) */
    if ((millis()-STTM)<1000) delay(1);

    /* UXTM -> RTCレジスタ */
    uxt2RT(UXTM);    // UnixTime->RT構造
    rtc_setRT();     // RT構造->RTCレジスタ
  }

  /* VL off */
//@@  else { ss.print("VL off\n"); delay(3); }

}

///// メインループ
void loop() {
  // RTC時刻 ->RT ->UXTM
  rtc_getRT();        // RTCレジスタ->RT構造
  UXTM = RT2uxt();    // RT構造->UnixTime

  // 前回起動時刻取得
  time_t uxtm = rb_pque();

  // 指定時間未経過は記録しない
  if ((UXTM-uxtm)<MD_ITVL) {
    mos_stop();
    return;
  }

  // 現在時刻(UnixTime) EEPROM書込み
  rb_eque(UXTM);

  // 格納データの送信タイミングチェック
  int num = rb_num();
//@@  ss.print(num, DEC); ss.print("\n"); delay(2);

  /* 指定回数 or 1日経過で送信 */
  if (num>=MD_MAX || (UXTM-uxtm)>=24UL*60UL*60UL) {
    // ESP8266 リセット要求(min 100ns)
    digitalWrite(RES, LOW);
    delay(1);
    digitalWrite(RES, HIGH);

    // ESP8266 起動待ち
    urcv_xon();

    // データ送信
    /* データブロック開始符号 */
    ss.print("SOH\n");
    delay(2);

    /* データ */
    while (true) {
      UXTM = rb_dque();
      if (!UXTM) break;
      ss.print(UXTM, DEC);
      ss.print('\n');
      delay(5);
    }
    /* データブロック終了符号 */
    ss.print("EOT\n");
    delay(2);
  }
  // 終了処理
  mos_stop();
}

void mos_stop() {
  // sleep 処理
//@@  ss.print("AT85 sleep(");
//@@  ss.print((millis()-STTM)); 
//@@  ss.println("ms)");
//@@  delay(10);
  sleep_enable();
  sleep_cpu();
  delay(100);
  /* 無限ループ(先頭に戻らないよう担保) */
  while (true) {delay(10);}
}

///// UART Control /////
// UARTで Xonを受信する
boolean urcv_xon() { return urcv_xon(0);}
boolean urcv_xon(int timeout) {
  unsigned long now = millis();
  while (true) {
    String str = ss.readString();
    //String str = ss.readStringUntil('\n');  // 88byte多い
    str.trim();
    if (str.equals("Xon")) return true;
    if (timeout>0 && millis()-now<=timeout) return false;
    delay(1);
  }
}

// UARTで1行受信する
String urcv_str() { return urcv_str(0);}
String urcv_str(int timeout) {
  unsigned long now = millis();
  while (true) {
    String str = ss.readString();
    //String str = ss.readStringUntil('\n');  // 88byte多い
    str.trim();
    if (str.length() > 0) return str;
    if (timeout>0 && millis()-now<=timeout) return "";
    delay(1);
  }
}

///// I2C Control /////
// i2cのデータが確定するまで待つ
inline void i2c_wait() { while(TinyWireM.available()<1); }

///// RTC8564 Control /////
// VLビット読込み
bool rtc_chkvl() { return (rtc_regRD(RTC_SEC) & 0x80); }

// RT-> RTC RT構造からRTCレジスタへ時刻をセット
void rtc_setRT() {
  // stop ビットセット
  rtc_regWR(RTC_CTR1, 0x20);

  // RT構造上の時刻データをBCDに変換してセット
  rtc_regWR(RTC_SEC,     dec2bcd(RT.second));
  rtc_regWR(RTC_SEC + 1, dec2bcd(RT.minute));
  rtc_regWR(RTC_SEC + 2, dec2bcd(RT.hour));
  rtc_regWR(RTC_SEC + 3, dec2bcd(RT.day));
  rtc_regWR(RTC_SEC + 4, dec2bcd(RT.week));

  uint8_t month = dec2bcd(RT.month);
  uint8_t year  = RT.year;

  // CENTURY補正
  if (year > 100) {
    month |= 0x80;
    year  -= 100;
  }
  rtc_regWR(RTC_SEC + 5, month);
  rtc_regWR(RTC_SEC + 6, dec2bcd(year));

  // stop ビットクリア
  rtc_regWR(RTC_CTR1, 0x00);
  delay(100);
}

// RTC->RT RTCのレジスタから時刻を読出しRT構造にセット
int rtc_getRT() {
  RT.second = bcd2dec(rtc_regRD(RTC_SEC    ) & 0x7F);
  RT.minute = bcd2dec(rtc_regRD(RTC_SEC + 1) & 0x7F);
  RT.hour   = bcd2dec(rtc_regRD(RTC_SEC + 2) & 0x3F);
  RT.day    = bcd2dec(rtc_regRD(RTC_SEC + 3) & 0x3F);
  RT.week   = bcd2dec(rtc_regRD(RTC_SEC + 4) & 0x07);

  uint8_t month = rtc_regRD(RTC_SEC + 5);
  RT.month  = bcd2dec(month & 0x1F);
  RT.year   = bcd2dec(rtc_regRD(RTC_SEC + 6));

  // CENTURY補正
  if (month & 0x80) RT.year += 100;
  //ss.print("rtc_getRT: ");
  //prt_rt(&RT);

  return 0;
}

// RTCの指定レジスタからデータを読込む
uint8_t rtc_regRD(uint8_t reg) {
  TinyWireM.beginTransmission(RTC_ADR);
  TinyWireM.send(reg);
  TinyWireM.endTransmission();
  TinyWireM.requestFrom(RTC_ADR, 1);
  i2c_wait();
  return TinyWireM.read();
}

// RTCのアクセスするレジスタを書込む
void rtc_regWR(uint8_t reg, uint8_t value) {
  TinyWireM.beginTransmission(RTC_ADR);
  TinyWireM.send(reg);
  TinyWireM.send(value);
  TinyWireM.endTransmission();
  delay(10);
}

///// UnixTime Controle /////
// UnixTimeからRT構造を得る (UnixTime->RT構造)
void uxt2RT(time_t uxtm) {
  uint32_t seconds = uxtm;
  RT.second = seconds % 60;
  seconds   /= 60;
  RT.minute = seconds % 60;
  seconds   /= 60;
  RT.hour   = seconds % 24;
  uint16_t days = seconds / 24;

  // 年・月・日を計算(1970年から)
  uint16_t year = 1970;
  while (true) {
    bool leap = (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0));
    uint16_t daysInYear = leap ? 366 : 365;
    if (days < daysInYear) break;
    days -= daysInYear;
    year++;
  }
  RT.year = year - 2000;

  const uint8_t daysInMonth[] = {31,28,31,30,31,30,31,31,30,31,30,31};
  uint8_t month = 0;
  while (true) {
    uint8_t dim = daysInMonth[month];
    if (month == 1 && (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0))) dim++; // うるう年
    if (days < dim) break;
    days -= dim;
    month++;
  }
  RT.month = month + 1;
  RT.day   = days + 1;
}

// うるう年判定
bool chk_leap(uint16_t year) {
  return (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0));
}

// RT構造からUnixTimeを得る (RT構造->UnixTime)
unsigned long RT2uxt() {
  const uint16_t dayMon[] = {31,28,31,30,31,30,31,31,30,31,30,31};
  uint16_t year = RT.year + 2000;
  unsigned long days = 0;

  // 年の積算
  for (uint16_t y = 1970; y < year; y++) {
    days += chk_leap(y) ? 366 : 365;
  }

  // 月の積算
  for (uint8_t m = 1; m < RT.month; m++) {
    days += dayMon[m - 1];
    if (m == 2 && chk_leap(year)) days += 1;
  }

  // 日の加算
  days += RT.day - 1;

  // 秒換算
  return (days      * 86400UL +
          RT.hour   * 3600UL +
          RT.minute * 60UL +
          RT.second);
}

///// Ringbuffer(RB) Control /////
// RBポインタ inc
inline int rb_inc(int idx) {
  if (++idx >= RB_MAX) return 0;
  else return idx;
}

// RBポインタ dec
inline int rb_dec(int idx) {
  if (idx == 0) return RB_MAX;
  return --idx;
}

// RB内のデータ数を求める
int rb_num() {
  int rbt, rbe;
  EEPROM.get(ADR_RBT, rbt);
  EEPROM.get(ADR_RBE, rbe);
  if (rbt > rbe) rbe += RB_MAX;
  return rbe - rbt;
}

// RBポインタから EEPROMのアドレスを求める
inline int rb_idx2adr(int idx) {return idx * MD_SIZE;}

// エンキュー(enqueue) val->EEP
void rb_eque(time_t val) {
  /* EEP->RBend */
  int rbe;
  EEPROM.get(ADR_RBE, rbe);

  /* val->EEP */
  int adr = rb_idx2adr(rbe);
  EEPROM.put(adr, val);
  /* RBend inc */
  rbe = rb_inc(rbe);
  EEPROM.put(ADR_RBE, rbe);
}

// デキュー(dequeue) EEP->val
time_t rb_dque() {
  /* EEP->RBtop,RBend */
  int rbt, rbe;
  EEPROM.get(ADR_RBT, rbt);
  EEPROM.get(ADR_RBE, rbe);
  if (rbt==rbe) return 0;

  /* EEP->val */
  time_t val;
  int adr = rb_idx2adr(rbt);
  EEPROM.get(adr, val);
  /* RBtop inc */
  rbt = rb_inc(rbt);
  EEPROM.put(ADR_RBT, rbt);
  return val;
}

// 前のキュー読出し(previous queue) EEP->val
time_t rb_pque() {
  /* EEP->RBend */
  int rbe;
  EEPROM.get(ADR_RBE, rbe);
  rbe = rb_dec(rbe);

  /* EEP->val */
  time_t val;
  int adr = rb_idx2adr(rbe);
  EEPROM.get(adr, val);
  return val;
}

///// Other Control /////
/* BCD Control */
inline int dec2bcd(int dec) {
  return (((dec / 10) << 4) | (dec % 10));
}
 
inline int bcd2dec(int bcd) {
  return ((bcd >> 4) * 10 + (bcd & 0x0f));
}

/* 指定されたrtcTime構造を年月日-時分秒形式で表示 */
void prt_rt (rtcTime *rt) {
  ss.print(rt->year);
  ss.print("/");
  ss.print(rt->month);
  ss.print("/");
  ss.print(rt->day);
  ss.print(" ");
  ss.print(rt->hour);
  ss.print(":");
  ss.print(rt->minute);
  ss.print(":");
  ss.println(rt->second);
  //ss.println(rt->week);
  delay(20);
}


<初期定義>
AT85の起動はモーションセンサーのリセット信号により行われます。
ここでは必要なモジュールのインクルードや各種定数・グローバル変数を定義してます。別途インストールが必要なモジュールは<Installed Library>にまとめてあり、ボードの指定が間違っていると<BSP Built-in Library>でエラーが発生します。
なお電池寿命に関連する定数は以下の二つです。
MD_MAX: AT85での測定を何回分まとめてWR02に送るかの設定
MD_ITVL: 前回測定時間から次のセンス時間までに最低限あける秒数

<setup>
AT85、シリアル、i2c、EEPROMなどの設定を行い最後にRTCのVLビットをチェックしてオンならばWR02に現在時刻の要求を行います。Xon受信後NTPを返信するとWR02はNTPで取得したUnixtimeを返信してくるのでそれをRTCにセットします。

今回はデバッグ用に表示させていたタイトルが文字化けしたためOSCCALの値を変更して動作する値に追い込んでいます。当初38400ボーより9600ボーの方が文字化けが増えるという現象に悩まされいろいろと対策を取ってみたのですが、結局はAT85の周波数ズレという事で落ち着きました。デフォルト値74に対しうまく表示されたのが58〜70だったのでその中間値64をセットしておきました。この辺はAT85を使うにあたり少々めんどくさい点でしょうか。

<loop>
loopの全体的な流れを箇条書きにすると
・現在時刻が前回センス時間より指定時間以上離れていれば現在時刻をエンキュー
・指定された回数のデータがたまればWR02にリセット信号を送信
・Xon,SOHでハンドシェイク
・リングバッファ内のデータを全てデキューして送信
・スリープ

という感じです。リングバッファはデータの出し入れ管理が楽でとても助かります。

RTCやUnixtimeの制御はrtcTime構造を持つグローバルな「RT」を定義しそれをZ80のアキュームレータのような使い方をして操作しています。要は各関数はRTのポインタ渡しを行なわず直接RTを読み書きしています。そのため関数の使い方に順番管理が必要になります。

<各関数>
各関数はコメントを比較的マメに書いてましたのでそちらの方でご理解をいただくという事で。大きく次の項目に分かれています。
・USRT コントロール
・I2C 〃
・RTC8564 〃
・Unixtime 〃
・RB(リングバッファ) 〃
・その他

timeライブラリが使えないためUnixtime時間の変換は真面目に計算して求めていたり、年月日時分秒を表示するのにポインタ渡しのprint文羅列で表示させていたりしているところがひと手間必要な部分でした。

◯やってみて

できるまでは結構大変でしたが動き出したら快調に動いています。ふ〜。
スケッチのメモリ使用率は84%、RAM使用率は44%となりました。

ちなみにAT85は製造が中止されたとの事で値段が結構上がっています。なぜかRTCも500円から800円(秋月電子)へ爆上がりしていました。8564は確か一番リーズナブルなチップのはずだったんですが...
どうやら使用するチップの選択にも気を配らないと出費が増える時代に入っているようです。

モーションセンサー(人感センサー)の電池長寿命化 (1) ATtiny85でRTC8564を制御する

◯やりたいこと

特定の場所の利用状況をチェックするためESP8266で実験をしているモーションセンサー、

電源を乾電池(エネループ)で動作させているのですが、赤外線感知の度にESP8266をリセット起動・WiFi接続する方式のため感知数が多い場合あっという間に電池がなくなってしまうという問題が発生していました。

そこでBME280の温湿度・気圧ロガーで電池の長寿命化を図ったように

greensoybean.hatenablog.com

モーションセンサーでもハードを追加して電池の長寿命化を図ることにしました。

◯やったこと

実験してきたモーションセンサーは
ブレッドボード版(ver.0)

電池ボックス版(v1)

プラスチックケース版(v2)

と進化を続けてきました。
今回はAT85とRTCの力を借りて電池の長寿命化を図るバージョン、ver.3の開発です。

・回路図の決定

電池の持ちをよくするためデータが一定程度たまったらWR02を起動してUARTでデータを送信しWiFiSQLに転送するという前回と同様の方式をとります。

ただこの時困るのが感知した時刻の保存方法。今まではSQLに転送されてきた時刻をセンス時間としてデータに保存していたのですが、AT85側にデータを保管するとなるとAT85側で時刻データを保存しなければなりません。そこでRTC8564(以下RTC)を追加しそこから取得したUnixtimeをデータとしてEEPROMに保管することにしました。モーションセンサーひとつにRTCを使うという超贅沢な設計ですが、ここでAT85によるRTCの制御ノウハウを身につけとけば後々役に立つだろうと皮算用してます。
回路図は以下の通りです。

アクティブHのモーションセンサー出力を微分してエッジを作成し、FETのAND回路を通してAT85にリセットをかけます。ANDを通しているのはWR02がデータ送信などの作業中にリセットがかからないようにするためです。
AT85は指定回数のセンスを終了後WR02をリセット起動させ格納している時刻データをUARTで送信、WR02はそのデータを有効利用します。私の場合はSQLに送信し先に掲載したようにグラフデータに利用しています。

・スケッチの開発

最初にAT85でRTCをコントロールするスケッチの作成に入ります。早速ですがこの間作ったWR02によるAT85のライターが活躍します。

ここでAT85特有の調整(OSCのキャリブレーションなど)を行い、加えてRTCの制御系のスケッチが完全に動作するようになってからWR02ライターに載せ替えWR02のスケッチを開発していきます。

でもあいかわずAT85のスケッチ開発は難しいですね。UARTラインはAT85、WR02、CH9102の三つ巴をダイオードORでコントロールし各チップの出力を可視可能にしておかなければとても正解にたどり着く事ができませんでした。特に今回使用したAT85のOSCのずれが大きくUART通信のボーレートが合わなかったためだいぶ苦労しました。

スケッチの詳細についてはAT85とWR02の2つで結構大きくなりますので次回で解説したいと思います。

・動作確認

スケッチを開発したブレッドボードにモーションセンサーを取り付け、既存モーションセンサーの隣に置いて動作・比較試験を行っていきます。

1日動作さてみたところ旧型の検知回数が158回、新型が366回、新型は10秒以内の検知を無効としているにもかかわらず何と2倍も検知回数が多く出てました。おそらく検知のたびにWiFiを起動している旧型より、サッと立ち上がってサッと終了する新型の方が次の測定までの準備期間が短いせいなんでしょう。
それでいてWiFiの送信回数は158回に対し37回と1/4へ減少。精度が上がって消費電力は減る、こりゃ思った以上に効果が期待できるかもしれません。

・製作

動作確認が取れたのでさっそく製作に入ります。ケースはver2のものを流用しますので同じように単3電池サイズのユニバーサルボードに回路を組み上げます。

ボードにはムリクリMCP1640のDC-DCコンバータを載せてみました。
ハンダ面はまだ途中ですがこんな感じ。

センサー信号からリセットパルスを作成する回路はボード上に載らなかったのでモーションセンサーと一体型にしときました。

これにそれぞれのチップをセットして

ケースに組み上げます。

新型なのにケースがすでに日に焼けて古ぼけて見えるのは...見ないことにしてます。

◯やってみて

試験ラン1.5ヶ月10000回の測定で

現在単3電池2本の電圧が2.92v。使っているDCDCコンバータが入力0.65vまで動作可能な優秀品で、動作終了時の電池電圧が1vを切っていることを考慮するとまだ半分くらいはある勘定でしょうか。
とてもいい感じで動いてくれています。

WROOM-02でATtiny85の書き込み機(ライター)を作る

◯やりたいこと

WROOM02(以下WR02)をISPプログラマーとしたATtiny85(以下AT85)用の書込み機を製作する。


◯やったこと

・ESP8266 as Arduino ISPプロジェクトのダウンロード

前回作成したarduino UNOで作成したAT85の書込み機、

greensoybean.hatenablog.com

便利は便利でいいんですがUNOのガタイが大きくてどうも取り回しに不便を感じることが少々。USB端子も今時はやりのtype-Cではありませんし...

そこで何とかもっとコンパクトに作れないものかとAIに聞いたところやりたいことにピッタリのプロジェクトを紹介されました。

github.com
(作成された方に感謝!)

なんとありがたい。
さっそくダウンロードしてライターの製作に入ります。

・WR02ライターを作成・スケッチの書込み

ダウンロードしたスケッチ(8266asISP.ino)はまずWR02に書き込まなくてはなりません。そこでWR02の書込み機から製作を始めます。

最近マイブームの秋月電子から購入したCH9102Fモジュールとアマゾンで購入したESP01ブレークアウトボードを利用し、オートリセット機能を持たせたWR02書込み機をブレッドボードに組み上げます。このUSBモジュールは後にAT85の書込み用COMポートになりますのでムダにはなりません。

そこへWR02のシンプル版モジュールをセットしボードをesp8266に指定してダウンロードしたスケッチを書き込みます。

ちなみにあえて書込み機を作らなくても他のライターで書き込んだものをここにセットしても問題ありません。

・AT85への書込み

AT85へ書き込めるようにするためESP8266 as Arduino ISPプロジェクトのreadmeに書いてある通りにWR02とAT85を結線します。ただ私の場合RST信号にIO2を使用すると起動時のモード設定とかち合うのか動作が不安定になったのでIO5に変更して使用しています。

WR02 > AT85
SCK (IO14) > pin 7 (SCK)
MISO (IO12) > pin 6 (MISO)
MOSI (IO13) > pin 5 (MOSI)
RST (IO5) > pin 1 (RESET)
GND > pin 4
3.3v > pin 8

この時注意しなければならないのがWR02のリセット端子に入れるコンデンサ

コレを入れることによりWR02のオートリセットパルスがなまって無効化されるためISPプログラムが中断することなく動作してUSBモジュールからのデータがAT85に書き込まれます。ただしこれによりWR02への書込みができなくなる点は注意です。
ちなみに回路にオートリセット機能を載せていない場合はコンデンサは不要です。

あとは簡単、いつも通りIDEでスケッチを作成しボードにattiny85、シリアルポートにWR02につながっているUSBモジュールのCOMポート、書込装置にarduino as ISPを指定して書き込めばオーケー。順調です。

◯やってみて

さっそくLチカのスケッチをAT85に書き込んでみました。

もちろんバッチリ動作。
順調にいくとやはりうれしいですね。

今回無事ブレッドボードに書込み機を作ることができましたので前回書込み機と違い簡単に実験回路を作ることが可能になりました。ダイオードOR回路を組み込むのも簡単です。AT85のスケッチをオンライン開発できるようになるまであともう一歩です。

◯注意点

8266asISP.inoをWR02で動作させる場合WeMos用からWR02用へ変更した方がいい点がいくつかあります。私はソフトの権利関係に詳しくなくスケッチの中身を勝手に変えていいのかがわからないので、何気にWR02用に変更した方がいいかな〜と感じる部分を文章で書き出しておきます。

・WeMos用のD4,D5,D6,D7指定はESP8266 用のIO番号に変更
・デフォルトで設定されているSPI端子名称(SS,MISO,MOSI,SCK)はそのまま使用せず実際に使うIO番号を指定

スケッチ先頭のifdef文をAIにコピペして「esp8266利用した場合のスケッチに変更」してもらい10行ほどにまとめてしまうという裏ワザもありかもしれません。世の中便利が加速し続けてホント驚きます。

ATtiny85を書込み時の抜差しなしでスケッチ開発する

◯やりたいこと

Wroom-02で「書込みながら、走らせながらをモジュールの抜差しをせずに」スケッチ開発したようにATtiny85(以下AT85)でも同じ事をできるようにしたい。


◯やったこと

・キット購入

aitendoのHPを見ていたら面白そうなキットを見つけました。

基盤のパターンを追ってみるとどうもAT85の書き込み機能を持ちながら書込み後そのまま走らせてスケッチのチェックができそうな感じ。これはすぐに実験してみなくてはとさっそくキットを買ってみました。


・ホントに可能か動作チェック

書込みながら終了後すぐにスケッチを走らせる場合注意しなければならないのが信号線のコンフリクト。ISP書込み時はSCK,MOSI,SSの3ラインはプッシュプル出力となるため、AT85に開発用のi2c機器がつながっていると間違いなく出力の衝突がおこります。しかもつながるi2cはマスターあり、スレーブあり。そこでダイオードOR(DTL7432:AND=反転入力NOR)で衝突を防止させることにしました。
書込み用スケッチのarduinoISPをチェックしてみると書込み終了後は3端子ともフローティング(入力)にセットされるようなので書込み時だけ注意すればよいようです。

注意点として信号OR用のダイオードは対象信号がSPIのため若干早めのダイオードが必要。今回使ったのは秋月で売っていた小信号用のスイッチングダイオード1N4148で、これを使って実際に動作するかどうかを確認してみます。

ブレッドボードに回路を組み上げ

UNOに接続して

Lチカを書込み。
すると

おーバッチリ。キチンと書込み後に書き込んだスケッチが動いています。回路的にも部品的にも問題はないようです。

・製作

回路や部品に間違いがないことがわかったのでさっそく製作に入ります。キットに付属してきた基盤を

これから作る回路に合わせて魔改造

これにAT85をのせ

部品をのせると

魔改造完了。

ちなみにですが、スケッチアップロード時に発生するarduinoのオートリセットパルスを無効化するためのコンデンサをリセット端子に入れておかないと書込み時に失敗する様です。


◯やってみて

完成品にLチカを書き込んでみると

バッチリチカチカ。順調に動いています。

「フッフ、これでAT85も抜差しせずにスケッチを開発できる。」
そう考えたらなんか思わず頬がゆるみます。

次はこのシールドにのせる孫シールドの製作です。
・UARTのUSB変換モジュールをのせ常時シリアル出力ができるシールド
・3.3Vレベル変換、Grove端子を持たせたi2c開発シールド
etc...
どんどん夢がふくらみます。

◯やってみて2

手元にあったUSBミニ変換モジュール、

リセット線をダイオードORで分離するとそのまま孫シールドになる事が判明。
さっそくLチカとシリアル出力のスケッチを書き込んでみると

バッチリ動作。
シリアル出力も普通にできました。

AT85デバッグに役立つ強力なアイテムを1つ追加です。

おわり

電池で動かすBME280温湿度計(5) 1年稼働目指して実機製作 スケッチ編

◯やりたいこと

(4)で製作したロガーのスケッチ編。

◯やったこと

・スケッチの作成

今回作成したスケッチはAT85用とWR02用の2つがあります。ミソは2つをつなぐUART通信。私のシリアル通信の理解力はカンサス・サッポロシティスタンダードのようなカセットインターフェースで挑戦してたのが最初、その後8251などでRS232Cをイタズラしていたのが最後ぐらいのレベルです。IDEではATP3012、GPS受信機以来。双方向のスケッチ作成は久しぶりです。

※今回スケッチ作成にはAIを多用しました。AIが参照したであろう参照元の先輩諸氏の皆様方に感謝いたします。
※我流スケッチ 読みにくさ御免。

・AT85のスケッチ

 /**
*
*    AT85_BME280 v0.5.1 (ok)
*      BME280の測定結果を一定回数保存後 Wroom02へUARTで送信
*
**/

// BSP(Board Support Package)
//   ATtiny25/45/85(No bootloader) v1.5.2

// Built-in Library (Arduino IDE v1.8.19)
#include            <Arduino.h>
#include            <EEPROM.h>
#include            <SoftwareSerial.h>

// BSP Built-in Library
#include            <avr/sleep.h>

// Installed Library
#include            <TinyWireM.h>         // v1.1.3

// User defined
/* ATtiny */
typedef uint32_t    time_t;               // time_t(4byte)定義

// Global Variable
/* RESET */
#define RES         1                     // PB1(pin6) リセット要求ピン

/* UART */
#define UART_TX     3                     // PB3(pin2)
#define UART_RX     4                     // PB4(pin3)
SoftwareSerial      ss(UART_RX, UART_TX);

/* Measurement DATA */
#define MD_MAX      6                     // [回]  何回分をまとめて送信するか
struct  MDATA {
  int32_t  tmp;                           // 温度 ℃
  uint32_t hum;                           // 湿度 %
  uint32_t prs;                           // 気圧 Hpa
             };
int     MD_SIZE =   sizeof(MDATA);        // long(4byte)x3

/* EEPROM */
#define EEP_CODE    1234                  // EEPROM 初期化確認code
#define EEP_SIZE    512                   // EEPROM サイズ

#define ADR_CODE    EEP_SIZE- 4
#define ADR_RBT     EEP_SIZE- 8           // RB(リングバッファ)の読出し先ポインタ
#define ADR_RBE     EEP_SIZE-12           // RBの書出し先ポインタ

#define RB_MAX      (EEP_SIZE-12)/MD_SIZE // RBの最大値

/* BME280 */
#define BME280_ADR  0x76
uint32_t            hum_raw,  pres_raw;
int32_t             temp_raw, t_fine;
uint16_t            dig_T1;
int16_t             dig_T2, dig_T3;
uint16_t            dig_P1;
int16_t             dig_P2, dig_P3, dig_P4, dig_P5;
int16_t             dig_P6, dig_P7, dig_P8, dig_P9;
int8_t              dig_H1, dig_H3, dig_H6;
int16_t             dig_H2, dig_H4, dig_H5;

/* Time */
time_t   UXTM =     0;                    // UnixTime
uint32_t STTM =     0;                    // スタート時間 milli()

/* Function Prototype */

///// ハードウエア 初期設定
void setup() {
  // Start time
  STTM = millis();

  // ATtiny85
  /* 8MHzモードに設定 */
  CLKPR   =  0x80;
  CLKPR   =  0x00;        // ÷1 8MHz

  /* 内部オシレータ微調整 */
  //   UARTで文字化けしない値を設定 
  //OSCCAL=  74;          // 初期値: 74(0x4A)
  //OSCCAL=  64;          // 正常動作値 5870

  /* WDTを停止 */
  WDTCR  |=  (1<<WDCE) | (1<<WDE); // WDT変更モード
  WDTCR   =  0x00;        // WDT 無効化

  /* ADCを停止 */
  ACSR   |=  (1<<ACD);    // アナログコンパレータ 無効化 (ACSR |=_BV(ACD);)
  ADCSRA &= ~(1<<ADEN);   // ADC 無効化 (ADCSRA &=~_BV(ADEN);) 
  PRR    |=  (1<<PRADC);  // ADC 電力カット

  /* USIを停止 */
  USICR   =  0x00;        // USIのすべての機能を無効化

  /* タイマー1のクロック停止 */
  PRR    |=  (1<<PRTIM1); // Timer1 無効化 PWM,各種割込み

  /* sleep mode */
  set_sleep_mode(SLEEP_MODE_PWR_DOWN);

  // UART
  /* シリアル起動, ESP8266起動時レポートの送信待ち */
  ss.begin(19200);
  delay(200);

  // タイトル,OSC 表示
//@@  ss.print("\nAT85 start(");
//@@  ss.print("OSC 0x");
//@@  ss.print(OSCCAL, HEX);
//@@  ss.print(')\n');

  // pinモード
  digitalWrite(RES, HIGH);
  pinMode(RES, OUTPUT);

  // I2C
  TinyWireM.begin();
  delay(200);

  // EEPROM 初期化チェック
  int code;
  EEPROM.get(ADR_CODE, code);
  if (code!=EEP_CODE) {
    /* EEPROM ヘッダクリア */
    EEPROM.put(ADR_CODE, EEP_CODE);
    EEPROM.put(ADR_RBT , 0);
    EEPROM.put(ADR_RBE , 0);
  }

  // BME280 初期化:強制モード
  writeReg(0xF2, 0x03);     // n/a(5bit),湿度オーバーサンプリングx3(3bit)
  writeReg(0xF4, 0x25);     // 温度OSx1(3bit),気圧OSx1(3bit),mode:強制(2bit)
  writeReg(0xF5, 0xA0);     // スタンバイ1sec(3bit),フィルタoff(3bit),SPI(2bit)

  // センサー測定時間待ち
  delay(120);
}

///// メインループ
void loop() {
  MDATA    md;              // 測定データ格納先
  int32_t  tmp, prs;        // long: 4byte
  uint32_t hum;             // long: 4byte

  // BME280 データ読込
  readTrim();               // 校正データ 読込
  readData();               // 測定データ 読込

  // BME280 データ校正
  tmp = calibration_T(temp_raw);
  prs = calibration_P(pres_raw);
  hum = calibration_H(hum_raw) ;

  // 受信データ格納
  md.tmp = tmp;
  md.hum = hum;
  md.prs = prs;
  rb_eque(&md);             // データ エンキュー
  
  // 格納データのWiFi送信タイミングチェック
  int num = rb_num();
  ss.println(num, DEC); delay(2);

  /* 指定回数経過で送信 */
  if (num>=MD_MAX) {
    // ESP8266 リセット要求(min 100ns)
    digitalWrite(RES, LOW);
    delay(1);
    digitalWrite(RES, HIGH);

    // ESP8266 起動待ち
    urcv_xon();
    //if (!urcv_xon(10000)) mos_stop();

    // データ送信
    /* データブロック開始符号 */
    ss.print("SOH\n");
    delay(2);

    /* データ */
    while (true) {
      /* データ デキュー なければ終了 */
      if (!rb_dque(&md)) break;
      /* 1測定分送信 */
      ss.println(md.tmp);
      delay(3);
      ss.println(md.hum);
      delay(3);
      ss.println(md.prs);
      delay(4);

      /* 1測定分のデータ区切り符号 */
      ss.print("ETX\n");
      delay(2);
    }
    /* データブロック終了符号 */
    ss.print("EOT\n");
    delay(2);
  }

  // タイマー0のクロック停止
  PRR |= (1<<PRTIM0);    // Timer0 無効化 SoftwareSerial,millis

  // 終了処理
  sen_stop();
}

void sen_stop() {
  sleep_enable();
  sleep_cpu();
  delay(100);
  /* 無限ループ(先頭に戻らないよう担保) */
  while (true) {delay(10);}

  //delay(3000);  
}

///// UART Control /////
// UARTで Xonを受信する
boolean urcv_xon() { return urcv_xon(0);}
boolean urcv_xon(int timeout) {
  uint32_t now = millis();
  while (true) {
    String str = ss.readString();
    //String str = ss.readStringUntil('\n');  // 6byte多い
    str.trim();
    if (str.equals("Xon")) return true;
    if (timeout>0 && millis()-now<=timeout) return false;
    delay(1);
  }
}

///// BME280 Control /////
// 校正値読出し
void readTrim() {
  uint8_t data[32], i = 0;
  TinyWireM.beginTransmission(BME280_ADR);
  TinyWireM.send(0x88);
  TinyWireM.endTransmission();
  TinyWireM.requestFrom(BME280_ADR, 24);
  while (TinyWireM.available()) {
    data[i] = TinyWireM.read();
    i++;
  }
  TinyWireM.beginTransmission(BME280_ADR);
  TinyWireM.send(0xA1);
  TinyWireM.endTransmission();
  TinyWireM.requestFrom(BME280_ADR, 1);
  data[i] = TinyWireM.read();
  i++;
  TinyWireM.beginTransmission(BME280_ADR);
  TinyWireM.send(0xE1);
  TinyWireM.endTransmission();
  TinyWireM.requestFrom(BME280_ADR, 7);
  while (TinyWireM.available()) {
    data[i] = TinyWireM.read();
    i++;
  }
  dig_T1 = (data[ 1] << 8) | data[ 0];
  dig_T2 = (data[ 3] << 8) | data[ 2];
  dig_T3 = (data[ 5] << 8) | data[ 4];
  dig_P1 = (data[ 7] << 8) | data[ 6];
  dig_P2 = (data[ 9] << 8) | data[ 8];
  dig_P3 = (data[11] << 8) | data[10];
  dig_P4 = (data[13] << 8) | data[12];
  dig_P5 = (data[15] << 8) | data[14];
  dig_P6 = (data[17] << 8) | data[16];
  dig_P7 = (data[19] << 8) | data[18];
  dig_P8 = (data[21] << 8) | data[20];
  dig_P9 = (data[23] << 8) | data[22];
  dig_H1 =  data[24];
  dig_H2 = (data[26] << 8) | data[25];
  dig_H3 =  data[27];
  dig_H4 = (data[28] << 4) | (0x0F & data[29]);
  dig_H5 = (data[30] << 4) | ((data[29] >> 4) & 0x0F);
  dig_H6 =  data[31];
}

// 測定値読出し
void readData() {
  int i = 0;
  uint32_t data[8];
  TinyWireM.beginTransmission(BME280_ADR);
  TinyWireM.send(0xF7);
  TinyWireM.endTransmission();
  TinyWireM.requestFrom(BME280_ADR, 8);
  while (TinyWireM.available()) {
    data[i] = TinyWireM.receive();
    i++;
  }
  pres_raw = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4);
  temp_raw = (data[3] << 12) | (data[4] << 4) | (data[5] >> 4);
  hum_raw  = (data[6] <<  8) |  data[7];
}

// レジスタ書込み
void writeReg(uint8_t reg_address, uint8_t data) {
  TinyWireM.beginTransmission(BME280_ADR);
  TinyWireM.send(reg_address);
  TinyWireM.send(data);
  TinyWireM.endTransmission();
}

// 温度校正
int32_t calibration_T(int32_t adc_T) {
  int32_t var1, var2, T;
  var1 = (((( adc_T >> 3) - ((int32_t)dig_T1 << 1))) * ((int32_t)dig_T2)) >> 11;
  var2 = (((((adc_T >> 4) - ((int32_t)dig_T1)) * ((adc_T >> 4) - ((int32_t)dig_T1))) >> 12) * ((int32_t)dig_T3)) >> 14;
  t_fine = var1 + var2;
  T = (t_fine * 5 + 128) >> 8;
  return T;
}

// 気圧校正
uint32_t calibration_P(int32_t adc_P) {
  int32_t  var1, var2;
  uint32_t P;
  var1 = (((int32_t)t_fine) >> 1) - (int32_t)64000;
  var2 = (((var1 >> 2) * (var1 >> 2)) >> 11) * ((int32_t)dig_P6);
  var2 = var2 + ((var1 * ((int32_t)dig_P5)) << 1);
  var2 = (var2 >> 2) + (((int32_t)dig_P4) << 16);
  var1 = (((dig_P3 * (((var1 >> 2) * (var1 >> 2)) >> 13)) >> 3) + ((((int32_t)dig_P2) * var1) >> 1)) >> 18;
  var1 = ((((32768 + var1)) * ((int32_t)dig_P1)) >> 15);
  if (var1 == 0) {
    return 0;
  }
  P = (((uint32_t)(((int32_t)1048576) - adc_P) - (var2 >> 12))) * 3125;
  if (P < 0x80000000) {
    P = (P << 1) / ((uint32_t) var1);
  } else {
    P = (P / (uint32_t)var1) * 2;
  }
  var1 = (((int32_t)dig_P9) * ((int32_t)(((P >> 3) * (P >> 3)) >> 13))) >> 12;
  var2 = (((int32_t)(P >> 2)) * ((int32_t)dig_P8)) >> 13;
  P = (uint32_t)((int32_t)P + ((var1 + var2 + dig_P7) >> 4));
  return P;
}

// 湿度校正
uint32_t calibration_H(int32_t adc_H) {
  int32_t v_x1;
  v_x1 = (t_fine - ((int32_t)76800));
  v_x1 = (((((adc_H << 14) - (((int32_t)dig_H4) << 20) - (((int32_t)dig_H5) * v_x1)) +
            ((int32_t)16384)) >> 15) * (((((((v_x1 * ((int32_t)dig_H6)) >> 10) *
                (((v_x1 * ((int32_t)dig_H3)) >> 11) + ((int32_t) 32768))) >> 10) + (( int32_t)2097152)) *
                ((int32_t) dig_H2) + 8192) >> 14));
  v_x1 = (v_x1 - (((((v_x1 >> 15) * (v_x1 >> 15)) >> 7) * ((int32_t)dig_H1)) >> 4));
  v_x1 = (v_x1 < 0 ? 0 : v_x1);
  v_x1 = (v_x1 > 419430400 ? 419430400 : v_x1);
  return (uint32_t)(v_x1 >> 12);
}

///// Ringbuffer Control /////
// RBポインタ inc
inline int rb_inc(int idx) {
  if (++idx >= RB_MAX) return 0;
  else return idx;
}

// RB内のデータ数を求める
int rb_num() {
  int rbt, rbe;
  EEPROM.get(ADR_RBT, rbt);
  EEPROM.get(ADR_RBE, rbe);
  if (rbt > rbe) rbe += RB_MAX;
  return rbe - rbt;
}

// RBポインタから EEPROMのアドレスを求める
inline int rb_idx2adr(int idx) {return idx * MD_SIZE;}

// エンキュー(enqueue) ptr->EEP
void rb_eque(struct MDATA *ptr) {
  /* EEP->RBend */
  int rbe;
  EEPROM.get(ADR_RBE, rbe);
  
  /* ptr->EEPROM */
  int adr = rb_idx2adr(rbe);
  EEPROM.put(adr   , *ptr);
  /* RBend inc */
  rbe = rb_inc(rbe);
  EEPROM.put(ADR_RBE, rbe);
}

// デキュー(dequeue) EEP->ptr
boolean rb_dque(struct MDATA *ptr) {
  /* EEP->RBtop,RBend */
  int rbt, rbe;
  EEPROM.get(ADR_RBT, rbt);
  EEPROM.get(ADR_RBE, rbe);
  if (rbt==rbe) return false;

  /* EEPROM->ptr */
  int adr = rb_idx2adr(rbt);
  EEPROM.get(adr   , *ptr);
  /* RBtop inc */
  rbt = rb_inc(rbt);
  EEPROM.put(ADR_RBT, rbt);
  return true;
}

(2025.09.16 v.0.5.1 へバージョンアップ)

<スケッチのおおまかな流れ>
スケッチの流れを箇条書きで説明しますと
(start)
・10分毎のRTCリセットパルスで起動
(setup)
・省電力化のため不要な機能を全て停止
・ピン設定、i2c起動、EEPROM初期化
・スリープモード設定(PWR_DOWN)
(loop)
BME初期化
・温湿度・気圧を測定しEEPROMに書き込み
・指定回数の測定が終了していればWR02にリセット要求
・WR02とハンドシェイク (Xon,SOH)
・保存してあったデータを一気に送信 (data,data..ETX,data,data..ETX,..EOT)
・省電力化を追加してスリープ
(chk_eep)
・ EEPROMが初回アクセスの場合初期値をセット
(readTrim)
BMEからキャリブレーションデータを読込む
(readData)
BMEから測定データを読込む
(writeReg)
BMEレジスタにデータを書込む
(calibration_T,P,H)
BME測定値を校正する

となってます。

スケッチ開発で結構大変だったのがスケッチサイズをいかに小さくするかでした。そのため
BME用のモジュールは使わずレジスタを直接アクセス
・数字はfloat型を使わず全てint型
・EEPROMで文字・文字列は扱わない
などを行っています。

ちなみにシリアル通信でASCIIのコントロールコードもどきの文字列が出てきますがASCIIコードとは全く関係ありません。単なる任意の3文字です。ABCでもNHKでも自分の好きな文字列に変更が可能です。

(2025.09.16 v0.5.1)
・リングバッファを利用してEEPROMの書込み回数を平準化

・WR02のスケッチ

/*
*    (AT_WR) WR02_BME280 v0.5.5 (ok)
*      CPU       : WROOM-02 (ESP8266)
*      通信先    : ATtiny85
*/

// BSP(Board Support Package)
//   ESP8266 Boards v3.1.2
//     FlushSize ESP01:1MB Wroom02D:2MB Wroom02:4MB

// Built-in Library (Arduino IDE v1.8.19)
#include            <Arduino.h>
#include            <time.h>
#include            <Wire.h>
#include            <EEPROM.h>
#include            <HardwareSerial.h>

// BSP Built-in Library
#include            <ESP8266WiFi.h>
extern "C" {
  #include          "user_interface.h"
}

// Installed Library

// Global Variable
/* WiFi */
#define WIFI_SSID     "YOUR_WiFiSSID"
#define WIFI_PASS     "YOUR_WiFiPASS"

/* TCP/IP */
IPAddress ip          (YOUR_IP);          // ex. (192,168,0,2)
IPAddress gw          (YOUR_GW);
IPAddress dns         (YOUR_DNS);
IPAddress mask        (YOUR_MASK);        // ex. (255,255,255,0)

/* UART */
#define UART_TX     1
#define UART_RX     3
HardwareSerial      hs(0);

/* I2C */
#define I2C_SDA     2
#define I2C_SCK     0
#define I2C_CLK     100000                // fSCL=100KHz

/* NTP DATA */
#define NTP_SRV1    "ntp.nict.jp"
#define NTP_SRV2    "ntp.jst.mfeed.ad.jp"

/* Measurement DATA */
#define MD_MAX      32                    // UART受信可能な最大データ数 
#define MD_ITVL     10                    // [分] 測定間隔
struct  MDATA {
  float tmp;                              // 温度 ℃
  float hum;                              // 湿度 %
  float prs;                              // 気圧 Hpa
             };
int     MD_SIZE =   sizeof(MDATA);        // float(4byte)x3
MDATA   MD          [MD_MAX];

/* RTC Memory */
#define RTC_CODE    1234
#define RTC_TOP     64                    // 
#define RTC_END     192                   // 
#define RTC_SIZE    RTC_END-RTC_TOP       // 128個(512byte)

#define ADR_CODE    RTC_TOP
#define ADR_RBT     RTC_TOP+1
#define ADR_RBE     RTC_TOP+2

/* EEPROM */
#define EEP_SIZE    2048                  // EEPROM size
#define RB_MAX      EEP_SIZE/MD_SIZE      // RB(リングバッファ)最大値

/* RTC8564 */
#define RTC_ADR     0x51                  // I2Cアドレス
#define RTC_CTR1    0x00                  // RTC CTRL1レジスタ
#define RTC_CTR2    0x01
#define RTC_SEC     0x02
#define RTC_TMRC    0x0E
#define RTC_TMR     0x0F

struct rtcTime {
  uint8_t second;                         // 0-59
  uint8_t minute;                         // 0-59
  uint8_t hour;                           // 0-23
  uint8_t day;                            // 1-31
  uint8_t month;                          // 1-12
  uint8_t year;                           // 00-199
  uint8_t weekday;                        // 0(Sun)-6(Sat)
};
rtcTime RT =        {0,0,0,1,1,0,0};

/* Time */
time_t   UXTM =     0;                    // UnixTime
uint32_t STTM =     0;                    // スタート時間 milli()

///// ハードウエア 初期設定
void setup() {
  // Start time
  STTM = millis();

  // UART
  /* シリアル起動, ESP8266起動時レポートの送信待ち */
  hs.begin(19200); delay(200);

  /* ESP8266起動時レポート用の改行送信 */
  hs.print('\n'); delay(5);

  /* タイトル表示 */
//@@  hs.println("\nESP8266 ready!"); delay(10);

  /* WiFi */
  //WiFi.setOutputPower(10); // [dBm] max 20.5dBm

  // I2C
  Wire.begin(I2C_SDA, I2C_SCK);
  Wire.setClock(I2C_CLK);
  delay(10);

  // EEPROM
  EEPROM.begin(EEP_SIZE);

  // RTC memory
  rtcm_chk();

  // RTC VLビットチェック
  /* VL on */
  if (rtc_chkvl()) {
    // RTC8564 安定化待ち,初期化
    if ((millis()-STTM)<1000) delay(1);
    rtc_init();

    // NTP時刻をRTCにセット WiFi->NTP->UXTM->RT->RTC
    rtc_setNTP();
  }

  /* VL off */
  else {
    /* RTC時刻->dt, RTCタイマーセット */
    rtc_init();
    rtc_getRT();

    /* 定時時刻合わせチェック */
    if(RT.day==1 && RT.hour==1 && (RT.minute/10)==3) {
      // NTP時刻をRTCにセット WiFi->NTP->UXTM->RT->RTC
      rtc_setNTP();
    }
  }

  // 正時に起動するようタイマーセット
  if ((RT.minute % MD_ITVL)!=0) {
    int itvl = MD_ITVL-(RT.minute % MD_ITVL);
    //if (RT.second>=59) itvl--;
    rtc_setTMR(itvl);
    wifi_end();
  }
  else{
  }
}

///// メインルーチン
void loop() {
  // ESP8266起動時レポートのATtiny側での処理待ち
  delay(1500);

  // 初期値
  String  line = "";

  // Xon送信後 ATからのヘッダ(SOH)待ち
  //while (hs.available()>0) {char d = hs.read();}
  unsigned long tout = millis();
  while (millis() - tout < 3000) {
    hs.println("Xon"); delay(2);
    line = read_line(2000);

    /* データ転送 mode */
    if (line.equals("SOH")) break;
    delay(100);
  }

  // 返信なしはスリープ
  if (line.length()<=0) {
    wifi_end();
  }

  // 受信開始(受信データはメモリに格納=高速処理)
  int mdcnt = 0;
  int mecnt = 0;
  unsigned long timeout = millis();
  while (millis() - timeout < 5000) {
    line = read_line(1500);
    if      (line.length()<=0)   { break; }  // 受信タイムアウト
    else if (line.equals("EOT")) { break; }  // 全データ受信終了
    else if (line.equals("ETX")) { mdcnt++; mecnt=0;}
    // 受信データ格納
    else {
      float act = line.toInt();
      if (mecnt==0) {
        act /=  100.0;
        MD[mdcnt].tmp = round(act*10)/10;
      }
      else if (mecnt==1) {
        act /= 1000.0;
        MD[mdcnt].hum = round(act*10)/10;
      }
      else if (mecnt==2) {
        if (act) { act = (act/100.0)-1000; };
        MD[mdcnt].prs = round(act*10)/10;
      }
      mecnt ++;
    }
  }

  // MD->RB保存(EEPROMにゆっくり格納)
  for (int i=0; i<mdcnt; i++) rb_eque(&MD[i]);

  // RBにデータがあればデータ送信へ
  int num = rb_num();
  if (num>0) {
    // WiFi接続チェック(接続成功は データ送信)
    if (wifi_connect(60000)) {
      /* 現在時刻取得(RTC->RT->UXTM) */
      rtc_getRT();
      UXTM = rt2uxt();
      /* 測定時間をさかのぼる */
      UXTM -= (num-1) * MD_ITVL*60;

      /* リングバッファにデータがあれば送信 */
      struct MDATA md;
      while (true) {
        /* データがなければ終了 */
        if (!rb_peek(&md))       break;  // RB->md

        ~ ここで測定データを利用 ~

        /* キューポインタ,時刻をinc */
        rb_dque(&md);
        UXTM += MD_ITVL*60;
        delayy(100);
      }
    }
  }
  // 送信終了
  wifi_end();
}

///// UART Control /////
// 1行読込(タイムアウト付)
String read_line(int timeout) {
  String line = "";
  unsigned long tout = millis();
  while (millis() - tout < timeout) {
    if (hs.available() > 0) {
      line = hs.readStringUntil('\n');
      line.trim();
      return line;
    }
  }
  return "";
}

///// I2C Control /////
// i2cのデータが確定するまで待つ
inline void i2c_wait() { while(Wire.available()<1); }

///// RTC8564 Control /////
// RTC 初期化
/* 時刻再設定なし,タイマー初期化 */
void rtc_init() {
  rtc_setTMR(MD_ITVL);
}

/* 指定時刻でRTC設定,タイマー初期化 */
void rtc_init(time_t uxtm) {
  /* UnixTimeを RTC時刻(RT構造)にセット */
  uxt2rt(uxtm);

  /* RTCへ指定時刻を設定,タイマー初期化 */
  rtc_setRT();
  rtc_setTMR(MD_ITVL);
}

// RTCのレジスタへRT構造から時刻をセット
void rtc_setRT() {
  // stop ビットセット
  rtc_regWR(RTC_CTR1, 0x20);

  // RT構造上の時刻データをBCDに変換してセット
  uint8_t data[7];
  data[0] = dec2bcd(RT.second);
  data[1] = dec2bcd(RT.minute);
  data[2] = dec2bcd(RT.hour);
  data[3] = dec2bcd(RT.day);
  data[4] = dec2bcd(RT.weekday);
  data[5] = dec2bcd(RT.month);

  // yearが100年を超えた場合CENTURYビットを立てる
  if (RT.year > 100) {
    data[6] = dec2bcd(RT.year - 100);
    data[5] |= 0x80;  // RTCS8564_CAL_CENTURY
  } else {
    data[6] = dec2bcd(RT.year);
  }

  // 7レジスタ 一気に書込む
  rtc_regSET(RTC_SEC, 7, data);

  // stop ビットクリア
  rtc_regWR(RTC_CTR1, 0x00);
}

// RTCのレジスタから時刻を読出しRT構造にセット
int rtc_getRT() {
  uint8_t data[7];

  rtc_regGET(RTC_SEC, 7, data);
  RT.second  = bcd2dec(data[0] & 0x7f);
  RT.minute  = bcd2dec(data[1] & 0x7f);
  RT.hour    = bcd2dec(data[2] & 0x3f);
  RT.day     = bcd2dec(data[3] & 0x3f);
  RT.weekday = bcd2dec(data[4] & 0x07);
  RT.month   = bcd2dec(data[5] & 0x1f);
  RT.year    = bcd2dec(data[6]);
   
  if (data[5] & 0x80) {  // RTCS8564_CAL_CENTURY
    RT.year += 100;
  }
   
  return 0;
}

// 指定時間でタイマー初期化
void rtc_setTMR(int tmr) {
  rtc_regWR(RTC_CTR2, 0x11);  // bit4 1:割込繰返し bit0 1:INT発生
  rtc_regWR(RTC_TMRC, 0x83);  // bit7 1:タイマー有効 bit1,0 3:min 2:sec
  rtc_regWR(RTC_TMR,   tmr);  // カウント値
}

// VLビット読込み
bool rtc_chkvl() { return (rtc_regRD(RTC_SEC) & 0x80); }

// RTCのタイマー値を読込む
int rtc_gettmr() { return rtc_regRD(RTC_TMR); }

// RTCのストップビットクリア
void rtc_stpclr() { rtc_regWR(RTC_CTR1, 0x00); }

// RTCの指定レジスタからデータを読込む
uint8_t rtc_regRD(uint8_t reg) {
  Wire.beginTransmission(RTC_ADR);
  Wire.write(reg);
  Wire.endTransmission();
  Wire.requestFrom(RTC_ADR, 1);
  return Wire.read();
}

// RTCの指定レジスタにデータを書込む
void rtc_regWR(uint8_t reg, uint8_t value) {
  Wire.beginTransmission(RTC_ADR);
  Wire.write(reg);
  Wire.write(value);
  Wire.endTransmission();
}

void rtc_regSET(uint8_t reg, int num, uint8_t *data) {
  Wire.beginTransmission(RTC_ADR);
  Wire.write(reg);
  Wire.write(data, num);
  Wire.endTransmission();
}
 
void rtc_regGET(uint8_t reg, int num, uint8_t *data) {
  Wire.beginTransmission(RTC_ADR);
  Wire.write(reg);
  Wire.endTransmission();
  Wire.requestFrom(RTC_ADR, num);
  for (int i = 0; i < num; i++) {
    i2c_wait();
    data[i] = Wire.read();
  }
}
 
// RTCから時刻(RT構造)を読込みUnixTimeを得る
time_t rtc_getUXTM() {
  rtc_getRT();
  return rt2uxt();
}

// NTPから時刻を取得しRTCにセット
boolean rtc_setNTP() {
  /* WiFi接続:未接続はRTC時刻を更新しない */
  if (wifi_connect(60000)) {
    if (ntp_connect(10000)) {
      /* NTP時刻 -> UXTM,RTC */
      UXTM = time(NULL);
      rtc_init(UXTM);
      return true;
    }
  }
  return false;
}

// UnixTimeからRTC時刻(RT構造)を得る (UnixTime->tm構造->RT構造)
void uxt2rt(time_t uxtm) {
  struct tm *t = localtime(&uxtm);
  RT.year = (t->tm_year + 1900) % 100; // tm_year: 1900年から
  RT.month = t->tm_mon + 1;            // tm_mon : 0$301C11
  RT.day = t->tm_mday;
  RT.hour = t->tm_hour;
  RT.minute = t->tm_min;
  RT.second = t->tm_sec;
}

// RTC時刻(RT構造)からUnixTimeを得る (RT構造->tm構造->UnixTime)
time_t rt2uxt() {
  struct tm t;
  t.tm_year = RT.year + 100 ; // tm_year: 1900年から
  t.tm_mon  = RT.month- 1;    // tm_mon : 0$301C11
  t.tm_mday = RT.day;
  t.tm_hour = RT.hour;
  t.tm_min  = RT.minute;
  t.tm_sec  = RT.second;
  t.tm_isdst = -1;             // 夏時間設定を無効化

  return mktime(&t);
}

///// WiFi Control /////
/* WiFi接続開始処理(ノンブロッキング風)
 *   <引数> Timeout: 接続制限時間(0 or 省略は接続するまで無限ループ)
 *   <戻値> 接続:true 未接続:False
 */
boolean wifi_connect() { return wifi_connect(0); }
boolean wifi_connect(int timeout) {
  // 初期化
  unsigned long start = 0;
  unsigned long last  = 0;   // ノンブロッキング用時間変数
  unsigned long now   = 0;
  unsigned long itvl  = 500; // クライアント再接続時のインターバル時間

  // WiFi 初期化
  wifi_init();

  // WiFiが未接続の場合のみbeginする
  if (WiFi.status() != WL_CONNECTED) {
    WiFi.begin(WIFI_SSID, WIFI_PASS);
    delayy(100);
  }

  // WiFi接続(指定時間内) 接続:true 接続失敗:false
  start = millis();
  /* タイムアウトチェック */
  while (timeout==0 || millis() - start < timeout) {
    // WiFi接続は非同期(ノンブロッキング)風で
    now = millis();
    if (now-last >= itvl) {
      last = now;
      // 接続チェック
      if (WiFi.status() == WL_CONNECTED) {
        return true;
      }
      // 未接続の場合,0.3-0.5sec おきに再接続
      itvl = random(300, 500);
    }
    yield();
  }
  // 接続不可 エラーリターン
  return false;
}

/* WiFi 初期化処理 */
boolean wifi_init() {
  // WiFi初期化 (順番大事)
  //   disconnect(true)されて再接続の場合 mode,configの再設定が必須
  WiFi.mode(WIFI_STA);                      // 1.WiFi モード設定
  WiFi.persistent(false);                   // 2.WiFi設定情報 保存しない
  if (!WiFi.config(ip, gw, mask, dns)) {    // 3.固定ip 設定
    delayy(100);
    return false;
  }
  delayy(100);
  return true;  
}

/* WiFi stop処理 */
void wifi_stop() {
  // WiFiが接続の場合のみdisconnectする
  if (WiFi.status() == WL_CONNECTED) {
    WiFi.disconnect(true);
  }
}

/* 終了処理 */
void wifi_end() {
  // WiFi切断
  wifi_stop();

  // sleep
  ESP.deepSleep(0);
  delay(100);
}

///// NTP Control /////
/* NTP 同期処理(ノンブロッキング風)
 *   <引数> Timeout[ms]:同期制限時間(0 or 省略は同期するまで無限ループ)
 *   <戻値> Unixtime or 0(エラー)
 */
time_t ntp_connect() { return ntp_connect(0); }
time_t ntp_connect(int timeout) {
  // WiFi接続している時のみ時間取得する
  if (WiFi.status() != WL_CONNECTED) {
    return 0;
  }

  // 初期化
  unsigned long now;
  unsigned long start;
  unsigned long itvl = 250; // NTP再接続時のインターバル時間
  unsigned long last = 0;   // ノンブロッキング用時間変数

  // タイムゾーン,NTPサーバー設定
  configTzTime("JST-9", NTP_SRV1, NTP_SRV2);
  /* 最初に一定時間待機すると同期時にループが省かれる(短時間化) */
  delayy(50);

  // NTPサーバー接続 (timeout秒間 接続確認)
  start = millis();
  while (timeout == 0 || millis()-start < timeout) {
    // NTP接続は非同期(ノンブロッキング)風で
    now = millis();
    if (now-last >= itvl) {
      last = now;
      // 現在時刻の取得
      time_t uxtm  = time(NULL);        // unix時間
 
      // NPT同期チェック (RTC初期値=1000000000)
      if(uxtm>1000000000) {
        return uxtm;
      }

      // 未接続の場合,0.2-0.3sec おきに再接続
      itvl = random(200, 300);
    }
    yield();
  }

  // 同期不可 エラーリターン
  return 0;
}

///// Time Controle /////
// 日時文字列"yyyy-mm-dd hh:mm:ss"を unixtimeに変換
time_t dat2uxt(String str) {
  // String->char 変換
  int len = str.length()+1;
  char chr[len];
  str.toCharArray(chr, len);

  // 日時文字列を tm構造に格納
  struct tm t;
  sscanf(chr, "%d-%d-%d %d:%d:%d",
    &t.tm_year, &t.tm_mon, &t.tm_mday,
    &t.tm_hour, &t.tm_min, &t.tm_sec);
  t.tm_year -= 1900;
  t.tm_mon -= 1;

  // 上記 tm構造から unixtimeを求めてリターン
  return mktime(&t);
}

// unixtimeを日時文字列"yyyy-mm-dd hh:mm:dd"に変換
String uxt2dat(time_t t) {
  char date[64];
  // フォーマット作成
  size_t strftime(char *s, size_t max, const char *format, const struct tm *tm);

  // unixtime->日時文字列
  strftime(date, sizeof(date), "%Y-%m-%d %H:%M:%S", localtime(&t));
  return date;
}

///// Ringbuffer Control /////
// RBポインタ inc
inline int rb_inc(int idx) {
  if (++idx >= RB_MAX) return 0;
  else return idx;
}

// RB内のデータ数を求める
int rb_num() {
  /* RTC->RBtop,RBend */
  int rbt = rtcm_get(ADR_RBT);
  int rbe = rtcm_get(ADR_RBE);
  if (rbt > rbe) rbe += RB_MAX;
  return rbe - rbt;
}

// RBポインタから EEPROMのアドレスを求める
inline int rb_idx2adr(int idx) {return idx * MD_SIZE;}

// エンキュー(enqueue) ptr->EEP
void rb_eque(struct MDATA *ptr) {
  /* RTC->RBend */
  int rbe = rtcm_get(ADR_RBE);
  
  /* ptr->EEPROM */
  int adr = rb_idx2adr(rbe);
  EEPROM.put(adr   , *ptr);
  EEPROM.commit();
  /* RBend inc */
  rbe = rb_inc(rbe);
  rtcm_put(ADR_RBE, rbe);
}

// デキュー(dequeue) EEP->ptr
boolean rb_dque(struct MDATA *ptr) {
  /* RTC->RBtop,RBend */
  int rbt = rtcm_get(ADR_RBT);
  int rbe = rtcm_get(ADR_RBE);
  if (rbt==rbe) return false;

  /* EEPROM->ptr */
  int adr = rb_idx2adr(rbt);
  EEPROM.get(adr   , *ptr);
  /* RBtop inc */
  rbt = rb_inc(rbt);
  rtcm_put(ADR_RBT, rbt);
  return true;
}

// peek(非破壊読み出し)
boolean rb_peek(struct MDATA *ptr) {
  /* RTC->RBtop,RBend */
  int rbt = rtcm_get(ADR_RBT);
  int rbe = rtcm_get(ADR_RBE);
  if (rbt==rbe) return false;

  /* EEPROM->ptr */
  int adr = rb_idx2adr(rbt);
  EEPROM.get(adr   , *ptr);
  return true;
}

///// RTCmemory(ESP8266) Control /////
// RTCメモリ 初期化チェック
void rtcm_chk() {
  int code = rtcm_get(ADR_CODE);
  if (code!=RTC_CODE) {
//@@    ss.println("RTC"); delay(2);
    /* RTCメモリ ヘッダクリア */
    rtcm_put(ADR_CODE, RTC_CODE);
    rtcm_put(ADR_RBT , 0);
    rtcm_put(ADR_RBE , 0);
  }
}

// RTCメモリに書込む
bool rtcm_put(uint32_t addr, uint32_t val) {
  return system_rtc_mem_write(addr, &val, sizeof(val));
}

// RTCメモリから読出す
uint32_t rtcm_get(uint32_t addr) {
  uint32_t val = 0;
  system_rtc_mem_read(addr, &val, sizeof(val));
  return val;
}

///// Other Control /////
/* BCD Control */
inline int dec2bcd(int dec) {
  return (((dec / 10) << 4) | (dec % 10));
}
 
inline int bcd2dec(int bcd) {
  return ((bcd >> 4) * 10 + (bcd & 0x0f));
}

/* yield delay(ノンブロッキング ディレイ) */
inline void delayy(unsigned long tm) {
  unsigned long now = millis();
  while (millis()-now < tm) { yield();}
}

(2025.06.25 v0.1.1a 一部バグ,WiFi,NTP接続周り修正)
(2025.09.16 v0.5.5 へバージョンアップ)


<スケッチのおおまかな流れ>
(start)
・AT85からのリセットパルスで起動
(setup)
・UARTを19200ボーで起動。
・WR02起動時のメッセージ送信後'\n'を追加送信してAT85を正常待機にさせる
・i2c起動
・RTC初期化
(loop)
・AT85が受信待機に入るまでdelay
・一定時間Xonを送信しAT85からSOHが来るのを待つ。返信無しはスリープ
・任意個数の測定データをEOTまで受信 (data,data..ETX, data,data..ETX,..EOT)
WiFiに接続しNTPから時刻を得る
・受信したデータを利用
・スリープ
(read_line)
・ 指定時間内でUARTから1行を読込む
(rtc_read)
・RTCのレジスタから直接読込む
(rtc_write)
・RTCのレジスタへ直接書込む
(wifi_connect)
・一定時間WiFi接続にトライする
(ntp_connect)
・一定時間NTPから時間取得にトライする

WR02のスケッチに関しては特段難しい点はありません。起動したらUARTでAT85とハンドシェイクしデータを一気に受け取ってそのデータを処理します。私の場合はSQLへ送信していますが、ここ1時間分のデータをLCDに表示するなどもいいかもしれません。

(2025.09.16 v0.5.5)
・時刻取得先をRTC8564に変更
・リングバッファを利用してEEPROMの書込み回数を平準化
・リングバッファのエンキュー,デキューポインタにWR02内のRTCメモリを利用
WiFi周りにノンブロッキングを意識

◯やってみて

今回はプランB、省電力化をハードウエアの力を借りてやってみました。現状では皮算用でアルカリ電池だと8〜9ヶ月持つかもしれない実測値になっています。あくまで皮算用ですけど。1年にはまだ未達ですがAT85に保存するデータ量を増やすとすぐに寿命が伸びる設計ですのでちょい操作するだけで1年間持たせるのは可能と読んでます。

ただ現状の1時間に1回のデータ転送でさえリアルタイム性がかなりダウンしています。今すぐ温度が見たいという需要には不向きかもしれません。倉庫の温度など後からのデータチェックでよいのであればかなり使い手があります。EEPROMの容量いっぱいまで使って数年持つロガーなんていうのもあるかも。
まずはここ何ヶ月か稼働させてみてその後応用を広げていく事にしましょう。

電池で動かすBME280温湿度計(4) 1年稼働目指して実機製作開始

◯やりたいこと

(3)で長期稼働のメドがたったので実際に運用する実機を製作する。


◯やったこと

・動作シーケンスの決定

実験を通じて求めた動作シーケンスを測定を担当するAT85とデータをWiFi送信するWR02の2つのパートに分けて以下の通りに決定します。

<ATtiny85部>

・RTC8564(以下RTC)の出す10分間隔パルスでリセット起動
・ATtiny85(以下AT85)の i2c でBME280(以下BME)を制御
・AT85の動作速度は8MHz、i2cスピードは100KHz
・温湿度・気圧を測定後データを内蔵EEPROMに保存してスリープ
・6回1時間分の測定終了時 Wroom-02(以下WR02)向けにリセットパルス出力
・起動したWR02へ19200ボーのUART通信で測定データを送信
・スリープ

当初消費電流を気にしてクロック1MHz、(必然的に)UART4800ボーにしていたのですが、送信データを待つWR02がその何十倍も電力を消費して待機している事に気付き動作速度を8MHzまで上げています。UARTは38400ボーまで通信可能でしたがバッファのオーバーフローが発生したため19200ボーで止めてあります。
またスケッチサイズがメモリギリギリになってしまったので変数はfloat型を使用せず全てint型だけにしています。転送するデータも整数値です。フロート値に戻す担当はWR02が担います。

<WROOM02部>

・AT85からのリセットパルスで起動
・WR02のi2cでRTCを制御しAT85用のリセットパルスを10分間隔で発生させる
・WR02の動作速度は80MHz、i2cスピードは100KHz
・リセット起動後必要であればRTCを初期化
・UARTでAT85とハンドシェイク後測定データを受信
・全てのデータを受信後WiFiを起動してデータを一括送信
・DeepSleep

WR02の動作上の問題点はWiFi接続の失敗とNTP時刻の取得失敗。接続を開始しようと何度もトライしたり時刻を取得できず永遠にループを続けたり。この時かなりの無駄な電力を消費します。特に初めてのアドレスで繋ごうとするとなぜか接続失敗が増えます。パケットのルートが確立していないせいなのか?
つながらない時は30秒もつながらず、実を言うとここが電池寿命を決める大きなポイントになっています。
ただスケッチ作成自体はAT85に比べ天国のように楽。デッドロックして無駄に電力を消費しない様注意しています。

・回路図の決定

上記動作を実現する回路です。

WR02のi2cラインのプルアップ抵抗はWR02の起動モード設定を兼用させています。パスコンは適宜電源周りに挿入し、起動時に大電流が流れるWR02用に100μFを入れてあります。ただ安いチップコンを使用しているので実質的には10μF程度でしょうか。
予算はWR02単独動作に比べAT85(240円),RTC(500円)の追加となりました。ほんと最近の電子部品の値上がりはすごいですね。ただこれで電池交換の手間と電池代が1/6になるというならまあお得かなと思って自分を強制納得させてます。

・製作

部品を集めいつも通り単3電池ボックスに収めるように電池サイズユニバーサルボードに組み上げます。

ユニバーサルボードは再利用品、チップ部品に頼りまくると何とか基板内に収まりそうです。

んでもって出来上がったのがこれ。
(部品面)

(ハンダ面)

ランドに余裕がないためどうしてもハンダ面での被覆配線が必要になってしまいました。

これに電池スナップをつけ

電池ボックスにセットすれば

プランBのロガーが完成。配線の都合上WR02が電池の上に重なる作りになってます。

電池交換の時少々やりにくいのがたまにキズですが何とか単3電池ケースにおさめる事ができました。

・スケッチ

ここにスケッチをはりつけて説明していたら全体がとても長くなってしまったので回を分ける事にします。
スケッチは別に(5)で紹介していきます。

◯やってみて

ひと通りハードウエアのバグ取りが終了した実機、

無事倉庫の温湿度測定を開始しています。
現在1ヶ月が経過。

単4エネループでの稼働時間はもともとは2週間程でしたので少なくても2倍の長寿命化に成功中。このまま順調に稼動してもらって予定の3ヶ月稼働!までいけるとうれしいですね。

◯やってみて2

(3)で作成したブレッドボード実験機に

BH1750を追加搭載してグレードアップし動作実験をやってみたところ

動作時間が驚きの5日と2.5時間(30秒おき20倍測定)。

BH1750を追加したのに稼働時間が伸びています。
原因はRTC。時刻の取得をNTPでなくRTCにしたためWi-Fi接続時間が短縮し全体として消費電力の節約につながったようです。単純計算で122.5時間x20=2450時間、24時間で割ると102日間の動作。3000mAhの単3リチウム電池を使ったとすると382日動く計算、目標の1年動作達成です。なんと嬉しい。
どうやら次のバージョンの仕様が決定したようです。

ちなみに外気温測定には低温に強いリチウム電池で測定しないと放電が止まってしまいますのでリチウム電池の利用が必須です。実験通りにロングランしてくれひと冬電池の交換がいらなくなるなんて夢のようですが、高容量のリチウム電池は高いので現状(800mAh)とアルカリ電池とで夏冬使い分けるのもありかもしれません。
うまくいくのを楽しみにデバッグしてます。

電池で動かすBME280温湿度計(3) 1年稼働目指してプランBへ移行

◯やりたいこと

スケッチの改造だけで長期稼働を目指そうとしていたBME280温湿度計ロガーですがやはりソフトだけでは少々無理があったようです。そこで準備してあった「プランB」に移行しハードウエアの力も借りながら長期稼働を目指します。


◯やったこと

・プランBとは

前回作成したロガー、1カ月動かして電池交換は2回。

結局のところ多少は電池の持ちがよくなりましたが毎10分おきに送信していたロガーとはそれほど差がつきませんでした。1年間電池交換なしなんて夢のまた夢です。

でも...

あえて言い訳をすると、実はこの状況は想定済みでした。
ロガーの電力消費の大部分はWroom02の「起動時」に消費します。WiFi送信の数を少々減らしたからと言ってそうそう電力消費が減るわけがありません。起動回数を減らさない事には根本的な対策とはならないのです。

そこで考えてあったプランBがこれ。

ATtiny85の追加。
10分おきに起動して測定をする担当を電力消費の少ないATtiny85にまかせ測定データはチップ内のEEPROMに保存、1時間分をためたあとWroomを起動してデータを一括送信するというものです。これですと大電力を消費するwroomの起動回数は1/6になりますし、1MHz動作のATtinyは測定時が2〜5mAで数十ms、未測定時の消費電流も数μAオーダーと、単純に電池の持ちが6倍になりそうな感じです。
「だったら寿命2カ月だった電池が6倍の1年もつんじゃないか?」と言うかなり楽観的な皮算用に基づくプランがプランBの正体です。

・実験機の作成

さっそくプランB実験機の製作に入ります。
ボードにはWroom02用のスケッチ書き込み機能も搭載し開発効率を向上させることにしました。チップの抜き差し作業が発生しないのでだいぶ楽になります。
シリアル変換モジュールは秋月電子で売っていたCH9102FモジュールをESP01のブレークアウトボードを利用して取付けし電源もこのモジュールから供給します。

さすがにATtinyの方はスケッチ書き換え時に抜き差しが発生しますがまあ手間は半分に減ります。
またUARTモジュールのRXにはATtinyの出力も監視できるようダイオードを利用したOR回路を入れてあります。この機能はホント必須です。
もちろんWroomのRXにもUARTモジュールとATtiny のTX出力がコンフリクトしないようOR回路が入っています。

ダイオードは整流用、小信号用、ショットキーダイオード各種使ってみました。笑
チップ間のボーレートは4800ボーで行こうと思っていましたのでこの辺はラフで構わないと思います。

このボードでさっそくスケッチの製作に入っていく事にします。


・UART通信

ATtinyのスケッチって制約が多くて結構面倒に感じるのは私だけでしょうか。当初wroom同士のUART通信で開発したスケッチをそのまま移植したのではうまく動作しませんでした。

それでも一つ一つバグをフィックスして何とか姿が見えるところまでたどり着いています。

ATtinyでBME280を一定間隔で測定、データはEEPROMへ格納、データがたまったらWroomを起動してUARTで送信、Wroomは受信したデータをWiFiSQLに送信、と言う段階まで来ました。まあまあ素直に動いてくれています。

ただここに来て発生した問題がATtinyを定期起動させる手段。10分単位となると自身のタイマーでは不可能ですしピンの空きがないので起動方法はリセット起動のみ。無安定マルチバイブレータ、555によるワンショット、いずれもコンデンサが巨大になりそうですしそもそもそこにためる電荷で幾分かの待機電力がまかなえるレベル。充放電のためだけに使うなんてもったいなくてとてもできません。

部品は増えるのですが消費電力の意味からいってもRTCによる定期起動が現実的なんでしょうか。時計を持つ事でNTPへのアクセスが不要になりWiFi電力の消費削減にもなりそうですし。次へのステップ移行で少々悩んでいます。

・ATtiny85の起動方法

結局秋月電子で売っていたRTC8564を使う事にしました。

最終的にチップ2つの部品追加です。単3電池サイズのユニバーサルボードにどうやって載せるかを新たに悩み始めました。

とはいえこれで実験回路が無事完成という事で単独運転のテストに入り始めていて、待機時の電流実測値も50μAとESP8266:20μ、レギュレータ:30μのカタログ値に比べてまずまずな値を示してくれています。電池直接駆動や不要回路を取り外す事などでまだまだ改善できそうな感じです。

ただまだソフトにはバグが少々。しばらく実験稼働してソフト・ハード共にバグ取りを進めていきます。

・実験稼働

とりあえずバグ取りが終了したので実稼働試験に入ってます。

本来10分おきに計測するところを1分おき(消費電力10倍)にして電池の持ちをチェックしていきます。
ロガーの全体の流れを紹介すると

・Wroom、ATtiny共にsleep
・Measure:0〜5
ATtiny85がRTCのリセットパルスにより起動しBMEからデータ(整数)を取得しEEPROMに保存
・ヘンテコマーク
データがたまったのでATからのリセットパルスでWroomが起動
・Xon、SOH
UARTでハンドシェイク(Xon,SOH)
・数字、ETX
ATのEEPROMからデータを読み出しWroomへUART転送
・EOT
データを全部受信したのでWiFiに接続してSQLへデータ転送後sleepする。

という流れです。
ロガーの消費電流は実測値で待機時20μA、BME測定時1.2mA、Wroom起動時80mAとなっており750mAhの単4エネループだと9日くらいは動作する予定。10分毎の測定に換算すると90日、単3エネループだと7カ月動作する勘定。皮算用ですが電池で1年稼働が視野に入ってきました。

◯やってみて

実験稼働が順調にすすんだので(1)、(2)と同じ電池を使い実験が早く終わるよう20秒おきの測定(30倍の負荷)で比較試験をやってみました。
すると..

3日と4時間の稼働。


(グラフは2日半ぐらいの時のものです)

76時間の稼働です。すごい。
30倍の負荷ですので10分おき測定に換算すると 76h×30=2280h、24hで割ると95日稼働。3か月動く換算値です。2週間動いていた前回に比べちょうど6倍、予定通りの稼働時間になります。とりあえずプランBは成功といったところでしょうか。

これを元にさっそく実機の製作に入っていきたいと思います。

電池で動かすBME280温湿度計(番外編) 電池交換なしで1年3カ月稼働中のロガー

◯やりたいこと

電池交換なしでの運用を目指し、太陽光パネル(PV)+リチウム電池で実験してきた温湿度・気圧・照度計ロガーの現状をレポートする。


◯やったこと

3年ほど前から実験を繰り返していた温湿度計ロガー、

greensoybean.hatenablog.com

PVとバッテリーの組み合わせをいろいろやってみた結果、現在では
太陽光パネル 秋月電子 1.15W (開放電圧 5V 短絡電流230mA)
リチウム電池 3.7V 900mAh
の組み合わせで落ち着いています。

太陽光パネルの裏にリチウム電池と充電モジュールテープで貼り付け、

携帯電話スタンドの上に

チョコンとのっけて使っています。

現在は子供部屋の出窓スペースに置いてあります。

当初はスタンドに直接取り付けた電池ケースにロガーをセットしてコンパクト運用していたのですが、やはり直射日光に当たる筐体に直接センサーを取り付けると測定温度と室温にかなりの乖離が発生してしまいました。

そこで現在は電源コードを延長し、ぬいぐるみ達の協力を得て日陰を作ってそこの状態を測定しています。

電源部はレースカーテンの向こうに置いて太陽の光を一身に受光中。

出窓スペースですので夜は部屋側のカーテンを閉めるため測定は出窓空間内の測定になっていますが、まあ乾電池なしで運用できるかどうかの実験が主目的ですのでたくさん置いてあるぬいぐるみ達の生活環境を計るという理由で現状維持しています。

◯やってみて

現在電池交換なしで全く手がかかっていない期間は1年3カ月まで伸びてきています。

データの欠落もなくほぼ運用上は問題ないのではと考えています。
今後はUSBケーブルのお古を使って電源を延長しロガーを室内側に持ってこようかと思っていました。

いずれにしてもWroom02+BME280+BH1750ロガーはお日様があれば電池なし運用が可能な事を実証できそうな感じです。

電池で動かすBME280温湿度計(2) 1年稼働目指してスケッチを改造

◯やりたいこと

(1)のデバッグに基づき実機での温度測定に入る。


◯やったこと

・前回の動作チェック

(1)の動作チェックを一晩実施してみました。

やはり一部データの欠落があったのですが、調べたところスケッチの問題ではなく連続してデータを送信したためにESP8266の処理が間に合わず送信データが欠落した事に因がありました。そこで送信ループの中にdelayを入れて対応。諸処の条件によりまた欠落が発生するかもしれませんのでその時はまた調整していきたいと思います。

・実験に利用する電池

我が家の測定器達の電源はDCDCコンバータを利用しているロガーにはアルカリ電池、直電圧を利用しているロガーにはエネループと使い分けています。外気温を測定するロガーには低温特性のよいリチウム電池を利用。

繰り返し利用に備え充電式のリチウム電池で運用しているのですが問題点はその容量の低さ。800mAhしかありません。実際に動かしてみると低温環境の影響もあってか実働日数は約2週間。しょっちゅう取り替えていなければならず結構なストレスになっていました。ただ今回は実験ですので早くなくなってくれた方がいい?ため容量の少ないCR123で進める事にします。

ちなみに使い切りタイプのCR123を使うと同じロガーでも1カ月チョイもちます。

少々お高いのですが厳冬期、吹雪の中で電池交換をする事を考えると少しでも交換回数の少ない方がいい?
いつも葛藤してます。

・実機稼働

いよいよ実機での測定に入ります。

スケッチからデバッグ用に挿入していたserial.print文を全て削除し、BME280,BH1750を能動化して乱数でセットしていた測定値を実際に測定した値をセットする様に変更して実験開始です。

実際動作版のスケッチだとうまく動作しているかどうかはSQLに飛んでくるデータをチェックするしか方法がありません。バグがあってそのデータすら飛んでこないなどとなるともうお手上げ。そこで最低限の出力装置としてLEDを追加しています。情報量はわずか1Bitですが点滅の長短、点滅の回数等で情報を使い分けデバッグに利用していきます。チカチカ点滅してくれると動作してくれているのがわかり安心感倍増です。Lチカの真髄といったところでしょうか。

現実的には(1)でデバッグがある程度完了していますので今のところ順調にデータを送信してきてくれています。

満タンにした実験用の電池で

さっそくロングラン実験に入る事にしましょう。
結果は「◯やってみて」追加していきます。

という事でまた1年後に…
(いきなり早く終わって1カ月後くらいだったりするかも)

◯やってみて

稼働して1週間が経過しました。測定場所を2階の物置に変更して記録を取り続けています。

やはりスケッチに少々バグがあり deepSleepの起動時間調整も必要なようです。修正すべき点が一つ々々見つかってきてました。やってみてあたりまえといえばあたりまえなのですが、1時間に一度のデータ転送だとリアルタイム性がなく後からじっくりデータを分析する測定に向いているのを感じてます。

ちなみに物置の温度湿度が比較的一定なのに驚いています。物の保存には最適な場所だというのがわかりました。

〜 続く 〜

電池で動かすBME280温湿度計(1) 1年稼働目指してスケッチを改造

◯やりたいこと

10分おきに測定を実施している我が家の温湿度測定ロガー、現在単3電池2本で約2ヶ月稼動が標準なのですがデータをまとめ送信することによって省電力化を図り1年間稼働させる事を目指します。

◯やったこと

・作戦計画

現在稼動しているデータロガー、

10分おきに温湿度・気圧・照度を記録しています。
これは居間のデータを一年分表示したグラフですが、

紫の縦線が電池を交換したタイミング。1回交換すると概ね2カ月ほど稼動しています。後半の方が1カ月チョイしか稼動していないのは安い訳あり電池を買ってしまったため。安い電池はやはり安いしかないというのは超実感しているところです。

ロガーの電力消費のほとんどが起動時およびWiFi接続・データ送信で占めてられているので測定間隔を広げれば簡単に電池寿命を伸ばすことができるのですが、やはり最低10分間隔のデータは欲しいところ。そこで1時間はwroom内蔵タイマーに頼ってEEPROMにデータを保存し、それをまとめて送信する事により消費電力をケチろうというのが今回の計画です。精度の非常に悪いWROOMの内蔵タイマーですが1時間位ならまあなんとかなるだろうという算段で向かいます。

・実験機製作

まずは部品を準備してブレッドボードに実験機をつくります。

回路図はこんな感じ。

WROOM-02のi2cラインにBME280とBH1750をぶら下げているだけなので回路的には簡単です。電池を最後まで使い切るためにDC-DCコンバータをはさんでいるのが少々手が込んでいる点でしょうか。いずれにしてもササっと組み上げてしまいます。

いつもながらですがブレッドボードは実験までの手順が早くていいですね。
ちなみにDeepSleepを使用するためIO16とリセットは1KΩの抵抗でつないでおく必要があります。


(写真はチップ抵抗でつないだ例です)

この実験機を元にスケッチの製作に入ります。

デバッグ作業

デバッグが未了ということで完成されたスケッチは別の回に紹介することにし今回はデバッグ作業中の状況を紹介。デバッグはセンサー測定値等を乱数で発生させSerial.printで動きを監視するパターン、要はWroom02書込み機にセットしたままの状態でデバッグを行っていきます。

この状態だと動作を確認しスケッチを書き換えてもすぐに書き込むことが出来るのでデバッグ初期の段階は非常に有用です。新幹線移動中でも具体的なチップの抜き差しが発生しないのでリモートでデバッグが可能です。

スケッチ内にあちこちに挿入したSerial.print文の出力を見てみると、EEPROMにデータが書き込まれていない場合は初回起動としてEEPROMデータをクリアしその後10分毎にDeepSleepして起動時にデータを記録しているのが確認できます。


(ちなみに意味不明の文字列はESP8266が起動時に出力する文字列でシリアルのボーレートが不一致のため不規則文字になっています。多少見にくいのですが「再起動したマーク」くらいの認識で見といてください。)

さらに60分目にはWiFiに接続して6回分のデータを一気に送信し、NTPから取得した新しい時刻をセットして次の1時間を始めているのがわかります。順調,順調。


(表示されるUnixtimeはデータブロックの先頭データ時間を表しており、時間経過がわかるのはブロック内のどの位置のデータかを示すDCNTを見ます。10分おきに一つづつカウントアップしているのが確認できます。)

転送されたデータを確認してみると正10分おきに1時間分がキチンと転送されています。

ただ内部クロック誤差のせいか起動時間の関係で同じ時刻・10分飛んだ時刻の部分があります。デープスリープの時間調整が少々必要なようです。しばらく動かしながら順次追い込んでいくことにしましょう。

〜 次回続く 〜

皿立てに並んだラズパイ

◯やりたいこと

階段下倉庫に置いてあるスチールラック、ONUルーター、電話親機、PCやラズパイなどをひとまとめに設置して稼働させているのですが、ラズパイ達が増えてごちゃごちゃになってきたので一度整理をする事にしました。


◯やったこと

・ラズパイを整理

以前自作PCの上に並べていたラズパイ達ですが、

数が増えてきたせいでお互いに干渉し転ぶことがしばしば。何かいい方法がないかなといろいろ探していたのですが最近落ち着いた形がこれ。

皿立て。100均で売ってたヤツでワイヤー間が40mmとメタルケースに入れたラズパイ達にとって適度なスキマもありちょうどいい感じ。

ラズパイ5NVMeタイプのケースは少々キツめでしたがなんとか入りました。さらに下にHUBが入るのでLANケーブルの引き回しも便利。見た目スッキリいい感じです。

・ACアダプタ

ラズパイ達がこのようにズラッと並ぶと困るのが電源。アダプタ用のコンセントが不足し100V系だけでもかなり混雑してしまいます。そこでいろいろなテーブルタップを試してみたのですがやっとたどり着いたのがこれ。

コンセント幅が32mmありコンセント口が並びに対し直角方向のタップ。このタップのおかげで 3B,4B向け幅30mmタイプのアダプタがキレイにまとまります。

35mm幅のアダプタも30mmタイプと交互だと挿入可能。

このタップをスチールラックに結束バンドで固定しラズパイの上から電源を供給しています。電源周りがかなりスッキリです。

Windowsマシン

今までラズパイ達を載せていたWindowsマシンは一段下の段に移動して活躍してもらう事に。

移動してみたらラズパイを置ける空間がさらにできたような気がします。

シメシメ、です。

・ワイヤーラティス

スチールラックのサイドはワイヤーラティスを結束バンドで固定しておりいろいろな機器を固定しています。USBハブなんかはこんな感じ。

外付けSSDや大きめのACアダプタ、ラズパイzeroやESPなどを固定しています。マグネットボードを取り付けて開発中の機器を貼り付けておいたりもしてます。スチールラックの補強にもなり結構重宝しています。

・電源

このスチールラックにはキャスターをつけ移動可能にしています。そのため外部との接続は極力減らし、ONUケーブル、宅内LAN、100Vの3つだけにしています。さらに100Vはポータブルバッテリーで一度受けてからラック内の機器に供給。

これで停電時もバッチリ稼働が可能です。電話回線が生きていればWiFi・インターネットも使えますしIP電話も通話が可能です。もちろんWindowsやラズパイ達も稼働可能。全体で6~70wの消費となっていますので6〜7時間ほどは動かせる勘定です。

◯やってみて

ラズパイ達が非常にスッキリしました。さらに増設エリアも確保できたのでこれからの楽しみが倍増。ラックの一サイドにはエネループの充電器群もワイヤーラティスに固定したので毎回の充電がとても楽になりました。
スチールラックにワイヤーラティス、ラズパイと皿立て、どちらもベストコンビのようです。