Seit ca. 4 Wochen beschäftige ich mich jetzt auch mit der tinyAVR(R) 0-series (bei den Experimenten mit dem ATtiny1604) und bin "eigentlich" überrascht und erfreut über den Chip (aber eben nur "eigentlich").
Das erste mal ist mir ein (vllt. vermeintlicher) Bug aufgefallen, den ich hier gepostet hatte:
Schnell stellte sich heraus, dass (aus welchen Gründen auch immer) ein in einer Funktion lokal definertes Array wohl nicht korrekt addressiert wird. Ursprünglich dachte ich auch an einen Compilerbug, weil die Installation der neuesten Version von avr-gcc das Problem scheinbar nicht zeigte, aber:
Auch die Version zeigt das gleiche Problem, ich hatte nur nicht dasselbe Programm verwendet.
Nachdem ich nun mir so schön langsam meinen eigenen Hardwarelayer und mein eigenes Framework zusammenbastle, stellt sich das Problem stärker als zuvor da.
Recherchiert habe ich jetzt nach dem schönen Fingerzeig von peda über das Problem des ATtiny26 und dem Befehl "lPM Rd, Z+" und vom ihm dankenswerterweise gegebenen Link einer Diskussion hierzu.
Dieses Problem kann der ATtiny1604 (getestet sind auch 1614, 214 und 412) nicht haben, denn beim Studium des Listfiles wird nur ein einziges mal "lpm 24, Z" (nicht Z+) verwendet, ein "lpm Rd,Y+" überhaupt nicht.
puts_ram("\n\r Bonjour le monde"); // funktioniert nicht !
43
while(1);
44
45
}
Die Funktion "puts_ram" funktioniert also nur mit einem globalen, schon vorbelegten Array. Natürlich geht bspw. ein Beschreiben des Arrays mittels strcpy o.ä. natürlich auch nicht, was meinen "Verdacht" auf fehlerhafte Adressierung nur unterstreicht.
Dann dachte ich, dass evtl. der Bootloader mit der Compileroption -Wl,--section-start=.text=0x200 stört und habe das compiliert für Gebraucht ohne Bootloader (-Wl,--section-start=.text=0x000) mit keiner Änderung am Verhalten.
Die Funktion "puts_ram" funktioniert mit jedem von mir getesteten Controllern: AVR der älteren Serie (bspw. ATmega328), STM32, MCS51, STM8, LPC und sogar auf Padauks PFS154.
Nur eben nicht auf ATtiny1604 !!
Für diejenigen, die immer nach dem gesamten Quellcode "schreien" habe ich das gesamte "Programm" mit angehängt und extra auf das nötigste (hier eine UART-Anbindung) reduziert.
Im Netz habe ich nichts zu einem Hardwarebug oder einem Bug im Compiler zur tinyAVR 0-series gefunden.
Ich suche ja immer noch den Fehler bei mir (am ehesten), dann am Compiler (will ich nicht so recht glauben), an einen Hardwarebug (will ich noch weniger glauben).
Kennt jemand von Euch einen Weg, mit dem ich bei der tinyAVR 0-series mittels avr-gcc ein lokales Array adressieren kann?
:-) einfach nur, damit ich mein Setup weiter ausbauen kann.
Einen Gruß und ein schönes Wochenende,
JJ
PS: jetzt am Wochenende kann ich nur wenig (und selten hier schreiben), weil ansonsten meine liebe bessere Hälfte meint, sie komme nicht zu ihrem Recht. Zudem: wenn ich einmal anfange bei solchen Dingen die Fehler zu suchen, habe ich keinerlei Zeitgefühl. Also erlege ich mir selbst auf, die Finger vom Rechner (meistens) zu lassen
Das Problem könnte sein, daß man bei den AVR0 ja wieder alles komplett umgemodelt hat. Die Applikation kann nicht mehr unahängig vom Bootloader an 0x0000 starten, sondern erst irgendwo in der Pampa hinter dem Bootloader. Es könnte also ein Problem mit dem Linkerscript sein.
Versuch mal, ob es geht, wenn die Applikation wieder an 0x0000 startet, also ohne Bootloader über ISP geladen wird.
Man könnte auch den Bootloader wie bei den alten ATtiny benutzen, d.h. der Bootloader schnappt sich das RJMP der Applikation an 0x0000, ersetzt es durch ein RJMP zu sich selbst und programmiert den Sprung zur Applikation auf die letzte Adresse vor sich selbst. Dann steht der Bootloader wieder hinten und die Applikation braucht kein spezielles Linkerscript.
Sehe gerade, das mit 0x0000 scheinst Du schon probiert zu haben.
Als weitere Abweichung ist mir aufgefallen, daß der Flash als Data an 0x8000 gemappt wird. Es kann also sein, daß der LPM-Befehl nicht mehr verfügbar ist, sondern LD mit 0x8000 Offset benutzt werden muß. Das Instructionset ist blöderweise nicht mehr im Datenblatt enthalten.
Das Programm zeigt auf einem ATmega4809 dasselbe Fehlverhalten; auf
einem AVR128DB28 hingegen funktioniert es korrekt.
Also entweder ein Compiler- oder ein Hardware-Bug.
Das aufzuklären, sollte ziemlich simpel sein: einfach das Listfile anschauen der beiden Varianten anschauen. Wenn der Assemblercode(!) lt. InstructionsetReference funktionieren müsste, dann ist's ein Hardware-Bug. Wenn nicht, ist's ein Compiler-Bug.
D.h. sie benutzt ebenfalls die gemappten Adressen. Das ist meiner Meinung nach nicht korrekt. Diese Daten müssten stattdessen mit in .text landen, damit sie beim Flashen mit übertragen werden.
Im ELF-File gibt es dafür eine Section .rodata, die so aussieht:Contents
of section .rodata:
8186 0a0d2042 6f6e6a6f 7572206c 65206d6f .. Bonjour le mo
8196 6e646500 0a0d2048 616c6c6f 2075432d nde... Hallo uC-
81a6 436f6d6d 756e6974 790a0d00 Community...
D.h. sie benutzt ebenfalls die gemappten Adressen. Das ist meiner
Meinung nach nicht korrekt. Diese Daten müssten stattdessen mit in .text
landen, damit sie beim Flashen mit übertragen werden.
Ich würde das Problem im Linkerscript vermuten …
genau aus dem Grund habe ich das komplette Progrämmchen mit angehängt und auch absichtlich nicht eine einzige Datei hinzugelinkt.
Das sind jetzt zumindest mal die Stellen, die ich aus meinem Makefile extrahiert und in ein Script gegeben habe, um weitere Fehlerquellen auszuschließen. Ausprobiert habe ich das nun mittlerweile mit avr-gcc 7.3.0 und auf einem anderen Rechner jetzt mit avr-gcc 12.1.0
dann wird das von <puts_ram> korrekt angezeigt (weil ja eben nicht auf dem Stack sondern statisch feste Adresse).
Aaaaaaber (neue Baustelle), ich hatte ja auch das Problem bei einem anderen Anzeigeprogramm, bei dem ich ein uint16_t Array gebraucht habe. Hier funktioniert dann:
dummerweise nicht !!!
Bspw. liefert hier ein zahl= zpotenz[1]; irgendeinen Wert, aber garantiert nicht 1000.
Die Adressierungen der 0-series (und die Versuche hierzu) bringen mich schier um den Verstand.
@Jörg: Der Grundstamm des Buildprozesses bei mir (weil du ein Problem im Linkerscript vermutest) ist für die Standard-AVR sowie für die tinyAVR 0-series derselbe (das gleiche Grund-Makefile, gefüttert mit anderen Namen der Datei die das main enthält, sowie Angaben über F_CPU und eben den verwendeten Chip). Dieses Makefile-Grundgerüst hat in den letzten Jahren immer klaglos funktioniert (was nicht heißen soll, das es nicht fehlerbehaftet ist)
Der Grundstamm des Buildprozesses bei mir (weil du ein Problem
im Linkerscript vermutest) ist für die Standard-AVR sowie für die
tinyAVR 0-series derselbe
Nö, muß unterschiedlich sein.
Wie schon gesagt, die tinyAVR 0 können kein LPM.
Nö, muß unterschiedlich sein.
Wie schon gesagt, die tinyAVR 0 können kein LPM.
Okay (und das ist ein fragendes okay), was ist an dem oben beschriebenen Buildprozess falsch? Ich hatte das extra ohne hinzuzulinkende Dateien gemacht um zusätzliche Fehlerquellen zu verhindern. Hier ist also momentan nur eine einzig zu kompilierende Datei vorhanden und ein einziger Linkeraufruf für eben diese eine *.o Datei.
( mit Geundstamm meinte ich das Aufrufen des Compilers und Linkers mit zusätzlichen Angaben für bspw. defines wie F_CPU und nicht die für unterschiedliche Controller notwendige Compiler und Linkerflags ... oder gar andere Softwarebibliotheken)
Nö, muß unterschiedlich sein.
Wie schon gesagt, die tinyAVR 0 können kein LPM.
Das ist auch gut so.
Das Mapping von Flash und Eeprom in den Speicheraum ist sowieso viel eleganter. Es ist nicht Schuld der neuen AVRs daß C-Compiler bzw. deren Umgebung sich noch nicht auf diese bessere Architektur eingestellt haben.
Das Mapping von Flash und Eeprom in den Speicheraum ist sowieso viel
eleganter.
Für Eleganz kann man sich aber nichts kaufen. Es muß erstmal jemand die Änderungen implementieren.
Hat denn schonmal jemand das neueste Michrochip Studio installiert, ob es damit geht?
Damit kannst du dir zumindest schon mal sicher sein, dass .rodata auf keinen Fall mit im Flash landet. ;-)
Frage: warum überhaupt erst das objcopy und nicht gleich das ELF-File benutzen?
Bei letzterem hängt es allerdings von der AVRDUDE-Version ab, wie das behandelt wird. Traditionell wurde auch da nur eine Section (dann typisch .text) geflasht. Jüngere Änderungen flashen alle ladbaren Sections aus dem ELF-File, damit könnte das sogar so funktionieren, wie es jetzt ist.
Der Grundstamm des Buildprozesses bei mir (weil du ein Problem
im Linkerscript vermutest) ist für die Standard-AVR sowie für die
tinyAVR 0-series derselbe
Nö, muß unterschiedlich sein.
Wie schon gesagt, die tinyAVR 0 können kein LPM.
Also, es ging lt. OT um den ATtiny1604. Und der kann lt. aktuellem Datenblatt (Seite 536) durchaus lpm in allen üblichen Adressierungsarten (und sogar einer mehr, wenn man die "klassischen" AVR8 als Bezug nimmt). Und die Errata sagen auch nichts Gegenteiliges.
Nun habe ich zwar kein reales Objekt, um das zu überprüfen, aber ich würde bis zum Beweis des Gegenteils mal davon ausgehen, dass das Datenblatt hier Recht hat, selbst eins von MC...
So ein gravierender Fehler hätte ziemlich sicher bereits irgendwelche belastbaren Spuren im Web hinterlassen. So ganz neu ist die Tiny-0-Serie ja nun auch wieder nicht. Auch wäre dann sehr spannend, was die lpm-Opcodes dann tatsächlich tun...
Anstatt in C rumzupröbeln, sollte man das also erstmal in pure Asm überprüfen, notfalls mit manuell lt. InstructionSetReference codierten Opcodes. Wenn man so ein Target verfügbar hat, ist das ja nun wirklich kinderleicht.
Wenn dabei rauskommt, dass das DB lügt, dann isses halt so. Ich bezweifele aber ernsthaft, dass es wirklich so ist, räume dem aber immerhin eine gewisse Wahrscheinlichkeit ein. Es gab ja im AVR-Assembler leider schon immer etliche gefakte Memnonics. Es wäre also nicht völlig auszuschließen, dass der Assembler die lpm's bei dem entsprechenden Target letztlich zu irgendwelchen ld*-Opcodes auflöst (eben um lpm-Errata zu verbergen). Das wäre natürlich äußerst fies und völlig unentschuldbar.
LPM kann er immer noch:
"The entire Flash memory is mapped in the memory space and is accessible with normal LD/ST instructions as well as the LPM instruction. For LD/ST instructions, the Flash is mapped from address 0x8000. For the LPM instruction, the Flash start address is 0x0000."
Es könnte noch ein Compilerfehler mit dem Offset 0x8000 bestehen.
Kein LPM können aber die ATtiny4..10 und deren Offset ist 0x4000.
Mein erster Beitrag zielte darauf ab, dass das C-Compilat für den zur gleichen Gruppe gehörenden ATmega4809 auch den Fehler bringt, während für die neueste Gruppe des AVR128DB28 korrekt übersetzt wird.
Da ist tatsächlich nur noch ein Link auf die allgemeine ISR enthalten. Aber keinerlei Hinweis (explizit oder implizit), dass lpm nicht unterstützt werden würde. Wie also kommst du darauf, das mit dem Brustton der völligen Überzeugung zu behaupten?
Wie also kommst du darauf, das mit dem
Brustton der völligen Überzeugung zu behaupten?
Wie hört man denn einen "Brustton" bei einem sachlich geschrieben Text heraus?
Ich habs doch korrigiert, siehe mein Zitat aus dem Datenblatt:
Peter D. schrieb im Beitrag #7379072:
LPM kann er immer noch:
"The entire Flash memory is mapped in the memory space and is accessible
with normal LD/ST instructions as well as the LPM instruction. For LD/ST
instructions, the Flash is mapped from address 0x8000. For the LPM
instruction, the Flash start address is 0x0000."
Warum kann niemand einen Post vollständig lesen und verstehen?
Mein erster Beitrag zielte darauf ab, dass das C-Compilat für den zur
gleichen Gruppe gehörenden ATmega4809 auch den Fehler bringt, während
für die neueste Gruppe des AVR128DB28 korrekt übersetzt wird.
Es wird für beide korrekt übersetzt. Ich habe doch schon dargelegt, dass das Problem weiter hinten in der Kette liegt.
Ein AVR mit mehr als 32 KiB Flash kann zwangsweise das Mapping des Flashs in den linearen Adressraum, wie es bei den kleinen AVR0 gemacht wird, nicht mehr haben. Daher bleibt für den ausschließlich der klassische Weg über LPM übrig.
Bei den kleineren nutzt der Compiler das Flash-Mapping aus (was ja Sinn hat, wofür ist es sonst da?). Nun muss man nur noch den Rest der Toolchain dazu bringen, dass er die Daten auch korrekt in den Chip bekommt. Einen Fehler in deiner Toolchain habe ich dir schon gezeigt (dem Hexfile fehlen die Initialisierungsdaten). Den Rest schaue ich mir nochmal an.
Ein AVR mit mehr als 32 KiB Flash kann zwangsweise das Mapping des
Flashs in den linearen Adressraum, wie es bei den kleinen AVR0 gemacht
wird, nicht mehr haben. Daher bleibt für den ausschließlich der
klassische Weg über LPM übrig.
Doch, kann er.
Die eingeblendete Page kann konfiguriert werden.
Ein AVR mit mehr als 32 KiB Flash kann zwangsweise das Mapping des
Flashs in den linearen Adressraum, wie es bei den kleinen AVR0 gemacht
wird, nicht mehr haben. Daher bleibt für den ausschließlich der
klassische Weg über LPM übrig.
Doch, kann er.
Die eingeblendete Page kann konfiguriert werden.
Ja, aber gerade hier gibt es einige (immerhin wohldokumentierte) Hardware-Bugs. Für das konkrete Problem dieses Threads sind die aber vollkommen irrelavant. Der Tiny1604 ist klein genug, um seinen gesamten Flash 1:1 im SRAM-Adressraum abzubilden. Ist also gerade nicht von den Bugs bei den größeren Teilen betroffen, die das nur pagewise können.
Ja, und das passt ja: auch beim ATmega4809 kann alles direkt in den SRAM-Bereich gemappt werden; und er bringt denselben Fehler wie der ATtiny1604.
Es war eben nur die Aussage mit den 32 KiB falsch; hinzu kommt der Einwand von Martin W., dass per 'Flash Section Mapping' alles möglich ist.
Das hilft nur beschränkt, denn dann müsste der Compiler das ja alle nasenlang umkonfigurieren. Ich habe mir die Codegenerierung jetzt nicht angesehen, gehe aber einfach mal davon aus, dass er für alle MCUs mit mehr als 32 KiB, bei denen das simple Mapping ab 0x8000 nicht funktioniert, dann schlicht alles wieder mit LPM macht.
Für diejenigen, die immer nach dem gesamten Quellcode "schreien" habe
ich das gesamte "Programm" mit angehängt und extra auf das nötigste
(hier eine UART-Anbindung) reduziert.
Danke übrigens nochmal, dass du das auf ein minimalistisches, compilierbares Beispiel herunter gebrochen hast, das das Problem demonstriert. Das hat bei der Suche nach der Ursache gut geholfen.
Danke übrigens nochmal, dass du das auf ein minimalistisches,
compilierbares Beispiel herunter gebrochen hast, das das Problem
demonstriert. Das hat bei der Suche nach der Ursache gut geholfen.
Naja: minimal ist das Beispiel keineswegs. Einiges hätte man noch weglassen können.
Das Unkomplizierteste wäre eigentlich, wenn AVRDUDE das direkt aus dem ELF korrekt handhabt. Muss ich aber noch gucken (nicht heute Abend, habe gerade noch anderes vor).
Das Unkomplizierteste wäre eigentlich, wenn AVRDUDE das direkt aus dem
ELF korrekt handhabt.
Tut es, zumindest in Version 7.1 aufwärts. Habe zwar hier keinen dieser kleinen ATtinys, aber ich habe mal testhalber das ELF-File auf einen AVR128DA48 geflasht, und ich sehe im Flash hinterher alle Strings. Das heißt, dass er auch die .rodata ordentlich geschrieben hat.
Tut es, zumindest in Version 7.1 aufwärts. Habe zwar hier keinen dieser
kleinen ATtinys, aber ich habe mal testhalber das ELF-File auf einen
AVR128DA48 geflasht, und ich sehe im Flash hinterher alle Strings. Das
heißt, dass er auch die .rodata ordentlich geschrieben hat.
Damit kannst du dir zumindest schon mal sicher sein, dass .rodata auf
keinen Fall mit im Flash landet. ;-)
dieses avr-objcopy ist aus einer alten Datei reingerutscht und hätte hier gar nicht auftauchen sollen und mir ist auch nicht aufgefallen, dass das auf .text und auf .data begrenzt ist. Das wird auch bei vorherigen Versuchen das gewesen sein, dass ich beim ersten Setup einer 12.1 Compilertoolchain mit avr-gcc noch eine richtiges avr-objcopy eingerichtet hatte und danach eben ein altes, falsch parametriertes avr-objcopy aus einem anderen Projekt genommen hatte.
Als erste Abhilfe, probier mal:
avr-objcopy -j .text -j .rodata -j .data -O ihex puts_vers.elf
puts_vers.hex
Natürlich funktioniert das, aber auch ein:
1
avr-objcopy -O ihex puts_vers.elf
funktioniert !
Und wie gesagt: Höchst peinlich für mich. Andererseits: wieder etwas gelernt (und wieder mal den Fehler wo anderst gesucht als er war .... )
:-) das einzige das noch funktioniert hat war mein "Instinkt" der mir sagt: "Ich habe was falsch gemacht"
Danke übrigens nochmal, dass du das auf ein minimalistisches,
compilierbares Beispiel herunter gebrochen hast, das das Problem
demonstriert. Das hat bei der Suche nach der Ursache gut geholfen.
:-) hier lernt man, sein Problem so umfassend es geht zu beschreiben. Leider wird dann das Eröffnungsthread bisweilen recht lang.
Ich bedanke mich bei allen Diskutanten (vor allem bei Jörg). Gehabt es Euch wohl und eine angenehme Nachtruhe
könnte mir mal bitte jemand die Abbruchbedingung erklären? Wann wird die
Schleife beendet? Danke.void uart_puts_rom(const uint8_t *dataPtr)
{
uint8_t c;
for (c=pgm_read_byte(dataPtr); c; ++dataPtr, c=pgm_read_byte(dataPtr))
uart_putchar(c);
}
Zum Einen ging es hier nie darum, einen String aus dem Flashrom anzuzeigen, denn dieses funktionierte ja und diente nur zur Verdeutlichung, dass etwas funktioniert.
ob das hier so richtig ist bezweifel ich.
Was macht c im Schleifenkörper? Bspw. Abbruchbedingung?
Könnte Zufallseffekte auslösen.
Die Schleife hier stammt aus meinen Anfängen der AVR-Zeit und habe ich nach deren funktionieren nicht mehr geändert und werde es auch nicht tun (never change a running system, egal wie trivial etwas ist).
:-) ... grundsätzlich besteht eine For-Schleife aus 3 Teilen:
Startbedingung
Abbruchbedingung
Bedingung(en) die bei einem Schleifenende ausgeführt wird.
Die erste Bedingung der Schleife ist klar: es wird das Zeichen gelesen, auf den der Pointer gerade zeigt.
Die Abbruchbedingung ist eigentlich einfach. In C liefert ein Vergleich, bspw. ein "if (i> 9) " zu einer 1 oder 0, je nachdem ob eben i> 9 (liefert eine 1) oder eben nicht > 9 ist (liefert eine 0). Für Abbruchbedingungen aber auch der Bedingung (if) ob etwas ausgeführt werden soll oder nicht, wird nur die 0 kontrolliert. Das will heißen, dass jeglicher Wert > 0 als 1 gewertet wird, selbst dann wenn der Wert bspw. 5 oder 120 sein sollte. Es wird also nur etwas gecheckt nach der Art und Weise:
==> ist etwas 0, wenn ja dann, wenn nicht, dann...
In meiner Schleife wurde in die Variable c eben ein Zeichen des Strings eingelesen. Bei den Strings in C handelt es sich (meistens) um sogenannte Ascii-Zero Strings. D.h. das Ende eines Strings wird mit einem Char mit dem Wert 0 markiert. Steht dieser Wert nun in der Abbruchbedingung der For-Schleife, wird diese nicht weiter ausgeführt.
Die Bedingungen am Schleifenende sollten klar sein, sie bestehen aus 2 Anweisungen:
den Zeiger auf das nächste Zeichen setzen ( ++dataPtr, ) und das nächste Zeichen zu lesen ( c=pgm_read_byte(dataPtr)
1000 Wege führen nach Rom und es gibt viele Wege, einen AsciiZ String anzuzeigen, aber wie gesagt: Never change a running system... und diese Funktion hat schon viele male funktioniert.
:-) in diesem Sinne hoffe ich, dass dieser Thread jetzt abgeschlossen ist !
void puts_p(const char *progmem_s )
{
volatile char c {0};
while ( (c = pgm_read_byte(progmem_s++)) )
putChar(c);
}
Ist das ein "Angst"-volatile?
Hast Du das dazu gesetzt?
Die originale Funktion von Peter Fleury lautete so. Vorab. Peter trifft hier keine Schuld. Ist für C programmiert wurden und schon viele Jahre alt. Nicht das es jemand falsch versteht.
Da "register" mittlerweile deprecated ist, hatte ich das anscheinend damals mit volatile ersetzt und seitdem nie geändert. Das werde ich jetzt nachholen und löschen. Danke für den Hinweis. Die Funktion ist wenigstens lesbar. ;-)
Nein, bitte nicht noch eine Diskussion über volatile auch wenn Wilhelm M. da gerne ganze Abhandlungen drüber schreibt.
Hat er in anderen Threads zur genüge und ist hier OT.
Nein, bitte nicht noch eine Diskussion über volatile auch wenn Wilhelm
M. da gerne ganze Abhandlungen drüber schreibt.
Hat er in anderen Threads zur genüge und ist hier OT.
Die Antworten zeigen jedoch - sagen wir mal - einen gewissen Nachholbedarf. Ich denke, dass Veit D. das vllt auch noch anderswo falschermaßen eingesetzt hat. und nun froh über den Hinweis ist.
Die Antworten zeigen jedoch - sagen wir mal - einen gewissen
Nachholbedarf. Ich denke, dass Veit D. das vllt auch noch anderswo
falschermaßen eingesetzt hat. und nun froh über den Hinweis ist.
Hat jetzt aber mit dem Threadtitel so gar nichts zu tun...
Die Antworten zeigen jedoch - sagen wir mal - einen gewissen
Nachholbedarf. Ich denke, dass Veit D. das vllt auch noch anderswo
falschermaßen eingesetzt hat. und nun froh über den Hinweis ist.
Hat jetzt aber mit dem Threadtitel so gar nichts zu tun...
Das mag sein. Und ist bei den meisten Threads hier so.
Auch zu dem Code des TO könnte man ja noch mehr sagen. Die gröbsten Schnitzer sollte man auch ansprechen, selbst wenn nicht explizit danach gefragt wurde.
ich kann nicht mehr sagen wann und warum ich dort volatile verwendet hatte. Das ist auch schon paar Jahre her. Mittlerweile denke ich volatile & Co verstanden zu haben. Ohne den Hinweis wäre es mir vielleicht nie aufgefallen. Danke nochmal.
Das Letzte volatile (volatile uint8_t *) in meiner USART Lib wird für Hardwareregisterzugriffe verwendet.
Das hängt damit zusammen, dass dort die Registerblöcke der einzelnen Geräte jeweils in einer Struktur zusammengefasst werden. Wenn es das Gerät in mehreren Instanzen gibt, bleibt die struct die gleiche, nur die Adresse ändert sich. Der Zeiger wird daher erst dann gebildet (und zeigt auf die struct), wenn das Gerät instanziiert wird.
Bei den alten AVRs war das noch nicht durchgehend so, daher wurden alle Register eines jeden Geräts alle einzeln benannt (TCCR1A, TCCR3B etc.) und dann jeweils über einen solchen (dereferenzierten) Zeiger abgebildet.
Zeig mal ein Beispiel, wo Du glaubst, dass das notwendig wäre. Könnte sein, dass da ein Missverständnis vorliegt.
Normalerweise ist das bei den etwas moderneren AVRs so, das die Register, die zu einer internen Peripherie-Komponente gehören, zusammenhängend in den Adressbereich eingeblendet werden, so dass man sie sinnvoll in einem struct zusammenfassen kann (bei den alten megas lagen die teilweise verstreut). Anschließend mapped man diese struct auf die Anfangsadresse der Komponente, also man castet die Adresse des ersten Registers auf den Typ struct XYZ*. Damit liegen alle Strukturkomponenten dann auch an den richtigen Adressen. Dieses Verfahren nennt sich structure-mapping.
Beispiel AVR128DA64:
1
typedefstructAC_struct
2
{
3
register8_tCTRLA;/* Control A */
4
register8_tCTRLB;/* Control B */
5
register8_tMUXCTRL;/* Mux Control A */
6
register8_treserved_1[2];
7
register8_tDACREF;/* DAC Voltage Reference */
8
register8_tINTCTRL;/* Interrupt Control */
9
register8_tSTATUS;/* Status */
10
}AC_t;
und dann findet folgendes Mapping statt:
1
#define AC0 (*(AC_t *) 0x0680) /* Analog Comparator */
2
#define AC1 (*(AC_t *) 0x0688) /* Analog Comparator */
3
#define AC2 (*(AC_t *) 0x0690) /* Analog Comparator */
D.h. hier liegt AC0.CTRLA auf der Adresse 0x0680, AC0.CTRLB auf der Adresse 0x0681, usw.
Natürlich ist register_t hier ein volatile-qualified uint8_t, damit der Zugriff hierauf auch tatsächlich an der Stelle im Code ausgeführt wird, wo er steht, weil er einen Seiteneffekt auslöst, und v.a. keine Umsortierung gegenüber anderen volatile-Zugriffen stattfindet. Denn die Reihenfolge des Registerzugriffs ist ja wesentlich für die korrekte Funktion.
Damit sollte eigentlich alles ok sein.
An welcher Stelle (außer ggf. Objekte, die zwischen ISRs und Rest geteilt werden) meinst Du denn sonst noch volatile einsetzen zu müssen?
In aller Kürze: bezogen auf den Typ register_t und das volatile darin ist das richtig.
Allerdings mal auf die schnelle noch folgende Anmerkungen. Die Funktionen können nicht constexpr sein, weil sie ein reinterpret_cast enthalten. Von dem constexpr bleibt also nur das implizite inline übrig. Diese Funktionen können also nie in einem constexpr-Kontext eingesetzt werden. Das merkst Du etwa bei:
1
constexprautoa=PortmegaAVR0::regVPORTdir(1);
Weiterhin kannst Du auch mit Referenzen arbeiten, dann es der Anwender einfacher:
Du solltest Dir bewusst sein, dass das recht viel Aufwand produziert, falls Du diese Funktionen mal nicht mit einer compile-time Konstanten aufrufst, etwa:
1
uint8_tpin;
2
intmain(){
3
*PortmegaAVR0::regVPORTdir(pin)=0x55;
4
}
Damit wandert die LUT in die .roData section (RAM) und die Berechnung findet zur Laufzeit statt. Falls Du das nicht möchtest, so sollte man verhindert, dass diese Funktion zur Laufzeit ausgewertet wird. Die macht man normalerweise mit consteval, da Du jedoch ein reinterpret_cast verwendest, geht das nicht. Es bleibt dann also nur der Weg über eine Meta-Funktion.
die berechneten Adressen pro Pin werden in Template Klassen verwendet. Die Objekte sind Template Klassen mit konstantem initialisierten Pin. Eine Pinänderung bzw. Übergabe zur Laufzeit findet nicht statt.
Bsp.
OutputPin <5> userLed;
Das wird am Ende in den zur Verfügung gestellten Elementfunktionen verwendet. Bsp.
1
template<uint8_tpin>
2
classPin
3
{
4
private:
5
static_assert(pin<Pins::Addr::ANZAHLPINS,"pin number not available for this controller");
6
7
// ...
8
// Outputs
9
voidinline__attribute__((always_inline))init(){
10
*regVPORTdir(pin)=*regVPORTdir(pin)|getMask(pin);
11
}
12
13
voidinline__attribute__((always_inline))toggle(){
14
*regVPORTin(pin)=getMask(pin);
15
}
16
// ...
17
}
So wie du das beschreibst das nur implizite inline übrig bleibt habe ich das noch gar nicht betrachtet. Wenn ich so darüber nachdenke muss ich ja nicht von Anfang an einen Zeiger mitschleppen sondern nur die berechnete Adresse als reine Zahl. Erst am Ende wenn ich wirklich Registerinhalte ändere benötige ich einen Zeiger. Dann könnte man ggf. consteval verwenden. Da muss ich einmal in Ruhe nachdenken ob und wie ein Umbau möglich ist. Danke für die weitere Anmerkung.
die berechneten Adressen pro Pin werden in Template Klassen verwendet.
Die Objekte sind Template Klassen mit konstantem initialisierten Pin.
Eine Pinänderung bzw. Übergabe zur Laufzeit findet nicht statt.
Das habe ich mir gedacht.
Wenn also das Argument für Deine Funktionen wie regVPORTdir() eine Compile-Zeit-Konstante ist, dann wird der Optimizer natürlich den Funktionsaufruf ebenfalls zu einem Compile-Zeit konstanten Wert überführen und keinen tatsächlichen Funktionsaufruf mehr ausführen zur Laufzeit. Alles andere wäre ein ziemlich dummer Compiler.
Diese Funktionen kannst Du aber wegen des reinterpret_cast nicht consteval machen.
Wenn Du nun verhindern willst, dass "jemand" diese Funktionen tatsächlich zur Laufzeit aufruft, und damit ein tatsächlichen Lookup in der Tabelle auslöst, was ja zur Folge hat, das Viel Laufzeit UND RAM durch die LUT verbraucht wird, dann kannst Du bspw. den Parameter zu einem NTTP (non-type-template-parameter) machen. Dann ist das zwar immer noch ein Laufzeitaufruf, allerdings ist der Index in die Tabelle dann eben Compile-Zeit konstant. Auch hier muss natürlich eine Phase des Optimizers dies Template-Funktion, die dann einen Compile-Zeit konstanten Wert zurück gibt, durch diese Konstante selbst ersetzen. Aber auch das ist natürlich für den Optimizer super easy.
Wenn Du allerdings beispielsweise auch ohne jede Optimierung besseren Code haben möchtest (Debugging), dann solltest Du den Code so formulieren, dass der Compiler auch ohne Optimizer erträglichen Code produziert.
Dies erreicht man, indem man eben Compile-Zeit-Berechnungen auch definitiv zur Compile-Zeit ausführen lässt. Leider ist in Deinem Anwendungsfall consteval nicht möglicht wegen reinterpret_cast.
Was bleibt, sind klassische Meta-Funktionen. Mit Meta-Funktionen kann man 4 unterschiedliche Arten von Abbildungen machen:
Type -> Type
Type -> Wert
Wert -> Type
Wert -> Wert
Es gibt keine festgelegte Syntax für Meta-Funktion, aber es ist üblich sie als Klassentemplates zu schreiben.
Nun, da Du jetzt schon einigen Code hast, denke ich, dass Du diesen (besseren) Weg deswegen aber nicht verfolgen wirst.
Bsp.
OutputPin <5> userLed;
Anwendung:
userLed.init();
userLed.toggle();
Andere Anmerkungen:
Du verwendest das Klassentemplate als Monostate, d.h. dieses Objekt userLed hat keinen eigenen Zustand. Auch hier vertraust Du darauf, dass der Optimizer dieses Objekt eliminiert. Was auch sicher stattfindet.
Aber auch hier bin ich ein Freund davon, genau zu spezifizieren, was man will. Das Monostate-Pattern ist eine Krücke. Besser ist es, die Template-Klasse OutputPin<5> uninstantiierbar zu machen und nur static Elementfunktionen darin zu haben.
Deine Abbildung [äußerer Pin -> MCU-Register-Adressen] ist eigentlich eine Verknüpfung von zwei Abbildungen: f1:[äußerer Pin -> MCU-Pin] und
f2:[MCU-Pin -> MCU-Register]. Die erste ist vom Board abhängig, die zweite von der MCU. Um es also etwas universeller zu halten, würde ich auch zwei Abbildungen verwenden, damit kannst Du dann wenigstens f2 universell auch woanders benutzen
So wie du das beschreibst das nur implizite inline übrig bleibt habe ich
das noch gar nicht betrachtet. Wenn ich so darüber nachdenke muss ich ja
nicht von Anfang an einen Zeiger mitschleppen sondern nur die berechnete
Adresse als reine Zahl. Erst am Ende wenn ich wirklich Registerinhalte
ändere benötige ich einen Zeiger. Dann könnte man ggf. consteval
verwenden. Da muss ich einmal in Ruhe nachdenken ob und wie ein Umbau
möglich ist. Danke für die weitere Anmerkung.
soweit so gut, verstehe fast alles davon. Den Vorschlag zur anderen Auftrennung muss ich mir auch überlegen. Da hat ja jeder so seine eigene Denkweise. Ich werde mich damit nochmal befassen.
Danke.
ich habe mir mal par Gedanken gemacht mit Experimenten für die Basisfunktionen.
Das wäre die Basis nach alten Prinzip nur etwas umgearbeitet. Könnte man bestimmt wieder ein struct daraus bauen, nur dann werden die Zeilen ewig lang. Weiß noch nicht recht.
Mit mehr Templates bin ich hierbei abgebrochen. Hier gefallen mir die getAddr... Funktionen noch nicht. Es gibt kein sauberes return wenn kein Vergleich gültig ist.
Desweiteren versuche ich immer selbst komplexe Zusammenhänge möglichst einfach zu halten. Ich habe nur leider das Gefühl alles zu verkomplizieren. Viel Tipparbeit für wenig Nutzen. Wie schätzt man ab was wann Sinn macht? Die wichtigste Frage ist allerdings. Egal wie ich zu meiner endgültigen Adresse komme, am Ende muss ich doch wieder einen Cast machen. Genau das soll doch am Ende vermieden werden wenn ich dich richtig verstanden hatte. Wie macht man das?
Und wegen der Trennung
äußerer Pin -> MCU-Pin
MCU-Pin -> MCU-Register
Ich habe doch eigentlich bisher auch nicht mehr Aufwand. Die Registertabelle (LUT) ist Controller spezifisch geschrieben. Und die Klassen sind allgemein gehalten und greifen auf die LUT zu. Das heißt alles ist für eine Controllerfamilie gültig und nur die LUTs werden entsprechend µC der Familie eingebunden. Worin siehst du dabei einen Nachteil? Irgendeine Tabelle musst du doch auch sicherlich Controller spezifisch immer anpassen und der Rest bleibt gleich?
Ich weiß ehrlich gesagt noch nicht wie ich weitermache bzw. weitermachen soll. Ich habe jetzt hier auch sicherlich keine konkreten Fragen gestellt, weil ich nicht weiß was ich fragen soll. Es ist zu viel offen an Möglichkeiten und ich weiß nicht welcher Weg wirklich Sinn macht. Eigentlich wollte ich irgenwann einmal bspw. Pin Objekte erstellen wie
Output <PA, 6> led0; // Port A Bit 6