Proyecto: Calimero’s Rampage
- Introducción
Elaboración de un sistema empotrado basado en la placa de microcontrolador abierto Arduino Uno o similares, documentando su concepción, desarrollo y prototipado.
Título: Juego arcade de cacería de gansos basado en sistema láser.
Descripción: Juego de una ronda de tiempo donde con una escopeta (Elegoo Uno): láser IR, buzzer y led, al pulsar un botón se envía a objetivos receptores una señal que recoge un administrador de juego (Elegoo Mega 2560), que emite sonido y muestra una puntuación en un display HD44780 LCD de acuerdo a los aciertos, así como guarda las puntuaciones en memoria no volátil para comparar las puntuaciones.
Inspirado en el videojuego clásico de Nintendo Duck Hunt (1984) y la funcionalidad del Wii Remote (2006), Calimero’s Rampage pretende emular una máquina recreativa que carga un videojuego de simulación de prácticas de tiro en su memoria. Para ello, se introduce una moneda para iniciar una partida y se manipula la pistola inalámbrica que acompaña a la máquina para interactuar con el juego.
2. Objetivos: propuesta de valor
Las principales motivaciones en la concepción del proyecto serían:
- Solución a la comunicación espacial entre dos sistemas empotrados mediante láser infrarrojo.
- Demostración de la utilización de diversos módulos para sistemas empotrados con propósitos académicos.
2.1 Implementación hardware
El proyecto consta de un sistema emisor y otro receptor.
El sistema emisor ha sido denominado como Escopeta, Shotgun en inglés, y parte de la Figura siguiente.
Requiere, como mínimo, de un emisor infrarrojo, y un botón para decidir cuándo se envía la señal al sistema receptor, así como una fuente de alimentación para que sea inalámbrico.
Además, adicionalmente por razones de usabilidad, consta de un led rojo, que indica cuándo se puede disparar tras un tiempo de espera, un buzzer, que emite un sonido al haber completado la transmisión del láser IR, y un emisor láser rojo para ayudar al usuario a apuntar al objetivo.
Por otro lado, el sistema receptor se ha denominado Administrador de Juego, Game Manager en inglés, y parte de la Figura siguiente
Se compone de una pantalla LCD para mostrar los resultados de recepción, un receptor y sensor y un transmisor láser para detectar la moneda que entra en el arcade, y por supuesto al menos un receptor de IR.
Igualmente, por motivos de usabilidad, se han paralelizado tres receptores IR en vez de uno, que produce una puerta CMOS OR a la hora de percibir señales, pero no es capaz de determinar de cuál receptor recibe la señal. También, se ha incluido un buzzer que emite sonidos que reflejan los estados de la máquina.
2.2 Gestión de tareas
Lo primero fue conocer qué componentes serían de utilidad para el desarrollo de la práctica, y por tanto sus adquisiciones y debidas pruebas software. Las pruebas determinaron que la distancia máxima de emisión es de 4.22 m para que el receptor reciba la señal correctamente.
Algún componente como la pantalla LED tuvo que soldarse para su correcto funcionamiento.
Segundo, se estimó el espacio que ocuparía el proyecto final para medir y acomodar los componentes funcionales en un encapsulado, en este caso de madera. Las dimensiones de la caja son 13 x 16.5 x 5 cm.
Hubo que serrar y unir las piezas para componer la caja e integrar adecuadamente los componentes.
Tercero, se arregló la apariencia del proyecto para darle un acabado más profesional, como si fuera a exponerse, aún siendo un prototipo.
Por último y después de validar la funcionalidad de todos los componentes en conjunción del código realizado, solo faltó acabar la apariencia visual.
Desde el comienzo hasta la finalización, se han descartado algunos elementos por su complejidad que hubiera demorado el proyecto, o por su falta de utilidad en el diseño.
La elección de una placa de microcontrolador ATmega2560 para el Game Manager fue un acierto en cuanto a la memoria. La placa Elegoo Mega 2560 tiene máximo 253 952 bytes de espacio de almacenamiento del programa (memoria flash), del que se usa finalmente un 5%, y de 8 192 bytes máximo que tiene la memoria dinámica (SRAM), solo se emplea un 11% para variables globales. Además, contiene 4 096 bytes de memoria EEPROM integrada, que como se puede visualizar en el código, ¡permite almacenar la puntuación de hasta 4 096 partidas diferentes!
En el caso de haber seleccionado una placa de microcontrolador ATmega328p, la memoria flash estaría ocupada en un 37%, la SRAM un 44%, y a lo sumo podrían grabarse 1 024 bytes internos, que representa alrededor de un 75% menos de espacio que la propuesta de la placa Elegoo Mega 2560.
Asimismo, la placa Elegoo Mega 2560 hace uso de 10 pines digitales, que en su disposición representa cerca de un 18.5% de su capacidad, mientras que de la placa UNO sería un 71% aproximado de ocupación.
2.3 Proyección financiera
El coste total del proyecta ha sido de 126.33€, sin herramientas.
Como se puede observar, ha habido un remanente de componentes que podría disminuirse adquiriendo las piezas por separado. Con ello, también se habrían reducido los costes, así como solicitando los componentes a través de AliExpress.
2.4 Implementación software
Todo el código de los programas o sketches se encuentra comentado y contiene las referencias necesarias para comprender la elaboración del proyecto y su desempeño.
Cabe reseñar el uso de la memoria EEPROM con el objeto de guardar las partidas jugadas con anterioridad y crear un ranking de puntuaciones, que aunque pierda corriente esté grabado en la memoria.
Se ha orientado la programación a un modelo donde se aproveche siempre que se pueda la memoria flash en vez de la dinámica. Por ello, todo el código, en la medida de lo posible, muestra por pantalla las acciones en el monitor con la función Serial.print(F()), que libera SRAM, así como se han usado constantes y variables define en la medida de lo posible.
Por tanto, el código se ha comprimido al máximo y definido con la mayor claridad, eficacia y eficiencia según lo descrito anteriormente.
2.5 Casos de uso
Adjunto se encuentra un vídeo explicativo y demostrativo de los componentes y funcionalidades que finalmente presenta el desarrollo del proyecto.
3. Resultados
El resultado es un prototipo altamente funcional de una máquina arcade que resuelve el problema de comunicar dos sistemas a distancia mediante láseres IR y además ejemplifica el uso de varios módulos y componentes para placas de microcontrolador como la ATmega2560 y la ATmega328p.
Además, presenta una comparativa en cuanto a las capacidades de una y otra placa, para qué se orienta sus distintos tipos de memoria y su uso aplicado en proyectos similares al presente.
Se han adquirido competencias en la concepción, el diseño, implementación y presentación de un proyecto basado en un sistema empotrado, con el objeto de ganar soltura en un posible desempeño empresarial, así se han obtenido conocimientos académicos aplicados a este área de conocimiento.
4. Problemas y soluciones
Los problemas más relevantes en el desarrollo han sido:
- Ausencia de documentación técnica de algunos productos adquiridos.
- Uso de librerías modernas, que no se encuentran tan empleadas en la actualidad ni hay una extensa documentación y ejemplificación en Internet.
- Ajuste de ángulo de emisión y posición/ocultación del transceptor y receptor IR, así como el empleo de los protocolos. Los láseres IR reflejan en abundancia sus rayos por todo tipo de superficies, lo que genera ruido y más detecciones de las esperadas para este proyecto.
Las soluciones encontradas, respectivamente, fueron:
- Adquisición de nuevos componentes correctamente documentados.
- Iteración continua de pruebas de componentes y software, con el fin de encontrar el más sencillo y que cumpla con las expectativas de realización.
- Iteración continua de pruebas de componentes y software también, hasta lograr una emisión que sea recibida correctamente solo si se incide la alineación apropiada. El protocolo usado es el NEC, que es el contiene menos bits de emisión.
Apéndices
Lista 1: GameManager source code written in c
/*
* Written by Alejandro Asensio. DSE Course 2023/2024.
* King Juan Carlos University (URJC).
*
* Game Manager
*
* This sketch is part of the Calimero's Rampage project.
* Designed to be used with ATmega2560 microcontroller.
* Manages two states: idle and ingame.
* · When in idle state, it plays a music and waits for the
* player to start the game (coin inserted).
* · When in ingame state, it waits for the player to shoot
* while keeps track of the time left and the score. When
* the time is over, it shows the results, saves and
* returns to idle state.
*/
// Standard libraries
#include <Arduino.h>
#include <EEPROM.h>
// Ables to decode NEC and Universal Pulsedistance infrared signals
#define DECODE_NEC
#define DECODE_DISTANCE_WIDTH
// IRremote pin definitions library
#include "PinDefinitionsAndMore.h"
// Pin definitions
#define IR_RECEIVE_PIN 22
#define BUZZER_PIN 8
#define LASER_TRANSCEIVER_PIN 50
#define LASER_RECEIVER_PIN 35
// LCD pin definitions
const uint8_t rs = 12, e = 11, d4 = 5, d5 = 4, d6 = 3,
d7 = 2;
// IRremote library with implementations ver. 4.2.0
#include <IRremote.hpp>
#include <LiquidCrystal.h>
#include <pitches.h>
// LCD input manager definition
LiquidCrystal lcd(rs, e, d4, d5, d6, d7);
// Global const variables
const uint32_t IDLE_PERIOD = 5000L;
const uint32_t GAME_PERIOD = 60000L;
const int SHOWDOWN_PERIOD = 10000;
const int SCORE_UPDATE_PERIOD = 1000;
const int MAX_SCORE = 255;
// Global to crash variables
bool onGame = false;
int iScore = 0;
int eeAddress = 0;
// Musical piece definition for the idle state. Src:
// +----------------------------------------------------+
// | https://github.com/hibit-dev/buzzer/tree/master |
// +----------------------------------------------------+
const int Melody[] =
{
NOTE_E2, NOTE_E2, NOTE_E3, NOTE_E2, NOTE_E2, NOTE_D3,
NOTE_E2, NOTE_E2, NOTE_C3, NOTE_E2, NOTE_E2, NOTE_AS2,
NOTE_E2, NOTE_E2, NOTE_B2, NOTE_C3, NOTE_E2, NOTE_E2,
NOTE_E3, NOTE_E2, NOTE_E2, NOTE_D3, NOTE_E2, NOTE_E2,
NOTE_C3, NOTE_E2, NOTE_E2, NOTE_AS2,
NOTE_E2, NOTE_E2, NOTE_E3, NOTE_E2, NOTE_E2, NOTE_D3,
NOTE_E2, NOTE_E2, NOTE_C3, NOTE_E2, NOTE_E2, NOTE_AS2,
NOTE_E2, NOTE_E2, NOTE_B2, NOTE_C3, NOTE_E2, NOTE_E2,
NOTE_E3, NOTE_E2, NOTE_E2, NOTE_D3, NOTE_E2, NOTE_E2,
NOTE_C3, NOTE_E2, NOTE_E2, NOTE_AS2
};
const int Durations[] =
{
8, 8, 8, 8, 8, 8,
8, 8, 8, 8, 8, 8,
8, 8, 8, 8, 8, 8,
8, 8, 8, 8, 8, 8,
8, 8, 8, 2,
8, 8, 8, 8, 8, 8,
8, 8, 8, 8, 8, 8,
8, 8, 8, 8, 8, 8,
8, 8, 8, 8, 8, 8,
8, 8, 8, 2
};
// Saves the score in the EEPROM memory.
// Arduino Mega has 4kb or 4 096 bytes of EEPROM storage.
// +----------------------------------------------------+
// | SaveScore. I/O: None, void |
// +----------------------------------------------------+
void SaveScore()
{
if (eeAddress == EEPROM.length()) eeAddress = 0;
// Max score is 25 points * 255 max per byte = 6 375
if (iScore < MAX_SCORE) EEPROM.put(eeAddress, iScore/25);
else EEPROM.put(eeAddress, MAX_SCORE);
Serial.print(F("WRITTEN "));
Serial.print(iScore);
Serial.print(F(" IN "));
Serial.print(eeAddress);
Serial.println(F(" POS"));
}
// Plays a trigger sound a number of times.
// +----------------------------------------------------+
// | PlayTriggerSound. I/O: int, void |
// +----------------------------------------------------+
void PlayTriggerSound(int num)
{
for (int i = 0; i < num; i++)
{
tone(BUZZER_PIN, NOTE_DS7, 125);
delay(162.5);
noTone(BUZZER_PIN);
}
}
// Reads the scores from the EEPROM memory and shows the
// best score and the player's score.
// +----------------------------------------------------+
// | ShowResults. I/O: None, void |
// +----------------------------------------------------+
void ShowResults()
{
int bestScore = iScore;
bool isLastValue = false;
Serial.println(F("VALES READ FROM EEPROM"));
for (int idx = 2; idx < EEPROM.length() && !isLastValue;
idx++)
{
int value = EEPROM.read(idx) * 25;
int nextValue = EEPROM.read(idx + 1) * 25;
if (nextValue == 0 || nextValue == 6375)
{
isLastValue = true;
eeAddress = idx + 1;
}
if (value > bestScore) bestScore = value;
Serial.print(idx);
Serial.print(F("\t"));
Serial.print(value);
idx++;
}
lcd.clear();
char a[16];
sprintf (a, "BEST SCORE: %d ", bestScore);
lcd.setCursor(0, 0);
lcd.print(a);
char b[16];
if (bestScore != iScore) sprintf(b, "YOUR SCORE: %d ",
iScore);
else sprintf(b, "YOU ARE THE BEST");
lcd.setCursor(0,1);
lcd.print(b);
PlayTriggerSound(3);
delay(SHOWDOWN_PERIOD);
}
// Shows the ingame screen with the time left and score.
// +----------------------------------------------------+
// | ShowIngame. I/O: None, void |
// +----------------------------------------------------+
void ShowIngame()
{
char a[16];
sprintf (a, "60s TO SHOWDOWN!");
lcd.setCursor(0, 0);
lcd.print(a);
char b[16];
sprintf (b, "SCORE: %d ", iScore);
lcd.setCursor(0,1);
lcd.print(b);
Serial.println(F("GAME STARTED"));
}
// Shows the score and a message to continue playing.
// +----------------------------------------------------+
// | UpdateScore. I/O: None, void |
// +----------------------------------------------------+
void UpdateScore()
{
char s[16];
sprintf (s, "SCORE: %d, GO! ", iScore);
lcd.setCursor(0,1);
lcd.print(s);
delay(SCORE_UPDATE_PERIOD);
}
// Plays the idle music note by note.
// +----------------------------------------------------+
// | PlayIdleMusic. I/O: int, void |
// +----------------------------------------------------+
void PlayIdleMusic(int note)
{
int duration = 1000 / Durations[note];
tone(BUZZER_PIN, Melody[note], duration);
int pauseBetweenNotes = duration * 1.30;
delay(pauseBetweenNotes);
noTone(BUZZER_PIN);
}
// Shows the countdown to start the game.
// +----------------------------------------------------+
// | ShowCountdownToPlay. I/O: int, void |
// +----------------------------------------------------+
void ShowCountdownToPlay()
{
for (int i = 9; i >= 0; i--)
{
char s[16];
sprintf (s, " %ds TO START! ", i);
lcd.setCursor(0,1);
lcd.print(s);
lcd.blink();
tone(BUZZER_PIN, NOTE_DS7, 125);
delay(875);
}
}
// Initializes the LCD screen.
// +----------------------------------------------------+
// | ShowIdle. I/O: None, void |
// +----------------------------------------------------+
void ShowIdle()
{
lcd.begin(16,2);
lcd.print("CALIMERO RAMPAGE");
lcd.setCursor(0,1);
lcd.print("25c INSERT COIN");
lcd.setCursor(15,1);
lcd.blink();
iScore = 0;
}
// Setup function. Initializes the serial port, IRremote
// and the other components.
void setup()
{
Serial.begin(115200);
// Wait for serial port to connect
while (!Serial);
// Initialize IRremote
Serial.println(F("Start " __FILE__ " from " __DATE__));
Serial.println(F("Using library version " VERSION_IRREMOTE));
IrReceiver.begin(IR_RECEIVE_PIN, ENABLE_LED_FEEDBACK);
Serial.println(F("Ready to receive IR signals of protocols: "));
printActiveIRProtocols(&Serial);
Serial.println(F("at pin " STR(IR_RECEIVE_PIN)));
// Initialize the other components
ShowIdle();
pinMode (LASER_TRANSCEIVER_PIN, OUTPUT);
pinMode(LASER_RECEIVER_PIN, INPUT);
digitalWrite (LASER_TRANSCEIVER_PIN, HIGH);
// Show the EEPROM memory length
Serial.print(F("EEPROM length: "));
Serial.println(EEPROM.length());
}
// Loop function. Manages the two states: idle and ingame.
void loop()
{
if (onGame)
{
// Show the ingame screen and wait for the player to shoot
ShowIngame();
for(uint32_t tStart = millis();
(millis()-tStart) < GAME_PERIOD;)
{
// Check if the player has shot
if (IrReceiver.decode())
{
Serial.print(F("Decoded protocol: "));
Serial.print(getProtocolString(IrReceiver.decodedIRData.protocol));
Serial.print(F(", decoded raw data: "));
Serial.println(IrReceiver.decodedIRData.decodedRawData, HEX);
// If the player has shot, update the score. Discard noise
if (getProtocolString(IrReceiver.decodedIRData.protocol) != "UNKNOWN"
&& IrReceiver.decodedIRData.decodedRawData >= 4)
{
iScore += 25;
UpdateScore();
PlayTriggerSound(2);
}
// Ready to receive next value
IrReceiver.resume();
}
}
// End of the game. Stop receiving IR signals
// Show the results, save score and return to idle state
ShowResults();
SaveScore();
ShowIdle();
onGame = false;
}
else
{
// Play the idle music and wait for the player to start
int note = 0;
for(uint32_t tStart = millis();
(millis()-tStart) < IDLE_PERIOD;)
{
note++;
PlayIdleMusic(note);
int value = digitalRead(LASER_RECEIVER_PIN);
// If the player has inserted a coin, start the game
if (value == 1)
{
ShowCountdownToPlay();
PlayTriggerSound(2);
onGame = true;
break;
}
}
// End of the idle period. Stop playing the music
}
// Repeat the loop and find out the next state
}
Lista 2: Shotgun source code written in c
/*
* Written by Alejandro Asensio. DSE Course 2023/2024.
* King Juan Carlos University (URJC).
*
* Shotgun
*
* This sketch is part of the Calimero's Rampage project.
* Designed to be used with Atmega328p microcontroller.
* Controls the shotgun's infrared laser transceiver.
*/
// Standard libraries
#include <Arduino.h>
// Disables restarting receiver after each send. Saves mem
#define DISABLE_CODE_FOR_RECEIVER
// IRremote pin definitions library
#include "PinDefinitionsAndMore.h"
// IRremote library with implementations ver. 4.2.0
#include <IRremote.hpp>
#include <pitches.h>
// Pin definitions
#define LED_PIN 13
#define BUTTON_PIN 2
#define BUZZER_PIN 8
#define LASER_SENDER_PIN 12
// Global to crash variables
uint16_t sAddress = 0x0102;
uint8_t sCommand = 0x34;
uint8_t sRepeats = 1;
int buttonState = 0;
// Setup function. Initializes the serial port, IRremote
// and the other components.
void setup()
{
Serial.begin(115200);
// Wait for serial port to connect
while (!Serial);
// Initialize IRremote
Serial.println(F("Start " __FILE__ " from " __DATE__));
Serial.println(F("Using library version " VERSION_IRREMOTE));
Serial.print(F("Send IR signals at pin "));
Serial.println(IR_SEND_PIN);
IrSender.begin();
Serial.print(F("Send now: address=0x00, command=0x"));
Serial.print(sCommand, HEX);
Serial.print(F(", repeats="));
Serial.println(sRepeats);
Serial.println(F("Send standard NEC with 8 bit address"));
Serial.flush();
// Initialize the other components
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, HIGH);
pinMode(LASER_SENDER_PIN, OUTPUT);
digitalWrite (LASER_SENDER_PIN, HIGH);
pinMode(BUZZER_PIN, OUTPUT);
}
// Loop function. Reads the button state and sends the IR
void loop()
{
buttonState = digitalRead(BUTTON_PIN);
if (buttonState == 1)
{
// Send the IR signal
// Turn off the laser and led as feedback
digitalWrite (LASER_SENDER_PIN, LOW);
digitalWrite(LED_PIN, LOW);
// Send the IR signal
Serial.print(F("Sending: 0x"));
Serial.print(sAddress, HEX);
Serial.print(sCommand, HEX);
Serial.println(sRepeats, HEX);
// Avoids disturbing the software PWM gen by serial interrupts
Serial.flush();
IrSender.sendNEC(sAddress, sCommand, sRepeats);
// Play the shot sound
tone(BUZZER_PIN, NOTE_C7, 250);
delay(162.5);
noTone(BUZZER_PIN);
// Delay must be greater than 5 ms if short signal
delay(2837.5);
// Turn on the laser and led as feedback
digitalWrite (LASER_SENDER_PIN, HIGH);
digitalWrite(LED_PIN, HIGH);
}
// Now the button is released
}