Robomaster電控入門(3)RM系列電機控制


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] << 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的電控繞不過去的坎,有很多很多的坑需要自己實踐時踩過了才會懂,畢竟實踐出真知嗎。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM