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

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

ラズパイZERO+TrixieでReSpeaker 2-Mics Pi HATを動かす

◯やりたいこと

アマゾンで見つけたサードパーティ品のReSpeakerをラズパイZERO+Trixieで動作させる

◯やったこと

・ReSpeaker

アマゾンを何気に見ていたら偶然 ReSpeakerのサードパーティ品を見つけました。

しかも1200円。安い!
ちょうどbookwormで動いていたReSpeakerを trixieへ乗り換えようと思っていたところだったので、これは挑戦してみる価値あり!とさっそく買ってみました。
ReSpeakerは「えだまめ」用のネットワークマイクとして利用してきた経緯があり

https://greensoybean.hatenablog.com/entry/2020/01/11/Julius_サーバーにネットワークマイクを接続する

お安ければ気兼ねなく数を増やしていけそうです。ついでに日本語解析部分もJuliusから精度の上がっているVoskに切り替えスマートスピーカー部分をバージョンアップしようかとも考えてました。

・ReSpeakerのインストール

本来であればSeeed Studioのリポジトリからファイルをダウンロードしdtboファイルを作成してオーバーレイフォルダに格納という手順を踏むのですが、trixieではなんとWM8960ファイルが標準で実装されていました。

これは助かります。サードパーティ品なのでseeedのドライバが動くかどうか心配だったのですがWM8960が公式に認定されているとあれば安心して使えます。

標準実装ということでインストールはとても簡単。config.txtの1番最後に次の3行を追加するだけです。

sudo nano /boot/firmware/config.txt

dtparam=i2c_arm=on
dtparam=i2s=on
dtoverlay=wm8960-soundcard

再起動して以下のコマンドを実行し

aplay -l ; arecord -l

下記の様に

**** ハードウェアデバイス PLAYBACK のリスト ****
カード 0: vc4hdmi [vc4-hdmi], デバイス 0: MAI PCM i2s-hifi-0 [MAI PCM i2s-hifi-0]
  サブデバイス: 1/1
  サブデバイス #0: subdevice #0
カード 1: wm8960soundcard [wm8960-soundcard], デバイス 0: bcm2835-i2s-wm8960-hifi wm8960-hifi-0 [bcm2835-i2s-wm8960-hifi wm8960-hifi-0]
  サブデバイス: 1/1
  サブデバイス #0: subdevice #0
**** ハードウェアデバイス CAPTURE のリスト ****
カード 1: wm8960soundcard [wm8960-soundcard], デバイス 0: bcm2835-i2s-wm8960-hifi wm8960-hifi-0 [bcm2835-i2s-wm8960-hifi wm8960-hifi-0]
  サブデバイス: 1/1
  サブデバイス #0: subdevice #0

とwm8960が追加されていれば成功です。
何と簡単になったんでしょう。ありがたいことです。

・初期設定

Voskとの通信は一般的なTCPストリーミングで行うものとし、WM8960用に以下の様な初期設定モジュールを作成しました。
(えだまめ用のものを一般的に使える形にAIに修正してもらいました。)

"""
    ///// io_ini_WM8960.py /////
        WM8960入力ミキサー設定初期化モジュール (汎用版)

            実行内容:
                ・入力ブースト有効化
                ・入力ブーストゲイン設定
                ・ADCハイパスフィルター有効化
                ・Capture音量設定
                ・ALSA設定保存
            対象:
                サウンドカード番号 1
                WM8960(ReSpeaker)
"""

# --- 標準モジュール ---
import sys
import subprocess
from time import perf_counter

sys.dont_write_bytecode = True

# --- ハードウェア設定 ---
DEFAULT_CARD = 1

# Captureの有効範囲は、amixer表示上 0~63。
DEFAULT_CAPTURE = 39
MIN_CAPTURE = 0
MAX_CAPTURE = 63

# LINPUT1/RINPUT1の有効範囲は 0~3。
INPUT_BOOST_GAIN = 2

# スイッチ型コントロール
SWITCH_CONTROLS = (
    "Left Input Mixer Boost",
    "Right Input Mixer Boost",
    "ADC High Pass Filter",
)

