Won't initialize SSD1306 if certain code is included

I have the following robot-eye-animation code that I'm trying to debug. It runs on an official Arduino UNO R3 if the first line (#define DEBUGGING) is set to 0, but if I set it to 1 (which enab les some print/println around lines 209-228) it fails to allocate the SSD1306 device.

As a background, I'm trying to determine why eyeLevel never reaches a value > 11, but that's not material to the question.

When I compile this with DEBUGGING set to 1 (the scenario that doesn't work), I have the following result:

Sketch uses 21336 bytes (66%) of program storage space. Maximum is 32256 bytes.
Global variables use 807 bytes (39%) of dynamic memory, leaving 1241 bytes for local variables. Maximum is 2048 bytes.
/home/dennis/.arduino15/packages/arduino/tools/avrdude/8.0.0-arduino1/bin/avrdude -C/home/dennis/.arduino15/packages/arduino/tools/avrdude/8.0.0-arduino1/etc/avrdude.conf -v -patmega328p -carduino -P/dev/ttyACM0 -b115200 -D -Uflash:w:/tmp/arduino_build_176337/robot_vector.ino.hex:i 
Avrdude version 8.0-arduino.1
Copyright see https://github.com/avrdudes/avrdude/blob/main/AUTHORS

System wide configuration file is /home/dennis/.arduino15/packages/arduino/tools/avrdude/8.0.0-arduino1/etc/avrdude.conf
User configuration file /home/dennis/.avrduderc does not exist

Using port            : /dev/ttyACM0
Using programmer      : arduino
Setting baud rate     : 115200
AVR part              : ATmega328P
Programming modes     : SPM, ISP, HVPP, debugWIRE
Programmer type       : Arduino
Description           : Arduino bootloader using STK500 v1 protocol
HW Version            : 3
FW Version            : 4.4

AVR device initialized and ready to accept instructions
Device signature = 1E 95 0F (ATmega328P, ATA6614Q, LGT8F328P)
Reading 21336 bytes for flash from input file robot_vector.ino.hex
in 1 section [0, 0x5357]: 167 pages and 40 pad bytes
Writing 21336 bytes to flash
Writing | ################################################## | 100% 3.44s
Reading | ################################################## | 100% 2.74s
21336 bytes of flash verified

Avrdude done.  Thank you.

The above, seems to me, implies that I have enough memory, so I'm not sure what's going on.

Although I do understand about posting minimal code, in this case I don't know how to force the issue without sharing the entier code block. Please understand I am not looking for someone to debug the code; only for someone to explain why I cannot successfully include the print/println lines.

Thanks

#define DEBUGGING 1

// based largely on PicaioVideos post at https://github.com/Picaio/roboteyes/
// Features added for moving eyes up and down, automatic random eye movements + code cleanup, macro names etc.

#include <Adafruit_GFX.h>
#include <Adafruit_GrayOLED.h>
#include <Adafruit_SPITFT.h>
#include <Adafruit_SPITFT_Macros.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_SSD1306.h>
#include <gfxfont.h>
#include <splash.h>
#include <Wire.h>

#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
#define OLED_RESET     -1 // Reset pin # (or -1 if sharing Arduino reset pin)
// Declaration for an SSD1306 display connected to I2C (SDA, SCL pins)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
#define BUTTON_LEFT  8    // Pin number for button 1
#define BUTTON_RIGHT  9   // Pin number for button 2
#define BUTTON_MOOD 10    // Pin number for button 3
#define BUTTON_UP 11      // Pin number for button 4
#define BUTTON_DOWN 12    // Pin number for button 5
#define PRESSED_LEFT 0x01
#define PRESSED_RIGHT 0x02
#define PRESSED_MOOD 0x04
#define PRESSED_UP 0x08
#define PRESSED_DOWN 0x10
#define MIN_EYE_X 0
#define MAX_EYE_X 31
#define MIN_EYE_Y 0
#define MAX_EYE_Y 31
#define MAX_WAIT_FOR_BUTTON 60000 // 60000 milliseconds = 60 seconds
#define MIN_EMULATE_BUTTONS_WAIT 1000 // 1000 milliseconds = 1 second
#define MIN_AUTO_MOOD_CHANGE_TIME 30000 // 30000 milliseconds = 30 seconds
#define MOOD1 0
#define MOOD_NORMAL 1
#define MOOD3 2
#define MOOD4 3
#define MOOD4 4
#define MOOD5 5
#define MAX_MOOD 5

#define DISPLAY_EYE 0
#define BLINK_EYE 1

#include "eyes.h"

unsigned long lastButtonPressTime ;
unsigned long lastButtonActionTime ;
unsigned long lastMoodChangeTime ;
int xp = 16;
int mood = 1;

void setup() {
  Serial.begin(115200);
  Serial.println("Booting");

  pinMode(BUTTON_LEFT,  INPUT_PULLUP); // Button 1
  pinMode(BUTTON_RIGHT, INPUT_PULLUP); // Button 2
  pinMode(BUTTON_MOOD,  INPUT_PULLUP); // Button 3
  pinMode(BUTTON_UP,    INPUT_PULLUP); // Button 4
  pinMode(BUTTON_DOWN,  INPUT_PULLUP); // Button 5

  if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3c)) { // Address 0x3C for 128x64
    Serial.println(F("SSD1306 allocation failed"));
    for (;;); // Don't proceed; loop forever
  }

  // Show initial display buffer contents on the screen --
  // the library initializes this with an Adafruit splash screen.
  display.display();

  // Clear the buffer
  display.clearDisplay();
  display.display();
  lastButtonPressTime = millis() ;
  lastButtonActionTime = millis() ;
  lastMoodChangeTime = millis() ;
  updateEyes(1) ; // Init only
}


