Forum: PC-Programmierung C Objekte als Struct effizient kapseln


von Martin J. (bluematrix) Benutzerseite


Lesenswert?

Hallo an Alle,
benötige ich bei Microcontroller-Anwendungen mehrere Instanzen von 
verschiedenen Programmteilen, so realisiere ich dies durch die Kapselung 
in einem Struct (unten das Beispiel wenn ich mehrere DACs von einem Typ 
verbaut habe).
Ist man konsequent in dieser Umsetzung, dann erhält man eine sehr gut 
leserliche, übersichtliche und in sich "gekapselte" Software. Leider 
erhält man durch die ganzen Übergaben auch viel overhead an code.

Wie macht ihr das, bzw. was muss man beachten um das Ganze trotzdem so 
effizient wie möglich umzusetzen.
1
typedef struct ad5660_data_t  {
2
  uint8_t    pin_cs;
3
  PORT_t    *port_cs;
4
  SPI_Master_t  *spi;
5
}ad5660_data_t;
6
7
8
void ad5660_init(ad5660_data_t *ad_data, SPI_Master_t *spi, PORT_t *port_cs, uint8_t pin_cs)
9
{
10
  ad_data->pin_cs    = pin_cs;
11
  ad_data->port_cs  = port_cs;
12
  ad_data->spi    = spi;
13
    
14
  sbi(ad_data->port_cs->OUT, ad_data->pin_cs);  
15
  sbi(ad_data->port_cs->DIR, ad_data->pin_cs);  
16
  
17
  ad5660_setOutput(ad_data, AD5660_PWR_NORMAL, 0);
18
}
19
20
void ad5660_setOutput(ad5660_data_t *ad_data, uint8_t mode, uint16_t data)
21
{
22
  cbi(ad_data->port_cs->OUT, ad_data->pin_cs);  
23
  
24
  SPI_MasterTransceiveByte(ad_data->spi,mode);  
25
26
  SPI_MasterTransceiveByte(ad_data->spi, data >> 8);  
27
  SPI_MasterTransceiveByte(ad_data->spi, data & 0xff);
28
  
29
  sbi(ad_data->port_cs->OUT, ad_data->pin_cs);  
30
}

von Hans (Gast)


Lesenswert?

Der Code sieht meiner Meinung nach nicht ineffizient aus. Hast Du mal 
nachgeschaut bzw. gemessen, wie viel "Overhead" entsteht? Und im 
Vergleich zu was?

Ein paar Performance-Tipps:
- Optimierung des Compilers einschalten (GCC: -Os)
- Link Time Optimization einschalten (GCC: -flto bei Compiler und 
Linker)
- Den am häufigsten benutzten Member in der Struktur an die erste 
Position setzen. Dieser Member kann ohne Addition eines Offset auf den 
Zeiger auf die Struktur verwendet werden.
- Darauf achten, dass in der Struktur keine Lücken durch 
unterschiedliche Alignment-Anforderungen der Member entstehen.
- Ggf. mehrmals innerhalb einer Funktion über Funktionsaufrufe hinweg 
verwendete Member in lokale Variablen kopieren. Der Compiler weiß z.B. 
nicht unbedingt, dass SPI_MasterTransceiveByte den Wert von ad_data->spi 
nicht ändert. Dann muss er ihn jedes Mal neu aus der Struktur laden 
anstatt ihn sich in einem Register zu merken.

Zum Software-Design:
Ich würde die Zugriffe auf Pins in eine eigene Klasse auslagern, sprich 
pin_cs und port_cs in einer Struktur mit entsprechenden 
Zugriffsfunktionen kapseln. Die sbi/cbi-Aufrufe mit zweifacher 
Dereferenzierung in die Struktur verletzen nämlich das "Law of Demeter":
https://de.wikipedia.org/wiki/Gesetz_von_Demeter

von Roland E. (roland0815)


Lesenswert?

Die ganzen Struck-Geschichten sind für uns Menschen zum besseren 
Verständnis, aber für den Kontrollör doch nur Variablen. Ich denke, bei 
einem modernen Compiler kommt gleich viel Code raus, wie wenn ein ganzer 
Haufen diskreter Variablen angelegt wird.

von MaWin (Gast)


Lesenswert?

Martin J. schrieb:
> Leider erhält man durch die ganzen Übergaben auch viel overhead an code.

Du erfindest gerade C++ mit dem self pointer neu.

Ja, damit kann man ineffizient programmieren

Roland E. schrieb:
> Ich denke, bei einem modernen Compiler kommt gleich viel Code raus, wie
> wenn ein ganzer Haufen diskreter Variablen angelegt wird.

Leider nein. Indirekte Zugriffe über pointer und offset sind schon 
aufwändiger als Zugriffe an absolut adressierte globale Variablen, aber 
wenn man nicht 1 sondern mehrere Schnittstellen bedienen will, ist der 
pointer oft effektiver als ein array mit Index.

von Volle22 (Gast)


Lesenswert?

Martin J. schrieb:
> Wie macht ihr das, bzw. was muss man beachten um das Ganze trotzdem so
> effizient wie möglich umzusetzen.


Deine Definition  von Effizient ist  "ungewöhnlich"

Mehr Aufwand bei Codieren und Verschwendung von Speicherplatz  nennen 
ich immer Ineffizient



Strukturen werden alligent erzeugt d.h zwischen dem ersten Byte und dem 
Zeiger sind bei dir drei Byte Lücke.

Es kann hilfreich sein relativ zu einem Basiszeiger zu Adressieren
da sind solche Modulstruckturen hilfreich. Lohnt  i.d.r aber den Aufwand 
nicht.


eine gute Möglichkeit für Kapselung nennt sich C++

von Felix Adam (Gast)


Lesenswert?

Dass diese Kapselung mit C++ besser geht stimmt schon. Leider gibt es 
aber Debugger (oder IDEs mit Debugger), die diese Klassen (genauer ihre 
Variablenwerte) nicht anzeigen können. Als Beispiel hier AvrStudio 6.2 
mit den JTAGICE3 oder auch dem Atmel ICE.

Und davon abgesehen wird das auf die oben genannte Weise selbst bei den 
ARM-Controllern so gehandhabt (siehe STM32 mit den Libraries).

Allerdings kenne ich den Grund dafür nicht, er würde mich aber sehr 
interessieren.

von Roland E. (roland0815)


Lesenswert?

MaWin schrieb:
>
> Leider nein. Indirekte Zugriffe über pointer und offset sind schon
> aufwändiger als Zugriffe an absolut adressierte globale Variablen, aber
> wenn man nicht 1 sondern mehrere Schnittstellen bedienen will, ist der
> pointer oft effektiver als ein array mit Index.

Woher will man wissen, dass der Compiler ersteres verwendet und nicht 
letzteres? Richtig, in dem man sich den Assembler ansieht.

Meine Erfahrungen haben bisher keinen Unterschied im Ergebnis zwischen 
beiden Varianten im C-Code ergeben. Für den Rechner ist es ja auch 
völlig wumpe, wie wir doofen Menschen die Adresse 0xirgendwas nennen.

: Bearbeitet durch User
von Yalu X. (yalu) (Moderator)


Angehängte Dateien:

Lesenswert?

Martin J. schrieb:
> Ist man konsequent in dieser Umsetzung, dann erhält man eine sehr gut
> leserliche, übersichtliche und in sich "gekapselte" Software. Leider
> erhält man durch die ganzen Übergaben auch viel overhead an code.

Das ist der Preis für die Generizität der Funktionen :)

Bei einem ARM- oder i86-Prozessor hält sich der Overhead in Grenzen. Ich
vermute aber, du verwendest eher einen einfachen Controller wie bspw.
einen AVR, der sich in diesem Fall in zwei Dingen recht schwer tut:

- Er kann nicht besonders gut mit Pointern umgehen, da er zum einen nur
  wenige Adressregister (X, Y, und Z) und keine indirekte Adressierung
  mit Displacement hat.

- Er kann nativ keine Shifts mit variabler Shift-Weite (wie sie hier für
  die Bit-Maskierung benötigt werden). Sie können nur in Form von
  Schleifen implementiert werden.

