Hallo,
ich bin bei weitem kein Profi in der objektorientierten Programmierung,
daher hoffe ich, dass mir jemand mit mehr Erfahrung auf die Sprünge
helfen kann.
Szenario: Ich habe einen Sensor (Lidar), der über UART kontinuierlich
einen Datenframe von 9 Byte, inkl. 2 Startbytes am Anfang sendet.
Dieser Sensor wird mehrfach verbaut und daher habe ich dafür eine Klasse
angelegt. Jedem der Sensoren wird ein Hardware USART Port zugeordnet
Nun könnte ich, getriggert von einem Timer, kontinuierlich den USART
Ringbuffer auf Länge testen und wenn diese größer als 9 Bytes ist, den
Header suchen und das Payload umwandeln. Ich möchte aber keinen Timer
dafür hergeben, sondern das ganze getriggert vom USART Interrupt
ausführen, den ich sowieso benutze um die Daten aus dem USART FIFO in
den Ringbuffer zu schreiben.
Mein Problem:
Ich kann im USART Interrupt Handler natürlich keine Memberfunktionen
ausführen, also habe ich eine gemeinsame Funktion "miniLidarRead()" für
alle Objekte der Klasse gemeinsam und ein Array mit Pointern auf alle
instanziierten Objekte als globale Variable. Jeder der Hardware USART
Interrupts führt diese Funktion aus und sendet die Nummer des Interrupts
mit. Anhand dieser Nummer bestimme ich das Objekt innerhalb des
Objekt-Arrays und kann dann die Funktionen auf Objektebene benutzen. Das
funktioniert so auch alles.
Meine Frage: Geht das irgendwie eleganter? Das ganze produziert
natürlich viel Overhead für relativ triviale Funktionen und die
Verständlichkeit wird sicherlich auch nicht unterstützt. Vielleicht gibt
es ja einen sehr viel besseren Weg, den ich noch nicht in Betracht
gezogen habe.
Die Klasse:
Usart_serial;// aufgesetzte Klasse auf den Hardware USART
13
uint8_t_frameArray[7];// Hält die 7 bytes Payload
14
uint16_t_distance;// Abstand
15
uint16_t_strength;// Signalstärke
16
uint8_t_objectNumber;// Nummer des instanziierten Lidar Objekts
17
friendvoidminiLidarRead(uint8_tobjectNumber);// Funktion die nach jedem USART Interrupt ausgeführt wird
18
friendvoidconvertData();
19
};
(Usart _serial ist lediglich eine aufgesetzte Klasse auf einen Hardware
USART mir integriertem RingBuffer und einfacherem Interface)
Und hier der Source-Code:
1
uint8_tactiveObjects=0;
2
MiniLidar*activeLidar[5];
3
4
MiniLidar::MiniLidar(){
5
_frameArray={0x00,0x00,0x00,0x00,0x00,0x00,0x00};
6
activeObjects++;
7
_objectNumber=activeObjects-1;// weise dem Objekt die nächste Nummer zu
8
activeLidar[_objectNumber]=this;// setze den Pointer von diesem Objekt in das Array
(Syntax-Fehler können noch vorhanden sein, ist eben hingeschrieben, es
geht mir eher um das Konzept)
Ich hoffe ich konnte umreißen was mein Problem ist, falls nicht kann ich
nochmal versuchen es etwas klarer darzustellen.
Vielen Dank!
Gruß
Dustin Lehmann schrieb:> Jeder der Hardware USART> Interrupts führt diese Funktion aus und sendet die Nummer des Interrupts> mit. Anhand dieser Nummer bestimme ich das Objekt innerhalb des> Objekt-Arrays [...]
Mir ist nicht ganz klar, wie du die Objektadressen aus der
Interruptroutine übergibst, bzw. wie die Interruptroutine aussieht. Die
Liste hört sich nicht so gut an, das deutet auf eine (aufwendige) Suche
durch die Liste hin. Wenn du jeder ISR bei der Initialisierung die
Objektadresse zuweisen kannst, dann würde sich die Liste ja erübrigen.
Deinem Lidar-Ojbekt kannst du noch eine Variable "LetzterInListe" geben,
welche am Ende von "miniLidarRead()" abgefragt wird und ggf. die
Verarbeitung aufruft.
So, jetzt eingeloggt.
Die Objektnummern sind momentan hartkodiert, an diesem Punkt hab ich
Stop gemacht und mich dazu entschieden nachzufragen wie man das
konzeptuell am Besten löst. Ich könnte natürlich alles hartkodieren in
meinem Projekt, dann wär das kein Problem, ich würds nur ganz gern
modular gestalten und Funktionen in anderen Projekten wiederbenutzen. An
die ISR kann ich ja leider nichts übergeben, ist ja ein void ISR(void).
Die Frage ist ja quasi:
Wie kann ich in einer ISR am Besten die Member eines Objektes
manipulieren, wenn das ganze reusable in einer Bibliothek geschrieben
werden soll?
Ja, OOP und Hardware-Verhalten passen nicht gut zusammen.
Was du brauchst ist ein Dispatching vom Interrupt auf die zuständige
UART-Klasse. Das sollte man im Interrupt-Handler machen. Alles was der
Interrupt-Handler machen sollte ist anhand des Interrupts die zuständige
Instanz suchen und eine Behandlungs-Methode aufrufen. Alles andere, wie
das Auslesen, die State-Machine, usw. gehören in die Klasse.
D.h.:
* Fast alles was du in miniLidarRead() hast gehört in eine (normale)
Methode.
* Für den Schönheitspreis:
- Der Interrupt-Handler, der das Dispatching auf eine Instanz macht,
wird eine static Methode der Klasse.
- Die Liste der Instanzen wird ein static Member der Klasse, keine
globale Variable.
- Eine static Methode initialisiert die UARTs, erzeugt die Instanzen,
füllt die Liste mit den Instanzen, initialisiert die Interrupts, usw.
Dustin L. schrieb:> reusable in einer Bibliothek geschrieben> werden soll?
Mist, da schaut man eine Minute nicht hin und schon werden Anforderungen
nachgeschoben. Dieses stückchenweise Verraten was du wirklich willst ist
scheiße.
Entschuldige bitte, ich dachte ich hätte das in meinen Eingangspost
geschrieben, war aber nicht so. Ich habe das momentan so:
Mein UART Interrupt Handler sieht wie folgt aus:
usart1RxFun() ist ein Pointer auf eine Funktion vom Typ
1
typedefvoid(*isrFun)(void)
. Diese sind globale Variablen in meiner USART-Klasse:
1
isrFunusart1RxFun=&NOP;
2
isrFunusart2RxFun=&NOP;
3
isrFunusart3RxFun=&NOP;
4
isrFunusart4RxFun=&NOP;
5
isrFunusart5RxFun=&NOP;
6
isrFunusart6RxFun=&NOP;
Dieser Pointer kann vom jeweiligen Objekt, abhängig vom angehängten
Hardware-USART verändert werden.
Für meine Lidar Objekte ist das ein Pointer auf die miniLidarRead()
Funktion. Die Übergabe der Objektnummer ist nicht komplett implementiert
gerade, ist aber machbar. Meine Frage ist ja eher ob es ein besseres
Konzept gibt, das in OOP umzusetzen, oder ob ich mich damit abfinden
muss, dass OOP, reusable und Hardware nicht wirklich zusammenpassen.
Gruß
Ohne jetzt dein Problem detailliert auseinander genommen zu haben, ich
mach das immer so in der Art (ob das jetzt ein Array ist oder nicht ist
egal):
1
MiniLidarlidar1(USART1);
2
MiniLidarlidar2(USART2);
3
4
extern"C"USART1_IRQHandler(){
5
lidar1.onUsartInterrupt();
6
}
7
8
extern"C"USART2_IRQHandler(){
9
lidar2.onUsartInterrupt();
10
}
So kann man in der onUsartInterrupt-Funktion ganz normal auf alle
Member-Variablen zugreifen. Der Funktions-Overhead wird ggf.
wegoptimiert. Die Zuordnung USART-Modul zu Klasse ist ja sowieso fix,
daher macht es nix in der ISR direkt das richtige Objekt zu nehmen. Ich
packe die ISR's und das Anlegen der Instanzen immer zusammen in die
main.cpp, da hat man dann einen Überblick was wie zusammenhängt. Alle
anderen Dateien sind unabhängig von konkreten Zuordnungen.
Dustin Lehmann schrieb:> void miniLidarRead(uint8_t objectNumber) {>> static uint8_t lastByte = 0x00;> static uint8_t currentByte = 0x00;> static uint8_t counter = 0;
Wenn man schon OOP macht, würde ich funktionslokale "static"-Variablen
vermeiden - versteckter Zustand.
Dustin Lehmann schrieb:> USART_TypeDef* _USARTx; // Hardware USART
Bezeichner die mit Unterstrich + Großbuchstabe anfangen, oder mit 2
Unterstrichen, sind in C und C++ der Standard-Bibliothek vorenthalten.
Ich würde daher auf Bezeichner die mit Unterstrich anfangen ganz
verzichten.
Dustin Lehmann schrieb:> Nun könnte ich, getriggert von einem Timer, kontinuierlich den USART> Ringbuffer auf Länge testen und wenn diese größer als 9 Bytes ist, den> Header suchen und das Payload umwandeln
Das geht eleganter: Die USART vom STM32 kann einen IDLE-Interrupt
auslösen, wenn gerade nichts übertragen wird. In diesem Interrupt kannst
du das DMA anweisen, 9 Bytes zu empfangen und in einen Puffer zu packen.
Somit hast du, wenn der DMA-Interrupt kommt, genau 1 Paket im Puffer und
musst keinen Anfang suchen. Das setzt natürlich vorraus, dass zwischen
den Paketen eine Pause ist. Der Ringpuffer ist dann auch hinfällig.
Du brauchst doch überhaupt keine Laufzeitpolymorphie. Es steht doch
bereits zur Compilerzeit fest, welche Uarts Du für welchen Zweck
verwendest. Wenn Du ein Template mit statischen membern verwendest,
kommst Du der Sache wahrscheinlich schnell viel näher:
Torsten R. schrieb:> template < USART_TypeDef* Uart, GPIO_TypeDef* TxPort, uint32_t TxPin,> GPIO_TypeDef* TxPort, uint32_t TxPin >
Man kann leider nicht USART1 und Co per Template übergeben, weil dessen
Definition einen reinterpret cast enthält, welcher somit keine constant
Expression ist. Man könnte höchstens einen uintptr_t übergeben und in
der Klasse casten. Oder einen Index übergeben und die Pointer in ein
globales Array packen und darüber auflösen.
Du solltest dich von akademischen Fürzen trennen und dich dem Problem
widmen.
Da das Projekt eh nie auf einen PC portiert werden wird, brauchst du die
Portabilitaet nicht.
Dynamische Objekte bringen keinen Vorteil hier, also lass sie weg.
Bei einem normalen Losungsansatz ist das Problem von selbst weg.
Auf der letzten Embo++ hat der Emil Fresk einen Vortrag gehalten. Dabei
hat er ein Task-System vorgestellt und am Ende aufgelöst, dass dieses
Tasksystem komplett in der Hardware eines Cortex-M läuft und "fast"
keine Unterstützung zur Laufzeit braucht:
https://github.com/korken89/crect
Das klingt sehr spannend, finde ich :-)
Dustin L. schrieb:> Die Frage ist ja quasi:>> Wie kann ich in einer ISR am Besten die Member eines Objektes> manipulieren, wenn das ganze reusable in einer Bibliothek geschrieben> werden soll?
Mit einem Callback der in etwa die Funktionalität von std::function
abdeckt. Durch dessen interne type erasure lässt sich dort alles
reinstopfn was callable is...
Eine UART hat in einer Lidar Klasse meiner Meinung nach überhaupt nichts
verloren, schon gar nicht dessen Interrupt.
Vincent H. schrieb:> Mit einem Callback der in etwa die Funktionalität von std::function> abdeckt. Durch dessen interne type erasure lässt sich dort alles> reinstopfn was callable is...
Warum? Ich würde mal behaupten, in 99% der Fälle ist bereits zur
Compile-Zeit bekannt, welche Funktion auf welchem Objekt aus dem ISR
heraus zu rufen ist.
Torsten R. schrieb:> Vincent H. schrieb:>>> Mit einem Callback der in etwa die Funktionalität von std::function>> abdeckt. Durch dessen interne type erasure lässt sich dort alles>> reinstopfn was callable is...>> Warum? Ich würde mal behaupten, in 99% der Fälle ist bereits zur> Compile-Zeit bekannt, welche Funktion auf welchem Objekt aus dem ISR> heraus zu rufen ist.
Weil da steht es soll eine Bibkiothek werden und die sollte imho mit
allem funktionieren was callable is.
std::function selbst erzeugt bei Übergabe eines Lambdas mit this capture
auf einem Cortex M4 ~24 Instruktionen bei -O2.
Will und braucht man wirklich das Minimum von 2 Instruktionen könnte man
den Callback natürlich auch als Pointer übergeben. Das wär dann wohl mal
ein Fall wo sich Template Class Type Deduction lohnt, damit man den
Pointer Typ nicht explicit übergeben muss.
Und ja, für all diese Überlegungen müsste man Lidar und UART trennen.
Torsten R. schrieb:> Das klingt sehr spannend, finde ich :-)
O ja, Zitat:
"crect (pronounced correct) is a C++ library for generating a scheduler
(at compile time) for Cortex-M series MCUs, which guarantees dead-lock
free and data-race free execution. It utilizes the Nested Vector
Interrupt Controller (NVIC) in Cortex-M processors to implement a Stack
Resource Policy (SRP) based scheduler. Thanks to the compile time
creation of the scheduler, the resource requirements at run-time are
minimal with:..."
Also thanks to the compile time_scheduler... Schönen Dank auch. Nee, das
klingt fast so grandios wie Dunkels Protothreads...
W.S.
Dustin Lehmann schrieb:> Nun könnte ich, getriggert von einem Timer, kontinuierlich den USART> Ringbuffer auf Länge testen und wenn diese größer als 9 Bytes ist, den> Header suchen und das Payload umwandeln. Ich möchte aber keinen Timer> dafür hergeben, ...
Nicht für jede Aufgabe, die innerhalb einer TimerISR ausgeführt wird,
brauchst du einen eigenen Timer. Da können durchaus mehrere Dinge
abgearbeitet werden.
W.S. schrieb:> Also thanks to the compile time_scheduler... Schönen Dank auch. Nee, das> klingt fast so grandios wie Dunkels Protothreads...
Also gar nicht ausprobiert oder mit irgendwas verglichen, aber erstmal
lästern? :)
Hallo,
danke für die vielen Antworten, ich habe jetzt auf Anregung hin den
USART aus der Klasse herausgenommen und fülle jetzt per DMA und
Idle-Line-Detection ein Array innerhalb der Klasse.
Generell habe ich mal versucht eine Memberfunktion über std::function
als Pointer zu übergeben um innerhalb einer Bibliothek die
Memberfunktion aufrufen zu können. Das funktioniert sogar, ist aber
leider 80kB groß im Flash, von daher nicht so praktikabel. Geht das auch
kleiner?
Weiterhin habe ich mal die Klasse "FunctionPointer" aus dem mBed-OS
eingebunden, damit funktioniert es ebenfalls und belegt kaum Platz im
Flash, der Nachteil ist natürlich dass es keine wirklichen Zeiger auf
Funktionen sind, sondern Objekte und ich habe bisher noch nicht bestimmt
wie viel Overhead das erzeugt und ob dadurch sehr viel Zeit verloren
geht.
Wie bestimmt man so etwas am Besten? ASM-Code ansehen? Einen Timer
laufen lassen und schauen wieviel Zeit vergangen ist?
Gruß
Dustin Lehmann schrieb:> Szenario: Ich habe einen Sensor (Lidar), der über UART kontinuierlich> einen Datenframe von 9 Byte, inkl. 2 Startbytes am Anfang sendet.
Gibt es zu dem Sensor eine Bezeichnung, Datenblatt,
Protokollbeschreibung?
Sendet der wirklich kontinuierlich oder ist zwischen den Paketen eine
Pause?
Hallo,
es ist dieser Sensor:
http://benewake.com/en/tfmini.html
Der Sensor sendet mit 100hz ein Datenpaket von 9 Bytes bei 115200 bit/s.
Es ist also zwischendurch genug Zeit für Verarbeitung.
Ich habe jetzt einen DMA eingerichtet, der nach jedem IDLE auf dem UART
getriggert wird, und dann 9 Bytes in ein Array schreibt. Beim DMA TC
Interrupt wird die Berechnung der physikalischen Werte ausgeführt.
Dustin L. schrieb:> Generell habe ich mal versucht eine Memberfunktion über std::function> als Pointer zu übergeben um innerhalb einer Bibliothek die> Memberfunktion aufrufen zu können. Das funktioniert sogar, ist aber> leider 80kB groß im Flash, von daher nicht so praktikabel. Geht das auch> kleiner?
Das wird wahrscheinlich daran liegen, dass std::function<> den heap
verwendet. Wenn Du einfach einen function pointer verwendest, dann kann
das nicht passieren. Ich würde aber einfach auf jede Form von runtime
dispatching verzichten.
Dustin L. schrieb:> der Nachteil ist natürlich dass es keine wirklichen Zeiger auf> Funktionen sind, sondern Objekte und ich habe bisher noch nicht bestimmt> wie viel Overhead das erzeugt und ob dadurch sehr viel Zeit verloren> geht.> Wie bestimmt man so etwas am Besten? ASM-Code ansehen? Einen Timer> laufen lassen und schauen wieviel Zeit vergangen ist?
Wenn du das nicht mit mehreren hundert kHz aufrufst, vergiss den
Overhead.
Weiter eine allgemeine Empfehlung: Wann immer du merkst, dass dich die
Struktur deines Programms behindert, mach einen Prototypen der sich
einige Dinge einfach festlegt. Werde konkret. Nur so kannst du die
Erkenntnisse sammeln, für dir später erlauben die Struktur so anzupassen
wie es für dich passt.
Keep it simple.
Dustin L. schrieb:> Generell habe ich mal versucht eine Memberfunktion über std::function> als Pointer zu übergeben um innerhalb einer Bibliothek die> Memberfunktion aufrufen zu können. Das funktioniert sogar, ist aber> leider 80kB groß im Flash, von daher nicht so praktikabel. Geht das auch> kleiner?Torsten R. schrieb:> Das wird wahrscheinlich daran liegen, dass std::function<> den heap> verwendet. Wenn Du einfach einen function pointer verwendest, dann kann> das nicht passieren. Ich würde aber einfach auf jede Form von runtime> dispatching verzichten.
std::function verwendet eine small buffer optimization (im libstdc++
Fall glaub ich 8 Bytes). Selbst wenn man "vanilla" std::function
verwendet läuft man bei Interrupts quasi nie Gefahr auf Heap zugreifen
zu müssen. Trotzdem würde ich eher zur Nutzung von "inplace_function"
raten, einem Proposal zu einer "Heap"-freien std::function, die nie
allokieren darf. Da gibts wohl die ein oder andere Implementierung auf
github.
Die 80kB klingen eher nach <iostream>...
Ich hab grad eben ausprobiert wie viel Platz vanilla std::function für
den Aufruf eines "Member Interrupts" braucht, sprich einer Member
Funktion die maximal 1x Parameter schluckt.
Fazit:
-Og Programm wurde um 96B größer
-Os Programm wurde um 24B größer
Und das ist jetzt der Vergleich OHNE Interrupt <-> mit Interrupt wohl
gemerkt, nicht verglichen zu einem Raw Pointer oder ähnlichem...
im Interrupt-Handler wird mein Code etwa 70kB größer, lasse ich den
Aufruf weg bleibt das Programm klein. Mache ich was falsch? Wenn ich
zuhause bin, kann ich mal ein kompilierbares Minimalbeispiel erstellen,
meine Bibliotheksfunktionen zum Interruptpin habe ich gerade nicht hier.
Gruß
Hm, keine Ahnung. ;)
Das reine Anlegen der Klasse und der bind erzeugt auch kaum Code...
Kleiner Tip am Rande, std::bind lässt sich quasi immer durch Lambdas
substituieren. Das hat bezüglich Code-Größe und Geschwindigkeit nur
Vorteile.
Ich benutze Eclipse CDT mit AC6 Plugin für STM32, kompiliert mit GCC.
Kann ich irgendwas an den Einstellungen ändern damit das nicht so
exorbitant groß ist? Oder sollte ich das Konstrukt lieber vergessen?
Gruß
Dustin L. schrieb:> Oder sollte ich das Konstrukt lieber vergessen?
Vergiss std::function<> einfach, wenn Du keinen heap verwendest. Selbst
wenn es small object optimizations gibt: es gibt Dir keiner eine
Garantie, dass dein Compiler so eine Optimierte Library mitbringt und
auch keine Garantie, wie groß small ist.
Wenn es nun unbedingt ein Binding zu Laufzeit sein muss, dann kannst Du
Dir eine Art std::function<> selbst bauen (so in etwa):