Teensy and SSI (Synchronous Serial Interface) encoder (without RS422 outputs)

Matthew

Member
Maybe there will be somebody like me who encounter the problem with interfacing the SSI encoders with Teensy 3.x SPI ports. For them I putting here my solution.

There was a discussion here about this topic with a lot of good information about the hardware (about the encoder which had RS422 outputs, mine has single ended outputs) but there are more things to take into considerations using SSI like timing, frame size, delay first clock and so on that wasn't stated there.

Timing diagram:

In the mentioned thread the George1988 used RMB20SC encoder - here is datasheet. On the page 5 there is a timing diagram:

Screenshot from 2019-08-28 13-23-14.jpg

The important thing with SSI is that the Data is clocked-out MSB first on the first rising edge, so sampling should be made on the falling edges. We cannot use any from the SPI modes straightforward, but we can use MODE 1 with some CLK line tweaking. Here is SPI Mode 1 timing diagram (SPI Mode 1, CPOL = 0, CPHA = 1: CLK idle state = low, data sampled on the falling edge and shifted on the rising edge):

SPI_MODE1.jpg

With Mode 1 we have proper timing - data is clocked out on the rising edge (dotted blue line), and sampled on the falling edge (orange dotted line), but the CLK line should be HIGH when idle. So for this purpose we have to initially set CLK line to HIGH, then before sending/reading data from SPI port we have to set CLK to LOW, then we make transfer and after all we have to drive CLK HIGH once again. Thanks to this, the CLK line will look like Clock line from RMB20SC datasheet.

My tested encoder

I used the AksIM™ rotary absolute encoder - the option with SSI interface, 20 bits resolution and single ended outputs. Here is the datasheet. The Aksim enkoder SSI timing diagram is very similar to RMB20SC encoder, here it is:

Screenshot from 2019-08-28 14-01-24.jpg

There is shown the Delay First Clock time (tDFC). Which allow us to clock SSI with frequency up to 2,5 MHz (for this encoder). I will use it later.

The structure of my encoder data is:

Screenshot from 2019-08-28 14-08-05.png

We expect 31 bits of data. So we have to make 4x transfers (SPI.transfer()), and it will made the 32 clock cycles on CLK line. There is a possibility that your encoder must be provided with the number of clock cycles equal to the frame length (in this case it would be 31 clocks). My encoder has no problems with additional clocks, but at the end I will show the program with clock cycles management which will be helpful for application using BiSS C protocol as well.

Connections

My encoder has single ended outputs with data signals at 3.3V. If yours encoder has RS422 outputs (like CLK+, CLK-) you have to use RS422 line driver to desymetrize lines for Teensy. And if yours signals are above 3.3V level you have to use level shifter. That was discussed the in mentioned thread.

Code:
+----------------------+------------+
|         Aksim <-> Teensy 3.6      |
+----------------------+------------+
| Aksim                | Teensy 3.6 |
| PIN 1 (5V)           | 5V         |
| PIN 2 -              | -          |
| PIN 3 (Data)         | MISO (12)  |
| PIN 4 (Clock)        | SCK (13)   |
| PIN 5 -              | -          |
| PIN 6 GND            | GND        |
+----------------------+------------+

Example program with no frame length management

Here is the working code for reading the Aksim data packet and getting the absolute position from sensor. I am using the Delay First Clock function as well. The code is commented out so it should be pretty easy to figure it out. In makeTransfer() function there is that mentioned tweaking with the CLK line before and after the SPI transfer to meet the requirements of SSI interface. Look for the comments.

Code:
#include <Arduino.h>
#include <SPI.h>

#define CLOCK_SPEED         2'500'000       // 2.5 MHz SSI Clock
#define SCK_PIN             13              // SSI CLK line

#define SCK_PIN_13_INDEX    0
#define SCK_PIN_14_INDEX    1
#define SCK_PIN_27_INDEX    2

SPISettings settingsA(CLOCK_SPEED, MSBFIRST, SPI_MODE1);

uint8_t encoderBuf[4] = {0,0,0,0};

void makeTransfer();
uint32_t decodeEncoderFrame();

void calculateAndPrintPosition(uint32_t& encoderData);

void setup()
{
    SerialUSB.begin(9600);
    SPI.begin();                            // SPI.begin() will initialize the SPI port, as well as CLK pin

    pinMode(SCK_PIN, OUTPUT);               // pinMode() will initialize the CLK pin as output (SPI port can't use it now!)
    digitalWriteFast(SCK_PIN, HIGH);        // Set CLK line HIGH (to meet the requirements of SSI interface)

    while(!SerialUSB){}
    SerialUSB.println("Started!");   
}

void loop()
{
    makeTransfer();
    uint32_t encoderData = decodeEncoderFrame();
    calculateAndPrintPosition(encoderData);

    delay(200);
}

void makeTransfer()
{
    digitalWriteFast(SCK_PIN, LOW);         // Set CLK line LOW (to inform encoder -> latch data)

    delayMicroseconds(1);                   // Running above 500kHz perform Delay First Clock function

                                            // Before in setup() the pinMode() change the CLK pin function to output,
                                            // now we have to enable usage of this pin by SPI port with calling SPI.begin():
    // SPI.begin();                         
    
    // Or use this one below - a bit faster but SPI port and SCK pin dependent option. I belive there is a more elegant and more
    // scalable solution, maybe even supported by hardware for this purpose? - if anyone can point me it out I would be grateful:

    uint8_t pinIndex = SCK_PIN_13_INDEX;
    volatile uint32_t* reg = portConfigRegister(SPIClass::spi0_hardware.sck_pin[pinIndex]);
	*reg = SPIClass::spi0_hardware.sck_mux[pinIndex];

    SPI.beginTransaction(settingsA);        // We use transactional API
    
    for(int i = 0; i < 4; i++)
    {
        encoderBuf[i] = SPI.transfer(0xAA); // Transfer anything and read data back
    }

    SPI.endTransaction();                   // We use transactional API

    pinMode(SCK_PIN, OUTPUT);               // A while before we set CLK pin to be used by SPI port, now we have to change it manually...
    digitalWrite(SCK_PIN, HIGH);            // ... back to idle HIGH
}

uint32_t decodeEncoderFrame()
{
    uint32_t data = static_cast<uint32_t>(encoderBuf[0] << 24) |
                    static_cast<uint32_t>(encoderBuf[1] << 16) |
                    static_cast<uint32_t>(encoderBuf[2] << 8)  |
                    static_cast<uint32_t>(encoderBuf[3]);           // here transfer8 was made

    // Shift one bit right - (MSB of position was placed on at MSB of uint32_t, so make it right and shift to make MSB position at 30'th bit):
    data = data >> 1;

    return data;
}

void calculateAndPrintPosition(uint32_t& encoderData)
{
    // encoderPosition is placed in front (starting from MSB of uint32_t) so shift it for 11 bits to align position data right
    uint32_t encoderPosition = (encoderData >> 11);

    float angularAbsolutePosition = static_cast<float>(encoderPosition) / static_cast<float>(1 << 20) * 360.0F;

    SerialUSB.println("Encoder Positon = " + String(encoderPosition) + " Angle = " + String(angularAbsolutePosition, 4));
}

On the oscilloscope the timing looks like below, where the DFC feature can be seen and > 20us delay after last CLC cycle.

DS0003.PNG

Here is the output from the terminal.
Code:
Encoder Positon = 251913 Angle = 86.4875
Encoder Positon = 251911 Angle = 86.4868
Encoder Positon = 251910 Angle = 86.4864
Encoder Positon = 251913 Angle = 86.4875
Encoder Positon = 251912 Angle = 86.4871

Example program with additional frame length management

My encoder with the SSI interface option and 20 bits of position resolution has exactly the 31 bits in packet data frame (20 bits of position, 2 bits o general status and 9 bits of detailed status).

In order to change the frame data length we can use the SPIClass::transfer16() method, which internally change the used CTAR register to clocked the rest of packet bits which are not multiples of 8. In my case I am using SPIClass::transfer() to read 3x8 bits and SPIClass::transfer16() to read the lasts 7 bits of encoder data which gives me exactly 31 clock cycles.

CTAR registers are used to define different transfer attributes and SPIClass::transfer16() change it from CTAR0 to CTAR1 by setting the CTAS fields in the SPIx_PUSHR. You can see in the SPIClass::begin() with what default values these registers are set with cross-checking the reference manual - 57.4.3 Clock and Transfer Attributes Register and 57.4.7 PUSH TX FIFO Register. By default in the SPIClass::begin() CTAR1_FMSZ is set to 16 bit frame length (value 15 is passed - SPI_CTAR_FMSZ(15)).

