Author Topic: Low-footprint printf replacement for embedded dev  (Read 5703 times)

0 Members and 1 Guest are viewing this topic.

Online SiliconWizardTopic starter

  • Super Contributor
  • ***
  • Posts: 15927
  • Country: fr
Low-footprint printf replacement for embedded dev
« on: December 09, 2024, 03:57:01 am »
For those not already knowing it and who are looking for a "printf" implementation that is relatively low-footprint and doesn't make any dynamic allocation, short of writing your own, I can recommend this one:

https://github.com/charlesnicholson/nanoprintf
 
The following users thanked this post: kgavionics, peter-h, newbrain, Dazed_N_Confused

Online newbrain

  • Super Contributor
  • ***
  • Posts: 1812
  • Country: se
Re: Low-footprint printf replacement for embedded dev
« Reply #1 on: December 09, 2024, 09:10:21 am »
Interesting, thanks!
I mostly use this: https://github.com/mpaland/printf
Works quite well, and can be tailored to reduce footprint vs. functionality (as can nanoprintf, AFAICS), but it's not been updated in a while.

I might give nanoprintf a go.

Not a great fan of Unlicense, but the author smartly dual-licenses it under 0BSD.
Nandemo wa shiranai wa yo, shitteru koto dake.
 

Online SiliconWizardTopic starter

  • Super Contributor
  • ***
  • Posts: 15927
  • Country: fr
Re: Low-footprint printf replacement for embedded dev
« Reply #2 on: December 09, 2024, 09:34:52 am »
Will be interesting to compare the footprint for equivalent functionality between the two.
 

Online newbrain

  • Super Contributor
  • ***
  • Posts: 1812
  • Country: se
Re: Low-footprint printf replacement for embedded dev
« Reply #3 on: December 09, 2024, 10:25:23 am »
Will be interesting to compare the footprint for equivalent functionality between the two.
Yes, if I feel like it, I might try to replace it in an STM32H7 project where I use the mpaland one and check the mem size change.
Being it an STM32H750, flash space is at premium more than RAM.
Nandemo wa shiranai wa yo, shitteru koto dake.
 

Online peter-h

  • Super Contributor
  • ***
  • Posts: 4432
  • Country: gb
  • Doing electronics since the 1960s...
Re: Low-footprint printf replacement for embedded dev
« Reply #4 on: December 09, 2024, 02:55:22 pm »
I use the Marco Paland one and am totally happy with it. No problems at all found.

Not sure how to find the size because it includes loads of functions so it would take time. I see 0x238 for f_printf.

With 32F chips you need to be very careful to select the right libc.a library, for hardware single floats, etc. Some long previous threads on libc.a you can look up... I spent ages on that.

Sorry - just realised you know all this already; way more than I do :)
« Last Edit: December 09, 2024, 04:33:19 pm by peter-h »
Z80 Z180 Z280 Z8 S8 8031 8051 H8/300 H8/500 80x86 90S1200 32F417
 

Offline dare

  • Contributor
  • Posts: 40
  • Country: us
Re: Low-footprint printf replacement for embedded dev
« Reply #5 on: December 09, 2024, 05:20:38 pm »
Also consider eyalroz printf (https://github.com/eyalroz/printf), which is an evolved version of mpaland printf.  The latter appears to have been abandoned by its author and hasn't had an update since 2019.

eyalroz forked the original codebase with the goal of continuing and improving upon Marco's work.  I've used it in a number of embedded projects.
 
The following users thanked this post: newbrain

Offline HwAoRrDk

  • Super Contributor
  • ***
  • Posts: 1618
  • Country: gb
Re: Low-footprint printf replacement for embedded dev
« Reply #6 on: December 09, 2024, 05:24:08 pm »
Nice. Hadn't come across this one before. Or maybe I had, but forgot. ;D

I tried plugging this into a project where I was already using the mpaland printf, and it's smaller when configured like-for-like. Before my overall firmware size was 13,136 bytes, and now with nanoprintf it's 12,880 bytes - a saving of 256 bytes. :-+ This was when compiled for RISC-V, with GCC 13.3.0.

The mpaland printf was configured with:

Code: [Select]
#define PRINTF_DISABLE_SUPPORT_FLOAT
#define PRINTF_DISABLE_SUPPORT_EXPONENTIAL
#define PRINTF_DISABLE_SUPPORT_LONG_LONG

And nanoprintf was configured with:

Code: [Select]
#define NANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS 1
#define NANOPRINTF_USE_PRECISION_FORMAT_SPECIFIERS 1
#define NANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS 0
#define NANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS 0
#define NANOPRINTF_USE_BINARY_FORMAT_SPECIFIERS 0
#define NANOPRINTF_USE_WRITEBACK_FORMAT_SPECIFIERS 0

Although, I suppose I should probably have set NANOPRINTF_USE_BINARY_FORMAT_SPECIFIERS to '1' for a fairer comparison, because IIRC the mpaland printf supports binary output specifiers by default. But I'm not using that, so I disabled it.
 

Online newbrain

  • Super Contributor
  • ***
  • Posts: 1812
  • Country: se
Re: Low-footprint printf replacement for embedded dev
« Reply #7 on: December 09, 2024, 08:53:17 pm »
With 32F chips you need to be very careful to select the right libc.a library, for hardware single floats, etc. Some long previous threads on libc.a you can look up... I spent ages on that.
STM32H7xx's FPU supports DP too.
Since we are talking alternatives to library functions, I started using picolibc years ago on a whim, and make sure to compile it at best for MCU at hand.
For quick RPi pico(2) stuff, I just use their default library as it takes care of their weird FP accelerations.

Sorry - just realised you know all this already; way more than I do :)
;) See my signature...still a lot of learning opportunities!
Nandemo wa shiranai wa yo, shitteru koto dake.
 

Online nctnico

  • Super Contributor
  • ***
  • Posts: 28502
  • Country: nl
    • NCT Developments
Re: Low-footprint printf replacement for embedded dev
« Reply #8 on: December 09, 2024, 09:10:37 pm »
Interesting, thanks!
I mostly use this: https://github.com/mpaland/printf
Works quite well, and can be tailored to reduce footprint vs. functionality (as can nanoprintf, AFAICS), but it's not been updated in a while.

I might give nanoprintf a go.

Not a great fan of Unlicense, but the author smartly dual-licenses it under 0BSD.
Still rather complicated. A really efficient printf comes with an older GCC for the MSP430 microcontrollers. Easy to follow code as well. If you want to skip formatting, even smaller printfs can be constructed (or found).
There are small lies, big lies and then there is what is on the screen of your oscilloscope.
 

Online SiliconWizardTopic starter

  • Super Contributor
  • ***
  • Posts: 15927
  • Country: fr
Re: Low-footprint printf replacement for embedded dev
« Reply #9 on: December 09, 2024, 09:39:17 pm »
Depending on which of the xxprintf functions you use and the formats you use, sometimes what's provided by newlib nano takes less code size than nanoprintf. But the win here is that the latter doesn't use any heap allocation.

As I think was also discussed before, alternatives to printf altogether can be a good idea too - but sometimes you just need printf-compatible for an existing code base, or the flexibility they provide.
 

Offline brucehoult

  • Super Contributor
  • ***
  • Posts: 4837
  • Country: nz
Re: Low-footprint printf replacement for embedded dev
« Reply #10 on: December 09, 2024, 10:14:21 pm »
Since we are talking alternatives to library functions, I started using picolibc years ago on a whim, and make sure to compile it at best for MCU at hand.

Can't have been very long ago ... I'm pretty sure Keith only started work on it in maybe late 2019 or early 2020 when we were at SiFive together.
 
The following users thanked this post: newbrain

Offline brucehoult

  • Super Contributor
  • ***
  • Posts: 4837
  • Country: nz
Re: Low-footprint printf replacement for embedded dev
« Reply #11 on: December 09, 2024, 10:26:12 pm »
Interesting, thanks!
I mostly use this: https://github.com/mpaland/printf
Works quite well, and can be tailored to reduce footprint vs. functionality (as can nanoprintf, AFAICS), but it's not been updated in a while.

I might give nanoprintf a go.

Not a great fan of Unlicense, but the author smartly dual-licenses it under 0BSD.
Still rather complicated. A really efficient printf comes with an older GCC for the MSP430 microcontrollers. Easy to follow code as well. If you want to skip formatting, even smaller printfs can be constructed (or found).

These all look rather large.

