String/sprintf alternative specifically for Arduino.

I have finished a major overhaul, with far more possibilities.

I have created a generic printing class called NonStreamingIO and combined with the Arduino core provided 'Printable' Class, the eeprom code ( EString ) functions the same as the GString, except prints to ROM.

The Printable class allows printing strings directly into the print class. There is still more to go, but here is what the builtin version of GString and EString will look like, the printf functionality is builtin to Print now.

These allow things like:

EString e( 0, 10 ); //Set string from 0 to 0 + 10, or 11 characters.

//Send current string from EEPROM over serial.
Serial.print( e );

//Write a float to EEPROM.
e.print( 123.456f, 3 );

That code above is how the Print library and sprintf will deal with EEPROM and SRAM reading writing ( printing out / printing to ).

This is only a preliminary attempt, but here is the SRAMWriterand EEPROMWriter, which are typedef'd to GString and EString.

#ifndef HEADER_PRINTER
  #define HEADER_PRINTER
  
  class NonStreamingIO : public Print{
    public:
      size_t count( void ) const { return End - Start; }
    
      virtual size_t write(uint8_t) = 0;
    
      template< typename T > NonStreamingIO &operator +=( const T &t )
        {
          print( t );
          return *this;
        }  

      template< typename T > NonStreamingIO &concat( const T &t )
        {
          print( t );
          return *this;
        }
        
      template< typename T > NonStreamingIO &concat( const T &t, const int i )
        {
          print( t, i );
          return *this;
        }    

      template< typename T > inline void assign( const T *u_DataPtr, const uint16_t u_Length )
        {
          Start = ( uint8_t* ) u_DataPtr;
          End = Start + u_Length;
        }
    protected:
      NonStreamingIO( uint8_t *u_DataPtr ) : Start( u_DataPtr ) , End( Start ) 
        { return; }  
        
      NonStreamingIO( uint8_t *u_DataPtr, const unsigned int u_Length ) : Start( u_DataPtr ) , End( Start + u_Length ) 
        { return; }          
        
      uint8_t  *Start;
      uint8_t *End;        
    private:
  };
  
  #include <avr/eeprom.h>
  class EEPROMWriter : public NonStreamingIO, public Printable{
    public:
      template< typename T > EEPROMWriter( const T *t_DataPtr ) : NonStreamingIO( ( uint8_t* ) t_DataPtr ), Printable() 
          { return; }
      template< typename T > EEPROMWriter( const T *t_DataPtr, const unsigned int u_Length  ) : NonStreamingIO( ( uint8_t* ) t_DataPtr, u_Length ), Printable() 
          { return; }  

      size_t write( uint8_t u_Data )
        { 
          if( eeprom_read_byte( End ) != u_Data )
            eeprom_write_byte( End, u_Data );
          ++End;
          return 0x01; 
        }
    protected:
      friend Print;
      
      size_t printTo(Print& p) const  
        {
          uint8_t  *u_Cursor = Start;
          while( u_Cursor <= End ) p.write( ( uint8_t ) eeprom_read_byte( u_Cursor++ ) );
          return count();
        }
    private:
  };
  
  class SRAMWriter : public NonStreamingIO{
    public:
      template< typename T > SRAMWriter( const T *t_DataPtr ) : NonStreamingIO( ( uint8_t* ) t_DataPtr ) 
        { return; }
      template< typename T > SRAMWriter( const T *t_DataPtr, const unsigned int u_Length  ) : NonStreamingIO( ( uint8_t* ) t_DataPtr, u_Length ) 
          { return; }  
          
      operator char*( void ) 
        { 
          *End = '\0';
          return ( char* ) Start;
        }  

      void end( void ) { *End++ = '\0'; }  

      size_t write( uint8_t u_Data )
        { 
          *End++ = u_Data;
          return 0x01; 
        }      
    protected:
    private:
  };
  
  typedef SRAMWriter GString;
  typedef EEPROMWriter EString;
    
#endif

I'll have to post the full patch later on tonight after work.

Just a quick question for all interested.

