Issues getting native protocol working

Hello all,

I have ran into a problem when using the O-drive, I will try to explain the problem as detailed as possible so this post might get a bit long, apologies for that.

I want to control the O-drive in current control mode (i.e. torque control mode with torque constant of 1) via an external microcontroller board (K64F).
Because I want a fairly fast update rate to do measurements (up to about 100hz) I want to use the native protocol to communicate over the UART, with an increased baud rate of 912600 baud.

For now the procedure is to initialize the O drive with a few laptop commands, and then transition to the K64F for sending the torque commands.

The O drive has the following configuration (see the json config file here)
UART baud rate 921600
Current limit 5 A
Velocity limit 9999999
Brake resistance 2 ohm
Pole pairs 7
torque constant 1
motor type MOTOR_TYPE_HIGH_CURRENT
encoder counts 16384 (4096*4)
controller mode CONTROL_MODE_TORQUE_CONTROL

Then after boot, the following commands are issued by laptop to initialize the O-drive for use:
// Calibration sequence
odrv0.axis0.requested_state = AXIS_STATE_FULL_CALIBRATION_SEQUENCE
// Set mode to closed loop controller
odrv0.axis0.requested_state = AXIS_STATE_CLOSED_LOOP_CONTROL

So at this point, a command can be used to set the desired current:
odrv0.axis0.controller.input_torque = current required in amps

But again, that last line I want to be able to do via microcontroller.

I have confirmed to have the O-drive working with a laptop, and I have confirmed that my K64F is spitting out UART data. The UART Tx is connected to GPIO 2 pin of the O-drive.

An example packet directly captured from the uC tx port shows the following data:

38 6A 00 5A 00 00 7F 80 00 00 92 DC [all in hex]
Sequence number: 38 6A (although according to documentation seq nums are not used)
Endpoint ID: 00 5A (dec: 90, as this seems to be AXIS__CONTROLLER__CURRENT_SETPOINT in the python odrivetool)
Expected response size: 00 00 (I don’t care about a response from O-drive)
Payload: 7F 80 00 00 (32 bit float, little endian, 4.6e-41)
CRC16: 92 DC (polynomial 0x3d65, init val 0x1337, calculated over just the data bytes, or should it be over all previous bytes? Correct value as tested with the suggested CRC calculator)

How can I confirm that the O-drive actually receives packages? Am I missing something in how the package should be formatted? Is the CRC calculated over just the data bytes, or over all the previous bytes of the message?

Thank you very much in advance for your time.

OK, it looks like you have several issues here. I will summarize then get into details.

  1. first thing I see is that you do not have the stream header wrapped around the packet. For Serial data you have to send the Stream header, the Packet, then the Stream CRC. The stream CRC16 is calculated for the command packet only. Not including the stream header, or the Stream CRC bytes

  2. Endpoint 90 is not Current endpoint in software version 0.5.1 that I am using, but it might be in yours!!!

  3. Payload. Do you really want to set the setpoint to an infinitesimal value? I would think something like 0x66660e40 ( 2.224999904632568359375 amps ) would be more useful

  4. CRC16. This value is not a CRC that has anything to do with the command sent. It would be more accurately described as a “API Signature” derived from the CRC16 of the command set available in the firmware of the ODrive. For firmware version 0.5.1 the “API Signature” would be 0x9b40

For firmware V0.5.1 A valid packet to setting the Current Setpoint for axis0 to 2.2249 Amps would be:

0xaa, 0x0c, 0xE1, 0x00, 0x94, 0x00, 0x94, 0x00, 0x00, 0x66, 0x66, 0x0e, 0x40, 0x40, 0x9b, 0x7D, 0x03

Let me know how it works, or if you have any questions
-John

1 Like

Oh yes…

Also see…

by @mbilsky

2 Likes

Hey John,

