Saturday, February 18, 2017

LiteSerialLogger: Zero SRAM Serial Logging

Last week, I talked about Arduino library memory use - and pointed out that the Serial library, in particular, is entirely wasteful for the purpose of simply writing log messages out to the serial port.

What's one to do about this?  Well, one can do many things, but what I chose to do is to write my own serial logging library - that uses zero SRAM except when actually writing messages to the serial port!

This isn't the first time I've written something along these lines.  Every few years, I seem to find myself writing yet another serial output library for some reason or other, and serial UARTs are pretty boring to bang bits into at this point.  They're slightly more exciting if they have a FIFO bolted on and a high speed clock, but not by much.

In any case - take a look at this!  A basic Arduino sketch uses 9 bytes of dynamic memory or SRAM (for the millisecond timer), and with an awful lot of serial logging, I also use a mere 9 bytes!


What's this library?  How did I write it?  And how can you use it?  Read on!


Quick Summary

I'm going to put the summary up at the top - in case you just want to use the library in your sketches.

  1. Green "Clone or Download" button: Download the zip file.  You should end up with LiteSerialLogger-master.zip in your downloads folder.
  2. Sketch -> Include Library -> Add .ZIP File.  Select the downloaded zip file.
That's it!  You should now have the LiteSerialLogger example file in File -> Examples -> LiteSerialLogger (down at the bottom).

Add the header to your source file:

#include <LiteSerialLogger.h>

Where you'd normally call Serial.begin(9600) or something along those lines - call LiteSerial.begin(9600) instead.  You can run at 115200 baud, if you wish (and I would recommend doing so, but I default to the lower baud rate in my example).

Replace your Serial.print() calls with LiteSerial.print().

Done!

Concept Overview

The Arduino programming environment is an embedded programming environment.  This means it doesn't have much in the way of resources, and you should be smart with them.  The Arduino Uno, the most commonly used device, has a whopping 2048 bytes of RAM and 32kb of program storage.  My watch has 512MB RAM and 4GB flash, though it is a bit short on GPIO pins.

What this means is that libraries need to be designed for this environment - they need to be minimal in resource use.  This sometimes means making time/memory tradeoffs (use more time to use less memory - calculating CRCs with an algorithm instead of using static tables would be an example here), and it sometimes means that you shouldn't be using a kitchen sink do-everything library to do some common subset of those tasks, when you can build a custom library that does what you need with significantly less resource use.

It's a safe bet that the vast majority of Arduino sketches written use the Serial library - and it's also a good bet that the vast majority of the times the Serial library is used, it is only being used to print log output for debugging or status reporting.

If you use the Serial library, it consumes 177 bytes of SRAM - permanently.  This includes input and output buffers, and the storage from the classes it inherits from (Stream and Print).  This is fine for tiny projects, but if you're doing something that requires a good bit of system memory, that's nearly 10% of the 2048 bytes the Arduino Uno has - just for printing log messages!


This annoyed me greatly, so I wrote something better to do logging over the serial port.  My library doesn't do everything - you cannot use it for any serial input, but for logging, it uses a whole lot less memory (zero bytes, unless you're printing something, then it might use another dozen or so, depending on the values used).  I also don't support serial configurations other than 8,N,1 (8 bits of data, no parity, 1 stop bit) - but that's the standard configuration for ASCII logging anyway.

UART Details

Hardware serial communication is handled by a UART - Universal Asynchronous Receiver and Transmitter.  In a nutshell, you shove a byte of data in, and it gets serialized out over a serial line.

If you really want to understand the details, the Atmega 328 datasheet will tell you everything you want to know, but for the purposes of writing serial output, things are pretty simple.

The Arduino UART is a single byte UART - you write a byte in, and then you have to wait for the byte to get sent out on the serial line (at 9600 baud, it takes right about 1ms to transmit a character, and at 115200 baud, about 0.085ms).

There's a status bit in register UCSR0A one can check to see if the transmit register is empty.  If it is, you can put a new byte in.  If not, you need to wait.

The code to handle this style of output looks something like this:

void LiteSerialLogger::put_char(const char c) {
  loop_until_bit_is_set(UCSR0A, UDRE0);
  UDR0 = c;
}

It's what I described above, in code form.  Wait until the UDRE0 (bit 5) of register USCR0A (the status register) is set, then write the data into register USR0.  It's worth noting that the order here is deliberate.  The first byte will be sent immediately because the status register is clear, and the function will return immediately after stuffing the last byte in the UART, before it's gone out.

Initialization

Initializing a UART can be a bit tricky - setting baud rates involves playing with base frequencies, dividers, etc.  Fortunately, there's a perfectly good example of doing this - the begin() function in HardwareSerial.cpp!

I use their baud initialization code, and then change the tail end of the function for my needs.   Instead of enabling transmit/receive and interrupts, I only enable transmit (outbound data from the Arduino) and disable interrupts entirely.

Since this function doesn't store anything in global SRAM (the results are stored in the various configuration registers of the UART), I'm set!

Converting to Strings

The code is pretty straightforward - you can take a look here: LiteSerialLogger.cpp.

One such function looks like this:

int LiteSerialLogger::print(const unsigned long &value, const byte base) {
  // Unsigned Long: 0 to 4294967295: 10 plus null 
  // Hex: ffffffff: 8 plus null.
  char buffer[11];
  ultoa(value, buffer, base);
#ifdef DEBUG_ASSERT
  if (strlen(buffer) >= sizeof(buffer)) {
      this->println(F("ERROR: String length exceeds buffer size!"));
  }
#endif
  return print(buffer);
}

I have a function for each data type supported by the Arduino IDE, and I allocate a character buffer of the maximum length possible for that data type, call the proper "type to ascii" function (itoa, ltoa, utoa, etc) to convert the number to a string representation in the desired base, and then print the buffer out.

The DEBUG_ASSERT code isn't normally compiled in, but lets me check (during development) that the buffer lengths are sufficient.  Overflowing your stack variables is bad form and leads to crashes or data corruption, so I just test that.  Note the ">=" compareison - strlen() doesn't return the terminating null byte, so if you shove 6 characters of string into a 6 byte buffer, you've actually gone over by one.

The "println" functions are pretty much just a wrapper that calls the matching print function, then prints a CRLF ("\r\n") afterwards.

int LiteSerialLogger::println(const unsigned long &value, const byte base) {
  int bytes_written = print(value, base);
  bytes_written += print(reinterpret_cast<const __FlashStringHelper*>(crlf));
  return bytes_written;
}

FlashStringHelper is an empty class that is used to indicate to code that a string is stored in program memory, instead of in data memory - don't worry about it, just use your F() macro and all will work!

Storing Strings in Program Memory

If you're going to the lengths of using this library to save your SRAM, it makes no sense to store strings in SRAM.  This is a proper Harvard architecture, with totally separate program and data memories.  You have more 16x program memory than data memory (on the Uno), so put your static strings there!

There are two ways to store the strings in program memory.

The first, and most common, is the F() macro - used like this:

LiteSerial.println(F("Hi!"));

The second, slightly more complex method, is useful if you have a string that needs to be repeated multiple times.  Store it in program memory with the PROGMEM directive - and then cast it to a __FlashStringHelper * before printing it so the correct function is called.

Declare your string up in the globals section like this:
const char HelloWorld[] PROGMEM = "Hello, World!";

Then, when you're using it, you need to manually cast it to a __FlashStringHelper * so the matching function is called.  Either of these ways works - and I'm not going to say you should use one or the other.  One is old C style, one is a more modern C++ style.  Both work identically.

LiteSerial.println((__FlashStringHelper *)HelloWorld);
LiteSerial.println(reinterpret_cast<const __FlashStringHelper*>(HelloWorld));

If you do it right, you shouldn't see any extra global memory used!  If you do it wrong, your print functions will output garbage.  Or your code might crash.  So make sure you do it right.


That pretty much covers use - it's a small, simple library that is designed to be as compatible as possible with the logging functions of the Serial library.

Final Thoughts

If you're going to write Arduino libraries - don't be stupid about memory.  You're not the only library on the system, and you should go out of your way to use less resources.

This is, in my (admittedly biased) opinion, a pretty solid library for logging.  I can't think of a way to simplify it much further.  If you can do it better, though, let me know!  I'd love to shrink this down even more.

If you run into any problems or corner cases where things don't work right, please, let me know (or file a bug on Github).  I want this to be useful, and as much as possible, a drop-in replacement for the Serial library, for the purposes of logging.

No comments:

Post a Comment