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上寫的比較好的博客,推薦大家也讀一下
-
標准數據幀
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] << 8 | can_rx_data[1]);
if (ptr->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] << 8 | can_rx_data[3]);
//satori:data[4]和data[5]拼接成實際轉矩電流
ptr->given_current = (int16_t)(can_rx_data[4] << 8 | can_rx_data[5]);
}
CAN發送過程調用順序如下:
can_msg_bytes_send->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的電控繞不過去的坎,有很多很多的坑需要自己實踐時踩過了才會懂,畢竟實踐出真知嗎。
