Lüftersteuerung

Ich habe das mal mit zwei Lüftern ausprobiert. Funktioniert.

Das Bild ist jetzt nur nicht mehr ganz so übersichtlich wie das erste:

PWM ist parallel angeschlossen. Die Anschlüsse für 12V und Tacho wurden einfach zwei mal (fast) identisch (entsprechend dem Schaltplan #7) aufgebaut, wobei der Tachoanschluss von Lüfter Eins an Pin D2, der von Lüfter Zwei an D3 angeschlossen ist.

Der angepasste Quellcode für zwei Lüfter:

#include <Arduino.h>
#include <util/atomic.h>
#include <Streaming.h>
Print &cout = Serial; 

#define MAX_FANS 2U  // Define number of fans 1 or 2 !
static_assert(MAX_FANS> 0 && MAX_FANS < 3,"Only 1U or 2U are allowed");

//////////////////////////////////////////////////////////////////////////////
/// @brief Timerclass for millis() 
/// 
//////////////////////////////////////////////////////////////////////////////
class Timer {
public:
  void start() { timeStamp = millis(); }
  bool operator()(const unsigned long duration) const { return (millis() - timeStamp >= duration) ? true : false; }

private:
  unsigned long timeStamp {0};
};


//////////////////////////////////////////////////////////////////////////////
/// @brief Class for calculating a simple average
/// 
/// @tparam T           Datatype
/// @tparam MAX_SIZE    Maximum number of individual values for calculating the average 
//////////////////////////////////////////////////////////////////////////////
template <typename T, uint8_t MAX_SIZE = 1> class Average {
public:
  uint8_t getIdx() { return idx; };

  const void setValue(T val) {
    values[idx] = val;
    if (++idx >= maxIdx) idx = 0;
  }

  T getAverage() {
    T tmp = 0;
    for (auto value : values) { tmp += value; }
    return tmp / maxIdx;
  }

private:
  const uint8_t maxIdx {MAX_SIZE};
  T values[MAX_SIZE];
  uint8_t idx {0};
};

//////////////////////////////////////////////////////////////////////////////
/// @brief Calculates the average rotation frequency of a fan.
///
/// @tparam T         Type
/// @tparam MAX_SIZE  Arraysize
/// @param duration   Time between two edge signals
/// @param readings   Object which is used to calculate the average
/// @return uint32_t  Average value
//////////////////////////////////////////////////////////////////////////////
template <typename T, uint8_t MAX_SIZE = 1> uint32_t calcAvgFrequency(volatile uint32_t &duration, T &readings) {
  uint32_t freq {0};
  if (duration > 0) { freq = 100000000 / duration; }
  readings.setValue(freq);
  return readings.getAverage();
}

//////////////////////////////////////////////////////////////////////////////
/// global constants
/// 
//////////////////////////////////////////////////////////////////////////////

constexpr uint8_t PIN_ANALOG {A0};

constexpr uint16_t HERTZ {25000};
constexpr uint16_t PRESCALER {1};
constexpr uint8_t CLOCKSET {_BV(CS10)};
constexpr uint16_t ICOUNTER {static_cast<uint16_t>((F_CPU / (2UL * PRESCALER * HERTZ)) - 1)};

constexpr uint16_t INTERVAL_MS {1000};
constexpr uint8_t MAX_AVERAGE_IDX {12};

volatile uint32_t durationInt0;
volatile uint32_t lastDurInt0;

volatile uint32_t durationInt1;
volatile uint32_t lastDurInt1;

//////////////////////////////////////////////////////////////////////////////
/// Global variables / objects
/// 
//////////////////////////////////////////////////////////////////////////////

Timer timer;
Average<uint32_t, MAX_AVERAGE_IDX> rpmInt[MAX_FANS];

//////////////////////////////////////////////////////////////////////////////
/// @brief Enable external interrupts for INT0 and INT1 Pins (D2, D3)
///
//////////////////////////////////////////////////////////////////////////////
void enableExternalInterrupts() {
  EICRA = _BV(ISC11) | _BV(ISC10) | _BV(ISC01) | _BV(ISC00);   // Int0 & Int1 -> rising edge
  EIMSK = _BV(INT1) | _BV(INT0);
}

// External Interrupt Pin D2
//
//////////////////////////////////////////////////////////////////////////////
/// @brief  To determine the speed, measure the time between the edge changes (rising edge).
///         A fan provides two edge changes per revolution.
//////////////////////////////////////////////////////////////////////////////
ISR(INT0_vect) {
  uint32_t timestamp = micros();
  durationInt0 = (timestamp - lastDurInt0);
  lastDurInt0 = timestamp;
}


// External Interrupt Pin D3 second fan
ISR(INT1_vect) {
  uint32_t timestamp = micros();
  durationInt1 = (timestamp - lastDurInt1);
  lastDurInt1 = timestamp;
}

//////////////////////////////////////////////////////////////////////////////
/// @brief Init PWM signal (25kHz)
///
//////////////////////////////////////////////////////////////////////////////
void pwmInit() {
  DDRB |= _BV(PB1);   // OCR1A Pin (PB1 / D9)
  TCCR1A = _BV(WGM11);
  TCCR1B = _BV(WGM13);
  ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { ICR1 = ICOUNTER; }
}

//////////////////////////////////////////////////////////////////////////////
/// @brief Set dutycycle of the PWM signal
///
/// @param dc   0-100%
//////////////////////////////////////////////////////////////////////////////
void pwmSetDutyCycle(uint8_t dc) {
  uint16_t ocr1a = (dc > 100) ? 100 : dc;
  ocr1a = static_cast<uint16_t>((((ICR1 * 10UL * ocr1a) / 100) + 5) / 10);   // Result must be 16Bit
  ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { OCR1A = ocr1a; }
}

//////////////////////////////////////////////////////////////////////////////
/// @brief start PWM signal
///
//////////////////////////////////////////////////////////////////////////////
void pwmStart() {
  TCCR1A |= _BV(COM1A1);
  TCCR1B |= CLOCKSET;
}

//////////////////////////////////////////////////////////////////////////////
/// @brief stop PWM signal
///
//////////////////////////////////////////////////////////////////////////////
void pwmStop() {
  TCCR1B &= ~(_BV(CS10) | _BV(CS11) | _BV(CS12));
  TCCR1A &= ~(_BV(COM1A1));
  PORTB &= ~(_BV(PB1));   // OCR1A Pin (PB1 / D9) to LOW
}

//////////////////////////////////////////////////////////////////////////////
/// @brief Show Frequency and RPM on the serial console
///
/// @param freq
//////////////////////////////////////////////////////////////////////////////
void showRPM(uint32_t freq, uint8_t fanNr) {
  uint32_t rpm = freq * 60;
  rpm /= 200;
  cout << "Lüfter Nr: " << fanNr << " Frequenz: " << freq / 100 << "." << freq % 100 << "Hz Rpm: " << rpm << endl;
}

//////////////////////////////////////////////////////////////////////////////////////
// Main program
//////////////////////////////////////////////////////////////////////////////////////
void setup(void) {
  Serial.begin(115200);
  enableExternalInterrupts();
  pwmInit();
  pwmSetDutyCycle(100);
  pwmStart();
  delay(10000);   // full speed for 10 seconds

  timer.start();
}

void loop() {
  static uint16_t oldValue {0};
  static uint8_t show[MAX_FANS] {0};
  static uint32_t freq[MAX_FANS] {0};

  // Determine the potentiometer position and set the duty cycle accordingly.
  uint16_t value = map(analogRead(PIN_ANALOG), 0, 1015, 20, 100);
  if (value != oldValue) {
    oldValue = value;
    pwmSetDutyCycle(value);
  }

  // Determine the fan speed and output it via the serial interface.
  // Do this every INTERVAL_MS milliseconds
  if (timer(INTERVAL_MS) == true) {
    timer.start();
    freq[0] = calcAvgFrequency(durationInt0, rpmInt[0]);
#if (MAX_FANS > 1)
    freq[1] = calcAvgFrequency(durationInt1, rpmInt[1]);
    for (uint8_t i = 0; i < MAX_FANS; ++i) {
      if (show[i] < (MAX_AVERAGE_IDX + 1)) {
        ++show[i];
      } else {
        showRPM(freq[i],i);
      }
    }
    cout << endl;
#else
    if (show[0] < (MAX_AVERAGE_IDX + 1)) {
      ++show[0];
    } else {
      showRPM(freq[0],1);
    }
#endif
  }
}

Eine Ausgabe auf ein Display sollte sich da leicht einbauen lassen.