# ゲイン型コントロール
VOLUME_CONTROLS = (
    "Left Input Boost Mixer LINPUT1",
    "Right Input Boost Mixer RINPUT1",
)

# --- 関数定義 ---
def run_command(command: list[str]) -> str:
    print(f"cmd -> {' '.join(command)}")

    result = subprocess.run(
        command,
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        text=True,
        check=False,
    )

    if result.stdout:
        print(f" <- {result.stdout.rstrip()}")

    if result.returncode != 0:
        raise RuntimeError(f"command failed: rc={result.returncode}")

    return result.stdout


def run_amixer(card: int, *args: str) -> str:
    command = [
        "amixer",
        "-c",
        str(card),
        *args,
    ]
    return run_command(command)


def set_switch(card: int, control: str, value: str) -> None:
    run_amixer(card, "set", control, value)


def set_volume(card: int, control: str, value: int) -> None:
    run_amixer(card, "set", control, str(value))


def set_capture(card: int, capture: int) -> None:
    run_amixer(card, "set", "Capture", f"{capture},{capture}")


def configure_mixer(card: int, capture: int) -> None:
    for control in SWITCH_CONTROLS:
        set_switch(card, control, "on")

    for control in VOLUME_CONTROLS:
        set_volume(card, control, INPUT_BOOST_GAIN)

    set_capture(card, capture)


def verify_control(card: int, control: str) -> None:
    run_amixer(card, "sget", control)


def verify_mixer(card: int) -> None:
    controls = (
        "Capture",
        "ADC High Pass Filter",
        "Left Input Mixer Boost",
        "Right Input Mixer Boost",
        "Left Input Boost Mixer LINPUT1",
        "Right Input Boost Mixer RINPUT1",
    )
    for control in controls:
        verify_control(card, control)


def save_mixer(card: int) -> None:
    run_command(["sudo", "alsactl", "store", str(card)])


def initialize(**kwargs) -> None:
    print("iniz -> start WM8960 initialization")

    card = kwargs.get("card", DEFAULT_CARD)
    capture = kwargs.get("capture", DEFAULT_CAPTURE)
    save = kwargs.get("save", False)

    if not MIN_CAPTURE <= capture <= MAX_CAPTURE:
        print(f"Error: capture must be {MIN_CAPTURE} to {MAX_CAPTURE}")
        return

    try:
        print(" -> configure WM8960 mixer")
        configure_mixer(card, capture)

        print(" -> verify WM8960 mixer")
        verify_mixer(card)

        if save:
            print(" -> save ALSA mixer state")
            save_mixer(card)

        print(" -> mixer setup completed")

    except FileNotFoundError as e:
        print(f"Error: command not found: {e.filename}")

    except Exception as e:
        print(f"Error: mixer setup failed: {e}")

    return

これをTCPストリーミングを走らせる前に一度呼び出しておけばオーケーです。各種諸元は環境に合わせて調整してください。DEFAULT_CAPTURE、NPUT_BOOST_GAINがマイクの音量設定です。実際に録音し再生してみて音が割れていないか、小さすぎないかをチェックしてみると確実です。

arecord -D hw:1,0 -d 5 -f cd test.wav
aplay -D hw:1,0 test.wav

◯やってみて

いや〜一発動作でした。正規品と相違わず動作してくれています。
以下はネットワークマイク側の動作ログとVosk側の動作ログです。

いくつかのワードを固定して実験してみたのですがとても順調に動作しています。今後スマートスピーカーとして利用するのはもちろん、「無音状態がしばらく続いた後音がしたら照明をつける」みたいなスマート照明など値段が安い分応用箇所を増やしていけそうでなんとなくルンルンしてました。♪

ラズパイZERO+Torixieで赤外線送信を行う

◯やりたいこと

ラズパイZERO+Trixieでir-ctlによる赤外線リモコンを作成した際すったもんだして結局送信ができませんでした。その後いろいろな組み合わせを行い全体でやっと動作できたのでそれを記録しておきます。

◯やったこと

・ir-ctlへの移行

ラズパイのOSがTrixieになってからサーバー側のpigpioが公式リポジトリから完全に非搭載になりました。LIRCもインストールはできますがその立場がどんどん弱くなっている様です。
そこで「えだまめ」としては今回Ver2.0にバージョンアップしたのを期に赤外線制御も最新トレンドの ir-ctlに移行することにしました。

