Things used in this project

Hardware components:
Attiny85
Atmel ATTiny85
×1
09590 01
LED (generic)
×3
Mfr 25fbf52 221r sml
Resistor 221 ohm
Or similar, I used 220 ohm
×3
Omron b3f 1000 image 75px
Tactile Button Switch (6mm) Single
or similar
×1
Adafruit industries ada653 image 75px
Coin Cell Battery Holder
×1
Adafruit industries ada654 image 75px
Coin Cell Battery CR2032
any 3v cell that fits is fine
×1
Hand tools and fabrication machines:
09507 01
Soldering iron (generic)

Schematics

eagle pcb layout
Use with eagle software
wozitdo_SmNAPUpR0k.brd
schematic
eagle schematic, use wit eagle software
wozitdo_fKiEi8RHfj.sch
Vero Board Layout
A possible vero-board layout. You probably don't need the programming header at the right hand side; I developed most of the code on one of these and it was helpful to be able to program it without pulling and replugging the MCU.

Code

wozItDo.inoArduino
Source code (Less .h header). Build with ardunio IDE (with ATTINY85 support added). Then upload using "Arduino as an ISP" or using an AVR Dragon programmer or similar.
/*wozItDo: A wozItDo is a puzzle based on the attiny85, 3 LEDs, a
  push button switch and two button cells.   Tony McGregor (c) 31 Mar 2014.

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.

tony@mcgregor.org.nz
*/


#include <avr/sleep.h>
#include <avr/interrupt.h>
#include <setjmp.h>     /* jmp_buf, setjmp, longjmp */
#include "wozItDo.h" 

//#define DBG
#ifdef DBG
#   define DBG_LED_PIN 2
#else
# define DBG_LED_PIN
#endif

const uint8 SW_PIN  = 0; //NOTE: Interrupt set for pin0, only INT0 can wake CPU
const uint8 LED_PIN[] = { 1, 4, 3, DBG_LED_PIN };
const byte MAX_LED_ALL = sizeof(LED_PIN) / sizeof(uint8) - 1;// 0 .. MAX_LED_ALL
#ifdef DBG
   const byte MAX_LED = MAX_LED_ALL - 1 ;                         
#  define DBG(a) a
#  define DBG_LED(state) {digitalWrite(DBG_LED_PIN, state);}
#else
   const byte MAX_LED = MAX_LED_ALL;                         
#  define DBG_LED(state)
#  define DBG(a) 
#endif

#define LED(n) (0b0001 <<(n))
const byte LED0and1     = LED(0) | LED(1); 
const byte LED1and2     = LED(1) | LED(2); 
const byte LED0and2     = LED(0) | LED(2); 
const byte LED0and1and2 = LED(0) | LED(1) | LED(2);
const byte NO_LEDS      = 0;
const byte ALL_LEDS     = (LED(MAX_LED) <<1) - 1;//all but the debug led

const uint   TEMP_OFFSET = 271; //The temperature  needs calibrating. The data
                                // sheet suggests an offset of 275 at 25oC

const wdTime SUPER_FAST_FLASH_TIME = T32ms;    
const wdTime EXTRA_FAST_FLASH_TIME = T96ms;    
const wdTime FAST_FLASH_TIME       = T128ms;    
const wdTime MEDIUM_FLASH_TIME     = T250ms;
const wdTime DIGIT_GAP_TIME        = T500ms;  //between flashes
const wdTime NUMBER_GAP_TIME       = T2s;     //after showing a num in binary
const wdTime RESULT_TIME           = T6s;     //after an answer (e.g. dice tho)

const ushort MIN_MODE_CHANGE_TIME = 4000;  //ms button down to change mode
const ushort MIN_HIDDEN_MODE_CHANGE_TIME = 10000;
const ulong  DEBOUNCE_TIME        =  32;  //ms since last toggle

const    uint8 DEFAULT_MODE    = 0;
volatile uint8 mode            = DEFAULT_MODE; 
         uint8 lastMode        = DEFAULT_MODE+1;
typedef  void (*modeFunc)();
const    modeFunc modeFuncs[]   = { //normal modes
                                    diceMode, countMode, tempMode, setSpeed, 
				    //hidden modes
			  	    storyMode, deathMode};
const   uint8 MAX_NORMAL_MODE  = 3;
const   uint8 MAX_MODE         = MAX_NORMAL_MODE + 2;
ulong   lastToggle              = 0;        //last time the button changed state

bool  returnOnButton;                       //return to mainline on button down
const ushort IDLE2DEFAULT_TIME  = 300;      //seconds (300=5min)
const ushort IDLE2STALL_TIME    = 8 * 3600; //seconds (3600 = 1 hour)
bool  userNotIdle               = TRUE;     //Inhibit slowdown
volatile bool button            = 0;        //TRUE=presed, FALSE=up
const wdTime MAIN_LOOP_FAST     = T1s;      //setSpeed mode cycles through these
const wdTime MAIN_LOOP_MEDIUM   = T4s;      // idle timeout sets mainloopSleep
const wdTime MAIN_LOOP_SLOW     = T8s;      // to MAIN_LOOP_BORED
const wdTime MAIN_LOOP_CRAWL    = T1m;      
const wdTime MAIN_LOOP_BORED    = T15m;     
const wdTime MAIN_LOOP_DEFAULT  = MAIN_LOOP_MEDIUM;
      wdTime mainLoopSleep      = MAIN_LOOP_DEFAULT;          
bool  mainLoopButtonWait        = FALSE;
volatile ulong lostMillis       = 0;        //Clock stops while sleeping
#define MILLIS() (millis() + lostMillis)
const uint8 ERROR_REPETITIONS = 3;
volatile bool watchdogInterrupt = FALSE;    //a WDog intr woke us up
volatile bool buttonInterrupt   = FALSE;    //a button inter woke us up

//---------------------------------------------------------------------------
void setup(){
  stopWatchdog();                //A non-power-cycle reset doesn't
				 // stop the watchdog
  pinMode(SW_PIN, INPUT);
  digitalWrite(SW_PIN, HIGH);    //Turn on pull up resistor for the button pin
  sbi(GIMSK,PCIE);               //Turn on Pin Change interrupt
  PCMSK = 1<<PCINT0;             //For just PIN 0

  outputPins(TRUE);
# if DBG
  pinMode(DBG_LED_PIN, OUTPUT);
  digitalWrite(DBG_LED_PIN, OFF);
# endif
  lastToggle = millis();        //pretend a button was pressed at boot so we
}                               //don't immediately change mode

jmp_buf env;
//---------------------------------------------------------------------------
//-----------------MAIN LOOP-------------------------------------------------
//---------------------------------------------------------------------------
void loop(){

  /*This use of longjmp() is a dreadful wonderful hack.  We longjmp()
  back here when the mode changes or when a button is pressed if the
  mode specific code requests that.  We come from inside the button
  toggle interrupt service routine and some unknown sequence of code
  before that.  Quite often that will be goToSleep() from within a
  POWER DOWN.  We need to reset things to a clean state.  I.E. Turn on
  interrupts, turn off the watchdog timer, enable the LED outputs.

  The alternative it to pepper the code with mode and button check if
  statements that gracefully return back through the layers of
  function calls.  Clean but ugly. Instead we use longjmp() a.k.a. goto
  on steroids. */

  static bool envSet = FALSE;
  if ( ! envSet ){
    /*First iteration of loop so we need to setup env to jump back
    to.  BTW We could probably have avoided envSet by putting this
    code in setup() but that would be adding ugly to hideous. */
    envSet = TRUE;
    if ( uint8 from = setjmp(env) ){ 
      /*setjmp() returns 0 if it was explicitly called and something else
      if we got here from a later from a longjmp(). I.E. this code
      runs after the longjmp().*/
      stopWatchdog();
      sei();
      outputPins(TRUE);
      if ( from == 2 ){   //a button return, not a mode change
	goto MODE_RETURN;
      }
    }
  }


  //Display new mode
  while ( mode != lastMode ){
    if ( mode > MAX_MODE ) { error(3,mode,NON_FATAL); mode = 0; }
    lastMode = mode;
    ledTrain(0, 2);
    pause(DIGIT_GAP_TIME);
    flashDigit(LED(2), mode+1);
    pause(DIGIT_GAP_TIME);
  }

  //execute main function for the mode
  returnOnButton     = FALSE;
  mainLoopButtonWait = FALSE;       
  buttonWait(UP, OFF, Tindefinite);
  modeFuncs[mode]();
MODE_RETURN:
  returnOnButton = FALSE;
  buttonWait(UP, OFF, Tindefinite);

  if ( mode == lastMode ) {
    //If we haven't changed mode and the button hasn't been pressed for
    //a long time we either slow down or stop altogether depending on
    //how long it is since the button was pressed.
    if ( userNotIdle){
      userNotIdle = FALSE;
    } else {
      uint idle = (MILLIS() - lastToggle)/1000;
      if ( idle >= IDLE2DEFAULT_TIME ){
	mode = DEFAULT_MODE;               
	if ( idle >= IDLE2STALL_TIME ){
	  //no user activity for ages, go totally to sleep until a button press
	  ledCountdown(NUMBER_GAP_TIME);
	  mainLoopButtonWait = TRUE;
	} else {                              
	  mainLoopSleep = MAIN_LOOP_BORED;
	}
      } //little activity from the user
    } //idle slowdown not inhibited
	  

    /*loop again at the appropriate time.  The mode function can
    request we wait for a button press before recalling it.  If
    that's not the case, and we didn't just change mode, we sleep
    for the current main loop delay time. This is set in speedMode
    and also when we haven't had a button press for a long time.*/
    if ( mainLoopButtonWait ){            //set above or in mode code
      buttonWait(DOWN, OFF, T15m);
      mainLoopButtonWait = FALSE;
    } else {
      deepPause(mainLoopSleep);
    }
  }//mode hasn't changed
}


//---------------------------------------------------------------------------
/* We turn all the output pins off when in sleep mode.  It may save a
 little power (or a lot if one of the LEDs is on).  We have to leave
 the button pull up resistor on because we need the button to be able
 to wake us up from a sleep.*/
inline void outputPins(const bool on){
  for (uint8 led = 0; led <= MAX_LED; ++led){
    if ( on ){
      pinMode(LED_PIN[led], OUTPUT);
    } else {
      pinMode(LED_PIN[led], INPUT);     
      digitalWrite(LED_PIN[led], LOW);     // ensure pull up resistor is off
    }
  }//for each led pin, NOT including the debug led (if there is one)
}

//---------------------------------------------------------------------------
wdTime halveWdTime(wdTime t){
  if ( t.prescalar > 0 ){
    t.prescalar =  t.prescalar >>1;
  } else {
    t.cycles = t.cycles >>1;
  } 
  return t;
}

//---------------------------------------------------------------------------
void flashDigit(const byte led, int count){
  if ( count >= 10 ){
    for (int z = 0; z < count; ++z){
      flash(led, FAST_FLASH_TIME);      if ( button ){ return; }
    } 
  } else if ( count == 0 ){
    flash(led, SUPER_FAST_FLASH_TIME);
  } else {
    for (; count > 0; --count){
      flash(led, MEDIUM_FLASH_TIME);    if ( button ){ return; }
    } 
  }
  pause(DIGIT_GAP_TIME);
}

//---------------------------------------------------------------------------
void flash(const byte leds, const wdTime wdTime){
  setLeds(leds, HIGH);
  pause(halveWdTime(wdTime));                

  setLeds(leds, LOW);  
  pause(wdTime);                
  pause(halveWdTime(wdTime));                
}


//---------------------------------------------------------------------------
// Set one or more LEDs on or off.  
void setLeds(const byte leds2set, const bool val){
  for (uint8 led = 0; led <= MAX_LED_ALL; ++led){
    if ( leds2set & LED(led) ){ digitalWrite(LED_PIN[led], val); }
  } //for each led including the debug led if there is one
}



//---------------------------------------------------------------------------
void showBinary(const byte num){
  for (uint8 led = 0; led <= MAX_LED; ++led){
    digitalWrite(LED_PIN[led], (num & LED(led))==0?LOW:HIGH);
  } //for each led NOT including the debug led if there is one
}


//---------------------------------------------------------------------------
void ledCountdown(const wdTime delay){
  byte bits = ALL_LEDS;
  do {
    showBinary(bits); 
    pause(delay);
  } while( bits>>=1 );
}

//---------------------------------------------------------------------------
void ledTrain(const bool direction, const uint8 count){
  byte leds = 0;

  for(uint8 z = 0; z <= count; ++z){
    leds = direction ? LED(0) : LED(MAX_LED);
    while ( leds <= LED(MAX_LED) && leds != 0 ){
      flash(leds, EXTRA_FAST_FLASH_TIME);
      leds = direction ? leds<<1 : leds>>1;
    }
  }
  setLeds(ALL_LEDS, OFF);
}


//---------------------------------------------------------------------------
void fadeLeds(const byte leds){
  setLeds(leds, ON);

  for (int8 led = MAX_LED; led >= 0; --led){
    if ( ! leds & LED(led) ){ continue; }  //this led isn't included

    for (uint8 percent = 100; percent > 0; percent -= percent/20+1){
      uint8 frac = 0;
      for (uint cycle = 1; cycle <= 500; ++cycle){
	if ( (frac += percent) >= 100 ){
	  frac -= 100;
	  setLeds(LED(led), ON);
	} else {
	  setLeds(LED(led), OFF);
	}
      }//light led at a given percentage
      if( button ){ return; }
    }//each percentage
    setLeds(LED(led), OFF);
  }//for each led
}

//--------------------------------------------------------------------------
void nextMode(const modeType_t modeType){
  ++mode;
  if ( modeType == HIDDEN && mode <= MAX_NORMAL_MODE ){
    mode = MAX_NORMAL_MODE + 1;
  }

  if ((modeType == NORMAL && mode > MAX_NORMAL_MODE) || 
      (modeType == HIDDEN && mode > MAX_MODE) ){ 
    mode = 0; 
  }

  setLeds(ALL_LEDS, OFF);
}


//--------------------------------------------------------------------------
/* Display an error message.  There's no exit from this state (until a
 reset) Note, we use delay() here so we're not dependent on the
 sleep/watchdog interrupt mechanism.*/
void error(const uint8 code, const uint p, const fatal_t fatal){
  int shown = 0;

  stopWatchdog();
  while ( TRUE ){
    //something distinctive
    byte bits = LED0and2;            
    for (int z = 0; z < 10; ++z){
      setLeds(bits, ON);
      delay(300);
      setLeds(bits, OFF);
      bits = (~bits) & ALL_LEDS;
    }
    setLeds(ALL_LEDS, OFF);
    delay(500);

    //display code
    for (uint8 z = 0; z < code; ++z){
      setLeds(LED(0), ON);
      delay(800);
      setLeds(LED(0), OFF);
      delay(800);
    }
    delay(1500);

    //display parameter
    uint8 count = 1;
    bits = p>>15;
    uint p2 = p << 1;
    while ( count <= 16 && bits == 0){ bits = p2>>13; p2 = p2 <<3; count+=3; }
    while ( count <= 16 ){
      setLeds(ALL_LEDS, ON);
      delay(32);
      setLeds(ALL_LEDS, OFF);
      delay(64);
      setLeds(bits, ON);
      delay(4000);
      setLeds(ALL_LEDS, OFF);
      bits = p2>>13;
      p2 = p2 <<3;
      count += 3;
    }
    setLeds(ALL_LEDS, OFF);

    //loop again, return or wait for a button press depending on how
    //many times we've displayed the message and whether the error is
    //fatal
    if ( shown++ >= ERROR_REPETITIONS ){
      if ( NON_FATAL ){ return; }
      buttonWait(DOWN, OFF, Tindefinite);
      buttonWait(UP, OFF, Tindefinite);
    } //waited for a button press
  } //loop forever
} 

//---------------------------------------------------------------------------
//----------------TEMP MODE--------------------------------------------------
//---------------------------------------------------------------------------
void tempMode(void){
  // Flash the temperature in degrees Celsius.
  short temp = getTemp();

  returnOnButton = TRUE;
  if ( temp >= 100 ){
    error(1, 1, NON_FATAL);
    return;
  } else if ( temp <= 0 ){
    error(1, 2, NON_FATAL);
    return;
  } else {
    flashDigit(LED(0), temp / 10);
    flashDigit(LED(1), temp % 10);
  }
}


//---------------------------------------------------------------------------
short getTemp(void){
  short wADC;

  //The internal temperature has to be used with the internal
  // reference of 1.1V. Set the internal reference and mux.  NOTE:
  // These are different for the ATtiny85 to the ATmega328
  ADMUX = ( _BV(REFS1) |
	   _BV(MUX0) |_BV(MUX1) | _BV(MUX2) | _BV(MUX3) );
  sbi(ADCSRA,ADEN);     //Switch A2D ON
  delay(3);             //Wait for 1.1v reference to become stable.

  sbi(ADCSRA, ADSC);               //Start the ADC conversion
  while (bit_is_set(ADCSRA,ADSC)); //wait until ADC is done

                       //ADCW is ADCH <<8 | ADCL.
  wADC = ADCW - TEMP_OFFSET;   
  cbi(ADCSRA,ADEN);    //Switch A2D OFF
  return(wADC);
}


//---------------------------------------------------------------------------
//-----------------COUNT MODE------------------------------------------------
//---------------------------------------------------------------------------
const wdTime SHOW_VALUE_TIME  = T1s;   //How long to show the current count for
const wdTime BLINK_DELAY      = T256ms;
const uint8  FINISHED_FLASHES = 50;
const ushort ABANDON_SET_TIME = 20000; //Give up set mode after this many ms
const ushort COUNT_SET_WAIT   = 2000;  //ms with no press before moving
 			    	       // to next digit in set mode
void countMode(void){
  static uint8 count = 0;

  if ( count == 0 ){    
    count = setCount();
    ++count;                     //we pre-decrement count before we display it
  } else {
    returnOnButton = TRUE;
    if ( --count > 0 ){
      //normal case
      showBinary(count); 
      pause(SHOW_VALUE_TIME);
      setLeds(ALL_LEDS, OFF);
    } else {
      //time is up, show our "visual alarm"
      mainLoopButtonWait = TRUE;//wait for a button press before going again
      for (uint8 f = 0; f < FINISHED_FLASHES; ++f){
	flash(ALL_LEDS, EXTRA_FAST_FLASH_TIME); 
      } //each flash
    } //time up
  } 
   
  userNotIdle = 1; 
  /*Normally, we require a button press to indicate our user is still
  around but for this mode a lot of time might go by while we're
  still active so we don't start counting idle time until we're done.
  Note that, if we've counted down and are waiting for a button to
  start again, we wait in the main loop so we never get here and idle
  time does count toward the inactivity time.*/
}


