通过CAN总线控制VESC驱动直流无刷电机

最近在驱动一个直流无刷电机,驱动这一块不是我的研究重点,只是拿来用。但系统上用到CAN总线,找来找去找到了VESC这种神级物品,自然是拿一块来玩玩。

拿到我手上的VESC是国内某工作室的改版VESC V6.4(应部分网友需求,放出链接)。硬件方案是STM32F405+DRV8301+NVMFS5C628,带有CAN口、PPM口、USB口。

一个完全不知道参数的星型直流无刷电机,就这么1分钟就能转动。不得不说,本杰明大神的VESC Tool真是个神器,傻瓜式的一键调参。但是,本人的需求并不是通过VESC Tool让电机转速来,而是通过CAN口来向VESC下发指令,间接地控制直流无刷电机按需转动。

一开始把VESC源代码拿出来看,各种各样的线程串来串去,没什么嵌入式操作系统经验的我真看蒙了。这个ChibiOS,本杰明大神怎么选择了这个系统?!
在网上没完没了地搜索,也没发现有把VESC的CAN通讯控制给说明白的。有个CSDN的博主到时写了篇标题疑似的,但是VIP可见,这。。。想爆粗口。

只能自己慢慢摸索吧。
VESC里面的CAN通讯程序里,其实写得比较明白。只是一开始没看懂。我们来看一下店家给我的VESC 3.4固件 里面接收CAN指令的代码:

static THD_FUNCTION(cancom_process_thread, arg) { 
(void)arg;
chRegSetThreadName("Cancom process");
process_tp = chThdGetSelfX();
int32_t ind = 0;
unsigned int rxbuf_len;
unsigned int rxbuf_ind;
uint8_t crc_low;
uint8_t crc_high;
bool commands_send;
for(;;) { 
chEvtWaitAny((eventmask_t) 1);
while (rx_frame_read != rx_frame_write) { 
CANRxFrame rxmsg = rx_frames[rx_frame_read++];
if (rxmsg.IDE == CAN_IDE_EXT) { 
uint8_t id = rxmsg.EID & 0xFF;
CAN_PACKET_ID cmd = rxmsg.EID >> 8;
can_status_msg *stat_tmp;
if (id == 255 || id == app_get_configuration()->controller_id) { 
switch (cmd) { 
case CAN_PACKET_SET_DUTY:
ind = 0;
mc_interface_set_duty(buffer_get_float32(rxmsg.data8, 1e5f, &ind));
timeout_reset();
break;
case CAN_PACKET_SET_CURRENT:
ind = 0;
mc_interface_set_current(buffer_get_float32(rxmsg.data8, 1e3f, &ind));
timeout_reset();
break;
case CAN_PACKET_SET_CURRENT_BRAKE:
ind = 0;
mc_interface_set_brake_current(buffer_get_float32(rxmsg.data8, 1e3f, &ind));
timeout_reset();
break;
case CAN_PACKET_SET_RPM:
ind = 0;
mc_interface_set_pid_speed(buffer_get_float32(rxmsg.data8, 1e0f, &ind));
timeout_reset();
break;
case CAN_PACKET_SET_POS:
ind = 0;
mc_interface_set_pid_pos(buffer_get_float32(rxmsg.data8, 1e6f, &ind));
timeout_reset();
break;
case CAN_PACKET_FILL_RX_BUFFER:
memcpy(rx_buffer + rxmsg.data8[0], rxmsg.data8 + 1, rxmsg.DLC - 1);
break;
case CAN_PACKET_FILL_RX_BUFFER_LONG:
rxbuf_ind = (unsigned int)rxmsg.data8[0] << 8;
rxbuf_ind |= rxmsg.data8[1];
if (rxbuf_ind < RX_BUFFER_SIZE) { 
memcpy(rx_buffer + rxbuf_ind, rxmsg.data8 + 2, rxmsg.DLC - 2);
}
break;
case CAN_PACKET_PROCESS_RX_BUFFER:
ind = 0;
rx_buffer_last_id = rxmsg.data8[ind++];
commands_send = rxmsg.data8[ind++];
rxbuf_len = (unsigned int)rxmsg.data8[ind++] << 8;
rxbuf_len |= (unsigned int)rxmsg.data8[ind++];
if (rxbuf_len > RX_BUFFER_SIZE) { 
break;
}
crc_high = rxmsg.data8[ind++];
crc_low = rxmsg.data8[ind++];
if (crc16(rx_buffer, rxbuf_len)
== ((unsigned short) crc_high << 8
| (unsigned short) crc_low)) { 
if (commands_send) { 
commands_send_packet(rx_buffer, rxbuf_len);
} else { 
commands_set_send_func(send_packet_wrapper);
commands_process_packet(rx_buffer, rxbuf_len);
}
}
break;
case CAN_PACKET_PROCESS_SHORT_BUFFER:
ind = 0;
rx_buffer_last_id = rxmsg.data8[ind++];
commands_send = rxmsg.data8[ind++];
if (commands_send) { 
commands_send_packet(rxmsg.data8 + ind, rxmsg.DLC - ind);
} else { 
commands_set_send_func(send_packet_wrapper);
commands_process_packet(rxmsg.data8 + ind, rxmsg.DLC - ind);
}
break;
case CAN_PACKET_SET_CURRENT_REL:
ind = 0;
mc_interface_set_current_rel(buffer_get_float32(rxmsg.data8, 1e5f, &ind));
timeout_reset();
break;
case CAN_PACKET_SET_CURRENT_BRAKE_REL:
ind = 0;
mc_interface_set_brake_current_rel(buffer_get_float32(rxmsg.data8, 1e5f, &ind));
timeout_reset();
break;
case CAN_PACKET_SET_CURRENT_HANDBRAKE:
ind = 0;
mc_interface_set_handbrake(buffer_get_float32(rxmsg.data8, 1e3f, &ind));
timeout_reset();
break;
case CAN_PACKET_SET_CURRENT_HANDBRAKE_REL:
ind = 0;
mc_interface_set_handbrake_rel(buffer_get_float32(rxmsg.data8, 1e5f, &ind));
timeout_reset();
break;
default:
break;
}
}
switch (cmd) { 
case CAN_PACKET_STATUS:
for (int i = 0;i < CAN_STATUS_MSGS_TO_STORE;i++) { 
stat_tmp = &stat_msgs[i];
if (stat_tmp->id == id || stat_tmp->id == -1) { 
ind = 0;
stat_tmp->id = id;
stat_tmp->rx_time = chVTGetSystemTime();
stat_tmp->rpm = (float)buffer_get_int32(rxmsg.data8, &ind);
stat_tmp->current = (float)buffer_get_int16(rxmsg.data8, &ind) / 10.0f;
stat_tmp->duty = (float)buffer_get_int16(rxmsg.data8, &ind) / 1000.0f;
break;
}
}
break;
default:
break;
}
} else { 
if (sid_callback) { 
sid_callback(rxmsg.SID, rxmsg.data8, rxmsg.DLC);
}
}
if (rx_frame_read == RX_FRAMES_SIZE) { 
rx_frame_read = 0;
}
}
}
}

