-
Notifications
You must be signed in to change notification settings - Fork 5
How To stdio (printf scanf stdout)
Written by @ssilverman in this forum thread.
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.
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.
Print *stdPrint = &Serial;
Next, define _write()
somewhere in your project:
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:
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.
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.
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.
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:
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);
}
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.
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.
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;
}
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.
Since the Arduino ecosystem is based on C++, include at the top of those files that need to use the stdio functions.
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():
asm(".global _printf_float");
To enable floats for the scanf family of functions, add this:
asm(".global _scanf_float");
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 was imported), the debug printing worked.
Teensy is a PJRC trademark. Notes here are for reference and will typically refer to the ARM variants unless noted.