VESC UART Control with Spin

in Propeller 1
As mentioned in another thread, I'm working on an electric skateboard.
I'm hoping to use a Propeller to interface with several VESC (Vedder Electronic Speed Controller) boards. One reason for using VESC boards is VESC is an open source project. The source code for the VESC is available. The C code is out of my comfort zone. Rather than attempting to modify the VESC code, I'm hoping to interface with the VESC board using a Propeller.
There's a guide to Communicating with the VESC using UART but I find the instructions a bit sparse.
I'm attempting to figure out the commands and how they are used from the source code. I'm hopeful I'll be able to figure this all out but I thought I'd check here in case any of you have done something similar.
Do any of you have any suggestions on how to interface a Propeller (Spin or PASM preferred) with a VESC?
Searching the forums for "VESC" didn't produce any hits.
I'm hoping to use a Propeller to interface with several VESC (Vedder Electronic Speed Controller) boards. One reason for using VESC boards is VESC is an open source project. The source code for the VESC is available. The C code is out of my comfort zone. Rather than attempting to modify the VESC code, I'm hoping to interface with the VESC board using a Propeller.
There's a guide to Communicating with the VESC using UART but I find the instructions a bit sparse.
I'm attempting to figure out the commands and how they are used from the source code. I'm hopeful I'll be able to figure this all out but I thought I'd check here in case any of you have done something similar.
Do any of you have any suggestions on how to interface a Propeller (Spin or PASM preferred) with a VESC?
Searching the forums for "VESC" didn't produce any hits.
Comments
The VESC communicates over UART using packets with the following format: One Start byte (value 2 for short packets and 3 for long packets) One or two bytes specifying the packet length The payload of the packet Two bytes with a CRC checksum on the payload One stop byte (value 3)
Why on earth would you use a stop byte value that is the same as a possible start byte value ?
Bean
I don't know. I think the original comm protocol used CAN. I'm guessing the "3" value for start and stop doesn't present the same issue when using CAN.
The VESC communicates over UART using packets with the following format: One Start byte (value 2 for short packets and 3 for long packets) One or two bytes specifying the packet length The payload of the packet Two bytes with a CRC checksum on the payload One stop byte (value 3)
The line "The payload of the packet" is disappointingly vague. I'm reading through the code attempting to understand how this payload is structured.
I'll likely load one of the Arduino based examples and monitor the com line with a logic analyzer to see if the payload structure becomes understandable.
I was kind of hoping there would be a list of commands and what they do someplace (other than within the code). Something like this one for Dynamixel actuators would be great.
I like to think I'll be able to figure this out from the code and from monitoring communication from an Arduino. I'll post what I learn to the forum in case anyone else wants to control a VESC using a Propeller.
I believe the commands are listed in this file (part of a Raspberry Pi VESC project).
There's also a Python file to get values from the VESC and set values in the VESC.
Hopefully this is enough info for me to write a Spin program to interface with the VESC. I'll post an update whether or not it works.
Packet structure is described in the blog
One Start byte (value 2 for short packets and 3 for long packets) One or two bytes specifying the packet length The payload of the packet Two bytes with a CRC checksum on the payload One stop byte (value 3)
the start byte value basically indicates the index where the payload starts (ie length > 255, two bytes needed, use 3 other wise 2)
For the payload you need to look in the bldc_interface.c file. It seems to consist of a command followed by a set of parameters depending on the command. The commands used are defined in datatypes.h.
Example using the command given in the blog:
bldc_interface.c
void bldc_interface_set_current(float current) { if (motor_control_set_func) { motor_control_set_func(MOTOR_CONTROL_CURRENT, current); return; } int32_t send_index = 0; send_buffer[send_index++] = COMM_SET_CURRENT; buffer_append_float32(send_buffer, current, 1000.0, &send_index); send_packet_no_fwd(send_buffer, send_index); }
datatypes.h
typedef enum { MOTOR_CONTROL_DUTY = 0, MOTOR_CONTROL_CURRENT, MOTOR_CONTROL_CURRENT_BRAKE, MOTOR_CONTROL_RPM, MOTOR_CONTROL_POS } motor_control_mode; .... typedef enum { COMM_FW_VERSION = 0, COMM_JUMP_TO_BOOTLOADER, COMM_ERASE_NEW_APP, COMM_WRITE_NEW_APP_DATA, COMM_GET_VALUES, COMM_SET_DUTY, COMM_SET_CURRENT, COMM_SET_CURRENT_BRAKE, COMM_SET_RPM, COMM_SET_POS, COMM_SET_HANDBRAKE, COMM_SET_DETECT, COMM_SET_SERVO_POS, COMM_SET_MCCONF, COMM_GET_MCCONF, COMM_GET_MCCONF_DEFAULT, COMM_SET_APPCONF, COMM_GET_APPCONF, COMM_GET_APPCONF_DEFAULT, COMM_SAMPLE_PRINT, COMM_TERMINAL_CMD, COMM_PRINT, COMM_ROTOR_POSITION, COMM_EXPERIMENT_SAMPLE, COMM_DETECT_MOTOR_PARAM, COMM_DETECT_MOTOR_R_L, COMM_DETECT_MOTOR_FLUX_LINKAGE, COMM_DETECT_ENCODER, COMM_DETECT_HALL_FOC, COMM_REBOOT, COMM_ALIVE, COMM_GET_DECODED_PPM, COMM_GET_DECODED_ADC, COMM_GET_DECODED_CHUK, COMM_FORWARD_CAN, COMM_SET_CHUCK_DATA, COMM_CUSTOM_APP_DATA, COMM_NRF_START_PAIRING } COMM_PACKET_ID;
packet payload
0: 6 (COMM_SET_CURRENT) 1-4: float32 representation of 10.0 5-8: float32 representation of 1000.0
Problem: my google-fu was not good enough to find how gcc stores a float32 (might be the same as binary32 ??)
CRC = CRC16 value of the payload
I like to think I'm slowly making sense of it. Of course it's easier to think I understand it when I haven't tested my knowledge yet.
I'm pretty sure the float in the VESC is packaged the same way F32.spin packages a 32-bit float.
I've used CRC16 in other projects. I found some code by Mike Green which works great.
PUB Crc16(bufferPtr, size) '' This method was written by Mike Green. '' Found on Parallax forums. result := $FFFF repeat size result ^= byte[bufferPtr++] repeat 8 result := result >> 1 ^ ($A001 & (result & 1 <> 0))
Thanks for your help in deciphering how the payload it built.
I just found this reply from Vedder on a forum. It looks really helpful. I had the CRC bytes reversed.
I'll give this another try with this new info.
Here's the code I tried.
PUB GetValues Pst.str(string(11, 13, "GetValues")) vescPayload := Vesc#COMM_GET_VALUES_4 'vescBuffer[2] vescSize := 1 'vescBuffer[1] result := Crc16(@vescPayload, 1) vescBuffer[3] := result >> 8 vescBuffer[4] := result vescBuffer[5] := 3 TxOut(@vescBuffer, 6, globalEchoFlag) DAT ' VESC Packet Buffer vescBuffer byte 2 vescSize byte 0-0 vescPayload byte 0-0[257]
The above code sent the following data.
<$02> <$01> <$04> <$83> <$BE> <$03>
Hopefully it's obvious those are bytes displayed in hexadecimal.
I'll likely purchase the appropriate hardware to test the example code provided by Vedder. Hopefully I'll be able to figure out what I'm doing wrong by studying logic analyzer captures of valid packets.
I found an Arduino program to communicate with the VESC and here's a capture from my logic analyzer.
The output from the Arduino code is:
<$02> <$01> <$04> <$40> <$84> <$03>
The proper CRC is $40 $84.
I'll need to figure out the correct way to calculate the CRC.
unsigned short crc16(unsigned char *buf, unsigned int len) { unsigned int i; unsigned short cksum = 0; for (i = 0; i < len; i++) { cksum = crc16_tab[(((cksum >> 8) ^ *buf++) & 0xFF)] ^ (cksum << 8); } return cksum; }
The array "crc16_tab" has 256 16-bit elements. I'll include this table with the Spin code.
Edit: As JonnyMac points out below, my CRC code is wrong. Look for updated code later in this thread. (Coming soon.)
PUB GetValues Pst.str(string(11, 13, "GetValues")) vescPayload := Vesc#COMM_GET_VALUES_4 'vescBuffer[2] vescSize := 1 'vescBuffer[1] result := Crc16Vesc(@vescPayload, 1) vescBuffer[3] := result >> 8 vescBuffer[4] := result vescBuffer[5] := 3 TxOut(@vescBuffer, 6, globalEchoFlag) PUB Crc16Vesc(bufferPtr, size) | tableIndex repeat size tableIndex := (((result >> 8) | byte[bufferPtr++]) & $FF) | (result << 8) result := crcTable[tableIndex] DAT ' CRC Table 256 elements crcTable word $0000, $1021, $2042, $3063, $4084 word $50a5, $60c6, $70e7, $8108, $9129, $a14a, $b16b, $c18c, $d1ad word $e1ce, $f1ef, $1231, $0210, $3273, $2252, $52b5, $4294, $72f7 word $62d6, $9339, $8318, $b37b, $a35a, $d3bd, $c39c, $f3ff, $e3de word $2462, $3443, $0420, $1401, $64e6, $74c7, $44a4, $5485, $a56a word $b54b, $8528, $9509, $e5ee, $f5cf, $c5ac, $d58d, $3653, $2672 word $1611, $0630, $76d7, $66f6, $5695, $46b4, $b75b, $a77a, $9719 word $8738, $f7df, $e7fe, $d79d, $c7bc, $48c4, $58e5, $6886, $78a7 word $0840, $1861, $2802, $3823, $c9cc, $d9ed, $e98e, $f9af, $8948 word $9969, $a90a, $b92b, $5af5, $4ad4, $7ab7, $6a96, $1a71, $0a50 word $3a33, $2a12, $dbfd, $cbdc, $fbbf, $eb9e, $9b79, $8b58, $bb3b word $ab1a, $6ca6, $7c87, $4ce4, $5cc5, $2c22, $3c03, $0c60, $1c41 word $edae, $fd8f, $cdec, $ddcd, $ad2a, $bd0b, $8d68, $9d49, $7e97 word $6eb6, $5ed5, $4ef4, $3e13, $2e32, $1e51, $0e70, $ff9f, $efbe word $dfdd, $cffc, $bf1b, $af3a, $9f59, $8f78, $9188, $81a9, $b1ca word $a1eb, $d10c, $c12d, $f14e, $e16f, $1080, $00a1, $30c2, $20e3 word $5004, $4025, $7046, $6067, $83b9, $9398, $a3fb, $b3da, $c33d word $d31c, $e37f, $f35e, $02b1, $1290, $22f3, $32d2, $4235, $5214 word $6277, $7256, $b5ea, $a5cb, $95a8, $8589, $f56e, $e54f, $d52c word $c50d, $34e2, $24c3, $14a0, $0481, $7466, $6447, $5424, $4405 word $a7db, $b7fa, $8799, $97b8, $e75f, $f77e, $c71d, $d73c, $26d3 word $36f2, $0691, $16b0, $6657, $7676, $4615, $5634, $d94c, $c96d word $f90e, $e92f, $99c8, $89e9, $b98a, $a9ab, $5844, $4865, $7806 word $6827, $18c0, $08e1, $3882, $28a3, $cb7d, $db5c, $eb3f, $fb1e word $8bf9, $9bd8, $abbb, $bb9a, $4a75, $5a54, $6a37, $7a16, $0af1 word $1ad0, $2ab3, $3a92, $fd2e, $ed0f, $dd6c, $cd4d, $bdaa, $ad8b word $9de8, $8dc9, $7c26, $6c07, $5c64, $4c45, $3ca2, $2c83, $1ce0 word $0cc1, $ef1f, $ff3e, $cf5d, $df7c, $af9b, $bfba, $8fd9, $9ff8 word $6e17, $7e36, $4e55, $5e74, $2e93, $3eb2, $0ed1, $1ef0
My Propeller version of the CRC produces the same output as the Arduino.
So far, I've only computed the CRC for a single byte payload. Hopefully this method works for larger payloads as well.
Now I need to figure out how to parse out the reply message. The reply includes 16-bit floats which I haven't used with the Propeller before. Hopefully it won't be hard to convert 16-bit floats into 32-bit floats so I can use F32.spin to perform appropriate operations on the floating point values.
float f = ((h&0x8000)<<16) | (((h&0x7c00)+0x1C000)<<13) | ((h&0x03FF)<<13);
edit: making good progress
Wow! Thank you. That's just what I tried to find but couldn't . I had hoped it would be as easy as shifting some bits.
I didn't realize my C was that rusty. Thank you!
I love this forum!
I finally found my other mistakes and my calculated CRC now agrees with the one sent from the VESC. I also modified my CRC code to deal with a circular buffer. Here's my current CRC code.
PUB Crc16Vesc(bufferAddress, bufferSize, byteIndex, dataSize) | tableIndex, localByte, shiftedHigh ' bufferAddress is the beginning of a ring buffer. ' bufferSize is the size of the ring buffer. ' byteIndex is the buffer index of the first byte to be used in the CRC calculation. ' dataSize is the number of bytes to be used in the CRC calculation. ' The method AdvanceWithRollover will zero byteIndex when the end of the buffer is reached. repeat dataSize localByte := byte[bufferAddress][byteIndex] ' It safest to perform bit operations on longs, not bytes. AdvanceWithRollOver(@byteIndex, bufferSize) ' Make sure byteIndex doesn't go past end of buffer. tableIndex := ((result >> 8) ^ localByte) & $FF shiftedHigh := result << 8 result := crcTable[tableIndex] ^ shiftedHigh result &= $FFFF ' This is required since we're using 32-bit longs.
The "Get Values" command is successfully sent and I receive a 73 byte payload in return. I haven't started parsing the payload yet but here's one of the payloads returned.
goodCrcCount = 3, badCrcCount = 0 vescRxPacketExpected = 73 vescRxPacketSizeSoFar = 73 vescHead = 234, vescTail = 233 crcAsReceived = 21958 = $55C6, crcComputed = 21958 = $55C6 <$04> <$01> <$43> <$00> <$E3> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$01> <$CF> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$01> <$00> <$00> <$00> <$03> <$00> <$10> <$D8> <$22> <$60> <$60> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$FF> <$FF> <$FF> <$FD> checkInputCount = 6241 Rx Character = g GetValues TxOut, size = 6 <$02> <$01> <$04> <$40> <$84> <$03>
I also included the CRCs to show the calculated value agrees with the received value. The six bytes at the bottom of the text block are the bytes sent to the VESC.
Thanks again to everyone who has helped so far.
The first byte of the payload is 4 which indicates it's a reply for my request for "values" but I doubt I have the various values link to the correct variables.
processedData = 12, processedValues = 12 @valueNameText = 408 @valueNameText = vIn <$04> <$01> <$35> <$00> <$DA> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$01> <$D2> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$10> <$00> <$00> <$00> <$B2> <$00> <$06> <$02> <$15> <$F8> <$60> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$00> <$03> vIn = 30.9 tempMos1 = 21.8 tempMos2 = 0.0 tempMos3 = 0.0 tempMos4 = 0.0 tempMos5 = 0.0 tempMos6 = 0.0 tempPcb = 0.00 currentMotor = 0.00 currentIn = 0.000 rpm = 466 dutyNow = 0.0 ampHours = 0. ampHoursCharged = 0. wattHours = 0 wattHoursCharged = 0 tachometer = 1048576 tachometerAbs = 11665414 faultCode = 2 extraData = 21, 248, 96, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3
I'm not sure why "vIn" is set to 30.9. The voltage of the batteries I'm using adds up to about 45V.
The temperature of the first MOSFET seems reasonable. I'm guessing my board only has one temperature sensor which is why the other MOSFETs are reported as zero degrees.
I attempted to set the RPM to 60.
SetRpm TxOut, size = 10 <$02> <$05> <$08> <$00> <$00> <$00> <$3C> <$F5> <$F2> <$03>
Either I'm doing something wrong with this command or 60 is too low for the RPM to produce motion. I didn't want the motors to go crazy on me since I don't have the secured very well right now. It's also possible I have to set some other parameter in order to set the RPM directly.
The Arduino program I used as a guide for the "Get Values" command, doesn't have a function to set the RPM so I couldn't use my logic analyzer to check to see if my code generated the correct packet.
I've ordered a STM32F0DISCOVERY board from Digikey. There's a very full featured example which uses this board. Hopefully I can monitor the UART with my logic analyzer and figure out how to set parameters and not just read them (not that I have the reading part all figured out).
I'm making some progress, but it's painfully slow progress right now.
Of course this is assuming I'm reading the correct byte in the payload.
I used the VESC-Tool to set up the board initially. I set the battery as a 12 cell LiPo. 30.9V would be too low of a voltage for a 12 cell battery but my battery is closer to 45V. I just measured the battery with my Fluke and it was close to 46V. I'm back to being confused.
At least I was right about something. I'm pretty sure I was using the wrong list to assign the various values to variables. I used this structure:
// VESC Types struct mc_values { float v_in; float temp_mos1; float temp_mos2; float temp_mos3; float temp_mos4; float temp_mos5; float temp_mos6; float temp_pcb; float current_motor; float current_in; float rpm; float duty_now; float amp_hours; float amp_hours_charged; float watt_hours; float watt_hours_charged; int32_t tachometer; int tachometer_abs; mc_fault_code fault_code; };
The code below assigns values to variables one at a time. I'm now using this code to determine which bytes correspond to which variable. Notice the location of the "v_in" variable. I was treating a temperature value as the battery voltage value. Just one reason why I was/am so confused.
switch (packetId) { case COMM_GET_VALUES: ind = 0; values.temp_mos1 = buffer_get_float16(message, 10.0, &ind); values.temp_mos2 = buffer_get_float16(message, 10.0, &ind); values.temp_mos3 = buffer_get_float16(message, 10.0, &ind); values.temp_mos4 = buffer_get_float16(message, 10.0, &ind); values.temp_mos5 = buffer_get_float16(message, 10.0, &ind); values.temp_mos6 = buffer_get_float16(message, 10.0, &ind); values.temp_pcb = buffer_get_float16(message, 10.0, &ind); values.current_motor = buffer_get_float32(message, 100.0, &ind); values.current_in = buffer_get_float32(message, 100.0, &ind); values.duty_now = buffer_get_float16(message, 1000.0, &ind); values.rpm = buffer_get_int32(message, &ind); values.v_in = buffer_get_float16(message, 10.0, &ind); values.amp_hours = buffer_get_float32(message, 10000.0, &ind); values.amp_hours_charged = buffer_get_float32(message, 10000.0, &ind); ind += 8; //Skip 9 bit values.tachometer = buffer_get_int32(message, &ind); values.tachometer_abs = buffer_get_int32(message, &ind); values.fault_code = (mc_fault_code)message[ind++]; return true; break;
When I originally read the above code, I thought the data was packaged as a combination of floats and integers. I'm now confident all the data is packaged as integers and the floats are only used to display the values.
Looking back at the data, I see the battery voltage was read as 46.6V. This is pretty close to what I read using my multimeter. A bit of a silver lining to this confusing dark cloud.
My next attempt will be to set the current value. I'm pretty sure the VESC is set to current control so hopefully this will allow me some control of the motor from the Propeller. As I mentioned previously, my attempt to control the RPM didn't work.
float buffer_get_float16(const uint8_t *buffer, float scale, int32_t *index) { return (float)buffer_get_int16(buffer, index) / scale; }
EDIT: mmm that means the same should apply to setting the data in the command buffer, and yes indeed
void buffer_append_float32(uint8_t* buffer, float number, float scale, int32_t *index) { buffer_append_int32(buffer, (int32_t)(number * scale), index); }
Sorry for not digging deep enough when looking through the code
I asked about converting 16-bit floats to 32-bit floats and you answered my question. I certainly wasn't expecting anyone to dig through the code. Thanks again for the information your provided.
I have made some more progress on this project. I can controller the two motors independently now.
One of my initial problems was thinking I could just send occasional UART commands. The VESC expects a regular (several times a second?) stream of data over the UART or it will timeout and shut off the motor. This is certainly a reasonable thing to do.
I now have the program send MOTOR_CURRENT commands at about 50 Hz. This seems to work well. I also wired up separate UART lines to each of the two ports on the VESC. There should be a way to send to just one port but I don't know how to do this.
The motor doesn't start turning until the current is set to about 500. I thought this was 500mA but with both motors set to 500, the combined draw is about 160mA from the 47V battery. I don't know what sort of units the "500" is but I'm doubtful it's milliamps.
Now that I can independently control two motors, I kind of agree with you.
I'd like to figure out how to control these motors using RPM rather than current. If needed, I could monitor the encoders and adjust the current to produce the desired RPM. While I think this route is possible, I feel like I'd be duplicating work already possible with the VESC itself.
Edit: I just realized I never tried sending RPM control signals repeatedly. It's possible this would work but I think I have to change some mode setting.