RLS Orbis - encoder

Hi,

Anyone tried to implement this type of encoder? I tried but only get rubbish data.

https://www.rls.si/eng/fileuploader/download/download/?d=1&id=171&title=Data+Sheet%3A+Orbis™+true+absolute+rotary+encoder+(BRD01)

// Delay (us) after CS gets low and SPI communication starts
#define RLS_ORBIS_CS_DELAY 3

unsigned int readEncoderPos()
{
    SPI.beginTransaction(SPISettings(1'000'000, MSBFIRST, SPI_MODE0));

    digitalWrite(chip_select_pin, LOW);

    // wait at least 2.5us, as per datasheet.
    delayMicroseconds(RLS_ORBIS_CS_DELAY);

    unsigned int reading = SPI.transfer16(0x3300);  // 0x3300, since we're sending MSB first.

    digitalWrite(chip_select_pin, HIGH);

    // not needed if you moved SPI.beginTransaction() to setup. 
    SPI.endTransaction();

    Serial.print("reading: ");
    Serial.println(reading);

    // shift out the 2 status bits
    return reading >> 2;
}

Thanks!

I’ve had a quick scan of datasheet. 0x33 is a command for usart but you are using spi, so that is not a valid command.

I expect the following applies:

If command byte does not match any of listed commands, encoder will send only Position, Status, CRC data. If additional data is 
not required, MOSI line of the encoder should be tied to GND.

I think crc+status+position might be 5bytes long so you might need 5x spi.transfer(0x0).

Or you could try a valid cmd e.g temp which i think means you want spi.transfer(0x74) + 6x spi.transfer(0x00) as i think that is a 7 byte command

Hi Owen,

I try read 5 bytes but data looks wierd, always different without touching the motor shaft, encoder also light green = encoder position is valid, I tried also different speeds and modes.

 FF FF FF FF 1
 FF FF FF FF FF
 FF FF F8 0 1
 FF FF FF FF FF
 FF FF FF FF F8
 FF FF FF FF 80
 FF FF FF FF 0
 FF FF FF 80 0
 FF FF 0 0 0
 C0 0 0 0 0
uint8_t buffer[5] = {0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t encoder_buffer_size = 5;

unsigned int readEncoderPos()
{
    // Setting speed to 1MHz, but the encoder can probably do better

    SPI.beginTransaction(SPISettings(1'500'000, MSBFIRST, SPI_MODE0));

    digitalWrite(chip_select_pin, LOW);

    // wait at least 2.5us, as per datasheet.
    delayMicroseconds(3);

    for (uint8_t n = 0; n < encoder_buffer_size; ++n)
    {
      buffer[n] = SPI.transfer(0x0);
    }
    digitalWrite(chip_select_pin, HIGH);

    // not needed if you moved SPI.beginTransaction() to setup. 
    SPI.endTransaction();

    // shift out the 2 status bits
    return 0.0;
}

for (uint8_t n = 0; n < encoder_buffer_size; ++n)
{
  Serial.print(" ");
  Serial.print(buffer[n], HEX);
}
Serial.println("");

Couple of things to try/check.

1.The datasheet calls for a pause time of 20us before pulling chip select high
2. Some beginTransaction, endTransaction implementations do the chipselect high/low themselves if you’ve passed in an optional pin select pin. Might be worth stepping through to see if chip select is happening earlier than you think.
3. It looks like you print outs are ‘slipping’ to right which might suggest the output is 6 or 7 bytes. Maybe try using a known command e.g temp which has a documented length

1 Like

Hi,

I manage to get the data output correct when put little higher delay between readings and constant read 6 bytes, Thanks!

But something is going on with sensor.update() without it the output is stable and return correct angle all the time.

2.45
2.45
2.45
2.45
2.45
2.45

But once I call sensor.update() before it start to jump between correct value (2.45) to 12.05, why is this?

2.45
12.05
2.45
2.45
12.05
2.45
2.45
2.45
2.45
2.45
2.45

That is odd. Probably need to see more of your code. Does your loop look something like this?

void loop() {
  sensor.update();
  Serial.println(sensor.getAngle());
}

Maybe you can put some addional logging in the sensor.update() function.
The sensor.getAngle() returns a cached value that is created during the sensor.update(); function (which calls your getSensorAngle() function). Perhaps there is some bug with the way full_rotations is being worked out?

Hi,

You are right, using getAngle() the value looks correct, do you know how to verify the crc from encoder? I notice that sometimes it stop running and maybe that is because encoder return wrong position sometimes?

Not done much with CRCs but you could use the arduino CRC library.

I asked gpt4 how it might be used in your context and here is some code it created (the 0x97 magic number is mentioned in datasheet)

#include <CRC.h>

void setup() {
  Serial.begin(9600);
  // Wait for the serial connection to initialize
  while (!Serial) {}

  // Your data here (excluding the CRC byte)
  uint8_t data[5] = {0x01, 0x02, 0x03, 0x04, 0x05};

  // Initialize CRC8 with your specific parameters
  // Polynomial: 0x97, Initial value: 0xFF, XOR out: 0xFF (inversion), both reversals set to false based on your earlier message
  CRC8 crc(0x97, 0xFF, 0xFF, false, false);

  // Add your data to the CRC calculation
  crc.add(data, sizeof(data));

  // Calculate the CRC
  uint8_t crcResult = crc.calc();

  // Output the calculated CRC
  Serial.print("Calculated CRC: 0x");
  Serial.println(crcResult, HEX);
}

void loop() {
  // put your main code here, to run repeatedly:

}

I’ve not read through the code at all so ‘buyer beware’!

It there are CRC problems then something isn’t right with SPI, e.g. interference, wrong mode, timing, etc, so I wouldn’t want to live with spotting and ignoring CRC errors but would want to get to root cause.

1 Like

I integrated this encoder, it’s pretty nice. I even coded all the calibration routines and zero offset stuff. It is very fast and accurate. I can post up the code tomorrow if you want - FYI I did a table lookup for the crc checks

3 Likes

Hi Chris!

That would be awesome :ok_hand:

Thanks

Can you share the encoder code you use for the Orbis? Thank you!

#include <SimpleFOC.h>
#include <BLDCMotor.h>
#include <float.h>

#include <SPI.h> /* include the SPI library necessary for the Orbis Absolute Position Encoder: */

const int slaveSelectPin = 10; //Pin 10 is the Slave Select for the Orbis Encoder
uint8_t OffsetHigh, OffsetLow; //Set Encoder Zero Offset variables
uint16_t EncoderZero; //Set Encoder Zero Offset 16 bit value
uint8_t Byte1, Byte2, Byte3, Byte4, Byte5, Byte6, EncoderGenStatus; //Orbis Encoder Data Transfer Variables
uint8_t crc_in;
uint8_t b_Index = 0;
uint8_t b_CRC = 0;
uint16_t EncoderPosition = 8191; // position in counts - 8191 is the encoder midway point
float encoder_angle = _PI; // encoder angle in radians defaults to encoder “zero” ie halfway point = PI

/* Absolute Encoder CRC Lookup Table /
/
Lookup table for polynome = 0x97 */

uint8_t tableCRC[256] = {
0x00, 0x97, 0xB9, 0x2E, 0xE5, 0x72, 0x5C, 0xCB, 0x5D, 0xCA, 0xE4, 0x73, 0xB8, 0x2F, 0x01, 0x96,
0xBA, 0x2D, 0x03, 0x94, 0x5F, 0xC8, 0xE6, 0x71, 0xE7, 0x70, 0x5E, 0xC9, 0x02, 0x95, 0xBB, 0x2C,
0xE3, 0x74, 0x5A, 0xCD, 0x06, 0x91, 0xBF, 0x28, 0xBE, 0x29, 0x07, 0x90, 0x5B, 0xCC, 0xE2, 0x75,
0x59, 0xCE, 0xE0, 0x77, 0xBC, 0x2B, 0x05, 0x92, 0x04, 0x93, 0xBD, 0x2A, 0xE1, 0x76, 0x58, 0xCF,
0x51, 0xC6, 0xE8, 0x7F, 0xB4, 0x23, 0x0D, 0x9A, 0x0C, 0x9B, 0xB5, 0x22, 0xE9, 0x7E, 0x50, 0xC7,
0xEB, 0x7C, 0x52, 0xC5, 0x0E, 0x99, 0xB7, 0x20, 0xB6, 0x21, 0x0F, 0x98, 0x53, 0xC4, 0xEA, 0x7D,
0xB2, 0x25, 0x0B, 0x9C, 0x57, 0xC0, 0xEE, 0x79, 0xEF, 0x78, 0x56, 0xC1, 0x0A, 0x9D, 0xB3, 0x24,
0x08, 0x9F, 0xB1, 0x26, 0xED, 0x7A, 0x54, 0xC3, 0x55, 0xC2, 0xEC, 0x7B, 0xB0, 0x27, 0x09, 0x9E,
0xA2, 0x35, 0x1B, 0x8C, 0x47, 0xD0, 0xFE, 0x69, 0xFF, 0x68, 0x46, 0xD1, 0x1A, 0x8D, 0xA3, 0x34,
0x18, 0x8F, 0xA1, 0x36, 0xFD, 0x6A, 0x44, 0xD3, 0x45, 0xD2, 0xFC, 0x6B, 0xA0, 0x37, 0x19, 0x8E,
0x41, 0xD6, 0xF8, 0x6F, 0xA4, 0x33, 0x1D, 0x8A, 0x1C, 0x8B, 0xA5, 0x32, 0xF9, 0x6E, 0x40, 0xD7,
0xFB, 0x6C, 0x42, 0xD5, 0x1E, 0x89, 0xA7, 0x30, 0xA6, 0x31, 0x1F, 0x88, 0x43, 0xD4, 0xFA, 0x6D,
0xF3, 0x64, 0x4A, 0xDD, 0x16, 0x81, 0xAF, 0x38, 0xAE, 0x39, 0x17, 0x80, 0x4B, 0xDC, 0xF2, 0x65,
0x49, 0xDE, 0xF0, 0x67, 0xAC, 0x3B, 0x15, 0x82, 0x14, 0x83, 0xAD, 0x3A, 0xF1, 0x66, 0x48, 0xDF,
0x10, 0x87, 0xA9, 0x3E, 0xF5, 0x62, 0x4C, 0xDB, 0x4D, 0xDA, 0xF4, 0x63, 0xA8, 0x3F, 0x11, 0x86,
0xAA, 0x3D, 0x13, 0x84, 0x4F, 0xD8, 0xF6, 0x61, 0xF7, 0x60, 0x4E, 0xD9, 0x12, 0x85, 0xAB, 0x3C
};

//
/
Read Orbis Encoder Position and Convert to Radians /
/
/
float readSensor() {
SPI.beginTransaction(SPISettings(2000000, MSBFIRST, SPI_MODE1));
digitalWrite (slaveSelectPin, LOW);
delayMicroseconds(3);
Byte1 = SPI.transfer(0x64);
Byte2 = SPI.transfer(0);
Byte3 = SPI.transfer(0);
Byte4 = SPI.transfer(0);
digitalWrite (slaveSelectPin, HIGH);
SPI.endTransaction();

b_Index = Byte1;
b_CRC = Byte2;
b_Index = b_CRC ^ tableCRC[b_Index];
b_CRC = Byte3;
b_Index = b_CRC ^ tableCRC[b_Index];
b_CRC = tableCRC[b_Index];

if ((255 - Byte4) == b_CRC) {
EncoderPosition = ((((Byte1 << 8) | Byte2) & 0xFFFC) >> 2) + 0; //for the 14 bit encoder position from Byte1 and Byte2 data
EncoderGenStatus = Byte2 % 0b0000011;
return (float)(EncoderPosition * _2PI / 16383.0);
}
else {
return (float)(EncoderPosition * _2PI / 16383.0);
}
}

//
/
Initialize the Orbis Encoder SPI Interface /
/
/
void initSensor() {
pinMode (slaveSelectPin, OUTPUT);
SPI.begin();
}

// generic sensor class contructor
GenericSensor Orbis = GenericSensor(readSensor, initSensor);

//
/
Setup and Initialization /
/
/
void setup() {

// initialise magnetic sensor hardware
Orbis.init();
Orbis.update();
Orbis.getVelocity();
Orbis.getMechanicalAngle();
Orbis.getAngle();

// link the motor to the sensor
motor.linkSensor(&Orbis);

Orbis.update();
Orbis.getVelocity();
Orbis.getMechanicalAngle();
Orbis.getAngle();

}

//
/
Main Program Loop /
/
Read the most recent EncoderPosition variable and convert to radians, external to SimpleFOC but SimpleFOC is updating the /
/
variable EncoderPosition when it calls the readsensor() function /
/
/
/
/

void loop() {

encoder_angle = (float)EncoderPosition * _2PI / 16383.0;

}

//*******************************CRC8 decode routines ******************************************

static uint8_t updateCrc8(uint8_t crc, uint8_t crc_seed) {
uint8_t crc_u = crc;
crc_u ^= crc_seed;
for (int i = 0; i < 8; i++) {
crc_u = ( crc_u & 0x80 ) ? 0x7 ^ ( crc_u << 1 ) : ( crc_u << 1 );
}
return (crc_u);
}

uint8_t calculateCrc8(const uint8_t *Buf, const uint8_t BufLen) {
uint8_t crc = 0;
for (int i = 0; i < BufLen; i++) {
crc = updateCrc8(Buf[i], crc);
}
return crc;
}

//
/
Set Encoder Zero Offset Value /
/
/

void SetEncoderZeroOffset(uint8_t OffsetHigh, uint8_t OffsetLow) {
SPI.beginTransaction(SPISettings(2000000, MSBFIRST, SPI_MODE1));

// Detailed Status Data Command “Z”
digitalWrite (slaveSelectPin, LOW);
delay(1);
SPI.transfer(0xCD); //Unlock Code
delay(1);
digitalWrite (slaveSelectPin, HIGH);
delay(1);

digitalWrite (slaveSelectPin, LOW);
delay(1);
SPI.transfer(0xEF); //Unlock Code
delay(1);
digitalWrite (slaveSelectPin, HIGH);
delay(1);

digitalWrite (slaveSelectPin, LOW);
delay(1);
SPI.transfer(0x89); //Unlock Code
delay(1);
digitalWrite (slaveSelectPin, HIGH);
delay(1);

digitalWrite (slaveSelectPin, LOW);
delay(1);
SPI.transfer(0xAB); //Unlock Code
delay(1);
digitalWrite (slaveSelectPin, HIGH);
delay(1);

digitalWrite (slaveSelectPin, LOW);
delay(1);
SPI.transfer(0x5A); // Z command
delay(1);
digitalWrite (slaveSelectPin, HIGH);
delay(1);

digitalWrite (slaveSelectPin, LOW);
delay(1);
SPI.transfer(0x00); //Offset 0 Byte
delay(1);
digitalWrite (slaveSelectPin, HIGH);
delay(1);

digitalWrite (slaveSelectPin, LOW);
delay(1);
SPI.transfer(0x00); //Offset 0 Byte
delay(1);
digitalWrite (slaveSelectPin, HIGH);
delay(1);

digitalWrite (slaveSelectPin, LOW);
delay(1);
SPI.transfer(OffsetHigh); //Offset High Byte
delay(1);
digitalWrite (slaveSelectPin, HIGH);
delay(1);

digitalWrite (slaveSelectPin, LOW);
delay(1);
SPI.transfer(OffsetLow); //Offset Low Byte
delay(1);
digitalWrite (slaveSelectPin, HIGH);
delay(1);

SPI.endTransaction();
}

//
/
Save Encoder Configuration to Non-Volatile Memory /
/
/

void SaveEncoderConfig() {
SPI.beginTransaction(SPISettings(2000000, MSBFIRST, SPI_MODE1));

digitalWrite (slaveSelectPin, LOW);
delay(1);
SPI.transfer(0xCD); //Unlock Code
delay(1);
digitalWrite (slaveSelectPin, HIGH);
delay(1);

digitalWrite (slaveSelectPin, LOW);
delay(1);
SPI.transfer(0xEF); //Unlock Code
delay(1);
digitalWrite (slaveSelectPin, HIGH);
delay(1);

digitalWrite (slaveSelectPin, LOW);
delay(1);
SPI.transfer(0x89); //Unlock Code
delay(1);
digitalWrite (slaveSelectPin, HIGH);
delay(1);

digitalWrite (slaveSelectPin, LOW);
delay(1);
SPI.transfer(0xAB); //Unlock Code
delay(1);
digitalWrite (slaveSelectPin, HIGH);
delay(1);

digitalWrite (slaveSelectPin, LOW);
delay(1);
SPI.transfer(0x63); // c command
delay(1);
digitalWrite (slaveSelectPin, HIGH);

SPI.endTransaction();
}

//
/
Reset Encoder Configuration Factory Default /
/
/

void EncoderConfigReset() {
SPI.beginTransaction(SPISettings(2000000, MSBFIRST, SPI_MODE1));

digitalWrite (slaveSelectPin, LOW);
delay(1);
SPI.transfer(0xCD); //Unlock Code
delay(1);
digitalWrite (slaveSelectPin, HIGH);
delay(1);

digitalWrite (slaveSelectPin, LOW);
delay(1);
SPI.transfer(0xEF); //Unlock Code
delay(1);
digitalWrite (slaveSelectPin, HIGH);
delay(1);

digitalWrite (slaveSelectPin, LOW);
delay(1);
SPI.transfer(0x89); //Unlock Code
delay(1);
digitalWrite (slaveSelectPin, HIGH);
delay(1);

digitalWrite (slaveSelectPin, LOW);
delay(1);
SPI.transfer(0xAB); //Unlock Code
delay(1);
digitalWrite (slaveSelectPin, HIGH);
delay(1);

digitalWrite (slaveSelectPin, LOW);
delay(1);
SPI.transfer(0x72); // r command
delay(1);
digitalWrite (slaveSelectPin, HIGH);

SPI.endTransaction();
}

//
/
Trigger Self-calibration Function /
/
/

void EncoderCalibration() {
SPI.beginTransaction(SPISettings(2000000, MSBFIRST, SPI_MODE1));

digitalWrite (slaveSelectPin, LOW);
delay(1);
SPI.transfer(0xCD); //Unlock Code
delay(1);
digitalWrite (slaveSelectPin, HIGH);
delay(1);

digitalWrite (slaveSelectPin, LOW);
delay(1);
SPI.transfer(0xEF); //Unlock Code
delay(1);
digitalWrite (slaveSelectPin, HIGH);
delay(1);

digitalWrite (slaveSelectPin, LOW);
delay(1);
SPI.transfer(0x89); //Unlock Code
delay(1);
digitalWrite (slaveSelectPin, HIGH);
delay(1);

digitalWrite (slaveSelectPin, LOW);
delay(1);
SPI.transfer(0xAB); //Unlock Code
delay(1);
digitalWrite (slaveSelectPin, HIGH);
delay(1);

digitalWrite (slaveSelectPin, LOW);
delay(1);
SPI.transfer(0x41); // A command
delay(1);
digitalWrite (slaveSelectPin, HIGH);

SPI.endTransaction();
}

1 Like

Thank you! What microcontroller are you using? And is this code running on the same controller that is running SimpleFOC for the motor?

Teensy 4.0, running SimpleFOC in position mode at 35 KHz - plus a bunch of other stuff, this was part of a system implementing a “virtual swashplate” on a coaxial helicopter - each rotor hub had 4 BLDC motors doing direct blade pitch control - it worked well !