Can someone explain EEPROM.put()/.get() to me?

I have a query. I am using I2C eeprom chips en-masse and I would like to write my own functions for put() and get().

Example:

Currently I have

void I2cEeprom::put( uint16 eeAddress, Event *event ) 
{
    uint8 *ptr ;
    ptr = &event->data1 ;

    Wire.beginTransmission( i2cAddress ) ;
    Wire.write( highByte(eeAddress) ) ;
    Wire.write( lowByte(eeAddress) ) ;

    for( int i = 0 ; i < sizeof(Event) ; i ++ )
    {
        uint8 val = *ptr ;
        Wire.write( val ) ;
        ptr ++ ;
    }
    uint8 state = Wire.endTransmission() ;
}

But as you can see, it is taylored to be used with my own Event object. And I would like it to be more compatible with the arduino style. This helps me move from parts from built-in eeprom to i2c eeprom in source code.

I looked up the source code from arduino for the eeprom usage. But this C++ syntax is beyond me.

struct EEPROMClass{
    
    //Functionality to 'get' and 'put' objects to and from EEPROM.
    template< typename T > T &get( int idx, T &t ){
        EEPtr e = idx;
        uint8_t *ptr = (uint8_t*) &t;
        for( int count = sizeof(T) ; count ; --count, ++e )  *ptr++ = *e;
        return t;
    }
    
    template< typename T > const T &put( int idx, const T &t ){
        EEPtr e = idx;
        const uint8_t *ptr = (const uint8_t*) &t;
        for( int count = sizeof(T) ; count ; --count, ++e )  (*e).update( *ptr++ );
        return t;
    }
};

I am not familiair with template < ... >. I understand that these templates exist to handle the different parameter datatypes.

But how do I use template in my own put() method?

Kind regards,

Bas

Maybe this short intro to templates will help:

Arduino, C++, arrays and templates | by Jay Proulx | Medium

For those who may not be able to reach the link, I attached a copy:

Arduino Array Template (Jay_Proulx).pdf (916.5 KB)

It feels like you would be most of the way there if you copy the Arduino source and just splice your wire stuff around it - the for loop is where the work is done, just tweak it a little. You can probably take the template code as is and ignore it.

FYI:
This does not do the same as the EEPROM.put() method. The EEPROM.put() method checks if the byte value that you want to write is the same as what is already stored and will only write if the stored byte value is different from the byte value.

Something like this could work for your needs?

I would start by making my method declaration look like the built-in library method:

