Hallo zusammen,
ihr kennt das sicher auch. Man hat plötzlich viele Funktionen, die
praktisch das gleiche machen.
Fangen wir die Geschichte mit einem LCD an. Die Adafruit-Library gefällt
mir nicht, also schreibe ich mir meine eigenen Ansteuerungs-Funktionen.
Auf der untersten Ebene ist das:
Die Displays sind gut, aber die 8-Bit-Ansteuerung nervt. Aber endlich
kommt auch die SPI-Variante mit der Post. Also entsteht die SPI-Variante
(als Bitbanging-Version, weil gerade kein Hardware-SPI frei ist):
Das langsame Bitbanging nervt, also werden auf dem Eval-Board Drähte
durchgetauscht, bis das Display an einem Hardware-SPI seinen Platz
findet. Folge sind wieder sieben neue Funktionen:
Das sind ganz schön viele Funktionen, die das Gleiche machen - und ich
benötige jeweils nur eine. Wie gehe ich weiter vor?
*A: "Linker for the win"*
Die Funktionen mit gleicher Funktionalität für unterschiedliche
Ziel-Hardware heißen gar nicht unterschiedlich, sondern gleich. Damit
liegt es am Linker, jeweils für das richtige Target die richtige Datei
zu linken.
Dass irgendwer einmal den Quelltextdateihaufen in einer anderen
Entwicklungsumgebung bauen will, ist unwahrscheinlich oder interessiert
nicht.
Suchfunktionen in der Entwicklungsumgebung werden auch eher nicht
genutzt.
*B: "Preprocessor - this is some serious empty translation unit"*
Wie in A heißen die Funktionen gar nicht unterschiedlich, sondern
gleich. In den Dateien wird eine Konfigurationsdatei "included" und
Präprozessor-Makro-Vergleiche klammern die ungewünschten Code-Teile aus.
*C: "Der Präprozessor trägt die frohe Kunde hinaus"*
In einer Header-Datei werden die Zeichenketten in den aufrufenden
Funktionen umbenannt:
Mit Präprozessor-Makros kann dafür gesorgt werden, dass als
Quelltext-Dateien auch für alle Targets kompilieren und zumindest eine
anspringbare Dummy-Funktion liefern. Das trifft auch auf die folgenden
Varianten zu.
*D: "Nicht einmal der Präprozessor weiss mehr, wie Du heißt"*
In einer Header-Datei werden die Zeichenketten der aufzurufenden
Funktionen umbenannt:
Mit einer vernünftigen Gaming-Maus kann man so einen Header auch schnell
durchscrollen.
*F: "The Switcher Wrapper"*
In einer Header-Datei werden die aufzurufenden Funktionen gewrappt:
Auch hier kann man mit einer vernünftigen Gaming-Maus so einen Header
auch schnell durchscrollen.
*G: "Vtable: I'll make my own C++ with blackjack and hookers"*
Es wird ein struct mit Funktionszeigern auf die entsprechende Funktion
angelegt und von den
aufrufenden Funktionen ausschließlich dieser benutzt.
Ein gewisser Laufzeit-Overhead ist vorhanden, aber dafür könnte man das
Display sogar Hot-Plug-fähig gestalten.
*H: "Das Problem habe ich gar nicht."
*Welche Variante bevorzugst Du in Deinen Projekten?*
Displayinterface Treiber aufsplitten auf mehrere Dateien
(tft_soft_spi.c, tft_hard_spi.c, tft_8bit.c) und nur die zur Hardware
passende Datei im Projekt haben?
Wie oft möchtest du das Interface denn innerhalb des Projekts wechseln?
Flo schrieb:> Wie oft möchtest du das Interface denn innerhalb des Projekts wechseln?
Normalerweise vier, fünf mal am Tag.
Ich habe momentan 6 Targets:
- Hardware-Revision 0.0 (Debug) -> 8bit-LCD
- PC-Mockup -> LCD-Emulation
- Modultests auf dem PC -> kein LCD, Konsolenausgabe
- Modultests auf dem Target -> kein LCD, UART-Ausgabe
- Hardware-Revision 0.1 (Debug) -> SPI-LCD
- Hardware-Revision 0.1 (Release)-> SPI-LCD
Flo schrieb:> und nur die zur Hardware> passende Datei im Projekt haben?
Okay, das wäre eine neue Kategorie
H: "Library und viele Unterprojekte im Versionskontrollsystem", wenn ich
das richtig verstehe?
> Die Adafruit-Library gefällt mir nicht,
Du solltest dir die Arduino-Libraries aber trozdem als Beispiel nehmen.
Dort kann man alle möglichen MCUs auswählen und der richtige Treiber
wird kompiliert.
Bei den TFT Libraries lässt sich der Hardwaretreiber durch die Wahl der
Schnittstellenklasse einstellen.
G. Und natürlich setzt jede Implementierung die Funktionszeiger selber.
Die Auswahl erfolgt durch die entsprechende init-Funktion.
Habe ich schon vor 20 Jahren bei dem PIC18 so gemacht.
Segger machte das bei seinem emWin auch ohne struct: eine Codezeile ist
identisch, wenn sie eine Funktion oder einen funktionsptr aufruft.
A. S. schrieb:> G
Hast Du einen Codeschnipsel, wie das bei Dir aussieht? Bei mir sah der
Versuch recht wild aus mit den ganzen unterschiedlichen typedefs, um die
verschiedenen Parameterlisten abbilden zu können. Mit nackten
void-Zeigern hebele ich ja Warnungen zu den Funktionsparametern komplett
aus.
Walter T. schrieb:> Es wird ein struct mit Funktionszeigern auf die entsprechende Funktion> angelegt und von den> aufrufenden Funktionen ausschließlich dieser benutzt.
Warum nicht einfach eine Klasse pro Variante? ggf. mit virtuellen
Funktionen? Oder schlicht einem Typalias (typedef), welches die zu
nutzende Klasse festlegt. Dann kann man sogar leicht zwischen
Zur-Laufzeit-Umschalten und Beim-Kompilieren-Entscheiden wechseln.
Bei geringfügigen Unterschieden kann man der Klasse im Konstruktor auch
eine Konfiguration mitgeben (Pin-Nummer o.ä.). Außerdem kann man
Overloading nutzen, sodass die Funktionen alle "put" heißen und
verschiedene Datentypen ausgeben können.
Walter T. schrieb:> *Welche Variante bevorzugst Du in Deinen Projekten?*
Auf jeden Fall die C++-Variante... Die anderen sind nur fiese Krücken um
die Lücken von C auszugleichen.
// Das Gleiche nochmal für DisplayHardSPI, DisplaySDL ...
32
33
// Alias definieren, z.B. so um fix immer DisplaySoftSPI zu nutzen
34
35
usingMyDisplay=DisplaySoftSPI;
36
37
// Oder so, um zur Laufzeit umschalten zu können
38
39
usingMyDisplay=Display;
40
41
42
// Eine Funktion, die etwas mit einem Display macht
43
voiddoit(MyDisplay&disp){
44
}
45
46
intmain(){
47
// Instanz anlegen
48
MyDisplaydisp(...);
49
50
// Etwas damit machen
51
doit(disp);
52
}
Spart die Fummelei mit Funktionszeigern. Je nachdem, wie man MyDisplay
definiert, wird eine der Schnittstellen genutzt, oder eben
Laufzeit-Umschaltung ermöglicht. Der Trick daran ist, dass bei der
Nutzung von "using MyDisplay = DisplaySoftSPI" o.ä. der
Laufzeit-Overhead des Umschaltens entfällt, weil die Klasse "final" ist
und die Aufrufe somit devirtualisiert werden, d.h. die vtables und
indirekten Aufrufe verschwinden. Somit ist es gleichzeitig flexibel und
immer so effizient wie möglich. Und ganz nebenbei kann man so mehrere
Displays mit unterschiedlichen(!) Interfaces in einem Projekt
unterbringen (viel Spaß das mit den C-Varianten zu machen).
Walter T. schrieb:> Hast Du einen Codeschnipsel
Ich habe das nochmal probiert, und so wild sieht es wirklich nicht aus.
Eigentlich sieht es sogar am wenigsten schlimm von allen Varianten aus.
Der Compiler warnt zwar nicht, wenn die Konstante nicht zugewiesen wird
und rennt dann zur Laufzeit unweigerlich in einen Nullzeiger-Fehler,
aber dadurch, dass es eine Konstante ist, gibt es auch nur eine Stelle,
wo die Initialisierung liegen kann.
Walter T. schrieb:> /* .h */> extern void (*const tft_init)(void);> extern exit_t (*const tft_put)(uint8_t, uint16_t, size_t);
das baut C++ ansatzweise nach, ohne die Typsicherheit, Optimierungen und
Erweiterungen die C++ bietet.
Da ist selbst die AdafruitGFX deutlich besser. Was machst du wenn mal
zwei oder mehr Displays angesteuert werden sollen? Bei den kleinen OLED
oder z.B. runden Displays durchaus wünschenswert. Dann nimmt man in C
eine Struktur von Funktionszeigern und ist endgültig bei C++ vTables.
Walter T. schrieb:> Ich habe das nochmal probiert, und so wild sieht es wirklich nicht aus.
Nachtrag: Und EmBitz findet den Funktionszeiger nicht als Funktion, so
dass man die IDE-Suchfunktionen und Auto-Vervollständigen nicht nutzen
kann.
Johannes S. schrieb:> das baut C++ ansatzweise nach, ohne die Typsicherheit, Optimierungen und> Erweiterungen die C++ bietet.
Meine selbst auferlegte Regel: Ändere nie in der Mitte oder kurz vor
Ende eines Projekts die Programmiersprache. Eine neue Programmiersprache
fängt man am Anfang eines Mini-Projekts an.
Ich mache normalerweise eine Variation von G. Einfach mit
-ffunction-sections -fdata-sections -Wl,--gc-sections kompilieren, der
compiler wird das ungenutzte dann schon raus schmeissen. Ich mache das
immer wieder etwas anders. Ein Beispiel wäre:
interface.h
1
#define INTERFACE_HELPER(T,RET,NAME,PARAMS) RET (*NAME) PARAMS;
Manchmal schreibe ich das auch ohne die macros aus. Neulich habe ich
angefangen, statt Makros zum sparen der Schreibarbeit einen eigenen
kleinen Parser / präprozessor zu nehmen.
https://onlinegdb.com/oH-hhxN5k
Walter T. schrieb:> ihr kennt das sicher auch. Man hat plötzlich viele Funktionen, die> praktisch das gleiche machen.
Nein, ein derartig verwickeltes Interface kenne ich aus meiner Praxis
nicht. Aber das ist eine Planungs-Angelegenheit. Bei mir gibt es eine .h
für den/die diversen Lowlevel-Treiber und deren Innereien gehen alle
darüber angeordneten Schichten nichts an.
Du hingegen versuchst, alle dir nur einfallenden Ansteuer-Versionen
(parallel, SPI, SPI+DMA usw.) mit jeweils eigenen Namen zu führen, was
letztlich zu einem Gewirr bei der/den Headerdateien .h führt. Und das,
obwohl es dem eigentlichen Displaytreiber egal zu sein hat, wie seine
Erinstellwerte für den Displaycontroller zum Display geschaufelt werden
sollen.
W.S.
Ich habe das mal so bei einer seriellen Schnittstelle gemacht:
serial.h listet alle Funktionen auf, die man "von außen" benutzen darf.
serial.c implementiert einige davon und enthält am Ende dies:
1
#if defined(UDR) || defined(UDR0)
2
#include"serial_atmega.c"
3
#else
4
#include"serial_xmega.c"
5
#endif
In diesen beiden Dateien sind die restlichen hardwarespezifischen
Varianten implementiert.
Du kannst dazu ja auch ein eigenes "define" verwenden, welches du ins
Makefile schreibst, bzw. in die Projektkonfiguration.
W.S. schrieb:> Und das,> obwohl es dem eigentlichen Displaytreiber egal zu sein hat, wie seine> Erinstellwerte für den Displaycontroller zum Display geschaufelt werden
Kommt drauf an - das klappt nicht, wenn das Übertragungsprotokoll von
der darunter liegenden Schicht abhängig ist. Selbst im OSI-Modell ist
das so - die oberen Schichten müssen wissen, auf welcher Schicht sie
aufsetzen, und sich dementsprechend verhalten. Wenn z.B. IPv4 auf
Ethernet eingesetzt wird, muss IPv4 das ARP benutzen.
Daniel A. schrieb:> Neulich habe ich> angefangen, statt Makros zum sparen der Schreibarbeit einen eigenen> kleinen Parser / präprozessor zu nehmen.
Typisch C - eine eingeschränkte Sprache verwenden und mit eigenen
Präprozessoren aufbohren, statt eine Sprache zu nutzen die solche
Probleme nicht hat...
Programmierer schrieb:> Typisch C
Nö, das ist nicht typisch "irgendeine Programmiersprache", sondern
typisch nicht genug geplant, bevor mit dem Quellcodeschreiben angefangen
wurde.
So herum.
W.S.
Wie würdest Du das denn sinnvollerweise perfekt vorab geplant angehen?
Ich kann mir gerade nicht wirklich vorstellen, wie es sinnvoll gehen
soll, alles in eine Funktion zu bringen, die wahlweise an µC-Pins
wackelt, eine SPI-Peripherie oder eine Windows-Library aufruft.
Die frage ist wer entwickelt / pflegt software für verschiedene
entwicklungssstände einer HW. vor allem wenn sie so wie es hier sich
anhört um HW protypen handelt? Die wenigsten. Ein Alter protyp wird
nicht mehr mit neuer SW versogt. (oder nur für einen begrenzten
zeitraum.) ist einfach zu viel aufwand.
Dein PC code hört sich für mich wie testing an. hat damit eigentlich
nichts im Produktiv code zu suchen. Ja er verwendet die sorcen daraus,
... Ja da sind code schnitstellen die sich nicht so einfach ändern
dürfen, ... aber die mocups für nicht vorhandene HW und die umschaltung
sollten für mich nicht im produktiv code rumfliegen.
Sicher es gibt leute die müssen aufgrund von Bauteil abkündigungen,
preis ersparnissen, ... die gleiche SW auf verschiedene HW bringen, ...
die haben dann aber auch ein passendes Varianten management, ... und
verwalten z.B. die varianten dann auch getrennt im git und mergen die
änderungen von a nach b bzw wieder zurück.
ich hab das vor einiger Zeit wie folgt gelößt:
Auf unterster Ebene hab ich ein SPI Treiber, von dem gibt es mehrer
Varianten
zum Einen HW SPI (2 mal weil ich 2 HW SPI Schnittstellen habe) und
Software SPI. Darüber liegt die Schicht mit der log Ansteuerung des /
der Displays
Darüber dann der Ausgabe Layer (Line, Char, Circle ...)
Kommt ein neues Display muss halt der HW Treiber neu gemacht werden (I2C
oder par). Dann ev noch die Display Ebene und fertig.
Ich gehe da mit W.S. konform. Deine App kommt nur mit dem Ausgabe Layer
in Kontakt der im Idealfall immer gleich ist. Ganz komfortabel ist es
dann das ganze in Libs bereitzuhalten die nur noch zum Code gelinkt
werden müssen.
123 schrieb:> Dein PC code hört sich für mich wie testing an. hat damit eigentlich> nichts im Produktiv code zu suchen.
Den schönen Test-Code, der der einzige Grund ist, warum ich mich traue,
alte Klamotten überhaupt noch einmal anzufassen entfernen? Gefällt mir
gar nicht. Da ich kein hauptberuflicher Software-Entwickler bin, ich für
mich jeder Stand ein Entwicklungsstand.
123 schrieb:> ein passendes Varianten management, ... und> verwalten z.B. die varianten dann auch getrennt im git und mergen die> änderungen von a nach b bzw wieder zurück.
Okay, das ist Variantenmanagement auf Dateiebene. Passt grob unter "H"
oder wenn man die Versions-/Variantenverwaltung als Teil des
Build-Systems sieht unter "A".
Thomas Z. schrieb:> Deine App kommt nur mit dem Ausgabe Layer> in Kontakt der im Idealfall immer gleich ist.
Die "App" oder der Rest der Firmware: Ja. Aber hier geht es ja gerade um
den "glue layer" (ich habe kein besseres Wort dafür).
Thomas Z. schrieb:> Ganz komfortabel ist es> dann das ganze in Libs bereitzuhalten die nur noch zum Code gelinkt> werden müssen.
Also machst Du Variante A.
Walter T. schrieb:> Also machst Du Variante A.
naja um ehrlich zu sein es gibt wohl immer auch ein paar Zwitter
Lösungen. Das mit den Libs ist ja auch Verwaltungsaufwand. Für ein
simples char LCD lohnt sich das natürlich nicht. Ich hab mir aber schon
einiges von kommerziell erhältlichen FW Bibliotheken abgeschaut.
In manchen Fällen hatte ich auch schon mal Mokups auf dem PC zu
Demozwecken programmiert um ein Gefühl für das Look & Feel zu bekommen.
Das waren dann aber immer größere Projekte wo ich nur einen (kleinen)
Teil beisteuerte.
Walter T. schrieb:> a. Aber hier geht es ja gerade um> den "glue layer" (ich habe kein besseres Wort dafür).
In der C++ Welt könnte "glue layer" eine Implementierung des "Bridge"-
oder "Adapter Design Pattern" sein.
Evtl. kannst dir da ja was (zumindest die Idee) abschauen.....
Ich habe ein ähnliches Problem (allerdings nicht embedded) mit A gelöst
und cmake den schwierigen Teil überlassen. Dass die IDE beim Klick auf
die Funktion nicht in der gewollten Implementierung landet, lässt sich
wohl nicht vermeiden.
Mini-Beispiel im Anhang, die Library-Varianten würde man
sinnnvollerweise auf mehr Verzeichnisse verteilen und vielleicht auch
nicht alles auf einmal bauen.