avrgcc: verteilte Array-Initialisierung

OP #7848997
Lesenswert?

Folgendes Problem: Ich habe eine Sammlung Quellmodule, die per #define gesteuert Funktionen ein- oder ausschließen.

Namen und Adressen dieser Funktionen sollen in eine Namenstabelle eingetragen werden, wenn die jeweilige Funktion im Objektcode enthalten ist. Die Tabelle soll im Flasspeicher angelegt werden. Das soll automatisch zur Generierungszeit passieren.

Die Tabelle wird zur Laufzeit von einer Routine durchsucht, die die Funktionsadressen in Namensstrings umsetzt. Es muss also auch die variable Länge der Tabelle zur Laufzeit feststellbar sein.

Wir bekommt man das hin?

Moderator Persönliche Seite #7849014
Lesenswert?

Alternativ: überall da, wo via #define irgendwas hinzu kommt, kommt auch gleich der Tabelleneintrag mit dazu (bspw. in Form einer eigenen memory section).

Das spart es, den ganzen Salat via Postprocessing nachzuverarbeiten, was ja am Ende auf zweimaliges Linken hinaus läuft (einmal, um zu wissen, was alles "an Bord" ist, das zweite Mal, um die mit diesem Wissen gebaute Tabelle dazu zu linken).

Beitrag #7849046 wurde von einem Moderator gelöscht.
#7849050
Lesenswert?

Ich würde das über #defines machen, und mir den ganzen Tabellen-Zirkus sparen. Der Linker macht das ja für dich.

Wenn du tatsächlich unterschiedliche Memory-Regions brauchst -was bei den 8bit AVR eher die Ausnahme ist- kannst du ja einfach alle denkbaren Memory-Regions im Linkerscript eintragen, und via #define kontrollieren, welche tatsächlich genutzt werden.

Ungenutzte Zuweisungen im Linkerscript, tun ja nicht weh.

Ist zwar kein AVR, aber in diesem Projekt kannst du sehen, wie man in einer config.h mit nem Haufen #define kontrolliert, was da gerade gebaut wird.:

Beitrag "Re: [STM32/HAL] simples U(S)ART-Library"

OP #7849082
Lesenswert?

Harry L. schrieb:

Ich würde das über #defines machen, und mir den ganzen Tabellen-Zirkus sparen. Der Linker macht das ja für dich.

Mir scheint, ich habe mich nicht klar genug ausgedrückt…

Ich habe einen Scheduler als Quelltextbibliothek.

Für die Task-Tabelle des Schedulers gibt es eine Dumpfunktion, die im Moment eine Adresse-nach-Namensumsetzungsroutine in der Anwendung aufruft. Dafür muss eine Tabelle gepflegt werden, die den Task-Adressen den Klarnamen zuordnet. Die Pflege der Tabelle will ich umgehen, indem ich einfach in jeder Task-Routine einen Macro aufrufe, der den zugehörigen Umsaetzungstabelleneintrag generiert.

Die Tabelleneinträge müssen hintereinander in einer Flash-Section landen – das ist Linkers Job.

Damit die Suchroutine weiß, wo die Tabelle endet, muss hinter dieser Section eine weitere leere liegen, die die Adresse hinter der Tabelle liefert.

Ich habe etwas herum experimentiert: der Compiler nimmt Deklarationen wie diese an:

1
#define N(name)         { name, FSTR(#name) }
2
static const Name __flash1 NameTable[] = {
3
  N(powerBankRefreshed),
4
  N(powerBankRefresher),
5
};
6
#undef N

wobei __flash1 der der allgemeinen Form __flashN entspricht. Wenn ich dahinter einen leeren Array in __flash2 anlege, könnte das funktionieren, wenn die Sections hintereinander liegen.

Nur leider finde ich im .map-File dazu überhaupt nichts, weder Sectionnamen, noch Variablennamen. (Dieser .map-File scheint mir überhaupt nicht zu allzuviel nutze zu sein…)

#7849085
Lesenswert?

wieso eigenes Linker-Script? Du brauchst doch nur eine weitere Memory section für Deine Tabelle. Und der Linker liefert Dir nachher Adresse und Länge der Section. Also nur ein paar Zeilen im Script.

Wieso brauchst Du überhaupt getrennte Quellen? Wenn Du in ein Sammelfile alle Header einbinden kannst, kann das der Precompiler für Dich erledigen.

Wenn es nur eine Handvoll Teilprojekte sind, kannst du auch eine Liste von (Teilprojekt-)Tabellen anlegen.

Wenn es Dir nur um das Listen-/Tabellenende geht, mache sie "Nullterminiert" (natürlich nicht wenn der Linker die anordnet).

OP #7849094
Lesenswert?

Bruno V. schrieb:

wieso eigenes Linker-Script? Du brauchst doch nur eine weitere Memory section für Deine Tabelle. Und der Linker liefert Dir nachher Adresse und Länge der Section. Also nur ein paar Zeilen im Script.

In welchem Skript?

Wieso brauchst Du überhaupt getrennte Quellen? Wenn Du in ein Sammelfile alle Header einbinden kannst, kann das der Precompiler für Dich erledigen.

Ganz simpel: ich will den Scheduler einfach in ein Projekt aufnehmen, also scheduler.c und scheduler.h

In der Anwendung sind dann die Task-Funktionen verstreut. Wenn jede Taskfunktion es schafft, den zugehörigen Umsetzungstabellen-Eintrag in eine spezielle Section zu bugsieren und der Name des Section-Anfangs und -Endes dem Scheduler bekannt ist, dann muss man sich um die Tabelle nicht mehr weiter kümmern.

Wenn es nur eine Handvoll Teilprojekte sind, kannst du auch eine Liste von (Teilprojekt-)Tabellen anlegen.

Nein, solches Zeug will ich tunlichst vermeiden – das wird schnell furchtbar unübersichtlich und ist bald unpflegbar…

Moderator Persönliche Seite #7849120
Lesenswert?

Moriz schrieb:

Nur leider finde ich im .map-File dazu überhaupt nichts, weder Sectionnamen, noch Variablennamen. (Dieser .map-File scheint mir überhaupt nicht zu allzuviel nutze zu sein…)

Das Map-File ist nützlich zum Debuggen, wenn man Probleme mit dem Linker hat. Das ist ganz sicher nicht dafür da, irgendwie maschinenlesbar zu sein.

Die Symboltabelle wäre vielleicht interessanter für dich.

Moriz schrieb:

Wie findet man das heraus?

Indem du den GCC mit -v startest, dann sagt er dir, welche Backends er so aufruft.

#7849147
Lesenswert?

Moriz schrieb:

Ich habe eine Sammlung Quellmodule, die per #define gesteuert Funktionen ein- oder ausschließen.

Die Tabelle wird zur Laufzeit von einer Routine durchsucht, die die Funktionsadressen in Namensstrings umsetzt.

Wozu diese Umstände? Ein #define läßt sich doch prima schon zur Compilezeit auswerten.

1
#define MODULE_CMD_GETCALIB       "GetCalib"           // Diagnostics only. Returns the calbiration signature
2
#define MODULE_CMD_READ_ADC       "ReadAdc"            // Diagnostics only, doesnt work for muxed ADC.
3
#define MODULE_CMD_GETSTACK       "GetStack"           // Diagnostics only. Returns stack usage.
4

5
#define CMD_SIZE        20              // for sscanf format, decimal number only !
6

7
typedef char cmd_str_t[CMD_SIZE+1];
8

9
typedef void (*func)(void);
10

11
typedef struct
12
{
13
  cmd_str_t cmd;
14
  cmd_str_t cmd_short;
15
  func func;
16
} command_t;
17

18
command_t const cmd_table[] PROGMEM =
19
{
20
#ifdef MODULE_CMD_GETSTACK
21
  {MODULE_CMD_GETSTACK "\0",      "gst", cmd_get_stack},
22
#endif
23
#ifdef MODULE_CMD_GETCALIB
24
  {MODULE_CMD_GETCALIB "\0",      "gc",  cmd_get_calib_signature},
25
#endif
26
#ifdef MODULE_CMD_READ_ADC
27
  {MODULE_CMD_READ_ADC "\0",      "ra",  cmd_read_adc},
28
#endif
29
  {"", "", 0},
30
};
#7849161
Lesenswert?

Moriz schrieb:

Nein, solches Zeug will ich tunlichst vermeiden – das wird schnell furchtbar unübersichtlich und ist bald unpflegbar…