Thank you very much for your quick and concise reply. I will have a look at things, and report back if I run into any issues (or none at all).
Also, that is quite a useful video, I was not aware of it, thanks for linking it!

1 Like

Your welcome.

One thing to verify is what version of the odrive firmware you are using. If you are not using 0.5.1, the command I built for you will not work. Also, I have not tested that command, so there might be a problem with… :wink:

-John

1 Like

After spending some time modifying my code, I think I have got it mostly working now.

Endpoint 90 is not Current endpoint in software version 0.5.1 that I am using, but it might be in yours!!!

I downloaded the endpoints straight from the odrive, and it seems that this is now 199 (AXIS__CONTROLLER__TORQUE_SETPOINT). So I have changed this. I noticed you have used AXIS__MOTOR__CURRENT_CONTROL__ID_SETPOINT = 148, but I assume that these are identical if the torque constant is set to 1, correct?

Payload. Do you really want to set the setpoint to an infinitesimal value? I would think something like 0x66660e40 ( 2.224999904632568359375 amps ) would be more useful

This was just a value put out by the torque control loop running on my microcontroller, and as the torque setpoint was 0 this is kind of to be expected. But yes, for debugging it is better to cut the control loop out of the equation.

CRC16. This value is not a CRC that has anything to do with the command sent. It would be more accurately described as a “API Signature” derived from the CRC16 of the command set available in the firmware of the ODrive. For firmware version 0.5.1 the “API Signature” would be 0x9b40

Luckily this is pretty easy to implement as it doesn’t change (unless the firmware for the O-drive changes).

One thing to verify is what version of the odrive firmware you are using. If you are not using 0.5.1

I have flashed the most recent version, and double checked with the odrivetool that it is indeed 0.5.1 as well.

For firmware V0.5.1 A valid packet to setting the Current Setpoint for axis0 to 2.2249 Amps would be:
0xaa, 0x0c, 0xE1, 0x00, 0x94, 0x00, 0x94, 0x00, 0x00, 0x66, 0x66, 0x0e, 0x40, 0x40, 0x9b, 0x7D, 0x03

This leaves just 1 question, which is over what data is the last CRC16 calculated? If I use the whole set of bytes of the example you provided (data until 0x7D, 0x03), I get a crc16 value of 0xC749 (poly of 0x3d65, initial val of 0x1337) with the calculator.

Apart from this CRC at the end, my code now seems to produce the correct data.