For EEPROM & PROGMEM control via printf, would you prefer either

  • Passing the address as a parameter to printf.
  • Have the address as part of the format string.

There are pros & cons to both sides, I have used the parameter approach for now.
Both methods could be written to allow formatting ( printf just prints the stored strings for now ).

Reading from EEPROM I have posted a few lines before using the %r with some modifiers.

addr = 12;
g.sprintf("%d : %r ", addr, addr); // prints 12 : 75 (assuming the value 75 is in location 12)

Writing to EEPROM, -> passing the address as a parameter. Similar to

The format specifier can also contain sub-specifiers: flags, width, .precision and modifiers (in that order), which are optional and follow these specifications:
width description
* The width is not specified in the format string, but as an additional integer value argument preceding the argument that has to be formatted.
see - http://www.cplusplus.com/reference/cstdio/printf/ -

Another alternative might be that you create a class EELOC (== EEPROMlocation) which derives from print, to be able to print to EEPROM.

EELOC store = 15;
store.printf("a %d ", 10);

could be a 'prototype'for other external storage classes e.g. I2C EEPROM

In a previous post here. I have an EEPROMWriter class, that can be used to print the data to eeprom and is passable to print so you can write the eeprom string out to another print, like serial.

EDIT, thanks for your printf input. I agree, the parameter method is best. I didn't intend to write to eeprom from it, just read. Which is what my EEPROMWriter class was originally for.

In addition to the code I linked to in my last post, here is the PROGMEMReader class.

Usage.

char c0[] PROGMEM = "Test string one.";
char c1[] PROGMEM = "Test string two.";

void setup(){
  
  PString p = c0;
  
  Serial.begin(115200);
  Serial.println( "-----------------------" );
  Serial.println( p );
  Serial.println( p = c1 );
  Serial.println( "-----------------------" );
}

void loop(){}

Output.


Test string one.
Test string two.

Class.

	class PROGMEMReader : public Printable{
		public:
			template< typename T > 
				PROGMEMReader( T *t_DataPtr ) : Start( ( uint8_t* ) t_DataPtr ) 
					{ return; }
					
			PROGMEMReader &operator =( uint8_t *t )			
				{ 
					Start = t;
					return *this; 
				}				
		protected:

			friend class Print;
			size_t printTo(Print& p) const	
				{
					uint8_t *u_Cursor = Start;
					size_t s_Return = 0;
					
					while( true ){
						unsigned char u_Current = pgm_read_byte( u_Cursor++ );
						if ( u_Current == 0 ) break;
						s_Return += p.write( u_Current );
					}
					return s_Return;				
				}
				
		private:
			uint8_t *Start;
	};

typedef PROGMEMReader PString;

This is included in the code I will release.

Woo Hoo. My test worked perfectly.

It writes a PROGMEM string into EEPROM then prints the EEPROM text over serial.
This code is also safe, the EString won't overwrite unchanged data, so it only writes the string once.

Code.

char c0[] PROGMEM = "Test string one.";

void setup(){
  
  PString p = c0;
  EString e( 0 );
  
  Serial.begin(115200);
  e.print( p );
  Serial.println( "-----------------------" );
  Serial.println( e );
  Serial.println( "-----------------------" );
}
void loop(){}

Output.


Test string one.

1

I have finished adding the last few bits to sprintf/printf. Here is an example of the latest features.

This expects the EEPROM value written in my previous post to still be valid.

char c0[] PROGMEM = "Test string two.";

void setup(){
  Serial.begin(115200);
  Serial.printf( "%30n\nEEPROM: %16r\nPROGMEM: %p\n%30n", '=', 0, c0, '=' );
}
void loop(){}

Output.

==============================
EEPROM: Test string one.
PROGMEM: Test string two.

I'll have the code ready shortly.

impressive, major step forwards!

how much does the final version add to footprint compared to regular print?

The s/printf function is large due to it including support for float & decimal conversions, PROGMEM, EEPROM.
You will see its benefit in apps where you already use these features, so no more code is added, just the sprintf function code.