Ich hab nen Kollegen, der ist Fan von überall im Code verteilten sscanf() zum Parsen. Mit dem Erfolg, daß gerne mal Schreibfehler passieren oder sogar Namen doppelt vergeben werden bzw. fehlen. Für einen anderen Leser ist es sehr unübersichtlich, wenn die Strings und Aufrufe überall wahllos verteilt sind. Außerdem erzeugt jedes weitere sscanf() auch massig Codeoverhead. In einer Tabelle kann man viel besser auf einheitliche Syntaxregeln achten und Fehler erkennen.

#7849176
Lesenswert?

Harald K. schrieb:

Ob man es an drei, zehn oder fünfzig Stellen im Code verwendet, ändert an dessen Größe nichts.

sscanf() Aufrufe bekommen oft sehr viele Parameter übergeben, die auf dem Stack abgelegt werden müssen. Man kann sich mal im Listing anschauen, was für ein Bohei vor jedem Aufruf veranstaltet wird.

1
  cmd_data.args = sscanf_P(buf, PSTR(CMD_FMT CMD_FMT "%g"), 
2
                           cmd_data.cmd, cmd_data.par, &cmd_data.val);
3
    4890:  2e e6         ldi  r18, 0x6E  ; 110
4
    4892:  3c e0         ldi  r19, 0x0C  ; 12
5
    4894:  3f 93         push  r19
6
    4896:  2f 93         push  r18
7
    4898:  c9 e5         ldi  r28, 0x59  ; 89
8
    489a:  dc e0         ldi  r29, 0x0C  ; 12
9
    489c:  df 93         push  r29
10
    489e:  cf 93         push  r28
11
    48a0:  24 e4         ldi  r18, 0x44  ; 68
12
    48a2:  3c e0         ldi  r19, 0x0C  ; 12
13
    48a4:  3f 93         push  r19
14
    48a6:  2f 93         push  r18
15
    48a8:  23 e0         ldi  r18, 0x03  ; 3
16
    48aa:  3e e0         ldi  r19, 0x0E  ; 14
17
    48ac:  3f 93         push  r19
18
    48ae:  2f 93         push  r18
19
    48b0:  9f 93         push  r25
20
    48b2:  8f 93         push  r24
21
    48b4:  0e 94 1f 45   call  0x8a3e  ; 0x8a3e <sscanf_P>
OP #7849259
Lesenswert?

Peter D. schrieb:

Mit dem Erfolg, daß gerne mal Schreibfehler passieren oder sogar Namen doppelt vergeben werden bzw. fehlen.

Im Prinzip hast du Recht, aber hier liegt ein spezieller Fall vor, für den das eben gerade nicht gilt, während es für die global zu pflegende Tabelle zutrifft.

In schedule.h gibt es bereits einen Macro SCHEDULER_TASK(name), der eigentlich nur syntaktischer Zucker ist und die Lesbarkeit des Codes verbessert:

1
#define SCHEDULER_TASK(name)   void name()

Wenn es gelingt, diesen Macro so umzubauen, dass er gleich den entsprechenden Namenstabelleneintrag generiert, dann braucht man keine globale Tabelle mehr, der Code bleibt lesbar und Namensänderungen schlagen sofort auf die Tabelle durch.

Zusätzlich kann man auch zwei Versionen davon bauen, eine mit eingebautem Tabellen-Dump und einen ohne und die ganze Chose über ein in der Kommandozeile des Compilers angebbares Symbol steuern.

Dann wäre das alles völlig automatisiert und trotzdem transparent.

Die Tabelleneintragsgenerierung kann man mit dem Trick mit der verzeigerten Liste aus https://www.mikrocontroller.net/articles/C_Linkerhacks_%2B_Modularisierung dezentralisieren. (Das mit den Sections funktioniert beim AVR nach dem Artikel nicht, weil die Reihenfolge der Sections nicht definiert ist.)

#7849275
Lesenswert?

Moriz schrieb:

Das mit den Sections funktioniert beim AVR nach dem Artikel nicht, weil die Reihenfolge der Sections nicht definiert ist

Das eigentliche Problem beim AVR ist das linker script. Die custom sections kommen nicht in .text oder .data rein, die Objekte landen dann nicht auf dem AVR. Und wenn man seine Sections z.B. .data.bla nennt, dann sollte es zwar in .data rein kommen, aber dann generiert der Linker die _start und _stop symbole nicht mehr, die gibts nur wenn der Sektionsname ein gültiger c identifier wäre, und "." kann man da halt nicht haben. Eine echt blöde Situation...

OP #7849277
Lesenswert?

Peter D. schrieb:

Ein Macro kann also nicht weitere Macros erzeugen und es sind auch keine nested Macros möglich.

Muss es auch nicht. Sieh dir den Artikel an, den Daniel A. hier Beitrag "Re: avrgcc: verteilte Array-Initialisierung" verlinkt hat…

Ein Problem könnte es geben, wenn in zwei Quellmodulewn der Macro zufällig auf gleichen Zeilennummern aufgerufen wird. Das könnte man aber leicht beheben, z.B. mit einer für jeden Quellfile individuellen Modul-ID – das habe ich in meinem Projekt auch bereits eingebaut, um fatale Fehler mit einer LED blinkend zu kommunizieren.

OP #7849281
Lesenswert?

Peter D. schrieb:

Das Problem an C-Macros ist, daß es nur eine Textersetzung durchführt.

Das wird in dem zitierten Artikel mit dem alten CPP CONCAT-Trick umgangen. Womöglich kannst du da noch was lernen ;-)

Neue Macros in einem Macro erzeugen geht nicht, aber das braucht man hier auch nicht. Für solche Schweinereien muss dann der olle m4 her… Dessen Syntax ist allerdings gewöhnungsbedürftig.

Persönliche Seite #7849287
Lesenswert?

Moriz schrieb:

Hier: Beitrag "Re: AVR GCC .text is full - ld scripts finden" steht, es sei avr2.

Also vermutlich /usr/lib/avr/lib/ldscripts/arv2.x

avr2 auf gar keinen Fall.

Das ld script trägt den Namen der Emulation, also was im Linkeraufruf auf -m folgt:

1
(echo "" | avr-gcc -xc - -r -mmcu=atmega328p -Wl,-v 2>&1) | grep -o -- ' -mavr[a-z_0-9]*\b' | sed -e 's: -m::'
2
avr5

Das verwendete ld Script ist also avr5.x.

Erklärung: Es wird ein leeres Programm mit -Wl,-v gelinkt. Der Linker zeigt mit -v wie er aufgerufen wurde:

1
collect2 version $version
2
$prefix/bin/../lib/gcc/avr/$version/../../../../avr/bin/ld ... -mavr5 ...
3
GNU ld (GNU Binutils) $version

grep und sed filtern dann die Emulation raus.

Zur Referenz sind die ld Scripte installiert unter $prefix/avr/lib/ldscript.

Moderator Persönliche Seite #7849290
Lesenswert?

Moriz schrieb:

Jörg W. schrieb:

Hast du malenv LANG=C man ld probiert?

Damit geht es, Teufel…

Normalerweise sollte das man-Kommando natürlich auf die englische Version zurück fallen, wenn keine in der Regionalsprache installiert ist. Da scheint also generell was bei dem System kaputt zu sein.

Für das avr-*-Zeug hilft das nichts.

Das wiederum dürfte an demjenigen liegen, der die avr-* Pakete gebaut hat. Aber von avr-libc abgesehen (die via "avr-man" bei Bedarf einen Sack voller eigener man pages installieren kann), sind die man pages von GCC und binutils natürlich eh die gleichen wie die ohne "avr-".

OP #7849305
Lesenswert?

Peter D. schrieb:

sscanf() Aufrufe bekommen oft sehr viele Parameter übergeben, die auf dem Stack abgelegt werden müssen. Man kann sich mal im Listing anschauen, was für ein Bohei vor jedem Aufruf veranstaltet wird.

Mit ziemlicher Sicherheit gibts noch viel mehr Bohei, wenn man das, was ein sscanf-Aufruf macht, zu Fuß in einzelne Funktionsaufrufe auflöst.

Aber wegen einem einzigen sscanf den ganzen Modul zu laden, sollte man sich im Zweifelsfall schon gut überlegen.

OP #7849333
Lesenswert?

Jetzt habe ich mal so ein Macro gestrickt:

1
#ifndef DUMP_TASK_TABLE
2
    #define SCHEDULER_TASK(n)   static void n()
3
#else
4
    typedef struct Schd_NameTableEntry {
5
        const void                          *taskAddr;
6
        const char const __flash            *name;
7
        struct Schd_NameTableEntry          *next;
8
    } Schd_NameTableEntry;
9

10
    #ifndef CONCAT
