Styroporkügelchen schweben lassen mit Ultraschall hat (noch) nicht geklappt

Ich hatte beim Stöbern einen Bausatz gefunden, mit dem es möglich ist, leichte Teile wie zum Beispiel Styropor Kügelchen per Ultraschall schweben zu lassen.
Die Schwebeteilchen werden zwischen zwei aufeinander ausgerichteten Ultraschallwandler Sensoren platziert und die beiden Sensoren (in dem Falle eigentlich Aktoren) erzeugen mit 40kHz eine gleichbleibende stehende Schallwelle, so das das dazwischen befindliche Teilen sich nicht wegbewegt.
Dieses physikalische Phänomen nennt man akustische Levitation. Könnte ich ja mal probieren mit einem Arduino nachzubauen.

Bei weiterer Recherche, wie das tatsächlich funktionieren kann, wurde aber recht schnell klar, das wird eine Herausforderung. Wobei die verwendeten Ultraschallwandler Sensoren hatte ich doch schon mal bei der Entfernungsmessung verwendet.

Der Arduino kann ja Pulsweitenmodulation (PWM) durchführen. Er erzeugt das elektrische Signal mit der passenden Frequenz.. Der Arduino allein hat nicht genug Kraft (Spannung/Strom), um die Wandler direkt zu betreiben. Ein Motortreiber sollte das Signal verstärken können und da habe ich ja noch den HW-095.

Bei weiterer Recherche bin ich auf amarnathkr/Ultrasonic-Levitation gestoßen, der genau das schon mal umgesetzt hat. Das schaue ich mir hier genauer an:

Hardware:
Entwicklungsboard Arduino Nano
2 x Ultraschallwandlersensor vom Typ „T“
MotoDrive2 L298N

Verdrahtung:
https://github.com/amarnathkr/Ultrasonic-Levitation/blob/main/Ckt%20Diagrm.png

Quellcode mit Erläuterungen:

// ---------------------------------------------------------------------------
// Ultraschall‑Treiber: 40 kHz Rechtecksignal auf PORTC (A0–A5)
// Jeder zweite Pin erhält das invertierte Signal.
// Erzeugt durch Timer1 im CTC‑Modus (Compare Match Interrupt).
// ---------------------------------------------------------------------------

// Startmuster: 10101010
// Dadurch sind benachbarte Pins phaseninvertiert.
// Dieses Muster wird im Interrupt bei jedem Durchlauf invertiert.
byte TP = 0b10101010;

void setup() {

  // -------------------------------------------------------------------------
  // 1. PORTC als Ausgang konfigurieren
  // -------------------------------------------------------------------------
  // DDRC steuert die Richtung der Pins:
  // 1 = Ausgang, 0 = Eingang
  // 0b11111111 bedeutet: alle 8 Pins von PORTC sind Ausgänge
  DDRC = 0b11111111;


  // -------------------------------------------------------------------------
  // 2. Timer1 initialisieren
  // -------------------------------------------------------------------------
  // Während der Konfiguration Interrupts deaktivieren,
  // damit der Timer nicht zwischendurch startet.
  noInterrupts();

  // Timer‑Register vollständig zurücksetzen
  TCCR1A = 0;   // Steuerregister A löschen
  TCCR1B = 0;   // Steuerregister B löschen
  TCNT1  = 0;   // Timerzähler auf 0 setzen

  // -------------------------------------------------------------------------
  // 3. Compare‑Match‑Wert setzen
  // -------------------------------------------------------------------------
  // Der Timer läuft mit 16 MHz (Arduino Uno).
  // Ohne Prescaler zählt er also 16.000.000 Schritte pro Sekunde.
  //
  // OCR1A = 200 bedeutet:
  //   Interruptfrequenz = 16 MHz / 200 = 80.000 Hz
  //
  // Da im Interrupt das Ausgangsmuster invertiert wird,
  // entsteht ein 40 kHz Rechtecksignal.
  OCR1A = 200;

  // -------------------------------------------------------------------------
  // 4. Timer1 in den CTC‑Modus schalten
  // -------------------------------------------------------------------------
  // WGM12 = 1 → CTC (Clear Timer on Compare Match)
  // Der Timer zählt von 0 bis OCR1A und springt dann wieder auf 0.
  TCCR1B |= (1 << WGM12);

  // -------------------------------------------------------------------------
  // 5. Prescaler auf 1 setzen (keine Teilung)
  // -------------------------------------------------------------------------
  // CS10 = 1 → Timer läuft mit voller CPU‑Geschwindigkeit
  TCCR1B |= (1 << CS10);

  // -------------------------------------------------------------------------
  // 6. Compare‑Match‑Interrupt aktivieren
  // -------------------------------------------------------------------------
  // OCIE1A = 1 → Interrupt wird ausgelöst, wenn TCNT1 == OCR1A
  TIMSK1 |= (1 << OCIE1A);

  // Konfiguration abgeschlossen → Interrupts wieder aktivieren
  interrupts();
}


// ---------------------------------------------------------------------------
// 7. Interrupt Service Routine (ISR)
// ---------------------------------------------------------------------------
// Diese Funktion wird 80.000‑mal pro Sekunde aufgerufen.
// Bei jedem Aufruf:
//   1. PORTC erhält das aktuelle Muster TP
//   2. TP wird invertiert → erzeugt ein 40 kHz Rechtecksignal
// ---------------------------------------------------------------------------
ISR(TIMER1_COMPA_vect) {
  PORTC = TP;   // Muster auf alle Pins von PORTC ausgeben
  TP = ~TP;     // Muster invertieren (10101010 → 01010101 → 10101010 ...)
}


