CUI Rotary encoder with MATLAB and Arduino SPI Bus

6 views (last 30 days)
I recently discovered that MATLAB now supports Arduino hardware as well which made me very excited because I was looking for a cheap and easy platform to read out a rotary encoder with a SPI interface. The rotary encoder is a CUI AMT203-V.
The encoder requires the following steps to read out the position:
Command 0x10: rd_pos (read position) This command causes a read of the current position. To read position this sequence should be followed:
  1. Master sends rd_pos command. Encoder responds with idle character.
  2. Continue sending nop_a5 command while encoder response is 0xA5
  3. If response was 0x10 (rd_pos), send nop_a5 and receive MSB position (lower 4 bits of this byte are the upper 4 of the 12-bit position)
  4. Send second nop_a5 command and receive LSB position (lower 8 bits of 12-bit positon) Note that it is possible to overlap commands.
For instance, instead of issuing nop_a5 for steps 3 and 4, you could begin another read position sequence since the position data is already in the buffer. The read and write FIFOs for the streams are 16 bytes long and it is up to the user to avoid overflow.
As a first try I used the following code with the Arduino IDE:
#include <SPI.h>
#define CS 10 //Chip or Slave select
uint16_t ABSposition = 0;
uint16_t ABSposition_last = 0;
uint8_t temp[1];
float deg = 0.00;
void setup()
{
pinMode(CS,OUTPUT);//Slave Select
digitalWrite(CS,HIGH);
SPI.begin();
SPI.setBitOrder(MSBFIRST);
SPI.setDataMode(SPI_MODE0);
SPI.setClockDivider(SPI_CLOCK_DIV32);
Serial.begin(115200);
Serial.println("starting");
Serial.flush();
delay(2000);
SPI.end();
}
uint8_t SPI_T (uint8_t msg) //Repetive SPI transmit sequence
{
uint8_t msg_temp = 0; //vairable to hold recieved data
digitalWrite(CS,LOW); //select spi device
msg_temp = SPI.transfer(msg); //send and recieve
digitalWrite(CS,HIGH); //deselect spi device
return(msg_temp); //return recieved byte
}
void loop()
{
uint8_t recieved = 0xA5; //just a temp vairable
ABSposition = 0; //reset position vairable
SPI.begin(); //start transmition
digitalWrite(CS,LOW);
SPI_T(0x10); //issue read command
recieved = SPI_T(0x00); //issue NOP to check if encoder is ready to send
while (recieved != 0x10) //loop while encoder is not ready to send <<--- This does not work in MATLAB
{
recieved = SPI_T(0x00); //cleck again if encoder is still working
delay(2); //wait a bit
}
temp[0] = SPI_T(0x00); //Recieve MSB
temp[1] = SPI_T(0x00); // recieve LSB
digitalWrite(CS,HIGH); //just to make sure
SPI.end(); //end transmition
temp[0] &=~ 0xF0; //mask out the first 4 bits
ABSposition = temp[0] << 8; //shift MSB to correct ABSposition in ABSposition message
ABSposition += temp[1]; // add LSB to ABSposition message to complete message
if (ABSposition != ABSposition_last) //if nothing has changed dont wast time sending position
{
ABSposition_last = ABSposition; //set last position to current position
deg = ABSposition;
deg = deg * 0.08789; // aprox 360/4096
Serial.println(deg); //send position in degrees
}
delay(10); //wait a bit till next check
}
With this code I can read out the position and it works as expected.
When I try with MATLAB I already run into troube at step 3, I never receive rd_pos (0x10 or 16 in uint8) back but the first response is always 24. I use the following code:
clc
clear all
close all
%% Message definitions
rd_pos = hex2dec('10');
nop_a5 = hex2dec('00');
set_zero_point = hex2dec('70');
SuccesFullZeroSet = hex2dec('80');
%% Arduino properties
Arduino = arduino('COM5', 'UNO', 'Libraries', 'SPI');
%% SPI bus properties
SPI_CSB_pin = 'D10'; % Valid for Uno
SPI_BUS_mode = 0;
SPI_BUS_BitOrder = 'msbfirst'; % From manual of amt20 sensor
SPI_BUS_Bitrate = 1e9; % From manual of amt20 sensor
SPI_BUS = spidev( Arduino, SPI_CSB_pin,...
'Mode', SPI_BUS_mode,...
'BitOrder',SPI_BUS_BitOrder, ...
'Bitrate',SPI_BUS_Bitrate);
%% Check if devide is ready to provide a position
MaxAttemps = 100;
SPI_BUS.writeRead(rd_pos,'uint8');
resp = SPI_BUS.writeRead(nop_a5,'uint8');
nAttemps = 0;
while ~(resp==rd_pos)
if nAttemps>MaxAttemps
error(['Tried to read encoder ' num2str(nAttemps) ' times, no valid response'])
end
resp = writeRead(SPI_BUS,nop_a5,'uint8');
nAttemps = nAttemps + 1;
end
I expect to receive a resp=16 back at some point but I always see 24 and then positions start coming in.
When I accept the 24 and read the following bytes by:
% Read the upper 4 bits
FirstResp = writeRead(SPI_BUS,nop_a5,'uint8');
tmp = dec2bin(FirstResp);
L = length(tmp);
if L>4
UpperFour = bin2dec(tmp(L-4:L));
else
UpperFour = bin2dec(tmp);
end
% Read the lower 8 bits
LowerEight = writeRead(SPI_BUS,obj.nop_a5);
% Combine bits and convert to integer
AbsEncoderSteps = bitshift(uint16(UpperFour),8) + uint16(LowerEight);
% Convert to angle in rad
Angle = double(AbsEncoderSteps)/4096*360;
and make a plot of the position when I turn the encoder in one direction I get the following:
so the angle varies between 0 and 360 degrees but it does not decrease as I expected and always shows strange behavior around 180 degrees. Since the very first check already fails (I don't receive 16 but 24 as an acknowledgement), I don't think I can trust the calculated angles.
Edit: The problem is solved, the bitrate had to be 1e6 instead of 1e9, very basic mistake -_-'

Answers (2)

Jan
Jan on 5 Feb 2019
I realized that my previous post was quite extensive, therefore I reduced it to a minimum working example with the following Arduino code:
#include <SPI.h>
#define CS 10 //Chip or Slave select
uint8_t temp[1];
void setup() {
pinMode(CS,OUTPUT);//Slave Select
digitalWrite(CS,HIGH);
SPI.begin();
SPI.setBitOrder(MSBFIRST);
SPI.setDataMode(SPI_MODE0);
SPI.setClockDivider(SPI_CLOCK_DIV32);
Serial.begin(115200);
Serial.println("starting");
Serial.flush();
delay(2000);
SPI.end();
}
uint8_t SPI_T (uint8_t msg) //Repetive SPI transmit sequence
{
uint8_t msg_temp = 0; //variable to hold recieved data
digitalWrite(CS,LOW); //select spi device
msg_temp = SPI.transfer(msg); //send and recieve
digitalWrite(CS,HIGH); //deselect spi device
return(msg_temp); //return recieved byte
}
void loop() {
uint8_t recieved = 0xA5;
SPI.begin(); //start transmition
SPI_T(0x10); //issue read command
while (recieved != 0x10) //loop while encoder is not ready to send
{
recieved = SPI_T(0x00); //cleck again if encoder is still working
delay(2); //wait a bit
Serial.print("Acknowledgement = ");
Serial.print(recieved,DEC);
Serial.print('\n');
}
temp[0] = SPI_T(0x00); //Recieve MSB
temp[1] = SPI_T(0x00); // recieve LSB
Serial.print("First message = ");
Serial.print(temp[0],DEC);
Serial.print('\n');
Serial.print("Second message = ");
Serial.print(temp[1],DEC);
Serial.print('\n');
SPI.end(); //end transmition
delay(1000);
This gives me the following output at the serial monitor:
starting
Acknowledgement = 165
Acknowledgement = 165
Acknowledgement = 16 %<-- Here the encoder acknowledges that it's ready
First message = 12 % This is the first message containing the upper four bits
Second message = 115 % These are the lower eight bits
Acknowledgement = 165
Acknowledgement = 165
Acknowledgement = 16 %<-- Here the encoder acknowledges that it's ready
First message = 12
Second message = 115
Acknowledgement = 165
Acknowledgement = 165
I tried to make this in MATLAB by:
clc
clear all
close all
%% Message definitions
rd_pos = hex2dec('10');
nop_a5 = hex2dec('00');
%% Arduino properties
Arduino = arduino('COM5', 'UNO', 'Libraries', 'SPI');
%% SPI bus properties
SPI_CSB_pin = 'D10'; % Valid for Uno
SPI_BUS_mode = 0;
SPI_BUS_BitOrder = 'msbfirst'; % From manual of amt20 sensor
SPI_BUS_Bitrate = 1e9; % From manual of amt20 sensor
SPI_BUS = spidev( Arduino, SPI_CSB_pin,...
'Mode', SPI_BUS_mode,...
'BitOrder',SPI_BUS_BitOrder, ...
'Bitrate',SPI_BUS_Bitrate);
%% Check if devide is ready to provide a position
resp = 0;
MaxAttemps = 10;
SPI_BUS.writeRead(rd_pos);
nAttemps = 0;
while ~(resp==rd_pos)
if nAttemps>MaxAttemps
error(['Tried to read encoder ' num2str(nAttemps) ' times, no valid response'])
end
resp = writeRead(SPI_BUS,nop_a5);
nAttemps = nAttemps + 1;
disp(['Acknowledgement = ' num2str(resp) ])
end
%% Read the upper 4 bits
FirstResp = writeRead(obj.SPI_BUS,obj.nop_a5,'uint8');
disp(['First message = ' num2str(FirstResp) ])
UpperFour = bitand(hex2dec('0F'),FirstResp,'uint8'); % Mask the first four bits
%% Read the lower 8 bits
LowerEight = writeRead(obj.SPI_BUS,obj.nop_a5);
disp(['Second message = ' num2str(LowerEight) ])
I never get out of the while loop and receive the following response:
Acknowledgement = 247
Acknowledgement = 24
Acknowledgement = 14
Acknowledgement = 255
Acknowledgement = 247
Acknowledgement = 247
Acknowledgement = 247
Acknowledgement = 247
Acknowledgement = 247
Acknowledgement = 247
Acknowledgement = 247
Error using Rotary_encoder_test (line 30)
Tried to read encoder 11 times, no valid response
What I see is that when I use Arduino IDE I receive 165 and then 16 when it's ready (as expected). With MATLAB I receive 247 and then 24 (when I think it is ready).
I read the help from the readwrite command in MATLAB where the following example is given:
writeCmd = bin2dec('0000 0010');
address = [255 0];
data = [250 155];
dataIn = [writeCmd address data];
out = writeRead(dev,dataIn)
What surprises me is that and address and data is is defined. When I use the same address and data as in the example, I receive a vector back everytime. This vector also does not contain the expected output.
I was thinking that it maybe does not give back uint8 but some other format but in which format is 0x10 respresented by 24?

stephan schulz
stephan schulz on 20 Sep 2019
Hi Jan.
I have the same encoder and the same issue.
I am not using MATLAB but see the same print out in the serial monitor.
Have you been able to solve this?
thx.
  2 Comments
Ferdinand
Ferdinand on 28 Jul 2020
Edited: Ferdinand on 28 Jul 2020
Hallo Stephan,
habe ebenfalls ein ähnliches Problem, jedoch benutze ich Matlab/Simulink. Hast du mitlerweile eine Lösung des Problems gefunden. Wie sind die Registeradressen der Commands?
Beste Grüße,
Ferdinand
Edgar Baal
Edgar Baal on 5 May 2024
Greetings,
I managed to read out the angle in MATLAB. I will share my code with you guys. Referring to the code Jan shared, there could be the following error points:
  • The datasheet of the AMT203-V states a bitrate of "typically" 1 MHz. From what we learn in physics class 1e9 is Giga so your bitrate is a little too high ;D. In my code I use 500000 kHz since I saw many examples online do the same and its enough for my application.
  • If your Voltage-Level is too low the encoder cannot work properly and starts behaving strangely. Check if your wires supply the right voltage between 4.5 V and 5.5 V according to the datasheet.
  • According to the datasheet a small delay of 20 microseconds (20e-6) between every read-command is recommended to avoid extending the read time by forcing wait sequences. Since this is just a recommendation I don't think it causes the issue but I wanted to share it with you anyways.
With my solution I am able to read out very accurate and correct angles at a sufficient speed. I never ran into any timing problems and never got to see my limit-timeout-case in action though...
It's still in german but I think you should understand it anyways. For my example I used an Joy-It Arduino Nano V3, which is only usable with MATLAB if you declare you board as an 'ProMini328_5V'. I hope my code still works and my little changes didn't wreck it. Dont forget to clear the arduino-object and device-variable after you interrupted the program. If you don't and restart it, you might get false values from the sensor. After a quick power reset everything should be finde again.
%% Workspace säubern und Umgebung vorbereiten
close all; % Alle Grafiken schließen
clear all; % Alle Variablen/Funktionen bereinigen
clc; % Command Window bereinigen
%% Headline
fprintf('#### Testprogramm zum auslesen des Winkels vom AMT203-V über MATLAB und Arduino ####\n\n');
%% BITTE ANPASSEN: Schnittstelle zu dem Microkontroller definieren
port = 'COM4'; % Kommunikationsport am PC definieren.
board = 'ProMini328_5V'; % Entwicklerboard wählen. Der Arduino Nano V3 Klon von Joy-IT funktioniert nur mit der 'ProMini328_5V'-Eingabe. Alternative Boards:'Uno' 'Nano3' 'Nano33IoT' 'Nano33BLE' 'ProMini328_5V' 'ProMini328_3V' 'Mega2560' 'MegaADK' 'Due' 'DigitalSandbox' 'Leonardo' 'Micro' 'MKR1000' 'MKR1010' 'MKRZero' 'ESP32-WROOM-DevKitV1' 'ESP32-WROOM-DevKitC'
CS = 'D10'; % CS-Pin definieren. Bei Arduino Nano V3 von Joy-IT ist das 'D10'.
timeoutLimit = 100; % Zähler für Anzeige von Kommunikationsproblemen.
winkel = 0; % Variable für Ausgabewinkel.
a = 0; % Platzhalter.
%% Aufsetzen der SPI-Kommunikation
nanoV3 = arduino(port, board, 'Libraries', 'SPI'); % Die Schnittstelle zum Arduino Nano V3 Klon aufsetzen und die notwendigen Bibliotheken installieren.
disp('Die Verbindung zu dem Arduino Nano V3 wurde aufgebaut!');
amt203v = device(nanoV3, 'SPIChipSelectPin', CS, 'BitRate', 500000, 'SPIMode', 0, 'BitOrder', 'msbfirst'); % Alternativ auf 1000000 aus Datenblatt. % Ohne Semikolon schreiben, um MISO, MOSI, SCK etc. angezeigt zu bekommen. Default-Settings: device(a, 'SPIChipSelectPin', CS', 'SPIMode', 0, 'BitOrder', 'msbfirst', 'BitRate', 4000000 --> Maximum);
pause(200e-3); % Initialisierungsphase des Encoders. Währenddessen muss dieser stationär sein. Beginnt eigentlich ab Power-Up und ist somit nicht nötig. Hier dennoch sicherheitshalber eingebaut.
disp('Die SPI-Schnittstelle zum Arduino Nano V3 wurde aufgesetzt!');
pause(2);
%% Auslesen der Winkelposition
% Funktionale Kommentare weiter unten bei der Funktionsbeschreibung, da
% sich der Code doppelt.
while(true)
a = input('Enter-Taste drücken, um Winkel abzufragen!');
winkel = getAngle(amt203v, timeoutLimit);
if(winkel == -1)
clear nanoV3 amt203v;
disp('Die Verbindung zu dem Arduino Nano V3 wurde beendet!');
return;
end
end
%% Clean-Up
clear nanoV3 amt203v; % Die SPI-Verbindung beenden.
disp('Die Verbindung zu dem Arduino Nano V3 wurde beendet!');
%% Funktionsdefinition
function wink = getAngle(amt203v, timeoutLimit)
% Beschreibung:
% Diese Funktion kommuniziert über das SPI mit dem
% AMT203-V Absolutwert-Encoder, liest dessen aktuelle Winkelposition
% ein, rechnet diese von einem 12 Bit Binärwert in Grad um und gibt
% diesen Wert aus.
%
% Eingaben:
% amt203v: Das SPI-Device zu welchem eine Schnittstelle
% über ein Arduino-Gerät aufgebaut wurde.
%
% timeoutLimit: Grenze an Kommunikationsversuchen, die
% Fehlschlagen dürfen bis die Verbindung
% aufgelöst wird.
%
% Ausgaben:
% wink: Der in Grad umgerechnete Winkelposition
% des AMT230-V. Diese wird ausgehend von dessen
% Nullposition gemessen, welche ebenfalls über SPI
% gesetzt werden kann.
timeoutCounter = 0;
nop_a5 = 0x00; % No Operation. Der Encoder macht nichts außer Daten im Speicher zurückzusenden. Wenn keine Daten verfügbar sind wird mit 0xA5 (idle character) geantwortet.
rd_pos = 0x10; % Position lesen. Dem Encoder wird gesagt, dass er die Position einlesen soll. Bis der Encoder-Chip die aktuelle Position bestimmt hat wird 0xA5 (idle character) als Antwort gesendet. Nun muss so oft nop_a5 gesendet werden, bis die Antwort rd_pos entspricht. Die nächsten zwei Antworten auf nop_a5 sind dann die beiden Bytes, die zusammen den in 12 Bit codierten Winkel ergeben.
set_zero_point = 0x70; % Nullsetzen des Winkelgeber. Wird dauerhaft im EEPROM gespeichert. Nach dem Zurücksetzen nop_a5 senden bis Antwort 0x80 ist. Danach ein Mal ausschalten und wieder einschalten, um die neue Nullposition dauerhaft zu speichern.
sig = writeRead(amt203v, rd_pos, 'uint8'); % Initialanfrage, dass die Aktuelle Winkelposition benötigt wird.
pause(20e-6); % Kurze Pause nach jedem Lesevorgang, damit der AMT203-V nicht zwischen Positionserkennung und SPI-Kommunikation priorisieren muss.
while((sig ~= rd_pos) && (timeoutCounter <= timeoutLimit)) % So oft erneut anfragen, bis die Antwort "rd_pos" lautet. Die nächsten zwei Bytes beinhalten die Winkelinformation. Wird der Timeout überschritten wird die Schleife übergangen.
sig = writeRead(amt203v, nop_a5, 'uint8');
pause(20e-6);
timeoutCounter = timeoutCounter + 1;
end
if(timeoutCounter <= timeoutLimit) % Gab es keine Timeout-Überschreitung, wird die Winkelinformation über die nächsten zwei Bytes eingelesen. Die ersten vier Bit des ersten Bytes sind dabei irrelevant, da der Encoder eine Auflösung von 12 Bit hat.
winkel_temp_MSByte = writeRead(amt203v, nop_a5, 'uint8'); % Einlesen des MS-Byte.
pause(20e-6);
winkel_temp_LSByte = writeRead(amt203v, nop_a5, 'uint8'); % Einlesen des LS-Byte.
pause(20e-6);
winkel_temp_MSByte = bitand(winkel_temp_MSByte, 0x0F); % Maskieren der ersten vier Bit des MS-Bytes.
winkel_temp = typecast([winkel_temp_LSByte winkel_temp_MSByte], 'uint16'); % Zusammenfügen von MS- und LS-Byte zu einem 16 Bit unsigned Integer, dessen ersten vier Bit "0" sind. Reihenfolge beachten!
wink = double(winkel_temp) * 360 / 4096; % Umrechnen der Winkelinformation in Grad. 2^12 = 4096 Werte auf 360° verteilt. ACHTUNG: Umwandlund in Double wichtig, da sonst mit einem fragwürdigen Wert gerechnet wird.
fprintf('Der aktuelle Winkel lautet: %f\n\n', winkel);
else % Bei Timeout-Überschreitung wird als Winkel "-1" ausgegeben und die und das Programm im Anschluss umgehend beendet.
fprintf('Fehler! Zeitüberschreitung bei der Winkelabfrage.\n\n');
wink = -1;
end
pause(20e-6);
end
Hopefully I could help you and everyone else that tries to find a solution for this after all these years.
Sincerely
Edgar

Sign in to comment.

Products

Community Treasure Hunt

Find the treasures in MATLAB Central and discover how the community can help you!

Start Hunting!