To make it work we have to change the FMSZ fields in the CTAR1 register. The number of bits transferred per frame is equal to the FMSZ value plus 1. When we use transactional SPI API we have to change FMSZ after SPIClass::beginTransaction() method because it resets the CTARx register values to core defaults.

We can change CTAR1 content by breaking the API and making the SPIClass::port() method public, or by using the KINETISK_SPI0 macro. I chose the option without braking the API. Here is the working example - pretty similar to the previous one with some important changes. Please look at the comments in makeTransferWithFrameLengthManagement() and decodeEncoderFrame(), there are important things - setting the proper frame size and forming the proper encoder packet.

Code:
#include <Arduino.h>
#include <SPI.h>

#define CLOCK_SPEED                 2'500'000       // 2.5 MHz SSI Clock
#define SCK_PIN                     13              // SSI CLK line

#define SCK_PIN_13_INDEX            0
#define SCK_PIN_14_INDEX            1
#define SCK_PIN_27_INDEX            2

#define LAST_TRANSFER_FRAME_SIZE    7               // (3x8 (transfer8) + 1x7 (transfer16 with CTAR1_FRSZ setted) = 31 bits)

SPISettings settingsA(CLOCK_SPEED, MSBFIRST, SPI_MODE1);

uint8_t encoderBuf[4] = {0,0,0,0};

void makeTransferWithFrameLengthManagement();
void setCTAR1_FMSZ(KINETISK_SPI_t* spi, uint8_t frameSize);
uint32_t decodeEncoderFrame();

void calculateAndPrintPosition(uint32_t& encoderData);

void setup()
{
    SerialUSB.begin(9600);
    SPI.begin();

    pinMode(SCK_PIN, OUTPUT);
    digitalWriteFast(SCK_PIN, HIGH);

    while(!SerialUSB){}
    SerialUSB.println("Started!");   
}

void loop()
{
    makeTransferWithFrameLengthManagement();
    uint32_t encoderData = decodeEncoderFrame();
    calculateAndPrintPosition(encoderData);

    delay(200);
}

void makeTransferWithFrameLengthManagement()
{
    digitalWriteFast(SCK_PIN, LOW); 

    delayMicroseconds(1);

    // SPI.begin();                         

    uint8_t pinIndex = SCK_PIN_13_INDEX;
    volatile uint32_t* reg = portConfigRegister(SPIClass::spi0_hardware.sck_pin[pinIndex]);
	*reg = SPIClass::spi0_hardware.sck_mux[pinIndex];

    SPI.beginTransaction(settingsA);        // We use transactional API. The CTARx registers content are set to defult values!

    setCTAR1_FMSZ(&KINETISK_SPI0,           // Here we change the CTAR1_FMSZ fields values to desired one! 
                  LAST_TRANSFER_FRAME_SIZE);
    
    for(int i = 0; i < 3; i++)
    {
        encoderBuf[i] = SPI.transfer(0xAA); // Transfer anything and read data back (3*8 bits) using transfer8() method!
    }

    encoderBuf[3] = SPI.transfer16(0xFF);   // Transfer anything and read data back (1x7 bits) using transfer16() method!

    SPI.endTransaction();

    pinMode(SCK_PIN, OUTPUT);
    digitalWrite(SCK_PIN, HIGH);
}

void setCTAR1_FMSZ(KINETISK_SPI_t* spi, uint8_t frameSize)
{
    volatile uint32_t reg = spi->CTAR1;                                     // Get actual value

    reg &= static_cast<volatile uint32_t>(~SPI_CTAR_FMSZ(15));              // Clear FMSZ fields
    reg |= static_cast<volatile uint32_t>(SPI_CTAR_FMSZ(frameSize - 1));    // set new value to FMSZ fields decremented by 1

    spi->CTAR1 = reg;                                                       // Apply changes
}

uint32_t decodeEncoderFrame()
{
    uint32_t data = static_cast<uint32_t>(encoderBuf[0] << 24) |
                    static_cast<uint32_t>(encoderBuf[1] << 16) |
                    static_cast<uint32_t>(encoderBuf[2] << 8)  |
                    static_cast<uint32_t>(encoderBuf[3] << 1);              // Here transfer16 was made (frame size = 7 bits)
                                                                            // so on the 6'th bit (counting from zero) encoderBuf[3]
                                                                            // is MSB of the last part of packet from the encoder
                                                                            // so we are sift it 1 << left to align data to left

    // Shift one bit right - (MSB of position was placed on at MSB of uint32_t, so make it right and shift to make MSB position at 30'th bit):
    data = data >> 1;

    return data;
}

