0

Platform

  • Attiny85 with AttinyCores
  • 1306 OLED i2c with Tiny4KDisplay

Goal

  • using a joystick for menu navigation
    • up/down choose
    • left enter (not finished yet)
    • right back (not finished yet)
    • inverse display the chosen item.

I come from python word which is why I am using a linked list for menu navigation. I found a weird bug that stops me from displaying some of the long text. I found that if the defined Nodes over a certain amount of number, item text over 5 chars will not display. I don't know why this is related to the number of instances. I managed to narrow down the range of the bug. In the code. if those 3 lines are commented, all the items are displayed correct, if not, only up to ccccc but not dddddd.

without the 3 lines, (commented) with the 3 lines, (uncommented)

The flash and RAM consumption of the failure situation is

Sketch uses 5180 bytes (63%) of program storage space. Maximum is 8192 bytes.
Global variables use 280 bytes (54%) of dynamic memory, leaving 232 bytes for local variables. Maximum is 512 bytes.

My code is as below. I broke it into parts to highlighted the problem. But it is a continuous file actually.

Thank you for any insight.

PS: I know there are methods that avoid oop, but I think this weird bug is so close to being fixed, (or proved to be an actual bug in the package/compiler). So I would like to pursue an answer to this question.

Update:

I tried to use another method that uses a List of Strings instead of a class to achieve the same function. The Bug is still there. I guess it is because there are too many instances of String. The question is not changed: "Is there a limitation of how many instances of the String class?".


#include <Tiny4kOLED.h>

// input setup
int x = 512;
int y = 512;
int lastx = 512;
int lasty = 512;

bool left = false;
bool right = false;
bool up = false;
bool down = false;

char input() {
  x = analogRead(A2);
  y = analogRead(A3);
  if( (lastx > 256) & (x <= 256) ){
    left = true;
  }
  if( (lastx < 768) & (x >= 768) ){
    right = true;
  }
  if( (lasty > 256) & (y <= 256) ){
    down = true;
  }
  if( (lasty < 768) & (y >= 768) ){
    up = true;
  }
  lastx = x;
  lasty = y;
}

// item data
class Node  
{  
  public:
    String text; 
    uint8_t id;
    Node * next;
    Node * previous;
    Node * parent;
    Node * child;
    Node(String s, int n){
      text = s;
      id = n;
      next = NULL;
      previous = NULL;
      parent = NULL;
      child = NULL;
    }
    void append_next(Node * other){
      this->next = other;
      other->previous = this;
      other->parent = this->parent;
    }
    void append_child(Node * other){
      this->child = other;
      other->parent = this;
    }
};  

Node main_0("aaa", 1);
Node main_1("bbbb", 2);
Node main_2("ccccc", 3);
Node main_3("dddddd", 4);
Node main_4("eeeeeee", 5);
Node main_5("ffffffff", 6);

This is how the error can be repeated.

/* Uncomment The following 3 lines will cause the problem*/
//Node item_0_0("aaa", 7);
//Node item_0_1("bbb", 8);
//Node item_0_2("ccc", 9);
Node * current_sel = (&main_0);
Node * screen_top = (&main_0);

void setup() {
  // oled setup
  oled.begin(0,0);
  oled.enableChargePump();
  oled.setRotation(1);
  // oled.setInternalIref(true);
  oled.enableZoomIn();
  oled.setFont(FONT6X8);
  oled.clear();
  oled.switchRenderFrame();
  oled.clear();
  oled.switchFrame();
  oled.on();
  // oled setup end

  // input setup
  pinMode(A2, INPUT); // X
  pinMode(A3, INPUT); // Y
  // input setup end

  // data
  main_0.append_next(&main_1);
  main_1.append_next(&main_2);
  main_2.append_next(&main_3);
  main_3.append_next(&main_4);
  main_4.append_next(&main_5);
}

void loop() {
  // input read
  input();

  // move current;
  if(up){
    up = false;
    if(current_sel->previous != NULL){
      if(screen_top == current_sel){
        screen_top = screen_top->previous;
      }
      current_sel = current_sel->previous;
    }
  }

  if(down){
    down = false;
    if(current_sel->next != NULL){
      if(screen_top->next->next->next == current_sel){
        screen_top = screen_top->next;
      }
      current_sel = current_sel->next;
    }
  }

  // oled update
  oled.clear();
  
  oled.setCursor(64, 0);
  oled.print(current_sel->text);

  Node * current_draw = screen_top;
  int i = 0;
  while(true){
  
    oled.setCursor(0,i);
    if(current_draw == current_sel){
      oled.invertOutput(true);
    }
    oled.print(current_draw->text);
    oled.invertOutput(false);
    
    if(current_draw->next == NULL){
      break;
    }
    
    current_draw = current_draw->next;
    i++;
    if(i >= 4){
      break;
    }
  }
  
  oled.switchFrame();
  delay(50);
}
2
  • what is the memory consumption reported be Arduino IDE at the end of the compilation?
    – Juraj
    Dec 26, 2020 at 18:49
  • 1
    @Juraj Thanks for your comment. The report of the failed one is "Sketch uses 5180 bytes (63%) of program storage space. Maximum is 8192 bytes. Global variables use 280 bytes (54%) of dynamic memory, leaving 232 bytes for local variables. Maximum is 512 bytes. "
    – River
    Dec 26, 2020 at 18:52

2 Answers 2

1

Each instance of your class uses RAM to store its data. There is always a limit to the number of instances; not a fixed number, but a limit imposed by the quantity of RAM available. What Majenko wrote about Strings applies equally to class instances, if instances are constructed and destroyed during the run.

A fixed number of global instances, as you have used, is more efficient but there is still only room for so many. Complicating this is the fact that RAM is a shared resource. Static variables like globals, including global class-instances, are assigned from the bottom of RAM, upward, after all of the compilations are done and before uploading to the chip. The program's run-time stack, which includes each function's automatic local data, builds from the top of RAM, downward, during the run. Stack memory is assigned and released as each function is entered and exited. But when a function calls a function which calls a function, which calls ..., the stack has to keep growing until the final call returns and the previous calls may then return.

If you attempted to define a large number of instances with a small amount of RAM available, the IDE (actually the linker, which puts together all of your code and the libraries you use) would catch the problem before your program gets uploaded.

But in the case where there all of the RAM didn't get assigned at compile- or link-time, then at run time, your stack could still possibly grow downward until it collides with the global- and fixed-data at the bottom of RAM, with (unfortunately) unpredictable results, but definitely unexpected and improper operation.

Update: I did gloss over some details, as two commenters were kind enough to point out. Yes, the heap - memory allocated dynamically at run-time - occupies the space just above the globals and other static memory allocations. Heap space can grow, the more it is used to allocate and de-allocate memory, and being low in memory, would have to grow upward, toward the stack. It is the heap where String data is allocated and re-allocated and is one of the reason's behind Majenko's warning against using the String type - especially if the strings will be edited at run time, which results in re-allocation that may not be obvious.

It is also worth noting that any class uses memory, whether Strings or user defined classes such as the OP's nodes. They are both classes and their instances will be handled similarly.

Also in OP's case, note that the class instances are allocated 'statically' - all instances are known and allocated during the run setup and not altered during the run. In 'quotes', because the allocation actually does happen during run-time, but at the very start of the run, and prior to the user sketch receiving control.

2
  • 1
    Great answer. It should probably also mention heap fragmentation though. (It sounds like the OP uses a fixed number of statically allocated objects, so it might no be relevant here, but if you're gonna give a nice complete answer...)
    – Duncan C
    Dec 28, 2020 at 0:48
  • 1
    The stack will collide with the heap before reaching .bss or .data. Dec 28, 2020 at 13:27
2

Do not use String. Ever. Especially not on a chip with such a tiny amount of RAM.

String uses dynamic allocation to store the string data in RAM. This is very inefficient, especially when you are working with string literals.

Those literals must be stored in flash, not RAM, so you need to be working with const char * and PROGMEM, along with the F() macro.

When using a chip with only a few hundred bytes of RAM you need to be as efficient as you possibly can. You need to account for each and every byte you use - and you just can't do that with String.


As an example, let's look at what this does:

Node main_5("ffffffff", 6);

You have a string of 8 characters plus a NULL terminating character. That's 9 bytes. Those 9 bytes are copied into RAM at boot time. Current total 9 bytes. You then convert that to a String and copy the character data into that temporary object - that's now 18 bytes plus the size of the String object. Then:

text = s;

You allocate 9 bytes inside the text String object and copy the data in. You're now at 27 bytes plus two String objects (one of which you created earlier in your class). A little over 5% of your entire memory just to move text around the place. A third of that you claw back from your heap as the objects leave scope, but then you have a hole in your heap. And this is the last set of operations after you've already made a complete mess of your heap with the earlier ones.

Whereas

const char main_5[] PROGMEM = "ffffffff";

uses just 2 bytes of RAM to store the pointer and nothing else.

1
  • Thank you so much for your answer. I accepted the other one because it shows more insight into why that bug is happening. But your answer actually led me in the right direction. I end up writing a python code dealing with the linked list stuff and generates related C code that defines data in the Flash memory.
    – River
    Dec 31, 2020 at 0:26

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.