MAVLink通訊協議在STM32上移植,並自定義協議(這篇還寫了在STM32上怎麼收發資料,呼叫哪些函式)

詩筱涵發表於2020-10-04

這篇還寫了在STM32上怎麼收發資料,呼叫哪些函式,這個不錯。

 

摘自:https://www.jianshu.com/p/e57aa664103f?from=singlemessage

MAVLink通訊協議在STM32上移植,並自定義協議

 

anxiaozhu

0.1382017.09.12 15:25:40字數 2,016閱讀 5,218

mavlink全稱是(Micro Air Vehicle Message Marshalling Library),從名字可以看出,mavlink是主要面向飛控的一種開源通訊協議。因此它預設定義了很多適用於飛控的資訊格式,比如heartbeat(心跳訊號,每隔一兩秒主從通訊一次,以驗證通訊是否正常)。

首先要說明的是,mavlink作為一個非常可靠(至少兩位元組校驗)、支援型別豐富(message ID、component ID等)的通訊協議,每次通訊時,除了payload以外,還要佔用至少8個位元組的冗餘資訊,具體的這八個位元組都是什麼,可以參考別人的詳細介紹。因此在使用mavlink之前需要考慮,在硬體資源非常有限的情況下,是否有必要犧牲效率來換取可靠性
先放一些參考文章

MAVLink除了能夠支援ardupilot等無人機通訊協議外,最大的特點是可以定製通訊協議。前面兩篇文章主要在講MAVLink的主要結構,後面三篇出自同一個人,完整再現了一個如何從自動生成程式碼並移植到STM32上的過程,本文參考其甚多,但是正如前面所言,這裡面沒有對如何定製通訊協議進行討論,並且也沒有對整個MAVLink的結構有介紹,在移植的過程中總是報錯。

定製通訊協議

MAVLink的通訊協議是根據xml檔案自動生成的。

image.png

從官網下載MAVLink的原始碼後,可以得知定義通訊協議的xml檔案位於message_definitions/v1.0/下面,其中參考文章3、4和5就利用的common.xml進行自動生成的。

image.png

 

test.xml是其中最簡單的一種協議,test.xml的程式碼如下所示:

<?xml version="1.0"?>
<mavlink>
  <version>3</version>
  <messages>
    <message id="0" name="TEST_TYPES">
      <description>Test all field types</description>
      <field type="char" name="c">char</field>
      <field type="char[10]" name="s">string</field>
      <field type="uint8_t" name="u8">uint8_t</field>
      <field type="uint16_t" name="u16">uint16_t</field>
      <field print_format="0x%08x" type="uint32_t" name="u32">uint32_t</field>
      <field type="uint64_t" name="u64">uint64_t</field>
      <field type="int8_t" name="s8">int8_t</field>
      <field type="int16_t" name="s16">int16_t</field>
      <field type="int32_t" name="s32">int32_t</field>
      <field type="int64_t" name="s64">int64_t</field>
      <field type="float" name="f">float</field>
      <field type="double" name="d">double</field>
      <field type="uint8_t[3]" name="u8_array">uint8_t_array</field>
      <field type="uint16_t[3]" name="u16_array">uint16_t_array</field>
      <field type="uint32_t[3]" name="u32_array">uint32_t_array</field>
      <field type="uint64_t[3]" name="u64_array">uint64_t_array</field>
      <field type="int8_t[3]" name="s8_array">int8_t_array</field>
      <field type="int16_t[3]" name="s16_array">int16_t_array</field>
      <field type="int32_t[3]" name="s32_array">int32_t_array</field>
      <field type="int64_t[3]" name="s64_array">int64_t_array</field>
      <field type="float[3]" name="f_array">float_array</field>
      <field type="double[3]" name="d_array">double_array</field>
    </message>
  </messages>
</mavlink>

裡面的定義比較清晰,參考前面1、2文章,相信大多數人是很容易看懂是什麼意思的,此處不再贅述。
我們定義我們傳送的資料叫pressure,裡面只包含一個double型的變數,名叫PP(此處也可以定義更多變數),其定義xml如下:

<?xml version="1.0"?>
<mavlink>
  <version>3</version>
  <messages>
    <message id="0" name="pressure">
      <description>Test all field types</description>
      <field type="double" name="PP">double</field>
    </message>
  </messages>
</mavlink>

message id為0的情況在無人機通訊協議中一般代指heartbeat,這裡我們直接忽略,就命其為pressure。可以理解為pressure就類似結構體的名字,PP就是裡面的成員變數的名字,型別是double。

