Low-footprint printf replacement for embedded dev - Page 1
Forum section: electronics / general

author: SiliconWizard
author_rank: Super Contributor
timestamp: December 09, 2024, 03:57:01 am
content: 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

author: newbrain
author_rank: Super Contributor
timestamp: December 09, 2024, 09:10:21 am
content: Interesting, thanks!I mostly use this:https://github.com/mpaland/printfWorks 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.

author: SiliconWizard
author_rank: Super Contributor
timestamp: December 09, 2024, 09:34:52 am
content: Will be interesting to compare the footprint for equivalent functionality between the two.

author: newbrain
author_rank: Super Contributor
timestamp: December 09, 2024, 10:25:23 am
content: Quote from: SiliconWizard on December 09, 2024, 09:34:52 amYes, 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.

author: peter-h
author_rank: Super Contributor
timestamp: December 09, 2024, 02:55:22 pm
content: 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

author: dare
author_rank: Contributor
timestamp: December 09, 2024, 05:20:38 pm
content: 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.

author: HwAoRrDk
author_rank: Super Contributor
timestamp: December 09, 2024, 05:24:08 pm
content: Nice. Hadn't come across this one before. Or maybe I had, but forgot.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:#define PRINTF_DISABLE_SUPPORT_FLOAT#define PRINTF_DISABLE_SUPPORT_EXPONENTIAL#define PRINTF_DISABLE_SUPPORT_LONG_LONGAnd nanoprintf was configured with:Code:#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 0Although, I suppose I should probably have setNANOPRINTF_USE_BINARY_FORMAT_SPECIFIERSto '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.

author: newbrain
author_rank: Super Contributor
timestamp: December 09, 2024, 08:53:17 pm
content: Quote from: peter-h on December 09, 2024, 02:55:22 pmSTM32H7xx's FPU supports DP too.Since we are talking alternatives to library functions, I started usingpicolibcyears 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.Quote from: peter-h on December 09, 2024, 02:55:22 pmSee my signature...still a lot of learning opportunities!

author: nctnico
author_rank: Super Contributor
timestamp: December 09, 2024, 09:10:37 pm
content: Quote from: newbrain on December 09, 2024, 09:10:21 amStill 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).

author: SiliconWizard
author_rank: Super Contributor
timestamp: December 09, 2024, 09:39:17 pm
content: 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.

author: brucehoult
author_rank: Super Contributor
timestamp: December 09, 2024, 10:14:21 pm
content: Quote from: newbrain on December 09, 2024, 08:53:17 pmCan'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.

author: brucehoult
author_rank: Super Contributor
timestamp: December 09, 2024, 10:26:12 pm
content: Quote from: nctnico on December 09, 2024, 09:10:37 pmThese 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 bloatingprintf(), I'd be happy to use a separateftoa()to a buffer on the few times I need it and thenprintf()that.

author: westfw
author_rank: Super Contributor
timestamp: December 10, 2024, 09:20:47 am
content: 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...

author: nfmax
author_rank: Super Contributor
timestamp: December 10, 2024, 10:24:29 am
content: 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

author: Nominal Animal
author_rank: Super Contributor
timestamp: December 10, 2024, 11:38:25 am
content: I likebuilding the strings in reversewhen severely constrained.

author: Siwastaja
author_rank: Super Contributor
timestamp: December 10, 2024, 12:19:33 pm
content: Quote from: nfmax on December 10, 2024, 10:24:29 amOr, 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 genericterminal 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 ofthose 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.

author: newbrain
author_rank: Super Contributor
timestamp: December 10, 2024, 01:08:58 pm
content: Quote from: westfw on December 10, 2024, 09:20:47 amThat's just the one used by picolibc, BTW!Quote from: brucehoult on December 09, 2024, 10:14:21 pmSounds about right...

author: brucehoult
author_rank: Super Contributor
timestamp: December 10, 2024, 01:10:47 pm
content: 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)

author: nctnico
author_rank: Super Contributor
timestamp: December 10, 2024, 01:25:26 pm
content: Quote from: Siwastaja on December 10, 2024, 12:19:33 pmBad 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.

author: peter-h
author_rank: Super Contributor
timestamp: December 10, 2024, 02:06:54 pm
content: The EYALROZ printf statesMarco 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 implementationNO 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 licenseMarco'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...

author: AVI-crak
author_rank: Regular Contributor
timestamp: December 10, 2024, 02:28:08 pm
content: 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/printoprinto("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,

author: HwAoRrDk
author_rank: Super Contributor
timestamp: December 10, 2024, 02:55:01 pm
content: Quote from: peter-h on December 10, 2024, 02:06:54 pmOnly thing I can find is here, in_ftoa():https://github.com/mpaland/printf/blob/d3b984684bb8a8bdc48cc7a1abecb93ce59bbe3e/printf.c#L346Code:// powers of 10static 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.

author: peter-h
author_rank: Super Contributor
timestamp: December 10, 2024, 05:04:52 pm
content: 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:#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__LONG1#endif#ifsizeof(double) == sizeof(long) && DBL_MAX_EXP == 128#definefrexp(val, ptr) (void)(*(ptr) = ((*(unsigned long *)&val >> 23) & 255) - 126)#endif#ifdef__LONG#definevaluelong#defineNDIG12/* max number of digits to be printed */#else#definevalueint#defineNDIG6/* max number of digits to be printed */#endifconst static unsigned valuedpowers[] ={1, 10, 100, 1000, 10000,#ifdef__LONG100000, 1000000, 10000000, 100000000,1000000000#endif};const static unsigned valuehexpowers[] ={1, 0x10, 0x100, 0x1000,#if__LONG0x10000, 0x100000, 0x1000000, 0x10000000#endif};const static unsigned valueoctpowers[] ={1, 010, 0100, 01000, 010000, 0100000,#ifdef__LONG01000000,010000000, 0100000000, 01000000000, 010000000000,0100000000000#endif};#ifdef__FLOAT#ifsizeof(long) != sizeof(double)#errorThis fnum is only for 32 bit doubles#endif#ifDBL_MAX_10_EXP > 120#defineexponint#else#defineexponsigned char#endifextern const double_powers_[], _npowers_[];extern unsigned long_div_to_l_(float, float);/* this routine returns a value to round to the number of decimalplaces specified */doublefround(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_;}/* this routine returns a scaling factor equal to 1 to the decimalpower supplied */static doublescale(expon scl){if(scl < 0) {scl = -scl;if(scl > 10)return _npowers_[scl/10+9] * _npowers_[scl%10];return _npowers_;}if(scl > 10)return _powers_[scl/10+9] * _powers_[scl%10];return _powers_;}#endif/* __FLOAT */#defineOPTSIGN0x00#defineSPCSIGN0x01#defineMANSIGN0x02#defineNEGSIGN0x03#defineFILL0x04#defineLEFT0x08#defineLONG0x10#defineUPCASE0x20#defineTEN0x00#defineEIGHT0x40#defineSIXTEEN0x80#defineUNSIGN0xC0#defineBASEM0xC0#defineEFMT0x100#defineGFMT0x200#defineFFMT0x400#defineALTERN0x800#defineDEFPREC0x1000#ifdef_HOSTED#definepputc(c)(putc(c, fp) != EOF && ++ccnt)intvfprintf(FILE * fp, register const char * f, register va_list ap){charcbuf;#else/* _HOSTED */#definepputc(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 */intprec;charc;intwidth;unsignedflag;intccnt = 0;#ifdef__FLOATdoublefval;intexp;union {unsigned value_val;struct {#ifi8086 && SMALL_DATAfar char *_cp;#elsechar *_cp;#endifunsigned_len;}_str;double_integ;}_val;#elseunion {unsigned value_val;struct {#ifi8086 && SMALL_DATAfar char *_cp;#elsechar *_cp;#endifunsigned_len;}_str;}_val;#endif#defineval_val._val#definecp_val._str._cp#definelen_val._str._len#defineinteg_val._integflag = 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;dowidth = 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__FLOATflag |= DEFPREC;#endif}#ifdef__LONGloop:#endifswitch(c = *f++) {case 0:return ccnt;case 'l':#ifdef__LONGflag |= LONG;goto loop;#elsecp = "(non-long printf)";goto strings;#endif#ifndef__FLOATcase 'E':case 'f':case 'e':case 'g':cp = "(non-float printf)";goto strings;#elsecase 'f':flag |= FFMT;break;case 'E':flag |= UPCASE;case 'e':flag |= EFMT;break;case 'g':flag |= GFMT;break;#endifcase 'o':flag |= EIGHT;break;case 'd':case 'i':break;case 'X':case 'p':flag |= UPCASE;case 'x':flag |= SIXTEEN;break;case 's':#ifi8086 && SMALL_DATAif(flag & LONG)cp = va_arg(ap, far char *);else#endifcp = va_arg(ap, char *);#if!defined(__FLOAT)strings:#endifif(!cp)cp = "(null)";len = 0;while(cp)len++;dostring:if(prec && prec < len)len = prec;if(width > len)width -= len;elsewidth = 0;if(!(flag & LEFT))while(width--)pputc(' ');while(len--)pputc(*cp++);if(flag & LEFT)while(width--)pputc(' ');continue;case 'c':#if_HOSTEDval = va_arg(ap, int);c = val >> 8;if(flag & LONG && c && (unsigned char)c != 0xFF) {cbuf = c;cbuf = val;len = 2;} else {cbuf = val;len = 1;}cp = cbuf;goto dostring;#elsec = va_arg(ap, int);#endifdefault:cp = &c;len = 1;goto dostring;case 'u':flag |= UNSIGN;break;}#ifdef__FLOATif(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;elsec = exp;#if0if(!(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 || integ - (float)(unsigned long)integ < fround(prec-c-1)) {val = (long)integ;flag |= LONG;prec = 0;goto integer;}}#endifif(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 - 1)c = sizeof dpowers/sizeof dpowers - 1;elsec = prec;fval *= scale(c-exp);if((unsigned long)fval >= dpowers[c+1]) {fval *= 1e-1;exp++;} else if((unsigned long)fval < dpowers) {fval *= 10.0;exp--;}#if0if(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--;}}#endiffval += 0.5;if(flag & GFMT && !(flag & ALTERN)) {/* g format, precision means something different */if(prec > (int)(sizeof dpowers/sizeof dpowers))prec = sizeof dpowers/sizeof dpowers;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 + '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');elsepputc('e');if(exp < 0) {exp = -exp;pputc('-');} elsepputc('+');if(exp >= 100) {pputc(exp / 100 + '0');exp %= 100;}pputc(exp / 10 + '0');pputc(exp % 10 + '0');if((flag & LEFT) && width)dopputc(' ');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 ; c++)if(val < dpowers)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)-1) {exp -= sizeof dpowers/sizeof dpowers-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 ; c++)if(val < dpowers)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) % 10);while(exp > 0) {pputc('0');exp--;}if(prec > (int)(sizeof dpowers/sizeof dpowers))c = sizeof dpowers/sizeof dpowers;elsec = 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)dopputc(' ');while(--width);continue;}#endif/* __FLOAT */if((flag & BASEM) == TEN) {#ifdef__LONGif(flag & LONG)val = va_arg(ap, long);else#endifval = (value)va_arg(ap, int);if((value)val < 0) {flag |= NEGSIGN;val = -val;}} else {#ifdef__LONGif(flag & LONG)val = va_arg(ap, unsigned long);else#endifval = va_arg(ap, unsigned);}#ifdef__FLOATinteger:#endifif(prec == 0 && val == 0)prec++;switch((unsigned char)(flag & BASEM)) {case TEN:case UNSIGN:for(c = 1 ; c != sizeof dpowers/sizeof dpowers ; c++)if(val < dpowers)break;break;case SIXTEEN:for(c = 1 ; c != sizeof hexpowers/sizeof hexpowers ; c++)if(val < hexpowers)break;break;case EIGHT:for(c = 1 ; c != sizeof octpowers/sizeof octpowers ; c++)if(val < octpowers)break;break;}if(c < prec)c = prec;else if(prec < c)prec = c;if(width && flag & NEGSIGN)width--;if(width > prec)width -= prec;elsewidth = 0;if((flag & (FILL|BASEM|ALTERN)) == (EIGHT|ALTERN)) {if(width)width--;} else if((flag & (BASEM|ALTERN)) == (SIXTEEN|ALTERN)) {if(width > 2)width -= 2;elsewidth = 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)dopputc('0');while(--width);} else {if(width && !(flag & LEFT))dopputc(' ');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) % 10 + '0';break;case SIXTEEN:c = (flag & UPCASE ? "0123456789ABCDEF" : "0123456789abcdef")[(val / hexpowers) & 0xF];break;case EIGHT:c = ((val / octpowers) & 07) + '0';break;}pputc(c);}if((flag & LEFT) && width)dopputc(' ');while(--width);}return ccnt;}

author: westfw
author_rank: Super Contributor
timestamp: December 11, 2024, 02:58:58 am
content: QuoteAh; 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.

author: SiliconWizard
author_rank: Super Contributor
timestamp: December 11, 2024, 09:02:41 am
content: Quote from: Nominal Animal on December 10, 2024, 11:38:25 amYes, 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!)

author: brucehoult
author_rank: Super Contributor
timestamp: December 11, 2024, 09:52:55 am
content: Quote from: SiliconWizard on December 11, 2024, 09:02:41 amThe 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:# BC = count, DE = loPtr, HL = hiPtrloop:ld a,(de) #7ld b,(hl) #7ld (hl),a #7ld a,b #4ld (de),a #7inc de #6dec hl #6dec bc #6ld a,b #4or c #4jp nz,loop #10So 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 thedec bc;ld;orcan simply be replaced by a 4 cycledec b. So 58 cycles per byte pair.Or course that's much worse than a modern ISA e.g.Code:# a0 = loPtr, a1 = hiPtrloop:lb a2,(a0)addi a0,a0,1lb a3,(a1)addi a1,a1,-1sb a2,-1(a1)sb a3,-1(a0)blt a0,a1,loop16 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.

author: westfw
author_rank: Super Contributor
timestamp: December 11, 2024, 10:24:35 am
content: QuoteThat'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...

author: brucehoult
author_rank: Super Contributor
timestamp: December 11, 2024, 10:49:26 am
content: Quote from: westfw on December 11, 2024, 10:24:35 amI 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+r7orr7 <- W+r7using 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?

author: brucehoult
author_rank: Super Contributor
timestamp: December 11, 2024, 10:53:14 am
content: Quote from: westfw on December 11, 2024, 10:24:35 amAlso 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).

author: Nominal Animal
author_rank: Super Contributor
timestamp: December 11, 2024, 12:35:56 pm
content: Quote from: westfw on December 11, 2024, 10:24:35 amNo, 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 examplecharbuf; //SIZEbeing 127 or lessint_fast8_tlen= sizeofbuf;with conversion functions returning the updatedlen:int_fast8_t prepend_TYPE(int_fast8_tlen, charbuf,TYPEvalue);with each function immediately returninglenif it is already negative.After all conversions are done, one need only check iflenis 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((sizeofbuf) -len)characters in the buffer.One case where you don't want to do this, is when you have an interface where theprintf()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.lenis 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 iflenis 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.)

author: peter-h
author_rank: Super Contributor
timestamp: December 11, 2024, 03:53:04 pm
content: Surely the bottom line is that any "small" printf willnotbe 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 herehttps://www.eevblog.com/forum/microcontrollers/is-st-cube-ide-a-piece-of-buggy-crap/msg4577884/#msg4577884I 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.

author: rstofer
author_rank: Super Contributor
timestamp: December 11, 2024, 05:37:02 pm
content: 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 workingStep 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.

author: AVI-crak
author_rank: Regular Contributor
timestamp: December 11, 2024, 07:29:25 pm
content: Quote from: peter-h on December 11, 2024, 03:53:04 pmReasonable 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.

author: Siwastaja
author_rank: Super Contributor
timestamp: December 12, 2024, 07:55:28 am
content: Quote from: peter-h on December 11, 2024, 03:53:04 pmYeah, 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 offamiliarity(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 thattheyuse 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 thenworse: 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?

author: AVI-crak
author_rank: Regular Contributor
timestamp: December 12, 2024, 09:00:11 am
content: 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.

author: westfw
author_rank: Super Contributor
timestamp: December 12, 2024, 10:33:43 am
content: 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…)

author: Nominal Animal
author_rank: Super Contributor
timestamp: December 12, 2024, 01:03:14 pm
content: 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 toNbytes 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).

author: brucehoult
author_rank: Super Contributor
timestamp: December 12, 2024, 01:16:29 pm
content: Quote from: Nominal Animal on December 12, 2024, 01:03:14 pmPeople 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 thetoString()or whatever function is automatically called on each argument.If you don't mind giving that up then the C++ overloadedoperator<<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.

