I2C: 7 or 8 Bit Addresses? [Fixed! Pull Request Submitted.]

I’m working with a little OLED display here, which has a 7-Bit address of 0x27. Normally, this is what you’d use with Arduino and what’s listed in the Spark Docs. Unfortunately, according to both my Logic 16 and Bus Pirate, it’s trying to write to 0x26. It doesn’t matter if I put 0x27 or 39 into beginTransmission. However, if I use 0x4E (which is the full write address) it works.

0b0100111  in hex => 0x27
0b01001110 in hex => 0x4E
0b01001111 in hex => 0x4F

I’ve tried resetting the Core to factory firmware, then flashing my code over (which was the cause of my last mysterious I2C problem) to no avail. Anyone else seeing similar issues?

1 Like

In I2C, all writes are even, and reads are odd… the LSB determines if it’s read or write in the address only. Perhaps you should not try to write to 0x27, but rather 0x4E?

It might say it’s a 7 bit address, and it is (left justified), but it has to actually send an extra 8th bit to tell your OLED that it’s trying to write to to that address. Technically, 0x4E is 0x27 left justified, plus the write bit.

Also to note that I2C transmits MSB first, so that 8th bit that it sends is actually bit0 in the 8bit address field.

If that doesn’t make sense, link me to your datasheet and I’ll see if they clarify better.

1 Like

Yeah, I know how I2C works. See the code block in my OP? 0x4E is 0x27 with a zero tacked on to the end. 0x27 = 7-Bits and 0x4E = 8-Bits. (Which you covered.) Wire is supposed to accept 7-Bit addresses; if you give it an 8-Bit address it even drops the last bit. So that part is working, because I can write to 0x4E and 0x4F just fine. (If I can write to the read address [0x27>>1=0x4F] then the library must be going from 8 to 7 to 8 bits with the address like the documentation claims.)

In Energia on my MSP430, I can use 0x27 to talk to this OLED, so I assume Arduino would be the same.

Does that make more sense?

Ya know… 0x27 is technically an 8-bit number, and should be treated as 0x26 during a write. How would the routine know 0x27 should be left justified? … only if you configured it as 7BIT somehow, then it would left justify everything you send it.

There are a lot of commented out parts, so I’m guessing I2C is not fully implemented yet:
https://github.com/spark/core-firmware/blob/master/src/spark_wiring_i2c.cpp#L212

txAddress just gets set to the value you send… not sure what happens after that.

Just use 0x4E and get dumb! :smile:

1 Like

Haha, well, you are technically correct. (Which is the best type of correct!)

The routine is supposed to accept 7b addresses:

beginTransmission()

Begin a transmission to the I2C slave device with the given address. Subsequently, queue bytes for transmission with the write() function and transmit them by calling endTransmission(). Parameters: address: the 7-bit address of the device to transmit to.

But how does it know it's supposed to be 7b? I2C has an address range from 0 to 127.

127 in binary => 0b01111111

So the highest address is seven ones. Remember, in 8-Bit Binary the MSB represents decimal 128. So, the Wire library should be dropping the MSB and adding the read or write bit as the new LSB.

However, in the Spark Team's implementation, it seems clear that they're just flat out replacing the LSB without doing any sort of shifting. This fully explains 0x27 becoming 0x26 and 0x4F/0x4E both working as write addresses.

I'll start tracking down the trouble in the sources. That byte swapping should be happening well after the code you linked @BDub. Thanks for taking a peak though! :smiley:

1 Like

Here we go!

Specifically this bit right here: Address &= OAR1_ADD0_Reset;

If we look up those #define statements, we see this:

 /* I2C ADD0 mask */
#define OAR1_ADD0_Set           ((uint16_t)0x0001)
#define OAR1_ADD0_Reset         ((uint16_t)0xFFFE)

With a little math, we find this:

0x27 & 0xFFFE => 0x26
0x4E & 0xFFFE => 0x4E
0x4F & 0xFFFE => 0x4E

Looking elsewhere we see that even this library (the STM32 driver) is expecting a 7-bit address. It looks like the pre-processing that exists in the Arduino Wire library isn’t present here. So my next step is to go check that code and see if I can make the same changes here!

Annnd I fixed it. I just need to run some more tests to make sure all cases are valid, once I confirm I’ll post a patch here and submit a pull request for the Spark Team. :smiley:

2 Likes

I will agree their implementation is not the standard. There really isn’t 7-bit AND 8-bit addressing… just 7-bit, but some vendors include the read/write bit as part of the address so it makes it look like it’s 8-bit. Technically it’s just 7-bit, especially if you are using a .write(); method which implies that the 8th bit is 0, the read/write bit.

The arduino twi.c library expects a 7-bit address and left shifts it by one and ORs in the read/write bit.

