0

This is a somewhat tricky question dealing with implementation and best-practices of the ESP8266. I'd ideally hope that it's answered by people with in-depth knowledge on the problem.

As you are hopefully aware, the Arduino (AVR) platform requires the use of the PROGMEM or F() instructions to avoid copying all strings to RAM. The ESP8266 platform also provides a PROGMEM instruction, but is memory-mapped.

I thus have several questions about what the PROGMEM instruction actually does on the ESP8266.

For the next few questions, until otherwise marked, I am referring to character-array-style C strings, not the Arduino String type.

  1. Storage of constant strings: If I were to declare a string (global, for now) on the ESP8266, would it be read from flash only when it's accessed, or will it always take up RAM (like it would on the AVR. Assume that in this case I do NOT declare it as PROGMEM; I wish to find out what the default behavior is on the ESP8266)?

  2. Local Constant Strings: What happens with strings that are used inside a function? I am fairly sure that they ALSO exist in RAM, at least in AVR. Does the ESP8266 do this too, or does it (potentially) store it in flash until it is read (it is fine if it ends up inside a cache somewhere, as long as it can be evicted if needed)?

  3. Use of String(F("mystring")): If I wrap a PROGMEM string in this manner, does this truly convert the string into a RAM-based string only when this line is reached, or does this for some reason initialize the String() early (and consume the RAM anyway)? Do the systems differ in this behavior?

  4. If I define several instances of the above (as in, the identical wrapped string shown above appears several times, perhaps in different functions), do all of them point to the SAME F() instance (since they are identical) or does it get duplicated since it's part of code rather than a referenced variable? On ESP8266? On AVR?

  5. On the ESP8266, I discovered that the server.on() function for the built-in webserver read F()'d constant strings just fine, but failed to work once the file grew beyond the size of the RAM. It was clearly stored correctly, but it could not be read. I thus conjecture that the method was making a local copy to RAM, even if the file was stored in Flash. There was also a on_P version that worked after that point (I believe I used PSTR rather than F at this point as well, since -- correct me if I am wrong -- the two are not compatible). Is this the real point of PROGMEM on ESP8266? That the data is only stored in Flash, but it also tells the function reading it to make as few copies as possible or chunk it?


I am also curious what happens with large strings that aren't constant. The web server can return data that was passed via querystring, for instance, which in my case can be quite large. If I copy that data to a string (and then into a function), does it pass the heap pointer directly, or does each String make a new copy? I am concerned about fragmentation or other fun ways of running out of RAM even if one copy should in theory fit.

Does doing something like bool myFunction(String &str){...} help? Some sort of way to actually ensure that it's the same string, rather than copying (and can I do String &myStr=server.arg(...) -- is a return value different from an assignment)? It's confusing for me, since strings are clearly passed by value, despite being a complex datatype with dynamic memory somewhere in there. So, did they override the assignment operator (and/or the one that handles reference passing, since I think there's more than one) to duplicate the string as pass-by-value of a basic type would normally do, or do they share pointers (and thus memory) as a class passed-by-value would normally do?

1 Answer 1

2

On the ESP8266 flash is a very special case. Because the ESP8266 itself does not have any flash, but instead uses an external SPI-connected flash chip, accessing it is not an easy or technically obvious operation.

Reading from the flash chip is slow. Very very slow (by comparison to reading RAM). Because of this where possible certain things are copied into RAM. Also, there are different sections of RAM that are dedicated to different purposes.

  • dram0 - This is your "Data" RAM. It's where your variables and such are stored (80kB total).
  • iram1 - Instruction RAM. It's here that interrupt routines need to be stored for rapid execution (32kB).
  • icache - Instruction cache. 16kB of data that is cached when reading from the flash chip. Executed code usually gets copied here.

Normally when you execute code the block of code that the instructions you are executing are copied from flash into the icache memory block and then executed. This is reasonably fast (reasonably in that there may be brief delays while copying the data from flash, but fast when actually executing. I don't know how efficient the cache is, whether it's split into different blocks that are loaded in the background by the memory controller as needed, but it seems to be a pretty efficient system that is used in practice).

Because of this disassociation of the CPU and flash string constants, unless specified otherwise, will be copied into dram0 during startup. This makes it very fast and simple to get at the data. Anything that you don't specify as PROGMEM will get copied into dram0. Note that you don't get all 80kB of the RAM to play with since the SDK uses a portion of it. You only get about 50kB at most (station mode - softap mode will use more).

Now... what does PROGMEM do on the ESP8266 then? Well, it adds an attribute to the definition that tells the linker to place the data in a specific section in flash (.irom.text.<filename>.<line>.<number>). This prevents it being copied into dram0 at startup.

So how do you get at it? That's where the *_P variants of the string functions (and certain overloaded functions in the Arduino core) come into play.

The whole of the flash chip is mapped (or can be, depending on register settings) into the ESP8266's memory map (address 0x40200000). Which means that it's possible to get directly at the data. However since this access doesn't go through the icache RAM block it's slow. Very very slow. About 12x slower by some accounts. Also because data can be in different locations within the chip the addresses of your string data can't be directly hard coded. There has to be some form of "it's over there" redirection when reading the data (for example if you use OTA updates you get two distinct blocks of flash where your code can be stored - the currently executing code and the previously executing version). The *_P variants of the string functions take that into account and look up where in flash the current code is located before reading from it.

So to summarise: Not using PROGMEM gives you fast access to your string data at the cost of RAM, and limits the size of the data you can use. Using PROGMEM removes the size restriction, doesn't use RAM, but is considerably slower.

To specifically answer your questions:

  1. The string data gets copied to RAM at startup.
  2. The string data gets copied to RAM at startup.
  3. The string data gets loaded from flash (using *_P variants of functions) when the String object is constructed (RAM is allocated on the heap at that moment in time). The RAM is freed when the String object is destroyed (goes out of scope).
  4. The compiler has recently had an update that will deduplicate constant PROGMEM strings.
  5. As mentioned above *_P copies directly from flash and is slow, but has no size limitation.
5
  • That was very helpful. Annoying that it's not a Harvard system but has the same end effect... :D The one thing I'm still not sure on was when the web server read the PROGMEM'd web page without need for the _P function. But given that it has that option, I'd suspect it streams the data from flash for the latter, and the former makes a RAM copy first, which of course failed because the page is very large.
    – user47164
    Dec 30, 2019 at 21:52
  • @RDragonrydr A function doesn't have to have _P to read from flash - that's just a naming convention in the C library. The web server has multiple overloaded functions that accept different data types - one of which is a "flash string".
    – Majenko
    Dec 30, 2019 at 22:05
  • Yeah, that's what I had noticed. I figure it's a fair warning that they seem to have optimized the _P version, though. The other works, right up until you try to serve a really big page and it abruptly fails, probably due to an inability to allocate a buffer somewhere. I had to switch to the dedicated progmem version despite the compatibility because of that.
    – user47164
    Jan 2, 2020 at 2:07
  • Does string de-duplication happen at all? I saw this (forum.arduino.cc/index.php?topic=194603.0) and wondered if that has been implemented for either system or if the "compiler changes" mentioned would have been added.
    – user47164
    Jan 4, 2020 at 15:27
  • 1
    @RDragonrydr I have just run some tests and yes, it looks like it does now do string deduplication for const char *foo PROGMEM variables. I just made a little sketch and with two differing strings ("this is foo" and "this is bar") they end up pointing to different locations. With both strings having "this is foo" in them they both point to the same address.
    – Majenko
    Jan 4, 2020 at 15:56

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.