Arduino Indiziergerät mit leichten Schwächen

Hallo liebe Leute des Arduino Forums,

im Rahmen meiner Masterarbeit habe ich unter Verwendung eines Arduino Mega ein Indiziergerät gebaut, welches die Drehzahl eines Motors und fünf analoge Spannungswerte misst. Zusätzlich wird ein analoges Ausgangssignal (Sinusanregung bzw. Sprungfunktion) ausgegeben. Ziel ist es mit dem Ausgangssignal das System also den Motor anzuregen und dessen Antwort in Form der Drehzahl zu messen. Die restlichen fünf aufzuzeichnenden analogen Werte dienen der Messung des Zylinderinnendruckes und der linearen Wegbestimmung eines Öffnungsschiebers. Die Daten werden bei jedem eingehenden Drehzahlsignal auf einer SD-Card gespeichert.

Zur Messung der Drehzahl verwende ich eine Lochscheibe mit 60 Löchern plus einem Loch für die Stellung des oberen Totpunktes und zwei Gabellichtschranken. Um ein sauberes Signal zu bekommen habe ich einen Comparator und einen Flipflop (eingestellt auf die steigende Flanke) hinter die Gabellichschranken geschaltet, sodass nur noch alle 12° also alle zwei Löcher gemessen wird. Dies musste ich machen, da die Auslese- und Schreibgeschwindigkeit auf die SD-Card von ca. 900 micro Sekunden zu langsam waren und das Programm ständig abgestürzt ist. Der zu erreichende Drehzahlbereich des Motors soll zwischen 600…1500 1/min liegen was einer maximale Lese- und Schreibgeschwindigkeit von 1/((1500/60)*60)=666 micro Sekunden entspricht. Wird hingegen nur jedes Zweite Loch genutzt 1/((1500/30)*60)=1333 micro Sekunden > 900 micro Sekunden so läuft das Programm halbwegs stabil.

Zum schreiben der Daten (1x1byte,7x2byte,1x3byte) auf die SD-Card nutze ich die SdFat Libary und die Funktion “file.printField()” da dies in einigen Tests die besten Ergebnisse geliefert hat.

Hier nun meine erste Frage: Geht das noch schneller? Und wenn ja wie?

Das Programm hat drei Interrupt Routinen bei denen ich mir nicht ganz sicher bin ob und wie sich diese gegenseitig beeinflussen bzw. in welcher Priorität diese abgearbeitet werden. Ich habe gerade bei den Drehzahlwerten also den Zeitwerten von Timer5 viele Ausreißer wie das angehangene Bild bzw. die Daten zeigen.

Fällt hier jemandem auf was da noch falsch läuft oder was man da noch verbessern kann?

Meiner Meinung nach müssten die Interrupt Routinen in folgender Reihenfolge abgearbeitet werden:
1:Attach-Interrupt-Routine: für die Bestimmung des oberen Totpunktes
2:Input-Capture-Interrupt-Routine Timer5: für die Messung der Drehzahl bzw. der Winkelgeschwindigkeit und zum setzen des Anregungssignals
3:Overflow-Interrupt-Routine Timer5: zur Aufsummierung der Überläufe des Timer5

Über Anregungen und Verbesserungen würde ich mich freuen.

Das Programm befindet sich im Anhang ;).

Programmtest.ino (17.3 KB)

DATEN.TXT (446 KB)

Heinrich: Dies musste ich machen, da die Auslese- und Schreibgeschwindigkeit auf die SD-Card von ca. 900 micro Sekunden zu langsam waren

Es gibt keine konstante Zeit, die ein "file.printField()" dauert. Nicht einmal ansatzweise ist die Zeit dafür konstant.

Der Grund liegt darin, dass auf eine SD-Karte immer sektorweise in Form von 512-Byte großen Sektoren geschrieben wird. Deine SD-Library arbeitet daher wie die SD-Karte mit 512-Byte Sektoren beim Schreiben einer Datei und die Library reserviert sich dafür 512-Byte große Puffer als Hilfsmittel zum Zwischenpuffern.

Rechenbeispiel: Du schreibst immer 5 Bytes auf einmal in die Datei, mit einem Befehl der Art "file.printField(FuenfBytes)". Die Library macht nun folgendes: - 1. Schreibvorgang ==> 5 Bytes in den 512-Byte Puffer packen, fertig (5 Bytes im Schreibpuffer) - 2. Schreibvorgang ==> 5 Bytes in den 512-Byte Puffer packen, fertig (10 Bytes im Schreibpuffer) - 3. Schreibvorgang ==> 5 Bytes in den 512-Byte Puffer packen, fertig (15 Bytes im Schreibpuffer) ... ... - 100. Schreibvorgang ==> 5 Bytes in den 512-Byte Puffer packen, fertig (500 Bytes im Schreibpuffer) - 101. Schreibvorgang ==> 5 Bytes in den 512-Byte Puffer packen, fertig (505 Bytes im Schreibpuffer) - 102. Schreibvorgang ==> 5 Bytes in den 512-Byte Puffer packen, fertig (510 Bytes im Schreibpuffer) Und jetzt wird es interessant: Beim nächsten Schreibvorgang wird der Puffer voll. Dann passiert das: - 103. Schreibvorgang ==> erstmal noch 2 Bytes in den 512-Byte Puffer packen, der ist dann mit 512 Bytes voll. Dann wird der Puffer tatsächlich auf die SD-Karte geschrieben, danach der Schreibpuffer geleert, die restlichen 3 Byte kommen in den Schreibpuffer und fertig.

Das heißt, Du hast in diesem Fall 102 scheinbar blitzschnelle "Schreibvorgänge", bei denen tatsächlich nichts anderes passiert, als dass die SD-Library die Bytes in den Schreibpuffer überträgt, und danach kommt der hammermäßig lange 103. Schreibvorgang, bei dem der volle Puffer tatsächlich auf die SD-Karte geschrieben wird. Das dauert etliche Millisekunden.

Ersteinmal vielen Dank an jurs für die ausführliche Erklärung der Funktionsweise der Speicherung von Daten auf eine SD-Card. Ich hatte mir auch die Beispiele aus der SdFat Libary angeschaut und mich immer gefragt warum bei den schnellen Datenloggern immer ein 512 byte Puffer verwendet wurde. Nun weiß ich das.

Ich vermute mal, dass dies auch der Grund für die Ausreißer in den Daten ist. Wenn also der 512 byte Puffer voll ist dauert der Schreibvorgang auf die SD-Card zu lang und es werden fehlerhafte Werte abgespeichert.

Die Frage die sich nun stellt ist: Wie kann man das vermeiden?

Mit dem Aufruf von SREG dürfte ja erst dann ein Interrupt verloren gehen wenn es erneut eintritt. Also müsste ich doch das schreiben des 512 byte Blockes beschleunigen oder?

Ich werde heute mal versuchen die benötigte Zeit für die Schreibvorgänge zu plotten um zu schauen was da genau passiert.

Heinrich: Die Frage die sich nun stellt ist: Wie kann man das vermeiden?

Durch ein grundlegend anderes Design Deiner Software.

Heinrich: Mit dem Aufruf von SREG dürfte ja erst dann ein Interrupt verloren gehen wenn es erneut eintritt. Also müsste ich doch das schreiben des 512 byte Blockes beschleunigen oder?

Das Schreiben/Lesen der Sektoren auf der SD-Karte kannst Du kaum beschleunigen, das ist bei SPI-Zugriffen über einen Mikrocontroller langsam. Auf mehr als eine Schreibrate von ca. 100 KB/s entsprechend 200 Sektoren a 512 Bytes = 1/200s pro Sektor = ca. 5ms pro geschriebenem Sektor dürftest Du kaum kommen.

Nicht mit der standardmäßigen Library und auch nicht mit der gepimpten Library, die Größenordnung beim Schreibdurchsatz ist immer gleich und die Libraries unterscheiden sich eher im geringen Prozentbereich in der Schreibgeschwindigkeit.

Die Lösung dürfte eher in einer Programmlogik liegen, die wie folgt funktioniert: 1. interrupt-getriggerte Datenerfassung 2. wegschreiben der Daten in der loop-Funktion

Im Detail zu 1.) Du stößt die Datenerfassung mit Interrupts an. Das sind entweder Hardware-Interrupts von Deiner Lochscheibe, und/oder Timer-Interrupts von laufenden Timern. In den Interrupt-Behandlungsroutinen erfasst Du die Messwerte und schiebst die Werte in einen von Dir verwalteten FIFO-Puffer. Mal angenommen, Du hast einen Timer-Interrupt 1000 mal pro Sekunde laufen und in jedem Interrupt machst Du vier Messungen, dann schiebst Du im stets gleichen Abstand von 1ms 4 Werte in den FIFO-Puffer.

Im Detail zu 2.) Innerhalb der loop-Funktion läuft nur eines ab: Es wird nachgesehen, ob Daten in Deinem FIFO-Puffer drin sind und wenn Daten vorhanden sind, werden diese zum Schreiben an die SD-Karte übergeben. Falls nun der Schreibpuffer leer ist, geschieht diese Datenübergabe blitzschnell, kaum sind Daten im FIFO, werden sie schon wieder herausgeholt und blitzschnell an die SD-Library übergeben. Falls nun aber ein tatsächlicher Schreibvorgang von z.B. 5ms auftritt, landen interruptgesteuert alle Daten während dieser 5ms in Deinem FIFO-Puffer und sammeln sich dort an. Und sobald die loop-Funktion den SD-Schreibvorgang ausgeführt hat, geht das Datenschaufeln vom FIFO-Puffer in den SD-Schreibpuffer weiter.

Wenn Du mir mal möglichst detailliert Deine Programmlogik beschreibst, wie es ablaufen soll, kann ich Dir mal ein Programmgrundgerüst als Proof-of-Concept Code machen und hier posten. Aus dem von Dir geposteten Code kann ich nämlich an einigen Stellen nicht so wirklich erkennen, was ablaufen soll. Insbesondere ist mir das mit der Ausgangssignalerzeugung unklar.

Möglichst effektiv wäre das Programm übrigens dann, wenn Du bei der Datenerfassung auf Berechnungen verzichten würdest und diese irgendwann später bei der Auswertung machst. Die Verarbeitung von Gleitkommazahlen, Multiplikationen, Divisionen und trigonometrische Funktionen, das sind alles Dinge, die kräftig Zeit kosten und die mögliche Datenrate bei der Erfassung runterdrücken. Aber dieser Verzicht auf Berechnungen ist bei Deinem Anwendungsfall wohl nicht möglich, weil Du aus den Messwerten gleich noch ein Ausgangssignal in Echtzeit errechnen möchtest?

Eventuell müßtest Du um schneller zu werden, auch zwei Controller-Boards einsetzen: Du fütterst dann beide Mikrocontroller mit denselben Eingangssignalen, einer der Mikrocontroller macht Messungen und zeichnet auf SD-Karte auf, und der zweite Mikrocontroller erzeugt aus den Eingangsdaten das Ausgangssignal.

Ok dann werd ich mal mein Bestes geben das Programm verständlich zu machen.

