Hallo alle zusammen,
ich bin gerade dabei mein persönliches Mikrocontroller-Projekt zu
schreiben und dabei ist mir zum ersten mal aufgefallen, dass viele
Hobby-Programmierer für die Daten wie zum Beispiel Analogwerte,
Messwerte, Regelungs- und Steuerungsparameter und Rechendaten einfach
kreuz und quer durch den Sourcecode liegen haben und auch die internen
(booleschen) Hilfsvariablen im Sourcecode verstreut sind und sogar
extern in anderen Objects verlinkt werden.
Nun ist meine Frage, wie machen es Profis, was für Datenstrukturen
nutzen sie, um ihre Daten mit verschiedenen Typen übersichtlich,
zentral, ganzheitlich und vor allem konsistent zu verwalten.
Ich meine eine Datenhaltung mit Default-Werten zur Initialisierung,
Ober- und Untergrenze, Speicherort im EEPROM, Getter und Setter,
Beschreibung mit einem Kommentar und auch Schnittstellen zur Anbindung
an UART-, CAN-, TCP-Interfaces und auch Zugriff vom Modbus-Stack.
Hat jemand da guten Open-Source C-Code zur Hand oder selbst was
programmiert?
Wie sind die "Good Practices" bei so einem SW-Modul? Man findet so wenig
darüber im Internet, weil das Thema nicht so ernst genommen wird und ich
auch noch keine Begriffe für so eine praktische Datenstruktur kenne.
Naja, das ist ganz einfach warum das so ist.
Die meisten Beispiele, die man im Netz findet, sind halt recht einfache
Programme. Wie solls auch anders sein, alles was komplex ist, dauert
Jahre bis man es verstanden hat (U-Boot, Linux etc.).
Das ist sozusagen ein Henne-Ei Problem. Für kleine Programme macht es
einfach wenig Sinn sich die tollsten Konstrukte zu überlegen und
aufzubauen, weil es letztlich einfache Programme bleiben. Und für
komplexe Programme gibt es keine universelle Aussage, wie man es dort
konkret machen kann/soll.
Grundsätzlich gilt allerdings (IMHO), dass die Daten dort erzeugt,
initialisiert und benutzt werden, wo sie gebraucht werden. Es ist nur
für sehr, sehr wenige Daten überhaupt notwendig, dass man die
physikalische Addresse kennt, wichtiger ist das man sich vernünftige
Linker sections überlegt und diese konsequent nutzt, dann braucht man da
nicht mehr viel händisch machen und hat das an einer definierten Stelle
(Linker-script).
Das liegt nur an zu vielem verfuegbarem Speicher.
in einem kleinen PIC kann man jedes Register noch persoenlich gruessen.
Selbst in einem C64 hatten prominente Speicheradressen ihr eigenes
Tuerschild.
Durch die schiere Masse ist das heute nur ein graues gesichtsloses
Etwas.
Das mache ich z.B. mit
typedef struct xyz
{
uint8_t x;
float y;
bool z;
} xyz_type_t;
(evtl. mit extern xyz_type_t my_struct_var;)
in einer Header-Datei.
Die kann man ja mit #include in die c. Datei einbinden und dort dann
eine Variable deklarieren und/oder definieren:
xyz_type_t my_struct_var;
Feste werte kann man u.a. in einer Init-Prozedur festlegen:
my_struct_var.x = <Wert>;
Dem lässt sich aushelfen, indem man auch aufm Mikrocontroller C++
verwendet und alles in Klassen abbildet. Static Variablen vermeiden.
Variablen die nur kurz benötigt werden dort erzeugen wo sie gebraucht
werden - hier jedoch keine endlos langen Funktionen erzeugen
(Zeilenlänge) und Variablen dann sogar am Anfang der Methode
deklarieren.
Das erhöht die Sichtbarkeit schonmal enorm. Und alle Variablen, die über
den Ablauf einer Methode / Funktion benötigt werden, erhalten einen
sinnvollen sprechenden Namen in der Klasse.
Das alles geht natürlich auch im klassischen C, ist dann nur etwas mehr
Tipparbeit.
Die einzige globale Variable wird dann auf der main.cpp erzeugt die so
aussehen könnte. Mehr steht in meinen main.cpp's in der Regel nicht
drin.
Hallo,
naja, dann sollte man aber auch die Microcontroller-Familie
dazuschreiben.
Auf einem AVR z.B. kann das schnell im Crash enden, weil der Ram
fragmetiert ist, der hat davon meist nicht allzuviel.
Gruß aus Berlin
Michael
Michael U. schrieb:> Auf einem AVR z.B. kann das schnell im Crash enden, weil der Ram> fragmetiert ist, der hat davon meist nicht allzuviel.
Falls du das auf Bezug von C++ nennst: Natürlich verwendet man kein new,
delete oder malloc. Keine virtuellen Methoden, kein std::map,
std::string, std::vector oder sonstige Monster der Standard Bibliothek.
Ich verwende es ausschließlich (in der Regel) um meinen Code zu
strukturieren und dafür ist es super geeignet. Dazukommt, dass die
meisten µC inzwischen mit C++ programmierbar sind (AVR, sämtliche
ARMs,...).
Achja, die Mainklasse beinhaltet natürlich als Membervariablen sämtliche
Peripherie als Objekte von Klassen abgeleitet. Alles wird sinnigerweise
über die Konstruktoren initialisiert.
Bei mir gibt es jeweils duzende Konfigurationswerte, welche eigentlich
fast alle als theoretisch aenderbar angenommen werden. Die stehen im
EEPROM, Zentral im Code aufgefuehrt als EEPROM content, funktionell
sortiert.
Nach diesen Variablen kann man dann auch in der source suchen.
Grad unten dran stehen die entsprechenden RAM Variablen. Selbe
Reihenfolge. Die werden bei powerup erst mal aus dem EEPROM uebernommen.
Dabei steht auch noch welche Commands auf welche Variablen lesen oder
schreiben.
> Nun ist meine Frage, wie machen es Profis, was für Datenstrukturen> nutzen sie, um ihre Daten mit verschiedenen Typen übersichtlich,> zentral, ganzheitlich und vor allem konsistent zu verwalten.
Sie haben dafuer Datenbanken, spezielle Software und koennen ganze
Abteilungen damit beschaeftigen. Die generiert dir dann automatisch
Teile deines Codes und die notwendigen Tools fuer Service und Diagnose.
Glaub mir, du willst gar nicht wissen wie viel Zeit man darin versenken
kann. :)
Olaf
Ich hasse das wilde Sammelsurium an verstreuten Variablen in den
diversen Quellcodedateien. In C gibt es doch Strukturen. Da kann man mit
einer globalen Struktur und weiteren Unterstukturen die Daten schön
anordnen. Jede Variable, die nicht nur temporär in einer Funktion
benutzt wird, ordne ich in einer Struktur an an.
Variablen die zusammengehören, gehören zusammen. Strukturen, gleicher
Ort, Namenbildungsregeln, etc.
Variablen, die nicht zusammen gehören, gehören nicht zusammen.
Variablen, die gleichartigen Zugriff benötigen, sind von gleicher
Struktur (nicht selbe, nur gleicher type), haben gleiche
Namensbestandteile und mechanismen. Aber natürlich lokal zum Modul, zu
dem sie gehören.
Zur Sichtbarkeit: natürlich so klein wie möglich. Ein Schleifenzähler i
im innersten Block. Es macht aber keinen Sinn, ein zusammengehöriges
flagfeld in 3 zu zerreißen, nur weil einige Global, andere lokal und
wieder andere woanders
Und mit den getter/Setter: es gibt so Junkies, die machen die überall
hin. Dann braucht man über Strukturen und Sinn aber auch nicht wirklich
zu diskutieren. Das sind religiöse Eiferer, die jeden steinigen, der
Jehova sagt.
Ben S. schrieb:> ...Membervariablen sämtliche> Peripherie als Objekte von Klassen abgeleitet.> Alles wird sinnigerweise> über die Konstruktoren initialisiert.
Überleg mal das als Template-Parameter zu machen.
Dann hat der Compiler den ganzen Aufwand es Flexibel zu halten, der
Optimizer kann seine Stärken entfalten, und am Schluss kommt ein
Compilat raus, was dann fast wie Hand-Optimierter ASM-Code aussieht.
Εrnst B. schrieb:> Überleg mal das als Template-Parameter zu machen.> Dann hat der Compiler den ganzen Aufwand es Flexibel zu halten, der> Optimizer kann seine Stärken entfalten, und am Schluss kommt ein> Compilat raus, was dann fast wie Hand-Optimierter ASM-Code aussieht.
Ich steh grad aufm Schlauch. Beispiel bitte!
Ben S. schrieb:> Ich steh grad aufm Schlauch. Beispiel bitte!
Stell dir vor, deine "Mainklasse" soll auch mit einem Soft-UART
funktionieren.
So:
Ben S. schrieb:> class Mainklasse{> private:> USART oUsart1;> };
geht das dann nicht mehr, du müsstest eine UART-Basisklasse/interface
verwenden, die Member als Pointer, Methodenaufruf virtuell mit
vtable-lookup usw.
Wenn du diese Flexibilität nur zur Compilezeit brauchst, dann kannst du
den vtable Overhead wegsparen, und deine UARTs als Template-Parameter
einsetzen.
aus "oUsart1.write(0x55);" könnte der Optimizer dann z.B. ein
ge-inlintes "UDR1=0x55;" oder eben ein "softuart_putchar(0x55);" machen.
Ben S. schrieb:> Εrnst B. schrieb:>> Überleg mal das als Template-Parameter zu machen.>> Dann hat der Compiler den ganzen Aufwand es Flexibel zu halten, der>> Optimizer kann seine Stärken entfalten, und am Schluss kommt ein>> Compilat raus, was dann fast wie Hand-Optimierter ASM-Code aussieht.>> Ich steh grad aufm Schlauch. Beispiel bitte!Ben S. schrieb:> Wie man Templates verwendet weiß ich.
Gut.
Ben S. schrieb:> Was soll man da besonderes sehen?
Besonderes?
Nix....
Nur das, wonach du gefragt hast.
Im Kompilat würde man sehen, dass der dortige Umgang mit den IOs in
wenige ASM Statements zerfällt.
Die Instanzen selber verschwinden.
Eben dank der Templates und der Optimierung.
Die Timer halten einen Status, und zerfallen von daher nicht so weit.
Εrnst B. schrieb:> Stell dir vor, deine "Mainklasse" soll auch mit einem Soft-UART> funktionieren.
Wer benutzt denn heutzutage noch eine Softusart :D.
Arduino Fanboy D. schrieb:> Im Kompilat würde man sehen, dass der dortige Umgang mit den IOs in> wenige ASM Statements zerfällt.
Was ja auch Sinn ergibt, oder? Wenn ich quasi für jeden GPIO mittels
Template eine eigene Klasse generiere, dann kriegen dessen Methoden
immer das gleiche gefüttert. Ich erstze quasi viele Variablen durch
Konstanten. Das ist doch der Grund, oder?
Ich finde es nur hchst bedauerlich, dass Templates nur als Headeronly
funktioniert. Das ruiniert die ganze Gestaltung der Programmierung.
> Wer benutzt denn heutzutage noch eine Softusart :D.
Nicht alle koennen 2 Euro fuer einen Arm mit 12 konfigurierbaren
Schnittstellen rauswerfen.
Es gibt Leute, fuer die ist alles groesser als ein ATTiny voellig
groessenwahnsinniger Overkill und sowieso viel zu teuer.
Die verwenden dann gerne hochoptimierende Compiler, welche aus 100
Zeilen C++ eine einzige ASM Zeile machen koennen.
Stefanus schrieb:> Wie sind die "Good Practices" bei so einem SW-Modul? Man findet so wenig> darüber im Internet, weil das Thema nicht so ernst genommen wird und ich> auch noch keine Begriffe für so eine praktische Datenstruktur kenne.
Da scheint die Frage: "wie räum' ich das schön auf" zu sein.
M.E. lautet die Antwort: gar nicht. Variablen gehören nicht "schön und
geordnet aufgeräumt", katalogisiert und ins Regal gestellt, sondern
immer so eng (lokal) wie möglich dahin geschrieben, wo sie benutzt
werden. Sie sind Teil des Algorithmus, deswegen gehören sie auch zu dem.
Wenn sie in einem Block benutzt werden, lokal zum Block
Wenn sie in einer Funktion benutzt werden, lokal zur Funktion.
Wenn sie in einem Modul benutzt werden, lokal zum Modul.
Wenn sie (ausnahmsweise) global benutzt werden, dann - und nur dann -
halt global
Wenn man in dieser Liste um eine Zeile nach unten muss, sollte man immer
einen sehr guten Grund dafür haben.
Ben S. schrieb:> Michael U. schrieb:>> Auf einem AVR z.B. kann das schnell im Crash enden, weil der Ram>> fragmetiert ist, der hat davon meist nicht allzuviel.
...
> private:> USART oUsart1;> USART oUsart2;> SPI oSPI1;> };>> Mainklasse::Mainklasse() : oSPI(SPI3), oUsart1(USART4), oUsart2(USART2)> {};
So ähnlich mache ich das auch. Wobei ein new bei mir erlaubt ist.
Manchmal, je nach Konfiguration, brauche ich eben diese oder jene
Objekte. Die werden dann beim Start erzeugt, aber nie wieder gelöscht.
Markus F. schrieb:> Wenn man in dieser Liste um eine Zeile nach unten muss, sollte man immer> einen sehr guten Grund dafür haben.
Dem stimme ich natürlich zu. Doch auch die Regel wird zuweilen
missbraucht, um Strukturen zu zerreißen. In global und static local.
Oder noch schlimmer: statt einer sinnvollen mehrstufigen globalen
Struktur mit 10 Elementen werden 30 setter/getter kreiert, um lokal und
konsistent zu bleiben.