Forum: Compiler & IDEs Fiese Zeiger Hacks und PROGMEM


von Moritz E. (devmo)


Lesenswert?

Hallo,

Ich habe schon länger eine Unklarheit zu erdulden wenn ich mit 
verschachtelten Zeiger-Konstrukten hantiere, welche auf 
unterschiedlichen Ebenen type-qualifier und außerdem das PROGMEM 
Attribut verwenden.


I)
z.b. wenn es um Arrays im Flash geht.

Beispiel, wie in doku avr-lib c erläutert:

Es soll ein Array aus Strings im Flash landen. Dies soll man 
folgendermaßen machen:
1
const char cmd1P[] PROGMEM = "v5 ";
2
const char cmd2P[] PROGMEM = "vs ";
3
4
const char (const * tab_P[ARRAYSIZE]) PROGMEM = {
5
  cmd1P,
6
  cmd2P,
7
  0
8
};

in avr-libc wird PGM_P verwendet, ich habe versucht das Äquivalent in 
const char ... PROGMEM umzuwandeln.

Obiger Ausdruck soll Folgendes bedeuten, von innen nach außen:

- ein Array tab_P mit ARRAYSIZE Elementen der im Flash landet
(soweit ich verstehe wirkt PROGMEM immer auf das innerste konstrukt, 
also den Array,

- der Array hat Zeiger als Elemente,

- die Zeiger sind const,
(hier bin ich mir nicht sicher ob das const innerhalb der Klammer genau 
das bewirkt, und ob z.b. (* const tab_P[ARRAYSIZE]) äquivalent ist, also 
ein Array mit const Zeiger Elementen (const * array[]) das selbe ist wie 
ein const Array mit Zeiger elementen (* const array[]). Die Klammerung 
in der Dekl. ist für Lesbarkeit, sie wegzulassen müsste ein äquivalenter 
Ausdruck sein, meine ich.)

- die Zeiger zeigen auf Objekte vom typ const char im Flash.




II)
nun versuche ich einen Array mit Zeiger auf Funktionen in den Flash zu 
packen, dessen returntype zudem dabei weggecasted werden soll:
1
extern uint8_t PWR_V5_Enable(void);
2
extern uint8_t PWR_VS_Enable(void);
3
4
void (* const func_tab_P[ARRAYSIZE])(void) PROGMEM = {
5
  
6
  (void (*)()) PWR_V5_Enable,
7
  (void (*)()) PWR_VS_Enable,
8
  0
9
10
};

Hier ist auch die Frage ob c den cast so versteht wie ich meine:

(void ()) wäre ein cast auf eine void func?
(void (*)()) wäre ein cast auf einen Zeiger auf eine void func?

Die Bedeutung des obigen Ausdrucks soll nach meinem Verständnis also 
sein:

void (* const func_tab_P[ARRAYSIZE])(void) PROGMEM
=>

- Array in Progmem, der const ist,

- welcher Elemente von Typ Zeiger hat,

- welche selbst auf void (void) Funktionen zeigen (also auf Flash 
Adressen)


Anders als beim String Array sind die Objekte, dessen Adressen die 
Array-Elemente sind, selbst zuvor nicht mit Progmem einzeln deklariert, 
wie ich meine ist das unnötig, da die Objekte Funktionsadressen sind, 
also Flashadressen (somit den Status einen const ... PROGMEM Objektes 
haben). Daher sollte der Compiler also NICHT eine Ramvariable anlegen, 
dort die Funktionsadresse reinpacken, und die Elemente des Flasharrays 
auf diese Ramvariable zeigen lassen, wie es der Fall wäre, wenn man beim 
ersten Beispiel die Strings nicht einzeln per PROGMEM deklariert?

Das alles compiliert ohne Warnings bezüglich inkompatiblen 
typezuweisungen.

von Peter D. (peda)


Lesenswert?

Hast Du mal eine Abschätzung gemacht, wie groß Deine Flash-Daten werden?

Ich würde Progmem nur dann verwenden, wenn dadurch wirklich eine 
kritische Menge RAM eingespart wird.
Wenn z.B. Dein AVR 4kB RAM hat, dann tun 2kB konstante Daten darin 
überhaupt nicht weh.

Progmem schaltet auch die Fehlermeldungen ab. D.h. Du kannst die 
üblichen Zugriffsoperatoren * oder [] verwenden) ohne es zu merken. Das 
Programm macht dann natürlich völligen Unsinn.


Peter

von Karl H. (kbuchegg)


Lesenswert?

Moritz E. schrieb:

