Hi everyone. This is my first micro controller project. It was a fun learning experience, and so far I'm happy with the results. There is still some work to do to get it finished and deployed, but I think the electronics design and coding portion is more or less in its final form. I might resize the current limiting resistor for the IR LED to stretch the battery life, but that would probably be the biggest change. Now I need to fit it in some waterproof containers and make sure that the lens and the IR LED are positioned correctly.
As stated in the thread title, this is an active IR beam break sensor. I am using it to tell a DSLR to take a set of pictures when the beam is broken, aiming for a system I can set up in the woods for an extended period to take pictures of animals that are usually elusive. There are two halves to this, each controlled by an attiny85. The first half sends a modulated IR signal, and the second half tells the camera to take pictures if it misses an IR pulse. That's the main function, though there is also voltage monitoring of the batteries on each end and input on startup on the receiver end to set the number of pictures it takes each time and how much time should be between each picture.
I'm testing now to see how long a pair of 18650 batteries on each end will last, but I'd like to have them last a few weeks. A month or two would be great. I'm also still working on the PCB design, but that may wait until the battery needs are finalized.
Thanks for taking a look! Suggestions welcome if you see something that could be done better.
Emitter Code
Clockspeed is 1MHz
Programmed with ATTinyCore using Arduino as ISP
/*ATTiny85 Pinout
(PCINT5/RESET#/ADC0/dW)PB5 1| |8 VCC
Unused PCINT3/XTAL1/CLKI/OC1B#/ADC3)PB3 2| |7 PB2(SCK/USCK/SCL/ADC1/T0/INT0/PCINT2) Unused
Unused (PCINT4/XTAL2/CLKO/OC1B/ADC2)PB4 3| |6 PB1(MISO/DO/AIN1/OC0B/OC1A/PCINT1) ***PWM output
GND 4| |5 PB0 (MOSI/DI/SDA/AIN0/OC0A/OC1A/AREF/PCINT0) Unused
*/
#include <avr/io.h>
#include <avr/interrupt.h>
#include "tinysnore.h"
//Initiallize LED_pin variable and set it to digital pin 1 (PB1), which is physical pin 6
const int LED_pin = 1;
float battVaverage = 0;
int n = 0;
void setup() {
checkVoltageSetup();
//Set LED_pin to output and a low state
pinMode(LED_pin, OUTPUT);
digitalWrite(LED_pin, LOW);
//Set up timer for 38kHz signal in fast PWM mode
TCCR1 = 0; // Stop Timer1
TCNT1 = 0; // Reset Timer1 count
GTCCR |= (1<<PSR1); // Reset prescaler
//Turn on fast PWM mode (1<<PWM1A), enable output on OC1A pin (1<<COM1A1),
//and set prescaler to 1 (1<<CS10). See register description in section 12.3 of ATTiny85 datasheet
TCCR1 = B01100001;//(1<<PWM1A)|(1<<COM1A1)|(1<<CS10);
OCR1C = 25; //Set TOP value of counter to 25 to get a 38.4kHz signal. Follow formula on p.87 of datasheet
OCR1A = 12; //Set approximately 50% duty cycle (half of OCR1C)
//Set unused pins to input and disable pullups. See 'Configuring the Pin' on p. 54 of datasheet and http://www.gammon.com.au/power
DDRB = (0<<DDB4) | (0<<DDB3) | (0<<DDB2) | (0<<DDB0);
MCUCR |= (1<<PUD);
}
void loop() {
TCCR1 |= (1<<COM1A1);//Turn on PWM output
delayMicroseconds(500);
TCCR1 &= ~(1<<COM1A1);//turn off PWM output
n++; //increase count up to voltage measurement
if (n > 85713){ //checks voltage approximately every 30 minutes
checkVoltage();
n = 0;
if (battVaverage < 3.15){ //Want to shut down before batter reaches 3V, adding 5% margin of error.
shutDown();
}
}
else {
snore(20);//Power down for 20ms
}
}
void checkVoltageSetup(){
ADMUX = 0; //clear ADMUX which sets Vcc as reference voltage
ADMUX |= (1 << MUX3) | (1 << MUX2); // use 1.1 bandgbap voltage for input
ADCSRA |= (1 << ADPS2); // set prescaler to 16 since using 1MHz clock speed
}
void checkVoltage(){
ADCSRA |= (1 << ADEN); // Enable ADC
delayMicroseconds(70); // allow bandgap voltage to settle. P.165 ATTiny85 datasheet
int samples = 4;
float battVmeasure [samples];
float battVsum = 0;
long adcvalue = 0;
for(int i=0; i<samples; i++){
ADCSRA |= (1 << ADSC); // start ADC measurement
while (ADCSRA & (1 << ADSC) ); // wait till conversion complete
adcvalue = ADC;
// Convert ADC value to voltage. Bandgap voltage should be between
// 1.0 and 1.2 and needs experimentation for each processor to get the right value
battVmeasure[i] = 1.09 * 1024 / adcvalue;
battVsum += battVmeasure[i]; // Add each voltage as loop progresses
}
battVaverage = battVsum / samples; // Average all the voltage measurements
PORTB |= (1<<PORTB4); // turn on pull up resistor for PB4
ADCSRA &= ~(1<<ADEN); // turn off ADC
}
void shutDown(){
TCCR1 |= (1<<COM1A1); //Turn on PWM output
delay(3); //Send long pulse to tell receiver unit to shut down
TCCR1 &= ~(1<<COM1A1); //turn off PWM output
MCUCR |= (1<<SE) | (1<<SM1) | (0<<SM0); //Enable power-down sleep mode
\
do { \
__asm__ __volatile__("sleep" \
"\n\t" ::); \
} while (0);
}
Receiver Code
Clockspeed is 1MHz
Programmed with ATTinyCore using Arduino as ISP
/*This code waits for pulses of IR light from the other end of the camera trap trigger, the IR emitter.
When it receives a short pulse, which is repeated once every 20ms, it wakes the device and resets the
ATTiny85's watchdog timer. The timer is set up to overflow once ever 32ms. An overflow will wake the
device and send a signal to the DSLR to take a set number of pictures. The number of pictures (1-9) and
the time between them is set via buttons on startup. As long as the short pulse is received every 20ms,
the timer should never overflow. An obstruction blocking the IR will allow the watchdog timer to
overflow. If a longer pulse is received, that means that the IR emitter is low on battery and will
shut down. This triggers the receiver end to shut down as well.*/
/*ATTiny85 Pinout
(PCINT5/RESET#/ADC0/dW)PB5 1| |8 VCC
Button input PCINT3/XTAL1/CLKI/OC1B#/ADC3)PB3 2| |7 PB2(SCK/USCK/SCL/ADC1/T0/INT0/PCINT2) SCL
Camera (PCINT4/XTAL2/CLKO/OC1B/ADC2)PB4 3| |6 PB1(MISO/DO/AIN1/OC0B/OC1A/PCINT1) PCINT
GND 4| |5 PB0 (MOSI/DI/SDA/AIN0/OC0A/OC1A/AREF/PCINT0) SDA
*/
#include <avr/io.h>
#include <avr/interrupt.h>
#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd(0x27, 16, 2); //set the LCD address to 0x27 for a 16 chars and 2 line display
int shutter = 4; //PB4 is used to send signal to camera.
volatile int IRsensor = 1; //B1 is used for input from sensor
volatile int n = 0; //counter
volatile long signalStart = 0; //time that signl from IR sensor starts
volatile int signalLength = 0; //Length of time of signal from IR sensor
int num_photo = 1; //number of photos to take each time the device is triggered. Set during photoTimeInput()
int WDTcycles = 0; //number of 16ms
volatile bool tripwire = false; //flag set by watchdog timer to trigger photo routine
float battVaverage = 0; //average of ADC readings of VCC
volatile int oldPin1; //value of pin 1, used to determine if it caused interrupt
void setup() {
photoTimeInput(); //take input to set number of pictures/trigger and time between them. See below
checkVoltageSetup(); //Set up ADC to check Vcc. Important to do after photoTimeInput as it uses ADC for button input.
//Interrupts setup
cli(); //Disable interrupts.
USICR = 0; //Disable serial communication to LCD. See p. 116 of datasheet
USISR = 0; //I'm not sure if all of these are needed, but the interrupts
USIDR = 0; //in the serial communicaiton with the LCD were causing problems
USIBR = 0; //with the other interrupts, and the LCD is not needed anymore.
//Pin change interrupt setup
DDRB |= (0 << DDB1); //set PB1 as input
PORTB |= (1 << PORTB1); //enable pullup resistor
PCMSK = 0; //Clear PCMSK, disable all pin change interrupts
PCMSK |= (1 << PCINT1); //Enable pin change interrupt on PB1
GIFR = 0; //Reset interrupt flags
GIMSK |= (1 << PCIE); //Enable pin change interrupts
sei(); //Enable interrupts
}
void loop() {
if (tripwire == true) {
photo();
tripwire = false;
}
if (n > 85713) { //checks voltage approximately every 30 minutes
checkVoltage();
n = 0;
if (battVaverage < 3.15) { //Want to shut down before batter reaches 3V, adding 5% margin of error.
watchdogOff(); //Make sure watchdog timer is disabled so the MCU stays powered down.
shutDown();
}
}
if (signalLength > 2500) { //If the emitter is low on battery, it will send a long 3s signal to tell this receiver to power down too.
watchdogOff(); //Make sure watchdog timer is disabled so the MCU stays powered down.
shutDown();
}
watchdog32(); //Set watchdog timer to go off after 32ms
shutDown();
watchdogOff(); //Turn watchdog timer off when MCU wakes up
}
void watchdog32() {
//Watchdog timer 32ms setup. See Watchdog Timer Control Register on p. 45-46 of datasheet.
// Enable WDT change. See p. 43 of datasheet
WDTCR |= (1 << WDE) | (1 << WDCE);
// Enable WDT (1<<WDE), set to interrupt instead of reset (1<<WDIE), clear flag (1<<WDIF), set prescaler to 4k for 32ms timeout (1 << WP0)
WDTCR = B11001001;
}
void watchdog16() {
//Watchdog timer 16ms setup. See Watchdog Timer Control Register on p. 45-46 of datasheet.
// Enable WDT change. See p. 43 of datasheet
WDTCR |= (1 << WDE) | (1 << WDCE);
// Enable WDT (1<<WDE), set to interrupt instead of reset (1<<WDIE), clear flag (1<<WDIF), set prescaler to 2 for 16ms timeout WPD[2:0] = 0
WDTCR = B11001000;
}
void watchdogOff() {
//Turn off watchdog timer
//Enable WDT change. See p. 43 of datasheet
WDTCR |= (1 << WDE) | (1 << WDCE);
//Disable WDT
WDTCR = 0;
}
ISR(PCINT0_vect) {
int newPin1 = PINB & B00000010; //Faster version of digitalRead(). Replacing digitalRead with this allowed the code in the ISR to run fast enough
if (newPin1 != oldPin1) { //Check to see if pin changed. If not, it is an interrupt from some other source and the rest of the actions will be skipped.
oldPin1 = newPin1;
if (newPin1 == 0) { //Check to see if value is LOW. This means the interrupt was caused by the falling edge at the start of the LOW signal from IR sensor.
n++; //Increase the voltage check counter
signalStart = micros(); //Log the time that the signal started.
} else { //If the value is HIGH, the interrupt was caused by the rising edge at the end of the LOW signal.
signalLength = micros() - signalStart; //Measure the length of the signal. This is used to watch for the emitter's shutdown signal.
}
}
}
ISR(WDT_vect) {
tripwire = true; //When the watchdog timer goes off, set this flag to start photo routine.
}
void photo() {
GIMSK &= ~(1 << PCIE); //Disable pin change interrupts to not be disrupted if signal resumes.
for (int i = 0; i < num_photo; i++) { //sets a loop to repeat the number of photos taken per trigger
digitalWrite(shutter, HIGH); //sends a signal to the shutter release to take a picture
watchdog32(); //Set watchdog timer for 32ms to make sure camera gets the signal
shutDown(); //Go to sleep
watchdogOff(); //Turn off timer
digitalWrite(shutter, LOW); //turns off signal to shutter release
for (int x = 0; x < WDTcycles; x++) { //Cycle through 16ms watchdog timers and wakeups to reach approximate amount input by user
watchdog16();
shutDown();
watchdogOff();
}
}
GIFR = 0; //Reset interrupt flags
GIMSK |= (1 << PCIE); //Enable pin change interrupt on PB1
}
void shutDown() {
//set sleep mode to power down and enable sleep mode. See p.37-38 of datasheet.
MCUCR &= ~(1 << SM0);
MCUCR = (1 << SE) | (1 << SM1);
//Same as sleep_cpu() if that is included in a library
do {
__asm__ __volatile__("sleep"
"\n\t" ::);
} while (0);
MCUCR &= ~(1 << SE); // disable sleep mode
}
void checkVoltageSetup() {
ADMUX = 0; //clear ADMUX which sets Vcc as reference voltage
ADMUX |= (1 << MUX3) | (1 << MUX2); // use 1.1 bandgbap voltage for input
ADCSRA |= (1 << ADPS2); // set prescaler to 16 since using 1MHz clock speed
}
void checkVoltage() {
ADCSRA |= (1 << ADEN); // Enable ADC
delayMicroseconds(70); // allow bandgap voltage to settle. See p.165 of ATTiny85 datasheet
int samples = 4;
float battVmeasure[samples];
float battVsum = 0;
long adcvalue = 0;
//Take number of measurements equal to samples variable and average them.
for (int i = 0; i < samples; i++) {
ADCSRA |= (1 << ADSC); // start ADC measurement
while (ADCSRA & (1 << ADSC))
; // wait till conversion complete
adcvalue = ADC;
// Convert ADC value to voltage. Bandgap voltage should be between
// 1.0 and 1.2 and needs experimentation for each processor to get the right value
battVmeasure[i] = 1.02 * 1024 / adcvalue;
battVsum += battVmeasure[i]; // Add each voltage as loop progresses
}
battVaverage = battVsum / samples; // Average all the voltage measurements
PORTB |= (1 << PORTB4); // turn on pull up resistor for PB4
ADCSRA &= ~(1 << ADEN); // turn off ADC
}
void photoTimeInput() {
//Take input via buttons and display on LCD
int T_ones = 0;
int T_tenths = 0;
int T_hunds = 0;
int photo_deltaT = 0;
float buttonV = 1023;
float lastbuttonV = 1023;
float Vin = 1023;
float Vb1 = 0;
float Vb2 = Vin * 1 / 4;
float Vb3 = Vin * 1 / 2;
float Vb4 = Vin * 3 / 4;
ADMUX = 0; //Set all bits to 0, results in Vcc being used as reference voltage
ADMUX |= (1 << MUX1) | (1 << MUX0); // use ADC3 for input
ADCSRA |= (1 << ADPS2); // set prescaler to 16 since using 1MHz clock speed
PORTB &= ~(1 << PORTB3); // turn off pull up resistor for PB3
ADCSRA |= (1 << ADEN); // Enable ADC
int x = 1;
int y = 1;
while (x > 0) {
//Request number of photos from user
lcd.init();
lcd.noBacklight();
lcd.setCursor(0, 0);
lcd.print("# of pics per");
lcd.setCursor(0, 1);
lcd.print("trigger?");
lcd.setCursor(11, 1);
lcd.print(num_photo);
lcd.setCursor(11, 1);
lcd.blink();
//Take button input for photo value. Button 2 decreases, button 3 increases, button 4 selects.
while (x == 1) {
y = 1;
lastbuttonV = buttonV;
ADCSRA |= (1 << ADSC); // start ADC measurement
while (ADCSRA & (1 << ADSC))
; // wait till conversion complete
buttonV = ADC;
if (lastbuttonV == 1023 && lastbuttonV != buttonV) {
if (buttonV > Vb2 * 0.9 && buttonV < Vb2 * 1.1) {
if (num_photo > 1) {
num_photo = num_photo - 1;
lcd.setCursor(11, 1);
lcd.print(num_photo);
lcd.setCursor(11, 1);
}
}
if (buttonV > Vb3 * 0.9 && buttonV < Vb3 * 1.1) {
if (num_photo < 9) {
num_photo = num_photo + 1;
lcd.setCursor(11, 1);
lcd.print(num_photo);
lcd.setCursor(11, 1);
}
}
if (buttonV > Vb4 * 0.9 && buttonV < Vb4 * 1.1) {
x = 2;
if (num_photo == 1) {
x = 0;
y = 0;
}
}
}
}
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Time between");
lcd.setCursor(0, 1);
lcd.print("shots?");
lcd.setCursor(11, 1);
lcd.print(String(T_ones) + "." + String(T_tenths) + String(T_hunds) + "s");
lcd.setCursor(11, 1);
while (x == 2) {
//Take button input for ones place of time between pictures in seconds.
//Button 1 goes back to previous selection, button 2 decreases, button 3 increases, button 4 selects.
while (y == 1) {
lastbuttonV = buttonV;
ADCSRA |= (1 << ADSC); // start ADC measurement
while (ADCSRA & (1 << ADSC))
; // wait till conversion complete
buttonV = ADC;
if (lastbuttonV == 1023 && lastbuttonV != buttonV) {
if (buttonV > Vb3 * 0.9 && buttonV < Vb3 * 1.1) {
if (T_ones < 9) {
T_ones = T_ones + 1;
lcd.setCursor(11, 1);
lcd.print(T_ones);
lcd.setCursor(11, 1);
}
}
if (buttonV > Vb2 * 0.9 && buttonV < Vb2 * 1.1) {
if (T_ones > 0) {
T_ones = T_ones - 1;
lcd.setCursor(11, 1);
lcd.print(T_ones);
lcd.setCursor(11, 1);
}
}
if (buttonV > Vb4 * 0.9 && buttonV < Vb4 * 1.1) {
y = 2;
lcd.setCursor(13, 1);
}
if (buttonV < Vb1 + 50) {
x = 1;
y = 0;
}
}
}
//Take button input for tenths place of time between pictures in seconds.
//Button 1 goes back to previous selection, button 2 decreases, button 3 increases, button 4 selects.
while (y == 2) {
lastbuttonV = buttonV;
ADCSRA |= (1 << ADSC); // start ADC measurement
while (ADCSRA & (1 << ADSC))
; // wait till conversion complete
buttonV = ADC;
if (lastbuttonV == 1023 && lastbuttonV != buttonV) {
if (buttonV > Vb3 * 0.9 && buttonV < Vb3 * 1.1) {
if (T_tenths < 9) {
T_tenths = T_tenths + 1;
lcd.setCursor(13, 1);
lcd.print(T_tenths);
lcd.setCursor(13, 1);
}
}
if (buttonV > Vb2 * 0.9 && buttonV < Vb2 * 1.1) {
if (T_tenths > 0) {
T_tenths = T_tenths - 1;
lcd.setCursor(13, 1);
lcd.print(T_tenths);
lcd.setCursor(13, 1);
}
}
if (buttonV > Vb4 * 0.9 && buttonV < Vb4 * 1.1) {
y = 3;
lcd.setCursor(14, 1);
}
if (buttonV < Vb1 + 50) {
y = 1;
lcd.setCursor(11, 1);
}
}
}
//Take button input for hundredths place of time between pictures in seconds.
//Button 1 goes back to previous selection, button 2 decreases, button 3 increases, button 4 selects.
while (y == 3) {
lastbuttonV = buttonV;
ADCSRA |= (1 << ADSC); // start ADC measurement
while (ADCSRA & (1 << ADSC))
; // wait till conversion complete
buttonV = ADC;
if (lastbuttonV == 1023 && lastbuttonV != buttonV) {
if (buttonV > Vb3 * 0.9 && buttonV < Vb3 * 1.1) {
if (T_hunds < 9) {
T_hunds = T_hunds + 1;
lcd.setCursor(14, 1);
lcd.print(T_hunds);
lcd.setCursor(14, 1);
}
}
if (buttonV > Vb2 * 0.9 && buttonV < Vb2 * 1.1) {
if (T_hunds > 0) {
T_hunds = T_hunds - 1;
lcd.setCursor(14, 1);
lcd.print(T_hunds);
lcd.setCursor(14, 1);
}
}
if (buttonV > Vb4 * 0.9 && buttonV < Vb4 * 1.1) {
x = 0;
y = 0;
}
if (buttonV < Vb1 + 50) {
x = 2;
y = 2;
lcd.setCursor(13, 1);
}
}
}
}
}
//Display results of selection
photo_deltaT = T_ones * 1000 + T_tenths * 100 + T_hunds * 10;
WDTcycles = round((photo_deltaT - 32) / 16);
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("# photos: ");
lcd.print(num_photo);
lcd.setCursor(0, 1);
lcd.print("Delta T : ");
lcd.print(photo_deltaT);
lcd.print("ms");
lcd.noBlink();
delay(3000);
}


