Fan-controller for the APS 3005D bench power supply

Like for the Lavolta power supply, I wanted to build a simple fan-controller circuit. Actually, I built this one first, and unlike the other one, which used the ATtiny85, I decided to build this one with a ATtiny13a. Because of this, the library for the DS18b20 digital sensor was too big to fit into the memory of the ATtiny13a. So I decided to use a TMP36 analog temperature sensor, since the controller already has a 10 Bit ADC and there is no need for another library.

This bench power supply actually came with a simple but working fan “controller”: The manufacturer ships these devices with a temperature switch, that turns the fan on, when it reaches a certain temperature (and off, if it drops below). There is nothing wrong with that! Still, I decided to go for the more complicated solution, since the switch can only turn the fan fully on or completely off, there is no in-between.

This is a temperature switch, like the one the device came with. These switches come with different fixed temperature settings and there are “normally closed” and “normally open” types.

The basic setup is very simple: The micro-controller directly controls the base of a TIP120 Darlington transistor, which switches the 24 V power for the fan (using PWM). I know that a MOSFET would have been more efficient, but since the fan runs with 24 V, the voltage drop doesn’t matter that much. The LM7805 voltage regulator powers the micro-controller and the sensor (24 V to 5 V DC). Because the fan is a 24 V type, there was no need to use a buck converter or linear regulator.

The layout on the perfboard I used. The ADC uses pin 3 (TMP36 sensor). Pin 6 controls the Darlington transistor (PWM). Both button and LED are only for debugging purposes, since no one will see or touch them when the case is closed.
The built board including all components but the sensor.
Mounted inside the case – without the fan. The grey/white/black wire is the TMP36 temperature sensor. The (empty) connector below the TIP120 is for the 24 V DC fan.
Mounted inside the case with fan and heatsink.
The fan that came with the device. It turned out to be working very well with a PWM’ed power supply.

Below you’ll find the source code written using avr-libc and avr-gcc for compiling it.

#ifndef __AVR_ATtiny13A__
#define __AVR_ATtiny13A__
#endif

#include <avr/io.h>
#include <avr/interrupt.h>

#define P_LED PB0
#define PORT_LED PORTB0
#define DD_LED DDB0

#define P_FANPWM PB1
#define DD_FANPWM DDB1

#define P_BUTTON PB2
#define PORT_BUTTON PORTB2
#define PIN_BUTTON PINB2

#define P_TMP36 PB4
#define ADC_TMP36 ADC2

/*
 * Pin 1 - Reset
 * Pin 2 - N/A
 * Pin 3 - [ADC2] TMP36 Sensor
 * Pin 4 - GND
 * Pin 5 - [PB0] LED
 * Pin 6 - [OC0B] PWM Lüfter
 * Pin 7 - [PB2] Taster
 * Pin 8 - VCC
*/

const uint8_t PWM_BOTTOM = 0;
const uint8_t PWM_TOP = 47;  // 48 Werte/Stufen (0-47)
const uint8_t FAN_MINIMUM = 10;  // Achtung: Lüfter läuft erst bei 7 an!
const int8_t TEMPERATURE_OFFSET = 0;  // Ausgleichwert für Temperatur
const uint8_t VREF = 5;

uint16_t volatile to_counter;  // Zähler für Timer-Overflows
uint8_t seconds_counter;

uint16_t volatile adc_raw;  // Letzer gemessener ADC-Wert

int8_t temperature_1s_ago;
int8_t temperature_1m_ago;


// Timer Overflow Interrupt ISR
// Aufruf der ISR setzt das TOV0 Flag im TIFR Register zurück.
ISR(TIM0_OVF_vect) {
  to_counter++;
}

// ADC Conversion Complete Interrupt ISR
// Aufruf der ISR setzt das ADIF Flag im ADCSRA Register zurück.
ISR(ADC_vect) {
  adc_raw = ADC;
  // Nächste Messung startet automatisch (Free Running Mode)
}

void setupTimer0(void) {
  // Register TCCR0A/B initialisieren
  // Fast-PWM Modus wählen (TOP = OCR0A)
  // OCR0B entspricht Duty-Cycle
  TCCR0A |= (1 << COM0B1) | (0 << COM0B0) | (1 << WGM01) | (1 << WGM00);

  // Counter initialisieren bzw. festlegen
  OCR0A = PWM_TOP;
  OCR0B = PWM_BOTTOM;  // Duty-Cycle

  // Prescaler auf 8 setzen
  // Formel für Fast-PWM-Frequenz: F_CPU / (Prescaler * 256)
  // 9,6 MHz: 9.600.000 / (8 * 48) = 25.000 Hz --> 25 kHz
  TCCR0B |= (1 << WGM02) | (0 << CS02) | (1 << CS01) | (0 << CS00);

  // Timer Overflow Interrupt einschalten
  // Erhöht Zählervariable
  TIMSK0 |= (1 << TOIE0);
}

void setupADC(void) {
  // ADC Multiplexer Selection Register
  // REFS0: VCC als Referenzspannung (Standard).
  // ADLAR: Left adjust ausschalten (siehe unten!)
  // MUX[1:0]: ADC2 (PB4, Pin 3) als analogen Eingang festlegen.
  ADMUX |= (0 << REFS0) | (0 << ADLAR) | (1 << MUX1) | (0 << MUX0);

  // Digitalen Input für analogen Pin abschalten
  DIDR0 |= (1 << ADC2D);

  // ADC Control and Status Register A
  // ADEN: ADC einschalten
  // ADSC: Einzelne Messung starten
  // ADIE: ADC Conversion Complete Interrupt einschalten
  // ADPS[2:0]: ADC Prescaler auf 64 (Ziel: zwischen 50 und 200 kHz)
  // 9,6 MHz: 9.600.000 Hz / 64 = 150.000 Hz --> 150 kHz
  ADCSRA |= (1 << ADEN) | (1 << ADSC) | (1 << ADIE) | (1 << ADPS2) | (1 << ADPS1) | (0 << ADPS0);

  // Ergebnis wird in ADCL und ADCH (ADC Low Byte und ADC High Byte)
  // gespeichert, da 10 Bit Wert. ADC enthält vollständigen Wert.
  while (ADCSRA & (1 << ADSC));  // Ergebnis abwarten...
  (void) (ADC);  // Erste Messung verwerfen (lt. Datenblatt empfohlen)
}