・移行計画

現在のえだまめはmosquitoを利用したMQTTシステムに移行していて、デバイスの一つであるラズパイは以下の様なプロセス構成で稼働しています。

・eda_mqtt_main.py →各プロセスを起動し稼働を監視するメインプロセス。
  ∟ eda_mod_mqtt.py →自分宛のトピックをSUBしコマンド実行
   ∟ rpcexe →自分宛のRPCをSUBし実行、結果をPUB (cmd→resp)
   ∟ catexe →自分宛のtele,event,logなどをSUBし実行
  ∟ io_prc_xxx.py →I/Oデバイスを管理するプロセス xxx:gpio,i2c,spi...
   ∟ io_xxx_yyy.py →デバイスxxxに対しコマンドyyyを実行するモジュール

ここに
  ∟ io_prc_irctl.py →デバイス /dev/lirc0,1を管理するプロセス
   ∟ io_irctl_send.py →登録されている赤外線コードを送信するモジュール

という赤外線送信プロセスを追加して赤外線送信命令をSUBし指定のコードを送信させる計画です。

・沼

移行計画ができたのでササっと移行しようと簡単に考えていたのですがここからが沼の始まりでした。どんな方法を取ってもどうしても送信がうまくいかないのです。
初めはZEROに対しプロセスが多いからかなとも思い単独動作させたりしてたのですが動かず。コードを見直して気がついたところを直してみたりAIに聞いてそのまま言う通りにやってみたり。しかしガンとして動きません。日を置いてAIに聞き直して違うアプローチでとりついてみたり、ふと思いついたコードを試してみたり。10回以上コードを全面書き換えしたでしょうか。そうこうするうちに2ヶ月ほどが無駄に過ぎていってしまいました。

・成功

さすがに根負けし、しばらくZEROでの送信から離れて次の目的である赤外線の解読に向かうことにしました。これはテレビ前に常駐しているラズパイ4に

greensoybean.hatenablog.com

・io_prc_mach.py
 →赤外線を受信し登録されたコードと一致すればそれに応じたコマンドを実行

と言うプロセスを追加し、受信した赤外線コードに応じたコマンドを実行させようというものです。使わなくなったリモコンのコードを学習させ現在利用中のリモコンコードに合わせた赤外線を送信させます。リモコンコード変換器とでも言うのでしょうか。
アマゾンで買ったリモコンを携帯用の部屋の照明リモコンにもしたりしてます。

幸いな事にこの受信パターンのマッチング検出は思いのほか順調に進みすんなり稼働してくれたため、こりゃラッキーという事でこの調子イケイケでZEROで動かないコードをラズパイ4で実行させてみました。すると...

驚きの完璧動作。

なんだコリャ?と3B、ZERO2でもやってみたみたところこちらも完璧動作。何とZEROだけが送信に失敗すると言うZERO特有の症状だということがわかったのです。プロセス稼働からポート設定のタイミング、遅延インポート、zeroに合わせて取っていた対策が上位機種にはまったく不要で普通通りにやればよいだけでした。まったくもう、無駄に費やした時間を返せ〜です。

・対策

でも原因がわかれば対策は簡単。送信用デバイスにZEROを使うのを諦め受信専用デバイスに変更します。やってみたところZEROでも受信は普通に動作しますしパターンマッチも難なくこなします。そこで受信した赤外線コードに応じ送信専用のZERO2に赤外線を送信してもらう形をとる事にしました。現在のえだまめではブローカーにZERO2宛のトピックとペイロードを送るだけで済みます。

(居間で赤外線送信を担当するラズパイZERO2)
greensoybean.hatenablog.com

これで2階に置いてあるZEROから使わなくなったリモコンで1階のエアコンをコントロールできますし、外部からの制御も簡単で留守中在宅のフリをする照明点灯もコマンドひとつでできます。受信したラズパイの位置により一つの照明リモコンで点灯先を変えることも可能なので何かと便利になります。

・ir-ctl用コード

ir-ctlは以下の手順でインストールします。

sudo apt update
sudo apt install v4l-utils

インストール後起動時に config.txtの定義に基づき赤外線TX,RX用のデバイス /dev/lirc0,lirc1を確保しようとするので config.txtファイル最後尾に自分の機器に合わせて以下の文を追加しておきます。

sudo nano /boot/firmware/config.txt

dtoverlay=gpio-ir,gpio_pin=4
dtoverlay=gpio-ir-tx,gpio_pin=13

lirc1にTXを割り当てるよう記述の順番を意識します。無事取得できたかは以下で確認できます。

ls /dev/lirc* -la
crw-rw---- 1 root video 250, 0  814 02:55 /dev/lirc0
crw-rw---- 1 root video 250, 1  814 02:55 /dev/lirc1

もちろんここで定義したpinは他でgpio入出力として定義・利用することは禁止になります。

そして以下が実際に送信を担当している部分のコード。実際に使用しているものを一般的に使えるようAIに書き直してもらいました。

import os
import subprocess
import tempfile
from time import sleep

def send_ir_signal(pulse_list, device="/dev/lirc1", repeat=3):
    """
    赤外線パルスリストを一時ファイルに書き出し、ir-ctlを使って送信する関数。
    
    :param pulse_list: (p_type, p_val) のタプル/リスト
    :param device: 送信先のlircデバイスパス (例: "/dev/lirc0")
    :param repeat: 送信回数 (デフォルト3回)
    :return: 成功した場合は True、失敗した場合は False
    """
    if not pulse_list:
        return False

    # 1) パルスデータを整形
    raw_pulses = [(str(p_type).strip(), str(p_val).strip()) for p_type, p_val in pulse_list]

    # 2) 一時ファイルにパルスデータを書き出す
    with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', dir='/tmp', delete=False, encoding='utf-8') as tmp:
        tmp_path = tmp.name
        for p_type, p_val in raw_pulses:
            tmp.write(f"{p_type} {p_val}\n")

    # 3) 指定回数送信
    try:
        for loop in range(repeat):
            cmd = ["ir-ctl", "-d", device.strip(), "-s", tmp_path]
            result = subprocess.run(cmd, capture_output=True, text=True)
            
            if result.returncode != 0:
                print(f"エラー: {loop+1}回目の送信に失敗しました: {result.stderr.strip()}")
                return False

            # 1ブロック送信毎にインターバル時間挿入
            if loop < repeat - 1:
                sleep(0.1)
        return True

    finally:
        # 一時ファイル削除
        if tmp_path and os.path.exists(tmp_path):
            os.remove(tmp_path)

(スレッドセーフにするため親プロセス側で子側のデバイス利用中にデバイスロックをかけています。)

パルスリストはSQLに以下の形式で格納されていて

インデックス
+----+--------------+--------+
| id | name         | length |
+----+--------------+--------+
|  2 | bd7020_volup |    100 |
|  3 | bd7020_voldn |    100 |
|  6 | bj_toggle    |     72 |
|  7 | bj_ok        |     72 |
|  8 | bj_up        |     68 |
...

データ
+-----------+------+-------+-------+
| remote_id | idx  | type  | value |
+-----------+------+-------+-------+
|         2 |    0 | pulse |  3400 |
|         2 |    1 | space |  1700 |
|         2 |    2 | pulse |   425 |
|         2 |    3 | space |   425 |
|         2 |    4 | pulse |   425 |
...

SQLから渡されたデータを最終的にモード2(LIRC mode2 互換)形式に変換し送信しています。モード2にした理由はトレンドなのもありますが、赤外線コードの学習・整形時にデータ偶数奇数個の問題などを排除しやすかったためです。

pulse 9100
space 4400
pulse 650
space 500
pulse 650
space 1600
pulse 650
...

普通の使用では最初からデータをモード2の形で格納しておく方がいいかもしれません。その場合スクリプト内1),2)のコードは不要になります。今回は赤外線の学習データ格納とパターンマッチングの関係でSQLの力を借りるためこの様な形にしています。

◯やってみて

一旦ハードルを越えると後は順調に進んでくれました。現在は io_irctl_insert.pyという赤外線学習プロセスも起動中で、学習させると1サイクルのみを抽出してSQLに格納するという便利な機能も走っています。まあ精度が悪いのでパルス幅数値の正規な値への修正とともにAIを使って補正してはいますが...