Das Programm dient zur Erfassung der Regelstrecke eines Motors. Dazu wird dieser am “Gaspedal” mit einer Sinusschwingung angeregt. Die Antwort des Systems wird über die Erfassung der Motordrehzahl, welche ebenfalls eine Sinusschwingung ergibt, gemessen. Hierfür soll im Anschluss eine Übertragungsfunktion gefunden werden, welche die Regelstrecke beschreibt. Um das Verhalten des Motors besser zu verstehen, werden die Zylinderinnendrücke und die Stellung des “Gaspedals” mit erfasst.

Setup()

Im Setup werden wird Timer 1 im Fast-PWM Mode für ein 12bit Ausgangssignal mit dem Prescaler 1 initialisiert.
Timer 5 wird ebenfalls mit Prescale 1 betrieben und erzeugt ein Input-capture-Interrupt wenn das Signal, welches von der Lochscheibe kommt ansteigend ist. Um die Zeit zu erfassen nutze ich ebenfalls dem Timer 5 und den dazugehörigen overflow Interrupt. Mit den bitClear() Befehlen beschleunige ich das analoge Lesen der Sensoren um den Faktor 8. Danach wird ein Textfile auf der SD-Card zum schreiben geöffnet welches erst am Ende der Messung wieder geschlossen wird.

void loop()

“SREG” hab ich nun rausgeschmissen, da es aufgrund der zu langen Schreibzyklen nichts bringt. Das Schreiben der Daten erfolgt nach 10 Sekunden und auch nur dann wenn der input-capture-Interrupt die Laufvariable erhöht hat. Es werden also keine gleichen Werte abgespeichert, sondern nur dann wenn ein Signal durch die Lochscheibe erzeugt wurde. Gespeichert wird die Zeit “Zeit_neu_T5” in micro Sekunden wenn ein input-capture-Interrupt ausgelöst wird, die Zeit zwischen den input-capture-Interrupts “Delta_t” (hab ich mittlerweile rausgeschmissen), die Laufvariable “lauf” (Anzahl der input-capture-Interrupts oder Anzahl der überfahrenen Löcher), fünf analoge Sensorwerte mit “analogRead()” und das zum Zeitpunkt des input-capture-Interruptes anliegende sinusförmige Ausgangssignal. Nach 60 Sekunden stoppt das Programm, schließt die Datei und schaltet die Timer und die Interruptroutinen ab.

ISR(TIMER5_OVF_vect)

Der Überlaufinterrupt wird ausgelöst wenn das Zählregister TCNT5 bis 2^16 hochgezählt hat und erhöht die Variable “ueberlauefe_T5” um eins.

ISR(TIMER5_CAPT_vect)

Folgender Abschnitt soll bewirken, dass wenn der input-capture-Interrupt ausgelöst wurde und gleichzeitig der Zähler des Timer5 überläuft die Variable “ueberlaeufe_T5” um eins erhöht wird. Der overflow-Interrupt wird also vorgezogen.

convert32to8 cap;        // Variablendeklaration
   
  cap.i8l = ICR5L;         // low Byte zuerst, high Byte wird gepuffert
  cap.i8m = ICR5H;  
  // overflow verpasst, wenn ICR1H klein und wartender Overflow Interrupt
  if ((cap.i8m < 128) && (TIFR5 & (1<<TOV5))) // T Oh V 5
  { // wartenden timer overflow Interrupt vorziehen
    ueberlaeufe_T5++;   
    TIFR5 = (1<<TOV5);    // timer overflow int. löschen, da schon hier ausgeführt  
  }

Dann wird die aktuelle Zeit in micro Sekunden anhand der Überläufe und des Zählerstandes von Timer5 berechnet, das Ausganssignal “OCR1A” der Sinusfunktion (fast PWM mit 12 bit Auflösung) anhand der zuvor bestimmten Zeit berechnet und gesetzt (hier habe ich Optimierungspotenzial indem ich die sin()-Funktion rausschmeiße) und die Variable “lauf” um eins erhöht.

void OT()

In der attach-Interrupt-Routinte für den oberen Totpunkt wird wieder der Timer overflow vorgezogen und die Variable “lauf” auf null gesetzt. Das bedeutet, dass immer wenn “lauf==0” ist, der OT überschritten wurde. Ich nutze die Laufvariable zur Identifizierung des OT und zur Überprüfung auf Vollständigkeit des Datensatzes.

Ja das war die ausführliche Beschreibung. Ich hoffe ich habe nichts vergessen.

Ich hab die benötigten Zeiten für jeden “file.printField()” Aufruf nun mal geplottet und angehangen. Das dauert bis zu 60 milli Sekunden.

Heinrich:
Ich hab die benötigten Zeiten für jeden “file.printField()” Aufruf nun mal geplottet und angehangen. Das dauert bis zu 60 milli Sekunden.

Ja, das kann hinkommen.

Und das wäre bei Deinem derzeitigen Programmkonzept der zeitliche Mindestabstand, der zwischen zwei zu schreibenden Datensätzen mindestens liegen müßte, Bei 60 Messungen pro Umdrehung mal 60 ms zeitlicher Mindestabstand wären das 3600 ms pro Umdrehung, entsprechend einer Drehzahl von 60/3,6 = 16,6 Umdrehungen pro Minute, die Dein Motor nicht überschreiten darf, wenn Du keine Messwerte verlieren möchtest. Denn Du mußt beim Schreiben der Daten ja vom “Worst Case” ausgehen, d.h. das Schreiben KANN 60 ms dauern, und es muß bei Deinem Programmkonzept erst beendet sein, bevor neu gemessen wird, denn sonst geht der nächste Messwert nach einem “langen” Speichervorgang verloren.

Nochmal zur Beschreibung Deines Anwendungsfalls.

Also Deine Beschreibung ist für mich immer noch zu kompliziert. Ich versuche es mal einfacher:

Du hast einen Motor und im wesentlichen sollen zwei Funktionen realisiert werden:

  1. Eine “Sinus-Anregung”
  2. Messen von Daten und Speichern der Werte auf SD-Karte

Zu 1.) Die “Sinus-Anregung” soll mit einer Periodendauer von T=5 Sekunden als Pseudo-Sinus nach dem PWM-Verfahren an einem PWM-Ausgang erzeugt werden.

Zu 2.) Du hast einen Motor, der mit bis zu 1500 U/min = max. 25 U/s dreht.
Er hat auf der Kurbelwelle eine Lochscheibe, die einmal pro Umdrehung ein OT-Signal gibt und pro Umdrehung 60 Lochraster-Signale (360°/60= 6°) alle 6° Kurbelwellendrehung ein Signal.

Du möchtest nun bei jedem dieser Lochraster-Signale, 60 mal pro Umdrehung mal 25 Umdrehungen pro Sekunde = 1500 mal pro Sekunde je fünf Messungen per analogRead machen und mit einem hochauflösenden Zeitstempel abspeichern?

Also pro Sekunde bis zu 1500 mal 5 = 7500 Messungen per “analogRead” und die Ergebnisse der Messungen fortlaufend in einem ASCII-Textformat auf SD-Karte abspeichern.

Kann man das in Kurzform so zusammenfassen?

Oder habe ich da etwas wesentlich falsch dargestellt und Du möchtest etwas anderes?

Das ist eine vollkommen korrekte Zusammenfassung. Nur das ich aufgrund des Flipflop nur jedes zweite Loch als steigende Flanke habe. Das bedeutet die Anzahl der Löcher halbiert sich auf 30 und 3750 Messungen pro Sekunde. Das könnte ich im Programm über das auskommentierte Umschalten von steigender Flanke auf fallende Flanke (toggle) zwar unterbinden, jedoch bringt mir das aufgrund der jetzt schon zu knappen Zeit recht wenig.

Hallo, ich will euch beide hier ja nicht stören, aber es sieht wie ein "TimingProblem aus" Kannst Du keine Lochscheibe mit größerem Durchmesser nehmen? Gleiche Anzahl von Löcher- aber auf einem größerem UmlaufRadius? Der Abstand der Löcher würde sich vergrößern, Du hättest also bei gleicher Anzahl der Messungen mehr Zeit zwischen den Messungen. Gruß und Spaß Andreas

Hallo SkobyMobil,

die Winkelgeschwindigkeit Omega berechnet sich unabhängig vom Durchmesser mit \omega=2*\pi*n. Was du meinst ist die Umfangsgeschwindigkeit die sich bei konstanter Drehzahl erhöht v_{Umfang}=\pi*d*n oder anders gesagt die Zeit die zwischen den Löchern verstreicht ist bei konstanter Drehzahl immer die gleiche egal welchen Durchmesser du nimmst. Der Weg der dabei an den unterschiedlichen Teilkreisdurchmessern zurückgelegt wird und die Umfangsgeschwindigkeit nimmt mit einer Vergrößerung des Durchmessers zu.

SkobyMobil: ich will euch beide hier ja nicht stören, aber es sieht wie ein "TimingProblem aus" Kannst Du keine Lochscheibe mit größerem Durchmesser nehmen? Gleiche Anzahl von Löcher- aber auf einem größerem UmlaufRadius? Der Abstand der Löcher würde sich vergrößern, Du hättest also bei gleicher Anzahl der Messungen mehr Zeit zwischen den Messungen.

Nein. 30 Impulse pro Umdrehung sind immer 30 Impulse pro Umdrehung. Egal ob der Impulsgeber auf kleinerem oder größerem Umfang den Drehwinkel abgreift. Da beißt die Maus keinen Faden ab.

@Heinrich Wenn es ein Problem wird, zeitlich beide Funktionen "PWM Pseudo-Sinuserzeugung" und "Messen und auf SD-Speichern" ausreichend schnell implementiert zu bekommen, wäre es dann vielleicht eine Alternative, beide Funktionen auf getrennten Mikrocontrollern laufen zu lassen?

Arduino-1: erzeugt einen 5-Sekunden-Sinus, und gibt bei jedem Start einer neuen Sinuswelle einen kurzen Impuls auf einem Pin ab (z.B. 10ms HIGH beim Starten jeder neuen Sinuswelle)

Arduino-2: erkennt am Abstand der von Arduino-1 generierten Impulse die exakte Dauer des Sinus. Die Phasenlage des Sinus ergibt sich dann stets aus der Zeit seit dem letzten erkannten Impuls geteilt durch die Schwingungsdauer. So dass Arduino-2 dann auch stets über die Phasenlage des Sinus informiert ist und diese Phasenlage zusammen mit den Messwerten aufzeichnen kann, und das mit geringster zeitlicher Belastung: Es muss nur ein einziger Hardware-Interrupt alle 5 Sekunden verarbeitet werden, um stets zu wissen, wo der Sinus steht. Und die Generierung des Sinus benötigt auf diesem Controller keine Zeit, da sie von Arduino-1 übernommen wird.

Wäre das notfalls machbar, die Sinuserzeugung vom Messprogramm abzukoppeln?

Was in der SdFat Doku auch ständig erwähnt wird ist das die Schreibgeschwindigkeit stark von der Karte abhängt. z.B. CardPerformance.txt:

This is what is happening when you see long clock activity. The card indicates busy by holding data out low. The spec allow a card to go busy for up to 250 ms.

Cards have two write modes, single block random mode and multiple block sequential mode. For file writes I must use single block mode. This is always slow but often extremely slow in high end cards. microSD cards also have poor support for this mode.

I have several applications that use multiple block mode and they run much faster. The binaryLogger.pde example can log 40,000 16-bit samples per second without dropping data on a good SanDisk video card.

The write time for a 512 byte block in sequential mode is about 850 microseconds with occasional busy times of 2 - 3 ms on a SanDisk Extreme 30 MB/sec card. In random mode this card is often slower than five year old class 2 cards.

Wenn du normal schreibst überlässt du als der Karte wo genau so die Datei in ihren Speicher schreibt. Man kann das aber auch selbst festlegen und dann soll das wesentlich schneller sein.

Und aus dem RawWrite Beispiel:

If a high quality SanDisk card is used with this sketch no overruns occur and the maximum block write time is under 2000 micros.

Das Beispiel solltest du dir mal ansehen. Da werden die Daten direkt in den Puffer der Lib geschrieben und diese dann auf die Karte wenn er voll ist (nach 512 Bytes). Das ist zwar komplizierter, aber es ist überschaubar.

Vor allem lass es mal laufen. Da bekommst du angezeigt wie lange es dauert einen Block so zu schreiben!

Habs bei mir mal getestet und es sind etwa 32ms mit einigen Overruns. Ich habe aber auch eine billigere uralte Karte die hier rumlag. Bei mit kommt es nicht auf Performance an.

Sicherlich wäre das eine Möglichkeit. Wobei die Sinusfunktion zwar langsam ist aber nicht so entscheidend wie das abspeichern der Daten. Ich habe auch schon Testläufe ohne die Sinusfunktion mit einem konstanten Drehzahlwert gemacht und konnte keine wirkliche Verbesserung feststellen.

Ich habe sogar schon das auslesen und speichern der analogen Daten weggelassen und die Drehzahl auf 500 1/min herunter genommen. Alles ohne Erfolg. Sobald ich mit dem Speichern der Daten beginne spuckt er fehlerhafte Werte aus. Nicht viele aber sie sind vorhanden.

Ich muss die Daten irgendwie zwischenpuffern wenn auf die SD-Card geschrieben wird.

Das RAW-Write Beispielprogramm aus der SdFat-Libary habe ich heute auch schon testweise in mein Programm eingebunden und es hat noch länger gedauert als die Ursprungsversion. Wenn ich es im Orginal laufen lasse komme ich auf eine maximale Schreibgeschwindigkeit von 1500 micro Sekunden.

Heinrich:
Ich muss die Daten irgendwie zwischenpuffern wenn auf die SD-Card geschrieben wird.

Dann die FIFO Geschichte die schon erwähnt wurde. Wenn es möglich ist dass Daten ans eine Ende des Puffers geschrieben werden, während so vom anderen Ende gelesen werden, sollte das eventuell gehen. Den Zugriff auf die head und tail Variablen sollte man vielleicht mit cli()/sei/() sichern.

Wegen der Interrupt Priorität übrigens:
Die ist auf dem AVR festgelegt. Und zwar einfach nach der Reihenfolge der Interruptvektor Tabelle. Die steht im Datenblatt ab Seite 101 glaube ich. Und ist genauso wie du gesagt hast. Die externen Interrupts kommen ganz oben. Dann unter anderem die Timer Inerrupts. Und Input Capture geht vor Overflow.

Und Interrupts können sich nicht gegenseitig unterbrechen.

Ja an den FIFO werde ich mich am Wochenende mal rantasten. Viel Hoffnung habe ich allerdings nicht.

Die Aufgabe finde ich sehr interessant, sehr praxisnah. Geht im fast Richtung Motormanagement. Bessere konstruktive Tipps als meine Vorredner kann ich nicht geben, haben halt mehr drauf als ich. 8)

Ist der Atmega nicht generell etwas zu langsam für so etwas ? Fifo ist natürlich schon eine Lösung nur müsste man diesen auch zeitnah auslesen. Und im Hintergrund auch handeln.

Der Due wäre doch eine Alternative, die Lib's müssen natürlich für den Due passen. In wie weit dies zutrifft weiß ich nicht.

Heinrich: Ja an den FIFO werde ich mich am Wochenende mal rantasten. Viel Hoffnung habe ich allerdings nicht.

Könntest Du mit einem Datenlogger etwas anfangen, der pro Sekunde 500 komplette Datensätze auf die SD-Karte bringt?

Geloggt mit diesen Daten pro Datensatz und als Tabbed-Delimited-ASCII Text in Datei auf SD-Karte geschrieben:

struct data_t{   // Typendeklaration für Datenstruktur zum Loggen von Daten
  unsigned long timeMicros;  // Stand des Mikrosekundenzählers
  unsigned int counter;  // Fortlaufend hochzählende Variable
  int wert1;   // erster ADC-Messwert
  int wert2;  // zweiter ADC-Messwert
  int wert3; // dritter ADC-Messwert
  int wert4; // vierter ADC-Messwert
};

500 Datensätzen pro Sekunde entsprechen 30000 Datensätze in 60 Sekunden = 1 Minute Bei 1000 U/min wären das exakt 30 Datensätze pro Umdrehung, die meine Konfiguration hier locker wegschreibt.

Meine Konfiguration: Arduino MEGA mit der SD-Karte auf dem Noname Ethernet Shield und einer alten 2GB Sandisk SD-Karte.

