Skip to content

Instantly share code, notes, and snippets.

@jayveeangeles
Last active October 16, 2020 04:39
Show Gist options
  • Save jayveeangeles/ef40b77e291d6823ed64372bf429d64f to your computer and use it in GitHub Desktop.
Save jayveeangeles/ef40b77e291d6823ed64372bf429d64f to your computer and use it in GitHub Desktop.
Sample PC Fan Controller (Arduino Leonardo)
// 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));
}
}
@jayveeangeles
Copy link
Author

jayveeangeles commented Oct 1, 2020

fancontroller_bb

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment