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

Arduino HDD POV Clock (Persistence of Vision)

Аn interesting unusual clock made from an old hard drive and an Arduino Nano.

AdvancedFull instructions provided6,901
Arduino HDD POV Clock (Persistence of Vision)

Things used in this project

Hardware components

Arduino Nano R3
Arduino Nano R3
×1
DS1307 64 x 8, Serial, I²C Real-Time Clock
Maxim Integrated DS1307 64 x 8, Serial, I²C Real-Time Clock
×1
rgb led strip 5050
×1
IR receiver (generic)
×1
IR transmitter (generic)
×1
General Purpose Transistor NPN
General Purpose Transistor NPN
×3
Resistor 1k ohm
Resistor 1k ohm
×3

Schematics

schematic

Code

code

Arduino
/**
 hdpov.c - Arduino driver for Hard Drive POV
 Copyright (C) 2008  Giles F. Hall 

 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, version 2;

 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, write to the Free Software
 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
**/

/**
 ** Author: Giles F. Hall 
 ** Email: <ghall -at- csh (dot) rit (dot) edu>
 **/

#ifndef F_CPU
#define F_CPU           16000000UL  // 16 MHz
#endif

#include                <stdlib.h>
#include                <avr/io.h>
#include                <avr/interrupt.h>
#include                <util/delay.h>
#include                <Wire.h>
#include                "Chronodot.h"

// arduino redefines int, which bugs out stdio.h (needed for sscanf)
// see: http://www.arduino.cc/cgi-bin/yabb2/YaBB.pl?num=1207028702/3
#undef int
#include                <stdio.h>

byte incomingByte;
int ser_hour,ser_minute,ser_second;

int r1 = 1;
int g1 = 0;
int b1 = 1;
int background;

// The number of "pie slices" that make up a single image around the platter
#define DIVISIONS       0xFF

// Red pin, on PORTD
#define RED             3

// Green Pin, on PORTD
#define GRN             4

// Blue pin, on PORTD
#define BLU             5

// Macro used to communicate serial status
#define OK              1

// Number of pages in frame buffer
#define PAGES           2

// Helper macro to build LED values for PORTD
#define RGB(R,G,B)      (R << RED | G << GRN | B << BLU)

// The timers are configured with the prescaler set to 8, which means every
// 8 clock cycles equals on tick on the counter.  This is a constant to help
// convert timer cycles back to real time.
#define FREQ_DIV_8      (F_CPU / 8)

// Defines how many ticks a millisecond equals on our clock
#define MILLITICK       (FREQ_DIV_8 * .001)

// I have found that the sensor in my rig is prone to triggering the interrupt
// immediately after a real signal, likely related to capactive effects.  This
// tricks the system into thinking the platter is suddenly traveling a lot
// faster, and can cause visible glitches.  A 15K RPM drive spins 250 times a
// second, where a single revolution requires 4ms.  4ms is exactly 8000 timer
// cycles on our timer, which means anything half of 8000 is going faster than
// any known HD.  If such a value is captured, it is ignored.
#define SPURIOUS_INT    (2 * MILLITICK)

// Helper macros for frobbing bits
#define bitset(var,bitno) ((var) |= (1 << (bitno)))
#define bitclr(var,bitno) ((var) &= ~(1 << (bitno)))
#define bittst(var,bitno) (var& (1 << (bitno)))

// It is technically possible to change the number of divisions, even
// though this is not currently implemented.  This const serves as a place
// holder for future enhancements.
const unsigned char divisions = DIVISIONS;

// The current slice being drawn
volatile unsigned char current_slice;

// The period of the platter in timer ticks
int period;

// The value of the hidden page
volatile int page_hidden;

// The value of the visible page
volatile int page_visible;

// A flag representing the need for the pages to be flipped
volatile unsigned char page_flip_flag;

// The double buffered frame buffer
unsigned char FrameBuffer[PAGES][DIVISIONS];

/**
 ** FrameBuffer / Page routines
 ** 
 ** The framebuffer is double buffered, which allows you to write updates
 ** to the "hidden" page while the code draws from the "visible" page.
 **
 ** page_flip() makes the hidden page the visible page, and vice-versa.
 **
 ** copy_page() allows you to copy the visible page back to the hidden page, 
 ** so you don't have to redraw the entire frame if you are just making a 
 ** small change.  
 **
 ** clear_page() clears the hidden page.
 **
 ** write_page() writes to the hidden page.
 ** 
 ** See RunStartupDisplay() for a simple example.
 **/

// sit and wait for the page to be flipped
void __inline__ wait_for_page_flip(void)
{
    while (page_flip_flag) {}
}

// request a page flip, and wait for the sensor interrupt to flip the page
void __inline__ flip_page(void)
{
    page_flip_flag = 1;
    wait_for_page_flip();
}

// copy the visible page to the hidden page
void __inline__ copy_page(void)
{
    int x;
    for(x = 0; x < divisions; x++)
    {
        FrameBuffer[page_hidden][x] = FrameBuffer[page_visible][x];
    }
}

// write a value to the hidden page
void __inline__ write_page(unsigned char slice, unsigned char val)
{
    FrameBuffer[page_hidden][slice] = val;
}

// clear the hidden page
void clear_page(void)
{
    int x;
    for(x = 0; x < divisions; x += 1)
    {
        FrameBuffer[page_hidden][x] = 0;
    }
}

// called by the interrupt to flip to the next page
void __inline__ flip_to_next_page(void)
{
    if (page_flip_flag)
    {
        page_visible = page_hidden;
        page_hidden = !page_hidden;
        page_flip_flag = 0;
    }
}


/**
 ** Setup
 **/

// Simple test pattern displaying all the available colors, evenly spaced
void InitTestPattern1(void)
{
  int x;
  for(x = 0; x < divisions; x++)
  {
      switch (x / (divisions / 8))
      {
        case 0:
          write_page(x, RGB(0,0,0));
          break;  
        case 1:
          write_page(x, RGB(1,0,0));
          break;
        case 2:
          write_page(x, RGB(0,1,0));
          break;
        case 3:
          write_page(x, RGB(0,0,1));
          break;
        case 4:
          write_page(x, RGB(1,1,0));
          break;  
        case 5:
          write_page(x, RGB(1,0,1));
          break;  
        case 6:
          write_page(x, RGB(0,1,1));
          break;  
        case 7:
        default:
          write_page(x, RGB(1,1,1));
          break;
      }
    }
}

// Simple test pattern displaying all the available drawable slices.
void InitTestPattern2(void)
{
  int x;
  for(x = 0; x < divisions; x++)
  {
      switch (x % 8)
      {
        case 0:
          write_page(x, RGB(0,0,0));
          break;  
        case 1:
          write_page(x, RGB(1,0,0));
          break;
        case 2:
          write_page(x, RGB(0,1,0));
          break;
        case 3:
          write_page(x, RGB(0,0,1));
          break;
        case 4:
          write_page(x, RGB(1,1,0));
          break;  
        case 5:
          write_page(x, RGB(1,0,1));
          break;  
        case 6:
          write_page(x, RGB(0,1,1));
          break;  
        case 7:
          write_page(x, RGB(1,1,1));
          break;
      }
    }
}

