//////////////////////////////////////////////////////////////////////////////
/// @brief Example program
/////////////////////////////////////////////////////////////////////////////////
/// @file main.cpp
/// @author Kai R. ()
/// @brief PWM Fan control - Suitable for microcontroller boards based on the ATMega328.
///
/// @date 2024-02-25
/// @version 1.0
///
//////////////////////////////////////////////////////////////////////////////
#include <Arduino.h>
#include <Streaming.h> // https://github.com/janelia-arduino/Streaming
#include <LiquidCrystal_I2C.h> // https://github.com/johnrickman/LiquidCrystal_I2C
#include <util/atomic.h>
//////////////////////////////////////////////////////////////////////////////
/// global definitions / constants
///
//////////////////////////////////////////////////////////////////////////////
// #define SERIALOUT
#ifdef SERIALOUT
Print &cout = Serial;
#endif
#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");
constexpr uint8_t PIN_ANALOG {A0};
// Values for generating the PWM signal
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)};
// Timeconstants
constexpr uint16_t INTERVAL_MS {1000};
constexpr uint16_t STARTUP_TIME_SEC {10};
constexpr uint8_t MAX_AVERAGE_IDX {12}; // Number of array elements for calculating the average
// LCD values
constexpr uint8_t LCD_LINES {4}; // Set 2 for 2x16 Display, 4 for 4x20 Display
constexpr uint8_t LCD_COLONS {(LCD_LINES == 2) ? 16 : 20};
constexpr uint8_t SECOND_LINE {(LCD_LINES == 2) ? 1 : 2};
constexpr uint8_t LEFT_COL {(LCD_LINES == 2) ? 0 : 2};
constexpr uint8_t LEFT_COL_CNTDWN {(LCD_LINES == 2) ? 2 : 4};
constexpr uint8_t DATA_DIGITS {5};
constexpr uint8_t DATA_WIDTH {DATA_DIGITS + 2};
constexpr uint8_t DATA_COL {LEFT_COL + DATA_DIGITS + 1};
constexpr uint8_t CNTDWN_BAR {10};
constexpr uint8_t CNTDWN_CHAR {'*'};
//////////////////////////////////////////////////////////////////////////////
/// @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() const { return idx; };
const void setValue(T val) {
values[idx] = val;
if (++idx >= maxIdx) { // array is filled
divideByMaxIdx = true;
idx = 0;
}
}
T getAverage() const {
T tmp = 0;
for (auto value : values) { tmp += value; }
// Only divide by the total number of array elements
// once the array has been completely filled with measured values.
return (divideByMaxIdx == true) ? tmp / maxIdx : tmp / idx;
}
private:
const uint8_t maxIdx {MAX_SIZE};
T values[MAX_SIZE] {0};
uint8_t idx {0};
bool divideByMaxIdx {false};
};
//////////////////////////////////////////////////////////////////////////////
/// @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 = 100000000UL / duration; }
readings.setValue(freq);
return readings.getAverage();
}
//////////////////////////////////////////////////////////////////////////////
/// Global variables / objects
///
//////////////////////////////////////////////////////////////////////////////
// used by interrupt routine(s)
volatile uint32_t durationInt0;
volatile uint32_t lastDurInt0;
volatile uint32_t durationInt1;
volatile uint32_t lastDurInt1;
Timer timer;
Average<uint32_t, MAX_AVERAGE_IDX> rpmInt[MAX_FANS];
// LiquidCrystal_I2C lcd(0x27, LCD_COLONS, LCD_LINES); // I2C Addr 2x16
LiquidCrystal_I2C lcd(0x3F, LCD_COLONS, LCD_LINES); // I2C Addr 4x20
Print &coutLcd = lcd;
//////////////////////////////////////////////////////////////////////////////
/// @brief Enable external interrupts for INT0 and INT1 Pins (D2, D3)
///
//////////////////////////////////////////////////////////////////////////////
void enableExternalInterrupts() {
#if (MAX_FANS > 1U)
EICRA = _BV(ISC11) | _BV(ISC10) | _BV(ISC01) | _BV(ISC00); // Int0 & Int1 -> rising edge
EIMSK = _BV(INT1) | _BV(INT0);
#else
EICRA = _BV(ISC01) | _BV(ISC00); // Int0 & Int1 -> rising edge
EIMSK = _BV(INT0);
#endif
}
// 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
//////////////////////////////////////////////////////////////////////////////
#ifdef SERIALOUT
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;
}
#endif
//////////////////////////////////////////////////////////////////////////////
/// @brief Show start sequence on LCD
///
//////////////////////////////////////////////////////////////////////////////
void lcdCountDown() {
uint8_t counter {STARTUP_TIME_SEC};
lcd.setCursor(LEFT_COL_CNTDWN, 0);
coutLcd << F("Start up: ") << STARTUP_TIME_SEC;
lcd.setCursor(LEFT_COL_CNTDWN, SECOND_LINE);
coutLcd << "[" << _PAD(CNTDWN_BAR, ' ') << "]";
timer.start();
do {
if (timer(INTERVAL_MS) == true) {
timer.start();
lcd.setCursor(LEFT_COL_CNTDWN + CNTDWN_BAR, 0);
coutLcd << _FMT(("%"), _WIDTH(--counter, 2));
lcd.setCursor(LEFT_COL_CNTDWN + 1, SECOND_LINE);
coutLcd << _PAD(CNTDWN_BAR - counter, CNTDWN_CHAR);
}
} while (counter > 0);
delay(1000);
}
//////////////////////////////////////////////////////////////////////////////
/// @brief Display the main output text
///
//////////////////////////////////////////////////////////////////////////////
void lcdMain() {
lcd.clear();
lcd.setCursor(LEFT_COL, 0);
coutLcd << "FAN 1:" << _PAD(DATA_WIDTH, ' ') << "RPM";
#if (MAX_FANS > 1U)
lcd.setCursor(LEFT_COL, SECOND_LINE);
coutLcd << "FAN 2:" << _PAD(DATA_WIDTH, ' ') << "RPM";
#endif
}
//////////////////////////////////////////////////////////////////////////////
/// @brief Display the RPM
///
/// @param freq
/// @param fanNr
//////////////////////////////////////////////////////////////////////////////
void lcdData(uint32_t freq, uint8_t fanNr) {
#if (MAX_FANS > 1U)
uint8_t lcdLine = (fanNr == 0) ? 0 : SECOND_LINE;
#else
uint8_t lcdLine {0};
#endif
uint32_t rpm = freq * 60;
rpm /= 200;
lcd.setCursor(DATA_COL, lcdLine);
coutLcd << _FMT(("%"), _WIDTH(rpm, DATA_WIDTH - 1));
}
//////////////////////////////////////////////////////////////////////////////////////
// Main program
//////////////////////////////////////////////////////////////////////////////////////
void setup(void) {
#ifdef SERIALOUT
Serial.begin(115200);
#endif
lcd.init();
Wire.setClock(400000); // Comment out if there are problems with the display.
lcd.backlight();
enableExternalInterrupts();
pwmInit();
pwmSetDutyCycle(100);
pwmStart();
lcdCountDown();
lcdMain();
timer.start();
}
void loop() {
static uint16_t oldValue {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 / LCD.
// Do this every INTERVAL_MS milliseconds
if (timer(INTERVAL_MS) == true) {
timer.start();
freq[0] = calcAvgFrequency(durationInt0, rpmInt[0]);
freq[0] = random(100,120);
#if (MAX_FANS > 1U)
freq[1] = calcAvgFrequency(durationInt1, rpmInt[1]);
freq[1] = random(200,220);
for (uint8_t i = 0; i < MAX_FANS; ++i) {
#ifdef SERIALOUT
showRPM(freq[i], i);
#endif
lcdData(freq[i], i);
}
#ifdef SERIALOUT
cout << endl;
#endif
#else
#ifdef SERIALOUT
showRPM(freq[0], 0);
#endif
lcdData(freq[0], 0);
#endif
}
}