author: Siwastaja
author_rank: Super Contributor
timestamp: December 12, 2024, 01:35:31 pm
content: Quote from: brucehoult on December 12, 2024, 01:16:29 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. I have been doing stuff like this for years:Code:print("Value of x is "); print_i32(x); print("\n");It's not that different from:Code: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);

author: brucehoult
author_rank: Super Contributor
timestamp: December 12, 2024, 02:03:37 pm
content: Quote from: Siwastaja on December 12, 2024, 01:35:31 pmNothing 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).

author: peter-h
author_rank: Super Contributor
timestamp: December 13, 2024, 05:49:12 am
content: QuotePresumably IPV4 (uint32_t) not IPV6 (some struct thing)I like that - very useful!

author: westfw
author_rank: Super Contributor
timestamp: December 13, 2024, 09:48:28 am
content: QuoteYep. 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.

author: Nominal Animal
author_rank: Super Contributor
timestamp: December 13, 2024, 11:08:23 am
content: If you have separate print functions for each type you use, then in C11 and later one can use preprocessor macros and_Genericto implement a variadicprint()macro that expands to individual function calls; i.e.print("x = ", x, ", y = ", y, "\n");expanding to sayprint_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:struct var_desc {void *const ref;const char *const name;int_fast8_t (*const get)(uint_fast8_t maxlen, char buf, void *ref);int_fast8_t (*const set)(uint_fast8_t maxlen, char buf, void *ref);};// Getters convert the referred to variable to a string into bufextern int_fast8_t vardesc_get_int(uint_fast8_t maxlen, char buf, void *ref);extern int_fast8_t vardesc_get_float(uint_fast8_t maxlen, char buf, void *ref);// Setters convert the string to the referred to variableextern int_fast8_t vardesc_set_int(uint_fast8_t maxlen, char buf, void *ref);extern int_fast8_t vardesc_set_float(uint_fast8_t maxlen, char buf, 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, thevardescsection 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_vardescat the beginning of the section, and__stop_vardescat the address just past the section, as is usual; and this section can/should reside in Flash.For example, you might havestatic 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_Nstatic 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.namemember 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.

author: SiliconWizard
author_rank: Super Contributor
timestamp: December 13, 2024, 11:20:29 am
content: Quote from: Nominal Animal on December 13, 2024, 11:08:23 amYes, 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".

author: cv007
author_rank: Frequent Contributor
timestamp: December 14, 2024, 01:18:14 am
content: If using C++, you can write a cout 'style' replacement without much trouble. This example lives in a single header-https://godbolt.org/z/rfrfYrsvzIn 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.

author: i509VCB
author_rank: Contributor
timestamp: December 16, 2024, 03:50:48 am
content: 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.

author: brucehoult
author_rank: Super Contributor
timestamp: December 16, 2024, 04:43:42 am
content: Quote from: i509VCB on December 16, 2024, 03:50:48 amI already suggested doing this kind of thing inhttps://www.eevblog.com/forum/microcontrollers/low-footprint-printf-replacement-for-embedded-dev/msg5742613/#msg5742613Has 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.

author: Nominal Animal
author_rank: Super Contributor
timestamp: December 16, 2024, 11:06:02 am
content: One important point to consider is the difference betweensnprintf()andprintf()/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 basewritten=write(buffer,length)functionality.The main difference is the buffer handling and partial writes.Forsnprintf()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.Forprintf()-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 callbeginsthe 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.)

author: SiliconWizard
author_rank: Super Contributor
timestamp: December 16, 2024, 09:49:35 pm
content: Quote from: Nominal Animal on December 16, 2024, 11:06:02 amYes, 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.

author: Nominal Animal
author_rank: Super Contributor
timestamp: December 17, 2024, 10:51:53 am
content: Quote from: brucehoult on December 12, 2024, 02:03:37 pmActually, 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.Quote from: SiliconWizard on December 16, 2024, 09:49:35 pmYes, 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.)

author: martinribelotta
author_rank: Regular Contributor
timestamp: December 17, 2024, 01:18:28 pm
content: A few years ago I develop a little deferred printf library for log system on flying embedded devices:https://github.com/martinribelotta/elogIs 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.

author: tellurium
author_rank: Frequent Contributor
timestamp: December 29, 2024, 09:09:58 am
content: Quote from: westfw on December 13, 2024, 09:48:28 amThis implementationhttps://github.com/cesanta/strexpands 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.