Arduino Sample Code for SPI Absolute Encoders

By Damon Tarry, Design Applications Engineer, Same Sky

This Arduino sample code tutorial aims to give users a solid starting point for configuring and reading data from Same Sky’s AMT22 absolute encoders with Serial Peripheral Interface (SPI) communication. The tutorial will provide what hardware and software is needed, key setup requirements, and sample code packages and instructions for both single-turn and multi-turn output options. Here is a list of what is required to get started:

AMT22 absolute encoder overview

Same Sky’s (formerly CUI Devices) AMT22 is an absolute encoder offered in either 12-bit or 14-bit resolution, meaning it provides a precise number of unique positions per revolution. For the 12-bit variant, this translates to 4,096 distinct positions, while the 14-bit model features 16,384 positions per revolution. Regardless of how many times the device is rotated, it continuously reports its absolute position, giving users accurate feedback on the device’s exact angle.

This encoder is available in both single-turn and multi-turn models. The single-turn variant measures position within a single 360-degree rotation, while the multi-turn version tracks not only the position within a rotation but also the total number of complete rotations. Additionally, the single-turn variants feature a programmable zero point, enabling users to define a custom reference for the encoder’s origin.

Getting started

Ensure that the device is in RUN mode by adjusting the switch located on the back of the encoder to the appropriate position (Figure 1). Now mount the AMT22 encoder to a motor or assembly using the AMT mounting instructions to ensure proper installation. The AMT22 supports 9 different shaft sizes ranging from 2 mm to 8 mm.

Diagram of Same Sky AMT22 encoder switched to RUN modeFigure 1: Flip the switch on the back of the AMT22 encoder to RUN mode. (Image source: Same Sky)

The connections outlined in Figure 2 and Table 1 are specifically for the Arduino Uno board, but the provided code should be compatible with most Arduino boards. However, keep in mind that pin configurations may differ across various Arduino models. For precise connection details on other boards, it's recommended to refer to the corresponding Arduino documentation.

Diagram of Arduino Uno wiring connections with the AMT22 encoderFigure 2: Arduino Uno wiring connections with the AMT22 encoder. (Image source: Same Sky)

Function Encoder Pin Number Arduino Uno Pin AMT-DBC-1-036
+5 V 1 5 V White/green
SCLK 2 13 Blue/white
MOSI 3 11 White/blue
GND 4 GND Green/white
MISO 5 12 Orange/white
CS 6 2 White/orange

Table 1: Arduino Uno wiring connections further defined. (Image source: Same Sky)

The AMT22 encoder starts transmitting its absolute position data immediately when the SPI communication begins, eliminating the need for a traditional command-response structure. During the first byte of the SPI transfer, the host sends 0x00, and the AMT22 responds simultaneously with valid position data.

If the host needs to issue a command (Table 2), such as a zero-setting command, it will be sent in the second byte of the transmission. This is referred to as an extended command. For detailed technical specifics, refer to the AMT22 datasheet.

Command Byte Notes
Get Position 0x00 0x00
Set Zero 0x00 0x70 Single-turn only
Get Turns 0x00 0xA0 Multi-turn only

Table 2: AMT22 commands defined. (Image source: Same Sky)

Code tutorial – includes and defines

Since the Arduino's SPI bus is being used to interface with the AMT22 encoder, the SPI library needs to be included in the code. To send the position data from the Arduino to the computer, the built-in USB-serial connection within the Arduino IDE is utilized, configured at a baud rate of 115200.

In addition, the commands used by the AMT22 need to be defined. Since the encoder doesn't process the content of the first byte, a NOP (no-operation) is assigned to simplify the communication process (Listing 1).

Copy
/* Include the SPI library for the arduino boards */
#include <SPI.h>
 
/* Serial rates for UART */
#define BAUDRATE      115200
 
/* SPI commands */
#define AMT22_NOP     0x00
#define AMT22_ZERO    0x70
#define AMT22_TURNS   0xA0

Listing 1: Setting up the SPI interface.

Initialization

In the setup() function (Listing 2), begin by initializing all required SPI pins and configuring the serial interfaces for communication.

The serial port should be initialized to allow data transmission to the host computer. This is done by passing the defined BAUDRATE into the Serial.begin() function.

Before enabling SPI, ensure the chip select (CS) line is set to the appropriate state to prepare the encoder for communication.

Select a clock rate for the SPI bus to communicate with the AMT22. For prototyping purposes, a clock rate of 500 kHz is suitable, although the AMT22 supports rates up to 2 MHz. Achieving 500 kHz can be done using the SPI_CLOCK_DIV32 setting. Given the Arduino Uno’s 16 MHz clock, this division results in a 500 kHz SPI clock rate. For more details on SPI clock configuration, consult Arduino documentation.

After configuring everything, the SPI bus can be initialized using SPI.begin(), which will set up the three dedicated SPI pins: MISO, MOSI, and SCLK, preparing the system for communication with the encoder.

Copy
void setup()
{
  uint8_t cs_pin = 2;
 
  //Set the modes for the SPI CS
  pinMode(cs_pin, OUTPUT);
  //Get the CS line high which is the default inactive state
  digitalWrite(cs_pin, HIGH);
 
  //Initialize the UART serial connection for debugging
  Serial.begin(BAUDRATE);
 
  //set the clockrate. Uno clock rate is 16Mhz, divider of 32 gives 500 kHz.
  //500 kHz is a good speed for our test environment
  //SPI.setClockDivider(SPI_CLOCK_DIV2);   // 8 MHz
  //SPI.setClockDivider(SPI_CLOCK_DIV4);   // 4 MHz
  //SPI.setClockDivider(SPI_CLOCK_DIV8);   // 2 MHz
  //SPI.setClockDivider(SPI_CLOCK_DIV16);  // 1 MHz
  SPI.setClockDivider(SPI_CLOCK_DIV32);    // 500 kHz
  //SPI.setClockDivider(SPI_CLOCK_DIV64);  // 250 kHz
  //SPI.setClockDivider(SPI_CLOCK_DIV128); // 125 kHz
 
  //start SPI bus
  SPI.begin();
}

Listing 2: The setup() function which initializes the all SPI pins.

SPI Communication

SPI communication with the AMT22 is handled through the Arduino's SPI library, while the chip select (CS) control is managed through the code using digital I/O pins. The digitalWrite() function is used to assert or deassert the CS line (Listing 3).

The AMT22 expects two bytes of 0x00 to be sent, and it returns data immediately after receiving these bytes. Due to this fast response, certain minimum timing requirements must be followed, which are outlined in the AMT22 datasheet.

Regardless of whether the encoder is a 12-bit or 14-bit version, it always responds with two bytes (16 bits) of data. The upper two bits are check-bits, used to verify data integrity. For the 12-bit version, the lower two bits are both 0, and the returned value must be shifted right by 2 bits (or divided by 4) for proper use.

To obtain position data, the SPI.transfer() function is called, sending the AMT22_NOP command. The CS line remains low during this process. The AMT22 sends the high byte first, so the received byte is shifted left by 8 bits to align it in the upper half of a uint16_t variable. This value is assigned to the encoderPosition variable in one operation. After a brief delay to meet the timing requirements, a second SPI.transfer() call is made to send another AMT22_NOP command. The result is OR'ed with the current value in encoderPosition, effectively combining the two received bytes into a single uint16_t variable. Finally, the CS line is released, completing the communication.

Copy
uint8_t cs_pin = 2;
 
//set the CS signal to low
digitalWrite(cs_pin, LOW);
delayMicroseconds(3);
 
//read the two bytes for position from the encoder, starting with the high byte
uint16_t encoderPosition = SPI.transfer(AMT22_NOP) << 8; //shift up 8 bits because this is the high byte
delayMicroseconds(3);
encoderPosition |= SPI.transfer(AMT22_NOP); //we do not need a specific command to get the encoder position, just no-op
 
//set the CS signal to high
digitalWrite(cs_pin, HIGH);

Listing 3: Setting up SPI communication.

Checksum verification

After completing the SPI transfer, it is essential to validate the received data using a checksum (Listing 4).

To implement this validation, a function can be created based on the equation provided in the datasheet. The checksum is contained in the upper two bits of the received value, and it utilizes odd parity across the odd and even bits in the position response.

The function will perform the following steps:

  1. Calculate the parity for the odd bits (bits 1, 3, 5, 7, 9, 11, 13)
  2. Calculate the parity for the even bits (bits 0, 2, 4, 6, 8, 10, 12, 14)
  3. Compare the calculated parities against the values indicated by the checksum bits

The function will return true if the checksum is valid, indicating that the data integrity is confirmed. If the checksum is invalid, the function will return false, signaling a potential error in the received data.

Copy
/*
 * Using the equation on the datasheet we can calculate the checksums and then make sure they match what the encoder sent.
 */
bool verifyChecksumSPI(uint16_t message)
{
  //checksum is invert of XOR of bits, so start with 0b11, so things end up inverted
  uint16_t checksum = 0x3;
  for(int i = 0; i < 14; i += 2)
  {
    checksum ^= (message >> i) & 0x3;
  }
  return checksum == (message >> 14);
}

Listing 4: Validating the checksum.

Data formatting

If the checksum validation confirms the integrity of the data, the next step is to update the encoderPosition variable by removing the upper two bits (Listing 5). This can be achieved by applying a bitwise AND operation with 0x3FFF (or 0b0011111111111111), which effectively retains all 14 lower bits of the position data.

Additionally, it is necessary to account for the encoder's resolution—whether it is 12-bit or 14-bit. If the resolution is 12 bits, the encoderPosition value must be shifted 2 bits to the right to adjust for the lower resolution. This ensures that the position data is accurately represented in the encoderPosition variable, reflecting the actual position of the encoder based on its specified resolution.

Copy
if (verifyChecksumSPI(encoderPosition)) //position was good
{
  encoderPosition &= 0x3FFF; //discard upper two checksum bits
  if (RESOLUTION == 12) encoderPosition = encoderPosition >> 2; //on a 12-bit encoder, the lower two bits will always be zero
 
  Serial.print(encoderPosition, DEC); //print the position in decimal format
  Serial.write('\n');
}
else //position is bad
{
  Serial.print("Encoder position error.\n");
}

Listing 5: Updating the encoderPosition.

Set zero position (single-turn only)

Certain variants of the AMT22 encoder offer a programmable zero position feature. To set this zero position, a specific two-byte command sequence must be sent. The process involves sending the AMT22_NOP command first, followed by a brief wait to meet the minimum timing requirements specified by the AMT22. After this wait, the AMT22_ZERO command is sent while ensuring the chip select (CS) line is released. Once the encoder receives this command, it will perform a reset operation (Listing 6).

To avoid communication with the encoder during this reset period, a delay of 250 ms is implemented, ensuring that no commands are sent to the encoder during its power-on time.

While it is possible for the code to set the zero position of the encoder at the beginning of operation, it is more common in typical applications to set the zero position only once during the initial configuration of the device for use within the system. This practice helps maintain the integrity of the encoder's position feedback throughout its operational lifespan.

Copy
/*
 * The AMT22 bus allows for extended commands. The first byte is 0x00 like a normal position transfer,
 * but the second byte is the command.
 * This function takes the pin number of the desired device as an input
 */
void setZeroSPI(uint8_t cs_pin)
{
  //set CS to low
  digitalWrite(cs_pin, LOW);
  delayMicroseconds(3);
 
  //send the first byte of the command
  SPI.transfer(AMT22_NOP);
  delayMicroseconds(3);
 
  //send the second byte of the command
  SPI.transfer(AMT22_ZERO);
  delayMicroseconds(3);
 
  //set CS to high
  digitalWrite(cs_pin, HIGH);
 
  delay(250); //250 millisecond delay to allow the encoder to reset
}

Listing 6: Setting the zero position of a single-turn AMT22 encoder.

Read turns counter (multi-turn only)

Certain variants of the AMT22 encoder support a multi-turn counter, allowing users to read both the position and the turn count in a single data retrieval sequence.

If the position data received is invalid, the system should notify the user of the error. In contrast, if the position is valid, the program should report the position in decimal format (Listing 7). This capability enhances the encoder's functionality by providing comprehensive feedback on both the absolute position and the number of complete turns, facilitating more accurate monitoring and control in applications requiring precise rotational data.

Copy
uint8_t cs_pin = 2;
 
//set the CS signal to low
digitalWrite(cs_pin, LOW);
delayMicroseconds(3);
 
//read the two bytes for position from the encoder, starting with the high byte
uint16_t encoderPosition = SPI.transfer(AMT22_NOP) << 8; //shift up 8 bits because this is the high byte
delayMicroseconds(3);
encoderPosition |= SPI.transfer(AMT22_TURNS); //we send the turns command (0xA0) here, to tell the encoder to send us the turns count after the position
 
//wait 40us before reading the turns counter
delayMicroseconds(40);
 
//read the two bytes for turns from the encoder, starting with the high byte
uint16_t encoderTurns = SPI.transfer(AMT22_NOP) << 8; //shift up 8 bits because this is the high byte
delayMicroseconds(3);
encoderTurns |= SPI.transfer(AMT22_NOP);
delayMicroseconds(3);
 
//set the CS signal to high
digitalWrite(cs_pin, HIGH);

Listing 7: Reading the encoderPosition and the turns counter in a multi-turn AMT22 encoder.

Running the code

With the code successfully created, it’s time to upload it to the Arduino and establish communication with the AMT22 encoder.

To monitor the output, open the serial monitor in the Arduino IDE and ensure that the data rate is set to 115200 baud. This will allow users to observe the encoder's operation and view the reported position data in real time. Once the serial monitor is active, the encoder should start transmitting its position information, demonstrating its functionality within the system (Figure 3).

Image of reported position from the encoderFigure 3: The reported position from the encoder, received by the Arduino (Image source: Same Sky)

Multiple encoders

One significant advantage of using an SPI device is the ability to communicate with multiple encoders on the same bus. To facilitate this, an additional digital I/O pin needs to be allocated for each encoder, allowing for individual chip select (CS) control.

In the example code (Listing 8), an array of CS pins is utilized to support an arbitrary number of encoders. This design allows for scalable communication, enabling the user to easily add more encoders as needed. By modifying the functions to accept the pin number corresponding to the desired device, the code can dynamically control which encoder is active on the SPI bus, ensuring that each device can be accessed and operated independently.

Copy
uint8_t cs_pins[] = {2}; //only one encoder connected, using pin 2 on arduino for CS
//uint8_t cs_pins[] = {2, 3}; //two encoders connected, using pins 2 & 3 on arduino for CS

Listing 8: Setting up an array for reading multiple encoders.

The next step is to loop through each CS pin in the array and read the position from each connected encoder. This allows the system to activate each encoder by asserting its chip select line, performing the SPI transfer, and retrieving the position data. The code will sequentially select each encoder, execute the SPI communication, and release the CS line, ensuring that all connected devices are queried for their position information (Listing 9).

Copy
void loop()
{
  for(int encoder = 0; encoder < sizeof(cs_pins); ++encoder)
  {
    uint8_t cs_pin = cs_pins[encoder];
 
    //set the CS signal to low
    digitalWrite(cs_pin, LOW);
    delayMicroseconds(3);
 
    //read the two bytes for position from the encoder, starting with the high byte
    uint16_t encoderPosition = SPI.transfer(AMT22_NOP) << 8; //shift up 8 bits because this is the high byte
    delayMicroseconds(3);
    encoderPosition |= SPI.transfer(AMT22_NOP); //we do not need a specific command to get the encoder position, just no-op
 
    //set the CS signal to high
    digitalWrite(cs_pin, HIGH);
 
    if (verifyChecksumSPI(encoderPosition)) //position was good, print to serial stream
    {
      encoderPosition &= 0x3FFF; //discard upper two checksum bits
      if (RESOLUTION == 12) encoderPosition = encoderPosition >> 2; //on a 12-bit encoder, the lower two bits will always be zero
 
      Serial.print("Encoder #");
      Serial.print(encoder, DEC);
      Serial.print(" position: ");
      Serial.print(encoderPosition, DEC); //print the position in decimal format
      Serial.write('\n');
    }
    else //position is bad, let the user know how many times we tried
    {
      Serial.print("Encoder #");
      Serial.print(encoder, DEC);
      Serial.print(" position error.\n");
    }
  }
 
  //For the purpose of this demo we don't need the position returned that quickly so let's wait a half second between reads
  //delay() is in milliseconds
  delay(500);
}

Listing 9: Reading the encoderPosition variable from multiple encoders.

After the data transfer, a minimum wait time is required before releasing the chip select line. According to the datasheet, this minimum time is 3 microseconds. While this delay is typically observed naturally at slower data rates, it is good practice to implement it explicitly in the code to ensure proper operation and adherence to the timing specifications. This ensures reliable communication with the AMT22 encoder.

Conclusion

Users should now have a basic understanding of configuring and reading data from Same Sky’s AMT22 absolute encoders. This article focused on the AMT22 absolute encoders. Same Sky also has a line of AMT modular encoders which offer a range of incremental, absolute, and commutation versions.

Disclaimer: The opinions, beliefs, and viewpoints expressed by the various authors and/or forum participants on this website do not necessarily reflect the opinions, beliefs, and viewpoints of DigiKey or official policies of DigiKey.

About this author

Damon Tarry, Design Applications Engineer, Same Sky