Ik ben bezig met een programma in "C" voor een Arduino en het werkt niet zoals ik verwacht en ik weet niet waarom.

Initialisering (dus globale variabelen) :

// Define state constants for the state machine
#define MAIN_MENU        0
#define LEVEL_1_MENU     1
#define LEVEL_2_MENU     2
#define LEVEL_3_MENU     3
#define LEVEL_1_SUBMENU  4
#define LEVEL_2_SUBMENU  5
#define LEVEL_2_SUBSUBMENU 6
#define LEVEL_3_SUBMENU  7
#define LEVEL_3_SUBSUBMENU 8

// Define the current state variable
char currentState = MAIN_MENU;
const char* const MenuWhereAmI[] 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      *",
  NULL
};

Dus de bedoeling is dat tijdens het printen de juiste string uit MenuWhereAmI[] te pulken aan de hand van de waarde van currentState.
Als ik dan de functie MainMenuHeader() aanroep gebeurt er het volgende.

Het switch statement werkt, dus ik ga er van uit dat de waarde van currentState 0 is.
Dan deze werkt :

Serial.println(MenuWhereAmI[0]);

Deze werkt ook:

 Serial.println(MenuWhereAmI[MAIN_MENU]);

En deze geeft een lege regel

Serial.println(MenuWhereAmI[currentState]);

Voor alle zekerheid de waarde van currentState nog eens extra geprint en die is inderdaad 0

Moet je er niet een \n (newline character) achteraan zetten?

Het is natuurlijk evident dat de '0' en de 'MAIN' variant compile-time opgelost worden - dat genereert andere code.

Een string in C is feitelijk een array van characters. En zo declareer je die ook. Nu wil je een array van strings. Dus een array van character arrays. En dat dan ook nog aan een functie mee geven - die ongetwijfeld meerdere keren overloaded is om goed 'weg' te komen met het type wat je 'm voert (of - als het echt C is - compile-time gematched wordt op wat je 'm voert).
Ik heb te lang niet met dergelijke dingen gestoeid om je per direct te kunnen zeggen waar je de mist in gaat. Maar dit zijn de meest voor de hand liggende pijnpunten.

Stel dat de overloading het goed doet, dan verwacht die println waarschijnlijk een pointer naar een 0-terminated string. Uitgaande van 16-it pointer, voer je het ding waarschijnlijk met 'sterretje spatie' als adres. Daar zal wel wat staan, wat waarschijnlijk in println als blanco over komt.

[Bericht gewijzigd door EricP op (18%)]

Waarschijnlijk zit het in die PROGMEM. Haal die eens even weg (FLASH wordt dan wel naar ram gecopieerd bij opstarten). Je moet volgens mij dat ik me herinner een speciale functie aanroepen om de FLASH space te kunnen lezen.

Waarom die eerste 2 wel werken is waarschijnlijk dat de compiler dat weg optimaliseerd en domweg de strings in de code zit waar de constante MAIN_MENU of 0 gebruikt wordt. Bij het gebruikt van een variable kan de compiler dat niet en dus krijg je code die niet werkt.

Zowiezo is die hele PROGMEM raar, de compiler hoort dit gewoon te snappen als je een const char * const ... hebt dat dit in flash moet komen en bij dereferencing de juiste code moet genereren om dat weer uit te lezen. Is en blijft arduino hobby spul.

Ik zal eens kijken of ik dat nog ergens terug kan vinden.

-edit- Zou met iets a la pgm_read_... moeten. Maar dan kan je alleen zo te zien byte voor byte lezen, of een functie maken die even de string naar ram copieerd en dan print.

Er zijn veel arduino's tegenwoordig.

Je gebruikt PROGMEM, dus dan draait het waarschijnlijk op een ATMEGA328 Atmel processor?

Dan stuurt die PROGMEM de zaak in de war. De Atmel heeft speciale instrukties nodig om data uit PROGMEM te lezen. Daar zijn subroutines voor die je hier niet gebruikt hebt. Dus eigenlijk is het vreemd dat er soms toch bruikbare tekst uit komt.

Bovendien is de declaratie van je array met strings waarschijnlijk niet wat je wilt. Want op deze manier komt alleen de array met pointers in PROGMEM, de strings zelf staan in SRAM.

Dat is ook de oorzaak dat de eerste 2 goed gaan. De compiler weet in de eerste twee gevallen al welke string je wilt, en die krijg je ook. In het derde geval weet de compiler het niet en krijg je code die de pointer tabel gebruikt. Maar dat gaat mis omdat die wel het adres van de pointer tabel krijgt maar niet de instructies om die in PROGMEM te gaan halen. Daar heb je een extra subroutine voor nodig:


Serial.println(pgm_read_ptr(&MenuWhereAmI[currentState]));

Op zaterdag 22 maart 2025 23:31:18 schreef deKees:
Bovendien is de declaratie van je array met strings waarschijnlijk niet wat je wilt. Want op deze manier komt alleen de array met pointers in PROGMEM, de strings zelf staan in SRAM.

Als het goed is niet (vanwege de 2x const). Of moet je 2x PROGMEM erin zitten? Ik kijk nergens meer van op bij die arduino spullen.
-edit- => Blijkbaar moet hier dan ook nog PGM_P voor zettem, die de link van Bobosje.

Dat is ook de oorzaak dat de eerste 2 goed gaan.

Nope, dat komt door de optimalisatie omdat de index een constante is die compiletime bekend is.

Serial.println(pgm_read_ptr(&MenuWhereAmI[currentState]));

Ik denk niet dat dit gaat werken. Volgens bij lees je nu een pointer in PGM space want je kunt een PGM space pointer niet "casten" naar iets wat je direct kunt printen. Zover ik net opgezocht heb kun je alleen en tijdelijke copie maken met pgm_read_byte en die printen.

Dat is waarschijnlijk ook de reden dat de compiler zelfs bij "const char *const var" er zelf een PGM space van maakt en ook geen code kan genereren om het wel te kunnen accessen.

Info over AVR / GCC :

- Storing and Retrieving Data in the Program Space
- Storing and Retrieving Strings in the Program Space

https://www.nongnu.org/avr-libc/user-manual/pgmspace.html

Grmbl... In plain C moest je 'progmem' inderdaad separaat lezen. Dus bij alle meuk en overloading die er bij Arduino bij zit, is dat niet opgelost blijkbaar. Da's toch wel jammer...

Op zaterdag 22 maart 2025 23:51:52 schreef EricP:
Grmbl... ... Dus bij alle meuk en overloading die er bij Arduino bij zit, is dat niet opgelost blijkbaar. Da's toch wel jammer...

Inderdaad ronduit slordig.

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.

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

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.

@deKees _/-\o_ _/-\o_

Nu is me nog niet helemaal duidelijk waar die 33 vandaan komt.

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.

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 :

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