Welcome to Hackster!
Hackster is a community dedicated to learning hardware, from beginner to pro. Join us, it's free!
John Bradnam
Published © GPL3+

NOTUA NV-FS1 Plus Fan

A 3D printed version of a NOTUA NV-FS1 Desktop Fan that displays fan speed, temperature and time.

IntermediateFull instructions provided24 hours384
NOTUA NV-FS1 Plus Fan

Things used in this project

Hardware components

Microchip ATtiny3224
×1
DS1302 RTC
32,768 watch crystal + CR1220 Battery & SMD holder
×1
OLED Display 128x32 0.91 inches with I2C Interface
DIYables OLED Display 128x32 0.91 inches with I2C Interface
×1
XL6009 400KHz 60V 4A Switching Current Boost / Buck-Boost / Inverting DC/DC Converter
See description as there are many variants.
×1
RV09 10K Potentiometer
×1
Tactile Switch, Top Actuated
Tactile Switch, Top Actuated
13mm shaft
×1
Mini 360 DC Buck Converter Step Down Module
×1
G6K-2F-Y Relay
5V SMD
×1
Other components
Resistors (0805): 3x10K 1%, 3x4K7, 1x1K, 1x20K 1%, 1x3K3 1% Capacitors (0805): 3x0.1uF Diodes: 1x1N4148 SOD80C Transistors: 1xBC817
×1
DS18B20 Temperature Sensor 1m
HARDWARIO DS18B20 Temperature Sensor 1m
×1
92mm x 92mm x 25mm 12V Fan
×1

Software apps and online services

Arduino IDE
Arduino IDE

Hand tools and fabrication machines

3D Printer (generic)
3D Printer (generic)
Soldering iron (generic)
Soldering iron (generic)

Story

Read more

Custom parts and enclosures

STL Files

Files for 3D printing

Schematics

Schematic

PCB

Eagle files

Schematic & PCB in Eagle format

Code

NOTua_Desktop_Fan_V2.ino

Arduino
/**************************************************************************
 NOTua Desktop Fan V2

 Concept: numpty-in-training - https://www.thingiverse.com/thing:6752850 - September 03, 2024
 Portions of this code by David Johnson-Davies - http://www.technoblogy.com/show?2G8T

 Schematic & PCB at https://www.hackster.io/john-bradnam/contact-digital-thermometer-ed18d2
 
 2024-10-04 John Bradnam (jbrad2089@gmail.com)
   Create program for ATtiny3224

 --------------------------------------------------------------------------
 Arduino IDE:
 --------------------------------------------------------------------------
  BOARD: ATtiny3224/1614/1604/814/804/414/404/214/204
  Chip: ATtiny3224
  Clock Speed: 20MHz
  millis()/micros(): "Enabled (default timer)"
  Programmer: jtag2updi (megaTinyCore)

  ATTiny1614/3224 Pins mapped to Ardunio Pins
 
              +--------+
          VCC + 1   14 + GND
  (SS)  0 PA4 + 2   13 + PA3 10 (SCK)
        1 PA5 + 3   12 + PA2 9  (MISO)
  (DAC) 2 PA6 + 4   11 + PA1 8  (MOSI)
        3 PA7 + 5   10 + PA0 11 (UPDI)
  (RXD) 4 PB3 + 6    9 + PB0 7  (SCL)
  (TXD) 5 PB2 + 7    8 + PB1 6  (SDA)
              +--------+
  
 **************************************************************************/

#include <OneWire.h>
#include <U8g2lib.h>
#include <TimeLib.h>
#include <DS1302RTC.h>
//#include <EEPROM.h>
#include "Button.h"

#define TEMPERATURE_TIME 15000
#define FAN_TIME 200

#define FAN_PIN 0     //PA4
#define SW_PIN 1      //PA5
#define GND_PIN 2     //PA6
#define EN_PIN 3      //PA7
#define TEMP_PIN 4    //PB3
#define TXD_PIN 5     //PB2
#define SDA_PIN 6     //PB1
#define SCL_PIN 7     //PB0
#define SCLK_PIN 8    //PA1
#define IO_PIN 9      //PA2
#define CS_PIN 10     //PA3

#define TEMP_PORT PORTB
#define TEMP_BM PIN3_bm

U8G2_SSD1306_128X32_UNIVISION_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);
DS1302RTC rtc(CS_PIN, IO_PIN, SCLK_PIN);

//Modes
enum modeEnum { FAN_MODE, HOUR_MODE, MIN_MODE };
modeEnum currentMode = FAN_MODE;
#define FLASH_TIMEOUT 200
unsigned long setFlash;
bool setColon;
int setHour;
int setMinute;

//Define buttons
enum ButtonEnum { STOP_BTN, UP_BTN, DOWN_BTN };
Button stopButton; 
Button upButton; 
Button downButton; 
//Forward references
void stopButtonPressed(void);
void upButtonPressed(void);
void downButtonPressed(void);

// Here you can define your voltagedivider from the boostconverter output
#define REFERENCE_VOLTAGE 5.0
#define R2 20000
#define R3 3300
#define VMAX 19
#define VMIN 4

bool fanEnabled = false;
float lastBoostVoltage = 0.0;
unsigned long fanUpdate = 0;
int lastFanSpeed = 0;


tmElements_t newTime;           //Used to store new time
int lastSeconds = -1;

unsigned long temperatureUpdate = 0;
int currentTemperature = 0;

char line1[32]; 
char line2[32]; 

//--------------------------------------------------------------------
// One Wire Protocol 
//--------------------------------------------------------------------

#define READ_ROM 0x33
#define MATCH_ROM 0x55
#define SKIP_ROM 0xCC
#define CONVERT_T 0x44
#define READ_SCRATCH_PAD 0xBE

// Buffer to read data or ROM code
static union {
  uint8_t DataBytes[9];
  unsigned int DataWords[4];
};

//-------------------------------------------------------------------------
// Initialise Hardware
//-------------------------------------------------------------------------

void setup(void)
{
  pinMode(EN_PIN,OUTPUT);
  digitalWrite(EN_PIN,LOW);
  pinMode(FAN_PIN,INPUT);
  
  OneWireSetup();

  //Initialise Buttons
  stopButton = Button(STOP_BTN, SW_PIN, 600, 699);
  upButton = Button(UP_BTN, SW_PIN, 400, 599);
  upButton.Repeat(upButtonPressed);
  downButton = Button(DOWN_BTN, SW_PIN, 0, 100);
  downButton.Repeat(downButtonPressed);
  
  u8g2.begin();
  u8g2.clearBuffer();         // clear the internal memory
  u8g2.sendBuffer();          // transfer internal memory to the display

  setSyncProvider(rtc.get);          // the function to get the time from the RTC
  if(timeStatus() != timeSet)
  {
    u8g2.drawStr(20,14,"RTC Sync Bad");   // x,y,text
    delay(2000);
    newTime.Year = CalendarYrToTm(2024);
    newTime.Month = 10;
    newTime.Day = 01;
    newTime.Hour = 14;
    newTime.Minute = 53;
    newTime.Second = 0;
    time_t t = makeTime(newTime);
    setTime(t);
    rtc.set(t);
    
    if (rtc.set(t) != 0) 
    {
      u8g2.drawStr(8,29,"Set Time Failed");
      delay(2000);
    }
  }
  
  newTime.Year = CalendarYrToTm(year());
  newTime.Month = month();
  newTime.Day = day();
  newTime.Hour = hour();
  newTime.Minute = minute();
  newTime.Second = second();
  
  showSplashScreen();
  setFlash = millis() + FLASH_TIMEOUT;
  
}

//--------------------------------------------------------------------
// Main program loop
//--------------------------------------------------------------------

void loop(void)
{
  bool force = false;
  
  //Callback will be invoked when up or down button is pressed
  upButton.Pressed();
  downButton.Pressed();

  //Timer for reading temperature
  if (millis() >= temperatureUpdate)
  {
    temperatureUpdate = millis() + TEMPERATURE_TIME;
    currentTemperature = readTemperature();
    force = true;
  }

  //Timer for reading temperature
  if (millis() >= fanUpdate)
  {
    fanUpdate = millis() + FAN_TIME;
    int fanSpeed = readFanSpeed();
    force = force || (lastFanSpeed != fanSpeed);
    lastFanSpeed = fanSpeed;
  }

  //Timer for flashing digits in set time modes
  if (millis() >= setFlash)
  {
    setColon = !setColon;
    setFlash = millis() + FLASH_TIMEOUT;
    force = true;
  }

  //Get short/long press on stop button
  //Long press goes straight to set hour mode
  PressEnum sw = stopButton.TimedPress(true);
  while (force || sw != NOT_DOWN || stopButton.IsDown())
  {
    switch (currentMode)
    {
      case FAN_MODE:
        if (sw == LONG_PRESS)
        {
          currentMode = HOUR_MODE; 
          setColon = true;
          setHour = hour();
          setMinute = minute();
        }
        else if (sw == SHORT_PRESS)
        {
          fanEnabled = !fanEnabled;
          digitalWrite(EN_PIN,(fanEnabled) ? HIGH : LOW);
          updateMainScreen(true);
        }
        else
        {
          updateMainScreen(force);
        }
        break;
        
      case HOUR_MODE:
        refreshTime(setHour, setMinute, setColon, true);
        
        if (sw == SHORT_PRESS)
        {
          currentMode = MIN_MODE; 
          setFlash = millis() + FLASH_TIMEOUT;
        }
        break;
        
      case MIN_MODE:
        refreshTime(setHour, setMinute, true, setColon);
        
        if (sw == SHORT_PRESS)
        {
          currentMode = FAN_MODE; 
          if (newTime.Hour != setHour || newTime.Minute != setMinute)
          {
            newTime.Hour = setHour;
            newTime.Minute = setMinute;
            time_t t = makeTime(newTime);
            setTime(t);
            rtc.set(t);
          }
          updateMainScreen(true);
        }
        break;
    }
    //Disable Stop button
    sw = NOT_DOWN;
    
    //Callback will be invoked when up or down button is pressed
    upButton.Pressed();
    downButton.Pressed();
    force = false;
  }
}  

//--------------------------------------------------------------------
// Show splash screen
//--------------------------------------------------------------------

void showSplashScreen()
{
  u8g2.setFont(u8g2_font_helvB12_tr); // helvetica bold
    
  //Splash screen 1
  u8g2.clearBuffer();
  u8g2.drawStr(16,14,"ATtiny3224");   // x,y,text
  u8g2.drawStr(8,29,"Desktop Fan");
  u8g2.sendBuffer();                // transfer internal memory to the display
  delay(3000);

  updateMainScreen(true);
}

//---------------------------------------------------------------
// Update display every second
//  force - always show time even if 1 second has not passed
//---------------------------------------------------------------

void updateMainScreen(bool force)
{
  force = force || (lastSeconds != second());
  if (force) 
  {
    u8g2.clearBuffer();
    
    //Time
    u8g2.setFont(u8g2_font_helvB24_tr);
    lastSeconds = second();
    int h = hour();
    //h = (h == 0) ? 12 : (h > 12) ? h - 12 : h;
    showTime(h, minute(), true, true, (second() & 0x01));

    //Temperature
    showTemperature(currentTemperature);
    
    //Fan speed
    showFanSpeed();
    
    u8g2.sendBuffer();
  }
}

//---------------------------------------------------------------
// Update display when setting time
//  h - hour
//  m - minute
//  he - hour enable 
//  me - minute enable 
//---------------------------------------------------------------

void refreshTime(int h, int m, bool he, bool me)
{
  u8g2.clearBuffer();
  u8g2.setFont(u8g2_font_inb24_mf);
  showTime(h, m, he, me, true);
  u8g2.sendBuffer();
}

//---------------------------------------------------------------
// Handle UP btton
//---------------------------------------------------------------

void upButtonPressed()
{
  switch(currentMode)
  {
    case HOUR_MODE: 
      setHour = (setHour + 1) % 24;
      refreshTime(setHour, setMinute, true, true);
      break;
      
    case MIN_MODE: 
      setMinute = (setMinute + 1) % 60;
      refreshTime(setHour, setMinute, true, true);
      break;

    default:
      break;
  }
}  

//---------------------------------------------------------------
// Handle UP btton
//---------------------------------------------------------------

void downButtonPressed()
{
  switch(currentMode)
  {
    case HOUR_MODE: 
      setHour = (setHour + 24 - 1) % 24;
      refreshTime(setHour, setMinute, true, true);
      break;
      
    case MIN_MODE: 
      setMinute = (setMinute + 60 - 1) % 60;
      refreshTime(setHour, setMinute, true, true);
      break;

    default:
      break;
  }
}  

//---------------------------------------------------------------
// Update the temperature in the display buffer
// temp - deg in celsius (-1 for err 1, -2 for err 2)
//---------------------------------------------------------------

void showTemperature(int temp)
{
  int width;

  line1[0] = '\0';
  line2[0] = '\0';
  if (temp == -1) 
  {
    strcat(line1,"Er1");
  } 
  else if (temp == -2) 
  {
    strcat(line1,"Er2");
  }
  else
  {
    //int fahrenheit = (temp * 9) / 5 + 32;
    sprintf(line1,"%dC",temp);
  }

  u8g2.setFont(u8g2_font_helvB12_tr); // helvetica bold
  width = u8g2.getStrWidth(line1);
  u8g2.drawStr(128-width,14,line1);
}

//---------------------------------------------------------------
// Update the fan speed in the display buffer
//---------------------------------------------------------------

void showFanSpeed()
{
  int fanSpeed = readFanSpeed();
  if (fanSpeed == -1)
  {
    strcpy(line1,"OFF");
  }
  else
  {
    sprintf(line1,"%d%%",fanSpeed);
  }
  u8g2.setFont(u8g2_font_helvB12_tr); // helvetica bold
  int width = u8g2.getStrWidth(line1);
  u8g2.drawStr(128-width,31,line1);
}

//---------------------------------------------------------------
// Update the time in the display buffer
//  h - hour
//  m - minute
//  he - hour enable 
//  me - minute enable 
//  ce - colon enable 
//---------------------------------------------------------------

void showTime(int h, int m, bool he, bool me, bool ce)
{
  ce = true;
  char c = (ce) ? ':' : ' ';
  if (he)
  {
    if (me)
    {
      sprintf(line1,"%02d%c%02d",h,c,m);
    }
    else
    {
      sprintf(line1,"%02d%c  ",h,c);
    }
  }
  else
  {
    if (me)
    {
      sprintf(line1,"  %c%02d",c,m);
    }
    else
    {
      sprintf(line1,"  %c  ",c);
    }
  }
  
  u8g2.clearBuffer();
  //unsigned int width = u8g2.getStrWidth(line1);
  int height = u8g2.getAscent();
  int x = 0; //max((u8g2.getDisplayWidth() - width) / 2,0);
  int y = u8g2.getDisplayHeight() - max((u8g2.getDisplayHeight() - height) / 2,0);
  u8g2.drawStr(x,y,line1);
  //u8g2.drawFrame(0, 0, u8g2.getDisplayWidth(), u8g2.getDisplayHeight());
  //u8g2.sendBuffer();
}

//---------------------------------------------------------------
// Read the temperature
//  returns Temp in celsious
//          -1 - One wire reset failure
//          -2 - CRC failure
//---------------------------------------------------------------

int readTemperature()
{
  if (OneWireReset() != 0) 
  {
    return -1;
  } 
  else 
  {
    OneWireWrite(SKIP_ROM);
    OneWireWrite(CONVERT_T);
    while (OneWireRead() != 0xFF);
    OneWireReset();
    OneWireWrite(SKIP_ROM);
    OneWireWrite(READ_SCRATCH_PAD);
    OneWireReadBytes(9);
    if (OneWireCRC(9) == 0) 
    {
      int temp = DataWords[0];
      int celsius = (temp+8) >> 4;        // Round to nearest degree
      return celsius;
    } 
    else
    {
      return -2;
    }
  }
}

//---------------------------------------------------------------
// Read the fan speed
//  returns Fan speed as a percentage
//          -1 - Fan off
//---------------------------------------------------------------

