Serial port & boost

com-portПри разработке роботов регулярно нужно общаться с кучей внешних устройств, старый добрый COM-port не раз выручал разработчиков и заслуженно занимает почетное место среди портов передачи данных. Он нашел широкое распространение в самых разных областях и сейчас есть во многих современных компьютерах. Часто для разработки своих устройств или отладки работы устройств сторонних производителей приходиться работать с этим портом. Существует множество различных программ для работы с ним, в которых можно отправлять и принимать данные, выставляя настройки, вести простенькую отладку и тестирование, но что делать если необходимо написать свое приложение использующее этот порт? Какие средства существуют и что использовать? Этому и посвящена эта статья.
Существует замечательная библиотека, написанная на С++, ее название boost. Библиотека негласно является уже почти стандартом языка и содержит кучу полезного. Одно из ключевых преимуществ библиотеки - ее кроссплатформенность. Представьте, код работающий с "железом", можно легко скомпилировать как в Windows так и различных Linux системах. Это дает огромное преимущество этому инструменту по сравнению с платформенно зависимыми библиотеками.

Установку библиотеки я упущу - информации на эту тему хватает.

Для использования библиотеки необходимо подключить заголовочные файлы:

#include "boost/asio.hpp"

В make file необходимо подключить следующие библиотеки:

LIBS += -lboost_iostreams
LIBS += -lboost_system
LIBS += -lboost_thread

Если библиотека используется в Windows, то дополнительно нужно подключить

LIBS += -lwsock32

Класс, который будет использовать наш порт будет содержать следующие члены данных

private:
    // COM PORT variables
    boost::asio::io_service                 m_Io;
    boost::asio::serial_port                m_Port;

Класс содержит два public метода для соединения и отсоединения от порта

int connectToPort(const char* pPortName);
void disconnectFromPort();
 
int CMyClass::connectToPort(const char* pPortName)
{
     int nPortOpenResult = -1;
 
    m_Port.open(pPortName);
 
    if(m_Port.is_open())
    {
        nPortOpenResult = 0;
 
        m_Port.set_option( boost::asio::serial_port_base::baud_rate( 1000000 ) );
        m_Port.set_option( boost::asio::serial_port_base::character_size( 8 ));
        m_Port.set_option( boost::asio::serial_port_base::flow_control(boost::asio::serial_port_base::flow_control::none));
        m_Port.set_option( boost::asio::serial_port_base::parity(boost::asio::serial_port_base::parity::none));
        m_Port.set_option( boost::asio::serial_port_base::stop_bits(boost::asio::serial_port_base::stop_bits::one));
    }
    return nPortOpenResult;
}
 
void CMyClass::disconnectFromPort()
{
    if(m_Port.is_open())
    {
        m_Port.close();
    }
}

Теперь опишим методы позволяющие писать и читать из порта

writeDataToNetwork(const unsigned char *transmitData, int writeSize);
readDataFromNetwork(unsigned char *transmitData, int readSize);
 
void CMyClass::writeDataToNetwork(const unsigned char *transmitData, int writeSize)
{
    boost::asio::write(m_Port, boost::asio::buffer(transmitData, writeSize));
}
 
void CMyClass::readDataFromNetwork(unsigned char *transmitData, int readSize)
{
    boost::asio::read(m_Port, boost::asio::buffer(transmitData, readSize));
}

Теперь рассмотрим простенький пример работы класса. Напишем в порт массив из четырех байт.

int main(int argc, char *argv[])
{
    CMyClass MyPort;
 
    MyPort.connectToPort("/dev/ttyUSB0");
 
    unsigned char HelloString[] = {255, 255, 2, 12};
    MyPort.writeDataToNetwork(HelloString, sizeof(HelloString));
 
    MyPort.disconnectFromPort();
 
    return 1;
}

Вызов boost::asio::read(m_Port, boost::asio::buffer(transmitData, readSize)) -блокирующий, т.е. если мы делаем чтение и с "другого конца" ничего не приходит, то наша программа/поток виснет. Иногда это необходимо, иногда вызовет сбой работы. В библиотеке существует возможность вести чтение с таймаутом, т.е. после определенного времени если считывания не произошло, то вызов прекратиться без чтения и нам нужно будет обработать это событие, но об этом в следующей статье.

Также хотелось бы отметить, что если попробовать открыть несуществующий или занятый порт, или открыть один и тот же порт дважды, то это вызовет ошибку. Целесообразно запретить конструктор копирования класса. Это можно сделать так:

private:
    CMyClass(const CMyClass&);
    CMyClass& operator = (const CMyClass&);