Ist allerdings ohne Sinusanregung.

Den Sketch könnte ich bei Interesse hier posten, ich würde dann nur vorher vielleicht noch ein paar Kommentare dazu mit reinschreiben, damit man dann den Code auch nachvollziehen kann, was da gemacht wird.

Oder soll ich die Performance erst nochmal boosten? Eine Idee dazu hätte ich nämlich auch schon... weiß aber nicht, wieviel das bringen würde.

Ich kann mit allem was anfangen Hauptsache es hilft. So langsam gehen mir nämlich auch die Ideen aus. Wenn du das Programm posten könntest wäre mir das schon eine riesen Hilfe.

Ich habe heute erstmal ein wenig am Gehäuse gebastelt und ich finde es sieht schon richtig professionell aus. Das darf ja immer nix kosten ;).

Heinrich:
Ich kann mit allem was anfangen Hauptsache es hilft. So langsam gehen mir nämlich auch die Ideen aus. Wenn du das Programm posten könntest wäre mir das schon eine riesen Hilfe.

OK, ich zeige mal, was ich habe:

#include <SD.h>

#define SD_CABLESELECT 4 // SD Karte CS ist Pin-4 beim Ethernet Shield
#define startTriggerPin 5 // Trigger LOW startet die Messung

struct data_t{   // Typendeklaration für Datenstruktur zum Loggen von Daten
  unsigned long timeMicros;
  unsigned int counter;
  int wert1;
  int wert2;
  int wert3;
  int wert4;
};

data_t data2write;  // Datenvariable zum Schreiben
data_t data2read;   // Datenvariable zum Lesen


#define FIFOSIZE 128  // Maximale Anzahl Datensätze im FIFO-Puffer

data_t fifoBuf[FIFOSIZE]; // FIFO-Puffer als Ringpuffer
volatile unsigned int fifoReadIndex=0;
volatile unsigned int fifoWriteIndex=0;
volatile boolean fifoOverrun=false;
volatile unsigned int sampleCount=0;


static boolean fifoWrite(void* dataPtr, byte dataSize)
// write data into buffer
// returns true ==> success, or false ==> fail (buffer full)
{
  memcpy(&fifoBuf[fifoWriteIndex],dataPtr,dataSize); // store the char
  if ((fifoWriteIndex+1)%FIFOSIZE!=fifoReadIndex)
  {
    fifoWriteIndex=(fifoWriteIndex+1)%FIFOSIZE;
    return true;
  }
  return false;
} 


static boolean fifoRead(void* dataPtr, byte dataSize)
// reads data from FIFO
// returns true ==> success (data read), or false ==> no data davailable
{ 
  if (fifoReadIndex==fifoWriteIndex) return false;
  memcpy(dataPtr,&fifoBuf[fifoReadIndex],dataSize); // store the char
  noInterrupts();
  fifoReadIndex=(fifoReadIndex+1)%FIFOSIZE;
  interrupts();
  return true;
}  


void setFastADC()
{
  // Define various ADC prescaler
  const unsigned char PS_16 = (1 << ADPS2);
  const unsigned char PS_32 = (1 << ADPS2) | (1 << ADPS0);
  const unsigned char PS_64 = (1 << ADPS2) | (1 << ADPS1);
  const unsigned char PS_128 = (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0);
  // set up faster ADC
  ADCSRA &= ~PS_128;  // remove bits set by Arduino library
  // you can choose a prescaler from above constants,
  ADCSRA |= PS_32;    // set our own prescaler to 32
}

void startTimer2()  // Startet Timer mit vorgegebener Interruptrate
{
  noInterrupts();
  // Timer 2 CTC mode
  TCCR2B = (1<<WGM22) | (1<<CS22)  | (1<<CS20);
  TCCR2A = (1<<WGM21);
  OCR2A = 249;   // 249==500,  124==1000 interrupts per second
               // 63 ==2000,  31==4000
               // 15 ==8000,   7==16000
  TIMSK2 = (1<<OCIE2A); // enable Timer 2 interrupts
  interrupts();
}

void stopTimer2() // hält den Timer-Interrupt an
{
  noInterrupts();
  TIMSK2 = 0;
  interrupts();
}



ISR(TIMER2_COMPA_vect)  // Timer Interruptbehandlung
{
  data2write.timeMicros=micros();
  data2write.counter=sampleCount;
  sampleCount++;
  data2write.wert1=analogRead(A0);
  data2write.wert2=analogRead(A1);
  data2write.wert3=analogRead(A2);
  data2write.wert4=analogRead(A3);
  if (!fifoWrite(&data2write,sizeof(data2write))) fifoOverrun=true;
}



void setup()
{
  pinMode(startTriggerPin, INPUT_PULLUP); // internen PullUp am startTriggerPin aktivieren
  Serial.begin(115200);
  Serial.println("FIFO-Buffering with writing to SD card by 'jurs'");
  Serial.print("Initializing SD card...");
  pinMode(SS, OUTPUT);
  delay(20);
  if (!SD.begin(SD_CABLESELECT)) // On the Ethernet Shield, SD card CS is pin 4.
  {
    Serial.println("initialization failed!");
    return;
  }
  Serial.println("initialization done.");  
  
  setFastADC();
  startTimer2();
  delay(20);
}

char* createFilename()
{
  static char str[13]="MESSUNG.TXT";
  return str;
}

unsigned long messSchleife(long duration)
{
  char* fileName=createFilename();
  SD.remove(fileName);
  Serial.print("Dateiname: ");Serial.println(fileName);
  Serial.print(duration);
  Serial.println(" Sekunden Aufzeichnung startet...");
  Serial.flush();
  duration*=1000000L; // von Sekunden in Mikrosekunden umrechnen
  File file=SD.open(fileName, FILE_WRITE);
  noInterrupts();
  fifoReadIndex=fifoWriteIndex; // Puffer virtuell "leeren"
  fifoOverrun=false;
  sampleCount=0;
  interrupts();
  unsigned long startZeit=micros();
  if (file)
  {
    while (micros()-startZeit<duration)
    {
      if (fifoOverrun) return(micros()-startZeit);
      noInterrupts();
      boolean havaData=fifoReadIndex!=fifoWriteIndex;
      interrupts();
      if (havaData)
      {
        fifoRead(&data2read, sizeof(data2read));
        file.print(data2read.timeMicros);file.print("\t");
        file.print(data2read.counter);file.print("\t");
        file.print(data2read.wert1);file.print("\t");
        file.print(data2read.wert2);file.print("\t");
        file.print(data2read.wert3);file.print("\t");
        file.print(data2read.wert4);file.print("\n");
     }
    }
    file.close();
    Serial.println("Aufzeichnung beendet!");
  }
  else Serial.println("Error: Could not open File");
  return 0;
}

void showMessungen()
{
  File file=SD.open(createFilename(), FILE_READ);
  while (file.available()) Serial.write(file.read());
  file.close();
}


boolean startTrigger()
{
  if (digitalRead(startTriggerPin)==LOW) return true;
  else return false;
}

void loop()
{
  Serial.println("Warten auf Messbeginn...");
  while (!startTrigger()); // warten auf Messbeginn, startTriggerPin==LOW
  unsigned long result=messSchleife(60);
  if (result!=0)
  { 
    Serial.print("ERROR: FIFO-BUFFER OVERRUN nach ");
    Serial.print(result/1000000.0,6);
    Serial.println(" Sekunden");
  }
  showMessungen();
  while(1); // Endlosschleife
}

Meine Test-Konfiguration ist ein Ethernet-Shield, also Chipselect der SD-Karte an Pin-4.
Serielle Baudrate: 115200

Nach dem Programmstart geht das Programm in einen “Warten auf Messbeginn” Modus.

Eine Messung mit der angegebenen Messdauer startet, wenn Pin-5 getriggert wird, ich gehe davon aus, dass ein Taster zwischen Pin-5 und GND dient. Wenn sofort nach dem Starten des Sketches die Messung automatisch beginnen soll, ziehe einfach eine direkte Verbindung von Pin-5 nach GND.

In der derzeitigen Konfiguration habe ich zum Messen einen Timer aufgesetzt, der 500 mal pro Sekunde läuft, und Du bekommst geloggte Daten sehr schön im stets gleichmäßigen Abstand von 2 Millisekunden (2000 Mikrosekunden).

Der gleichmäßige Abstand wird durch FIFO-Pufferung erreicht: Gemessen wird im Interrupt als 2 ms, und in der loop werden die Daten auf SD-Karte weggeschrieben. Wenn das mal länger dauert, z.B. 60 ms, erhöht sich zwischenzeitlich der Füllstand des FIFO-Puffers, aber der Regelmäßigkeit der Messungen ändert sich nichts.

Damit man nicht ständig die SD-Karte zwischen Arduino und PC wechseln muß, wird die Datei MESSUNG.TXT nach Abschluss der Messung gleich wieder von SD-Karte zurückgelesen und im seriellen Monitor ausgegeben, so dass man die Daten (mit zeitlicher Verzögerung) direkt auf den Monitor bekommt (von dort könnte man die Daten dann auch in die Windows-Zwischenablage kopieren, zwecks Weiterverarbeitung).

Wenn Du die Messungen statt mit einem Timer-Interrupt lieber mit einem Hardware-Interrupt triggern möchtest, z.B. über Deine Lochscheiben, wäre auch das möglich. Allerdings schafft das Programm derzeitig kaum viel mehr als 500 Datensätze pro Sekunde, das entspricht bei 30 Lochscheiben-Interrupts pro Umdrehung einer Drehzahl von 500/30*60 = 1000 Umdrehungen pro Minute.

Das Programm hat eine eingebaute “Buffer Overrun” Erkennung, d.h. wenn der FIFO-Puffer seinen maximalen Füllstand erreicht hat, erkennt dieser Sketch das und bricht die Messung mit einer Fehlermeldung ab, ohne dass es Aussetzer oder ausgefallene Messwerte gibt.

Ich bin zuversichtlich, die Messrate nochmal um “gut Faktor 2” erhöhen zu können, so dass Du mindestens 1000 (oder noch mehr) komplette Datensätze pro Sekunde bekommen kannst.

Probier’s mal aus, wenn etwas unklar ist, einfach fragen!

Ich habe jedenfalls noch Fragen zur PWM-Sinuserzeugung, das ist mir immer noch recht unklar. Wie soll der Sinus denn nun exakt verlaufen? Mit PWM kannst Du Werte von 0 bis 255 erzeugen. Also als Sinusanregung per PWM zum Beispiel einen Verlauf von 127 als Mittelwert +/-127, so dass die Sinuswelle zwischen 0 und 254 verläuft. Oder einen Mittelwert bei 200 +/-55, dann verläuft die Anregung der Welle zwischen 145 und 255.

Aus Deinem Code werde ich nicht schlau. Da steht was von “Amplitude=400 plus irgendwas”, aber das Ergebnis weist Du dann an ein 8-Bit Register zu (was nur Werte von 0 bis 255 annehmen kann). Kannst Du bitte nochmal genau erklären, wie die Sinusanregung zeitlich verlaufen soll? Frequenz= 5 Hz habe ich verstanden. Aber ich habe nicht verstanden, zwischen welchen PWM-Stufen (0…255) bzw. welchen Spannungen (0…VCC) die Sinusanregung nun eigentlich pendeln soll.

Nochmal zur Sinus-Anregung.

Ich habe mal einen Test-Sketch gemacht, wie so eine Sinusanregung verlaufen könnte, der Sketch gibt nur die Werte aus, wie sie im Zeitverlauf innerhalb von 5000 ms auftreten würden:

void setup() {
  float val;
  Serial.begin(9600);
  Serial.println();
  Serial.println("millis\tValue\tPWM\tca. Volt");
  for (int i=0;i<=5000;i=i+10)
  {
    float phase= 2*PI*i/5000.0;
    val=127.5+127.5*sin(phase);
//    val=200+55*sin(phase);  
    
    Serial.print(i);
    Serial.print('\t');
    Serial.print(val);
    Serial.print('\t');
    Serial.print(lround(val));
    Serial.print('\t');
    Serial.print(lround(val)*5.0/255);
    Serial.println();
  }
}

void loop() {
}

Die Zeile " val=127.5+127.5sin(phase);" würde eine Anregung von 0…255 (0V…5V) erzeugen, die Zeile "val=200+55sin(phase); " eine Anregung zwischen 145…255 (2.84V…5V).

So sinus-wellenförmig soll dann irgendwie die “Anregung” mit 5s Periodendauer verlaufen?
Oder kannst Du es konkreter vorgeben wie der Verlauf Deiner Sinusanregung sein soll?

Hallo jurs,

ersteinmal vielen Dank für das FIFO Beispiel. Ich bin heute noch nicht dazu gekommen es genauer unter die Lupe zu nehmen aber das werde ich noch machen.

Ich hab jetzt mal die Sinusschwingung in einem kleineren und hoffentlich verständlicherem Beispielprogramm umgesetzt. Ich arbeite mit einem Mega und habe das PWM-Signal an PIN 11. Für einen Uno muss der PWM-Pin auf Pin 9 umgestellt werden.

Hier nun der Beispielcode:

const    uint8_t PWM_Pin=11; // analoges Ausgangssignal 0...4095 für Uno auf PIN 9 umschalten!!!

// Kontinuierliches Sinussignal
float Zeit=0;      
float T=5;         // Periodendauer T=5 Sekunden für Sinusanregung T=1/f 
float pi=3.14159266;
float omega=2*pi*(1/T)/1000; // Kreisfrequenz Omega in 1/s (/1000 für die Umrechnung von millisekunden in s)
float Drehzahlmittelwert=1000; // 1/min Drehzahlmittelwert der Sinusanregung hier gebe ich die gewünschte Drehzahl vor
float Amplitude=400; // 1/min Amplitude der Sinusanregung +- 400 1/min ausgehend vom Drehzahlmittelwert

void setup(){ 

 pinMode(PWM_Pin, OUTPUT);      // PWM PIN 11 für Sinusanregung
 Serial.begin(9600); 

//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
//// Einstellungen Timer 1  
//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
TCCR1A = (1 << COM1A1) | (1 << WGM11);                // Enable Fast PWM on OC1A (Pin 11)
TCCR1B = (1 << WGM13) | (1 << WGM12) | (1 << CS10);   // Mode 14 Fast PWM/ (TOP = ICR1), pre-scale = 1
  
ICR1 = 4095;  //Set the TOP value for 12-bit PWM
OCR1A =0;     //PWM-PIN 11 auf LOW
} 
//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

void loop()
{
 Zeit=millis();  // Zeit in Sekunden
 //   Wertetabelle Tastverhältnis zur Drehzahl
 //   OCR1A | Drehzahl 1/min
 //   300   | 158
 //   500   | 328
 //   1000  | 810
 //   1200  | 1012
 //   1400  | 1199
 //   1600  | 1400
 //   Funktion bei linearer Annäherung->  Drehzahl=0.95954*OCR1A-141.71 umgestellt nach OCR1A: OCR1A=(Drehzahl+141.71)/0.95954 
 //   und in die Gleichung für eine harmonische Sinusschwingung: Drehzahl=Amplitude*sin(Kreisfrequenz*Zeit+Phasenverschiebung)+Drehzahlmittelwert eingesetzt 
 //   ergibt: 
 OCR1A = (Amplitude*sin(omega*Zeit)+Drehzahlmittelwert+141.71)/0.95954; //setzt Tastverhältnis an PWM_PIN 11
 
 Serial.print(Zeit);
 Serial.print('\t');
 Serial.println(OCR1A);
 delay(100);
 
}//Ende Hauptprogramm
//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

Das Programm setzt das Tastverhältnis am PWM-Pin auf Basis einer vorher bestimmten Drehzahlfunktion. Ich habe also eine Wertetabelle erstellt, in der ich bei einem bestimmten Tastverhältnis die Drehzahl an meinem Versuchsstand gemessen habe und hinterher daraus eine Funktion erstellt, die die gewünschte Drehzahl in das Tastverhältnis umrechnet.

Ich arbeite hier wie gesagt noch mit einem Versuchsstand, bei dem mit dem Ausgangssignal die Drehzahl eines Elektromotors angesteuert wird. Später wird dies der Steuerschieber eines Motors sein. Das macht aber von der reinen Theorie her keinen Unterschied.

Anregungssignal ->Regelstrecke->Ausgangssignal

Ich hab mal ein paar Fotos vom Versuchsstand gemacht, damit man sich das besser vorstellen kann.