Tonwürfel mit TTTL Klingeltönen

TTTL Klingelton Spielzeug mit Arduino

Ich wollte schon immer mein eigenes Stefan Raab Nippelboard haben. Zeit, so etwas mal zu basteln, nur nicht ganz so plump. Dazu soll noch etwas mehr Formfaktor und ein wenig mehr Spaß und Spiel dabei sein. Ich war auch schon immer ein Fan von Bit-Sounds und Chiptune. Also versuche ich alles zusammenzubringen und baue einen Tonkubus, also in Würfelform. Das finde ich etwas stilvoller als ein ordinäres Brett. Die Grundidee stammt vom Nippelboard, es gibt also einen Input und darauf folgt eine Ausgabe. Nicht in Form eines Videos wie beim Original, sondern als RTTTL-Klingelton. Je nachdem, auf welcher Seite der Würfel liegt, soll ein anderer Sound gespielt werden. Die Würfelseiten ersetzen also die Nippel. Eine Seite des Würfels bleibt ohne Funktion, dort soll der Ein/Aus-Schalter hin. Und um alles abzurunden, gibt es ein retro-mäßiges Gehäuse aus Lego.

Material

Alles Material habe ich vorwiegend bei Amazon gefunden. Insgesamt kostet alles nicht mehr als 50€. Wichtig beim Piezo ist, dass es einer ohne Festspannung ist, damit man später die Frequenz mit PWM verändern kann (es gibt auch welche mit Festspannung, die geben aber nur einen Ton von sich, dafür aber besonders laut, wie beim Rauchmelder z.B. Das sind die falschen!). Fürs Prototyping braucht man natürlich noch ein Breadboard und Jumper Wire. Zum Testen nehme ich auch gern meinen größeren Arduino Uno, das ist nicht so fummelig. Bei den Lego Einzelteilen braucht es 6 Platten für die Würfelseiten, die müssen mindestens 8x8 Noppen groß sein (Teilenummer bei Lego z.B. 4210802). Zum Verbinden der Platten eignen sich Eckteile, z.B. 473326 oder 4253815. Ich habe 12 von denen bestellt, Teilenummer 4162443.

Einzelteile mit Arduino, Piezzo Summer und Gyrosensor

Vorbereitung

Im Vorfeld hatte ich bereits die benötigten Libraries für den Gyro und das Piezo ausgesucht. Der Gyro selbst arbeitet mit Wire.h, der Piezo Buzzer nutzt die Standard Tone() Funktion von Arduino. Für den Gyro nutze ich die digitalen Pins und für den Piezo einen analogen PWM Pin. Um den Piezo zum Singen zu bringen, braucht es noch ein paar schöne RTTTL Noten. Hier sind die beiden unabhängigen Sketches.

#include<Wire.h>

const int MPU_addr=0x68;
int16_t AcX,AcY,AcZ,Tmp,GyX,GyY,GyZ;

void setup(){
  Wire.begin();
  Wire.beginTransmission(MPU_addr);
  Wire.write(0x6B);  // PWR_MGMT_1 register
  Wire.write(0);     // set to zero (wakes up the MPU-6050)
  Wire.endTransmission(true);
  Serial.begin(9600);
}

void loop(){
  Wire.beginTransmission(MPU_addr);
  Wire.write(0x3B);
  Wire.endTransmission(false);
  Wire.requestFrom(MPU_addr,14,true);  // request a total of 14 registers
  AcX=Wire.read()<<8|Wire.read();  // 0x3B (ACCEL_XOUT_H) & 0x3C (ACCEL_XOUT_L)
  AcY=Wire.read()<<8|Wire.read();  // 0x3D (ACCEL_YOUT_H) & 0x3E (ACCEL_YOUT_L)
  AcZ=Wire.read()<<8|Wire.read();  // 0x3F (ACCEL_ZOUT_H) & 0x40 (ACCEL_ZOUT_L)
  GyX=Wire.read()<<8|Wire.read();  // 0x43 (GYRO_XOUT_H) & 0x44 (GYRO_XOUT_L)
  GyY=Wire.read()<<8|Wire.read();  // 0x45 (GYRO_YOUT_H) & 0x46 (GYRO_YOUT_L)
  GyZ=Wire.read()<<8|Wire.read();  // 0x47 (GYRO_ZOUT_H) & 0x48 (GYRO_ZOUT_L)
  Serial.print("X = "); Serial.print(GyX); Serial.print("\t");
  Serial.print("Y = "); Serial.print(GyY); Serial.print("\t");
  Serial.print("Z = "); Serial.println(GyZ); Serial.print("\t");
  Serial.print("X = "); Serial.print(AcX); Serial.print("\t");
  Serial.print("Y = "); Serial.print(AcY); Serial.print("\t");
  Serial.print("Z = "); Serial.println(AcZ); Serial.print("\t");
  delay(2000);
}
const int tonePin = 11;  // for rEDI board

