Control with CAN

1. VESC CAN Message Structure

English
Korean
We analyze the CAN protocol used in the VESC firmware. VESC CAN communication follows a CAN communication protocol called Extended ID (EID) or Extended Format. While Base Format consists of an ID value of 11 bits, Extended Format consists of an ID value of 29 bits. Therefore, VESC sends controller_id and command together as ID values, which are described in more detail below.
  • Comparison between Base Frame format vs. Extended Format
https://en.wikipedia.org/wiki/CAN_bus

Base frame format: with 11 identifier bits / Extended frame format: with 29 identifier bits

1. Periodically Sending Messages

In VESC CAN Protocol, there is can status message which is periodically sent(aka Telemetry). You can check the rate and message mode in VESC-Tool. Currently, there are two VESCs connected on same CAN bus (local controller_id is 101 and the other one is 27) and baudrate is 500Kbps. The periodically sending status msgs rate is 20Hz and the message mode is 1, 2, 3, 4, 5 which is sending 5 different type of messages.
VESC CAN setting shown at VESC-Tool
If you check CAN message using CAN analyzing tool(in my case CANable board and UCCBViewer). I bought CANable board at here. UCCBViewer can be downloaded at https://github.com/UsbCANConverter-UCCbasic/uCCBViewer/releases and you can run the program using below command.
sudo java -jar -Dsun.java2d.uiScale=2.5 uCCBViewer-2.5.jar
UCCBViewer 2.5 using CANable board
Experimental setup
VESC 101 is connected to PC via USB and VESC 101, 27 and CANable board are connected via CAN BUS. (Caution! How to use terminal resistor)
CAN Terminal Resistor usage
I used CANable board to observe CAN messages. The periodic message are as below.
CAN status msg from controller_id 101 (0x65)
Let's decode Id. CAN messages from VESC use an extended ID (EID), containing the command and controller_id. The lower 1 byte(8 bits) represents the VESC controller_id and the remaining higher byte is the index value of CAN_PACKET_ID.
  • 00000965h means,
    • 0x0000009 = 9(CAN_PACKET_STATUS)
    • 0x65 = 101(controller_id)
  • 00000e65h means,
    • 0x000000e = 14(CAN_PACKET_STATUS_2)
    • 0x65 = 101(controller_id)
  • 00000f65h means,
    • 0x000000f = 15(CAN_PACKET_STATUS_3)
    • 0x65 = 101(controller_id)
  • 00001065h means,
    • 0x0000010 = 16(CAN_PACKET_STATUS_4)
    • 0x65 = 101(controller_id)
  • 00001b65h means,
    • 0x000001b = 27(CAN_PACKET_STATUS_5)
    • 0x65 = 101(controller_id)
So, this message are CAN status msgs from controller_id 101. And the Data of CAN status msgs are as below
  • CAN_PACKET_STATUS :
    • rpm(4 byte), current*10.0(2 byte), duty*1000.0(2 byte)
  • CAN_PACKET_STATUS_2
    • amp_hours*10000.0(4 byte), amp_hours_charged*10000.0(4 byte)
  • CAN_PACKET_STATUS_3
    • watt_hours*10000.0(4 byte), watt_hours_charged*10000.0(4 byte)
  • CAN_PACKET_STATUS_4
    • temp_fet*10.0(2 byte), temp_motor*10.0(2 byte), current_in*10.0(2 byte), pid_pos_now*50.0(2 byte)
  • CAN_PACKET_STATUS_5
    • tacho_value(4 byte), v_in*10.0(2 byte), reserved as 0(2 byte)
Let's decode data;
  • CAN_PACKET_STATUS : 00 00 0c f2 00 02 00 64
    • rpm = 3314(0x00000cf4)
    • current = 0.2(0x0002)
    • duty = 0.1(0x0064)
  • CAN_PACKET_STATUS_2 : 00 00 00 00 00 00 00 00
    • amp_hours = 0
    • amp_hours_charged = 0
  • CAN_PACKET_STATUS_3 : 00 00 00 00 00 00 00 00
    • watt_hours = 0
    • watt_hours_charged = 0
  • CAN_PACKET_STATUS_4 : 01 75 01 c1 00 00 2c c6
    • temp_fet = 37.3(0x0175)
    • temp_motor = 44.9(0x01c1)
    • current_in = 0(0x0000)
    • pid_pos_now = 229.24(0x2cc6)
  • CAN_PACKET_STATUS_5 : 00 04 aa d5 00 c2 00 00
    • tacho_value = 305877(0x0004aad5)
    • v_in = 19.4(0x00c2)
    • reserved as 0(2 byte)
VESC-Tool Capture
There is a slight time difference between the data in CAN Status Msg and the data in VESC-Tool, so it is assumed that the values are not exactly the same but roughly correct.

2. Command Messages

Now, let me know the CAN messages for controlling motor.
@comm_can.c, we use below function to send can message basically.
void comm_can_transmit_eid_replace(uint32_t id, const uint8_t *data, uint8_t len, bool replace) {
if (len > 8) {
len = 8;
}
#if CAN_ENABLE
#ifdef HW_HAS_DUAL_MOTORS
if (app_get_configuration()->can_mode == CAN_MODE_VESC) {
if (replace && ((id & 0xFF) == utils_second_motor_id() ||
(id & 0xFF) == app_get_configuration()->controller_id)) {
uint8_t data_tmp[10];
memcpy(data_tmp, data, len);
decode_msg(id, data_tmp, len, true);
return;
}
}
#else
(void)replace;
#endif
CANTxFrame txmsg;
txmsg.IDE = CAN_IDE_EXT;
txmsg.EID = id;
txmsg.RTR = CAN_RTR_DATA;
txmsg.DLC = len;
memcpy(txmsg.data8, data, len);
chMtxLock(&can_mtx);
canTransmit(&HW_CAN_DEV, CAN_ANY_MAILBOX, &txmsg, MS2ST(5));
chMtxUnlock(&can_mtx);
#else
(void)id;
(void)data;
(void)len;
(void)replace;
#endif
}
And, above function called as below. For example, to control the current, we send the VESC CAN ID (controller_id) and index number of CAN_PACKET_ID (CAN_PACKET_SET_CURRENT) are muxed and sent to the id value in comm_can_transmit_eid_replace() function. And we send 4 byte current data (current value * 1000) in the DATA frame.
void comm_can_set_current(uint8_t controller_id, float current) {
int32_t send_index = 0;
uint8_t buffer[4];
buffer_append_int32(buffer, (int32_t)(current * 1000.0), &send_index);
comm_can_transmit_eid_replace(controller_id |
((uint32_t)CAN_PACKET_SET_CURRENT << 8), buffer, send_index, true);
}
The typical CAN message commands available in VESC are tabulated. If the controller_id is 101(0x65), we can use below commands.
Message Name
Extended ID
DLC
(Data Length Code)
DATA
Set Duty Cycle (index=0)
0x00000065
4
duty * 100000.0
Set Current (index=1)
0x00000165
4
current * 1000.0
Set Current Brake (index=2)
0x00000265
4
current * 1000.0
Set RPM (index=3)
0x00000365
4
rpm
Set Position (index=4)
0x00000465
4
pos * 1000000.0
If we send 10% duty cycle command to the controller_id 101, the can message is as below. At DATA, 0.1*100000.0 = 10000 = 0x2710. When using UCCBViewer, you should turn on Ext and Repeat option.
10% Duty Cycle Command
If we send 5A current brake command to the controller_id 101, the can message is as below. At DATA, 5*1000.0 = 5000 = 0x1388
5A Current Brake Command
If we send 1000 ERPM command to the controller_id 101, the can message is as below. At DATA, 1000 = 0x03E8
1000 ERPM (ERPM means Electrical RPM which is ERPM = RPM / number of pole)
ERPM values observed in VESC-Tool

3. CAN Forward

In VESC-Tool, we can access all the functions of CAN connected device using CAN Forward function. Now, my VESC-Tool is connected to VESC (controller_id 101) by USB (ttyACM0). When we select Motor 27, it turns on CAN Forward function automatically, then we can find CAN Forward Messages as below.
VESC-Tool when using CAN Forward
Sending command current 3.0A using CAN Forward
CAN Status function is disabled on both controller. Above figure shows the trace of CAN messages when I command 3.0A current using CAN Forward.
Let's decode this;
  • 0000081bh means,
    • 0x000008 = 8(CAN_PACKET_PROCESS_SHORT_BUFFER)
    • 0x1b = 27(controller_id)
  • 0x65 00 06 00 00 0b b8 means,
    • 0x65 = 101(controller_id)
    • 0x00 ('send' value at comm_can_send_buffer() function)
    • 0x06 = 6(COMM_SET_CURRENT)
    • 0x00000bb8 = 3000(3000/1000.0 = 3A)
The 3.0A current command is given to VESC 101 from VESC-Tool via USB.The commands sent from VESC-Tool are probably as follows:
  • 0x221b0600000bb8 which means
    • 0x22 = 34(COMM_FORWARD_CAN)
    • 0x1b = 27(CAN Forward Target VESC controller_id)
    • 0x06 = 6(COMM_SET_CURRENT)
    • 0x00000bb8 = 3000(current *1000.0 so, it means 3000/1000.0 = 3A)
This messages is processed on 'command.c' using below code. In our case, HW_HAS_DUAL_MOTORS is not defined.
case COMM_FORWARD_CAN: {
send_func_can_fwd = reply_func;
#ifdef HW_HAS_DUAL_MOTORS
if (data[0] == utils_second_motor_id()) {
mc_interface_select_motor_thread(2);
commands_process_packet(data + 1, len - 1, reply_func);
mc_interface_select_motor_thread(1);
} else {
comm_can_send_buffer(data[0], data + 1, len - 1, 0);
}
#else
comm_can_send_buffer(data[0], data + 1, len - 1, 0);
#endif
} break;
That is, the data[0] is CAN Forward Target VESC controller_id(0x1b) and the rest of the data(0x0600000bb8) is passed by CAN bus. The actual code of comm_can_send_buffer() function is as below and the len of original data (0x221b0600000bb8) is 7 (7byte), so the len -1 = 6.
So, at the below code, actual values of input variables are as follows:
  • uint8_t controller_id = 0x1b
  • uint8_t *data = 0x0600000bb8
  • unsigned int len = 6
  • uint8_t send = 0
Because len <=6, send_buffer[] will be packed up as follows;
  • send_buffer[0] = 101(0x65) -> local VESC controller_id
  • send_buffer[1] = 0
  • send_buffer[2] = 0x06
  • send_buffer[3] = 0x00
  • send_buffer[4] = 0x00
  • send_buffer[5] = 0x0b
  • send_buffer[6] = 0xb8
void comm_can_send_buffer(uint8_t controller_id, uint8_t *data, unsigned int len, uint8_t send) {
uint8_t send_buffer[8];
if (len <= 6) {
uint32_t ind = 0;
send_buffer[ind++] = app_get_configuration()->controller_id;
send_buffer[ind++] = send;
memcpy(send_buffer + ind, data, len);
ind += len;
comm_can_transmit_eid_replace(controller_id |
((uint32_t)CAN_PACKET_PROCESS_SHORT_BUFFER << 8), send_buffer, ind, true);
} else {
unsigned int end_a = 0;
for (unsigned int i = 0;i < len;i += 7) {
if (i > 255) {
break;
}
end_a = i + 7;
uint8_t send_len = 7;
send_buffer[0] = i;
if ((i + 7) <= len) {
memcpy(send_buffer + 1, data + i, send_len);
} else {
send_len = len - i;
memcpy(send_buffer + 1, data + i, send_len);
}
comm_can_transmit_eid_replace(controller_id |
((uint32_t)CAN_PACKET_FILL_RX_BUFFER << 8), send_buffer, send_len + 1, true);
}
for (unsigned int i = end_a;i < len;i += 6) {
uint8_t send_len = 6;
send_buffer[0] = i >> 8;
send_buffer[1] = i & 0xFF;
if ((i + 6) <= len) {
memcpy(send_buffer + 2, data + i, send_len);
} else {
send_len = len - i;
memcpy(send_buffer + 2, data + i, send_len);
}
comm_can_transmit_eid_replace(controller_id |
((uint32_t)CAN_PACKET_FILL_RX_BUFFER_LONG << 8), send_buffer, send_len + 2, true);
}
uint32_t ind = 0;
send_buffer[ind++] = app_get_configuration()->controller_id;
send_buffer[ind++] = send;
send_buffer[ind++] = len >> 8;
send_buffer[ind++] = len & 0xFF;
unsigned short crc = crc16(data, len);
send_buffer[ind++] = (uint8_t)(crc >> 8);
send_buffer[ind++] = (uint8_t)(crc & 0xFF);
comm_can_transmit_eid_replace(controller_id |
((uint32_t)CAN_PACKET_PROCESS_RX_BUFFER << 8), send_buffer, ind++, true);
}
}
When the CAN message is actually sending, the extended ID (eid) contains the command(CAN_PACKET_PROCESS_SHORT_BUFFER=8).
As a result, we can see that the CAN message equals the value observed by the UCCBViewer.
On the same principle, we can decode all the messages
Id
Data
Meaning
0x0000081b
65 00 1e
CAN_Forward to 0x1b(27),
0x1e = 30(COMM_ALIVE)
0x0000081b
65 00 06 00 00 00 00
CAN_Forward to 0x1b(27), 0x06 = 6(COMM_SET_CURRENT)
0x00000000 = 0(current *1000.0)
! Note that COMM_ALIVE is used to maintain the current state of control.
For short instructions with a length of less than 6, the CAN_FORWARD command is processed as shown above.
This time, let's talk about a longer command.
I sended CAN messages (Id = 0x0000081, DLC = 3, DATA = 0x 650004). This message means follows:
  • 0000081bh means,
    • 0x000008 = 8(CAN_PACKET_PROCESS_SHORT_BUFFER)
    • 0x1b = 27(controller_id)
  • 0x650004 means,
    • 0x65 = 101(controller_id)
    • 0x00 ('send' value at comm_can_send_buffer() function)
    • 0x04 = 4(COMM_GET_VALUES)
The above message is handled by VESC 27 and the code below is called because the command is 'CAN_PACKET_PROCESS_SHOT_BUFFER' from 'comm_can.c'.
case CAN_PACKET_PROCESS_SHORT_BUFFER:
ind = 0;
rx_buffer_last_id = data8[ind++];
commands_send = data8[ind++];
if (is_replaced) {
if (data8[ind] == COMM_JUMP_TO_BOOTLOADER ||
data8[ind] == COMM_ERASE_NEW_APP ||
data8[ind] == COMM_WRITE_NEW_APP_DATA ||
data8[ind] == COMM_WRITE_NEW_APP_DATA_LZO ||
data8[ind] == COMM_ERASE_BOOTLOADER) {
break;
}
}
switch (commands_send) {
case 0:
commands_process_packet(data8 + ind, len - ind, send_packet_wrapper);
break;
case 1:
commands_send_packet_can_last(data8 + ind, len - ind);
break;
case 2:
commands_process_packet(data8 + ind, len - ind, 0);
break;
default:
break;
}
break;
static void send_packet_wrapper(unsigned char *data, unsigned int len) {
comm_can_send_buffer(rx_buffer_last_id, data, len, 1);
}
In here,
  • rx_buffer_last_id = 0x65
  • commands_send = 0
  • commands_process_packet ( 0x04, 1, function pointer )
Third input of function 'commands_process_packet()' is function pointer of 'send_packet_wrapper()' and eventually 'comm_can_send_buffer()' is called to reply to messages. As a result, we can observe below replying messages about 'COMM_GET_VALUES'.
  • 00000565h
    • 0x000005 = 5(CAN_PACKET_FILL_RX_BUFFER) -> receive data stream
    • 0x65 = 101
  • 00000765h
    • 0x000007 = 7(CAN_PACKET_PROCESS_RX_BUFFER) -> process received data
    • 0x65 = 101
In the data of Id (00000565h), every first byte indicate data length Index, that is
  • 0x00 = 0
  • 0x07 = 7
  • 0x0e = 14
  • 0x15 = 21
  • 0x1c = 28
  • 0x23 = 35
  • 0x2a = 42
  • 0x31 = 49
  • 0x38 = 56
  • 0x3f = 63
  • 0x46 = 70
Except for the first byte, the remaining data corresponds to continuous data in the reply message for COMM_GET_VALUE. Separating the data into bytes per variable shown in 'commands.c'. It can be interpreted as follows ( | indicate bytes separation):
Id
Data
Meaning
00000565h
00 | 04 | 01 42 | 01 6d | 00 00
07 | 00 00 | 00 00 00 00 | 00 0e | 00 00 00 | 00 00 00 00 | 15 | 00 00 | 00 04 94 00 | 00 1c | c2 | 00 00 00 00 | 00 00 23 | 00 00 | 00 00 00 00 | 00
2a | 00 00 00 | 00 06 9c 78 |
31 | 00 06 9c ca | 00 | 00 00
38 | 00 00 | 1b | 00 00 | 00 00 |
3f | 00 00 | 00 00 00 05 | 00
46 | 00 00 00
00 = 0(length index)
04 = 4(COMM_GET_VALUES)
01 42 = temp_fet*10 ->temp_fet = 32.2 01 6d = temp_motor*10 ->temp_motor = 36.5
00 00 00 00 = motor_current*100
00 00 00 00 = input_current*100 00 00 00 00 = id*100 00 00 00 00 = iq*100
00 00 = duty*1000
00 04 94 00 = rpm ->rpm=300032 00 c2 = volt*10 ->volt = 19.4
00 00 00 00 = amp_hours*10000
00 00 00 00 = amp_hours_charged*10000
00 00 00 00 = watt_hours_charged*10000
00 00 00 00 = watt_hours_charged*10000
00 06 9c 78 = tacho ->433272
00 06 9c ca = tacho_abs ->433354
00 = fault_code
00 00 00 00 = pid_pos_now*1000000
1b = 27 (controller_id)
00 00 = temp_mos1*10
00 00 = temp_mos2*10
00 00 = temp_mos3*10
00 00 00 05 = vd*1000 ->vd=0.005
00 00 00 00 = vq*1000
00000765h
1b 01 00 49 13 99
1b = 27 (controller_id)
01 = send
00 49 = 73 = length of data
13 99 = CRC
Above, we introduce how to obtain data by requesting 'COMM_GET_VALUE' in a 'CAN_FORWARD' manner. The method of using "CAN_FORWARD" can be used to transmit virtually all the "commands" of the VESC, but this is somewhat complicated because it requires proper interpretation of the data for the reply message as seen earlier. However, I think it will be able to use it well if necessary.
VESC 펌웨어에서 사용하는 CAN 프로토콜에 대해 분석한다. VESC의 CAN통신에서는 Extended ID (EID) 혹은 Extended Format이라는 방식의 CAN 통신 프로토콜을 따른다. Base Format은 11bit의 ID 값으로 이루어지는 반면, Extended Format은 29bit의 ID값으로 이루어지는 것이 크게 다른 점이다. 그래서 VESC에서는 CAN Message의 ID값에 controller_id와 command를 mux하여 전송하게 되는데 이는 아래에서 더 자세하게 설명한다.
  • Comparison between Base Frame format vs. Extended Format
https://en.wikipedia.org/wiki/CAN_bus

Base frame format: with 11 identifier bits / Extended frame format: with 29 identifier bits

1. Periodically Sending Messages

VESC CAN 프로토콜에서는 Status Message라고 하는 주기적으로 보내지는 데이터가 있고 이는 Telemetry라고도 불린다. VESC-Tool에서 아래 그림과 같이 Status Message의 주기와 Mode를 확인 할 수 있으며 현재 설정은 CAN_STATUS 1 2 3 4 5 모로 20Hz가 설정되어 있다. CAN STATUS 1 2 3 4 5는가지의 CAN Message를 보내는 것으로 아래에서 좀 더 자세하게 설명한다.
VESC CAN setting shown at VESC-Tool
나의 경우 CAN Message를 확인하기 위하여 CANable board 와 UCCBViewer를 사용하였다. CANable 보드는 에서 구입하였다. UCCBViewer 는 아래 주소에서 다운로드 가능하며 jar파일을 다운로드하여 다음과 같은 명령어로 실행가능하다. (전제조건 JRE 1.6.0 이상 버전을를 깔아야한다.)
sudo java -jar -Dsun.java2d.uiScale=2.5 uCCBViewer-2.5.jar
UCCBViewer 2.5 using CANable board
Experimental setup
현재 2개의 VESC를 CAN통신으로 연결시켰다. VESC의 Controller_id는 101과 27로 지정되어있으며이중 101번 VESC에 USB를 통해 PC의 VESC-Tool과 연결되어 있다. CANable 보드의 CANH, CANL신호는 daisy chain 방식으로 두개의 VESC와 같은 Bus상에 연결하였고 종단저항(Terminal Resistor)를 활성화하여 원활한 통신이 가능하도록 하였다. (Caution! How to use terminal resistor)
CAN Terminal Resistor usage
CANable 보드를 이용하여 CAN Message를 확인한 결과 주기적인 Status 신호가 다음과 같이 읽혔다.
CAN status msg from controller_id 101 (0x65)
위 신호를 해석해보다. CAN Message에서 Id값은 Extended ID(EID) 포멧으로 전송되며 이는 command와 controller_id값을 포함하고 있다. 하위 1byte (8bit)가 VESC controller_id값을 저장하고 있으며 나머지 bit는 CAN_PACKET_ID의 값을 저장하고 있다. CAN_PACKET_ID은 VESC 펌웨어의 'datatype.h'에 지정되어 있다.
  • 00000965h means,
    • 0x0000009 = 9(CAN_PACKET_STATUS)
    • 0x65 = 101(controller_id)
  • 00000e65h means,
    • 0x000000e = 14(CAN_PACKET_STATUS_2)
    • 0x65 = 101(controller_id)
  • 00000f65h means,
    • 0x000000f = 15(CAN_PACKET_STATUS_3)
    • 0x65 = 101(controller_id)
  • 00001065h means,
    • 0x0000010 = 16(CAN_PACKET_STATUS_4)
    • 0x65 = 101(controller_id)
  • 00001b65h means,
    • 0x000001b = 27(CAN_PACKET_STATUS_5)
    • 0x65 = 101(controller_id)
위의 해석된 내용은 CAN Status Msgs가 101번 VESC로부터 오고 있으며 8 Byte 길이의 5가지 Status 정보를 보내오고 있다. 각 Status 정보는 아래와 같이 정의되며 이는 VESC 펌웨어의 'comm_can.c' 파일에 나와있다.
  • CAN_PACKET_STATUS :
    • rpm(4 byte), current*10.0(2 byte), duty*1000.0(2 byte)
  • CAN_PACKET_STATUS_2
    • amp_hours*10000.0(4 byte), amp_hours_charged*10000.0(4 byte)
  • CAN_PACKET_STATUS_3
    • watt_hours*10000.0(4 byte), watt_hours_charged*10000.0(4 byte)
  • CAN_PACKET_STATUS_4
    • temp_fet*10.0(2 byte), temp_motor*10.0(2 byte), current_in*10.0(2 byte), pid_pos_now*50.0(2 byte)
  • CAN_PACKET_STATUS_5
    • tacho_value(4 byte), v_in*10.0(2 byte), reserved as 0(2 byte)
이를 바탕으로 위의 Status Msgs의 실제값을 읽어보면 다음과 같다.
  • CAN_PACKET_STATUS : 00 00 0c f2 00 02 00 64
    • rpm = 3314(0x00000cf4)
    • current = 0.2(0x0002)
    • duty = 0.1(0x0064)
  • CAN_PACKET_STATUS_2 : 00 00 00 00 00 00 00 00
    • amp_hours = 0
    • amp_hours_charged = 0
  • CAN_PACKET_STATUS_3 : 00 00 00 00 00 00 00 00
    • watt_hours = 0
    • watt_hours_charged = 0
  • CAN_PACKET_STATUS_4 : 01 75 01 c1 00 00 2c c6
    • temp_fet = 37.3(0x0175)
    • temp_motor = 44.9(0x01c1)
    • current_in = 0(0x0000)
    • pid_pos_now = 229.24(0x2cc6)
  • CAN_PACKET_STATUS_5 : 00 04 aa d5 00 c2 00 00
    • tacho_value = 305877(0x0004aad5)
    • v_in = 19.4(0x00c2)
    • reserved as 0(2 byte)
VESC-Tool Capture
20Hz의 주기로 값이 전송되고 있어, CAN Status Msg의 값과 VESC-Tool에서 실시간으로 확인한 값이 약간의 차이가 있긴 하지만, CAN Status Message를 정상적으로 해석한 것으로 판단할 수 있다.

2. Command Messages

이번에는, 제어메세지를 보내기 위한 부분에 대해 알아본다. 기본적으로, CAN Messages는 'comm_can.c' 파일에 있는 아래 함수를 통해 전송된다.
void comm_can_transmit_eid_replace(uint32_t id, const uint8_t *data, uint8_t len, bool replace) {
if (len > 8) {
len = 8;
}
#if CAN_ENABLE
#ifdef HW_HAS_DUAL_MOTORS
if (app_get_configuration()->can_mode == CAN_MODE_VESC) {
if (replace && ((id & 0xFF) == utils_second_motor_id() ||
(id & 0xFF) == app_get_configuration()->controller_id)) {
uint8_t data_tmp[10];
memcpy(data_tmp, data, len);
decode_msg(id, data_tmp, len, true);
return;
}
}
#else
(void)replace;
#endif
CANTxFrame txmsg;
txmsg.IDE = CAN_IDE_EXT;
txmsg.EID = id;
txmsg.RTR = CAN_RTR_DATA;
txmsg.DLC = len;
memcpy(txmsg.data8, data, len);
chMtxLock(&can_mtx);
canTransmit(&HW_CAN_DEV, CAN_ANY_MAILBOX, &txmsg, MS2ST(5));
chMtxUnlock(&can_mtx);
#else
(void)id;
(void)data;
(void)len;
(void)replace;
#endif
}
그리고 위 함수는 아래와 같이 다른 함수에서 호출이 된다. 아래 함수는 CAN으로 전류제어를 할 때 이용하는 함수로 EID값으로 VESC CAN ID (controller_id) 와 CAN_PACKET_ID (CAN_PACKET_SET_CURRENT) 값을 mux해서 사용하는 것을 알 수 있다. 그리고 4 bytes 크기의 전류값을 데이터 프레임에로 실어보내는데 전류값에 1000을 곱한 값을 보낸다.
void comm_can_set_current(uint8_t controller_id, float current) {
int32_t send_index = 0;
uint8_t buffer[4];
buffer_append_int32(buffer, (int32_t)(current * 1000.0), &send_index);
comm_can_transmit_eid_replace(controller_id |
((uint32_t)CAN_PACKET_SET_CURRENT << 8), buffer, send_index, true);
}
이와 같이 CAN Message를 이용해서 VESC에 제어명령을 내릴 수 있는데 이를 아래 테이블에 정리하였다. 아래 테이블에서는 controller_id가 101(0x65)일 때의 값을 예로 정리하였다.
Message Name
Extended ID
DLC
(Data Length Code)
DATA
Set Duty Cycle (index=0)
0x00000065
4
duty * 100000.0
Set Current (index=1)
0x00000165
4
current * 1000.0
Set Current Brake (index=2)
0x00000265
4
current * 1000.0
Set RPM (index=3)
0x00000365
4
rpm
Set Position (index=4)
0x00000465
4
pos * 1000000.0
만약 10% duty cycle을 101번 VESC에 가할 때는 아래와 같이 전송한다. Id에 CAN_PACKET_ID 는 1을 duty 값 0.1에 100000 를 곱한 값인 10000 (0x2710)를 전송하는데 UCCBViewer를 이용할 경우에는 아래 그림과 같이 Ext, Repeat 옵션을 켜줘야 한다.
VESC의 모든 제어 명령어는 기본적으로 한번 전송된 명령어는 1초동안 유지가 되고 이후에는 제어가 자동으로 풀리는 방식으로 작동한다. 이를 timeout 기능이라고 하고 그러므로 연속적인 제어를 위해서는 1Hz 이상의 제어신호를 주기적으로 보내줘야한다.
10% Duty Cycle Command
만약 5A 전류 브레이크 명령어를 101번 VESC에 전송할 경우는 아래와 같이 한다. 이 경우 CAN_PACKET_ID 는 2, 데이터에 전류값은 5*1000=5000(0x1388) 값을 보내면 된다.
5A Current Brake Command
만약 1000(0x03E8) ERPM을 명령할 경우에는 아래와 같이 한다.
1000 ERPM (ERPM means Electrical RPM which is ERPM = RPM / number of pole)
ERPM values observed in VESC-Tool

3. CAN Forward

VESC-Tool에서 주로 사용되는 CAN Forward라는 기능은 말그대로 다른 통신포트(USB, UART등)에서 들어오는 데이터를 CAN통신 포워딩 해주는 기능이다. 현재, 우리의 실험 세팅은 VESC 101번이 USB를 통해 VESC-Tool에 연결되어 있으며 VESC 27번은 CAN으로 101번 VESC와 연결되어 있다. VESC-Tool을 처음 작동시키면 VESC-Tool에 나오는 모든 데이터는 101번 VESC의 값에 해당하는데 여기서 Motor 27을 클릭하면 자동으로 CAN Forward가 작동하면서 모든 데이터가 27번 VESC의 값으로 바뀌게 된다. 즉, VESC-Tool은 101번에 USB로 연결되어 있지만 CAN Forward를 통해 VESC 27번을 조종할 수 있게 된다.
VESC-Tool when using CAN Forward
CAN Status 기능을 VESC 101과 27번 모두 Disable 해 놓고 3A의 전류제어 명령어를 CAN_Forward 상태에서 내려보았다.
Sending command current 3.0A using CAN Forward
위 신호를 해석하면 아래와 같다.
  • 0000081bh means,
    • 0x000008 = 8(CAN_PACKET_PROCESS_SHORT_BUFFER)
    • 0x1b = 27(controller_id)
  • 0x65 00 06 00 00 0b b8 means,
    • 0x65 = 101(controller_id)
    • 0x00 ('send' value at comm_can_send_buffer() function)
    • 0x06 = 6(COMM_SET_CURRENT)
    • 0x00000bb8 = 3000(3000/1000.0 = 3A)
3.0A 전류명령은 101번 VESC에 USB를 통해 전달되었을 것이며 이는 아마 아래와 같은 내용이었을 것이다.
  • 0x221b0600000bb8 which means
    • 0x22 = 34(COMM_FORWARD_CAN)
    • 0x1b = 27(CAN Forward Target VESC controller_id)
    • 0x06 = 6(COMM_SET_CURRENT)
    • 0x00000bb8 = 3000(current *1000.0 so, it means 3000/1000.0 = 3A)
이 메세지는 'command.c' 에 있는 아래 코드에서 처리된다.
case COMM_FORWARD_CAN: {
send_func_can_fwd = reply_func;
#ifdef HW_HAS_DUAL_MOTORS
if (data[0] == utils_second_motor_id()) {
mc_interface_select_motor_thread(2);
commands_process_packet(data + 1, len - 1, reply_func);
mc_interface_select_motor_thread(1);
} else {
comm_can_send_buffer(data[0], data + 1, len - 1, 0);
}
#else
comm_can_send_buffer(data[0], data + 1, len - 1, 0);
#endif
} break;
즉, CAN Forward Target VESC의 controller_id가 27(0x1b)이며 나머지 데이터에 해당하는 값인 0x0600000bb8 는 CAN Bus를 통해 다시 전송된다. 위 코드에서 comm_can_send_buffer() 함수에 Input으로 들어가는 실제 값은 아래와 같을 것이다.
  • uint8_t controller_id = 0x1b
  • uint8_t *data = 0x0600000bb8
  • unsigned int len = 6
  • uint8_t send = 0
len <=6 이므로, send_buffer[] 에 들어가는 내용은 다음과 같다.
  • send_buffer[0] = 101(0x65) -> local VESC controller_id
  • send_buffer[1] = 0
  • send_buffer[2] = 0x06
  • send_buffer[3] = 0x00
  • send_buffer[4] = 0x00
  • send_buffer[5] = 0x0b
  • send_buffer[6] = 0xb8
void comm_can_send_buffer(uint8_t controller_id, uint8_t *data, unsigned int len, uint8_t send) {
uint8_t send_buffer[8];
if (len <= 6) {
uint32_t ind = 0;
send_buffer[ind++] = app_get_configuration()->controller_id;
send_buffer[ind++] = send;
memcpy(send_buffer + ind, data, len);
ind += len;
comm_can_transmit_eid_replace(controller_id |
((uint32_t)CAN_PACKET_PROCESS_SHORT_BUFFER << 8), send_buffer, ind, true);
} else {
unsigned int end_a = 0;
for (unsigned int i = 0;i < len;i += 7) {
if (i > 255) {
break;
}
end_a = i + 7;
uint8_t send_len = 7;
send_buffer[0] = i;
if ((i + 7) <= len) {
memcpy(send_buffer + 1, data + i, send_len);
} else {
send_len = len - i;
memcpy(send_buffer + 1, data + i, send_len);
}
comm_can_transmit_eid_replace(controller_id |
((uint32_t)CAN_PACKET_FILL_RX_BUFFER << 8), send_buffer, send_len + 1, true);
}
for (unsigned int i = end_a;i < len;i += 6) {
uint8_t send_len = 6;
send_buffer[0] = i >> 8;
send_buffer[1] = i & 0xFF;
if ((i + 6) <= len) {
memcpy(send_buffer + 2, data + i, send_len);
} else {
send_len = len - i;
memcpy(send_buffer + 2, data + i, send_len);
}
comm_can_transmit_eid_replace(controller_id |
((uint32_t)CAN_PACKET_FILL_RX_BUFFER_LONG << 8), send_buffer, send_len + 2, true);
}
uint32_t ind = 0;
send_buffer[ind++] = app_get_configuration()->controller_id;
send_buffer[ind++] = send;
send_buffer[ind++] = len >> 8;
send_buffer[ind++] = len & 0xFF;
unsigned short crc = crc16(data, len);
send_buffer[ind++] = (uint8_t)(crc >> 8);
send_buffer[ind++] = (uint8_t)(crc & 0xFF);
comm_can_transmit_eid_replace(controller_id |
((uint32_t)CAN_PACKET_PROCESS_RX_BUFFER << 8), send_buffer, ind++, true);
}
}
CAN message 가 실제 전송될 때, extended ID (eid) 는 command로 CAN_PACKET_PROCESS_SHORT_BUFFER (=8) 값을 가지게 되며 결과적으로 아래와 같이 실제 CAN 메세지를 확인할 수 있다.
같은 원리로 UCCBViewer에서 관측된 메세지를 모두 해석하면 아래와 같다.
Id
Data
Meaning
0x0000081b
65 00 1e
CAN_Forward to 0x1b(27),
0x1e = 30(COMM_ALIVE)
0x0000081b
65 00 06 00 00 00 00
CAN_Forward to 0x1b(27), 0x06 = 6(COMM_SET_CURRENT)
0x00000000 = 0(current *1000.0)
! Note : COMM_ALIVE 는 현재의 제어상태를 1초 더 유지시키는 명령어이다.
길이가 6 bytes이하의 데이터는 앞서 이야기 한 데로 수행되는데 이보다 큰 데이터의 경우에는 아래와 같은 방식으로 이루어진다.
예를 들어 CAN messages 로 다음과 같은 값을 보낼 경우 (Id = 0x0000081, DLC = 3, DATA = 0x 650004),
  • 0000081bh means,
    • 0x000008 = 8(CAN_PACKET_PROCESS_SHORT_BUFFER)
    • 0x1b = 27(controller_id)
  • 0x650004 means,
    • 0x65 = 101(controller_id)
    • 0x00 ('send' value at comm_can_send_buffer() function)
    • 0x04 = 4(COMM_GET_VALUES)
위 메세지는 VESC 27 번에 CAN_Forward를 통해 COMM_GET_VALUES 명령어를 전송할 때의 CAN Message를 UCCBViewer를 통해 보낸 경우이다. 이때 'CAN_PACKET_PROCESS_SHOT_BUFFER' 를 처리하면 코드는 아래와 같이 'comm_can.c'에 있다.
case CAN_PACKET_PROCESS_SHORT_BUFFER:
ind = 0;
rx_buffer_last_id = data8[ind++];
commands_send = data8[ind++];
if (is_replaced) {
if (data8[ind] == COMM_JUMP_TO_BOOTLOADER ||
data8[ind] == COMM_ERASE_NEW_APP ||
data8[ind] == COMM_WRITE_NEW_APP_DATA ||
data8[ind] == COMM_WRITE_NEW_APP_DATA_LZO ||
data8[ind] == COMM_ERASE_BOOTLOADER) {
break;
}
}
switch (commands_send) {
case 0:
commands_process_packet(data8 + ind, len - ind, send_packet_wrapper);
break;
case 1:
commands_send_packet_can_last(data8 + ind, len - ind);
break;
case 2:
commands_process_packet(data8 + ind, len - ind, 0);
break;
default:
break;
}
break;
static void send_packet_wrapper(unsigned char *data, unsigned int len) {
comm_can_send_buffer(rx_buffer_last_id, data, len, 1);
}
여기,
  • rx_buffer_last_id = 0x65
  • commands_send = 0
  • commands_process_packet ( 0x04, 1, function pointer )