Также полезно будет использовать проверку is_open() там где это необходимо. Как я сделал например в disconnectFromPort().

Проект целиком для Windows/Linux

Если вы хотите передать пару байт, то их можно засунуть в массив и на этом остановиться. Если вы хотите сделать нечто более существенное и надежное, то идем дальше.

Если соединение с портом не произошло по каким либо причинам, то все упадет - это не есть хорошо. В языке C++ существует механизм исключений. Наш код можно модифицировать так:

    try
    {
        m_Port.open(pPortName.c_str());
    }
    catch(...)
    {
 
    }

Я оставляю блок обработки исключения пустым - этого достаточно чтобы все не упало заполните его так, как сочтете нужным. Например, при разработке робота  для соревнований Eurobot 2011 в подобных блоках обработки исключений мы выводили отладочную информацию на индикаторы - это заметно ускоряло поиск неисправностей в системе при подготовке к матчу. Далее так как соединение не произошло проверка на открытие порта не позволит создать новые ошибки.

Данные можно передавать не просто массивом, а запихнув и упаковав их в структуру - это гораздо удобнее нагляднее, в коде где конкретному полю структуры со своим именем идет присвоение значения куда проще разобраться, чем в длинной строке бай, где затруднительно разобраться в том, что означает каждый байт.

Рассмотрим следующий код:

struct SmallPacket
{
    static const int startSequence = 1023;
    static const int smallPacketLength = 0x02;
    static const int instrDef = 0x04;
 
    uint16_t StartBytes;
    uint8_t Id;
    uint8_t Length;
    uint8_t Instruction;
    uint8_t CheckSum;
 
    SmallPacket(int id)
        :   StartBytes(startSequence)
        ,   Id(id)
        ,   Length(smallPacketLength)
        ,   Instruction(instrDef)
    {
 
    }
} __attribute__ ((packed));

В нем мы создали структуру в которой мы видим 5 полей фиксированного размера. При передаче данных если задать тип int то его размер будет произволен и зависим от того на какой платформе и при каких условиях это было скомпилировано. Задавая тип с помощью uint8_t и других типов фиксированной длины можно быть уверенным, что данные на "другом конце" будут обработаны верно.

Далее рассмотрим упаковку структуры. Зачем это нужно ? В памяти задаваемые переменные могут укладываться не последовательно, а по секциям фиксированного размера. Может получиться так, что вы задали структуру содержащую всего два байта, при попытке узнать ее размер в программе вы обнаружите что она весит 4 или все 8 байт. Это связанно с представлением данных в памяти мне бы не хотелось лезть в эти дебри. Команда __attribute__ ((packed)) - это указание компилятору gcc упаковать все поля структуры последовательно - теперь если мы создадим в структуре две переменные по байту, то ее размер и будет два байта. Хотелось бы заметить, что если создать union с битовыми полями, то наши данные будут упаковываться последовательно секциями и минимальный размер секции будет 1 байт, это нужно учесть в своих изысканиях.

Последнее, что мы не рассмотрели - это значения по умолчанию в конструкторе структуры. Зачем они нужны ? При создании структуры ее поля будут проанализированы случайным образом, и если мы попытаемся записать эти данные, не проведя инициализации далее то можем получить самый абсурдный результат работы устройства, вплоть до его выхода из эксплуатации. Поэтому лучше себя обезопасить и задать в конструкторе структуры значения по умолчанию, которые например не вызывают никакого действия.

Рассмотрим пример программы передающий эту структуру.

    SmallPacket testPacket(1);
 
    MyPort.writeDataToNetwork(reinterpret_cast(&testPacket), sizeof(SmallPacket));

Вот в принципе и все. Да тут нужно использовать именно такое преобразование типов указателей. Другие не будут работать. И небольшая тонкость. Если вы работаете с портом в Linux то позаботьтесь о том, чтобы он был открыт для вашего пользователя, ибо по умолчанию порты доступны только для root.

Доработанный проект целиком

ВНИМАНИЕ !!! Мною была найдена более полная чем моя - библиотека для работы с последовательным портом. Она аналогична, НО имеет чтение по таймауту -позволяющее более интересно общаться с устройствами. Скачать это маленькое чудо можно тут www.github.com/wjwwood/serial , распространяется это чудо под лицензией MIT - бери и используй в любых замыслах))).

One Comment

  1. fonin:

    > Команда __attribute__ ((packed)) – это указание компилятору gcc упаковать все поля
    > структуры последовательно – теперь если мы создадим в структуре две переменные по байту,
    > то ее размер и будет два байта.
    #pragma pack(1)
    делает то же самое

Leave a Reply

You must be logged in to post a comment.