Forum: Mikrocontroller und Digitale Elektronik Programmablauf wie macht man's am besten?


von Erik D. (dareal)


Angehängte Dateien:

Lesenswert?

Habe heute malwieder gut was geschafft ...

Mein derzeitiges Projekt:

Stromanzeige für die Hifi-Anlage im KFZ

Bisher habe ich eine 4x40 Anzeige angesteuert von einem Atmega8 (auf 
STK500).
Auf dem Display werden die Ströme angezeigt, Momentanwert (etwas 
gemittelt), Peak und ein kleiner Bargraph. Das ganze für 4 Kanäle. Die 
Datenaufbereitung geschieht mittels vier Stromsensoren von LEM welche 
vor den jeweiligen Endstufen sitzen (bis 500A), welche über eine Bürde 
gute 5V bei 500A primärseitig erzeugen.


Derzeitiger Programmablauf sieht wie folgt aus:


# Timerint
- Einlesen eines Kanals
- Peak vergleichen bzw. neu schreiben
- aktuellen Wert zum Summierregister addieren
-- das ganze für alle 4 Kanäle nacheinander
--- alle 4 Kanäle werden zusammen 64 mal gesampled und aufsummiert

- Mittelwerte > BCD und an LCD Ausgeben, alle 4 Kanäle nacheinander
- Peaks > BCD und an LCD ausgeben
- bargraph berechnen und ausgeben (ebenfalls für jeden kanal)


Mein Problem ist derzeit: für den Bargraph ist die Anzeige zu langsam, 
für den Mittelwert zu schnell zum ablesen.

Welche Mögleichkeiten gibt es die Aufgaben geschickter zeitlich zu 
verteilen?

Am liebsten wäre mir:
- kontinuierliches Auslesen der Kanäle und mit Peak vergleichen
- Werte für Mittelwertbildung in einem Ringspeicher(LIFO)
- LCD Ausgabe so schnell wie möglich (für saubere Bar-Graph-Anzeige)
- Mittelwertanzeige etwas langsamer, damit man das auch besser ablesen 
kann


Vor einer kon. Auswertung im Mainloop bin ich bisher zurückgeschritten, 
weil ich angst habe, dass der Timer dann vllt. in irgendeine Berechnung 
eingreift und mir dann teils nur müll ausgibt bzw. verfälschte werte.


Im Anhang ist mal ein Beispiel, wie das derzeit aussieht ... die Werte 
sind aber nicht REAL ;)

von Hannes L. (hannes)


Lesenswert?

Den ADC würde ich im Free-Run-Mode laufen lassen und im Timer-Interrupt 
reiherum die ADC-Werte einlesen und dabei gleitende Mittelwerte über 256 
Messungen bilden. ADC-Takt und Timer-Intervall werden so aufeinander 
abgestimmt, dass zwischen den Abfragen mindestens zwei Wandlungen 
stattgefunden haben, von denen die erste ignoriert wird.

Die Timer-ISR macht dann also Folgendes:

- ADMUX einlesen, um die aktuell gemessene Quelle zu bestimmen,

- Pointer auf der Quelle entsprechende RAM-Zelle positionieren,

- ADC-Mittelwert und Nachkommawert aus RAM holen,

- 1/256 des Mittelwertes davon subtrahieren (Byteshifting),

- ADC-Messwert einlesen und mit 1/256 Wertigkeit (Byteshifting) 
addieren,

- Mittelwert und Nachkommawert zurück ins RAM,

- Messquelle erhöhen und Zählumfang begrenzen,

- neue Messquelle in ADMUX schreiben

Die Mainloop kann dann reiherum die vier Messwerte aus dem RAM holen, in 
ASCII umwandeln und ausgeben und nach jedem Messwert in ASCII alle 4 
Messwerte als Bargraph. Damit gibt es ein schnelles Bargraph-Update, 
aber eine langsamere Textausgabe. Falls der Text noch zu schnell kommt, 
dann eben zweimal (oder öfter) alle Bargraphen ausgeben, ehe der nächste 
Text ausgegeben wird.

...

von Thomas (kosmos)


Angehängte Dateien:

Lesenswert?

die Auflösung des Bargraps ist einfach zu gering drum erscheint es dir 
so langsam. Du könntest ja eigene Zeichen aufs LCD ausgeben und so die 
Auflösung um das 5fache erhöhen. Hier mal ein Beispiel von 5 Zeichen die 
du selbst generieren müsstest. Auflösunf ist etwas klein ab besten 
abspeichern und dann mit einem Programm öfnnen mit dem du es vergrößern 
kannst, falls dein Browser das nicht kann.

von Johannis Kraut (Gast)


Lesenswert?

Zusätzlich könntest du noch einen Peakholder anzeigen lassen der alle 
~1s refreshed wird. Aber ist es wirklich nötig die Anzeige der 
Stromaufname so schnell wie ein dB-Meter zu machen?

von Erik D. (dareal)


Lesenswert?

@Hannes

Danke für die Aufschlüsselung ... das mit dem Pointer muss ich mir 
erstmal anschauen, lade das derzeit alles manuell aus dem RAM und in den 
RAM an fest vorgeschriebene Stellen.
Wobei ich langsam schon mit der optimierung anfange.

Derzeit habe ich eh nicht mehr ganz so große Probleme mit dem LCD, da 
das jetzige schon per UART angesteuert wird .. was vieles vereinfacht.
Leider nur bis max. 19200 Baud, was bei 4x40 Zeichen + Steuerbefehle 
dann nicht wirklich viel ist.

Vllt sollte man die Darstellung separieren ... bargraph so oft wie 
möglich aktualisieren, Peak-Werte nur neu schreiben, wenn neue sind und 
die aktuellen werte nur alle 2-3 mal pro Sekunde.


@Thomas

Nein, es läuft tatsächlich etwas sehr langsam ... selbst wenn ich ihn 
auf 40 Symbole aufpluster wirkt es ansatzweise lahm.


@Johannis

Peak-Hold ist angedacht, muss ich aber sehen. Bei 8 Zeichen Bargraph 
meiner Meinung nach nicht wirklich sinnvoll. Bei nem grafikschen Display 
wäre dann dann schon eher interessant.



PS: Die Bragraphen habe ich gestern noch etwas angepasst, an die 
tatsächlichen Wertebereiche ...
Subwoofer: bis 128 A
HT, MT und Charge: bis 64 A

Damit bekomme ich dann wenigstens was angezeigt.
Meine Peak-Messwerte sind derzeit
woofer: 110 A
mt: 39 A
ht: 39 A
charge: 39 A

von Hannes L. (hannes)


Lesenswert?

Erik D. wrote:
> @Hannes
>
> Danke für die Aufschlüsselung ... das mit dem Pointer muss ich mir
> erstmal anschauen, lade das derzeit alles manuell aus dem RAM und in den
> RAM an fest vorgeschriebene Stellen.

Das ist eigentlich gaaaanz einfach...

Für gleitenden Mittelwert über 256 Messungen setze ich gern den hier 
vorgestellten Algorithmus ein:
Beitrag "Re: ADC Werte sauber aufs display bringen"

Der Code ist aber nur für einen ADC-Kanal gedacht, weshalb die drei 
Bytes (Mittelwert H, Mittelwert L und Nachkomma) auch in drei Registern 
liegen.

Bei mehreren ADC-Kanälen muss man diese ins RAM auslagern, sonst werden 
die Register knapp. Es sind (bei 10 Bit Auflösung) 3 Bytes je Kanal, bei 
8 Bit Auflösung nur 2 Bytes. Für 4 Kanäle (möglichst ADC0, ADC1, ADC2 
und ADC3, denn da kann man schön den Zählumfang mit ANDI zahl,3 
begrenzen) und 10 Bit Auflösung brauchst Du 12 Bytes RAM. Um die zu 
reservieren brauchst Du ja im DSEG ein Label:

.dseg
adc_werte:   .byte 12   ;12 Bytes für ADC-Werte reservieren

Dieses Label ist Deine Basisadresse für den Pointer. Du lädtst also den 
Z-Pointer oder Y-Pointer (der X-Pointer taugt dazu nix, da er kein 
LDD/STD unterstützt) mit dem Label:

 ldi zl,low(adc_werte)      ;Pointer auf Anfang
 ldi zh,high(adc_werte)     ;des Arrays

Dann addierst Du die Nummer des ADC-Kanals dazu:

 add zl,nummer              ;Nummer dazu,
 adc zh,null                ;evtl. Übertrag (Carry) auch

Nun zeigt je nach Nummer (0 bis 3) der Pointer auf eines der 4 ersten 
Bytes des Arrays. Der Einfachheit halber haben wir vereinbart, dass in 
den ersten 4 Bytes das H-Byte aller 4 Kanäle steht, dann kommen die 
L-Bytes aller 4 Kanäle und danach die Nachkommawerte aller 4 Kanäle. 
Warum?? - Der AVR hat die Befehle LDD und STD, mit denen mit nur einem 
Pointer und einem als Konstante angegebenem Offset mehrere Bytes 
adressiert werden können. Mit:

 ldd temp1,z+0              ;H-Byte holen,
 ldd temp2,z+4              ;L-Byte holen,
 ldd temp3,z+8              ;Nachkomma holen

holst Du alle drei Bytes über einen Pointer (mit ADC-Kanal als Index) in 
drei Register, um sie (gemäß Link) zu verarbeiten. Der Pointer wurde 
dabei nicht verändert und kann nach der Verarbeitung wieder zum 
Zurückschreiben benutzt werden:

 std z+0,temp1              ;H-Byte,
 std z+4,temp2              ;L-Byte und
 std z+8,temp3              ;Nachkomma zurück ins RAM

> Wobei ich langsam schon mit der optimierung anfange.

Naja, bis zur wirklichen Optimierung ist noch ein weiter Weg...

>
> Derzeit habe ich eh nicht mehr ganz so große Probleme mit dem LCD, da
> das jetzige schon per UART angesteuert wird .. was vieles vereinfacht.
> Leider nur bis max. 19200 Baud, was bei 4x40 Zeichen + Steuerbefehle
> dann nicht wirklich viel ist.

Da kann aber ein herkömmlicher LCD-Treiber schneller sein. Man darf nur 
nicht direkt in das LCD schreiben wollen. Für ein 4x40-LCD nutze ich 160 
Bytes "Bildschirmspeicher" im RAM des AVRs. Die LCD_Data-Routine wirft 
die auszugebenden Zeichen einfach (an die richtige Position) in diesen 
Bildschirmspeicher. Im Hintergrund (entweder im Timer-Int, oder durch 
Timer-Int synchronisiert in der Mainloop) wird dann alle etwa 1 ms ein 
Zeichen aus dem Bildschirmspeicher an das LCD geschaufelt. Das sind 6 
Updates pro Sekunde, so schnell können wir nicht lesen.

Wenn das zu langsam für die Bargraphen ist, dann lässt sich die 
Update-Routine auch so umschreiben, dass sich eine der oberen drei 
Zeilen mit der unteren Zeile abwechselt. Das ergibt dann 12 Updates pro 
Sekunde für die Bargraphen und 4 Updates pro Sekunde für die oberen drei 
Zeilen.

>
> Vllt sollte man die Darstellung separieren ... bargraph so oft wie
> möglich aktualisieren, Peak-Werte nur neu schreiben, wenn neue sind und
> die aktuellen werte nur alle 2-3 mal pro Sekunde.

