ラズパイのCPUクロックをPythonでPID制御しCPU温度コントロール

2021/05/10

IoT Python ガジェット ラズパイ 開発

RaspberrypiにSenseHATを取り付けたイメージ

こんにちはイチケンです。

これまでの記事で、LiDARカメラ搭載ラズパイと三菱PLCの連携によりFAで使えるな、というとこまでは来ました。しかし長期安定運用しようと思うと環境耐性や熱コントロールが重要になってきますよね。

ポーリングに全力出されちゃうのは無駄だなーと考えていたところ、いつも参考にさせていただいているたのじぃさんがちょうどラズパイのオーバークロックを再起動なしでテストしていたので、これを応用させていただきました。

ラズパイ オーバークロック限界に挑む!

結論としてはPythonプログラム内からラズパイのCPUクロックをコントロール可能です。今回はその手順と、実験で使ったPID制御プログラムを紹介します。

この記事でわかること

ラズパイCPUクロックをPythonプログラムでコントロールする方法がわかります。また今回は熱コントロールにPID制御を用いたので、実際にPID制御する方法もわかります。

  • ラズパイのCPUオーバークロック
  • PythonからCPUクロックコントロール
  • PID制御を実際にやる方法
  • マルチスレッド

前提条件

まず前提条件となる私の環境です。パッシブクーリングケースを使っているので全力処理を続けるとあっという間に85℃に到達してしまいます。

Raspberry Pi 4 Model B 8GB
Raspberry Pi OS Raspbian GNU/Linux 10 (buster)
VSCode 1.55.2
Python 3.7.3

ラズパイのPython開発環境はVSCodeを使用します。インストールはこちらを参照してください。

【IoT×キャンプ|Python開発環境VSCode編】意地でもラズパイとキャンプへ!らずキャン△プロジェクト第6回

サンプルコードの実行結果

サンプルでは負荷をかけながらCPU温度がターゲット温度になるようにPID制御しています。

実際に実行したときの上昇時のグラフはこちら。青:CPUクロック周波数(GHz) 赤:CPU温度(℃)。ターゲットを70℃に設定しています。67℃付近までは周波数が2.0GHzに張り付いていますが、67℃を超えてからは調整のために上下しています。

CPU温度が上昇しているときのグラフ

次のグラフが70℃到達後の動きです。周波数をコントロールすることで温度をざっくり一定に保っていることがわかります。

CPU温度が狙いの70℃付近になってからのグラフ

CPUのオーバークロック設定

では実際に試していきましょう。オーバークロックする方法は検索ですぐ見つかります。ただしどれも再起動が必要ですが。公式はこちら。

Overclocking options in config.txt

サンプルコードでは下限を0.5GHz、上限を2.0GHzにしています。まずは再起動が必要なオーソドックスな方法でオンデマンドの上下限をこの値に設定しましょう。下記コードをLXTerminalで実行してファイルを開きます。

sudo nano /boot/config.txt

スクリーンショットのように設定値を追記します。最大2GHzにオーバークロックし電圧も上げておきます。

over_voltage=6
arm_freq=2000
arm_freq_min=500
クロック周波数変更

数値を間違えていると立ち上がらなくなる可能性があるので注意してください。「Ctrl+O」で保存し「Ctrl+X」で終了します。その後ラズパイを再起動してください。オーバークロックの設定は以上です。

再起動が完了して無事立ち上がれば成功です。この時点での動きは「0.5GhZ ~ 2.0GHzの間でクロックを動的に変化させる」です。

もし立ち上がらない場合は、別のラズパイへUSBアダプタでmicroSDを挿入し、config.txtを直接編集すれば復旧可能です。

CPUクロックコントロール

次にCPUをコマンドでコントロールするためのツールをインストールします。

LXTerminalで次のコマンドを実行してください。

sudo apt install cpufrequtils

これを使うことにより、ターミナルからコマンドでCPUクロックをコントロールできるようになります。変更時に再起動も不要です。

governorと呼ばれるものをuserspaceとondemandに切り替えることでクロックを固定にするか動的にするかを選べます。ただし、userspaceを意図的に設定する必要はなく、クロックを設定すると自動的にモードがuserspaceになります。コマンド例は次の通り。実行にはsudoが必要なのでご注意を。クロックを2GHzに固定します。

sudo cpufreq-set -f 2.0GHz

動的クロックに戻す場合のコマンドはこちら

sudo cpufreq-set -g ondemand

次のコマンドで情報を確認できます。オプションは-hで何があるか見れます。-fを指定すると現在のクロックのみ表示できます。

cpufreq-info

これでコマンドでクロックをコントロールできるようになりましたね。では実際にPythonコードからコントロールしてみましょう。

PythonコードでCPUクロックコントロールするサンプル

Githubのリポジトリはこちら

https://github.com/ichiken-usa/raspi-temp-control

結果確認のためグラフ出力にmatplotlibを使用しています。未導入の方はインストールしてください。

sudo apt install python3-matplotlib

サンプルコードはこちら。クロック変更にsudoパスワードが必要です。init内のpasswordの部分をご自身のパスワードに変えてください。ソフトにsudoパスワード直打ちはかなり脆弱な作りなので、そのうちなんとかしたいですね。


#!/usr/bin/env python
# -*- coding: utf-8 -*-

# インストール必要
# sudo apt install cpufrequtils
# sudo apt install python3-matplotlib

import time
import subprocess 
import threading
import matplotlib.pyplot as plt 
import numpy as np


