3

I'm writing a serial interface for an MCU, and I want to know how one would create a printf-like function to write to the serial UART. I can write to the UART, but to save memory and stack space, and avoid temp string buffers, I would prefer to do that write directly instead of doing sprintf() to a string and then writing the string via serial. There is no kernel and no file handling, so FILE* writes like those from fprintf() won't work (but sprintf() does).

Is there something that processes formatted strings for each char, so I can print char-by-char as it parses the format string, and applies the related arguments?

We are using newlib as part of the efm32-base project.

UPDATE

I would like to note that ultimately we implemented the _write() function because thats all newlib needs to light up printf.

KJ7LNW
  • 1,437
  • 5
  • 11
  • When you say "write to the UART ... directly" how do you mean? What code are you hoping to use? – Galik Apr 04 '21 at 20:56
  • BTW, temporary string buffers on the stack are basically free because they can be cleaned up immediately after use and have zero creation overhead. – Galik Apr 04 '21 at 20:59
  • 1
    Do you have `fopencookie` on your system? – Craig Estey Apr 04 '21 at 21:07
  • I don't understand well your question, but if you are using C++ I think the best way to do it is using `iostream`. Something like: `uart << my_string << " is equal to " << number;`. I use it with `atmega328` and I like it a lot. You can check an example [here](https://github.com/amanuellperez/mcu/blob/master/src/avr/test/USART/iostream/main.cpp) and [here is the implementation](https://github.com/amanuellperez/mcu/blob/master/src/avr/avr_UART_iostream.h). – Antonio Apr 04 '21 at 21:20
  • You might like to hack away at this: https://rsync.samba.org/doxygen/head/snprintf_8c-source.html – Paul Sanders Apr 04 '21 at 23:42
  • This is clearly embedded, and the OP is trying to keep stack sizes down. OP - you using a tasking/thread lib/RTOS? – Martin James Apr 05 '21 at 00:55

3 Answers3

2

Standard C printf family of functions don't have a "print to a character callback" type of functionality. Most embedded platforms don't support fprintf either.

First try digging around the C runtime for your platform, it might have a built-in solution. For example, ESP-IDF has ets_install_putc1() which essentially installs a callback for printf (though its ets_printf already prints to UART0).

Failing that, there are alternative printf implementations designed specifically for embedded applications which you can adapt to your needs.

For example mpaland/printf has a function taking the character printer callback as the first argument:

int fctprintf(void (*out)(char character, void* arg), void* arg, const char* format, ...);

Also see this related question: Minimal implementation of sprintf or printf.

rustyx
  • 80,671
  • 25
  • 200
  • 267
1

