RoboMaster視覺教程(8)串口通訊

概覽

這幾天一直在做一個小車打算做好了再往下寫的,但是由於我兩年沒寫stm32的程序了,寫好程序還是很吃力的。再加上這幾天要準備考科目三(考駕照好辛苦 T_T )準備開學考試(沒錯!開學就要考試,還要考三門我沒學過的課 T_T )事比較多,就停更了兩個星期,下一篇我也不清楚什麼時候發不過有時間會一點一點地寫。

在視覺識別中一般是用妙算或者其他迷你電腦作上位機完成複雜的識別功能,在識別到目標後通過串口向下位機傳送命令指揮小車雲臺運動。

妙算或者其他使用linux系統的機器直接用DJI或者東南大學開源代碼中的串口部分就可以了,使用windows系統的機器可以參考東北林大的開源代碼中的串口部分。

DJI開源代碼串口部分

大疆的開源代碼中串口部分寫的比較簡潔,主要就四個函數。openPort、configurePort、sendXYZ和praseDatafromCar。

串口的操作其實和文件讀寫類似,或者說IO相關的操作其實都差不多,都是先獲取文件描述符fd再使用read和write函數進行讀寫操作。

#include <stdio.h>      // standard input / output functions
#include <string.h>     // string function definitions
#include <unistd.h>     // UNIX standard function definitions
#include <fcntl.h>      // File control definitions
#include <errno.h>      // Error number definitions
#include <termios.h>    // POSIX terminal control definitionss

int openPort(const char * dev_name){
    int fd; // file description for the serial port
    fd = open(dev_name, O_RDWR | O_NOCTTY | O_NDELAY);
    if(fd == -1){ // if open is unsucessful
        printf("open_port: Unable to open /dev/ttyS0. \n");
    }
    else  {
        fcntl(fd, F_SETFL, 0);
        printf("port is open.\n");
    }
    return(fd);
}

int configurePort(int fd){                      // configure the port
    struct termios port_settings;               // structure to store the port settings
    cfsetispeed(&port_settings, B115200);       // set baud rates
    cfsetospeed(&port_settings, B115200);

    port_settings.c_cflag &= ~PARENB;           // set no parity, stop bits, data bits
    port_settings.c_cflag &= ~CSTOPB;
    port_settings.c_cflag &= ~CSIZE;
    port_settings.c_cflag |= CS8;

    tcsetattr(fd, TCSANOW, &port_settings);     // apply the settings to the port
    return(fd);
}

bool sendXYZ(int fd, double * xyz){
    unsigned char send_bytes[] = { 0xFF,0x00,0x00,0x00,0x00,0x00,0x00,0xFE};
    if(NULL == xyz){
        if (8 == write(fd, send_bytes, 8))  //Send data
            return true;
        return false;
    }
    short * data_ptr = (short *)(send_bytes + 1);
    data_ptr[0] = (short)xyz[0];
    data_ptr[1] = (short)xyz[1];
    data_ptr[2] = (short)xyz[2];
    if (8 == write(fd, send_bytes, 8))      //Send data
        return true;
    return false;
}
//這個函數是在RemoteController.cpp中的
void RemoteController::praseDatafromCar(){
    char buf[255]={0};
    size_t bytes = 0;
    ioctl(fd_car, FIONREAD, &bytes);
    if(bytes > 0 && bytes < 255)
        bytes = read(fd_car, buf, bytes);
    else if(bytes >= 255)
        bytes = read(fd_car, buf, 255);
    else
        return;

    praseData(buf, bytes);
}

在打開串口時需要提供串口設備的文件地址類似於/dev/ttyUSB0

如果使用妙算的話可以使用妙算自帶的GPIO上的幾個串口。

如果使用USB串口,在插拔的過程中有可能會出現串口號變化的情況,比如上次是ttyUSB0然後程序掛了或串口出錯了,插拔usb轉串口之後串口號可能變成ttyUSB1。對於這種情況可以先將當前系統中有效的串口找出來然後再打開串口,可以參考stakoverflow中

https://stackoverflow.com/questions/2530096/how-to-find-all-serial-devices-ttys-ttyusb-on-linux-without-opening-them

串口通信爲保證上下位機數據準確需要制定一個通信協議,我最開始做比賽的時候是用字符串來對發送數據進行描述的,類似於“Y010P020"來代表yaw軸轉10度pitch軸轉20度,在下位機stm32上用最原始的加法和乘法把字符組合成數據。

這種方式不僅不好看效率也很低也很容易出錯,合理的方法是定義發送幀和接收幀,用幀頭和幀尾來校驗幀是否正確,幀頭和幀尾中間放數據。

這是DJI開源代碼中定義的上位機向下位機傳輸的幀的格式,可以看到中間的數據部分是用兩個字節組合成一個16位的整數。

frame1

串口發送時一般是發送一段unsigned char數組,其中的每一個字節都可以通過強制類型轉換來表示其他的類型。這有點類似於c語言中的union用一段內存來表示不同的數據類型。

以上述幀爲例第一個字節爲幀頭0xFF,中間6個字節組成三個16位整數,就可以像下面這樣寫。

unsigned char data[] = { 0xFF,0x00,0x00,0x00,0x00,0x00,0x00,0xFE};
*(short*)(data+1)=(short)value1;
*(short*)(data+3)=(short)value2;
*(short*)(data+5)=(short)value3;

下位機在接收的時候首先找0xFF找到後再接收7個字節,對比最後一個字節是否是0xFE若不是則丟棄,若是則本次數據有效,之後同樣可以採用強制類型轉換的方式來提取數據。

如果想發送其他類型的數據的話同樣可以用這種方式,但要根據類型的大小分配好需要的字符數組大小以防越界。

大疆開源代碼中下位機對上位機的幀的格式如下:

frame2.png)

對應的串口接收和命令解析由void RemoteController::praseData(const char * data, int size)函數完成。

這個函數中比較有意思的一段是

case 2:{                        // pitch angle
    int a = 0;
    a |= (0x7f & cmd2);
    a = (0x80 & (cmd2)) == 0 ? a : a | 0xffffff80;
    other_param->angle_pitch = (int)a / 4.0;
    //std::cout << "angle_pitch:" << other_param->angle_pitch << '\n';
    break;
}

a |= (0x7f & cmd2);這行幹啥的呢?就是將cmd2這個字節的後7位賦值給a。而下一句a = (0x80 & (cmd2)) == 0 ? a : a | 0xffffff80;則是判斷cmd2是否是負數如果是就把a的剩餘位數都賦1變成負數。

我覺得這段代碼寫得不好,明明有更好看易懂的寫法,直接int a=*(char*)&cmd2不就好啦 ^_^

另外串口波特率需要設置合理,如果波特率太高則誤碼率會增加,波特率太低則發送速度太慢。

以波特率115200爲例,它表示每秒發送115200位,換算成字節每秒是11520(不加校驗位)也就是除以十,按上例每次發送的數據爲8字節,則除8得到每秒最大可發送指令1440次,這樣對於100多幀的攝像頭來說是夠用的(我覺得串口的發送速度至少要比攝像頭的幀率大10倍以上)。

如果想每次多傳些數據,那就需要提高波特率了,在東南大學的開源代碼中他們把波特率設置爲460800也就是115200的4倍,他們定義的幀每次發送需要傳輸16字節接收需要20字節在這個波特率下可以滿足性能需要。

東南大學開源代碼串口部分

東南大學的串口部分的開源代碼兼顧了調試需要,對一些異常情況也考慮的比較周到。最值得稱讚的地方就是他們定義的幀格式考慮很周全,這樣在設計自己的通信協議時極大地減少了工作量。

/*
 * @Brief: 控制戰車幀結構體
 */
struct ControlFrame
{
    uint8_t  SOF;
    uint8_t  frame_seq;
    uint16_t shoot_mode;
    float    pitch_dev;
    float    yaw_dev;
    int16_t  rail_speed;
    uint8_t  gimbal_mode;
    uint8_t  EOF;
}_controlFrame;

