Quick Guide: Using printf() on Teensy ARM

kbob

Member
Here is an example sketch that uses printf() to write the serial port on an ARM-based Teensy. I did not see this documented anywhere.

Code:
int Serial_write(void *cookie, const char *buf, int size)
{
  for (int i = 0; i < size; i++)
    Serial.print(buf[i]);
  return size;
}

void setup()
{
  Serial.begin(9600);
  while (!Serial)
    continue;
  stdout = funopen(NULL, NULL, Serial_write, NULL, NULL);
  if (!stdout)
    Serial.println("funopen failed");
}

void loop() {
  
  printf("Hello World\n");
}

N.B., if I remove the error checking, "if (!stdout) Serial.println(...)", then the build fails with an unresolved reference to _write. (I'm using Teensyduino 1.22.)

This adds about 4,500 bytes to the sketch's code size and 124 byte of RAM vs the same sketch that uses Serial directly. But stdio allegedly also calls malloc(). I don't know how much additional memory is malloc'd.

If you need to print floating point values, see Float in sscanf on Teensy 3.1.
 
A macro hack for printf is as below, since T3's library implements such.

#define printf Serial.printf
or

#define printf Serial1.printf
 
I wanted this same behaviour and found that this works for me, to replace `stdout`:
Code:
  stdout = fwopen(&Serial, [](void *cookie, const char *buf, int count) {
    return reinterpret_cast<Print *>(cookie)->write(buf, count);
  });
  setbuf(stdout, nullptr);

The `setbuf` is needed or else you need to call `fflush(stdout)` because it doesn't look like newlines in `printf` actually flush the stream. But I don't know what's going on under the covers with `malloc` anyway, so that's another reason to set the stream to unbuffered.

I just pass the address of the `Print` object I need to `fwopen`, and then use that. The value could be a variable (here, it's "Serial").

With this code, `printf` works for me under Teensyduino v1.53.
 
I should add: This works for Teensy 4 compilation but not for earlier versions, as the original poster stated about the unresolved reference to `_write`.
 
I'll add another interesting detail: The code compiles and operates fine for Teensy 3 with the Teensyduino IDE but not with PlatformIO. With PlatformIO, it complains of the missing `_write`. Maybe PlatformIO is doing something different with the linker...

Here's my complete program:
Code:
#include <cstdio>

void setup() {
  Serial.begin(115200);
  while (!Serial && millis() < 4000) {
    // Wait for Serial initialization
  }
  Serial.println("Hello, World!");

  stdout = fwopen(&Serial, [](void *cookie, const char *buf, int count) {
    return reinterpret_cast<Print *>(cookie)->write(buf, count);
  });
  setbuf(stdout, nullptr);
  printf("Hi!\n");
}

void loop() {
  // Test that we haven't frozen
  Serial.println("Hello");
  delay(2000);
}

Update: In reality, my program is larger and the simple program above doesn't complain of a missing `_write` under PlatformIO; my larger program does. I'll update if I find out what's actually causing the "In function `_write_r': undefined reference to `_write'" problem...

Update 2: So I found the problem. To not get that error, the `Serial.println("Hello, World!")` call needs to be there (try commenting that line out and see what happens). (It can also come after the `println`, it just has to be there.) If this is there then there is no "missing _write" problem. I don't know why this is. It also turns out that it needs to be a `println` and not a `print`. Having just a `print`causes the error too.

Update 3: Using `Serial.write` doesn't work either. It has to be a `println`.
 
Last edited:
Ok, I think I understand what's going on, now. @luni that link helped, thanks.
[Update 2 text fix: The "println" should be "printf" in here: "(It can also come after the `println`"]

  • Fact: The `printf` family of functions need a `_write` function defined somewhere (this is how the current libraries do it, at least).
  • Fact: `_write` is defined in Print.cpp.
  • For Teensy 3: Unless you call a `Print` function, Print.cpp doesn't get linked in, and therefore, printf's under-the-covers `_write` doesn't get linked in.
  • For Teensy 4: Something is linking in Print.cpp (containing the `_write` implementation), even if no `Print` function is called. I don't know what that is.
I still don't get this: Why would `println` link in Print.cpp but not `print`? (See Updates 2 & 3 above.)

Here's my new, simpler, code:
Code:
#include <cstdio>

auto &debugOut = Serial;

extern "C" {
int _write(int fd, const char *buf, int len) {
  // Send both stdout and stderr to debugOut
  if (fd == stdout->_file || fd == stderr->_file) {
    return debugOut.write(reinterpret_cast<const uint8_t *>(buf), len);
  }

  // Doing the following keeps this compatible with Print.cpp's requirements
  Print *p = reinterpret_cast<Print *>(fd);
  return p->write(reinterpret_cast<const uint8_t *>(buf), len);
}
}

void setup() {
  debugOut.begin(115200);
  while (!debugOut && millis() < 4000) {
    // Wait for Serial initialization
  }

  // Optional:
  // setbuf(stdout, null);

  printf("Hi!\n");
}

void loop() {
  // Test that we haven't frozen
  Serial.println("Hello");
  delay(2000);
}
 
I've been playing with this some more and, for posterity, I'll post my latest "correct and it's really perfect now" (HA!) code. I'll post here if there's more improvements.

Here's my "stdio implementation" block:
Code:
// ---------------------------------------------------------------------------
//  stdio implementation
// ---------------------------------------------------------------------------

extern "C" {
// https://forum.pjrc.com/threads/28473-Quick-Guide-Using-printf()-on-Teensy-ARM
int _write(int file, const char *buf, int len) {
  // Send both stdout and stderr to debugOut
  if (file == stdout->_file || file == stderr->_file) {
    if (!stdserialReady) {
      return 0;
    }
    return stdserial.write(reinterpret_cast<const uint8_t *>(buf), len);
  }

  // Doing the following keeps this compatible with Print.cpp's requirements;
  // it also allows us to pass our own Print* as a file descriptor
  Print *p = reinterpret_cast<Print *>(file);
  if (p == nullptr) {
    errno = EBADF;
    return -1;
  }
  return p->write(reinterpret_cast<const uint8_t *>(buf), len);
}

// https://forum.pjrc.com/threads/27827-Float-in-sscanf-on-Teensy-3-1
// This link shows how to enable float scanning.
int _read(int file, char *buf, int len) {
  if (file == stdin->_file) {
    if (len <= 0 || !stdserialReady) {
      return 0;
    }
    int toRead = stdserial.available();
    if (toRead <= 0) {
      return 0;
    }
    if (toRead > len) {
      toRead = len;
    }
    return stdserial.readBytes(buf, toRead);
  }
  // No other possible input file dscriptors (for our program)
  // so return an error
  errno = EBADF;
  return -1;
}
}

One critical point for the above code is that `_read` returns `0` if nothing's available. This indicates the EOF condition and the error condition must be cleared when reading if you want stdin to continue properly. This is a non-blocking style and its use is shown in the `readLine()` example below.

A blocking approach to `_read` might look something like this:
Code:
int _read(int file, char *buf, int len) {
  if (file == stdin->_file) {
    if (len <= 0) {
      return 0;
    }
    int toRead;
    while ((toRead = stdserial.available()) <= 0) {
      yield();
    }
    if (toRead > len) {
      toRead = len;
    }
    return stdserial.readBytes(buf, toRead);
  }
  errno = EBADF;
  return -1;
}

Here's a function for reading a single line that uses the non-blocking approach. It's assumed that the line string is cleared before entering the state that loops and calls `readLine()`. i.e. clear the string and then loop; don't clear the string before each call to `readLine()`. It correctly handles the three line ending forms: LF, CR, and CRLF:
Code:
String line;
bool readLine() {
  static bool inCR = false;

  while (true) {
    int c;
    switch (c = fgetc(stdin)) {
      case '\r':
        inCR = true;
        break;

      case '\n':
        inCR = false;
        return true;

      default:
        if (c < 0) {
          clearerr(stdin);  // This is so we can continue using stdin
          if (inCR) {
            inCR = false;
            return true;
          }
          return false;
        }
        if (inCR) {
          inCR = false;
          ungetc(c, stdin);
          return true;
        }
        inCR = false;
        line.append(static_cast<char>(c));
    }
  }
}

This returns true if a line is found, even an empty one, and false if more checking is needed.
 
Is this all the best way of getting a traditional c-style "printf(" to work?

If so, when I copy-paste the code from #8 above, it fails to compile because: 'stdserialReady' was not declared in this scope

What is stdserialReady and how to get I get it defined?

Chip
 
It's a volatile `bool` that gets set to true in `setup()` after the serial port is initialized. i.e. after (the common) `while (!Serial && millis() < 4000)` code completes (others may use a different timeout or even no timeout at all). You may also have gathered that `stdserial` just gets set to the serial port you're using (I used a reference when assigning, for that code).

Here's my latest version:
Code:
Stream *stdStream = &Serial;
volatile bool stdStreamReady = false;

void setup() {
  // Serial initialization, for printing things
  Serial.begin(115200);
  while (!Serial && millis() < 4000) {
    // Wait for Serial initialization to complete or a time limit
  }
  stdStreamReady = true;

  // ...code...
}

void loop() {
  // ...stuff...
}

// ...function()s...

extern "C" {
// https://forum.pjrc.com/threads/28473-Quick-Guide-Using-printf()-on-Teensy-ARM
int _write(int file, const char *buf, size_t len) {
  if (!stdStreamReady) {
    return 0;
  }

  Print *out;

  // Send both stdout and stderr to stdserial
  if (file == stdout->_file || file == stderr->_file) {
    out = stdStream;
    if (out == nullptr) {
      return len;
    }
  } else {
    out = reinterpret_cast<Print *>(file);
    if (out == nullptr) {
      errno = EBADF;
      return -1;
    }
  }

  if (len == 0) {
    return 0;
  }
  return out->write(reinterpret_cast<const uint8_t *>(buf), len);
}

// https://forum.pjrc.com/threads/27827-Float-in-sscanf-on-Teensy-3-1
// This link shows how to enable float scanning.
int _read(int file, char *buf, size_t len) {
  if (!stdStreamReady) {
    return 0;
  }

  Stream *in;
  if (file == stdin->_file) {
    in = stdStream;
    if (in == nullptr) {
      return 0;
    }
  } else {
    in = reinterpret_cast<Stream *>(file);
    if (in == nullptr) {
      errno = EBADF;
      return -1;
    }
  }

  if (len == 0) {
    return 0;
  }
  int avail = in->available();
  if (avail <= 0) {
    return 0;
  }
  size_t toRead = avail;
  if (toRead > len) {
    toRead = len;
  }
  return in->readBytes(buf, toRead);
}
}  // extern "C"
 
Back
Top