Allgemein und perfekt lässt sich dieses Problem leider nicht lösen, aber
ein Bisschen kann man dem Compiler schon auf die Sprünge helfen, indem
man ihm schon zur Compilezeit möglichst viele Informationen liefert, die
er zur Optimierung verwenden kann.

In deinem Beispiel gibt es mehrere Strukturen, die Konfigurationsdaten
wie bspw. Ports, Bitnummern u.ä. enthalten. Diese Daten sind meist
unveränderlich, aber der Compiler weiß das nur, wenn man ihn explizit
darauf hinweist.

Wenn man diese Strukturen als const deklariert und dafür sorgt, dass der
Compiler jederzeit ihre Initialisierungswerte sehen kann, ist schon viel
gewonnen. Strukturen die, in mehreren Übersetzungseinheiten benötigt
werden, sollten deswegen in einem gemeinsam genutzten Header-File als
static const definiert und initialisiert werden. Das static bewirkt
zudem oft, dass die Strukturen nicht einmal Speicherplatz belegen.

Wird so eine konstante Struktur als Pointer-Argument an eine Funktion
übergeben, geht die Information über die Initialisierungswerte leider
verloren, es sei denn, die Funktion wird geinlinet. Dann kann der
Compiler für jeden Funktionsaufruf einen der Situation angepassten,
optimalen Code generieren. Inlinen kann der Compiler aber nur dann, wenn
er den Quellcode der Funktion sehen kann. Sollen die zu optimierenden
Funktionen aus mehreren Übersetzungseinheiten aufrufbar sein, sollte man
sie ebenfalls in einem gemeinsam genutzten Header-File definiert werden,
und zwar als static inline (ggf. auch mit dem GCC-spezifischen Attribut
always_inline).

Da dein Beispiel unvollständig ist (wichtige Funktionsdefinitionen und
Strukturdeklarationen fehlen), konnte ich dein Beispiel nicht gemäß den
obigen Vorschlägen umschreiben. Stattdessen habe ich mir ein einfaches,
aber trotzdem nicht zu triviales Beispiel ausgedacht (s. Anhang). Darin
wird eine 8-bit-parallele Datenübertragung (IEEE 1284 in abgespeckter
Form) implementiert.

Wie in deinem Beispiel sind sämtliche Konfigurationsdaten in Strukturen
festgelegt, und die Ausführung erfolgt über mehrere Unterprogrammebenen.
Sämtliche typedef-Deklarationen und static-Definitionen können auch in
ein Header-File ausgelagert werden, so dass sie auch in mehreren
Übersetzungseinheiten verfügbar sind.

Durch die Implemetierung gemäß den obigen Vorschlägen ist der Compiler
in der Lage, den gesamten Code auf eine einzige Funktion (test) zu
reduzieren, die nur die tatsächlich erforderlichen I/O-Befehle enthält.

Hier ist der vom Compiler generierte Assembler-Output:

1
test:
2
  ldi r24,lo8(-1)
3
  out 0x17,r24
4
  sbi 0x14,3
5
  sbi 0x15,3
6
  ldi r24,lo8(123)
7
  out 0x18,r24
8
  cbi 0x15,3
9
  sbi 0x15,3
10
.L2:
11
  sbic 0x13,5
12
  rjmp .L2
13
  ret

Datenspeicher wird außer für den Programmstack keiner benötigt. Die
Ausgabe von avr-size:

1
   text    data     bss     dec     hex filename
2
     22       0       0      22      16 parport.o

Besser geht es auch mit handgeschriebenem Assembler nicht.

Lässt man zum Vergleich alle const und static weg, dann entsteht an
ähnlichen Stellen wie in deinem Beispiel ein deutlicher Overhead. Der
Programmcode verzehnfacht sich (bei der Laufzeit dürfte der Faktor noch
größer sein), und es kommen 18 Bytes Daten für die Strukturen (sowohl im
Flash als auch im RAM) hinzu:

1
   text    data     bss     dec     hex filename
2
    224      18       0     242      f2 parport.o

Das hört sich jetzt alles ganz toll an, allerdings hat die Methode auch
einen Nachteil: Die Optimierung funktioniert nur dann so gut, wenn alle
beteiligten Funktionen geinlinet werden. Das führt aber bei komplexeren
Funktionen, die von vielen Stellen im Programm aufgerufen werden, zu
wesentlich mehr Programmcode. In diesem Fall sollte man die Methode auf
zeitkritische Programmteile beschränken, wenn man nicht gerade Flash-
Speicher im Überfluss hat.