/*
 * @Brief: 戰車回傳數據幀結構體
 */
struct FeedBackFrame
{
    uint8_t  SOF;
    uint8_t  frame_seq;
    uint8_t  task_mode;
    uint8_t  bullet_speed;
    uint8_t  rail_pos;
    uint8_t  shot_armor;
    uint16_t remain_HP;
    uint8_t  reserved[11];
    uint8_t  EOF;
}_feedBackFrame;

/*
 * @Brief: 比賽紅藍方
 */
enum TeamName
{
    BLUE_TEAM       =   (uint16_t)0xDDDD,
    RED_TEAM        =   (uint16_t)0xEEEE
};

/*
 * @Brief: control frame mode
 */
enum ControlMode
{
    SET_UP          =   (uint16_t)0xCCCC,
    RECORD_ANGLE    =   (uint16_t)0xFFFF,
    REQUEST_TRANS   =   (uint16_t)0xBBBB
};

/*
 * @Brief: 發射方式
 */
enum ShootMode
{
    NO_FIRE         =   (uint16_t)(0x00<<8),//不發射
    SINGLE_FIRE     =   (uint16_t)(0x01<<8),//點射
    BURST_FIRE      =   (uint16_t)(0x02<<8) //連發
};

/*
 * @Brief: 發射速度
 */
enum BulletSpeed
{
    HIGH_SPEED      =   (uint16_t)(0x01),   //高速
    LOW_SPEED       =   (uint16_t)(0x02)    //低速
};

/*
 * @Breif:所需控制模式
 */
enum TaskMode
{
    NO_TASK         =   (uint8_t)(0x00),    //手動控制
    SMALL_BUFF      =   (uint8_t)(0x01),    //小符模式
    BIG_BUFF        =   (uint8_t)(0x02),    //大符模式
    AUTO_SHOOT      =   (uint8_t)(0x03)     //自動射擊
};

/*
 * @Brief: 哨兵雲臺工作模式
 */
enum GimbalMode
{
    PATROL_AROUND   =   (uint8_t)(0x01),    //旋轉巡邏
    PATROL_ARMOR_0  =   (uint8_t)(0x02),    //巡邏裝甲板0
    PATROL_ARMOR_1  =   (uint8_t)(0x03),    //巡邏裝甲板1
    SERVO_MODE      =   (uint8_t)(0x04)     //伺服打擊
};

/* @Brief:
     *      SYSTEM_ERROR:   System error catched. May be caused by wrong port number,
     *                      fragile connection between Jetson and STM, STM shutting
     *                      down during communicating or the sockets being suddenly
     *                      plugged out.
     *      OJBK:         Everything all right
     *      PORT_OCCUPIED:  Fail to close the serial port
     *      READ_WRITE_ERROR: Fail to write to or read from the port
     *      CORRUPTED_FRAME: Wrong frame format
     *      TIME_OUT:       Receiving time out
     */
enum ErrorCode
{
    SYSTEM_ERROR    = 1,
    OJBK            = 0,
    PORT_OCCUPIED   = -1,
    READ_WRITE_ERROR= -2,
    CORRUPTED_FRAME = -3,
    TIME_OUT        = -4
};

Qt編寫串口助手

Qt是非常好用的跨平臺開源的GUI程序開發庫,我非常喜歡Qt。Qt有詳細的文檔和大量的示例程序,很多示例程序只需要稍微改一改就可以寫出我們想要的功能,對比之下用GTK開發就困難多了。

這裏我們就來用Qt自帶的串口終端的例子來實現一個串口助手,Qt編寫的代碼是跨平臺的也就是三大主流系統Windows、Linux、macOS都可以用一套代碼實現相同的功能,這個例子我在Windows和Linux下測試都是好用的。

打開Qt Designer 在Welcome界面中點擊Example 在搜索框中搜索 terminal 就能找到串口終端的例子

/8/qt1.png)/8/qt2.png)雙擊後選擇複製項目並打開會進入配置界面,按默認配置即可

