加湿器をラズパイでPID制御して湿度を一定にコントロールしてみた
我が家の加湿器は湿度コントロールが付いていないので、床がびちょびちょに濡れてしまうことが多々ありました。
そこで、ラズパイを使って湿度を一定にコントロールして床濡れ問題を解決しようというのが今回の目的です!
湿度センサーで湿度を測定して、PID制御で湿度を一定に保つように制御してみたいと思います。
PID制御とは
PID制御とは、簡単なフィードバック制御です。
PはProportional(比例)、IはIntegral(積分)、DはDifferential(微分)です。
基本式は次のように表されます。
\[
u(t) = K_{p}e(t) + K_{i}\int_0^t e(\tau) d\tau + K_{d}\frac{ de(t) }{ dt }
\]
u(t)は操作量、Kpは比例ゲイン、Kiは積分ゲイン、Kdは微分ゲインです。
e(t)は目標値と現在の値の差です。
この式だけでは何のこっちゃ分からないと思うので、実際に湿度コントロールするイメージで説明をしてみます。
湿度を60%に保とうとする場合、目標値=60%と湿度センサーで取得した現在の湿度との差(誤差)を元に操作量を決めます。
今回の場合であれば、操作量は加湿器の噴霧量をコントロールするツマミを回す量ということになります。
誤差が大きいほど操作量の変化が大きくなり、目標値に近づくほど変化が小さくなります。
なので、始めは一気に目標値に近づいて、その後はゆっくり目標値と一致するように制御されます。
では、比例・積分・微分制御がそれぞれどんな意味を持つかを解説していきたいと思います。
P制御
目標値と現在値の差(誤差)に比例した値で操作量を決めます。
P制御だけの場合の制御イメージはこのようになります。
問題点は、
- 収束するまで時間がかかる
- 目標値に到達しない
の2つです。
Pを大きくすると目標値に近づきますが、揺れ(リンギング)が徐々に大きくなり、大きくし過ぎると収束せずに発振してしまいます。
目標値に到達しない理由は、外乱(今回の場合は風で水蒸気が流されるなど)の影響があること。
例えば、外乱が弱い場合に合わせてゲインKpを設定した場合、外乱が大きくなった場合に目標値に到達しなくなります。
外乱の強さによってKpを動的に変えるのは難しいので、積分制御を加えて外乱が変化しても目標値に到達するようにします。
I制御
比例制御に積分制御を加えたものをPI制御と言います。
I制御は長い時間誤差が残っているほど操作量の変化を大きくします。
よって、I制御を加えることでこのように目標値に到達できるようになります。
Iを大きくすると目標値に近づく時間は短くなりますが、リンギングが大きくなり、収束するまで時間がかかるようになります。
D制御
PI制御を行っても、リンギングが収まるのに時間がかかっています。
これを抑えるのが微分制御(D制御)です。
D制御は誤差の変化量に応じて操作量を調整します。
変化量が大きいほど操作量の変化を抑えようとするので、行き過ぎにブレーキをかける役割を果たします。
これにより、リンギングが抑えることができます。
Dを大きくすると変動を抑える力が強くなりますが、大きくし過ぎると過敏に反応しすぎて安定しにくくなります。
回路図
今回使用した主な部品は次の3つです。
これらを直接接続するだけです。
とっても簡単ですね。
サーボは針金棒人間に持たせて高さを調整しました。
ちょっとシュールな絵面です。
PID制御のプログラム
ラズパイピコを使うのでMicroPythonでプログラムを組んでいきます。
from machine import Pin, I2C
from machine import Pin, PWM
import utime
Kp = 45 #比例ゲイン
Ki = 1 #積分ゲイン
Kd = 50 #微分ゲイン
dt = 1 #時間ステップ(秒)
integral = 0 #積分部分の初期値
ePre = 0 #1回前の誤差の初期値
target = 50 #目標湿度(%)
#サーボの初期設定
pwm = PWM(Pin(22))
pwm.freq(50)
pwm.duty_u16(1638)
utime.sleep(5)
#I2Cの設定
i2c = I2C(0,scl=Pin(5), sda=Pin(4), freq=100000)
#温湿度センサーの初期設定
addr = 0x45
i2c.writeto_mem(addr, 0x30, b'\xA2')
utime.sleep_ms(30)
i2c.writeto_mem(addr, 0x30, b'\x41')
utime.sleep_ms(30)
i2c.writeto_mem(addr, 0x30, b'\x66')
utime.sleep_ms(30)
previousTime = 0
while True:
#1秒ごとに測定&計算を行う
currentTime = utime.ticks_ms()
if currentTime - previousTime >= dt * 1000:
previousTime = currentTime
#クロック・ストレッチ無効,繰り返し精度レベル高
i2c.writeto_mem(addr, 0x24, b'\x00')
utime.sleep_ms(30)
#0x00から6バイト分読み出し
data = i2c.readfrom_mem(addr, 0x00, 6)
#湿度の計算
fbVal = (100.0 * ((data[3] * 256.0) + data[4])) / 65535.0
#PID制御
e = target - fbVal
integral += (ePre + e) * dt
differential = (ePre - e) / dt
ePre = e
U = Kp * e + Ki * integral + Kd * differential #操作量Uはサーボの角度
#Uがサーボの制御範囲:±90°を超えないようにする
if U < -90:
U = -90
if U > 90:
U = 90
#Uを元にPWM DUTYを計算
ton = 2 * (U + 90) / 180 + 0.5
dc = ton * 50 / 1000
dc_conversion = dc * 65535
pwm.duty_u16(int(dc_conversion))
print('湿度: %-3.1f%' % fbVal)
else:
utime.sleep_ms(10)
温湿度センサとサーボの制御については下記記事で解説しているので、ここでは割愛します。
PID制御のコード解説
50行目で温湿度センサで取得した湿度を計算し、fbValとしています。
53行目で、誤差:eはe = target – fbValとしています。
targetは目標湿度で、11行目で60%と設定しています。
つまり、誤差:eは目標湿度と現在の湿度との差です。
54行目で積分制御を規定しています。
integralはPID制御の基本式の
\[
\int_0^t e(\tau) d\tau
\]
の部分です。
誤差の時間積分なのですが、簡単にするため現在の誤差:eと前回の誤差:ePreの和に解析時間:dtを掛けています。
台形近似になるので2で割るべきですが、式をシンプルにするために積分ゲインKiに含めます。
繰り返しごとに前のintegralと足し合わせているので、0~tの積分になるわけです。
55行目は微分制御を規定しています。
differentialはPID制御の基本式の
\[
\frac{ de(t) }{ dt }
\]
の部分です。
微分なので、前回と今回の誤差の差分をdtで割っています。
58行目でPID制御を全て足し合わせて操作量:Uとしています。
操作量はサーボの回転角度です。
そのため、SG90の制御範囲の±90°を超えないようにします(61~64行目)
そして、角度UをPWMのDUTYに変換し、SG90へ出力します(67行目~70行目)
パラメータチューニング
はじめにKpの値を決めます。
湿度を目標値の±2%の範囲内で制御するようにするため、Kp=45としました。
これでP制御だけの場合は、湿度=48%の時にサーボの回転角が90°(最大噴霧)、湿度=52%の時に-90°(噴霧量=0)となります。
次にシリアルでintegral、differential、Uの値を出力し、モニタしながらパラメータを調整していきます。
湿度の変化はゆっくりなため、Kiは小さめで1としました。
それでも、初期値が目標値と大きく離れている場合は、目標値に達する頃にはintegralが大きくなりすぎてしまいます。
しかし、これ以上Kiを下げると、目標値になかなか達しないという問題が発生しました。
そこで、Ki=1のままにし、integralに制限をかけました。
if integral < -30 / Ki:
integral = -30 / Ki
if integral > 30 / Ki:
integral = 30 / Ki
どれだけ時間が経過しても、サーボの回転角換算で±30°までの影響しか与えないようにしました。
これにより収束性が向上しました。
最後にD制御ですが、湿度の変化が緩やかなので、differentialは0.01程度でした。
そこでKdは大き目の50としました。
1秒で湿度が0.1%変化すれば、変化を抑える側に5°サーボを回転させるくらいの影響度です。
湿度モニタ結果
部屋全体の湿度がある程度一定になるまで、オーバーシュートが出ています。
急に湿度が下がった時に、D制御が効いて一気に湿度が上がっているようです。
部屋全体が加湿されると、湿度の変動が緩やかになり安定します。
外乱を与えてもすぐに目標値に戻っています。
地味ですが、サーボで加湿器のツマミをちょこちょこ調整している様子です。
加湿器をサーボでPID制御しました。 pic.twitter.com/rPSpmhgH5Q
— りょうのすけ (@ryo_analog) December 11, 2022