Fan-controller for the Lavolta BPS305 bench power supply

The Lavolta BPS305 is a cheap bench power supply of Chinese origin. The device comes with a 24 V DC fan, but without any means to control it in any way. The fan will just run the whole time, which is pretty annoying. Because of that, I decided very build a fan-controller using a microcontroller and a sensor to measure the current temperature in the case.

Perfboard design was made with DIYLC. The board only contains an LM7805 to power the microcontroller. It uses a DS18b20 digital temperature sensor (orange trace) to get the current temperature value and then creates a PWM signal for the 12 V PWM case fan (yellow trace).
The ready built board. The connector on the right side is for the temperature sensor. The connector on the left side is for the PWM signal of the 12V case fan.
This is a cheap adjustable buck converter, which uses the internal 24 V DC of the power supply to switch it down to 12 V, since the fan is a 12 V fan (the only one I could get with a dedicated PWM signal). I used a piece of aluminum to mount the PCB on top of the fan itself.
This is the already mounted fan and fan controller.
The whole board is powered by 12 V DC from the buck converter above the fan.
The yellow/green/blue wire is the DS18b20 temperature sensor, which I placed somewhere near the heatsink of the power-transistors.
Done.

I used the avr-libc and avr-gcc to write and compile the code.
At first I had a more sophisticated solution in mind. But after a while I decided to go with a simpler if-then-else kind of solution. For communicating with the DS18b20 sensor, I used a 3rd party library.

#ifndef __AVR_ATtiny85__
#define __AVR_ATtiny85__
#endif

#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>
#include <stdio.h>

#include "ds18b20/ds18b20.h"

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

#define P_PWM PB1
#define PORT_PWM PORTB1
#define DD_PWM DDB1

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

#define P_TACHO PB3
#define PORT_TACHO PORTB3
#define PIN_TACHO PINB3
#define PCINT_TACHO PCINT3

#define P_DS18B20 PB4
#define PORT_DS18B20 PORTB4
#define PIN_DS18B20 PINB4

/*
 * Pin 1 - Reset
 * Pin 2 - [PB3|PCINT3] Fan tacho signal
 * Pin 3 - [PB4] DS18b20 temperature sensor
 * Pin 4 - GND
 * Pin 5 - [PB0] Status LED
 * Pin 6 - [PB1|OC0B] Fan PWM signal wire
 * Pin 7 - [PB2] Button
 * Pin 8 - VCC
*/

const uint8_t PWM_BOTTOM = 2;  // Lowest PWM value to actually use
const uint8_t PWM_TOP = 39;  // 0-39 ==> 40 values

uint8_t volatile timer1_irq_counter;
uint8_t seconds_counter;

// uint8_t volatile fan_tacho_signals;  // Fan half-revolutions per second
// uint16_t fan_rpm;  // Fan rounds per minute

int16_t temperature_raw;  // Temperature value from sensor
int8_t temperature;  // Current temperature
int8_t temperature_1s_ago;
int8_t temperature_1m_ago;

// Timer1 Compare Match Interrupt ISR
// Call to ISR resets OCF1A flag in the TIFR register.
ISR(TIM1_COMPA_vect) {
  timer1_irq_counter++;
}

void setupTimer0(void) {
  // Set timer to Fast PWM mode (TOP = OCR0A)
  // Non-inverted PWM
  TCCR0A |= (1 << COM0B1) | (0 << COM0B0) | (1 << WGM01) | (1 << WGM00);

  // Init counter variables
  OCR0A = PWM_TOP;
  OCR0B = PWM_BOTTOM;  // Duty-Cycle

  // Set prescaler to 8
  // Formula for Fast PWM mode: F_CPU / (Prescaler * 256)
  // 8,0 MHz: 8.000.000 / (8 * 40) = 25.000 Hz --> 25 kHz
  TCCR0B |= (1 << WGM02) | (0 << CS02) | (1 << CS01) | (0 << CS00);
}

