Friday, June 14, 2013

Arduino serial I/O and the Big Bug

As the main program came together, there were a number of conditions that could arise that would cause the program to stop.  For example, you wouldn't want to fill the balloon unless the dumper was up, and you wouldn't want to move the clip into position unless the proboscis was raised.

Chris and I talked about main Arduino loop() behavior, too.  He had decided it was intentional that we not allow parallel functions to be occurring.  I feel like I'm doing a poor job explaining, so let's lay out a few models.

Model 1: suppose the main loop() were being called rapidly.  Each time we'd go through the loop, the purpose would be to check status on everything, and act accordingly.  In this way, you might have one balloon moving into position for filling, while the last one was just out of the dumper and on its way down the chute to the launcher.

Model 2: instead of expecting the main loop() to be called rapidly, we'd take full control of the CPU.  So, in one call to loop(), we'd check state of the proboscis, raise it, move the clip, lower the proboscis, fill the balloon, etc., until someone hit the big red button and launched the balloon, and the launch arm had returned to position.  Only after all that had happened would we return from loop() and release control of the CPU to the caller.

The path Chris chose was model 2.  We'd hold onto control.  What that demanded was that we have some mechanism for getting everything started.  To do that, I looked into the Serial.begin / Serial.available / Serial.read functions, and added an "input" tab to the code.  Along with it, there were several state variables added to globals.h.  The main input function was to check to see if any new data had been sent in via the Arduino Serial Monitor.  If the "go" command was seen, it would set the "running" flag to true, and return.  In this way, the loop() could check for input and return control if the running state remained in a "stopped" state.  More generally, this allowed the input function to process a whole bunch of commands for setting and querying state variables before the "go" was seen.  Eventually, this would allow us to manage important aspects of the WBMD like how long to run the spinner, how long to delay between spins, and how many times to spin.  (By the way, we'd given up on the ultrasonic sensor approach, so a timed spin was in the design by this point.)

The input code had two flavors: <name>=<value> and <name>.   The former allowed for setting state variables or running commands a number of times, and the latter allowed for simple, one-word command processing.

The combination of having a set of commands and error conditions gave rise to having a large number of hardcoded strings in the system, the lion's share for emitting user-friendly and error messages.

I didn't know it at the time, but what this led to was the Big Bug.  As the program became more complex, the number of error messages, and more importantly the byte size of the error messages, became larger and larger.  Eventually, I'd compile the program and run it, and it would start skipping recognition of error messages, or reboot the system completely.  It was somewhat unpredictable, in the sense that on any given compilation, it might show up.  On the other hand, it was annoyingly repeatable.  Once the program was in a messed up state, it stayed that way.

I did find that the common solution was to reduce the message string sizes, and the problem would go away.  But being a fan of providing explicit error messages with helpful suggestions on next steps, I didn't want to trim the messages down too much.

This got me thinking about whether I really wanted all the computational horsepower programmed on the Arduino, or if we'd be better off having the main code written in Java, and having a pre-defined protocol between the computer and the Arduino sketch.  But more on that later when we talk about the Frankenplotter.

It took a while, but eventually I stumbled upon http://arduino.cc/en/Tutorial/Memory.  It turns out the Big Bug was caused by my long messages and other variables eating up what little SRAM is available!  The page thankfully provided ways to use the PROGMEM qualifier to force storage in flash (program) memory rather than SRAM.  That meant pulling all messages out to a msg tab (proper programming technique should have required that in the first place, though normally it'd be done for proper internationalization, not this memory selection purpose) and providing a function for retrieving messages by ID number.  After that big chore was complete, I haven't seen a Big Bug recurrence again.

No comments:

Post a Comment