-
Notifications
You must be signed in to change notification settings - Fork 10
/
fancontrol.ino
401 lines (324 loc) · 9.49 KB
/
fancontrol.ino
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
/* fancontrol
*
* fancontrol is free software.
* Copyright (C) 2017 Matteo Bonora ([email protected]) - All Rights Reserved
*
* fancontrol is available under the GNU LGPLv3 License which is available at <http://www.gnu.org/licenses/lgpl.html>
This is a temperature-based fan controller using PID logic and PWM signals to control PC fans.
Check out my instructable on this project for more info
https://www.instructables.com/id/Temperature-Control-With-Arduino-and-PWM-Fans/
The PWM frequency is set to 23437 Hz that is within the 21k-25k Hz range so it should work with any PC fan.
For more details on how PWM works on the 32u4, check this link:
http://r6500.blogspot.it/2014/12/fast-pwm-on-arduino-leonardo.html
Note that the 32u4 has 2 pins (6 and 13) hooked to Timer4, but since my board doesn't have pin 13 I only configure pin 6.
A RPM reading system is also featured in this example (although it has proven to be not that accurate, at least on my setup).
This code has been tested on a SparkFun Pro Micro 16MHz Clone with 4 Arctic F12 PWM PST Fans connected to the same connector.
*/
#include <PID_v1.h> // https://github.com/br3ttb/Arduino-PID-Library
#include <DHT.h> // https://github.com/markruys/arduino-DHT
#include <LedControl.h> // https://github.com/wayoda/LedControl
#include <EEPROM.h>
// Change this if you want your current settings to be overwritten.
#define CONFIG_VERSION "f01"
// Where to store config data in EEPROM
#define CONFIG_START 32
// Pin 6 shortcut
#define PWM6 OCR4D
// Terminal count
#define PWM6_MAX OCR4C
/* Pinouts */
#define SPD_IN 7 // RPM input pin
// 7-segment display
#define SEG_DIN 16
#define SEG_CLK 14
#define SEG_CS 15
#define RELAY 9 // Relay output pin
#define TEMP_IN 4 // Temperature sensor input pin
#define TARGET_UP 18 // Up/Down buttons pins
#define TARGET_DOWN 19
#define DBG true
// Debug macro to print messages to serial
#define DEBUG(x) if(DBG && Serial) { Serial.print (x); }
// Tells the amount of time (in ms) to wait between updates
#define WAIT 500
#define DUTY_MIN 64 // The minimum fans speed (0...255)
#define DUTY_DEAD_ZONE 64 // The delta between the minimum output for the PID and DUTY_MIN (DUTY_MIN - DUTY_DEAD_ZONE).
#define KP 0.4
#define KI 0.4
#define KD 0.05
/* Target set vars */
bool targetMode = false;
bool lastUp = false, lastDown = false;
bool up, down = false;
/* RPM calculation */
volatile unsigned long duration = 0; // accumulates pulse width
volatile unsigned int pulsecount = 0;
volatile unsigned long previousMicros = 0;
int ticks = 0, speed = 0;
unsigned long prev1, prev2, prev3 = 0; // Time placeholders
double duty;
// Display temp, .5 rounded and Compute temp, integer (declared as double because of PID library input);
double dtemp, ctemp;
// Fan status
bool fanRunning = true;
// Settings
struct StoreStruct
{
// This is for mere detection if they are your settings
char version[4];
// The variables of your settings
double target;
} storage = { // Default values
CONFIG_VERSION,
40
};
// Initialize all the libraries.
PID fanPID(&ctemp, &duty, &storage.target, KP, KI, KD, REVERSE);
LedControl lc = LedControl(SEG_DIN, SEG_CLK, SEG_CS, 1);
DHT sensor;
/* Configure the PWM clock */
void pwm6configure()
{
// TCCR4B configuration
TCCR4B = 4; /* 4 sets 23437Hz */
// TCCR4C configuration
TCCR4C = 0;
// TCCR4D configuration
TCCR4D = 0;
// PLL Configuration
PLLFRQ = (PLLFRQ & 0xCF) | 0x30;
// Terminal count for Timer 4 PWM
OCR4C = 255;
}
// Set PWM to D6 (Timer4 D)
// Argument is PWM between 0 and 255
void pwmSet6(int value)
{
OCR4D = value; // Set PWM value
DDRD |= 1 << 7; // Set Output Mode D7
TCCR4C |= 0x09; // Activate channel D
}
/* Called when hall sensor pulses */
void pickRPM ()
{
volatile unsigned long currentMicros = micros();
if (currentMicros - previousMicros > 20000) // Prevent pulses less than 20k micros far.
{
duration += currentMicros - previousMicros;
previousMicros = currentMicros;
ticks++;
}
}
/* Settings management on the EEPROM */
void loadConfig()
{
// Check if saved bytes have the same "version" and loads them. Otherwise it will load the default values.
if (EEPROM.read(CONFIG_START + 0) == CONFIG_VERSION[0] &&
EEPROM.read(CONFIG_START + 1) == CONFIG_VERSION[1] &&
EEPROM.read(CONFIG_START + 2) == CONFIG_VERSION[2])
for (unsigned int t = 0; t < sizeof(storage); t++)
*((char*)&storage + t) = EEPROM.read(CONFIG_START + t);
}
void saveConfig()
{
for (unsigned int t = 0; t < sizeof(storage); t++)
EEPROM.update(CONFIG_START + t, *((char*)&storage + t));
}
/* LCD MANAGEMENT FUNCTIONS */
/* Writes 'str' to the lcd, starting at 'index' */
void writeSeg(const char str[], byte index)
{
int size = strlen(str);
for (int i = 0; i < size; i++)
{
lc.setChar(0, index + i, str[(size - 1) - i], false);
}
}
/* writes the temperature on the lcd. 'off' defines the offset and dInt defines whether the temp is an int or a float */
void writeTemp(float temp, byte off, bool dInt = false)
{
byte t[3];
if (!dInt) // If it's a float, then multiply by 10 to get rid of the decimal value
{
temp *= 10;
}
// Split the value in an array of bytes
t[0] = (int)temp % 10;
temp /= 10;
t[1] = (int)temp % 10;
if (!dInt)
{
temp /= 10;
t[2] = (int)temp % 10;
}
// Do the actual printing
for (byte i = 1; i < 4; i++)
{
lc.setDigit(0, i + off, t[i - 1], (i == 2 && !dInt));
}
lc.setChar(0, off, 'C', false);
}
/* Calls the right functions to fill the left half of the lcd */
void writeLeft()
{
if (targetMode)
{
writeSeg("Set ", 4);
}
else
{
writeTemp(dtemp, 4);
}
}
/* Calls the right functions to fill the right half of the lcd */
void writeRight()
{
if (targetMode)
{
writeTemp(storage.target, 0, true);
}
else
{
char tmp[5];
if (fanRunning)
{
sprintf(tmp, "%4u", map(round(duty), 0, 255, 0, 100));
}
else
{
strcpy(tmp, " 0ff");
}
writeSeg(tmp, 0);
}
}
/* Routine that updates the display */
void printSeg()
{
writeRight();
writeLeft();
}
void setup()
{
Serial.begin(115200);
if (DBG)
{
while (!Serial) {} /* WAIT FOR THE SERIAL CONNECTION FOR DEBUGGING */
}
DEBUG("Fans...");
pinMode(SPD_IN, INPUT);
pinMode(RELAY, OUTPUT);
pinMode(TARGET_UP, INPUT_PULLUP);
pinMode(TARGET_DOWN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(SPD_IN), pickRPM, FALLING);
DEBUG("Display...");
lc.clearDisplay(0);
lc.shutdown(0, false);
lc.setIntensity(0, 15);
writeSeg("Fan Ctrl", 0);
pwm6configure();
loadConfig();
DEBUG("PID...");
// Setup the PID to work with our settings
fanPID.SetSampleTime(WAIT);
fanPID.SetOutputLimits(DUTY_MIN - DUTY_DEAD_ZONE, 255);
fanPID.SetMode(AUTOMATIC);
DEBUG("Fans...");
pwmSet6(255);
// Let the fan run for 5s. Here we could add a fan health control to see if the fan revs to a certain value.
delay(5000);
DEBUG("Sensor...");
sensor.setup(TEMP_IN);
DEBUG("Ready.\n\n");
lc.clearDisplay(0);
prev1 = millis();
}
void loop()
{
unsigned long cur = millis();
bool shouldPrint = false;
lastUp = up;
lastDown = down;
up = !digitalRead(TARGET_UP);
down = !digitalRead(TARGET_DOWN);
if (cur - prev3 >= sensor.getMinimumSamplingPeriod())
{
if (sensor.getStatus() == 0)
{
prev3 = cur;
double t = sensor.getTemperature();
/* Sometimes I get a checksum error from my DHT-22.
To avoid exceptions I check if the reported temp is a number.
This should work only with the "getStatus() == 0" above, but it gave me errors anyway, So I doublecheck */
if (!isnan(t))
{
dtemp = round(t * 2.0) / 2.0;
ctemp = round(t);
}
}
else
{
// If there's an error in the sensor, wait 5 seconds to let the communication reset
prev3 += 5000;
sensor.setup(TEMP_IN);
}
}
fanPID.Compute(); // Do magic
if (cur - prev1 >= WAIT)
{
prev1 = cur;
unsigned long _duration = duration;
unsigned long _ticks = ticks;
duration = 0;
// Calculate fan speed
float Freq = (1e6 / float(_duration) * _ticks) / 2;
speed = Freq * 60;
ticks = 0;
// Turn the fans ON/OFF
if (round(duty) < DUTY_MIN)
{
digitalWrite(RELAY, HIGH);
PWM6 = 0;
fanRunning = false;
}
else
{
fanRunning = true;
PWM6 = duty;
digitalWrite(RELAY, LOW);
}
shouldPrint = true; // Things have changed. remind to update the display
DEBUG(sensor.getStatusString());
DEBUG(" - Target: ");
DEBUG(storage.target);
DEBUG(" - Temp: ");
DEBUG(ctemp);
DEBUG(" - Duty: ");
DEBUG(map(round(duty), 0, 255, 0, 100));
DEBUG("\n");
}
/* Checks if the +/- buttons are pressed and if it's not the first time they've been pressed. */
if (up && !lastUp == up && targetMode && storage.target < 255)
{
storage.target++;
}
if (down && !lastDown == down && targetMode && storage.target > 0)
{
storage.target--;
}
/* If either + or - buttons are pressed, enter target mode and display the current target on the lcd. */
if (up || down)
{
targetMode = true;
shouldPrint = true;
prev2 = cur;
}
/* If 3 secs have elapsed and no button has been pressed, exit target mode. */
if (targetMode && cur - prev2 >= 3000)
{
targetMode = false;
shouldPrint = true;
saveConfig(); // Save the config only when exiting targetMode to reduce EEPROM wear
}
if (shouldPrint)
printSeg();
}