Electronic Sculpture Archive
Bruce Cannon

This site is an archive of my older interactive,  electronic and computer-controlled sculpture.  You can always find out about my latest projects via my gateway, brucecannon.com.

   

timelife.c

Code Listing for the sculpture The Time of Your Life

Below is the code inside this counting sculpture.  More details are available on the technical notes page, and this piece's main page.   This code also relies on my standard header file my_prefs.h, my HP display driver hdsp2113.c, and my Dallas real time clock driver my_1302.c.  This is code for Custom Computer Services PIC C compiler.

As with all my code listings, you may need to copy them to an editor which supports 4-char tabs to see and print them correctly.

/*
timelife.C
05-28-99
Bruce Cannon
The Time of your Life is a personal seer, a Nostradamus' predictor for its owner.  The owner
tells me their birthday and other info, I look up their statistical demise in the actuarials,
and then set this piece.  It tells them how long they have left to go whenever they insert a
skeleton key.  An LED display decrements an eight bit binary number each second.
----------
This is the first piece in which I have added settability.  All my earlier pieces will need to be
reprogrammed whenever their battery dies.  This piece has a hidden user interface, so that I can
simply open it up, replace the battery, and reprogram it.  Because 20 years from now, who knows
whether I'll be able to get another PIC, whether I'll be able to program it, etc!
An extension of this idea would be to design all my PCBs with extra sockets for spare elements.
This piece would have a spare PIC, a spare ISD chip, and a spare display hidden in it.
----------
This piece used a simple recording strategy, in which I spoke all the text needed in a single message,
and trimmed out each element's address and duration by trial and error.  This works well!  The next
speech piece I do of this complexity I may try setting up an automated PC-based programming system,
creating sound files and trimming them, then recording them onto the chip at known addresses.  This
would allow me to use the auto-stop feature of the ISD chip, monitoring the EOM line instead of
timing speech.  But on the other hand, that presents its own set of difficulties when multitasking
as the EOM pulse is short.
*/
/**************************************************************************************************
Preprocessor setup.
**************************************************************************************************/
#INCLUDE <16c73a.h>
#fuses XT,NOWDT,NOPROTECT,PUT,BROWNOUT
#use delay(clock=4000000)
#include <my_prefs.h>
#include <my_1302.c>
#include <hdsp2113.c>
/***************************************************************************************************
System data definitions.
***************************************************************************************************/
#define _ISD_CE RA0
//Three pins defined in my_ds1302.c
#define KEY RA4
#define LED RA5
//Port B used as data bus, two pins doing double duty as set inputs:
#define SET_BUTTON RB6
#define INCREMENT_BUTTON RB7
enum {CURRENT,TARGET};			//Used by get_months(), store_months() and set_register().
enum {TARGET_HI,TARGET_LO};		//Used by get_months() and store_months(); NVRAM addresses.
//Speech-array handling labels:
enum {ADDRESS,DURATION};
enum {ONES,TEENS,DECADES,PHRASES};
enum {ONE = 1,TWO,THREE,FOUR,FIVE,SIX,SEVEN,EIGHT,NINE};
enum {TEN,ELEVEN,TWELVE,THIRTEEN,FOURTEEN,FIFTEEN,SIXTEEN,SEVENTEEN,EIGHTEEN,NINETEEN};
enum {TWENTY = 2,THIRTY,FORTY,FIFTY,SIXTY,SEVENTY,EIGHTY,NINETY};
enum {YOU_HAVE,A_MONTH,MONTHS,TO_LIVE,CHEATED_DEATH_BY,HUNDRED,CLOCK_PROGRAMMED,SCULPTURE_NEEDS_SERVICE};
//Array has three dimensions.  One accesses either sound starting addrs or corresponding durations.
//One selects from four categories of numbers and phrases.  One selects from one of up to ten elements
//in each category:
byte const speech[2][4][10] =
{
	//Addresses of words and phrases:
	{
		//Addresses for "one" - "nine":
		{0,	0x00,	0x02,	0x04,	0x06,	0x08,	0x0A,	0x0C,	0x0E,	0x10},
		//"Ten" - "nineteen":
		{0x12,	0x14,	0x16,	0x19,	0x1B,	0x1D,	0x1F,	0x22,	0x24,	0x26},
		//"Twenty" - "ninety":
		{0,	0,	0x29,	0x2B,	0x2D,	0x2F,	0x31,	0x33,	0x35,	0x37},
		//Various phrases:
		{0x39,	0x3C,	0x3F,	0x41,	0x44,	0x47,	0x4A,	0x4E,	0,	0}
	},
	//Durations of words and phrases in 100ms increments:
	{
		//Durations for "zero" - "nine":
		{0,	6,	6,	7,	7,	7,	7,	9,	6,	10},
		//"Ten" - "nineteen":
		{8,	7,	8,	7,	9,	8,	9,	8,	10,	9},
		//"Twenty" - "ninety":
		{0,	0,	7,	7,	7,	7,	7,	7,	7,	7},
		//Various phrases:
		{11,	11,	8,	9,	14,	9,	15,	15,	0,	0}
	}
};
/***************************************************************************************************
Switches.
***************************************************************************************************/
#define PREDICTED 504				//This is me at 38yrs, with 42 years left (504 mos).
#define BETWEEN_WORDS 20			//Pause between speech elements, in ms.
/***************************************************************************************************
Global variables.
***************************************************************************************************/
bit update_display;				//RTCC int sets this flag, main clears it.
byte rollovers,seconds;				//RTCC counter variables.
/***************************************************************************************************
Prototypes.
***************************************************************************************************/
void initialize_pic();
void set_system();
void set_register(byte which_register);
word get_DS_months(byte which_register);
void store_DS_months(byte which_register, word new_value);
byte BCD_to_bin(byte bcd);
byte bin_to_BCD(byte bin);
void heartbeat();
void display();
void enable_speech();
void say(byte dimension, byte element);
void say_number(word number);
void tell_fate();
void pause(byte length);
bit check_button(byte which_button);
bit check_clock();
/***************************************************************************************************
RTCC ISR, written for 4M xtal and RTCC div 256 prescaler, and tweaked to look good.
***************************************************************************************************/
#int_RTCC
clock()
{
	if(++rollovers >= 16)					//Increment rollover counter.
	{
		rollovers = 0;					//Clear counter.
		seconds--;					//Decrement display counter.
		update_display = YES;				//Set flag.
	}
}
/***************************************************************************************************
Initializes ports and peripherals, checks clock status and reports error if stopped,
speak time remaining if key turned, update binary count on display if RTCC flag.
***************************************************************************************************/
main()
{
	initialize_PIC();					//Set up ports.
	initialize_display();					//Set up HDSP2113.
	if(check_button(SET_BUTTON))				//Check for set each powerup.
	{
		set_system();					//If button pressed, enter set routine.
		say(PHRASES,CLOCK_PROGRAMMED);			//Give feedback when done.
	}
	if(!check_clock())
	{
		disable_interrupts(RTCC_ZERO);			//Turn off RTCC int.
		display_screen("SERVICE!");			//Print service message,
		blink_display(ON);				//enable all-character flashing.
		while(TRUE)					//loop here forever.
		{
			if(input(KEY))				//check for key turn,
			{
				say(PHRASES,SCULPTURE_NEEDS_SERVICE);	//Redundant message.
				while(input(KEY));			//Wait for unpress.
			}
		}
	}
	else							//If clock is running,
	{
		seconds = 255;					//Clear counter,
		update_display = NO;				//unset flag,
		while(TRUE)					//loop here forever:
		{
			if(input(KEY))				//check for key turn,
			{
				disable_interrupts(RTCC_ZERO);	//turn off RTCC int,
				tell_fate();			//get time/date and speak,
				while(input(KEY));		//wait for unpress.
				update_display = NO;		//unset flag.
				rollovers = 0;			//Clear counter.
				enable_interrupts(RTCC_ZERO);	//Turn int back on.
			}
			if(update_display)			//If clock_ISR set flag,
			{
				heartbeat();			//blink LED,
				display_binary(seconds);	//write new seconds value to HDSP2113,
				update_display = NO;		//unset flag again (set by RTCC ISR).
			}
		}
	}
}
/***************************************************************************************************
Sets up internal peripherals and ports.
***************************************************************************************************/
void initialize_pic()
{
	setup_adc(NO_ANALOGS);					//Unused.
	setup_adc(ADC_CLOCK_INTERNAL);
	setup_adc(ADC_OFF);
	setup_timer_1(T1_INTERNAL);				//Unused.
	setup_counters(RTCC_INTERNAL,RTCC_DIV_256);		//At 4MHz, gives ~15 rollovers per sec.
	enable_interrupts(RTCC_ZERO);				//Turn it on.
	enable_interrupts(global);
	//Port A:
	output_high(_ISD_CE);					//Neg true.
	output_low(DS_SCLK);
	output_low(DS_IO);
	output_high(_DS_RST);					//Neg true.
	output_low(LED);
	input(KEY);
	//Port B:
	set_tris_b(0);						//Address bus for HDSP and ISD.
	//Port C:
	//set_tris_c(0);					//Control pins for HDSP.
	//this port set up by HDSP2113.c
}
/***************************************************************************************************
Checks RTC clock-halt flag, and checks NVRAM for valid range.  Clock data can get corrupted without 
the clock stopping, so this isn't a perfect solution.  But it's better than nothing.
Takes:
-bit indicating real time registers or NVRAM registers.
Returns:
-word value of desired registers, converted from BCD and/or ANDed.
Assumes:
-constants CURRENT and TARGET are present.
-my_DS1302.c is present.
-BCD_to_bin() is present.
***************************************************************************************************/
bit check_clock()
{
	word test_months;
	
	if(read_ds1302(SC_ADDR) > 127)
	{
		return(NO);
	}
	test_months = get_DS_months(TARGET);
	if(test_months > 999)
	{
		return(NO);
	}
	return(YES);
}
	


/***************************************************************************************************
Using my DS1302 driver, gets current date data and converts from BCD, or stored NVRAM
data and returns it as is.
As DS is set up as a calender, to use it as a counter requires translating from the
following notation: years 0-99, months 1-12.
Takes:
-bit indicating real time registers or NVRAM registers.
Returns:
-word value of desired registers, converted from BCD and/or ANDed.
Assumes:
-constants CURRENT and TARGET are present.
-my_DS1302.c is present.
-BCD_to_bin() is present.
***************************************************************************************************/
word get_DS_months(byte which_register)
{
	byte bcd;
	word num_months;
	if(which_register == TARGET)
	{
		num_months = (word)read_DS_NVR(TARGET_HI);	//Get high byte of target year.
		num_months <<= 8;				//Shift up.
		num_months += (word)read_DS_NVR(TARGET_LO);	//AND in low byte of target year.
	}
	else
	{
		bcd = read_DS1302(YR_ADDR);			//Get current year.
		num_months = BCD_to_bin(bcd);			//Decode BCD years to binary.
		num_months *= 12;				//Multiply by 12 to get months.
		bcd = read_DS1302(MT_ADDR);			//Get current month.
		bcd = BCD_to_bin(bcd);				//Decode BCD months to binary.
		bcd--;						//Adj DS 1-based to my 0-based count.
		num_months += bcd;				//Add them together.
	}
	return(num_months);					//Send back result.
}
/***************************************************************************************************
Using my DS1302 driver, BCD encodes supplied real time months and writes to appropriate
time registers, or breaks supplied target months into two bytes and stores in DS NVRAM.
Takes:
-bit selecting either real time registers or NVRAM registers.
-word holding current or target months.
Returns:
-nothing.
Assumes:
-my_DS1302.c is present.
-bin_to_BCD() is present.
***************************************************************************************************/
void store_DS_months(byte which_register, word new_value)
{
	byte BCD,temp;
	word years;
	write_DS1302(WRITE_PROTECT,UNPROTECT);		//Enable writes.
	if(which_register == TARGET)			//Value provided is destined for NVRAM.
	{
		temp = (byte)(new_value >> 8);		//Isolate high byte.
		write_DS_NVR(TARGET_HI,temp);		//Store it.
		temp = (byte)new_value;			//Isolate low byte.
		write_DS_NVR(TARGET_LO,temp);		//Store it.
	}
	else						//Value destined for time registers.
	{
		temp = (byte)(new_value / 12);		//Calculate number of years.
		temp = bin_to_BCD(temp);		//Convert to BCD.
		write_DS1302(YR_ADDR,temp);		//Set new time value.
		temp = (byte)(new_value % 12);		//Calculate remaining months.
		temp++;					//Convert 0-based to 1-based.
		temp = bin_to_BCD(temp);		//Convert to BCD.
		write_DS1302(MT_ADDR,temp);		//Set new time value.
		write_DS1302(DT_ADDR,1);		//Set default time values,
		write_DS1302(DY_ADDR,1);		//	"
		write_DS1302(HR_ADDR,1);		//	"
		write_DS1302(MN_ADDR,0);		//	"
		write_DS1302(SC_ADDR,0);		//setting secs 0 will start clock if stopped.
	}
	write_DS1302(WRITE_PROTECT,PROTECT);		//Disable writes.
}
/***************************************************************************************************
Converts a BCD byte to a binary byte.
***************************************************************************************************/
byte BCD_to_bin(byte BCD)
{
	byte number;
	number = (BCD >> 4) * 10;			//Isolate ten's digit and promote.
	number += (BCD & 0x0F);				//Isolate ones digit and add in.
	return(number);
}
/***************************************************************************************************
Converts a binary value 0-99 to a BCD byte.
***************************************************************************************************/
byte bin_to_BCD(byte bin)
{
	byte bcd;
	BCD = (bin / 10) << 4;				//Isolate ten's digit and promote.
	BCD += (bin % 10);				//Isolate ones digit and add in.
	return(bcd);
}
/***************************************************************************************************
Gets DS1302 values, calculates time and speaks appropriate messages.
***************************************************************************************************/
void tell_fate()
{
	word current_months,target_months,difference;
	current_months = get_DS_months(CURRENT);	//Get time converted to binary months.
	target_months = get_DS_months(TARGET);		//Gets NVRAM values converted to binary months.
	say(PHRASES,YOU_HAVE);					//All cases need this speech.
	if(current_months <= target_months)			//If not there yet,
	{
		difference = target_months - current_months;	//get count.
		if(difference <= 1)				//If there,
		{
			say(PHRASES,A_MONTH);			//tell them.
		}
		else						//If not,
		{
			say_number(difference);			//tell how many left,
			say(PHRASES,MONTHS);			//say "months."
		}
		say(PHRASES,TO_LIVE);				//Say "to live."
	}
	else							//If past expiration,
	{
		say(PHRASES,CHEATED_DEATH_BY);			//tell them:
		difference = current_months - target_months;	//get count,
		if(difference <= 1)				//if a month or less,
		{
			say(PHRASES,A_MONTH);			//say it,
		}
		else						//otherwise,
		{
			say_number(difference);			//say specific number,
			say(PHRASES,MONTHS);			//say "months."
		}
	}
}
/***************************************************************************************************
Used to get around the compiler limitation on the size of numbers passed to the built-in
functions.
***************************************************************************************************/
void pause(byte length)
{
	byte time;
	for(time = 0; time <= length; time++)			//wait word length.
	{
		delay_ms(100);
	}
}
/***************************************************************************************************
Speaks the specified word or phrase.
***************************************************************************************************/
void say(byte dimension, byte element)
{
	byte time,length;
	length = speech[DURATION][dimension][element];		//Get sound duration.
	rp0 = PORT;						//Select register page 0.
	PORTB = speech[ADDRESS][dimension][element];		//Load speech address.
	output_low(_ISD_CE);					//Turn speech on.
	for(time = 0; time <= length; time++)			//wait word length.
	{
		delay_ms(100);
	}
	output_high(_ISD_CE);					//Turn it off again.
	delay_ms(BETWEEN_WORDS);				//For a better rhythm.
}
/***************************************************************************************************
Decodes the recieved binary number and speaks in english format.
***************************************************************************************************/
void say_number(word number)
{
	byte address;
	if(number > 999)					//Number too large.
	{
		display_screen(CLEAR_DISPLAY);			//For debugging.
		display_screen("ERROR");			//	"
	}
	else
	{
		if(number > 99)					//Say any hundreds digit:
		{
			say(ONES,(byte)(number / 100));		//say the digit,
			say(PHRASES,HUNDRED);			//say "hundred,"
			number %= 100;				//Reduce.
		}
		if(number > 19)					//If a decade value,
		{
			say(DECADES,(byte)(number / 10));	//mask to decades array pointer,
			number %= 10;				//and reduce again.
		}
		if(number > 9)					//If remainder is between ten and nineteen,
		{
			say(TEENS,(byte)number % 10);		//mask for teens array pointer and quit.
		}
		else						//If remainder is nine or less,
		{
			say(ONES,(byte)number % 10);		//mask for ones array and say final digit.
		}
	}
}
/***************************************************************************************************
Convenience function.
***************************************************************************************************/
void enable_speech()
{
	output_low(_ISD_CE);
	delay_cycles(1);
	output_high(_ISD_CE);
}
/***************************************************************************************************
Blinks LED in a heartbeat-like pattern.
***************************************************************************************************/
void heartbeat()
{
	output_high(LED);
	delay_ms(150);
	output_low(LED);
	delay_ms(150);
	output_high(LED);
	delay_ms(100);
	output_low(LED);
}
/***************************************************************************************************
Checks the state of the specified button, then returns that pin to an output.
Takes:
-byte holding CCS's port literal value.
Returns:
-bit mirroring state of input pin.
Assumes:
-values passed correspond to CCS pin codes.
***************************************************************************************************/
bit check_button(byte which_button)
{
	bit result;
	if(which_button == SET_BUTTON)
	{
		result = input(SET_BUTTON);
		output_low(SET_BUTTON);
	}
	else
	{
		result = input(INCREMENT_BUTTON);
		output_low(INCREMENT_BUTTON);
	}
	return(result);
}
/***************************************************************************************************
Sets system clock and stored target values using buttons inside piece.
On set button press, enters set mode: uses SET button to select data field, INCREMENT
button to change field contents.  On exiting set mode via another set button press, saves
new values to RTC and NVRAM.  Uses display's blinking character feature to indicate field
being changed.  Other than constraining fields, does not check for valid data.  Upon exit,
clock is started, set flag is unset.
Takes:
-nothing.
Returns:
-nothing.
Assumes:
-nothing.
***************************************************************************************************/
void set_system()
{
	initialize_DS1302();					//Un-write-protect clock, set up
								//charger, start count.
	set_register(CURRENT);
	set_register(TARGET);
	write_DS1302(WRITE_PROTECT,PROTECT);			//Lock the clock down again.
	while(check_button(SET_BUTTON));			//Wait for final unpress.
	delay_ms(500);						//Debounce.
}
/***************************************************************************************************
Gets specified system value from NVRAM, displays, accepts user inputs, saves changes.
Uses SET button to select data field, INCREMENT button to change field contents.
On exiting set mode via another set button press, saves new values to RTC and NVRAM.
Uses display's blinking character feature to indicate field being changed.
Other than constraining fields to '0'-'9', does not check for valid data.
Takes:
-Bit selecting which system value, current months or target months.
Returns:
-nothing.
Assumes:
-nothing.
***************************************************************************************************/
void set_register(byte which_register)
{
	byte position,dimension,digit;
	byte digits[2][3];
	word num_months;
	num_months = get_DS_months(which_register);				//Get current value.
	if(num_months > 999)							//If corrupt, reset.
	{
		num_months = 0;							//Never attempt to display
	}									//if out of bounds.
	//Decode selected number for setting:
	digits[which_register][0] = (byte)(num_months / 100);			//Isolate hundreds digit.
	num_months %= 100;							//Isolate lower digits.
	digits[which_register][1] = (byte)(num_months / 10);			//Isolate tens digit.
	digits[which_register][2] = (byte)(num_months % 10);			//Isolate ones digit.
	//Put up set screen:
	display_screen(CLEAR_DISPLAY);						//Clear to position zero.
	if(which_register == TARGET)						//Indicate register being set.
		display_screen("TRGT ");					//	"
	else
		display_screen("CURR ");					//	"
	//Display current number value:
	for(digit = 0; digit < 3; digit++)					//Display each digit in number.
	{
		display_screen(digits[which_register][digit] + '0');		//	"
	}
	//Perform set routine for each digit in number:
	for(digit = 0, position = 5; digit < 3; digit++,position++)
	{
		flash_character(ON,position);					//Flash digit.
		while(check_button(SET_BUTTON));				//Wait for unpress.
		delay_ms(500);							//Debounce.
		while(!check_button(SET_BUTTON))				//Wait for press again.
		{
			if(check_button(INCREMENT_BUTTON))			//Every button press,
			{
				++digits[which_register][digit];		//increment character,
				if(digits[which_register][digit] > 9)		//wrapping as needed.
				{
					digits[which_register][digit] = 0;
				}
				display_char(position,digits[which_register][digit] + '0');	//update screen.
				delay_ms(500);							//Debounce.
				while(check_button(INCREMENT_BUTTON));				//Wait for unpress.
			}
		}								//On next set button,
		flash_character(OFF,position);					//Turn off digit.
	}
	//Recode new value for storage:
	num_months = (word)digits[which_register][0] * 100;			//Convert hundreds place.
	num_months += ((word)digits[which_register][1] * 10);			//Convert and add in tens place.
	num_months += (word)digits[which_register][2];				//Add ones place.
	store_DS_months(which_register, num_months);				//Save new value.
}

 

 

Site contents licensed under a
Creative Commons License.

Creative Commons License