-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathFan_speed.ino
347 lines (293 loc) · 11.7 KB
/
Fan_speed.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
// Preconfigured board options:
// Digispark Pro (One fan)
// Arduino Uno/Nano (One fan)
// Teensy 2.0 (Two fans)
// Arduino Mega (Four fans)
// And a custom config to allow any other board with one 16-bit timer and free hardware interrupt pin per fan configured
// Uses Timer1/3/4/5 for each fan respectively as these are usually the 16-bit timers if the board has them
// Example fans:
// In (Noctua NF S12A):
// Fan speed 300-1200 RPM
// 600-2400 half_revolutions/min
// 10-40 Hz
// **100-25 ms**
// Out (Emulating: Nidec V12E12BS1B570):
// Fan speed 1600?-6400 RPM
// 3200-12800 half_revolutions/min
// 53.33-213.33 Hz
// 18.75-4.69 ms
// **4687-1172 counts**
// Factor 5.333
// For each fan x (0-3), the following pins need to be configured:
// Connected to the fan to read its actual RPM
// #define IN_PIN_x 3 // INT0/1/2/...
// Connected to the end device to output the modified signal to
// #define OUT_PIN_x 5 // Any other non-restricted pin
// And optionally, to use a potentiometer to set the modification factor between fan speed and output speed
// #define POT_PIN_x A0 // An analog input pin
// Can also define the same pin to control multiple fans from the same potentiometer, e.g.:
// #define POT_PIN_3 POT_PIN_1
// There are defaults for a few boards configured below.
// Ideally comment out fan configs that aren't being used on your board if any.
#if defined(PINMAPPING_DIGI)
// Could use defined(__AVR_ATtinyX7__) instead for more general boards, but not sure on pin mapping
// Use the ATTinyCore for the Digispark Pro boards
// Possibly need to make sure "Digispark" is selected from the Tools -> Pin Mapping submenu
// Extra info:
// https://github.com/SpenceKonde/ATTinyCore/blob/v2.0.0-devThis-is-the-head-submit-PRs-against-this/avr/extras/ATtiny_x7.md
// https://github.com/SpenceKonde/ATTinyCore/blob/c0b7e78f750111bbfeeb36088ef9be3d5728a927/avr/variants/tinyx7_digi/pins_arduino.h
#define IN_PIN_0 PA3 // INT1/Digital pin 9 on the Digispark Pro
#define OUT_PIN_0 5 // Any other non-restricted pin
// #define POT_PIN_0 A0 // Uncomment to enable potentiometer (An analog input pin)
#elif defined(ARDUINO_AVR_UNO) || defined(ARDUINO_AVR_NANO)
#define IN_PIN_0 3 // INT1
#define OUT_PIN_0 5 // Any other non-restricted pin
// #define POT_PIN_0 A0 // Uncomment to enable potentiometer (An analog input pin)
#elif defined(__AVR_ATmega32U4__) // Teensy 2.0 etc.
#define IN_PIN_0 5 // INT0
#define OUT_PIN_0 4 // Any other non-restricted pin
// #define POT_PIN_0 A0 // Uncomment to enable potentiometer (An analog input pin)
#define IN_PIN_1 6 // INT1
#define OUT_PIN_1 7 // Any other non-restricted pin
// #define POT_PIN_1 A1 // Uncomment to enable potentiometer (An analog input pin)
#elif defined(ARDUINO_AVR_MEGA2560)
// INTx as defined by the Arduino library rather than the chip:
// https://www.arduino.cc/reference/en/language/functions/external-interrupts/attachinterrupt/#_interrupt_numbers
#define IN_PIN_0 3 // INT1
#define OUT_PIN_0 5 // Any other non-restricted pin
// #define POT_PIN_0 A0 // Uncomment to enable potentiometer (An analog input pin)
#define IN_PIN_1 21 // INT2
#define OUT_PIN_1 7 // Any other non-restricted pin
// #define POT_PIN_1 A1 // Uncomment to enable potentiometer (An analog input pin)
#define IN_PIN_2 20 // INT3
#define OUT_PIN_2 9 // Any other non-restricted pin
// #define POT_PIN_2 A2 // Uncomment to enable potentiometer (An analog input pin)
#define IN_PIN_3 19 // INT4
#define OUT_PIN_3 11 // Any other non-restricted pin
// #define POT_PIN_3 A3 // Uncomment to enable potentiometer (An analog input pin)
#else
// Define custom boards here
// https://www.arduino.cc/reference/en/language/functions/external-interrupts/attachinterrupt/
// #define IN_PIN_0 ? // INT0/1/2/3/... (A free hardware interrupt pin)
// #define OUT_PIN_0 ? // Any other non-restricted pin
// #define POT_PIN_0 A0 // Uncomment to enable potentiometer (An analog input pin)
#endif
// Conversion factor between fan speed and output speed
// Set by a potentiometer if respective pin is defined above
#define FACTOR_POT_MIN 0.1f
#define FACTOR_POT_MAX 10.0f
// Otherwise set to a fixed value here:
#define FACTOR 2.0f
// Time in milliseconds between updates to the RPM (longer than 6 sec can cause overflow)
// Can be helpful to make this shorter if the fan gets tested for it's speed range at startup
#define UPDATE_INTERVAL_MS 500
// Rate at which average updates to new data
// Usually want lower alpha/rate if the update interval is bigger if output smoothing is desired
// Set to 1 and 0 respectively to disable averaging and use instantaneous timing instead
#define ALPHA 0.5f
#define ONE_MINUS_ALPHA 0.5f
// TODO: CS1x is only for Timer1, unsure if it really matters that using it for other timers?
// Uncomment desired resolution setting of output RPM:
// clk/64 (011):
// 0.000,004 sec resolution & Max interval: 0.262 sec
// Min pulse frequency: 3.8 Hz & Max: 250,000 Hz
// Min(2 pulses/rev): 144.4 RPM & Max: 7,500,000 RPM
// RPM resolution:
// @300RPM: ~0.003
// @1200RPM: ~0.048
// @10000RPM: ~3.3
#define PRESCALER (0 << CS12) | (1 << CS11) | (1 << CS10)
#define US_TO_COUNT 4.0f
// clk/256 (100):
// 0.000,016 sec resolution & Max interval: 1.049 sec
// Min pulse frequency: 0.95 Hz & Max: 62500 Hz
// Min(2 pulses/rev): 28.6 RPM & Max: 1,875,000 RPM
// RPM resolution:
// @300RPM: ~0.012
// @1200RPM: ~0.192
// @10000RPM: ~13.4
// #define PRESCALER (1 << CS12) | (0 << CS11) | (0 << CS10)
// #define US_TO_COUNT 16.0f
// clk/1024 (101):
// 0.000,064 sec resolution & Max interval: 4.194 sec
// Min pulse frequency: 0.238 Hz & Max: 15625 Hz
// Min(2 pulses/rev): 7.15 RPM & Max: 468,750 RPM
// RPM resolution:
// @300RPM: ~0.048
// @1200RPM: ~0.769
// @10000RPM: ~53.9
// #define PRESCALER (1 << CS12) | (0 << CS11) | (1 << CS10)
// #define US_TO_COUNT 64.0f
volatile unsigned long previousMicros[4] = {0, 0, 0, 0};
// Initialise at slowest fan speed for Noctua NF S12A
volatile float avgInterval[4] = {100.0f, 100.0f, 100.0f, 100.0f};
uint16_t toggleA[4] = {4687, 4687, 4687, 4687};
uint16_t toggleB[4] = {2343, 2343, 2343, 2343};
void setup() {
#ifdef POT_PIN_0
pinMode(POT_PIN_0, INPUT);
#endif
#ifdef POT_PIN_1
pinMode(POT_PIN_1, INPUT);
#endif
#ifdef POT_PIN_2
pinMode(POT_PIN_2, INPUT);
#endif
#ifdef POT_PIN_3
pinMode(POT_PIN_3, INPUT);
#endif
cli(); // disable global interrupts
#ifdef IN_PIN_0
// Timer1
pinMode(IN_PIN_0, INPUT_PULLUP);
// Connect OUT_PIN_0 to ground
pinMode(OUT_PIN_0, OUTPUT);
digitalWrite(OUT_PIN_0, LOW);
attachInterrupt(digitalPinToInterrupt(IN_PIN_0), []() {halfRevolution(0);}, RISING);
TCCR1A = 0; // Disconnect output pins from timer 1
TCCR1B = (1 << WGM12); // Set Timer 1 to CTC mode
OCR1A = toggleA[0]; // Set 16-bit Output Compare Register A (TOP value / High trigger time)
OCR1B = toggleB[0]; // Set 16-bit Output Compare Register B (Low trigger time)
TIMSK1 |= (1 << OCIE1A); // Enable TIMER1_COMPA interrupt
TIMSK1 |= (1 << OCIE1B); // Enable TIMER1_COMPB interrupt
TCCR1B |= PRESCALER;
#endif
#ifdef IN_PIN_1
// Timer3
pinMode(IN_PIN_1, INPUT_PULLUP);
pinMode(OUT_PIN_1, OUTPUT); digitalWrite(OUT_PIN_1, LOW);
attachInterrupt(digitalPinToInterrupt(IN_PIN_1), []() {halfRevolution(1);}, RISING);
// Skip to Timer3 since Timer2 seems to usually be 8-bit by convention
TCCR3A = 0; // Disconnect output pins from timer 3
TCCR3B = (1 << WGM32) | PRESCALER; // Set Timer 3 to CTC mode and add prescaler
OCR3A = toggleA[1]; OCR3B = toggleB[1];
TIMSK3 |= (1 << OCIE3A) | (1 << OCIE3B);
#endif
#ifdef IN_PIN_2
// Timer4
pinMode(IN_PIN_2, INPUT_PULLUP);
pinMode(OUT_PIN_2, OUTPUT); digitalWrite(OUT_PIN_2, LOW);
attachInterrupt(digitalPinToInterrupt(IN_PIN_2), []() {halfRevolution(2);}, RISING);
TCCR4A = 0; // Disconnect output pins from timer 4
TCCR4B = (1 << WGM42) | PRESCALER; // Set Timer 4 to CTC mode and add prescaler
OCR4A = toggleA[2]; OCR4B = toggleB[2];
TIMSK4 |= (1 << OCIE4A) | (1 << OCIE4B);
#endif
#ifdef IN_PIN_3
// Timer5
pinMode(IN_PIN_3, INPUT_PULLUP);
pinMode(OUT_PIN_3, OUTPUT); digitalWrite(OUT_PIN_3, LOW);
attachInterrupt(digitalPinToInterrupt(IN_PIN_3), []() {halfRevolution(3);}, RISING);
TCCR5A = 0; // Disconnect output pins from timer 5
TCCR5B = (1 << WGM52) | PRESCALER; // Set Timer 5 to CTC mode and add prescaler
OCR5A = toggleA[3]; OCR5B = toggleB[3];
TIMSK5 |= (1 << OCIE5A) | (1 << OCIE5B);
#endif
sei(); // re-enable global interrupts
}
void loop() {
delay(UPDATE_INTERVAL_MS);
float convFactor;
#ifdef IN_PIN_0
#ifdef POT_PIN_0
convFactor = mapFloat(analogRead(POT_PIN_0), 0, 1023, FACTOR_POT_MIN, FACTOR_POT_MAX);
#else
convFactor = FACTOR;
#endif
calculateOutputInterval(0, convFactor);
#endif
#ifdef IN_PIN_1
#ifdef POT_PIN_1
convFactor = mapFloat(analogRead(POT_PIN_1), 0, 1023, FACTOR_POT_MIN, FACTOR_POT_MAX);
#else
convFactor = FACTOR;
#endif
calculateOutputInterval(1, convFactor);
#endif
#ifdef IN_PIN_2
#ifdef POT_PIN_2
convFactor = mapFloat(analogRead(POT_PIN_2), 0, 1023, FACTOR_POT_MIN, FACTOR_POT_MAX);
#else
convFactor = FACTOR;
#endif
calculateOutputInterval(2, convFactor);
#endif
#ifdef IN_PIN_3
#ifdef POT_PIN_3
convFactor = mapFloat(analogRead(POT_PIN_3), 0, 1023, FACTOR_POT_MIN, FACTOR_POT_MAX);
#else
convFactor = FACTOR;
#endif
calculateOutputInterval(3, convFactor);
#endif
}
float mapFloat(float value, float fromLow, float fromHigh, float toLow, float toHigh) {
return (value - fromLow) * (toHigh - toLow) / (fromHigh - fromLow) + toLow;
}
void calculateOutputInterval(byte fanIndex, float convFactor) {
float maxCount;
maxCount = avgInterval[fanIndex] / (convFactor * US_TO_COUNT);
maxCount = constrain(maxCount, 2.0f, 65535.0f);
toggleA[fanIndex] = lround(maxCount);
toggleB[fanIndex] = toggleA[fanIndex]/2;
}
/* Interrupt Functions */
void halfRevolution(byte fanIndex) {
// For each rotation of the fan, this interrupt function is triggered twice
unsigned long currentMicros = micros();
// Add some smoothing to the speed
avgInterval[fanIndex] = (ALPHA*(currentMicros-previousMicros[fanIndex])) + (ONE_MINUS_ALPHA*avgInterval[fanIndex]);
previousMicros[fanIndex] = currentMicros;
}
#ifdef IN_PIN_0
ISR(TIMER1_COMPA_vect) {
// Interrupt releasing OUT_PIN_0 (Floating/Letting PC pull HIGH)
pinMode(OUT_PIN_0, INPUT);
// OCR1A/B are set here to simulate double buffering that avoids
// setting TOP lower than the current timer and thus missing the interrupt
if (OCR1A != toggleA[0]) {
OCR1A = toggleA[0];
OCR1B = toggleB[0];
}
}
ISR(TIMER1_COMPB_vect) {
// Interrupt connecting OUT_PIN_0 to ground (LOW)
pinMode(OUT_PIN_0, OUTPUT);
}
#endif
#ifdef IN_PIN_1
ISR(TIMER3_COMPA_vect) {
pinMode(OUT_PIN_1, INPUT);
if (OCR3A != toggleA[1]) {
OCR3A = toggleA[1];
OCR3B = toggleB[1];
}
}
ISR(TIMER3_COMPB_vect) {
pinMode(OUT_PIN_1, OUTPUT);
}
#endif
#ifdef IN_PIN_2
ISR(TIMER4_COMPA_vect) {
pinMode(OUT_PIN_2, INPUT);
if (OCR4A != toggleA[2]) {
OCR4A = toggleA[2];
OCR4B = toggleB[2];
}
}
ISR(TIMER4_COMPB_vect) {
pinMode(OUT_PIN_2, OUTPUT);
}
#endif
#ifdef IN_PIN_3
ISR(TIMER5_COMPA_vect) {
pinMode(OUT_PIN_3, INPUT);
if (OCR5A != toggleA[3]) {
OCR5A = toggleA[3];
OCR5B = toggleB[3];
}
}
ISR(TIMER5_COMPB_vect) {
pinMode(OUT_PIN_3, OUTPUT);
}
#endif