von Martin J. (bluematrix) Benutzerseite


Lesenswert?

Hallo,

vielen Dank für die vielen Antworten.
Verwendet werden diverse kleiner Arm- oder Xmega-Controller.
Die Xmega verwende ich einfach sehr gerne auf Grund ihrer tollen 
Peripherie. Diese Peripherie schreit ja auch regelrecht nach 
Instanzierung oder C++.

Bei einer Diskussion wie dieser wird immer gleich C++ genannt. Bei 
meiner Suche bin ich aber auf keine wirklich guten Beispiele/Vorlagen 
gestoßen. Alle C++ Bibliotheken für Controller sind nur zur Hälfte oder 
kaum fertig und unterstützen nur die einfachsten Hardwarefunktionen. 
Neben den Hardware-Bibliotheken hat man dann auch das grundlegende 
Problem mit Interruptroutinen. siehe auch die Diskussionen hier 
Beitrag "C++ für Mikrocontroller"

Bei all den Möglichkeiten, fand ich die Umsetzung mit den Structs in C 
am sinnvollsten. Daher auch die Frage was man hier noch beachten muss um 
solch eine Umsetzung noch so effizient wie möglich zu bekommen.

Wer gute Bibliothken oder Vorlagen für C++ hat soll diese nennen.

von Felix Adam (Gast)


Angehängte Dateien:

Lesenswert?

Hier mal exemplarisch für SPI und Timer auf dem XMega. Aber wie 
geschrieben lassen sich diese Klassen schlecht debuggen.

von Martin J. (bluematrix) Benutzerseite


Lesenswert?

Hallo Felix,

danke für das Beispiel, so wie ich dein SPI-Code verstehe, kann ich 
diesen immer für eine SPI Schnittstelle verwenden. Da ich den 
verwendeten SPI-Port ja über globale #defines vorgebe. Damit bringt mir 
ja die Flexibilität von C++ nix.

von Felix Adam (Gast)


Lesenswert?

Kann man so sehen.


Hier mal das Erstellen und Nutzen als Beispiel:
1
static SpiMaster*               p_spi_master;
2
3
int main (void) {
4
    cli();
5
6
    SpiMaster             spi_master(&SPID, SPI_INTLVL_HI_gc);
7
    p_spi_master = &spi_master;
8
9
    PMIC.CTRL = (1<<PMIC_HILVLEN_bp) | (1<<PMIC_MEDLVLEN_bp) | (1<<PMIC_LOLVLEN_bp);
10
    sei();
11
12
13
    while (1) {
14
        // tue hier was
15
        data[0] = 12;
16
        data[1] = 17;
17
        data[2] = 19;
18
19
        spi_master->setBuffer(data, data, &active);
20
        spi_master->config(spi_mode, spi_prescaler, DUMMY_BYTE);
21
        spi_master->startTransfer(3, 0, 0, cs_port, cs_pin);
22
23
        // tue was anderes
24
    }
25
}
26
27
// ISR for SPI master handler
28
ISR (SPID_INT_vect)
29
{
30
    p_spi_master->handler();
31
}


Du kannst auch zusätzlich für SPI auf Port C einen anlegen. Und ggfs. 
für Port E. Punkt ist, dass der Code nur einmal vorhanden ist. Dank der 
Klasse sind die Daten aber für die drei Schnittstellen gekapselt.



Ich denke, der einzige Vorteil ist hier, dass du die Daten nicht 
außerhalb der spi.c/h halten musst, sondern diese nur innerhalb der 
Klasse existieren.

Aber aufgrund der Debugprobleme habe ich SPI aschon wieder auf Structs 
umgebaut. Und es wird damit nicht merkbar größer. Allerdings muss mah 
dann den Struct immer mitführen, was bei der Klasse nicht der Fall ist. 
Hier passiert das durch ihren Namen.

Ich denke, das ist letztlich eine Frage der eigenen Präferenz. Und 
jemand sagte mir mal: "C++ auf Mikrocontrollern tut man einfach nicht".