These small examples are not a good test, I'll post a sketch testing all the string functionality where you can see the size drop just from my underlying framework, then converting the blocks into printf calls makes for even smaller code.

The three classes GString, EString, and PString are designed for use with print and println calls.

As a control:

  Serial.begin(115200);
  Serial.println( "Hi" );

1,926 bytes.

Testing printf:

  Serial.begin(115200);
  Serial.printf( "%30n\nEEPROM: %16r\nPROGMEM: %p\n%30n", '=', 0, c0, '=' );

5,318 bytes.

The 'small' app equivalent of the printf line above:

  EString e( 0, 16 );
  PString p = c0;  
  Serial.begin(115200);
  Serial.repeat( '=', 30 );
  Serial.print( "\nEEPROM: " );
  Serial.print( e );
  Serial.print( "\nPROGMEM: " );
  Serial.println( p );  
  Serial.repeat( '=', 30 );

2,492 bytes.

Each feature not found in the Arduino standard 'sprintf' can be disabled by defining certain macros.
As you can see, the last version is only half a kb more than the control. Its the float and decimal conversions that are hurting the code.

I have a few other things I'm testing, but they aren't included in my patch, yet.

I forgot this version, which also compiles to 2492 bytes.

  EString e( 0, 16 );
  PString p = c0;    
  Serial.begin(115200);
  Serial.repeat( '=', 30 );
  Serial.concat( "\nEEPROM: " ).concat( e ).concat( "\nPROGMEM: " ).concatln( p );
  Serial.repeat( '=', 30 );

Okay, here is the patch.

It adds in:

  • Print::repeat()
  • Print::concat()
  • Print::concatln()
  • Print::printf()
  • sprintf
  • ftoa
  • EString: EEPROM printable class.
  • PString: PROGMEM printable class.
  • GString: SRAM printable class.
  • NonStreamingIO: Inheritable Print extension.

Documentation will be available soon.

There are two files to download. But there are some steps required to install them properly.

Step 1.

Download files to the core folder where Print.h & Print.cpp are located.
On my machine, I have arduino in my D:\ so the path is here: 'D:\arduino-1.0.5\hardware\arduino\cores\arduino'
If you use the beta 1.5.X, you will have to find the AVR core folder, I don't have it but maybe someone can post the directoy.

Step 2.

Open Print.h.
Scroll to the very bottom.
Before the closing brace of the Print class, add the line: #include "Print_Ext.h"

Here is the last 10 lines of Print.h with the mod:

size_t println(long, int = DEC);
size_t println(unsigned long, int = DEC);
size_t println(double, int = 2);
size_t println(const Printable&);
size_t println(void);

#include "Print_Ext.h"
};

#endif

Save changes and close.

Step 3.

Open Print.cpp
Underneath the line '#include "Print.h"' add the line: #include "Print_Ext.source"

Here is the first 10 lines ( after comments ) of code in Print.h with the mod:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <math.h>
#include "Arduino.h"

#include "Print.h"
#include "Print_Ext.source"

// Public Methods //////////////////////////////////////////////////////////////

Save changes and close.

Step 4. ( optional, otherwise PROGMEM code is doubled up. )

In Print.cpp find the function:

size_t Print::print(const __FlashStringHelper *ifsh)
{
  const char PROGMEM *p = (const char PROGMEM *)ifsh;
  size_t n = 0;
  while (1) {
    unsigned char c = pgm_read_byte(p++);
    if (c == 0) break;
    n += write(c);
  }
  return n;
}

Comment it out and replace it with this:

size_t Print::print(const __FlashStringHelper *ifsh)
{
  return print( PString( ifsh ) );
}

Hopefully thats it.

Print_Ext.h (7.96 KB)

Print_Ext.source (11.1 KB)

I have just uploaded a copy of the code to google, so anybody not logged in can try the library.

The file is PrintPatch_001.zip.
https://code.google.com/p/arduino-extensions/downloads/detail?name=PrintPatch_001.zip

Inside the zip are the two files attached to the post above. So the installation instructions are the same.

Thank You, the modifications were basically some copy and paste stuff. I'm assuming that when I install Ver 1.05 (Enried's) Will I need to copy the files to all the Arduino installations?. Somehow I think that's a dumb question... ?