/8/qt3.png)
/qt4.png)]
在彈出配置界面的同時也會彈出該例子的說明幫助,Qt的幫助文檔都寫得非常規範,讀了會很有收穫
qt5.png)]
這個例子直接編譯運行就能得到一個簡易的串口助手。
/8/qt6.png)]
點擊齒輪按鈕可以打開串口配置窗口,左邊列出了目前系統中存在的串口,右邊是波特率校驗位等的設置。配置好後點擊連接按鈕就可以打開串口了。
/8/qt8.png)]
這個示例程序實現了數據發送和接收功能,在黑框中可以直接輸入要發送的數據,同時接收數據也會傳到黑框中。

數據的接收和發送就是調用read和write函數

void MainWindow::writeData(const QByteArray &data)
{
    m_serial->write(data);
}
void MainWindow::readData()
{
    const QByteArray data = m_serial->readAll();
    m_console->putData(data);
}

如果希望一打開軟件就能自動連接串口可以在MainWindow的構造函數中加入openSerialPort();因爲Qt的這個例程默認會將搜到的第一個有效串口設置爲要打開的串口,所以加上這句後每次只要提前把串口插入再打開軟件就會以默認參數打開這個串口(這非常的方便,不用選串口不用設置波特率什麼的,插上就能用)。

網上有很多串口軟件很傻屌,有的把所有com號都列出來讓用戶自己找有效的,有的只給四個com號讓用戶選,如果com號剛好分配到這四個號以外還要到設備管理器裏改com號。

而通過調用QSerialPortInfo::availablePorts()就可以把這個問題完美解決了。

void SettingsDialog::fillPortsInfo()
{
    ui->serialPortInfoListBox->clear();
    QString description;
    QString manufacturer;
    QString serialNumber;
    //查找有效的串口
    const auto infos = QSerialPortInfo::availablePorts();
    //遍歷填充窗口信息
    for (const QSerialPortInfo &info : infos) {
        QStringList list;
        description = info.description();
        manufacturer = info.manufacturer();
        serialNumber = info.serialNumber();
        list << info.portName()
             << (!description.isEmpty() ? description : blankString)
             << (!manufacturer.isEmpty() ? manufacturer : blankString)
             << (!serialNumber.isEmpty() ? serialNumber : blankString)
             << info.systemLocation()
             << (info.vendorIdentifier() ? QString::number(info.vendorIdentifier(), 16) : blankString)
             << (info.productIdentifier() ? QString::number(info.productIdentifier(), 16) : blankString);

        ui->serialPortInfoListBox->addItem(list.first(), list);
    }

    ui->serialPortInfoListBox->addItem(tr("Custom"));
}

通過簡單的修改,我們就可以實現一個交互式的控制軟件以方便調試,也可以結合Qt中的其他模塊實現數據可視化、數據分析等功能。

例如我給我的小車方便調試做的一個簡單的控制車輪速度的軟件如下:
/8/qt7.png)]
給閒魚上淘來的寫字機器人寫的控制軟件(就是那個小朋友買來抄作業的):
/8/qt9.png)]

說一點題外話

之前有網友向我要識別風車能量機關的代碼我沒有給,一方面是我覺得我寫得不夠好沒做優化只是個原型,只在哈工大的場地測試過沒有在場上用過,另一方面是這個代碼其實也算是隊裏的祕密吧,不經過隊裏的同意擅自開源是對自己對大家都不負責任的行爲。

我的這一系列教程中基本上每篇都在給東南大學打廣告,我其實很想給自己的學校打打廣告,例如說在哈爾濱工程大學創夢之翼戰隊的視覺開源代碼中怎麼怎麼樣,但是因爲代碼並沒有開源所以不能寫。

今年的復活賽上在學弟學妹們的努力下終於把視覺預測做好了,包括打符和自瞄都很好用。隊里正在醞釀開源計劃,不過預測部分不會開源,這個希望大家理解。

申請了一個自己的公衆號江達小記,打算將自己的學習研究的經驗總結下來幫助他人也方便自己。感興趣的朋友可以關注一下。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章