void setupTimer1(void) {
  // Prescaler 4096
  // 8.000.000 Hz / 4096 = 1953,125 Hz
  // 1953,125 Hz / 217 = 9,000576 Compare Match Interrupts per second
  // Clear timer after compare match with OCR1C(!)
  TCCR1 |= (1 << CTC1) | (1 << CS13) | (1 << CS12) | (0 << CS11) | (1 << CS10);
  TCNT1 = 0;
  OCR1A = 217;  // The match with OCR1A will fire the interrupt
  OCR1C = OCR1A;  // The match with OCR1C will reset the timer

  TIMSK |= (1 << OCIE1A);  // Enable compare match interrupt

  // (There is no compare match interrupt for OCR1C and no CTC for OCR1A.)
}

int main(void) {
  DDRB |= (1 << DD_PWM) | (1 << DD_LED);  // Set to output
  // PORTB |= (1 << PORT_BUTTON) | (1 << PORT_TACHO);  // Enable pull-up

  setupTimer0();  // PWM
  setupTimer1();  // Timer for main loop
  // setupTachoRead();

  // Init temperature variables with current temperature
  ds18b20convert(&PORTB, &DDRB, &PINB, (1 << PIN_DS18B20), NULL);
  _delay_ms(1500);
  if (ds18b20read(&PORTB, &DDRB, &PINB, (1 << PIN_DS18B20), NULL, &temperature_raw) == DS18B20_OK) {
    temperature = ((temperature_raw + 8) / 16);
  } else {
    temperature = 40;  // Set fail safe temperature if no/invalid value.
  }
  temperature_1s_ago = temperature;
  temperature_1m_ago = temperature;

  timer1_irq_counter = 0;
  // fan_tacho_signals = 0;
  seconds_counter = 0;

  sei();  // Enable global interrupts

  while(1) {
    if (timer1_irq_counter >= 9) {  // If one second has passed...
      cli();

      PORTB ^= (1 << PORT_LED);  // Toggle LED (user feedback)

      // fan_rpm = (fan_tacho_signals * 60) / 2;
      // fan_tacho_signals = 0;

      // Get temperature from sensor
      static uint8_t conversion_wait_counter = 0;
      if (conversion_wait_counter++ == 0) {  // Start temperature conversion
        ds18b20convert(&PORTB, &DDRB, &PINB, (1 << PIN_DS18B20), NULL);
      } else if (conversion_wait_counter > 4) {
        if (ds18b20read(&PORTB, &DDRB, &PINB, (1 << PIN_DS18B20), NULL, &temperature_raw) == DS18B20_OK) {
          temperature = ((temperature_raw + 8) / 16);
        } else {
          temperature = 40;  // Set fail safe temperature if no/invalid value.
        }
        conversion_wait_counter = 0;
      }

      // Temperature value calculation
      int16_t tv = temperature_1m_ago * 15;
      tv += temperature_1s_ago * 4;
      tv += temperature;
      tv = (tv + 5) / 20;

      if (tv <= 23) {
        OCR0B = PWM_BOTTOM;  // Fan's slowest setting
      } else if (tv == 24) {
          OCR0B = 5;
      } else if (tv == 25) {
          OCR0B = 9;
      } else if (tv == 26) {
          OCR0B = 13;
      } 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 {
        OCR0B = PWM_TOP;
      }

      if (++seconds_counter >= 60) {  // If ~60 seconds have passed...
        seconds_counter = 0;
        temperature_1m_ago = temperature;
      }

      temperature_1s_ago = temperature;

      timer1_irq_counter = 0;
      sei();
    }

    // If the button is pressed: Start a simple fan test
    if (~PINB & (1 << PIN_BUTTON)) {
      cli();
      PORTB |= (1 << PORT_LED);  // LED on
      OCR0B = PWM_BOTTOM;
      _delay_ms(5000);
      for (uint8_t i = PWM_BOTTOM; i < PWM_TOP; i++) {  // Spin up
        OCR0B = i;
        _delay_ms(500);
      }
      OCR0B = PWM_TOP;
      _delay_ms(5000);
      for (uint8_t i = PWM_TOP; i > PWM_BOTTOM; i--) {  // Spin down
        OCR0B = i;
        _delay_ms(500);
      }
      PORTB &= ~(1 << PORT_LED);  // LED off
      sei();
    }
  }
}

Leave a Reply

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