欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  科技

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

程序员文章站 2022-12-02 16:56:05
最近在驱动一个直流无刷电机,驱动这一块不是我的研究重点,只是拿来用。但系统上用到CAN总线,找来找去找到了VESC这种神级物品,自然是拿一块来玩玩。拿到我手上的VESC是国内某工作室的改版VESC V6.0。硬件方案是STM32F405+DRV8301+NVMFS5C628,带有CAN口、PPM口、USB口。一个完全不知道参数的星型直流无刷电机,就这么1分钟就能转动。不得不说,本杰明大神的VESC Tool真是个神器,傻瓜式的一键调参。但是,本人的需求并不是通过VESC Tool让电机转速来,而是通过...

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

拿到我手上的VESC是国内某工作室的改版VESC V6.4(应部分网友需求,放出链接)。硬件方案是STM32F405+DRV8301+NVMFS5C628,带有CAN口、PPM口、USB口。
通过CAN总线控制VESC驱动直流无刷电机
一个完全不知道参数的星型直流无刷电机,就这么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):

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

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

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