99% of the time, all I need is printing chars, strings, and long/ulong in dec/hex, with optional field padding with spaces or 0s. Rather than bloating printf(), I'd be happy to use a separate ftoa() to a buffer on the few times I need it and then printf() that.
 

Offline westfw

  • Super Contributor
  • ***
  • Posts: 4361
  • Country: us
Re: Low-footprint printf replacement for embedded dev
« Reply #12 on: December 10, 2024, 09:20:47 am »
It might also be worthwhile to look at the printf() implementation in avr-libc.
It has "some" avr-specific bits, but it looks like they'd be pretty easy to port to anything...
 

Offline nfmax

  • Super Contributor
  • ***
  • Posts: 1627
  • Country: gb
Re: Low-footprint printf replacement for embedded dev
« Reply #13 on: December 10, 2024, 10:24:29 am »
An alternative approach might be to send an unformatted packet (byte count, type, bytes) using a minimal function on the target, and interpose a packet interpreter using another microcontroller that does nothing other than interpret the packets and fully printf() their payload
 

Offline Nominal Animal

  • Super Contributor
  • ***
  • Posts: 7308
  • Country: fi
    • My home page and email address
Re: Low-footprint printf replacement for embedded dev
« Reply #14 on: December 10, 2024, 11:38:25 am »
I like building the strings in reverse when severely constrained.
 

Offline Siwastaja

  • Super Contributor
  • ***
  • Posts: 9449
  • Country: fi
Re: Low-footprint printf replacement for embedded dev
« Reply #15 on: December 10, 2024, 12:19:33 pm »
An alternative approach might be to send an unformatted packet (byte count, type, bytes) using a minimal function on the target, and interpose a packet interpreter using another microcontroller that does nothing other than interpret the packets and fully printf() their payload

Or, in a simpler way, since the UART (or similar) would be connected to some sort of target PC, do the parsing on that PC. Yeah, it prevents using Hyperterminal if the customer wants to look at the prints, but then again today's customers usually want custom tools anyway (and don't even know about the existence of generic terminal programs).

Hence, tooling. Tooling almost always pays back for itself. It makes very little sense to have run-time parsing of printf format strings (both code size and execution time) on small embedded targets when you could just log the binary data itself. Like, timestamp, some identifier code and data values. This way you can fit surprisingly lot even on a small embedded RAM buffer, and transfer that blob to a target PC, and parse it there.

Not only you save on the implementation itself; you also save by not needing the strings.

I mean, if your program only prints 1-2 things, why bother with printf at all? Just do it with atoi() or whatever ad-hoc mess makes it work. If your program prints 1000 different things, I understand the convenience of the printf, but isn't the elephant in the room then the storage of those strings, not just the printf? If standard printf doesn't fit, would a smaller one? You would still have the strings.

Hence, get rid of both printf, and the strings. A two-byte identifier is able to describe 65536 different messages, which can be then parsed and printed on the host PC. Like a binary stream where U8 num_of_following_bytes, U16 message_identifier, variable-length bytes of data. Then a parser on PC where within switch-case you printf() the data.

For those who say it is a large project, I challenge you; just do it, I bet it's a smaller project than you think. Something similar to the time used trying out different printf implementations and fine-tuning their configuration macros trying to squeeze them down.
« Last Edit: December 10, 2024, 12:26:49 pm by Siwastaja »
 
The following users thanked this post: whitehorsesoft

Online newbrain

  • Super Contributor
  • ***
  • Posts: 1812
  • Country: se
Re: Low-footprint printf replacement for embedded dev
« Reply #16 on: December 10, 2024, 01:08:58 pm »
It might also be worthwhile to look at the printf() implementation in avr-libc.
It has "some" avr-specific bits, but it looks like they'd be pretty easy to port to anything...
That's just the one used by picolibc, BTW!

Can't have been very long ago ... I'm pretty sure Keith only started work on it in maybe late 2019 or early 2020 when we were at SiFive together.
Sounds about right...
Nandemo wa shiranai wa yo, shitteru koto dake.
 

Offline brucehoult

  • Super Contributor
  • ***
  • Posts: 4837
  • Country: nz
Re: Low-footprint printf replacement for embedded dev
« Reply #17 on: December 10, 2024, 01:10:47 pm »
Intermediate position, if you're not too worried about ROM/flash space, is to compile printfs as usual, but link to a library that doesn't actually format but just counts %s in the string and makes up a binary packet with the string address and N vararg arguments and a length count. The host can extract the format strings from the original elf file and do the formatting.

Slightly more work, before linking, replace each string body with a single byte saying how many arguments there are. Each format string will then have a unique address but only take 1 byte in flash. The host will then have to match the address in the reduced binary to the label, to the original string (maybe in the .s)
 
The following users thanked this post: Siwastaja

Online nctnico

  • Super Contributor
  • ***
  • Posts: 28502
  • Country: nl
    • NCT Developments
Re: Low-footprint printf replacement for embedded dev
« Reply #18 on: December 10, 2024, 01:25:26 pm »
Hence, get rid of both printf, and the strings. A two-byte identifier is able to describe 65536 different messages, which can be then parsed and printed on the host PC. Like a binary stream where U8 num_of_following_bytes, U16 message_identifier, variable-length bytes of data. Then a parser on PC where within switch-case you printf() the data.
Bad idea. You need to have a version of the debugging tool for Windows, Linux, Mac, Iphone and Android nowadays. And rights to install. I have been down this road many times: using Putty, Teraterm, Hyperterminal, serial USB terminal (Android), - whatever- is a much more convenient route compared to a dedicated tool. Keep in mind you need to change/update/distribute the tool when (not if) you add / change debugging info. And how about logging? Or uploading data? Terminal programs support protocols like xmodem/ymodem and sending/receiving plain files.

A super minimalistic printf takes like 10 lines of code and you keep the rest of the code standard.

A long time ago I inherited a project written by somebody who wanted to prevent using printf so he made all kinds of convoluted printing functions which where a nightmare to use. And on top of that, the C library's vuprintf got linked in anyway because this was used by some functions he wasn't aware of. IOW: solve the problem at the root, if the C library's printf (or more specifically: vuprintf) is too big, replace it with a smaller one.
« Last Edit: December 10, 2024, 01:53:19 pm by nctnico »
There are small lies, big lies and then there is what is on the screen of your oscilloscope.
 

Online peter-h

  • Super Contributor
  • ***
  • Posts: 4432
  • Country: gb
  • Doing electronics since the 1960s...
Re: Low-footprint printf replacement for embedded dev
« Reply #19 on: December 10, 2024, 02:06:54 pm »
The EYALROZ printf states