と言う事で時間はたっぷりかかりましたがおかげさまで無事動作中。快適な赤外線ライフを堪能中です。

秋月電子のIoT学習HATを作る (with BMP280+AHT20モジュール)

◯やりたいこと

この間久しぶりにひっぱり出してきた秋月さんのIoT学習HAT、

greensoybean.hatenablog.com

少々文句を言いながら使っていたところ実は結構使えるデバイスだと考えが変わってきました。その持っている機能の有用性はもちろんですが、デバイスが複数必要な場合部品を付ければいいだけのキットはとても気が楽。老眼の目にも優しいデバイスだと気がついたのです。
そこで再び久しぶりにキットを買って製作してみる事にしました。

◯やったこと

・キット内容

この学習キットの内容はこんな感じ。

少ない部品で多機能を追求しているよく考えられたキットだと思います。
今回は前回と同様モーションセンサーと温湿度・気圧測定を主眼に一定情報の表示を担当するデバイスとして利用しますので一部部品を別に調達して使っていきます。

今回はアマゾンで買えなくなってきたBME280でなく、最近よく売っているBMP280+AHT20モジュールで進めていく事にします。

・ハンダ付け

キットですので指定された部品を指定された位置に背の低い順に取り付けていけばいいだけですので気が楽です。

ちなみに私は部品の足を切ってから半田付けする派。
部品を取り付けて足の片方を切断、

位置向きを揃えてもう片方を半田付けします。

切った足は

大事にとっておいて基盤裏配線の貴重な配線材料に使っています。


・改造

測定モジュールはグローブ端子で外部に引き出す設計ですので裏側の改造を実施します。


・SDAのパターンをカットしてピンを独立させる
・トランジスタのエミッタをSDAとして利用
・SCLを0Ω抵抗でジャンパ(ショート防止用)
・モーションセンサーにパスコン取付
・100μFのコンデンサ追加

・完成

という事で物が完成。

早速温湿度・気圧測定モジュールをつけて測定開始です。


◯やってみて

無事測定を開始しています。

普段はLCDに無照明で時計を表示しています。モーションセンサーに反応があると赤色LEDが瞬灯、SWは温湿度などの現在の測定結果を10秒表示するのに使い、SWはどちらも押すとLCDバックライトが10秒点灯します。
データはモーションセンサーはその都度に、温湿度・気圧データは10分おきにMQTTでテレメトリー送信しSQLに格納しています。

(測定グラフ)

前回起動したデバイスは既に2週間動作しており不安だった安定動作もクリアできそうな感じで喜んでました。

ラズパイ zero を放熱兼用ケースへ格納(2)

◯やりたい事

前回使ってみたラズパイZERO用の放熱兼用ケース、

greensoybean.hatenablog.com

意外と使い勝手がよく重宝していたのですが、後ろのプラスチック板の厚さだけは少々閉口。
そこでアマゾンで見つけた後面パネルが金属板でできているゼロ用の放熱ケースを新たに試してみます。

◯やったこと

・組立

今回手に入れた放熱兼用ケースはこれ。

前回作ったものよりフィンが薄くWiFi電波用の空隙もないタイプなのですが裏板が薄手のアルミ板。これだと何かと操作性がよく放熱効果もダブルで期待できそうで期待大。ルンルン気分で組立に入りました。

ところがそこはやはり中華製。スタートから期待を裏切ってくれます。
まずはパーツとして皿ビスが入っているのに板の方には皿穴がありません。

しかも入っているスペーサは1種類だけで裏板側に入れると隙間が空きすぎて熱伝導パッドがラズパイに届かず。

こりゃー裏板とラズパイの保持はパッドで行えという設計?
う〜ん。さすが中華製...

「あ〜ぁ使うのをやめようかな」とも思ったのですが、裏板金属板の効果も試してみたかったので前回使った厚底放熱ケースのパーツを流用し

二個一の組み合わせで完成させる事にしました。

設計通りラズパイの保持は熱伝導パッドに任せる形を取り