我们可以看到,当VESC接收到扩展帧时,才会进行响应;接收到扩展帧后,将CAN ID作运算,取低8位识别为id;取其高21位(29-8)为指令。

uint8_t id = rxmsg.EID & 0xFF;//取低8位识别为id
CAN_PACKET_ID cmd = rxmsg.EID >> 8;//取其高21位(29-8)为指令
can_status_msg *stat_tmp;

当id为255(即0xFF)或者为控制器ID时(即app_get_configuration()->controller_id)),才对“指令”进行响应,也就是正式进入switch (cmd) {}语句里面。
VESC的指令表如下:

指令 指令值 数据 数据长度 数据类型 单位
CAN_PACKET_SET_DUTY 0 设置电机占空比 4字节 有符号整数 Thousandths of percent (5000 0 –> 50%)
CAN_PACKET_SET_CURREN T 1 设置电机电流 4字节 有符号整数 mA
CAN_PACKET_SET_CURREN T_BRAKE 2 设置制动电流 4字节 有符号整数 mA
CAN_PACKET_SET_RPM 3 设置(电)转速 4字节 有符号整数 ERPM
CAN_PACKET_SET_POS 4 设置电机转角位置
CAN_PACKET_FILL_RX_B UFFER 5
CAN_PACKET_FILL_RX_B UFFER_LONG 6
CAN_PACKET_PROCESS_RX _BUFFER 7
CAN_PACKET_PROCESS_SH ORT_BUFFER 8
请求状态CAN_PACKET_STATUS 9 Request status N/A
CAN_PACKET_SET_CURREN T_REL 10
CAN_PACKET_SET_CURREN T_BRAKE_REL 11

通过上表,再对比CAN接收指令的程序,我们就可以逐一明白我们该怎么通过CAN口去控制VESC转动。话不多说,直接上数据,这里以转速控制为例,其他的请自行举一反三。

通过CAN口驱动VESC控制电机转速:

注意,VESC这里给定的是电转速,它等于机械转速与电机极对数的乘积,即:
E R P M ( 电 转 速 ) = 机 械 转 速 × 极 对 数 . ERPM(电转速) = 机械转速×极对数. ERPM=×.
我们直接忽视VESC TOOL上设定的CAN ID,直接以0xFF后缀给定。以3对极直流无刷电机为例,当我们要控制电机以2000rpm转动时,直接在CAN总线上发送ID为0x3FF、数据长度为4的扩展帧,发送的数据为“00 00 17 70”,也就是0x00001770(即6000 ERPM):

可以看到电机转动了。但是它转一下就停了。这是因为VESC还有个Timeout,需要不断的发送指令才能让它一直转下去(这与汽车上的CAN指令的思想是一致的)。你只要在上位控制器中,定周期地向VESC发送报文数据即可。
这里,定周期向VESC给定电转速指令的dbc可以这样定义:

其他的请诸位举一反三(自行摸索)吧,祝百试百通。

本文地址:https://blog.csdn.net/jaysur/article/details/109001450

(0)
上一篇 2022年3月23日
下一篇 2022年3月23日

相关推荐