Marco therefore decided to write his own implementation, with the following goals in mind (I've dropped a few relative to his original description):

Very small implementation
NO dependencies on other packages or libraries; no multiple compiled objects, just one object file.
Support for all standard specifiers and flags, and all width and precision sub-specifiers (see below).
Support of decimal/floating number representation (with an internal, relatively fast itoa/ftoa implementation)
Reentrancy and thread-safety; malloc() freeness.
Clean, robust code.
Extensive test coverage.
MIT license

Marco's repository upheld most of these goals - but did not quite make it all of the way. As of mid-2021, it still had many C-standard-non-compliance bugs; the test suite was quite lacking in coverage; some goals were simply discarded (like avoiding global/local-static constants) etc.


Does anyone know where the "global/local-static constants" are used in Marco's code? That obviously does not make it thread-safe. But also it should be easy to fix.

The newlib printf uses statics and the heap and all kinds of crap. Extensively discussed here years ago. I dealt with that problem but it was complicated because ST did not supply the source to their libc.a. I won't post the whole story here but basically I kept some libc.a stuff (e.g. the heap code) so I mutexed the API for that stuff. I think heap code cannot possibly be thread-safe...



Z80 Z180 Z280 Z8 S8 8031 8051 H8/300 H8/500 80x86 90S1200 32F417
 

Offline AVI-crak

  • Regular Contributor
  • *
  • Posts: 141
  • Country: ru
    • Rtos
Re: Low-footprint printf replacement for embedded dev
« Reply #20 on: December 10, 2024, 02:28:08 pm »
When I was looking for a compact replacement for “printf”, I had to write my own variant. I had only 2k flash, and a double precision printf requirement.
https://github.com/AVI-crak/Rtos_cortex/tree/master/printo
printo("text", double, float, uint(8-16-32-64)_t, int(8-16-32-64)_t )
minimum weight +140 bytes, complete set 1684 bytes,
maximum speed 20~470 ticks,
 

Offline HwAoRrDk

  • Super Contributor
  • ***
  • Posts: 1618
  • Country: gb
Re: Low-footprint printf replacement for embedded dev
« Reply #21 on: December 10, 2024, 02:55:01 pm »
Does anyone know where the "global/local-static constants" are used in Marco's code? That obviously does not make it thread-safe. But also it should be easy to fix.

Only thing I can find is here, in _ftoa(): https://github.com/mpaland/printf/blob/d3b984684bb8a8bdc48cc7a1abecb93ce59bbe3e/printf.c#L346

Code: [Select]
  // powers of 10
  static const double pow10[] = { 1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000 };

But that's just literal constants, and unless I'm wrong, doesn't affect thread-safety or re-entrancy at all - such constant values will typically get stored in and read from flash.

Other than that, there's nothing I can see. There are no global variables at all.
 
The following users thanked this post: peter-h

Online peter-h

  • Super Contributor
  • ***
  • Posts: 4432
  • Country: gb
  • Doing electronics since the 1960s...
Re: Low-footprint printf replacement for embedded dev
« Reply #22 on: December 10, 2024, 05:04:52 pm »
Yes that should be fine.

I wonder if there is any info on "C-standard-non-compliance bugs; the test suite was quite lacking in coverage". I've been using Marco's printf for years and never saw anything weird.

There are many old printf sources floating (no pun intended) around. Here is one from 1993, which came with the Hitech C compiler. The company was taken over by Microchip and those tools have since been abandoned. This code doesn't use itoa, ltoa, etc.

Code: [Select]
#include <stdio.h>
#include <ctype.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <conio.h>
#include <sys.h>
#include <math.h>
#include <float.h>

/*
 * doprnt - long, non-float version depends on conditional compilation.
 * Copyright (C) 1993 HI-TECH Software
 *
 * This version only supports 32 bit floating point
 */


#if (sizeof(long) == sizeof(int) || defined(__FLOAT)) && !defined(__LONG)
#define __LONG 1
#endif

#if sizeof(double) == sizeof(long) && DBL_MAX_EXP == 128
#define frexp(val, ptr) (void)(*(ptr) = ((*(unsigned long *)&val >> 23) & 255) - 126)
#endif

#ifdef __LONG
#define value long
#define NDIG 12 /* max number of digits to be printed */
#else
#define value int
#define NDIG 6 /* max number of digits to be printed */
#endif

const static unsigned value dpowers[] = {1, 10, 100, 1000, 10000,
#ifdef __LONG
100000, 1000000, 10000000, 100000000,
1000000000
#endif
};
const static unsigned value hexpowers[] = {1, 0x10, 0x100, 0x1000,
#if __LONG
0x10000, 0x100000, 0x1000000, 0x10000000
#endif
};
const static unsigned value octpowers[] = {1, 010, 0100, 01000, 010000, 0100000,
#ifdef __LONG
01000000,
010000000, 0100000000, 01000000000, 010000000000,
0100000000000
#endif
};

#ifdef __FLOAT
#if sizeof(long) != sizeof(double)
#error This fnum is only for 32 bit doubles
#endif


#if DBL_MAX_10_EXP > 120
#define expon int
#else
#define expon signed char
#endif

extern const double _powers_[], _npowers_[];
extern unsigned long _div_to_l_(float, float);

/* this routine returns a value to round to the number of decimal
places specified */

double
fround(unsigned char prec)
{
/* prec is guaranteed to be less than NDIG */

if(prec > 10)
return 0.5 * _npowers_[prec/10+9] * _npowers_[prec%10];
return 0.5 * _npowers_[prec];
}

/* this routine returns a scaling factor equal to 1 to the decimal
   power supplied */

static double
scale(expon scl)
{

if(scl < 0) {
scl = -scl;
if(scl > 10)
return _npowers_[scl/10+9] * _npowers_[scl%10];
return _npowers_[scl];
}
if(scl > 10)
return _powers_[scl/10+9] * _powers_[scl%10];
return _powers_[scl];
}


#endif /* __FLOAT */


#define OPTSIGN 0x00
#define SPCSIGN 0x01
#define MANSIGN 0x02
#define NEGSIGN 0x03
#define FILL 0x04
#define LEFT 0x08
#define LONG 0x10
#define UPCASE 0x20
#define TEN 0x00
#define EIGHT 0x40
#define SIXTEEN 0x80
#define UNSIGN 0xC0
#define BASEM 0xC0
#define EFMT 0x100
#define GFMT 0x200
#define FFMT 0x400
#define ALTERN 0x800
#define DEFPREC 0x1000

#ifdef _HOSTED
#define pputc(c) (putc(c, fp) != EOF && ++ccnt)
int
vfprintf(FILE * fp, register const  char * f, register va_list ap)
{
char cbuf[2];
#else /* _HOSTED */
#define pputc(c) if(pb->ptr) (*pb->ptr++ = c),++ccnt; else ((pb->func(c)),++ccnt)
int
_doprnt(struct __prbuf * pb, register const  char * f, register va_list ap)
{
#endif /* _HOSTED */
int prec;
char c;
int width;
unsigned flag;
int ccnt = 0;
#ifdef __FLOAT
double fval;
int exp;
union {
unsigned value _val;
struct {
#if i8086 && SMALL_DATA
    far char * _cp;
#else
    char * _cp;
#endif
    unsigned _len;
} _str;
double _integ;
} _val;
#else
union {
unsigned value _val;
struct {
#if i8086 && SMALL_DATA
    far char * _cp;
#else
    char * _cp;
#endif
    unsigned _len;
} _str;
} _val;
#endif
#define val _val._val
#define cp _val._str._cp
#define len _val._str._len
#define integ _val._integ


flag = 0;
while(c = *f++) {
if(c != '%') {
pputc(c);
continue;
}
width = 0;
flag = 0;
for(;;) {

switch(*f) {

case '-':
flag |= LEFT;
f++;
continue;

case ' ':
flag |= SPCSIGN;
f++;
continue;

case '+':
flag |= MANSIGN;
f++;
continue;

case '#':
flag |= ALTERN;
f++;
continue;

case '0':
flag |= FILL;
f++;
continue;
}
break;
}
if(flag & MANSIGN)
flag &= ~SPCSIGN;
if(flag & LEFT)
flag &= ~FILL;
if(isdigit((unsigned)*f)) {
width = 0;
do
width = width*10 + *f++ - '0';
while(isdigit((unsigned)*f));
} else if(*f == '*') {
width = va_arg(ap, int);
f++;
}
if(*f == '.')
if(*++f == '*') {
prec = va_arg(ap, int);
f++;
} else {
prec = 0;
while(isdigit((unsigned)*f))
prec = prec*10 + *f++ - '0';
}
else {
prec = 0;
#ifdef __FLOAT
flag |= DEFPREC;
#endif
}
#ifdef __LONG
loop:
#endif
switch(c = *f++) {

case 0:
return ccnt;

case 'l':
#ifdef __LONG
flag |= LONG;
goto loop;
#else
cp = "(non-long printf)";
goto strings;
#endif

#ifndef __FLOAT
case 'E':
case 'f':
case 'e':
case 'g':
cp = "(non-float printf)";
goto strings;
#else
case 'f':
flag |= FFMT;
break;

case 'E':
flag |= UPCASE;
case 'e':
flag |= EFMT;
break;

case 'g':
flag |= GFMT;
break;
#endif
case 'o':
flag |= EIGHT;
break;

case 'd':
case 'i':
break;

case 'X':
case 'p':
flag |= UPCASE;
case 'x':
flag |= SIXTEEN;
break;

case 's':
#if i8086 && SMALL_DATA
if(flag & LONG)
cp = va_arg(ap, far char *);
else
#endif
cp = va_arg(ap, char *);
#if !defined(__FLOAT)
strings:
#endif
if(!cp)
cp = "(null)";
len = 0;
while(cp[len])
len++;
dostring:
if(prec && prec < len)
len = prec;
if(width > len)
width -= len;
else
width = 0;
if(!(flag & LEFT))
while(width--)
pputc(' ');
while(len--)
pputc(*cp++);
if(flag & LEFT)
while(width--)
pputc(' ');
continue;
case 'c':
#if _HOSTED
val = va_arg(ap, int);
c = val >> 8;
if(flag & LONG && c && (unsigned char)c != 0xFF) {
cbuf[0] = c;
cbuf[1] = val;
len = 2;
} else {
cbuf[0] = val;
len = 1;
}
cp = cbuf;
goto dostring;
#else
c = va_arg(ap, int);
#endif
default:
cp = &c;
len = 1;
goto dostring;

case 'u':
flag |= UNSIGN;
break;

}
#ifdef __FLOAT
if(flag & (EFMT|GFMT|FFMT)) {
if(flag & DEFPREC)
prec = 6;
fval = va_arg(ap, double);
if(fval < 0.0) {
fval = -fval;
flag |= NEGSIGN;
}
exp = 0;
frexp(fval, &exp); /* get binary exponent */
exp--; /* adjust 0.5 -> 1.0 */
exp *= 3;
exp /= 10; /* estimate decimal exponent */
if(exp < 0)
exp--;
integ = fval * scale(-exp);
if(integ < 1.0)
exp--;
else if(integ >= 10.0)
exp++;
if(exp <= 0)
c = 1;
else
c = exp;
#if 0
if(!(flag & ALTERN) && flag & FFMT && prec == 0) {
val = (long)(fval + 0.5);
flag |= LONG;
goto integer;
}
if(!(flag & ALTERN) && flag & GFMT && exp >= 0 && c <= prec) {
integ = fval + fround(prec - c);
if((unsigned)exp > sizeof dpowers/sizeof dpowers[0] || integ - (float)(unsigned long)integ < fround(prec-c-1)) {
val = (long)integ;
flag |= LONG;
prec = 0;
goto integer;
}
}
#endif
if(flag & EFMT || flag & GFMT && (exp < -4 || exp >= (int)prec)) { /* use e format */
if(prec && flag & GFMT)
prec--; /* g format precision includes integer digit */
if(prec > sizeof dpowers/sizeof dpowers[0] - 1)
c = sizeof dpowers/sizeof dpowers[0] - 1;
else
c = prec;
fval *= scale(c-exp);
if((unsigned long)fval >= dpowers[c+1]) {
fval *= 1e-1;
exp++;
} else if((unsigned long)fval < dpowers[c]) {
fval *= 10.0;
exp--;
}
#if 0
if(fval != 0.0) {
while(fval >= 10.0) {
fval *= 1e-1;
exp++;
if(flag & GFMT)
prec++;
}
while(fval < 1.0) {
fval *= 10.0;
exp--;
if(flag & GFMT)
prec--;
}
}
#endif
fval += 0.5;
if(flag & GFMT && !(flag & ALTERN)) { /* g format, precision means something different */
if(prec > (int)(sizeof dpowers/sizeof dpowers[0]))
prec = sizeof dpowers/sizeof dpowers[0];
val = (unsigned long)fval;
while(val && val % 10 == 0) {
prec--;
val /= 10;
}
if(prec < c) {
fval *= scale(prec-c);
c = prec;
}

}
width -=  prec + 5;
if(prec || flag & ALTERN)
width--;
if(flag & (MANSIGN|SPCSIGN))
width--;
if(exp >= 100 || exp <= -100) /* 3 digit exponent */
width--;
if(flag & FILL) {
if(flag & MANSIGN)
pputc(flag & SPCSIGN ? '-' : '+');
else if(flag & SPCSIGN)
pputc(' ');
while(width > 0) {
pputc('0');
width--;
}
} else {
if(!(flag & LEFT))
while(width > 0) {
pputc(' ');
width--;
}
if(flag & MANSIGN)
pputc(flag & SPCSIGN ? '-' : '+');
else if(flag & SPCSIGN)
pputc(' ');
}
val = (unsigned long)fval;
pputc(val/dpowers[c] + '0');
if(prec || flag & ALTERN) {
pputc('.');
prec -= c;
while(c) {
pputc('0' + (val/dpowers[--c]) % 10);
}
while(prec) {
pputc('0');
prec--;
}
}
if(flag & UPCASE)
pputc('E');
else
pputc('e');
if(exp < 0) {
exp = -exp;
pputc('-');
} else
pputc('+');
if(exp >= 100) {
pputc(exp / 100 + '0');
exp %= 100;
}
pputc(exp / 10 + '0');
pputc(exp % 10 + '0');
if((flag & LEFT) && width)
do
pputc(' ');
while(--width);
continue;
}
/* here for f format */

if(flag & GFMT) {
if(exp < 0)
prec -= exp-1;
val = (unsigned long)fval;
for(c = 1 ; c != sizeof dpowers/sizeof dpowers[0] ; c++)
if(val < dpowers[c])
break;
prec -= c;
val = (unsigned long)((fval-(double)val) * scale(prec)+0.5);
while(prec && val % 10 == 0) {
val /= 10;
prec--;
}
}
if(prec <= NDIG)
fval += fround(prec);
if(exp > (int)(sizeof dpowers/sizeof dpowers[0])-1) {
exp -= sizeof dpowers/sizeof dpowers[0]-1;
val = _div_to_l_(fval, scale(exp));
fval = 0.0;
} else {
val = (unsigned long)fval;
fval -= (float)val;
exp = 0;
}
for(c = 1 ; c != sizeof dpowers/sizeof dpowers[0] ; c++)
if(val < dpowers[c])
break;
width -= prec + c + exp;
if(flag & ALTERN || prec)
width--;
if(flag & (MANSIGN|SPCSIGN))
width--;
if(flag & FILL) {
if(flag & MANSIGN)
pputc(flag & SPCSIGN ? '-' : '+');
else if(flag & SPCSIGN)
pputc(' ');
while(width > 0) {
pputc('0');
width--;
}
} else {
if(!(flag & LEFT))
while(width > 0) {
pputc(' ');
width--;
}
if(flag & MANSIGN)
pputc(flag & SPCSIGN ? '-' : '+');
else if(flag & SPCSIGN)
pputc(' ');
}
while(c--)
pputc('0' + (val/dpowers[c]) % 10);
while(exp > 0) {
pputc('0');
exp--;
}
if(prec > (int)(sizeof dpowers/sizeof dpowers[0]))
c = sizeof dpowers/sizeof dpowers[0];
else
c = prec;
prec -= c;
if(c || flag & ALTERN)
pputc('.');
val = (long)(fval * scale(c));
while(c) {
pputc('0' + (val/dpowers[--c]) % 10);
}
while(prec) {
pputc('0');
prec--;
}
if((flag & LEFT) && width)
do
pputc(' ');
while(--width);
continue;
}
#endif /* __FLOAT */
if((flag & BASEM) == TEN) {
#ifdef __LONG
if(flag & LONG)
val = va_arg(ap, long);
else
#endif
val = (value)va_arg(ap, int);
if((value)val < 0) {
flag |= NEGSIGN;
val = -val;
}
} else {
#ifdef __LONG
if(flag & LONG)
val = va_arg(ap, unsigned long);
else
#endif
val = va_arg(ap, unsigned);
}
#ifdef __FLOAT
integer:
#endif
if(prec == 0 && val == 0)
prec++;
switch((unsigned char)(flag & BASEM)) {

case TEN:
case UNSIGN:
for(c = 1 ; c != sizeof dpowers/sizeof dpowers[0] ; c++)
if(val < dpowers[c])
break;
break;
case SIXTEEN:
for(c = 1 ; c != sizeof hexpowers/sizeof hexpowers[0] ; c++)
if(val < hexpowers[c])
break;
break;

case EIGHT:
for(c = 1 ; c != sizeof octpowers/sizeof octpowers[0] ; c++)
if(val < octpowers[c])
break;
break;
}
if(c < prec)
c = prec;
else if(prec < c)
prec = c;
if(width && flag & NEGSIGN)
width--;
if(width > prec)
width -= prec;
else
width = 0;
if((flag & (FILL|BASEM|ALTERN)) == (EIGHT|ALTERN)) {
if(width)
width--;
} else if((flag & (BASEM|ALTERN)) == (SIXTEEN|ALTERN)) {
if(width > 2)
width -= 2;
else
width = 0;
}
if(flag & FILL) {
if(flag & MANSIGN)
pputc(flag & SPCSIGN ? '-' : '+');
else if(flag & SPCSIGN)
pputc(' ');
else if((flag & (BASEM|ALTERN)) == (SIXTEEN|ALTERN)) {
pputc('0');
pputc(flag & UPCASE ? 'X' : 'x');
}
if(width)
do
pputc('0');
while(--width);
} else {
if(width && !(flag & LEFT))
do
pputc(' ');
while(--width);
if(flag & MANSIGN)
pputc(flag & SPCSIGN ? '-' : '+');
else if(flag & SPCSIGN)
pputc(' ');
if((flag & (BASEM|ALTERN)) == (EIGHT|ALTERN))
pputc('0');
else if((flag & (BASEM|ALTERN)) == (SIXTEEN|ALTERN)) {
pputc('0');
pputc(flag & UPCASE ? 'X' : 'x');
}
}
while(prec > c)
pputc('0');
while(prec--) {
switch((unsigned char)(flag & BASEM)) {

case TEN:
case UNSIGN:
c = (val / dpowers[prec]) % 10 + '0';
break;

case SIXTEEN:
c = (flag & UPCASE ? "0123456789ABCDEF" : "0123456789abcdef")[(val / hexpowers[prec]) & 0xF];
break;

case EIGHT:
c = ((val / octpowers[prec]) & 07) + '0';
break;
}
pputc(c);
}
if((flag & LEFT) && width)
do
pputc(' ');
while(--width);
}
return ccnt;
}
Z80 Z180 Z280 Z8 S8 8031 8051 H8/300 H8/500 80x86 90S1200 32F417
 
The following users thanked this post: RAPo

Offline westfw

  • Super Contributor
  • ***
  • Posts: 4361
  • Country: us
Re: Low-footprint printf replacement for embedded dev
« Reply #23 on: December 11, 2024, 02:58:58 am »
Quote
>> It might also be worthwhile to look at the printf() implementation in avr-libc.
That's just the one used by picolibc, BTW!
Ah; good.  It was my perception that picolibc was using some of the same ideas as avr-libc, but I haven't had the opportunity to actually look at it in any detail.
 

Online SiliconWizardTopic starter

  • Super Contributor
  • ***
  • Posts: 15927
  • Country: fr
Re: Low-footprint printf replacement for embedded dev
« Reply #24 on: December 11, 2024, 09:02:41 am »
I like building the strings in reverse when severely constrained.

Yes, that's an option of course, although reversing the string at the end of the conversion only takes a loop with half its length iterations and one byte of temporary storage (which should fit in a register unless you use a very, very small/odd target with only a handful of registers like, say, a Z80... or an 8-bit PIC!)
« Last Edit: December 11, 2024, 09:05:01 am by SiliconWizard »
 

Offline brucehoult

  • Super Contributor
  • ***
  • Posts: 4837
  • Country: nz
Re: Low-footprint printf replacement for embedded dev
« Reply #25 on: December 11, 2024, 09:52:55 am »
I like building the strings in reverse when severely constrained.

Yes, that's an option of course, although reversing the string at the end of the conversion only takes a loop with half its length iterations and one byte of temporary storage (which should fit in a register unless you use a very, very small/odd target with only a handful of registers like, say, a Z80... or an 8-bit PIC!)

The smallest PICs have 32 registers. And no RAM.

Z80 isn't too hard, since BC, DE, and HL can all be used to load/store A, and can all do 16 bit inc/dec.

Code: [Select]
    # BC = count, DE = loPtr, HL = hiPtr
loop:
    ld a,(de)  #7
    ld b,(hl)  #7
    ld (hl),a  #7
    ld a,b     #4
    ld (de),a  #7
    inc de     #6
    dec hl     #6
    dec bc     #6
    ld a,b     #4
    or c       #4
    jp nz,loop #10

So 13 bytes of code, 68 cycles per byte pair. You could make it a little bit quicker if count is 256 or less (so actually 512 byte string) as the dec bc;ld;or can simply be replaced by a 4 cycle dec b. So 58 cycles per byte pair.

Or course that's much worse than a modern ISA e.g.

Code: [Select]
    # a0 = loPtr, a1 = hiPtr
loop:
    lb a2,(a0)
    addi a0,a0,1
    lb a3,(a1)
    addi a1,a1,-1
    sb a2,-1(a1)
    sb a3,-1(a0)
    blt a0,a1,loop

16 bytes of code, quite likely 4 cycles per byte pair on a dual-issue in-order core.

Z80 needs a lot of instructions, but does have the benefit that most instructions are only 1 byte.
« Last Edit: December 11, 2024, 10:54:21 am by brucehoult »
 

Offline westfw

  • Super Contributor
  • ***
  • Posts: 4361
  • Country: us
Re: Low-footprint printf replacement for embedded dev
« Reply #26 on: December 11, 2024, 10:24:35 am »
Quote
The smallest PICs have 32 registers. And no RAM.
That's misleading.
The smallest PICs have an accumulator, and 25 data storage locations that behave somewhere between "registers" and "RAM" (they call it "Data Memory.") (and then an additional 7 peripheral registers, plus some additional memory ("call stack", Data direction registers) that is not directly accessible by the normal data instructions.)

In general, IMO, the 8bit PIC data memory behaves more like RAM than "registers", with most of the instructions operating between a data memory location and the accumulator (some instructions operate directly on the data memory (increment, decrement, bitwise operators), which I guess makes them somewhat "register-like", but pre-formalized-RISC there were a lot of architectures that would do some operations directly on memory.)

I think you have to go to the 16bit PICs before you get "real RAM", but the PIC16 and PIC18 can have up to 4k "registers" (in banks.  With weird addressing.)


---
I'm not sure I understand how building your strings backwards saves much code space.  The string reversal you'd normally get formatting integers isn't very much code...
 
The following users thanked this post: SiliconWizard

Offline brucehoult

  • Super Contributor
  • ***
  • Posts: 4837
  • Country: nz
Re: Low-footprint printf replacement for embedded dev
« Reply #27 on: December 11, 2024, 10:49:26 am »
Quote
The smallest PICs have 32 registers. And no RAM.
That's misleading.
The smallest PICs have an accumulator, and 25 data storage locations that behave somewhere between "registers" and "RAM" (they call it "Data Memory.") (and then an additional 7 peripheral registers, plus some additional memory ("call stack", Data direction registers) that is not directly accessible by the normal data instructions.)

I don't think it's at all misleading, especially when comparing against the Z80/8080.

W on the PIC is just like A on the Z80.  You can move things there, all 8 bit arithmetic is between another register and A/W. PIC is slightly more powerful in that you can choose between e.g. W <- W+r7 or r7 <- W+r7 using an extra bit in the instruction, while Z80 always puts the result in A.

Z80 has a 3 bit field in the instruction to choose which register to move/add/or/and etc to A, letting you choose any of A,B,C,D,E,H,L or (HL) (which 8080 calls "M") as the other operand source.

The smallest PICs have a 5 bit field which lets you choose any of the 32 registers to move to/from or do arithmetic with W.

It's very much the same thing.

Yes, only some of the PIC registers (25 you say, I haven't counted) are general purpose and the others (7 as you say) have special purposes. But they're all registers, and you can move stuff between them and W. One of the registers gets ALU result flags ... zero, negative, carry etc (though you can conditionally skip based on any bit of any of the 32 registers, not just the flags register). There's a FSR register to hold a RAM address, if you've got RAM other than the 32 registers), and an INDF register that then serves as an alias for the selected RAM address (8051 has similar special registers). And yeah some of the registers are GPIO ports.

FSR does allow you to point to any of the 32 registers, as well as to RAM (up to 224 bytes, depending which chip ... some have 0), which goes a bit against modern ISAs. But AVRs are the same (except the very newest I think), with the lowest 32 RAM addresses aliasing the 32 registers.

That's just the kind of thing people used to do in low end ISAs that were never intended to get superscalar OoO implementations.

You're not going to tell me that AVR doesn't really have registers are you?
 

Offline brucehoult

  • Super Contributor
  • ***
  • Posts: 4837
  • Country: nz
Re: Low-footprint printf replacement for embedded dev
« Reply #28 on: December 11, 2024, 10:53:14 am »
I'm not sure I understand how building your strings backwards saves much code space.  The string reversal you'd normally get formatting integers isn't very much code...

Also it's trivial to use the stack to temporarily store the least significant digits, with little code size or stack usage (with or without return addresses interspersed).
 

Offline Nominal Animal

  • Super Contributor
  • ***
  • Posts: 7308
  • Country: fi
    • My home page and email address
Re: Low-footprint printf replacement for embedded dev
« Reply #29 on: December 11, 2024, 12:35:56 pm »
I'm not sure I understand how building your strings backwards saves much code space.  The string reversal you'd normally get formatting integers isn't very much code...
No, but it all does add up.

All I can say is try it for yourself for your favourite architectures, and see if it makes a big enough difference for you.

The interface I suggest you use defines the buffer as for example
    char         buf[SIZE];   // SIZE being 127 or less
    int_fast8_t  len = sizeof buf;
with conversion functions returning the updated len:
    int_fast8_t  prepend_TYPE(int_fast8_t len, char buf[len], TYPE value);
with each function immediately returning len if it is already negative.

After all conversions are done, one need only check if len is negative.  If it is, you ran out of buffer space.  If it is zero or positive, it is also the offset to the beginning of the string, with ((sizeof buf) - len) characters in the buffer.

One case where you don't want to do this, is when you have an interface where the printf() style function can directly emit the data to a peripheral device, blocking until it has been completely sent.  Then, you only need enough buffer space for the largest dynamically constructed field.

The reason this yields smaller code is the compactness of typical accesses using this approach.  len is both the index to the start of the string, but also the number of characters available in the buffer.  It is very cheap to check if len is negative; much more code is needed to check if it exceeds a separately passed limit or constant.  If you have hardware division or modulo operation, then conversion of integers in any base is trivial.  (On AVR and similar, I do use repeated subtraction, with an array of ones in each decimal digit position, as that tends to yield very efficient code, much moreso than the alternatives.)
« Last Edit: December 11, 2024, 12:43:10 pm by Nominal Animal »
 

Online peter-h

  • Super Contributor
  • ***
  • Posts: 4432
  • Country: gb
  • Doing electronics since the 1960s...
Re: Low-footprint printf replacement for embedded dev
« Reply #30 on: December 11, 2024, 03:53:04 pm »
Surely the bottom line is that any "small" printf will not be standard compliant.

So what you are really getting is something that parses some subset of the format specifiers (%u etc) and outputs the data, and again with limitations. So you are probably getting only a bit more than a DIY job done with itoa, ltoa, ftoa, etc, and since almost nobody needs scientific notation output, the foregoing will do most jobs where you really want it tiny.

A standard compliant printf is pretty big. Look at the newlib sources (plenty of threads on newlib printf). The code to handle all the weird stuff is probably 90% of the total size. Some reading around here
https://www.eevblog.com/forum/microcontrollers/is-st-cube-ide-a-piece-of-buggy-crap/msg4577884/#msg4577884

I would also suggest that someone doing a very compact project is not really after a full printf implementation, by the very nature of what he's doing.
Z80 Z180 Z280 Z8 S8 8031 8051 H8/300 H8/500 80x86 90S1200 32F417
 

Offline rstofer

  • Super Contributor
  • ***
  • Posts: 9972
  • Country: us
Re: Low-footprint printf replacement for embedded dev
« Reply #31 on: December 11, 2024, 05:37:02 pm »
I just rip off the conversion functions from "The C Programming Language" - Kernighan & Ritchie and add some hex output functions for nibble, byte and word widths.

Step 1 - get the UART working
Step 2 - include string output function.  Print "Hello World\n"
Step 3 - include conversion functions and hex output functions
.. now start work on project.

No library functions are used and especially no heap use.
 

Offline AVI-crak

  • Regular Contributor
  • *
  • Posts: 141
  • Country: ru
    • Rtos
Re: Low-footprint printf replacement for embedded dev
« Reply #32 on: December 11, 2024, 07:29:25 pm »
I would also suggest that someone doing a very compact project is not really after a full printf implementation, by the very nature of what he's doing.
Reasonable idea -> cut as much as possible, but in a way that works.
I cut formatting completely. More than 93% of the source code was cut, the remaining 7% was rewritten differently.
Don't try to understand how it works.
 

Offline Siwastaja

  • Super Contributor
  • ***
  • Posts: 9449
  • Country: fi
Re: Low-footprint printf replacement for embedded dev
« Reply #33 on: December 12, 2024, 07:55:28 am »
Surely the bottom line is that any "small" printf will not be standard compliant.

Yeah, that's an interesting balance. You can cut quite many features out of printf() and 99% of developers still feel it's like the real thing, giving them the advantage of familiarity (which really is the only reason to include printf implementation at all). For example, writeback and exponential (scientific) format are rarely used. (But I realize when I say that, soon someone replies that they use these features all the time. Fair enough.)

But if you cut a tad too much, then it's too far from standard printf() and the familiarity is gone. It is then worse: developers waste time debugging the printf and wondering "why it doesn't work"*. Any custom solution with totally different interface would be easier, then.

*) and if they are competent and interested enough to immediately understand what is going on, evaluate different printf implementations, configure them and so on, I wonder why they are not rolling their own, better logging system which suits exactly to their needs and is likely much more efficient?
« Last Edit: December 12, 2024, 07:57:11 am by Siwastaja »
 

Offline AVI-crak

  • Regular Contributor
  • *
  • Posts: 141
  • Country: ru
    • Rtos
Re: Low-footprint printf replacement for embedded dev
« Reply #34 on: December 12, 2024, 09:00:11 am »
There are many printf() implementations, all contain a format field, all without exception. It is impossible to defeat the legacy, 99.99% of the code has classic printf(). Whether you want to or not, the library will be attached to the project.
When old solutions stop working, topics like this one appear.
 

Offline westfw

  • Super Contributor
  • ***
  • Posts: 4361
  • Country: us
Re: Low-footprint printf replacement for embedded dev
« Reply #35 on: December 12, 2024, 10:33:43 am »
We had a custom printf() that originally supported stuff like “%i” for an Internet address, and I think eventually let code dynamically add additional format specifiers.  Fun stuff.
(“Small” wasn’t so much a goal, although it might have turned out that way.  Had some “issues” when gcc started checking arguments against the format string…)

 

Offline Nominal Animal

  • Super Contributor
  • ***
  • Posts: 7308
  • Country: fi
    • My home page and email address
Re: Low-footprint printf replacement for embedded dev
« Reply #36 on: December 12, 2024, 01:03:14 pm »
When you drop printf compatibility requirements, you can consider some really funky alternatives.  As I mentioned, in very constrained situations I create strings bass-ackwards, using separate function calls for each part or field.

In network services, it is common to have a relatively slow pipe, and possibly many concurrent clients.  Instead of reserving memory for each client, one can create an array or linked list of structures formatting the response, and only reserve enough memory for describing dynamic state per client.  The idea is that given a state and a read-only formatting structure chain, a function computes the next 1 to N bytes of the response.  The function will always compute at least 1 byte, unless it is complete, and is called whenever the client output pipe has room for writing (and it is time to output data to that client).  Dynamic fields are computed on the fly if possible, otherwise at most one field in advance.  In essence, this is an async printf.

The downside of this approach is that it too easily balloons into a templating "language".  The main upside is that it allows very long and very complex outputs with relatively little RAM use (a little bit more than the longest dynamically constructed field needs).
 

Offline brucehoult

  • Super Contributor
  • ***
  • Posts: 4837
  • Country: nz
Re: Low-footprint printf replacement for embedded dev
« Reply #37 on: December 12, 2024, 01:16:29 pm »
When you drop printf compatibility requirements, you can consider some really funky alternatives.  As I mentioned, in very constrained situations I create strings bass-ackwards, using separate function calls for each part or field.

People just seem to like the convenience of writing a single literal string with places to insert other data marked out. A ton of other languages have copied that basic idea from C, but not included the formats in the string, just the locations e.g. with "%=" or "{3} {1} {2}" or something to indicate where to insert data, and the toString() or whatever function is automatically called on each argument.

If you don't mind giving that up then the C++ overloaded operator<< convention is one of the lowest visual and typing overhead ways to do it. And has the big benefit (as with any function-call-per-object aproach) of making it easy to not link in unneeded functionality.
 

Offline Siwastaja

  • Super Contributor
  • ***
  • Posts: 9449
  • Country: fi
Re: Low-footprint printf replacement for embedded dev
« Reply #38 on: December 12, 2024, 01:35:31 pm »
If you don't mind giving that up then the C++ overloaded operator<< convention is one of the lowest visual and typing overhead ways to do it. And has the big benefit (as with any function-call-per-object aproach) of making it easy to not link in unneeded functionality.

"Many" people are fine with the C++ way. In C operator overloading is not available, but nothing prevents you from using macros for syntactic sugar, or put multiple statements on a single line. I have been doing stuff like this for years:
Code: [Select]
print("Value of x is "); print_i32(x); print("\n");

It's not that different from:
Code: [Select]
std::cout << "Value of x is " << x << std::endl;

Both are more tedious to write in some specific cases with many values and separators mixed together where printf-type format strings shine. But how much printing do we actually have in microcontroller projects, does it matter at all if those few lines of code that print "a lot" require 30 more keystrokes?

Meanwhile, I think I have saved time and typing by a PR_VAR(x) macro which #stringifies the variable name and then prints " = " and value and linefeed. Easy to sprinkle around in code, less typing than printf("you_need_to_type_this_twice = %d\n", you_need_to_type_this_twice);
 

Offline brucehoult

  • Super Contributor
  • ***
  • Posts: 4837
  • Country: nz
Re: Low-footprint printf replacement for embedded dev
« Reply #39 on: December 12, 2024, 02:03:37 pm »
"Many" people are fine with the C++ way. In C operator overloading is not available, but nothing prevents you from using macros for syntactic sugar, or put multiple statements on a single line.

Nothing prevents you using the C++ compiler instead of the C compiler.

The Arduino library is C++ -- or at least requires the C++ compiler. Some would argue it doesn't use much of C++. But it does use it.

No doubt there is some overhead, but I've happily used Arduino C++ code to write programs for microcontrollers with 512 bytes of RAM (ATTiny85) and didn't have any RAM or flash size issues with the kind of simple things you do with an 8 pin package.

The ATTiny25 -- 128 bytes RAM, 2k flash -- is probably too small for C++. I haven't tried. I do know the Arduino IDE doesn't offer the '25 as a target option, but does offer the '45 (256 bytes RAM, 4k flash).
 

Online peter-h

  • Super Contributor
  • ***
  • Posts: 4432
  • Country: gb
  • Doing electronics since the 1960s...
Re: Low-footprint printf replacement for embedded dev
« Reply #40 on: December 13, 2024, 05:49:12 am »
Quote
We had a custom printf() that originally supported stuff like “%i” for an Internet address,

Presumably IPV4 (uint32_t) not IPV6 (some struct thing) ;)

I like that - very useful!
Z80 Z180 Z280 Z8 S8 8031 8051 H8/300 H8/500 80x86 90S1200 32F417
 

Offline westfw

  • Super Contributor
  • ***
  • Posts: 4361
  • Country: us
Re: Low-footprint printf replacement for embedded dev
« Reply #41 on: December 13, 2024, 09:48:28 am »
Quote
Presumably IPV4 (uint32_t)
Yep.  Also "%e" for ethernet addresses.
Compared the C++ way, I find printf formats to be a nice compromise in being able to visualize what the output will actually look like, not quite so explicit as fortran FORMAT or COBOL PICTUREs (?), but much better than individual function calls.

 

Offline Nominal Animal

  • Super Contributor
  • ***
  • Posts: 7308
  • Country: fi
    • My home page and email address
Re: Low-footprint printf replacement for embedded dev
« Reply #42 on: December 13, 2024, 11:08:23 am »
If you have separate print functions for each type you use, then in C11 and later one can use preprocessor macros and _Generic to implement a variadic print() macro that expands to individual function calls; i.e.
    print("x = ", x, ", y = ", y, "\n");
expanding to say
    print_cs("x = ");
    print_int(x);
    print_cs(", y = ");
    print_float(y);
    print_cs("\n");
I personally do not bother, because the multi-function sequence is just as readable to me as the single-function one.

For embedded uses, I like to use an ELF section containing a description of the variables, i.e.
Code: [Select]
struct var_desc {
    void *const ref;
    const char *const name;
    int_fast8_t (*const get)(uint_fast8_t maxlen, char buf[maxlen], void *ref);
    int_fast8_t (*const set)(uint_fast8_t maxlen, char buf[maxlen], void *ref);
};

// Getters convert the referred to variable to a string into buf
extern int_fast8_t  vardesc_get_int(uint_fast8_t maxlen, char buf[maxlen], void *ref);
extern int_fast8_t  vardesc_get_float(uint_fast8_t maxlen, char buf[maxlen], void *ref);

// Setters convert the string to the referred to variable
extern int_fast8_t  vardesc_set_int(uint_fast8_t maxlen, char buf[maxlen], void *ref);
extern int_fast8_t  vardesc_set_float(uint_fast8_t maxlen, char buf[maxlen], void *ref);

#define  MERGE4_(a,b,c,d)   a ## b ## c ## d
#define  MERGE4(a,b,c,d)    MERGE4_(a,b,c,d)
#define  VAR_DESC_NAME(prefix)   MERGE4(__, prefix, _, __LINE__)
#define  DECLARE_VAR(_var, _name, _getter, _setter) \
    static const struct var_desc  VAR_DESC_NAME(vardesc) \
    __attribute__((section ("vardesc"), used)) = { \
        .ref = &(_var), \
        .name = _name, \
        .get = _getter, \
        .set = _setter, \
    }
// Note: We could use _Generic for autoselection of .get and .set functions (based on _var),
//       and stringify variable name.  Sometimes, the name will differ, for example with a subsystem prefix.

extern const struct var_desc  __start_vardesc[];
extern const struct var_desc  __stop_vardesc[];
#define  __num_vardesc  (uintptr_t)(__stop_vardesc - __start_vardesc)
so that in the final linked binary, the vardesc section will collect all such declarations from all object files into a single contiguous array.  A simple text interface can then query and set any of these variables.  The above assumes that the linker script exports __start_vardesc at the beginning of the section, and __stop_vardesc at the address just past the section, as is usual; and this section can/should reside in Flash.

For example, you might have
    static int  step_count;
    DECLARE_VAR(step_count, "step_count", vardesc_get_int, vardesc_set_int);
either in a global scope, or in a function scope.  Note that the variable itself does not have global linkage, nor does the __vardesc_N static structure the declaration generates.

If one omits the setter, and adds say a debug level value for each, then this can easily be used for state dumping.  You might also use a char array instead of a pointer for the .name member so that the strings will also be included in the same section.

(The order of these entries will vary depending on compiler and linker version, and although it is possible to sort these so that lookup is O(log2N) instead of linear, sorting them is a bit annoying to do as it requires direct ELF object file modification.  My approach is to read the final linked ELF object file, then generate a new C source file with the actual array, that when compiled, reproduces the section, with a different section name.  The final linking is then redone, dropping the original section, and including the recompiled section.  This approach is very robust, and pretty portable across architectures.)

This is particularly useful when you have a build system that conditionally compiles and/or links in objects depending on build configuration, because this way the debug interface does not pull in all code, and does not need to know the build configuration.

I obviously use the same for command-response interfaces, where each command is described using a similar structure.  That way, commands do not need to be listed and edited in a central array or list, and instead can be described in their source files.  The actual descriptor array is then constructed at link time by the linker, with the array stored in Flash, nor RAM.
 

Online SiliconWizardTopic starter

  • Super Contributor
  • ***
  • Posts: 15927
  • Country: fr
Re: Low-footprint printf replacement for embedded dev
« Reply #43 on: December 13, 2024, 11:20:29 am »
If you have separate print functions for each type you use, then in C11 and later one can use preprocessor macros and _Generic to implement a variadic print() macro that expands to individual function calls;

Yes, I'm actually writing a replacement for printf functions, my approach is more with passing the various elements via an array to a single function call. I also use macros and _Generic to make defining this array easier.
I wish we had a slightly more elaborate preprocessor though. Variadic macros are pretty limited and "iterating" through a variadic macro arguments requires ugly "hacks".
 

Offline cv007

  • Frequent Contributor
  • **
  • Posts: 864
Re: Low-footprint printf replacement for embedded dev
« Reply #44 on: December 14, 2024, 01:18:14 am »
If using C++, you can write a cout 'style' replacement without much trouble. This example lives in a single header-

https://godbolt.org/z/rfrfYrsvz

In the above case, the online compiler used is x86 just so can view stdout and can develop/test in the online compiler and move to your pc when done. Any mcu with a c++ compiler will do, such as avr, any cortex-m, etc. The example uses string_view from the c++ standard library which the avr doe not have, but can easily change to eliminate string_view if not available.

