How to set up a command-based communication using I2C

Dear Community

I am trying to set up a master-slave based communication between two microcontrollers using I2C. The setup includes:

  • nRF52840 microcontroller (master)

  • Adruino M0 (slave)

In the end, I want to achieve the following behaviour:
The master sends a command (a payload of a few bytes) to the master, which then proccesses the command and computes some stuff based on the received information. When it is done, it sends a response back to the master. In both directions, the length of the payload is of variable length. Also, processing the command will take some time, so from the master's side the waiting for the response must be non-blocking.

For a first implementation, I want to implement a demo app in which both microcontrollers are connected to a serial monitor and forward the messages from the serial buffer to the I2C interface.

Code for master (code for response not included yet):

#include <Wire.h>

void setup() {
  Serial.begin(115200);
  while (!Serial) {} // wait for Serial Monitor to open
  Serial.println("Hello world!");
  Wire.begin();
  Wire.setClock(100000);
  Serial.println("I2C ok!");
}

void loop() {
  if (Serial.available()) {
      byte txData[20];
      size_t bytesRead = Serial.readBytes(txData, 20);
      Serial.println("Starting I2C transmission...");
      Wire.beginTransmission(0x42);   
      Wire.write(txData, 20);
      Wire.endTransmission();
      Serial.println("I2C transmission finished!");
  }
  delay(50);
}

Code for slave (can only receive information, sending not implemented yet):

#include <Wire.h>

void setup() {
  SerialUSB.begin(115200);
  while (!SerialUSB) {}
  SerialUSB.println("Hello world!");
  Wire.begin(0x42);                
  Wire.setClock(100000);
  Wire.onReceive(receiveEvent);
  SerialUSB.println("I2C ok!");
}

void receiveEvent(int nBytes){
  SerialUSB.println("i2C callback");
  while(Wire.available()){
    char c = Wire.read();
    SerialUSB.print(c);  
  }
}

void loop() {
  delay(50);
}

With this code, I am unable to receive any messages via the I2C bus. The callback receiverEvent() is never invoked. If I put the code of receiveEvent() inside the main loop, the same situation occurs, i.e. no data is ever received.

Problem 1:
The start-up of the master is very slow for some reason. It takes a couple of seconds until I see the welcome messages on my serial monitor. The slave always starts up immediately. Any idea why?

Problem 2:
How to correctly receive bytes with the Write library? Using Wire.read() never seems to return anything (no matter if called inside or outside the callback).

I would suggest posting schematics, not frizzy things with wire lengths (very important) showing all power and ground connections. The I2C protocol was originally establish for communication between two or more ICs (Integrated Circuits) on a PCB, hence that is why it's known as Inter-Integrated Circuit (I2C) communication. There are no general purpose I2C drivers for off board commutation however there are buffers. Please note I2C could also be used for communication between two ICs that are located on the same PCB. There are many other protocols designed for communication for devices not on the same PCB. With these such as CAN, RS485 etc you can monitor the communications at any time you want without interfering with communications between nodes.

Maybe look here: Gammon Forum : Electronics : Microprocessors : I2C - Two-Wire Peripheral Interface - for Arduino go down to "Request/response"

Do you know what happens when you call this function: SerialUSB.println().
It puts the data in a buffer, but also starts the USB interface. There is a lot going on.
Calling that from an interrupt routine might not work.

The I2C bus is not very good for communication between processors. Can you use the Serial/UART bus ?

How can a Slave send a message to a Master ?

We don't use the .readBytes() function. Do you know if it will return a zero-terminated string when there was a timeout ? I don't.

The I2C bus has three wires: SDA, SCL, GND.

You got the main idea alright, but all those little things together makes this not easy to fix.

You meant master and slave, I suppose. Nonetheless you got it right, because each device on an I2C bus can act as a master. I.e. A sends a command to B, and when ready B sends the response to A. For the command A is the master and B is the slave, for the response B is the master and A is the slave.

For your transmission problem I suggest that you start with the single byte transfer from the IDE examples. Once you got your hardware right you can continue with longer record transmission.

Hi all
Thank you very much for your help.

I would suggest posting schematics

Sure. The nRF microcontroller I use is mounted on a Adafruit Feather Sense board. Both are supplied by USB (not shown in image) and use 3.3V logic level. The overal picture looks like this:

Maybe look here: [...] go down to "Request/response"

Thank you for this hint. I will study the contents there.

The I2C bus is not very good for communication between processors. Can you use the Serial/UART bus ?

I totally agree that I2C is not the best option for the functionality I want. The problem is that I do not see another, easy method to establish a communication. On the slave's side (The M0 board), both UART and SPI are already in use (they are used for other peripherals). On the master's side, I2C is provided "out of the box" by the board where the nRF microcontroller is mounted (it is used to communicate with all the sensors that are also mounted on the board). Technically, I think I could use some digital pins to emulate a certain interface.

We don't use the .readBytes() function. Do you know if it will return a zero-terminated string when there was a timeout ? I don't.

Very good hintè Thank you.

You meant master and slave, I suppose.

Yes, sorry for the typo.

For your transmission problem I suggest that you start with the single byte transfer from the IDE examples.

Again a good hint, thanks.

Hi, @domi1
Welcome to the forum.

How far apart are the controllers, what are the I2C distances?
Can you post a basic block diagram showing your proposed I2C network.

Have you looked at CanBus?

Thanks.. Tom... :smiley: :+1: :coffee: :australia:

How far apart are the controllers, what are the I2C distances?

In the current setting, the boeards are next to each other, with a cable length of approximately 10cm. Once the boards run outside of my IDEs, they will be located next to each other, with a cable connection as short as possible.

Adafruit Feather Bluefruit nRF52840 Sense : https://www.adafruit.com/product/4516
Schematic : https://learn.adafruit.com/assets/89187.
It has 4k7 pullup resistors, that's good.

Wemos M0 : at Banggood
There are differences between the Arduino ARM M0+ boards. One was designed in Italy and the others are designed by the international Arduino team. I'm not sure, but I think the Arduino M0 was designed in Italy. The Wemos M0 is not a exact copy of the Arduino M0.
I can not find a schematic.

As soon as you call Wire.begin(0x42); in the Slave then a I2C Scanner sketch in the Master should be able to find it. Can you try that ?

You should ask Adafruit why that board is slow to start. Please be clear what you do and add links to other forums if you ask the same question elsewhere.

Dear all
I did lots of debugging today. Here are the results.

Evaluation 1
Let's check the datasheets which I2C configurations are possible, i.e. which clock frequencies are supportet. Having a look at my WeMos M0 clone, I can see that it has an "Atmel ATSAMD21" microcontroller.

[Here I would insert a picture of my M0 clone board such that other users could see the uP. But the forum does not allow me to do so. Thanks a lot!]

The other boards is as mentionned previously a nRF52840. Having a look in the datasheets reveals that 100kHz is supported for both boards.

[Here I would insert a snippled of the two datasheets with the I2C specifications. But the forum does not allow me to do so. Thanks a lot!]

Evaluation 2
Let's check which pins are used by the microcontollers.
On my Arduino M0 board:

SerialUSB.println(PIN_WIRE_SDA);  // This outputs '20'
SerialUSB.println(PIN_WIRE_SCL);  // This outputs '21'

And on my Adafruit Sense board:

Serial.prinln(PIN_WIRE_SDA); // This outputs '22'
Serial.prinlm(PIN_WIRE_SCL); // This outputs '23'

This seems to be in line with the datasheets.

Evaluation 3
I checket whether my master board (nRF uP) is sending something over the I2C interface. If I send some data via Wire.write() I can see the waveforms on the oscilloscope:

[Here I would insert a photo of my oscilloscope showing the waveforms. But the forum does not allow me to do so. Thanks a lot!]

So the master interface seems working. However, I noticed an issue:
When the nRF is idle, the SCL and SDA pin stay at high (as it should be); but when I connect the M0 board to it, the voltage sometimes drops to 0V unless the M0 board is pluged in / flashed with a software. The nRF is then unable to output a signal. Is this supposed to happen? I'm not sure what exactly the cause of this voltage drop is as it only happens sometimes, but not always.

If I plug in the slave first and flash a program, and then connect the master via I2C to it, everything seems to be ok according to the oscilloscope. However, I am still unable to receive any information by the slave. Wire.read() never never returns something, no matter if I call it in the main loop or in an interrupt handler.

Evaluation 4
Let's reverse the situation and see whether the slave (Arduino M0) outputs something on the SCL/SDA wires when I call Wire.write() (master not connected). Result: no signal can ever be observed on the oscilloscope. So maybe the pins are not correct? I was unable to find a scheematic for that particular M0 clone, so I used a multimeter to measure which pins are connected to the SDA/SCL wire. Here is the result:


Again, this matches the specifications of other M0 boards. So this seems to be ok. Nevertheless, I tested whether I can observe a signal on any of the digital pins when I continuously call Wire.write() in the loop function. Result: No digital pin outputs anything.

Evaluation 5
Maybe the Wire library is doing something odd? What about normal digital writes? I used this code for testing on my M0 board:

void setup() {
  SerialUSB.begin(115200);
  while (!SerialUSB) {}
  pinMode(PIN_WIRE_SDA, OUTPUT);
  pinMode(PIN_WIRE_SCL, OUTPUT);
}

void loop() {
  digitalWrite(PIN_WIRE_SDA, HIGH);
  digitalWrite(PIN_WIRE_SCL, LOW);
  delayMicroseconds(5);
  digitalWrite(PIN_WIRE_SDA, LOW);
  digitalWrite(PIN_WIRE_SCL, HIGH);
  delayMicroseconds(5);
}

Result: I can observe a square wave signal on my SDA and SCL pins (!)

Summary
Two issues have been found:

  1. There seems to be an electrical issue with the SCL/SDA connections (but I am not sure what it is)
  2. Using the Wire library on the slave never outputs something although the pin number is correct and the digital IO is working properly.

Connect the GND between the boards.
The I2C bus has three wires: SDA, SCL and GND.

The M0 board should not pull SDA or SCL low. I really should not do that.

Is your picture wrong ? The Black square is the Adafruit Sense board with nRF52840 processor ?

Make a sketch on the M0 with only Wire.begin(slave_address) and run a I2C Scanner sketch in the Adafruit Sense Master. The Master should see all the onboard I2C devices. Can you also see the M0 Arduino board appear and disappear if you connect and disconnect it ?
Are you sure that the I2C Slave address is not already in use by a sensor on the Adafruit Sense board ?

Suppose the Adafruit Master can see the other Arduino board.
Then turn it around. Put that Wire.begin(slave_address) in the Adafruit Sense board and run a I2C Scanner sketch in the M0. Can the M0 see all the sensors on the other board and also the processor ?

  1. You miss to connect GND in your Evaluation 4. A circuit diagram were much more helpful.
    You miss that the behaviour of the SCL/SDA pins differs for normal use and use as I2C signals.

  2. A slave can not output anything by itself because every transmission is started by a master.

  3. Insertion of useless or redundant information is blocked for good reason, as can be seen with your rants. Why do you think that you should copy&paste I2C specification when waiting for helpful answers of experts?

Do you have more questions?

Connect the GND between the boards.

The GND is connected between the two boards (always was).

Is your picture wrong ? The Black square is the Adafruit Sense board with nRF52840 processor ?

