14
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ラズベリーパイ用 24V入力 UPS基板作ってみた

14
Last updated at Posted at 2026-05-05

今回作った基板

IMG_4251.jpeg

販売ページ https://raspikoubou.theshop.jp/items/139877475

今回作ったのは、Raspberry Pi 用の産業用 UPS(無停電電源装置)HAT 基板です。
前作の簡易UPS基板が好評(自称)だったのですが...入力がDC5Vでした。
工場などの産業用途だと電源が DC24V なんですよね。なので、前作をそのまま使おうとすると別途降圧回路が必要で、めちゃくちゃ不便でした。

前作 https://qiita.com/RaspiKoubou/items/667cb0690f31298bb53c

ということで、今回は DC24V の産業用電源をそのまま直接入力できる UPS HAT 基板を設計・製作しました!


前作からの主な改善点は下記の通りです。

  • DC24V 入力対応(産業用電源をそのまま使える)
  • 再起動制御(EDLC に残電力があっても確実にラズパイが再起動するようにする。)
  • RTC(リアルタイムクロック)搭載(電源断後も時刻を保持)

工場や制御盤の中を覗いたことがある方はご存知かと思いますが、産業用途の電源はほぼ DC24V が標準です。センサー、PLC(プログラマブル・ロジック・コントローラ)、各種アクチュエータ、ほとんど24Vで動いていることが多いんですね。なんでなんですかね?

産業機器がDC24Vなのは、1970〜80年代にPLCメーカーが24Vを標準採用したことで業界全体に普及し、そのエコシステムが自己強化されてきたのが最大の理由。電圧的にも、5Vより配線損失・ノイズ耐性で有利で、かつ60V以下の安全電圧に収まる。結果として「センサーも電磁弁も電源も全部24V」という状態が半世紀続き、今さら変えるメリットがない。 (by Claude)

前作の UPS 基板は5V入力だったので、24V環境で使おうとすると「24V → 5V」に変換するDC-DCコンバータを別途用意する必要がありました。
部品点数も増えるし、スペースも取るし、コストもかかるし、正直面倒くさいですよね。

「だったら最初から24V入力に対応させればいいじゃないか」という、至ってシンプルな動機です。


では、各機能について順番に説明していきます。

回路について


回路概要

回路全体は大きく4つのブロックで構成されています。

  1. 降圧回路:24V → 4.4V に降圧する
  2. 充電回路:降圧した電力を EDLC に充電する
  3. 昇圧回路:4.4V(または EDLC)→ 5.16V に昇圧してラズパイへ供給する
  4. 再起動制御回路:24V 投入を検知し、昇圧回路をリセットする
    UPS・バッテリ回路に大層な仕組みは使っていません。ダイオードOR回路を使い、通常時は24Vの電力を使って、停電時はバッテリの電力を使っています。
    つまり電源が正常なときも停電時も、同じ昇圧回路を通してラズパイに5Vを供給します。そのため、停電を検知して電源を切り替える際の瞬断が発生しません。前作と同じアーキテクチャを引き継ぎつつ、24V対応と各種保護機能を追加した形です。

スクリーンショット 2026-03-29 21.19.31.png

回路図

こちらが詳細な回路図になります。
左上が外部の24V電源の入力、そこから右下のラズベリーパイのGPIOへ繋がっています。
[24V→5V降圧回路]→[UPS回路(昇圧回路)]→[ラズパイ]という流れになっています。
Untitled.jpg


バッテリーに EDLC(電気二重層コンデンサ)を使う理由

前作でも同じ話をしたのですが、改めて説明します。

「バッテリー」と聞くと、スマホのモバイルバッテリーに入っているリチウムイオン電池をイメージする方が多いと思います。
ただ、今回は EDLC(電気二重層コンデンサ) を使っています。

image.png

リチウムイオン電池ではなく EDLC を選んだ理由は以下の通りです。

  • 発火リスクが低い:リチウムイオン電池は電気エネルギーを化学反応として蓄えるため、短絡や過熱が起きると反応が暴走し、発火・爆発につながることがあります。一方 EDLCは電荷を静電気的に蓄えるだけで化学反応を伴いません。発火するリスクは大幅に低くなります。
  • 劣化しにくくメンテナンスフリー:コンデンサは室温であれば 5〜10 年は普通に使えます。電池交換の手間がなく、「気がついたらUPSが正常に動かなかった」なんてトラブルを回避できます。
  • 充電が速い:電源が復旧したら素早く充電が完了します。「充電中にまた停電した」という状況でも EDLCなら短時間で再充電されます。
  • コストが低い:リチウムイオン電池の管理回路と比べると、はるかに安上がりです。
  • エネルギー密度:リチウムイオン電池の方が容量が大きいですが、ラズパイのシャットダウン時間を稼ぐだけなら、EDLC のエネルギー密度で十分です。

今回使用した EDLC は 10F / 5.5V(型式:HT-5R5-Z106VYL19)です。
充電電圧は約4.4V となります。定格を超えると破裂の危険がありますので、大きめにマージンを取っています。
充電電流は100Ω×5並列 = 約20Ωの電流制限抵抗(R22〜R26)により、約220mA 以下 に制限しています。充電電流が大きすぎると、ラズパイのための電源を食いすぎてしまうため制限しています。


停電検知(GPIO27)

停電の検知は GPIO27 で行います。

24Vが遮断されると、ここが5V(HIGH)から0V(LOW)になりますので、それをGPIO27で検出します。
ポイントは ブリーダー抵抗(R14:1kΩ) の存在です。
停電検知ラインにはノイズ対策のコンデンサが入っています。コンデンサは電荷を蓄える性質があるため、停電直後もしばらく電圧が残ってしまいます。そこでブリーダー抵抗(R14)を追加し、停電と同時にコンデンサの電荷を素早く放電させて、検知信号をすぐ LOW にするようにしています。
また、ダイオード(D3)のリーク電流による誤検知も防止できます。地味だけど大事な工夫です。

サンプルコード(停電検知 → シャットダウン)

停電を検知したら即シャットダウンするコールバック実装例です。
GPIO27 の Falling エッジ割り込み を使うので、ポーリング不要でCPU負荷もほぼゼロです。

