🚧 Dinodialogomat: Mund-Servos mit Audio von MP3-Player steuern

Synopsis: Wählscheibentelefon

Zwei Dinofiguren treten in Dialog und ihre Münder werden über Servos synchron zur Sprache bewegt. Ein MP3-DFPlayer Mini liefert das Audio-Signal, das über Analogeingänge des Arduino in Servosignale umgewandelt wird. Der Start des Automaten geschieht über einen Ultraschallsensor.

Dinodialogomat Prototyp
Dinodialogomat Prototyp

Blockschaltbild der Prototypschaltung

Dinodialogomat Blockschaltbild
Dinodialogomat Blockschaltbild

Die Pin-Belegungen sind hier nicht hübsch geraten, bis auf die Analog-Eingänge aber frei wählbar. Die Münder der Figuren werden durch Servos bewegt. Der DFPlayer Mini ist über eine Software Serielle Schnittstelle verbunden, ein Widerstand R1 ist notwendig, weil der Player intern auf 3,3V läuft, der jedoch Arduino Uno jedoch 5V liefert, durch den Widerstand wird der Strom begrenzt. Die Stereoausgänge des Players sind mit den Analogeingängen des Arduino verbunden. Mit einem Ultraschallsensor wird gemessen ob sich eine Person vor dem Automat befindet. Das Poti R4 dient zur Lautstärke-Einstellung und um ggfs. die Endpositionen der Servos in einem Setup justieren zu können.

DFPlayer Mini und Library DFPlayer Mini Fast