You had said [in your top comments] that you had GNU, so fopencookie for the hooks [I've used it before with success].

Attaching to stdout may be tricky, but doable.

Note that we have: FILE *stdout; (i.e. it's [just] a pointer). So, simply setting it to the [newly] opened stream should work.

So, I think you can do, either (1):

FILE *saved_stdout = stdout;

Or (2):

fclose(stdout);

Then, (3):

FILE *fc = fopencookie(...);
setlinebuf(fc);  // and whatever else ...
stdout = fc;

You can [probably] adjust the order to suit (e.g. doing fclose first, etc.)

I had looked for something analogous to freopen or fdopen to fit your situation, but I didn't find anything, so doing stdout = ...; may be the option.

This works fine if you do not have any code that tries to write to fd 1 directly (e.g. write(1,"hello\n",6);).

Even in that case, there is probably a way.


UPDATE:

Do you know if FILE*stdout is a const? If so, I might need to do something crazy like FILE **p = &stdout and then *p = fopencookie(...)

You were right to be concerned, but not for quite the reason you think. Read on ...


stdout is writable but ...

Before I posted, I checked stdio.h, and it has:

extern FILE *stdout;        /* Standard output stream.  */

If you think about it, stdout must be writable.

Otherwise, we could never do:

fprintf(stdout,"hello world\n");
fflush(stdout);

Also, if we did a fork, then [in the child] if we wanted to set up stdout to go to a logfile, we'd need to be able to do:

freopen("child_logfile","w",stdout);

So, no worries ...


Trust but verify ...

Did I say "no worries"? I may have been premature ;-)

There is an issue.

Here is a sample test program:

#define _GNU_SOURCE
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

#if 1 || DEBUG
#define dbgprt(_fmt...) \
    do { \
        fprintf(stderr,_fmt); \
        fflush(stderr); \
    } while (0)
#else
#define dbgprt(_fmt...) \
    do { } while (0)
#endif

typedef struct {
    int ioport;
} uartio_t;

char *arg = "argument";

ssize_t
my_write(void *cookie,const char *buf,size_t len)
{
    uartio_t *uart = cookie;
    ssize_t err;

    dbgprt("my_write: ENTER ioport=%d buf=%p len=%zu\n",
        uart->ioport,buf,len);

    err = write(uart->ioport,buf,len);

    dbgprt("my_write: EXIT err=%zd\n",err);

    return err;
}

int
my_close(void *cookie)
{
    uartio_t *uart = cookie;

    dbgprt("my_close: ioport=%d\n",uart->ioport);
    int err = close(uart->ioport);
    uart->ioport = -1;

    return err;
}

int
main(void)
{

    cookie_io_functions_t cookie = {
        .write = my_write,
        .close = my_close
    };
    uartio_t uart;

    printf("hello\n");
    fflush(stdout);

    uart.ioport = open("uart",O_WRONLY | O_TRUNC | O_CREAT,0644);
    FILE *fc = fopencookie(&uart,"w",cookie);

    FILE *saved_stdout = stdout;
    stdout = fc;

    printf("uart simple printf\n");
    fprintf(stdout,"uart fprintf\n");
    printf("uart printf with %s\n",arg);

    fclose(fc);
    stdout = saved_stdout;

    printf("world\n");

    return 0;
}

Program output:

After compiling, running with:

./uart >out 2>err

This should produce an expected result. But, we get (from head -100 out err uart):

==> out <==
hello
uart simple printf
world

==> err <==
my_write: ENTER ioport=3 buf=0xa90390 len=39
my_write: EXIT err=39
my_close: ioport=3

==> uart <==
uart fprintf
uart printf with argument

Whoa! What happened? The out file should just be:

hello
world

And, the uart file should have three lines instead of two:

uart printf
uart simple printf
uart printf with argument

But, the uart simple printf line went to out instead of [the intended] uart file.

Again, whoa!, what happened?!?!


Explanation:

The program was compiled with gcc. Recompiling with clang produces the desired results!

It turns out that gcc was trying to be too helpful. When compiling, it converted:

printf("uart simple printf\n");

Into:

puts("uart simple printf");

We see that if we disassemble the executable [or compile with -S and look at the .s file].

The puts function [apparently] bypasses stdout and uses glibc's internal version: _IO_stdout.

It appears that glibc's puts is a weak alias to _IO_puts and that uses _IO_stdout.

The _IO_* symbols are not directly accessible. They're what glibc calls "hidden" symbols--available only to glibc.so itself.


The real fix:

I discovered this after considerable hacking around. Those attempts/fixes are in an appendix below.

It turns out that glibc defines (e.g.) stdout as:

FILE *stdout = (FILE *) &_IO_2_1_stdout_;

Internally, glibc uses that internal name. So, if we change what stdout points to, it breaks that association.

In actual fact, only _IO_stdout is hidden. The versioned symbol is global but we have to know the name either from readelf output or by using some __GLIBC_* macros (i.e. a bit messy).

So, we need to modify the save/restore code to not change the value in stdout but memcpy to/from what stdout points to.

So, in a way, you were correct. It is [effectively] const [readonly].

So, for the above sample/test program, when we want to set a new stdout, we want:

FILE *fc = fopencookie(...);
FILE saved_stdout = *stdout;
*stdout = *fc;

When we want to restore the original:

*fc = *stdout;
fclose(fc);
*stdout = saved_stdout;

So, it really wasn't gcc that was the issue. The original save/restore we developed was incorrect. But, it was latent. Only when gcc called puts did the bug manifest itself.

Personal note: Aha! Now that I got this code working, it seems oddly familiar. I'm having a deja vu experience. I'm pretty sure that I've had to do the same in the past. But, it was so long ago, that I had completely forgotten about it.


Workarounds / fixes that semi-worked but are more complex:

Note: As mentioned, these workarounds are only to show what I tried before finding the simple fix above.

One workaround is to disable gcc's conversion from printf to puts.

The simplest way may be to [as mentioned] compile with clang. But, some web pages say that clang does the same thing as gcc. It does not do the puts optimization on my version of clang [for x86_64]: 7.0.1 -- YMMV

For gcc ...

A simple way is to compile with -fno-builtins. This fixes the printf->puts issue but disables [desirable] optimizations for memcpy, etc. It's also undocumented [AFAICT]

Another way is to force our own version of puts that calls fputs/fputc. We'd put that in (e.g.) puts.c and build and link against it:

#include <stdio.h>

int
puts(const char *str)
{
    fputs(str,stdout);
    fputc('\n',stdout);
}

When we just did: stdout = fc; we were deceiving glibc a bit [actually, glibc was deceiving us a bit] and that has now come back to haunt us.

The "clean" way would be to do freopen. But, AFAICT, there is no analogous function that works on a cookie stream. There may be one, but I haven't found it.

So, one of the "dirty" methods may be the only way. I think using the "custom" puts function method above would be the best bet.

Edit: It was after I reread the above "deceiving" sentence that I hit on the simple solution (i.e. It made me dig deeper into glibc source).

Craig Estey
  • 30,627
  • 4
  • 24
  • 48
  • Can you really refine puts() without symbol collisions? I mean maybe static, but if that works without static then I'm curious why, and does it work with other libc functions too? – KJ7LNW Apr 12 '21 at 23:27
  • Also, FYI, we use -fno-builtins in our EFR32 builds IIRC. – KJ7LNW Apr 12 '21 at 23:32
0

depending on your standard library implementation you need to write your own versions of fputc or _write functions.

0___________
  • 60,014
  • 4
  • 34
  • 74
  • I'm upping this, whoever DV'ed was wrong! This was ultimately the correct answer for our implementation and I updated the OP to reflect that. However, I realize this didn't directly address the question which was about re-implementing printf. So, I'm split on where the answer should be, but this answer deserves props. So lots of other great answers that are directly addressing the subject, but this was the fix we used. – KJ7LNW Feb 10 '22 at 22:22
  • @KJ7LNW you do not reimplement printf if you write your putc or _write as they are called by the original printf function (which one is needed you need to see in your implementation). So you will be able to use standard `printf` – 0___________ Feb 10 '22 at 22:38
  • Correct, we didn't re-implement printf(), we just implemented _write(). – KJ7LNW Feb 11 '22 at 21:34