Help with Arduino Code for I2C Device Location

Hello, I'm new here. I'm learning Arduino recently, and I'm still a beginner. I need it for a project I'm designing as an architecture engineer. I've designed the system, but I'm not sure how to make the code work.

I'm sure the code is doable, and I've done my research, but I'm still learning, and it will take time to find the solution. I have the concept ready for the program, but I want to translate it into code.

First, I'm using the I2C protocol with a master device (Arduino Uno) and many slaves (Arduino Nano). Each slave has a unique address from 1-127. All the slaves are connected to the master with 5V, GND, SDA, and SCL wires. They are also connected to each other with at least one location pin (2, 3, 4, 5) like a grid system. The master has only one location pin (pin 2 = HIGH) connected to one of the slaves.

Scanning for Devices:

I want the master to first scan the devices and assign all connected devices to the ConnectedDevices[] array so I can count, display, detect changes in the system, and test locations later.

Finding the First Slave:

After scanning, I ask the master to go through the ConnectedDevices[] and ask them one by one if one of the 4 location pins reads (HIGH), until it finds the one connected directly to the master. This will be the first slave, and I will assign it to RegisteredDevice[0], marking its location on the grid as row[0]=0 and column[0]=0. Then I turn pin (2) to LOW.

Finding Other Slaves’ Locations:

After that, the code will go through RegisteredDevice[0] and ask it to make pin (2) an output (HIGH). Then the master will go through all ConnectedDevices[] that are not registered and see if any of their pins are connected to that pin. If none are connected, the slave will return 0, and the master will test the next pin (3) for all devices again, then pin (4) and pin (5) . If none read a value, the loop will go to the next ConnectedDevices[] and repeat the tests for the 4 pins in the RegisteredDevice[]. If any of the pins read a value, the device is connected, and the slave will return the pin number.

For example, if slave 0x3 gets a value from Pin 4, the master will know this slave is connected to the first address with pin 4, so the new location will be Row[i+1]=row[i] (no change in row), Column[i+1]=Column[i]+1 (the device is on top of the reference slave), and assign its address to RegisteredDevice[i+1].

Note: There may be none or more than one connected device (maximum 3), so it will register them one after another.

After that, it will go to the next RegisteredDevice[] and set one of their pins to HIGH at a time, testing all the connected devices again until finding all the slave locations.

Detection mode

Once all locations are found, the program will return to detection mode for any changes in the system (if any device is added or removed). The system will reset and find all locations again.

Here is the digram for the connected devices ( the slaves can be arranged in any different positions)

If anyone could guide me on how to implement this in code, I would greatly appreciate it! Please let me know if you need any other information. Thank you!

Will your design have to cater for long distances? If yes, I2C might not be the right choice as it's intended for short distances (25cm or so); though people claim to have covered up to 30 meters.

i wrote a code with the help of ChatGPT, the adress dedection work but the location system is not right as i asked, and im not sure how to fix it

if you are interested to see the code (Ai generated)

Master Code:

#include <Wire.h>

#define MAX_DEVICES 128  // Maximum number of I2C addresses (1 to 127)
#define MAX_MODULES 10   // Maximum number of modules you expect to handle

  // Arrays for storing registered module addresses and their respective row/column positions
  byte DetectedModules[MAX_MODULES];  // Store module addresses before registration
byte RegisteredModules[MAX_MODULES];  // Store module addresses after registration
int row[MAX_MODULES];                 // Store row positions
int column[MAX_MODULES];              // Store column positions
int DetectedCount = 0;                // Number of detected modules
int RegisteredCount = 0;              // Number of registered modules

// Arduino UNO location pin (for finding the first module)
const int locationPinMaster = 2;  // Example pin on the master Arduino (set to HIGH initially)

// Function to scan for all connected modules
void scanModules() {
  DetectedCount = 0;  // Reset the detected module count
  Serial.println("Scanning for connected modules...");

  // Loop through all possible I2C addresses to find connected modules
  for (byte address = 1; address < 127; address++) {
    Wire.beginTransmission(address);
    if (Wire.endTransmission() == 0) {  // Device found
      DetectedModules[DetectedCount] = address;
      Serial.print("Module detected at address 0x");
      Serial.println(address, HEX);
      DetectedCount++;
    }
  }

  Serial.print("Total connected modules: ");
  Serial.println(DetectedCount);
}

// Function to find the first module connected directly to the master
void findFirstModule() {
  if (DetectedCount == 0) {
    Serial.println("No modules detected. Skipping location assignment.");
    return;  // No modules found, skip
  }

  pinMode(locationPinMaster, OUTPUT);
  digitalWrite(locationPinMaster, HIGH);  // Set the master location pin to HIGH

  // Loop through all detected modules to find the one connected to the master
  for (int i = 0; i < DetectedCount; i++) {
    byte address = DetectedModules[i];
    Wire.requestFrom((uint8_t)address, (uint8_t)1);  // Ask the slave for location pin status
    if (Wire.available()) {
      byte pinStatus = Wire.read();
      if (pinStatus == 1) {  // A pin is high, meaning the module is connected to the master
        // Register the first module
        RegisteredModules[RegisteredCount] = address;
        row[RegisteredCount] = 0;     // First module at row 0
        column[RegisteredCount] = 0;  // First module at column 0
        Serial.print("First module detected at address 0x");
        Serial.println(address, HEX);
        Serial.print("Location of the first module: (");
        Serial.print(row[RegisteredCount]);
        Serial.print(", ");
        Serial.print(column[RegisteredCount]);
        Serial.println(")");
        RegisteredCount++;
        break;  // Exit loop after finding the first module
      }
    }
  }

  digitalWrite(locationPinMaster, LOW);  // Set the master location pin to LOW after the first module is found
}

// Function to check if a slave is already registered
bool isRegistered(byte address) {
  for (int i = 0; i < RegisteredCount; i++) {
    if (RegisteredModules[i] == address) {
      return true;  // Module is already registered
    }
  }
  return false;
}

// Function to register additional modules connected to existing ones
void findConnectedModules() {
  for (int i = 0; i < RegisteredCount; i++) {  // Loop through all registered modules
    for (int pin = 1; pin <= 4; pin++) {       // Loop through the 4 location pins (up, right, down, left)
      // Send a request to the registered module to activate the corresponding pin
      Wire.beginTransmission(RegisteredModules[i]);
      Wire.write(pin);  // Send the pin number to be activated (1 = UP, 2 = RIGHT, 3 = DOWN, 4 = LEFT)
      Wire.endTransmission();

      // Now check all unregistered slaves to see if any respond
      for (int j = 0; j < DetectedCount; j++) {
        byte address = DetectedModules[j];
        if (!isRegistered(address)) {                      // Only check unregistered slaves
          Wire.requestFrom((uint8_t)address, (uint8_t)1);  // Ask the slave for location pin status
          if (Wire.available()) {
            byte pinStatus = Wire.read();
            if (pinStatus == 1) {  // A pin is high, meaning the module is connected
              // Register the module
              RegisteredModules[RegisteredCount] = address;
              if (pin == 1) {                             // Connected on the UP pin
                row[RegisteredCount] = row[i] - 1;        // Adjust row position
                column[RegisteredCount] = column[i];      // Same column
              } else if (pin == 2) {                      // Connected on the RIGHT pin
                row[RegisteredCount] = row[i];            // Same row
                column[RegisteredCount] = column[i] + 1;  // Move to the right column
              } else if (pin == 3) {                      // Connected on the DOWN pin
                row[RegisteredCount] = row[i] + 1;        // Adjust row position
                column[RegisteredCount] = column[i];      // Same column
              } else if (pin == 4) {                      // Connected on the LEFT pin
                row[RegisteredCount] = row[i];            // Same row
                column[RegisteredCount] = column[i] - 1;  // Move to the left column
              }

              // Print detected module information
              Serial.print("Module detected at address 0x");
              Serial.print(address, HEX);
              Serial.print(" with location (");
              Serial.print(row[RegisteredCount]);
              Serial.print(", ");
              Serial.print(column[RegisteredCount]);
              Serial.println(")");

              RegisteredCount++;  // Increment registered module count
            }
          }
        }
      }

      // After checking for connected modules, turn off the pin on the current module
      Wire.beginTransmission(RegisteredModules[i]);
      Wire.write(0);  // Turn off the pin
      Wire.endTransmission();
    }
  }
}

void setup() {
  Wire.begin();        // Join I2C bus as master
  Serial.begin(9600);  // Start serial communication for debugging
  Serial.println("I2C Address Scanner with Address Mapping");

  scanModules();  // Scan all connected modules and print them

  findFirstModule();  // Find the first connected module and assign it to (0,0)
}

void loop() {
  findConnectedModules();  // Keep searching for connected modules
  delay(5000);             // Pause between scans for readability
}

Slave Code:

#include <Wire.h>

  int locationPins[] = { 2, 3, 4, 5 };  // Example pin numbers for UP, RIGHT, DOWN, LEFT
int SLAVE_ADDRESS = 3;                  // Set the slave's I2C address

void setup() {
  Wire.begin(SLAVE_ADDRESS);     // Join I2C bus with the specified slave address
  Wire.onReceive(receiveEvent);  // Register receive event handler
  Wire.onRequest(requestEvent);  // Register request event handler

  // Initialize the location pins as inputs
  for (int i = 0; i < 4; i++) {
    pinMode(locationPins[i], INPUT);
  }

  Serial.begin(9600);  // Start serial communication for debugging
  Serial.print("I2C Slave ready at address: ");
  Serial.println(SLAVE_ADDRESS);
}

void loop() {
  // Main loop does nothing, all work is done in the event handlers
}

// Function to respond to requests from the master
void requestEvent() {
  for (int i = 0; i < 4; i++) {
    if (digitalRead(locationPins[i]) == HIGH) {
      Wire.write(1);  // Send 1 if the location pin is HIGH
      return;
    }
  }
  Wire.write(0);  // Send 0 if none of the pins are HIGH
}

// Function to receive commands from the master
void receiveEvent(int howMany) {
  if (Wire.available()) {
    int pin = Wire.read();  // Read the pin number from the master
    if (pin > 0 && pin <= 4) {
      pinMode(locationPins[pin - 1], OUTPUT);
      digitalWrite(locationPins[pin - 1], HIGH);  // Activate the pin
      Serial.print("Activated pin ");
      Serial.println(pin);
    } else if (pin == 0) {
      // Deactivate all location pins
      for (int i = 0; i < 4; i++) {
        digitalWrite(locationPins[i], LOW);
      }
      Serial.println("Deactivated all pins");
    }
  }
}

please be aware that im not sure if its correct but i test the detection and it works but the location is wrong

Thank you for bringing that up. Yes, it would be around 10 meters. Are there any other communication methods that could achieve a better distance?

Hi, @jewey98
Welcome to the forum.

As mentioned I2C is not really suitable for long distances.
RS485 would be better I would think, using twisted wire.

Please do not ask chatGPT to write a mass of code for a project like yours.

You need to write it yourself and in stages so you understand how it works and it functions to your requirements.

What are the sensors?
Can you please tell us your complete project and its aims?

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

Thanks for reply Tom!

I'll look into RS485 further.

For now, the slave devices are empty, and my goal is to determine their location within the grid for the next stage. Each slave will eventually have a different sensor or load, all controlled by the master.

For example, the slave with address 0x22 might control a light, while 0x21 might control a fan. These devices will be automated and managed based on calculations (which I'm working on) from the master.

That is possibly due to the fact that the chatGpt code uses pins 2..5 and not pins A2..A5. Further you need to be aware that A4 and A5 on Uno and Nano are the I2C pins so you can not use them for anything else. Did you try pins 2..5 on the slaves?
The would just be a learning exercise.

Regarding RS485

  1. With Unos and Nanos you need to be aware that there is only one UART and it's used for communication with the PC; you more than likely do not want to loose the debugging capability and will have to resort to two other pins and SoftwareSerial.
  2. As alternatives to the Uno and Nanos you can consider a Leonardo or Micro; an Nano Every might also be an option but you will have to do research if the libraries for the sensors that you intend to use work with that board.
    With those boards, you have a free UART (Serial1, not used for communication with the PC).

I'm sorry, it was my mistake mentioning the pins. I used pins 2, 3, 4, and 5, and the connections are correct with those pins. Sometimes, I even get the location right, but when I add one more slave, the location becomes inaccurate.

From what I understand, RS485, UART, and I2C are all serial communication protocols but with different methods. I can't use RS485 or UART because they're already being used for Serial.print (for debugging), correct? Do they offer better reliability and longer distance?

I don't mind getting new boards if it will solve the issue. I also have a new Arduino Uno R4 WiFi on hand—will that make any difference?

Correct; but you can use SoftwareSerial (on two other pins) at a reasonable low baud rate (9600, 19200). The low baud rate is not necessarily a disadvantage.

SoftwareSerial provides a UART in software; you connect RS485 adaptors to it. It gets tricky when you need multiple on a single processor; hardware UARTs are from that perspective easier to work with.

The UART is a 5V signal and prone to interference from the outside world on longer distances. If you have your system laying on your desk and distances are short, that will not be a big problem. But you can not connect multiple Tx pins together; if one Nano is transmitting a bit that is 0, it will fight with another Nano that is keeping the Tx line high (1) while not transmitting.

I2C and RS485 are designed to have multiple devices on the bus. RS485 uses a twisted pair and differential inputs/outputs which makes it less prone to disturbances over longer distances; the same disturbance will be on both wires and will be eliminated by the differential receiver and distance can go up to 1000 meters.

You might also experience problems with you signals on pins 2..5 over longer distances. There are ways around that but I'm not much of an electrical engineer; others might be able to advise.

The Uno R4 WiFi will not make much difference but you (to my knowledge) will have the additional UART on pins 0 and 1 so can use a UART (Serial1).

Not being able to debug using Serial is a pain that you can overcome with SoftwareSerial. Once fully debugged and you have no need for the debugging (or uploading), it will be reasonably easy to use the hardware UART.

Thank you sterretje for the detailed response!

From my understanding, a low baud rate means increased latency, which is acceptable since the project doesn't require real-time processing.

The master will request data from one slave at a time, close the communication afterward, and then initiate communication with another slave. Since there's no need to retrieve data from multiple slaves simultaneously, this approach shouldn't pose any issues for my project.

That's great! However, if I use the main UART for communication, can I still connect a screen to display data from the master using SoftwareSerial?

A slave that does not transmit data keeps Tx HIGH. Another slave that does transmit data at that time will send HIGHs (OK) and LOWs which will cause problems; it might result in damaged Arduinos.

In theory; but if there is a need for a computer to display information received from the master, I would use the Uno R4 WiFi as the master.

Oh, I understand. I'll look into the protocol you mentioned and try to apply it while writing the code {myself this time}. I'll share it later.

Thanks again for your helpful support!

Rather than reinvent the wheel, you might consider the Modbus protocol. There are quite a few Arduino libraries and examples available.

Interisting! ill give it a try,
Thanks jim.

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.