简体   繁体   中英

i2c byte write function, how works this code? I can´t understand complety

Can someone explain me how works this lines

template <class T>
...const T& value)...
.
.
. 
const uint8_t* p = (const uint8_t*)(const void*)&value;

on this code (i2c byte write for eeprom)

template <class T>
uint16_t writeObjectSimple(uint8_t i2cAddr, uint16_t addr, const T& value){

      const uint8_t* p = (const uint8_t*)(const void*)&value;    
      uint16_t i;
      for (i = 0; i < sizeof(value); i++){
            Wire.beginTransmission(i2cAddr);
                  Wire.write((uint16_t)(addr >> 8));  // MSB
                  Wire.write((uint16_t)(addr & 0xFF));// LSB
                  Wire.write(*p++);
            Wire.endTransmission();
            addr++;
            delay(5);  //max time for writing in 24LC256
      }
      return i;
}

template <class T>
uint16_t readObjectSimple(uint8_t i2cAddr, uint16_t addr, T& value){

            uint8_t* p = (uint8_t*)(void*)&value;
            uint8_t objSize = sizeof(value);
            uint16_t i;      
            for (i = 0; i < objSize; i++){
                  Wire.beginTransmission (i2cAddr);
                        Wire.write((uint16_t)(addr >> 8));  // MSB
                        Wire.write((uint16_t)(addr & 0xFF));// LSB
                  Wire.endTransmission();
                  Wire.requestFrom(i2cAddr, (uint8_t)1);         
                  if(Wire.available()){
                        *p++ = Wire.read();
                  }
                  addr++;
            }
            return i;
}

I think the lines works like pointers? I can't understand how the code store correctly each type of data when I do that

struct data{
    uint16_t yr;
    uint8_t mont;
    uint8_t dy;
    uint8_t hr;
    uint8_t mn;
    uint8_t ss; 
};
.
.
.
data myString;
writeObjectSimple(0x50,0,myString);

And then recover the values correctly using

data myStringRead;
readObjectSimple(0x50,0,myStringRead)

the function i2c byte write detect some special character between each data type to store in the correct place?

thx

First I have to state, that this code has been written by a person not fully familiar with the differences between how C++ and C deal with pointer types. My impression that this person has a strong C background and was simply trying to shut up a C++ compiler to throw warnings.

Let's break down what this line of code does

const uint8_t* p = (const uint8_t*)(const void*)&value;

The intent here is to take a buffer of an arbitrary type – which we don't even know here, because it's a template type – and treat it as if it were a buffer of unsigned 8 bit integers. The reason for that is, that later on the contents of this buffer are to be sent over a wire bit by bit (this is called "bit banging").

In C the way to do this would have been to write

const uint8_t* p = (const void*)&value;

This works, because in C is perfectly valid to assign a void* typed pointer to a non-void pointer and vice versa. The important rule set by the C language however is, that – technically – when you convert a void* pointer to a non-void type, then the void* pointer must have been obtained by taking the address ( & operator) of an object of the same type. In practice however implementations allow casting of a void* types pointer to any type that is alignment compatible to the original object and for most – but not all! – architectures uint8_t buffers may be aligned to any address.

However in C++ this back-and-forth assignment of void* pointers is not allowed implicitly. C++ requires an explicit cast (which is also why you can often see C++ programmers writing in C code something like struct foo *p = (struct foo*)malloc(…) ).

So what you'd write in C++ is

const uint8_t* p = (const uint8_t*)&value;

and that actually works and doesn't throw any warnings. However some static linter tools will frown upon it. So the first cast (you have to read casts from right to left) first discards the original typing by casting to void* to satisfy the linter, then the second cast casts to the target type to satisfy the compiler.

The proper C++ idiom however would have been to use a reinterpret_cast which most linters will also accept

const uint8_t* p = reinterpret_cast<const uint8_t*>(&value);

However all this casting still invokes implementation defined behavior and when it comes to bit banging you will be hit by endianess issues (the least).

Bit banging itself works by extracting each bit of a value one by one and tickling the wires that go in and out of a processor's port accordingly. The operators used here are >> to shift bits around and binary & to "select" particular bits.

So for example when you see a statement like

(v & (1<<x))

then what is does is checking if bit number x is set in the variable v . You can also mask whole subsets of the bits in a variable, by masking (= applying the binary & operator – not to be confused with the unary "address of" operator that yields a pointer).

Similarly you can use the | operator to "overlay" the bits of several variables onto each other. Combined with the shift operators you can use this to build the contents of a variable bit-by-bit (with the bits coming in from a port).

The target device is an I2C EEPROM, so the general form for writing is to send the destination address followed by some data. To read data from the EEPROM, you write the source address and then switch to a read mode to clock out the data.

First of all, the line:

const uint8_t* p = (const uint8_t*)(const void*)&value;

is simply taking the templated type T and casting away its type, and converting it to a byte array ( uint8_t* ). This pointer is used to advance one byte at a time through the memory containing value .

In the writeObjectSimple method, it first writes the 16-bit destination address (in big-endian format) followed by a data byte (where p is a data pointer into value ):

Wire.write(*p++);

This writes the current byte from value and moves the pointer along one byte. It repeats this for however many bytes are in the type of T . After writing each byte, the destination address is also incremented, and it repeats.

When you code:

data myString;
writeObjectSimple(0x50,0,myString);

the templated writeObjectSimple will be instantiated over the data type, and will write its contents (one byte at a time) starting at address 0, to the device with address 0x50. It uses sizeof(data) to know how many bytes to iterate over.

The read operation works very much the same way, but writes the source address and then requests a read (which is implicit in the LSB of the I2C address) and reads one byte at a time back from the device.

the function i2c byte write detect some special character between each data type to store in the correct place?

Not really, each transaction simply contains the address followed by the data.

[addr_hi] [addr_lo] [data]

Having explained all that, operating one byte at a time is a very inefficient way of achieving this. The device is a 24LC256, and this 24LC family of EEPROMs support sequential writes (up to a page) in size in a single I2C transaction. So you can easily send the entire data structure in one transfer, and avoid having to retransmit the address (2 bytes for every byte of data). Have a look in the datasheet for the full details.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM