Close

Simon .. uh .. suggests.

A project log for weekend novelty projects

minimal advanced planning. built from stuff i already have around the shop. typically not very useful. generally completed in one weekend.

zakqwyzakqwy 12/21/2016 at 18:120 Comments

Codename MrBeepers. Any similarity to the popular electronic game from 1978 is purely coincidental. This is the project I referred to in the last project log that spawned the AVR Listener side side project.

[filmed through a 4" ring magnifier to support the phone, so the video is a bit distorted.]

I built this project as a Christmas present for my family's secret Santa recipient, so don't tell (as it's not yet shipped). Fortunately I don't think the target audience follows my Hackaday projects.

[a thousand apologies for the unnecessary Instagram filter. grow up, zach.]

As with many freehand FR4 projects, this one kinda designed itself as I went along. I based the overall geometry of the board on the 4xAA battery packs I discovered as part of the #NeuroBytes project. They use PC pins for mounting, and can be secured to a circuit board using a pair of McMaster nylon rivets that are amazingly the perfect size.

[1.6 second exposure at f22 and ISO200. it took a few trials to get the right pattern to play back in order to light up all four LEDs.]

I'm especially proud of the ergonomic-ish and aesthetically pleasing layout of the PCB; the four LEDs and buttons are symmetrically oriented in the four corners of the board, the main processor is in the center, and the other main circuit elements (power switch, regulator, and piezo elements) are in-line as well. The pushbuttons are some of the largest I could find, and are quite pleasing to the touch. At some point, I may spin up a proper PCB for this project, and likely won't change much about the layout; it works quite well. Okay, I may add a set of programming pogo pads, as the soldered on leads were a bit of a pain.

Parts list:

Initially, I used a smaller piezo element driven using an NPN transistor; however, the element wasn't nearly loud enough, and through a few happy accidents I discovered that driving the devices directly from the ATtiny's I/O ports didn't seem to cause any issues. I increased the size of the piezo element and added a second, allowing me to create the spooky and annoying two-tone harmony heard in the video at the top of this post.

Firmware is pretty simple. I'm trying to use better programming practices (hah!) so I broke out a hardware abstraction layer into a separate *.c and header file. Timer0 is used as a 1ms tick for game logic, while Timer1 is used in Fast PWM mode (since that way the TOP values are double-buffered and can be changed on the fly) to generate interrupts for tone generation. I originally used the compare output pins directly for the piezo elements, but changed to standard pins with ISR-driven toggles so I could produce two different tones simultaneously. As usual, button debounce is handled using Elliott Williams' pattern-based method, and the pseudo-random initialization is based around @Vojtak's implementation from #Simon game with ATtiny13. The recursive algorithm is seeded using an unconnected ADC pin and it's good enough to make the game feel random; I didn't implement the fancy watchdog timer jitter deal he did so it's not perfect. The game-over state is terminal -- it generates a low tone and then puts the processor to sleep, so the user has to hard-reset the device to play again. Yeah, I got tired of programming towards the end, and no, unlike his excellent and compact code, my less-capable implementation comes in over 1k (1356 bytes exactly).

Nothing past the code dump, so feel free to stop reading now (recommended). Everything is covered under the MIT License.

/* 
HAL.c

Released under the terms of the MIT License.

The MIT License (MIT)
Copyright (c) 2016 by Zach Fredin
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.	

_____________________
|+     -            |
| SW1           SW2 |
|    L1       L2    |
|                   |
|              B1   |
|                   |
|    L3       L4    |
| SW3    B2     SW4 |
|___________________|

SW1	PB2
SW2	PB1
SW3	PA3
SW4	PA1
L1	PA7
L2	PB0
L3	PA4
L4	PA0
B1	PA2
B2	PA5

*/

#include <avr/io.h>
#include <avr/interrupt.h>
#include "HAL.h"

void SystemInit(void) {
	DDRA |= ((1<<PA0) | (1<<PA4) | (1<<PA7) | (1<<PA2) | (1<<PA5));
	DDRA &= ~((1<<PA3) | (1<<PA1));
	DDRB |= (1<<PB0);
	DDRB &= ~((1<<PB1) | (1<<PB2));

	// switch pullups on:
	PORTA |= ((1<<PA3) | (1<<PA1));
	PORTB |= ((1<<PB1) | (1<<PB2));

	// leds off:
	PORTA &= ~((1<<PA0) | (1<<PA4) | (1<<PA7));
	PORTB &= ~(1<<PB0); 

	// set up tick timer:
	TCCR0A |= (1<<WGM01); //CTC at OCR0A
	TCCR0B |= (1<<CS02); //clk/256 (32.5 kHz)
	OCR0A = 30; //sets tick to ~1ms
	TIMSK0 |= (1<<OCIE0A); //output compare match 0A interrupt enable
		
	// set up beeper timer:
	TCCR1A |= ((1<<WGM10) | (1<<WGM11));
	TCCR1B |= ((1<<WGM12) | (1<<WGM13)); //fast PWM mode, with top at OCR1A
	TIMSK1 |= (1<<OCIE1A); //output compare match 1A interrupt enable

	sei();
}

void updateButtonHistory(uint8_t *button) {
	button[0] <<= 1;
	button[0] |= ((PINB & (1<<PB2)) == 0);
	button[1] <<= 1;
	button[1] |= ((PINB & (1<<PB1)) == 0);
	button[2] <<= 1;
	button[2] |= ((PINA & (1<<PA3)) == 0);
	button[3] <<= 1;
	button[3] |= ((PINA & (1<<PA1)) == 0);
}

uint8_t is_button_pressed(uint8_t *button_history, uint8_t button_number) {
	uint8_t pressed = 0;
	if ((button_history[button_number] & 0b11001111) == 0b00001111) {
		pressed = 1;
		button_history[button_number] = 0b11111111;
	}
	return pressed;
}

uint8_t is_button_released(uint8_t *button_history, uint8_t button_number) {
	uint8_t released = 0;
	if ((button_history[button_number] & 0b11001111) == 0b11000000) {
		released = 1;
		button_history[button_number] = 0b00000000;
	}
	return released;
}

uint8_t is_button_down(uint8_t *button_history, uint8_t button_number) {
	return (button_history[button_number] == 0b11111111);
}

uint8_t is_button_up(uint8_t *button_history, uint8_t button_number) {
	return (button_history[button_number] == 0b00000000);
}

void updateBeeper(uint16_t freq, uint16_t startTime, uint16_t *timeLeft) {
	if (*timeLeft == startTime) {
		OCR1A = freq;
		TCCR1B |= ((1<<CS11) | (1<<CS10)); //clk/64 (125 kHz)
		--*timeLeft;
	}
	else if(*timeLeft > 0) {
		--*timeLeft;
	}
	else {
		TCCR1B &= ~((1<<CS11) | (1<<CS10)); //clock stopped	
	}
}

void updateLEDs(uint8_t status) {
	if (status & (1<<0)) {
		PORTA |= (1<<PA7); //L1
	}
	else {
		PORTA &= ~(1<<PA7);
	}
	
	if (status & (1<<1)) {
		PORTB |= (1<<PB0); //L2
	}
	else {
		PORTB &= ~(1<<PB0);
	}

	if (status & (1<<2)) {
		PORTA |= (1<<PA4); //L3
	}
	else {
		PORTA &= ~(1<<PA4);
	}

	if (status & (1<<3)) {
		PORTA |= (1<<PA0); //L4
	}
	else {
		PORTA &= ~(1<<PA0);
	}
}
//HAL.h

#ifndef HAL_H_
#define HAL_H_

void SystemInit(void);

void updateButtonHistory(uint8_t *button);
uint8_t is_button_pressed(uint8_t *button_history,uint8_t button_number);
uint8_t is_button_released(uint8_t *button_history,uint8_t button_number);
uint8_t is_button_down(uint8_t *button_history,uint8_t button_number);
uint8_t is_button_up(uint8_t *button_history,uint8_t button_number);

void updateBeeper(uint16_t freq, uint16_t startTime, uint16_t *timeLeft);

void updateLEDs(uint8_t status);
#endif /* HAL_H_ */
/* 
main.c

Released under the terms of the MIT License.

The MIT License (MIT)
Copyright (c) 2016 by Zach Fredin
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.	
*/

#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/sleep.h>
#include "HAL.h"

volatile uint8_t tick = 0;
volatile uint8_t tone1 = 0;
volatile uint8_t tone1_reset = 3;
volatile uint8_t tone2 = 0;
volatile uint8_t tone2_reset = 8;
volatile uint16_t ctx;

ISR(TIM0_COMPA_vect) {
	tick = 1;
}

ISR(TIM1_COMPA_vect) {
	if (tone1 == tone1_reset) {
		PORTA ^= (1<<PA5);
		tone1 = 0;
	}
	if (tone2 == tone2_reset) {
		PORTA ^= (1<<PA2);
		tone2 = 0;
	}

	tone1++;
	tone2++;
}

uint8_t simple_random4(void) {
// random number generator taken from Vojtak's Simon Game, seeded from the ADC.
	ctx = 2053 * ctx + 13849;
	uint8_t temp = ctx ^ (ctx >> 8);
	temp ^= (temp >> 4);
	return (temp ^ (temp >> 2)) & 0b00000011;
}

int main(void) {
	uint8_t button[4] = {0,0,0,0};
	uint8_t LED_status = 0;
	uint8_t game_state = 0;
	uint8_t playback_mode = 0;
	uint8_t input_mode = 0;
	uint8_t gameover_mode = 0;
	uint16_t freq = 0;
	uint16_t freq_ref[4] = {30,40,50,60};
	uint8_t i = 0;
	uint16_t beep_time = 100;
	uint16_t beep_time_left = 0;
	uint16_t delay_time_beeps = 150;
	uint16_t delay_time_game_state = 500;
	uint16_t count = 0;	
	uint8_t level = 1;
	uint8_t level_count = 0;
	uint16_t seed;

	uint8_t temp;
	SystemInit();
	
	for(;;) {
		while(tick == 0) {} //idle until Timer0 compare match interrupt
		tick = 0;
		updateButtonHistory(button);
		switch(game_state) {
		case 0: //game initialization
			ADCSRA |= (1<<ADEN); //enable ADC
			ADMUX |= ((1<<MUX1) | (1<<MUX2)); //selects ADC6 (PA6)
			ADCSRA |= (1<<ADSC); //starts conversion
			while (ADCSRA & (1<<ADSC)); //waits for conversion
			seed = ADCL; //sets seed to lower ADC byte
			ctx = seed;
			beep_time = 100;
			playback_mode = 0;
			level = 1;
			level_count = 1;
			if (count == delay_time_game_state) {
				game_state = 1;
			}
			count++;
			break;
		case 1: //playback
			switch(playback_mode) {
				case 0: //calculate next and start beep
					temp = simple_random4();
					beep_time_left = beep_time;
					LED_status |= (1<<temp);
					freq = freq_ref[temp];
					playback_mode = 1;
					break;
				case 1: //beep
					if(beep_time_left == 0) {
						count = 0;
						LED_status &= ~(1<<temp);
						playback_mode = 2;
					}
					break;	
				case 2: //delay
					if (count == delay_time_beeps) {
						playback_mode = 0;
						if (level_count == level) {
							count = 0;
							level_count = 1;
							ctx = seed;
							game_state = 2;
							break;
						}
						level_count++;
					}
					count++;
					break;
			}
			break;
		case 2: //user input	
			switch(input_mode) {
				case 0: //check for inputs
					for (i=0;i<4;i++) {
						if (is_button_pressed(button,i)) {
							temp = i;
							beep_time_left = beep_time;
							freq = freq_ref[i];
							LED_status |= (1<<i);
							input_mode = 1;
						}
					}
					break;
				case 1: //delay
					if (beep_time_left == 0) {
						LED_status &= ~(1<<temp);
						input_mode = 2;
					}
					break;
				case 2: //evaluate input
					if (temp == simple_random4()) {
						if (level_count == level) { 
							level++;
							input_mode = 3;
							break;
						}
						level_count++;
						input_mode = 0;
					}
					else {
						count = 0;
						game_state = 3;
					}	
					break;
				case 3: //delay
					if (count == delay_time_game_state) {
						count = 0;
						level_count = 1;
						input_mode = 0;
						ctx = seed;
						game_state = 1;
					}
					count++; 
					break;
			}
			break;
		case 3: //game over
			switch(gameover_mode) {
				case 0: //delay
					if (count == delay_time_game_state) {
						gameover_mode = 1;
					}
					count++;
					break;
				case 1: //boom!
					count = 0;
					freq = 200;
					beep_time = 700;
					beep_time_left = beep_time;
					gameover_mode = 2;
					break;
				case 2: //delay
					if (beep_time_left == 0) {
						PORTA = 0;
						PORTB = 0;
						cli();
						set_sleep_mode(SLEEP_MODE_PWR_DOWN);
						sleep_enable();
						sleep_cpu();
					}
					break;
			}
			break;
		}
		updateLEDs(LED_status);
		updateBeeper(freq, beep_time, &beep_time_left);
	}
}	

Discussions