RoboMaster电控入门(3)RM系列电机控制

您所在的位置:网站首页 编码器4096和8192区别 RoboMaster电控入门(3)RM系列电机控制

RoboMaster电控入门(3)RM系列电机控制

2023-08-21 09:55| 来源: 网络整理| 查看: 265

RM系列电机,电调介绍

Robomaster官方提供了一系列性能各异,可以用于不同场景,且易于驱动的直流无刷减速电机及配套电调,这里主要介绍三款常用的电机&电调——M3508电机&C620电调,GM6020电机(内部集成电调),M2006&C610电调。

这些电调的手册,驱动demo等同样可以到官网上去下载

https://www.robomaster.com/zh-CN/products/components/general

直流无刷电机不使用传统有刷电机的电刷机械结构,而是通过电子换向器实现换向,相比传统电机有着许多的性能优势,一般使用直流无刷电机时需要有配套的电调,通过改变电调输出的电流大小和方向,可以改变电机的转速和转向。

Robomaster系列的电机内部都有霍尔传感器,可以反馈电机的转速,位置等信息,以供用户实现闭环控制。

一般使用电机时都是先将电机与配套电调连接,电调与电源以及主控板连接,这里以M3508电机为例,其他电机使用时也是同理,首先是将电机和电调互联,C620电调上有一个xt30电源输入口和一个2pin的CAN接口。

官方提供了中心板,电调的电源和信号口可以连接至中心板,再由中心板连接电池和主控。一个中心板上有4个xt30电源输出,4个2pin的CAN接口,1个xt60的电源输入和1个8pin的电源&CAN组合输出,刚刚好可以组成一个四轮底盘。

https://www.robomaster.com/zh-CN/products/components/detail/143

RM的A型主控板上一共有两路CAN,一路是CAN1,采用2pin接口,一路是CAN2,采用4pin接口,可以直接使用双头2pin线连接主控板CAN1接口&中心板或主控板CAN1接口&电调,也可以通过2pin转4pin线连接CAN2接口。

电机是整个机器人上最重要的执行器之一,基本上一个机器人的控制流中,所有输入的最终目的都是为了体现在电机的输出上。一个典型的robomaster步兵机器人身上一共会用到哪些电机呢?以大疆开源的ICRA机器人为例

https://www.robomaster.com/zh-CN/products/components/robot

其使用的电机如下表

电机位置 电机型号 底盘电机*4 M3508 云台yaw轴电机,pitch轴电机 GM6020 拨弹电机*1 M2006 摩擦轮电机*2 Snail 2305

其中Snail电机是前文中没有提到的,这是一个PWM控制的直流无刷电机,由于没有霍尔传感器,该电机不能实现闭环控制,有一些队伍会使用去掉减速箱的M3508电机作为替代方案。

不同的电机有着不同的性能,因此被用于不同的机构中,具体使用哪一款电机需要通过分析转速,扭矩等需求进行选型,获取这些参数的直接手段就是查阅官方的手册,这里依然是以M3508电机为例。

Robomaster系列的电机及配套电调几乎全部是通过CAN总线连接到主控的,即主控通过CAN总线发送数据给电调,实现电机的调速,电调通过CAN总线将电机数据反馈给主控。

RM系列的电机&电调是专门针对比赛进行过设计的,在实际的赛场环境中也确实有着很好的发挥,下面是官方论坛上发布的测评贴,内容很有趣,值得一读。

https://bbs.robomaster.com/thread-5009-1-1.html

CAN通讯

如上一小节所说,RM系列电机&电调大都是使用CAN进行通讯的,因此掌握了CAN通讯就搞定了一大半的电机驱动,其重要性不言而喻,但CAN是一个相对而言比较复杂的通讯协议,相比于UART,SPI,IIC这些常用的通讯协议,CAN有着更多的特性需要去记忆,本节将对CAN的一些比较重要的特性进行梳理,但是不会涉及到CAN的全貌,因为如果要介绍全的话可能要写很长很长了.......

硬件层面

差分信号

与其他通信方式重要差别之一是CAN采用的是“差分信号”,即通过组成总线的2根线(CAN-H和CAN-L)的电位差来确定总线的电平,信号是以两线之间的“差分”电压形式出现,总线电平分为显性电平和隐性电平。