11
        #define CONCAT_EVAL(a, b)    a ## b
12
        #define CONCAT(a, b)         CONCAT_EVAL(a, b)
13
    #endif // CONCAT
14

15
    #define SCHEDULER_TASK(n) \
16
       static void n(); \
17
       static Schd_NameTableEntry CONCAT( n, _Schd_NameTableEntry) = { \
18
          (n), FSTR(#n), NULL}; \
19
       \
20
       extern Schd_NameTableEntry *schd_NameTable; \
21
       \
22
       static void CONCAT( n, _Schd_NameTableEntry_ctor )() \
23
          __attribute__((constructor)); \
24
       static void CONCAT( n, _Schd_NameTableEntry_ctor )(){ \
25
          CONCAT( n, _Schd_NameTableEntry ).next = schd_NameTable; \
26
          schd_NameTable = &CONCAT( n, _Schd_NameTableEntry ); \
27
       } \
28
       \
29
       static void n()
30
#endif // DUMP_TASK_TABLE

Für den folgenden Aufruf

1
SCHEDULER_TASK(initReadSensors) {
2
   // Code der Taskroutine
3
}

generiert der C-Preprozessor diesen (handformatierten) Code:

1
static void initReadSensors();
2

3
static Schd_NameTableEntry initReadSensors_Schd_NameTableEntry = {
4
    (initReadSensors),
5
    ((const __flash char[]) { "initReadSensors" } ),
6
    ((void *)0)
7
};
8

9
extern Schd_NameTableEntry *schd_NameTable;
10

11
static void initReadSensors_Schd_NameTableEntry_ctor() __attribute__((constructor));
12
static void initReadSensors_Schd_NameTableEntry_ctor(){
13
    initReadSensors_Schd_NameTableEntry.next = schd_NameTable;
14
    schd_NameTable = &initReadSensors_Schd_NameTableEntry;
15
}
16

17
static void initReadSensors() {
18
  // Code der Taskroutine
19
}

Das sieht schon mal ganz gut aus… nur finde ich die Funktion initReadSensors_Schd_NameTableEntry_ctor() weder in der Symboltabelle, noch im .map-File. Irgend was scheint da mit der Behandlung von Konstruktoren nicht zu stimmen.

OP #7849418
Lesenswert?

Daniel A. schrieb:

Versuch es mal mit

1
__attribute__((constructor,used))

Damit funktioniert es.

Der gesamte Definitionsblock sieht dann so aus:

1
#ifndef DUMP_TASK_TABLE
2
    #define SCHEDULER_TASK(n)   static void n()
3
#else
4
    typedef struct Schd_NameTableEntry {
5
        const void                          *taskAddr;
6
        const char const __flash            *name;
7
        struct Schd_NameTableEntry          *next;
8
    } Schd_NameTableEntry;
9

10
    #ifndef CONCAT
11
        #define CONCAT_EVAL(a, b)    a ## b
12
        #define CONCAT(a, b)         CONCAT_EVAL(a, b)
13
    #endif // CONCAT
14

15
    #define SCHEDULER_TASK(n) \
16
       static void n(); \
17
       static Schd_NameTableEntry CONCAT( n, _Schd_NameTableEntry) = { \
18
          (n), FSTR(#n), NULL }; \
19
       \
20
       extern Schd_NameTableEntry *schd_NameTable; \
21
       \
22
       static void CONCAT( n, _Schd_NameTableEntry_ctor )() \
23
          __attribute__((constructor,used)); \
24
       static void CONCAT( n, _Schd_NameTableEntry_ctor )(){ \
25
          CONCAT( n, _Schd_NameTableEntry ).next = schd_NameTable; \
26
          schd_NameTable = &CONCAT( n, _Schd_NameTableEntry ); \
27
       } \
28
       \
29
       static void n()
30
#endif // DUMP_TASK_TABLE

Die Funktionsdefinition für eine Task:

1
SCHEDULER_TASK(machWas) {
2
   // …
3
}

Je nachdem, ob das Symbol DUMP_TASK_TABLE definiert ist, oder nicht, wird ein Namenstabelleneintrag generiert, oder nicht.

Antwort schreiben

Bitte melde dich an, um einen Beitrag zu schreiben.

oder

Mit Google-Account einloggen

Die Registrierung ist kostenlos und dauert nur eine Minute.

Jetzt registrieren