Es geht aber, wenn man will...

von Martin J. (bluematrix) Benutzerseite


Lesenswert?

Vielen Dank für das Beispiel. Das mit den Debug-Problemen bei C++ war 
mir noch nicht bekannt.
Ich setz mich nachher nochmal hin und werd mein Beispiel und die 
Umsetzung mit den Structs noch etwas ausführlicher machen.

von Martin J. (bluematrix) Benutzerseite


Angehängte Dateien:

Lesenswert?

Hallo Yalu X. im Anhang ist eine ausführlichere Version meiner 
Umsetzung.
Wie kann man den Code unter Verwendung von Structs schneller machen?
Die größe des Code ist nicht so kritisch, da die heutigen Controller 
meist genug Speicher haben.

von Wilhelm M. (wimalopaan)


Lesenswert?

Felix Adam schrieb:

> Ich denke, das ist letztlich eine Frage der eigenen Präferenz. Und
> jemand sagte mir mal: "C++ auf Mikrocontrollern tut man einfach nicht".

Diese Aussage kommt regelmäßig von Leuten, die kein vertieftes Wissen 
über C++ und speziell ab c++14/17 haben. Dinge wie constexpr, 
variadische templates, ... und auch constraints / concepts (zugegeben, 
ist noch nicht im Standard, kann der g++ aber schon wunderbar).

Aber die Diskussion dazu hier in diesem Forum ist sehr emotionsgeladen 
... be warned!

von Martin J. (bluematrix) Benutzerseite


Lesenswert?

Hallo Felix,
hast du noch mehr von deinen Xmega C++ Bibliothken. Ich würde das Ganze 
gerne mal testen, ohne gleich alles neu zu erfinden.
Danke.

von Felix Adam (Gast)


Lesenswert?

Hab dir ne Mail geschickt. Allerdings muss ich erst noch Beispiele 
sammeln, damit ein Upload hier auch Sinn macht. Sonst dürfte es eine 
ganze Menge Nachfragen bezüglich der Nutzung geben...

Schöne Ostern.

von Martin J. (bluematrix) Benutzerseite


Lesenswert?

Vielen, vielen Dank. Habe alles erhalten und werde in nächster Zeit mal 
bissl rumprobieren.
Dir auch frohe Ostern.

von Carl D. (jcw2)


Lesenswert?

Wilhelm M. schrieb:
> Felix Adam schrieb:
>
>> Ich denke, das ist letztlich eine Frage der eigenen Präferenz. Und
>> jemand sagte mir mal: "C++ auf Mikrocontrollern tut man einfach nicht".
>
> Diese Aussage kommt regelmäßig von Leuten, die kein vertieftes Wissen
> über C++ und speziell ab c++14/17 haben. Dinge wie constexpr,
> variadische templates, ... und auch constraints / concepts (zugegeben,
> ist noch nicht im Standard, kann der g++ aber schon wunderbar).
>
> Aber die Diskussion dazu hier in diesem Forum ist sehr emotionsgeladen
> ... be warned!

In C++14 kann man mit variadic Templates ganz problemlos einzelne 
Io-Pins abbilden, die man zu PinSets zusammenbauen kann, die einen 
Werte-Typ haben, die physisch auf verschiedenen Addressen liegen, z.B. 
WGM13..WGM10 und denen man einem (erlaubten) Wert aus der zugehörigen 
enumeie-Klasse zuweisen kann. Am Ende stehen Zugriffe auf die 2 Register 
TCCR1A/B.
Aber: zu emotionsgeladen um es zu veröffentlichen.

von Wilhelm M. (wimalopaan)


Lesenswert?

Carl D. schrieb:
> Wilhelm M. schrieb:
>> Felix Adam schrieb:
>>
>>> Ich denke, das ist letztlich eine Frage der eigenen Präferenz. Und
>>> jemand sagte mir mal: "C++ auf Mikrocontrollern tut man einfach nicht".
>>
>> Diese Aussage kommt regelmäßig von Leuten, die kein vertieftes Wissen
>> über C++ und speziell ab c++14/17 haben. Dinge wie constexpr,
>> variadische templates, ... und auch constraints / concepts (zugegeben,
>> ist noch nicht im Standard, kann der g++ aber schon wunderbar).
>>
>> Aber die Diskussion dazu hier in diesem Forum ist sehr emotionsgeladen
>> ... be warned!
>
> In C++14 kann man mit variadic Templates ganz problemlos einzelne
> Io-Pins abbilden, die man zu PinSets zusammenbauen kann, die einen
> Werte-Typ haben, die physisch auf verschiedenen Addressen liegen, z.B.
> WGM13..WGM10 und denen man einem (erlaubten) Wert aus der zugehörigen
> enumeie-Klasse zuweisen kann. Am Ende stehen Zugriffe auf die 2 Register
> TCCR1A/B.
> Aber: zu emotionsgeladen um es zu veröffentlichen.

Genau!
Und noch hinzu kommt mit dem g++ die Realisierung von concepts / 
constraints (concepts-lite). Und das ist für die generische 
Programmierung in C++ ein echter Hit wie ich finde: keine Angst mehr vor 
template-code-Fehlermeldungen, und es ist Überladung aufgrund von 
constraints möglich. Thats awesome ...

Aber posten werde ich hierzu in diesem Forum nichts mehr :-(

von Martin J. (bluematrix) Benutzerseite


Lesenswert?

Hallo Wilhelm,
gern bin ich auch an deiner Umsetzung interessiert. Bitte auch etwas 
mehr alls nur ein IO Beispiel. Wie wäre die Lösung zum Beispiel mit SPI 
oder TWI bei mehreren gleichen Slve-Bausteinen.

Mit Templates habe ich selber noch nicht wirklich gearbeitet.
Vielelicht noch kurz einige Vorteile/Nachteile zu dem anderen Vorgehen.

Viele Grüße
Martin

: Bearbeitet durch User
von Martin J. (bluematrix) Benutzerseite


Lesenswert?

abend ...
Hab nochmal im Netz gestöbert und bin bei der C++ Lösung mit Templates 
auf die folgenden Bibliotheken gestoßen: 
https://github.com/pichenettes/avrilx-examples
1
using namespace avrlibx;
2
3
Gpio<PortF, 0> led_a;
4
Gpio<PortF, 1> led_b;
5
6
typedef SPIMaster<PortD,Gpio<PortD, 4>,MSB_FIRST,SPI_PRESCALER_CLK_4,true> SpiInterface;
7
typedef SdCard<SpiInterface> SdCardInterface;
8
FATFileReader<SdCardInterface> reader;
9
10
void LedsPattern(uint8_t error) {
11
  led_a.High();
12
  led_b.set_value(error);
13
  for (uint8_t i = 0; i < 10; ++i) {
14
    ConstantDelay(100);
15
    led_a.Toggle();
16
    led_b.Toggle();
17
  }
18
}
19
20
int main(void) {
21
  SysInit();
22
  led_a.set_direction(OUTPUT);
23
  led_b.set_direction(OUTPUT);
24
  
25
  FsHandle h;
26
  uint8_t read_byte;
27
  // If the file TEST.TXT is present in the card and its first byte is '7',
28
  // Alternating blink pattern on the two LEDs.
29
  if (reader.Init() == FFR_OK && reader.Open("TEST    TXT", &h) == FFR_OK &&
30
      reader.Read(&h, 1, &read_byte) == 1 && read_byte == '7') {
31
    LedsPattern(0);
32
  } else {
33
    // Otherwise, the LEDs blink together.
34
    LedsPattern(1);
35
  }
36
  
37
  while (1);
38
}

von Eric B. (beric)


Lesenswert?

Folgendes YT-Video zeigt m.M.n. ganz schön wie man mit C++ kleine uC's 
effizient programmieren kann. Zwar ist das Zielsystem kein AVR oder ARM, 
aber klein -- nach heutigen Maßstaben -- ist es schon :-)

https://www.youtube.com/watch?v=zBkNBP00wJE

: Bearbeitet durch User
von Wilhelm M. (wimalopaan)


Lesenswert?

Zu dem Thema hatte ich schon mal ein paar Links zusammengestellt:

Beitrag "Informationen zu C vs C++ / aka Futter für die Diskussion"

Da ist der o.g. Beitrag auch drin ...

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