🚧 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.
Blockschaltbild der Prototypschaltung
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)
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:
- myMP3.isPlaying() wird über die Serielle Schnittstelle gemacht, braucht aber 64ms!
- Über den BUSY-Pin am Player, solange er Low ist spielt der Player.
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:
- Zusätzlicher Taster, wenn bei Start gedrückt in den Justagemodus gehen.
- 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”.
- Taste drücken. Ggfs. Info wenn Poti nicht auf Mitte steht.. Tonausgabe: “Bitte Mund auf Zu-Position bewegen und Taste drücken.”
- Taste drücken. Tonausgabe: “Bitte Mund auf Auf-Position bewegen und Taste drücken.”
- 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”.
- Taste drücken. Ggfs. Info wenn Poti nicht auf Mitte steht.. Tonausgabe: “Bitte Mund auf Zu-Position bewegen und Taste drücken”.
- 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);
}