#define OCTAVE_OFFSET 0

// These values can also be found as constants in the Tone library (Tone.h)
int notes[] = { 0,
262, 277, 294, 311, 330, 349, 370, 392, 415, 440, 466, 494,
523, 554, 587, 622, 659, 698, 740, 784, 831, 880, 932, 988,
1047, 1109, 1175, 1245, 1319, 1397, 1480, 1568, 1661, 1760, 1865, 1976,
2093, 2217, 2349, 2489, 2637, 2794, 2960, 3136, 3322, 3520, 3729, 3951
};

char *song = "The Simpsons:d=4,o=5,b=160:c.6,e6,f#6,8a6,g.6,e6,c6,8a,8f#,8f#,8f#,2g,8p,8p,8f#,8f#,8f#,8g,a#.,8c6,8c6,8c6,c6";

void setup(void)
{
  Serial.begin(9600);
}

void loop(void)
{
  play_rtttl(song);
  while(1);
}

#define isdigit(n) (n >= '0' && n <= '9')

void play_rtttl(char *p)
{
  // Absolutely no error checking in here

  byte default_dur = 4;
  byte default_oct = 6;
  int bpm = 63;
  int num;
  long wholenote;
  long duration;
  byte note;
  byte scale;

  // format: d=N,o=N,b=NNN:
  // find the start (skip name, etc)

  while(*p != ':') p++;    // ignore name
  p++;                     // skip ':'

  // get default duration
  if(*p == 'd')
  {
    p++; p++;              // skip "d="
    num = 0;
    while(isdigit(*p))
    {
      num = (num * 10) + (*p++ - '0');
    }
    if(num > 0) default_dur = num;
    p++;                   // skip comma
  }

  Serial.print("ddur: "); Serial.println(default_dur, 10);

  // get default octave
  if(*p == 'o')
  {
    p++; p++;              // skip "o="
    num = *p++ - '0';
    if(num >= 3 && num <=7) default_oct = num;
    p++;                   // skip comma
  }

  Serial.print("doct: "); Serial.println(default_oct, 10);

  // get BPM
  if(*p == 'b')
  {
    p++; p++;              // skip "b="
    num = 0;
    while(isdigit(*p))
    {
      num = (num * 10) + (*p++ - '0');
    }
    bpm = num;
    p++;                   // skip colon
  }

  Serial.print("bpm: "); Serial.println(bpm, 10);

  // BPM usually expresses the number of quarter notes per minute
  wholenote = (60 * 1000L / bpm) * 4;  // this is the time for whole note (in milliseconds)

  Serial.print("wn: "); Serial.println(wholenote, 10);

  // now begin note loop
  while(*p)
  {
    // first, get note duration, if available
    num = 0;
    while(isdigit(*p))
    {
      num = (num * 10) + (*p++ - '0');
    }

    if(num) duration = wholenote / num;
    else duration = wholenote / default_dur;  // we will need to check if we are a dotted note after

    // now get the note
    note = 0;

    switch(*p)
    {
      case 'c':
        note = 1;
        break;
      case 'd':
        note = 3;
        break;
      case 'e':
        note = 5;
        break;
      case 'f':
        note = 6;
        break;
      case 'g':
        note = 8;
        break;
      case 'a':
        note = 10;
        break;
      case 'b':
        note = 12;
        break;
      case 'p':
      default:
        note = 0;
    }
    p++;

    // now, get optional '#' sharp
    if(*p == '#')
    {
      note++;
      p++;
    }

    // now, get optional '.' dotted note
    if(*p == '.')
    {
      duration += duration/2;
      p++;
    }

    // now, get scale
    if(isdigit(*p))
    {
      scale = *p - '0';
      p++;
    }
    else
    {
      scale = default_oct;
    }

    scale += OCTAVE_OFFSET;

    if(*p == ',')
      p++;       // skip comma for next note (or we may be at the end)

    // now play the note

    if(note)
    {
      Serial.print("Playing: ");
      Serial.print(scale, 10); Serial.print(' ');
      Serial.print(note, 10); Serial.print(" (");
      Serial.print(notes[(scale - 4) * 12 + note], 10);
      Serial.print(") ");
      Serial.println(duration, 10);
      tone(tonePin, notes[(scale - 4) * 12 + note]);
      delay(duration);
      noTone(tonePin);
    }
    else
    {
      Serial.print("Pausing: ");
      Serial.println(duration, 10);
      delay(duration);
    }
  }
}