int readFanSpeed()
{
  if (fanEnabled)
  {
    // Read the analog input (0 - 1023)
    int analogValue = analogRead(FAN_PIN);     
  
    // Convert the analog reading to a voltage (0 - 3.3V)
    float vIn = analogValue * (REFERENCE_VOLTAGE / 1023.0);  
  
    // Calculate the input voltage using the voltage divider formula
    // use the two const values to give it a litte Low-pass filter
    float voltageBoostconverter = 0.4*(vIn * ((R2 + R3) / R3))+ 0.6 * lastBoostVoltage;
    
    int fanSpeed = mapFanPower(voltageBoostconverter, VMIN, VMAX, 0.0, 100.0);
    lastBoostVoltage = voltageBoostconverter;
    return fanSpeed;
  }
  else
  {
    return -1;
  }
}

// calculate the fan speed in %
int mapFanPower(float x, float in_min, float in_max, float out_min, float out_max) 
{
  int result = (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
  if( result < out_min) 
  {
    result = out_min;
  }
  if (result > out_max) 
  {
    result = out_max;
  }
  return result;
}

//--------------------------------------------------------------------
// One Wire Protocol 
//--------------------------------------------------------------------

inline void PinLow () 
{
  TEMP_PORT.DIRSET = TEMP_BM;
  TEMP_PORT.OUTCLR = TEMP_BM;
}

inline void PinRelease () 
{
  TEMP_PORT.DIRCLR = TEMP_BM;
}

// Returns 0 or 1
inline uint8_t PinRead () 
{
  return (TEMP_PORT.IN & TEMP_BM) ? 1 : 0;
}

void DelayMicros(unsigned int micro) 
{
  delayMicroseconds(micro);
}

void LowRelease(int low, int high) 
{
  PinLow();
  DelayMicros(low);
  PinRelease();
  DelayMicros(high);
}

void OneWireSetup() 
{
}

uint8_t OneWireReset() 
{
  uint8_t data = 1;
  LowRelease(480, 70);
  data = PinRead();
  DelayMicros(410);
  return data;                            // 0 = device present
}

void OneWireWrite(uint8_t data) 
{
  int del;
  for (int i = 0; i<8; i++) {
    if ((data & 1) == 1) del = 6; else del = 60;
    LowRelease(del, 70 - del);
    data = data >> 1;
  }
}

uint8_t OneWireRead () 
{
  uint8_t data = 0;
  for (int i = 0; i<8; i++) 
  {
    LowRelease(6, 9);
    data = data | PinRead()<<i;
    DelayMicros(55);
  }
  return data;
}

// Read bytes into array, least significant byte first
void OneWireReadBytes (int bytes) 
{
  for (int i=0; i<bytes; i++) 
  {
    DataBytes[i] = OneWireRead();
  }
}

// Calculate CRC over buffer - 0x00 is correct
uint8_t OneWireCRC (int bytes) 
{
  uint8_t crc = 0;
  for (int j=0; j<bytes; j++) 
  {
    crc = crc ^ DataBytes[j];
    for (int i=0; i<8; i++) crc = crc>>1 ^ ((crc & 1) ? 0x8c : 0);
  }
  return crc;
}

Button.h

C Header File
/*
Class: Button
Author: John Bradnam (jbrad2089@gmail.com)
Purpose: Arduino library to handle buttons
*/
#pragma once
#include "Arduino.h"

#define DEBOUNCE_DELAY 10

//Repeat speed
#define REPEAT_START_SPEED 500
#define REPEAT_INCREASE_SPEED 50
#define REPEAT_MAX_SPEED 50

enum PressEnum { NOT_DOWN, SHORT_PRESS, LONG_PRESS };
#define LONG_PRESS_TIME 2000

class Button
{
	public:
	//Simple constructor
  Button();
	Button(int name, int pin);
	Button(int name, int pin, int analogLow, int analogHigh, bool activeLow = true);

  //Background function called when in a wait or repeat loop
  void Background(void (*pBackgroundFunction)());
	//Repeat function called when button is pressed
  void Repeat(void (*pRepeatFunction)());
	//Test if button is pressed
	bool IsDown(void);
	//Test whether button is pressed and released
	//Will call repeat function if one is provided
	bool Pressed();
  //Test whether button is pressed and released
  //Will not call repeat function and returns NOT_DOWN, SHORT_PRESS, LONG_PRESS
  PressEnum TimedPress(bool stopWhenLong);
	//Return button state (HIGH or LOW) - LOW = Pressed
	int State();
  //Return button name
  int Name();

	private:
		int _name;
		int _pin;
		bool _range;
		int _low;
		int _high;
		bool _activeLow;
		void (*_repeatCallback)(void);
		void (*_backgroundCallback)(void);
};

Button.cpp

C/C++
/*
Class: Button
Author: John Bradnam (jbrad2089@gmail.com)
Purpose: Arduino library to handle buttons
*/
#include "Button.h"

Button::Button()
{
}

Button::Button(int name, int pin)
{
	_name = name;
	_pin = pin;
	_range = false;
	_low = 0;
	_high = 0;
  _backgroundCallback = NULL;
  _repeatCallback = NULL;
	pinMode(_pin, INPUT);
}

Button::Button(int name, int pin, int analogLow, int analogHigh, bool activeLow)
{
	_name = name;
	_pin = pin;
	_range = true;
	_low = analogLow;
	_high = analogHigh;
	_activeLow = activeLow;
  _backgroundCallback = NULL;
  _repeatCallback = NULL;
	pinMode(_pin, INPUT);
}

//Set function to invoke in a delay or repeat loop
void Button::Background(void (*pBackgroundFunction)())
{
  _backgroundCallback = pBackgroundFunction;
}

//Set function to invoke if repeat system required
void Button::Repeat(void (*pRepeatFunction)())
{
  _repeatCallback = pRepeatFunction;
}

  bool Button::IsDown()
{
	if (_range)
	{
		int value = analogRead(_pin);
		return (value >= _low && value < _high);
	}
	else
	{
		return (digitalRead(_pin) == LOW);
	}
}

//Tests if a button is pressed and released
//  stopWhenLong - True to return as soon as long press encountered
//  returns NOT_DOWN, SHORT_PRESS, LONG_PRESS
//  no repeat callback invoked
PressEnum Button::TimedPress(bool stopWhenLong)
{
  PressEnum pressed = NOT_DOWN;
  if (IsDown())
  {
    unsigned long start = millis();
    unsigned long wait = millis() + DEBOUNCE_DELAY;
    while (millis() < wait)
    {
      if (_backgroundCallback != NULL)
      {
        _backgroundCallback();
      }
    }
    if (IsDown())
    {
      while (IsDown() && (!stopWhenLong || ((millis() - start) < LONG_PRESS_TIME)))
      {
        if (_backgroundCallback != NULL)
        {
          _backgroundCallback();
        }
      }
      pressed = ((millis() - start) >= LONG_PRESS_TIME) ? LONG_PRESS : SHORT_PRESS;
    }
  }
  return pressed;
}

//Tests if a button is pressed and released
//  returns true if the button was pressed and released
//	if repeat callback supplied, the callback is called while the key is pressed
bool Button::Pressed()
{
  bool pressed = false;
  if (IsDown())
  {
    unsigned long wait = millis() + DEBOUNCE_DELAY;
    while (millis() < wait)
    {
      if (_backgroundCallback != NULL)
      {
        _backgroundCallback();
      }
    }
    if (IsDown())
    {
  	  //Set up for repeat loop
  	  if (_repeatCallback != NULL)
  	  {
  	    _repeatCallback();
  	  }
  	  unsigned long speed = REPEAT_START_SPEED;
  	  unsigned long time = millis() + speed;
      while (IsDown())
      {
        if (_backgroundCallback != NULL)
        {
          _backgroundCallback();
        }
    		if (_repeatCallback != NULL && millis() >= time)
    		{
    		  _repeatCallback();
    		  unsigned long faster = speed - REPEAT_INCREASE_SPEED;
    		  if (faster >= REPEAT_MAX_SPEED)
    		  {
    			  speed = faster;
    		  }
    		  time = millis() + speed;
    		}
      }
      pressed = true;
    }
  }
  return pressed;
}

//Return current button state
int Button::State()
{
	if (_range)
	{
		int value = analogRead(_pin);
		if (_activeLow)
		{
			return (value >= _low && value < _high) ? LOW : HIGH;
		}
		else 
		{
			return (value >= _low && value < _high) ? HIGH : LOW;
		}
	}
	else
	{
		return digitalRead(_pin);
	}
}

//Return current button name
int Button::Name()
{
	return _name;
}

Credits

John Bradnam
151 projects • 194 followers
Contact

Comments

Please log in or sign up to comment.