Chronodot RTC;
void chronotime(void){
  int h; int m; int s; int hour; int minute; int second; 
  
  DateTime now = RTC.now();
  
  h = now.hour();
  m = now.minute();
  s = now.second();
  
  if (h >= 12){
    h = h - 12; 
  }

  //Serial.println(h);

  hour = (DIVISIONS * (h / 12.0) + (DIVISIONS / 12.0) * (m / 60.0));
  minute = (DIVISIONS * (m / 60.0));
  second = (DIVISIONS * (s / 60.0));
  
  clear_page();
  
  int n;
  for (n = 0; n < DIVISIONS; n++){
    if (n == hour){
      write_page(n-5, RGB(1,0,0));
      write_page(n-4, RGB(1,0,0));
      write_page(n-3, RGB(1,0,0));
      write_page(n-2, RGB(1,0,0));
      write_page(n-1,RGB(1,0,0));   
      write_page(n, RGB(1,0,0));
      write_page(n+1, RGB(1,0,0));
      write_page(n+2, RGB(1,0,0));
      write_page(n+3, RGB(1,0,0));
      write_page(n+4, RGB(1,0,0));
      write_page(n+5, RGB(1,0,0));
} 
    if (n == minute){
      write_page(n-5, RGB(0,1,0));
      write_page(n-4, RGB(0,1,0));
      write_page(n-3, RGB(0,1,0));
      write_page(n-2, RGB(0,1,0));
      write_page(n-1,RGB(0,1,0));   
      write_page(n, RGB(0,1,0));
      write_page(n+1, RGB(0,1,0));
      write_page(n+2, RGB(0,1,0));
      write_page(n+3, RGB(0,1,0));
      write_page(n+4, RGB(0,1,0));
      write_page(n+5, RGB(0,1,0));
    }
    if (n == second){
      write_page(n-5, RGB(0,0,1));
      write_page(n-4, RGB(0,0,1));
      write_page(n-3, RGB(0,0,1));
      write_page(n-2, RGB(0,0,1));
      write_page(n-1,RGB(0,0,1));   
      write_page(n, RGB(0,0,1));
      write_page(n+1, RGB(0,0,1));
      write_page(n+2, RGB(0,0,1));
      write_page(n+3, RGB(0,0,1));
      write_page(n+4, RGB(0,0,1));
      write_page(n+5, RGB(0,0,1));

    } 
  
    int div = (DIVISIONS / 12.0);
    
    for (int x =0; x < 12; x++){
       int slice = int(x * div);
       write_page(slice, RGB(0,1,1)); 
    }
    
  
    if ( n != hour && n != minute && n != second){
      write_page(n, RGB(r1,g1,b1)); 
    }
  }
  flip_page();
  copy_page();
}

void tempature(void)
{

 DateTime now = RTC.now();
 
 int temp;
 temp = now.tempF();
 
 clear_page();
 
 int k;
 for (k =0; k < temp; k++){
  if (k < 25){
   write_page(k, RGB(0,0,1));
  } 
  if (k > 25 && k < 50){
   write_page(k, RGB(0,1,0));
  } 
  if (k > 50 && k < 75){
   write_page(k, RGB(1,1,0));
  } 
  if (k > 75 && k < 100){
   write_page(k, RGB(1,0,0)); 
  }
  if (k > 100  && k < 125){
    write_page(k, RGB(1,0,1));
  }
   
 }
 flip_page();
 copy_page();
  
}


// Go through all pages of the frame buffer, and set them to 0
void init_pages(void)
{
    int page;
    int x;
    for(page = 0; page < PAGES; page += 1)
    {
        for(x = 0; x < divisions; x += 1)
        {
            FrameBuffer[page][x] = 0;
        }
    }
    page_hidden = 0;
    page_flip_flag = 0;
}

// Configure the Serial Port, LED Pins, Timers, and Hardware Interrupt
void SetupHardware(void)
{
  // setup serial
  Serial.begin(9600);

  Wire.begin(9600);
  Serial.println("Initializing Chronodot.");
  RTC.begin();
  
  if (! RTC.isrunning()) {
    Serial.println("RTC is NOT running.");
    RTC.adjust(DateTime(__DATE__,__TIME__));
  }

  //RTC.adjust(DateTime(__DATE__,__TIME__));  //Un-comment to set time to comile time then re-commentf

  Serial.print("[");
  // setup output
  pinMode(RED, OUTPUT);
  pinMode(GRN, OUTPUT);
  pinMode(BLU, OUTPUT);
  Serial.print("L");

  // disable global interrupts
  cli();

  // setup timer0 - 8bit
  // resonsible for timing the LEDs
  TCCR0A = 0;
  TCCR0B = 0;  
  // select CTC mode
  bitset(TCCR0A, WGM01);
  // select prescaler clk / 8
  bitset(TCCR0B, CS01);
  // enable compare interrupt
  bitset(TIMSK0, OCIE0A);
  Serial.print("0");
  
  // setup timer1 - 16bit
  // responsible for timing the rotation of the platter
  TCCR1B = 0;
  TCCR1A = 0;
  // select prescaler clk / 8
  bitset(TCCR1B, CS11);
  // reset timer
  TCNT1 = 0;
  // enable overflow interrupt
  bitset(TIMSK1, TOIE1);
  Serial.print("1");

  // configure the platter interrupt PIN
  // int0, on any change
  EICRA = _BV(ISC00);
  //EICRA = _BV(ISC01);
  // Enable the hardware interrupt.
  EIMSK |= _BV(INT0);
  Serial.print("G");

  // set the rotational period to 0
  period = 0;
  
  // init pages
  init_pages();

  // enable global interrupts
  sei();

  Serial.println("]");
  Serial.flush();
}


