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:
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):
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:
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:
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.
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.
On the oscilloscope the timing looks like below, where the DFC feature can be seen and > 20us delay after last CLC cycle.
Here is the output from the terminal.
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:ort() 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.
On the oscilloscope the timing looks like below. You can see that there is one clock cycle less comparing to the previous one.
Here is the output from the terminal.
I hope that somebody will find it helpful!
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:
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):
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:
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:
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.
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:ort() 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.
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!