No the picture is supposed to show how the pins on the M0 clone board are connected to its microcontroller (the Atmel CPU). I was worried that since it is not an original board, it might have a different wiring on the board compared to other schematics of M0 boards. Sorry if that confused you.

Make a sketch on the M0 with only Wire.begin(slave_address) [...] The Master should see all the onboard I2C devices. Can you also see the M0 Arduino board appear and disappear if you connect and disconnect it ?

To be honest I do not know how to implement an I2C scan. But I suppose that observing the traces with the oscilloscope should do the job if set it on a single trigger event on the falling edge.
For comparison, I used

pinMode(PIN_WIRE_SDA, OUTPUT);
pinMode(PIN_WIRE_SCL, OUTPUT);
digitalWrite(PIN_WIRE_SCL, LOW);
digitalWrite(PIN_WIRE_SDA, LOW);

to test the trigger. This works fine, i.e. I see the falling edge on my oscilloscope. If I repeate the process with a single Wire.begin(slave_address) only, then the oscilloscope is never triggered. Is that sufficient?

A slave can not output anything by itself because every transmission is started by a master.

interesting, I did not know that the I2C works in that way. Based on this new insight, I tried the following: I entered the I2C as master with my M0 board to see whether it outputs something. Result: Yes it does.
But this does not answer the question why I never receive anything with Wire.read() inside my callbacks.

See the examples coming with the Wire library: i2c_scanner.
It helps to run the examples in order to become familiar with a protocol or library.

Again see the example programs: slave_receiver. If it doesn't work for you then something may be wrong with your cables. Also check the result of write() and endTransmission() on the master for possible problems.

I used the example programs to test the interface. They work like a charm for both combinations (M0 as master and nRF as slave and vice versa). Makes me feel kinda stupid, I have to admit.

Nevertheless, from here on we can start debugging again and see where it breaks down...
Result: The program stops woring once I add
Wire.setClock(100000UL); // also tried without the "UL" postfix
on both programms.
Which is super weird because the clock period I see on my oscilloscope is 10us, which corresponds to 100kHz. I.e. the Wire library defaults to 100kHz. So what is wrong with .setClock()!?

setClock() may not be supported on slaves (without I2C address), because the clock is always generated by the master.

I just cannot test with two boards, so please check the following:

Evaluation 1: Don't use setClock with the slave program.

Evaluation 2: Make both controllers a (possible) master by assigning each a (different) I2C address.

Evaluation 1: Don't use setClock with the slave program.

Programm is still working, even if I call Wire.setClock() on the master.

Evaluation 2: Make both controllers a (possible) master by assigning each a (different) I2C address.

Program no longer works, no matter if I call Wire.setClock() or not.

By the way, I still do not see what could possible be wrong with Wire.setClock(). Here is where this code ends up:

/* 
 * Function twi_setClock
 * Desc     sets twi bit rate
 * Input    Clock Frequency
 * Output   none
 */
void twi_setFrequency(uint32_t frequency)
{
  TWBR = ((F_CPU / frequency) - 16) / 2;
  /* twi bit rate formula from atmega128 manual pg 204
  SCL Frequency = CPU Clock Frequency / (16 + (2 * TWBR))
  note: TWBR should be 10 or higher for master mode
  It is 72 for a 16mhz Wiring board with 100kHz TWI */
}

F_CPU is defined as 48000000 for the M0 board. So this function evaluates to ((48000000/100000)-16)/2 = 232
Anything wrong with that?

Please show your modifications. Multiple masters on the same bus must work.

For a I2C bus, connect the SDA, SCL and GND wires.
The next thing to do is to run a I2C Scanner sketch.
Here is one: https://learn.adafruit.com/scanning-i2c-addresses/arduino.
Or in the menu of the Arduino IDE as DrDiettrich wrote in reply #14.
Arduino IDE, menu: File / Examples / Wire / i2c_scanner

It is useless to try other code or try to fix the code if you have not run a I2C Scanner sketch.