'commands_process_packet()' 함수의 3번째 input은 함수 포인터로 'send_packet_wrapper()' 를 실행하게 되며 실질적으로는 'comm_can_send_buffer()' 함수를 호출하도록 되어 있다. 결과적으로 'COMM_GET_VALUES' 명령어의 답변에 대한 메세지가 CAN Bus상에 아래와 같이 올라오게 된다.
  • 00000565h
    • 0x000005 = 5(CAN_PACKET_FILL_RX_BUFFER) -> 수신 데이터 스트
    • 0x65 = 101 -> 앞에서 101번 VESC가 COMM_GET_VALUES를 요청하였으므 101번이 수신이
  • 00000765h
    • 0x000007 = 7(CAN_PACKET_PROCESS_RX_BUFFER) -> 수신된 데이터를 처리하라는 명령
    • 0x65 = 101
Id (00000565h)의 데이터에, 매 첫번째 Byte 값은 데이터의 길이를 나타내는 indicator의 역할을 한다.
  • 0x00 = 0
  • 0x07 = 7
  • 0x0e = 14
  • 0x15 = 21
  • 0x1c = 28
  • 0x23 = 35
  • 0x2a = 42
  • 0x31 = 49
  • 0x38 = 56
  • 0x3f = 63
  • 0x46 = 70
첫번째 Byte를 제외한 나머지 7개의 Byte가 실질적인 데이터 스트림에 해당하고 이는 연속적인 데이터를 나눠서 보낸 것이기 때문에 아래와 같이 전송하는 쪽과 받는 쪽에서 정한 데이터의 길이 규약에 맞게 Byte를 나눠서 해석을 해야한다. 아래 표에 'commands.c' 파일에 정의된 데로 Byte를 나눠서 해석을 하였다.
Id
Data
Meaning
00000565h
00 | 04 | 01 42 | 01 6d | 00 00
07 | 00 00 | 00 00 00 00 | 00 0e | 00 00 00 | 00 00 00 00 | 15 | 00 00 | 00 04 94 00 | 00 1c | c2 | 00 00 00 00 | 00 00 23 | 00 00 | 00 00 00 00 | 00
2a | 00 00 00 | 00 06 9c 78 |
31 | 00 06 9c ca | 00 | 00 00
38 | 00 00 | 1b | 00 00 | 00 00 |
3f | 00 00 | 00 00 00 05 | 00
46 | 00 00 00
00 = 0(length index)
04 = 4(COMM_GET_VALUES)
01 42 = temp_fet*10 ->temp_fet = 32.2 01 6d = temp_motor*10 ->temp_motor = 36.5
00 00 00 00 = motor_current*100
00 00 00 00 = input_current*100 00 00 00 00 = id*100 00 00 00 00 = iq*100
00 00 = duty*1000
00 04 94 00 = rpm ->rpm=300032 00 c2 = volt*10 ->volt = 19.4
00 00 00 00 = amp_hours*10000
00 00 00 00 = amp_hours_charged*10000
00 00 00 00 = watt_hours_charged*10000
00 00 00 00 = watt_hours_charged*10000
00 06 9c 78 = tacho ->433272
00 06 9c ca = tacho_abs ->433354
00 = fault_code
00 00 00 00 = pid_pos_now*1000000
1b = 27 (controller_id)
00 00 = temp_mos1*10
00 00 = temp_mos2*10
00 00 = temp_mos3*10
00 00 00 05 = vd*1000 ->vd=0.005
00 00 00 00 = vq*1000
00000765h
1b 01 00 49 13 99
1b = 27 (controller_id)
01 = send
00 49 = 73 = length of data
13 99 = CRC
위에서 'COMM_GET_VALUE' 명령를 'CAN_FORWARD' 기능을 응용하여 CAN Bus 상에서 직접 사용하는 방법에 대해 소개하였다. "CAN_FORWARD" 를 사용하는 방법은 가상적으로 "commands.c" 에서 사용하는 모든 다른 기능들에 대해서도 적용 가능하기 때문에 자유도가 높다. 하지만, 앞에서 설명한 것과 같이 수신되는 데이터 스트림을 처리하는 부분은 프로토콜에 맞게 데이터를 나눠서 해석해야하므로 이 부분을 프로그래밍해서 사용해야 하는 번거로운은 있다. 하지만, 필요할 경우 "CAN_FORWARD" 기능은 요긴하게 사용 가할 것으로 생각된다.

Reference 1 : electric-skateboard.builders (VESC CAN Message Structure)

https://esk8content.nyc3.digitaloceanspaces.com/uploads/db2454/original/3X/b/4/b4d3b287266b1668fef1b6d91a67a7849bd0dd56.png

Reference 2

VESC6_CAN_CommandsTelemetry.pdf
271KB
PDF

4. CAN Bus Load Test

Condition:
  • CAN Bus Baudrate : 1Mbps
  • Command Freq : 1kHz
  • Status Message Freq : 1kHz
  • Status Message Data : pos(rad), vel(rad/s), curr(A), motor_temp(degree)
Result :
  • Max CAN bus Load : 85%
  • Max Controller : 6EA
30sec Short Version
Full Version