生成mavlink通訊協議的檔案

參考文章3,可以用Python根據xml檔案自動生成mavlink通訊所需的檔案。

    1. 在mavlink資料夾內執行
python -m mavgenerate

彈出下圖所示 MAVink Generator

image.png

    1. XML選擇message_definitions/v1.0/下已經定義好的檔案Out隨便選擇一個空資料夾
    1. 點選Generate即可在out資料夾內生成所需要的通訊檔案,全部都是.h檔案,其中帶有一個pressure資料夾,這個資料夾的名字和你XML的名字是一樣的

image.png

image.png

 

pressure資料夾內的檔案是針對pressure這一種message專門生成的,pressure外面資料夾內的檔案是較為通用的檔案,但是每個協議xml不同,生成的內容也不一樣。

修改檔案避免報錯

在移植到keil5中,需要修改的主要以下幾處,否則會報大量的錯誤。

    1. mavlink_types.h,

image.png

    1. mavlink_types.h

image.png

    1. checksum.h

image.png

    1. mavlink_conversions.h

image.png

image.png

    1. mavlink_helpers.h

 

image.png


至此,在keil5中編譯mavlink.h開頭的檔案都不會有錯了,使用時直接包含mavlink.h即可。
在我們使用中,pressure外面資料夾內的檔案定義了上層的通訊介面,每次生成都是一樣的(比如在pressure內再新增一個成員變數時),pressure資料夾內的檔案是根據xml檔案來的,如果再新增一條attitude資訊,則會根據attitude的定義,生成一個對應的資料夾,因此修改好外面這幾個錯誤,可以直接拷貝使用,不用每次換個協議就重新修改使用。

 

..\MAVLINK\fish_type\./mavlink_msg_pressure_collected_full.h(317): warning:  #191-D: type qualifier is meaningless on cast type

image.png

 

解決辦法:

 

image.png

 

--gnu 則根據實際情況新增或者不新增
這裡吐槽一下mavlink,它生成函式只有定義,沒有宣告,keil無法跳轉到函式定義,非常不方便。

打包資訊併傳送

MAVLink的關於pressure的函式都位於mavlink_msg_pressure.h中,我們最需要關心兩個問題

  • 1、如何傳送我採集到的pressure資料?
  • 2、如何接收並解析出上位機傳送給我的資料?
    對於問題1,mavlink分兩步走:
  • 1)mavlink_msg_pressure_pack、mavlink_msg_pressure_pack_chan、mavlink_msg_pressure_encode、mavlink_msg_pressure_encode_chan,這四個函式都在mavlink_msg_pressure中定義,是用來打包所需要傳送的資訊的,打包好的資訊裡面已經帶有校驗碼和順序等一系列資訊,因此無需再考慮新增校驗位的問題
  • 2)打包好的資訊並不是一個陣列,而是mavlink_message_t型別的,此型別名字不帶pressure,說明這是一個比較上層的結構。我們可以利用mavlink_msg_to_send_buffer函式將mavlink_message_t型別的資訊轉成char 陣列的形式,並返回陣列長度,有了此陣列可以呼叫對應微控制器的傳送模組(如串列埠)進行傳送
  • 3)注:mavlink還提供了上層程式碼和下層程式碼之間進行互相匹配的設定,預設是沒有開啟的。這一段程式碼在mavlink_msg_pressure.h中,即#define MAVLINK_USE_CONVENIENCE_FUNCTIONS後可以使用mavlink_msg_pressure_send、mavlink_msg_pressure_send_struct、mavlink_msg_pressure_send_buf等函式直接呼叫串列埠的傳送程式進行傳送。這四個函式的僅是介面略有不同,呼叫的核心函式都是一樣的。函式的呼叫過程為傳送函式 >> _mav_finalize_message_chan_send >> _mavlink_send_uart >> comm_send_ch,因此只需要定義好comm_send_ch即可使用上層函式通過串列埠傳送資料

傳送資訊的大致流程程式碼為:

mavlink_message_t message_buf;
// preesure_buffer的大小為8+sizeof(double)
uint8_t preesure_buffer[MAVLINK_NUM_NON_PAYLOAD_BYTES + MAVLINK_MSG_ID_pressure_LEN];
double PP= 123.5678; 
int length = 0;
// system_id、component_id隨便設定,不影響傳送,接收方自己能對號入座即可
mavlink_msg_pressure_pack(14, 15, &message_buf, PP);
length = mavlink_msg_to_send_buffer(preesure_buffer, &message_buf);
// serial_send(preesure_buffer, length);
// serial_write_buf(preesure_buffer, length);  //配合後面的mavlink_usart_fifo.c使用

接收資訊並解析

首先我們需要認識到,微控制器接收資料是按照位元組進行接收的,每一個位元組都會觸發接收中斷,但是微控制器事先是無法得知這一幀資料是多少個位元組的,即使知道位元組數,萬一出現丟失資料的情況,真實資料也無從得知。此處就體現出標準通訊協議的優勢了,我們不僅不需要考慮丟失資料校驗的問題,還能夠按照位元組處理資料,做到及時解析出正確資料和及時發現傳輸錯誤的資料。接收資料的關鍵函式在mavlink_helper.h中
MAVLink在接收資訊時,也需要兩步走:

  • 1)在不間斷的接收過程中,指示出何時接收到完整的一幀資料,並返回。mavlink_parse_char即可以不斷接收一個位元組的資料,並在接收到完整一條資料時返回1,否則返回0,並返回一個mavlink_message_t型別的資料
  • 2)在接收到完整的一幀資料時,可以用mavlink_msg_pressure_get_PP從mavlink_message_t型別中得到PP資料,也可用mavlink_msg_pressure_decode對mavlink_message_t進行解析得到一個mavlink_pressure_t的資料**。

接收資訊的處理大致流程為:

mavlink_message_t msg;
mavlink_status_t status;
mavlink_channel_t           chan;

void USART3_IRQHandler(void)
{           
  uint8_t c;    
  if(USART_GetITStatus(USART3, USART_IT_RXNE) != RESET)//資料接收終端
  { 
        c = USART_ReceiveData(USART3);  
        if(mavlink_parse_char(chan, c, &msg, &status))
        {
            double pp = mavlink_msg_pressure_get_PP(&msg);
            printf("Received message with ID %d, sequence: %d from component %d of system %d, pp = %.3f\n", \
                   msg.msgid, msg.seq, msg.compid, msg.sysid, pp);
        }
  }     
}

串列埠FIFO

具體到STM32,其作為一款嵌入式晶片,實時性是它優先考慮的。
一般來說串列埠是高速裝置,因此發生中斷時處理串列埠任務應時間應儘量短,同時,在傳送時,如果有大量的資料要傳送,會一直佔用串列埠資源,也會阻礙後續任務執行。因此考慮為串列埠裝置增加FIFO快取,以減輕高速裝置和低速任務之間速度不匹配的問題。
程式碼來自於文章5,這裡僅作備份。
mavlink_usart_fifo.h

// mavlink_usart_fifo.h
#ifndef _USART_FIFO_H_//×÷Õߣººã¾ÃÁ¦ÐÐ  qq:624668529
#define _USART_FIFO_H_
#include "stdint.h"
#define true 1
#define false 0
    
#define UART_TX_BUFFER_SIZE        120
#define UART_RX_BUFFER_SIZE        120

typedef struct _fifo
{
    uint8_t *buf;
    uint16_t length;
    uint16_t head;
    uint16_t tail;
} fifo_t;
uint8_t fifo_read_ch(fifo_t *fifo, uint8_t *ch);
uint8_t fifo_write_ch(fifo_t *fifo, uint8_t ch);
uint16_t fifo_free(fifo_t *fifo);
uint16_t fifo_used(fifo_t *fifo);
void fifo_init(fifo_t *fifo, uint8_t *buf, uint16_t length);
uint8_t serial_write_buf(uint8_t *buf, uint16_t length);
uint8_t serial_read_ch(void);
uint16_t serial_free(void);
uint16_t serial_available(void);
#endif  /*_USART_FIFO_H_*/

mavlink_usart_fifo.c

//mavlink_usart_fifo.c
#include "mavlink_usart_fifo.h"
#include "stm32f4xx.h"
#include "mavlink.h"

mavlink_message_t msg;
mavlink_status_t status;
extern mavlink_channel_t           chan;

fifo_t uart_rx_fifo, uart_tx_fifo;
uint8_t uart_tx_buf[UART_TX_BUFFER_SIZE], uart_rx_buf[UART_RX_BUFFER_SIZE];
/** @brief 讀FIFO
  * @param fifo 待讀緩衝區
    *        *ch   讀到的資料
    * @return 
    *        正確讀取,1; 無資料,0
  */
