1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ATOMS3でサーミスターの温度によりファンの回転数を制御する

Posted at

やりたいこと

水冷PCの水温に応じてラジエーターのファンの回転数をコントロールしたい。
ファン制御はPCとは分離して、独立した環境で動作させたい。

atomfantop-s.jpg

使用する機器と構成

  • ATOMS3
  • サーミスター 10KΩ(水冷用
  • M5Stack用ミニプロトユニット、Grove互換ケーブル
  • 抵抗 10KΩ
  • PWM対応のファン

今回はM5StackのATOMS3を使い、サーミスターの付いた温度センサーと接続して水温を測定します。サーミスターは10KΩのタイプで、10KΩの抵抗と分圧して使用します。なるべく配線を減らしたかったので、電源と入出力はGroveコネクタを使用しました。

主な機能

  • 水温に応じてファンの回転数を制御する(自動制御)
  • ボタン操作により任意の回転数を制御する(マニュアル制御)
  • Webブラウザから状態の確認や、設定の変更ができる
  • EEPROMに設定値を保存し、電源再投入後も前回の設定を維持できる

配線図

今回使用したBarrowのラジエーターキットには独自のファンコントローラーがついており、5Vの電源はファンコントローラーから取ります。PWMの信号は本来はマザーボードから取るようになっているのですが、直接ATOMS3と接続するようにします。

プログラム

Thermistor2FanPWM
/*
* サーミスタの温度に応じてファン制御用のPWMを出力する ver.0.3
*
* 使い方
*   ボタン長押しで Auto / Manual モード切り替え
*   短押しでファン回転数変更(Manualモード時のみ)
*   5秒長押しで強制リセット
*   ボタン押しながら起動するとEEPROMを初期値に戻す
*   ブラウザからの設定URL http://atomfan.local/conf
*
* 配線方法
*   GND ---- サーミスタ(10KΩ) ---- GPIO1 ---- 抵抗(10KΩ) ---- 5V
*   GPIO2 ---- FAN PWM pin
*/
#include <M5AtomS3.h>
#include <WebServer.h>
#include <ESPmDNS.h>
#include <EEPROM.h>

// 設定 WiFi
const char* WIFI_SSID = "xxxx";
const char* WIFI_PASS = "xxxx";
#define MDNS_NAME "atomfan"   // mDNSに登録するホスト名
WiFiClient client;
WebServer server(80);

// 設定 ATOMS3
#define GPIO_FAN_PWM 2        // 出力ピン PWM
#define GPIO_THERMISTOR 1     // 入力ピン サーミスタ
#define PWM_FREQ 25000        // PWM周波数
#define PWM_BIT 8             // PWMの分解能bit
#define PWM_CH 0              // PWMチャンネル
#define ADC_RESOLUTIUON 4095  // ADCの分解能(0-4095)

// その他
#define EMARGENCY_TEMP 50     // 強制的にファンをフル回転にする温度
#define EEPROM_ADDRESS 0      // EEPROMに書き込む先頭アドレス
#define TEMP_HISTERESIS 0.8   // 温度判定にヒステリシスを持たせる範囲

// 設定 サーミスタの特性
#define THERMISTOR_R25  10000  // 25℃のときの抵抗値
#define THERMISTOR_B    3435   // B定数
#define THERMISTOR_RREF 10000  // リファレンス側の抵抗値

// 内部電圧の計算用 ATOMS3の5Vと3.3Vの電圧値(必要なら変更する)
#define M5ATOMS3_3V3 3.37   // (未使用)
#define M5ATOMS3_5V0 5.0//4.91

// デバッグに便利なマクロ定義 --------
#define sp(x) USBSerial.println(x)
#define spn(x) USBSerial.print(x)
#define spf(fmt, ...) USBSerial.printf(fmt, __VA_ARGS__)
#define lp(x) M5.Lcd.println(x)
#define lpn(x) M5.Lcd.print(x)
#define lpf(fmt, ...) M5.Lcd.printf(fmt, __VA_ARGS__)
#define array_length(x) (sizeof(x) / sizeof(x[0]))

// 関数定義
void lcdtext(String text, int size=-1, int x=-1, int y=-1);
void setFanDury(int ratio);
double measureTemperature(int count=10);
void eepromSave();
void eepromLoad();
void handleTop();
void handle404();

// グローバル変数
double temp = 0;
int fan = 100;
bool manualmode = false;
uint8_t conf[10] = {  // 温度設定 温度(以上になったら), 回転数(%)
  45, 100, 
  40,  70, 
  35,  50, 
  30,  20, 
   0,  10 
};

// EEPROMに保存するデータ
struct EepromData {
  uint8_t _check;
  int fan;
  bool manualmode;
  uint8_t conf[10];
};
EepromData eeprom;

// セットアップ
void setup() {
  M5.begin(true, true, false, false);  // Init M5AtomS3
  EEPROM.begin(sizeof(eeprom));
  M5.Lcd.setRotation(2);  // USBコネクタが下側

  // GPIOの設定
  pinMode(GPIO_THERMISTOR, INPUT);
  pinMode(GPIO_FAN_PWM, OUTPUT);
  ledcSetup(PWM_CH, PWM_FREQ, PWM_BIT);  // pwm ch0 setting
  ledcAttachPin(GPIO_FAN_PWM, PWM_CH);   // pwm ch0 attach GPIO
  setFanDury(100);

  // EEPROMの内容を初期値に戻す(ボタン押しながら起動)
  if (M5.Btn.isPressed()) {
    eepromSave();
    sp("EEPROM initialized");
  }

  // WiFi接続
  lp("Wifi connecting...");
  spn("Wifi connecting.");
  for (int j=0; j<10; j++) {
    WiFi.begin(WIFI_SSID, WIFI_PASS);  //  Wi-Fi APに接続
    for (int i=0; i<30; i++) {
      if (WiFi.status() == WL_CONNECTED) break;
      spn(".");
      delay(500);
    }
    if (WiFi.status() == WL_CONNECTED) {
      spn("connected! ");
      sp(WiFi.localIP());
      break;
    } else {
      sp("failed");
      WiFi.disconnect();
    }
    delay(2000);
  }

  // Webサーバーの設定
  if (WiFi.status() == WL_CONNECTED) {
    if (MDNS.begin(MDNS_NAME)) {  // http://atomfan.local/ でアクセスできるようにする
      sp("mDNS registerd!");
    } else {
      sp("mDNS error!");
      delay(2000);
    }
    server.on("/", []() { server.send(200, "text/plain", "OK"); });
    server.on("/conf", handleConf);
    server.on("/set", handleSet);
    server.onNotFound( []() { server.send(404, "text/plain", "Not Found\n\n"); });
    server.begin();
  }

  // EEPROMからデータを読み出す
  eepromLoad();
}

// メインループ
void loop() {
  M5.update();
  server.handleClient();

  // 1秒に1回行う処理
  bool refresh = false;
  static unsigned long nexttime = 0;
  if (nexttime < millis()) {
    nexttime = millis() + 1000;
    refresh = true;

    // 温度測定
    temp = measureTemperature();

    // 自動ファンコントロール
    if (!manualmode) {
      for (int i=0; i<array_length(conf); i+=2) {
        if ((fan == conf[i+1] && temp >= (conf[i]-TEMP_HISTERESIS)) || (temp >= conf[i])) {
          fan = conf[i+1];
          break;
        }
      }
    }
  }

  // マニュアルモード
  static bool lastPressed = false;
  if (M5.Btn.wasReleased() && lastPressed) {  // 長押し
    if (!manualmode) fan = 100;
    manualmode = !manualmode;
    refresh = true;
    eepromSave();  // EEPROMにデータを保存する
  } else if (manualmode && M5.Btn.wasReleased()) {  // 短押し
    fan -= 20;
    if (fan < 0) fan = 100;
    refresh = true;
    eepromSave();  // EEPROMにデータを保存する
  } else if (M5.Btn.pressedFor(5000)) {   // 超長押し
    M5.Lcd.clear();
    lcdtext("reboot", 2, 10, 10);
    while (!M5.Btn.wasReleased()) M5.update();
    ESP.restart();
  }
  lastPressed = M5.Btn.pressedFor(500);

  // 非常冷却モード
  if (temp >= EMARGENCY_TEMP && fan < 100) {
    fan = 100;
    refresh = true;
  }

  // ファンの回転数を変更
  if (refresh) {
    setFanDury(fan);
  }
  
  // LCDに温度とファンを表示
  if (refresh) {
    M5.Lcd.clear();
    lcdtext(String(int(round(temp))), 4, 20, 15);
    lcdtext(" O", 1);
    lcdtext("C", 2);
    lcdtext(String(fan), 4, 20, 64);
    lcdtext(" %", 2);
    lcdtext(manualmode ? "Manual" : "Auto", 2, 20, 110);
    lcdtext(" "+String(WiFi.localIP()[3]), 1);  // IPアドレスの最後を表示
  }
  delay(10);
}

// LCDにテキストを表示
void lcdtext(String text, int size, int x, int y) {
  static int oldsize;
  if (x != -1 && y != -1) M5.Lcd.setCursor(x, y);
  if (size != -1) M5.Lcd.setTextSize(size);
  M5.Lcd.print(text);
  oldsize = size;
}

// ファンの回転数を変更する(0%-100%)
void setFanDury(int ratio) {
  int pwm_duty = pow(2, PWM_BIT) * (ratio / 100.0);
  spf("temp=%.1f ratio=%d%% duty=%d\n", temp, ratio, pwm_duty);
  ledcWrite(PWM_CH, pwm_duty);
}

// 温度を測定する NTCサーミスタ
double measureTemperature(int count) {
  double tempsum = 0;
  for (int i=0; i<count; i++) {
    double adcmv = analogReadMilliVolts(GPIO_THERMISTOR); // 電圧測定
    double rt = adcmv / (M5ATOMS3_5V0 * 1000 - adcmv) * THERMISTOR_RREF;  // 抵抗値
    tempsum += 1.0 / (1.0 / (25.0+273.15) + 1.0 / THERMISTOR_B * log(rt / THERMISTOR_R25)) - 273.15;
    if (i==0) spf("adc=%.0f Rt=%.1f temp=%.1f\n", adcmv, rt, tempsum);
    delay(10);
  }
  return tempsum / count;
}

// EEPROMにデータを保存する
void eepromSave() {
  eeprom._check = 123;  // 未初期化データの読み込み防止用
  eeprom.fan = fan;
  eeprom.manualmode = manualmode;
  memcpy(eeprom.conf, conf, sizeof(conf));
  EEPROM.put(EEPROM_ADDRESS, eeprom);
  EEPROM.commit();
  sp("EEPROM saved");
}

// EEPROMからデータを読み出す
void eepromLoad() {
  EEPROM.get(EEPROM_ADDRESS, eeprom);
  if (eeprom._check != 123) {
    fan = eeprom.fan;
    manualmode = eeprom.manualmode;
    memcpy(conf, eeprom.conf, sizeof(eeprom.conf));
    sp("EEPROM loaded");
  }
}

// Webページ /conf ステータス表示と変更フォーム
void handleConf() {
  String html = "<html lang=\"ja\"><head><title>ATOMS3</title></head><body>\n\
    <h1>ATOMS3 Fan Controller</h1>\n\
    <p>Temperature: " + String(temp) + " \'C</p>\n\
    <p>Fan Speed: " + String(fan) + " %</p>\n\
    <p>Mode: " + (manualmode ? "Manual" : "Auto")+ "</p>\n\
    <hr><h2>Settings</h2>\n\
    <p><form action=\"/set\" method=\"POST\">\n\
    <input type=\"checkbox\" name=\"manual\" value=\"1\" " + String(manualmode ? "checked":"") + ">\n\
    Manual Mode (Fan <input type=\"text\" name=\"fan\" value=\""+ String(fan) +"\" size=\"4\">%)<br />\n\
    <table><tr><th>Temperature<th></th><th>Fan (%)</th></tr>\n";
  for (int i=0; i<array_length(conf); i++) {
    html += (i % 2 == 0) ? "<tr>\n<td>temp. &gt; " : "<td> to ";
    html += "<input type=\"text\" name=\"" + String(i) + "\" value=\"" + String(conf[i]) + "\" size=\"4\">";
    html += (i % 2 == 0) ? "</td>\n" : " %</td></tr>\n";
  }
  html += "</table>\n\
    <input type=\"submit\" name=\"save\" value=\"Save\">\n\
    </form></p>\n\
    </body></html>\n";
  server.send(200, "text/html", html);
  sp("Web: /conf");
}

// Webページ /set 設定変更
void handleSet() {
  if (server.method() == HTTP_POST) {
    manualmode = (server.arg("manual") == "1");
    fan = server.arg("fan").toInt();
    //if (manualmode) setFanDury(fan);  // ファンの回転数を変更
    sp("Set fan to "+String(fan));
    for (int i=0; i<array_length(conf); i++) {
      conf[i] = server.arg(String(i)).toInt();
    }
    eepromSave();  // EEPROMにデータを保存する
  }
  server.send(200, "text/html", "<head><meta http-equiv=\"refresh\" content=\"0;url=/conf\"></head>");
  sp("Web: /set");
}

サーミスターの温度を求める方法

サーミスターは温度に応じて抵抗値が変化するパーツです。ATOMS3にはADCが内蔵されているので、まずは10KΩの抵抗と分圧しているGPIO1の電圧を求めます。analogReadMilliVolts()なんて便利な関数があるんですね!今までanalogRead()で求めたADコンバーターの値を計算させてましたが、これは楽でいいですね。電圧がわれば抵抗値が求められます。

double adcmv = analogReadMilliVolts(GPIO_THERMISTOR); // 電圧測定
double rt = adcmv / (M5ATOMS3_5V0 * 1000 - adcmv) * THERMISTOR_RREF;  // 抵抗値
temp = 1.0 / (1.0 / (25.0+273.15) + 1.0 / THERMISTOR_B * log(rt / THERMISTOR_R25)) - 273.15;

サーミスターの抵抗値から温度を求めるのはちょっと面倒です。計算方法はChatGPTに教えてもらいました。サーミスターには製品によって特性が異なり、B定数という値があります。ところが困ったことに、こういった製品の多くはB定数の記載がありません。仕方ないので実際に水とお湯の温度を測定してみて、温度が大きくずれない範囲の既製品の値にしてみました。精度はそれほど重要ではないのでだいたいあっていればOKです。

ヒステリシス

温度が上がったときは即座にファンの回転数が上がりますが、下がった場合は 設定値-0.8℃ 以下にならないと回転数を下げないようになっています。これは閾値付近で頻繁に回転数の変更が起こらないように、動作に幅を持たせています。

使い方

ボタン長押しでAutoモードとManualモードが切り替わります。
Manualモード時はボタン短押しでファン回転数が変わります。
5秒長押しで強制リセット。
起動時にボタン押しっぱなしにしているとEEPROMが初期値に戻ります。

Webブラウザからの設定

Webブラウザから設定画面にアクセスすることもできます。mDNSを使っているので以下のURLからアクセスできます。もしブラウザが非対応な場合は、画面の右下にある小さな数字がIPアドレスの最後の部分なので、atomfan.localの部分をプライベートIPアドレスにすればアクセスできるはずです。

http://atomfan.local/conf
FwZWKooacAAr50x-s.png

完成!

ということでこれで完成です!実際に水温を測定してみると、水冷用のフィッティングに取り付けるタイプの温度センサーは反応が遅めです。実際の温度より数分遅れて反応する感じです。直接サーミスタが水に接している構造ではないので、熱が伝わるまで時間がかかるのかもしれません。とりあえずこれで爆熱PCの夏対策の準備は整いました!
atomfansystem-s.jpg

1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?