How to: stdio (printf, scanf, stdout, stderr, stdin, fgetc, etc.)

shawn

Well-known member
I thought I'd share my experience with adding support for the stdio functions and standard streams.

The Teensy uses a standard library called "Newlib" (or "Newlib-nano", depending on how your program is built). Under the covers, this calls certain functions that perform the raw I/O operations. A simple view:
  • _write(args) for output, and
  • _read(args) for input.

The Teensy core defines these as weak functions, so you can override them by defining them yourself.

Key point: If we can define how the standard streams are wired up, and even how Print or Stream instances get wired up, then we can use any of the stdio functions, including printf, scanf, ferror, etc.


Output

Since the lowest level of output in Arduino-land is the Print class, we use a pointer to an instance of one of these to define where output goes. Let's call it stdPrint and assign it to point to Serial. We could actually let it point to anything that implements Print: serial ports, network streams, files, etc.

Code:
Print *stdPrint = &Serial;

Next, define _write() somewhere in your project:

Code:
int _write(int file, const void *buf, size_t len) {
  Print *out;

  // Send both stdout and stderr to stdPrint
  if (file == stdout->_file || file == stderr->_file) {
    out = stdPrint;
  } else {
    out = (Print *)file;
  }

  if (out == nullptr) {
    return len;
  }

  // Don't check for len == 0 for returning early, in case there's side effects
  return out->write((const uint8_t *)buf, len);
}

You'll notice a few things in the code:
  1. We use knowledge of Newlib's FILE structure to compare the passed-in file descriptor to the descriptors for the standard output streams. We've chosen here to send both stdout and stderr to stdPrint.
  2. The second half of that if statement casts the file descriptor to a pointer to Print because that's how Teensy's core implements Print::printf(). The pointer to the current instance of Print is cast to an int and then passed to Newlib's output functions. This goes for anything that extends from Print, including serial ports, network clients, etc.
  3. If the output goes nowhere, i.e. stdPrint is nullptr or the file descriptor is zero, then we consider as if all the output has been written by returning the length. Otherwise, we trust those variables and call the instance's write() function.

For errors or invalid state (for example, an invalid file descriptor), you can set errno to the desired error code and return -1.

Now printf, its variations, and other Print::printf implementations will work.


Input

The lowest level of input in Arduino-land is the Stream class. It derives from Print and adds input functions. We use a pointer to an instance of one of these to define where input comes from. Let's call it stdStream and assign it to point to Serial. Similar to Print, we can let it point to anything that implements Stream: serial ports, network streams, files, etc.

Note that we could use this same variable for output, because a Stream is also a Print, merging it with stdPrint.

Taking a similar approach as for output, let's define a _read() function:

Code:
Stream *stdStream = &Serial;

int _read(int file, void *buf, size_t len) {
  Stream *in;

  if (file == stdin->_file) {
    in = stdStream;
  } else {
    in = (Stream *)file;
  }

  if (in == nullptr) {
    return 0;
  }

  if (len == 0) {
    return 0;
  }

  // Non-blocking approach:
  int avail = in->available();
  if (avail <= 0) {
    return 0;
  }
  size_t toRead = avail;
  if (toRead > len) {
    toRead = len;
  }
  return in->readBytes((char *)buf, toRead);
}

Notes:
  • For errors, similar to the output implementation, we could set errno and return -1.
  • This version of the function takes a non-blocking approach and returns zero if there's no input available. Since a zero return value means EOF, the EOF condition will be set on the given file descriptor, meaning further reads from the associated file will require a call to clearerr(input_file).

For the non-blocking approach, you will need to manage your own regular or line-based input, calling clearerr(input_file) upon EOF detection. For example, on EOF, fgetc(stdin) will always return EOF (a constant negative int) unless the EOF condition is cleared with clearerr(stdin).

It is left as an exercise for the reader to implement line-based input in the presence of the different kinds of line endings.

Blocking version

It is common to want blocking behaviour for line-based input. However, different systems use different line endings. There's three common ones: CR, LF, and CRLF. The input function must manage these characters and keep reading until the requested character count has been reached or, optionally, a complete line is read. The following code shows this strategy.

Code:
int _read(int file, void *buf, size_t len) {
  Stream *in;

  if (file == stdin->_file) {
    in = stdStream;
  } else {
    in = (Stream *)file;
  }

  if (in == nullptr) {
    return 0;
  }

  static bool hasCR = false;

  char *b = (char *)buf;

  size_t count = 0;
  while (count < len) {
    // Note that readBytes is actually a timed read; it waits for input
    // See the Stream class for changing the timeout and
    // change as necessary on the object that stdStream points to
    char c;
    if (in->readBytes(&c, 1) == 0) {
      return count;
    }

    switch (c) {
      case '\r':
        hasCR = true;
        *(b++) = '\n';
        count++;
        return count;
      case '\n':
        if (!hasCR) {
          *(b++) = '\n';
          count++;
          return count;
        }
        // Skip this NL if it was preceded by a CR
        hasCR = false;
        break;
      default:
        hasCR = false;
        *(b++) = c;
        count++;
    }
  }

  return count;
}

Notes:
  • If a line ending is detected, no matter which kind, it will be replaced with a '\n' character and returned with the line. This allows the calling code to know if a complete line has been read and not itself have to worry about the different line endings; it will always end with a '\n'.

Now scanf and its variations will work.


Other notes

Include file

Since the Arduino ecosystem is based on C++, include <cstdio> at the top of those files that need to use the stdio functions.

Floating point in Newlib-nano

Some boards by default (or you've compiled it in by choice) use an alternative, smaller, library called Newlib-nano. For example, Teensy LC uses this by default. Floating point is not enabled in this version of the library. To enable floating point for the printf family of functions, add this to setup():
Code:
asm(".global _printf_float");

To enable floats for the scanf family of functions, add this:
Code:
asm(".global _scanf_float");

The printf gotcha

You may encounter calls to printf that just don't seem to work; they don't show output, say on the serial port. I encountered this when debugging the QNEthernet client code. The key point is to recognize which printf is being called. Because I was calling it from within a class that implemented its own printf (in my case because the class ultimately derived from Print), the call needed to be qualified with a namespace. Once the call was changed to std::printf() (and <cstdio> was imported), the debug printing worked.


References

 
Back
Top