CAN总线采用两种互补的逻辑数值"显性"和"隐性"。"显性"数值表示逻辑"0",而"隐性"表示逻辑"1"。当总线上同时出现“显性”位和“隐性”位时,最终呈现在总线上的是“显性”位。

与串口这种除了TX和RX,还需要用GND连接两个设备串行通讯方式不同,CAN总线只需要CAN_H和CAN_L两根线,就能够通过差分信号的方式表征逻辑"0"和逻辑"1"

帧仲裁

任何总线都不得不需要面临处理冲突的问题,因为多个设备都挂载在总线上,难免会出现若干个设备同时想要发送信号的情况,这种情况下就需要进行仲裁,判断哪个设备可以占用总线,而其他设备要转变为接收或者等待。

CAN的仲裁机制正好利用了差分信号的特性,即显性电平覆盖隐形电平的特性,如果出现多个设备同时发送的情况,则先输出隐形电平的设备会失去对总线的占有权。下图中D为显性电平,R为隐形电平,通过该图可以很容易地理解CAN的仲裁机制。

波特率

CAN有着很高的通讯速率,通过查阅手册可知,一般RM系列电调的通讯速率为1Mbps,只有波特率一致的情况下,主控才能成功与电调进行通讯,CAN的通讯速率的决定因素包括

同步段(SYNC_SEG):位变化应该在此时间段内发生。只有一个时间片的固定长度(1 x tq) 位段1(BS1):定义采样点的位置。其持续长度可以在 1 到 16 个时间片之间调整 位段2(BS2):定义发送点的位置。其持续长度可以在 1 到 8 个时间片之间调整 同步跳转宽度(SJW):定义位段加长或缩短的上限。它可以在 1 到 4 个时间片之间调整

在ST官方的手册中可以找到波特率的计算公式,通过用户对时钟树,分频值,以及上面4个值的设置,就可以得到想要的波特率。

当然,这种计算一般是有套路的,一个特定的波特率一般会有对应的一组值,比如针对RM系列电调,一套典型的设置值如下(来自官方开源代码),APB1外设时钟42MHz,分频值被设置为7,SJW被设置为1tq,BS1被设置为2tq,BS2被设置为3tq,可以计算出波特率恰好是1Mbps。当然,具体的设置还是要根据时钟树来进行。

hcan1.Instance = CAN1; hcan1.Init.Prescaler = 7; hcan1.Init.Mode = CAN_MODE_NORMAL; hcan1.Init.SyncJumpWidth = CAN_SJW_1TQ; hcan1.Init.TimeSeg1 = CAN_BS1_2TQ; hcan1.Init.TimeSeg2 = CAN_BS2_3TQ;

软件层面

过滤器

CAN的过滤器的目的是很容易理解的,由于总线上的信号是以广播的形式发送的,如果设备都在对于每一个被广播的信号都进行接收+判断,那么势必会浪费大量的时间在这项其实没有什么意义的工作上,解决的方法就是通过设置过滤器,屏蔽掉一些和自己无关的设备发来的信息。

我们都知道所有CAN设备都是有ID的,具体的ID我们可以从手册中获取。以C620电调为例,可以在C620电调的手册中看到,电调反馈信号时其ID为0x201-0x204。

https://www.robomaster.com/zh-CN/products/components/general/M3508

在知道了设备ID之后,为了实现过滤的功能,我们需要对CAN过滤器进行配置。

CAN的过滤器模式分为掩码模式和列表模式,列表模式简单来说就是制作一张ID表,如果来的数据的ID在这张表中则接收,否则不收。

重点介绍一下掩码模式的原理:掩码模式的思路很容易理解,举个例子,某所学校的学号构成方式为[4位10进制 入学年份]+[4位10进制 学生序号],比如一个2016年入学的学生,其学号可以是20161234,那么假如要开一个2016年毕业生的庆祝会,会场门口要检查每一个人的学号,只有2016级的才可以进入,这里应该使用什么样的判断方法呢?

首先,我们需要设置屏蔽码,屏蔽掉后四位的学生序号,因为他们和本次检测无关,反而增大了计算量。

然后设置检验码2016,如果屏蔽后的结果等于2016,则可以放行。

如下表所示,第一行为原码,第二行为掩码,将第一行表格中的数与掩码相乘,即得到第三行的屏蔽码,最后一行是验证码,屏蔽码和验证码比较确定一致后,就接收该学号。

2 0 1 6 1 2 3 4 1 1 1 1 0 0 0 0 2 0 1 6 0 0 0 0 2 0 1 6 0 0 0 0

这里依然是以官方开源代码为例,每一行添加注释说明其功能

can_filter_st.FilterActivation = ENABLE; //satori:激活滤波器 can_filter_st.FilterMode = CAN_FILTERMODE_IDMASK; //satori:采用掩码模式 can_filter_st.FilterScale = CAN_FILTERSCALE_32BIT; //satori:设置32位宽 can_filter_st.FilterIdHigh = 0x0000; //satori:设置验证码高低各4字节 can_filter_st.FilterIdLow = 0x0000; can_filter_st.FilterMaskIdHigh = 0x0000; //satori:设置屏蔽码高低各4字节 can_filter_st.FilterMaskIdLow = 0x0000; can_filter_st.FilterBank = 0; //satori:使用0号过滤器 can_filter_st.FilterFIFOAssignment = CAN_RX_FIFO0; //satori:通过CAN的信息放入0号FIFO HAL_CAN_ConfigFilter(&hcan1, &can_filter_st);

那么我们来看一下开源代码中验证码和屏蔽码这两项的配置,屏蔽码设为0x00000000,无论任何标识符通过之后都变成0x00000000,验证码为0x00000000,所以无论任何屏蔽码都能通过。可见其实并没有起到任何过滤作用,这是因为CAN总线上挂载的四个电调,我们的主控都需要接收其数据,所以无论来的标识符是哪个,都要照单全收,而CAN不配置完过滤器是无法开启的,所以才有这套验证码+屏蔽码都是0x00000000的操作。

最后贴一个CSDN上写的比较好的博客,推荐大家也读一下

https://blog.csdn.net/flydream0/article/details/52317532

标准数据帧

CAN的一个标准数据帧包括以下几个部分——

仲裁场中包含12位的标识符

仲裁场后跟随的是控制场,存放数据长度DLC,数据场中要填写CAN发送的数据

最后的CRC,应答这些就与校验,总线控制等有关了,和用户没有太大关系。

具体该怎么配置,还是需要根据手册走,这里以C620电调为例,在手册中我们可以找到如下内容——

电调接收报文格式:

电调反馈报文格式:

电调信号收发示例

依然是以官方开源代码为例,我们先看CAN接收的过程,即如何从CAN接收中断回调函数开始,一步一步送到解码函数中,调用过程如下

HAL_CAN_RxFifo0MsgPendingCallback->can1_motor_msg_rec->motor_device_data_update->get_encoder_data

编码器解码函数,主要完成的工作依然是数据拼接

