Arduino Nano RP2040: NPK Modbus Serial Communication

Hello,
Since the Arduino Nano RP2040 doesn't support SoftwareSerial, I need help writing a serial communication program using its hardware UART ports. Here's an example of how you can modify the code from the link you provided to use the hardware UART:
I am working on modifying this code: Measure Soil Nutrient using Arduino & Soil NPK Sensor
to read NPK an sensor from the Nano rp2040

Here is the recommended Pseudocode from Arduino:

#include <Arduino.h>
#include <Wire.h>
#include <SimpleModbusMaster.h>

// Define the NPK sensor's Modbus address
#define SENSOR_ADDRESS 1

// Define the hardware UART port to use
#define SERIAL_PORT Serial1

// ModbusMaster object
ModbusMaster mod;

// Variables to store sensor readings
uint16_t npkValue = 0;

void setup() {
  // Initialize the hardware UART port
  SERIAL_PORT.begin(9600);

  // Initialize the Modbus communication
  mod.begin(SENSOR_ADDRESS, SERIAL_PORT);

  // Set the Modbus timeout and baud rate
  mod.setTimeOut(1000);
  mod.setBaudrate(9600);
}

void loop() {
  // Read the NPK sensor value
  int8_t result = mod.readHoldingRegisters(0x0001, 1); // Read one register starting from address 0x0001

  // Check if the reading was successful
  if (result == mod.ku8MBSuccess) {
    npkValue = mod.getResponseBuffer(0); // Get the value from the response buffer
    // Process the NPK value as needed
    Serial.print("NPK Value: ");
    Serial.println(npkValue);
  } else {
    // Failed to read the NPK value
    Serial.println("Failed to read NPK value");
  }

  // Delay before the next reading
  delay(1000);
}

And here is the code I am trying to adjust without using the <SoftwareSerial.h> library:

#include <SoftwareSerial.h>
#include <Wire.h>
 
// Define RS485 pins for RE and DE to switch between transmit and receive mode
#define RS485_RE 8
#define RS485_DE 7
 
// Modbus RTU requests for reading NPK values
const byte nitro[] = {0x01, 0x03, 0x00, 0x1e, 0x00, 0x01, 0xe4, 0x0c};
const byte phos[] = {0x01, 0x03, 0x00, 0x1f, 0x00, 0x01, 0xb5, 0xcc};
const byte pota[] = {0x01, 0x03, 0x00, 0x20, 0x00, 0x01, 0x85, 0xc0};
 
// A byte array to store NPK values
byte values[11];
 
// SoftwareSerial object to communicate with the RS485 module
SoftwareSerial modbus(2, 3); // RX, TX
 
void setup() {
  // Start serial communication with the computer
  Serial.begin(9600);
 
  // Start serial communication with the RS485 module
  modbus.begin(9600);
 
  // Set RS485 pins as outputs
  pinMode(RS485_RE, OUTPUT);
  pinMode(RS485_DE, OUTPUT);
 
  // Turn off RS485 receiver and transmitter initially
  digitalWrite(RS485_RE, LOW);
  digitalWrite(RS485_DE, LOW);
 
  // Wait for the RS485 module to initialize
  delay(500);
}
 
void loop() {
  // Read NPK values and print them to the serial monitor
  Serial.print("Nitrogen: ");
  Serial.print(readValue(nitro));
  Serial.println(" mg/kg");
 
  Serial.print("Phosphorous: ");
  Serial.print(readValue(phos));
  Serial.println(" mg/kg");
 
  Serial.print("Potassium: ");
  Serial.print(readValue(pota));
  Serial.println(" mg/kg");
 
  // Wait for 2 seconds before reading values again
  delay(2000);
}
 
// Sends a Modbus RTU request and reads the response to get a value
byte readValue(const byte* request) {
  // Set RS485 module to transmit mode
  digitalWrite(RS485_RE, HIGH);
  digitalWrite(RS485_DE, HIGH);
 
  // Send Modbus RTU request to the device
  modbus.write(request, sizeof(request));
 
  // Set RS485 module to receive mode
  digitalWrite(RS485_RE, LOW);
  digitalWrite(RS485_DE, LOW);
 
  // Wait for the response to be received
  delay(10);
 
  // Read the response into the values array
  byte responseLength = modbus.available();
  for (byte i = 0; i < responseLength; i++) {
    values[i] = modbus.read();
  }
 
  // Return the value from the response
  return values[3] << 8 | values[4];
}

Any help is much appreciated!
\Thanks!

You want to use the second code instead of the first?
Do you realize that the second sketch does send byte arrays and constructs the resulting value by reading some bytes at an index. Completely unflexible and unmaintainable.

Use the first sketch and the library used there. I would change the 3 read requests into one because the 3 registers are consecutive.

2 Likes

Hi Pylon,
May you please demonstrate how to adjust the first code with a single read?
Moreover, how to actually read the sensor connected to the modbus from the arduino nano?

I’m a bit confused and very thankful for the help!

//#include <SoftwareSerial.h>
#include <Wire.h>
 
// Define RS485 pins for RE and DE to switch between transmit and receive mode
#define RS485_RE 8
#define RS485_DE 7
 
// Modbus RTU requests for reading NPK values
const byte nitro[] = {0x01, 0x03, 0x00, 0x1e, 0x00, 0x01, 0xe4, 0x0c};
const byte phos[] = {0x01, 0x03, 0x00, 0x1f, 0x00, 0x01, 0xb5, 0xcc};
const byte pota[] = {0x01, 0x03, 0x00, 0x20, 0x00, 0x01, 0x85, 0xc0};
 
// A byte array to store NPK values
byte values[11];
 
// SoftwareSerial object to communicate with the RS485 module
//SoftwareSerial modbus(2, 3); // RX, TX
#define modbus Serial1
 
void setup() {
  // Start serial communication with the computer
  Serial.begin(9600);
 
  // Start serial communication with the RS485 module
  modbus.setRx(5); 
  modbus.setTx(4); 
  modbus.begin(9600);
 
  // Set RS485 pins as outputs
  pinMode(RS485_RE, OUTPUT);
  pinMode(RS485_DE, OUTPUT);
 
  // Turn off RS485 receiver and transmitter initially
  digitalWrite(RS485_RE, LOW);
  digitalWrite(RS485_DE, LOW);
 
  // Wait for the RS485 module to initialize
  delay(500);
}
 
void loop() {
  // Read NPK values and print them to the serial monitor
  Serial.print("Nitrogen: ");
  Serial.print(readValue(nitro));
  Serial.println(" mg/kg");
 
  Serial.print("Phosphorous: ");
  Serial.print(readValue(phos));
  Serial.println(" mg/kg");
 
  Serial.print("Potassium: ");
  Serial.print(readValue(pota));
  Serial.println(" mg/kg");
 
  // Wait for 2 seconds before reading values again
  delay(2000);
}
 
// Sends a Modbus RTU request and reads the response to get a value
byte readValue(const byte* request) {
  // Set RS485 module to transmit mode
  digitalWrite(RS485_RE, HIGH);
  digitalWrite(RS485_DE, HIGH);
 
  // Send Modbus RTU request to the device
  modbus.write(request, sizeof(request));
 
  // Set RS485 module to receive mode
  digitalWrite(RS485_RE, LOW);
  digitalWrite(RS485_DE, LOW);
 
  // Wait for the response to be received
  delay(10);
 
  // Read the response into the values array
  byte responseLength = modbus.available();
  for (byte i = 0; i < responseLength; i++) {
    values[i] = modbus.read();
  }
 
  // Return the value from the response
  return values[3] << 8 | values[4];
}

https://arduino-pico.readthedocs.io/en/latest/serial.html

Simply change the number of registers you want to read in this line:

int8_t result = mod.readHoldingRegisters(0x0001, 1); // Read one register

and then read the register values using multiple calls to getResponseBuffer passing the response number such that 0 = first register, 1 = second register etc.

Don't forget to change the address of the starting register to match your requirements.

We didn't get a wiring diagram of your setup yet. Given you connected the sensor correctly the code should read the sensor's registers. Your code reads register 1 in the first sketch but registers 0x1e to 0x20 on the second. We don't have a manual of that sensor you're using so we have no clue which registers might be of interest.

Thanks for all the support. I’m getting closer to understand the problem.

Here is the wiring schematic and manual for registers.

What pins should I connect the modbus to for serial communication and how should I leverage the existing libraries to read all registers of the modbus.

Essentially blend the original to codes to read all 6 registers.

Thanks a million!


I cannot find that in your post. There is only a picture of the pinout of your sensor. We have no clue what kind of RS-485 adapter you have (you haven't provided a link to it yet), nor how you connected that to your RP2040.

The manual of your sensor is unreadable on that screenshot. Post a link to the online manual or post the PDF of the manual to the forum!

New users may not upload attachments…
May I please share the manual separately?

  1. Product Information
    4.1 Technical parameters
    Measurement parameters: soil temperature, soil volumetric water content, soil conductivity (EC value), soil pH, soil nitrogen/phosphorus/potassium
    Measurement unit: °C; % (m3/m3); μS/cm; pH; mg/kg (mg/L)
    Temperature range: -30~70°C (0~50°C or any other range can be customized)
    Moisture range: 0~100% (30%, 50% and other ranges can be selected or any range can be customized) Conductivity range: 0~2000μS/cm, 0~10000μS/cm, 0~20000μS/cm
    pH value range: 3~10
    Nitrogen/phosphorus/potassium range: 0~1999mg/kg (mg/L)
    Measurement accuracy: ±0.2°C; ±2% (m3/m3) within the range of 0 to 50% (m3/m3); ±2%; ±1; ±3% (mg/kg)
    Resolution: 0.1°C; 0.1%; 1μS/cm; 0.01; 1mg/kg (mg/L)
    Output signal: RS485 (standard Modbus-RTU protocol, device default address: 01)
    Supply voltage: 12~24V DC
    Working range: -30°C~70°C
    Stabilization time: 3 seconds after power on Response time: <1 second
    4.2 Physical Parameter
    Probe length: 55mm, φ3mm
    Probe Material: 316L Stainless Steel
    Sealing material: ABS engineering plastic, epoxy resin, waterproof grade IP68
    Cable specification: standard 2 meters (other cable lengths can be customized, up to 1200 meters)

  2. Dimensions

  3. Wiring Definition
    The soil multi-parameter sensor can be connected to various data collectors with differential inputs, data acquisition cards, remote data acquisition modules and other equipment. The wiring method is as follows:

  4. Data Conversion Method
    RS485 signal (default address 01): Standard Modbus-RTU protocol, baud rate: 9600;
    parity bit: none;
    data bit: 8; stop bit: 1

6.1 Change address
For example: change the sensor with address 1 to address 2, master→slave
If the sensor receives the correct data, the data will be returned in the same way.
Note: If you forget the original address of the sensor, you can use the broadcast address 0XFE instead. When using 0XFE, the host can only connect to one slave, and the return address is still the original address, which can be used as a method for address query.
6.2 Query Data
Query the data of the sensor (address 1) (soil temperature, soil moisture, soil conductivity, soil pH, soil nitrogen, phosphorus, potassium), master → slave
If the sensor is received correctly, it will return the following data, slave → master
Original address
Function code
Start register high
Start register low
Start address high
Start address low
CRC16 low
CRC16 high
0X01
0X06
0X00
0X30
0X00
0X02
0X08
0X04
Address
Function code
Start register high
Start register low
Register length high
Register length low
CRC16 low
CRC16 high
0X01
0X03
0x00
0x00
0X00
0X07
0X04
0X08
address
0X01
Function code
0X03
Data length
0X0E
Register 0 Data high
0XFF
Soil temperature: -3.5°C
Negative numbers are represented by complement
Register 0 data low
0XDD
Register 1 data high
0X01
Soil Moisture: 35.6%
Register 1 data low
0X64
Register 2 data high
0X04
Soil EC: 1234μS/cm
Register 2 data low
0XD2
Register 3 data high
0X02
PH: 6.86
Register 3 data low
0XAE
Register 4 data high
0X00
Nitrogen: 135mg/kg
Register 4 data low
0X87
Register 5 data high
0X00
Phosphorus: 138mg/kg

 Register 5 data low

0X8A
Register 6 data high
0X00
Potassium :142mg/kg
Register 6 data low
0X8E
CRC16 low
0X34
CRC16 high
0X37

Is it possible that this link contains the manual?

In that case this code should return the measured values:

  int8_t result = mod.readHoldingRegisters(0x001E, 3); // Read three registers starting from address 0x001E

  // Check if the reading was successful
  if (result == mod.ku8MBSuccess) {
    uint16_t nitroValue = mod.getResponseBuffer(0); 
    uint16_t phosValue = mod.getResponseBuffer(1); 
    uint16_t potaValue = mod.getResponseBuffer(2); 
    Serial.print("NPK Values: ");
    Serial.print(nitroValue);
    Serial.print(", ");
    Serial.print(phosValue);
    Serial.print(", ");
    Serial.println(potaValue);
  } else {
    // Failed to read the NPK values
    Serial.println("Failed to read NPK values");
  }

Very similar. Here is the correct PDF link: Adobe Acrobat

I presume the code is just about the same.

int8_t result = mod.readHoldingRegisters(0x001E, 3); // Read three registers starting from address 0x001E

  // Check if the reading was successful
  if (result == mod.ku8MBSuccess) {
    uint16_t tempValue = mod.getResponseBuffer(0); 
    uint16_t moistValue= mod.getResponseBuffer(1);
    uint16_t ecValue = mod.getResponseBuffer(2); 
    uint16_t phValue = mod.getResponseBuffer(3); 
    uint16_t nitroValue = mod.getResponseBuffer(4); 
    uint16_t phosValue = mod.getResponseBuffer(5); 
    uint16_t potaValue = mod.getResponseBuffer(6); 

    Serial.print(tempValue);
    Serial.print(", ");
    Serial.print(moistValue);
    Serial.print(", ");
    Serial.print(ecValue);
    Serial.print(", ");
    Serial.print(phValue);
    Serial.print(", ");
    Serial.print(nitroValue);
    Serial.print(", ");
    Serial.print(phosValue);
    Serial.print(", ");
    Serial.println(potaValue);
  } else {
    // Failed to read the NPK values
    Serial.println("Failed to read NPK values");
  }

That line reads only 3 registers, not 7.

That document doesn't show the register table. Given that example is correct, the base register would be at 0x00 instead of the 0x001E my document showed. So post a link to the register table of that sensor and please no link to Adobe again (my browser knows how to display PDFs).

manual for bgt-smps soil multi-parameter sensor[1].pdf (295.3 KB)
Here's the PDF. Looks like Start register low and high are 0x00.

Please help me double check the remainder of the registers.
Much thanks,
David

That document is the same as on the Adobe website. It simply shows an example but no register table. Ask the vendor for that table, without it the product is unusable or at least not fully usable. You can try above code with register address 0x00, maybe that gives you some results but I would want complete documentation for the devices I bought.

Let me know if you are able to see the tables under 6.1 Change Address and 6.2 Query Data sections.

I believe these examples are the corresponding registers.

Yes, I see them, but that's an example and not a documentation.

It might look that way but if that's the only documentation you got the documentation is so bad that I wouldn't expect even that example to run correctly. Given that part is not from Alibaba or Amazon I would contact the vendor and request a complete documentation.

I am bettering my understanding to the point of guess and check with a separate temp sensor

For the code, there are a few things I need your help clarifying.
QUESTIONS INCLUDE:

  1. SHOULD WE BE USING THE SoftwareSerial VERSION WITH nano rp2040? GitHub - angeloc/simplemodbusng: Modbus RTU Slave/Master for the Arduino Platform
#include <SimpleModbusMaster.h>
#include <SimpleModbusMasterSoftwareSerial.h>
  1. WHAT PINS DO WE WIRE THE MOD TO THE NANO RP2040
//Define the NPK sensor's Modbus address
#define MOD_ADDR 1
//Define the hardware UART port to use
#define SERIAL_PORT Serial1
  1. WHAT SHOULD mod.setTimeOut(); be set to?
    The sensor needs 3 seconds after powering on
mod.setTimeOut(3000);
  1. SHOULD WE STORE READINGS AS UINT16_T?
    AND HOW SHOULD WE CONVERT UINT16_T TO DECIMAL
void loop() {
  printMod();
  delay(5000);
}

void printMod(){
  // Original Addresses: 0x01, 0xFE, 0x0000
  // Read all seven registers starting from 0x0000
  // >>> HERE IS WHERE I WILL GUESS AND CHECK FROM 0X0000->0XFFFF
  int8_t result = mod.readHoldingRegisters(0x0000, 7);

  //Check if the reading was successful
  if(result == mod.ku8MBSuccess){
    uint16_t temp = mod.getResponseBuffer(0);
    uint16_t moist = mod.getResponseBuffer(1);
    uint16_t ec = mod.getResponseBuffer(2);
    uint16_t ph = mod.getResponseBuffer(3);
    uint16_t n = mod.getResponseBuffer(4);
    uint16_t p = mod.getResponseBuffer(5);
    uint16_t k = mod.getResponseBuffer(6);
    //uint16_t : is not decimal nor hex -> it is 0-2,165,000,000
    Serial.print("UINT16_T VALUES:\n
                  temp:", temp, "| moist:",moist,"| ec:", ec,"| ph:", ph,"| n:",n,"| p:",p,"| k:",k);
    //??? SHOULD WE STORE AS UINT16_T? 
    //??? HOW SHOULD WE CONVERT UINT16_T TO DECIMAL 
    //Hex String -> Decimal
    // long decimal_answer=strtol("000507007", NULL, 16);
  }else{
    //Failed to read the NPK values
    Serial.println("Failed to read the NPK values");
    // add a -1 to the values and add error to the frontend...
  }
}

HERE IS THE ENTIRE CODE FOR BETTER REFERENCE

#include <Arduino.h>
#include <Wire.h>
#include <stdlib.h>
//Needs 12-24v Supply Voltage

//???SHOULD WE BE USING THE SoftwareSerial VERSION WITH nano rp2040?
//https://github.com/angeloc/simplemodbusng
#include <SimpleModbusMaster.h>
#include <SimpleModbusMasterSoftwareSerial.h>


//???HOW DO WE WIRE THE MOD TO THE NANO RP2040
//Define the NPK sensor's Modbus address
#define MOD_ADDR 1
//Define the hardware UART port to use
#define SERIAL_PORT Serial1

//ModbusMaster object
ModbusMaster mod;

//Variables to store the sensor readings
uin16_t npkValue = 0;


void setup() {
  //Initialize the hardware UART port
  SERIAL_PORT.begin(9600);

  //Initialize the Modbus communication
  mod.begin(MOD_ADDR, SERIAL_PORT);

  //Set the Modbus timeout and baud rate
  //??? WHAT IS THE TimeOut rate? the sensor needs 3 seconds before reading
  mod.setTimeOut(3000);
  mod.setBaudrate(9600);

  //Needs 3 seconds to power on
  delay(5000);
}

void loop() {
  printMod();
  delay(5000);
}

void printMod(){
  // Original Addresses: 0x01, 0xFE, 0x0000
  // Read all seven registers starting from 0x0000
  // >>> HERE IS WHERE I WILL GUESS AND CHECK FROM 0X0000->0XFFFF
  int8_t result = mod.readHoldingRegisters(0x0000, 7);

  //Check if the reading was successful
  if(result == mod.ku8MBSuccess){
    uint16_t temp = mod.getResponseBuffer(0);
    uint16_t moist = mod.getResponseBuffer(1);
    uint16_t ec = mod.getResponseBuffer(2);
    uint16_t ph = mod.getResponseBuffer(3);
    uint16_t n = mod.getResponseBuffer(4);
    uint16_t p = mod.getResponseBuffer(5);
    uint16_t k = mod.getResponseBuffer(6);
    //uint16_t : is not decimal nor hex -> it is 0-2,165,000,000
    Serial.print("UINT16_T VALUES:\n
                  temp:", temp, "| moist:",moist,"| ec:", ec,"| ph:", ph,"| n:",n,"| p:",p,"| k:",k);
    //??? SHOULD WE STORE AS UINT16_T? 
    //??? HOW SHOULD WE CONVERT UINT16_T TO DECIMAL 
    //Hex String -> Decimal
    // long decimal_answer=strtol("000507007", NULL, 16);
  }else{
    //Failed to read the NPK values
    Serial.println("Failed to read the NPK values");
    // add a -1 to the values and add error to the frontend...
  }
}

float cToF(float c){
  return (c*1.8)+32;
}


No, never use SoftwareSerial if you're not forced to.

D0/D1.

I think 50 is a reasonable value for the timeout. The startup time is completely irrelevant here, you might want to put a delay(3000) into setup() if you think you have to care for that.

Modbus register values are always of uint16_t type. The documentation should tell you how to convert that value to a meaningful one. As your vendor seems to be unable to provide you a documentation you might have to guess what values you get. BTW, uint16_t is an integer value so printing it always converts it into a decimal representation string.