(GPIO端子と競合しないようパッドは手前側に寄せておいた方がいいようです。もちろん後で適当なスペーサを見つけて固定する予定です)

ラズパイをセットし

CPU部の放熱用にグリス、パッドをセット。

最後に放熱フィン板を取り付けると完成。

今回はお手製の赤外線送信HATを運用していきます。

景気よく動作熱を放熱してくれる事を期待しましょう。

ちなみに余った部品はまてめて取っておき次の部品取り用にしまっておくことにします。


◯やってみて

やはり裏板金属の効果は絶大です。

(1分おきCPU計測グラフ)

磁石でホワイトボードにつけたらボードの熱容量が大きいせいか動作温度が30.4℃まで急落です。やり〜。
その後常駐場所に移動しても2〜3℃は動作温度が下り裏板金属の効果は大きいという結果になりました。

中華製で一手間かかりますが効果が高かったという事でとりあえずよしとしときます。

IoT学習HATをラズパイTrixieで使う

◯やりたいこと

その昔色々とお世話になった秋月電子のIoT学習HATキット。今回BME280とモーションセンサーをラズパイ上で同時に使う機会が生じたため久し振りに引っ張り出して来て最新版OSのTorixieで動作させてみる事にしました。


◯やったこと

秋月電子で販売されている学習キット、特徴は
・8文字x2行のLCD(バックライト付き:AQM0802A-FLW-GBWを実装
・3色の抵抗内蔵LED(赤・黄・緑)、Φ9mmのブザーを実装
・焦電型赤外線(人感)センサー搭載用3Pソケットを実装
・BME280(温度・湿度・気圧センサー)搭載用6Pソケット実装
・シャットダウン用スイッチ実装(黒色:別途スクリプト使用)
・自由に使えるトリガ用スイッチ実装(白色)
(HPから)

と比較的お安いのに多機能。このうち今回はモーションセンサーとBME280の機能を主に使っていきます。

・問題点&解決方法

実を言うとこのキットにはちょっとした問題点があります。LCDのドライブ用にI2C-SDAへ専用の回路が組み込まれているためBME280を基板上にあるi2cソケットに接続するとi2c通信が全滅してしまいます。最初は目が点になりました。
ただこのエラーへの対策は比較的簡単。i2cクロックをマニュアルに書いてある50KHzに下げ、BME280にはソケットに来ているSDAでなくラズパイのSDAを直接つなげば普通に通信が復活します。

・改造

ソケットに来ているSDAを使わないとはいえi2c端子を外部に引き出すには6pinモジュール用の基板ランドを利用するのがやはり1番簡単。そこでソケット部分を改修しgroveソケットをムリクリ取り付けてそこにBME280を接続する事にします。
6pin端子には上手い具合にN.CがありますのでそこへハジにあるSCLをジャンパし、SDAに来ているパターンはカットしてトランジスタ(2SA1015)のエミッタにジャンパ、あとはgroveソケットを足を広げて取り付ければ改造完了です。


・チェック

ラズパイにターミナルで接続し i2cdetect コマンドを実行して

$ i2cdetect -y 1
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:                         -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- 3e --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- 76 --

のようにLCDとBME280のアドレスが表示されれば成功です。

・gpioをTrixieで使う

OSがbookwormになってからgipoを直接制御するpigpioが非推奨になりました。推奨のlgpio,gpiozeroを使った場合gpioを複数のプロセス・スレッドから呼び出すとbusyエラーが出てえらく苦労したのですが、最終的にはgpioの資源を取得するプロセスを常駐させLEDやSWなどのpin制御をモジュール化しそれをインポートして使っています。
確保したgpioのデバイス番号、インポートしたモジュールのインスタンスなどはglobal()を使ってデータを共有、スタートすると常駐プロセスがデバイス資源を握って離さずそれを子のモジュールが必要部分を借りて使い、親が資源を離す(close)するのはシステム停止時のみという形に落ち着きました。これはi2cやSPI、赤外線制御なども同様に行なっています。

では具体的にどうやっているんだ?とここでコードを示せればいいのですが、「えだまめ」はここ半年でver2にバージョンアップしMQTTに完全移行したためコードがあまり一般的ではありません。asyncioなどの非同期関数も多用しているのでどうも公開には向かない感じです。とはいえ何も示さないのも何ですので使用中のBMP280とAHT20用のモジュールをあげておきます。AIに作ってもらったものをBugFixして使ってます。

・BMP280

class IoFncBMP280:
	def __init__(self, bus_number, i2c_addr=0x76):
		self.bus_num = int(bus_number)
		self.addr = i2c_addr
		self.bus = None
		self.temperature = 0.0
		self.pressure = 0.0
		
		# 補正係数保持用
		self.calib_T = []
		self.calib_P = []

	def __enter__(self):
		self.bus = smbus2.SMBus(self.bus_num)
		self._load_calibration()	# 起動時に補正係数を読み込む
		return self

	def __exit__(self, exc_type, exc_val, exc_tb):
		if self.bus is not None:
			self.bus.close()

	#------------------------------------------------------
	# センサー動作状況 チェック
	#------------------------------------------------------
	def check_id(self):
		try:
			# BMP280のチップIDレジスタは 0xD0 (値は 0x58)
			chip_id = self.bus.read_byte_data(self.addr, 0xD0)
			return chip_id == 0x58
		except Exception:
			return False

	# BMP280固有の補正係数をレジスタから読み出す
	def _load_calibration(self):
		try:
			# 温度用 (dig_T1 ~ dig_T3) -> 6バイト
			b1 = self.bus.read_i2c_block_data(self.addr, 0x88, 6)
			self.calib_T.append((b1[1] << 8) | b1[0]) # dig_T1 (unsigned)
			
			for i in range(1, 3): # dig_T2, dig_T3 (signed)
				val = (b1[i*2+1] << 8) | b1[i*2]
				if val & 0x8000: val -= 65536
				self.calib_T.append(val)

			# 気圧用 (dig_P1 ~ dig_P9) -> 18バイト
			b2 = self.bus.read_i2c_block_data(self.addr, 0x8E, 18)
			self.calib_P.append((b2[1] << 8) | b2[0]) # dig_P1 (unsigned)
			
			for i in range(1, 9): # dig_P2 ~ dig_P9 (signed)
				val = (b2[i*2+1] << 8) | b2[i*2]
				if val & 0x8000: val -= 65536
				self.calib_P.append(val)
		except Exception as e:
			raise Exception(f"補正係数の読み込みに失敗: {e}")

	#------------------------------------------------------
	# 測定値 取得
	#------------------------------------------------------
	def forced(self):
		# ctrl_meas (0xF4): 湿度なし、温度x1, 気圧x1, Forcedモード(0x01) -> 0x29
		# config (0xF5): フィルターなし -> 0x00
		self.bus.write_byte_data(self.addr, 0xF5, 0x00)
		self.bus.write_byte_data(self.addr, 0xF4, 0x29)

		# 測定完了待ち (Forcedモードは測定後自動でスリープに戻る)
		for _ in range(10):
			sleep(0.01)
			status = self.bus.read_byte_data(self.addr, 0xF3)
			if (status & 0x08) == 0:	# measuring bit が 0 で完了
				break
		else:
			raise Exception("BMP280 測定タイムアウト")

		# データ読み出し (0xF7 から 6バイト [Pressure x3, Temperature x3])
		data = self.bus.read_i2c_block_data(self.addr, 0xF7, 6)
		
		raw_pres = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4)
		raw_temp = (data[3] << 12) | (data[4] << 4) | (data[5] >> 4)

		# --- 温度計算 (データシート準拠) ---
		v1 = (raw_temp / 16384.0 - self.calib_T[0] / 1024.0) * self.calib_T[1]
		v2 = ((raw_temp / 131072.0 - self.calib_T[0] / 8192.0) ** 2) * self.calib_T[2]
		t_fine = v1 + v2
		self.temperature = t_fine / 5120.0

		# --- 気圧計算 (データシート準拠) ---
		v1 = (t_fine / 2.0) - 64000.0
		v2 = v1 * v1 * self.calib_P[5] / 32768.0
		v2 = v2 + v1 * self.calib_P[4] * 2.0
		v2 = (v2 / 4.0) + (self.calib_P[3] * 65536.0)
		v1 = (self.calib_P[2] * v1 * v1 / 51200.0 + self.calib_P[1] * v1) / 524288.0
		v1 = (1.0 + v1 / 32768.0) * self.calib_P[0]

		if v1 == 0.0:
			self.pressure = 0.0
		else:
			p = 1048576.0 - raw_pres
			p = (p - (v2 / 4096.0)) * 6250.0 / v1
			v1 = self.calib_P[8] * p * p / 2147483648.0
			v2 = p * self.calib_P[7] / 32768.0
			self.pressure = (p + (v1 + v2 + self.calib_P[6]) / 16.0) / 100.0	# hPa変換