int8_t calculateTemperature(uint16_t adc_value) {
  // TMP36 Sensor: 10 mV entsprechen 1 °C (linearer)
  // Gemessener Wert wird zunächst auf die 5 V Referenz bezogen,
  // danach 0,5 V abgezogen, geteilt und das Ergebnis ganzzahlig gerunded.
  uint16_t voltage = ((uint32_t) adc_value) * VREF * 1000 / 1024;
  voltage -= 500;  // TMP36 Voltage Offset abziehen
  int8_t temperature = (voltage + 5) / 10;
  temperature += TEMPERATURE_OFFSET;
  return temperature;
}

void delayTOs(uint16_t overflows) {
  uint8_t sreg_before = SREG;  // Interrupt Status (ein/aus) speichern
  cli();
  uint16_t to_counter_before = to_counter;  // Zählerstand speichern
  to_counter = 0;
  sei();
  while (to_counter < overflows) {}  // Warten...
  cli();
  to_counter = to_counter_before;  // Zählerstand wiederherstellen
  SREG = sreg_before;  // Interrupts auf alten Zustand setzen
}

int main(void) {
  DDRB |= (1 << DD_FANPWM) | (1 << DD_LED);
  PORTB |= (1 << PORTB3);  // Unbenutzen Pin 2 Pull-up einschalten
  //PORTB |= (1 << PORT_BUTTON);  // Taster Pull-up ein (externer Pullup 10k)

  setupADC();
  setupTimer0();  // PWM, Timer-Overflow-Counter

  // Einmalige Messung zur Initialisierung der globalen Temperaturvariablen
  ADCSRA |= (1 << ADSC);
  while (ADCSRA & (1 << ADSC));  // Ergebnis abwarten...
  temperature_1s_ago = calculateTemperature(ADC);
  temperature_1m_ago = temperature_1s_ago;

  // ADC in Free Running Mode versetzen und erste Messung starten
  ADCSRA |= (1 << ADSC) | (1 << ADATE);

  to_counter = 0;
  seconds_counter = 0;

  sei();  // Interrupts einschalten

  while (1) {

    // Wenn eine Sekunde vergangen ist... (Regelung)
    if (to_counter > ((uint16_t) 25 * 1000)) {
      cli();

      PORTB ^= (1 << PORT_LED);  // LED umschalten (Aktivitätsanzeige)

      int8_t temperature = calculateTemperature(adc_raw);  // ADC-Wert verarbeiten

      // Berechnung des Temperaturwerts für PWM mit Gewichtung
      int16_t tv = temperature_1m_ago * 15;
      tv += temperature_1s_ago * 4;
      tv += temperature;
      tv = (tv + 5) / 20;

      // Achtung: Lüfter läuft erst bei einem PWM Duty-Cycle von 7 an!
      if (tv <= 23) {
        OCR0B = PWM_BOTTOM;  // Lüfter aus
      } else if (tv == 24) {
          OCR0B = FAN_MINIMUM;  // Minimale Drehzahl des Lüfters
      } else if (tv == 25) {
          OCR0B = 12;
      } else if (tv == 26) {
          OCR0B = 15;
      } else if (tv == 27) {
          OCR0B = 19;
      } else if (tv == 28) {
          OCR0B = 23;
      } else if (tv == 29) {
          OCR0B = 27;
      } else if (tv == 30) {
          OCR0B = 30;
      } else if (tv == 31) {
          OCR0B = 33;
      } else if (tv == 34) {
          OCR0B = 36;
      } else if (tv == 37) {
          OCR0B = 39;
      } else if (tv == 40) {
          OCR0B = 42;
      } else if (tv == 43) {
          OCR0B = 45;
      } else {
        OCR0B = PWM_TOP;
      }

      // Sicherstellen, dass der ADC läuft
      if (!(ADCSRA & (1 << ADSC))) {
        ADCSRA |= (1 << ADSC) | (1 << ADATE);
      }

      // Wenn eine Minute vergangen ist...
      if (++seconds_counter >= 60) {
        seconds_counter = 0;
        temperature_1m_ago = temperature;
      }

      temperature_1s_ago = temperature;
      to_counter = 0;
      sei();
    }

    // Wenn der Taster gedrückt wird... (Testlauf starten)
    if (!(PINB & (1 << PIN_BUTTON))) {
      cli();
      PORTB |= (1 << PORT_LED);  // LED ein
      OCR0B = PWM_BOTTOM;
      delayTOs((uint16_t) 50 * 1000);
      for (uint8_t i = PWM_BOTTOM; i < PWM_TOP; i++) {  // Hochdrehen
        OCR0B = i;
        delayTOs(5000);
      }
      OCR0B = PWM_TOP;
      delayTOs((uint16_t) 50 * 1000);
      for (uint8_t i = PWM_TOP; i > PWM_BOTTOM; i--) {  // Runterdrehen
        OCR0B = i;
        delayTOs(5000);
      }
      PORTB &= ~(1 << PORT_LED);  // LED aus
      sei();
    }
  }
}

Leave a Reply

Your email address will not be published. Required fields are marked *