static void get_encoder_data(motor_device_t motor, uint8_t can_rx_data[]) { motor_data_t ptr = &(motor->data); ptr->msg_cnt++; if (ptr->msg_cnt > 50) { motor->init_offset_f = 0; } if (motor->init_offset_f == 1) { get_motor_offset(ptr, can_rx_data); return; } ptr->last_ecd = ptr->ecd; //satori:data[0]和data[1]拼接成转子机械角度 ptr->ecd = (uint16_t)(can_rx_data[0] ecd - ptr->last_ecd > 4096) { ptr->round_cnt--; ptr->ecd_raw_rate = ptr->ecd - ptr->last_ecd - 8192; } else if (ptr->ecd - ptr->last_ecd < -4096) { ptr->round_cnt++; ptr->ecd_raw_rate = ptr->ecd - ptr->last_ecd + 8192; } else { ptr->ecd_raw_rate = ptr->ecd - ptr->last_ecd; } ptr->total_ecd = ptr->round_cnt * 8192 + ptr->ecd - ptr->offset_ecd; /* total angle, unit is degree */ ptr->total_angle = ptr->total_ecd / ENCODER_ANGLE_RATIO; //satori:data[2]和data[3]拼接成转子转速 ptr->speed_rpm = (int16_t)(can_rx_data[2] given_current = (int16_t)(can_rx_data[4] motor_can_send->motor_device_can_output->motor_can1_output_1ms

通过软件定时器设置CAN发送周期为1ms

同样分析一下发送函数,在can_msg_bytes_send函数中完成对帧格式的设置

uint32_t can_msg_bytes_send(CAN_HandleTypeDef *hcan, uint8_t *data, uint16_t len, uint16_t std_id) { uint8_t *send_ptr; uint16_t send_num; can_manage_obj_t m_obj; struct can_std_msg msg; send_ptr = data; msg.std_id = std_id; send_num = 0; if (hcan == &hcan1) { m_obj = &can1_manage; } else if (hcan == &hcan2) { m_obj = &can2_manage; } else { return 0; } while (send_num < len) { if (fifo_is_full(&(m_obj->tx_fifo))) { //can is error m_obj->is_sending = 0; break; } if (len - send_num >= 8) { msg.dlc = 8; } else { msg.dlc = len - send_num; } //memcpy(msg.data, data, msg.dlc); *((uint32_t *)(msg.data)) = *((uint32_t *)(send_ptr)); *((uint32_t *)(msg.data + 4)) = *((uint32_t *)(send_ptr + 4)); send_ptr += msg.dlc; send_num += msg.dlc; fifo_put(&(m_obj->tx_fifo), &msg); } if ((m_obj->is_sending) == 0 && (!(fifo_is_empty(&(m_obj->tx_fifo))))) { CAN_TxHeaderTypeDef header; uint32_t send_mail_box; header.StdId = std_id; //satori:设置帧格式为标准帧 header.IDE = CAN_ID_STD; header.RTR = CAN_RTR_DATA; while (HAL_CAN_GetTxMailboxesFreeLevel(m_obj->hcan) && (!(fifo_is_empty(&(m_obj->tx_fifo))))) { fifo_get(&(m_obj->tx_fifo), &msg); header.DLC = msg.dlc; //satori:调用HAL库函数进行发送 HAL_CAN_AddTxMessage(m_obj->hcan, &header, msg.data, &send_mail_box); m_obj->is_sending = 1; } } return send_num; }

在motor_device_can_output中进行数据,DLC,ID的设置

int32_t motor_device_can_output(enum device_can m_can) { struct object *object; list_t *node = NULL; struct object_information *information; motor_device_t motor_dev; memset(motor_msg, 0, sizeof(motor_msg)); var_cpu_sr(); /* enter critical */ enter_critical(); /* try to find device object */ information = object_get_information(Object_Class_Device); for (node = information->object_list.next; node != &(information->object_list); node = node->next) { object = list_entry(node, struct object, list); motor_dev = (motor_device_t)object; if(motor_dev->parent.type == Device_Class_Motor) { if (((motor_device_t)object)->can_id < 0x205) { //装填ID,装填数据 motor_msg[motor_dev->can_periph][0].id = 0x200; motor_msg[motor_dev->can_periph][0].data[(motor_dev->can_id - 0x201) * 2] = motor_dev->current >> 8; motor_msg[motor_dev->can_periph][0].data[(motor_dev->can_id - 0x201) * 2 + 1] = motor_dev->current; motor_send_flag[motor_dev->can_periph][0] = 1; } else { motor_msg[motor_dev->can_periph][1].id = 0x1FF; motor_msg[motor_dev->can_periph][1].data[(motor_dev->can_id - 0x205) * 2] = motor_dev->current >> 8; motor_msg[motor_dev->can_periph][1].data[(motor_dev->can_id - 0x205) * 2 + 1] = motor_dev->current; motor_send_flag[motor_dev->can_periph][1] = 1; } } } /* leave critical */ exit_critical(); for (int j = 0; j < 2; j++) { if (motor_send_flag[m_can][j] == 1) { if (motor_can_send != NULL) motor_can_send(m_can, motor_msg[m_can][j]); motor_send_flag[m_can][j] = 0; } } /* not found */ return RM_OK; }

实际上对帧格式,DLC,ID,数据的装填可以全部在一个函数中完成,官方代码写的相对而言比较复杂,多封装了好几层,读者可以去论坛上找一些相对而言简单一些的开源代码看看。

结语

这一讲应该是最硬核,也是我自己写的最累的一讲,CAN是每一个参加RM的电控绕不过去的坎,有很多很多的坑需要自己实践时踩过了才会懂,毕竟实践出真知吗。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3