1

So I've been running into a weird issue while trying to compartmentalize some test code I wrote for an Arduino/NeoPixel application. There are two scenarios: Scenario A, my code before the move, and Scenario B, my code after the move. The test code works as expected in Scenario A (a red light walks across my 8x8 led matrix). The very same code, when moved to a container class (Scenario B) results in odd behavior (A blotch of randomly colored LEDs appears and doesn't move). A simple move of functionality from one place to another doesn't seem like it would be able to cause these kinds of symptoms though, so I'm a bit lost.

Here are some pictures. Scenario A Scenario B

I've attached code for the two different scenarios below. I have removed sections of code and includes that aren't being referenced yet for clarity purposes.

I'm still more or less a hobbyist when it comes to Arduino/C++, so feel free to point minor things out as well.

Scenario A

Program.ino

#include <Arduino.h>
#include "Hardware.h"

Hardware* hardware = new Hardware();

void setup()
{
  hardware->Setup();
}

uint8_t i = 0;

void loop()
{
  auto screen = hardware->GetScreen();
  screen->Clear();
  screen->SetLedHSV(i++ % screen->Count(), 0, 255, 255);
  screen->Show();

  delay(100);
}

Hardware.h

#pragma once
#include "Screen.h"

class Hardware
{
private:
    Screen screen = Screen(8, 8, 14);

public:
    void Setup()
    {
        screen.Setup();
    }

    Screen* GetScreen() { return &screen; }
};

Screen.h

#pragma once
#include <Adafruit_NeoPixel.h>

class Screen
{
private:
    uint8_t width, height;
    uint8_t pin;

    Adafruit_NeoPixel pixels;

public:
    Screen(uint8_t width, uint8_t height, uint8_t pin) :
        width(width), height(height), pin(pin)
    {
        pixels = Adafruit_NeoPixel(width * height, pin, NEO_GRB + NEO_KHZ800);
    }

    void Setup()
    {
        pixels.begin();
        pixels.show();
        pixels.setBrightness(32);
    }

    uint16_t Count() { return width * height; }
    uint8_t GetWidth() { return width; }
    uint8_t GetHeight() { return height; }

    void Show()
    {
        pixels.show();
    }

    void Clear()
    {
        pixels.clear();
    }

    void SetLedHSV(uint16_t i, uint16_t h, uint8_t s, uint8_t v)
    {
        pixels.setPixelColor(i, pixels.ColorHSV(h, s, v));
    }

    void SetLedHSV(uint8_t x, uint8_t y, uint16_t h, uint8_t s, uint8_t v)
    {
        if (x < 0 || x >= width)
            return;
        if (y < 0 || y >= height)
            return;

        auto i = x + y * width;
        pixels.setPixelColor(i, pixels.ColorHSV(h, s, v));
    }
};

Scenario B

Program.ino

#include <Arduino.h>
#include "Hardware.h"
#include "TestApp.h"

unsigned long timestamp;
Hardware* hardware = new Hardware();
TestApp* app = new TestApp(hardware);

void setup()
{
  hardware->Setup();
}

void loop()
{
  app->Update();
  delay(100);
}

Hardware.h

Same as above.

Screen.h

Same as above.

TestApp.h

#pragma once
#include "Hardware.h"

class TestApp
{
    Hardware* hardware = 0;
    uint8_t i = 0;

public:
    TestApp(Hardware* hardware) : hardware(hardware) {}

    void Update()
    {
        auto screen = hardware->GetScreen();
        screen->Clear();
        screen->SetLedHSV(i++ % screen->Count(), 0, 255, 255);
        screen->Show();
    }
};
  • My guess: `pixels = Adafruit_NeoPixel(width * height, pin, NEO_GRB + NEO_KHZ800);` It utilizes default copy assigment operator, but it has explicit destructor, so the copy gets the same pointer as that temporary instance, and then its space is released by destructor. You might want to use another pointer and new – KIIV Jun 18 '21 at 20:25
  • Another look - it's in the constructor, so use constructor for pixels variable. Now you have default constructor, copy assigment from temporary created instance (and it's doing shallow copy and releases the memory) – KIIV Jun 18 '21 at 20:56
  • Oh wow! Looks like your guess was correct. I updated the 'pixels' field to a pointer and used the new keyword to create an instance. It's working properly now. I'm still a bit unsure what was going wrong though. You're saying the pixels was having it's destructor called somehow? – Toms Jensen Jun 19 '21 at 01:31
  • I'm also still a little confused as to why moving the code caused the issue. In both scenarios Screen.h and Hardware.h remained the same. I'd have assumed that the problem would either show up or not show up the same in both. – Toms Jensen Jun 19 '21 at 01:38

1 Answers1

0

The issue is apparently in the Screen constructor:

Screen(uint8_t width, uint8_t height, uint8_t pin) :
    width(width), height(height), pin(pin)
{
    pixels = Adafruit_NeoPixel(width * height, pin, NEO_GRB + NEO_KHZ800);
}

Now it creates "empty" pixels using default constructor and in the code block it creates another instance (now properly initialized) and makes a copy into pixels. As the copy assigment operator is not provided, default one is used, and it makes a shallow copy of pixels storage. And after that second instance goes out of scope and deletes that allocated pointer.

Now you have dangling pointer and if you allocate anything, it'll be allocated in the same space as the dangling pointer is pointing to.

Difference between those two test cases is that you are doing another allocation in the second program:

Hardware* hardware = new Hardware();  // dangling pointer is created here
TestApp* app = new TestApp(hardware); // <<-- allocated in the same space as that dangling pointer

Anyway pixels class member should be initialized in constructor's initializer list (exactly same as the rest of the values):

Screen(uint8_t width, uint8_t height, uint8_t pin) :
    width(width), height(height), pin(pin), pixels(width * height, pin, NEO_GRB + NEO_KHZ800)
{
    // btw: do you really have to store a "pin" value?
}

And why are you using pointers so much? It could be just like this (or it could be passed as a reference into the app too):

#include <Arduino.h>
#include "Hardware.h"
#include "TestApp.h"

unsigned long timestamp;
Hardware hardware;
TestApp app { &hardware };

void setup()
{
  hardware.Setup();
}

void loop()
{
  app.Update();
  delay(100);
}

Btw: It's caused by Adafruit library, that doesn't follow "Rule of three"/"Rule of five" guideline. It shouldn't allow to make a copy of object at all (as a deep copy could be even worst on small devices)

KIIV
  • 3,534
  • 2
  • 18
  • 23