fridge-cat-repeller/firmware/main.c

445 lines
14 KiB
C

/* 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/>.
*
*/
/* this firmware is for a cat repeller
* it uses:
* - andruino nano a micro-controller board
* - HC-SR501 PIR motion detector
* - E18-D80NK-N adjustable infrared sensor switch
* - piezo buzzer
*/
#include <stdint.h> // Standard Integer Types
#include <stdio.h> // Standard IO facilities
#include <stdlib.h> // General utilities
#include <stdbool.h> // Boolean
#include <string.h> // Strings
#include <avr/io.h> // AVR device-specific IO definitions
#include <util/delay.h> // Convenience functions for busy-wait delay loops
#include <avr/interrupt.h> // Interrupts
#include <avr/wdt.h> // Watchdog timer handling
#include <avr/pgmspace.h> // Program Space Utilities
#include <avr/sleep.h> // Power Management and Sleep Modes
#include "uart.h" // basic UART functions
#include "main.h" // main definitions
/* help strings */
const char help_00[] PROGMEM = "commands:\n";
const char help_01[] PROGMEM = "\thelp display this help\n";
const char help_02[] PROGMEM = "\tbeep T F beep for T ms at F Hz\n";
PGM_P const help_table[] PROGMEM = {
help_00,
help_01,
help_02
};
/* global variables */
char uart_in[25]; // user input from USART
volatile uint16_t ms = 0; // to count down the ms
/* flags, set in the interrupts and handled in the main program */
volatile bool uart_flag = false; // UART input data is available
volatile bool command_flag = false; // a command has been input
volatile bool countdown_flag = false; // set when the ms countdown reached 0
volatile bool fridge_flag = false; // when the fridge switch changed
volatile bool motion_flag = false; // when the PIR detected motion
volatile bool barrier_flag = false; // when the light barrier got cut
/* different melodies to play
* pair of time in ms and tone in Hz
* 0 ms to end
* 0xffff ms to restart/loop
*/
#ifdef DEBUG
const uint16_t MELODY_REPEL[] PROGMEM = {100,4000,100,0,100,3000,100,0,100,3000,100,0,100,3000,100,0,100,3000,100,0,500,5000,500,3000,500,5000,500,3000,500,5000,500,3000,0xffff}; // try to scare the cat
const uint16_t MELODY_WARN[] PROGMEM = {100,4000,3000,0,500,4000,500,5000,500,4000,500,5000,500,4000,500,5000,0xffff}; // warn human opened fridge (anyone if left open)
#else
const uint16_t MELODY_REPEL[] PROGMEM = {100,4000,100,0,100,23000,100,0,100,23000,100,0,100,23000,100,0,100,23000,100,0,500,25000,500,23000,500,25000,500,23000,500,25000,500,23000,0xffff}; // try to scare the cat
const uint16_t MELODY_WARN[] PROGMEM = {100,4000,30000,0,500,4000,500,5000,500,4000,500,5000,500,4000,500,5000,0xffff}; // warn human opened fridge (anyone if left open)
#endif
const uint16_t MELODY_ALARM[] PROGMEM = {500,5000,500,4000,0xffff,0xffff}; // sound alarm because cat opened the frigde
const uint16_t* melody = NULL; // the current melody to play
uint8_t melody_i = 0; // the current position in melody
/* switch off LED */
void led_off(void)
{
LED_PORT &= ~(1<<LED_IO); // remove power to LED
}
/* switch on LED */
void led_on(void)
{
LED_PORT |= (1<<LED_IO); // provide power to LED
}
/* toggle LED */
void led_toggle(void)
{
LED_PIN |= (1<<LED_IO);
}
/* disable watchdog when booting */
void wdt_init(void) __attribute__((naked)) __attribute__((section(".init3")));
void wdt_init(void)
{
MCUSR = 0;
wdt_disable();
return;
}
void help(void)
{
char* str;
for (uint8_t i=0; i<sizeof(help_table)/sizeof(PGM_P); i++) { /* display all help lines */
str = malloc(strlen_PF((uint_farptr_t)pgm_read_word(&(help_table[i]))));
strcpy_PF(str, (uint_farptr_t)pgm_read_word(&(help_table[i])));
printf(str);
free(str);
}
}
/* save the UART input into a string */
ISR(USART_RX_vect) { /* UART receive interrupt */
static uint8_t uart_in_i = 0; // UART input index
char c = getchar(); // current character
if ('\n'==c || '\r'==c) {
if (uart_in_i>0) { // end of line (ignore empty lines)
command_flag = true; // notify a command is ready
uart_in_i = 0; // reset input
}
} else if (uart_in_i<sizeof(uart_in)/sizeof(*uart_in)-1) { // save input only if there is space left
uart_in[uart_in_i] = c; // save input
uart_in_i++; // prepare for next character
uart_in[uart_in_i] = 0; // always end the string
uart_flag = true; // notify new character is there
}
}
/* timer 0 OCR0A match interrupt
* 1 ms has passed
*/
ISR(TIMER0_COMPA_vect)
{
if (ms>0) {
ms--; // count down
}
if (0==ms) { // always verify, also when not counting down
countdown_flag = true; // warn count down has finished
}
}
/* PCI2 Interrupt Vector for PCINT[23:16]/PORTD */
ISR(PCINT2_vect)
{
static uint8_t fridge_state = 0;
static uint8_t motion_state = 0;
static uint8_t barrier_state = 0;
if ((fridge_state&(1<<FRIDGE_IO))!=(FRIDGE_PIN&(1<<FRIDGE_IO))) { // fridge activity
fridge_flag = true;
fridge_state = FRIDGE_PIN;
}
if ((motion_state&(1<<MOTION_IO))!=(MOTION_PIN&(1<<MOTION_IO))) { // motion activity
motion_flag = true;
motion_state = MOTION_PIN;
}
if ((barrier_state&(1<<BARRIER_IO))!=(BARRIER_PIN&(1<<BARRIER_IO))) { // light barrier activity
barrier_flag = true;
barrier_state = BARRIER_PIN;
}
}
/* initialize GPIO */
void io_init(void)
{
/* use UART as terminal */
uart_init();
stdout = &uart_output;
stdin = &uart_input;
/* gpio */
/* LED */
LED_DDR |= (1<<LED_IO); // LED is driven by pin (set as output)
led_off();
/* piezo, connected on OC1A and OC1B */
DDRB |= ((1<<PB1)|(1<<PB2)); // set as output
PORTB &= ~((1<<PB1)|(1<<PB2)); // set to low
/* fridge switch */
FRIDGE_DDR &= ~(1<<FRIDGE_IO); // set as input
FRIDGE_PORT |= (1<<FRIDGE_IO); // enable pull-up
PCICR |= (1<<PCIE2); // enable interrupt on pin change for PORTD/PCINT[23:16]
PCMSK2 |= (1<<FRIDGE_PCINT); // enable interrupt on pin change for fridge switch
/* PIR motion detector */
MOTION_DDR &= ~(1<<MOTION_IO); // set as input
MOTION_PORT &= ~(1<<MOTION_IO); // disable pull-up
PCICR |= (1<<PCIE2); // enable interrupt on pin change for PORTD/PCINT[23:16]
PCMSK2 |= (1<<MOTION_PCINT); // enable interrupt on pin change for motion detector
/* light barrier */
LIGHT_DDR |= (1<<LIGHT_IO); // set as output
LIGHT_PORT |= (1<<LIGHT_IO); // switch barrier off
BARRIER_DDR &= ~(1<<BARRIER_IO); // set as input
BARRIER_PORT |= (1<<BARRIER_IO); // enable pull-up
PCICR |= (1<<PCIE2); // enable interrupt on pin change for PORTD/PCINT[23:16]
PCMSK2 |= (1<<BARRIER_PCINT); // enable interrupt on pin change for light barrier
/* use PWM to driver piezo-element
* use timer 1. with 16 bits it is the most precise one
* fast PWM fits for high frequency PWM
* use non-inverting and inverting outputs to drive the piezo element at 2x5V (the edge is the important part)
*/
TCCR1B |= (1<<WGM13)|(1<<WGM12); // set mode 14, fast PWM, TOP=ICR1
TCCR1A |= (1<<WGM11)|(0<<WGM10); // set mode 14, fast PWM, TOP=ICR1
TCCR1A |= (1<<COM1A1)|(0<<COM1A0); // set OC1A as non-inverting mode
TCCR1A |= (1<<COM1B1)|(1<<COM1B0); // set OC1B as inverting mode
/* user timer 0 to count milliseconds, for non-blocking waiting */
TCCR0B |= (0<<WGM02); // set mode 2, Clear Timer on Compare Match
TCCR0A |= (1<<WGM01)|(0<<WGM00); // set mode 2, Clear Timer on Compare Match
OCR0A = 62-1; // corresponds to 0.992 ms when using prescale 256
TIMSK0 |= (1<<OCIE0A); // enable interrupt when OCRA0A is matched
sei(); // enable interrupts
}
/* set tone of piezo at <freq> Hz */
void set_tone (uint32_t freq) {
// use prescale 1 to allow high frequencies and precision
const uint16_t PRESCALE = 1;
const uint8_t PRESCALE_CS = 1;
// minimum frequency (formula 16.9.4 in ATmega328P datasheet)
const uint32_t FREQ_MIN = (F_CPU/(PRESCALE*(1+(1UL<<16))));
// maximum frequency (min OCR1A is 0x0003)
const uint32_t FREQ_MAX = (F_CPU/(PRESCALE*(1+3)));
TCCR1B &= ~((1<<CS12)|(1<<CS11)|(1<<CS10)); // stop clock
TCNT1 = 0; // reset timer
if (0 == freq) { // switch of PWM
// clock already stopped
// PB1 and PB2 can not bet set low since OC1A and OC1B are used
} else if (freq < FREQ_MIN) {
printf("frequency to low. minimum: %lu Hz\n",FREQ_MIN);
} else if (freq > FREQ_MAX) {
printf("frequency to high. maximum: %lu Hz\n",FREQ_MAX);
} else {
ICR1 = (F_CPU/(PRESCALE*freq))-1; // set frequency
OCR1A = ICR1/2; // set 50% duty cycle
OCR1B = ICR1/2; // set 50% duty cycle
TCCR1B |= (PRESCALE_CS<<CS10); // set clock source
#ifdef DEBUG
uint32_t freq_real = F_CPU/(PRESCALE*(1+ICR1));
if (freq_real!=freq) {
printf("set tone at %lu Hz instead of %lu Hz\n",freq_real,freq);
}
#endif
}
}
/* use pizeo to beep at <freq> Hz for <time> ms */
void beep(uint16_t time, uint32_t freq)
{
set_tone(freq); // set frequency of tone
if (0==time) {
TCCR0B &= ~((1<<CS02)|(1<<CS01)|(1<<CS00)); // stop timer 0
} else {
TCCR0B |= (1<<CS02)|(0<<CS01)|(0<<CS00); // ensure timer 0 is on, set to prescale 256
}
TCNT0 = 0; // reset timer 0
ms = time; // start countdown
}
/* load a melody
* set to NULL to stop playing
* return if it succeeded loading
*/
bool load_melody(const uint16_t* melody_ptr)
{
if (NULL!=melody_ptr) { // verify if melody is malformed before loading it
if (0xffff==pgm_read_word(&(melody_ptr[0]))) { // the first time shouldn't be a loop
return false;
}
}
melody = melody_ptr; // set melody
melody_i = 0; // play melody from start
if (NULL==melody) {
beep(0,0); // stop beeping
}
return true;
}
/* play or continue the melody
* return true if tone is played
* return false if no melody is loaded of end of melody
*/
bool play_melody()
{
if (NULL==melody) { // verify a melody is loaded
return false;
}
uint16_t time = pgm_read_word(&(melody[melody_i])); // get time
if (0xffff==time) { // special time value to loop
melody_i = 0; // play melody from start
time = pgm_read_word(&(melody[melody_i])); // reread first time
} else if (0==time) { // special time value to end
return false;
}
melody_i++; // go to tone
uint16_t tone = pgm_read_word(&(melody[melody_i])); // read tone
melody_i++; // prepare to read next time value
beep(time,tone); // play tone
return true;
}
/* process command */
void command_action(char* command)
{
/* split command */
const char* delimiter = " ";
char* word = strtok(command,delimiter);
if (!word) {
goto error;
}
/* parse command */
if (0==strcmp(word,"help")) {
help();
} else if (0==strcmp(word,"beep")) {
/* expecting time */
word = strtok(NULL,delimiter);
if (!word) {
goto error;
}
uint16_t time = atol(word);
/* expecting frequency */
word = strtok(NULL,delimiter);
if (!word) {
goto error;
}
uint32_t freq = atol(word);
beep(time,freq);
} else {
goto error;
}
return;
error:
printf(PSTR("command not recognized\n"));
return;
}
int main(void)
{
uint8_t uart_out_i = 0; // how many UART input characters have been processed
io_init(); // initialize IOs
printf(PSTR("welcome on the cat repeller\n"));
fridge_flag = true; // verify fridge swtich pin state when booting
motion_flag = true; // verify motion pin state when booting
while (true) { // endless loop for micro-controller
/* process flags */
while (fridge_flag) { // fridge switch activity
if (FRIDGE_PIN&(1<<FRIDGE_IO)) { // fridge open
#ifdef DEBUG
printf(PSTR("fridge opened\n"));
#endif
if (!(LIGHT_PIN&(1<<LIGHT_IO)) && !(BARRIER_PIN&(1<<BARRIER_IO))) { // cat in front of barrier
load_melody(MELODY_ALARM); // load sound
} else {
load_melody(MELODY_WARN); // load sound
}
play_melody(); // start playing
LIGHT_PORT |= (1<<LIGHT_IO); // switch off light barrier
} else { // fridge close
#ifdef DEBUG
printf(PSTR("fridge closed\n"));
#endif
load_melody(NULL); // stop sound
}
fridge_flag = false;
}
while (motion_flag) { // PIR motion detector activity
if (!(FRIDGE_PIN&(1<<FRIDGE_IO))) { // only react if fridge closed
if (MOTION_PIN&(1<<MOTION_IO)) { // motion detected
led_on();
#ifdef DEBUG
printf(PSTR("motion detected\n"));
#endif
LIGHT_PORT &= ~(1<<LIGHT_IO); // switch on light barrier
} else {
led_off();
#ifdef DEBUG
printf(PSTR("motion stopped\n"));
#endif
LIGHT_PORT |= (1<<LIGHT_IO); // switch off light barrier
load_melody(NULL);
}
}
motion_flag = false;
}
while (barrier_flag) {
if (!(FRIDGE_PIN&(1<<FRIDGE_IO)) && !(LIGHT_PIN&(1<<LIGHT_IO))) { // only react if fridge closed and light activated
if (BARRIER_PIN&(1<<BARRIER_IO)) {
#ifdef DEBUG
printf(PSTR("nothing in front of barrier\n"));
#endif
load_melody(NULL);
} else {
#ifdef DEBUG
printf(PSTR("something in front of barrier\n"));
#endif
load_melody(MELODY_REPEL);
play_melody();
}
}
barrier_flag = false;
}
while (uart_flag) { // new UART input
while (0!=uart_in[uart_out_i]) { // go until end of string
putchar(uart_in[uart_out_i]); // print current character
uart_out_i++; // go to next character
}
uart_flag = false;
}
while (command_flag) { // UART input command is ready
putchar('\n'); // print new line
uart_out_i = 0; // reset string counter
command_action(uart_in); // process command
command_flag = false; // clear flag
}
while (countdown_flag) { // play melody
if (!play_melody()) { // play next tone
load_melody(NULL); // stop playing on last tone
}
countdown_flag = false;
}
/* go to sleep and wait for next interrupt */
if (NULL==melody) {
#ifdef DEBUG
set_sleep_mode(SLEEP_MODE_IDLE); // in idle UART input is possible
#else
set_sleep_mode(SLEEP_MODE_PWR_DOWN);
#endif
} else {
set_sleep_mode(SLEEP_MODE_IDLE);
}
sleep_mode();
}
return 0;
}