The Stream CRC16 at the end should be on the data starting with the 4th byte ( first byte after the 3 byte Stream Header, and ending with the third byte from the end ( byte before the Stream CRC16.

So in my example the CRC16 does not count bytes 0xaa, 0x0c, and 0xe1 at the start.
Nor does it include 0x7d and 0x03 at the end.

It looks like I might have an error on the CRC16 of the example. It looks like it should be 0xF249.

Oddly enough, It seems that if you send a bad or malformed packet, the odrive responds with the ASCII text “unknown command”. For example if you have a wrong crc16…

It would be much better if it responded with a Native Command packet that indicated a “unknown command”.

1 Like

The Stream CRC16 at the end should be on the data starting with the 4th byte ( first byte after the 3 byte Stream Header, and ending with the third byte from the end ( byte before the Stream CRC16.

It looks like I might have an error on the CRC16 of the example. It looks like it should be 0xF249.

That clears things up, and indeed makes my code work. First thing tomorrow I will test it on the hardware, and see if it will actually acknowledge the command with a current to the motor. Exciting stuff.

Oddly enough, It seems that if you send a bad or malformed packet, the odrive responds with the ASCII text “unknown command”. For example if you have a wrong crc16…
It would be much better if it responded with a Native Command packet that indicated a “unknown command”.

I have read about this somewhere else on the forum where someone indeed found that even if the ASCII protocol was disabled, he still got these unknown command messages in ASCII. Strange indeed.

I also do not really understand why you wouldn’t do the CRC16 over the entire data ‘stream’ packet, and omit the CRC8 in the beginning.

Thank you very much for your help!

I have adjusted the code, and with a logic analyzer I can see that the following packet is being transmitted:
// Sync bit, package length (12 bytes) and CRC8
0xAA 0x0C 0xE1

// – Packet start
// Sequence number (fixed at 0x94 for now)
0x00 0x94
// End point ID (0xC7 → 199 → AXIS__CONTROLLER__TORQUE_SETPOINT)
0x00 0xC7
// Response size (fixed at 0)
0x00 0x00
// “payload”, float, 2.224999904632568359375 [A]
0x66 0x66 0x0E 0x40
// CRC16 of endpoint list (fixed)
0x40 0x9B
// – Packet stop

// CRC16 of packet
0x7B 0x47

I get no response/data on the odrive TX, not even an ‘unkown command’ in ASCII. This seems to point at that it is not malformed or bad, but rather a wiring issue. Also I sometimes get ‘glitch spikes’ in the odrive TX line, hinting that it is a high impedance line, whilst we expect a TX to be low impedance. Then again, if I would have wired the two TX lines together, I wouldn’t expect the messages to show up on the bus either because the O-drive would force the bus low/high as well.
I have also confirmed that the TX of my uc is connected to GPIO 2 of the odrive (and the TX of odrive (GPIO 1) is connected to the logic analyzer as well).

O drive itself dumps no errors.

Can you confirm whether this packet is properly formed? And am I missing something that must be enabled to use native protocol with serial? The documentation says it does not, but that might have changed.

Well, it looks like i fumbled the packet construction again! :unamused: :unamused:

Also, we told the odrive in two different places that we did not want it to send us any information back.

In the Endpoint field bit 15 ( the MSB ), is set to 0. This tells the odrive that we do not expect any information back.

Also we set the response size to zero. This tell it how many bytes to send back.


So… Moving forward lets change things up a bit for testing…

First off lets send a couple packets to the odrive to test communications. Here are a couple of requests that get information from the odrive, but don’t change its state.

byte Request_Serial = { 0xaa, 0x08, 0x3d, 0x04, 0x00, 0x04, 0x80, 0x08, 0x00, 0x40, 0x9b, 0x9b, 0x94 };
byte Request_VBUS = { 0xaa, 0x08, 0x3d, 0x01, 0x00, 0x01, 0x80, 0x04, 0x00, 0x40, 0x9b, 0xd3, 0x58 };

Request_Serial will return the odrive Serial Number. It is a hex string
Request_VBUS will return the odrive main bus voltage. It is a four byte float

On both of the these, you should note that the high byte of the EndPoint is 0x80, and that the Response Size is non-zero. This means that they will return the requested values.

Now, with the packet we have been working with ( i am really sorry for the mangles packets :grimacing: ) I had the bytes for the Sequence Number and Endpoint reversed.

I fixed that, and now lets have it return an acknowledgement without any data. To do this we set bit 15 ( the MSB) of the endpoint and leave the Response Size set to 0x0000.

0xaa, 0x0c, 0xE1, 0x94, 0x00, 0x94, 0x80, 0x00, 0x00, 0x66, 0x66, 0x0e, 0x40, 0x40, 0x9b, 0x7D, 0x03

If we send the same packet but also set the response size, the odrive will send us the old setpoint value, then update the setpoint with the data we sent.

Let me know how these work.
-John

1 Like

Also, we told the odrive in two different places that we did not want it to send us any information back.
In the Endpoint field bit 15 ( the MSB ), is set to 0. This tells the odrive that we do not expect any information back.
Also we set the response size to zero. This tell it how many bytes to send back.

Is this a problem? I would imagine that if the MSbit of the endpoint field is set to 0, it will ignore the response size anyways.

I had the bytes for the Sequence Number and Endpoint reversed.

For the sequence number, that does not really matter anyways right? But it does clear up some confusion with me, as it seemed the endianness changed.

Some good news though!

Request_Serial will return the odrive Serial Number. It is a hex string
Request_VBUS will return the odrive main bus voltage. It is a four byte float

These two both get a response, so that does confirm that the Odrive is listening to what we send it.

However, also with the fixed endianness there’s no response or change in the situation.

0xaa, 0x0c, 0xE1, 0x94, 0x00, 0x94, 0x80, 0x00, 0x00, 0x66, 0x66, 0x0e, 0x40, 0x40, 0x9b, 0x7D, 0x03

And sadly sending these exact bytes also does not do anything either. The O-drive gives no response at all, and I have tried changing the response size to 0x0004, but that also gives no change.

Maybe the issue now is that not the proper endpoint is addressed?

edit:

When looking at this again:

0xaa, 0x0c, 0xE1, 0x94, 0x00, 0x94, 0x80, 0x00, 0x00, 0x66, 0x66, 0x0e, 0x40, 0x40, 0x9b, 0x7D, 0x03

It seems that the CRC16 is not correct both in value (should be 0x17A7), and in endianness as the packages that did receive a response use the CRC in big endian format (MSB first). After fixing this, I do get a response:
0xAA 0X02 0XDC 0X94 0X80 0X81 0X24
Which is an empty package, but that makes sense. However, still the O-drive does not move the output.

Fixing my own code does give an ASCII response now, namely the dreaded ‘unkown command’, so at least some progress is being made.

For completeness sake, this is the message at this point:
0xAA 0X0C 0XE1 | 0X44 0X6C 0XC7 0X00 0X00 0X00 0X66 0X66 0X0E 0X40 0X40 0X9B | 0X14 0XB0

Can you send the code you have creating your packet? I might be able to spot the problem.

Yea, forgot all about the CRC :grimacing: I don’t have things setup to test the current control packet. I guess I need to use more examples that I can test before putting here on the support board.

Sequence number does not really matter except it allows you to know what data you are receiving from the odrive. The sequence number is the only way you know what the received data from the odrive means, because all you get back is the Sequence number and the data.

In my case I set the low 15 bits to the same as my Endpoint. Then I know what the data represents. And I don’t have to keep track of sequence numbers. In theory, with a system like this you could get data returned out of order so sequence numbers let you match requests to responses.

In my case I don’t need to match them up, I just need to know what the response contains. If a packet gets dropped, it is OK, because I will be sending another request soon anyway.

Yes, the msb of the endpoint being zero should cause the odrive to skip any replies so the expected response should be ignored. I was just pointing out that we specified in two places that we did not want a response.

1 Like

These are the relevant functions, I think most of it is self-explanatory, but if anything is unclear just ask.
Note that everything under the odrive namespace is directly from the endpoint list downloaded from the O-drive.

// Top function used to request a required current from the O-drive
void OdriveComms::setCurrent(float currentSetpoint){
    this->sendFloat(odrive::AXIS__CONTROLLER__TORQUE_SETPOINT, currentSetpoint);
}

// Wrapper function to send a float unit over UART to abstract usage of the class
void OdriveComms::sendFloat( uint16_t address, float floatToSend) {
	uint8_t* ptr; // Let this point to the address of the 4 byte float

    ptr = (uint8_t*)&floatToSend; // A little C trick, let the uint8_t pointer point to the address of the integer by typecasting its address to the desired type

    // Now ptr[0] has the lowest significant byte
    if(CONST_LSB_FIRST == true){ // Note that ARM is generally little endian, so only flip if big endian is desired
        // Re-order the bytes into little endian format (MSByte first)
        for (int l = 0; l < 2; l++) {
            uint8_t buf = ptr[l];
            ptr[l] = ptr[3 - l];
            ptr[3 - l] = buf;
        }
    }
	// Construct and send the message
	this->sendMessage(address, ptr, 4); // Datasize is 4 
}

// Sends a message to the O-drive.
// Data provided should be little endian (i.e. data[0] = MSByte))
// Total package size limit is at 30 bytes
void OdriveComms::sendMessage(uint16_t address, uint8_t* data, uint8_t datasize) {
	// Size of a message depends on the payload size (data size)
	// For a payload of size N, the total message size in bytes is N + 8 
	// Message structure as follows:
	// B0 + B1 = sequence number
	// B2 + B3 = Endpoint ID (register of o-drive)
	// B4 + B5 = 0 (how many bytes expected in return)
	// B6 - BN+5 = payload
	// BN+6 + BN+7 = CRC over JSON, which is constant per Odrive version. Defined in parameters.h
	// For more details see "Protocal Analysis.odt"
	// The structure is little Endian, i.e. MSByte last 

    // Since dynamic memory cannot be used, static memory with safety check is used
    // Note that dynamic memory allocation compromises realtime behavior because it is unpredictable and potentially unacceptable in terms of performance.
    // It also lacks limits, and can give rise to memory fragmantation of the heap without proper management
    if(datasize + 8 < 31){

        uint8_t dataToSend[30]; // max size of 30 bytes, which should be plenty

        // Add the sequence number
        dataToSend[0] = this->sequenceNumber; // Upper part of the 16 bit variable (this first because little endian)
        dataToSend[1] = this->sequenceNumber >> 8; // Lower part of the 16 bit variable

        // Add enpoint ID (address)
        address &= 0x7FFF;// Ensure that the MSB is set to 0 as no return is expected in response
        dataToSend[2] = address; // Upper part of the 16 bit variable
        dataToSend[3] = address >> 8; // Lower part of the 16 bit variable

        // Add zeros for 'bytes expected in return'
        dataToSend[4] = 0x00;
        dataToSend[5] = 0x00;

        // Add payload
        memcpy(dataToSend + (sizeof(uint8_t)*6),data,datasize);

        // Add the JSON CRC which is constant and predefined. Defined in parameters.h
        dataToSend[datasize + 6] = odrive::json_crc; // Upper part of checksum
        dataToSend[datasize + 7] = odrive::json_crc >> 8; // Lower part of checksum

        // Send the message
        this->transmitPacket(dataToSend, datasize+8);

        // Increase sequence number in preperation for next packet that will be sent
        this->sequenceNumber++;

    }
}

// Wraps the packet into a stream and sends the bytes over UART
// The *data should contain the packet of size datasize
void OdriveComms::transmitPacket(uint8_t* data, uint8_t packetsize) {
	// https://docs.odriverobotics.com/protocol
    // Message structure as follows:
	// B0 = Sync byte, constant at 0xAA
	// B1 = Packet length
	// B2 = CRC8 of the B0 and B1
	// B3 - BN+3 = packet
	// BN+4 + BN+5 = CRC over previous bytes? TODO double check
	// The structure is little Endian, i.e. MSByte last 


    if(packetsize + 5 < 41){ // Double check the packet size, ignore if too big

        uint8_t dataStream[40]; // The stream that will be send
        uint8_t streamsize = packetsize + 5; // There are 5 additional 'wrapper bytes'


        // Wrap the original packet into a stream
        dataStream[0] = 0xAA; // Sync byte
        dataStream[1] = packetsize; // Packet length

        // Add the CRC8 of the previous 2 bytes
        dataStream[2] = this->calculateCRC8(dataStream, 2);

        // Add the packet
        memcpy(dataStream + (sizeof(uint8_t)*3),data,packetsize);

        // Add CRC16
        uint16_t CRC16_checksum = this->calculateCRC16(data, packetsize); // Do the CRC16 over just the data packet
        dataStream[packetsize + 3] = CRC16_checksum >> 8;   // Upper part of checksum
        dataStream[packetsize + 4] = CRC16_checksum;        // Lower part of checksum

        // Send bytes over UART
        // Only do so if serial port is initialized
        if(this->odriveserialport != NULL){
            this->odriveserialport->write(dataStream, streamsize); 

            // Below are a few debug statements that request serial number and bus voltage to test working of UART controls
            //uint8_t Request_Serial[] = { 0xaa, 0x08, 0x3d, 0x04, 0x00, 0x04, 0x80, 0x08, 0x00, 0x40, 0x9b, 0x9b, 0x94 };
            //uint8_t Request_VBUS[] = { 0xaa, 0x08, 0x3d, 0x01, 0x00, 0x01, 0x80, 0x04, 0x00, 0x40, 0x9b, 0xd3, 0x58 };
            //uint8_t Request_Current[] = {0xaa, 0x0c, 0xE1, 0x94, 0x00, 0x94, 0x80, 0x00, 0x00, 0x66, 0x66, 0x0e, 0x40, 0x40, 0x9b, 0x17, 0xA7};
            //this->odriveserialport->write(Request_Serial, 13);
            //this->odriveserialport->write(Request_VBUS, 13);
            //this->odriveserialport->write(Request_Current, 17);
        }
        // Note that when calling this function, the data gets copied to a hardware register, so the original data used to call the function can be modified/deleted etc without worries
        // Note that the serial port should be configured as non-blocking to ensure that the program does not hang here (especially if called via interrupts)
    }
}

In my case I don’t need to match them up, I just need to know what the response contains. If a packet gets dropped, it is OK, because I will be sending another request soon anyway.

In my application (which is a control system, the O-drive is just used as a current controller for the brushless motor) I don’t care about what the O-drive sends back regardless. So we don’t need to worry about any kind of response.

What might be important to mention, is that this control loop runs at 1khz.

Again, the message this creates is:
For completeness sake, this is the message at this point:
0xAA 0X0C 0XE1 | 0X44 0X6C 0XC7 0X00 0X00 0X00 0X66 0X66 0X0E 0X40 0X40 0X9B | 0X14 0XB0

OK, we are getting close…

{
“name”:“pos_setpoint”,
“id”:197,
“type”:“float”,
“access”:“r”
},
{
“name”:“vel_setpoint”,
“id”:198,
“type”:“float”,
“access”:“r”
},
{
“name”:“torque_setpoint”,
“id”:199,
“type”:“float”,
“access”:“r”
},

Endpoint 199 (0xc7) is a read only endpoint. That is why nothing is happening. You are asking it to return data, but telling it not to send any bytes.

However, as it turns out in the 0.5.x firmware, they have removed direct current control.

They have implemented a new set of inputs…

{
              "name":"input_pos",
              "id":194,
              "type":"float",
              "access":"rw"
           },
           {
              "name":"input_vel",
              "id":195,
              "type":"float",
              "access":"rw"
           },
           {
              "name":"input_torque",
              "id":196,
              "type":"float",
              "access":"rw"
           },

They have replaced current control with torque control (Endpoint 196). I am hoping that is actually what you want…
Also, at least until things are working, I would recommend setting bit 15 on the Endpoint so you get some sort of response.

-John

1 Like

Hey John,

Endpoint 199 (0xc7) is a read only endpoint. That is why nothing is happening. You are asking it to return data, but telling it not to send any bytes.

However, as it turns out in the 0.5.x firmware, they have removed direct current control.

This was indeed the issue! Changing the endpoint to 196 makes it work perfectly, thank you very much.

By the way, where did you find that handy list of endpoints? In the source code of the odrive?

They have implemented a new set of inputs…

They have replaced current control with torque control (Endpoint 196). I am hoping that is actually what you want…
Also, at least until things are working, I would recommend setting bit 15 on the Endpoint so you get some sort of response.

Yes this is perfect, by setting the torque constant to 1 I can effectively implement current control.

Again, many thanks for your feedback and info, I would have spend much much more time figuring this all out without your help.