I2C device not responding (or bad code)

Hi everyone,

I would like to build my projects entire on PCBs. And so to challenge myself, and to learn from, I designed a small PCB, an accelerometer that on runs on I2C, using the QMA6981 by QST. the schematic I build for the PCB looks like this:

which I made by consulting the datasheet at page 9.
and the PCB:

I then tried hooking it up to my Arduino Nano Every to test it out, in the following way, with a logic level converter

Which I then tried running with the following code:

#include <Wire.h>

void setup() {
  Wire.begin();                
  Serial.begin(9600);        

  // step 1: instruct sensor to read echoes
  Wire.beginTransmission(0x12); // transmit to device #12
  
  Serial.println("sending active to device");
  Wire.write(byte(0x11));      // to active mode -> 0x11
  Wire.endTransmission();      // stop transmitting

  delay(200);                  
  
}
  
int reading = 0;

void loop() {
  
  while(Wire.available()) {  
    reading = Wire.read();     // receive
    Serial.println(reading);   // print the reading
  }

  delay(250);
                  
}

To get the device to start transmitting in the setup (page 12 of datasheet), and then read out and print what it sends back to the MPU.

But nothing is being printed when uploading this script. (apart from "sending active to device")

I think it is most likely something in my code which is wrong, because when I run the following "I2C scanner" script I found:

#include <Wire.h>

void setup() {
  Wire.begin();

  Serial.begin(9600);
  while (!Serial); // Leonardo: wait for serial monitor
  Serial.println("\nI2C Scanner");
}

void loop() {
  int nDevices = 0;

  Serial.println("Scanning...");

  for (byte address = 1; address < 127; ++address) {
    // The i2c_scanner uses the return value of
    // the Write.endTransmisstion to see if
    // a device did acknowledge to the address.
    Wire.beginTransmission(address);
    byte error = Wire.endTransmission();

    if (error == 0) {
      Serial.print("I2C device found at address 0x");
      if (address < 16) {
        Serial.print("0");
      }
      Serial.print(address, HEX);
      Serial.println("  !");

      ++nDevices;
    } else if (error == 4) {
      Serial.print("Unknown error at address 0x");
      if (address < 16) {
        Serial.print("0");
      }
      Serial.println(address, HEX);
    }
  }
  if (nDevices == 0) {
    Serial.println("No I2C devices found\n");
  } else {
    Serial.println("done\n");
  }
  delay(5000); // Wait 5 seconds for next scan
}

It displays that an I2C device is found at adress 0x12. So I suspect there is an error in my program, and not in the way the device is connected.

I have been reading up on the I2C protocol and how to use it for some time now, but cannot find anything that helps me further right now. Do some of you know what the problem may be? Or do you have some source of information that might point me in the right direction?

Thanks in advance.

The I2C bus is working when it is found at address 0x12. That is a good start.

After power-up the sensor is in standby mode. You have to get it out of standby mode according to the datasheet.

The sensor has registers, when writing to a register, you first have to send the register-address.
For example a softreset is this:

Wire.beginTransmission( 0x12);   // I2C address
Wire.write( 0x36);   // register address
Wire.write( 0xB6);   // data
Wire.endTransmission();

My alternative explanation might be helpful: Explanation of the functions of the Wire library · Koepel/How-to-use-the-Arduino-Wire-library Wiki · GitHub.
The Wire.available() on its own does nothing, it is the Wire.requestFrom() that does (almost) everything.

The 2k2 pullup resistors are a low value. You also have 10k pullup on the level shifter and a internal pullup resistor. That makes the sink current: 5V/30k + 5V/10k + 3.3V/10k + 3.3V/2k2 = 2.5mA.
When you add another module (with onboard pullup resistors) then you might go over the limit of 3mA.
In the datasheet the 2k2 is just an example.

The schematic in the datasheet is not very nice. You have copied that :frowning: I think you can make it better with less GND wires and more use of the GND-symbol.

Hi Koepel,

Thank you for reading into my post.

I updated the program to

#include <Wire.h>

void setup() {
  Wire.begin();                
  Serial.begin(9600);        

  //Wire.beginTransmission(0x12); // transmit to device #12
  //Wire.write(byte(0x11));      // to active mode -> 0x11
  //Wire.endTransmission();      
  
  delay(200);                  
  
}
  
int reading = 0;

void loop() {
  // step 1: instruct sensor to read echoes

  Wire.requestFrom(0x12,10);

  while(Wire.available()) {  
    reading = Wire.read();     // receive
    Serial.println(reading);   // print the reading
  }
  
  delay(1000);
                  
}

which contains the wire.requestFrom you suggested. There is now data coming from the device, so that is a step in the right direction :slight_smile:

then serial monitor spits out

0
12
0
0
255
0
0
0
0
0
0
0
0
0
0
5
0
9
48
1
15
192
0
0
0
4
10
0
164
0
164
0
0
0
4
0
0
0
0
94
93
208
97
131
226
0
0

with then a lot of "0"s

Which I recon is trying to parse something. Unfortunately, I do not understand how to make sense of what it is writing.

The amount of bytes I request in "Wire.requestFrom" does not matter for the output, apart from the amount of lines written in the serial monitor each cycle. Just like the delay each loop; it only dictates how long it takes for the output above to be written, but does not affect the output itself.
I cannot make sense of this; I would expect the sensor to want to send its 'opening message' (which I assume the above output represents, as it is the same each time the script is ran). But if I would request fewer bytes than the message actually is, the master would just ignore this?;

With a Wire.requestFrom() it is even impossible to receive more data than requested. When a I2C Master is reading data, the Master defines how many bytes will be read. The Slave can try to send less or more, but that will not change the number of bytes that the Master will read.

as from the link you posted

Also, I somewhat expected the device to transmit the accelerometer data after the initial messages, as we put the device into 'active mode'

The Nano Every belongs to the AVR boards family, but it is a modern version of the AVR microprocessors. I'm not sure that the Wire library behaves in the same way.

