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:
- ATtiny44A, SOIC-14, qty 1
- 4xAA battery pack, qty 1
- 1A / 5VDC LDO, qty 1
- Piezo elements, qty 2
- 12mm pushbuttons, qty 4
- 0805 LEDs, QT Brightek series, 470nm, 525nm, 590nm, 625nm
- Various support components: current limiting resistors, reverse protection diodes for the piezo elements, tantalum capacitors for the LDO, ceramic bypass cap for the ATtiny44A
- Aforementioned nylon rivets
- 1/16" single sided FR4, tin-snipped to size
- 34 AWG polyurethane insulated magnet wire
- A goodly bit of RoHS-compliant solder and flux
- A bit of spray polyurethane to insulate and protect the whole works
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
Become a Hackaday.io Member
Create an account to leave a comment. Already have an account? Log In.