seventz
Published © GPL3+

Smart PC fan and watercooling controller

Smart control of the liquid cooling system in the PC to minimize noise, keeping temperatures in check!

IntermediateFull instructions provided2,874
Smart PC fan and watercooling controller

Things used in this project

Hardware components

Arduino UNO
Arduino UNO
×1
Lamptron SP105 Hub
Fan hub - to wire all the fans and also, to conveniently give power to the pump.
×1
MikroE Fan2Click
2x fan controller
×2
MikroE ADAC Click
10-bit ADC/DAC
×1
1x40 Male Pin Header
Can be cut to size. 2pcs are enough for plugging into Arduino and creating all the necessary headers for thesmistors and the PC reset button.
×2
Female Header 8 Position 1 Row (0.1")
Female Header 8 Position 1 Row (0.1")
to plug in the click boards from MicroE (6 needed), + 2 extra to cut to size and create headers where you need them (see: photo of the "PCB")
×8
Phobya Fan Power Connector 4Pin PWM male
×2
4pin pwm extension 30 cm
Get as many as you might need to wire everything
×2
M2.5X8mm screws
you need 4 screws to put together the LCD display enclosure
×1
Jumper wires (generic)
Jumper wires (generic)
get as many as you need to wire everything
×1
Alphacool Icicle temperature sensor
Water temperature sensors - 10k thermistors.
×2
ntc thermistor 10k bedrahtet
10k Thermistor for probing the air temperature
×1
USB-A to USB-B cable
To connect Arduino to PC, cable 1
×1
Internal USB to Motherboard cable
To connect Arduino to PC, cable 2
×1

Hand tools and fabrication machines

Soldering iron (generic)
Soldering iron (generic)
Solder Wire, Lead Free
Solder Wire, Lead Free
3D Printer (generic)
3D Printer (generic)
if you want to 3d-print the enclosures
UHU Super Power superglue
If you need to glue some things together

Story

Read more

Custom parts and enclosures

Case for I2C LCD

It worked for my model; I don't guarantee it will work with yours

Case for I2C LCD - backside

Enclosure Part 1

a simple brick to fit Arduino and the click boards

Enclosure part 2

Schematics

Wiring diagram

Wiring diagram 2

Schematic I made and used to wire everything together on the actual 3D-printed board

Code

Arduino sketch

C/C++
The Arduino program, including the custom functions to drive the fan controller ICs over I2C, this is why it grew so big and may look intimidating. I packed everything into meaningful functions, so I hope you are able to figure it out!
/* I2C LCD with Arduino example code. More info: https://www.makerguides.com */

// Include the libraries:
// LiquidCrystal_I2C.h: https://github.com/johnrickman/LiquidCrystal_I2C
#include <Wire.h> // Library for I2C communication
#include <LiquidCrystal_I2C.h> // Library for LCD
#include <AD5593R.h>
#include <math.h>

#define BAUD_RATE 115200
#define LOOP_INTERVAL 1200 //ms

/* FAN CONTROL CONFIGURATION DEFINITIONS */
#define FANCTRL_I2C_ADDR 0x50

#define FANCTRL_CR1_ADDR 0x00 //control register 1
#define FANCTRL_CR1_VAL B10011000

#define FANCTRL_CR2_ADDR 0x01 //control register 2
#define FANCTRL_CR2_VAL B00010001

#define FANCTRL_CR3_ADDR 0x02 //control register 3
#define FANCTRL_CR3_VAL B00110001

#define FANCTRL_FFDC_ADDR 0x03 //fan fail duty cycle
#define FANCTRL_FFDC_VAL 0xF2 // 95% duty cycle at failure detected

#define FANCTRL_AMR_ADDR 0x04 //alert mask register
#define FANCTRL_AMR_VAL B11111111 // ignore all alerts (for testing)

#define FANCTRL_DFCS_ADDR 0x50 //direct fan control PWM setting

#define FANCTRL_CPWM_ADDR 0x51 //current pwm reading

#define FANCTRL_TACHH_ADDR 0x52 //high bits of tach
#define FANCTRL_TACHL_ADDR 0x53 //low bits of tach

#define FANCTRL_TEMPH_ADDR 0x58 //high bits of temp
#define FANCTRL_TEMPL_ADDR 0x59 //low bits of temp

#define FAN_PWM_MIN 72 //65 is ~25%
#define FAN_PWM_MAX 255//110// 110 is 43%
#define PWM_DELTA_MAX 10 
#define PWM_SLOPE 5


#define TEMP_WATER_H T1
#define TEMP_WATER_L T2
#define TEMP_AIR_H T3

#define WATER_PUMP_CONTROL 1
#define FANS_CONTROL 2
#define TEMP_DIFF_TARGET 1.7 //-0.3//, target difference between TH and TL
//#define TEMP_TARGET 34.0 //, target TL

#define BASE_CONV 94
#define OFFS_CONV 33

#define READ_TIMEOUT 500

LiquidCrystal_I2C lcd = LiquidCrystal_I2C(0x27, 20, 4);
AD5593R AD5593R(0x10); //ADC init

bool my_DACs[8] = {1,0,0,0,0,0,0,0};
bool my_ADCs[8] = {0,1,1,1,0,0,0,0};
const int buttonPin = 2;

volatile bool backlightState = 1;
volatile bool buttonPressed = 0;

float v1, v2, v3, R1, R2, R3, T1, T2, T3, T4, TC, TM; // real variables: voltage, resistance and temperature
//const char degC[3] = {4,'C',0}; // degC string for display
const char degC[3] = {223,0}; // degC string for display
const char Tlo[3] = {6,0}; // T_LO string for display
const char Thi[3] = {7,0}; // T_HI string for display
const float TempTarget[2] = {45.0,40.0};//{40.0,25.0};//
const char TempNoData[5] = "--.-";//{165,165,'.',165,0};
const char modeChar[2] = {252,228};
const char coreChar = 4;
const char vramChar = 5;

const float vh = 2.0;//4.5; //V, high voltage for resistance measurement bias
const float T0 = 273.15; //K, temp equal 0
const float A = 2.5490E-4; //A parameter for recalculation of resistance to temp
const float B = 1.0062E-3; //B parameter for recalculation of resistance to temp
const float Rr = 20000.0;//19800.0;// kOhm
const int N = 80; //averaging count
//int i;

unsigned long previousTime = 0;
unsigned long currentTime = 0;
unsigned long prevButtonPress = 0;
unsigned long nowButtonPress = 0;
unsigned long serialReadingBegins;

String inputString;
String inputStringBuffer;
bool miningMode = true;
bool freshDataAvailable = false;


// Custom char definitions
byte barChars0[8] = {
  B10101,
  B00000,
  B10101,
  B00000,
  B10101,
  B00000,
  B10101,
  B00000
};
byte barChars1[8] = {
  B10101,
  B10000,
  B10101,
  B10000,
  B10101,
  B10000,
  B10101,
  B00000
};
byte barChars2[8] = {
  B10101,
  B10100,
  B10101,
  B10100,
  B10101,
  B10100,
  B10101,
  B00000
};
byte barChars3[8] = {
  B10101,
  B10101,
  B10101,
  B10101,
  B10101,
  B10101,
  B10101,
  B00000
};

byte loSign[8] = {
  B00000,
  B11111,
  B10111,
  B10111,
  B10001,
  B11111,
  B00000,
  B00000
};
byte hiSign[8] = {
  B00000,
  B11111,
  B10101,
  B10001,
  B10101,
  B11111,
  B00000,
  B00000
};
byte core[8] = {
  B11110,
  B11111,
  B11110,
  B11111,
  B11011,
  B11011,
  B11111,
  B11111
};
byte vram[8] = {
  B11110,
  B11111,
  B11110,
  B11011,
  B10101,
  B10101,
  B11011,
  B11111
};

void setup() {
  initializeI2CLCD();
  //Serial setup
  Serial.begin(BAUD_RATE);
  Serial.setTimeout(100);

  //Interrupt for button to toggle LCD backlight
  pinMode(buttonPin, INPUT_PULLUP);
  attachInterrupt(0, pin_ISR, FALLING);
  
  // Setup fan controllers
  configureFan2Click(1);
  configureFan2Click(2);
  
  configureADCs();

  // Reserve memory for incoming strings form serial
  inputString.reserve(20);  
  inputStringBuffer.reserve(20);
}

void loop() {
  currentTime = millis();

  //BACKLIGHT TOGGLE BUTTON CHECK W/ DEBOUNCER
  if ( buttonPressed ){
    nowButtonPress = millis();
    if (nowButtonPress-prevButtonPress>50){
      prevButtonPress = nowButtonPress;
      backlightState = !backlightState;
      buttonPressed = 0;
    }
  }
  
  trySerialRead();

  //BACKLIGHT SET
  if (backlightState){
    lcd.backlight();
  }
  else{
    lcd.noBacklight();
  }
  
  //MAIN ACITON
  if (currentTime - previousTime >= LOOP_INTERVAL) {
    previousTime = currentTime; // save the last executed time

    getTemperatures();

    changePWM(WATER_PUMP_CONTROL, TEMP_WATER_H - TEMP_WATER_L, TEMP_DIFF_TARGET);
    changePWM(FANS_CONTROL, TEMP_WATER_L, TempTarget[miningMode]); //mining mode bool chooses between the two array elements

    displayUpdate();

    freshDataAvailable = false; //set flag to await fresh data from serial
  }
}

void pin_ISR() { //pin interrupt for button press
    buttonPressed = 1;
}

///////////////////////////////
/// FUNCTIONS FOR FAN2CLICK ///
///////////////////////////////

void writeByteToFan2Click(int num, uint8_t addr, uint8_t value){
  //num - number of the fan controller, 1 or 2
  uint8_t i2cAddress = FANCTRL_I2C_ADDR+num-1;
  Wire.beginTransmission(i2cAddress);
  Wire.write(addr);
  Wire.write(value);
  Wire.endTransmission();  
}

uint8_t readByteFromFan2Click(int num, uint8_t addr){
  //num - number of the fan controller, 1 or 2
  uint8_t i2cAddress = FANCTRL_I2C_ADDR+num-1;
  uint8_t reading = 0;
  Wire.beginTransmission(i2cAddress);
  Wire.write(addr);
  Wire.endTransmission(); 
  Wire.requestFrom(i2cAddress, uint8_t(1));
  if (1 <= Wire.available()) { // if one byte was received
    reading = Wire.read();
  }
  return reading;
}

int read2BytesFromFan2Click(int num, uint8_t addr){
  //num - number of the fan controller, 1 or 2
  int reading = readByteFromFan2Click(num, addr);
  reading = reading<<8;
  reading |= readByteFromFan2Click(num, addr+1);
  return reading;
}

float readInternalTempFan2Click(int num){
  int reading = read2BytesFromFan2Click(num, byte(FANCTRL_TEMPH_ADDR));
  reading = reading>>5;
  float Temperature = (float)reading/8;
  return Temperature;
}

void setDirectPWMFan2Click(int num, byte pwm){
  writeByteToFan2Click(num, byte(FANCTRL_DFCS_ADDR), pwm);
}

byte getCurrentPWMFan2Click(int num){
  byte pwm_current = readByteFromFan2Click(num, byte(FANCTRL_DFCS_ADDR));
  return pwm_current;
}

float readTachometer(int num){
  int reading = read2BytesFromFan2Click(num, byte(FANCTRL_TACHH_ADDR));
  float Tach = 6e6/(float)reading/2; //division by 2 comes from number of tach pulses per revolution
                                     //6e6 is the number of seconds in a minute times tach counter clock frequency
  return Tach;
}

void displayTach(int num, int i, int j){
    //i,j - lcd cursor indexes horisontal and vertical respectively,
    //pwm - tach float value
    float Tach = readTachometer(num);
    lcd.setCursor(i, j);
    if (Tach<1000){
      lcd.print(" "); 
    }
    if (Tach<0){
      lcd.print("  ");
      Tach = 0;
    }
    lcd.print(Tach,0);
}

bool checkAndWriteByteToFan2Click(int num, uint8_t addr, uint8_t val){
  uint8_t val_read = readByteFromFan2Click(num, addr);
  bool rewritten = false;
  if(val_read != val){
    writeByteToFan2Click(num, addr, val);
    rewritten = true;
  }
  return rewritten;
}

void configureFan2Click(int num){
  int rewrittenSum = 0;
  rewrittenSum = rewrittenSum+checkAndWriteByteToFan2Click(num, byte(FANCTRL_CR1_ADDR),  byte(FANCTRL_CR1_VAL) );
  rewrittenSum = rewrittenSum+checkAndWriteByteToFan2Click(num, byte(FANCTRL_CR2_ADDR),  byte(FANCTRL_CR2_VAL) );
  rewrittenSum = rewrittenSum+checkAndWriteByteToFan2Click(num, byte(FANCTRL_CR3_ADDR),  byte(FANCTRL_CR3_VAL) );
  rewrittenSum = rewrittenSum+checkAndWriteByteToFan2Click(num, byte(FANCTRL_FFDC_ADDR), byte(FANCTRL_FFDC_VAL));
  rewrittenSum = rewrittenSum+checkAndWriteByteToFan2Click(num, byte(FANCTRL_AMR_ADDR),  byte(FANCTRL_AMR_VAL) );
  /* DEBUGGING DISPLAY
  lcd.setCursor(0, 0+(num-1)*2);
  lcd.print("Fan Controller #"); 
  lcd.print(num);
  lcd.setCursor(0, 1+(num-1)*2);  
  lcd.print(rewrittenSum);
  lcd.print(" settings changed."); */
}

void changePWM(int num, float Temp, float TempSetPoint){
    int pwm = getCurrentPWMFan2Click(num);

    //COMPUTE DELTA PWM
    int deltaPWM = (Temp-TempSetPoint)*PWM_SLOPE;
    if(deltaPWM>PWM_DELTA_MAX){
      deltaPWM = PWM_DELTA_MAX;
    }
    else if(deltaPWM<-PWM_DELTA_MAX){
      deltaPWM = -PWM_DELTA_MAX;
    }

    //CHANGE PWM BY PWM DELTA
    pwm = pwm+deltaPWM;
    if(pwm>FAN_PWM_MAX){
      pwm=FAN_PWM_MAX;
    }
    else if(pwm<FAN_PWM_MIN){
      pwm=FAN_PWM_MIN;
    }
    
    setDirectPWMFan2Click(num, pwm);
}

void displayPWMChart(int num, int i, int j){
  int pwm = getCurrentPWMFan2Click(num);
  int bars = (pwm+7)*18/255; //18 is the total number of bars
  int rest;
  int divn;
  char barChart[6] = {0,0,0,0,0,0};
  int k;
  divn = bars/3;
  rest = bars%3;
  for (k=0;k<divn;k++){
    barChart[k] = 3;
  }
  if(rest){
    barChart[divn] = rest;
  }
  lcd.setCursor(i, j);
//  lcd.print("F"); 
//  lcd.print(num); 
//  lcd.print(": "); 
  if (num==WATER_PUMP_CONTROL){
    lcd.print("Pump");
  }
  else if(num==FANS_CONTROL){
    lcd.print("Fans");
  }
  for(k=0;k<6;k++){
    lcd.print(barChart[k]); 
  }
}


///////////////////////
/// OTHER FUNCTIONS ///
///////////////////////


void displayTemp(int i, int j, float T, int n){
    //i,j - lcd cursor indexes horisontal and vertical respectively,
    //T - temp, n - digits after decimal point
    lcd.setCursor(i, j);
    if(T<10 && T>=0){
      lcd.print("+"); 
    }
    if(T<-50.0){ // if there is no temp data for this one, I assigned a value of -100.
      lcd.print(TempNoData);      
    }
    else{
      lcd.print(T,n); 
    }
    lcd.print(degC);
}

void getTemperatures(){
  
    T1 = 0;
    T2 = 0;
    T3 = 0;
    AD5593R.write_DAC(0,vh);
    int i;
    for(i=1;i<N+1;i++){
      v1 = AD5593R.read_ADC(1);
      v2 = AD5593R.read_ADC(2);
      v3 = AD5593R.read_ADC(3);
      R2 = Rr*v2/(vh-v1);
      R1 = Rr/(vh/v1-1)-R2;
      R3 = 0.5*Rr/(vh/v3-1);
      T1 = T1+(1/(A*log(R1)+B)-T0);
      T2 = T2+(1/(A*log(R2)+B)-T0); 
      T3 = T3+(1/(A*log(R3)+B)-T0);     
    }
    T1 = T1/N;
    T2 = T2/N;
    T3 = T3/N;
    T4 = readInternalTempFan2Click(1);
    AD5593R.write_DAC(0,0);
    
    if ( freshDataAvailable ){
      TC = .1*((inputString.charAt(1)-OFFS_CONV)+(inputString.charAt(0)-OFFS_CONV)*BASE_CONV);
      TM = 1.0*(inputString.charAt(2)-OFFS_CONV);
      miningMode = (inputString.charAt(3) == 'Y');
    }
    else{
      TC = -100.0; // values to indicate lack of data in the function that prints temps on LCD
      TM = -100.0;
      miningMode = true; 
    }
}

void initializeI2CLCD(){
  lcd.init();
  lcd.clear();
  lcd.createChar(0, barChars0);
  lcd.createChar(1, barChars1);
  lcd.createChar(2, barChars2);
  lcd.createChar(3, barChars3);
  lcd.createChar(4, core);
  lcd.createChar(5, vram);
  lcd.createChar(6, loSign);
  lcd.createChar(7, hiSign);
  backlightState = true;
}

void configureADCs(){
    AD5593R.enable_internal_Vref();
    AD5593R.set_DAC_max_1x_Vref();
    AD5593R.set_ADC_max_1x_Vref();
    AD5593R.configure_DACs(my_DACs);
    AD5593R.configure_ADCs(my_ADCs);
}

void displayUpdate(){
  lcd.setCursor(0, 0);
  lcd.print(Thi);
  displayTemp(1, 0, TEMP_WATER_H, 1);
    
  lcd.setCursor(0, 1);
  lcd.print(Tlo);
  displayTemp(1, 1, TEMP_WATER_L, 1);
     
  displayTemp(1, 2, TEMP_WATER_H - TEMP_WATER_L, 1);
          
  lcd.setCursor(0, 3);
  lcd.print(char(127));
  displayTemp(1, 3, TEMP_AIR_H, 1); 

  lcd.setCursor(6, 0);
  lcd.print(coreChar); 
  displayTemp(7, 0, TC, 1); 
  lcd.setCursor(6, 1);
  lcd.print(vramChar); 
  displayTemp(7, 1, TM, 1);
    
  lcd.setCursor(12, 0);
  lcd.print("[Mode:");
  lcd.print(modeChar[miningMode]);
  lcd.print("]");

  displayPWMChart(WATER_PUMP_CONTROL, 6, 2);
  displayPWMChart(FANS_CONTROL, 6, 3);  
    
  lcd.setCursor(17, 1);
  lcd.print("RPM");
  displayTach(WATER_PUMP_CONTROL, 16, 2);
  displayTach(FANS_CONTROL, 16, 3);
}

void trySerialRead(){
  if ( Serial.available() ){
    serialReadingBegins = millis();
    while(Serial.read()!=' ' && (millis()-serialReadingBegins)<READ_TIMEOUT){
      //Do nothing
    }
    inputStringBuffer = Serial.readStringUntil('\n');
    if(inputStringBuffer.indexOf(char(0x09))!=-1){ //if string contains char 0x09 (tabulator)
      backlightState = !backlightState;
    }
    if(inputStringBuffer.length()==4){
      inputString = inputStringBuffer;
      freshDataAvailable = true;
    }
  }
}

LCD backlight toggle Python program

Python
This program sends the spacebar and tabulator to Arduino over Serial, which triggers LCD backlight toggle. Important note: check line 10 and change the COM number to the one on which your Arduino is connected!
Also, if you want the program to run in the background unseen, save it with a ".pyw" extension instead of ".py".
import serial
import time

def main():
    ser = serial.Serial()
    ser.baudrate = 115200
    ser.timeout = 0.1
    ser.writeTimeout = 0.1
    ser.setDTR(False)
    ser.port = 'COM8'
    a = " \x09\n" #\x09 is the tabulator.
    trying = 1
    while trying:
        try:
            ser.open()
            ser.write(a.encode())
            ser.close()
            trying = 0
        except:
            time.sleep(0.1)
    
if __name__ == '__main__':
    main()

Python program to send GPU data to Arduino over Serial

Python
This program sends the GPU data (core and vram temps) and the info if the excavator is running. Important note: check line 25 and change the COM number to the one on which your Arduino is connected!
Also, if you want the program to run in the background unseen, save it with a ".pyw" extension instead of ".py".
from pynvraw import api, NvError, get_phys_gpu
import serial, time, psutil

def checkIfProcessRunning(processName):
    '''
    Check if there is any running process that contains the given name processName.
    '''
    #Iterate over the all the running process
    for proc in psutil.process_iter():
        try:
            # Check if process name contains the given name string.
            if processName.lower() in proc.name().lower():
                return True
        except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
            pass
    return False;

def main():
    cuda_dev = 0
    ser = serial.Serial()
    ser.baudrate = 115200
    ser.timeout = 0.1
    ser.writeTimeout = 0.1
    ser.setDTR(False) #THIS IS IMPORTANT - prevents Arduino from resetting
    ser.port = 'COM8' #Put the COM number to which your Arduino is connected
    dataBeginsInt = 32 #this is a spacebar, data follows the Space char.
    dataBeginsChar = dataBeginsInt;
    dataCharOffset = 33
    outputString = " {}{}{}{}\n"
    base = 94; #base for encoding temperature numbers
    while True:
        try:
            gpu = get_phys_gpu(cuda_dev)
        except ValueError:
            break
        try:
            api.restore_coolers(gpu.handle)
        except NvError as err:
            if err.status != 'NVAPI_NOT_SUPPORTED':
                raise
        
        cuda_dev += 1
        
    while True:
        try:
            if checkIfProcessRunning('excavator'):
                ExcavRunning = 'Y'
            else:
                ExcavRunning = 'N'
            gpuCoreTemp10xInt = int(gpu.core_temp*10);
            coreTempCharEncodedByte2 = int(gpuCoreTemp10xInt%base+dataCharOffset) #least significant byte of the core temp encoded
            coreTempCharEncodedByte1 = int((gpuCoreTemp10xInt-coreTempCharEncodedByte2+dataCharOffset)/base+dataCharOffset) #most significant byte of the core temp encoded
            vramTempCharEncodedByte1 = int(gpu.vram_temp)+dataCharOffset
            ser.open()
            ser.write(outputString.format(chr(coreTempCharEncodedByte1),chr(coreTempCharEncodedByte2),chr(vramTempCharEncodedByte1),ExcavRunning).encode())
            ser.close()
            time.sleep(0.5)
        except:
            time.sleep(0.10 )
            print('Port Busy\n')
        

if __name__ == '__main__':
    main()

Credits

seventz

seventz

0 projects • 1 follower

Comments