afancontrol/arduino/micro.ino
2021-10-26 12:58:36 +02:00

238 lines
7.2 KiB
C++

// The pins defined in this program are for Arduino Micro.
/////////////////////////
// FAN PWM outputs:
// https://www.pjrc.com/teensy/td_libs_TimerOne.html
// https://github.com/PaulStoffregen/TimerOne
// https://github.com/PaulStoffregen/TimerThree
#include <TimerOne.h>
#include <TimerThree.h>
// TimerOne/Three accepts PWM duty in range from 0 to 1023.
// The standard range for PWM fans on Linux is, however, from 0 to 255.
// This macros below does the conversion from 255 to 1023.
#define PWM_255_TO_1023(PWM) ((PWM == 0) ? 0 : (1L * PWM + 1) * 4 - 1)
// These are the pins connected to the Timers 1 and 3 on Arduino Micro.
// See https://www.pjrc.com/teensy/td_libs_TimerOne.html
byte currentPWM5;
byte currentPWM9;
byte currentPWM10;
byte currentPWM11;
#define SET_PWM(PIN, PWM) currentPWM##PIN = (PWM);
#define SET_PWM_HIGH(PIN) SET_PWM(PIN, 255)
#define PRINT_PWM_JSON(PIN) \
Serial.print("\""); \
Serial.print(PIN, DEC); \
Serial.print("\":"); \
Serial.print(currentPWM##PIN, DEC);
/////////////////////////
// FAN tachometer (RPM) inputs:
// FAN speed from tachometer is measured by counting the number
// of interrupts (pulses) for a small period of time `MEASUREMENT_TIME_MS`.
//
// Between the periods the current status (a JSON) is reported and
// an incoming command is read (if any) from the Serial port.
//
// The number of pulses for each time period is written to a ring buffer,
// which allows to compute the RPM on a larger time interval than a single
// small period of `MEASUREMENT_TIME_MS`, which yields a smoother RPM.
//
// The number of periods of the ring buffer is `PULSES_BUFFER_LEN`,
// so the total amount of time the pulses are measured for would be
// `PULSES_BUFFER_LEN` * `MEASUREMENT_TIME_MS`.
//
#define MEASUREMENT_TIME_MS 250
#define PULSES_BUFFER_LEN 6
int pulsesBufferPosition = 0;
// * 60 seconds in 1 minute (we count revolutions per *minute*);
// * 1000 to go from seconds to milliseconds;
// * Divided by the amount of time the pulses are measured.
//
// Be sure to select the dividers which yield an integer result after each division.
#define PULSES_MULTIPLIER (1L * 60 * 1000 / MEASUREMENT_TIME_MS / PULSES_BUFFER_LEN)
// When a PWM wire goes near the Tachometer wire, the Tachometer one might
// receive interference, which would be sensed by the interruptions,
// spoiling the RPM measurements.
//
// PWM works at 25kHz, Tachometer is in the range ~4hz - ~200hz (120 RPM - 6000 RPM),
// so the extraneous PWM pulses would have delay ~0.04 - 1ms, while
// the genuine Tachometer pulses would have delay 5ms-250ms.
//
// This problem is similar to the common one occurring with the switches
// (http://www.gammon.com.au/switches), when a click on a switch produces
// many short pulses instead of a single long one.
//
// This var defines the minimum delay (in ms) between the two RISING
// interrupts, which should be treated as a valid Tachometer pulse.
#define PULSES_ACCEPT_MIN_DURATION_MS 3
#define TACHO_PULSES_INT_FUNCTION(PIN) \
volatile int tachoPulses##PIN [PULSES_BUFFER_LEN]; \
volatile unsigned long lastPulse##PIN; \
void incTachoPulses##PIN () { \
unsigned long now = millis(); \
if (now - lastPulse##PIN < PULSES_ACCEPT_MIN_DURATION_MS) { lastPulse##PIN = now; return; } \
lastPulse##PIN = now; \
tachoPulses##PIN [pulsesBufferPosition] ++; \
}
#define TACHO_PULSES_ATTACH_INT(PIN) \
pinMode(PIN, INPUT); \
attachInterrupt(digitalPinToInterrupt(PIN), incTachoPulses##PIN, RISING); \
{ \
for (int i = 0; i < PULSES_BUFFER_LEN; i++) tachoPulses##PIN[i] = 0; \
lastPulse##PIN = 0; \
}
#define TACHO_PULSES_NEXT_BUCKET \
pulsesBufferPosition = (pulsesBufferPosition + 1) % PULSES_BUFFER_LEN;
#define TACHO_PULSES_RESET_CURRENT_BUCKET(PIN) \
tachoPulses##PIN[pulsesBufferPosition] = 0;
#define PRINT_RPM_JSON(PIN) \
Serial.print("\""); \
Serial.print(PIN, DEC); \
Serial.print("\":"); \
Serial.print(PULSES_MULTIPLIER * sumPulses(tachoPulses##PIN) / 2, DEC);
// ^^^ Regarding division by 2: PC fans do 2 pulses per each revolution,
// see https://electronics.stackexchange.com/q/8295
int sumPulses(volatile int tachoPulses [PULSES_BUFFER_LEN]) {
int sum = 0;
for (int i = 0; i < PULSES_BUFFER_LEN; i++) {
sum += tachoPulses[i];
}
return sum;
}
// These are the pins on Arduino Micro which support interrupts.
// See https://www.arduino.cc/reference/en/language/functions/external-interrupts/attachinterrupt/
TACHO_PULSES_INT_FUNCTION(0);
TACHO_PULSES_INT_FUNCTION(1);
TACHO_PULSES_INT_FUNCTION(2);
TACHO_PULSES_INT_FUNCTION(3);
TACHO_PULSES_INT_FUNCTION(7);
/////////////////////////
// Serial commands:
char setSpeedCommand = '\xf1'; // The only supported command currently.
char commandBuffer[3]; // Buffer for the incoming command: [command; pin; speed].
int commandPosition = 0; // The current position in the `commandBuffer`
/////////////////////////
void setup() {
// https://github.com/PaulStoffregen/TimerOne/blob/master/examples/FanSpeed/FanSpeed.pde
Timer1.initialize(40); // 40us == 25kHz
Timer3.initialize(40);
SET_PWM_HIGH(5);
SET_PWM_HIGH(9);
SET_PWM_HIGH(10);
SET_PWM_HIGH(11);
TACHO_PULSES_ATTACH_INT(0);
TACHO_PULSES_ATTACH_INT(1);
TACHO_PULSES_ATTACH_INT(2);
TACHO_PULSES_ATTACH_INT(3);
TACHO_PULSES_ATTACH_INT(7);
Serial.begin(115200);
}
void loop () {
Timer3.pwm(5, PWM_255_TO_1023(currentPWM5));
Timer1.pwm(9, PWM_255_TO_1023(currentPWM9));
Timer1.pwm(10, PWM_255_TO_1023(currentPWM10));
Timer1.pwm(11, PWM_255_TO_1023(currentPWM11));
// Measure RPM from tachometers:
TACHO_PULSES_RESET_CURRENT_BUCKET(0);
TACHO_PULSES_RESET_CURRENT_BUCKET(1);
TACHO_PULSES_RESET_CURRENT_BUCKET(2);
TACHO_PULSES_RESET_CURRENT_BUCKET(3);
TACHO_PULSES_RESET_CURRENT_BUCKET(7);
interrupts();
delay (MEASUREMENT_TIME_MS);
noInterrupts();
TACHO_PULSES_NEXT_BUCKET;
readSerialCommand();
// Print the status (in JSON):
Serial.print("{");
Serial.print("\"fan_inputs\": {");
PRINT_RPM_JSON(0);
Serial.print(",");
PRINT_RPM_JSON(1);
Serial.print(",");
PRINT_RPM_JSON(2);
Serial.print(",");
PRINT_RPM_JSON(3);
Serial.print(",");
PRINT_RPM_JSON(7);
Serial.print("}, ");
Serial.print("\"fan_pwm\": {");
PRINT_PWM_JSON(5);
Serial.print(",");
PRINT_PWM_JSON(9);
Serial.print(",");
PRINT_PWM_JSON(10);
Serial.print(",");
PRINT_PWM_JSON(11);
Serial.print("}");
Serial.print("}\n");
}
void readSerialCommand() {
while (Serial.available()) {
char c = Serial.read();
if (commandPosition == 0 && c != setSpeedCommand) {
Serial.print("{\"error\": \"Unknown command ");
Serial.print(c, HEX);
Serial.print("\"}\n");
continue;
}
commandBuffer[commandPosition] = c;
commandPosition++;
if (commandPosition >= 3) {
// The command buffer is now complete, process it:
processSerialCommand();
commandPosition = 0;
}
}
}
void processSerialCommand() {
// assert (commandBuffer[0] == setSpeedCommand);
byte pwm = (byte)commandBuffer[2];
switch (commandBuffer[1]) {
case 5: SET_PWM(5, pwm); break;
case 9: SET_PWM(9, pwm); break;
case 10: SET_PWM(10, pwm); break;
case 11: SET_PWM(11, pwm); break;
default:
Serial.print("{\"error\": \"Unknown pin ");
Serial.print((int)commandBuffer[1], DEC);
Serial.print(" for the set speed command\"}\n");
}
}