Hallo an Alle,
benötige ich bei Microcontroller-Anwendungen mehrere Instanzen von
verschiedenen Programmteilen, so realisiere ich dies durch die Kapselung
in einem Struct (unten das Beispiel wenn ich mehrere DACs von einem Typ
verbaut habe).
Ist man konsequent in dieser Umsetzung, dann erhält man eine sehr gut
leserliche, übersichtliche und in sich "gekapselte" Software. Leider
erhält man durch die ganzen Übergaben auch viel overhead an code.
Wie macht ihr das, bzw. was muss man beachten um das Ganze trotzdem so
effizient wie möglich umzusetzen.
Der Code sieht meiner Meinung nach nicht ineffizient aus. Hast Du mal
nachgeschaut bzw. gemessen, wie viel "Overhead" entsteht? Und im
Vergleich zu was?
Ein paar Performance-Tipps:
- Optimierung des Compilers einschalten (GCC: -Os)
- Link Time Optimization einschalten (GCC: -flto bei Compiler und
Linker)
- Den am häufigsten benutzten Member in der Struktur an die erste
Position setzen. Dieser Member kann ohne Addition eines Offset auf den
Zeiger auf die Struktur verwendet werden.
- Darauf achten, dass in der Struktur keine Lücken durch
unterschiedliche Alignment-Anforderungen der Member entstehen.
- Ggf. mehrmals innerhalb einer Funktion über Funktionsaufrufe hinweg
verwendete Member in lokale Variablen kopieren. Der Compiler weiß z.B.
nicht unbedingt, dass SPI_MasterTransceiveByte den Wert von ad_data->spi
nicht ändert. Dann muss er ihn jedes Mal neu aus der Struktur laden
anstatt ihn sich in einem Register zu merken.
Zum Software-Design:
Ich würde die Zugriffe auf Pins in eine eigene Klasse auslagern, sprich
pin_cs und port_cs in einer Struktur mit entsprechenden
Zugriffsfunktionen kapseln. Die sbi/cbi-Aufrufe mit zweifacher
Dereferenzierung in die Struktur verletzen nämlich das "Law of Demeter":
https://de.wikipedia.org/wiki/Gesetz_von_Demeter
Die ganzen Struck-Geschichten sind für uns Menschen zum besseren
Verständnis, aber für den Kontrollör doch nur Variablen. Ich denke, bei
einem modernen Compiler kommt gleich viel Code raus, wie wenn ein ganzer
Haufen diskreter Variablen angelegt wird.
Martin J. schrieb:> Leider erhält man durch die ganzen Übergaben auch viel overhead an code.
Du erfindest gerade C++ mit dem self pointer neu.
Ja, damit kann man ineffizient programmieren
Roland E. schrieb:> Ich denke, bei einem modernen Compiler kommt gleich viel Code raus, wie> wenn ein ganzer Haufen diskreter Variablen angelegt wird.
Leider nein. Indirekte Zugriffe über pointer und offset sind schon
aufwändiger als Zugriffe an absolut adressierte globale Variablen, aber
wenn man nicht 1 sondern mehrere Schnittstellen bedienen will, ist der
pointer oft effektiver als ein array mit Index.
Martin J. schrieb:> Wie macht ihr das, bzw. was muss man beachten um das Ganze trotzdem so> effizient wie möglich umzusetzen.
Deine Definition von Effizient ist "ungewöhnlich"
Mehr Aufwand bei Codieren und Verschwendung von Speicherplatz nennen
ich immer Ineffizient
Strukturen werden alligent erzeugt d.h zwischen dem ersten Byte und dem
Zeiger sind bei dir drei Byte Lücke.
Es kann hilfreich sein relativ zu einem Basiszeiger zu Adressieren
da sind solche Modulstruckturen hilfreich. Lohnt i.d.r aber den Aufwand
nicht.
eine gute Möglichkeit für Kapselung nennt sich C++
Dass diese Kapselung mit C++ besser geht stimmt schon. Leider gibt es
aber Debugger (oder IDEs mit Debugger), die diese Klassen (genauer ihre
Variablenwerte) nicht anzeigen können. Als Beispiel hier AvrStudio 6.2
mit den JTAGICE3 oder auch dem Atmel ICE.
Und davon abgesehen wird das auf die oben genannte Weise selbst bei den
ARM-Controllern so gehandhabt (siehe STM32 mit den Libraries).
Allerdings kenne ich den Grund dafür nicht, er würde mich aber sehr
interessieren.
MaWin schrieb:>> Leider nein. Indirekte Zugriffe über pointer und offset sind schon> aufwändiger als Zugriffe an absolut adressierte globale Variablen, aber> wenn man nicht 1 sondern mehrere Schnittstellen bedienen will, ist der> pointer oft effektiver als ein array mit Index.
Woher will man wissen, dass der Compiler ersteres verwendet und nicht
letzteres? Richtig, in dem man sich den Assembler ansieht.
Meine Erfahrungen haben bisher keinen Unterschied im Ergebnis zwischen
beiden Varianten im C-Code ergeben. Für den Rechner ist es ja auch
völlig wumpe, wie wir doofen Menschen die Adresse 0xirgendwas nennen.
Martin J. schrieb:> Ist man konsequent in dieser Umsetzung, dann erhält man eine sehr gut> leserliche, übersichtliche und in sich "gekapselte" Software. Leider> erhält man durch die ganzen Übergaben auch viel overhead an code.
Das ist der Preis für die Generizität der Funktionen :)
Bei einem ARM- oder i86-Prozessor hält sich der Overhead in Grenzen. Ich
vermute aber, du verwendest eher einen einfachen Controller wie bspw.
einen AVR, der sich in diesem Fall in zwei Dingen recht schwer tut:
- Er kann nicht besonders gut mit Pointern umgehen, da er zum einen nur
wenige Adressregister (X, Y, und Z) und keine indirekte Adressierung
mit Displacement hat.
- Er kann nativ keine Shifts mit variabler Shift-Weite (wie sie hier für
die Bit-Maskierung benötigt werden). Sie können nur in Form von
Schleifen implementiert werden.
Allgemein und perfekt lässt sich dieses Problem leider nicht lösen, aber
ein Bisschen kann man dem Compiler schon auf die Sprünge helfen, indem
man ihm schon zur Compilezeit möglichst viele Informationen liefert, die
er zur Optimierung verwenden kann.
In deinem Beispiel gibt es mehrere Strukturen, die Konfigurationsdaten
wie bspw. Ports, Bitnummern u.ä. enthalten. Diese Daten sind meist
unveränderlich, aber der Compiler weiß das nur, wenn man ihn explizit
darauf hinweist.
Wenn man diese Strukturen als const deklariert und dafür sorgt, dass der
Compiler jederzeit ihre Initialisierungswerte sehen kann, ist schon viel
gewonnen. Strukturen die, in mehreren Übersetzungseinheiten benötigt
werden, sollten deswegen in einem gemeinsam genutzten Header-File als
static const definiert und initialisiert werden. Das static bewirkt
zudem oft, dass die Strukturen nicht einmal Speicherplatz belegen.
Wird so eine konstante Struktur als Pointer-Argument an eine Funktion
übergeben, geht die Information über die Initialisierungswerte leider
verloren, es sei denn, die Funktion wird geinlinet. Dann kann der
Compiler für jeden Funktionsaufruf einen der Situation angepassten,
optimalen Code generieren. Inlinen kann der Compiler aber nur dann, wenn
er den Quellcode der Funktion sehen kann. Sollen die zu optimierenden
Funktionen aus mehreren Übersetzungseinheiten aufrufbar sein, sollte man
sie ebenfalls in einem gemeinsam genutzten Header-File definiert werden,
und zwar als static inline (ggf. auch mit dem GCC-spezifischen Attribut
always_inline).
Da dein Beispiel unvollständig ist (wichtige Funktionsdefinitionen und
Strukturdeklarationen fehlen), konnte ich dein Beispiel nicht gemäß den
obigen Vorschlägen umschreiben. Stattdessen habe ich mir ein einfaches,
aber trotzdem nicht zu triviales Beispiel ausgedacht (s. Anhang). Darin
wird eine 8-bit-parallele Datenübertragung (IEEE 1284 in abgespeckter
Form) implementiert.
Wie in deinem Beispiel sind sämtliche Konfigurationsdaten in Strukturen
festgelegt, und die Ausführung erfolgt über mehrere Unterprogrammebenen.
Sämtliche typedef-Deklarationen und static-Definitionen können auch in
ein Header-File ausgelagert werden, so dass sie auch in mehreren
Übersetzungseinheiten verfügbar sind.
Durch die Implemetierung gemäß den obigen Vorschlägen ist der Compiler
in der Lage, den gesamten Code auf eine einzige Funktion (test) zu
reduzieren, die nur die tatsächlich erforderlichen I/O-Befehle enthält.
Hier ist der vom Compiler generierte Assembler-Output:
1
test:
2
ldi r24,lo8(-1)
3
out 0x17,r24
4
sbi 0x14,3
5
sbi 0x15,3
6
ldi r24,lo8(123)
7
out 0x18,r24
8
cbi 0x15,3
9
sbi 0x15,3
10
.L2:
11
sbic 0x13,5
12
rjmp .L2
13
ret
Datenspeicher wird außer für den Programmstack keiner benötigt. Die
Ausgabe von avr-size:
1
text data bss dec hex filename
2
22 0 0 22 16 parport.o
Besser geht es auch mit handgeschriebenem Assembler nicht.
Lässt man zum Vergleich alle const und static weg, dann entsteht an
ähnlichen Stellen wie in deinem Beispiel ein deutlicher Overhead. Der
Programmcode verzehnfacht sich (bei der Laufzeit dürfte der Faktor noch
größer sein), und es kommen 18 Bytes Daten für die Strukturen (sowohl im
Flash als auch im RAM) hinzu:
1
text data bss dec hex filename
2
224 18 0 242 f2 parport.o
Das hört sich jetzt alles ganz toll an, allerdings hat die Methode auch
einen Nachteil: Die Optimierung funktioniert nur dann so gut, wenn alle
beteiligten Funktionen geinlinet werden. Das führt aber bei komplexeren
Funktionen, die von vielen Stellen im Programm aufgerufen werden, zu
wesentlich mehr Programmcode. In diesem Fall sollte man die Methode auf
zeitkritische Programmteile beschränken, wenn man nicht gerade Flash-
Speicher im Überfluss hat.
Hallo,
vielen Dank für die vielen Antworten.
Verwendet werden diverse kleiner Arm- oder Xmega-Controller.
Die Xmega verwende ich einfach sehr gerne auf Grund ihrer tollen
Peripherie. Diese Peripherie schreit ja auch regelrecht nach
Instanzierung oder C++.
Bei einer Diskussion wie dieser wird immer gleich C++ genannt. Bei
meiner Suche bin ich aber auf keine wirklich guten Beispiele/Vorlagen
gestoßen. Alle C++ Bibliotheken für Controller sind nur zur Hälfte oder
kaum fertig und unterstützen nur die einfachsten Hardwarefunktionen.
Neben den Hardware-Bibliotheken hat man dann auch das grundlegende
Problem mit Interruptroutinen. siehe auch die Diskussionen hier
Beitrag "C++ für Mikrocontroller"
Bei all den Möglichkeiten, fand ich die Umsetzung mit den Structs in C
am sinnvollsten. Daher auch die Frage was man hier noch beachten muss um
solch eine Umsetzung noch so effizient wie möglich zu bekommen.
Wer gute Bibliothken oder Vorlagen für C++ hat soll diese nennen.
Hallo Felix,
danke für das Beispiel, so wie ich dein SPI-Code verstehe, kann ich
diesen immer für eine SPI Schnittstelle verwenden. Da ich den
verwendeten SPI-Port ja über globale #defines vorgebe. Damit bringt mir
ja die Flexibilität von C++ nix.
Du kannst auch zusätzlich für SPI auf Port C einen anlegen. Und ggfs.
für Port E. Punkt ist, dass der Code nur einmal vorhanden ist. Dank der
Klasse sind die Daten aber für die drei Schnittstellen gekapselt.
Ich denke, der einzige Vorteil ist hier, dass du die Daten nicht
außerhalb der spi.c/h halten musst, sondern diese nur innerhalb der
Klasse existieren.
Aber aufgrund der Debugprobleme habe ich SPI aschon wieder auf Structs
umgebaut. Und es wird damit nicht merkbar größer. Allerdings muss mah
dann den Struct immer mitführen, was bei der Klasse nicht der Fall ist.
Hier passiert das durch ihren Namen.
Ich denke, das ist letztlich eine Frage der eigenen Präferenz. Und
jemand sagte mir mal: "C++ auf Mikrocontrollern tut man einfach nicht".
Es geht aber, wenn man will...
Vielen Dank für das Beispiel. Das mit den Debug-Problemen bei C++ war
mir noch nicht bekannt.
Ich setz mich nachher nochmal hin und werd mein Beispiel und die
Umsetzung mit den Structs noch etwas ausführlicher machen.
Hallo Yalu X. im Anhang ist eine ausführlichere Version meiner
Umsetzung.
Wie kann man den Code unter Verwendung von Structs schneller machen?
Die größe des Code ist nicht so kritisch, da die heutigen Controller
meist genug Speicher haben.
Felix Adam schrieb:> Ich denke, das ist letztlich eine Frage der eigenen Präferenz. Und> jemand sagte mir mal: "C++ auf Mikrocontrollern tut man einfach nicht".
Diese Aussage kommt regelmäßig von Leuten, die kein vertieftes Wissen
über C++ und speziell ab c++14/17 haben. Dinge wie constexpr,
variadische templates, ... und auch constraints / concepts (zugegeben,
ist noch nicht im Standard, kann der g++ aber schon wunderbar).
Aber die Diskussion dazu hier in diesem Forum ist sehr emotionsgeladen
... be warned!
Hab dir ne Mail geschickt. Allerdings muss ich erst noch Beispiele
sammeln, damit ein Upload hier auch Sinn macht. Sonst dürfte es eine
ganze Menge Nachfragen bezüglich der Nutzung geben...
Schöne Ostern.
Wilhelm M. schrieb:> Felix Adam schrieb:>>> Ich denke, das ist letztlich eine Frage der eigenen Präferenz. Und>> jemand sagte mir mal: "C++ auf Mikrocontrollern tut man einfach nicht".>> Diese Aussage kommt regelmäßig von Leuten, die kein vertieftes Wissen> über C++ und speziell ab c++14/17 haben. Dinge wie constexpr,> variadische templates, ... und auch constraints / concepts (zugegeben,> ist noch nicht im Standard, kann der g++ aber schon wunderbar).>> Aber die Diskussion dazu hier in diesem Forum ist sehr emotionsgeladen> ... be warned!
In C++14 kann man mit variadic Templates ganz problemlos einzelne
Io-Pins abbilden, die man zu PinSets zusammenbauen kann, die einen
Werte-Typ haben, die physisch auf verschiedenen Addressen liegen, z.B.
WGM13..WGM10 und denen man einem (erlaubten) Wert aus der zugehörigen
enumeie-Klasse zuweisen kann. Am Ende stehen Zugriffe auf die 2 Register
TCCR1A/B.
Aber: zu emotionsgeladen um es zu veröffentlichen.
Carl D. schrieb:> Wilhelm M. schrieb:>> Felix Adam schrieb:>>>>> Ich denke, das ist letztlich eine Frage der eigenen Präferenz. Und>>> jemand sagte mir mal: "C++ auf Mikrocontrollern tut man einfach nicht".>>>> Diese Aussage kommt regelmäßig von Leuten, die kein vertieftes Wissen>> über C++ und speziell ab c++14/17 haben. Dinge wie constexpr,>> variadische templates, ... und auch constraints / concepts (zugegeben,>> ist noch nicht im Standard, kann der g++ aber schon wunderbar).>>>> Aber die Diskussion dazu hier in diesem Forum ist sehr emotionsgeladen>> ... be warned!>> In C++14 kann man mit variadic Templates ganz problemlos einzelne> Io-Pins abbilden, die man zu PinSets zusammenbauen kann, die einen> Werte-Typ haben, die physisch auf verschiedenen Addressen liegen, z.B.> WGM13..WGM10 und denen man einem (erlaubten) Wert aus der zugehörigen> enumeie-Klasse zuweisen kann. Am Ende stehen Zugriffe auf die 2 Register> TCCR1A/B.> Aber: zu emotionsgeladen um es zu veröffentlichen.
Genau!
Und noch hinzu kommt mit dem g++ die Realisierung von concepts /
constraints (concepts-lite). Und das ist für die generische
Programmierung in C++ ein echter Hit wie ich finde: keine Angst mehr vor
template-code-Fehlermeldungen, und es ist Überladung aufgrund von
constraints möglich. Thats awesome ...
Aber posten werde ich hierzu in diesem Forum nichts mehr :-(
Hallo Wilhelm,
gern bin ich auch an deiner Umsetzung interessiert. Bitte auch etwas
mehr alls nur ein IO Beispiel. Wie wäre die Lösung zum Beispiel mit SPI
oder TWI bei mehreren gleichen Slve-Bausteinen.
Mit Templates habe ich selber noch nicht wirklich gearbeitet.
Vielelicht noch kurz einige Vorteile/Nachteile zu dem anderen Vorgehen.
Viele Grüße
Martin
Folgendes YT-Video zeigt m.M.n. ganz schön wie man mit C++ kleine uC's
effizient programmieren kann. Zwar ist das Zielsystem kein AVR oder ARM,
aber klein -- nach heutigen Maßstaben -- ist es schon :-)
https://www.youtube.com/watch?v=zBkNBP00wJE