/**
 ** Serial Output
 **/

// Report the status of the system, plus important variables over the serial
// port
void report_status_to_serial(void)
{
  unsigned int rpm = 0;
  if (period)
  {
      rpm = (int)(((float)FREQ_DIV_8) / ((float)period) * 60.0);
  }
  Serial.print("Revolutions / Minute: ");
  Serial.println(rpm, DEC);
  Serial.print("Ticks / Revolution: ");
  Serial.println(period, DEC);
  Serial.print("OCR0A: ");
  Serial.println(OCR0A, DEC);
  Serial.print("Divisons: ");
  Serial.println(divisions, DEC);
  Serial.print("Current Page: ");
  Serial.println((!(page_hidden)), DEC);
  Serial.print("Red: 0x");
  Serial.println(_BV(RED), HEX);
  Serial.print("Green: 0x");
  Serial.println(_BV(GRN), HEX);
  Serial.print("Blue: 0x");
  Serial.println(_BV(BLU), HEX);
}

// Read the entire visible page to the serial port, printing
// each byte as a hexadecimal character
void read_page_to_serial(int page)
{
    int slice;

    for(slice = 0; slice < divisions; slice++)
    {
        Serial.print("0x");
        Serial.println(FrameBuffer[page][slice], HEX);
        Serial.flush();
    }
}

/**
 ** Serial Input
 **/

// Sit and spin until there is serial data available
void __inline__ wait_for_serial_input()
{
    while (Serial.available() < 1) {}
}

// read a single byte from the serial port, encoded in hex
int read_byte(void)
{
    int ret;
    char rstr[2];

    wait_for_serial_input();
    rstr[0] = Serial.read();
    wait_for_serial_input();
    rstr[1] = Serial.read();
    sscanf(rstr, "%x", &ret);
    return ret;
}

// read an entire page from the serial port and write it to the framebuffer
int write_page_from_serial(void)
{
    int val;
    int slice;
    int retval = OK;

    for(slice = 0; slice < divisions; slice++)
    {
        val = read_byte();
        write_page(slice, val);
    }

    return retval;
}

/**
 ** Setup
 **/

// Simple animation to demonstrate the system is working
void RunStartupDisplay(void)
{
    int color;
    int slice;

    clear_page();
    flip_page();

    for (color = 0; color < 4; color++)
    {
        for (slice = 0; slice < divisions; slice += 2)
        {
            switch(color)
            {
                case 0:
                    /* red */
                    write_page(slice, _BV(RED));
                    write_page(slice+1, _BV(RED));
                    break;
                case 1:
                    /* green */
                    write_page(slice, _BV(GRN));
                    write_page(slice+1, _BV(GRN));
                    break;
                case 2:
                    /* blue */
                    write_page(slice, _BV(BLU));
                    write_page(slice+1, _BV(BLU));
                    break;
                case 3:
                    /* black */
                    write_page(slice, 0);
                    write_page(slice+1, 0);
                    break;
            }
            flip_page();
            copy_page();
        }
    }

    clear_page();
    flip_page();
}