void loop() {
  // Die gesamte Signal­erzeugung läuft im Interrupt.
  // Die Hauptschleife bleibt leer.
}

Detailierte Funktions­erklärung des Sketches

Der Sketch erzeugt ein 40 kHz Rechtecksignal auf allen Pins von PORTC (Arduino Uno: A0–A5), wobei jede zweite Leitung das invertierte Signal erhält.
Die Erzeugung erfolgt vollständig per Timer‑Interrupt, sodass die loop() leer bleiben kann.

1. Globale Variable

byte TP = 0b10101010; // Every other port receives the inverted signal
  • TP ist ein 8‑Bit‑Wert, der das Ausgangsmuster für PORTC enthält.
  • Das Muster 10101010 bedeutet:
    • PC7 = 1
    • PC6 = 0
    • PC5 = 1
    • PC4 = 0
    • PC3 = 1
    • PC2 = 0
    • PC1 = 1
    • PC0 = 0
  • Bei jedem Interrupt wird TP invertiert (~TP), sodass sich das Muster zwischen 10101010 und 01010101 abwechselt.

Damit entsteht auf jedem Pin ein Rechtecksignal, aber mit Phasenverschiebung zwischen benachbarten Pins.

2. setup(): Initialisierung

2.1 PORTC als Ausgang konfigurieren

DDRC = 0b11111111;
  • Alle 8 Pins von PORTC werden als Ausgänge gesetzt.
  • Auf dem Arduino Uno sind das die analogen Pins A0–A5 (PC0–PC5) plus PC6/PC7 (intern vorhanden).

2.2 Timer1 konfigurieren

Der Timer1 ist ein 16‑Bit‑Timer, ideal für präzise Frequenzen.

Interrupts deaktivieren

noInterrupts();

Damit wird verhindert, dass während der Konfiguration ein Interrupt ausgelöst wird.

Timerregister löschen

TCCR1A = 0;
TCCR1B = 0;
TCNT1 = 0;
  • Timer wird vollständig zurückgesetzt.
  • Zählerstand auf 0.

Compare‑Match‑Wert setzen

OCR1A = 200;

Das bedeutet:

Interruptfrequenz=16MHz200=80kHz


Da du im Interrupt das Ausgangsbit invertierst, entsteht:

  • 80 kHz Umschaltfrequenz
  • → 40 kHz vollständige Rechteckperiode

CTC‑Modus aktivieren

TCCR1B |= (1 << WGM12);
  • CTC = Clear Timer on Compare Match
  • Timer zählt von 0 bis OCR1A
  • Bei Erreichen des Wertes wird:
    • ein Interrupt ausgelöst
    • der Timer automatisch auf 0 gesetzt

Prescaler auf 1 (keine Teilung)

TCCR1B |= (1 << CS10);
  • Timer läuft mit voller CPU‑Taktfrequenz (16 MHz).

Compare‑Match‑Interrupt aktivieren

TIMSK1 |= (1 << OCIE1A);
  • Der Timer löst nun bei jedem Erreichen von OCR1A den Interrupt TIMER1_COMPA_vect aus.

Interrupts wieder aktivieren

interrupts();

3. ISR: Der Timer‑Interrupt

ISR(TIMER1_COMPA_vect) {
  PORTC = TP; // Send the value of TP to the outputs
  TP = ~TP;   // Invert TP for the next run
}

Diese Funktion wird 80 000‑mal pro Sekunde aufgerufen.

Was passiert in jedem Interrupt?

  1. PORTC wird auf den aktuellen Wert von TP gesetzt.
  2. TP wird invertiert, sodass beim nächsten Interrupt das Gegenteil ausgegeben wird.

Damit entsteht:

  • Ein 40 kHz Rechtecksignal auf jedem Pin.
  • Pins mit 1‑Bit im Muster starten HIGH.
  • Pins mit 0‑Bit starten LOW.
  • Durch das alternierende Muster sind benachbarte Pins immer gegensätzlich.

4. loop():

void loop() {
  // Nothing left to do here :)
}
  • Die gesamte Arbeit wird vom Timer‑Interrupt erledigt.
  • Die Hauptschleife bleibt leer.

Was erzeugt der Sketch?

PinSignalPhase
PC7Rechtecknormal
PC6Rechteckinvertiert
PC5Rechtecknormal
PC4Rechteckinvertiert
PC3Rechtecknormal
PC2Rechteckinvertiert
PC1Rechtecknormal
PC0Rechteckinvertiert
  • Frequenz: 40 kHz
  • Duty Cycle: 50 %
  • Phasenlage: abwechselnd invertiert

Fazit, aber bisher kein erfolgreiches Ergebnis:

Sehr interessant was der eigentliche kurze Sketsch von amarnathkr hervorbringt und obwohl ich mich damit schon einige Tage beschäftigt habe, bin ich trotzdem bisher auf keine erfolgreiches Ergebnis gekommen.

*** Die Kügelchen wollen nicht schweben!?! ***

Habe mir eine Fertiglösung gekauft damit ich zwei wirklich reine Sender „T“-Transducer ohne zusetzliche Elektronik verwende, oder den Nano gegen weitere Entwicklungsboards getauscht, jedoch ohne Erfolg.
Könnte mir auch vorstellen das es mit dem Abstand beider Ultraschall Sensoren zu tun.
Wenn ihr eine Idee habt die mir weiter helfen kann, lasst es mich wissen.

,