> I)
> z.b. wenn es um Arrays im Flash geht.
>
> Beispiel, wie in doku avr-lib c erläutert:
>
> Es soll ein Array aus Strings im Flash landen. Dies soll man
> folgendermaßen machen:
>
>
1
> const char cmd1P[] PROGMEM = "v5 ";
2
> const char cmd2P[] PROGMEM = "vs ";
3
> 
4
> const char (const * tab_P[ARRAYSIZE]) PROGMEM = {
5
>   cmd1P,
6
>   cmd2P,
7
>   0
8
> };
9
>
>
> in avr-libc wird PGM_P verwendet, ich habe versucht das Äquivalent in
> const char ... PROGMEM umzuwandeln.
>
> Obiger Ausdruck soll Folgendes bedeuten, von innen nach außen:
>
> - ein Array tab_P mit ARRAYSIZE Elementen der im Flash landet

ok

> - der Array hat Zeiger als Elemente,

Ohne die Klammern: ja.
Mit den Klammern: nein.

> - die Zeiger sind const,

mit den Klammern: ja
ohne die Klammern: nein

> Die Klammerung in der Dekl. ist für Lesbarkeit, sie wegzulassen
> müsste ein äquivalenter Ausdruck sein, meine ich.)

Diese Annahme stimmt nicht. Die Klammern verändern den ganzen Ausdruck. 
Aus einem Array von Pointern wird dann ganz schnell ein Pointer auf ein 
Array.

> II)
> nun versuche ich einen Array mit Zeiger auf Funktionen in den Flash zu
> packen, dessen returntype zudem dabei weggecasted werden soll:


Ein Tip:
Wenn du die Übersicht verlierst, dann benutze typedef um dir Datentypen 
zu schaffen und für Klarheit zu Sorgen.

Es spricht nichts dagegen, sich die einzelnen Datentypen erst mal 
mittels typedef zurecht zu legen und dann sukzessive die immer 
komplexeren Datentypen daraus (gegebenenfalls wieder mit einem typedef) 
aufzubauen.

Der Programmierer der nach dir kommt und das alles wieder entwirren 
muss, wird es dir danken.

Du willst Funktionspointer benutzen, die auf solche Funktionen
1
extern uint8_t PWR_V5_Enable(void);
2
extern uint8_t PWR_VS_Enable(void);
zeigen können.
Also machst du dir dafür erst mal einen Datentyp
1
typedef uint8_t (*FnctPtr)( void );
Damit hast du einen Datentyp FnctPtr (du wirst natürlich einen 
sinnvolleren Namen nehmen), der ein Pointer auf Funktionen von genau 
diesem Typ ist.

Und dann baust du ein Array aus derartigen Funktionspointern
1
FnctPtr func_tab_P[ARRAYSIZE] =
2
{
3
  PWR_V5_Enable,
4
  PWR_VS_Enable
5
};
und du willst dann dieses Array auch noch ins Flash verschieben
1
FnctPtr func_tab_P[ARRAYSIZE] PROGMEM =
2
{
3
  PWR_V5_Enable,
4
  PWR_VS_Enable
5
};
Der typedef macht dir die Sache jetzt leicht, weil du nicht lange mit 
Klammern und Sternen hantieren musst. Du hast einen Datentyp für einen 
Funktionspointer und mit dem arbeitest du weiter.

Und nochwas: Den Compiler bei Funktionssignaturen anzulügen (sprich mit 
Casts zu arbeiten) ist meistens eine ganz, ganz schlechte Idee. Wenn die 
Funktion etwas retourniert, dann muss der Compiler das auch wissen. Du 
kannst den retournierten Wert ignorieren und nichts damit machen, das 
ist ok. Aber dem Compiler eine Funktion, die einen uint8_t returniert 
als eine void unterzujubeln, kann je nach Compiler mächtig ins Auge 
gehen. Du bist nicht der erste, der für unbedachte Casts mit Abstürzen 
bestraft wird! Casts sind Waffen! Man setzt sie nie leichtfertig ein.

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Karl Heinz Buchegger schrieb:
> Wenn du die Übersicht verlierst, dann benutze typedef um dir Datentypen
> zu schaffen

Einziger Nachteil: Attribute kann man nicht (sinnvoll) in einem
typedef unterbringen.  Hatten wir ja in der avr-libc versucht, das
ist gründlich daneben gegangen. ;-)

Mit den named address spaces künftig wird das besser, denn die werden
auch über ein typedef mitgereicht (so ich das verstanden habe).

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Ja, named address spaces werden durch Qualifier abgebildet, nicht durch 
ein Attribut.