//---------------------------------------------------------------------------
/* Flash each bit in turn until the button is pressed. When it's
 pressed, toggle the bit on and off until it's not pressed for a short
 while at which point move on to the next bit.  Once they are all set,
 return with the result.*/
uint8 setCount(void){
  byte value = ALL_LEDS;
  for (byte bit = LED(0); bit <= LED(MAX_LED); bit = bit <<1 ){
    //for each bit 
    ushort count = 0;
    while ( !button ){
      if ( (count+= WDTIME2MS(BLINK_DELAY)) > ABANDON_SET_TIME ){
	//Nothings been pressed, give up on set
 	goto done;
      }
      showBinary(value);
      pause(BLINK_DELAY);
      value ^= bit;
    }
    buttonWait(UP, ON, Tindefinite);     
    value |= bit;      //default is bit set
    showBinary(value);

    ushort waited = 0;
    while ( waited < COUNT_SET_WAIT ){
      while ( !button && (waited < COUNT_SET_WAIT) ){
	pause(T16ms);   //shortest pause delay
	waited+=16;
      }
      if ( waited < COUNT_SET_WAIT ){
	//toggle digit
	value ^= bit;
	showBinary(value);
	waited = 0;
	buttonWait(UP, ON, Tindefinite);   
      }
    } //keep looping until delay between button presses is long enough
      //to move on to next bit (or finish)
  } //for each bit in the number

 done:
  setLeds(ALL_LEDS, OFF);
  return value;
}


//---------------------------------------------------------------------------
//----------------DICE MODE---------------------------------------------------
//---------------------------------------------------------------------------
void diceMode(void){
  uint8 result;
  uint8 giveUp = 128;

  mainLoopButtonWait = TRUE;
  while ( !button && giveUp-- > 0 ){
    showBinary(random(NO_LEDS,ALL_LEDS));
    pause(SUPER_FAST_FLASH_TIME);
    showBinary(0);
    pause(SUPER_FAST_FLASH_TIME);
    pause(SUPER_FAST_FLASH_TIME);
  }
  buttonWait(UP, OFF, Tindefinite);    
  
  returnOnButton = TRUE;
  result = random(1,7);    //from 1 to 6
  if ( result <= 3 ){
    //Turn the number into a sequence of bits that long. E.G. For 2, 1<<2 = 4
    //4 - 1 = 3 which in binary is 0b011, I.E. two bits
    showBinary((1<<result)-1);
    pause(RESULT_TIME);
  } else {
    for(ushort z = 0; 
	z <= WDTIME2MS(RESULT_TIME); 
	z += WDTIME2MS(DIGIT_GAP_TIME)){
      if ( z != 0 ) { pause(DIGIT_GAP_TIME); } //avoid extra pause at end

      showBinary(7);
      pause(DIGIT_GAP_TIME);
      showBinary(0);
      pause(FAST_FLASH_TIME);
      showBinary((1<<(result-3)) -1); 
      pause(DIGIT_GAP_TIME);
    }
  }
}



//---------------------------------------------------------------------------
//----------Set Speed Mode---------------------------------------------------
//---------------------------------------------------------------------------
const wdTime MAX_BUTTON_WAIT = T3s;
void setSpeed(void){
  byte bits     = LED(0);  //Start at MAIN_LOOP_MEDIUM (1 bit on)
  bool increase = TRUE;
  static const wdTime speed[] = { 
    MAIN_LOOP_FAST, MAIN_LOOP_MEDIUM, MAIN_LOOP_SLOW, MAIN_LOOP_CRAWL 
  };

  buttonWait(UP, OFF, Tindefinite);
  showBinary(bits);

  while ( TRUE ){
    buttonWait(DOWN, ON, MAX_BUTTON_WAIT);
    if ( ! button ){
      //We're done, return delay time
      uint8 n;
      for (n = 0; bits > 0; bits = bits>>1){ ++n; }  //count bits set
      mainLoopSleep = speed[n];
      nextMode(NORMAL);   //Only need to do set speed once
      return;
    }
    

    //set one more, or one less led depending on which way we're going
    bits = increase ? (bits<<1) | 0b001 : (bits>>1);
    if ( bits > ALL_LEDS ) { bits >>= 2; increase = FALSE;  }
    if ( bits == NO_LEDS ) { increase = TRUE; }
    showBinary(bits);

    //get ready to loop again
    buttonWait(UP, ON, Tindefinite);    
  } //while ( TRUE )
}



//---------------------------------------------------------------------------
//----------------STORY MODE-------------------------------------------------
//---------------------------------------------------------------------------
#define WORD_GAP_TIME T2s
void storyMode(void){
    static const char story[]  = "I am A McWozItDo? "
      "But you know wot cause you're reading this!\n"
      "I was born 29Mar2014 and I'm version 0.9\n"
      "My daddy is tony@mcgregor.org.nz";
      

  userNotIdle = 1;             //See count mode for explanation
  returnOnButton = TRUE;
  for (char *c = (char *)story; *c != '\0'; ++c){
    ushort bits = *c;          //MUST be ushort not byte (needs >=9 bits)
    for (short i = 0; i < 3; ++i){
      flash(ALL_LEDS, SUPER_FAST_FLASH_TIME);
      showBinary((bits & 0b111000000) >>6);
      pause(NUMBER_GAP_TIME);
      bits = bits <<3;
    }
    setLeds(ALL_LEDS, OFF);
    pause(WORD_GAP_TIME);                      
  } //for each byte in message
  mainLoopButtonWait = TRUE;   //don't restart until a button is pressed
}


//---------------------------------------------------------------------------
//----------------DEATH MODE-------------------------------------------------
//---------------------------------------------------------------------------
const uint8  MORSE_LED       = LED(0);
const uint8  LETTER_LED      = LED(2); //if != 0, flash between letters 
const wdTime MORSE_RATE      = T1s;
/*These are standard ratios for international Morse (ITU M.1677-1
10/09) and are defined in multiples of MORSE_RATE.  Because we always
include the intra-character and inter-word gaps, MORSE_CHAR_GAP is 2,
not 3 (the "intra-"character gap plus two more) and MORSE_WORD_GAP is
4 not 7.*/
const uint8 MORSE_SHORT_TIME = 1;  
const uint8 MORSE_LONG_TIME  = 3;           
const uint8 MORSE_SIGNAL_GAP = 1;
const uint8 MORSE_CHAR_GAP   = 2;
const uint8 MORSE_WORD_GAP   = 4;

#define MORSE_LONG_CODE  0b11
#define MORSE_SHORT_CODE 0b10

void deathMode(void){
  static const char *message = "  Press me or I will die  ";

  returnOnButton = TRUE;
  //Strictly speaking, SOS is one "letter" (prosign) 
  sendMorse("SOS", 3);                             

  //now send the rest of the message
  for (char *c = (char *)message; *c != '\0'; ++c){
    if ( *c == ' ' ){
      pauseMultiple(MORSE_RATE, MORSE_WORD_GAP);   
    } else {
      if ( *c > 'Z' ){ *c -= 'a' - 'A'; }  //Upper case it
      sendMorse(c, 1);                             
    }
  } //for each character in the message

  sendMorse((char *)"SK", 2);                      
  pause(T2s);

  //slowing heartbeat
  for ( ushort cycles = 1; cycles < 32; cycles += (cycles / 4) + 1){
    wdTime pulse;
    pulse.prescalar = WD_64ms;
    pulse.cycles = cycles;
    flash(ALL_LEDS, pulse);
  }

  //Last chance; fade out the leds one by one
  fadeLeds(ALL_LEDS);                                

  //all done, shutdown forever
  halt();
}

//---------------------------------------------------------------------------
/* Send a Morse character or prosign. We include a length so that we
 can pass sendMorse a single character or a string with a prosign.
 Prosigns have no intra letter gaps (they are, effectively, one longer
 character).*/
#define mL )<<2 | MORSE_LONG_CODE
#define mS )<<2 | MORSE_SHORT_CODE
#define mN )<<2 | 0
#define mI ((((0
static const byte morse[26] = { 
  /*A*/ mI mS mL mN mN, 
    /*B*/ mI mL mS mS mS,
    /*C*/ mI mL mS mL mS,
    /*D*/ mI mL mS mS mN,
    /*E*/ mI mS mN mN mN,
    /*F*/ mI mS mS mL mS,
    /*G*/ mI mL mL mS mN,
    /*H*/ mI mS mS mS mS,
    /*I*/ mI mS mS mN mN,
    /*J*/ mI mS mL mL mL,
    /*K*/ mI mL mS mL mN,
    /*M*/ mI mS mL mS mS,
    /*M*/ mI mL mL mN mN,
    /*N*/ mI mL mS mN mN,
    /*O*/ mI mL mL mL mN,
    /*P*/ mI mS mL mL mS,
    /*Q*/ mI mL mL mS mL,
    /*R*/ mI mS mL mS mN,
    /*S*/ mI mS mS mS mN,
    /*T*/ mI mL mN mN mN,
    /*U*/ mI mS mS mL mN,
    /*V*/ mI mS mS mS mL,
    /*W*/ mI mS mL mL mN,
    /*X*/ mI mL mS mS mL,
    /*Y*/ mI mL mS mL mL,
    /*Z*/ mI mL mL mS mS,
    }; 

void sendMorse(const char *mes, const uint8 len){
  for (uint8 i = 0; i < len; ++i){
    uint8 index = mes[i] - 'A';
    if ( index > 25 ){ error(6, mes[i], FATAL); }
	
    for (byte code = morse[index]; code > 0; code = code <<2 ){
      byte signal = (code >>6);  //a 2 bit Morse code (short or long)
      if ( signal == 0 ){ continue; } //a NULL, do nothing
	
      //send a short or a long
      setLeds(MORSE_LED, ON);
      if ( signal == MORSE_LONG_CODE ){
	pauseMultiple(MORSE_RATE, MORSE_LONG_TIME);    
      } else if ( signal == MORSE_SHORT_CODE ){
	pauseMultiple(MORSE_RATE, MORSE_SHORT_TIME);   
      } //a long or short 
      setLeds(MORSE_LED, OFF);
      pauseMultiple(MORSE_RATE, MORSE_SIGNAL_GAP);     
    } //for each 2 bit code (a Morse long or short)
  } //for each letter in the character (normally 1)
  if ( LETTER_LED ) { setLeds(LETTER_LED, ON); }
  pauseMultiple(MORSE_RATE, MORSE_CHAR_GAP);
  if ( LETTER_LED ) { setLeds(LETTER_LED, OFF); }
}


//---------------------------------------------------------------------------
//---------------Pause, Sleep and Interrupt Code-----------------------------
//---------------------------------------------------------------------------

//---------------------------------------------------------------------------
/* Wait for the button to be in the right state.  Debounce is handled
 in the button state change interrupt routine which also sets the value
 of the button variable used here.*/
void buttonWait(const bool b, const bool leds, wdTime t){
  if ( b == DOWN &&  button ){ return; }
  if ( b == UP   && !button ){ return; }

  byte flags = 0;
  if ( ! leds )   { flags |= IO_OFF; }
  if ( b == DOWN ){ flags |= BUTTON_DOWN; }
  if ( b == UP  ) { flags |= BUTTON_UP | CLOCK_RUNNING; }
  pauseWithType(t, flags);
}

//---------------------------------------------------------------------------
/* pause(), pauseMultiple and deepPause() are just shims for pauseWithType(); 

 pause et al.  work like delay. I.E. the main line stops for that
 time.  But, instead of busy/idling, we put the CPU to sleep and
 wake it up with the watchdog timer.  That saves a lot of power. The
 time is specified using our type wdTime which contains a watchdog
 timer prescalar and a number of iterations.  There are predefined
 values for this structure of the form T10s (10 seconds), T1m (1
 minute etc) */

void pause(const wdTime t){
  if ( isT0(t) ){ return; }
  pauseWithType(t, BUTTON_DOWN);
}
void deepPause(const wdTime t){
  pauseWithType(t, BUTTON_DOWN | IO_OFF);
}
void pauseMultiple(wdTime t, const uint8 m){
  t.cycles *= m;
  pauseWithType(t, BUTTON_DOWN);
}

//---------------------------------------------------------------------------
void pauseWithType(const wdTime t, const byte flags){
  if ( BUTTON_OK(flags) ) { return; }

  if ( isTindefinite(t) ){
    //indefinite: pause (until a button press)
#   if BUSY_WAIT    
    while ( ! BUTTON_OK(flags) ){ }
#   else
    goToSleep(t, flags | INDEFINITE);
#   endif
  } else { //not an indefinite wait
#   if BUSY_WAIT    
    ulong stopTime = MILLIS() + WDTIME2MS(t);
    while ( MILLIS() < stopTime && ! BUTTON_OK(flags) ){ }
#   else
    goToSleep(t, flags);
#   endif    
  }
}

//---------------------------------------------------------------------------
/* Put us into a low power sleep mode.  A CPU busy/idle loop uses
 about 1.5mA. IDLE mode (with timer1 running for millis()) uses
 about 800uA. POWER_DOWN with the watchdog timer uses about 7uA and
 without the watchdog about 0.5uA. The A2D and voltage reference use
 about 190uA. Each led uses about 10mA so they are the main power
 guzzlers.  Shutting down the output ports doesn't save anything
 much unless they were running a LED. Switching them off is more of
 a fail safe.  The flags control how what we can shutdown while
 we're sleeping.  We also check if we're in a button debounce period
 as this has no explicit end event other than the passing of time.
 We mustn't stop the millis() clock during a debounce as it's needed
 to know the debounce time is done. The flags also tell us what
 events should end a sleep (e.g. a button up event) The time to
 sleep is measured in watchdog timer cycles, i.e. a prescalar
 interval as defined above (WD_16ms, WD_8ms etc) and a number of
 iterations.  Waking up and returning to sleep doesn't seem to use
 any appreciable power (although the watchdog itself does).  If
 we're never going to wake, we turn everything off including the
 watchdog and the button input.

 goToSleep is never called directly, instead we use pause() (or
 deepPause()) which works like delay().

 The flags are:
  CLOCK_RUNNING:  use IDLE mode not SHUT_DOWN so millis() clock keep ticking
  IO_OFF:         Shutdown all IO except button input if needed
  BUTTON_DOWN:    Exit on a button down event
  BUTTON_UP:      Exit on a button up event*/

void goToSleep(wdTime time, const byte flags){
  cbi(ADCSRA,ADEN);    //Switch A2D OFF (shouldn't be on)
  if ( flags & IO_OFF ){ outputPins(FALSE); }     //Stop driving the output pins

  /*This is a wee bit tricky.  If we don't need the millis() clock, we
  just use the watchdog timer and sleep in POWER DOWN mode, which is
  the cheapest. A watchdog or button change interrupt will wake us
  up. If we need the millis() clock we don't start the watchdog
  timer and we use sleep mode IDLE.  This leaves the clock running
  but uses a lot more power.  We'll wake on a clock interrupt or a
  button change interrupt.

  When we wake, if it's a button interrupt and the button is now in
  the state we want, we're done.  If it's a watchdog interrupt we
  check if we've had enough watchdog interrupt for the amount of
  time we want to sleep and, if we have, we're done.  If it's
  neither of those it must be a clock interrupt.  We check if the
  clock shows we're done.  If it's none of those we need to wait
  longer.If we end sleeping because of the watchdog timer or a
  button press, we adjust the MILLIS() clock as best we can.

  When we wait again, we may change from using timer interrupt to
  the watchdog timer if the reason we used the timer is no longer
  valid.  In particular, the button debounce time has passed.  This
  is important because we may well start a long wait just after a
  button press and we don't want to use sleep mode IDLE for all
  that time.  When we do this, we adjust the number of watchdog
  interrupt cycles needed.*/

  ulong startTime = MILLIS();//Time when we started
  ulong endTime = startTime + WDTIME2MS(time);

  byte mode;                   //What sleep mode is in use (IDLE or POWER DOWN)
  bool watchdogRunning = FALSE;//Have we started the watchdog timer
  bool millisWait = FALSE;     //Are we already waiting with the clock running
  ulong now;                   //current time
  while ( TRUE ){
    now = MILLIS();
    if ( !isINDEFINITE(flags) && time.cycles == 0 ){ break; }
    if ( !isINDEFINITE(flags) && now >= endTime )  { break; }
    if ( BUTTON_OK(flags) ){ break; }

    //for one reason or another, we're not done waiting
    if ( flags & CLOCK_RUNNING || (now - lastToggle < DEBOUNCE_TIME) ){
      /*We need to keep millis() running during the key debounce time
      or for some other reason so we can't POWER DOWN. Instead we
      idle which leaves the millis() clock running*/
      mode = SLEEP_MODE_IDLE;
      millisWait = TRUE;
    } else {
      if ( millisWait ){  
	/*we were previously waiting on timer interrupts but can now
	change to using the watchdog.  First we need to adjust the
	watchdog cycles for the time we waited (if it was long enough
	to matter, mostly is isn't).*/
	if ( !(flags&INDEFINITE) && 
	     (now - startTime) >= WDPS2MS(time.prescalar)>>1){
	  ushort cyclesDone = ((now - startTime) / WDPS2MS(time.prescalar)) + 1;
	  if (  cyclesDone >= time.cycles  ) { break; } //waited long enough
	  time.cycles = time.cycles - cyclesDone;
	}
	millisWait = FALSE;
      }
      mode = SLEEP_MODE_PWR_DOWN;
      if ( !isINDEFINITE(flags) && ! watchdogRunning ){ 
	startWatchdog(time.prescalar); 
	watchdogRunning = TRUE; 
      }
    }

    watchdogInterrupt = FALSE;   //set in watchdog interrupt
    buttonInterrupt   = FALSE;   //set in button state change interrupt
    set_sleep_mode(mode);
    //-------------------------ZZZZZZZZ-----------------------------
    sleep_mode();                      //Sleep until an interrupt.  
    //-------------------------zzzzzzzz------------------------------
    //woke up because of watchdog interrupt, a counter interrupt or a
    //button toggle
    if ( watchdogInterrupt ){
      lostMillis += WDPS2MS(time.prescalar);
      if ( !(flags&INDEFINITE) ){ time.cycles--; }
    } else if ( buttonInterrupt && mode == SLEEP_MODE_PWR_DOWN ) {
      lostMillis += 1;                          //assume some time has passed
    }
  };
    
  //done sleeping
  if ( watchdogRunning ){ stopWatchdog(); }
  if ( flags & IO_OFF ) { outputPins(TRUE); }   //Reenable the output pins
}

//---------------------------------------------------------------------------
/* Go to sleep forever using as little power as possible (0.5uA).
 This is really a special case of goToSleep but that's complicated
 enough as it is so it's cleaner to reproduce a bit of code here*/
void halt(void){
  stopWatchdog();                //Shouldn't be on
  cbi(ADCSRA,ADEN);              //Switch A2D OFF (shouldn't be on)
  cbi(GIMSK,PCIE);               //Turn off Pin Change interrupt
  PCMSK = 0;                     // and clear pin change mask just to be sure
  digitalWrite(SW_PIN, LOW);     //Turn off button pull up resistor
  outputPins(FALSE);             //Shutdown all outputs except the  DBG_LED
# if DBG
  pinMode(DBG_LED_PIN, INPUT); //Shutdown debug LED
  digitalWrite(DBG_LED_PIN, OFF);
# endif 

  while ( TRUE ){
    set_sleep_mode(SLEEP_MODE_PWR_DOWN);
    sleep_mode();                //shouldn't ever wake up
  }
}


//---------------------------------------------------------------------------
/* The Watch Dog Timer Control Register (WDTCR) has:
 7: WDIF Interrupt flag (generated when an interrupt has occurred)
 6: WDIE Interrupt Enabled (generate an interrupt not restart). If WDIE is 1
         the watchdog runs even if WDE is 0.
 5: WDP3 Prescalar bit 3 
 4: WDCE Watch Dog Change Enable. This must be set when stopping the watchdog
         by writing a 0 to WDE (Watch Dog Enable) and when the
         pre-scalar is changed.  It is reset to 0 automatically after 4
         clock cycles. To disable the timer you must write a 1 to WDCE
         and WDE (even though WDE it may already be 1). Then, within 4
         clock cycles, write a 0 to WDE.  This is not needed on an
         enable. To change the prescalar, WDCE it just needs to be set
         along with the new prescalar value.

 3: WDE  Watchdog enable.  See WDCE for notes on setting WDE to 0. Also see
         note for WDIE; the watchdog may run even if WDE=0
 2-0: Prescalar bits 2-0. The _4_ bits of the prescalar set the timeout:  
      0=16ms, 1=32ms, 2=64ms, 3=128ms, 4=250ms, 5=500ms
      6=1s,   7=2s,   8=4s,   9= 8s

 The MCUSR (MCU Status Register) bit 3 is the Watchdog Reset Flag
 (WDRF). This bit is set when the watchdog causes a reset. We don't do
 that so we don't use it.

 The watchdog counter only resets in the following situations:
   1. A chip reset
   2. The watchdog timer is disabled
   3. A WDR (Watchdog Reset) instruction is executed (we use the wdt_reset()
      macro from the sketch but all it does is execute the WDR instruction).
      Note: this doesn't include a prescalar change. */

//---------------------------------------------------------------------------
// Start the watchdog timer in interrupt mode or change the prescalar
// if it's already running.
void startWatchdog(const uint8 prescalar) {
  byte wdtcr;
  if (prescalar > WD_MAX ){ error(2, prescalar, FATAL); }

  wdtcr = (prescalar & 0b0111) | ((prescalar & 0b1000)<<2) | 1<<WDIE;
  noInterrupts();
  WDTCR = (1<<WDCE) | (1<<WDE);   //See above.
  WDTCR = wdtcr;
  interrupts();
}

//---------------------------------------------------------------------------
void stopWatchdog(void){
  noInterrupts();
  WDTCR |= (1<<WDCE) | (1<<WDE);   //|= to leave prescalar alone. Setting it 
  WDTCR = 0;                       // to 0 could cause a timeout before we 
  interrupts();                    // do the disable. See the ATtiny85 
                                   // data sheet 8.4.2. It's important that we 
                                   // set both WDE and WDIE to 0.
}

//---------------------------------------------------------------------------
// Watchdog interrupt handler routine
ISR(WDT_vect){
  watchdogInterrupt = TRUE;
}


//---------------------------------------------------------------------------
// Button interrupt service routine. 
// We arrive here when the button state (down or up) changes.
ISR(PCINT0_vect) {
  static ulong lastPress  = 0;
  ulong now;

  buttonInterrupt = TRUE;
  //putting this at the start reduces the chance that we'll miss the
  //initial change if a quick bounce occurs.
  bool newState = (digitalRead(SW_PIN) == LOW);  //_LOW_ => button pressed

  now = MILLIS();
  //debounce
  if ( (now - lastToggle) < DEBOUNCE_TIME ){
    //this toggle event happened within a short time of the previous
    //one, too quickly for it to be real.  This is a bounce so ignore
    //it.
    lastToggle = now;
    return;
  }

  if ( button == newState ){
    /*We don't think there was a bounce but the button state hasn't
    changed.  What's probably happened is this is a bounce but it came
    hard on the heals of the original state change, while we were
    still processing the interrupt. So, we ignore the state change and
    DON'T (re-)start the debounce timer (i.e. update lastToggle).  If
    this was a bounce we'll return to the long term state of the
    button and they'll generate another change interrupt and the code
    below will get to run.*/
    return;
  }

  //a new event
  lastToggle = now;
  if (  Tequal(mainLoopSleep, MAIN_LOOP_BORED) ){
    //we slowed down because there was no button pressfor a long time.
    //Now a button has been pressed so return to normal speed
      mainLoopSleep = MAIN_LOOP_DEFAULT;
  }

  if ( newState ){ 
    button = TRUE;                       
    lastPress = now;
    DBG_LED(ON);
    if ( returnOnButton ) { longjmp(env, 2); }
  } else {         //button is released (some change and not pressed)
    button = FALSE;
    DBG_LED(OFF);
    if ( (now - lastPress) >= MIN_MODE_CHANGE_TIME ){
      //the button has been held down for a long time, we change mode.
      if ( (now - lastPress) >= MIN_HIDDEN_MODE_CHANGE_TIME ){
	nextMode(HIDDEN);
      } else {
	nextMode(NORMAL);
      }  // a long press
      longjmp(env, 1);
    } // a long or very long press
  } // button is released
}


// Emacs control
// Local Variables:
// eval: (c++-mode)
// End:
wozItDo.hArduino
Header file for wozItDo
/*wozItDo: A wozItDo is a puzzle based on the attiny85, 3 LEDs, a
  push button switch and two button cells.   Tony McGregor (c) 31 Mar 2014.

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.

tony@mcgregor.org.nz


 Typedefs need to be in a separate file from the .ino because the
 Arduino build chain auto inserts prototypes at the top of the
 file (after #includes). 

We also put a few "boiler plate" things here but, as we only have
one .ino file don't put most of the declarations and #defined here.
*/

#ifndef cbi  //clear bit
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
#endif
#ifndef sbi  //set bit
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))
#endif
#ifndef  wdt_reset //watchdog reset instruction
#define wdt_reset()  __asm__ __volatile__ ("wdr")
#endif

#define TRUE  true
#define FALSE false
#define ON  HIGH
#define OFF LOW

//These aren't considered good practise but I like them because they
//save space
typedef unsigned int   uint;
typedef unsigned long  ulong;
typedef unsigned short ushort;
typedef unsigned char  uint8;
typedef          char  int8;
const ushort USHORT_MAX = (~0);
const uint   UINT_MAX   = (~0);
const ulong  ULONG_MAX  = (~0);

//for button state
#define DOWN OFF
#define UP   ON
#define BUTTON_OK(f)(((f) & BUTTON_DOWN && button) || \
                     ((f) & BUTTON_UP && !button))


typedef struct {
  uint8  prescalar;
  ushort cycles;
} wdTime;


#define WD_16ms   0   
#define WD_32ms   1
#define WD_64ms   2
#define WD_128ms  3
#define WD_250ms  4    //These are from the data sheet but are not strictly
#define WD_500ms  5    // correct.  They should be powers of two (e.g. WD_256ms)
#define WD_1s     6    // but the watchdog isn't that accurate anyway so they're
#define WD_2s     7    // useful approximations.
#define WD_4s     8
#define WD_8s     9
#define WD_MAX    9
#define WDPS2MS(ps) ((ulong)(0x10<<(ps)))

const wdTime T0     = {0,0};
const wdTime T16ms  = {WD_16ms,1};
const wdTime T32ms  = {WD_32ms,1};
const wdTime T64ms  = {WD_64ms,1};
const wdTime T128ms = {WD_128ms,1};
const wdTime T250ms = {WD_250ms,1}; //really 256ms if the WDC was that accurate 
const wdTime T256ms = {WD_250ms,1}; 
const wdTime T500ms = {WD_500ms,1}; //512ms
const wdTime T512ms = {WD_500ms,1}; 
const wdTime T1s    = {WD_1s,1};    //1.024s
const wdTime T2s    = {WD_2s,1};    //2.048s
const wdTime T4s    = {WD_4s,1};    //4.096s
const wdTime T8s    = {WD_8s,1};    //8.192s  {WD_128ms*63}=8.064s

const wdTime T96ms  = {WD_32ms,3};   
const wdTime T3s    = {WD_1s,3};    //3.072s
const wdTime T6s    = {WD_2s,3};    //6.144s
const wdTime T10s   = {WD_1s,10};   //10.24s
const wdTime T1m    = {WD_1s,59};   //60.416s  {WD_2s,30}=61.44s
const wdTime T64s   = {WD_8s,8};    //65.536s 
const wdTime T5m    = {WD_8s,37};   //5.052m
const wdTime T15m   = {WD_8s,110};  //15.02m
const wdTime T1h    = {WD_8s,440};  //60.07m
const wdTime Tindefinite = {WD_MAX, USHORT_MAX};
const wdTime Tbutton     = {WD_MAX, USHORT_MAX};

#define isTindefinite(t) ((t).prescalar == WD_MAX && (t).cycles == USHORT_MAX)
#define isT0(t)          ((t).prescalar == 0      && (t).cycles == 0)
#define WD_MULT(t,m)   ((t).cycles *= (m))
#define WDTIME2MS(wdt) (WDPS2MS((wdt).prescalar) * (wdt).cycles)
#define WDTIME2S(wdt)  (WDTIME2MS(wdt)>>10) //dividing by 1024 is actually 
                                            // closer than 1000, see WD_250ms
                                            // above
#define Tequal(a, b)   (WDTIME2MS(a) == WDTIME2MS(b))

//Mode types for goToSleep
#define CLOCK_RUNNING    0b00000001
#define IO_OFF           0b00000010
#define BUTTON_DOWN      0b00000100
#define BUTTON_UP        0b00001000
#define INDEFINITE       0b00010000
#define isCLOCK_RUNNING(f)    ( (f) & CLOCK_RUNNING )
#define isIO_OFF(f)           ( (f) & IO_OFF )
#define isBUTTON_DOWN(f)      ( (f) & BUTTON_DOWN )
#define isBUTTON_UP(f)        ( (f) & BUTTON_UP )
#define isINDEFINITE(f)       ( (f) & INDEFINITE )

//for error()
typedef enum { FATAL, NON_FATAL } fatal_t;

//for nextMode()
typedef enum { NORMAL, HIDDEN } modeType_t;

Credits

Replications

Did you replicate this project? Share it!

Love this project? Think it could be improved? Tell us what you think!

Give feedback

Comments

Similar projects you might like

nRF24L01+ with ATtiny85 3 Pins
Intermediate
  • 26,169
  • 38

Full instructions

Sending and Receiving data with nRF24L01+ using only 3 pins of ATTiny85.

Darby's not dead.
Intermediate
  • 685
  • 17

Full instructions

atmega168 | pn532 | sparkCore | mac_osx > read Mifare cards over the internet!

SSD1306xLED Tinusaur ATtiny85 Library for SSD1306
Intermediate
  • 2,783
  • 12

Full instructions

SSD1306xLED is a C library for working with the SSD1306 display driver to control dot matrix OLED/PLED 128x64 displays.

LED Propeller (Rotating) Display
Intermediate
  • 1,198
  • 17

Persistence of vision enabling a propeller rotating at high speed to appear as a see-through circular display.

A Christmas tree, PCB ornament!
Intermediate
  • 292
  • 7

No more getting troubled on what Christmas gifts to get! These personalized PCB ornaments will leave everybody satisfied & inspired to hack!

Physical computing with ATtiny
Intermediate
  • 793
  • 15

Full instructions

Using Arduino to make home decoration more fun.

ProjectsCommunitiesContestsLiveAppsBetaFree StoreBlogAdd projectSign up / Login
Respect project
Feedback