void set_time()
{
   Wire.beginTransmission(104);
   Wire.send(0);
   Wire.send(decToBcd(ser_second));
   Wire.send(decToBcd(ser_minute));
   Wire.send(decToBcd(ser_hour));
   Wire.endTransmission();
}

byte decToBcd(byte val)
{
  return ( (val/10*16) + (val%10) );
}

byte bcdToDec(byte val)
{
  return ( (val/16*10) + (val%16) );
}

// Top level setup, called by the Arduino core
void setup(void)
{
    SetupHardware();
    RunStartupDisplay();
}

/**
 ** Main Loop
 **/

// Top level loop, call by the Arduino core
void loop(void)
{
    int cmd;
    int slice, value;
    int okval = OK;
    int i; int j;
    
    if (Serial.available()) {
    incomingByte = Serial.read();
    Serial.print(int(incomingByte));
    

    if ((int(incomingByte) == 240)) {
      background = Serial.read();
      r1 = 0; g1 = 1; b1 = 1;
         
    }

    if ((int(incomingByte) == 241)) {
      background = Serial.read();
      r1 = 1; g1 = 1; b1 = 0;
    }

    if ((int(incomingByte) == 242)) {
      background = Serial.read();
      r1 = 1; g1 = 0; b1 = 1;
    }

    if ((int(incomingByte) == 252)) {
      ser_hour = Serial.read();
      Serial.println("Set hour: ");
      Serial.println(ser_hour);
      // following line sets the RTC time
      set_time();
    }
    
    if ((int(incomingByte) == 253)) {
      ser_minute = Serial.read();
      Serial.println("Set minute: ");
      Serial.println(ser_minute);
      // following line sets the RTC time
      set_time();
    }    
    
    if ((int(incomingByte) == 254)) {
      ser_second = Serial.read();
      Serial.println("Set second: ");
      Serial.println(ser_second);
      // following line sets the RTC time
      set_time();
    }
    
    if ((int(incomingByte) == 255)) {
      for (j =0; j < 360; j++)
      {
       tempature(); 
      }
    }

  }else{
      chronotime();
   }
  
 }

/**
 ** Interrupts
 **
 ** The interrupts provide the accurate timing needed for this project.
 ** 
 **/

// This interrupt is called on every pulse of the sensor, which represents
// one full rotation of the platter.  It is responsible for timing the
// revolution, and for initiating the first draw instance.
ISR(INT0_vect)
{
  // Capture the 16bit count on timer1, this represents one revolution
  period = TCNT1;
  // If the period is shorter than our threshold, don't do anything
  if(period < SPURIOUS_INT)
  {
    return;
  }
  // Reset timer1 so it can clock the next revolution
  TCNT1 = 0;
  // Reset timer0 so it can start to accurately paint the slot 
  TCNT0 = 0;
  // If there is a page flip request, flip to the next page
  flip_to_next_page();
  // Write out the first slice to the LEDs to PORTD
  PORTD = FrameBuffer[page_visible][0];
  // Divide the time of single platter rotation by the number of drawable
  // divisions
  OCR0A = (period / divisions);
  // Set our current slice to 1, since we just drew slice 0
  current_slice = 1;
}

// This interrupt is called every time timer0 counts up to the 8bit value
// stored in the register "ORC0A", which is configured in INT0 interrupt.
// It is responsible for drawing out all the slices of the frame buffer
// during the exact moment the slot is in its proper rotational position.
// Using a 7200RPM drive with 255 divisions, this interrupt is called 
// 31,200 times a second.
ISR(TIMER0_COMPA_vect) {
  // Write out the LED value to the Frame Buffer
  PORTD = FrameBuffer[page_visible][current_slice];
  // Increment current slice, making sure to wrap it if it
  // has accidently gotten too large
  current_slice = ((current_slice + 1) % divisions);
}

// If the platter spin time overflows timer1, this is called
ISR(TIMER1_OVF_vect) {
}

Credits

Mirko Pavleski
173 projects • 1398 followers
Contact

Comments

Please log in or sign up to comment.