Natürlich müssen beide Sketches noch miteinander verbunden und entsprechend angepasst werden. Außerdem fehlt noch die Logik, die die Signale vom Gyro in eine richtige Würfelseite umwandelt und darauf reagiert. Das mache ich mit ein paar einfachen Zeilen. Interessant ist, dass der Gyro selbst kein eindeutiges Signal ausgibt, sondern lediglich permanente Beschleunigungswerte. Die Seite des Gyros, die dabei unten liegt, weist stets die stärkste Beschleunigung auf, denn das ist im Prinzip die Gravitation, also die Beschleunigung in Richtung Erdmittelpunkt. Mit diesem Peak-Wert kann ich also die Lage des Würfels bestimmen. Weiterhin soll der Würfel beim Einschalten noch nicht gleich losdudeln, und die Seite mit dem Schalter darf nicht belegt werden.

int face=0;
int memory=0;

void loop(void)
{
  sense_face();
  if(face != memory){
    switch(face)
      {
        case 1: play_rtttl(songA); break;
        case 2: play_rtttl(songB); break;
        case 3: delay(1000); break;
        case 4: play_rtttl(songC); break;
        case 5: play_rtttl(songD); break;
        case 6: play_rtttl(songE); break;
      }
      memory=face;
  }
  delay(1000);
}

void sense_face()
{
  Wire.beginTransmission(MPU_addr);
  Wire.write(0x3B);
  Wire.endTransmission(false);
  Wire.requestFrom(MPU_addr,14,true);
  AcX=Wire.read()<<8|Wire.read();
  AcY=Wire.read()<<8|Wire.read();
  AcZ=Wire.read()<<8|Wire.read();
  face=0;
  AcX >  10000 ? face=1 : false;
  AcX < -10000 ? face=2 : false;
  AcY >  10000 ? face=3 : false;
  AcY < -10000 ? face=4 : false;
  AcZ >  10000 ? face=5 : false;
  AcZ < -10000 ? face=6 : false;
  if(memory==0){
    memory=face;
  }
}

Arduino Libraries und Gyro Sensor testen

Prototyp

Nachdem alle Einzelteile geliefert sind und ich auch den endgültigen Nano habe, kann ich noch einmal einen Prototypen bauen, um die Kompatibilität mit dem neuen Controller zu testen und das Wiring zu optimieren. Ich setze die Pins etwas näher zusammen, damit später das Kabelchaos gering bleibt. Außerdem ergibt sich daraus die Pin-Tabelle, damit ich beim Löten dann keinen Bockmist baue. Auch der kombinierte Code kann dann schon aufgespielt werden. Ich habe ihn noch etwas komprimiert.

Nano GND VIN 5V D3 A4 A5
Sensor GND VCC SDA SCL
Piezo - +
Power - +

Arduino Wiring festlegen

#include<Wire.h>
#define OCTAVE_OFFSET 0

const int tonePin = 3;
const int MPU_addr=0x68;
int16_t AcX,AcY,AcZ,Tmp,GyX,GyY,GyZ;
int face=0;
int memory=0;