・AHT20

class IoFncAHT20:
	def __init__(self, bus_number, i2c_addr=0x38):
		self.bus_num = int(bus_number)
		self.addr = i2c_addr
		self.bus = None
		self.temperature = 0.0
		self.humidity = 0.0

	def __enter__(self):
		self.bus = smbus2.SMBus(self.bus_num)
		return self

	def __exit__(self, exc_type, exc_val, exc_tb):
		if self.bus is not None:
			self.bus.close()

	#------------------------------------------------------
	# センサー動作状況 チェック
	#------------------------------------------------------
	def check_id(self):
		# AHT20 初期化状態チェック (0x71コマンド)
		try:
			self.bus.write_byte(self.addr, 0x71)
			sleep(0.01)
			status = self.bus.read_byte(self.addr)
			# 状態ビット確認
			if (status & 0x18) == 0x18:
				return True
			return False
		except Exception:
			return False

	#------------------------------------------------------
	# 測定値 取得
	#------------------------------------------------------
	def forced(self):
		# 初期化状態チェック。未初期化なら 0xBE コマンドを送信
		if not self.check_id():
			self.bus.write_i2c_block_data(self.addr, 0xBE, [0x08, 0x00])
			sleep(0.01)

		# 1. 測定開始コマンド (0xAC, 0x33, 0x00) を送信
		self.bus.write_i2c_block_data(self.addr, 0xAC, [0x33, 0x00])
		
		# 2. 安全のために 100ms (0.1秒) しっかり待つ (ビジーチェック用のreadは挟まない)
		sleep(0.1)

		# 3. アドレス指定なしで 6バイト を一気に読み出す
		msg = smbus2.i2c_msg.read(self.addr, 6)
		self.bus.i2c_rdwr(msg)
		data = list(msg)

		# 4. ステータスバイト(先頭の1バイト目)のビジーフラグを念のため確認
		# ※もしそれでもビジーなら例外を投げる
		if (data[0] & 0x80) != 0:
			raise Exception("AHT20 が測定中(ビジー)のままです")

		# 5. 20bitの raw データから物理値へ変換
		raw_hum = ((data[1] << 12) | (data[2] << 4) | (data[3] >> 4))
		raw_tmp = (((data[3] & 0x0F) << 16) | (data[4] << 8) | data[5])

		self.humidity = (raw_hum / 1048576.0) * 100.0
		self.temperature = ((raw_tmp / 1048576.0) * 200.0) - 50.0

◯やってみて

今回はある部屋の室温を計りながら部屋の使用状況を測定する目的で使用しました。ESP8266でやってもよかったのですが電池交換が難しかったためラズパイで常時通電運転でデータを収集しました。LCDにはとりあえず日時を表示しておきLEDは点滅と人感検知表示をさせています。

我が家のえだまめネットワークもここ半年かけてやっとMQTT(Mosqutto)ベースのVer2に移行しました。この半年間はすっかりソフトざんまい。ボチボチハードの方も手をつけていこうかなと考えてます。

◯「学習用」別の意味での...

このキットは名前の通りあくまでも学習用キットなので本格的な連続運用に向いてはいません。現に現状のままでは数日程度でハングアップする事が多く無対策では少々信用のないデバイスです。
モーションセンサーなども自己ノイズで感知してしまう事が多くありパスコンは必須です。

長時間運転をさせるべく色々と対策を取る訓練ができるという意味でもある意味「学習用」なのかもしれません。笑

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)とアルカリ電池とで夏冬使い分けるのもありかもしれません。
うまくいくのを楽しみにデバッグしてます。