When the sensor is not connected, then Wire.available() returns 0.
When the sensor is connected and 10 bytes are requested, then Wire.available is probably 10. If the Slave stopped sending data after a few bytes, the Master reads 255 (all 1's, that is 0xFF).

I think you have to get the sensor out of the Standby mode.

The sensors with I2C work for 99% in the same way. They have registers that you can send data to and registers to read data from. I have not heard of a I2C sensor with a opening message. Is that written in the datasheet ? I can not find that, so assume there is no opening message. You have to tell the sensor what to do.

Why did you choose the QMA6981 ? I can not find an example for that sensor with Arduino.
If you want something that works, then you can buy a module from Adafruit, they show the schematic, give a tutorial and make a library and they have a forum to ask questions.

When the sensor is not connected, then Wire.available() returns 0.
When the sensor is connected and 10 bytes are requested, then Wire.available is probably 10. If the Slave stopped sending data after a few bytes, the Master reads 255 (all 1's, that is 0xFF).

Ah, that seems to make sense. Is that because the signal will be pulled high and thus the byte will be 1111 1111 ?

The sensors with I2C work for 99% in the same way. They have registers that you can send data to and registers to read data from. I have not heard of a I2C sensor with a opening message. Is that written in the datasheet ? I can not find that, so assume there is no opening message. You have to tell the sensor what to do.

I worked with a GPS unit that had some sort of opening statement, but iirc that was not I2C. I simply assumed that because the data transmitted was the same every time, there would be a standard message... but you are right, nothing about that is written in the datasheet. Do you have an idea for what this data might be otherwise?

Why did you choose the QMA6981 ? I can not find an example for that sensor with Arduino. If you want something that works, then you can buy a module from Adafruit, they show the schematic, give a tutorial and make a library and they have a forum to ask questions.

This sensor was available at the party I made the PCB at. And the goal is to learn. Would there be any reason why this device could not work with an arduino?

I think you have to get the sensor out of the Standby mode.

I reread some information and reviewed my code further. This is the program I came up with. Could you review my comments and tell me where my train-of-thought goes wrong?

#include <Wire.h>

void setup() 
{
  Wire.begin();
  Serial.begin(9600);
  Serial.println("doing something0"); //program got stuck some times, so wrote these in to be able to see how far it got.

  //bringing device in active mode:
  Wire.beginTransmission(byte(0x12)); // transmit to device #12
  Wire.write(byte(0x11));      // write in register 0x11, as per page 12
  Wire.write(byte(0x01));      // write 0x11 = 1, as per page 12
  Wire.endTransmission();      
  Serial.println("doing something1");
  
}

byte reading = byte(0x00); //declare a place for the reading to be stored

void loop() 
{
   // following partly copied from https://arduino.stackexchange.com/questions/60940/arduino-i2c-register-read
   Wire.beginTransmission(byte(0x12));    //we are still transmitting to 0x12
   Wire.write(byte(0x02));                // indicating the register we want to read from; x-acceleration: second half (to test), as per page 25
   Wire.endTransmission(false);           // false to not release the line, because the master needs to send more: a request from this register
    Serial.println("doing something2");
   Wire.requestFrom(byte(0x12),1);        // request bytes from the register at device 0x12
   
   while(Wire.available()) {              //only do this while a data from the device was send to the buffer
    reading = Wire.read();                // write the data from the buffer to 'reading'
    Serial.println(reading);              // print 'reading' to the monitor.
    Serial.println("doing something3");
  }
   Serial.println("doing something4");
   delay(1000);                           // wait a second to before repeating.
}

what is being displayed to the monitor now looks something like this:

doing something0
doing something1
doing something2
0
doing something3
doing something4
doing something2
0
doing something3
doing something4
doing something2
0
doing something3
doing something4
doing something2
doing something4
doing something2
0
doing something3
doing something4
doing something2
doing something4
doing something2
0
doing something3
doing something4
doing something2
0
doing something3
doing something4

Implying (I think?) that sometimes there is some data available to coming from the device.

chris_abc:
Ah, that seems to make sense. Is that because the signal will be pulled high and thus the byte will be 1111 1111 ?

Yes, when the Slave does nothing, the pullup resistor pulls SDA high and the Master reads only '1's.

chris_abc:
Do you have an idea for what this data might be otherwise?

Yes, it is rubbish :smiley_cat:

chris_abc:
This sensor was available at the party I made the PCB at. And the goal is to learn. Would there be any reason why this device could not work with an arduino?

That are strange parties that you go to :o
Of course it will work with an Arduino, but there are more sensors that need a certain startup sequence. Some sensors even need a few specific delays in that sequence.

chris_abc:
Could you review my comments and tell me where my train-of-thought goes wrong?

It is okay. I think you are actually reading a value of 0x00 from the sensor. I have not studied the datasheet.

I'm not sure about the "repeated start".

...
Wire.endTransmission(false);   // this 'false' wil make a repeated start.
Wire.requestFrom(byte(0x12),1);    
...

The 'false' parameter omits the STOP and then the Wire.requestFrom() makes a START. That will become a "repeated start" when there was no STOP. According to the datasheet, that is not needed. Every modern sensor supports it, but you can remove the 'false' parameter to be sure for now.
Looking better at the picture in the datasheet, I think that is a repeated start. So keep it in there !

When a I2C Scanner detects the device, the next step is to read an identifier from the chip.
Register-address 0x00 contains the "CHIP ID" of 0xBX. Can you try to read that ?

There is a tiny little thing that makes my hair curl:

Wire.endTransmission();      // stop transmitting

Can you remove the comment that it stops transmitting there. That is not what that function does. Just do the right sequence of Wire.beginTransmission() - Wire.write() - Wire.endTransmission() and let the library do its job. What actually happens is for later.

For 10 or 25 dollars you can buy a logic analyzer to see what the I2C is doing.
For example with sigrok / PulseView: https://sigrok.org/
The 8 channel 24MHz 10 dollar devices do work, but they are not well build.
I love the 25 dollar LHT00SU1. Turn off the analog channel to be able to sample the digital inputs with a high sample rate.

Hi Koepel,

Can you remove the comment that it stops transmitting there. That is not what that function does. Just do the right sequence of Wire.beginTransmission() - Wire.write() - Wire.endTransmission() and let the library do its job. What actually happens is for later.

done :confused:

When a I2C Scanner detects the device, the next step is to read an identifier from the chip.
Register-address 0x00 contains the "CHIP ID" of 0xBX. Can you try to read that ?

I updated the code to:

#include <Wire.h>

void setup() 
{
  Wire.begin();
  Serial.begin(9600);
  Serial.println("doing something0"); //program got stuck some times, so wrote these in to be able to see how far it got.

  //bringing device in active mode:
  Wire.beginTransmission(byte(0x12)); // transmit to device #12
  Wire.write(byte(0x11));      // write in register 0x11, as per page 12
  Wire.write(byte(0x01));      // write 0x11 = 1, as per page 12
  Wire.endTransmission();
  Serial.println("doing something1");
  
}

byte reading = byte(0x00); //declare a place for the reading to be stored

void loop() 
{
   // following partly copied from https://arduino.stackexchange.com/questions/60940/arduino-i2c-register-read
   Wire.beginTransmission(byte(0x12));    //we are still transmitting to 0x12
   Wire.write(byte(0x00));                // indicating the register we want to read from; 0x00 to obtain the chip-ID
   Wire.endTransmission(false);           // false to not release the line, because the master needs to send more: a request from this register
    Serial.println("doing something2");
   Wire.requestFrom(byte(0x12),1);        // request 1 byte from the register at device 0x12
   
   while(Wire.available()) {               //only do this while a data from the device was send to the buffer
    reading = Wire.read();                 // write the data from the buffer to 'reading'
    Serial.println(reading, HEX));       // print 'reading' to the monitor.
    Serial.println("doing something3");
  }
   Serial.println("doing something4");
   delay(1000);                           // wait a second to before repeating.
}

This results in the same output as I pasted in my previous post, unfortunately not the Chip-ID.

For 10 or 25 dollars you can buy a logic analyzer to see what the I2C is doing.
For example with sigrok / PulseView: https://sigrok.org/
The 8 channel 24MHz 10 dollar devices do work, but they are not well build.
I love the 25 dollar LHT00SU1. Turn off the analog channel to be able to sample the digital inputs with a high sample rate.

I have a BitScope micro oscilloscoop & logic analyser coming in today monday, so hopefully this will give me more insight to what is happening.

Put on your "trial-and-error" hat and try this (not tested):

#include <Wire.h>

int QMA6981 = 0x12;

void setup()
{
  byte myData;
  
  Wire.begin();
  Serial.begin(9600);
  Serial.println("Started");

  // Maybe the sensor has not started yet ?
  delay( 1000);

  // Try to read register 0x00
  Serial.print( "Register 0x00 = ");
  if( readByte( 0x00, myData))
  {
    Serial.print( "0x");
    if( myData < 0x10)
      Serial.print( "0");
    Serial.println( myData, HEX);
  }
  else
  {
    Serial.println( "Error");
  }


  // Let's try a reset and a delay
  if( writeByte( 0x36, 0xB6))
  {
    Serial.println( "Reset command send");
  }
  else
  {
    Serial.println( "Error, Reset command failed");
  }

  delay( 500);


  // Try to read register 0x00 again
  Serial.print( "Register 0x00 = ");
  if( readByte( 0x00, myData))
  {
    Serial.print( "0x");
    if( myData < 0x10)
      Serial.print( "0");
    Serial.println( myData, HEX);
  }
  else
  {
    Serial.println( "Error");
  }
}

void loop()
{
}

bool readByte( int register_address, byte &data)
{
  bool success = false;                  // set default to no success
  Wire.beginTransmission( QMA6981);
  Wire.write( register_address);         // select register
  int error = Wire.endTransmission( false);  // false for repeated start
  if( error == 0)                        // no error ? then data can be requested
  {
    int n = Wire.requestFrom( QMA6981, 1);
    if( n == 1)                          // received the bytes that were requested ?
    {
      data = (byte) Wire.read();
      success = true;                    // only now success is true
    }
  }
  
  return( success);
}

bool writeByte( int register_address, byte data)
{
  bool success = false;                  // set default to no success

  Wire.beginTransmission( QMA6981);
  Wire.write( register_address);         // select register
  Wire.write( data);                     // data to write to the register
  int error = Wire.endTransmission();
  if( error == 0)
  {
    success = true;
  }

  return( success);
}