| Weitere Artikel aus dem Elo-Magazin |
Ping-Pong Wettbewerb
Etch-A-Sketch mit dem Franzis Ping -Pong
Mit dem Programm "malen" ist es möglich Bilder zu zeichnen, wobei das linke Rad die Cursorposition in der Y-Achse (Höhe) und das rechte Rad die Position in auf der X-Achse steuert. Je länger der Cursor auf einer Position belassen wird, desto heller leuchtet danach das Pixel..
von Patrick Kaiser
Wer kennt nicht diese Maltafeln aus seiner Kindheit, bei denen man mit zwei Rädern ein Bild zeichnen und es durch Schütteln wieder löschen konnte?
Der Pong-Bausatz bietet sich mit seinen zwei Steuerrädern gerade dazu an ihn zum Malen zu verwenden und ist damit auch schon für die Kleinsten interessant.
Mit dem Programm "malen" ist es möglich Bilder zu zeichnen, wobei das linke Rad die Cursorposition in der Y-Achse (Höhe) und das rechte Rad die Position in auf der X-Achse steuert.
Je länger der Cursor auf einer Position belassen wird, desto heller leuchtet danach das Pixel.
Bis nach dem Einstellen der ersten Position das Pixel seine erste Helligkeitsstufe erreicht, vergeht etwa eine halbe Sekunde. Möchte man das Spiel ausschalten, dreht man beide Räder nach innen - an dieser Stelle weicht es vom Vorbild etwas ab. ;-)
Ganzen Artikel lesen...