# CPUクロックをPIDコントロールするスレッド
class ControlCpuThread(threading.Thread):

    def __init__(self):

        super(ControlCpuThread, self).__init__()
        self.freq_MAX = 2.0 # デフォルトは1.5GHz
        self.freq_MIN = 0.5
        self.password = 'password'+'\n' # sudoパスワード
        return
    

    def read_temp(self):

        result = subprocess.run('vcgencmd measure_temp', shell=True,  stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
        
        CpuTemp = result.stdout.split()
        temp = CpuTemp[0]
        temp = float(temp[5:9])

        return temp

    def set_freq(self,freq):

        if self.freq_MIN <= freq and freq <= self.freq_MAX:

            subprocess.run('sudo -S cpufreq-set -f '+str(freq)+'GHz',shell=True,input=self.password,text=True)
            #result = subprocess.run('cpufreq-info -f',shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE,text=True)

            print(f'Freq: {freq} GHz')

    def set_ondemand(self):

        result = subprocess.run('sudo -S cpufreq-set -g ondemand',shell=True,input=self.password,stdout=subprocess.PIPE,stderr=subprocess.PIPE,text=True)
        print('set ondemand')


    def output(self,power):

        freq = round(power * self.freq_MAX, 1) #小数点一桁丸め

        # 上限調整
        if freq > self.freq_MAX:
            freq = self.freq_MAX
        
        # 下限調整
        if freq < self.freq_MIN:
            freq = self.freq_MIN

        self.set_freq(freq)

        return freq

    def p(self,temp, target, kp):
        d = target - temp
        if d < 0:
            return 0
        power = d / target * kp
        return power

    def i(self,prev, now, target, ki):
        if prev == 0:
            return 0
        d1 = target - now
        d2 = target - prev
        if d1 < 0:
            return 0
        if d2 < 0:
            d2 = 0
        return (d1 + d2) / 2 * ki

    def d(self,prev, now, kp):
        d = prev - now
        if d < 0:
            return 0
        return d * kp

    def run(self):
        
        target = 70 # 狙いのCPU温度設定(摂氏)
        prev=0

        # グラフ用
        x = []
        y_freq = []
        y_temp = []

        try:
            for k in range(300):
                print('------------------------------------')
                temp = self.read_temp()
                print (f'Target = {target}℃  Now = {temp}℃  Deff = {temp-target:.1f} ℃')

                # PID Coefficient
                pg = self.p(temp, target,6)
                ig = self.i(prev, temp, target, 0.4)
                dg = self.d(prev, temp, 0.1)
                power = pg + ig + dg
                prev = temp

                print(f'P ={pg:.3f}')
                print(f'I ={ig:.3f}')
                print(f'D ={dg:.3f}')
                print(f'power = {power:.2f}')
                
                # CPUクロック設定
                freq = self.output(power)

                # グラフ用配列にデータ格納
                x.append(k)
                y_freq.append(freq)
                y_temp.append(temp)
                
                time.sleep(1)

        except Exception as e:
            print(f'Exception: {e}')

        finally:
            self.set_ondemand()
            
            # グラフ表示
            fig, ax1 = plt.subplots()
            ax2 = ax1.twinx()

            ax1.plot(x,y_temp,color='red')
            ax2.plot(x,y_freq)

            ax1.set_ylim([50,80])
            ax2.set_ylim([0,4.0])

            plt.show()

            print('負荷スレッドは動作継続中。Ctrl+Cで抜けてください。')


# 負荷をかけるスレッド
class LoadThread(threading.Thread):

    def run(self):

        while True:
            rand1 = np.random.rand(1000,1000)
            rand2 = np.random.rand(1000,1000)
            rand3 = rand1 * rand2


if __name__ == "__main__":

    thread1 = LoadThread() 
    thread2 = ControlCpuThread() 

    # 各スレッド起動
    thread1.start()
    thread2.start()    

サクサク解説

PID制御

PID制御に関してはこちらのサイトを参考にさせていただきました。シミュレーションしてるサイトはたくさんあるのですが、実際にモノをコントロールしているサイトが全然なくて探すのに苦労しました。

ラズベリーパイで低温調理器を作る!(ソフトウェア編)

ぴったり70℃にコントロールすることが目的ではないので、パラメータはほどほどにしかチューニングしていません。

CPUクロックコントロール

subprocessを使ってコマンドでCPU温度を取ってきてpowerを決定します。


def read_temp(self):

    result = subprocess.run('vcgencmd measure_temp', shell=True,  stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)

    CpuTemp = result.stdout.split()
    temp = CpuTemp[0]
    temp = float(temp[5:9])

    return temp

それに基づいてsubprocessでCPUクロックを設定しています。実際にコマンドを送っている部分はこちら。

def set_freq(self,freq):

    if self.freq_MIN <= freq and freq <= self.freq_MAX:

        subprocess.run('sudo -S cpufreq-set -f '+str(freq)+'GHz',shell=True,input=self.password,text=True)
        #result = subprocess.run('cpufreq-info -f',shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE,text=True)

        print(f'Freq: {freq} GHz')

マルチスレッド

負荷をかける処理を分けるためにマルチスレッドにしています。PythonではThreadクラスを継承してrunメソッドをオーバーライトすることで、引数を与えることができるスレッドが作れます。今回は引数与えてませんが。

ややこしい言葉が並んでしまったので、実際にどうするんだ?というポイントを簡単にまとめます。

  • class クラス名(threading.Thread):でマルチスレッドにしたい処理のクラスを作る。
  • そのクラス内にrunメソッドを作ってメインの部分を書く。
  • スレッドクラスのインスタンス作ってstartで開始。(runじゃない)

最後に

いかがでしたか?

この機能を使えば、ポーリング中はクロックを極力下げて発熱を抑制し、トリガーがかかるとクロックを上げて高負荷処理をこなす、みたいな作りのソフトにできるのではと思います。

Githubのリポジトリはこちら

https://github.com/ichiken-usa/raspi-temp-control

スポンサーリンク

フォロワー

Labels

Amazon (3) Apache (3) Apple (9) AppleSilicon (7) Bloggerカスタマイズ (12) EchoShow15 (1) IoT (25) Jetson (1) MySQL (1) PHP (3) Python (20) Web (3) アウトドア (11) アメリカ生活 (19) ガジェット (35) キャンプ (9) ディープラーニング (1) らずキャン△ (11) ラズパイ (24) 暗号資産 (5) 開発 (31) 旅行 (8)

QooQ