Hi everyone,
I'm setting up a Home Assistant control center in a RAM Promaster converted as an RV.
One of the system to control is a set of 5 PWM fans dedicated to cool the back of a fridge cabinet. The fans are all connected to a fan hub, meaning all fans get the same PWM signal and only one fan reports its speed.
I'm using an Arduino Uno R4 WiFi to read the temperature of the cabinet, to send the PWM signal and to read the RPM.
I used mariuste code to determine the PWM duty value based on temperature (Fan_Temp_Control/code/Fan_Temp_Control/Fan_Temp_Control.ino at main · mariuste/Fan_Temp_Control · GitHub).
I used Federico Dossena code to read fan RPM (How to properly control PWM fans with Arduino - Federico Dossena).
Since I'm using an Arduino Uno R4 WiFi, I had to change the PWM commands from timer control to PwmOut as described by pschatzmann in https://www.pschatzmann.ch/home/2023/07/01/under-the-hood-arduino-uno-r4-pwm/.
After a few days of testing, I found an inconsistency between the PWM duty commanded value and the corresponding fan RPM.
Based on the temperature reading, which is within 0.1 degree of the actual value, the PWM duty varies between 10% and 100%. According to the manufacturer of the fans (Arctic P12 Max), the fans have a range of 400 to 3300 RPM. So, according to fan data sheet, a duty of 100% should equate 3300 RPM, 50 % = 1650 RPM, etc.
I did lots of tests and confirmed the following:
When the PWM signal is disconnected from the fan PWM input, the fan turns at full speed and the RPM reading indicates 3300.
When the PWM duty commanded value is 100%, the RPM reading also indicates 3300.
But when the PWM duty commanded value is lower than 100%, the RPM reading is higher than expected value (ex. at 50%, the RPM reading is around 2000 instead of 1650.)
So, what am I doing wrong?
Please help
Here is my code:
/*
This code allows:
1. iPad mini on/off charging cycle between ~25% and ~80%.
2. Fridge cabinet fans speed control based on cabinet temperature.
3. RAM Promaster VSIM sensor data monitoring.
*/
/*
Fan speed control based on fridge cabinet temperature constants/variables:
tempLow (default 35) - Below this temperature (minus half hysteresis) the fan shuts off.
It shuts on again at this temperature plus half hysteresis.
tempHigh (default 50) - At and above this temperature the fan is at maximum speed.
hysteresis (constant 5) - Hysteresis to prevent frequent on/off switching at the threshold.
minDuty (constant 10) - Minimum fan speed to prevent stalling
maxDuty (constant 100) - Maximum fan speed to limit noise
PIN02 = Temperature sensor input pin
PIN10 = PWM output pin
Fan speed RPM reading:
debounce (0) - Used to filter out noise
fanstuckThreshold - Timer used to detect if no interrupts are received to consider the fan as stuck and report 0 RPM
PIN03 = RPM sensor input pin
*/
#include <Arduino.h>
#include <WiFiS3.h>
#include <ArduinoHA.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <pwm.h>
#define BROKER_ADDR IPAddress(x,x,x,x)
#define WIFI_SSID "ssid"
#define WIFI_PASSWORD "password"
WiFiClient client;
HADevice device;
HAMqtt mqtt(client, device);
// Temperature sensor
#define ONE_WIRE_BUS 2
OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);
const int tempSetInterval = 5000;
unsigned long tempLastReadAt = millis();
float tempLow = 35;
float tempHigh = 50;
const float hysteresis = 5;
const int minDuty = 10;
const int maxDuty = 100;
// PWM
#define PIN10 10
PwmOut pwm(PIN10);
const float pin10Freq = 25000;
bool fanState = HIGH;
float duty = 100;
float newDuty = 100;
// RPM
#define PIN03 3
#define debounce 0
#define fanstuckThreshold 500
unsigned long volatile ts1=0,ts2=0;
unsigned long rpmLastReadAt = millis();
// Arduino
#define PIN09 9
#define PIN13 13
unsigned long lastReadAt = millis();
bool Pin09lastInputState = false;
HABinarySensor Pin09("Pin09");
HASwitch Pin13("Pin13");
HASensorNumber fanTemperature("fanTemperature", HASensorNumber::PrecisionP1);
HASensorNumber fanRPM("fanRPM");
HANumber fanTempLow("fanTempLow", HANumber::PrecisionP1);
HANumber fanTempHigh("fanTempHigh", HANumber::PrecisionP1);
void setup()
{
pinMode(PIN09, INPUT_PULLUP);
pinMode(PIN13, OUTPUT);
digitalWrite(PIN13, LOW); // sets the digital pin 13 off
Pin09lastInputState = digitalRead(PIN09);
byte mac[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
WiFi.macAddress(mac);
device.setUniqueId(mac, sizeof(mac));
device.setName("Arduino Uno R4 WiFi");
device.setSoftwareVersion("1.0.0");
Pin09.setName("Pin09");
Pin09.setDeviceClass("door");
Pin09.setIcon("mdi:fire");
Pin09.setCurrentState(Pin09lastInputState);
Pin13.setName("Pin13");
Pin13.setIcon("mdi:battery-charging");
fanTemperature.setIcon("mdi:temperature-celsius");
fanTemperature.setName("Fan Temperature");
fanTemperature.setUnitOfMeasurement("°C");
fanTempLow.setIcon("mdi:temperature-celsius");
fanTempLow.setName("Fan Temperature Low");
fanTempLow.setUnitOfMeasurement("°C");
fanTempLow.setStep(0.5f);
fanTempLow.setMin(20);
fanTempLow.setMax(60);
fanTempLow.setCurrentState(tempLow);
fanTempHigh.setIcon("mdi:temperature-celsius");
fanTempHigh.setName("Fan Temperature High");
fanTempHigh.setUnitOfMeasurement("°C");
fanTempHigh.setStep(0.5f);
fanTempHigh.setMin(20);
fanTempHigh.setMax(60);
fanTempHigh.setCurrentState(tempHigh);
fanRPM.setIcon("mdi:rotate-3d-variant");
fanRPM.setName("Fan RPM");
fanRPM.setUnitOfMeasurement("RPM");
Pin13.onCommand(onSwitchCommand);
fanTempLow.onCommand(onFanTempLowCommand);
fanTempHigh.onCommand(onFanTempHighCommand);
Serial.begin(9600);
Serial.println();
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi connected");
device.enableSharedAvailability();
device.enableLastWill();
// PWM
pwm.begin(pin10Freq, duty);
// Temperature sensor - start up the temperature library
sensors.begin();
sensors.requestTemperatures();
// Temperature reporting
Serial.println("# Main Loop");
Serial.println("(temperature, state, Duty Cycle, Fan On/Off)");
Serial.println();
// RPM setup
pinMode(PIN03,INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(PIN03),tachISR,FALLING);
// MQTT broker connection
mqtt.begin(BROKER_ADDR, "username", "password");
}
void loop()
{
mqtt.loop();
if ((millis() - lastReadAt) > 30) { // read in 30ms interval
// library produces MQTT message if a new state is different than the previous one
Pin09.setState(digitalRead(PIN09));
Pin09lastInputState = Pin09.getCurrentState();
lastReadAt = millis();
}
if ((millis() - tempLastReadAt) > tempSetInterval) { // read in 5000ms interval
// measure temperature, calculate Duty cycle, set PWM
tempToPwmDuty();
tempLastReadAt = millis();
}
if ((millis() - rpmLastReadAt) > 1000) { // read in 1000ms interval
// measure RPM
fanRPM.setValue(calcRPM());
rpmLastReadAt = millis();
}
}
void onSwitchCommand(bool state, HASwitch* sender)
{
digitalWrite(PIN13, (state ? HIGH : LOW));
sender->setState(state); // report state back to the Home Assistant
}
void onFanTempLowCommand(HANumeric number, HANumber* sender)
{
tempLow = number.toFloat();
sender->setState(number); // report the selected option back to the HA panel
}
void onFanTempHighCommand(HANumeric number, HANumber* sender)
{
tempHigh = number.toFloat();
sender->setState(number); // report the selected option back to the HA panel
}
void tachISR()
{
unsigned long m=millis();
if ((m-ts2) > debounce) {
ts1=ts2;
ts2=m;
}
}
unsigned long calcRPM()
{
if (millis()-ts2<fanstuckThreshold && ts2!=0) {
return (60000/(ts2-ts1))/2;
} else return 0;
}
void tempToPwmDuty()
{
sensors.requestTemperatures();
float temp = sensors.getTempCByIndex(0);
Serial.print(temp);
Serial.print("°C, ");
fanTemperature.setValue(temp);
if (temp < tempLow) {
// distinguish two cases to consider hysteresis
if (fanState == HIGH) {
if (temp < tempLow - (hysteresis / 2) ) {
// fan is on, temp below threshold minus hysteresis -> switch off
Serial.print("a, ");
newDuty = 0;
} else {
// fan is on, temp not below threshold minus hysteresis -> keep minimum speed
Serial.print("b, ");
newDuty = minDuty;
}
} else if (fanState == LOW) {
// fan is off, temp below threshold -> keep off
Serial.print("c, ");
newDuty = 0;
}
} else if (temp < tempHigh) {
// distinguish two cases to consider hysteresis
if (fanState == HIGH) {
// fan is on, temp above threshold > control fan speed
Serial.print("d, ");
newDuty = map(temp, tempLow, tempHigh, minDuty, maxDuty);
} else if (fanState == LOW) {
if (temp > tempLow + (hysteresis / 2) ) {
// fan is off, temp above threshold plus hysteresis -> switch on
Serial.print("e, ");
newDuty = minDuty;
} else {
// fan is on, temp not above threshold plus hysteresis -> keep off
Serial.print("f, ");
newDuty = 0;
}
}
} else if (temp >= tempHigh) {
// fan is on, temp above maximum temperature -> maximum speed
Serial.print("g, ");
newDuty = maxDuty;
} else {
// any other temperature -> maximum speed (this case should never occur
Serial.print("h, ");
newDuty = maxDuty;
}
//set new duty
duty = newDuty;
Serial.print(duty);
Serial.print("%, ");
if (fanState==0) {Serial.println("OFF");} else {Serial.println("ON");}
Serial.print(tempLow);
Serial.print(", ");
Serial.println(tempHigh);
setPwmDuty();
}
void setPwmDuty()
{
if (duty == 0) {
fanState = LOW;
} else if (duty > 0) {
fanState = HIGH;
}
pwm.pulse_perc(duty);
}