int notes[] = { 0,
262, 277, 294, 311, 330, 349, 370, 392, 415, 440, 466, 494,
523, 554, 587, 622, 659, 698, 740, 784, 831, 880, 932, 988,
1047, 1109, 1175, 1245, 1319, 1397, 1480, 1568, 1661, 1760, 1865, 1976,
2093, 2217, 2349, 2489, 2637, 2794, 2960, 3136, 3322, 3520, 3729, 3951
};

char *songA = "korobyeyniki:d=4,o=5,b=160:e6,8b,8c6,8d6,16e6,16d6,8c6,8b,a,8a,8c6,e6,8d6,8c6,b,8b,8c6,d6,e6,c6,a,2a,8p,d6,8f6,a6,8g6,8f6,e6,8e6,8c6,e6,8d6,8c6,b,8b,8c6,d6,e6,c6,a,a";
char *songB = "smb:d=4,o=5,b=100:16e6,16e6,32p,8e6,16c6,8e6,8g6,8p,8g,8p,8c6,16p,8g,16p,8e,16p,8a,8b,16a#,8a,16g.,16e6,16g6,8a6,16f6,8g6,8e6,16c6,16d6,8b,16p,8c6,16p,8g,16p,8e,16p,8a,8b,16a#,8a,16g.,16e6,16g6,8a6,16f6,8g6,8e6,16c6,16d6,8b,8p,16g6,16f#6,16f6,16d#6,16p,16e6,16p,16g#,16a,16c6,16p,16a,16c6,16d6,8p,16g6,16f#6,16f6,16d#6,16p,16e6,16p,16c7,16p,16c7,16c7,p,16g6,16f#6,16f6,16d#6,16p,16e6,16p,16g#,16a,16c6,16p,16a,16c6,16d6,8p,16d#6,8p,16d6,8p,16c6";
char *songC = "addams:d=4,o=5,b=160:8c,8d,8e,8f,1p,8d,8e,8f#,8g,1p,8d,8e,8f#,8g,p,8d,8e,8f#,8g,p,8c,8d,8e,8f,1p,8c,f,8a,f,8c,b4,g.,8f,e,8g,e,8c,a4,f.,8c,f,8a,f,8c,b4,g.,8f,e,8c,d,8e,2f";
char *songD = "A-Team:d=8,o=5,b=125:4d#6,a#,2d#6,16p,g#,4a#,4d#.,p,16g,16a#,d#6,a#,f6,2d#6,16p,c#.6,16c6,16a#,g#.,2a#";
char *songE = "The Simpsons:d=4,o=5,b=160:c.6,e6,f#6,8a6,g.6,e6,c6,8a,8f#,8f#,8f#,2g,8p,8p,8f#,8f#,8f#,8g,a#.,8c6,8c6,8c6,c6";

void setup(void)
{
  Wire.begin();
  Wire.beginTransmission(MPU_addr);
  Wire.write(0x6B);
  Wire.write(0);
  Wire.endTransmission(true);
}

void loop(void)
{
  sense_face();
  if(face != memory){
    switch(face)
      {
        case 1: play_rtttl(songA); break;
        case 2: play_rtttl(songB); break;
        case 3: delay(1000); break;
        case 4: play_rtttl(songC); break;
        case 5: play_rtttl(songD); break;
        case 6: play_rtttl(songE); break;
      }
      memory=face;
  }
  delay(1000);
}

void sense_face()
{
  Wire.beginTransmission(MPU_addr);
  Wire.write(0x3B);
  Wire.endTransmission(false);
  Wire.requestFrom(MPU_addr,14,true);
  AcX=Wire.read()<<8|Wire.read();
  AcY=Wire.read()<<8|Wire.read();
  AcZ=Wire.read()<<8|Wire.read();
  face=0;
  AcX >  10000 ? face=1 : false;
  AcX < -10000 ? face=2 : false;
  AcY >  10000 ? face=3 : false;
  AcY < -10000 ? face=4 : false;
  AcZ >  10000 ? face=5 : false;
  AcZ < -10000 ? face=6 : false;
  if(memory==0){
    memory=face;
  }
}