void calculateAndPrintPosition(uint32_t& encoderData)
{
    // encoderPosition is placed in front (starting from MSB of uint32_t) so shift it for 11 bits to align position data right
    uint32_t encoderPosition = (encoderData >> 11);

    float angularAbsolutePosition = static_cast<float>(encoderPosition) / static_cast<float>(1 << 20) * 360.0F;

    SerialUSB.println("Encoder Positon = " + String(encoderPosition) + " Angle = " + String(angularAbsolutePosition, 4));
}

On the oscilloscope the timing looks like below. You can see that there is one clock cycle less comparing to the previous one.

DS0004.PNG

Here is the output from the terminal.
Code:
Encoder Positon = 251921 Angle = 86.4902
Encoder Positon = 251921 Angle = 86.4902
Encoder Positon = 251920 Angle = 86.4899
Encoder Positon = 251919 Angle = 86.4895
Encoder Positon = 251919 Angle = 86.4895


I hope that somebody will find it helpful!
 
Thanks Matthew for your advice via private message.

I think I got it working. I had to use SPI_MODE0 to make it work with teensy LC

Code:
//Teensy LC
//EMS22A encoder

#include <Arduino.h>
#include <SPI.h>

#define CLOCK_SPEED         2'000'000       // 2.5 MHz SSI Clock
#define CS_PIN             10
#define SCK_PIN             13              // SSI CLK line

#define SCK_PIN_13_INDEX    0
//#define SCK_PIN_14_INDEX    1
//#define SCK_PIN_27_INDEX    2

SPISettings settingsA(CLOCK_SPEED, MSBFIRST, SPI_MODE0);

boolean bDebug = false;

uint8_t encoderBuf[4] = {0, 0, 0, 0};

void makeTransfer();
uint16_t decodeEncoderFrame();

void calculateAndPrintPosition(uint16_t& encoderData);

void setup()
{
  SerialUSB.begin(9600);
  SPI.begin();                            // SPI.begin() will initialize the SPI port, as well as CLK pin

  pinMode(CS_PIN, OUTPUT);
  pinMode(SCK_PIN, OUTPUT);               // pinMode() will initialize the CLK pin as output (SPI port can't use it now!)
  digitalWriteFast(SCK_PIN, HIGH);        // Set CLK line HIGH (to meet the requirements of SSI interface)
  digitalWriteFast(CS_PIN, LOW);

  while (!SerialUSB) {}
  SerialUSB.println("Started!");
}

void loop()
{
  makeTransfer();
  uint16_t encoderData = decodeEncoderFrame();
  calculateAndPrintPosition(encoderData);

  delay(200);
}

void makeTransfer()
{


  digitalWrite(CS_PIN, LOW);

  //  digitalWriteFast(SCK_PIN, HIGH);
  digitalWriteFast(SCK_PIN, LOW);         // Set CLK line LOW (to inform encoder -> latch data)


  delayMicroseconds(1);                   // Running above 500kHz perform Delay First Clock function

  // Before in setup() the pinMode() change the CLK pin function to output,
  // now we have to enable usage of this pin by SPI port with calling SPI.begin():
  // SPI.begin();

  // Or use this one below - a bit faster but SPI port and SCK pin dependent option. I belive there is a more elegant and more
  // scalable solution, maybe even supported by hardware for this purpose? - if anyone can point me it out I would be grateful:

  uint8_t pinIndex = SCK_PIN_13_INDEX;
  volatile uint32_t* reg = portConfigRegister(SPIClass::spi0_hardware.sck_pin[pinIndex]);
  *reg = SPIClass::spi0_hardware.sck_mux[pinIndex];

  SPI.beginTransaction(settingsA);        // We use transactional API

  for (int i = 0; i < 4; i++)
  {
    encoderBuf[i] = SPI.transfer(0x00) ;//0xAA); // Transfer anything and read data back
    if (bDebug == true) {
      Serial.print(i);
      Serial.print(" i ");
      Serial.print(encoderBuf[i], DEC);
      Serial.println();
    }

  }

  SPI.endTransaction();                   // We use transactional API


  pinMode(SCK_PIN, OUTPUT);               // A while before we set CLK pin to be used by SPI port, now we have to change it manually...
  //  digitalWriteFast(SCK_PIN, LOW);
  digitalWriteFast(SCK_PIN, HIGH);            // ... back to idle HIGH

  digitalWrite(CS_PIN, HIGH);
}

uint16_t decodeEncoderFrame()
{
  //    uint32_t data = static_cast<uint32_t>(encoderBuf[0] << 24) |
  //                    static_cast<uint32_t>(encoderBuf[1] << 16) |
  //                    static_cast<uint32_t>(encoderBuf[2] << 8)  |
  //                    static_cast<uint32_t>(encoderBuf[3]);           // here transfer8 was made

  uint16_t data =  static_cast<uint16_t>(encoderBuf[0] << 8)  |
                   static_cast<uint16_t>(encoderBuf[1]);           // here transfer8 was made
  if (bDebug == true) {
    Serial.println("data ");
    Serial.print(data);
    Serial.println();
  }
  data = data >> 6; // shift right to cut status and parity bits only 'position data' is in data variable now!

  return data;
}

void calculateAndPrintPosition(uint16_t& encoderData)
{
  // encoderPosition is placed in front (starting from MSB of uint32_t) so shift it for 11 bits to align position data right
  // no shifting needed (we done it in decodeEncoderFrame())
  // int16_t encoderPosition = (encoderData >> 9);

  // you have 10 bits position data so max is 1 << 10 not 1 << 20 as in my case
  //float angularAbsolutePosition = static_cast<float>(encoderPosition) / static_cast<float>(1 << 20) * 360.0F;
  float angularAbsolutePosition = static_cast<float>(encoderData) / static_cast<float>(1 << 10) * 360.0F;

  SerialUSB.println("Encoder Positon = " + String(encoderData) + " Angle = " + String(angularAbsolutePosition, 4));
}
 
Little update:

I tested code with encoder which using BiSS-C protocol and has differential outputs/inputs. The code worked without any big changes - only the SPI.transfer()/transfer16() section and setCTAR1_FMSZ() content changed in order to fit into the encoder frame. Of course the decodeEncoderFrame() and calculateAndPrintPosition() had to be changed due to different packet composition. But the entire idea is still the same.
 
Little update:

I tested code with encoder which using BiSS-C protocol and has differential outputs/inputs. The code worked without any big changes - only the SPI.transfer()/transfer16() section and setCTAR1_FMSZ() content changed in order to fit into the encoder frame. Of course the decodeEncoderFrame() and calculateAndPrintPosition() had to be changed due to different packet composition. But the entire idea is still the same.

Hi Matthew,
Are you willing to share your code for BISS-C? I am doing a similar projekt. Thank you.

regards
Nathan
 
Hi Matthew,
Are you willing to share your code for BISS-C? I am doing a similar projekt. Thank you.

Unfortunately no - that was a long time ago. The "guide" I wrote should be sufficient for any BiSS-C encoder with Teensy 3.6.

Once you get the Teensy SPI to work like BiSS-C - that's basically the second snippet of the code I provided in first post: makeTransferWithFrameLengthManagement() - then you can decode the frame (and check CRC, statuses and get position). The idea is always the same - the only change with BiSS-C and AksIM-2 encoder (see at the end) is that I firstly do transfer16() call then transfer8() calls to read first bits (not equal to 8 bit length) of frame then read other N*8 bits of frame, so:

- control the CLK pin manually (digitalWriteFast()) to conform CLK line to BiSS-C standard (not doable with any SPI modes, so need to be handle manually - BiSS-C requires HIGH state in IDLE and sampling made on falling edge),
- pass the control over CLK pin to SPI module (portConfigRegister()),
- begin SPI transaction (beginTransaction()),
- set bit length of transfer16() transaction to conform your encoder frame length (setCTAR1_FMSZ()),
- do transfer16() with frame size set in previous step,
- do multiple transfer() calls to read rest of the frame,
- end transaction (endTransaction()),
- take control over CLK line and set HIGH state - to conform BiSS-C/SSI (pinMode(), digitalWrite()).

In case of AksIM-2 frame has 43 bit length. So I do transfer16() with frame size of 4bits and 5*transfer() which gives = 4 + 5*8 = 44 bits - that's because first bit of transfer16() is read while SLO line is still in HIGH state. That one bit is discarded later when I decode frame.

P.S.
At the time of writing the first post I used AksIM encoder with SSI single ended interface, then I used AksIM-2 with BiSC-C differential output.
 
Nathan, curious how that turned out. I want to use a 4.1 with a similar RM58 encoder to Matthew's.

Any code you have to share would be welcome.

Thanks,
Bryan
 
Back
Top