I have used this in various cortex-m as the newlib stdio code is too complex to understand (if you care about that). Whether is is better/worse than printf, not sure, but you do get any problems sorted at compile time rather than waiting to find out at runtime. Another advantage is it becomes easy to get formatted output of your own types, and is easy to 'attach' to any class which simply has to produce a single char output (whatever that may be- hardware, a buffer, doesn't matter).

It probably looks ugly if you cannot read C++, but there is really not much to it. There are 3 main functions that do the work (string output, number to string, double to string), and everything else is dealing with options and getting C++ to 'direct' incoming data to the right place. The example could be stripped of ansi things to make it smaller, but I left them in place as ansi output becomes easy (and the online compiler now seems to handle ansi codes). The 'double to string' conversion function is probably not perfect, but for my mcu use it seems to be fine. Also not going to win speed contests, but I do not enter any contests and just want reliable formatted output I can drag from mcu to mcu without change. You would think the code size could be a problem, but have found it makes little difference vs printf- even with extensive usage.

If I find any mistakes or changes I need/want, I have a single page of code to deal with and its not complex. Also note how many defines and macros are needed.
 

Offline i509VCB

  • Contributor
  • Posts: 29
  • Country: us
Re: Low-footprint printf replacement for embedded dev
« Reply #45 on: December 16, 2024, 03:50:48 am »
Maybe a little unrelated, but if your only use for printf is going to be debugging info and it the result is not shown to the user (i.e. serial console) then something like porting defmt (https://defmt.ferrous-systems.com/introduction) to act like printf from C could be very interesting. The gist of defmt is that at compile time the string format is converted to an ID. The "printf" in this case will just write the message ID and any arguments via whatever output you use (RTT for SWD debug, UART, etc). The host PC for debugging since it has the binary running on the target can interpret the ID and arguments and do the actual expensive printf formatting.
 

Offline brucehoult

  • Super Contributor
  • ***
  • Posts: 4837
  • Country: nz
Re: Low-footprint printf replacement for embedded dev
« Reply #46 on: December 16, 2024, 04:43:42 am »
Maybe a little unrelated, but if your only use for printf is going to be debugging info and it the result is not shown to the user (i.e. serial console) then something like porting defmt (https://defmt.ferrous-systems.com/introduction) to act like printf from C could be very interesting. The gist of defmt is that at compile time the string format is converted to an ID. The "printf" in this case will just write the message ID and any arguments via whatever output you use (RTT for SWD debug, UART, etc). The host PC for debugging since it has the binary running on the target can interpret the ID and arguments and do the actual expensive printf formatting.

I already suggested doing this kind of thing in https://www.eevblog.com/forum/microcontrollers/low-footprint-printf-replacement-for-embedded-dev/msg5742613/#msg5742613

Has the benefit of not having to change the C source code. Just some tricks with the .o files, or maybe compiling via .s.

I've done it a few times in the past.
 

Offline Nominal Animal

  • Super Contributor
  • ***
  • Posts: 7308
  • Country: fi
    • My home page and email address
Re: Low-footprint printf replacement for embedded dev
« Reply #47 on: December 16, 2024, 11:06:02 am »
One important point to consider is the difference between snprintf() and printf()/fprintf().

In embedded environments in C, I typically need the former.  In C++, the latter is usually an interface on top of a communications stream, implemented on top of a common base written=write(buffer,length) functionality.

The main difference is the buffer handling and partial writes.

For snprintf() and similar interfaces, the caller owns and is responsible for the buffer.  If the buffer has insufficient space, the caller can easily observe it from the result value.

For printf()-type interfaces, the underlying communications stream owns the buffer, and the call cannot return before all of the string has been buffered.

Neither is well suited for asynchronous interfaces where the call begins the buffering of the resulting string, with a separate interface provides information on the progress of the buffering/sending/storage.  Current C interfaces are particularly poorly suited for such asynchronous interfaces (and coroutines, which would make such async interfaces much easier to implement, by generating the result characters on an as-needed basis; noting, however, that the final reversal of the output string would not be feasible then).

(Such async printing interfaces are much more common in systems programming and "templated" responses from service daemons; that is where I encountered them first a couple of decades ago.  As IoT use increases, I suspect the usefulness of such interfaces will increase even in the microcontroller/small-embedded domains.)
 

Online SiliconWizardTopic starter

  • Super Contributor
  • ***
  • Posts: 15927
  • Country: fr
Re: Low-footprint printf replacement for embedded dev
« Reply #48 on: December 16, 2024, 09:49:35 pm »
Neither is well suited for asynchronous interfaces where the call begins the buffering of the resulting string, with a separate interface provides information on the progress of the buffering/sending/storage.  Current C interfaces are particularly poorly suited for such asynchronous interfaces (and coroutines, which would make such async interfaces much easier to implement, by generating the result characters on an as-needed basis; noting, however, that the final reversal of the output string would not be feasible then).

Yes, what you mean is that the printf functions are all "blocking" - they can only run to completion, even if said completion is a truncated string (in the case of the s*prinff functions).
For "async" behavior, one needs to retain the state. That would require passing a "state" variable (typically a structure). It does make things more complex to implement obviously.

For my replacement, I'm thinking of adding something like this. To make things simpler and more efficient, my thought was to handle the number conversions strictly synchronously and the overall string formatting asynchronous. Meaning if there are numbers to format, numbers would need to be each entirely converted, which would just require a relatively small minimum buffer (numbers will typically require only a few bytes as a string, maybe a couple tens of bytes for the very longest ones). If you see what I mean.
 

Offline Nominal Animal

  • Super Contributor
  • ***
  • Posts: 7308
  • Country: fi
    • My home page and email address
Re: Low-footprint printf replacement for embedded dev
« Reply #49 on: December 17, 2024, 10:51:53 am »
Nothing prevents you using the C++ compiler instead of the C compiler.
Actually, there is one thing: older AVR (and other Harvard architecture MCUs) and named address spaces, on plain hardware, outside Arduino core libraries.

For very specific but oddball reasons, g++ does not support named address spaces nearly as well as gcc does.  Clang has no issues in C or in C++, though, but unless I'm mistaken, clang's/llvm's AVR support is not as complete as gcc's.

The benefit is being able to differentiate between character data in RAM and character data in Flash at the type level; including _Generic() macros.  This is not really possible in Arduino (using g++) right now, at least the last time I checked.

For my replacement, I'm thinking of adding something like this. To make things simpler and more efficient, my thought was to handle the number conversions strictly synchronously and the overall string formatting asynchronous. Meaning if there are numbers to format, numbers would need to be each entirely converted, which would just require a relatively small minimum buffer (numbers will typically require only a few bytes as a string, maybe a couple tens of bytes for the very longest ones). If you see what I mean.
Yes, I do believe I do.  For standard printf-type formatting strings, it means converting the format string to the stateful structure(s) synchronously, plus supplying RAM for the temporary buffer needed for longest dynamically generated field.  It is "wasteful" in the sense that it would be possible to generate these structures at compile time, for lower run-time cost.

In fact, I have done something similar, except without the printf-like part: users supply an array of fields, temporary one-field conversion storage, plus the pointers to the format specifications or formatters for each field (these being in Flash, either read-only or code).  This worked well for both embedded and systems programming under a fully featured OS, so I'd say your main hurdle will be the string-to-structure conversion, and using compact and efficient structures.  (I found that separating the varying state kept in RAM, and the formatting specs kept in Flash, was quite important.)
 

Offline martinribelotta

  • Regular Contributor
  • *
  • Posts: 66
  • Country: ar
  • A camel is a horse designed by a committee
    • Martin Ribelotta design services
Re: Low-footprint printf replacement for embedded dev
« Reply #50 on: December 17, 2024, 01:18:28 pm »
A few years ago I develop a little deferred printf library for log system on flying embedded devices:

https://github.com/martinribelotta/elog

Is pretty simple and straight forward but work well in hard environments with low footprint requeriments.

The log packet may be better and the decoder may be more smart, but for the requeriments of these project was sufficient.
 
The following users thanked this post: pardo-bsso, mikerj

Offline tellurium

  • Frequent Contributor
  • **
  • Posts: 287
  • Country: ua
Re: Low-footprint printf replacement for embedded dev
« Reply #51 on: December 29, 2024, 09:09:58 am »
Quote
Presumably IPV4 (uint32_t)
Yep.  Also "%e" for ethernet addresses.
Compared the C++ way, I find printf formats to be a nice compromise in being able to visualize what the output will actually look like, not quite so explicit as fortran FORMAT or COBOL PICTUREs (?), but much better than individual function calls.

This implementation https://github.com/cesanta/str expands the idea of non-standard specifiers (%I, %e) to one single non-standard specifier %m/%M which requires a "printer" function. This way, you can custom-print anything: internet addresses, ethernet addresses, structures, automatically base64-encode strings, etc etc.
Open source embedded network library https://github.com/cesanta/mongoose
TCP/IP stack + TLS1.3 + HTTP/WebSocket/MQTT in a single file
 


Share me

Digg  Facebook  SlashDot  Delicious  Technorati  Twitter  Google  Yahoo
Smf