PROGMEM aka. __attribute__((progmem)) dient dazu, die Ablage von 
Objekten zu beeinflussen, mehr nicht. In einem typedef wäre es — selbst 
wenn es dort unterstützt würde — ziemlich witzlos.

Für Zugriffe auf mittels PORGMEM lokatierte Daten muss man eh inline 
Assembler verwenden, da führt kein Weg dran vorbei.

von Moritz E. (devmo)


Lesenswert?

Danke für die ausführliche Antwort!

Karl Heinz Buchegger schrieb:
>>
>> Obiger Ausdruck soll Folgendes bedeuten, von innen nach außen:
>>
>> - ein Array tab_P mit ARRAYSIZE Elementen der im Flash landet
>
> ok
>
zu:
1
const char (const * tab_P[ARRAYSIZE]) PROGMEM = {
wenn es mit Klammern nun ein Zeiger auf ein Array ist (s.u.), sollte das 
PROGMEM sich auf den Zeiger beziehen, und nur dieser im Flash landen, 
oder?

>> - der Array hat Zeiger als Elemente,
>
> Ohne die Klammern: ja.
> Mit den Klammern: nein.

Was wäre es denn mit den Klammern? Ein Pointer auf ein Array wäre ja
1
const char (* tab_P)[ARRAYSIZE] PROGMEM = {


>> - die Zeiger sind const,
>
> mit den Klammern: ja
> ohne die Klammern: nein

Ich habe nochmal nachgeguckt, und leider ist const char <=> char const, 
und nicht so, dass ein Type Qualifier auf das Konstrukt rechts wirkt, 
was für mich mehr Sinn gemacht hätte bezüglich Konsistenz der Sprache. 
Demnach muss es für ein Array mit const Zeigern auf const char heißen:
1
 const char * const tab_P[ARRAYSIZE] PROGMEM = {

Wobei meiner Annahme im ersten Post nach, ein PROGMEM ein const 
implizieren würde, und in diesem Fall unnötig sein sollte, die 
Zeigerelemente const zu machen, da ein Versuch ein PROGMEM Zeigerelement 
zu schreiben ein Fehler produzieren sollte.

>> Die Klammerung in der Dekl. ist für Lesbarkeit, sie wegzulassen
>> müsste ein äquivalenter Ausdruck sein, meine ich.)
>
> Diese Annahme stimmt nicht. Die Klammern verändern den ganzen Ausdruck.
> Aus einem Array von Pointern wird dann ganz schnell ein Pointer auf ein
> Array.

siehe oben, in dem konkreten Ausdruck, welchen Unterschied würde die 
Klammer bedeuten?


> Ein Tip:
> Wenn du die Übersicht verlierst, dann benutze typedef um dir Datentypen
> zu schaffen und für Klarheit zu Sorgen.
>
> Es spricht nichts dagegen, sich die einzelnen Datentypen erst mal
> mittels typedef zurecht zu legen und dann sukzessive die immer
> komplexeren Datentypen daraus (gegebenenfalls wieder mit einem typedef)
> aufzubauen.
>
> Der Programmierer der nach dir kommt und das alles wieder entwirren
> muss, wird es dir danken.
>
> Du willst Funktionspointer benutzen, die auf solche Funktionen
>
1
> extern uint8_t PWR_V5_Enable(void);
2
> extern uint8_t PWR_VS_Enable(void);
3
>
> zeigen können.
> Also machst du dir dafür erst mal einen Datentyp
>
1
> typedef uint8_t (*FnctPtr)( void );
2
>
> Damit hast du einen Datentyp FnctPtr (du wirst natürlich einen
> sinnvolleren Namen nehmen), der ein Pointer auf Funktionen von genau
> diesem Typ ist.
>

Ja das ist eine gute Idee, ich bin früher schonmal mit typedefs und 
PROGMEM gegen die Wand gefahren, was mich wohl so konditioniert hat, 
erstmal alle Deklarationen und Zuweisungen per Hand zu verwenden.

>
> Und nochwas: Den Compiler bei Funktionssignaturen anzulügen (sprich mit
> Casts zu arbeiten) ist meistens eine ganz, ganz schlechte Idee. Wenn die
> Funktion etwas retourniert, dann muss der Compiler das auch wissen.

Das extern gehört erstmal nicht dahin, ohne extern würde ich ja den 
Compiler nicht anlügen, bzw. ein ansi-compiler sollte das doch 
einwandfrei verarbeiten können (Analog zum Aufruf PWR_V5_Enable())"? Der 
Grund für void ist, dass in dem Zeiger-Array Funktionen sowohl mit int 
als auch mit void aufnehmen soll.

Du
> kannst den retournierten Wert ignorieren und nichts damit machen, das
> ist ok. Aber dem Compiler eine Funktion, die einen uint8_t returniert
> als eine void unterzujubeln, kann je nach Compiler mächtig ins Auge
> gehen. Du bist nicht der erste, der für unbedachte Casts mit Abstürzen
> bestraft wird! Casts sind Waffen! Man setzt sie nie leichtfertig ein.

von Karl H. (kbuchegg)


Lesenswert?

Moritz E. schrieb:

>> Und nochwas: Den Compiler bei Funktionssignaturen anzulügen (sprich mit
>> Casts zu arbeiten) ist meistens eine ganz, ganz schlechte Idee. Wenn die
>> Funktion etwas retourniert, dann muss der Compiler das auch wissen.
>
> Das extern gehört erstmal nicht dahin, ohne extern würde ich ja den
> Compiler nicht anlügen, bzw. ein ansi-compiler sollte das doch
> einwandfrei verarbeiten können (Analog zum Aufruf PWR_V5_Enable())"?

es geht nicht um das extern.
Es geht darum, dass deine Funktion einen uint8_t liefert.

> Der
> Grund für void ist, dass in dem Zeiger-Array Funktionen sowohl mit int
> als auch mit void aufnehmen soll.

Und wie soll der Compiler dann beim tatsächlichen Aufruf wissen, ob die 
aufgerufene Funktion etwas liefern wird (und wenn ja: was) oder nicht?

von Moritz E. (devmo)


Lesenswert?

Karl Heinz Buchegger schrieb:

> es geht nicht um das extern.
> Es geht darum, dass deine Funktion einen uint8_t liefert.
>
>> Der
>> Grund für void ist, dass in dem Zeiger-Array Funktionen sowohl mit int
>> als auch mit void aufnehmen soll.
>
> Und wie soll der Compiler dann beim tatsächlichen Aufruf wissen, ob die
> aufgerufene Funktion etwas liefern wird (und wenn ja: was) oder nicht?

Über die Zeiger werde ich die Funktionen nur ohne Rückgabewert 
verwenden, die Rückgabewerte sind nur exit-status codes. Anders fiele 
mir auch kein Weg ein, Funktionenzeiger mit unterschiedlichen Rückgabe 
Types in einen Array zu stecken.

Ich würde annehmen, das die Funktionswerte die in r24 liegen einfach 
liegen gelassen werden, die Annahme ist dann, das der Compiler bei einem 
Funktionsaufruf im Allgemeinen die für returnwerte vorgesehen Register 
r18-r25 nicht als den Aufruf überdauernd vorraussetzt, da in den 
Register Usage Guidelines diese unter "Caller-saved" stehen, also der 
Caller verantwortlich ist, diese regs vorher zu sichern und nach aufruf 
wieder herzustellen, soweit ich das Verstanden habe.

In diesem Fall sollte ein cast auf void doch Bombensicher sein, (anders 
als natürlich casts auf was anderes als void)?

von Karl H. (kbuchegg)


Lesenswert?

Moritz E. schrieb:

> Über die Zeiger werde ich die Funktionen nur ohne Rückgabewert
> verwenden

Das interessiert die Funktion aber nicht.
Die liefert!

Was du im Prinzip machst
1
uint8_t foo( void )
2
{
3
   return 8;
4
}
5
6
void foo( void );   // Prototyp für obige Funktion, der bewusst falsch ist
7
8
int main()
9
{
10
  foo();
11
}

Bei so etwas klopft dir der Compiler (zu Recht) auf die Finger. Die 
Tatsache, dass du mit Funktionspointern und umcasten diesen Code so 
verscheleierst, dass der Compiler das nicht mehr merkt, ändert daran 
nichts, dass das immer noch ein Fehler ist. (Dasselbe könnte man auch 
dadurch erreichen, dass man die Funktion in eine andere Compilation Unit 
verlagert und dafür sorgt, dass der Compiler den zugehörigen Prototypen 
nicht mit der Funktionsdefinition vergleichen kann)

Lüg deinen Compiler nicht an!


> die Rückgabewerte sind nur exit-status codes. Anders fiele
> mir auch kein Weg ein, Funktionenzeiger mit unterschiedlichen Rückgabe
> Types in einen Array zu stecken.

Entweder
* alle Funktionen sind gleich
* oder du machst dir zusätzlich eine Typkennung rein, damit du vorher 
den Zeiger wieder zum richtigen Funktionstyp casten kannst.

Die 2-te Variante ist die fehleranfälligere (wie alles, was darauf 
basiert, dass ein Programmierer keinen Fehler machen darf)


> Ich würde annehmen,

Grundregel:
Triff keine Annahmen darüber, wie der Compiler etwas implementiert. 
Damit fällst du nämlich irgendwann auf die Schnauze. Und laut Murphy 
genau dann, wenn du es am wenigsten gebrauchen kannst. Spiel nach den 
Regeln und erfind keine eigenen.

von hbl333 (Gast)


Lesenswert?

Ich verstehe nicht warum ihr euch mit so einem Müll
überhaupt beschäftigt. Wer nicht einmal eine gescheite
Speicherorganistaion zustande bringt gehört doch einfach
in die Tonne. Ich sag nur PROG_MEM get pgm_byte wusel
wusel würg. Das hat der Keil C51 schon vor 20 Jahren
besser gemacht. Leute verabschiedet eich von dem Dreck, für
wenig Geld gibt es professionelle Tools. Da kann man sich
seinen Problemen widmen und nicht diesem Compiler gewusel.

von Moritz E. (devmo)


Lesenswert?

Karl Heinz Buchegger schrieb:

>
> Was du im Prinzip machst
>
1
> uint8_t foo( void )
2
> {
3
>    return 8;
4
> }
5
> 
6
> void foo( void );   // Prototyp für obige Funktion, der bewusst falsch
7
> ist
8
> 
9
> int main()
10
> {
11
>   foo();
12
> }
13
>
>
> Bei so etwas klopft dir der Compiler (zu Recht) auf die Finger. Die
> Tatsache, dass du mit Funktionspointern und umcasten diesen Code so
> verscheleierst, dass der Compiler das nicht mehr merkt, ändert daran
> nichts, dass das immer noch ein Fehler ist.

Ich hätte es eher damit assoziiert:
1
uint8_t foo(void)
2
{
3
    return 8
4
}
5
6
int main()
7
{
8
   foo();
9
}

was ja auch völlig in Ordnung ist.

Zu unterscheiden ist natürlich ob der von mir verwendete cast 
"definiert" ist, also nach ansi-c der compiler dies konsistent und 
definiert umsetzt (ohne Architekturspezifische/Implementationsabhängige 
Seiteneffekte). Bei anderen casts ist das hochgeradig 
Implementationsabhängig bzw. nicht von ansi-c definiert, weshalb ich 
sonst auch casts nicht viel abgewinnen kann, als "dirty hack" um den 
compiler zu belügen wie du sagst. Ist es aber "definiert", ist es ja 
keine Lüge ;).
Ob ein Compiler so ein verhalten, wenn denn "definiert", korrekt 
umsetzt, ist dann eine andere Frage, auch das es nicht der "beste" Stil 
ist, insofern kann ich deinem Standpunkt auch folgen, dass sowas nicht 
als allgemeiner Still gepflegt werden sollte und nur mit Obacht 
verwendet werden sollte.

Eine Alternative wäre natürlich eine void Wrapper func, die das selbe 
liefert, nur ohne cast:
1
uint8_t intfoo( void )
2
{
3
   return 8;
4
}
5
6
void voidintfoo( void)
7
{
8
   intfoo();
9
}

Ich denke das werde dann auch besser so handhaben.

von Karl H. (kbuchegg)


Lesenswert?

Moritz E. schrieb:

> Ich hätte es eher damit assoziiert:
>
>
1
> uint8_t foo(void)
2
> {
3
>     return 8
4
> }
5
> 
6
> int main()
7
> {
8
>    foo();
9
> }
10
>
>
> was ja auch völlig in Ordnung ist.

Das ist in Ordnung. du musst den Returnwert nicht verwenden.
Aber: Das ist nicht das, was du mit dem Funktionspointer machst.

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Moritz E. schrieb:

> Wobei meiner Annahme im ersten Post nach, ein PROGMEM ein const
> implizieren würde, und in diesem Fall unnötig sein sollte, die
> Zeigerelemente const zu machen, da ein Versuch ein PROGMEM Zeigerelement
> zu schreiben ein Fehler produzieren sollte.

PROGMEM impliziert nicht const, man kann PROGMEM auch ohne const 
verwenden. In neuen Compilerversionen ergibt das aber einen Fehler:

int  a __attribute__((progmem)) = 1;

>> error: variable 'a' must be const in order to be put into
>> read-only section by means of '__attribute__((progmem))'

Das const zu PROGMEM fällt also nicht vom Himmel, man muss es 
hinschreiben, siehe http://gcc.gnu.org/PR44643

von Moritz E. (devmo)


Lesenswert?

Aha ok danke für den Hinweis Johann!

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.