uint8_t fifo_read_ch(fifo_t* fifo, uint8_t* ch)
{
    if(fifo->tail == fifo->head) return false;
    *ch = fifo->buf[fifo->tail];  
    
    if(++fifo->tail >= fifo->length) fifo->tail = 0;
  return true;
}
/** @brief 寫一位元組資料到FIFO
  * @param fifo 待寫入緩衝區
    *        ch   待寫入的資料
    * @return 
    *        正確,1; 緩衝區滿,0
  */
uint8_t fifo_write_ch(fifo_t* fifo, uint8_t ch)
{
    uint16_t h = fifo->head;
    
    if(++h >= fifo->length) h = 0;
    if(h == fifo->tail) return false;
    
    fifo->buf[fifo->head] = ch;
    fifo->head = h;
  return true;
}
/** @brief 返回緩衝區剩餘位元組長度
  * @param fifo 
    * @return 
    *        剩餘空間
  *
  * @note  剩餘位元組長度大於等於2時,才可寫入資料
  */
uint16_t fifo_free(fifo_t* fifo)  
{
    uint16_t free;
    
    if(fifo->head >= fifo->tail) free = fifo->tail + (fifo->length - fifo->head);
    else free = fifo->tail - fifo->head;
    
  return free;
}
uint16_t fifo_used(fifo_t* fifo)
{
    uint16_t used;
    
    if(fifo->head >= fifo->tail) used = fifo->head - fifo->tail;
    else used = fifo->head + (fifo->length - fifo->tail);
    
    return used;    
}
/** @brief 初始化緩衝區
  * @param *fifo
  *        *buf 
  *        length
  */
void fifo_init(fifo_t* fifo, uint8_t* buf, uint16_t length)  
{
    uint16_t i;
    
    fifo->buf = buf;
    fifo->length = length;
    fifo->head = 0;
    fifo->tail = 0;
    
    for(i=0; i<length; i++) fifo->buf[i] = 0;   
}
/** @brief 寫資料到串列埠,啟動發射
  *        
  * @note 資料寫入發射緩衝區後,啟動發射中斷,在中斷程式,資料自動發出
  */
uint8_t serial_write_buf(uint8_t* buf, uint16_t length) {
    uint16_t i;
    
    if(length == 0) return false;
  for(i = 0; length > 0; length--, i++) {
        fifo_write_ch(&uart_tx_fifo, buf[i]);
    }   
  USART_ITConfig(USART2, USART_IT_TXE, ENABLE);
    
    return true;
}
/** @brief 自串列埠讀資料 
  * @return 一位元組資料
  */
uint8_t serial_read_ch(void){
    uint8_t ch; 
    fifo_read_ch(&uart_rx_fifo, &ch);   
    
    return ch;
}
/** @breif 檢測發射緩衝區剩餘位元組長度 
  * @return 剩餘位元組長度
  */
uint16_t serial_free(void){
    return fifo_free(&uart_tx_fifo);
}
uint16_t serial_available(void){
    uint16_t used=0;
    used = fifo_used(&uart_rx_fifo);
    //printf("%d\n", used);
    return used;
}

// 資料傳送
void USART2_IRQHandler(void)
{           
  uint8_t c;
  if(USART_GetITStatus(USART2, USART_IT_RXNE) != RESET)//資料接收終端
  {  
    USART_ITConfig(USART2, USART_IT_RXNE, DISABLE);
  }
  if(USART_GetITStatus(USART2, USART_IT_TXE) != RESET)//資料傳送中斷
  {         
    if(fifo_read_ch(&uart_tx_fifo, &c)) 
        USART_SendData(USART2, c);     
    else 
        USART_SendData(USART2, 0x55);           
    if (fifo_used(&uart_tx_fifo) == 0) // Check if all data is transmitted . if yes disable transmitter UDRE interrupt
    {
      // Disable the EVAL_COM1 Transmit interrupt 
      USART_ITConfig(USART2, USART_IT_TXE, DISABLE);
    }
  }     
}

//資料接收
void USART3_IRQHandler(void)
{           
  uint8_t c;    
  if(USART_GetITStatus(USART3, USART_IT_RXNE) != RESET)//資料接收終端
  { 
        c = USART_ReceiveData(USART3);  
        //fifo_write_ch(&uart_rx_fifo, c);
        if(mavlink_parse_char(chan, c, &msg, &status))
        {
            double pp = mavlink_msg_pressure_get_PP(&msg);
            printf("Received message with ID %d, sequence: %d from component %d of system %d, pp = %.3f\n", \
                   msg.msgid, msg.seq, msg.compid, msg.sysid, pp);
        }
  }     
}

相關文章