import time
import subprocess
import RPi.GPIO as GPIO

GPIO_POWER_FAIL = 27   # 停電検知入力ピン (BCM番号)
BOUNCE_TIME_MS  = 300  # チャタリング対策 [ms]

def power_fail_callback(channel: int) -> None:
    """GPIO27 Falling エッジ検知時に呼ばれるコールバック。"""
    print(f"[警告] GPIO{channel}: 停電検知!シャットダウンを開始します...")
    # シャットダウン前に RTC の CLKOUT を無効化して EDLC 残量を温存
    rtc_disable_clkout()   # ← 後述の関数
    time.sleep(1)
    subprocess.run(["sudo", "shutdown", "-h", "now"], check=False)

# GPIO 初期化
GPIO.setmode(GPIO.BCM)
GPIO.setup(GPIO_POWER_FAIL, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.add_event_detect(
    GPIO_POWER_FAIL,
    GPIO.FALLING,
    callback=power_fail_callback,
    bouncetime=BOUNCE_TIME_MS
)
print("停電監視中... (Ctrl+C で終了)")
try:
    while True:
        time.sleep(1)
except KeyboardInterrupt:
    pass
finally:
    GPIO.cleanup()

再起動制御

前作で困っていた問題を解決するために追加した機能です。

前作の問題点

前作の UPS では、「シャットダウン後に電源を再投入しても、ラズパイが起動しない」問題が発生することがありました。

原因は EDLC の残留電力です。
シャットダウン直後は EDLC にまだ電力が残っています。そのせいで電源が復旧しても電源の立ち上がりが起こらないためラズパイからすると「電源が一瞬も切れていない」状態になってしまいます。
電源の立ち上がりエッジが見えないとラズパイは起動しないので、電源入れたのに起動しないという現象が起きていました。
image.png

解決策:モノステーブル・マルチバイブレータ

image.png

解決策として SN74LVC1G123DCTR(モノステーブル・マルチバイブレータ)を採用しました。
電源の再投入(0V → 24V)を検知すると、以下の順で動作します:
① SN74LVC1G123 の Q 信号が約2秒間 HIGH になる
② 昇圧IC(TPS61230A)の EN ピンが LOW になる
③ ラズパイへの5V供給が一時的に遮断される
→ これにより、確実にパワーサイクルがかかり、ラズパイが正常に再起動します。

EDLC に電力が残っていても、24V投入と同時に必ず一度5V出力が遮断されるので、確実にパワーサイクルがかかります。


実際に再投入した時の電圧波形です。このように一時的に電源を遮断します。
コンデンサがありそれの放電に時間がかかっているので、完全な0Vにはなっていませんが、問題なく起動します。
どこまで下げれば良いのか、公式の情報が見つからないですが、Raspberry Pi Forumでの情報では「3.5Vから4.1Vまでの間をどれだけ素早く立ち上がったか」というのが条件らしいですね。(https://forums.raspberrypi.com/viewtopic.php?t=391220)
image.png

再投入時のラズベリーパイのGPIO 5Vピンの電圧波形


瞬停保護(GPIO26)

瞬停(瞬間停電) とは、雷サージなどで数十ミリ秒〜数百ミリ秒だけ電源が落ちる現象です。
こういった瞬停のたびに上記の再起動(2.2秒間の5V遮断)が走ってしまうと、ラズパイが頻繁に再起動してしまいます。困ります。

そこで、瞬停保護機能を用意しました。

仕組み

image.png

GPIO26(37番ピン) を HIGH にしておくだけです。

GPIO26 を HIGH にしておくと、基板上の MOSFET(Q2)が働き、再起動トリガー回路(モノステーブル・マルチバイブレータ)の信号を無効化します。
つまり、瞬停が起きても5V遮断が走らず、ラズパイは再起動しません。
動作中は GPIO26 を HIGH にしておくだけで自動的に瞬停保護が有効になるため、プログラムの実装も簡単です。

サンプルコード(瞬停保護 ON/OFF)

import RPi.GPIO as GPIO
import time

GPIO_RESET_PREVENT = 26   # 瞬停保護出力ピン (BCM番号)

GPIO.setmode(GPIO.BCM)
GPIO.setup(GPIO_RESET_PREVENT, GPIO.OUT, initial=GPIO.LOW)

def enable_glitch_protection():
    """瞬停保護を有効化する(GPIO26 を HIGH にする)。"""
    GPIO.output(GPIO_RESET_PREVENT, GPIO.HIGH)
    print("瞬停保護:有効")

def disable_glitch_protection():
    """瞬停保護を無効化する(GPIO26 を LOW にする)。"""
    GPIO.output(GPIO_RESET_PREVENT, GPIO.LOW)
    print("瞬停保護:無効")

# アプリ起動時に瞬停保護を有効化
enable_glitch_protection()

try:
    while True:
        time.sleep(1)  # ここにアプリの処理を記述
except KeyboardInterrupt:
    pass
finally:
    GPIO.cleanup()

シャットダウンしたら GPIO26 は自動的にLOW に戻ります。すると次に24Vが正常に投入されたとき、再起動が正常に機能します。


RTC による時刻保持

産業用途では「時刻が正確であること」が重要になるケースが多いです。
しかし産業用途では、セキュリティ規定によりネットワークへの接続が許可されないケースも珍しくありません。NTP で時刻同期できない環境では、ラズパイがシャットダウンしている間に時刻がずれてしまいます。そこで今回は RTC を搭載しました。

使用部品

  • RTC IC:PCF8563T/5,518(NXP Semiconductors製)
  • 発振子:32.768kHz 水晶振動子(15pF負荷容量)
  • 通信方式:I2C(アドレス 0x51

どれくらい時刻を保持できるの?

EDLC の電力で RTC が動作し続けられる時間を実測データから計算した結果、約14日間 の時刻保持が可能です。
スクリーンショット 2026-04-04 11.59.31.png

上の図のように、停電後も EDLC は少しずつ放電を続けます。EDLC の電圧が 2.4V を下回ると昇圧IC が停止し、ラズパイへの5V供給が止まります。しかしその後も EDLC の電圧は下がり続け、1.0V になるまでは RTC が動作して時刻を保持し続けます。

項目
ラズパイ停止時のEDLC電圧 約2.4V
RTC 動作限界のEDLC電圧 約1.0V
2.4V→1.0Vの放電時間(実測から計算) 約14日間
時刻精度(実測) +1.79秒/日(約 +20.7 ppm 相当)

時刻精度は実測で 1日あたり約 +1.79秒の進み(約 +20.7 ppm 相当)でした。一般的な 32.768kHz 水晶の精度は室温で ±20〜30 ppm 程度とされており、実測値はその範囲内に収まっています。


サンプルコード(RTC 読み書き)

import smbus2
import datetime

I2C_BUS   = 1
RTC_ADDR  = 0x51
RTC_REG   = 0x02   # 秒レジスタ先頭

def bcd_to_int(v): return ((v >> 4) & 0x0F) * 10 + (v & 0x0F)
def int_to_bcd(v): return ((v // 10) << 4) | (v % 10)

def read_rtc():
    """RTC から時刻を読み取り、OS との差分も表示する。"""
    with smbus2.SMBus(I2C_BUS) as bus:
        raw = bus.read_i2c_block_data(RTC_ADDR, RTC_REG, 7)
    os_now = datetime.datetime.now().replace(microsecond=0)
    sec  = bcd_to_int(raw[0] & 0x7F)
    mn   = bcd_to_int(raw[1] & 0x7F)
    hr   = bcd_to_int(raw[2] & 0x3F)
    day  = bcd_to_int(raw[3] & 0x3F)
    mon  = bcd_to_int(raw[5] & 0x1F)
    yr   = bcd_to_int(raw[6]) + 2000
    rtc_dt = datetime.datetime(yr, mon, day, hr, mn, sec)
    diff   = (rtc_dt - os_now).total_seconds()
    print(f"RTC : {rtc_dt}")
    print(f"OS  : {os_now}")
    print(f"誤差: {diff:+.0f} 秒(正=RTCが進んでいる)")

def write_rtc():
    """OS の現在時刻を RTC に書き込む。"""
    now = datetime.datetime.now()
    wd  = (now.weekday() + 1) % 7   # 0=日曜
    data = [int_to_bcd(x) for x in
            [now.second, now.minute, now.hour,
             now.day, wd, now.month, now.year % 100]]
    with smbus2.SMBus(I2C_BUS) as bus:
        bus.write_i2c_block_data(RTC_ADDR, RTC_REG, data)
    print(f"RTC に書き込みました: {now:%Y/%m/%d %H:%M:%S}")

def rtc_disable_clkout():
    """CLKOUT 出力を無効化して消費電流を最小化する。"""
    with smbus2.SMBus(I2C_BUS) as bus:
        bus.write_byte_data(RTC_ADDR, 0x0D, 0x00)  # FE=0
    print("RTC CLKOUT を無効化しました(省電力)")

💡 rtc_disable_clkout() を呼ぶとデフォルトで出力されている 32.768kHz クロックが止まり、シャットダウン後の EDLC 消費を大幅に削減できます。忘れずに呼びましょう!


EDLC の電圧監視(ADS1110 ADC)

EDLC の残電圧を Raspberry Pi から読み取れるようにするため、ADC を搭載しています。
ちゃんと充電できているかの確認、それと停電時に残りの猶予時間を推定するためです。

使用部品と回路構成

  • ADC IC:ADS1110A0IDBVR(Texas Instruments製)/ I2C 0x48
  • 分解能:16ビット・デルタシグマ型 / 内蔵リファレンス 2.048V
  • 動作電圧:3.3V(GPIO 1番ピン) / SDA: GPIO2 / SCL: GPIO3

image.png


シングル変換モードによる超低消費電力化

これが地味に重要な話です。
ラズパイが停止した後は RTC のみがバッテリの残存電力で動き続け、ADC は使いません。しかし ADC をデフォルト(連続変換モード)のまま放置すると約240µA を消費し続け、EDLC の残量を消耗してしまいます。そのため ワンショットモード に設定し、使用しないときは自動でスリープさせることが重要です。

image.png

モード 待機電流 備考
連続変換(デフォルト) 約240µA 常時変換し続ける
ワンショット変換 約0.05µA 変換後に自動スリープ

サンプルコード(ADC 電圧読み取り)

import smbus2
import time

I2C_BUS       = 1
ADC_ADDR      = 0x48
ADC_VREF      = 2.048   # 内蔵リファレンス [V]
ADC_CMD_ONE_SHOT = 0x9C  # ST=1, SC=1(ワンショット), DR=11(15SPS), PGA=00(x1)
ADC_DIV_RATIO = 2.5     # 分圧比の逆数 (R28=200k / (R27+R28)=500k → 2/5 → x2.5)
ADC_CAL_FACTOR = 1.0571  # 入力インピーダンス補正係数(データシートより算出)

def read_edlc_voltage() -> float | None:
    """ワンショット変換で EDLC 電圧を取得して返す(失敗時は None)。"""
    try:
        with smbus2.SMBus(I2C_BUS) as bus:
            # 1. ワンショット変換トリガー送信
            bus.i2c_rdwr(smbus2.i2c_msg.write(ADC_ADDR, [ADC_CMD_ONE_SHOT]))
            # 2. 変換完了まで待機(15SPS = 約67ms)
            time.sleep(0.080)
            # 3. 結果読み取り(3バイト: MSB, LSB, Config)
            read_msg = smbus2.i2c_msg.read(ADC_ADDR, 3)
            bus.i2c_rdwr(read_msg)
            data = list(read_msg)
        if data[2] & 0x80:   # ST/DRDY=1 → まだ変換中
            return None
        raw = (data[0] << 8) | data[1]
        if raw > 0x7FFF: raw -= 0x10000   # 2の補数変換
        vin  = (raw / 32767) * ADC_VREF   # ADC入力電圧
        vsys = vin * ADC_DIV_RATIO * ADC_CAL_FACTOR  # 実電圧に換算
        return vsys
    except OSError:
        return None

# 使用例
v = read_edlc_voltage()
if v is not None:
    print(f"EDLC 電圧: {v:.3f} V")

💡 0x9C を送信することでワンショットモードになり、変換完了後は IC が自動でパワーダウン(0.05µA)します


実験結果まとめ

IMG_4263.jpg

直流安定化電源に繋ぎます


IMG_4264.jpg

電源投入すると24V、5VのLEDが光ります。
真ん中の放電LEDはバッテリの放電確認用で、基板中央のスイッチを"放電"側にすると光ります。

実際に組み立てて測定した結果をまとめます。

バックアップ時間(フル充電時)

状態 バックアップ持続時間
デスクトップ表示のみ 18秒

18秒あれば安全なシャットダウンには十分すぎるくらいですね。

充電時間(EDLC が 0V の状態から)

充電目標電圧 所要時間
2.4V(最低バックアップ電圧) 150秒(2分30秒)
4.4V(ほぼ満充電) 692秒(11分32秒)

電源投入後、約2分30秒以上でバックアップが効く状態になります。フル充電まではちょっと時間がかかりますが、最低限のバックアップ電圧(2.4V)までは意外と早く達します。

RTC 時刻保持時間

項目 実測値
時刻保持時間 14日間(実測データに基づく計算値)
時刻精度(実測) 1日あたり約 +1.79秒 の進み

使用 GPIO 一覧

機能 ピン番号 GPIO 番号
電源(5V) 2、4番ピン
電源(3.3V) 1番ピン
GND 6、9、14、20、25、30、34、39番ピン
停電検知信号(入力) 13番ピン GPIO27
瞬停保護リセットマスク(出力) 37番ピン GPIO26
I2C SDA 3番ピン GPIO2
I2C SCL 5番ピン GPIO3

テストスクリプトについて

今回の記事で紹介した全機能(RTC・ADC・停電検知・瞬停保護・充電モニタリング)をインタラクティブに操作できる CUI テストツールのフルコードを載せます。

実行するとこんなメニューが表示されます。

=========================================
    24V UPS HAT 基板テストツール
=========================================
  [1] RTCから時刻を取得して表示
  [2] OSの現在時刻をRTCに書き込む
  [3] ADCから現在の電圧を取得して表示
  [4] リセット防止信号 (GPIO26) を High にする
  [5] リセット防止信号 (GPIO26) を Low にする
  [6] RTC CLKOUT を無効化する (省電力テスト)
  [7] EDLC充電電圧モニタリング(30秒間)
  [9] 終了
-----------------------------------------
テストスクリプト
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
24V UPS HAT基板 テストスクリプト
対象機器: Raspberry Pi 5 + 自作HAT (24V UPS)

【搭載IC】
  IC3: RTC  PCF8563T   (I2C Addr: 0x51, Bus1)
  IC4: ADC  ADS1110A0  (I2C Addr: 0x48, Bus1)

【GPIO】
  GPIO27: 停電検知入力  (通常High → 停電時Falling)
  GPIO26: リセット防止信号出力 (初期Low)

依存ライブラリ:
  pip install smbus2 RPi.GPIO
"""

import sys
import time
import datetime
import subprocess

try:
    import smbus2
except ImportError:
    print("[エラー] smbus2 がインストールされていません。")
    print("  -> pip install smbus2  を実行してください。")
    sys.exit(1)

try:
    import RPi.GPIO as GPIO
except ImportError:
    print("[エラー] RPi.GPIO がインストールされていません。")
    print("  -> pip install RPi.GPIO  を実行してください。")
    sys.exit(1)


# ============================================================
# 定数定義
# ============================================================

# I2C バス番号
I2C_BUS = 1

# RTC (PCF8563T) 設定
RTC_ADDR          = 0x51   # I2Cアドレス
RTC_REG_START     = 0x02   # 秒レジスタ(先頭)
RTC_REG_COUNT     = 7      # 秒・分・時・日・曜日・月・年 の7バイト
RTC_REG_CLKOUT    = 0x0D   # CLKOUTレジスタ (クロック出力制御)
# CLKOUT レジスタ値
RTC_CLKOUT_OFF    = 0x00   # bit7(FE)=0: CLKOUT出力を無効化 → 消費電流を最小化

# ADC (ADS1110A0) 設定
ADC_ADDR          = 0x48   # I2Cアドレス
ADC_VREF          = 2.048  # 内蔵リファレンス電圧 [V]
ADC_FULLSCALE     = 32767  # 16bit符号付き最大値 (2^15 - 1)
ADC_DIV_RATIO     = 2.5    # 分圧比の逆数 (R27=300kΩ, R28=200kΩ → 2/5 → ×2.5)


# ワンショット変換用 設定レジスタ値 (データシート Table 2 参照)
# Bit7(ST)=1: 変換開始トリガー
# Bit4(SC)=1: ワンショットモード
# Bit3-2(DR)=11: 15SPS (16bit分解能)
# Bit1-0(PGA)=00: ゲイン×1
# → 1001_1100 = 0x9C
ADC_CMD_ONE_SHOT  = 0x9C

# ワンショット変換 完了ポーリング設定
# データシート p.8 より、15SPS時の1変換に要する時間 ≒ 67ms。
# 変換開始直後は出力レジスタに「前回の変換結果」が残っているため、
# まず ADC_CONV_WAIT 秒だけ待機して変換完了を確実にしてからポーリングを行う。
ADC_CONV_WAIT     = 0.080  # 変換完了待機時間 [s] (1/15SPS ≈ 67ms に余裕を加えた値)
ADC_POLL_INTERVAL = 0.010  # ポーリング間隔 [s] (10ms)
ADC_POLL_TIMEOUT  = 0.200  # タイムアウト上限 [s] (67ms の約3倍)

# GPIO ピン番号 (BCMモード)
GPIO_RESET_PREVENT = 26    # リセット防止信号出力
GPIO_POWER_FAIL    = 27    # 停電検知入力

# チャタリング対策時間 [ms]
BOUNCE_TIME_MS = 300


# ============================================================
# BCD エンコード / デコード ユーティリティ
# ============================================================

def bcd_to_int(bcd_val: int) -> int:
    """BCD値(1バイト)を整数に変換する。"""
    return ((bcd_val >> 4) & 0x0F) * 10 + (bcd_val & 0x0F)


def int_to_bcd(val: int) -> int:
    """整数をBCD値(1バイト)に変換する。"""
    return ((val // 10) << 4) | (val % 10)


# ============================================================
# 1. RTC 操作 (PCF8563T)
# ============================================================

def read_rtc() -> None:
    """
    RTCのレジスタ (0x02〜0x08) を読み取り、
    BCDデコードした現在時刻とOSとの時刻誤差をコンソールに表示する。
    """
    try:
        with smbus2.SMBus(I2C_BUS) as bus:
            # レジスタ先頭アドレスを指定してまとめて7バイト読み取り
            raw = bus.read_i2c_block_data(RTC_ADDR, RTC_REG_START, RTC_REG_COUNT)

        # RTC読み取り直後にOS時刻を取得(誤差比較用)
        # RTCはサブ秒を持たないため、OS時刻もマイクロ秒を切り捨てて秒単位で比較する
        os_now = datetime.datetime.now().replace(microsecond=0)

        # PCF8563T のマスクビット
        #   秒   [0x02]: bit7 = VL (電圧低下フラグ) → 下位7bitが有効
        #   分   [0x03]: 下位7bit有効
        #   時   [0x04]: 下位6bit有効
        #   日   [0x05]: 下位6bit有効
        #   曜日 [0x06]: 下位3bit有効
        #   月   [0x07]: bit7 = Century bit → 下位5bit有効
        #   年   [0x08]: 全8bit有効
        seconds  = bcd_to_int(raw[0] & 0x7F)
        minutes  = bcd_to_int(raw[1] & 0x7F)
        hours    = bcd_to_int(raw[2] & 0x3F)
        day      = bcd_to_int(raw[3] & 0x3F)
        # weekday  = raw[4] & 0x07  (今回は表示用途なし)
        month    = bcd_to_int(raw[5] & 0x1F)
        year     = bcd_to_int(raw[6]) + 2000  # PCF8563Tは下2桁格納

        # VLビットの確認 (bit7 of 秒レジスタ)
        vl_flag = bool(raw[0] & 0x80)

        try:
            # RTC時刻を datetimeオブジェクトに変換
            rtc_dt = datetime.datetime(year, month, day, hours, minutes, seconds)

            # OS時刻との差分を計算 (正: RTCが進んでいる / 負: RTCが遅れている)
            diff = rtc_dt - os_now
            diff_sec = diff.total_seconds()

            print(f"\n  [RTC 現在時刻]")
            print(f"    RTC 時刻 : {year:04d}/{month:02d}/{day:02d}  {hours:02d}:{minutes:02d}:{seconds:02d}")
            print(f"    OS  時刻 : {os_now.strftime('%Y/%m/%d  %H:%M:%S')}")
            print(f"    時刻誤差 : {diff_sec:+.0f} 秒  (正=RTCが進んでいる / 負=RTCが遅れている)")
            if vl_flag:
                print("  [警告] VLフラグが立っています。バックアップ電源が切れ、時刻データが信頼できない可能性があります。")
                print("         メニュー [2] を実行してOS時刻を書き込むことで解消されます。")

        except ValueError:
            print(f"\n  [RTC 現在時刻]")
            print(f"    RTC レジスタ値が不正です: {year:04d}/{month:02d}/{day:02d} {hours:02d}:{minutes:02d}:{seconds:02d}")
            print(f"    OS  時刻 : {os_now.strftime('%Y/%m/%d  %H:%M:%S')}")
            print("  [エラー] RTCのデータがカレンダーの範囲外(未初期化または破損)です。")
            if vl_flag:
                print("  [原因] VLフラグ(電圧低下)が検出されました。バックアップ電源切れにより時刻が失われています。")
            print("  [対策] メニュー [2] を実行して、OSの現在時刻をRTCに書き込んでください。")

    except OSError as e:
        print(f"\n  [エラー] RTC (0x{RTC_ADDR:02X}) への通信に失敗しました: {e}")
        print("    I2C配線・アドレス・有効化設定を確認してください。")


def write_rtc() -> None:
    """
    OSの現在時刻 (datetime.now()) を取得し、
    BCDエンコードしてRTCレジスタ (0x02〜0x08) に書き込む。
    """
    now = datetime.datetime.now()
    print(f"\n  [書き込み対象時刻] {now.strftime('%Y/%m/%d %H:%M:%S')}")

    # 曜日: PCF8563Tは 0=日曜 〜 6=土曜 (weekday()は0=月曜なので変換)
    weekday_py = now.weekday()          # 0=月曜 〜 6=日曜
    weekday_rtc = (weekday_py + 1) % 7  # 0=日曜 〜 6=土曜

    # BCDエンコード (年は下2桁のみ格納)
    data = [
        int_to_bcd(now.second),
        int_to_bcd(now.minute),
        int_to_bcd(now.hour),
        int_to_bcd(now.day),
        int_to_bcd(weekday_rtc),
        int_to_bcd(now.month),
        int_to_bcd(now.year % 100),
    ]

    try:
        with smbus2.SMBus(I2C_BUS) as bus:
            bus.write_i2c_block_data(RTC_ADDR, RTC_REG_START, data)
        print("  [OK] RTCへの時刻書き込みが完了しました。")

    except OSError as e:
        print(f"\n  [エラー] RTC (0x{RTC_ADDR:02X}) への書き込みに失敗しました: {e}")
        print("    I2C配線・アドレス・有効化設定を確認してください。")


def rtc_disable_clkout() -> None:
    """
    PCF8563T の CLKOUT レジスタ (0x0D) に 0x00 を書き込み、
    クロック出力(デフォルト: 32.768kHz)を無効化する。

    【省電力効果】
      CLKOUT 出力をOFFにすることで、EDLC バックアップ時の
      待機消費電流を削減し、RTC のバックアップ稼働時間を延ばす。

    【CLKOUTレジスタ (0x0D) ビット構成 (データシート参照)】
      bit7 (FE) : 1=CLKOUT有効(デフォルト ON), 0=CLKOUT無効
      bit1-0 (FD[1:0]): 出力周波数選択 (00=32768Hz, 01=1024Hz, 10=32Hz, 11=1Hz)
      → 0x00 を書き込むと FE=0 で完全に出力停止
    """
    try:
        with smbus2.SMBus(I2C_BUS) as bus:
            bus.write_byte_data(RTC_ADDR, RTC_REG_CLKOUT, RTC_CLKOUT_OFF)
        print("  [省電力] RTC CLKOUT を無効化しました (0x0D ← 0x00)。")
    except OSError as e:
        print(f"  [警告] RTC CLKOUT 無効化に失敗しました: {e}")
        # シャットダウン処理を止めないため例外は再送出しない


# ============================================================
# 2. ADC による電圧取得 (ADS1110A0)
# ============================================================

# DR[1:0] 設定値 → (データレート, 分解能ビット数, フルスケール値) のマッピング
# データシート Table 1 より
_DR_TABLE = {
    0b00: ("240 SPS", 12,  2047),   # 12bit: フルスケール = 2^11 - 1
    0b01: ("60 SPS",  14,  8191),   # 14bit: フルスケール = 2^13 - 1
    0b10: ("30 SPS",  15, 16383),   # 15bit: フルスケール = 2^14 - 1
    0b11: ("15 SPS",  16, 32767),   # 16bit: フルスケール = 2^15 - 1
}

# PGA[1:0] 設定値 → ゲイン倍率のマッピング
_PGA_TABLE = {0b00: 1, 0b01: 2, 0b10: 4, 0b11: 8}


def read_voltage() -> None:
    """
    ADS1110でワンショット変換を行い、分圧補正した実システム電圧を表示する。

    【ワンショット変換フロー (データシート p.10 参照)】
      1. Write: 0x9C を送信 → ST=1(変換開始), SC=1(ワンショット), DR=11(15SPS), PGA=00(×1)
      2. Poll : 設定レジスタの Bit7(ST/DRDY) が 0 になるまで読み出しを繰り返す
                0 = 変換完了、1 = 変換中
      3. Read : MSB, LSB, ConfigReg の3バイトを読み出して電圧計算
      ※変換完了後、ICは自動でパワーダウン状態(0.05µA)へ移行する

    【注意】ADS1110はレジスタアドレスポインタを持たないICのため、
    read_i2c_block_data() のように「レジスタアドレス」を指定してはならない。
    Write/Read とも i2c_rdwr() + i2c_msg を使って純粋なI2Cトランザクションを発行する。

    読み出しフォーマット (3バイト):
        Byte0: 変換結果 MSB
        Byte1: 変換結果 LSB
        Byte2: 設定レジスタ (ST/DRDY, DR, PGA 等)

    計算式:
        Vin  = (raw / fullscale) × VREF / PGA_gain  [V]
        Vsys = Vin × ADC_DIV_RATIO  (分圧比の逆数)    [V]
    """
    try:
        with smbus2.SMBus(I2C_BUS) as bus:

            # ── ステップ1: ワンショット変換トリガー送信 ──────────────────────
            # 0x9C = 1001_1100: ST=1(変換開始), SC=1(ワンショット), DR=11(15SPS), PGA=00(×1)
            #
            # 【注意】ADS1110の電源投入時のデフォルトはSC=0(連続変換モード)。
            # 0x9C 送信でワンショットモードへ切り替わるが、切り替え前の
            # 変換データが出力レジスタに残る場合がある(データシート p.8 参照)。
            # これを避けるため、変換完了ポーリングの前に ADC_CONV_WAIT 秒待機する。
            write_msg = smbus2.i2c_msg.write(ADC_ADDR, [ADC_CMD_ONE_SHOT])
            bus.i2c_rdwr(write_msg)

            # ── ステップ2: 変換完了待機 + ポーリング ─────────────────────────
            # まず 1変換周期以上(ADC_CONV_WAIT = 80ms)待機してから
            # ST/DRDY ビットのポーリングを開始する。
            # → 変換開始直後の古いデータ読み込みを防止する。
            #
            # データシート p.11 より
            #   ST/DRDY=0 の場合: 出力レジスタは「今回の変換で書き込まれた新しいデータ」。
            #   ST/DRDY=1 の場合: 出力レジスタは「以前に読み出し済みの古いデータ」または変換中。
            # また、「設定レジスタのいずれかのビットを読み出すと ST/DRDY が 1 に戻る」。
            # そのため 3バイト読み出し(データ2バイト + 設定1バイト)時の
            # 設定バイトの ST/DRDY が 0 であれば、直前の 2 バイトが新しいデータ。
            time.sleep(ADC_CONV_WAIT)  # 変換完了まで待機 (1/15SPS ≈ 67ms + 余裕)

            raw_bytes  = None
            deadline   = time.monotonic() + ADC_POLL_TIMEOUT
            while time.monotonic() < deadline:
                read_msg = smbus2.i2c_msg.read(ADC_ADDR, 3)
                bus.i2c_rdwr(read_msg)
                data = list(read_msg)
                config_reg = data[2]
                if not (config_reg & 0x80):  # ST/DRDY=0 → 変換完了・新しいデータ確認
                    raw_bytes = data
                    break
                time.sleep(ADC_POLL_INTERVAL)  # まだ変換中 → 10ms後に再試行

            # タイムアウト確認
            if raw_bytes is None:
                print(f"\n  [エラー] ADC 変換がタイムアウトしました ({ADC_POLL_TIMEOUT*1000:.0f}ms 以内に完了せず)")
                return

        # ── ステップ3: 変換結果の計算 ─────────────────────────────────────
        # バイト0: MSB、バイト1: LSB → 16bit 符号付き整数に変換
        msb = raw_bytes[0]
        lsb = raw_bytes[1]
        raw_value = (msb << 8) | lsb

        # 負値の場合を考慮した2の補数変換
        if raw_value > 0x7FFF:
            raw_value -= 0x10000

        # バイト2: 設定レジスタ → DR / PGA を取得して動的にフルスケール値を決定
        config_reg = raw_bytes[2]
        dr_bits  = (config_reg >> 2) & 0b11  # bit[3:2]
        pga_bits = config_reg & 0b11          # bit[1:0]

        rate_str, bits, fullscale = _DR_TABLE.get(dr_bits,  ("不明", 16, 32767))
        pga_gain                  = _PGA_TABLE.get(pga_bits, 1)

        # ADC入力電圧 (分圧後) を計算
        # PGAゲインが1以外の場合はVREFをゲインで割って実効フルスケールを補正
        vin = (raw_value / fullscale) * (ADC_VREF / pga_gain)

        # 実システム電圧を計算
        # ① 分圧回路の逆数 (ADC_DIV_RATIO = 2.5) を乗算して分圧前の電圧に戻す
        vsys = vin * ADC_DIV_RATIO

        print(f"\n  [ADC 電圧測定 (ワンショットモード)]")
        print(f"    設定レジスタ (raw)  : 0x{config_reg:02X}")
        print(f"    変換モード         : ワンショット (変換後パワーダウン: 0.05µA)")
        print(f"    分解能             : {bits} bit  (DR={dr_bits:02b}, {rate_str})")
        print(f"    PGAゲイン          : x{pga_gain}  (PGA={pga_bits:02b})")
        print(f"    フルスケール値     : {fullscale}")
        print(f"    ADC生値 (raw)      : {raw_value}")
        print(f"    ADC入力電圧        : {vin:.4f} V  (分圧後)")
        print(f"    実システム電圧     : {vsys:.3f} V  (分圧補正後)")

    except OSError as e:
        print(f"\n  [エラー] ADC (0x{ADC_ADDR:02X}) への通信に失敗しました: {e}")
        print("    I2C配線・アドレス・有効化設定を確認してください。")


def _read_voltage_raw() -> float | None:
    """
    ADS1110でワンショット変換を行い、補正済みシステム電圧 [V] を返す。
    変換失敗時は None を返す。

    ※ 本関数は monitor_edlc_charging() から繰り返し呼び出されるための
       内部ヘルパーです。表示処理は含みません。
    """
    try:
        with smbus2.SMBus(I2C_BUS) as bus:
            # ワンショット変換トリガー送信
            write_msg = smbus2.i2c_msg.write(ADC_ADDR, [ADC_CMD_ONE_SHOT])
            bus.i2c_rdwr(write_msg)

            # 変換完了まで待機してからポーリング
            time.sleep(ADC_CONV_WAIT)

            raw_bytes = None
            deadline  = time.monotonic() + ADC_POLL_TIMEOUT
            while time.monotonic() < deadline:
                read_msg = smbus2.i2c_msg.read(ADC_ADDR, 3)
                bus.i2c_rdwr(read_msg)
                data = list(read_msg)
                if not (data[2] & 0x80):  # ST/DRDY=0 → 変換完了
                    raw_bytes = data
                    break
                time.sleep(ADC_POLL_INTERVAL)

            if raw_bytes is None:
                return None  # タイムアウト

        # 変換結果を電圧に変換
        raw_value = (raw_bytes[0] << 8) | raw_bytes[1]
        if raw_value > 0x7FFF:
            raw_value -= 0x10000

        config_reg = raw_bytes[2]
        dr_bits    = (config_reg >> 2) & 0b11
        pga_bits   = config_reg & 0b11
        _, _, fullscale = _DR_TABLE.get(dr_bits,  ("不明", 16, 32767))
        pga_gain        = _PGA_TABLE.get(pga_bits, 1)

        vin  = (raw_value / fullscale) * (ADC_VREF / pga_gain)
        vsys = vin * ADC_DIV_RATIO
        return vsys

    except OSError:
        return None  # I2C通信エラー時は None を返す


def monitor_edlc_charging(
    duration_sec: int = 30,
    interval_sec: float = 1.0
) -> None:
    """
    指定時間(デフォルト30秒)にわたり、ADCを用いてEDLCの電圧を
    interval_sec 間隔でサンプリングし、充電時間と電圧推移を表示する。

    【動作フロー】
      1. モニタリング開始をアナウンスし、ヘッダを表示する。
      2. Ctrl+C で中断するまで、または duration_sec 秒経過するまでループ。
      3. 各ループで _read_voltage_raw() を呼び、結果を経過時間と共に表示。
      4. ループ終了後、計測結果のサマリ(最小値・最大値・電圧変化量)を表示。

    【表示例】
      経過時間 [s] |  EDLC電圧 [V]
      --------------|---------------
           0.0      |    0.000
           1.0      |    0.123
           ...
    """
    print(f"\n  [EDLC充電モニタリング開始] {duration_sec}秒間、{interval_sec:.1f}秒間隔で電圧を計測します。")
    print("  Ctrl+C で途中キャンセルもできます。")
    print()
    print(f"  {'経過時間 [s]':>12} | {'EDLC電圧 [V]':>13}")
    print(f"  {'-'*13}|{'-'*15}")

    # 計測結果を保持するリスト [(経過秒, 電圧), ...]
    results: list[tuple[float, float]] = []

    start_time = time.monotonic()
    try:
        for step in range(duration_sec):
            elapsed = time.monotonic() - start_time

            # ADCワンショット変換で電圧を取得
            vsys = _read_voltage_raw()

            if vsys is not None:
                results.append((elapsed, vsys))
                print(f"  {elapsed:>12.1f} | {vsys:>12.3f} V")
            else:
                # 変換失敗時はエラー表示(計測は継続)
                print(f"  {elapsed:>12.1f} | {'[変換エラー]':>13}")

            # 次のサンプリングタイミングまで待機
            # ワンショット変換にかかった分を差し引いて誤差を最小化する
            next_time = start_time + (step + 1) * interval_sec
            remaining = next_time - time.monotonic()
            if remaining > 0:
                time.sleep(remaining)

    except KeyboardInterrupt:
        print("\n  [中断] Ctrl+C によりモニタリングを中断しました。")

    # ── 計測サマリ表示 ────────────────────────────────────────────
    print()
    print("  " + "=" * 39)
    print("  [計測サマリ]")
    if results:
        voltages      = [v for _, v in results]
        v_min         = min(voltages)
        v_max         = max(voltages)
        v_change      = v_max - v_min
        total_elapsed = results[-1][0]
        print(f"    計測サンプル数   : {len(results)}")
        print(f"    計測時間         : {total_elapsed:.1f}")
        print(f"    電圧 最小値      : {v_min:.3f} V")
        print(f"    電圧 最大値      : {v_max:.3f} V")
        print(f"    電圧 変化量      : {v_change:+.3f} V  (最大 - 最小)")
    else:
        print("    有効なサンプルが取得できませんでした。")
    print("  " + "=" * 39)


# ============================================================
# 3. 停電検知コールバック (GPIO27)
# ============================================================

def power_fail_callback(channel: int) -> None:
    """
    GPIO27 の Fallingエッジ検知時に呼ばれるコールバック。
    停電が検知されたとしてシャットダウンコマンドを実行する。
    """
    print(f"\n\n  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
    print(f"  [警告] GPIO{channel}: 停電を検知しました!")
    print(f"  システムをシャットダウンします...")
    print(f"  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n")

    # ── シャットダウン前の省電力処理 ──────────────────────────────────
    # EDLC バッテリ残量を温存するため、不要な消費電流を削減する。
    # ① RTC の CLKOUT 出力を無効化 (PCF8563T: 0x0D ← 0x00)
    #    CLKOUT は通常 32.768kHz を出力しており、これをOFFにすることで
    #    RTC のバックアップ稼働時間(時刻保持時間)を最大化する。
    # ② ADC (ADS1110) は ワンショットモード の変換完了後に自動でパワーダウン
    #    (0.05µA) に移行するため、追加操作は不要。
    rtc_disable_clkout()
    # ────────────────────────────────────────────────────────────────

    time.sleep(1)  # メッセージを確認できるよう1秒待機
    subprocess.run(["sudo", "shutdown", "-h", "now"], check=False)


def setup_power_fail_detection() -> None:
    """
    GPIO27 を プルアップ付き入力に設定し、
    Fallingエッジ割り込みを登録する。
    """
    GPIO.setup(GPIO_POWER_FAIL, GPIO.IN, pull_up_down=GPIO.PUD_UP)
    GPIO.add_event_detect(
        GPIO_POWER_FAIL,
        GPIO.FALLING,
        callback=power_fail_callback,
        bouncetime=BOUNCE_TIME_MS
    )
    print(f"  [初期化] GPIO{GPIO_POWER_FAIL} (停電検知) の割り込み設定完了。")


# ============================================================
# 4. リセット防止信号操作 (GPIO26)
# ============================================================

def setup_gpio26() -> None:
    """
    GPIO26 を出力モードに設定し、初期値を Low にする。
    """
    GPIO.setup(GPIO_RESET_PREVENT, GPIO.OUT, initial=GPIO.LOW)
    print(f"  [初期化] GPIO{GPIO_RESET_PREVENT} (リセット防止) を出力・Low に設定完了。")


def set_gpio26_high() -> None:
    """GPIO26 を High に設定する。"""
    GPIO.output(GPIO_RESET_PREVENT, GPIO.HIGH)
    print(f"\n  [OK] GPIO{GPIO_RESET_PREVENT} を HIGH に設定しました。")


def set_gpio26_low() -> None:
    """GPIO26 を Low に設定する。"""
    GPIO.output(GPIO_RESET_PREVENT, GPIO.LOW)
    print(f"\n  [OK] GPIO{GPIO_RESET_PREVENT} を LOW に設定しました。")


# ============================================================
# 5. GPIO 初期化 / クリーンアップ
# ============================================================

def gpio_init() -> None:
    """GPIO の全体初期化を行う。"""
    GPIO.setmode(GPIO.BCM)      # BCM番号体系を使用
    GPIO.setwarnings(False)     # 再実行時の警告を抑制
    setup_gpio26()
    setup_power_fail_detection()


def gpio_cleanup() -> None:
    """使用したGPIOリソースを解放する。"""
    GPIO.cleanup()
    print("\n  [クリーンアップ] GPIO を解放しました。")


# ============================================================
# 6. CUIメニュー
# ============================================================

def print_menu() -> None:
    """操作メニューを表示する。"""
    print("\n" + "=" * 43)
    print("    24V UPS HAT 基板テストツール")
    print("=" * 43)
    print("  [1] RTCから時刻を取得して表示")
    print("  [2] OSの現在時刻をRTCに書き込む")
    print("  [3] ADCから現在の電圧を取得して表示")
    print("  [4] リセット防止信号 (GPIO26) を High にする")
    print("  [5] リセット防止信号 (GPIO26) を Low にする")
    print("  [6] RTC CLKOUT を無効化する (省電力テスト)")
    print("  [7] EDLC充電電圧モニタリング(30秒間)")
    print("  [9] 終了")
    print("-" * 43)


def main() -> None:
    """メインループ: 初期化 → メニュー表示 → 操作受付 → 終了処理。"""

    print("\n" + "=" * 43)
    print("  24V UPS HAT テストスクリプト 起動中...")
    print("=" * 43)

    # GPIO 初期化
    try:
        gpio_init()
    except Exception as e:
        print(f"[エラー] GPIO初期化に失敗しました: {e}")
        sys.exit(1)

    # メインループ
    try:
        while True:
            print_menu()
            try:
                choice = input("  選択してください: ").strip()
            except EOFError:
                # stdin が閉じられた場合の対処
                break

            try:
                if choice == "1":
                    read_rtc()
                elif choice == "2":
                    write_rtc()
                elif choice == "3":
                    read_voltage()
                elif choice == "4":
                    set_gpio26_high()
                elif choice == "5":
                    set_gpio26_low()
                elif choice == "6":
                    rtc_disable_clkout()
                elif choice == "7":
                    monitor_edlc_charging()
                elif choice == "9":
                    print("\n  終了します。")
                    break
                else:
                    print("\n  [入力エラー] 1〜7、または9を入力してください。")
            except Exception as e:
                print(f"\n  [実行エラー] 処理中に問題が発生しました: {e}")

    except KeyboardInterrupt:
        print("\n\n  Ctrl+C が押されました。終了します。")

    finally:
        # 必ずGPIOを解放する
        gpio_cleanup()
        print("  お疲れ様でした。\n")


# ============================================================
# エントリーポイント
# ============================================================

if __name__ == "__main__":
    main()


最後に

今回は産業用24V環境で使える Raspberry Pi 用 UPS HAT 基板を紹介しました。

この基板は らずぱい工房 にて販売しています。
工場・設備の IoT 活用やエッジコンピューティングなどに Raspberry Pi を活用したい方は、ぜひお試しください!

らずぱい工房 販売ページ:https://raspikoubou.theshop.jp/

14
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?