Wie sieht der richtige Zugriff auf Register mit structs & Bitfeldern
aus? Ich weiss dass der effektivste(!) Zugriff die Vermeidung von
Bitfeldern wäre, den Sinn/Unsinn von Bitfeldern für Registerzugriff
lassen wir bitte aussen vor, mir geht es vor allem um die Verwendung von
'volatile'.
1
typedefvolatileunion{
2
volatilestruct{
3
volatileuint8_tBit0:1;
4
volatileuint8_tBits3_1:3;
5
volatileuint8_t:1;//Freiraum
6
volatileuint8_tBit5:1;
7
volatileuint8_tBit6:1;
8
volatileuint8_tBit7:1;
9
};
10
volatileuint8_tRegValue;
11
}_REG;
12
13
#define REG ((_REG*) REG_ADR)
Soweit ich es verstanden habe, müssen auch die Member von structs/unions
als volatile gekennzeichnet werden, nur das struct/union als volatile zu
kennzeichnen reicht nicht aus. Wäre obiger Code dann so korrekt?
Besten Dank.
Ralf
Es genügt, volatile auf dem äußersten struct/union zu haben.
Die einfachste Möglichkeit ist aber, es an die selbe Stelle wie bein
normalen uint8_t zu setzen:
> Wäre obiger Code dann so korrekt?
IMO müsstest Du der Struct einen Namen geben:
1
typedefunion{
2
struct{
3
//...
4
}bits;
5
//...
Da würde ich aber einfach den Compiler zu befragen, der meckert wenn da
was falsch ist. ;-)
> Wie sieht der richtige Zugriff auf Register mit structs & Bitfeldern
aus?
Kannst Du nicht allgemeingültig machen.
Denn Der Compiler setzt Dir bei
1
REG->bits.Bit0=1;
praktisch denselben Code auf wie bei
1
REG->RegValue|=1;
Aber nur im zweiten Fall sieht der Programmierer auch dass das kein
reines Write sondern Read-Modify-Write ist.
Bei Registern kann es aber auch Seiteneffekte beim Lesen geben
(Beispiel: UART Datenregister), außerdem ist dieses Read-Modify-Write
nicht Interrupt-Sicher, da es aus mehreren Instruktionen besteht.
Bei Registern ohne Seiteneffekt beim Lesen könnte man was ganz ähnliches
mit Hilfe des Bitbandings im Cortex M aufsetzten. Dessen implizites
Read-Modify-Write ist nämlich auch für Interrupts sicher.
Ralf schrieb:> Wie sieht der richtige Zugriff auf Register mit structs & Bitfeldern> aus?
Eigentlich kann ich von der Verwendung von struct's bei den
Hardwareregistern nur abraten - obwohl eine ganze Reihe von Headerfiles
genau so organisiert ist.
Aber erstens ist das herzlich undurchsichtig und schlecht lesbar,
zweitens sieht man bei den Definitionen ganz oft eingeschobene
Dummy-Werte, um unbelegte Stellen zu überbrücken und das ist eigentlich
nur Krampf und drittens macht man es dem Compiler mit struct's nur
unnötig schwer. Wer's nicht glaubt, sollte sich mal ansehen, was der
Compiler beim Optimieren so erzeugt.
W.S.
W.S. schrieb:> Aber erstens ist das herzlich undurchsichtig und schlecht lesbar,
Wieso, so sieht man doch auf einen Blick welche Register z.B. zu GPIO
gehören. Außerdem kann man so einfach eine Referenz auf eine komplette
GPIO-Bank an Funktionen übergeben (eben in Form von struct-Pointern),
das geht bei Einzel-Definitionen nur umständlich.
W.S. schrieb:> zweitens sieht man bei den Definitionen ganz oft eingeschobene> Dummy-Werte, um unbelegte Stellen zu überbrücken und das ist eigentlich> nur Krampf
Macht nix, da man die ja nicht selber schreibt
W.S. schrieb:> und drittens macht man es dem Compiler mit struct's nur> unnötig schwer. Wer's nicht glaubt, sollte sich mal ansehen, was der> Compiler beim Optimieren so erzeugt.
Ich weiß ja nicht was du so für miese Compiler verwendest, aber
vernünftige wie der GCC haben da keinerlei Probleme mit.
Problematisch ist nur die Verwendung von Bitfields in Registern, denn
wenn man nacheinander einzelne Bits setzt, werden daraus mehrere
Read-Modify-Write-Zyklen, zwischen denen das Register möglicherweise
einen inkonsistenten Zustand erhält und die Peripherie irgendeinen
unbeabsichtigten Unsinn macht.
W.S. schrieb:> Wer's nicht glaubt, sollte sich mal ansehen, was der> Compiler beim Optimieren so erzeugt.
Hier noch ein Beispiel für Ungläubige, für den STM32F4:
W.S. schrieb:> Eigentlich kann ich von der Verwendung von struct's bei den> Hardwareregistern nur abraten
Es gibt Controller, die mehrere gleichartige I/O-Module besitzen. Also
beispielsweise mehrere UARTs, I2Cs, Timer, ... Wenn man das nicht grad
wie Atmel bei den Timern der AVRs macht, dann sehen die auch gleich aus,
d.h. es ist der gleiche Adressblock bei allen UARTs und es gibt auch nur
eine struct Deklaration für alle. In der struct-Version kann man den
gleichen Code für alle gleichartigen Module verwenden und muss nur den
Pointer auf die struct übergeben.
> und drittens macht man es dem Compiler mit struct's nur> unnötig schwer.
Tatsächlich ist es eher umgekehrt, wenn man mal die 8-Bitter verlässt
und bei 32-Bittern landet. Die können nämlich exzellent relativ zu einem
Pointer in einem Register adressieren. Also genau das, was man bei der
struct-Methode mit nicht-konstanter Adresse benötigt. Bei der viele
8-Bitter schlecht dastehen.
Bei absoluter Adressierung über eine konstante Adresse hingegen setzt
man voraus, dass der Compiler schlau genug ist, die versehentlich
vergessene struct im Geiste wieder hinzuzufügen um die Basisadresse des
Moduls einmalig in ein Register zu bringen. Er dazu also die relative
Nähe der konstanten Adressen feststellen muss. Statt jedes Mal
umständlich eine 32-Bit Adresse in ein Register zu wuchten.
> Wer's nicht glaubt, sollte sich mal ansehen, was der> Compiler beim Optimieren so erzeugt.
Genau. ;-)
Dr. Sommer schrieb:> 00000014 <directtest>:> 14: 4b03 ldr r3, [pc, #12] ; (24 <directtest+0x10>)> 16: 222a movs r2, #42 ; 0x2a> 18: 601a str r2, [r3, #0]> 1a: f240 5239 movw r2, #1337 ; 0x539> 1e: 615a str r2, [r3, #20]> 20: 4770 bx lr> 22: bf00 nop> 24: 40020400 .word 0x40020400
Wer es nicht gleich dekodiert kriegt: Hier wird trotz "vergessener"
struct-Deklaration die Adresse des I/O-Moduls in R3 geladen und relativ
dazu werden die beiden I/O-Register angesprochen. Der Compiler hat also
die Nähe der beiden explizit angegebenen Adressen erkannt und optimiert.
In der struct-Variante musste er das nicht. Deshalb ist dann auch der
direkte Weg unoptimiert länger als der relative.
W.S. schrieb:> Eigentlich kann ich von der Verwendung von struct's bei den> Hardwareregistern nur abraten - obwohl eine ganze Reihe von Headerfiles> genau so organisiert ist.
Dazu habe ich zwei Fragen:
(a) Warum?
(b) Wie ist es deiner Meinung nach besser?
S. R. schrieb:> Dazu habe ich zwei Fragen:> (a) Warum?> (b) Wie ist es deiner Meinung nach besser?
zu a: hatte ich doch bereits geschrieben
zu b: genau so formulieren, wie es im Referenzmanual steht.
Ganz genau so.
Man muß ja doch gelegentlich dort nachlesen und dann ist es tatsächlich
hilfreich, nicht erst zwischen den Bezeichnern im RefMan und den oftmals
unterschiedlichen Bezeichnern im Headerfile umdenken zu müssen.
Zusätzlich gibt es häufig genug dezente Unterschiede zwischen ansonsten
(fast) gleichen Peripherie-Cores, vor allem bei UART's, von denen dann
eben mal einer ein USART ist oder einer eben keine IR-Unterstützung hat
und und und. Sowas durch 5x gleichen struct zu beschreiben geht schon
irgendwie, ist aber Krampf. Schrieb ich übrigens auch schon.
W.S.
W.S. schrieb:> zu b: genau so formulieren, wie es im Referenzmanual steht.>> Ganz genau so.>> Man muß ja doch gelegentlich dort nachlesen und dann ist es tatsächlich> hilfreich, nicht erst zwischen den Bezeichnern im RefMan und den oftmals> unterschiedlichen Bezeichnern im Headerfile umdenken zu müssen.
Was hat das mit struct oder direkt zu tun? bei obigem STM32 Beispiel
lauten die Bezeichner exakt genau so wie im Handbuch und auch bei meinem
Freescale mit dem ich mich jeden Tag vergnüge ist das kein bisschen
anders. Sogar die Definitionen der einzelnen Bits in den Registern sind
wortwörtlich den Bezeichnungen im Handbuch entnommen.
Also nochmal die Fragen:
A: Warum?
B: Wie sonst wenn nicht so?
W.S. schrieb:> zu b: genau so formulieren, wie es im Referenzmanual steht.
Da steht normalerweise nur eine Tabelle mit Beschreibung. Die kann ich
schlecht so in mein C-Programm schreiben...
> Man muß ja doch gelegentlich dort nachlesen und dann ist es tatsächlich> hilfreich, nicht erst zwischen den Bezeichnern im RefMan und den oftmals> unterschiedlichen Bezeichnern im Headerfile umdenken zu müssen.
Achso, du regst dich nur darüber auf, dass im Datenblatt ABC steht, aber
im Headerfile z.B. PIO_ABC. Oder dass im Datenblatt die Bits XY1, XY2
und XY3 heißen, aber im Headerfile von XY_Msk und XY_MODE0 bis XY_MODE7
die Rede ist.
> Zusätzlich gibt es häufig genug dezente Unterschiede zwischen ansonsten> (fast) gleichen Peripherie-Cores, vor allem bei UART's, von denen dann> eben mal einer ein USART ist oder einer eben keine IR-Unterstützung hat> und und und. Sowas durch 5x gleichen struct zu beschreiben geht schon> irgendwie, ist aber Krampf. Schrieb ich übrigens auch schon.
Zusätzlich gibt es häufig auch Controller, wo ein Peripheriebaustein
mehrfach an unterschiedlichen Basisadressen verbaut ist, vor allem bei
GPIO's. Sowas durch unterschiedliche struct zu beschreiben geht schon
irgendwie, ist aber Krampf.
Irgendwo dazwischen gibt es einen Mittelweg, und
PERIPHERIE->REGISTER = PER_REG_A | PER_REG_B
empfinde ich als sehr gute Lösung - denn sie entspricht größtenteils
dem, was im Datenblatt steht.
"Löcher" in der struct mit reservierten Bezeichnern aufzufüllen ist
Aufgabe des Header-Schreibers, und damit nicht meins (solange ich die
Header nicht selbst schreibe).
S. R. schrieb:> Da steht normalerweise nur eine Tabelle mit Beschreibung. Die kann ich> schlecht so in mein C-Programm schreiben...
Grrrmpf...
Nein, ich reg mich da nicht auf, obwohl ich es nicht ausstehen kann, daß
da in solchen Headerfiles ganze Breitseiten von zusätzlichen Includes
stehen und daß da solche Headerfiles unsäglich groß werden. Ich hatte
neulich welche von Freescale, die über 1 MB lang waren und nebst
Includes nicht mal den SysTick mit seinem echten Namen enthielten - da
such mal, bist du das gefunden hast...
Also, der Rest der Welt mag ja tun was er will, aber ich bleibe dabei,
daß ich möglichst kurze und prägnante Headerfiles habe, wo die Namen der
HW-Register exakt so drinstehen, wie im RefMan. Möglichst noch mit
Verweis als Kommentar auf die Seite im RefMan, wo alles Nähere steht
(Bitbelegung etc).
W.S.
W.S. schrieb:> #define MCG_C1
Und was ist dann konkret der Unterschied?
Was soll an MCG_C1 einfacher sein als an MCG->C1?
> die über 1 MB lang waren
Ja, mit Deiner Methode wären die dann 3MB lang wenn alles doppelt und
dreifach drin wäre.
Aber warum überhaupt? Es ist doch vollkommen egal aus wievielen Teilen
die Header bestehen und wie lang sie sind.
> und nebst Includes nicht mal den SysTick> mit seinem echten Namen enthielten - da such mal,> bist du das gefunden hast...
Warum? Alles was zum Core gehört ist immer in den core_xxx Headern. Die
sind orginal von ARM. Was musst Du da überhaupt großartig suchen, die
werden automatisch mit reingezogen wenn Du es so verwendest wie es
gedacht ist. Schau halt mal in irgendeiner Beispielanwendung welche
header die dort includen und welche defines beim Kompilieren vorhanden
sein müssen und dann machs bei Deinen eigenen Projekten exakt genauso.
W.S. schrieb:> Nein, ich reg mich da nicht auf, obwohl ich es nicht ausstehen kann, daß> da in solchen Headerfiles ganze Breitseiten von zusätzlichen Includes> stehen und daß da solche Headerfiles unsäglich groß werden.
Stimme ich dir vollkommen zu.
> Also, der Rest der Welt mag ja tun was er will, aber ich bleibe dabei,> daß ich möglichst kurze und prägnante Headerfiles habe, wo die Namen der> HW-Register exakt so drinstehen, wie im RefMan.> Möglichst noch mit Verweis als Kommentar auf die Seite im RefMan, wo> alles Nähere steht (Bitbelegung etc).
Vergiss es. Während ich den Studenten einen ARM beigebracht habe, hat
Atmel ein neues Datasheet rausgeworfen, womit natuerlich alle meine
Referenzen (selbst die Kapitelnummern...) ins Leere liefen.
Und nein, nicht alle Studenten sind hell genug, das zu bemerken (und
dann das von mir bereitgestellte Datenblatt zu benutzen). Wenn du in
deinen Headern auf ein Datenblatt verweist, dann verweist du damit immer
auch auf eine bestimmte Version davon, und damit entgehen dir u.U.
Errata und so Zeugs.
W.S. schrieb:> #define MCG_C1 (*((volatile byte *) 0x40064000)) // MCG Control 1> Register (MCG_C1) 8 R/W 04h 25.3.1/470> #define MCG_C2 (*((volatile byte *) 0x40064001)) // MCG Control 2> Register (MCG_C2) 8 R/W 80h 25.3.2/472> ...
Nicht bei jedem Hersteller stehen die Registeradressen so im Datenblatt.
Oft gibt es tatsächlich nur eine Basisadresse plus Offsets (in
verschiedenen Dokumenten). Dann ist das Headerfile schreiben genauso
fehlerträchtig wie mit einer (packed) struct.
Aber was ich nicht verstehe: Was ist an "MCG_C2 = 0x800" so viel besser
als an "MCG->C2 = 0x800" (die Bitbelegungen mal weggelassen)? In beiden
Fällen ist doch offensichtlich, dass es um Register C2 vom MCG geht.
Bernd K. schrieb:> Ja, mit Deiner Methode wären die dann 3MB lang wenn alles doppelt und> dreifach drin wäre.
Da irrst du gewaltig. Hab grad mal nachgeschaut: Original Freescale ca.
1 MB, mein eigenes ca. 126 KB. Schreib also lieber keine aus der Luft
gegriffenen Vermutungen.
Bernd K. schrieb:> Aber warum überhaupt? Es ist doch vollkommen egal aus wievielen Teilen> die Header bestehen und wie lang sie sind.
Nun, dir ist es egal, mir nicht. Häufig genug wird man mit Eimern voll
zusätzlichen Zeuges (Bitbezeichnungs-Zeug) überschüttet, was zumindest
ICH gar nicht haben will.
S. R. schrieb:> Nicht bei jedem Hersteller stehen die Registeradressen so im Datenblatt.> Oft gibt es tatsächlich nur eine Basisadresse plus Offsets (in> verschiedenen Dokumenten). Dann ist das Headerfile schreiben genauso> fehlerträchtig wie mit einer (packed) struct.
Ja, ich weiß. Es machen ja auch viele. Aber wie ganz weit oben bereits
gesagt, ich rate davon ab. Die Einzeldefinition ist klarer, einfacher
und geradliniger, also auch besser lesbar. Immer das 'KISS' im
Hinterkopf haben (keep it small and simple) Natürlich kann man auf beide
Arten zu Potte kommen, aber dabei kommt dann sowas raus:
A. K. schrieb:> Und was ich nicht verstehe: Wie schreibt im W.S.-Stil Funktionen, die> gleichermassen für verschiedene UARTs nutzbar sind?>> Etwa so? void uart_init(int no)
Eben, da haben wir es: Du bist ein großer Zusammenfasser, der es für
universeller hält, eine einzige Funktion für alle und ein einziges Array
von struct's für die Peripheriecores zu schreiben - egal wieviele davon
im aktuellen Projekt überhaupt verfügbar sind.
Bei mir kommen solche doofen Funktionen nicht vor. Doof deshalb, weil
sie aus Sicht der Anwendung vorspiegeln, man hätte eine Breitseite von
Cores zur Verfügung - was in der Praxis definitiv NIE der Fall ist. Ich
bevorzuge daher dedizierte Funktionen, z.B. UART0_init(long baudrate);
und wenn ich (oder jemand anderes so blöd ist, UART3_init aufzurufen,
obwohl UART3 im aktuellen System garnicht benutzbar ist, da die Pins
anderweitig belegt sind, dann kriegt er das spätestens vom Linker an den
Kopf geworfen.
Ich halte Zusammenfassungen (UART[0..x] und so weiter) für ganz
schlechten Programmierstil, da - wie gesagt - damit etwas vorgespiegelt
wird, was überhaupt nicht real ist.
So - nun soll's genug sein damit.
W.S.
W.S. schrieb:> Eben, da haben wir es: Du bist ein großer Zusammenfasser,
Ich nenne das Systematik und Abstraktion und finde es nützlich und
sinnvoll.
> universeller hält, eine einzige Funktion für alle und ein einziges Array> von struct's für die Peripheriecores zu schreiben - egal wieviele davon> im aktuellen Projekt überhaupt verfügbar sind.
Nein. Kein Array. Eine Strukt definiert das Layout eines I/O-Moduls. Die
Adressen der Module wiederum werden im Include klassisch definiert, also
UART1, UART2, ..., jeweils als Adresse der gleichen UART-Struct. Den
Namen UART3 gibts dann auch nur, wenn es mindestens 3 UARTs gibt. So
definiert das beispielsweise CMSIS.
> Bei mir kommen solche doofen Funktionen nicht vor.
Ich neige zu Abstraktions-Layern. So wenig, wie ich Betriebssysteme doof
finde, finde ich HALs doof. Die nach oben hin eine leidlich ähnliche
UART oder CAN Schnittstelle definieren, auch wenn die Hardware recht
unterschiedlich aussieht. Erhöht die Wiederverwendbarkeit von Code.
> man hätte eine Breitseite von Cores zur Verfügung
Mehrer UARTs, SPIs, I2Cs sind heute nicht selten, mehrere GPIOs-Ports
und Timer fast immer vorhanden.
Ich hoffe mal, dass du mit "Core" das meinst, denn mit CPU-Cores hat das
ja nun reinweg nichts zu tun.
> - was in der Praxis definitiv NIE der Fall ist.
???
Gibts einen Microcontroller jeseits der 8-20-Pin Klasse, der pro
I/O-Modulklasse nur maximal ein Modul enthält? Also 0-1 Timer, 0-1 UART,
0-1 I2C, ...? Dass die AVRs lauter unterschiedliche Timer haben ist
nicht die Regel.
> Ich halte Zusammenfassungen (UART[0..x] und so weiter) für ganz> schlechten Programmierstil
Davon war im Thread auch nirgends die Rede.
A. K. schrieb:> Ich nenne das Systematik und Abstraktion und finde es nützlich und> sinnvoll.
Moment mal, DU warst das, der "void uart_init(int no) " ins Rennen
geschickt hat, nicht ich. Und wenn man schon ne Uart-Nummer in einem
Funktionsaufruf hat, dann läuft das entweder auf sowas hinaus:
T_UART const UARTS[x] = { ...
oder eben so, wie du weiter schriebest: "switch(uartnummer).."
Wo ist bei dir da die Systematik oder gar die Abstraktion? Ich sehe da
nur ein mit Gewalt zusammengepferchtes Bündel von UART's, die hinterher
wieder aufgedröselt werden müssen. Für mich ist es wesentlich
überzeugender, gleich ganz durchgängig void uart0_init(..) zu haben und
dort im Treiber eben uart0_registername. Ein struct im .h ist da einfach
unnötig.
Für die Abstraktion macht man das Ganze anders, etwa so:
Char_Out ('A', stdio);
wobei im BS-Kern stdio auf einen Kanal gelegt wurde, der nicht
zwangsläufig ein UART sein muß. Sowas abstrahiert und schafft
Systematik.
Vielleicht kommt - was das thema Headerfile betrifft - dir die
Erleuchtung, wenn du dir mal ansiehst, wie bei manchen Chips die
Peripherie-Cores sich darstellen. Der DMA-Core beim MK02FNxxx ist dafür
ein gutes Beispiel. Dort hat es zunächst einen ganzen Sack Register, die
für den gesamten DMA da sind. Und dann hat es 4 Sätze von Registern, die
für jeweils einen der 4 Kanäle da ist. Es ist schon ne Schreibarbeit
"DMA_TCD3_NBYTES_MLNO = xyz;" hinzuschreiben, aber dein struct-Konzept
angewandt, wäre das "DMA->TCD3->NBYTES_MLNO" was zwar auch gehen würde,
aber einen Gewinn an Systematik oder gar Abstraktion sehe ich beim
besten Willen nicht darin. Es wäre lediglch mit viel mehr Hampelei im .h
verbunden (was es im Original von Freescale ja auch ist).
Die eigentliche Hardware-Abstraktion ist in den Treibern und den bei
Bedarf darauf aufsetzenden höheren Betriebssystem-Funktionen (s.o.).
Aber nicht darin zu sehen, daß man ein struct bei der Deklaration der
Hardwareregister hat. Ein leuchtendes Beispiel dafür, wie man es nicht
machen sollte ist die unsägliche ST-Lib, die überhaupt nichts
abstrahiert, sondern den Programmierer in einer Flut von zusätzlichen
Identifiern und zu füllenden structs im RAM ersaufen läßt - und ihn
schlußendlich die ganze Arbeit ja dann doch selber machen läßt.
W.S.
W.S. schrieb:> Moment mal, DU warst das, der "void uart_init(int no) " ins Rennen> geschickt hat,
Zur Abschreckung. Die von mir favorisierte Variante stand darunter.
> Ich sehe da> nur ein mit Gewalt zusammengepferchtes Bündel von UART's, die hinterher> wieder aufgedröselt werden müssen.
Wo aufgedröselt? In der Anwendungsebe darüber? Da steht bei mir
uart_init(UART1);
statt deinem
uart1_init();
Nur dass bei mir der gleiche Code auch für
uart_init(UART2);
verwendet wird. Wenn im Programm 2 UARTs verwendet werden.
> Für die Abstraktion macht man das Ganze anders, etwa so:> Char_Out ('A', stdio);
Das ist noch eine Ebene weiter oben. Solch ein Schema wird sinnvoll,
wenn ähnliche Sachen zusammengefasst werden. Was ich in Form gleicher
und verschiedener Typen von Temperatursensoren schon hatte.
Also sowas wie sensors[i]->startConversion(); (C++).
Hoppla, da ist ja ein Array und eine Zusammenfassung. Ei der daus! :-)
> aber dein struct-Konzept> angewandt, wäre das "DMA->TCD3->NBYTES_MLNO"
Nein. Es wäre bei mir DMA->TCD[3]->NBYTES_MLNO.
Gerne aber auch etwas wie
#define DMAchannel 3
...
DMA->TCD[DMAchannel]->NBYTES_MLNO;
bzw. beispielweise sowas wie
DMA->IER |= 1<<DMAchannel;
Ja, da sind nun Arrays drin. Für Structs und Bits. Schockiert?
Solche Arrays gibts auch beim NVic der Cortex-M. Der entsprechende Code
heisst dann beispielsweise
nvic_set_priority(CANirq, CANirqLevel)
nvic_enable(CANirq);
Und der Treiber vom NVic enthält dann
if (no >= 0 && no < 240)
NVIC->ISER[no / 32] = 1 << (no % 32);
Bei 240 möglichen IRQs wär mir das ohne Byte/Bit-Array zu aufwändig.
> Ein leuchtendes Beispiel dafür, wie man es nicht> machen sollte ist die unsägliche ST-Lib, die überhaupt nichts> abstrahiert,
Jedenfalls kaum etwas. Die macht m.E. nur doppelte Arbeit. Da sind wir
beieinander. Der einzig gute HAL ist der eigene, denn nur der entspricht
dem eigenen Geschmack. Zumindest für ein paar Jahre. ;-)
Hmm, ich polemisiere einfach mal dazwischen - soll eigentlich nicht OT
sein, sondern zum Thema API-Design beitragen. Beide Beispiele so
(natürlich abgewandelt) in hardwarenahem Produktivcode gesehen und
stellen polymorphe Datenschnittstellen bereit.
Was ist der Unterschied zwischen:
void getData(int type, void* data, size_t len, int param )
17
{
18
if( NULL != data )
19
{
20
switch( type )
21
{
22
case MY_DATA_OP1:
23
if( len == sizeof(int) )
24
{
25
//...
26
*(int*)data = 3;
27
}
28
break;
29
case MY_DATA_OP2:
30
if( len == sizeof(double) )
31
{
32
// ...
33
*(double*)data = 3.14;
34
}
35
break;
36
case MY_DATA_OP3:
37
if( len == sizeof(short ))
38
{
39
// ...
40
*(short*)data = 2;
41
}
42
break;
43
default:
44
break;
45
}
46
}
47
}
48
};
Und zum Vergleich:
1
class BaseB
2
{
3
public:
4
virtual ~BaseB() {}
5
6
virtual int getOp1( int param ) const = 0;
7
8
virtual double getOp2( int param ) const = 0;
9
10
virtual short getOp3( int param ) const = 0;
11
};
12
13
class SampleB : public BaseB
14
{
15
public:
16
int getOp1( int param ) const { return 3; }
17
18
double getOp2( int param ) const { return 3.14; }
19
20
short getOp3( int param ) const { return 2; }
21
22
};
Dass die Konstruktoren jetzt privat sind, soll mal nicht stören. Imho
ist Beispiel B deutlichst einfacher zu lesen und wartungsfreundlicher
(Wartbarkeit), während Beispiel A die Symbole deutlich schlanker machen
kann (Codegröße). Was ist jetzt der richtige Weg? Den Compiler
typbasierte Policies abzuarbeiten (STL-style) oder eher C-Style von
Beispiel A.
Ich denke, das Beispiel kann man auf Eure Diskussion übertragen - es
kommt auf die Anwendung an.
Falk S. schrieb:> (natürlich abgewandelt) in hardwarenahem Produktivcode gesehen und> stellen polymorphe Datenschnittstellen bereit.
Yep. Beispielsweise die erwähnten Temperatursensoren verschiedenen Typs.
Macht sich einfach besser, bei Redundanz an kritischer Stelle
verschiedene Typen zu verwenden. Homogenisiert über Polymorphie in C++.
Auf einem Mega32, falls jetzt jemand sein "mit 16GB RAM..." loswerden
will. Ach ja, ein preemptive realtime scheduler passte auch noch rein.
A. K. schrieb:> Fix: DMA->TCD[3].NBYTES_MLNO / DMA->TCD[DMAchannel].NBYTES_MLNO;
Oh, am besten dazu noch ein
int DMAchannel;
Ach, laß mal, es wird dadurch auch bloß nicht besser.
Wer auf einem konkreten µC in einer konkreten Schaltung für ein
konkretes Projekt die Firmware entwickelt, weiß schon vor dem Tippen der
ersten Zeile, was er wofür zu verwenden gedenkt und da ist es im
Treiber, der nach Plan DMA-Kanal 3 verwenden soll, einfach das beste
weil geradlinigste,
DMA_TCD3_NBYTES_MLNO
hinzuschreiben. Ist sogar kürzer ;-)
Was Anderes kommt da ja sowieso nicht in Frage und das aufrufende
Programm sollte sich mit derartigen Dingen ohnehin nicht befassen
müssen. Was da von deiner Argumentation übrig bleibt, ist dein Drang
nach dem struct, den ich für nicht sinnvoll halte.
Ah, noch einer:
Falk S. schrieb:> Hmm, ich polemisiere einfach mal dazwischen - soll eigentlich nicht OT> sein, sondern zum Thema API-Design beitragen.
Naja, und was für ein API schwebt dir da vor? Ein allgemeines API, das
dann später konkretisiert wird, um es überhaupt für nen praktischen
Zweck benutzbar zu machen? Also zu allererst die allumfassende
Weltformel formulieren, um sie danach zwecks praktischen Gebrauches
herunterbrechen zu müssen, wobei tonnenweise Altlasten aus der
umfassendsten Version übrigbleiben? Nö, nicht mit mir. Ich halte es mit
"keep it small and simple". Das ist letztendlich weitaus effektiver und
wartbarer.
Deine beiden Ansätze erinnern mich an die Ideen von BWLlern zu
Waverprobern in der Halbleiterei: Die schwärmten von universellen
Vierquadrantenquellen, weil man da ja alles mit erledigen kann. Aber
das ist eben Bonzendenke und herzlich unpraktisch im Vergleich zur
Testschaltung direkt auf der Nadelplatine. Übersetzt heißt das:
angepaßte Treiber für konkrete Hardware und keine allumfassenden
Konstrukte, weil die in jedem konkreten Falle immer nur unnötigen,
komplizierenden, fehlerträchtigen, schwer wartbaren und unflexiblen
Overhead erzeugen.
W.S.
W.S. schrieb:> Oh, am besten dazu noch ein> int DMAchannel;>> Ach, laß mal, es wird dadurch auch bloß nicht besser.
Wenn man möchte. Vielleicht gibt es irgendwas das man erst zur Laufzeit
festlegen möchte. Immerhin hat man die Möglichkeit es einfach zu ändern.
W.S. schrieb:> Wer auf einem konkreten µC in einer konkreten Schaltung für ein> konkretes Projekt die Firmware entwickelt, weiß schon vor dem Tippen der> ersten Zeile, was er wofür zu verwenden gedenkt und da ist es im> Treiber, der nach Plan DMA-Kanal 3 verwenden soll, einfach das beste> weil geradlinigste,>> DMA_TCD3_NBYTES_MLNO>> hinzuschreiben. Ist sogar kürzer ;-)
Stimme ich zu. Aber ich dachte es ging darum wenn man halbwegs
wiederverwendbaren Code schreiben möchte. Nehmen wir das uart_init
Beispiel. Nimmst du wirklich jedes mal dein uart1_init als Vorlage und
ersetzt überall die Register wenn du in einem anderen Projekt jetzt
UART2 nutzen möchtest? Da ist ein einziges #define am Anfang oder
einzelner Parameter doch wesentlich einfacher zu ändern und man kann
kein Register vergessen.
W.S. schrieb:> Das ist letztendlich weitaus effektiver und> wartbarer.
Dein Ansatz ist das exakte Gegenteil von Wartbarkeit und/oder Effizienz.
Und er hat auch keine Vorteile in Bezug auf Performance oder
Ressourcenverbrauch, eher im Gegenteil. Was machst Du zum Beispiel wenn
Du zwei oder drei UARTs im selben Projekt gleichzeitig brauchst? Drei
fast identische Kopien der selben .c und .h Dateien verwenden?
W.S. schrieb:> Naja, und was für ein API schwebt dir da vor? Ein allgemeines API, das> dann später konkretisiert wird, um es überhaupt für nen praktischen> Zweck benutzbar zu machen?
Nu komm, zeig mal etwas Fantasie - was ist denn 'ne API? Eine mögliche
Definition: die Schnittstelle einer Bibliothek zu einer Anwendung. Wobei
die Anwendung von jemand anderem geschrieben werden muss. Willst Du
ernsthaft den Anwendungsprogrammierer in den Wahnsinn treiben, indem Du
ständig deine API änderst? Macht bei DLLs ganz besonders viel Spaß, ganz
besonders, wenn diese im System registriert werden müssen - Microsoft's
COM sollte die DLL-Versionshölle ja lösen.
> Also zu allererst die allumfassende> Weltformel formulieren, um sie danach zwecks praktischen Gebrauches> herunterbrechen zu müssen, wobei tonnenweise Altlasten aus der> umfassendsten Version übrigbleiben? Nö, nicht mit mir. Ich halte es mit> "keep it small and simple". Das ist letztendlich weitaus effektiver und> wartbarer.
Wie ist denn 'einfach' definiert? Einfach für den, der sie entwickelt
oder einfach für den, der sie anwendet? Oder einfachst möglich unter
Berücksichtigung beider Sichtweisen?
> Deine beiden Ansätze erinnern mich an die Ideen von BWLlern zu> Waverprobern in der Halbleiterei: Die schwärmten von universellen> Vierquadrantenquellen, weil man da ja alles mit erledigen kann. Aber> das ist eben Bonzendenke und herzlich unpraktisch im Vergleich zur> Testschaltung direkt auf der Nadelplatine.
Über Ansatz A habe ich in der praktischen Entwicklung oft genug gekotzt
- weil die fehlende Typbindung es sehr schwer gemacht hat,
nachzuvollziehen, ob bzw. was im Modul eigentlich passiert, wenn die
Funktion aufgerufen wird. Man kann so sehr gut Funktionalität
verstecken, kann aber nachträglich einfach ein neues #define machen.
> Übersetzt heißt das:> angepaßte Treiber für konkrete Hardware und keine allumfassenden> Konstrukte, weil die in jedem konkreten Falle immer nur unnötigen,> komplizierenden, fehlerträchtigen, schwer wartbaren und unflexiblen> Overhead erzeugen.
Was ist Overhead bei einer API? Wenn die Benutzung der API komplizierter
ist als die Funktionalität drunter. Hast Du 'ne Softwarekomponente, die
nie wieder erweitert wird - dann ist ja gut, schreib es minimal wie
möglich. Niemand zwingt Dich dazu, Overhead zu programmieren.
Hast du 'ne Komponente, wo du weißt, dass es wechselnde Anforderungen
gibt (Stichwort: auf Zuruf neue Funktion einbauen), dann könnten obige
Ansätze dir helfen. Offen für Erweiterung, geschlossen für Änderungen.
Sebastian V. schrieb:> Nimmst du wirklich jedes mal dein uart1_init als Vorlage und> ersetzt überall die Register wenn du in einem anderen Projekt jetzt> UART2 nutzen möchtest?
Ganz anders.
ich habe die Treiber für UART's, USB-CDC und so weiter in meinem
persönlichen Portfolio, gut ausprobiert und funktionabel. Anpassungen
braucht es dann bloß, wenn ich so einen Treiber auf einen neuen Chip
portieren will, denn da ändern sich eben Details. Ansonsten sind die
Headerfiles für alle Treiber systemübergreifend gleich. In config.c
werden dann solche Dinge wie das oben genannte 'stdio' geregelt und -
voila - alle höheren Firmwareschichten brauchen sich dadurch nie um
solche niederen Details zu kümmern. Da ist an keiner einzigen Stelle ein
"ersetzt überall die Register" nötig.
Sebastian V. schrieb:> Vielleicht gibt es irgendwas das man erst zur Laufzeit> festlegen möchte.
Geht ja auch. Neben 'stdio' kann man auch dediziert einzelne Kanäle
ansprechen oder zur Laufzeit den Kanal für 'stdio' zu verlegen. Es ist
alles drin und es ist dennoch sauber getrennt, so daß alle höheren
Schichten in der Firmware problemlos portiert werden können.
Bernd K. schrieb:> Was machst Du zum Beispiel wenn> Du zwei oder drei UARTs im selben Projekt gleichzeitig brauchst?
Dann binde ich selbige eben ein. Fertig. Wo ist das Problem?
Ich finde, du springst immer wieder schlichtweg zu kurz.
Natürlich sind die Treiber für jeden UART separat - was denn sonst?
Schließlich verwaltet ein jeder seine separaten I/O-Puffer und auch die
notwendigen Interrupt-Programme, die ja die Hauptarbeit leisten,
müssen separat sein.
Oder hast du etwa schon mal einen µC der ARM-Klasse gesehen, der keine
separaten Interrupthandler für die einzelnen Interrupts hat?
Ich nicht.
Eher noch gibt es für jeden einzelnen UART separate Interrupthandler für
Datenhandling und Fehlerhandling, siehe Kinetis. Da ist NIX mit System
Musketier (ein Handler für alle)
Also weite mal deinen Horizont. Mein Ansatz ist de facto exzellent
wartbar und effizient - im Gegensatz zu dem, was du (vermutlich)
bevorzugst. So wie du schreibst, möchtest du ein einziges Codestück für
alle Kanäle verwenden, beachtest dabei aber nicht, daß du dabei elend
oft zwischen den Kanälen unterscheiden mußt, was den Code aufbläht - und
wie du das Interrupt-Handling auf einen einzigen Handler legen willst,
ohne dir dabei selbst ein Bein zu stellen, hast du noch garnicht
bedacht.
W.S.
W.S. schrieb:> Deine beiden Ansätze erinnern mich an die Ideen von BWLlern zu> Waverprobern in der Halbleiterei:
Mir kam da vorhin eher die Idee von Ing vs Inf in den Sinn. ;-)
W.S. schrieb:> Natürlich sind die Treiber für jeden UART separat - was denn sonst?> Schließlich verwaltet ein jeder seine separaten I/O-Puffer
Nur die Daten sind getrennt, der ganze Code ist gemeinsam. Also 6 mal
FIFO-Puffer im RAM (aber nur einmal FIFO-Code im Flash), dreimal UART
Daten im RAM (aber nur einmal UART-Treiber-Code im Flash).
> und auch die> notwendigen Interrupt-Programme, die ja die Hauptarbeit leisten,> müssen separat sein.
Der besteht dreimal aus nur je einer einzigen Zeile, dem Aufruf des
gemeinsamen Handlers.
W.S. schrieb:> Sebastian V. schrieb:>> Nimmst du wirklich jedes mal dein uart1_init als Vorlage und>> ersetzt überall die Register wenn du in einem anderen Projekt jetzt>> UART2 nutzen möchtest?>> Ganz anders.> ich habe die Treiber für UART's, USB-CDC und so weiter in meinem> persönlichen Portfolio, gut ausprobiert und funktionabel.
Und für UART1 bis UART6 hast du dann 6mal den fast gleichen Code nur mit
den Registern getauscht?
A. K. schrieb:> Mir kam da vorhin eher die Idee von Ing vs Inf in den Sinn. ;-)
^^
Ich geb aber gerne zu, dass ich über das Thema selber früher auch anders
gedacht hatte. Nur das, was notwendig ist, zu implementieren, kann man
eben auch auf verschiedene Weisen lösen - das ist ja die Crux bei der
Softwareentwicklung. In 'ner Firmware ist das anders, als bei 'ner 150
kLOC Anwendung - das denke ich sollte klar sein.
Gut isses z.B., wenn es Coding-Guidelines gibt: dann schimpft zwar jeder
darüber, wie korinthenk*ckerisch die Guideline ist und was für tolle
Sprachfeatures man nicht nehmen darf - es sorgt aber auch dafür, dass
eine austauschbare Codebasis entsteht. Schlecht isses, wenn's keine
gibt: dann schreibt im Worst-Case jeder nach eigenem Gusto wie ihm die
Klaue gewachsen ist. Führt dann dazu, dass sich die in ihrerem
Teilprojekt erfahreneren Programmierer u.U. beschweren, warum die
weniger erfahrenen ständig mit Fragen gelatscht kommen, warum etwas so
oder so gelöst wurde und warum diese nicht einfach begreifen wollen,
dass es halt einfach funktioniert.
das gute an dem UART1->DR ist ja das die IDE hier oft mithilft.
oder bei anderen registern...
man muss nicht in 1,2,3,4 header files suchen wo denn nun die definition
steht
allein beim schreiben des -> kommt hier schon eine liste der
structurelemente
Ich persönlich arbeite gern mit der indirecten methode.
Ein Nachteil, den Registerbezeichner wie UART1_CTRL1 noch nach sich
ziehen, ist m.e.a. auch noch nicht genannt worden.
Wo kommt denn im Programm die Adresse her. Entweder wird die
Registeradresse mit zwei Befehlen MOV #imm16 und MOVT #imm16 geladen
oder die Adresse ist irgendwo im Flash gespeichertund wird mit LDR
geladen. Im letzen Fall schlaegt aber in den meisten Faellen die Flash
Zugriffslatenz zu. Bei UART1->CTRL1 wird man i.d.R. UART1 schon in einem
Register haben und das ganze ist ein einziger LDR/STR Befehl.
Sebastian V. schrieb:> Und für UART1 bis UART6 hast du dann 6mal den fast gleichen Code nur mit> den Registern getauscht?
Ja, so isses. Bei einem µC, wo man tatsächlich 6 UART's in Benutzung
hat, kommt es auf die 6x rund 100 Byte auch nicht mehr drauf an. Dafür
hat man aber eine komplette Entkopplung zwischen den verschiedenen
UART's. Denk bitte daran, daß deren Struktur und tatsächliche Benutzung
in solchen Fällen garantiert unterschiedlich ist. Da ist die komplette
Entkopplung überlebensnotwendig, es sei denn, man schlägt sich gern mit
Debug-Problemen herum: beim einen ist der Break zu ignorieren, beim
anderen muß darauf was Spezielles gemacht werden, der eine führt auf ne
simple V24 der nächste auf nen Modulator für IR oder so... usw usf.
Willst du zwecks Codezusammenfassung das alles in eine Funktion pressen?
Ich nicht.
Was da der Bernd schreibt, läuft jedesmal auf ein Datengewusel hinaus,
denn es reicht ja nicht, die verschiedenen Hardware-Registersätze zu
adressieren, nee, da kommen noch die Puffer dazu, die Pufferzeiger, die
NVIC-IR-Nummern, die oft unterschiedlichen Fehlerbehandlungen und so
weiter. Kurzum, ich halte es für wirklich ausgemachten Quatsch, das
alles in eine Multi-Funktion hineinpressen zu wollen. In deinem Falle
sechs mehr oder weniger unterschiedliche, aber recht kurze
Einzel-Funktionen aufgesplittet, ist wesentlich besser.
W.S.
Uwe B. schrieb:> Wo kommt denn im Programm die Adresse her. Entweder wird die> Registeradresse mit zwei Befehlen MOV #imm16 und MOVT #imm16 geladen> oder die Adresse ist irgendwo im Flash gespeichertund wird mit LDR> geladen.
Falls du den Keil verwendest, dann wirst du sehen, daß der es ganz
anders manch - je nach Optimierungsstrategie. Ich hab da schon gesehen,
daß er mit einer ausgeknobelten Basisadresse arbeitet, die man gar
keiner Peripherie zuordnen kann, die aber am besten geeignet ist, um
alle oder möglichst viele Hardware-Register dewr unterschiedlichsten
Peripherie-Cores damit zu erreichen. Offenbar interessiert den Keil
überhaupt nicht, ob man die Adressen einzeln liefert oder ob er irgend
einen struct zuvor in seine Einzelteile auseinandernehmen muß - er tut's
einfach und optimiert über alles, was im Blickfeld liegt.
W.S.
W.S. schrieb:> In deinem Falle sechs mehr oder weniger unterschiedliche, aber recht> kurze Einzel-Funktionen aufgesplittet, ist wesentlich besser.
Ich darf dich korrigieren?
6 Einzelfunktionen findest du(!) besser. Ob es wirklich besser ist, darf
angezweifelt werden. :-)
Grundsätzlich ist es eh schwer zu sagen, ob das eine oder das andere für
jede(!) Situation besser ist. Meistens wohl eher gemeinsame Routinen.
Redundanter Code? :-(
Beispiel aus "meiner Welt": Ich durfte UART-Treiber schreiben für eine
Karte, die 8 UARTs besitzt. Die Karte ist über Compact-PCI
angeschlossen. Ich habe für alle UARTs eine gemeinsame RX, TX, ....
Routine geschrieben. Wenn ich jetzt eine zweite Karte einstecken, habe
ich automatisch 16 UARTs ohne(!) Softwareänderung. Ich kann N Karten
stecken und muss nichts tun. Das PCI-Subsystem sagt mir einfach,
wieviele Karten diesen Typs vorhanden sind und der UART-Treiber verhält
sich entsprechend. Geht einfach. Mit einzelnen Routinen... Hmmmm
900ss D. schrieb:> Wenn ich jetzt eine zweite Karte einstecken,
In welchen Mikrocontroller willst du die einstecken?
Und wie realistisch ist das Ganze?
Es ist immer wieder die gleiche Krux mit Leuten, die alles auf die
Spitze treiben in Regionen, welche einfach im Abseits liegen - mal
fußballerisch gesagt. Was soll man mit einer Lösung für 1024 UART's und
deren Overhead, wenn man tatsächlich im praktischen Leben nur 1..2 davon
braucht?
In einem typischen µC hat man 1..4 UART's und evtl. 1..2 USART's und
davon 1 oder 2 tatsächlich in Benutzung, die anderen liegen tot, weil
erstens nicht gebraucht und zweitens kein Pin mehr dafür frei. Was also
soll das Beispiel mit 8 oder 16 UART's am PCI-Bus? Bloß um zu
widersprechen?
Mir ist das jetzt langsam zu doof.
W.S
W.S. schrieb:> wenn man tatsächlich im praktischen Leben nur 1..2 davon> braucht?
Schon beim zweiten sparst Du Zeit, Nerven und unnötige Redundanz und die
übrigen 1022 wären dann eine kostenlose Dreingabe.
W.S. schrieb:> In einem typischen µC hat man 1..4 UART's
1. Typisch ist da garnichts.
2. Es gibt viele "Welten", nicht nur die eine mit 1-4 UARTs. In meinem
Fall wurden erst 7 und dann 10 genutzt. Mehraufwand an Software auf
Treiberebene = 0. Woanders hatte ich schon 32 UARTs parallel in Betrieb.
Und das schafft auch ein ARM-Mikrocontroller.
Genau deshalb mein Beispiel.
Und auch bei 4 UARTs würde ich keinen redundanten Code schreiben.