unsigned char readButtons(void) {
  unsigned char ret = 0; // No button pressed
  if (digitalRead(BUTTON_LEFT)  == 0) ret |= PRESSED_LEFT ;
  if (digitalRead(BUTTON_RIGHT) == 0) ret |= PRESSED_RIGHT ;
  if (digitalRead(BUTTON_MOOD)  == 0) ret |= PRESSED_MOOD ;
  if (digitalRead(BUTTON_UP)    == 0) ret |= PRESSED_UP ;
  if (digitalRead(BUTTON_DOWN)  == 0) ret |= PRESSED_DOWN ;
  if (ret != 0)
      lastButtonPressTime = millis() ;
  return (ret);
}


void loop() {
  updateEyes(0) ;
}



void updateEyes(int init) {
  int n ;
  int buttonFlags ;
  static int xd = 0;
  static int delayTime = 0;
  static int step = DISPLAY_EYE;
  static int eyeLevel = 8 ;
  static int lastX1, lastX2, lastY, lastMood ;
  int x1, x2;
  unsigned long epoch ;
  int autoMood = 0 ;
  int sinceLastButtonAction = epoch - lastButtonActionTime ;
  int sinceLastMoodChange = epoch - lastMoodChangeTime ;

  if (init) {
    xd = 0 ;
    delayTime = 0 ;
    step = DISPLAY_EYE ;
    eyeLevel = 8 ;
    lastX1 = -1 ;
    lastX2 = -1 ;
    lastY = -1 ;
    lastMood = -1 ;
    return ;
  }
  if (delayTime > 0) { // was espera
    delayTime-- ;    // was espera
    delay(1);
  } else {
    x1 =   xd + (xp > 16 ? (16 + 2 * (xp - 16)) : xp);
    x2 = 64 + xd + (xp < 16 ? (-16 + (xp * 2))  : xp);
    switch (step) {
      case DISPLAY_EYE:
        display.clearDisplay(); // Clear the display buffer
        if (x1 != lastX1 || x2 != lastX2 || eyeLevel != lastY || mood != lastMood) {
          printStats(x1, x2, eyeLevel, mood) ;
          lastX1 = x1 ;
          lastX2 = x2 ;
          lastY = eyeLevel ;
          lastMood = mood ;
        }
        if (xp < 6) {
          display.drawBitmap(x1, eyeLevel, peyes[mood][2][0], 32, 32, WHITE);
          display.drawBitmap(x2, eyeLevel, peyes[mood][1][1], 32, 32, WHITE);
        } else if (xp < 26) {
          display.drawBitmap(x1, eyeLevel, peyes[mood][0][0], 32, 32, WHITE);
          display.drawBitmap(x2, eyeLevel, peyes[mood][0][1], 32, 32, WHITE);
        } else {
          display.drawBitmap(x1, eyeLevel, peyes[mood][1][0], 32, 32, WHITE);
          display.drawBitmap(x2, eyeLevel, peyes[mood][2][1], 32, 32, WHITE);
        }
        display.display();
        delayTime = random(250, 1000);
        n = random(0, 7);
        if (n == 6) { // One time out of 7, set step to 1, else 2
          step = BLINK_EYE ;
        } else {
          step = 2;
        }
        break;
      case BLINK_EYE :
        display.clearDisplay(); // Clear the display buffer
        display.drawBitmap(x1, eyeLevel, eye0, 32, 32, WHITE);
        display.drawBitmap(x2, eyeLevel, eye0, 32, 32, WHITE);
        display.display();
        delayTime = 100;
        step = DISPLAY_EYE ;
        break;
      case 2: // TODO What's happening here?
        n = random(0, 10);
        if (n < 5) xd--;
        if (n > 5) xd++;
        if (xd < -4) xd = -3;
        if (xd > 4) xd = 3;
        delayTime = 0;
        step = DISPLAY_EYE ;
        break;
    }
  }

  epoch = millis() ;
  if ((( buttonFlags = readButtons()) == 0) // No button press
       && (sinceLastButtonAction >= MIN_EMULATE_BUTTONS_WAIT)) { // Maybe emulate a button press
    n = random(0, 1000) ;
    if (n < 100) { // one time out of 10
        buttonFlags = PRESSED_LEFT ;
        if (n < 33)
            buttonFlags |= PRESSED_UP ;
        if (n >= 66)
            buttonFlags |= PRESSED_DOWN ;
    } else if (n < 200) { // one time out of 10
        buttonFlags = PRESSED_RIGHT ;
        if (n < 133)
          buttonFlags |= PRESSED_UP ;
        if (n >= 166)
          buttonFlags |= PRESSED_DOWN ;
    } else if ((n > 970) && (sinceLastMoodChange >= MIN_AUTO_MOOD_CHANGE_TIME)) { // 3 times out of 100
        buttonFlags = PRESSED_MOOD ;
        autoMood = 1 ; // Mood change was automatic, not by button press
    }
  }
  if (buttonFlags) { // If a button is pressed (or we're emulating same):
    lastButtonActionTime = epoch ;
    if (buttonFlags & PRESSED_UP) {
      if (DEBUGGING) {
          Serial.print("Pressed UP.  Eye level is currently ") ;
          Serial.print(eyeLevel) ;
      }
      eyeLevel = (eyeLevel <= MIN_EYE_Y) ? MIN_EYE_Y : eyeLevel - 1 ;   // Higher on the display if lower Y
      if (DEBUGGING) {
        Serial.print("... and now it is ") ;
        Serial.println(eyeLevel) ;
      }
    }
    if (buttonFlags & PRESSED_DOWN) {
      if (DEBUGGING) {
          Serial.print("Pressed DOWN.  Eye level is currently ") ;
          Serial.print(eyeLevel) ;
      }
      eyeLevel = (eyeLevel >= MAX_EYE_Y) ? MAX_EYE_Y : eyeLevel + 1 ;   // Lower on display if higher Y
      if (DEBUGGING) {
        Serial.print("... and now it is ") ;
        Serial.println(eyeLevel) ;
      }
    }
    if (buttonFlags & PRESSED_LEFT) {
      xp = (xp <= 0 ? 0 : xp - 1); // Decrease x position unless = zero
    }
    if (buttonFlags & PRESSED_RIGHT) {
      xp = (xp >= MAX_EYE_X ? MAX_EYE_X : xp + 1); // Increase X position unless = max
    }
    if (buttonFlags & PRESSED_MOOD) {
      if (autoMood) { // Randomly select a mood, but skip MOOD3
        while ((mood = random(0, MAX_MOOD)) == MOOD3) {} //select random until not equaL MOOD3
      } else {
        mood = (mood >= MAX_MOOD) ? 0 : mood + 1; // Between 0 and MAX_MOOD // TODO maybe mood = mod(mood+1, MAX_MOOD) ;
      }
      if (autoMood && mood == MOOD3) mood = MOOD4 ; // This is not a good automatically-chosen mood.
      lastMoodChangeTime = epoch ;
    }
    delayTime = 0; // delayTime was espera
    step = DISPLAY_EYE ;
    // TODO There is a (VERY SLIM) chance that a button would be pressed between the time we decided to emulate a button-press and now.
    // TODO If that happens, the following code will cause us to miss that button press.  We could avoid by changing the while to
    // TODO be sensitive to previous button-press value.
    do {} while (readButtons() != 0) ;   // Wait for button(s) to be released
  }
}