void play_rtttl(char *p)
{
  byte default_dur = 4;
  byte default_oct = 6;
  int bpm = 63;
  int num;
  long wholenote;
  long duration;
  byte note;
  byte scale;

  while(*p != ':') p++;
  p++;

  if(*p == 'd')
  {
    p++; p++;
    num = 0;
    while(isdigit(*p))
    {
      num = (num * 10) + (*p++ - '0');
    }
    if(num > 0) default_dur = num;
    p++;
  }

  if(*p == 'o')
  {
    p++; p++;
    num = *p++ - '0';
    if(num >= 3 && num <=7) default_oct = num;
    p++;
  }

  if(*p == 'b')
  {
    p++; p++;
    num = 0;
    while(isdigit(*p))
    {
      num = (num * 10) + (*p++ - '0');
    }
    bpm = num;
    p++;
  }

  wholenote = (60 * 1000L / bpm) * 4;

  while(*p)
  {
    num = 0;
    while(isdigit(*p))
    {
      num = (num * 10) + (*p++ - '0');
    }

    if(num) duration = wholenote / num;
    else duration = wholenote / default_dur;

    note = 0;

    switch(*p)
    {
      case 'c': note = 1;  break;
      case 'd': note = 3;  break;
      case 'e': note = 5;  break;
      case 'f': note = 6;  break;
      case 'g': note = 8;  break;
      case 'a': note = 10; break;
      case 'b': note = 12; break;
      case 'p':
      default: note = 0;
    }
    p++;

    if(*p == '#')
    {
      note++;
      p++;
    }

    if(*p == '.')
    {
      duration += duration/2;
      p++;
    }

    if(isdigit(*p))
    {
      scale = *p - '0';
      p++;
    }
    else
    {
      scale = default_oct;
    }

    scale += OCTAVE_OFFSET;

    if(*p == ',')
      p++;

    if(note)
    {
      tone(tonePin, notes[(scale - 4) * 12 + note]);
      delay(duration);
      noTone(tonePin);
    }
    else
    {
      delay(duration);
    }
  }
}

Fertigung

Verlöten von Arduino, Gyro Sensor und Piezzo Buzzer

Jetzt muss natürlich alles korrekt verlötet werden. Wenn das erledigt ist, kann auch schon der erste Test per Batterie folgen. Ich habe den VIN vom Batterieclip mit dem Kippschalter unterbrochen. Damit läuft der Würfel nur, wenn auch wirklich eingeschaltet ist.

Heißkleber mit Arduino, Sensor und Summer

Zum Zusammensetzen benutze ich gerne Heißkleber. Das geht schnell, einfach, hält gut und leitet nicht. Die Hitze vertragen die Controller auch ohne Probleme. Ich positioniere den Nano und den Sensor an derselben Würfelseite. Somit ist alles Technische beisammen, dazu kommt auch noch der Piezo. Ich habe darauf geachtet, dass der USB-Anschluss vom Nano möglichst nicht verdeckt wird und frei zugänglich bleibt, damit ich später unkompliziert neue Jingles draufladen kann. Auf die angrenzenden Würfelseiten verteile ich noch den Kippschalter. Hier ist eine kleine Bohrung nötig, die passt aber genau auf einen der Lego-Noppen. Ich habe einen weggeschliffen und dann an gleicher Stelle das Loch durchgebohrt. Auf der gegenüberliegenden Seite, die man später abheben kann, wird die Batterie platziert. Ich hatte noch einen Schrumpfschlauch übrig, der genau um den 9V-Block passt. Den habe ich zusammen mit der Batterie festgeklebt, das funktioniert ganz prima.

Arduino TTTL Klingelton Summer

Fertig. Hier noch ein paar Ressourcen für RTTTL-Klingeltöne. Ich denke, ich werde alles thematisch aufspielen, also alles Weihnachtssongs oder alle Sommerhits oder alle Fernsehserien oder so... Vielleicht baue ich mal einen als Geburtstagsgeschenk für andere 😃 Vielleicht bestelle ich auch noch kleine bunte Einzelplatten bei Lego (z.B. 4537251). Damit lassen sich sicher ganz toll die einzelnen Würfelseiten etwas kenntlicher machen. Oder man bestellt einfach gleich verschiedenfarbige Würfelseiten...

-code.google.com -ez4mobile.com -howardforums.com -arcadetones.emuunlim.com