Close

I2C

A project log for Ryobi 40V Battery investigations

Exploring the innards of the battery

donDon 08/11/2024 at 15:412 Comments

Soldered on to the SDA and SCL pins of U6. File uploaded.

I uploaded the raw data files and I'm having trouble converting the values. See the i2c data.ods (libreoffice calc document). I've split out the commands and data returned. However, I'm not familar with the nomenclature used in the Datasheet with regards to '2'complement' and 'atomic value'.


Discussions

jonfoster wrote 08/14/2024 at 11:28 point

=== "atomic" ===

TLDR: You have to read the 16-bit register in a single I2C read, you can't read the low byte and the high byte in separate I2C reads.

Long version:

An "atomic" thing happens all at once.  It's used when you have multi-thread or multi-processor systems, where multiple things can happen at the same time.

In this case, there is an analog to digital converter (ADC) that will occasionally read the battery voltage and store it in a 16-bit register on the controller chip.  In parallel, you are reading from that register.

You could read just the low byte of that register, and then read just the high byte of that register, but that would be a very bad idea.  Because you might read the low byte, then the ADC goes and updates the value, then you read the high byte of the new value.  Using a decimal example to make this clearer, if the battery voltage changed from "0099" to "0100" then you might first read the low byte "99" then the high byte "01" giving you "0199" which is completely wrong!

To avoid that problem, the chip designer gave you a way to read both the high byte and the low byte in a single I2C read.  The chip designer has designed the chip so that if the ADC tries to update the register at the same time, then you will either read both bytes from the old register value, or both bytes from the new register value.  You are guaranteed NOT to read one byte from the old register value and one byte from the new value.

The short way of writing that is: in a single I2C transaction, reading from the 16-bit register is atomic.

=== "2s complement" ===

Computers have a standard way of representing signed integers.  That's called "2's complement".

--- Decoding it in code, the easy way ---

Since this is the standard form computers use, it's easy to parse 8-bit, 16-bit, 32-bit, and 64-bit values.  For example, for a 16-bit value, in C it's just a cast:

    // Get the bytes we need to decode, somehow.

    uint8_t lsb = ... whatever ...;

    uint8_t msb = ... whatever ...;

    // Convert two bytes to a 16-bit unsigned value, in the usual way.

    uint16_t unsigned_value = (uint16_t)((((uint16_t)msb << 8u) | lsb));

    // Convert unsigned temporary to the real, signed value, using 2's complement rules.

    int16_t value = (int16_t)unsigned_value;

(Note that technically, the C standard does not guarantee the above code works.  It's "implementation-defined", which means the compiler vendor has to tell you how they chose to implement it.  However, in practise, every compiler vendor defines it in such a way that the above code will work.  E.g. GCC does that here: https://gcc.gnu.org/onlinedocs/gcc/Integers-implementation.html )

In Python, it's just struct.unpack(">h", data).  (Or "<h" if you have a little-endian byte array).  Other languages will have equivalents.

--- Decoding it manually ---

For fields that aren't a round number of bits, or if you're decoding it in a spreadsheet, or manually, then:

1. In this explanation I'm going to use NUM_VALUES to mean 2^N, where N is the number of bits in the field.  For example, for an 8-bit field NUM_VALUES is 256, for a 16-bit field NUM_VALUES is 65536, and for a 3-bit field NUM_VALUES is 8.

2. First decode the field as if it was an unsigned field, in the usual way.

3. If the unsigned value is strictly less than (NUM_VALUES / 2), then use that unsigned value as the result.

4. Otherwise, take the unsigned value and subtract NUM_VALUES.  This gives you a negative result.

(The word "strictly" in "strictly less", is clarifying what happens if the numbers are equal.  It means to use "<" in your code, not "<=".  So 127 is "strictly less" than 128, but 128 is NOT "strictly less" than 128.  So if you have an 8-bit field, then the unsigned value 128 decodes to -128, not +128).

For example, with a 8-bit field, unsigned values 0 through 127 inclusive decode to themselves.  Value 128 decodes to -128, value 129 decodes to -127, value 254 decodes to -2, 255 decodes to -1.

Another example with a 3-bit field: 0 is 0, 1 is 1, 2 is 2, 3 is 3, 4 is -4, 5 is -3, 6 is -2, 7 is -1.

Hope this helps!

  Are you sure? yes | no

jonfoster wrote 08/14/2024 at 15:21 point

To add, in case anyone else finds this and tries to use it to write code: There's another way to decode smaller 2's complement fields in code, which can be faster.  You shift it to the top bits of a variable, and then do a signed right shift to get the bits back to the bottom of that variable.  E.g for a 3-bit field, in C:

    uint8_t unsigned_value = ... whatever ...;

    uint32_t temp = (((uint32_t)unsigned_value) << (32 - 3))

    int32_t signed_value = (((int32_t)temp) >> (32 - 3));

    int8_t result = (int8_t)signed_value;  // Optional

You can replace the 3s in the above with whatever the width of the field is.

Note that the intermediate value needs to be at least as big as an "int" due to C language rules.  ("Integer promotion").  Hence using a 32-bit temporary here, which is big enough on any platform I've worked on.  If you're working on an embedded 16-bit platform, then using a 16-bit temporary will be better.

While this approach is faster to execute, it's not obvious why it works, and it doesn't help explain the concept.  That's why I left it out of my original response.

  Are you sure? yes | no