Hi,
wie abstrahiert ihr von eurer Mikrocontrollerumgebung wenn ihr
Chiptreiber schreibt? Z.B. für einen externen SPI ADC.
Ich habe hier 3 Beispiele geschrieben: https://godbolt.org/z/PEKv9a
Variante 1: Hardwareimplementierung durch abstrakte Methoden und
Vererbung
Variante 2: dependency injection von selbst bereitgestelltem Interface
Variante 3: dependency injection von Hersteller HAL
Variante 3 habe ich bisher benutzt und will davon weg, weil es schlechte
Abhängigkeiten reinzieht und nur schlecht auf einem PC testbar ist. Der
Vorteil ist dass man idR. weniger Code schreiben muss.
Variante 3b wäre es, seine eigene HAL zu schreiben und diese zu
injizieren...
Finde die Frage sehr gut und wichtig, keine Ahnung was hier abgeht.
Tatsächlich überlege ich im Moment, Variante 3 nachzubauen, aber in C,
dann eben mit Funktionszeigern.
Denn: wir kommen mit dem Schreiben unseres HAL nicht mehr hinterher. Und
auch, wenn es "nur" Fingerübungen sind, so baut man dort Bugs ein und
muss alles testen.
Ideal wäre eigentlich ein für ISO26262 o.ä. zertifzierter HAL vom
Hersteller, der vernünftig von der eigentlichen Business Logik, also dem
wertschaffenden Teil (IP) getrennt wird.
Die Frage ist: Wie macht man das sinnvoll? Entweder per Injection, da
kenne ich in C aber im Prinzip nur function pointer oder mit einer Makro
Orgie, auch nicht besonders sexy.
Kennt jemand guten Embedded Code für die genannte Controller Klasse
(CM3/CM4) in C oder C++, der einen Austausch des HAL möglich macht?
Mir ist schon klar, dass ein HAL eigentlich den Austausch der Hardware
ermöglichen soll, aber wenn man fest gegen das API des HAL programmiert,
bleibt man am Ende ja doch bei dem HAL des selben Herstellers hängen.
Jan K. schrieb:> Mir ist schon klar, dass ein HAL eigentlich den Austausch der Hardware> ermöglichen soll, aber wenn man fest gegen das API des HAL programmiert,> bleibt man am Ende ja doch bei dem HAL des selben Herstellers hängen.
Genau DAS ist ja auch vom Hersteller bezweckt. Aber wenn man sich sein
eigenes Portfolio an Treiberinterfaces erarbeitet hat, dann ist man von
so etwas unabhängig.
Das geht aber nicht für alles, denn Spezielles, was der eine µC bietet
hat der andere eben nicht und wenn man genau das verwendet, dann ist an
dieser Stelle keine Portabiität gegeben.
W.S.
W.S. schrieb:> Genau DAS ist ja auch vom Hersteller bezweckt.
Verstehe ich.
W.S. schrieb:> Aber wenn man sich sein> eigenes Portfolio an Treiberinterfaces erarbeitet hat, dann ist man von> so etwas unabhängig.
Verstehe ich auch, mein Chef aber nur begrenzt. Hab ich ja oben schon
versucht anzudeuten, wir kommen nicht hinterher. Ein anderer Kollege
(anderes Team, Einzelkämpfer) kommt jetzt mit CubeMX an und freut sich,
wie schnell er eine funktionierende Lösung zusammengebaut hat und nur
noch Integrationstest machen muss.
Ideal wäre doch beides - ein leichtgewichtiger (wie auch immer man das
konkret umsetzt) Abstraktionslayer, der wirklich universell ist und
gegen den in der Business Logik programmiert wird.
In etwa sowas, wie die CMSIS-Driver/CMSIS-RTOS2 von ARM/Keil. Nur in
besser :D
Die große Frage ist dann nur: Wie baut man eben nicht-universelle
Sonderfunktionen ein, die ja häufig auch Hardware features nutzen.
Ich schreibe auf ARM inzwischen nur noch in C++ und generischen Hardware
Klassen. Beim Portieren brauche ich diese dann nur noch im Quellcode
anzupassen, sonst nichts. Das Ziel ist es, sämtliche Hardwarezugriffe
aus dem eigentichen Programm fernzuhalten und durch sprechende Aufrufe
zu ersetzen.
Wird beispielweise eine USART verwendet, dann wird davon ein Objekt
erzeugt:
1
2
USARToUSART(USART2,9600);
3
oUSART.send(0xff);
Was dahinter passiert interessiert mich ab diesem Punkt nicht mehr. Es
ist aber selber in C++ geschrieben, quasi meine eigenen generische HAL.
Muss ich das portieren, versuche ich die Methoden und deren Parameter
nicht zu verändern.
In diesen Klassen nutze ich nur CMSIS mit direkten Registerzugriff. HAL
und sogar LL nutze ich nicht.
W.S. schrieb:> Genau DAS ist ja auch vom Hersteller bezweckt. Aber wenn man sich sein> eigenes Portfolio an Treiberinterfaces erarbeitet hat, dann ist man von> so etwas unabhängig.
Gerade bei den STM32 ist ein Wechsel doch so einfach und macht kaum
Arbeit. Beispielsweise ist fast durch die gesamte Reihe weg bis auf
wenige Ausnahmen der Zugriff auf die GPIOs gleich.
@BenS
Jetzt noch mit Interfaces arbeiten und die Klassen die einen UART nutzen
bekommen eine Referenz darauf.
UartSTM32 implementiert dann das Interface.
Schon ist die Anwendung zwischen den µC austauschbar.
Bzw selbst in der STM32 Serie gibts ja schhon verschiedenene Uarts.
Grobes Beispiel:
1
classIUart{
2
virtualWrite(constuint8_t*buf,uint32_tlen)=0;
3
};
4
5
classStm32Uart32Bit:publicIUart{
6
virtualWrite(constuint8_t*buf,uint32_tlen);
7
};
8
9
indenVariablenmitsnenstaticinstanceis:
10
Stm32Uart32Bituart2;
11
uart2.Init(UART2,9600);// UART2 ist der Pointer der UART2 Register aus dem CMSIS
12
13
classNeedsUartkekse;
14
kekse.Init(uart2);
Warum man das jetzt mit Injections machen muss leuchtet mir nicht ein.
Welche Argumente gibts denn dafür?
Jan K. schrieb:> Finde die Frage sehr gut und wichtig, keine Ahnung was hier abgeht.
moby geht ab und spamt das Forum voll wie geil er asm findet. Da er
Hausverbot hat sind die posts ganz schnell wieder weg.
Mit vituellen Methoden würde ich auf uC aufpassen, weil die doch einiges
an Overhead erzeugen. Da sind templates/compile-time Polymorphie ggf.
besser geeignet.
At run-time Implementierungen gegeneinander austauschen braucht man da
sowieso nicht.
Mw E. schrieb:> Jetzt noch mit Interfaces arbeiten und die Klassen die einen UART nutzen> bekommen eine Referenz darauf.
Referenz mache ich natürlich, Referenzen sind Pointern ebenso in nahezu
allen Belangen vorzuziehen. Interfaces nutze ich nicht, da ich zu selten
portiere und den Kram dann eh kaum anpassen muss - also kopieren,
Klassennamen umbenennen, Quellcode ändern, fertig und passt scho. Wer
jedoch viel portieren muss (z.B. beruflich), für den kann das ganz
praktisch sein.
Ich finde dass die Frage nicht so generell zu beantworten ist sondern
von der Art des Produkts, der produzierten Stückzahl und vor allem der
Langlebigkeit abhängt.
Für eine 10-Stück Kleinserie wo sich die Hardware wohl nie wieder ändert
ist es schwieriger den Aufwand einer eigenen HAL zu rechtfertigen als
für eine 200k-Serie die verschiedene Sub-Typen bekommt, eventuell
verschiedene Prozessoren nutzen muss und die nächsten 15 Jahre lebt.
Dependency Injection nutze ich für Hardware-Bibliotheken eigentlich
nicht. Ich sehe Hardware eher als was globales an und nutze einen
Config-Header mit using declarations
(https://en.cppreference.com/w/cpp/language/using_declaration) um meine
Peripherie-Klassen der Applikation zuzuordnen.
Z.b.
1
namespacememory{
2
3
usingEepromI2c=mylib::I2c<3>;
4
5
}
Und irgendwo im Code dann statische Funktion der peripherie Klasse:
Al Fine schrieb:> Mit vituellen Methoden würde ich auf uC aufpassen, weil die doch einiges> an Overhead erzeugen. Da sind templates/compile-time Polymorphie ggf.> besser geeignet.
Da stecke ich jetzt nicht tief genug in der Materie. Aber sollte der
Compiler nicht erkennen, dass ich da einfach eine dynamische Klasse
erstelle? Warum sollte der beim USART Beispiel Overhead erzeugen und
welchen?
Auf mikrocontrollern, wo bei meinen bisherigen Projekten alles statisch
war, arbeite ich mit Dependency Injection. Da ändert sich ja zur
Laufzeit nie etwas. Erst ein USART Objekt erstellen und anschließend
jedes Objekt, das eine USART benötigt, wird initialisiert mit einer
Referenz ein auf ein (dieses) USART Objekt. Ein Initialisieren ohne
Referenz ist nicht möglich.
Das ist aber mein Weg, den ich gut und klar finde.
Ihr könntet euch mal im Bereich Autosar umschauen, wie das da machen,
dort nennen die das MCAL (Microcontroller Abstraction Layer).
Die Spezifikationen sind alle frei zugänglich. Vielleicht lässt sich da
etwas abschauen. ISO26262 wird dort sicherlich auch behandelt.
Bin leider noch nicht dazu gekommen, in das Thema etwas tiefer
einzusteigen.
Ben S. schrieb:> Wird beispielweise eine USART verwendet, dann wird davon ein Objekt> erzeugt:> USART oUSART(USART2, 9600);> oUSART.send(0xff);
Meine Erfahrung ist, dass diese Schnittstelle nicht die Hardware
abstrahiert, sondern eine logische Funktion (Bytes rauspusten,
empfangen). Hier passt man eher den HW-nahen Code an die Applikation an.
Mag sich kleinkarriert anhören, ist aber ein komplett anderes Paradigma.
Der Applikationsprogrammierer gibt dabei vor, was er braucht, und der
HW-nahe implementiert das irgendwie (und nur das).
Die Möglichkeiten und Verwendungen eines uarts sind weit zahlreicher als
die konkreten Verwendungen in einer Firma über (fast) alle Projekte
A. S. schrieb:> Meine Erfahrung ist, dass diese Schnittstelle nicht die Hardware> abstrahiert, sondern eine logische Funktion (Bytes rauspusten,> empfangen). Hier passt man eher den HW-nahen Code an die Applikation an.>> Mag sich kleinkarriert anhören, ist aber ein komplett anderes Paradigma.
Das meinte ich vermutlich auch eher. Ich decke natürlich nicht alles ab
was eine USART kann, sondern entwerfe benötigte Funktionalitäten frei
nach meiner Definition. Die einfachste bei einer USART wäre, 8bit
rauszuschupsen. Das heißt dann bei mir aber projektübergreifend
1
voidUSART::send(uint8_tu8Val);
Ich glaube ich habe den Begriff dependency injection falsch verstanden.
Ben S. schrieb:> Aber sollte der> Compiler nicht erkennen, dass ich da einfach eine dynamische Klasse> erstelle? Warum sollte der beim USART Beispiel Overhead erzeugen und> welchen?
Naja, was der Compiler wann rausoptimieren kann und was nicht ist immer
ein wenig Glückssache.
Jedenfalls ist ein virtueller Funktionsaufruf im Allgemeinen (ohne
Rausoptimiert zu sein) ein Aufruf eines Funktionspointers, der in einer
Tabelle nachgeschlagen wird, die sich aus dem this-pointer ergibt.
Das sind so grob mindestens 3-4 Instructions pro Aufruf einer virtuellen
Funktion. Außerdem muss immer der this-Pointer mitgeschleift werden.
Ob das relevant ist, hängt sicher vom use-case ab. Auf einem AtTiny war
mir jedenfalls mal ziemlich schnell der Programmspeicher vollgelaufen.
Das mit dem globalen Objekt spart aber sicher einiges gegenüber einem
naiven Aufruf.
Al Fine schrieb:> Mit vituellen Methoden würde ich auf uC aufpassen, weil die doch einiges> an Overhead erzeugen.
Das ist am Ende wie ein Aufruf eines Funktionspointer, das bekommen die
heutigen Compiler schon ganz gut hin.
Ben S. schrieb:> Interfaces nutze ich nicht, da ich zu selten> portiere und den Kram dann eh kaum anpassen muss - also kopieren,> Klassennamen umbenennen, Quellcode ändern, fertig und passt scho. Wer> jedoch viel portieren muss (z.B. beruflich), für den kann das ganz> praktisch sein.
Is ja auch beruflich, aber das ging so gut auf, dass ich das privat
jetzt auch so mache.
Al Fine schrieb:> Naja, was der Compiler wann rausoptimieren kann und was nicht ist immer> ein wenig Glückssache.
Aktuelle Compiler sind schon ziemlich gut darin, genau diesen Fall zu
erkennen und zu optimieren.
Al Fine schrieb:> Ob das relevant ist, hängt sicher vom use-case ab. Auf einem AtTiny war> mir jedenfalls mal ziemlich schnell der Programmspeicher vollgelaufen.
Ohne RTFM sollte man halt nicht unterwegs sein. RTFM hätte in dem Fall
darauf hingewiesen, daß g++, dynamische Polymorphie und AVRs keine gute
Kombination sind.
Oliver
ARM M4 (stm32) schrieb im Beitrag #6453402:
> wie abstrahiert ihr von eurer Mikrocontrollerumgebung wenn ihr> Chiptreiber schreibt? Z.B. für einen externen SPI ADC.>> Ich habe hier 3 Beispiele geschrieben:
Keins der drei Beispiele löst das Problem. Sie unterscheiden sich nicht
in ihrem Abstraktionsniveau, sondern nur in Implementierungsdetails.
Jan K. schrieb:> Ideal wäre doch beides - ein leichtgewichtiger (wie auch immer man das> konkret umsetzt) Abstraktionslayer, der wirklich universell ist und> gegen den in der Business Logik programmiert wird.
Vollkommen falsch, total falsch.
A. S. schrieb:> Meine Erfahrung ist, dass diese Schnittstelle nicht die Hardware> abstrahiert, sondern eine logische Funktion (Bytes rauspusten,> empfangen). Hier passt man eher den HW-nahen Code an die Applikation an.
Genau so wird es gemacht. So und nicht anders.
> Mag sich kleinkarriert anhören, ist aber ein komplett anderes Paradigma.
Nein, es ist nicht kleinkariert, sondern inzwischen ein seit Jahrzehnten
bekanntes Grundlagenprinzip:
Abhängigkeiten immer in Richtung der Abstraktion!
Das heißt: Die Business-Logik gibt die Schnittstelle vor. Dagegen wird
die Hardware programmiert, und nicht umgekehrt.
Beispiel: Wertet die Applikation einen Temperatur-Sensor aus, definiert
die Applikation eine Schnittstelle dafür. Im einfachsten Falle z.B.:
1
externint16_treadTemperatureCentiCelsius();
Das Ding kann natürlich auch in einer virtuellen Klasse, mit tausend
anderen Schnittstellen definiert sein, als Singleton, als Dependency
Injection, oder sonst irgendeinem Mechanismus bzw.
Implementierungsdetail. Für ein komplexe Schnittstelle einer fetten
Anwendung können das auch unzählige Klassen sein. Piep egal.
Der Punkt ist: Gegen diese Schnittstelle der Business-Logik wird die
Hardware programmiert. Ob nun die Temperatur über einen ADC am SPI-Bus,
per I2C, oder per RS485-Busprotokoll von irgendwo her kommt, spielt für
die Business-Logik keine Rolle.
Experte schrieb:> Nein, es ist nicht kleinkariert, sondern inzwischen ein seit Jahrzehnten> bekanntes Grundlagenprinzip:>> Abhängigkeiten immer in Richtung der Abstraktion!>> Das heißt: Die Business-Logik gibt die Schnittstelle vor. Dagegen wird> die Hardware programmiert, und nicht umgekehrt.
Whow. Für mich war das eher die Quintessenz aus mehreren
Jahren/Projekten/Portierungen Erfahrung und für meine Kollegen
(API-Götter) eher eine Kröte, die sie "gegen besseres Wissen" schlucken
mussten.
Hast Du eine Quelle oder ein Schlagwort dafür?
(Mit dem blinden Huhn und so: Am Anfang meiner Laufbahn hat sich mit
meinem (erfahreneren) Kollegen eine Arbeitsweis ergeben, die kurze Zeit
später als "Pair Programing" hip wurde)
ARM M4 (stm32) schrieb im Beitrag #6453402:
> Z.B. für einen externen SPI ADC.
ADC ist ein recht komplexes Thema, den kann man nicht so einfach dem SPI
übergeben und gut is. Jeder Typ kocht da sein eigenes Süppchen und man
muß ein ganz bestimmtes Timing einhalten. Manche haben einen separaten
Busy-Ausgang, manche einen Sync-Eingang, bei manchen geht nur, die
Wandlungszeit abzuwarten usw. Oft sind auch weitere Wartezeiten
notwendig, wenn Mux, Gain oder Referenz umgeschaltet werden.
Und oft sind auch die Datenformate recht krude und man muß die Kommandos
und das Ergebnis hin- und herschieben.
Es ist daher sinnvoll, sich nur auf einen oder wenige ADC-Typen
festzulegen und dann dafür den bzw. die Treiber zu schreiben.
@peda:
Dafür gibts den hier schon angesprochenen MCAL.
Dieser abstrahiert den SPI.
Darauf baut dann ein DAL auf (Device Abstraction Layer).
Der nutzt dann zB einen Timer fürs Warten, den IRQ/NVIC für Änderungen
am Busyflag (wenns am Portpin IRQ hängt).
Dazu vllt. noch einen DMA für schnelle ADC wo man viele Werte braucht.
(auch alles per MCAL bereitgestellt)
Der DAL dieses ADC stellt dann die Einlesefunktion bereit und
Einstellfunktionen für den genannten Gain.
Unter der Haube kann der ja vllt schon die ganze Zeit den letzten Wert
einlesen (IRQ getrieben Statemachine mit Handlerfunktion in der
Mainloop, oder mit RTOS und Task) und die Get Funktion ließt den letzten
zwischengespeicherten.
Die MCAL Teile reicht man dann dem DAL über hier vorgeschlagene
Methodiken:
dependency injection
interfaces
using declarations
Mw E. schrieb:> Dafür gibts den hier schon angesprochenen MCAL.> Dieser abstrahiert den SPI.
Ich benutze für das SPI einfach eine Inline-Funktion.
Die 8 Takte für ein Byte per Polling abzuwarten, geht erheblich
schneller, als erstmal das ganze riesen Getöse für einen Kontextwechsel
anwerfen zu müssen.
Grade eben haste aber noch von einem viel größerem Aufwand für einen ADC
gesprochen ;)
Wenns nen ADC ist der das nicht braucht, dann sind DAL+MCAL
dementsprechend "leer".
ich benutze Variante 3, aber mit einem OO OS und damit nicht die direkte
Abhängigkeit einer HAL.
In den Konstruktor des SPIADC dann eine Referenz auf das SPI Object das
benutzt werden soll. Man könnte das SPI Objekt im SPIADC erzeugen und
nur die Pinnames übergeben, aber gerade SPI kann ja von mehreren devices
verwendet werden und da ist es sinnvoll das SPI Objekt ausserhalb zu
instanziieren.
Mit virtuellem read/write können dann auch abgeleitete SPI Objekte
verwendet werden, um z.B. ein Software SPI mit dem SPIADC verwenden zu
können.
Experte schrieb:> Jan K. schrieb:>> Ideal wäre doch beides - ein leichtgewichtiger (wie auch immer man das>> konkret umsetzt) Abstraktionslayer, der wirklich universell ist und>> gegen den in der Business Logik programmiert wird.>> Vollkommen falsch, total falsch.
Interessante These, aber viel zum Diskutieren, find ich gut.
>> A. S. schrieb:>> Meine Erfahrung ist, dass diese Schnittstelle nicht die Hardware>> abstrahiert, sondern eine logische Funktion (Bytes rauspusten,>> empfangen). Hier passt man eher den HW-nahen Code an die Applikation an.>> Genau so wird es gemacht. So und nicht anders.>>> Mag sich kleinkarriert anhören, ist aber ein komplett anderes Paradigma.>> Nein, es ist nicht kleinkariert, sondern inzwischen ein seit Jahrzehnten> bekanntes Grundlagenprinzip:>> Abhängigkeiten immer in Richtung der Abstraktion!>
Hast du da mal Literatur zu?
> Das heißt: Die Business-Logik gibt die Schnittstelle vor. Dagegen wird> die Hardware programmiert, und nicht umgekehrt.>> Beispiel: Wertet die Applikation einen Temperatur-Sensor aus, definiert> die Applikation eine Schnittstelle dafür. Im einfachsten Falle z.B.:>>
1
>externint16_treadTemperatureCentiCelsius();
2
>
>> ...>> Der Punkt ist: Gegen diese Schnittstelle der Business-Logik wird die> Hardware programmiert. Ob nun die Temperatur über einen ADC am SPI-Bus,> per I2C, oder per RS485-Busprotokoll von irgendwo her kommt, spielt für> die Business-Logik keine Rolle.
Ich habe das Gefühl, dass wir hier von zwei verschiedenen Ebenen reden.
Möglicherweise habe ich auch den Begriff Business Logik im falschen
Kontext verwendet.
Mir ist schon klar, dass z.B. ein High Level Kommunikationsmodul, das
sich über UART/CAN/whatever unterhält nicht die eigentliche physische
Schnittstelle kennt. Oder dass, wenn jemand von außen deine Temperatur
vom x-beliebigen Tempsensor haben möchte, das Comm-Modul nicht am SPI
rumfummelt.
Natürlich gibt es dann ein eigenständiges Modul/Klasse/compilation
unit/..., "TemperaturSensor", das deine Funktion int16_t
readTemperatureCentiCelsius(); hat.
Natürlich kapselt das Modul nach außen hin, wie es implementiert ist und
welche Schnittstellen es benutzt. Fakt ist aber doch, dass irgendwelche
Hardware Schnittstellen benutzt werden.
In deinem Beispiel würdest du dann für jeden neuen µC und vor allem
jedes angepasste Layout (z.B. mit unterschiedlichem Routing, anderen SPI
Pinnen, anderer Clock config) das gesamte TemperaturSensor Modul (und
noch mehr) neu schreiben?!
Warum hier nicht die Hardware Schnittstelle, die eben gegen ein
Interface programmiert wurde irgendwie übergeben. Dann ist es nämlich
auch wieder egal, ob es eine SPI oder ein I2C ist, denn die öffentlichen
Funktionen heißen bei beiden gleich, nämlich
1
uint8_treadData()
und
1
voidwriteData(uint8_tdata)
oder so? Und da würde man dann eben den HAL ansetzen.
Habe ich dich falsch verstanden, oder sind wir auf zwei
unterschiedlichen Ebenen unterwegs?
Danke :)
Jan
Jan K. schrieb:> Dann ist es nämlich auch wieder egal, ob es eine SPI oder ein I2C ist,> denn die öffentlichen Funktionen heißen bei beiden gleich, nämlich> uint8_t readData() und void writeData(uint8_t data) oder so? Und da> würde man dann eben den HAL ansetzen.
Ja, das klingt gut. Funktioniert nur in der Praxis nicht bzw. nur im
eigenen Saft. Wenn die HW aus einem Device besteht, nur eine mainloop
(Task) verwendet wird, Rechenleistung keine Rolle spielt.
In der Praxis stehst Du direkt vor der Frage: blockierend, pollend,
asynchron, DMA? Und dann schreibst Du 4 Versionen vom Treiber und
überlegst Dir im Hauptprogramm (High Level), ob bei der aktuellen
Taktrate von CPU und SPI die blockierende Version angemessen ist.
Jan K. schrieb:> Hast du da mal Literatur zu?
DIP: Dependency Inversion Principle.
Das D in SOLID
A. S. schrieb:> In der Praxis stehst Du direkt vor der Frage: blockierend, pollend,> asynchron, DMA?
ADC ist oft ein schwieriges Thema, da kann man selten eine Lesefunktion
schreiben und vergessen. Man muß immer die Seiteneffekte beachten.
Z.B. man hat einen integrierenden ADC mit 10Messungen/s. Dann muß man
die Lesefunktion in mehrere Teile aufteilen: Starten der Messung, Warten
auf Ready, Lesen des Ergebnisses.
Oft ist auch die interne Statemaschine des ADC freilaufend, d.h. das
Starten erfolgt irgendwann, nachdem der gerade laufende interne Zyklus
fertig ist. Das Ergebnis ist also irgendwann nach 100%..199,9% der
Meßzeit gültig.
Ich hatte mal nen ADC, bei dem während einer Messung kein Status lesen
möglich war. Die einzige Möglichkeit, das Ende einer Messung zu erkennen
war daher, den ADC immer wieder auszulesen, bis sich 2
aufeinanderfolgende Werte unterschieden haben. Diese Testplatine wurde
verschrottet und ein anderer ADC genommen.