MCUdude / MegaCore

Arduino hardware package for ATmega64, ATmega128, ATmega165, ATmega169, ATmega325, ATmega329, ATmega640, ATmega645, ATmega649, ATmega1280, ATmega1281, ATmega2560, ATmega2561, ATmega3250, ATmega3290, ATmega6450, ATmega6490, AT90CAN32, AT90CAN64 and AT90CAN128
Other
374 stars 115 forks source link

How do I put text strings into ROM? #183

Closed migry closed 2 years ago

migry commented 2 years ago

I am helping a friend who is porting "arduino" code to an existing board which uses an ATMEGA128 CPU. The "Megacore" seems perfect for this task and compiles without issues, although we need more work to move away from some custom libraries. FWIW: The original code was compiled on a Teensy.

The application did not run. We established that it was a lack of run time RAM (determined by commenting out one of the modules). So I am working to try to optimise RAM usage. In the "serial GUI" modules I have lots of writes of fixed strings to the serial port (via a helper function - see below). I figured out that these strings are not only stored in ROM, but are in RAM and must get initialised from ROM by the C runtime. So these fixed strings consume valuable RAM.

On other Arduino projects I have used the "F()" macro to wrap fixed strings, but this does not compile. Just BTW there is the main ino, but the GUI is in it's own ".cpp" module, and so explicit #defines are needed for library functions. In the ".ino" this is all done for you, so was another discovery.

#include <WString.h>

extern void SerialPrint(const char *s);

const char msg_OK[] __attribute__((section(".FAR_MEM1"))) = "OK";

void bug(void)
{
   SerialPrint(msg_OK);  //<- compiles, but unsure if actually works
   SerialPrint(F("OK")); // gives error
}
G:\TEMP\WINDOWS\arduino_build_673644\sketch\bug.cpp: In function 'void bug()':
bug.cpp:10:23: error: cannot convert 'const __FlashStringHelper*' to 'const char*' for argument '1' to 'void SerialPrint(const char*)'
    SerialPrint(F("OK")); // gives error
                       ^
Using library I2C_BitBang_SSD1306 at version 2.5.1 in folder: C:\Users\abc\Documents\Arduino\libraries\I2C_BitBang_SSD1306 
Using library Adafruit_BitBang_GFX at version 1.10.3 in folder: C:\Users\abc\Documents\Arduino\libraries\Adafruit_BitBang_GFX 
Using library EEPROM at version 2.0 in folder: C:\Users\abc\AppData\Local\Arduino15\packages\MegaCore\hardware\avr\2.1.3\libraries\EEPROM 
Using library Timer at version 2.1 in folder: C:\Users\abc\AppData\Local\Arduino15\packages\MegaCore\hardware\avr\2.1.3\libraries\Timer 
exit status 1
cannot convert 'const __FlashStringHelper*' to 'const char*' for argument '1' to 'void SerialPrint(const char*)'

So the question is, how do I put fixed text strings into ROM and then pass a pointer to the Serial print wrapper function?

In order to print to the serial port, I have the following functions. A pointer to the string is passed to the main ".ino" where Serial.print(s) can be used.

// functions in main sketch which output on the serial port
extern void SerialPrint(const char *s);
extern void SerialPrintln(const char *s);

Support code in main ".ino" for writing a string to the Serial port...

void SerialPrint  (const char *s) { Serial.print  (s); }          
void SerialPrintln(const char *s) { Serial.println(s); } 

Thank you for any help.

MCUdude commented 2 years ago

Hi!

If you don't want to mess around with 32-bit pointers, just use the F() macro.

Support code in main ".ino" for writing a string to the Serial port...

void SerialPrint  (const char *s) { Serial.print  (s); }          
void SerialPrintln(const char *s) { Serial.println(s); } 

Why would you do this? I see no benefit here except that it won't work with the F() macro. You can use Serial.print/println in the cpp file by including Arduino.h at the very top of the file: #include <Arduino.h>

Here's some example code you can use as a reference, that does work

const char str_in_progmem[] PROGMEM = {"This is a string stored in PROGMEM the old fasion way!\n"};

void setup()
{
  delay(1000);
  Serial.begin(9600);

  Serial.println(F("Example using the F() macro!"));

  // Print str_in_progmem array
  for(uint8_t i = 0; i < sizeof(str_in_progmem); i++)
  {
    char c = pgm_read_byte(str_in_progmem + i);
    Serial.write(c);
  }
}

void loop()
{
  Serial.printf(F("Milliseconds since start: %ld ms\n"), millis());
  delay(1000);
}
migry commented 2 years ago

Apologies for not replying sooner. I just saw the issue closed email.

Firstly thank you for your reply and the information which you gave. It was invaluable and was of great help.

The problem was NOT with Megacore, but a result of my lack of knowledge of C++ and the AVR architecture. Apologies for raising an issue which is unrelated to this project.

Your suggestion of including "Arduino.h" and not individual dot h files was the solution to allow me to access the Serial port from the dot cpp module.

Just in case anyone arrives here from google, here is some things I understand better since having raised the issue...

If I understand correctly, fixed strings used in the source code are stored in ROM, but are copied to RAM during the C startup, and take up valuable RAM. It is easy for the unaware programmer (i.e. me) to eat away at the valuable RAM resource, without being aware. Prints of various flavours are the main reason, and most of the time these fixed strings can be "forced" into ROM. In my case the lack of RAM (unnecessarily consumed by my fixed strings) led to the program crashing due to lack of stack and/or heap space (or likely the stack and heap overlapped). So be aware and use a number of different ways to avoid this unnecessary use.

I found this information on this web page invaluable...

https://www.e-tinkers.com/2020/05/do-you-know-arduino-progmem-demystified

The Teensy has a linear address range, and the issue of strings in RAM/ROM gets taken care of without needing to use the PROGMEM keyword. Also it has lots of RAM, which sort of hides the issue of string RAM usage from you.

The PROGMEM keyword is needed on the AVR (e.g. Atmega128) to force strings into ROM, e.g.

const char Serial1[] PROGMEM = "* SERIAL BUFFER OVERFLOW **\n";

Warning...

const char Serial2[] = "* OPERATION COMPLETE **\n"; // gets put in RAM on the AVR but (only) ROM on the Teensy

The issue arises since the AVR has a Harvard architecture which means that there is 64k of addresses of both ROM and RAM. On the Atmega128 I noted in the machine code listing that when an address/pointer was passed there was nothing to distinguish a ROM address from a RAM address, so it was the responsibility of the called routine to know what is was receiving. So I now use "pgm_read_byte(ptr)" when I know "ptr" holds a ROM address.

I could be wrong here, but any function which expects a pointer to char, will either get a RAM address or a ROM address, depending upon what "thing" you pass as the parameter (e.g. Serial1 or Serial2 above). If you pass a string which is in ROM (thanks to PROGMEM) then you need to use pgm_read_byte() to force the compiler to read from ROM (byte by byte), otherwise the read will be asumed to be from RAM. There are some useful functions for string manipulation such as strcpy_P() - please google for information as to how and when to use them.

My less than perfect fix is to have two print functions, one which expects a ROM pointer and the other which expects a RAM pointer. C++ experts may well have a solution to allow you to pass either type of pointer, but I am only "C" grade :-) .

Note, fixed strings can be wrapped in the F() macro, and this works with a lot of standard Arduino library functions such as Serial.print(). This macro was incorrectly used in my own code, which led to the error message. Again please google for more information.

I hope this helps anyone in a similar situation as to me, or anyone else unknowingly eating up precious RAM. Apologies in advance for any mistakes or misunderstandings in my explaination.