De optimalisatie zorgt er inderdaad voor dat de eerste 2 goed gaan, maar dat kan alleen doordat de strings zelf in SRAM staan.
'const' heeft er niks mee te maken, en 'arduino hobby' ook niet.
Dit is een eigenschap van de Atmel controllers en daar heb je in AVR studio evengoed last van als met arduino.
Het probleem is dat PROGMEM en SRAM verschillende adres ruimten zijn in de AVR. En die overlappen elkaar, dus aan de pointer kun je niet zien in welke adres ruimte je moet zijn.
Arduino heeft een truc bedacht om een string uit progmem af te drukken:
Serial.println("Dit is een string uit SRAM");
Serial.println( F("Deze uit PROGMEM"));
Die F() is een soort van cast die het type van de string verandert zodat println weet dat de string uit progmem moet komen. Maar dat werkt alleen met string literals omdat die F() niet alleen een cast doet maar ook de string naar PROGMEM verplaatst.
Je kunt de hele tabel in één keer in PROGMEM zetten door een 2-dimensionele tabel te maken, dus een array-of-strings ipv een array-of-pointers-naar strings:
const char const MenuWhereAmI[][33] PROGMEM =
{
"* Main Menu *",
"* LEVEL_1_MENU *",
"* LEVEL_2_MENU *",
"* LEVEL_3_MENU *",
"* LEVEL_1_SUBMENU *",
"* LEVEL_2_SUBMENU *",
"* LEVEL_2_SUBSUBMENU *",
"* LEVEL_3_SUBMENU *",
"* LEVEL_3_SUBSUBMENU *",
};
Die kun je dan zo gebruiken:
const char MenuWhereAmI[][33] PROGMEM =
{
"* Main Menu *",
"* LEVEL_1_MENU *",
"* LEVEL_2_MENU *",
"* LEVEL_3_MENU *",
"* LEVEL_1_SUBMENU *",
"* LEVEL_2_SUBMENU *",
"* LEVEL_2_SUBSUBMENU *",
"* LEVEL_3_SUBMENU *",
"* LEVEL_3_SUBSUBMENU *",
};
#define NR_SUBMENUS(a) (sizeof(a) / sizeof(a[0]))
void setup()
{ Serial.begin(115200);
Serial.println(F("String from Progmem"));
}
void loop()
{
delay(2000);
for(uint8_t i = 0; i < NR_SUBMENUS(MenuWhereAmI); i++)
{ Serial.println( (const __FlashStringHelper *)MenuWhereAmI[i] );
}
for ( ;; )
;
}
De cast naar (const __FlashStringHelper *) zorgt ervoor dat println de string in PROGMEM gaat halen.
EricP
mét CE
Het probleem is dat PROGMEM en SRAM verschillende adres ruimten zijn in de AVR. En die overlappen elkaar, dus aan de pointer kun je niet zien in welke adres ruimte je moet zijn.
In SDCC heeft men dat voor de 8051 anders opgelost. Een pointer is daar 16 bits (om 64k te kunnen adresseren) en dat worden 2 bytes als het type bekend is (compile time weet men bijna altijd of het ROM of XRAM is).
Bij een void pointer is dat compile time niet bekend. Die wordt dus 3 bytes met een extra byte om het type aan te geven. Run-time wordt er op basis van die extra byte code uitgevoerd die de juiste opcodes voor het juiste memory bevat.
Inderdaad, het is wat omslachtig. Maar daarmee maak het dus niet meer uit waar die pointer 'zit'.
Hoe ik hier achter gekomen ben? Nou, ooit eens een stuk proof of concept geschreven voor een ASIC waar oa. een 8051 core in zit om de rest van de hardware te besturen. Draaide prima. Nu een generieke lib ervan maken. Draaide voor geen meter. Traag als dikke str*nt door een dunne trechter (op 12MHz en elke instructie minimaal 12 cycles houd je ook niet zoveel over). Kijken naar de gegenereerde assembly vertelde dit verhaal.
Allererst iedereen bedankt voor de reacties.
Het gaat inderdaad om een AVR Atmega328.
Laat ik beginnen met welk probleem ik eigenlijk op probeerde te lossen.
Ik heb een wat uitgebreidere menustructuur te bedienen via de seriele poort en heeft dus nogal wat tekst te printen om de keuzes op het scherm te zetten.
Wat er gebeurde was dat na compileren de 2K SRAM binnen de kortste keren overvol was, ik daar tijdens het compileren een foutmelding (meer warning) over kreeg en het helemaal niet werkte (kwam wel door de compiler).
Ik dacht dat dat kwam door de hoeveelheid statische tekst (strings met menu opties) dus dat probeerde ik op te lossen door alle statische tekst (menu opties) naar "PROGMEM" (flash) te halen. Dat lijkt dus faliekant mislukt.
De gesuggereerde oplossing van deKees lijkt dus inderdaad de oplossing die ik zoek, daar heb ik overigens nog 2 vraagjes over :
#define NR_SUBMENUS(a) (sizeof(a) / sizeof(a[0]))Is dit een stukje om het aantal menu opties te definieren / uit te vogelen ? (Omdat dit later in je loop gebruikt word. Dit is (lijkt) me netter dan een andere "oplossing" die ik zag door in de array op het einde een NULL toe te voegen en daarop te testen.
En deze begrijp ik nog niet helemaal :
Serial.println( (const __FlashStringHelper *)MenuWhereAmI[i] );Zelfs niet met deze uitleg :
De cast naar (const __FlashStringHelper *) zorgt ervoor dat println de string in PROGMEM gaat halen.
Maar dat zit er meer in dat ik niet weet wat een "cast" is/doet
rew
four NANDS do make a NOR . Kijk ook eens in onze shop: http://www.bitwizard.nl/shop/
Stel je hebt een variabele die "signed char" is. Dus waardes tussen -128 en +127.
Maar nu wil je hem als echte byte gebruiken.
signed char b;
unsigned char a;
a = (unsigned char) b;
Nu cast je de variabele b naar de "unsigned" a.
Het is in C de manier om te zeggen: "Let even niet op de types, die X bits moet je nu als type Y interpreteren!" .
Dit kan nodig zijn voor allerlei redenen. Bijvoorbeeld, als je een DMA controller hebt, dan wil die in een "u32_t" register een adres hebben. Wil je die op "het begin van een array" zetten dan moet je dus de pointer naar het begin van het array bit-voor-bit in dat register zetten en wil je niet dat de compiler gaat zeuren dat de types niet overeenkomen. Dan gebruik je een cast.
Op zaterdag 22 maart 2025 22:51:41 schreef Bobosje:
Moet je er niet een \n (newline character) achteraan zetten?
Serial.println (Serial PrintLine) voegt automatisch een CR toe
Serial. print doet dat niet.
Op zondag 23 maart 2025 00:46:45 schreef deKees:
const char const MenuWhereAmI[][33] PROGMEM = { "* Main Menu *", "* LEVEL_1_MENU *", "* LEVEL_2_MENU *", "* LEVEL_3_MENU *", "* LEVEL_1_SUBMENU *", "* LEVEL_2_SUBMENU *", "* LEVEL_2_SUBSUBMENU *", "* LEVEL_3_SUBMENU *", "* LEVEL_3_SUBSUBMENU *", };
Wat betreft const char const MenuWhereAmI[][33] PROGMEM = is het me ook nog niet helemaal duidelijk waar die [33] vandaan komt, de strings zijn maar 31 karakters lang ?
En deze begrijp ik nog niet helemaal :
Serial is een class met allerlei member funkties. Ze gebruiken de C++ overloading om de juiste funktie te matchen met de source code.
Zo is er een Serial::println(const char *) funktie die een string kan printen. Die werkt voor strings uit Sram. Maar er is ook een andere funktie Serial.println(const __FlashStringHelper *) die eigenlijk hetzelfde doet, maar dan wel de string in PROGMEM gaat halen.
MenuWhereAmI[i]resulteert in het adres van de string die je wilt printen. Die pointer heeft wel de juiste waarde, maar dat is dan wel een (const char*). Door de cast verandert het type van die pointer en wordt er dus een andere println() ingezet.
Die __FlashStringHelper is een Arduino poging om het probleem op te lossen. Die wordt ook gebruikt in de implementatie van de F() macro. Om het helemaal 'netjes' te doen zou je ook de menu tabel kunnen definieren als __FlashStringHelper :
const __FlashStringHelper MenuWhereAmI[][33] PROGMEM =
{
"* Main Menu *",
"* LEVEL_1_MENU *",
"* LEVEL_2_MENU *",
"* LEVEL_3_MENU *",
"* LEVEL_1_SUBMENU *",
"* LEVEL_2_SUBMENU *",
"* LEVEL_2_SUBSUBMENU *",
"* LEVEL_3_SUBMENU *",
"* LEVEL_3_SUBSUBMENU *",
};
Dan heb je de cast niet meer nodig:
Serial.println( MenuWhereAmI[i] );
En inderdaad, NR_SUBMENUS(a) is een macro die berekent hoeveel strings er in de tabel zitten. Dat is een vrij standaard methode die de lengte van het geheel 'sizeof(a)' deelt door de lengte van de eerste entry 'sizeof(a[0])'. Die berekening wordt door de compiler gedaan.
Of nee, toch niet.
Die __FlashStringHelper is niet gedefiniëerd, dus die kun je niet gebruiken om een menu aan te maken. Er is alleen een prototype zodat je alleen een pointer naar een __FlashStringHelper kun gebruiken. Jammer dan.
Zie arduino Wstring.h:
class __FlashStringHelper;
#define F(string_literal) (reinterpret_cast<const __FlashStringHelper *>(PSTR(string_literal)))
In een 2-dimensionale array mag alleen de eerste dimensie onbepaald zijn. De 33 is de lengte van elke string. Die zijn dus allemaal even lang, 32 chars, en eentje extra voor de nul terminator.
O, wacht, de strings zijn maar 31 lang? Telfoutje van mij. Dan zou 32 ook kunnen. Langer mag altijd van de compiler, die vult het dan op met \0 chars. Maar als je te weinig opgeeft dan krijg je foutmeldingen.
[Bericht gewijzigd door deKees op (36%)]
Een 2-dimensionaal array van strings is niet erg efficient en kan verkwistend met progmem (flash) geheugen zijn omdat het gereserveerde geheugen voor elke string in dat array even groot is als de langste string in dat array. Als een string bijv. 100 karakters lang is en de rest maar 10 karakters lang dan wordt ook voor elk van de strings van 10 karakters lang 100 karakters aan geheugen gereserveerd wat je dus niet gebruikt of kan gebruiken voor andere doeleinden.
Beter is dan om alle strings buiten het array te declareren en een array van pointers naar die strings te maken en te gebruiken. Op die manier ga je efficient om met het beschikbare (flash) geheugen.
EricP
mét CE
Zo heel beperkend is 'ROM' (in deze: flash) doorgaans niet meer. Dus ja, je hebt gelijk. In de praktijk loopt het doorgaans wel los. Los genoeg om er niet bij voorbaat tijd aan te besteden.
Complimenten, deKees. Voor je diepgaande kennis en nog paraat ook. Ik programmeer nog te weinig C om het nog 'hapklaar' te hebben.
Overigens, nog iets wat jullie misschien al weten maar wat ik vrij handig vind voor het resetten van mijn processor.
Ik gebruik een AVRisp MKII (kloon) voor het programmeren. Na iedere nieuwe/andere versie die er dmv de programmer in geflashed word zal de processor (hardwarematig) gereset worden door de programmer.
Maar soms wil ik de processor wel eens resetten zonder dat er een nieuwe hex file in geprogrammeerd word.
Ik was aan het zoeken naar truukjes met de DTR oid van mijn USB-Seriele adapter.
Wat blijkt, als je de programmer het commando (via AVRDude) geeft : "Detect" (de processor) zal hij na de detectie (die ik niet nodig heb) de processor ook resetten.
Ik gebruik een standaard AVRIsp opzetje :
SparkyGSX
Een manager is iemand die denkt dat negen vrouwen in één maand een kind kunnen maken
@ErikP: nu kom ik zeer zelden flash tekort, maar een betrekkelijk simpele oplossing is om alle strings achter elkaar te zetten met een terminator (\0 is handig), en dan een array van pointers te maken die je bij het opstarten vult met de juiste pointers door één keer door de hele string te lopen. Je hebt dan het voordeel van minimale verspilling en je kunt er alsnog snel bij, als die lijst met pointers eenmaal gemaakt is, en dat maken is verwaarloosbaar bij het opstarten.
Is niet eens nodig om het zo te doen.
Gewoon alle strings apart declareren en een array van pointers naar die strings declareren en de compiler doet de rest.
Voorbeeld staat hier : https://www.nongnu.org/avr-libc/user-manual/pgmspace.html
Ik zie eigenlijk geen voordeel in die pointers.
Het voorbeeld dat we hier hebben heeft een vaste tabel met allemaal even lange strings. Als je daar pointers aan toevoegt heb je alleen maar meer geheugen nodig om die -extra- pointers op te slaan. En als die dan run-time wilt initialiseren dan moet je die pointers ook nog in je kostbare SRAM gaan zetten. Dan zet ik toch dikke vraagtekens bij het nut van zo een benadering.
En ook als de strings niet allemaal even lang zijn dan kun je die strings in PROGMEM zetten zonder dat je daar pointers voor nodig hebt.
Je hoeft runtime niets te initialiseren.
Het array van pointers wordt door de compiler voor je aangemaakt (gevuld met de pointers naar de apart gedeclareerde strings) en is bij aanvang van het programma al reeds klaar voor gebruik.
[Bericht gewijzigd door Bobosje op (15%)]
benleentje
Golden Member
Ik gebruik zelf altijd enum ipv #define om aan begrip of woord een waarde te geven. Volgens mij is enum een placeholder of tijdelijke constante variabele die enkel voor het compileren gebruikt word maar na het compileren zijn de enums vervangen door de getallen zelf. En #define is ook zoiets? Of zit daar toch verschil tussen
rew
four NANDS do make a NOR . Kijk ook eens in onze shop: http://www.bitwizard.nl/shop/
Op een AVR moeten strings die NIET PROGMEM gedeclareerd zijn naar RAM gecopieerd worden. Want de strings gedragen zich in C als pointers en pointers zijn in de AVR altijd naar "RAM". Program memory is wat anders, wordt op een andere manier geadresseerd.
Dan kan je een truuk verzinnen om "gewoon" te adresseren als het hoogste bitje van de pointer "0" is en "program-memory-manier" als het bitje 1 is. Klinkt leuk, maar dan is iedere pointer access ineens veel lastiger/meer code/langer.
Dus de compiler guys voor de AVR hebben er voor gekozen dat je gewone strings gewoon naar RAM worden gecopieerd. Vervolgens zijn er toevoegingen gemaakt om ze toch in program memory te kunnen laten staan en om die dan te accessen.
Dit alles is dus een "gedoe". veroorzaakt veel frusttratie en zo.
Mijn advies is eigenlijk om al die PROGMEM shit gewoon weg te halen, gewone pointers te gebruiken. Kost wat RAM maar dat moet tegenwoordig geen probleem zijn.
Mocht het wel een probleem worden, dan raad ik aan om gewoon naar een ARM processor over te stappen. Die kan je nauwelijks kleiner dan 16k RAM en 64k Flash kopen. Dus 8/2x groter dan je AVR.
@benleentje:
typedef enum {MAIN_MENU, LEVEL_1_MENU, LEVEL_2_MENU, LEVEL_3_MENU,
LEVEL_1_SUBMENU, LEVEL_2_SUBMENU, LEVEL_2_SUBSUBMENU,
LEVEL_3_SUBMENU, LEVEL_3_SUBSUBMENU} menu_pos_t;
doet ongeveer hetzelfde als de defines van de TS in de openingspost.
Maar nu heeft het een type gekregen, dus kan de compiler net wat meer er mee.
Als je dan bijvoorbeeld
switch (menu_pos) {
case MAIN_MENU: ...
...
}
een van de opties vergeet, dan kan de compiler daarvoor waarschuwen. Maar een array indexeren... Ik denk dat dat een foutmelding geeft als je geen cast naar "int" doet.
Vroeger was er een aparte preprocessor die #defines uitwerkte en gewoon de getalletjes invulde. De compiler zag dan altijd "4" ipv "LEVEL_1_SUBMENU". Tegenwoordig zit het meer "verwoven". En dus die ENUM is een compiler-feature, niet preprocessor. Het lijkt heel erg op mekaar maar is het dus net niet.
(Ik heb ooit op een computer uit 1975 gewerkt. Ding had 264k bytes aan RAM. 1) Dan zat ie vol. 2) Ding draaide gewoon multi-user-unix in dat geheugen. 3) Had gewoon een C compiler, maar dus wel aparte preprocessor en compiler in aparte processen die zonodig achter mekaar konden draaien. 1975 is "jonge jaren" voor de taal C.)
Mocht het wel een probleem worden, dan raad ik aan om gewoon naar een ARM processor over te stappen.
Arduino is inmiddels ook overgestapt. Een Arduino UNO draait tegenwoordig op een RENESAS ARM R7FA4M1, met 256K flash en 32K ram. En geen 'gedoe' met PROGMEM.
Mijn advies is eigenlijk om al die PROGMEM shit gewoon weg te halen, gewone pointers te gebruiken. Kost wat RAM maar dat moet tegenwoordig geen probleem zijn.
Op de ATMega328 heb ik maar 2K
Zoals gezegd alle strings apart (PROGMEM) declareren en een array van pointers naar de strings (PROGMEM) declareren of een 2-dimensionaal (PROGMEM) array declareren en accepteren dat je flash ruimte verkwist en in beide gevallen wat code overhead accepteren om de strings in het programma te kunnen gebruiken, het is niet anders bij AVR.
Hier staat hoe : https://www.nongnu.org/avr-libc/user-manual/pgmspace.html
rew
four NANDS do make a NOR . Kijk ook eens in onze shop: http://www.bitwizard.nl/shop/
Op zondag 23 maart 2025 21:25:58 schreef bprosman:
[...]
Op de ATMega328 heb ik maar 2K
Weet ik. En dat is tegenwoordig: "Jezelf martelen omdat je het leuk vind".
Prima als je het inderdaad leuk vind.
Maar goed. Zonder progmem declaraties gaat het bij beginnende programmeurs gewoon ook heel lang goed, maar dan gewoon "zonder gezeik". Pas na veel strings zit je RAM ineens vol en moet je gaan kijken wat je naar progmem kan verplaatsen. En omdat je op dat moment er direct mee bezig bent, kan je direct alle bugs er uit vissen omdat je weet dat het door het "verplaatsen naar progmem" moet komen dat het ineens niet meer werkt.
Ik stop sinds jaar en dag een 128kB flash ARM op een project van mij. Daar is na jarenlange ontwikkeling nu 28k van in gebruik. En toch durf ik niet de 64k versie te kopen.
Op zondag 23 maart 2025 22:59:04 schreef rew:
[...]Weet ik. En dat is tegenwoordig: "Jezelf martelen omdat je het leuk vind".Prima als je het inderdaad leuk vind.
Het is en blijft inderdaad hobby, mijn uren zijn "gratis", de rest van de code is helemaal niet zo spannend (wat HC595 aansturing) en met "a little help of my friends" (die ik zeer waardeer) hier is het probleem waar ik tegenaan liep (SRAM die ineens vol liep) opgelost en meteen weer een hoop geleerd over "C" en "C" in combinatie met Arduino in het bijzonder. Ik ben bij lange na geen "Ervaren programmeur" maar de knutsels/code die ik maak doen wat ik wil en daar gaat het me om.
Andre_avr
Golden Member
Van deze topic heb ik weer hoop c kennis opgefrist.
"Jezelf martelen omdat je het leuk vind". Dat is charme van hobby. De ene persoon vindt bv een legpuzzel verschikkelijk, de ander geniet ervan.
Een goede uitleg vond ik ook deze site:
https://www.gammon.com.au/progmem