RS485 multiple NPK Sensors on Arduino Uno R3

Hi there, I have three NPK sensors from DFRobot and I am attempting to get them all working on one RS485 MAX module. With testing I have found one sensor works fine, but when I use two sensors the result shift down by one position, and will add together as seen below. All three sensors are powered on the same benchtop power supply at 20V with current fluctuating between 30-60mA.

I need some help figuring out if its the code that’s an issue, or the way I have setup my circuit.

Typical Results:

Sensor 1

FF FF FF FF FF FF FF

383838080FC66

132017F98E

Nitrogen: 255 mg/kg

Phosphorous: 128 mg/kg

Potassium: 23 mg/kg

Sensor 2

11 37 26 4 E4 BC 22

FFFFFFFFFFFFFF

FFFFFFFFFFFFFF

Nitrogen: 228 mg/kg

Phosphorous: 255 mg/kg

Potassium: 255 mg/kg

Here is the code I am using:

#include <SoftwareSerial.h>// for RS485 MAX module 

//Define pins for RS485
#define DI 2 // RX Pin
#define DE 3 //Drive enable
#define RE 4 //Recieve enable
#define R0 5 // TX Pin
//Software serial object to communicate with MAX485
SoftwareSerial mod(R0,DI); 

/*                                                         |CRC16 / IBM MODBUS check sum split in two 8 bit portions|
Address | Function | Return the            | temporary     | Low bit of | High bit      |
code    | code     | number of valid bytes | storage value | check code | of check code |
*/ 	 	 	 	 	
//const byte code[]= {0x01, 0x03, 0x00, 0x1e, 0x00, 0x03, 0x65, 0xCD};
/*https://crccalc.com/?crc=0x03,0x03,%200x00,%200x1f,%200x00,%200x01&method=CRC-16/MODBUS&datatype=hex&outtype=hex 
the link above checks cycle redundancy check sum for the first 6 numbers in the arrays below
*/
//sensor 1
const byte nitro1[] = {0x01,0x03, 0x00, 0x1e, 0x00, 0x01, 0xe4, 0x0c};
const byte phos1[] = {0x01,0x03, 0x00, 0x1f, 0x00, 0x01, 0xb5, 0xcc}; //CRC-16 modbus HEX shows 0xCCB5 or 0xb5, 0xcc
const byte pota1[] = {0x01,0x03, 0x00, 0x20, 0x00, 0x01, 0x85, 0xc0};
 
//sensor 2
const byte nitro2[] = {0x02,0x03, 0x00, 0x1e, 0x00, 0x01, 0xe4, 0x3f};
const byte phos2[] = {0x02,0x03, 0x00, 0x1f, 0x00, 0x01, 0xb5, 0xff};
const byte pota2[] = {0x02,0x03, 0x00, 0x20, 0x00, 0x01, 0x85, 0xf3};

//sensor 3
const byte nitro3[] = {0x03,0x03, 0x00, 0x1e, 0x00, 0x01, 0xe5, 0xee};
const byte phos3[] = {0x03,0x03, 0x00, 0x1f, 0x00, 0x01, 0xb4, 0x2e};
const byte pota3[] = {0x03,0x03, 0x00, 0x20, 0x00, 0x01, 0x84, 0x22};
byte values[11];

void setup() {
  // put your setup code here, to run once:
  
  //initialse serial communication
  Serial.begin(9600);
  mod.begin(9600);

  pinMode(DE, OUTPUT);
  pinMode(RE, OUTPUT);

  digitalWrite(DE, LOW);
  digitalWrite(RE, LOW);


  int num=5;
  while(num > 0){
    Serial.println(num);
    delay(1000);
    num--;
  }

}

void loop() {
  byte val1,val2,val3;
  
 
  Serial.println("Sensor 1");
  val1 = nitrogen(nitro1, sizeof(nitro1));
  delay(250);
  val2 = phosphorous(phos1, sizeof(phos1));
  delay(250);
  val3 = potassium(pota1, sizeof(pota1));
  delay(250);
  printNPK(val1, val2, val3);

  Serial.println("Sensor 2");
  val1 = nitrogen(nitro2, sizeof(nitro2));
  delay(250);
  val2 = phosphorous(phos2, sizeof(phos2));
  delay(250);
  val3 = potassium(pota2, sizeof(pota2));
  delay(250);
  printNPK(val1, val2, val3);
/*
  Serial.println("Sensor 3");
  val1 = nitrogen(nitro3, sizeof(nitro3));
  delay(250);
  val2 = phosphorous(phos3, sizeof(phos3));
  delay(250);
  val3 = potassium(pota3, sizeof(pota3));
  delay(250);
  printNPK(val1, val2, val3);
*/
  


}
 
byte nitrogen(const byte *nitro, byte len) {
  memset(values, 0, sizeof(values));  // Clear the buffer first
  digitalWrite(DE, HIGH); // send signal
  digitalWrite(RE, HIGH); // recieve signal
  delay(10);
  
  if (mod.write(nitro, len) == 8) { //checking for 8-byte data return 
    digitalWrite(DE, LOW); 
    digitalWrite(RE, LOW);
    
    // Print all received bytes
    for (byte i = 0; i < 7; i++) {
      values[i] = mod.read();
      Serial.print(values[i], HEX);
      Serial.print(" ");
    }
    Serial.println();
  }
  
  return values[4]; // This assumes the NPK value is always at this position, check this
}
 
byte phosphorous(const byte *phos, byte len){
  memset(values, 0, sizeof(values));  // Clear the buffer first
  digitalWrite(DE,HIGH);
  digitalWrite(RE,HIGH);
  delay(10);
  if(mod.write(phos,len)==8){
    digitalWrite(DE,LOW);
    digitalWrite(RE,LOW);
    for(byte i=0;i<7;i++){
    //Serial.print(mod.read(),HEX);
    values[i] = mod.read();
    Serial.print(values[i],HEX);
    }
    Serial.println();
  }
  return values[4];
}
 
byte potassium(const byte *pota, byte len){
  memset(values, 0, sizeof(values));  // Clear the buffer first
  digitalWrite(DE,HIGH); //drive enabled
  digitalWrite(RE,HIGH); //recieve enabled
  delay(10); //stabilisation
  if(mod.write(pota,len)==8){ //sends pota array modbus request 8 bytes
    digitalWrite(DE,LOW); //drive disabled
    digitalWrite(RE,LOW); //recieve disabled
    for(byte i=0;i<7;i++){ //reads 8 byte array from sensor 
    //Serial.print(mod.read(),HEX);
    values[i] = mod.read();
    Serial.print(values[i],HEX);
    }
    Serial.println();
  }
  return values[4];
}

void printNPK(byte val1, byte val2, byte val3){
    Serial.print("Nitrogen: ");
  Serial.print(val1);
  Serial.println(" mg/kg");
  Serial.print("Phosphorous: ");
  Serial.print(val2);
  Serial.println(" mg/kg");
  Serial.print("Potassium: ");
  Serial.print(val3);
  Serial.println(" mg/kg");
  delay(200);

}

Circuit setup:


any help would be appreciated (>'-'<).

While everyone looks at your code, post a wiring diagram. The pictures do not show much information.

Have you calibrated each device against scientifically gathered data? Does each device report data true to the experiment, individually?

When you see 0xFF or 255, that tends to indicate "device not connected" which you can verify by disconnecting the device(s).

1 Like

Thanks for your response.

"While everyone looks at your code, post a wiring diagram. The pictures do not show much information."

Is this okay for a wiring diagram?

"Have you calibrated each device against scientifically gathered data? Does each device report data true to the experiment, individually?"

I am not at the point of calibration yet, I am just trying to get the sensors working first then I will do calibration.

"When you see 0xFF or 255, that tends to indicate "device not connected" which you can verify by disconnecting the device(s)."

This is where I feel like I have an error in the code I have removed a sensor while still polling each sensor as seen in the code. But the three output values will increase and decrease based on there being one or two sensors.

For example one sensor

Sensor 1

FF FF FF FF FF FF FF

5400A9F4FFFF

9222A07450FF

Nitrogen: 255 mg/kg

Phosphorous: 78 mg/kg

Potassium: 52mg/kg

Sensor 2

1 40 0 20 2 39 C4

FFFFFFFFFFFFFF

FFFFFFFFFFFFFF

Nitrogen: 1 mg/kg

Phosphorous: 255 mg/kg

Potassium: 255 mg/kg

Two sensors

Sensor 1

FF FF FF FF FF FF FF

5400A9F4FFFF

9222A07450FF

Nitrogen: 255 mg/kg

Phosphorous: 244 mg/kg

Potassium: 116 mg/kg

Sensor 2

1 40 0 20 2 39 C4

FFFFFFFFFFFFFF

FFFFFFFFFFFFFF

Nitrogen: 2 mg/kg

Phosphorous: 255 mg/kg

Potassium: 255 mg/kg

Your 3 routines to request the N,P & K values have issues.

Firstly, you need to know that the modbus values that come back are 16-bit values, not 8-bit values. You need to combine 2 bytes to get the complete 16-bit value.

Secondly, your serial handling has a major flaw. You write out the command message and then instantly start reading bytes back in. You will probably get FF (-1) as the serial port is trying to tell you that no data is available.

The problem then gets worse when the second command message (phosphorus) goes out. The serial port likely now has received all the bytes from the first command message (nitrogen). You then read in these values and assign them as the phosphorus value when they are really the nitrogen value.

1 Like

Yes they will. When using an RS485 bus, each individual device on that bus needs to have a unique identifier. From the look of those sensors there is nothing to differentiate one device from another, hence they all respond.

So basically you can't wire it like you have done and get the results you want. You have to use one sensor per RS485 bus, and so have two more buses. Or you have to have some separate sort of logic around your NPK sensors to give them an identity (address). Then you have to add in this address into your RS485 protocol (software).

I don't know how this last part would be done, that is a design problem you have. Maybe this could be a small Arduino like the ATtiny85 as a device to hang your sensors off.

I used the RS485 bus extensively when I worked in the access control industry. Each device on the bus was actually a door controller, so it had its own identity. The software was not easy.

1 Like

@169an as @Grumpy_Mike says, you need to assign a different address to each of your sensors. The factory default is 1, so you need to figure out how to assign address 2 to a second NPK sensor and address 3 to the third NPK sensor.

That shouldn't be a problem because the user manual you got with the sensor should detail how to do this. You did get a user manual with your sensor didn't you.....

1 Like

You need to connect the sensors one by one and write the new slave address to register holding that. Or use a PC software if you have it.

1 Like

You can find the address protocol on this page:-
Sen0605
Under the communications protocols section.

1 Like

so the command to change slave address to 2 should be:
{0x01,0x06, 0x07, 0xD0, 0x00, 0x02, 0x02, 0x73}

1 Like

Okay so grab both 8-bit values. Combine them into a 16-bit value to readout, then add a delay till the data has been fully received before moving onto the next one?

Thanks for this, I initially had a RS485 bus per sensor. But I was unsure if that was right, as I was only getting a response from one sensor. I will look design options as well.

I was actually given these sensors and found the documentation online, its the one @Grumpy_Mike has linked in the comments. I will give another read on assigning addresses, looks like I based the slave addresses wrongly.

Is this software from an arduino library example or something like this:

The simple fix is to send the request, delay for a period of time and then check whether the expected number of bytes have been received.

You can make an educated guess as to the amount of time to wait: sensor response time + (expected bytes / ( baud rate / 10)). If the timing of your setup isn't critical, then delay for 100ms or so.8

1 Like

No. But I have seen that some of these sensors came with PC-software.Like here:

Anyway, you can use any modbus-RTU tool like free CAS Modbus Scanner to read the registers if you have USB dongle.
Or just send the command I posted above.

Hi all,

good news, I finally have the sensors working and am able to progress with my experiments. Thanks everyone for the help much appreciated ☆*: .。. o(≧▽≦)o .。.:*☆.

My next stage of testing is preliminary trials of NPK sensors. Then comparing those with chemical analytical techniques such as Olsen P.

@Grumpy_Mike, luckily I did not need to change my setup so it's the same as in the schematic I sent earlier. Luckily @kmin led me on the right path. However I had to redo the CRC check since this was not changing the address.

this is the command with the correct CRC low and high:
{0x01,0x06, 0x07, 0xD0, 0x00, 0x02, 0x08, 0x86}

CRC-16 website (Specify Modbus): https://crccalc.com/

This is the NPK sensor address change code:

#include <SoftwareSerial.h> // RS485

#define DI 2
#define DE 3
#define RE 4
#define R0 5

SoftwareSerial mod(R0, DI);
//CRC check website https://crccalc.com/?crc=&method=CRC-16/MODBUS&datatype=hex&outtype=hex
const byte readd[] = {0x01,0x06, 0x07, 0xD0, 0x00, 0x03, 0xC9, 0x46}; // change 1 -> 3,  CRC-16 check	0x46C9 -> 0xC9, 0x49
//{0x01,0x06, 0x07, 0xD0, 0x00, 0x02, 0x08, 0x86} // change 1 -> 2,  	0x8608 CRC-16 check --> 0x08, 0x86

byte values[11];

void printFrame(const byte *frame, byte len, const char* label) {
  Serial.print(label);
  Serial.print(": ");
  for (byte i = 0; i < len; i++) {
    if (frame[i] < 0x10) Serial.print('0');
    Serial.print(frame[i], HEX);
    Serial.print(" ");
  }
  Serial.println();
}

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

  pinMode(DE, OUTPUT);
  pinMode(RE, OUTPUT);

  digitalWrite(DE, LOW);
  digitalWrite(RE, LOW);

  Serial.println("Powering up sensor...");
  delay(8000);  // longer warm-up

  Serial.println("Starting readdress attempt...");

  memset(values, 0, sizeof(values));

  // Print what we're sending
  printFrame(readd, sizeof(readd), "Command Sent");

  // Send command
  digitalWrite(DE, HIGH);
  digitalWrite(RE, HIGH);
  delay(10);

  mod.write(readd, sizeof(readd));
  mod.flush();

  digitalWrite(DE, LOW);
  digitalWrite(RE, LOW);

  // Read response
  unsigned long startTime = millis();
  byte i = 0;
  while (i < 8 && (millis() - startTime < 1000)) {
    if (mod.available()) {
      values[i++] = mod.read();
    }
  }

  // Print response
  printFrame(values, i, "Response Received");

  if (i == 8 && values[0] == 0x01 && values[1] == 0x06) {
    Serial.print("Address change to ");
    Serial.print(values[5]);
    Serial.println(" succeeded!");
  } else {
    Serial.print("Address change to ");
    Serial.print(values[5]);
    Serial.println(" failed");  
  }
}

void loop() {
}

Hi @markd833, I used this code above for reading data I will tighten the read wait time eventually, for now this works fine.

  // Read response
  unsigned long startTime = millis();
  byte i = 0;
  while (i < 8 && (millis() - startTime < 1000)) { 
    if (mod.available()) {
      values[i++] = mod.read();
    }
  }

Finally, here's the working code for three NPK sensors using one RS485 max connected to an Arduino Uno R3:

#include <SoftwareSerial.h>// for RS485 MAX module 

//Define pins for RS485
#define DI 2 // RX Pin
#define DE 3 //Drive enable
#define RE 4 //Recieve enable
#define R0 5 // TX Pin
//Software serial object to communicate with MAX485
SoftwareSerial mod(R0,DI); 

/*                                                         |CRC16 / IBM MODBUS check sum split in two 8 bit portions|
Address | Function | Return the            | temporary     | Low bit of | High bit      |
code    | code     | number of valid bytes | storage value | check code | of check code |
*/ 	 	 	 	 	
//const byte code[]= {0x01, 0x03, 0x00, 0x1e, 0x00, 0x03, 0x65, 0xCD};
/*https://crccalc.com   
the link above checks cycle redundancy check sum for the first 6 numbers in the arrays below
*/
//sensor 1
const byte nitro1[] = {0x01,0x03, 0x00, 0x1e, 0x00, 0x01, 0xe4, 0x0c};
const byte phos1[] = {0x01,0x03, 0x00, 0x1f, 0x00, 0x01, 0xb5, 0xcc}; //CRC-16 modbus HEX shows 0xCCB5 or 0xb5, 0xcc
const byte pota1[] = {0x01,0x03, 0x00, 0x20, 0x00, 0x01, 0x85, 0xc0};
 
//sensor 2
const byte nitro2[] = {0x02,0x03, 0x00, 0x1e, 0x00, 0x01, 0xe4, 0x3f};
const byte phos2[] = {0x02,0x03, 0x00, 0x1f, 0x00, 0x01, 0xb5, 0xff};
const byte pota2[] = {0x02,0x03, 0x00, 0x20, 0x00, 0x01, 0x85, 0xf3};

//sensor 3
const byte nitro3[] = {0x03,0x03, 0x00, 0x1e, 0x00, 0x01, 0xe5, 0xee};
const byte phos3[] = {0x03,0x03, 0x00, 0x1f, 0x00, 0x01, 0xb4, 0x2e};
const byte pota3[] = {0x03,0x03, 0x00, 0x20, 0x00, 0x01, 0x84, 0x22};

//const byte sensor_address_2[] = {0x01,0x06, 0x07, 0xD0, 0x00, 0x02, 0x02, 0x73}; //Set Sensor address to 2 or 0x02
//const byte sensor_address_3[] = {0x01, 0x06, 0x07, 0xD0, 0x00, 0x03, 0x43, 0xB3};  // Set Sensor address to 3 or 0x03
byte values[11];



void setup() {
  // put your setup code here, to run once:
  
  //initialse serial communication
  Serial.begin(9600);
  mod.begin(9600);

  pinMode(DE, OUTPUT);
  pinMode(RE, OUTPUT);

  digitalWrite(DE, LOW);
  digitalWrite(RE, LOW);


  int num=5;
  while(num > 0){
    Serial.println(num);
    delay(1000);
    num--;
  }

}

void loop() {
  byte val1,val2,val3;
  
  Serial.println("Sensor 1");
  val1 = readNPK(nitro1, sizeof(nitro1),"Nitrogen");
  delay(250);
  val2 = readNPK(phos1, sizeof(phos1),"Phosphorous");
  delay(250);
  val3 = readNPK(pota1, sizeof(pota1),"Potassium");
  delay(250);
  printNPK(val1, val2, val3);

  Serial.println("Sensor 2");
  val1 = readNPK(nitro2, sizeof(nitro1),"Nitrogen");
  delay(250);
  val2 = readNPK(phos2, sizeof(phos1),"Phosphorous");
  delay(250);
  val3 = readNPK(pota2, sizeof(pota1),"Potassium");
  delay(250);
  printNPK(val1, val2, val3);

  Serial.println("Sensor 3");
  val1 = readNPK(nitro3, sizeof(nitro1),"Nitrogen");
  delay(250);
  val2 = readNPK(phos3, sizeof(phos1),"Phosphorous");
  delay(250);
  val3 = readNPK(pota3, sizeof(pota1),"Potassium");
  delay(250);
  printNPK(val1, val2, val3);

}
 
int readNPK(const byte *query, byte len, const char* label) {
  memset(values, 0, sizeof(values));  // Clear buffer before use

  // Enable transmit mode (send request)
  digitalWrite(DE, HIGH); //enable 
  digitalWrite(RE, HIGH);
  delay(10);

  mod.write(query, len);
  mod.flush();

  // Enable receive mode
  digitalWrite(DE, LOW);
  digitalWrite(RE, LOW);

  // Read response with timeout
  unsigned long startTime = millis();
  byte i = 0;
  while (i < 7 && (millis() - startTime < 1000)) {
    if (mod.available()) {
      values[i++] = mod.read();
    }
  }

  // Print received response
  Serial.print(label);
  Serial.print(" response: ");
  for (byte j = 0; j < i; j++) {
    Serial.print(values[j], HEX);
    Serial.print(" ");
  }
  Serial.println();

  // Validate response
  if (i == 7 && values[1] == 0x03) {
    return (values[3] << 8) | values[4];
  } else {
    Serial.print(label);
    Serial.println(" response invalid or incomplete");
    return 255;
  }
}


void printNPK(byte val1, byte val2, byte val3){
  Serial.print("Nitrogen: ");
  Serial.print(val1);
  Serial.println(" mg/kg");
  Serial.print("Phosphorous: ");
  Serial.print(val2);
  Serial.println(" mg/kg");
  Serial.print("Potassium: ");
  Serial.print(val3);
  Serial.println(" mg/kg");
  delay(200);

}



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