-
3DやPCVRの使用でGPUの温度が上がり過ぎて熱暴走するのでGPUコア自動冷却システムを開発しました。(ECCS、EjectCoreCoolingSystem)
-
PCケースUSBファンのON,OFFを手動でやってましたが結構面倒です。常時ファンを回すと騒音が気になるので自動化しました。
必要となる技術は下記の4点です。
- 1.PC側でGPU状況の取得
- 2.PC側管理画面作成とシリアルデータ送信
- 3.Arduino側のシリアル受信
- 4.ファンを稼働させるハードウェア
LM35温度センサによる測定からnvidia-smiでデータを取る方式に作り直し。
-
nvidia-smiは上記リンクに詳しく載ってます。
-
データの流れは GetGPUData() -> nvidia-smi -> 整形 -> ListBox
-
GPU温度等を取得し閾値を超えると、Arduino基板にシリアルでフラグを送り、MOSFET ONにしてファンを回す。
-
閾値を切ると停止。閾値項目等は自由に変更可。
-
nVidia以外の場合はLibreHardwareMonitorLib等を使用しGPU情報を取得するなど少し工夫する必要があります。
1.起動するとタスクトレイに常駐(TSR)します
- アイコン(この場合は音符)をダブルクリックするか右ボタン->設定でAF System管理画面が出ます。
- また管理画面が出てる状態でアイコンダブルクリックで非表示化。トグルです。
2.AF System (WPF) 管理画面
- 初期は自動モードになっています。
-
15秒毎にGPU情報を表示している様子。
-
xを押すと画面は消えますがトレイに常駐・稼働してます。
-
nvidia-smiは0,40,11,719,17.79 という形式で出力させてるので切り出して整形。0 は回転割合、40はGPU温度。
-
[自動スクロール]...更新があると最終行に移動。履歴を見る時はオフにします。
-
[自動ボタン]...45W以上又はGPU搭載ファンが30%以上で稼働させる自動モード。項目、閾値は自由に変更可。
-
[強制稼働]...GPUの状況に関係なくファンを回す。強制停止はその反対。
-
自動モードで閾値以下の場合、[自動停止中]と表示。
3.高負荷時の例
- VR等行うと180W超えてGPU温度も上がっているのが分かる。
- 最大200W消費していた事を履歴で確認。
4.温度低下を示す表示
- 06:36:04はファンが停止中。15秒後には30%で回転を始めたので併せて外付けファンが起動。
- 2分後には23度温度降下。アイドル中なので下がりやすい。
- 60度以上が続くと内蔵ファンが起動する設定の模様。
5.リアルタイムモニタ
- 一行表示まで縮めるとリアルタイムモニタになる。
- ハード作成を省いて、GPU監視アプリとしても利用可。
6.AF System 回路図 / Arduino UnoR3(MPU ATmega328P)
- フラグ増やしてPWM制御にする事も可能
7.Arduino Uno R3 / C++ USB電源制御コード
//--------------------------
// Arduino AF System R1.00
// inf102 2025.
//--------------------------
void setup() {
Serial.begin( 9600 );
pinMode(8, OUTPUT);
}
void loop() {
char key;
// digitalWrite(8, HIGH);
// 受信データがあった時だけ処理を行う
if ( Serial.available() ) { // 受信データがあるか?
// digitalWrite(8, HIGH);
key = Serial.read();
if (key=='1') digitalWrite(8, HIGH);
if (key=='0') digitalWrite(8, LOW);
}
}
8.試験運用
試作の段階ではブレッドボードを使用すると良い9.ラグ版で組み立て(本番環境、アッセンブリ化用)
MosFETヒートシンクは適当なのがあったので取り付け。10.自作基板とArduino基板を結合させてアッセンブリ化
- アッセンブリ化して装置に3つのコネクタを刺すだけで良い作りにした
11.制御装置をPCケースに設置
- ファンはUSB電源供給されると回る物にする。
- トグルスイッチ式等。そうでないと自動化されない。
12.WPF (制御画面)
- タスクトレイに常駐します。アイコンファイルは適当に用意して下さい。
- nvidia-smi.EXEのパス等も変えて下さい。ドライバが入っていれば何処かに入ってます。
/////////////////////////////////////////////
// AF System R1.00 EjectCoreCoolingSystem
// WPF .NETFRAMEWORK / inf102 2025.
////////////////////////////////////////////
using System;
using System.Diagnostics;
using System.Drawing; // Icon用
using System.IO.Ports;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Forms;
using System.Windows.Media; // NotifyIcon用
// 参照追加 System.Windows.Forms / System.Drawing
// 二重起動防止はApp.xaml.cs
namespace TSRTEST01 {
public partial class MainWindow : Window {
const int WAIT=15000; // 更新間隔 mmSec 1000以上
static bool AUTO=true;
static int ENA=3; // 0=強制停止 1=強制稼働
private NotifyIcon notifyIcon;
public void m(string s) {
System.Windows.MessageBox.Show(s);
}
public void L1(string s){
if (listBox1.Items.Count >=5000 ) listBox1.Items.Clear();
listBox1.Items.Add(s);
}
// VS上では例外がでることある その際は削除
public void L2() {
var border = VisualTreeHelper.GetChild(listBox1, 0) as Border;
var listBoxScroll = border.Child as ScrollViewer;
listBoxScroll.ScrollToEnd();
}
public MainWindow() {
InitializeComponent();
SetupNotifyIcon();
this.Hide(); // 起動時にwindow出したい時は消す
L1("AFSystem R1.00.");
L1("\r\nGPU情報.");
// GPU初期表示
string cmd= "c:\\tmp\\nvidia-smi.EXE \" --query-gpu=name,fan.speed,temperature.gpu,utilization.gpu,memory.used,power.draw --format=csv,nounits\"";
ProcessStartInfo psInfo = new ProcessStartInfo();
psInfo.FileName = cmd;
psInfo.CreateNoWindow = true;
psInfo.UseShellExecute = false;
psInfo.RedirectStandardOutput = true;
Process p = Process.Start(psInfo);
string output = p.StandardOutput.ReadToEnd();
output = output.Replace("\r\r\n", "\n");
L1(output);
L1( (WAIT/1000).ToString() +"秒毎に更新されます.");
L1("自動モードでは消費電力45W以上又はファン回転率30%以上で稼働.");
FAN_STOP();
AutoMode();
STA.Content="自動モード";
AUTO=true;
// MAIN-LOOP
ST();
}
// MAIN-LOOP
public async void ST() {
while (true){
string outst="";
// GPUデータの取得と分割
string[] words=GetGPUData().Split(' ');
// 消費電力
double wat=float.Parse(words[7]);
// 回転率
int ter=int.Parse(words[0]);
// 自動モード
if (AUTO == true) {
ENA=3;
if (wat >= 45 || ter >=30) {
outst=DateTime.Now.ToString("HH:mm:ss") + " 自動稼働";
STA.Content="自動稼働中";
FAN_START();
}
else {
outst=DateTime.Now.ToString("HH:mm:ss") + " 自動停止";
STA.Content="自動停止中";
FAN_STOP();
}
}
// 強制稼働モード
if (ENA == 1) {
AUTO=false;
outst=DateTime.Now.ToString("HH:mm:ss") + " 強制稼働";
STA.Content="強制稼働中";
FAN_START();
}
// 強制停止モード
if (ENA == 0) {
AUTO=false;
outst=DateTime.Now.ToString("HH:mm:ss") + " 強制停止";
STA.Content="強制停止中";
FAN_STOP();
}
L1 (outst + " : ファン回転率 " +int.Parse(words[0]).ToString("00") +"% : GPU "+words[2]+"度 : 使用率 "+
int.Parse(words[3]).ToString("00")+"% : 使用メモリ "+ int.Parse(words[5]).ToString("0000")+"MB : 消費電力 "+ Math.Ceiling(decimal.Parse(words[7])).ToString()+"W");
await Task.Delay(WAIT);
// 強制スクロール
if (AUTO_S.IsChecked == true ) L2();
}
}
private void SetupNotifyIcon(){
notifyIcon = new NotifyIcon();
notifyIcon.Icon = new Icon("C:\\TMP\\ICON.ico"); // アイコンファイル
notifyIcon.Visible = true;
notifyIcon.Text = "AFSystem";
// 右クリックメニューの作成
var contextMenu = new ContextMenuStrip();
// MENU1 (WINDOW OPEN) ////////////////////////////////////
var menuItem = new ToolStripMenuItem("設定");
menuItem.Click += (s, e) => {
this.Show();
this.WindowState = WindowState.Normal;
this.Activate();
};
contextMenu.Items.Add(menuItem);
//////////////////////////////////////////////////////////
// MENU2 (EXIT) ///////////////////////////////////////////
menuItem= new ToolStripMenuItem("終了");
menuItem.Click += (s, e) => {
notifyIcon.Visible = false;
System.Windows.Application.Current.Shutdown();
};
contextMenu.Items.Add(menuItem);
//////////////////////////////////////////////////////////
// メニュー追加
notifyIcon.ContextMenuStrip = contextMenu;
// ダブルクリックでウィンドウを表示
notifyIcon.DoubleClick += (s, e) =>{
// 非表示化
if (this.IsVisible){
this.Hide();
return;
}
this.Show();
this.WindowState = WindowState.Normal;
this.Activate();
};
// 最小化時にウィンドウを隠す
this.StateChanged += (s, e) =>{
if (this.WindowState == WindowState.Minimized){
this.Hide();
}
};
}
protected override void OnClosed(EventArgs e) {
FAN_STOP();
notifyIcon.Visible = false;
notifyIcon.Dispose();
base.OnClosed(e);
}
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e) {
this.Hide();
e.Cancel = true;
}
// 自動ボタン
private void Button_Click3(object sender, RoutedEventArgs e) {
AutoMode();
if (AUTO_S.IsChecked == true ) L2();
}
public void AutoMode(){
AUTO=true;
ENA=3;
L1(DateTime.Now.ToString("HH:mm:ss") + " -- 自動モード --");
STA.Content="自動モード";
}
// 強制稼働ボタン
private void Button_Click(object sender, RoutedEventArgs e) {
AUTO=false;
ENA=1;
L1(DateTime.Now.ToString("HH:mm:ss") + " -- 強制稼働 --");
STA.Content="強制稼働中";
FAN_START();
if (AUTO_S.IsChecked == true ) L2();
}
public void FAN_START() {
try {
System.IO.Ports.SerialPort serialPort = new SerialPort("COM3", 9600, Parity.None, 8, StopBits.One); // N81XN
serialPort.NewLine = Environment.NewLine;
serialPort.Open();
byte[] SendData = new byte[10];
SendData[0] = (byte)('1');
serialPort.Write(SendData, 0, 1);
serialPort.Close();
serialPort.Dispose();
}
catch(Exception ex) {
m(ex.ToString());
}
}
// FAN強制停止ボタン
private void Button_Click2(object sender, RoutedEventArgs e) {
AUTO=false;
ENA=0;
L1 (DateTime.Now.ToString("HH:mm:ss") + " -- 強制停止 --");
STA.Content="強制停止中";
FAN_STOP();
if (AUTO_S.IsChecked == true ) L2();
}
public void FAN_STOP() {
try {
System.IO.Ports.SerialPort serialPort = new SerialPort("COM3", 9600, Parity.None, 8, StopBits.One);
serialPort.NewLine = Environment.NewLine;
serialPort.Open();
byte[] SendData = new byte[10];
SendData[0] = (byte)('0');
serialPort.Write(SendData, 0, 1);
serialPort.Close();
serialPort.Dispose();
}
catch(Exception ex) {
m(ex.ToString());
}
}
// GPU DATA
public string GetGPUData() {
// command = "c:\\tmp\\nvidia-smi.EXE \" --query-gpu=name,fan.speed,temperature.gpu,utilization.gpu,memory.used,power.draw --format=csv,nounits\"";
string cmd= "c:\\tmp\\nvidia-smi.EXE \" --query-gpu=,fan.speed,temperature.gpu,utilization.gpu,memory.used,power.draw --format=csv,noheader\"";
ProcessStartInfo psInfo = new ProcessStartInfo();
psInfo.FileName = cmd;
psInfo.CreateNoWindow = true;
psInfo.UseShellExecute = false;
psInfo.RedirectStandardOutput = true;
Process p = Process.Start(psInfo);
string output = p.StandardOutput.ReadToEnd();
p.Close();
p.Dispose();
output = output.Replace("\r\r\n", "\n");
return(output);
}
}
}
13.XAML (制御画面)
<Window x:Class="TSRTEST01.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:TSRTEST01" mc:Ignorable="d"
Title="AFSystem R1.00" Height="450" Width="920" Closing="Window_Closing">
<Grid>
<Grid Margin="0,0,0,0">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="36"/>
</Grid.RowDefinitions>
<ListBox x:Name="listBox1" Margin="2,2,2,2" Background="LightBlue" FontSize="20" Foreground="Black" />
<Button Content="強制稼働(_R)" FontSize ="15" HorizontalAlignment="Left" Grid.Row="1" VerticalAlignment="Center" Width="90" Height="30" Click="Button_Click" Margin="104,0,0,0" />
<Button Content="強制停止(_S)" FontSize ="15" Grid.Row="1" VerticalAlignment="Center" Height="30" Click="Button_Click2" Margin="199,0,0,0" HorizontalAlignment="Left" Width="90" />
<Button Content="自動(_A)" FontSize ="15" HorizontalAlignment="Left" Grid.Row="1" VerticalAlignment="Center" Width="90" Height="30" Click="Button_Click3" Margin="9,0,0,0"/>
<CheckBox Content="自動スクロール(_K)" x:Name="AUTO_S" IsChecked="True" FontSize="13" HorizontalAlignment="Left" Margin="315,0,0,0" Grid.Row="1" VerticalAlignment="Center"/>
<Label Content="Label" x:Name="STA" Foreground="RED" HorizontalAlignment="Left" Margin="522,0,0,0" FontSize="25" VerticalAlignment="Bottom" Width="192" Grid.RowSpan="2" />
</Grid>
</Grid>
</Window>