Arduino Uno R3, MAX485 & Stepper Motor Absolute Encoder Reading Registers

Hi All,
I've been programming on a Arduino Uno R3 for about 2 months (a few hours a week learning / researching and coding). I have a Nema 17 stepper motor that I can control well using an A4988 or TB6600 driver. I recently undertook a project to read and use the data from the stepper motor's absolute encoder. The encoder can communicate digitally via RS485 MODBUS with the following specs:

A B C D E F G H I J K
Word Address Function Word Length Word 0 Word 1 Word 2 Word 3 Word 4 Word 5 Word 6 Word 7
0000h Encoder Data 3 Angle Data Turn Count Temperature ---- ---- ---- ---- ----
1000h Zero offset 1 01/02 ---- ---- ---- ---- ---- ---- ----

Angle data
Ranges from 0-65535. Example: HEX: C000 = 49152 angle value
Zero Offset
When 1 is written in zero offset, it will change the current position to main data angle = muti turn count = 0
When 2 is written in zero offset, magnetic sensor will adjust zero point only with main shaft, therefore, main angle data = 0
UART Specification
Data bit length: 8 bit
Data Transfer Direction: LSB
Parity: None
Stop bit : 1 bit
Baud rate: 115200 bps
MODBUS Specification
NODE: 01 fixed
Communication Mode: ASCII
Half duplex transmit/receive switching time: Max 1ms
(1/115200[bps] * 10[bit] * 1000 *3.5[word] = 0.304ms, expect to have enough margin)
Endianness (word, double-word): Big endianness, but CRC is fixed with little endianess

Arduino Uno R3 with MAX485 hookup is as follows:
MAX485:
B-> RS485- from encoder
A-> RS485+ from encoder
VCC -> 5V on Arduino Uno
GND -> GND on Arduino Uno

DI -> 7 on Arduino Uno
DE: -> 5 on Arduino Uno
RE: -> 5 on Arduino Uno
RO: -> 6 on Arduino Uno.

Code

#include <ModbusMaster.h>
#include <SoftwareSerial.h>

#define ENABLE485_RX 6  // EIA-485 serial receive pin
#define ENABLE485_TX 7 // EIA-485 serial transmit pin
#define MAX485_DE 5

// instantiate ModbusMaster object
ModbusMaster node;
SoftwareSerial RS485Serial(ENABLE485_RX, ENABLE485_TX); // RX, TX

void preTransmission()
{
  digitalWrite(MAX485_DE, 1);
}

void postTransmission()
{
  digitalWrite(MAX485_DE, 0);
}

void setup()
{

  pinMode(MAX485_DE, OUTPUT);
  digitalWrite(MAX485_DE, 0);

  Serial.begin(9600);
  RS485Serial.begin(115200);

  node.begin(1, RS485Serial);
  // Callbacks allow us to configure the RS485 transceiver correctly
  node.preTransmission(preTransmission);
  node.postTransmission(postTransmission);
}

void loop()
{
  uint8_t result;
  uint16_t data[6];

  // Read 02 registers starting at 03)
result = node.readHoldingRegisters(0x03,2);

Serial.println(node.getResponseBuffer(0x00));
Serial.println(node.getResponseBuffer(0x01));

}

Through a COM port sniffer, the encoder's output looks like this:
Modbus Request (COM3)
Address: 1
Function: 3 (0x03) - Read Holding Registers
Starting Address: 0
Quantity: 3
Checksum: 0x00f9 - OK


Modbus Response (COM3)
Address: 1
Function: 3 (0x03) - Read Holding Registers
Byte Count: 6
Values: ff 54 03 e7 00 ef
Register0: 65364
Register1: 999
Register2: 239
Checksum: 0x00ca - OK


Register 0 = Word0 angle data
Register1 = Word1 turn count data
Register2 = Word2 temperature data

I can read and understand the encoder data via a RS485 to USB converter cable. I am trying to have the Arduino read the same encoder RS485 data through a MAX485 board (Amazon.com) so that I can use the encoder rotational information when controlling the Nema 17 stepper motor. I have been researching for more than a week and tried numerous test codes but have not read the encoder data correctly yet.

I would greatly appreciate your guidance.
Thank you,
Robert

At a glance it looks pretty good to me. Here's some thoughts/potential issues:

  • Why do software serial? This will hurt performance compared to hardware serial, especially at if the peripheral is particularly timing-sensitive. I believe pins 0 and 1 are hardware RX/TX, and you can initialize it using Serial1(baud).

  • Not familiar with the Modbus library, so pardon if this is totally off-base. Your use of node.readHoldingRegisters() seems odd to me - it assumes that readHoldingRegisters stops and waits until all data has been read from the peripheral. Typically these sorts of transactions happen asynchronously (non-blocking), since IO operations take a very long time compared to the rest of the code. Usually the IO code is structured like:

    1. Initiate a "read" operation, which takes place in the background (via an interrupt); the function returns immediately.
    2. Check if the operation is complete.
    3. Consume the data only if the operation is complete.
    4. Start a new read.
      For example, when reading from a serial port, we usually write code like
    if (Serial.available()) {
        char inputByte = Serial.read();
        // process inputByte
    }
    

    because if we call Serial.read() without anything in the hardware buffer, it may return garbage; we have no idea unless we first ensure Serial.available().
    So (without knowing anything about this library), it would be strange for readHoldingRegisters to actually return data, as it would force your loop to idly wait for like 99% of the total available processing time.

  • The MAX485 uses DE and NOT(RE) as inputs, allowing them to be bridged, like you do here. But are we sure the module passes the DE/RI pins directly to the MAX485? If they chose to invert RE for "consistency" or some other reason, you'd have to wire DE and RE to two separate Arduino pins and write opposite values to toggle transmit/receive. Seems unlikely, but can't be too careful about making assumptions about random breakout boards.

Thank you for your reply and ideas. Much appreciated!!
I will try the hardware TX / RX and the "if(Serial.available()) too. Both make sense to me.

Do you have any thoughts about how to ensure reading the serial port words correctly? I did a bunch of research online looking at other posted codes and what they were reading; my code is honestly my best guess as I don't have any experience with byte register reading / writing and then separating the word into the three usable values. Any observations and suggestions would be most appreciated.

Thanks much.

Arduino uno has only one hardware serial and you use that for serial monitor.
115200 is probably too high for softwareserial.
Board with multiple hw serial ports woul be ideal.

Hi kmin...
I think you might be right. I read another forum topic (Unable to communicate with Arduino Uno using RS 485 to TTL module). There seems to be a lot of similarities with my challenges and this posting. In the end, a Mega was used to avoid the softwareSerial baud rate and other issues with the person having success.

I decided to go ahead and get a Mega as I will most likely need it in the future as this project grows, so might as well get it now. I will incorporate "ms_1" comments too regarding the "if" lookup to ensure there is actually data in the serial port before reading.

Attached is the encoder details (best I have).
Absolute Encoder Details.pdf (464.1 KB)

Attached is the MAX485 details.
MAX485 Module 5V logic TTL to RS-485.pdf (1.1 MB)

I would still be grateful if someone with knowledge about reading the MODBUS byte words and separating them into usable data could share their thoughts on the stepper motor encoder protocol. Thank you.

When I get the Mega hooked-up, I will post a schematic, etc.

I suggest that you just get your Mega reading bytes from the MAX485. Interpreting them should be fairly simple (I hope) once you can show us what was received.

It's good choice to have always Mega in drawer for prototyping, even if in final setup you decide to use some "smaller" board.

Got things working with the Mega 2560 Rev 3. :smiley:
Here is the schematic and code. I only posted the code for the RS485 encoder testing; the schematic shows the Nema 17 motor too.

#include <ModbusMaster.h>

#define MAX485_DE 3  // Mega pin 3
// MAX485 DE & RE connected together
// RS485 input (DI) to Mega pin 18 - Serial 1 transmit
// RS485 output (RO) to Mega pin 19 - Serial 1 receive

// Wait for Start Button to be pressed before beginning program
int start_button_pin = 8; // Mega pin 8
int start_button = 0;

// Encoder zero reset button defaults
int zero_reset_button_pin = 9; // Mega pin 9
unsigned long lastDebounceTime = 0;
unsigned long debounceDelay = 50; // Wait 50ms before accepting button press again
int buttonState = 0;
int lastButtonState = 0;
int currentButtonState = 0;

// RS485 communication data
uint8_t result;
uint8_t result_zero;
uint16_t data[2];

// instantiate ModbusMaster object
ModbusMaster node;

void preTransmission()
{
  digitalWrite(MAX485_DE, 1);
}

void postTransmission()
{
  digitalWrite(MAX485_DE, 0);
}

void setup()
{

  pinMode(MAX485_DE, OUTPUT);
  pinMode(start_button_pin, INPUT_PULLUP); 
  pinMode(zero_reset_button_pin, INPUT_PULLUP); 

  digitalWrite(MAX485_DE, 0);

// Serial monitor
  Serial.begin(115200);

// Encoder Modbus communication runs at 115200 baud
  Serial1.begin(115200, SERIAL_8N1);
  
// Modbus ID 1
  node.begin(1, Serial1);
// Starting Modbus Transaction
  node.preTransmission(preTransmission);
  node.postTransmission(postTransmission);

// Wait for Start Button to be pressed before beginning program
  while (digitalRead(start_button_pin) == HIGH)
  {
    if (start_button == 0)
    {
      start_button = 1;
      Serial.println("Press Start Button to Begin Demo");
    }
  }
// Stay in this loop until Start Button pressed

 Serial.println("Start Button Pressed");
// Continue with program after Start Button pressed

}

void loop()
{

// Check for zero reset button press and debounce for 50 milliseconds
  currentButtonState = digitalRead(zero_reset_button_pin);

  if(currentButtonState != lastButtonState)
  {
    lastDebounceTime = millis();
  }

  if((millis() - lastDebounceTime) > debounceDelay)
  {
    if (currentButtonState != buttonState)
    {
      buttonState = currentButtonState;
      if (buttonState == LOW)
      {
        Serial.println("Zero Reset Button Pressed");
// Encoder zero reset requires "1" to be written in at address 1000
        result_zero = node.writeSingleRegister(0x1000, 1);
      }
    }
  }
  lastButtonState = currentButtonState;
// End zero reset button press section

// Read encoder 3 registers of address "0000" through RS485 Modbus protocol
   result = node.readHoldingRegisters(0x0000, 3);

// 1st register is shaft rotational angle from 0 to 65535 increments
      data[0] = node.getResponseBuffer(0);
      Serial.println(data[0]);
      
// 2nd register is shaft 360° rotations count from 0 to 1000
      data[1] = node.getResponseBuffer(1);
      Serial.println(data[1]);

// 3rd register is temperature sensor in degrees Celsius (must divide by 10 to get actual value)
      data[2] = node.getResponseBuffer(2);
      Serial.println(data[2]);

// Don't know correct delay value yet (still testing)
    delay(70);
}

Even with the Mega, I had to research more in-depth about the Modbus protocol and the specific encoder registers. I started to read online manuals (https://github.com/4-20ma/ModbusMaster/blob/master/extras/ModbusMaster%20reference-2.0.1.pdf) and discovered that I needed to use "readHoldingRegisters" instead of "readInputRegisters." Once I did that and started to play around with the the address and number of registers, things started to fall into place.

Thank you to all those who posted guidance on the forum to others with RS485 issues. Reading other topics and how they resolved the problems were incredibly valuable and insightful.

I am now progressing to integrate the encoder reading / zero reset writing with control of the Nema 17 motor and making good progress.

I greatly appreciate your help!!

You had readHoldingRegisters in your original post.
Nice that you made it work!