Doc

Docedison:
Will I need to copy the files to all the Arduino installations?.

Not a dumb question, for the printf functionality, yes you will need to install it into the core.

I'm going to do up a new GString.h which will include all the functionality except the built in Print::printf ( not possible without inclusion into the core ).

The string sprintf, GString, PString & EString will all be available for reading and writing where applicable using printf.

I'm going to make the new include Due compatible as well. Maybe I can modify the printf functionality so it can pipe the output down any Print enabled class, so you can use something like this ( only an idea ):

//In setup.
SetPrintTarget( Serial );

//Later on in code.
printf( "Test number: %d", 1234 ); //Gets spit down serial.

This is a very viable option which I'll implement soon, I however do prefer the object orientated approach ( Serial.printf(); ),
whereas this method may help the c-style programmers feel more at home.

However the built in methods ( can have both ), can be tailored to use the smallest footprint. One example is the String library, it uses itoa, ltoa and such. These can be rewritten to use GString, so there is potentially less code to compile, and the String lib uses them automatically. Even memory handling can rewritten using the GStirng.

Here is a small selection of re-implementations for:

  • memset
  • memcpy
  • strcpy
  • strcat
  • ftoa
  • itoa
  • utoa
  • ltoa
  • ultoa
	inline void *memset( void *ptr, int value, size_t num )
		{
			GString( ptr ).repeat( value, num );
			return ptr;
		}
		
	inline void *memcpy( void * destination, const void * source, size_t num )
		{
			GString( destination ).write( ( uint8_t* ) source, num );
			return destination;
		}	
		
	inline char *strcpy( char *destination, const char *source )
		{
			GString( destination ).print( source );
			return destination;
		}

	inline char *strcat( char *destination, const char *source )
		{
			GString( strchr( destination, 0x00 ) ).print( source );
			return destination;
		}		
		
	inline char *ftoa( float value, char * str )
		{
			GString( str ).print( value );
			return str;
		}

	inline char *itoa( int value, char * str, int base )
		{
			GString( str ).print( ( long ) value, base );
			return str;
		}
		
	inline char *utoa( unsigned int value, char * str, int base )
		{
			GString( str ).print( ( unsigned long ) value, base );
			return str;
		}		
		
	inline char *ltoa( long value, char * str, int base )
		{
			GString( str ).print( value, base );
			return str;
		}
		
	inline char *ultoa( unsigned long value, char * str, int base )
		{
			GString( str ).print( value, base );
			return str;
		}

sprintf seems quite handy :slight_smile:

Is there a way to format a float like having 4 decimals? Now it seems that sprintf(buf, "insert into math (pi) value (%f);", pi); gives 2 decimals like 3,14. Where in earth the 2 decimals is written so I could tweak it if it can not be given in format?

Check - http://www.cplusplus.com/reference/cstdio/printf/ - for all details of (s)printf formatting.

Nice try, but no banana :smiley:
Please walk me through this...

#include <GString.h>
...
    char buff[64];
    sprintf(buff, "Pi %f.", 3.14159);
    Serial.println(buff);
    sprintf(buff, "Pi %e.", 3.14159);
    Serial.println(buff);
    sprintf(buff, "Pi %7.4f.", 3.14159);
    Serial.println(buff);
    sprintf(buff, "Pi %7f.", 3.14159);
    Serial.println(buff);
    sprintf(buff, "Pi %07f.", 3.14159);
    Serial.println(buff);
    sprintf (buff, "floats: %4.2f %+.0e %E", 3.1416, 3.1416, 3.1416);
    Serial.print("expected 'floats: 3.14 +3e+000 3.141600E+000', but we get '");
    Serial.print(buff);
    Serial.println("' instead.");
Pi 3.14.
Pi .
Pi        4f.
Pi    3.14.
Pi 0003.14.
expected 'floats: 3.14 +3e+000 3.141600E+000', but we get 'floats:     2f .0e ' instead.

you can also check the experimental code for the core print lib found in these two threads