Ja, sicher, wobei ich mir über die Peaks noch keine Gedanken gemacht 
habe. Die müsste man irgendwie bei der Mittelwert-Berechnung mit 
behandeln. Z.B. für jeden Kanal einen weiteren Wert im RAM halten, 
diesen bei jeder Messung herunterzählen und auf den aktuellen Messwert 
anheben, falls dieser größer war. Somit erfasst man damit auch Spitzen, 
die in den Mittelwert gar nicht einfließen.

Möglichkeiten gibt es viele. Ich hoffe, Dir ein paar Denkanstöße gegeben 
zu haben.

...

von Erik D. (dareal)


Lesenswert?

Oh vielen dank .. sehr ausführlich ... habe das eben mal überflogen und 
verstehe noch Bahnhof aber werd das nachher mal schritt für schritt 
durchgehen.

Die peaks ermittle ich derzeit nach jeder Einzelmessung. D.h. ADC 
einlesen, Peaks aus RAM holen, vergleichen, ggf. neu schreiben und 
normal weitermachen.

von Hannes L. (hannes)


Lesenswert?

Erik D. wrote:
> Oh vielen dank .. sehr ausführlich ... habe das eben mal überflogen und
> verstehe noch Bahnhof aber werd das nachher mal schritt für schritt
> durchgehen.
>
> Die peaks ermittle ich derzeit nach jeder Einzelmessung. D.h. ADC
> einlesen, Peaks aus RAM holen, vergleichen, ggf. neu schreiben und
> normal weitermachen.

Aha, dann kann das die Mittelwertbildung nebenher mit erschlagen.

Schreib' mal, ob Du mit 8 Bit ADC-Auflösung auskommst, oder ob Du 
unbedingt die vollen 10 Bit brauchst. Denn am Bargraph kannst Du die 10 
Bit sowiso nicht darstellen, ja nichtmal die 8 Bit. Begrenzung auf 8 Bit 
macht die Sache nämlich einfacher und schneller.

...

von Erik D. (dareal)


Lesenswert?

Derzeit nutze ich die vollen 10 Bit, summiere 64 Werte und das passt 
dann alles in zwei Register :)

Der Bargraph hat ja nur sogesehn 3 Bit ;D

Die erste Zeile ist eh statisch und muss auch nur einmal geschrieben 
werden, somit spare ich mir damit schon einen ganzen String zu senden :)

von Hannes L. (hannes)


Lesenswert?

Erik D. wrote:
> Derzeit nutze ich die vollen 10 Bit,

Ich wollte eigentlich nicht wissen, ob Du sie nutzt, sondern ob Du sie 
brauchst.

> summiere 64 Werte und das passt
> dann alles in zwei Register :)

Das ist eine Methode, die ich (ich persönlich) nicht besonders mag. Denn 
man bekommt nur alle 64 Messungen einen neuen Wert, wobei dieser auch 
noch sehr stark vom vorherigen Wert abweichen kann. Und wenn man es dann 
noch mit Busywait macht, wie im Tutorial vorgeschlagen wurde, dann hat 
der AVR seine ABM, die ihn schon fast auslastet. Ich mache sowas lieber 
"im Vorbeigehen" und nutze die Pausen dazwischen für andere Arbeiten. 
Aber es soll Jeder machen, was er für richtig hält.

>
> Der Bargraph hat ja nur sogesehn 3 Bit ;D
>
> Die erste Zeile ist eh statisch und muss auch nur einmal geschrieben
> werden, somit spare ich mir damit schon einen ganzen String zu senden :)

...

von Erik D. (dareal)


Lesenswert?

Naja, wenn ich auf 8 Bit runtergehe wäre das sicherlich auch kein 
Verlust.

Hatte eh vor die Vref auf 2,5 bzw. 2,56 Volt runterzusetzen, da der 
bereich bis 250 A sicher ausreichen sollte. Die Bürde an den 
Stromwandler ist so ausgelegt, dass ich bei 500 A -> 5 V erhalte. Ich 
weiß jetzt nicht, ob es alternativ möglich wäre den Bürdenwiderstand 
weiter zu erhöhen um z.B. schon bei 250A meine 5V zu erhalten, laut 
Datenblatt ist meine Bürde so ziemlich am ausreizen des machbaren.

Der Wandler setzt den primärstrom 2000:1 in einen Sekundärstrom um. Bei 
500A wären das 250 mA, dieser wird über eine Bürde (Widerstand) 
geschickt um eine proportionale Spannung zu erhalten. Derzeit sind diese 
Bürden bei mir mit 20 Ohm bemessen, was bei 250 mA Stromfluss 5 V 
ergibt. Laut Dtaenblatt sind für meinen Strombereich, bei der 
Versorgungsspanung von +/- 15V maximal 25 Ohm als Bürde angegeben.


Ja, derzeit arbeitet mein Programm noch mit diesem Busy-Flag und wartet 
jeweils auf ne fertige Wandlung. Dass man diese Zeit sinnvoller nutzen 
kann habe ich mir auch schon gedacht. Vllt ist es sinnvoller die 
AD-Wandlung zu starten, dann die Arbeit zu machen, welche zwischen den 
Wandlungen stattfindet und dann den neuen wert abrufen und weitermachen.

von Hannes L. (hannes)


Lesenswert?

Erik D. wrote:
> Naja, wenn ich auf 8 Bit runtergehe wäre das sicherlich auch kein
> Verlust.

Das macht Einiges einfacher...

> Der Wandler ...

Da halte ich mich raus. Stromwandler waren für mich vor 40 Jahren ein 
Thema, aber die konnten nur Wechselstrom und brauchten eine sehr 
niederohmige Bürde, die mit 5A belastet wurde... ;-)

> Ja, derzeit arbeitet mein Programm noch mit diesem Busy-Flag und wartet
> jeweils auf ne fertige Wandlung. Dass man diese Zeit sinnvoller nutzen
> kann habe ich mir auch schon gedacht. Vllt ist es sinnvoller die
> AD-Wandlung zu starten, dann die Arbeit zu machen, welche zwischen den
> Wandlungen stattfindet und dann den neuen wert abrufen und weitermachen.

Vielleicht ginge in Deinem Fall ja Folgendes:

Der ADC läuft im Free-Run-Mode ohne Interrupt durch. Du rechnest Dir 
(anhand Vorteiler und Wandlungsdauer) aus, wie lange es dauert, zwei 
Wandlungen durchzuführen und rundest diese Zeit etwas auf (bis auf knapp 
3 Wandlungen). Dies ist nun das Intervall für einen Timer, der nun im 
Prinzip das ganze Programm synchronisiert. In dessen ISR setzt Du einen 
Merker (Bitvariable), der dem Hauptprogramm mitteilt, dass es "Zeit 
ist", mal was Anderes zu tun.

Im Hauptprogramm läuft als Schleife die LCD-Ausgabe. Diese hat 
prinzip-bedingt ein Busywait (Abfrage, ob LCD (oder UART) bereit ist). 
Immer vor diesem Busywait prüfst Du Deinen Merker und verzweigst bei 
gesetztem Merker zu einem Unterprogramm, das den Merker wieder löscht 
und den ADC ausliest (mit Mittelwert- und Peak-Aufbereitung). Es können 
auch noch Berechnungen stattfinden, falls das nötig ist. Somit hast Du 
im günstigsten Fall die Arbeit gemacht, ehe das LCD wieder bereit ist, 
im schlechtesten Fall muss das LCD eben etwas länger warten.
Die Schleife im Hauptprogramm organisierst Du am besten so, dass Du 
wechselweise die Zahlen eines Kanals und die Bargraphen aller Kanäle 
ausgibst, also etwa so (Pseudocode):

 Pointer=Basis (Datensatz 0)
mainloop:
 rcall printwerte
 rcall printbargraph
 adiw Pointer,1
 if pointer > Basis+4 then Pointer=Basis
 rjmp mainloop

printwerte:
 LCD-Position für Wert aus Datensatz holen und an LCD schicken
 ADC-Mittelwert und Nachkomma (zusammen 16 Bit) aus Datensatz holen,
 rcall print16
 LCD-Position für Peak aus Datensatz holen und an LCD schicken
 Peak aus Datensatz holen, 0 als Nachkomma, ergibt auch 16 Bit ;-)
 rcall print16
 ret

printbargraph:
 LCD-Position auf Beginn Bargraph 0
 Peak0 absolut aus RAM holen
 rcall machgraph
 LCD-Position auf Beginn Bargraph 1
 Peak1 absolut aus RAM holen
 rcall machgraph
 LCD-Position auf Beginn Bargraph 2
 Peak2 absolut aus RAM holen
 rcall machgraph
 LCD-Position auf Beginn Bargraph 3
 Peak3 absolut aus RAM holen
 rcall machgraph
 ret

machgraph:
 Erstellung der ASCII-Zeichen wie bisher,
 rcall lcd_data für jedes Zeichen
 ret

print16:
 durch Subtraktionsmethode in ASCII-Ziffern zerlegen,
 wenn eine Ziffer da ist, rcall lcd_data,
 wenn alle Ziffern durch sind, dann ret.

lcd_data:
 Merker fragen, ob gültiger ADC-Wert vorliegt
 wenn ja, rcall holadc
 LCD noch beschäftigt? - wenn ja, rjmp lcd_data
 LCD-Ausgabe wie auch immer...
 ret

holadc:
 cbr merker,1<<adcda  ;Merker löschen, Job wird ja erledigt
 push zl              ;Pointer sichern
 push zh
 ldi zl,low(werte)    ;Pointer auf Anfang Array
 ldi zh,high(werte)
 in temp1,admux       ;akt. Messquelle holen
 andi temp1,3         ;nur die Messquelle stehen lassen
 add zl,temp1         ;als Index auf Array
 adc zh,null          ;zum Pointer addieren
 in temp3,adch        ;ADC-Wert H holen
 inc temp1            ;Messquelle erhöhen
 andi temp1,3         ;auf 0 bis 3 begrenzen
 ori temp1,muximmer   ;Konstante mit Bits für Referenz und adlar dazu
 out admux,temp1      ;Messquelle ist umgeschalten, temp1 wieder frei
 ldd temp1,z+8        ;Peak holen
 cp temp1,temp3       ;ist neuer Wert > peak?
 brcc holadc1         ;nein...
 mov temp1,temp3      ;ja, übernehmen
holadc1:
 dec temp1            ;Peak verringern
 brcc holadc2         ;Unterlauf? - nein
 clr temp1            ;ja, zurück auf 0
holadc2:
 std z+8,temp1        ;neuen Peak speichern, temp1 wieder frei
 ldd temp1,z+0        ;Mittelwert holen
 ldd temp2,z+4        ;Nachkomma holen
 sub temp2,temp1      ;1/256 rausnehmen
 sbc temp1,null       ;natürlich mit Übertrag
 add temp2,temp3      ;1/256 vom Neuwert reinlegen
 adc temp1,null       ;Übertrag (im Carry) natürlich auch
 pop zh               ;Pointer wiederherstellen
 pop zl
 ret                  ;fertig

Im SRAM anzulegendes Array:
werte:   .byte 20
- 4 Bytes Mittelwert
- 4 Bytes Nachkomma
- 4 Bytes Peak
- 4 Bytes LCD-Position Stromausgabe (Werte per Init erzeugt)
- 4 Bytes LCD-Position Peakausgabe (Werte per Init erzeugt)

Der Code ist "aus dem Hut" geschrieben, also nicht geprüft, und kann 
daher Fehler enthalten. Er sollte aber verständlich machen, was ich 
meine.

...

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.