template< typename T > void I2cEeprom::put( uint16 eeAddress, const T &t ) {

Then modify the body of the method for the new declaration:

template< typename T > void I2cEeprom::put(  uint16 eeAddress, const T &t ) 
{
  uint8_t *ptr = (uint8_t *) &t;
  // NOTE: You will have to implement '.update()' in
  // your class to read the byte and write the new
  // value only if it is different from the current value.
  for( int count = sizeof(T) ; count ; --count, ++e )  
    update(eeaddress++, *ptr++ );
}
1 Like

Yes, off course.
My sketch it's just for quick online testing.

Tnx for the pointers. Most helpful! I am going to tinker and build me some new code.

I have one side question. Never ever have I seen a , in a for loop. How does this work?

for( int count = sizeof(T) ; count ; --count, ++e ) 

I suppose you can iterate with more than one iterating variable??

Kind regards,

Bas

EDIT:
Got me another one. What is the purpose of const in this example?

template< typename T >
uint8_t I2cEeprom::put( uint16_t eeAddress, const T &data )
{
    const uint8_t *ptr = (const uint8_t*) &data;

    Wire.beginTransmission( i2cAddress ) ;
    Wire.write( highByte(eeAddress) ) ;
    Wire.write( lowByte(eeAddress) ) ;

    for ( int i = 0 ; i < sizeof(data) ; i++ )
    {
        uint8_t val = *ptr++ ;
        Wire.write( val ) ;
    }
    return Wire.endTransmission() ;
}

It's a "comma expression". The comma is a very low priority operator that evaluates the left-hand side and the right-hand side and returns the value of the left-hand side. It is used when you want to put two expressions in place of one.

It's a promise to the caller that the function won't write into the variable being passed. It allows the function to be called with 'const' values. Without it you would get an error if you passed a const expression.

Once you promise not to change the variable you can't cast the const argument as a non-const value because that would make it too easy to break the promise.

EVERY read-only argument for every function should be declared 'const' so they can accept 'const' values.

Ok I got me an arm wrestle with the compiler/linker. That is why I sometime postpone things until the last possible moment :smiling_face_with_tear:

I made source file:
( I do not make use of the update method, I know what it does, I simply do not need it)

#include "i2cEeprom.h" 
#include <Wire.h>

I2cEeprom::I2cEeprom() {}

void I2cEeprom::begin( uint8 _i2cAddress )
{
    i2cAddress = _i2cAddress ;
}

template< typename T >
void I2cEeprom::get( uint16_t eeAddress, const T &data ) // will be uint8_t
{
    const uint8_t *ptr = (const uint8_t*) &data;

    Wire.beginTransmission( i2cAddress ) ;
    Wire.write( highByte(eeAddress) ) ;
    Wire.write(  lowByte(eeAddress) ) ;
    uint8 state = Wire.endTransmission() ;

    Wire.requestFrom( i2cAddress, sizeof( data ) ) ;
    for( int i = 0 ; i < sizeof(data) ; i ++ )
    {
        uint8 val = Wire.read() ; 
        *ptr = val ; 
        ptr ++ ;
    }

    /*return state ;*/
}

template< typename T >
void I2cEeprom::put( uint16_t eeAddress, const T &data ) // will be uint8_t
{
    const uint8_t *ptr = (const uint8_t*) &data;

    Wire.beginTransmission( i2cAddress ) ;
    Wire.write( highByte(eeAddress) ) ;
    Wire.write( lowByte(eeAddress) ) ;

    for ( int i = 0 ; i < sizeof(data) ; i++ )
    {
        uint8_t val = *ptr++ ;
        Wire.write( val ) ;
    }
    /*return*/ Wire.endTransmission() ;
}

And a header file

#include <Arduino.h>
#include "macros.h"

class I2cEeprom
{
public:
    I2cEeprom() ; 
    void begin( uint8 ) ;

	template< typename T >  
    void put( uint16, const T &data ) ;    // am I correct protoype?

	template< typename T > 
    void get( uint16, const T &data ) ;

private:
    uint8   i2cAddress ;

} ;

This seems to compile. But than..... I made a library with which I can record and replay user input and this makes use of the i2c eeprom.

The header file declares a private i2cEeprom object

#include <Arduino.h>
#include "macros.h"
#include "i2cEeprom.h"  // library included!

class EventHandler
{
public:
    EventHandler( uint32, uint32        ) ;
    EventHandler( uint32, uint32, uint8 ) ; // for I2C_EEPROM

    void    startRecording() ;      // need begin function? to init I2c bus??
    void    stopRecording() ;
    void    startPlaying() ;
    void    stopPlaying() ;
    void    resetProgram() ;
    void    sendFeedbackEvent( uint16 ) ;
    void    update() ;
    void    begin() ;
    void    storeEvent( uint8, uint16, uint8 ) ;
    uint8   getState() ;

private:
    Event       event ;
    Event       getEvent() ;
    I2cEeprom   i2cEeprom ;    //   <--  eyes here

    uint32  beginAddress ;  // I have typedefs for this with the missing _t, in case you are wondering
    uint8   i2cAddress ;
    uint32  eeAddress ;
    uint32  maxAddress ;
    uint32  prevTime ;
    uint16  newSensor ;
    uint8   recordingDevice ;
    uint8   eepromType ;
};

In the source files of me event handler I have 2 methods which make use of the i2c eeprom;

Event EventHandler::getEvent()
{
    Event localEvent ;

    if( eepromType == INTERNAL_EEPROM )   EEPROM.get( eeAddress, localEvent ) ;
    else                               i2cEeprom.get( eeAddress, localEvent ) ;  //  <-- I give problems

    if( displayGetMemory ) displayGetMemory( eeAddress ) ;

    eeAddress += sizeof( localEvent ) ;            // increase EEPROM address for next event ;

    return localEvent ;
}

// and I also have 
i2cEeprom.put( eeAddress, localEvent ) ;  // also gives problem
// elsewhere

The compiler/linker's complaints:

path\sketch\src/event.cpp:117: undefined reference to `void I2cEeprom::put<Event>(unsigned int, Event const&)'
.ltrans0.ltrans.o: In function `EventHandler::getEvent()':
path..\sketch\src/event.cpp:56: undefined reference to `void I2cEeprom::get<Event>(unsigned int, Event const&)'

2 times the 'undefined reference'. I know what it means but I do not understand why I am having this message.

I have include the library. I obviously done something wrong. Are the method protypes of i2cEeprom.h correct?

Kind regards,

Bas

For templates to work the compiler needs access to the complete definition/implementation of the function(s) in order to instantiate/create them. With your current implementation you haven't provided the actual implementation of the functions, the compiler will then simply assume that they are provided elsewhere in your source. Since you don't have any template specialization for the type Event anywhere in your source, you will get this undefined reference error.

A code example will make it more clear. Consider the following (alert: this is bad code, its just for the sake of describing the problem);

#include <iostream>

// In your header file
template<typename T>
T sum(T a, T b);

int main()
{
    std::cout << sum(int(10), int(10));
}

This will give you the following error:

/tmp/ccppEJmY.o: In function `main':
main.cpp:(.text.startup+0xf): undefined reference to `int sum<int>(int, int)'
collect2: error: ld returned 1 exit status

Now you could fix it by explicitly defining (read: specializing) your template for the type int since that is what your pass to the function when called in main.

#include <iostream>

// In your header file
template<typename T>
T sum(T a, T b);

// In your source file, a specialization of function sum for type `int`
template<>
int sum<int>(int a, int b) { return a + b; }

int main()
{
    std::cout << sum(int(10), int(10));
}

With this, everything compiles just fine, since there exists a specialized version with the actual implementation of the sum function where type T is int. However, if we would now change the call in main to use a double type, e.g.:

// ...
std::cout << sum(double(3.14), double(2.0));

We have the same undefined reference error again, there is no specialization available of the sum function where type T is double. Now you could provide another specialization for a double in your source file, but this completely defeats the purpose of using templates. That is, making generic code, implement once, and use it for many different types. If you are going to do it like I showed above, you could've just removed the whole template garbage and just provide two function overloads of sum where one accepts int and one accepts double , e.g.:

int sum(int a, int b) { return a + b; }
double sum(double a, double b) { return a + b; }

Then how to implement this properly? Simply provide the actual implementation of the sum function in the template definition:

#include <iostream>

// In your header file
template<typename T>
T sum(T a, T b) { return a + b; }

int main()
{
    std::cout << sum(int(10), int(10));
    std::cout << sum(double(3.14), double(2.0));
}

With this implementation the compiler has access to the function implementation (e.g. the body of the function) the moment we make two calls in main. Because of this the compiler is able to instantiate two sum functions; one where the type is int and one where the type is double. So general rule of thumb, if you have a template function in your header file, you need to provide the definition of that function in the header file as well.


Back to your code, to fix it simply copy the function bodies you have in your source file for put and get over to your header file and it should compile. I would personally do it a bit different, because if you copy the contents of those two functions to the header file, it means you will also have to include Wire.h in your own I2cEeprom header. This is just a bit ugly since everyone who will include your I2cEeprom header file will get a free and unwanted include of Wire.h, not so nice IMHO.

My suggestion would be something as follows (it compiles, but is not tested):

#include <cstdint>
// In your header file
class I2cEeprom
{
public:
    template<typename T>  
    uint16_t put(uint16_t addr, T const & data)
    {
        put_n(addr, &data, sizeof(data));
        return sizeof(data);
    }

    template<typename T> 
    uint16_t get(uint16_t addr, T & data) // Note that I removed `const` here, const is immutable, since we need to write bytes to it, don't make it `const`
    {
        get_n(addr, &data, sizeof(data));
        return sizeof(data);
    }

private: 
    void put_n(uint16_t addr, void const * src, int size);
    void get_n(uint16_t addr, void * src, int size);
};

// In your source file
// #include <Wire.h>

void I2cEeprom::put_n(uint16_t addr, void const * src, int size)
{
    uint8_t const * data = reinterpret_cast<uint8_t const *>(src);
    // Copy `size` bytes from `data` into eeprom
}

void I2cEeprom::get_n(uint16_t addr, void * src, int size)
{
    uint8_t * data = reinterpret_cast<uint8_t *>(src);
    // Copy `size` bytes from eeprom into `data`
}

struct Foo
{
    int a;
    int b;
};

int main()
{
    I2cEeprom eeprom;
    
    double val1 = 10.0;
    int val2 = 3;
    Foo val3 = {10, 20};
    
    // Write to eeprom
    {
        uint16_t address = 0;
        address += eeprom.put(address, val1);
        address += eeprom.put(address, val2);
        address += eeprom.put(address, val3);
    }
    
    // Read from eeprom
    {
        uint16_t address = 0;
        address += eeprom.get(address, val1);
        address += eeprom.get(address, val2);
        address += eeprom.get(address, val3);
    }
}

This would still give you the benefit of passing in different types, e.g. an int, a double or a struct Foo, but you won't pollute your header file with the implementation of writing things to eeprom which requires the inclusion of Wire.h. Why is this better than not just publicly exposing put_n and get_n? Well, then you burden the user with providing the correct size for the type, he/she can make mistakes doing that. With these helper template functions you don't have to worry about that anymore.

Obviously users can still pass in types that are not trivially copyable (e.g. types that can't just be copied with memcpy or alike). I'm not entirely sure if the compiler that comes with Arduino support it, but something like these static asserts can work. If you pass in an argument with a type that is not copyable, the code won't compile.

// See: https://coliru.stacked-crooked.com/a/c4d8404b96805ed9 for a full example
// ...
#include <type_traits>

// ...
template<typename T> 
uint16_t put(uint16_t addr, T const & data)
{
    static_assert(std::is_trivially_copyable_v<T>);
    // ...
}

template<typename T> 
uint16_t get(uint16_t addr, T & data)
{
    static_assert(std::is_trivially_copyable_v<T>);
    // ...
}

Can't see what is wrong with casting the pointer to some object to byte *, and using sizeof(). If you want to make it more generic without actually using generics syntax (the <...>) then perhaps let you function accept a byte *buf plus a size, and write that to the EEPROM.

However, your code looks a bit odd. You get an Event *event, then set ptr to &event->data. Is the data object also an Event? If not, the loop over 0 to sizeof(Event) may be wrong.

sizeof (byte *) == 2
(or 4 on a 32-bit processor)

The size of the pointer doesn't really matter? But I should have said uint8_t ...

The sizeof was of the data, not the pointer, obviously.

The OP was concerned with creating code more compatible with "Arduino-style", which i read as making it more generic. I think the following is quite generic, at low level, C style.

void writeToEEPROM (uint8_t *buf, int count) { 
	...
}

Then somewhere else, in a specific application dealing with specific data, of course you write

void saveEvent (Event e) {
	writeToEEPROM ((uint8_t *) &e, sizeof(Event));
}

@ LimpSquid

Thank you very much for your elaborate answer. It took me a few evenings, but the put() n get() functions are now working and I can deploy my i2cEeprom files to different projects now.

I have learned about templates and static_assert. And I vaguely get what

 reinterpret_cast<uint8_t const *>

does. I believe you create a new 8-bit pointer 'data' and cast the void pointer 'src' to an 8-bit pointer to handle the different data types, correct?

Kind regards,

Bas

Glad to hear :slight_smile:.


You can think of a reinterpret_cast as a literal cast, i.e. you instruct the compiler to interpret the type of the variable between the parenthesis to be of the type between the angled brackets. Since the compilers will not warn you about unsafe casts you should use this cast with caution, a small example to explain why it's considered to be unsafe:

int main()
{
    int foo = 123;
    
    // Saved by the compiler, casting from `int *` to `double *`, which is no bueno!
    // A static_cast is similar to doing this: `double * bli(&foo);`
    double * bli = static_cast<double *>(&foo); 
    
    // The compiler will allow this cast, but it's actually not correct
    // since we "can't" cast an `int *` to `double *`. 
    double * bla = reinterpret_cast<double *>(&foo);
    
    // Avoid unused variable warning
    (void)(bli);
    (void)(bla);
    return 0;
}

If you were really pedantic you would cast a void * to unsigned char * with the use of a static_cast, since a void pointer could legitimately originate from an unsigned char pointer, hence such expression is considered OK by the compiler. My reasoning of using a reinterpret_cast over static_cast, is to alert myself or other people that are using my code to pay special attention to this section since incorrect changes can lead to undefined behavior.

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.