Dass der Cursor etwas flackert, hängt damit zusammen, dass er besonders hell ist und sich vom dunkleren Hintergrund absetzt. Je weniger er flackert, desto heller das Pixel hinterher. Man kann den Cursor somit auch in einem hellen Bereich verstecken nachdem man sich zum Beispiel ein Reklame-Schild gezeichnet hat. Beim kreativen Malen wünsche ich viel Spaß und würde mich freuen, wenn ich nochmal Feedback mit einem so entstandenen Kunstwerk bekäme.
Das Spiel wurde mit der Arduino-Umgebung in der Sprache C++ geschrieben.
Im Folgenden beschreibe ich, wie man sich die Umgebung einrichtet um das Spiel selbst zu kompilieren. I
ch hoffe damit den Grundstein für eine eigene Pong-Umgebung setzen zu können, denn die Umgebung bietet einige Vorzüge zu denen ich später noch kommen werde.
Jetzt an's Einrichten: In meinem Fall ist das Betriebssystem Windows XP. Zunächst lädt man sich von arduino.cc die aktuellste Version der Umgebung herunter:
Anschließend entpackt man sie in seinen Lieblings-Pong-Ordner. Möchte man, dass Einstellungen im Programmverzeichnis (statt im Benutzerordner) gespeichert werden, muss man außerdem in den Unterordner "lib" wechseln und in der Datei "perferences.txt" den Eintrag "#settings.path=data" ändern in "settings.path=./settings" (ohne "#" !) um die Konfigurationsdatei im Unterverzeichnis "settings" zu speichern. Hier kann man außerdem den Eintrag "#build.path=build" anpassen, wenn man die temporären Daten (u.a. die kompilierte *.hex-Datei) in einem bestimmten Ordner haben möchte. Das kann Sinn machen, wenn man mit dem eingebauten Flash-Programm Schwierigkeiten bekommt.
Als nächstes geht man in das Unterverzeichnis "hardware/arduino" und editiert die Datei "programmers.txt" - in meinem Fall habe ich unten einfach folgenden Abschnitt hinzugefügt:
##############################################################
pong.name=Pong mit ATmega8
pong.upload.protocol=stk500
pong.upload.maximum_size=8192
pong.upload.speed=19200
#pong.bootloader.low_fuses=0xe4
#pong.bootloader.high_fuses=0xd9
pong.bootloader.path=atmega8
pong.bootloader.file=ATmegaBOOT.hex
#pong.bootloader.unlock_bits=0xFF
#pong.bootloader.lock_bits=0xFF
pong.build.mcu=atmega8
pong.build.f_cpu=8000000L
pong.build.core=arduino
Die Angaben zum Bootloader sollten eigentlich ignoriert werden - sicherheitshalber kommentieren wir sie trotzdem mit "#" aus. Der Takt wurde hier auf die 8MHz unseres Pong angepasst. Nun muss die Umgebung einmal gestartet und im Menü "Tools->Board" der neu angelegte Eintrag "Pong mit ATmega8" ausgewählt werden.
Anschließend schließen wir die Umgebung wieder, da erst jetzt die endgültige Datei mit den Einstellungen angelegt wird. Sie befindet sich in meinem Fall im Unterordner "settings" und heißt "perferences.txt", hat aber mit der gleichnamigen Datei von zuvor nichts gemein!
Etwa in der Mitte (Zeile 26) der Datei befindet sich der Eintrag "upload.using=bootloader". Will man direkt seinen Programmer in der Umgebung betreiben und keinen Bootloader für seriellen Upload, muss der Eintrag geändert werden.
Orientieren kann man sich an den Einträgen in der Datei "hardware/arduino/programmers.txt" jeweils vor dem Punkt. In meinem Fall möchte ich Pong wie gewohnt mit dem Programmer flashen und mein Programmer ist ein AVR ISP MKII, also lautet der Eintrag bei mir "upload.using=avrispmkii". Nun kann man die Umgebung starten und versuchen ein "int main(){while(1);}" zu flashen - gelingt das nicht, muss man vermutlich den libusb-filter (libusb-win32-filter-bin-0.1.12.2.exe )installieren weil avrdude (der Flasher) den USB-Programmer nicht gefunden hat. Alternativ flasht man das Hex-File aus dem Build-Pfad (am Anfang besprochen) manuell.
Wofür der ganze Aufwand? Wer die Arduino-Platform kennt, weiß die Umgebung, die Community drum herum und die vielen kostenfreien Bibliotheken zu schätzen: Es gibt Bibliotheken um Töne über angeschlossene Lautsprecher (an beliebigen Pins) abzuspielen oder Servos anzusteuern, oder Displays oder I2C oder... Damit wäre es zum Beispiel auch möglich eine Pong- oder PongDisplay-Klasse zu schreiben und daraus eine Bibliothek zu machen, die per Mausklick hinzugefügt werden kann. (Leider musste ich diesmal im Streit mit dem Compiler klein beigeben, weil meine Kentnisse in C++zu schwach sind, sonst hätte ich das natürlich mitgeliefert...) Außerdem arbeitet die Umgebung plattformübergreifend - das heißt die Linux-User sind auch wieder mit im Boot.
Beim Arbeiten mit der Umgebung sollte man allerdings auch über die Besonderheiten bescheid wissen: Zum einen nummeriert die Umgebung alle Pins und unterscheidet zwischen analogen und digitalen Pins. Möchte man analoge Pins digital nutzen, kann man sie mit der Nummer ansprechen, die sie hätten, wenn sie die letzten digitalen Pins wären. Eine Übersicht findet sich unter www.arduino.cc
Zum Anderen muss man wissen, dass Arduino-Programme gewöhnlich eine "void setup(){...}"- Funktion und eine "void loop(){...}"-Funktion haben. Die setup-Funktion wird benutzt um alles zu initialisieren, was zu initialisieren ist und die loop-Funktion wird von der Umgebung automatisch immer wieder aufgerufen (praktisch wie ein while(1)-Block). Diese Funktionen muss man nicht, aber sollte man nutzen, da die delay-Funktion mit Timern arbeitet und die Bibliotheken davon ausgehen, dass die Umgebung so benutzt wird, wie sie gedacht ist. Anmerkung: Der Timer0 ist dann tabu und er sollte auch mit "init();" als erstes nach einem "sleep" wieder in Gang gebracht werden. Man kann aber prinzipiell (wenn man die io.h weglässt) auch normalen AVR-Studio-Code oder C+ + - Code darin einbringen - dann sollte man allerdings die Delay-Funktionen des gcc verwenden: In der Datei "hardware\arduino\cores\arduino\wiring.h" entsprechende delay-Funktionen auskommentieren und unten Folgendes einfügen:
#define F_CPU 8000000UL // CPU Takt (für delay-Routine)
#include <util/delay.h> // Definition der Verzögerungsfunktionen (_delay_ms)
#define delayMicroseconds(x) _delay_us(x)
#define delay(x) _delay_ms(x)
Außerdem sollte man dann die Inhalte der Datei "wiring.c" praktisch komplett auskommentieren. Vermutlich ist es also wirklich einfacher sich mit den beigelegten Funktionen anzufreunden. Ich würde mich freuen vielleicht bald an dieser Stelle eine einfach zu bedienende Pong-Library vorzufinden, damit man kein Elektroniker mehr sein muss um mit dem Pong-Bausatz zu programmieren.
Download: Adruino-Projekt und Hexfile
#include <avr/interrupt.h> /* Interruptbehandlungsroutinen (für Timerinterrupt) */
#include <avr/sleep.h>
#define WIDTH 12 /* Breite des Displays - entspricht x*/
#define HEIGHT 10 /* Höhe des Displays - entsprcht y*/
#define disp_pin0 14
#define disp_pin1 15
#define disp_pin2 16
#define disp_pin3 17
#define disp_pin4 4
#define disp_pin5 5
#define disp_pin6 6
#define disp_pin7 7
#define disp_pin8 8
#define disp_pin9 9
#define disp_pin_data 12
#define disp_pin_clock 11
#define disp_pin_strobe 10
#define INT0pin 2 //fragt Münzschlitz ab und gibt Strom für Potis
#define frame_slowness 0x8; //Wie lange gewartet wird bis nächste Spalte gezeichnet wird. - Wert nicht zu
klein machen!
volatile uint8_t leds[WIDTH][HEIGHT]; //Höhe = die 10 (der 16) Bits
volatile uint8_t aktZeile; //Spalten-Abtast-Counter
volatile uint8_t brightness_cnt;
volatile uint16_t poti_a;
volatile uint16_t poti_b;
volatile uint8_t adc_switch;
volatile uint16_t permanent_cnt; //Hilft bei Zeit-Funktionen
//alte Position speichern um Bewegung zu erkennen
uint8_t pa_alt;
uint8_t pb_alt;
void one_reg();
void null_reg();
void empty_reg();
void fill_reg(); //=Bildschirm löschen weil Negativ-Schaltung mit Register
void set_disp_ports();
void clr_disp_ports();
void adc_init();
void component_inits();
void power_off();
void power_on();
ISR(ADC_vect);
ISR(TIMER2_COMP_vect);
ISR(INT0_vect);
void setup()
{
cli();
aktZeile=0;
brightness_cnt=0;
permanent_cnt=0;
//Bildchen init/generieren
for(int x=0;x<WIDTH;x++){
for(int y=0;y<HEIGHT;y++){
leds[x][y]=0;
}
}
/*---------------------------------------------------
* Ports konfigurieren (Ein-/Ausgänge)
*---------------------------------------------------*/
pinMode(disp_pin0,OUTPUT);
pinMode(disp_pin1,OUTPUT);
pinMode(disp_pin2,OUTPUT);
pinMode(disp_pin3,OUTPUT);
pinMode(disp_pin4,OUTPUT);
pinMode(disp_pin5,OUTPUT);
pinMode(disp_pin6,OUTPUT);
pinMode(disp_pin7,OUTPUT);
pinMode(disp_pin8,OUTPUT);
pinMode(disp_pin9,OUTPUT);
pinMode(disp_pin_data,OUTPUT);
pinMode(disp_pin_clock,OUTPUT);
pinMode(disp_pin_strobe,OUTPUT);
pinMode(INT0pin,OUTPUT);
digitalWrite(INT0pin,LOW);
fill_reg(); //mit HIGH um LEDs auszuschalten
component_inits();
set_sleep_mode(SLEEP_MODE_ADC);
sei();
//bringt vor allem Performance wenn man es einfach dauer-an lässt:
digitalWrite(disp_pin_strobe, HIGH);
/*
//alte Position speichern um Bewegung zu erkennen
uint8_t pa_alt;
uint8_t pb_alt;
*/
}
void loop(){
//liest Potis aus
if((ADCSRA & _BV(ADSC)) == 0){ //_bv = bit_value
sleep_mode();
}
//berechnet Poti-Wert auf Display-Maße
uint8_t pa=poti_a/84;
uint8_t pb=poti_b/96;
if(pa>=WIDTH) pa=WIDTH-1;
if(pb>=HEIGHT) pb=HEIGHT-1;
int t=leds[WIDTH-pa-1][pb];//Helligkeitswert zwischenspeichern
leds[WIDTH-pa-1][pb]=5;//Helligkeit auf ganz hoch -> Positionscursor
if((pa==pa_alt)&&(pb==pb_alt)){
if(permanent_cnt>1000){
if(t<5) t++;
//prüft ob ausgeschaltet wird
if((poti_a>1010)&&(poti_b>1010)){
power_off();
power_on();
t=0;
}
permanent_cnt=0;
}
} else permanent_cnt=0;
pa_alt=pa;
pb_alt=pb;
delay(1);
leds[WIDTH-pa-1][pb]=t;
}
ISR(TIMER2_COMP_vect){
cli();
if(aktZeile==0){
clr_disp_ports(); //anti-flirr ;-)
null_reg();
set_disp_ports();
aktZeile++;
} else {
clr_disp_ports(); //anti-flirr
one_reg();
set_disp_ports();
aktZeile++;
if(aktZeile>=WIDTH){
aktZeile=0;
brightness_cnt++;
if(brightness_cnt>5) brightness_cnt=0;
}
}
permanent_cnt++;
sei();
}
void one_reg()
{
digitalWrite(disp_pin_data, HIGH); /* Eins schicken (PB4 = HIGH) */
//digitalWrite(disp_pin_strobe, HIGH);
digitalWrite(disp_pin_clock, HIGH);
digitalWrite(disp_pin_clock, LOW);
//digitalWrite(disp_pin_strobe, LOW);
digitalWrite(disp_pin_data, LOW); /* Data wieder auf LOW rücksetzen (PB4 = LOW) */
}
void null_reg()
{
digitalWrite(disp_pin_data, LOW);
//digitalWrite(disp_pin_strobe, HIGH);
digitalWrite(disp_pin_clock, HIGH);
digitalWrite(disp_pin_clock, LOW);
//digitalWrite(disp_pin_strobe, LOW);
}
//füllt mit LOW
void empty_register()
{
for(int i=0;i<16;i++){
null_reg();
}
}
//füllt mit HIGH
void fill_reg()
{
for(int i=0;i<16;i++){
one_reg();
}
}
void set_disp_ports()
{
//-----------------------------------------
if(leds[aktZeile][0]<=brightness_cnt) digitalWrite(disp_pin0,LOW); else digitalWrite(disp_pin0,HIGH);
if(leds[aktZeile][1]<=brightness_cnt) digitalWrite(disp_pin1,LOW); else digitalWrite(disp_pin1,HIGH);
if(leds[aktZeile][2]<=brightness_cnt) digitalWrite(disp_pin2,LOW); else digitalWrite(disp_pin2,HIGH);
if(leds[aktZeile][3]<=brightness_cnt) digitalWrite(disp_pin3,LOW); else digitalWrite(disp_pin3,HIGH);
if(leds[aktZeile][4]<=brightness_cnt) digitalWrite(disp_pin4,LOW); else digitalWrite(disp_pin4,HIGH);
if(leds[aktZeile][5]<=brightness_cnt) digitalWrite(disp_pin5,LOW); else digitalWrite(disp_pin5,HIGH);
if(leds[aktZeile][6]<=brightness_cnt) digitalWrite(disp_pin6,LOW); else digitalWrite(disp_pin6,HIGH);
if(leds[aktZeile][7]<=brightness_cnt) digitalWrite(disp_pin7,LOW); else digitalWrite(disp_pin7,HIGH);
if(leds[aktZeile][8]<=brightness_cnt) digitalWrite(disp_pin8,LOW); else digitalWrite(disp_pin8,HIGH);
if(leds[aktZeile][9]<=brightness_cnt) digitalWrite(disp_pin9,LOW); else digitalWrite(disp_pin9,HIGH);
//-----------------------------------------
}
void clr_disp_ports()
{/
/-----------------------------------------
digitalWrite(disp_pin0,LOW);
digitalWrite(disp_pin1,LOW);
digitalWrite(disp_pin2,LOW);
digitalWrite(disp_pin3,LOW);
digitalWrite(disp_pin4,LOW);
digitalWrite(disp_pin5,LOW);
digitalWrite(disp_pin6,LOW);
digitalWrite(disp_pin7,LOW);
digitalWrite(disp_pin8,LOW);
digitalWrite(disp_pin9,LOW);
//-----------------------------------------
//Hier könnte das Register auch noch gelöscht werden...
}
ISR(ADC_vect){
cli();
if(adc_switch==6){
poti_b=ADCW;
ADMUX =0b01000111;
adc_switch=7;
} else {
poti_a=ADCW;
ADMUX =0b01000110;
adc_switch=6;
}
sei();
}
void adc_init(){
adc_switch=6; //adc6
//starte ADC6+ADC7
//ref-spannung auf AVCC, adjust left für 10bit-ergebnis, am ende 0110=ADC6 0111=ADC7
//welcher, kann während umwandlung verändert werden und bleibt bis sie abgeschlossen ist
ADMUX =0b01000110; //ADC6
ADCSRA = _BV(ADEN) | _BV(ADIE) | _BV(ADPS2) | _BV(ADPS1) | _BV(ADPS0);
//ADCSRA=0b11101111;
}
void component_inits(){
aktZeile=0;
brightness_cnt=0;
permanent_cnt=0;
//Bildchen init/generieren
for(int x=0;x<WIDTH;x++){
for(int y=0;y<HEIGHT;y++){
leds[x][y]=0;
}
}
// Timer0 ist leider schon in Verwendung, wenn nicht die entsprechenden Abschnitte in wiring.c und
wiring.h auskommentiert werden
// mit der folge, dass die delay-Routinen aus der avr-gcc-lib übernommen werden müssen
// er muss aber trotzdem init()ialisiert werden, sonst wacht er aus dem Schlaf nicht mehr auf (diese
Routine wird nach dem
// Einschalten ebenfalls aufgerufen)
init();
//Timer 2 (8-Bit) für Multiplexing der LEDs
TCCR2 = (1<<CS22)|(1<<CS21)|(1<<WGM21); // 1/256 Vorteiler, CTC
OCR2= frame_slowness; // einfach ein init-wert
TIMSK |= (1<<OCIE2); // compare-irq enabled
adc_init(); //ADC on
}
void power_off()
{
//sleep einstellen
set_sleep_mode(SLEEP_MODE_PWR_DOWN);
ADCSRA=0; //ADC complete conversion
ADCSRA=0; //ADC off
//timer deaktivieren
TCCR0=0;
TCCR1B=0;
TCCR2=0;
fill_reg();
clr_disp_ports();
//irgendwie gehts auch ohne folgende Zeile *schulterzuck*
pinMode(INT0pin,INPUT);
digitalWrite(INT0pin,HIGH); //PullUP = Spg von Potis nehmen und INT0 sensitiv für gnd-schalter
machen
//int0 aktivieren (auf low-level sensitiv)
MCUCR &= ~(0x3); // levelgesteuerter Interrupt an INT0
GICR |= (1 << INT0); // externen Interrupt freigeben
//schlaf bis int0 (schalter) aufweckt
sleep_mode();
}
void power_on()
{
pinMode(INT0pin,OUTPUT); // 5V wieder deaktivieren (->Potis aktiviert)
digitalWrite(INT0pin,LOW);
digitalWrite(disp_pin_strobe, HIGH); //damit reg die daten übernimmt
set_sleep_mode(SLEEP_MODE_ADC);
component_inits();
sei();
}
ISR(INT0_vect) {
GICR &= ~(1 << INT0); // externen Interrupt sperren
}
Nachtrag 31.3.:
Ich habe nochmal ausprobiert und für das einfache Kompilieren noch eine einfachere Lösung gefunden für die temporäre Installation (allerdings geht der Timer dann falsch, die Einstellungen sind im Benutzerordner und man hat weniger Platz fürs Kompilat weil der Bootloader eingerechnet wird - vlt kompiliert er aber trotzdem auch Größeres): Man kann einfach das Programm in eine beliebige Arduino-Umgebung laden, als Board "Arduino/NG or older w/ATmega8" (das ist der letzte Punkt; siehe auch Screenshot in dem eingereichten Beitrag) wählen und auf den Play-Button drücken. Danach kann man das hex-File aus dem Temporären Ordner fischen und zum Beispiel mit AVR-Studio flashen.Cooler finde ich aber, wenn man alles aus der Umgebung machen kann! ;-) Die Unterschiede der so entstandenen hex-Files scheinen wohl nicht soo groß zu sein (angehängt sind die zwei unterschiedlichen Kompilate und die Disassemblierung - die Unterschiede zeigt zum Beispiel "WinMerge" gut an indem man beide Dateien im Explorer markiert und mit Rechtsklick WinMerge wählt).
Download: Malen2
Nachtrag 31.3.: Apfelmännchen
Weil ich weniger begabt bin im malen, habe ich mal ein Foto vom Apfelmännchen auf dem Pong geschossen.
Da habe ich allerdings etwas geschummelt und den Processing-Code (auf Processing basiert die Arduino-Umgebung und wird gerne kombiniert) von http://www.cognitiones.de/doku.php/mandelbroetchen etwas umgeschrieben und in das Malprogramm-Gerüst gesteckt.
Das Besondere daran ist, dass ich eine Art von AntiAliasing verwendet habe:
Ich rechne mit doppelter Auflösung als das Pong hat und rechne diese als Helligkeit in die umliegenden Pixel mit ein.
![]() | Franzis Ping-Pong Produktart: Softwarebox 29,95 € |













