Metronom/BPM Counter - grundsätzliches Vorgehen?

Hey Forum!

Letzte Woche erhielt ich meinen ersten Arduino Uno und bin nun bei ersten Experimenten. LEDs faden, eine gelötetes “Keypad” mit Widerstandsleiter und RC Entprellung kann ich erfolgreich am Analogeingang auslesen... Ich habe vor 20 Jahren Pascal und rudimentär Assembler gelernt, bin zwar etwas eingerostet, aber fange nicht bei Null an.

Doch zur eigentlichen Frage, die geplante Anwendung ist die folgende: Ich möchte ein Programm schreiben, welches in regelmäßigen Abständen einen Impuls ausgibt, sagen wir zum Anfang, eine LED soll kurz blitzen. Soweit, so einfach.

recorded_time = millis()
if millis() > recorded_time + delay_between_steps do Flash

Jetzt zum Problem: Während dieser Loop läuft, möchte ich via Tastendruck delay_between_steps manipulieren (up/down). Dabei soll einerseits kein Tastendruck verlorengehen, andererseits darf der Loop nicht stolpern.

Desweiteren brauche ich durch Tastendruck ausgelöst die Funktion start_now, d.h. unter Beibehaltung von delay_between_steps möche ich den Triggerpunkt innerhalb dieses Zeitfensters verschieben, nennen wir es trigger_within_delay.

Luxus wäre später noch eine “Tap” Funktion wie am Drumcomputer, also eine Funktion, welche im Hintergrund den Abstand zwischen 2 oder für mehr Genauigkeit 4 Tastendrücken misst und in einen delay_between_steps Wert umsetzt.

Wofür das Ganze? Es geht darum, ein DMX Lichtpult mit periodischen Impulsen zu versorgen (ob via Midi oder Audio weiss ich noch nicht), um Lichteffekte zur Musik zu synchronisieren. Dabei darf kein Impuls ausfallen oder verzögert kommen und kein Tastendruck verloren gehen. Es soll absolut synchron zur (elektronischen) Musik laufen und um die “Latenz” der angeschlossenen (glühwendenhaltigen) Leuchtmittel “vorgehen”, damit die Lampe “auf den Punkt” leuchtet und nicht erst angeht.

Jede Funktion einzeln ist für mich kein Problem, dass Problem ist, wie bekommt man das alles in einen Loop - mit dem Anspruch der Timinggenauigkeit. Geht das überhaupt? Eigentlich bräuchte man ja 2 voneinander völlig unabhängige "Tasks". Wenn ich versuche, mir das als Struktogramm aufzuschreiben, habe ich immer das Dilemma, dass ich entweder Gefahr laufe, einen Impuls zu verlieren, z.B. wegen einer Interruptroutine oder eben einen Tastendruck...

An dieser Stelle habe ich einen Knoten im Hirn und wäre für jeden Vorschlag von Euch sehr dankbar.

Oder sollte ich gleich mit 2 Arduinos planen, einen nur zum Keypad Lesen, delay_between_steps und trigger_within_delay berechnen und übergeben, den anderen nur zum Variable Annehmen und Impulse Ausgeben?

Mittelfristig ist die Erweiterung um ein LCD Shield geplant, um z.B. die aktuellen BPM auszugeben.

Ich hoffe, dass Kernproblem ist verständlich. Für Eure Hilfe danke ich im voraus.

Mit besten Grüßen

Helmuth

Hallo,
es gibt Timer und Interupts, damit müßte dein Problem lösbar sein. Soweit ich das verstanden habe machst du deine Eingabe per Taster, ich glaube nicht, dass da die paar Prozessortakte für den Interupt bei der Impulsgebung ins Gewicht fallen.

Helmuth:
Während dieser Loop läuft, möchte ich via Tastendruck delay_between_steps manipulieren (up/down). Dabei soll einerseits kein Tastendruck verlorengehen, andererseits darf der Loop nicht stolpern.
Desweiteren brauche ich durch Tastendruck ausgelöst die Funktion start_now, d.h. unter Beibehaltung von delay_between_steps möche ich den Triggerpunkt innerhalb dieses Zeitfensters verschieben, nennen wir es trigger_within_delay.

Vielleicht sowas?

// Dx
#define BUTTON_UP 2
#define BUTTON_DOWN 3
#define BUTTON_START 4
#define LED 13

unsigned long tick;
unsigned int delayBetweenSteps = 1000; // Blinkt alle 1000ms ...
unsigned int blinkLength = 20; // ... 20ms lang

void setup() {
}

void loop() {
  if (digitalRead(BUTTON_UP))
    delayBetweenSteps += 10; // ms
  if (digitalRead(BUTTON_DOWN))
    delayBetweenSteps -= 10; // ms
  if (digitalRead(BUTTON_START))
    tick = millis();
  if (millis()-tick > delayBetweenSteps)
    tick += delayBetweenSteps;
  digitalWrite(LED, millis()-tick<blinkLength);
}

Ungetestet, daher ohne Gewähr! :wink:

Vielleicht sowas?

  if (digitalRead(BUTTON_START))

tick = millis();
 if (millis()-tick > delayBetweenSteps)
   tick += delayBetweenSteps;



Ungetestet, daher ohne Gewähr! ;)

Hallo Joghurt,

millis()-tick darf in Zusammenhang mit der Starttaste im laufenden Betrieb nicht größer als delayBetweenSteps werden, sonst geht ja ein "Beat" verloren.

Ich dachte - wenn delayBetweenSteps, triggerWithinDelay und ImpulseLegth bekannt sind - schematisch an so etwas:

loop
ReadKeys
CalculateDelayAndTrigger
delay(triggerWithinDelay)
SendImpulse
delay(delayBetweenSteps - triggerWithinDelay - ImpulseLengh)

Delay kann ich nicht verwenden, dann bekomme ich ja die Tastendrücke währenddessen nicht mehr geslesen...also Vergleiche mit millis() - wenn dann aber ein Interrupt wegen Tastendruck dazwischen kommt, wird es ungenau.

Oder denke ich zu kompliziert?

Wie gesagt, wenn du den Start per Tastendruck vorgibst, kannst du auf die paar Microsekunden des Interupts verzichten.

Helmuth:
millis()-tick darf in Zusammenhang mit der Starttaste im laufenden Betrieb nicht größer als delayBetweenSteps werden, sonst geht ja ein "Beat" verloren.

Nicht wirklich: Der Impuls gleitet so pro Zyklus vielleicht um eine oder zwei Millisekunden, aber verlieren tust Du so nichts. Wenn ich mich nicht vertan hab. XD
Und es ist um einiges einfacher als eine Version mit Interrupts.

Würde es einfach mal in Deinen Arduino laden und gucken obs tut. :wink:

Hi Joghurt,

habe mir gerade den Bootloader vom PC zerschossen und nur noch ein Live Linux vom Stick - ohne Möglichkeit die Arduino IDE zu installieren...

Danke für Deine Antworten, sobald ich wieder mit dem Arduino sprechen kann, berichte ich vom Test Deines Codes.

Schau Dir mal dieses Beispiel an: POV Reloaded | Blinkenlight. Der Punkt ist, dass das Blinken in Interrupts stattfindet und deshalb die Hauptschleife unabhängig davon läuft. Dieses Prinzip kannst Du für Deine Zwecke entsprechend anpassen, dann geht das schon was Du vorhast.

Nachtrag: das hier http://arduino.cc/forum/index.php/topic,51439.0.html dürfte Dich in dem Kontext sicher auch noch interessieren.

Zwischenbericht: Betriebssysteme sind gerettet und die I/O mit dem Board klappt wieder...

@Joghurt: Deine Code produziert leider nur eine durchgehend leuchtende LED 13...und diesen Befehl verstehe ich nicht:

digitalWrite(LED, millis()-tick<blinkLength);

Was macht der Vergleichsoperator <blinkLength an dieser Stelle? Ist das ein C spezifischer Trick?

@Udo Klein: Tolle Sachen baust Du da, danke für die Links. Als ich Deine Landelicher für den Quadrocopter gesehen habe, musste ich spontan an den LM3914 denken (Dot/Bar Display Driver, kaskadierbar), Datenblatt hier: http://www.ti.com/lit/ds/symlink/lm3914.pdf

Doch zurück zum Thema, dem Metronom. Ich habe das jetzt entsprechend meinen momentanen Programmierkenntnissen erstmal komplett mittels Verwendung von Delays realisiert. Mit - wie zu erwarten - unbefriedigender Genauigkeit. Und gerade eben habe ich delayMicroseconds() entdeckt. :slight_smile:

Das DFRobot LCD Shield funktioniert soweit und die Audiokopplung ans DMX Pult (über einen 10k Widerstand) auch.

Wenn ich mit dem Code und dessen Genauigkeit zufrieden bin, poste ich das Zwischenergebnis.

Grüße

Helmuth.

Helmuth:
@Joghurt: Deine Code produziert leider nur eine durchgehend leuchtende LED 13...

Mist. :frowning:

Helmuth:
und diesen Befehl verstehe ich nicht:

digitalWrite(LED, millis()-tick<blinkLength);

Was macht der Vergleichsoperator <blinkLength an dieser Stelle? Ist das ein C spezifischer Trick?

Naja, da kommt entweder "true" oder "false" raus, je nachdem ob du innerhalb der ersten blinkLength Millisekunden eines Zyklus bist oder nicht. Ich war der wohl irrigen Annahme, dass "true" gleich "HIGH" wäre und "false" gleich "LOW", aber dem ist wohl nicht so...?

Probier mal

digitalWrite(LED, millis()-tick<blinkLength ? HIGH : LOW);

Hallo Forum, hier der vorläufige Stand der Dinge:

//
// First Arduino Project by Pitt Tesla 2012
//
// BPM counter and metronome for triggering a DMX console with an audio input
// for precise light to sound show
//
// In gratitude for the LCD library and examples.
// 
// ...tested with Arduino Uno v3 and DFRobot LCD Shield
//

#include <LiquidCrystal.h>

LiquidCrystal lcd(8, 9, 4, 5, 6, 7); // Configuration of DFRobot LCD Shield

int lcd_key        = 0;    // for button reading
int adc_key_in     = 0;    // for button reading
long x             = 0;    // later used to compare with millis()
long y             = 0;    // used to measure TAP time
int tap_counter    = 0;    // counts the TAPs
int tap_max        = 4;    // number of TAPs used for calculation
float BPM          = 0;    // to display beats/minute
int time           = 500;  // ms between impulses = 120 BPM
int SCHRITT        = 10;   // UP/DOWN steps
int schritt        = 1;    // UP/DOWN steps fine
int shift          = 33;   // delay of LED flash (displays latency of my DMX chain) for better control)
int LEDpin         = 2;    // LED flash 
int AUDIOpin       = 3;    // PWM output with 10k to AUDIO in
int frequency      = 500;  // Audio signal that seems to be easy to detect
int BUTTONpin      = 0;    // Buttons switching resistors from 5V to A0
int flashon        = 10;   // LED flash time
int wait_after_key = 150;  // for debouncing & key autorepeat
int BACKLIGHTpin   = 10;   // why dont´t use the backlight for flashing?!
int backlight      = 50;   // brightness to give a good contrast for flashing
int range          = 0;    // switches for rough and fine tuning

#define buttonSTART 0
#define buttonUP    1
#define buttonDOWN  2
#define buttonRANGE 3
#define buttonTAP   4
#define buttonNONE  5

int read_LCD_buttons()      // reads the LCD shield buttons 
{
  adc_key_in = analogRead(BUTTONpin);      
  if (adc_key_in > 1000) {
    return buttonNONE;
  } 
  if (adc_key_in < 50)   {
    return buttonSTART;
  } 
  if (adc_key_in < 195)  {
    return buttonUP;
  }
  if (adc_key_in < 380)  {
    return buttonDOWN;
  }
  if (adc_key_in < 555)  {
    return buttonRANGE;
  }
  if (adc_key_in < 790)  {
    return buttonTAP;
  }  
  return buttonNONE;  
}

void ledACTION () {
  digitalWrite(LEDpin, HIGH);
  delay(flashon);
  digitalWrite(LEDpin, LOW);
}

void impulseACTION() {
  x = millis();
  tone(AUDIOpin, frequency, 5);          // send audio impulse
  analogWrite(BACKLIGHTpin, 255);        // flash backlight in "realtime"
  delay(flashon);
  analogWrite(BACKLIGHTpin, backlight); 
  delay(shift-flashon);
  ledACTION();                           // flash external LED synchronized to latency of DMX processing
}

void setup()
{
//  Serial.begin(9600);                  // just for debugging...
  lcd.begin(16, 2);                      // init LCD
  lcd.setCursor(0,0);
  lcd.print(" /  Range   ");
  lcd.setCursor(2,0);
  lcd.print(tap_max);
  lcd.setCursor(0,1);
  lcd.print("   ms        BPM");
  pinMode(LEDpin, OUTPUT);               // init pins
  pinMode(AUDIOpin, OUTPUT);
  pinMode(BACKLIGHTpin, OUTPUT);
  analogWrite(BACKLIGHTpin, backlight);  // adjust brightness
}

void loop()
{
  float BPM = ((float)1000/(float)time) * (float)60;  //calculate BPM
  lcd.setCursor(0,1);           
  lcd.print(time);             // ms between impulses
  lcd.setCursor(6,1);           
  lcd.print(BPM);              // impulses/minute
  lcd.setCursor(13,0);           
  lcd.print(millis()/60000);   // power on time in minutes
  lcd.setCursor(0,0);           
  lcd.print(tap_counter);      // count of taps
  lcd.setCursor(10,0);           
  lcd.print(range);            // big or small steps to adjust

  lcd_key = read_LCD_buttons();
  if (millis() - x > time) {   
    impulseACTION();
  }

  switch (lcd_key)              // in order to pressed button do something         
  {
  case buttonSTART:             // to start an impulse immediately
    {                         
      impulseACTION;
      break;
    }
  case buttonRANGE:             // select range of adjustion
    {
      range = range + 1;
      if (range == 2) { 
        range = 0;
      }
      delay(wait_after_key); 
      break;
    }
  case buttonUP:
    {
      if (range == 0) {          // big step up
        time = time - SCHRITT;
      } 
      else {                     // small step up
        time = time - schritt;
      } 
      delay(wait_after_key); 
      break;
    }
  case buttonDOWN:
    {
       if (range == 0) {      
        time = time + SCHRITT;
      } 
      else {
        time = time + schritt;
      }
      delay(wait_after_key); 
      break;
    }
  case buttonTAP:                // for synchronizing speed to music
    {
      tap_counter = tap_counter + 1;

      if (tap_counter == 1) {
        y = millis();
      }
      if (tap_counter == tap_max) {
        time = (millis() - y) / (tap_max - 1);
        tap_counter = 0;
        impulseACTION();
      }
      delay(wait_after_key); 
      break;
    }
  case buttonNONE:
    {
      break;
    }
  }
}

Bin soweit zufrieden, das Gerät macht, was es soll und die Synchronisation zu Musik gelingt schnell.
Einfach mittels buttonTAP Tempo vorgeben und dann nur noch feinjustieren.

Ich werde morgen noch die zeitliche Auflösung durch zählen von Microsekunden verbessern.

Was noch nicht optimal ist, ist die Funktion von buttonSTART: warum trotz Aufruf von impulseACTION nicht immer sofort ein Blitz ausgegeben wird, verstehe ich nicht.
Ist momentan mehr eine Gefühlssache, den Startpunkt richtig zu erwischen.

Jeder Hinweis dazu und auch grundsätzliche Kritik am ersten Code seit vielen Jahren ist ausdrücklich erwünscht und willkommen!

Beste Grüße

Helmuth

@Udo Klein: Tolle Sachen baust Du da, danke für die Links. Als ich Deine Landelicher für den Quadrocopter gesehen habe, musste ich spontan an den LM3914 denken (Dot/Bar Display Driver, kaskadierbar), Datenblatt hier: http://www.ti.com/lit/ds/symlink/lm3914.pdf

Danke für die Blumen. Nur wieso kommt jeder auf die "naheliegende" Idee irgendwelche Chips zu kaskadieren und keiner merkt, dass ein Arduino alleine das ohne weiteres könnte? Es gibt ja nur sehr wenige verschiedene Zustände und die könnte ich direkt treiben. Nur habe ich eben ziemliche viele Arduinos und Blinkenlight Shields sowieso da. Der Punkt ist nur zu zeigen, dass man einen Ausgabepin trotzudem noch gleichzeitig für Eingaben benutzen kann wenn man es geschickt anstellt.

Hi Udo,

wie gesagt, es kam mir einfach in den Sinn.

Nur wieso kommt jeder auf die "naheliegende" Idee irgendwelche Chips zu kaskadieren und keiner merkt, dass ein Arduino alleine das ohne weiteres könnte?

Also ich kam auf die Idee, weil ich mit "irgendwelchen Chips" mehr Erfahrung habe, als mit den Möglichkeiten des Arduino. Und für Landelicht und Knight Rider Effekt reicht ja eine Dot Anzeige. Habe nicht behauptet, das sei besser oder sonstwas.

Und wenn man auf einem Sack voll "ziemliche viele Arduinos und Blinkenlight Shields" sitzt, kann man natürlich auf andere Ideen kommen. :slight_smile:

...wenn man es geschickt anstellt...

Ja, da wird wohl noch ein bisschen Zeit verstreichen, bis ich Deinen Code komplett verstehe...und mich dann auch geschickt anstellen kann...

Beste Grüße

Helmuth

Und wenn man auf einem Sack voll "ziemliche viele Arduinos und Blinkenlight Shields" sitzt, kann man natürlich auf andere Ideen kommen. smiley

Eben. Der limitierende Faktor sind die Arduinos. Ich glaube im Moment habe ich ~9 Stück. Bei den Shields hingegen sitze ich ja direkt an der Quelle :wink: