Last active
October 16, 2020 04:39
-
-
Save jayveeangeles/ef40b77e291d6823ed64372bf429d64f to your computer and use it in GitHub Desktop.
Sample PC Fan Controller (Arduino Leonardo)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// http://www.gammon.com.au/timers | |
// measure time from two triggers instead to get instantaneous readings | |
// | |
// create an overflow interrupt for Timer3 here and whenever there's an external interrupt, | |
// read the current value of Timer3 + the recorded overflows multiplied by 2^16 (Timer3 | |
// counts up to 2^16 before overflowing). The external interrupt ISR will record the first | |
// and second time the ISR was triggered for a particular pin. The interrupt will also be | |
// disabled for that pin until the main loop is able to get the values for the start and | |
// finish times and calculate the frequency. | |
#define attachMyInterrupt(pin, mode) attachInterrupt(digitalPinToInterrupt(pin), +[](){ myInterruptHandler(pin); }, mode) | |
const unsigned long INTERVAL = 1000; | |
const byte OC1A_PIN = 9; | |
const byte OC1B_PIN = 10; | |
const byte OC1C_PIN = 11; | |
const byte ALL_FANS[3] = {0, 1, 2}; | |
//const byte OC3A_PIN = 5; | |
const word PWM_FREQ_HZ = 25000; //Adjust this value to adjust the frequency | |
const word TCNT_TOP = 16000000/(2*PWM_FREQ_HZ); | |
const byte CMD_MASK = 15; //b'00001111 | |
const byte FAN_MASK = 192; //b'11000000 | |
const byte OC1A_READ_PIN = 2; | |
const byte OC1B_READ_PIN = 3; | |
const byte OC1C_READ_PIN = 7; | |
volatile unsigned long overflowCount; | |
byte message[2]; | |
typedef struct { | |
uint8_t pinNumber; | |
bool first; | |
bool triggered; | |
unsigned long startTime; | |
unsigned long finishTime; | |
byte pulses; | |
byte interruptFlag; | |
} FanConfig; | |
volatile FanConfig fanConfig[3] = { | |
{2, false, false, 0, 0, 0, INTF1}, | |
{3, false, false, 0, 0, 0, INTF0}, | |
{7, false, false, 0, 0, 0, INTF6} | |
}; | |
void handleSerial(); | |
void setPwmDuty(byte, byte); | |
// function to enable interrupt for pin; reset flags | |
void prepareForInterrupt(volatile FanConfig& fan) { | |
// get ready for next time | |
EIFR = bit (fan.interruptFlag); // clear flag for interrupt 0 | |
fan.first = true; | |
fan.triggered = false; // re-arm for next time | |
switch(fan.pinNumber) { | |
case OC1A_READ_PIN: | |
attachMyInterrupt(OC1A_READ_PIN, RISING); | |
break; | |
case OC1B_READ_PIN: | |
attachMyInterrupt(OC1B_READ_PIN, RISING); | |
break; | |
case OC1C_READ_PIN: | |
attachMyInterrupt(OC1C_READ_PIN, RISING); | |
break; | |
} | |
} | |
// calculate frequency and enable interrupt for that pin afterwards | |
void checkFreq() { | |
for (uint8_t i = 0; i < 3; i++) { | |
if (fanConfig[i].triggered) { | |
unsigned long elapsedTime = fanConfig[i].finishTime - fanConfig[i].startTime; | |
fanConfig[i].pulses = byte (F_CPU / float (elapsedTime)); | |
prepareForInterrupt(fanConfig[i]); | |
} | |
} | |
} | |
void setup() { | |
pinMode(OC1A_PIN, OUTPUT); | |
pinMode(OC1B_PIN, OUTPUT); | |
pinMode(OC1C_PIN, OUTPUT); | |
// Clear Timer1 control and count registers | |
TCCR1A = 0; | |
TCCR1B = 0; | |
TCNT1 = 0; | |
// Set Timer1 configuration | |
// COM1A(1:0) = 0b10 (Output A clear rising/set falling) | |
// COM1B(1:0) = 0b10 (Output B clear rising/set falling) | |
// COM1C(1:0) = 0b10 (Output C clear rising/set falling) | |
// WGM(13:10) = 0b1010 (Phase correct PWM) | |
// ICNC1 = 0b0 (Input capture noise canceler disabled) | |
// ICES1 = 0b0 (Input capture edge select disabled) | |
// CS(12:10) = 0b001 (Input clock select = clock/1) | |
TCCR1A |= (1 << COM1C1)| (1 << COM1B1) | (1 << COM1A1) | (1 << WGM11); | |
TCCR1B |= (1 << WGM13) | (1 << CS10); | |
ICR1 = TCNT_TOP; | |
// Set Timer1 configuration | |
// COM3A(1:0) = 0b10 (Output A clear rising/set falling) | |
// WGM(13:10) = 0b1010 (Phase correct PWM) | |
// ICNC1 = 0b0 (Input capture noise canceler disabled) | |
// ICES1 = 0b0 (Input capture edge select disabled) | |
// CS(12:10) = 0b001 (Input clock select = clock/1) | |
// set all PWM pins to 0 first | |
setPwmDuty(0, 0); | |
setPwmDuty(0, 1); | |
setPwmDuty(0, 2); | |
// setup ISR | |
pinMode(OC1A_READ_PIN, INPUT_PULLUP); | |
pinMode(OC1B_READ_PIN, INPUT_PULLUP); //INPUT_PULLUP | |
pinMode(OC1C_READ_PIN, INPUT_PULLUP); | |
// start serial | |
Serial.begin(115200); | |
// reset Timer 3 | |
TCCR3A = 0; | |
TCCR3B = 0; | |
// Timer 3 - interrupt on overflow | |
TIMSK3 = bit (TOIE3); // enable Timer3 Interrupt | |
// zero it | |
TCNT3 = 0; | |
overflowCount = 0; | |
// start Timer 3 | |
TCCR3B = bit (CS30); // no prescaling | |
for (uint8_t i = 0; i < 3; i++) { | |
prepareForInterrupt(fanConfig[i]); | |
} | |
} | |
// timer overflows (every 65536 counts), increment overflow every time | |
ISR(TIMER3_OVF_vect) { | |
overflowCount++; | |
} // end of TIMER1_OVF_vect | |
void loop() { | |
checkFreq(); | |
handleSerial(); | |
} | |
void handleSerial() { | |
while (Serial.available()) { | |
int x = Serial.readBytes(message, 2); | |
if (x < 2) return; | |
// message[0] = hi byte (command) | |
// message[1] = lo byte (data) | |
byte cntrlCmd = message[0] & CMD_MASK; | |
byte fanNumber = (message[0] & FAN_MASK) >> 6; | |
if (cntrlCmd == 1) { | |
byte pwmLevel = constrain(message[1], 0, 100); | |
setPwmDuty(pwmLevel, fanNumber); | |
byte newMessage[2] = {message[0], pwmLevel}; | |
Serial.write(newMessage, 2); // echo result with constraints added | |
} else if (cntrlCmd == 2) { | |
byte newMessage[2] = {message[0], 0}; | |
switch(fanNumber) { | |
case 0: | |
newMessage[1] = fanConfig[0].pulses; | |
break; | |
case 1: | |
newMessage[1] = fanConfig[1].pulses; | |
break; | |
case 2: | |
newMessage[1] = fanConfig[2].pulses; | |
break; | |
} | |
Serial.write(newMessage, 2); | |
} else if (cntrlCmd == 3) { | |
byte pwmLevel = constrain(message[1], 0, 100); | |
for (auto fanNum : ALL_FANS) { | |
setPwmDuty(pwmLevel, (byte)fanNum); | |
} | |
byte newMessage[2] = {message[0], pwmLevel}; | |
Serial.write(newMessage, 2); // echo result with constraints added | |
} else if (cntrlCmd == 4) { | |
byte newMessage[4] = {message[0], fanConfig[0].pulses, fanConfig[1].pulses, fanConfig[2].pulses}; | |
Serial.write(newMessage, 4); | |
} | |
Serial.flush(); | |
} | |
} | |
void setPwmDuty(byte duty, byte fanNum) { | |
switch(fanNum) { | |
case 0: | |
OCR1A = (word) (duty*TCNT_TOP)/100; | |
break; | |
case 1: | |
OCR1B = (word) (duty*TCNT_TOP)/100; | |
break; | |
case 2: | |
OCR1C = (word) (duty*TCNT_TOP)/100; | |
break; | |
default: | |
break; | |
} | |
} | |
// This interrupt checks for the current count of Timer3, records it down | |
// and adds that value to the overflows. We need to multiply the overflows | |
// by 2^16 since Timer3 is a 16-bit timer, and it resets to zero after | |
// 2^16 - 1. If it's recording the initial trigger, it will initial | |
// count to startTime. If it's triggered again, it will save the value | |
// of Timer3 now to finishTime and set the triggered flag so that it | |
// won't service this interrupt again next time until all the values | |
// have been read and all flags have been reset. | |
void myInterruptHandler(uint8_t pin) { | |
unsigned int counter = TCNT3; // quickly save it | |
for (uint8_t i = 0; i < 3; i++) { | |
if (fanConfig[i].pinNumber != pin) | |
continue; | |
if (fanConfig[i].triggered) | |
return; | |
if (fanConfig[i].first) { | |
fanConfig[i].startTime = (overflowCount << 16) + counter; | |
fanConfig[i].first = false; | |
return; | |
} | |
fanConfig[i].finishTime = (overflowCount << 16) + counter; | |
fanConfig[i].triggered = true; | |
detachInterrupt(digitalPinToInterrupt(pin)); | |
} | |
} |
Author
jayveeangeles
commented
Oct 1, 2020
•
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment