0

I recently gave the students in my Arduino-based programming class a project to write some simple unit tests and fix the bugs that they encountered as they did. The class in question mostly stores a bunch of Arduino-style String objects, with various getters and setters and a couple of constructors.

The test code that they're supposed to write involved creating two instances of the class (on the stack) and verifying that the constructors set the values correctly. When I went through the exercise myself, everything worked.

When the students repeated the exercise, the first object worked. The second object returned empty strings. If they commented out the first object, the second object began working. I couldn't find any obvious mistakes that could explain these failures.

After staring at their code for about ten minutes to make sure I wasn't losing it, I compared Arduino IDE versions to make sure my machine was running the same version. It was.

Next, I unplugged one of their Arduino Uno devices and plugged in my Mega 2560. The exact same source code passed the unit tests as expected. That means the test failures were caused by some subtle difference between the Arduino Uno and the Mega 2560—either in the hardware or in the compiler.

Before I file a bug against the Arduino Uno compiler (v1.6.7, though I don't see anything in the 1.6.8/1.6.9 release notes that imply any compiler changes since then), I wanted to make sure I wasn't missing something subtle. Could somebody take a look at this and see if I missed anything?

I've included a condensed version of the class in question below (with irrelevant bits removed). You can see the complete set of exercises at:

https://sites.google.com/a/cabrillo.edu/cs-11m/schedule/unit-testing-exercises

The tests failed unexpectedly after they added the second AddressBookEntry object.

Class Declaration (partial)

class AddressBookEntry {

  public:
    AddressBookEntry(void);
    AddressBookEntry(String title, String firstName,
                     String middleName, String lastName,
                     String suffix);

    String title(void);
    String firstName(void);

...

  private:
    String _title;      // Mr.
    String _firstName;  // Barack
    String _middleName; // Hussein
    String _lastName;   // Obama
    String _suffix;     // Sr.

    String _addressLine1; // The White House
    String _addressLine2; // 1600 Pennsylvania Ave.
    String _city;         // Washington
    String _state;        // DC
    String _zip;          // 20500
    String _plusFour;     // 0001   (from 20500-0001)

    String _homePhone;    // Unknown
    String _mobilePhone;  // Unknown
    String _workPhone;    // 202-456-1414

Implementation (partial)

#define assert(condition) { \
  if (!(condition)) { \
    Serial.print(__FILE__); \
    Serial.print(":"); \
    Serial.print(__LINE__); \
    Serial.println(": Assertion failed: " #condition); \
    while (1); \
  } \
}

bool AddressBookEntry::runTests(void)
{
  Serial.println("Running tests.\n"); Serial.flush();

  AddressBookEntry testEntry("Mr.", "Jim", "Bob", "Murphy", "Sr.");

  Serial.println(testEntry.title());
  Serial.flush();

  assert(testEntry.title() == "Mr.");
  assert(testEntry.firstName() == "Jim");
  assert(testEntry.middleName() == "Bob");
  assert(testEntry.lastName() == "Murphy");
  assert(testEntry.suffix() == "Sr.");
  testEntry.setMiddleName("Phil");
  assert(testEntry.middleName() == "Phil");

  AddressBookEntry obamaEntry("Mr.", "Barack", "Hussein", "Obama", "");


  assert(obamaEntry.title() == "Mr."); // fails
  assert(obamaEntry.firstName() == "Barack"); // fails
  assert(obamaEntry.middleName() == "Hussein"); // stopped checking here.
  assert(obamaEntry.lastName() == "Obama");
  assert(obamaEntry.suffix() == "");

  Serial.println("All tests passed.\n"); Serial.flush();
}

AddressBookEntry::AddressBookEntry()
{
  _title = "";
  _firstName = "";
  _middleName = "";
  _lastName = "";
  _suffix = "";

  _addressLine1 = "";
  _addressLine2 = "";
  _city = "";
  _state = "";
  _zip = -1; // Deliberate bug
  _plusFour = -1; // Deliberate bug

  _homePhone = "";
  _mobilePhone = "";
  _workPhone = "";
}

AddressBookEntry::AddressBookEntry(String title, String firstName,
                                   String middleName, String lastName,
                                   String suffix)
{
  _title = title;
  _firstName = firstName;
  _middleName = middleName;
  _lastName = lastName;
  _suffix = suffix;

  _addressLine1 = "";
  _addressLine2 = "";
  _city = "";
  _state = "";
  _zip = -1; // Deliberate bug
  _plusFour = -1; // Deliberate bug

  _homePhone = "";
  _mobilePhone = "";
  _workPhone = "";

String str = "";
  Serial.println(str + "title is \"" + title + "\"");
}

String AddressBookEntry::title()
{
  return _title;
}

String AddressBookEntry::firstName()
{
  return _firstName;
}

... // remaining getters are similar

Calling Code (from setup() function)

  Serial.begin(9600);
  AddressBookEntry myEntry;
  myEntry.runTests();
  • 4
    That's a lot of strings. Sounds like a lack of RAM to me. – Majenko May 14 '16 at 23:19
  • I agree with @Majenko, the most likely culprit is RAM which is why it's compiling on the Mega, since it has much more RAM than the Uno. – dinotom May 14 '16 at 23:31
  • 1
    This is really not an appropriate application for a tiny embedded system with only 2048 bytes of RAM and an inability to make ordinary access to constants stored in flash. – Chris Stratton May 14 '16 at 23:54
2

That means the test failures were caused by some subtle difference between the Arduino Uno and the Mega 2560—either in the hardware or in the compiler.

Not a subtle difference - a big difference. The Mega2560 has considerably more RAM than the UNO (8kB compared to 2kB).

All those strings are using RAM. Lots of RAM. Your Mega2560 has enough RAM to cope (just), but the UNO just dumps its stack in a heap all over the floor1 at the mere thought of trying to handle that quantity of string data.


1: That's a technical way of describing the stack and the heap meeting and, if there were no protection, colliding into each other.

  • That's certainly possible, but I would expect to get some sort of error if the String class tried to allocate memory and failed, rather than a silent misbehavior like this. – dgatwood May 15 '16 at 2:42
  • 1
    @dgatwood - how would you expect a tiny embedded system to communicate a runtime error? Without an MMU, even detecting one requires extra code, nevermind that there's no established way to report the situation as it is not a safe assumption that "Serial" is "stderr". This exercise is one you should probably have your students build to run on the development machines - save the embedded target for tasks that fit it, and contexts where you are prepared to keep the limitations constantly in mind. – Chris Stratton May 15 '16 at 3:01
  • You mean at the String class level, I assume, by detecting a NULL pointer exception. I would have expected the code to die much sooner than that, the moment the heap allocator failed to find space. That's usually considered a fatal error, and if the allocator is already detecting the error, it would make more sense to spew an error on the serial port and spin forever than to silently fail in unpredictable ways. But maybe that's just because I'm used to kernel code, where if an allocator fails and is allowed to return NULL, somebody's disk is going to get wiped.... – dgatwood May 15 '16 at 3:20
  • Okay, the more I study this, the more baffled I am by what I'm seeing. if I add the following line to a function: String x = "Mr.";, the global variable space remains constant, as expected, but if I add this line: assert(obamaEntry.title() == "Mr."); in the same function, it adds 48 bytes to the global variable size, despite not adding any global variables. Something is not right here. At best, that should have constructed some temporary String objects on the stack, then immediately discarded them, but global storage shouldn't have changed at all. Yet it did. A lot. – dgatwood May 15 '16 at 4:28
  • 1
    You should learn the difference between Harvard and Princeton architectures and why things like the F() macro are needed. – Majenko May 15 '16 at 8:21

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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