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.
I would like to note that ultimately we implemented the _write() function because thats all newlib needs to light up printf.
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.
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).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With