Optimizing SRAM, etc

Hello again,

I’m in the process of testing this approach and currently am unaware of any unwanted side effects this might introduce. I’m aware of heap fragmentation and pretty much the concepts of optimizing an Arduino program based on what I’ve read here: Optimizing SRAM | Memories of an Arduino | Adafruit Learning System

So I basically took the “Think Globally, Allocate Locally” thought to the next level and rather than instantiate library variables in global scope, I’ve moved them to their own functions and with their library return type.

As an example:

#include <Wire.h>
#include <Adafruit_RGBLCDShield.h>
#include <Adafruit_MCP23017.h>

Adafruit_RGBLCDShield lcd = Adafruit_RGBLCDShield();

void setup()
{
  lcd.begin(16, 2);
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Loading...");
}

void loop()
{

}
// Binary sketch size: 4,776 bytes (used 15% of a 32,256 byte maximum) (0.71 secs)
// Minimum Memory Usage: 273 bytes (13% of a 2048 byte maximum)

Now compare this to the following:

#include <Wire.h>
#include <Adafruit_RGBLCDShield.h>
#include <Adafruit_MCP23017.h>

// Experimental self-disposing pattern (avoids declaring global variables permanently in SRAM)
Adafruit_RGBLCDShield getLcd()
{
  const uint8_t
    LCD_COLS = 16,
    LCD_ROWS = 2;
  
  Adafruit_RGBLCDShield lcd = Adafruit_RGBLCDShield();
  lcd.begin(LCD_COLS, LCD_ROWS);

  return lcd;
}

void setup()
{
  Adafruit_RGBLCDShield lcd = getLcd();
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Loading...");
}

void loop()
{
}
// Binary sketch size: 4,828 bytes (used 15% of a 32,256 byte maximum) (0.75 secs)
// Minimum Memory Usage: 245 bytes (12% of a 2048 byte maximum)

Here is a more complete context:

#include <Wire.h>
#include <Adafruit_NFCShield_I2C.h>
#include <Adafruit_RGBLCDShield.h>
#include <Adafruit_MCP23017.h>

// Experimental self-disposing pattern (avoids declaring global variables permanently in SRAM)
Adafruit_RGBLCDShield getLcd()
{
	const uint8_t
		LCD_COLS = 16,
		LCD_ROWS = 2;
	
	Adafruit_RGBLCDShield lcd = Adafruit_RGBLCDShield();
	lcd.begin(LCD_COLS, LCD_ROWS);

	return lcd;
}

Adafruit_NFCShield_I2C getNfc()
{
	const uint8_t
		IRQ = 2,
		RESET = 3,
		MAX_TRIES = 3;

	const uint16_t RETRY_DELAY = 1000;

	Adafruit_NFCShield_I2C nfc(IRQ, RESET);
	for (uint8_t i = 1; i < MAX_TRIES; i++)
	{
		if (nfc.getFirmwareVersion())
		{
			nfc.SAMConfig();
			break;
		}

		delay(RETRY_DELAY);
	}

	return nfc;
}

void lcdPrint(const char *str)
{
	Adafruit_RGBLCDShield lcd = getLcd();
	lcd.clear();
	lcd.setCursor(0, 0);
	lcd.print(str);
}

void scanTag(uint8_t *dst, uint8_t len)
{
	// ...
}

void setup()
{
	lcdPrint("Loading!");
}

void loop()
{
}
// Binary sketch size: 5,636 bytes (used 17% of a 32,256 byte maximum) (0.73 secs)
// Minimum Memory Usage: 360 bytes (18% of a 2048 byte maximum)

This follows more of a “single responsibility” approach to coding but it seems this is really the way we should be using these huge libraries when instantiating their variables that take up so much space at boot and are left in SRAM forever even when not in use (SdFat anyone?). If local variables are truly 100% reclaimed when the function exits, self-disposing seems like an added incentive for simple yet optimal code. I’m curious to know what the side effects might be other than slower performance perhaps?

Your thoughts?

What you've done is created a bug. You are creating that lcd object on the stack when the function runs. That stack will get over-written in subsequent function calls, and, at some point, the object will be over-written, and your program will misbehave, and likely crash.

Regards,
Ray L.

How exactly does that matter to the program if the function itself is merely re-creating the object each time it is called? Even if previous instances got overwritten, subsequent calls would receive the newly created object as in lcdPrint().

Wouldn't the problem be more of a stack running out of space issue?

Just for grins, I threw that lcdPrint() into a for loop to call it 200+ times and it seems to avert the "subsequent calls" issue in that it isn't misbehaving in any way. Not sure if that's a legitimate way of testing it...

I don't see a huge problem with creating a global variable, if it is something that is always needed to be around.

You have just moved the problem from one place to another.

RayLivingston:
What you've done is created a bug. You are creating that lcd object on the stack when the function runs. That stack will get over-written in subsequent function calls, and, at some point, the object will be over-written, and your program will misbehave, and likely crash.

Regards,
Ray L.

As the function does not return a pointer or reference, the returned object is simply copied into stack on the callers side. As long as C++ can implicitly generate a copy constructor (nothing requiring deep copying), then everything should copy fine.

All the copying can be avoided though.

void getLcd( Adafruit_RGBLCDShield &lcd )
{
  const uint8_t LCD_COLS = 16, LCD_ROWS = 2;
  lcd.begin(LCD_COLS, LCD_ROWS);
  return;
}

void setup()
{
  Adafruit_RGBLCDShield lcd;
  getLcd( lcd );

  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Loading...");
}

The problem I see, is the fact you could be running an expensive routine (lcd.begin()) each time you need to do something. And how you checked that when you .begin() your screen for the nth time, that it does not corrupt whats already on the screen?

pYro_65:
...
The problem I see, is the fact you could be running an expensive routine (lcd.begin()) each time you need to do something. And how you checked that when you .begin() your screen for the nth time, that it does not corrupt whats already on the screen?

Yes, it would be an expensive routine to run each time...but what's more expensive, limiting already limited SRAM permanently by initializing the LCD and/or SD instance variables in global scope, or creating them only when needed (on-demand)? My goal is to maximize that vitally important SRAM space that if not somehow "recycled" (e.g. overwritten), causes my sketch to become more consistently prone to system instability.

As far as corrupting what's already on the screen, if you'll notice that my lcdPrint() function is the only one that calls getLcd() and is followed by lcd.clear(). But to your point, I could probably just add lcd.clear() to the getLcd() routine as part of instantiation.

As it stands, the approach I've outlined seems to remain valid but I would greatly appreciate any additional feedback from anyone else (please carefully analyze the original sketch example from a "runtime optimization" standpoint).

Thanks again!

P.S. Just stumbled across Stino which is a [so far] excellent working plugin for adding Arduino support to SublimeText :slight_smile: Started using it tonight and it's sooooo much better, especially on a Mac where Visual Studio + VisualMicro is unavailable.

And gone from a problem that can be easily identified at compile-time to a problem that requires run-time analysis to identify.

pYro_65:
...And how you checked that when you .begin() your screen for the nth time, that it does not corrupt whats already on the screen?

After looking at the actual C++ file in the library, begin() calls clear() on its own already so a second clear() in the sketch is not needed.