Forum: Mikrocontroller und Digitale Elektronik GCC / ARM Cortex, Registerzugriff über Pointer & struct


von Ralf (Gast)


Lesenswert?

Hallo,

beim ARM Cortex Kern muss der Zugriff auf SFRs ja über Pointer gemacht 
werden, in der Regel werden die Pointer so angelegt:
1
#define REG (volatile uint8_t *) REG_ADR  //REG_ADR = Register-Adresse

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
typedef volatile union {
2
  volatile struct {
3
    volatile uint8_t Bit0 : 1;
4
    volatile uint8_t Bits3_1 : 3;
5
    volatile uint8_t : 1;  //Freiraum
6
    volatile uint8_t Bit5 : 1;
7
    volatile uint8_t Bit6 : 1;
8
    volatile uint8_t Bit7 : 1;
9
  };
10
  volatile uint8_t RegValue;
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

von Clemens L. (c_l)


Lesenswert?

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:
1
#define REG ((volatile _REG*) REG_ADDR)

: Bearbeitet durch User
von Jim M. (turboj)


Lesenswert?

> Wäre obiger Code dann so korrekt?

IMO müsstest Du der Struct einen Namen geben:
1
typedef union {
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.

von W.S. (Gast)


Lesenswert?

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.

von Dr. Sommer (Gast)


Lesenswert?

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.

von Dr. Sommer (Gast)


Lesenswert?

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:
1
#include <stdio.h>
2
3
#define     __IO    volatile             /*!< Defines 'read / write' permissions              */
4
5
// Definition struct für GPIO-Register
6
typedef struct
7
{
8
  __IO uint32_t MODER;    /*!< GPIO port mode register,               Address offset: 0x00      */
9
  __IO uint32_t OTYPER;   /*!< GPIO port output type register,        Address offset: 0x04      */
10
  __IO uint32_t OSPEEDR;  /*!< GPIO port output speed register,       Address offset: 0x08      */
11
  __IO uint32_t PUPDR;    /*!< GPIO port pull-up/pull-down register,  Address offset: 0x0C      */
12
  __IO uint32_t IDR;      /*!< GPIO port input data register,         Address offset: 0x10      */
13
  __IO uint32_t ODR;      /*!< GPIO port output data register,        Address offset: 0x14      */
14
  __IO uint16_t BSRRL;    /*!< GPIO port bit set/reset low register,  Address offset: 0x18      */
15
  __IO uint16_t BSRRH;    /*!< GPIO port bit set/reset high register, Address offset: 0x1A      */
16
  __IO uint32_t LCKR;     /*!< GPIO port configuration lock register, Address offset: 0x1C      */
17
  __IO uint32_t AFR[2];   /*!< GPIO alternate function registers,     Address offset: 0x20-0x24 */
18
} GPIO_TypeDef;
19
20
// Definition der Adresse der Register für Port B
21
#define PERIPH_BASE           ((uint32_t)0x40000000) /*!< Peripheral base address in the alias region                                */
22
#define AHB1PERIPH_BASE       (PERIPH_BASE + 0x00020000)
23
#define GPIOB_BASE            (AHB1PERIPH_BASE + 0x0400)
24
#define GPIOB               ((GPIO_TypeDef *) GPIOB_BASE)
25
26
27
// Test Zugriff auf Register von Port B über struct
28
void structtest () {
29
  GPIOB->MODER = 42;
30
  GPIOB->ODR = 1337;
31
}
32
33
// Definition der Port-Register von Port B ohne struct, direkt über die Adresse
34
#define GPIOB_MODER (*((volatile uint32_t*) 0x40020400))
35
#define GPIOB_ODR (*((volatile uint32_t*) 0x40020414))
36
37
// T est Zugriff auf Register von Port B ohne struct
38
void directtest () {
39
  GPIOB_MODER = 42;
40
  GPIOB_ODR = 1337;
41
}
Zusammenkopiert aus den Headern von ST.
Kompiliert & Dissasembliert mit
1
arm-none-eabi-gcc -Os -mthumb -mcpu=cortex-m4 -c test.c -o test.o
2
arm-none-eabi-objdump -d test.o
ergibt sich
1
00000000 <structtest>:
2
   0:  4b03        ldr  r3, [pc, #12]  ; (10 <structtest+0x10>)
3
   2:  222a        movs  r2, #42  ; 0x2a
4
   4:  601a        str  r2, [r3, #0]
5
   6:  f240 5239   movw  r2, #1337  ; 0x539
6
   a:  615a        str  r2, [r3, #20]
7
   c:  4770        bx  lr
8
   e:  bf00        nop
9
  10:  40020400   .word  0x40020400
10
11
00000014 <directtest>:
12
  14:  4b03        ldr  r3, [pc, #12]  ; (24 <directtest+0x10>)
13
  16:  222a        movs  r2, #42  ; 0x2a
14
  18:  601a        str  r2, [r3, #0]
15
  1a:  f240 5239   movw  r2, #1337  ; 0x539
16
  1e:  615a        str  r2, [r3, #20]
17
  20:  4770        bx  lr
18
  22:  bf00        nop
19
  24:  40020400   .word  0x40020400
Man sieht, es kommt exakt der gleiche Code heraus.

Schaltet man die Optimierung ab (-O0), ist die Variante ohne struct 
sogar langsamer & größer...

von Dr. Sommer (Gast)


Lesenswert?

PS: Hups, es sollte natürlich "#include <stdint.h>" sein.

von (prx) A. K. (prx)


Lesenswert?

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. ;-)

: Bearbeitet durch User
von (prx) A. K. (prx)


Lesenswert?

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.

: Bearbeitet durch User
von S. R. (svenska)


Lesenswert?

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?

von W.S. (Gast)


Lesenswert?

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.

von Bernd K. (prof7bit)


Lesenswert?

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?

von S. R. (svenska)


Lesenswert?

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).

von W.S. (Gast)


Lesenswert?

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.

von W.S. (Gast)


Lesenswert?

Nachtrag, so in etwa diesem Stil:
1
/**************************  MCG  ****************************************/
2
// Chapter 25 Multipurpose Clock Generator (MCG)
3
4
#define MCG_C1    (*((volatile byte  *) 0x40064000)) //  MCG Control 1 Register (MCG_C1) 8 R/W 04h 25.3.1/470
5
#define MCG_C2    (*((volatile byte  *) 0x40064001)) //  MCG Control 2 Register (MCG_C2) 8 R/W 80h 25.3.2/472
6
#define MCG_C3    (*((volatile byte  *) 0x40064002)) //  MCG Control 3 Register (MCG_C3) 8 R/W Undefined 25.3.3/473
7
#define MCG_C4    (*((volatile byte  *) 0x40064003)) //  MCG Control 4 Register (MCG_C4) 8 R/W Undefined 25.3.4/474
8
#define MCG_C6    (*((volatile byte  *) 0x40064005)) //  MCG Control 6 Register (MCG_C6) 8 R/W 00h 25.3.5/475
9
#define MCG_S     (*((volatile byte  *) 0x40064006)) //  MCG Status Register (MCG_S) 8 R 10h 25.3.6/476
10
#define MCG_SC    (*((volatile byte  *) 0x40064008)) //  MCG Status and Control Register (MCG_SC) 8 R/W 02h 25.3.7/477
11
#define MCG_ATCVH (*((volatile byte  *) 0x4006400A)) //  MCG Auto Trim Compare Value High Register (MCG_ATCVH) 8 R/W 00h 25.3.8/478
12
#define MCG_ATCVL (*((volatile byte  *) 0x4006400B)) //  MCG Auto Trim Compare Value Low Register (MCG_ATCVL) 8 R/W 00h 25.3.9/478
13
#define MCG_C7    (*((volatile byte  *) 0x4006400C)) //  MCG Control 7 Register (MCG_C7) 8 R/W 00h 25.3.10/479
14
#define MCG_C8    (*((volatile byte  *) 0x4006400D)) //  MCG Control 8 Register (MCG_C8) 8 R/W See section 25.3.11/479

W.S.

von Bernd K. (prof7bit)


Lesenswert?

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.

: Bearbeitet durch User
von S. R. (svenska)


Lesenswert?

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.

von (prx) A. K. (prx)


Lesenswert?

Und was ich nicht verstehe: Wie schreibt im W.S.-Stil Funktionen, die 
gleichermassen für verschiedene UARTs nutzbar sind?

Etwa so?
1
void uart_init(int no) 
2
{
3
   switch (no) {
4
   case 1: UART1_CTRL1 = ...; break;
5
   case 2: UART2_CTRL1 = ...; break;
6
   case 3: UART3_CTRL1 = ...; break;
7
   default: crash_and_burn();
8
   }
9
}
Mir käm da eher das in den Sinn:
1
void uart_init(UART_t *uart) 
2
{
3
   uart->CTRL1 = ...;
4
}
Ggf. mit assert() drin, für optionalen Parametercheck beim Debug.

: Bearbeitet durch User
von W.S. (Gast)


Lesenswert?

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.

von (prx) A. K. (prx)


Lesenswert?

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.

: Bearbeitet durch User
von W.S. (Gast)


Lesenswert?

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.

von (prx) A. K. (prx)


Lesenswert?

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. ;-)

: Bearbeitet durch User
von Falk S. (db8fs)


Lesenswert?

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:
1
#define MY_DATA_OP1 1                                          
2
#define MY_DATA_OP2 2
3
#define MY_DATA_OP3 3
4
5
class BaseA                                                    
6
{                                                              
7
public:                                                        
8
    virtual ~BaseA() {}                                        
9
                                                               
10
    virtual void getData(int, void*, size_t, int) = 0;         
11
};
12
13
class SampleA : public BaseA                                   
14
{                                                              
15
public:                                                        
16
    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.

von (prx) A. K. (prx)


Lesenswert?

A. K. schrieb:
> Nein. Es wäre bei mir DMA->TCD[3]->NBYTES_MLNO.

Fix: DMA->TCD[3].NBYTES_MLNO / DMA->TCD[DMAchannel].NBYTES_MLNO;

: Bearbeitet durch User
von (prx) A. K. (prx)


Lesenswert?

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.

: Bearbeitet durch User
von 900ss (900ss)


Lesenswert?

A. K. schrieb:
> ein preemptive realtime scheduler

OT: Welcher denn? Oder selbstgehäkelt?
Danke.
900ss

von (prx) A. K. (prx)


Lesenswert?

900ss D. schrieb:
>> ein preemptive realtime scheduler
>
> OT: Welcher denn?

AvrX

Die ursprüngliche Webseite dazu gibts nicht mehr. Den Code aber schon: 
https://github.com/kororos/AvrX

PS: Das Internet vergisst nichts:
https://web.archive.org/web/20090224191823/http://barello.net/avrx/overview.htm

: Bearbeitet durch User
von W.S. (Gast)


Lesenswert?

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.

von Sebastian V. (sebi_s)


Lesenswert?

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.

: Bearbeitet durch User
von Bernd K. (prof7bit)


Lesenswert?

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?

: Bearbeitet durch User
von Falk S. (db8fs)


Lesenswert?

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.

von W.S. (Gast)


Lesenswert?

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.

von (prx) A. K. (prx)


Lesenswert?

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. ;-)

von Bernd K. (prof7bit)


Lesenswert?

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.
1
/**
2
 * Pseudo-Code, unvollständig, nur zur
3
 * Veranschaulichung des Arguments
4
 */
5
6
static void on_interrupt(int number) {
7
    struct uart* self = instances[number];
8
    fifo_put(&self->rx, self->USARTx->DR);
9
}
10
11
void USART1_IRQHandler() {
12
    on_interrupt(0);
13
}
14
15
void USART2_IRQHandler() {
16
    on_interrupt(1);
17
}
18
19
void USART6_IRQHandler() {
20
    on_interrupt(2);
21
}

von Sebastian V. (sebi_s)


Lesenswert?

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?

von Falk S. (db8fs)


Lesenswert?

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.

von 7856ujtzuitzu (Gast)


Lesenswert?

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.

von Uwe B. (Firma: TU Darmstadt) (uwebonnes)


Lesenswert?

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.

von (prx) A. K. (prx)


Lesenswert?

Uwe B. schrieb:
> Bei UART1->CTRL1 wird man i.d.R. UART1 schon in einem
> Register haben und das ganze ist ein einziger LDR/STR Befehl.

GCC ist klug genug, konstante Adressen auf Nähe zu untersuchen:
Beitrag "Re: GCC / ARM Cortex, Registerzugriff über Pointer & struct"

Bei anderen Compilern wäre das zu untersuchen.

: Bearbeitet durch User
von Uwe B. (Firma: TU Darmstadt) (uwebonnes)


Lesenswert?

Wenn Deine Funktion aber schon die Bsisadresse als Argument bekommt, 
dann braucht die Routine auch nicht meher die Basisadresse zu laden...

von W.S. (Gast)


Lesenswert?

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.

von W.S. (Gast)


Lesenswert?

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.

von W.S. (Gast)


Lesenswert?

Nachtrag: "anders macht"

von 900ss (900ss)


Lesenswert?

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

: Bearbeitet durch User
von W.S. (Gast)


Lesenswert?

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

von Bernd K. (prof7bit)


Lesenswert?

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.

von Wertha (Gast)


Lesenswert?

W.S. schrieb:

> Mir ist das jetzt langsam zu doof.

Du hast dich einfach verrannt.

von 900ss (900ss)


Lesenswert?

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.

: Bearbeitet durch User
Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.