Links zum Player: [https://wiki.dfrobot.com/DFPlayer_Mini_SKU_DFR0299]

Dateien auf SD-Karte speichern

DateistrukturDateien sollten mit Nummern wie 0001.mp3 gespeichert sein.
Wenn die Dateien im Root- (Haupt-) Verzeichnis abgelegt werden, gibt die Reihenfolge beim Kopieren auf die SD-Karte die Nummerierung vor, unabhängig vom Dateinamen. In Unterverzeichnissen wie “mp3” wird dagegen die Nummer im Dateinamen beachtet. Hinter den Nummern dürfen durchaus weitere beschreibende Texte stehen, siehe Beispiel rechts.
Bei diesem Beispiel wird mit der DFPlayerMini-Library aus dem Verzeichnis “mp3” mit playFromMP3Folder(nummer) gespielt.

Weitere Info: [https://wolles-elektronikkiste.de/dfplayer-mini-ansteuerung-mit-dem-arduino] (Gute Beschreibung der Verzeichnisstruktur der SD-Karte)

Dateistruktur SD-Karte
Dateistruktur SD-Karte

Arduino Library zu Ansteuerung des Players

Ich verwende die Library 🔗DFPlayerMini Fast.

Abfragen ob der Player gerade spielt

Es gibt zwei Möglichkeiten dafür:

  1. myMP3.isPlaying() wird über die Serielle Schnittstelle gemacht, braucht aber 64ms!
  2. Über den BUSY-Pin am Player, solange er Low ist spielt der Player.
Library DFPlayerMini_Fast
Library DFPlayerMini_Fast

Hardware Info und Erfahrungen

AD-Wandler Referenzspannung

Die Analogsignale des Players und des Potentiometers werden mit dem AD-Wandler des Arduino gemessen. Als Spannungsreferenz dient normalerweise die Versorgungsspannung des µC, beim Uno 5V. Siehe [arduino.cc/reference/en/language/functions/analog-io/analogreference/]
Die Auflösung ist dabei typischerweise 10 Bit -> 0..1023 als Messwert. Siehe [https://docs.arduino.cc/language-reference/de/funktionen/analog-io/analogRead/]
Nachteilig ist dabei, dass für kleine Messwerte die AD-Wandlerwerte klein sind und ausserdem Spannungsschwankungen und Rauschen der Versorgungsspannung die Messungen verschlechtern. Daher verwende ich die interne 1,1V Referenz des ATmega328P. Im setup() eingestellt mit analogReference(INTERNAL); Die Referenzspannung wird dabei auf dem AREF-Pin des µC ausgegeben an den ich das Poti angeschlossen habe.
Bei einem anderen µC ist die Referenzspannungseinstellung entsprechend an zu passen, z.B. beim Arduino Mega auf INTERNAL1V1.
Die Lautstärke des MP3-Players sollte nicht <20 eingestellt werden, da die Signale dann zu klein werden.
ToDo: Messung der Signale als Bild

Servos

Die verwendeten Servos benötigen einen kräftigen Strom >1A, daher bekommen sie eine unabhängige 5V Spannungsversorgung.
Vor Inbetriebnahme müssen bislang die Endpositionen der Servos bekannt sein um sie entsprechend zu programmieren. Ein Servotester mit Ausgabe der Impulsbreite wäre praktisch: 3.2 Servo steuern mit PWM
Beim Einschalten bekommen die Servos noch kein Servosignal, normalerweise tun sie dann auch nichts, leider fahren meine Servos nach dem Einschalten in eine Position, bei der das Getriebe krachende Geräusche von sich gibt.

ToDo: Verfahren zum Justieren der Servos bei Inbetriebnahme entwickeln. Idee:

  1. Zusätzlicher Taster, wenn bei Start gedrückt in den Justagemodus gehen.
  2. Servoachsen sind bei Erstjustage noch ohne Servohebel, es wird auf linkem Servo 1500µs Pulsweite ausgegeben, Tonausgabe: “Bitte linken Mund auf Mittenposition aufstecken und Poti auf die Mitte stellen”.
  3. Taste drücken. Ggfs. Info wenn Poti nicht auf Mitte steht.. Tonausgabe: “Bitte Mund auf Zu-Position bewegen und Taste drücken.”
  4. Taste drücken. Tonausgabe: “Bitte Mund auf Auf-Position bewegen und Taste drücken.”
  5. Taste drücken. Es wird auf dem Servo 1500µs Pulsweite ausgegeben, Tonausgabe: “Bitte rechten Mund auf Mittenposition aufstecken und Poti auf die Mitte stellen”.
  6. Taste drücken. Ggfs. Info wenn Poti nicht auf Mitte steht.. Tonausgabe: “Bitte Mund auf Zu-Position bewegen und Taste drücken”.
  7. Taste drücken. Tonausgabe: “Bitte Mund auf Auf-Position bewegen und Taste drücken.”

Synopsis: [arduino.cc/en/Tutorial/LibraryExamples/EEPROMWrite] [docs.arduino.cc/learn/built-in-libraries/eeprom/]

Arduino Software

ToDo Doku:

  • Entfernungsmessung mit Ultraschall
  • Einbinden Schwaetzo
  • Wecker
  • Zustandsautomat, enum
// Dinodialogomat V0.2 © 8.9.2024 Oliver Mezger MezMedia.de CC BY-SA 4.0
#include <SoftwareSerial.h>
#include <DFPlayerMini_Fast.h>
#include "SchwaetzoLib.h"

#define TRIGGER 12 // Digitalpin 12 zum Auslösen einer Entfernungsmessung
#define ECHO 13    // Pin 13 empfängt den Messimpuls
#define BUSY 2     // MP3-Player Busy-Pin, low aktiv
#define POTI A2    // Pin des Potentiometers
#define NAHE_ENTFERNUNG 60 // beim Unterschreiten ist Person nahe
SoftwareSerial mySerial(10, 11); // RX, TX, fuer DF-Player
DFPlayerMini_Fast myMP3;
Schwaetzo schwaetzoL(A1,9,180,140); // AudioIn, ServoOut, mundZu, mundAuf
Schwaetzo schwaetzoR(A0,8,170,130);

enum zustandstyp {RUHE_ENTRY,RUHE,NAHE,SPIELEN}; // definiere Aufzählungstyp
enum zustandstyp zustand = RUHE_ENTRY; // Definiere und initialisiere Variable
byte lautstaerke = 20; // 0..30
byte stueck=2; // das zu spielende Stueck
void einstellenLautstaerke(){
  unsigned int l = analogRead(POTI)/34; // POTI einelsen und 0..1023 auf 0..30 umsetzen
  if (l>30) l=30;
  if (l!=lautstaerke){
    lautstaerke=l;
    Serial.print(F("Neue Lautstaerke: "));
    Serial.println(l);
    myMP3.volume(l);
  }
}
void setup() {
  Serial.begin(115200);      // Ausgabe ueber serieller Monitor
  mySerial.begin(9600);      // Anbindung des DF-Player
  myMP3.begin(mySerial);
  analogReference(INTERNAL); // A/D Referenzspannung https://www.arduino.cc/reference/en/language/functions/analog-io/analogreference/
  einstellenLautstaerke();
  delay(200);
  //myMP3.playFromMP3Folder(1);
  myMP3.pause();
  pinMode(TRIGGER, OUTPUT); // Trigger-Pin ist ein Ausgang
  pinMode(ECHO, INPUT);     // Echo-Pin ist ein Eingang
  pinMode(BUSY,INPUT);      // Abfragen ob MP3-Player spielt
}
unsigned int messeEntfernung(){    // Messung ob jemand vor dem Automat steht
  static unsigned int entfernung=0; // persistente Variable für Entfernung in cm
  unsigned int mess;
  digitalWrite(TRIGGER, HIGH); // Trigger-Pin high zum Start der Messung
  delayMicroseconds(10);       // 10 µs reichen 
  digitalWrite(TRIGGER, LOW);  // Trigger-Pin low
  mess = pulseIn(ECHO, HIGH,12000); // auf 12ms begrenzen für max 206cm
  mess = mess/58; // näherungsweise Berechnung der cm 
  if (mess==0||mess>200){
    entfernung = 200;
  }
  else{
    entfernung = (entfernung+mess)/2; // Messungen glätten
  }
  //Serial.println(entfernung);
  return entfernung;
}
unsigned long weckzeit; // wann der Wecker "klingeln" soll
void stelleWecker(unsigned int n){ // n ist Zeit in ms
  weckzeit = millis()+n;
}
bool abgelaufenWecker(){
  return millis()>=weckzeit;
}
void loop() {
  static unsigned char naheZyklen=0;
  switch (zustand){
    case RUHE_ENTRY:
      stelleWecker(400); // 0,4 Sekunden
      schwaetzoL.aus();
      schwaetzoR.aus();
      zustand=RUHE;
      break;
    case RUHE: // Warten auf Person
      if (abgelaufenWecker()){ // wenn Zeit um ist
        if(messeEntfernung()<NAHE_ENTFERNUNG){
          zustand=NAHE;
          naheZyklen=0;
        }
        stelleWecker(400); // 0,4s
      }
      break;  
    case NAHE:
      if (abgelaufenWecker()){
        if(messeEntfernung()<NAHE_ENTFERNUNG){
          naheZyklen++;
        }
        else naheZyklen=0;
        stelleWecker(400);
      }
      if(naheZyklen>3){ // Person steht vor Automat
        schwaetzoL.an(); // Servosignal anschalten
        schwaetzoR.an();
        Serial.print(F("Spiele Stueck: "));
        Serial.println(stueck);
        myMP3.playFromMP3Folder(stueck);
        stueck++;
        if (stueck>4)stueck=0;
        stelleWecker(1000); // 1 Sekunde
        zustand=SPIELEN;
      }
      break;
    case SPIELEN:
      schwaetzoL.go();  // Audio messen und auf Servo ausgeben
      schwaetzoR.go();
      if(abgelaufenWecker()){
        /*if(!myMP3.isPlaying()){ // Stück zu ende? Braucht 64ms für Abfrage
          zustand=RUHE_ENTRY;
        }*/
        if(digitalRead(BUSY)){ // MP3 spielt? Low aktiv
          zustand=RUHE_ENTRY;
        }
        stelleWecker(1000); // 1 Sekunde
        einstellenLautstaerke();
      }
    break;
  }
}

Schwaetzo-Library

ToDo Doku:

  • Konstruktor, Servoendpositionen
  • AD-Wandler Referenzspannung
  • Messprinzip Sampels nehmen und Maximum bestimmen, Abtastfrequenz
  • Verhalten von Servos bei keinem Signal, Problem bei den verwendeten Servos
  • analogRead(audioEingang)>>3; // Audiosignal lesen 4 Bedeutung des >>3
// SchwaetzoLib.cpp V1.0 (c) Oliver Mezger 8.9.2024

#include "Arduino.h"
#include "SchwaetzoLib.h"

Schwaetzo::Schwaetzo(byte ein,byte aus,byte mzu, byte moffen){ // AudioEingang ServoAusgang MundZu MundOffen
  audioEingang = ein;
  servoAusgang = aus;
  mundZu = mzu;
  mundOffen = moffen;
  pinMode(aus, OUTPUT); // Servosignal = 0
  digitalWrite(aus,LOW);
}

void Schwaetzo::go(){ // Sample aufnehmen braucht 116µs somit bei 2 Kanälen 4,1 kHz Abtastfrequenz
  unsigned int n;
  if (soundSampels >=150){ // wurden genug Sampels aufgenommen 4100Hz/50Hz=82 
    soundSampels =0;
    if (audio > maxAudio){  // Maximallautstaerke des Stueckes merken
      maxAudio = audio;
      Serial.print(F("SoundLevel Max: "));
      Serial.println(maxAudio);
    }
    /*if (audio > (maxAudio>>stillSchwelle)){ // Schwelle 1/8 Maximum
    //sprachSituation |= 1;
    //ausTimer =0;
    }*/

    if (mundOffen < mundZu){ // mundOffen < mundZu
      n = mundZu -(audio*2*(mundZu-mundOffen)/maxAudio);
      if (n<mundOffen) servo.writeMicroseconds(mundZu*10);
      else servo.writeMicroseconds(n*10);
    }
    else{ // mundOffen > mundZu
      n = mundZu +(audio*2*(mundOffen-mundZu)/maxAudio);
      if (n>mundOffen) servo.writeMicroseconds(mundOffen*10);
      else servo.writeMicroseconds(n*10);
    }
    audio=0;
    //Serial.println(n);
  }
  else{
    n= analogRead(audioEingang)>>3; // Audiosignal lesen 4
    if(n>audio) audio=n; // Maximum finden
    soundSampels++;
  }
}

void Schwaetzo::an(){  // Servo wird angeschlossen, bekommt Signal
  maxAudio = DefaultMaxAudio;
  servo.attach(servoAusgang,1000,2000);
  Serial.print(F("Servo angeschlossen "));
  Serial.println(servoAusgang);
}

void Schwaetzo::aus(){  // Servosignal wird abgeschaltet
  //servo.writeMicroseconds((mundZu+mundOffen)*5); // in die Mitte
  servo.writeMicroseconds(mundZu*10);
  delay(100);
  servo.detach();
  Serial.print(F("Servo aus MaxLevel "));
  Serial.println(maxAudio);
}
// SchwaetzoLib.cpp V1.0 (c) Oliver Mezger 8.9.2024

#include "Arduino.h"
#include "SchwaetzoLib.h"

Schwaetzo::Schwaetzo(byte ein,byte aus,byte mzu, byte moffen){ // AudioEingang ServoAusgang MundZu MundOffen
  audioEingang = ein;
  servoAusgang = aus;
  mundZu = mzu;
  mundOffen = moffen;
  pinMode(aus, OUTPUT); // Servosignal = 0
  digitalWrite(aus,LOW);
}

void Schwaetzo::go(){ // Sample aufnehmen braucht 116µs somit bei 2 Kanälen 4,1 kHz Abtastfrequenz
  unsigned int n;
  if (soundSampels >=150){ // wurden genug Sampels aufgenommen 4100Hz/50Hz=82 
    soundSampels =0;
    if (audio > maxAudio){  // Maximallautstaerke des Stueckes merken
      maxAudio = audio;
      Serial.print(F("SoundLevel Max: "));
      Serial.println(maxAudio);
    }
    /*if (audio > (maxAudio>>stillSchwelle)){ // Schwelle 1/8 Maximum
    //sprachSituation |= 1;
    //ausTimer =0;
    }*/

    if (mundOffen < mundZu){ // mundOffen < mundZu
      n = mundZu -(audio*2*(mundZu-mundOffen)/maxAudio);
      if (n<mundOffen) servo.writeMicroseconds(mundZu*10);
      else servo.writeMicroseconds(n*10);
    }
    else{ // mundOffen > mundZu
      n = mundZu +(audio*2*(mundOffen-mundZu)/maxAudio);
      if (n>mundOffen) servo.writeMicroseconds(mundOffen*10);
      else servo.writeMicroseconds(n*10);
    }
    audio=0;
    //Serial.println(n);
  }
  else{
    n= analogRead(audioEingang)>>3; // Audiosignal lesen 4
    if(n>audio) audio=n; // Maximum finden
    soundSampels++;
  }
}

void Schwaetzo::an(){  // Servo wird angeschlossen, bekommt Signal
  maxAudio = DefaultMaxAudio;
  servo.attach(servoAusgang,1000,2000);
  Serial.print(F("Servo angeschlossen "));
  Serial.println(servoAusgang);
}

void Schwaetzo::aus(){  // Servosignal wird abgeschaltet
  //servo.writeMicroseconds((mundZu+mundOffen)*5); // in die Mitte
  servo.writeMicroseconds(mundZu*10);
  delay(100);
  servo.detach();
  Serial.print(F("Servo aus MaxLevel "));
  Serial.println(maxAudio);
}