John Bradnam
Published © GPL3+

UV Exposure Box

A UV Exposure Box to expose photoresist or solder masks for single and double sided boards.

IntermediateFull instructions provided2 days678
UV Exposure Box

Things used in this project

Hardware components

LED (generic)
LED (generic)
5mm Ultraviolet
×168
LED (generic)
LED (generic)
5mm Red
×66
Microchip ATtiny1614 microprocessor
×1
MAX7219
SOIC
×1
LM1117-5
5V Regulator SOT-223
×1
1N4148 – General Purpose Fast Switching
1N4148 – General Purpose Fast Switching
SOD80C
×1
MMTB2222
SOT-23
×1
Omron G6H-2F Relay
5V DPDT 10Pin SMD
×1
4-Digit 7-Segment CC 0.56in Display
×1
PTS 645 Series Switch
C&K Switches PTS 645 Series Switch
12mm shaft
×4
Passive components
Resistors 0805 SMD - 2 x 22k, 1 x 33k, 2 x 39k, 1 x 1k, 56 x 91R, 18 x 220R, 6 x 330R Capacitors 0805 SMD - 2 x 0.1uF
×1

Software apps and online services

Arduino IDE
Arduino IDE

Hand tools and fabrication machines

Soldering iron (generic)
Soldering iron (generic)

Story

Read more

Schematics

Schematic - Timer

Schematic - LEDs

2 Required.

PCB - Timer

PCB - LEDs

2 Required

Eagle Files

Schematics and PCB layouts in Eagle Format

Code

UV_Light_Box_V1.ino

C/C++
// UV Light Box
// Based on a light box design by ELECTROBOB (https://www.electrobob.com/uv-exposure-box-part-1-the-box/)
//
// 
// V1: jlb (jbrad2089@gmail.com)
//  - Wrote software for a ATtiny1614 and MAX7219 driving a 4-Digit 7 Segment CC Display
//

/**
 * ATTiny1614 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 <EEPROM.h>
#include "LedControl.h"
#include "button.h"

//IO Pins
#define MAX7219_DATA 0 //PA4
#define MAX7219_CLK 3  //PA7
#define MAX7219_LOAD 2 //PA6
#define SWITCHES 10    //PA3
#define LEDS 9         //PA2
#define LID 8          //PA1

//LED order 0,7,3,2
int8_t digits[] = {0, 7, 3, 2};

#define SETUP_FLASH_RATE 200;
unsigned long setupTimeout;
bool setupDisplayState = false;

#define COLON_FLASH_RATE 500;
unsigned long colonTimeout;
bool colonDisplayState = false;

uint8_t timerMinutes;
uint8_t timerSeconds;
uint8_t timerBrightness;
bool timerRunning = false;
bool timerUpdated = false;

//EEPROM handling
#define EEPROM_ADDRESS 0
#define EEPROM_MAGIC 0x0BAD0DAD
typedef struct {
  uint32_t magic;
  uint8_t minutes = 0;
  uint8_t seconds = 0;
  uint8_t brightness = 4;
} EEPROM_DATA;

EEPROM_DATA EepromData;       //Current EEPROM settings

enum MenuModesEnum { TIMER, MINUTE_SET, SECOND_SET, BRIGHT_SET };
MenuModesEnum menuMode = TIMER;

LedControl lc=LedControl(MAX7219_DATA,MAX7219_CLK,MAX7219_LOAD,1);

enum buttonEnum { BTN_NONE, BTN_MENU, BTN_UP, BTN_DOWN, BTN_START, BTN_LID };

void menuButtonPressed(void);
void upButtonPressed(void);
void downButtonPressed(void);
void startButtonPressed(void);
void lidButtonRaised(void);

Button* menuButton;
Button* upButton;
Button* downButton;
Button* startButton;
Button* lidButton;

//---------------------------------------------------------------
// Initialise Hardware
void setup() 
{
  pinMode(LEDS, OUTPUT);
  digitalWrite(LEDS, LOW);

  //Eprom
  readEepromData();
  timerRunning = false;
  timerMinutes = EepromData.minutes;
  timerSeconds = EepromData.seconds;
  timerBrightness = EepromData.brightness; 

  //Initialise buttons
  menuButton = new Button(BTN_MENU, SWITCHES, 0, 100, false);
  upButton = new Button(BTN_UP, SWITCHES, 570, 669, false);
  upButton->Repeat(upButtonPressed);
  downButton = new Button(BTN_DOWN, SWITCHES, 460, 569, false);
  downButton->Repeat(downButtonPressed);
  startButton = new Button(BTN_START, SWITCHES, 670, 750, false);
  lidButton = new Button(BTN_LID, LID);
  
  lc.shutdown(0,false);
  lc.setIntensity(0, timerBrightness);              //1 to 15
  lc.clearDisplay(0);

  // Initialize RTC
  while (RTC.STATUS > 0);                           // Wait until registers synchronized
  
  RTC.PER = 1023;                                   // Set period 1 second
  RTC.CLKSEL = RTC_CLKSEL_INT32K_gc;                // 32.768kHz Internal Oscillator  
  RTC.INTCTRL = RTC_OVF_bm;                         // Enable overflow interrupt
  RTC.CTRLA = RTC_PRESCALER_DIV32_gc | RTC_RTCEN_bm;// Prescaler /32 and enable

  //Display current time
  menuMode = TIMER;
  updateDisplay(true);
}

//---------------------------------------------------------------
// RTC Interrupt Handler
ISR(RTC_CNT_vect)
{
  if (timerRunning)
  {
    if (timerSeconds > 0)
    {
      timerSeconds--;
    }
    else if (timerMinutes > 0)
    {
      timerSeconds = 59;
      timerMinutes--;
    }
    timerUpdated = true;
  }
  RTC.INTFLAGS = RTC_OVF_bm;                         // Reset overflow interrupt
}

//---------------------------------------------------------------
// Main program loop
void loop() 
{
  testButtons();

  if (menuMode == TIMER && timerRunning)
  {
    if (millis() > colonTimeout)
    {
      colonDisplayState = !colonDisplayState;
      colonTimeout = millis() + COLON_FLASH_RATE;
      timerUpdated = true;
    }
    if (timerUpdated)
    {
      timerUpdated = false;
      if (timerMinutes == 0 && timerSeconds == 0)
      {
        //Turn off light and stop timer (same as pressing start button to pause)
        startButtonPressed();
      }
      else
      {
        //Display updated time (flashing colon if necessary)
        displayMinutes(timerMinutes, true, colonDisplayState);
        displaySeconds(timerSeconds, true);
      }
    }
  }
  else if (menuMode != TIMER)
  {
    updateDisplay(false);
  }
   
  delay(100);
}

//---------------------------------------------------------------
//Test if any buttons have been pressed
void testButtons()
{
  //Single press buttons
  if (menuButton->Pressed())
  {
    menuButtonPressed();
  }
  if ((lidButton->State() == LOW && timerRunning) || startButton->Pressed())
  {
    //Lid has been openned or start button has been pressed
    startButtonPressed();
  }
  
  //Don't need to check result of pressed since the button handler will invoke its repeat function
  upButton->Pressed();
  downButton->Pressed();
}

//---------------------------------------------------------------
//Handle Menu btton
void menuButtonPressed()
{
  if (menuMode != TIMER || !timerRunning)
  {
    menuMode = (menuMode == BRIGHT_SET) ? TIMER : (MenuModesEnum)((int)menuMode + 1);

    switch (menuMode)
    {
      case MINUTE_SET:
        updateDisplay(false);
        break;

      case SECOND_SET:
        updateDisplay(false);
        break;
        
      case BRIGHT_SET:
        updateDisplay(false);
        break;

      case TIMER:
        //Update EEPROM if any changes
        if (timerMinutes != EepromData.minutes || timerSeconds != EepromData.seconds || timerBrightness != EepromData.brightness)
        {
          EepromData.minutes = timerMinutes;
          EepromData.seconds = timerSeconds;
          EepromData.brightness = timerBrightness; 
          writeEepromData();
        }
        
        //Start in pause mode
        timerRunning = false;
        displayMinutes(timerMinutes, true, true);
        displaySeconds(timerSeconds, true);
        //Now wait for start button
        break;
    }
  }
}

//---------------------------------------------------------------
//Handle Menu btton
void startButtonPressed()
{
  if (menuMode == TIMER)
  {
    if (timerRunning || (timerMinutes == 0 && timerSeconds == 0))
    {
      //Pause the timer
      digitalWrite(LEDS, LOW);
      timerRunning = false;
      displayMinutes(timerMinutes, true, true);
      displaySeconds(timerSeconds, true);
    }
    else if (lidButton->State() != LOW)  //Don't start if lid open
    {
      //Timer runs continiously so just turn on light
      digitalWrite(LEDS, HIGH);
      timerRunning = true;
    }
  }
}

//---------------------------------------------------------------
//Handle DOWN btton
void downButtonPressed()
{
  switch(menuMode)
  {
    case MINUTE_SET:
      timerMinutes = (timerMinutes > 0) ? timerMinutes - 1 : 0; 
      break;
    
    case SECOND_SET: 
      timerSeconds = (timerSeconds > 0) ? timerSeconds - 1 : 0; 
      break;
      
    case BRIGHT_SET: 
      timerBrightness = (timerBrightness > 1) ? timerBrightness - 1 : 1; 
      lc.setIntensity(0, timerBrightness);   //1 to 15
      break;
  }
  updateDisplay(true);
}

//---------------------------------------------------------------
//Handle UP btton
void upButtonPressed()
{
  switch(menuMode)
  {
    case MINUTE_SET:
      timerMinutes = (timerMinutes < 59) ? timerMinutes + 1 : 59; 
      break;
    
    case SECOND_SET: 
      timerSeconds = (timerSeconds < 59) ? timerSeconds + 1 : 59; 
      break;
      
    case BRIGHT_SET: 
      timerBrightness = (timerBrightness < 15) ? timerBrightness + 1 : 15; 
      lc.setIntensity(0, timerBrightness);   //1 to 15
      break;
  }
  updateDisplay(true);
}

//---------------------------------------------------------------
//Flash the value being changed
void updateDisplay(bool force)
{
  setupDisplayState = setupDisplayState | force;
  force = force || (millis() > setupTimeout);
  if (force) 
  {
    setupTimeout = millis() + SETUP_FLASH_RATE;
    bool on = setupDisplayState;
    setupDisplayState = !setupDisplayState;

    if (menuMode == BRIGHT_SET)
    {
      displayBrightness(timerBrightness, on);
    }
    else
    {
      displayMinutes(timerMinutes, on || (menuMode != MINUTE_SET), true);
      displaySeconds(timerSeconds, on || (menuMode != SECOND_SET));
    }
  }
}

//---------------------------------------------------------------
//Show the minutes value
void displayMinutes(uint8_t num, bool on, bool colon)
{
  for (int i = 0; i < 2; i++)
  {
    if (on)
    {
      lc.setDigit(0, digits[i + 2], num % 10, (colon && (i == 0)));
    }
    else
    {
      lc.setChar(0, digits[i + 2], ' ', (colon && (i == 0)));
    }
    num = num / 10;
  }
}

//---------------------------------------------------------------
//Show the seconds value
void displaySeconds(uint8_t num, bool on)
{
  for (int i = 0; i < 2; i++)
  {
    if (on)
    {
      lc.setDigit(0, digits[i], num % 10, false);
    }
    else
    {
      lc.setChar(0, digits[i], ' ', false);
    }
    num = num / 10;
  }
}

//---------------------------------------------------------------
//Show the brightness value
void displayBrightness(uint8_t num, bool on)
{
  //_abc defg
  lc.setRow(0, digits[3], 0x1F);    //b
  lc.setRow(0, digits[2], 0x05);    //r
  
  for (int i = 0; i < 2; i++)
  {
    if (on)
    {
      lc.setDigit(0, digits[i], num % 10, false);
    }
    else
    {
      lc.setChar(0, digits[i], ' ', false);
    }
    num = num / 10;
  }
}

//---------------------------------------------------------------
//Write the EepromData structure to EEPROM
void writeEepromData()
{
  //This function uses EEPROM.update() to perform the write, so does not rewrites the value if it didn't change.
  EEPROM.put(EEPROM_ADDRESS,EepromData);
}

//---------------------------------------------------------------
//Read the EepromData structure from EEPROM, initialise if necessary
void readEepromData()
{
  //Eprom
  EEPROM.get(EEPROM_ADDRESS,EepromData);
  if (EepromData.magic != EEPROM_MAGIC)
  {
    EepromData.magic = EEPROM_MAGIC;
    EepromData.minutes = 0;
    EepromData.seconds = 0;
    EepromData.brightness = 4;
    writeEepromData();
  }
}

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

class Button
{
	public:
	//Simple constructor
	Button(int pin);
	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();
	//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(int pin)
{
  _name = pin;
  _pin = pin;
  _range = false;
  _low = 0;
  _high = 0;
  _backgroundCallback = NULL;
  _repeatCallback = NULL;
  pinMode(_pin, INPUT_PULLUP);
}

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

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
//  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

John Bradnam

141 projects • 167 followers
Thanks to ELECTROBOB.

Comments