Documentation should be changed from:
Parameters: address: the 7-bit address of the device to transmit to.

To something like:
Parameters: address: the 7-bit address of the device to transmit to. Note: do not include the r/w bit with the address, or the address will be corrupted. once it’s fixed that is…

1 Like

So here's my fix for the I2C address issue!

As it turns out, all the magic happens within the platform specific I2C libraries on Arduino. There's no pre-processing or anything, they simply expect you to be providing an 7-Bit address. The fix was actually quite simple, really!

../core-common-lib/STM32F10x_StdPeriph_Driver/src/stm32f10x_i2c.c:

void I2C_Send7bitAddress(I2C_TypeDef* I2Cx, uint8_t Address, uint8_t I2C_Direction)
{
  /* Check the parameters */
  assert_param(IS_I2C_ALL_PERIPH(I2Cx));
  assert_param(IS_I2C_DIRECTION(I2C_Direction));
  /* Test on the direction to set/reset the read/write bit */
  if (I2C_Direction != I2C_Direction_Transmitter)
  {
	/* Shift the address one bit to the left */
	Address = Address << 1;
	/* Set the address bit0 for read */
	Address |= OAR1_ADD0_Set;
  }
  else
  {
	/* Shift the address one bit to the left */
	Address = Address << 1;
	/* Reset the address bit0 for write */
	Address &= OAR1_ADD0_Reset;
  }
  /* Send the address */
  I2Cx->DR = Address;
}

So, basically what's happening here is this:

  1. I2C_Send7bitAddress is called anytime you're setting up to read or write over the I2C interface. It's called surprisingly late in the code, coming after the START bit has already been transmitted. (I say this is surprising, because a lot of microcontrollers would have already put the slave address into read and write registers during beginTransmission, whereas the STM32 has a single address register that's only being set once endTransmission is called and communication has started.)
  2. if (I2C_Direction != I2C_Direction_Transmitter) is checking to see if we're supposed to be reading from this address. If so, we'll use Address |= OAR1_ADD0_Set; to perform a bitwise OR operation between it and 0x0001.
  3. If we're supposed to be writing to the address, we'll perform a bitwise AND between it and 0xFFFE like this: Address &= OAR1_ADD0_Reset;
  4. However, before we can do either #2 or #3, we need to shift the address one bit to the left. That's what the problem was! I'm not sure if this was previously being done somewhere else or what? Anyway, I've added Address = Address << 1; to the top of the if and else statements.
  5. Finally, we call I2Cx->DR = Address; to transfer the full 8-Bit address into the data register, which is then transmitted across the I2C bus!

I've tested this with ten different I2C devices and everything seems to working fine with my changes. (So long as you enter a valid 7-Bit address between 0 and 127.)

I double checked with my Logic 16 and Tektronix MSO2024B and there were no changes to the signal integrity. (As expected, I just like to be thorough!)

@zach I'll be making a pull request with my fix shortly. :smiley:

As an aside, this is a really neat little article/guide on the inner-workings of the ATmega328P TwoWire library. If you want to start understanding just how different Arduino is from actual "low level" C programming on a microcontroller, give it a read.

4 Likes

@timb Man, thank you so much for this. I was working with another I2C device and spent HOURS… until I gave this little shift a chance.
@BDub Thanks a lot for mentioning it on the BMP085 post, otherwise I would have kept killing myself.

2 Likes

@Iv4n No worries man! @zachary said they were going to accept my pull request, so the change should be in the master branch soon! I’m not sure when that gets pushed to their compile-server2 branch, but when it does your I2C code will most likely break until you change the I2C address back to 7-Bit (assuming you’re just adding the R/W bit to the address now yourself).

So you may want to note that in a comment at the top of your code and use a global variable (such as byte address = 0x4E) instead of manually assigning it on each call.

1 Like

Sorry to bump such an old thread, but it doesn’t appear that timb’s fix was ever pulled into firmware. I have been having issues with the 7/8-bit issue on an I2C device, and building a custom firmware with his changes resolved it. Is there some work-around other than firmware modification for this, or can particle support comment as to when this will be pulled in, and if not, why not?

Can you explain why you think this wasn’t pulled in? I don’t see any open PRs across any of the firmware repos that are related to I2C.

This code. Specifically the Address = Address << 1; byte shift.. this isn't present in any of the new firmware, but it fixed the issue presented in this thread... wondering if particle has an alternative solution?

"@Iv4n No worries man! @zachary said they were going to accept my pull request, so the change should be in the master branch soon! I'm not sure when that gets pushed to their compile-server2 branch, but when it does your I2C code will most likely break until you change the I2C address back to 7-Bit (assuming you're just adding the R/W bit to the address now yourself)."