void printStats(int x1, int x2, int eyeLevel, int mood) {
  static int minX = 256, maxX = 0, minY = 256, maxY = 0 ;
  char buffer[128] ;
  minX = min(x1, min(x2, minX)) ;
  maxX = max(x1, max(x2, maxX)) ;
  minY = min(eyeLevel, minY) ;
  maxY = max(eyeLevel, maxY) ;
  sprintf(buffer, "X1=%2d X2=%2d Y=%2d Mood=%d  Min X=%d, Max X=%d, Min Y=%d, max Y = %d", x1, x2, eyeLevel, mood, minX, maxX, minY, maxY) ;
  Serial.println(buffer) ;
}

Adafruit_SS1306 wants to allocate a 1024 byte screen buffer at runtime. That bumps your used RAM up to 1831 bytes, 89.4%. 217 bytes of RAM isn't much to run everything else.

Your DEBUGGING code has lots of character string literals that aren't enclosed in the F() macro. Therefore they take up space in RAM when they don't need to.

All those character string literals you're printing? Enclose them in F() and see what happens.

Why included twice?

When you do this, use the 'F' macro to store the character array in program memory rather than as a variable in dynamic memory:

Serial.print(F("Pressed UP.  Eye level is currently ")) ;

The SSD1306 required 1024 bytes of RAM for its frame buffer so you don't have quite as much memory available as the compiler output indicates. I believe there are libraries that implement either no framebuffer or a reduced sized one (U8g2?).

(Post crossed with #2)

Note that you are allocating 128 bytes for a local variable, that along with the original 807 bytes + 1024 for the display buffer, only leave you 89 for all the stack and other local variables.

I was looking at this in particular to show you how to use sprintf_P to store the format string in PROGMEM.

void printStats(int x1, int x2, int eyeLevel, int mood) {
  static int minX = 256, maxX = 0, minY = 256, maxY = 0 ;
  char buffer[128] ;
  minX = min(x1, min(x2, minX)) ;
  maxX = max(x1, max(x2, maxX)) ;
  minY = min(eyeLevel, minY) ;
  maxY = max(eyeLevel, maxY) ;
  sprintf_P(buffer, PSTR("X1=%2d X2=%2d Y=%2d Mood=%d  Min X=%d, Max X=%d, Min Y=%d, max Y = %d"), x1, x2, eyeLevel, mood, minX, maxX, minY, maxY) ;
  Serial.println(buffer) ;
}

All excellent suggestions! Now I've learned more than I expected to about memory usage. Thanks so much for your suggestions; the problem is solved.

As there are other things I want to do with this (evenutally becoming part of a robot), I will be ordering a more advanced board.

Thanks for your suggestions!

While you are correct, there is no need, most quality libraries, this one included, start off with a guard check of the following form:

#ifndef _Adafruit_SSD1306_H_
#define _Adafruit_SSD1306_H_

So, while poor form, this part of our OP's code doesn't do any overt harm - the library "locks the door" on the first invocation, blocking the second.

For @delovelady : Why do I call it poor form? Because unfortunately, there are far too many libraries out there that do NOT include this guard #define structure, and would therefore potentially create two complete images of any memory required by the library. You wouldn't get two copies of any code, because that would involve function redefinitions (among other booboos), generating compile errors, but it certainly wouldn't complete, and might be confusing for you to sort out.

Yeah, it was an oversight when I was cleaning up the code for this post. TBH, I don't know how it originally got therte twice (one of many downsides to IDEs), but as you say, it did no harm. I will say the line is not there due to anything I did in the eitor. Anyway, the issue is resolved. Thanks for your message.