Forum Rule: Always post complete source code & details to reproduce any issue!
Results 1 to 11 of 11

Thread: Quick Guide: Using printf() on Teensy ARM

  1. #1
    Junior Member
    Join Date
    Apr 2015
    Posts
    12

    Quick Guide: Using printf() on Teensy ARM

    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.

  2. #2
    Senior Member
    Join Date
    Jun 2013
    Location
    So. Calif
    Posts
    2,825
    A macro hack for printf is as below, since T3's library implements such.

    #define printf Serial.printf
    or

    #define printf Serial1.printf

  3. #3
    Senior Member
    Join Date
    Mar 2017
    Location
    Oakland, CA, USA
    Posts
    795
    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.

  4. #4
    Senior Member
    Join Date
    Mar 2017
    Location
    Oakland, CA, USA
    Posts
    795
    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`.

  5. #5
    Senior Member
    Join Date
    Mar 2017
    Location
    Oakland, CA, USA
    Posts
    795
    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 by shawn; 01-09-2021 at 05:25 AM.

  6. #6
    Senior Member
    Join Date
    Apr 2014
    Location
    Germany
    Posts
    2,106
    Looks like you stumbled over the same issue as already found in another context here: https://forum.pjrc.com/threads/65469...blems?p=264465

    Seems to be some weird linker issue. See also #8 in the linked thread for an idea how to fix it.

  7. #7
    Senior Member
    Join Date
    Mar 2017
    Location
    Oakland, CA, USA
    Posts
    795
    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);
    }

  8. #8
    Senior Member
    Join Date
    Mar 2017
    Location
    Oakland, CA, USA
    Posts
    795
    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.

  9. #9
    Senior Member
    Join Date
    Oct 2015
    Location
    Vermont, USA
    Posts
    322
    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

  10. #10
    Senior Member
    Join Date
    Mar 2017
    Location
    Oakland, CA, USA
    Posts
    795
    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"

  11. #11
    Senior Member
    Join Date
    Oct 2015
    Location
    Vermont, USA
    Posts
    322
    I ended up using the code in post #7 and it worked great on my T4.1.

    Thanks for your work!

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •