ModBus RTU & PC

При создании программы, управляющей исполнительными механизмами робота TurboT и собирающей данные с его всех датчиков мне предстояло написать на C++ классы устройств: сервис платы(плата сбора данных), платы для управления двигателями ... проблема была в том, что в 2011 году все разработанные нами платы были переведены на стандартный протокол обмена ModBus. Мне предстояло либо написать код для мастера самостоятельно, либо использовать готовый код если он существует и устраивает меня... Я избрал второй вариант =). Библиотеку для реализации этого протокола можно скачать с сайта разработчика libmodbus.org. Библиотека написана на С и по заявлению разработчика библиотеки она поддерживает следующие операционные системы: Linux, Mac OS X, FreeBSD, QNX and Windows. На момент написания статьи библиотека представлена в двух версиях 2.0.3(stable) и 2.9.2/2.9.3(unstable). Хочу заметить, что интерфейсы старой и новой версий библиотеки сильно отличаются и автор рекомендует использовать более свежую версию, пусть еще и не до конца стабильную. Для большого количества задач все ее нестабильности не будут заметны. Я использовал версию библиотеки 2.9.2 в операционной системе opensuse 11.3, в качестве среды разработки я использовал IDE Qt Creator.

В make file проекта необходимо добавить

LIBS += -lmodbus

Необходимо подключить заголовочный файл.

#include <modbus/modbus.h>

Рассмотрим простой пример использования библиотеки.

1
2
3
4
5
6
7
8
uint16_t tab_reg[32];
// 'E' - event work mode, 8 - data bits, 1 - stop bit - serial port configuration
modbus_t* my_modbus = modbus_new_rtu("/dev/ttyUSB0", 57600, 'E', 8, 1);
modbus_set_slave(my_modbus, 0x0B);
modbus_connect(my_modbus);
modbus_read_registers(my_modbus, 0, 5, tab_reg);
modbus_close(my_modbus);
modbus_free(my_modbus);

Как видите код довольно логичен, чтобы избежать непонимания я все же его поясню. В первой строке создается буфер в который будет идти чтение. Во третьей строке создается структура ModBus RTU в нее передаются настройки: имя порта, скорость передачи, контроль четности и прочие настройки.В четвертой строке мы устанавливаем slave устройство где второй параметр 0x0B это так называемый board Adress(адрес устройства в сети не путать с board Identification). Далее происходит подсоединение к порту, если оно выполнено успешно то функция вернет 0. Следующей строчкой идет чтение рабочих регистров с устройства. Размер регистра равен двум байтам. В седьмой строке мы закрываем порт. В последней строке уничтожаем структуру my_modbus. Как видите все просто.

Чтобы иметь полноценный класс устройства работающего с ModBus и продемонстрировать другие возможности библиотеки я создал класс MyModBusAdapter. Вот его заголовочный файл:

#ifndef MY_MODBUS_ADAPTER_H
#define MY_MODBUS_ADAPTER_H
 
#include <string>
 
#include <modbus/modbus.h>
 
class MyModbusAdapter
{
public:
    enum EServoMotorID { eServoMotor_0 = 0, eServoMotor_1, eServoMotor_2,
                         eServoMotor_3, eServoMotor_4, eServoMotor_5,
                         eServoMotor_6, eServoMotor_7, eServoMotorNumber };
 
    enum EDistanceSensorID { eDSensor_0 = 0, eDSensor_1, eDSensor_2,
                             eDSensor_3, eDSensor_4, eDSensor_5, eDSensor_6,
                             eDSensor_7, eDSensorNumber };
 
    enum ERelayId { eRelay_0 = 0, eRelay_1, eRelay_2, eRelay_3, eRelayNumber };
 
private:
    enum EStatusDataRegister { eStatusDataStartRegister = 1100, eStatusDataEndRegister = 1107,
                               eStatusDataLength = eStatusDataEndRegister - eStatusDataStartRegister + 1};
    enum EWorkBoardRegister { eWorkDataStartRegister = 1150,  eWorkDataEndRegister = 1161,
                              eWorkDataLength = eWorkDataEndRegister - eWorkDataStartRegister + 1};
 
    enum EServoLimit { eServoMotorMinAngle = 0, eServoMotorMaxAngle = 180, eServoMotorDefaultAngle = 90 };
 
    struct StatusBoardData;
    struct WorkBoardData;
 
    enum { boarBaudrate = 57600 };
 
    enum { boadAdress = 0x0B };
    enum { boardIdentification = 0xBB };
 
public:
    explicit MyModbusAdapter(std::string&amp; portName);
    ~MyModbusAdapter();
 
    bool connectToPort();
    void disconnectFromPort();
 
    bool connectStatus();
 
    bool readState();
    bool writeData();
 
    uint16_t getDistanceSensorValue(int id);
    void setServoMotorAngle(int id, int angle);
    void setRelayState(int id, bool state);
 
private:
    MyModbusAdapter(const MyModbusAdapter&);
    MyModbusAdapter& operator = (const MyModbusAdapter&);
 
    StatusBoardData*            m_pStatusBoardData;
    WorkBoardData*              m_pWorkBoardData;
 
    modbus_t*                   m_my_modbus;
 
    bool                        m_portState;
};
 
#endif // MY_MODBUS_ADAPTER_H

Внутри класса содержится вся информация об устройстве: скорость работы платы, ее адрес и идентификационный номер. Enum EWorkBoardRegister и EStatusDataRegister содержат адреса в которые ведется запись или из которых происходит чтение и длину записываемого/читаемого блока - эти адреса соответствуют данным в реальном устройстве. Если вы разрабатываете и устройство или можете повлиять на его ход разработки, то лучше назначьте читаемые и записываемые регистры в двух разных частях рабочей области например читаемые с 0 до 50 записываемые с 50 до 100. Такой подход позволяет легко добавлять, перемещать и удалять данные внутри этих блоков без особого вреда для все системы - это удобно=). Внутри класса запрещены конструктор копирования и оператор присваивания, это необходимо, так как класс работает с устройством и дважды открывать один и тот же порт нельзя В примере мы рассмотрим запись и чтение данных с платы и некоторые вспомогательные действия. Читаемые и записываемые данные я храню в структурах: StatusBoardData, WorkBoardData. Так удобнее строить логику программы и передавать данные на чтение/запись. В класс включены несколько set, get методов которые демонстрируют работу с классом не более того.

CPP файл класса выглядит так:

#include <mymodbusadapter.h>
 
#include <stdint.h>
#include <assert.h>
 
struct MyModbusAdapter::StatusBoardData
{
    enum { distanceSensorNumber = 8 };
 
    uint16_t distanceSensor[distanceSensorNumber];
 
    StatusBoardData()
    {
        for(int i = 0; i < distanceSensorNumber; ++i)
            distanceSensor[i] = 0;
    }
 
} __attribute__ ((packed));
 
struct MyModbusAdapter::WorkBoardData
{
    enum { servoMotorNumber = 8 };
    enum { relayNumber = 4 };
 
    uint16_t servoMotorAngle[servoMotorNumber];
    uint16_t relayStatus[relayNumber];
 
    WorkBoardData()
    {
        for(int i = 0; i < servoMotorNumber; ++i)
            servoMotorAngle[i] = eServoMotorDefaultAngle;
        for(int i = 0; i < relayNumber; ++i)
            relayStatus[i] = false;
    }
 
} __attribute__ ((packed));
 
MyModbusAdapter::MyModbusAdapter(std::string&amp; portName)
    :   m_portState(false)
{
    // 'E' - event work mode, 8 - data bits, 1 - stop bit - serial port configuration
    m_my_modbus = modbus_new_rtu(portName.c_str(), boarBaudrate, 'E', 8, 1);
    modbus_set_slave(m_my_modbus, boadAdress);
 
    m_pStatusBoardData = new StatusBoardData;
    m_pWorkBoardData = new WorkBoardData;
}
 
MyModbusAdapter::~MyModbusAdapter()
{
    disconnectFromPort();
 
    modbus_free(m_my_modbus);
 
    delete m_pStatusBoardData;
    delete m_pWorkBoardData;
}
 
bool MyModbusAdapter::connectToPort()
{
    int portState = modbus_connect(m_my_modbus);
    m_portState = !portState;
    return m_portState;
}
 
void MyModbusAdapter::disconnectFromPort()
{
    if(m_portState)
    {
        modbus_close(m_my_modbus);
        m_portState = false;
    }
}
 
bool MyModbusAdapter::connectStatus()
{
    return m_portState;
}
 
bool MyModbusAdapter::readState()
{
    return modbus_read_registers(m_my_modbus, eStatusDataStartRegister,
                                 eStatusDataLength,
                                 reinterpret_cast<uint16_t*>(m_pStatusBoardData)) == eStatusDataLength;
}
 
bool MyModbusAdapter::writeData()
{
    return modbus_write_registers(m_my_modbus, eWorkDataStartRegister,
                                  eWorkDataLength,
                                  reinterpret_cast<uint16_t*>(m_pWorkBoardData)) == eWorkDataLength;
}
 
uint16_t MyModbusAdapter::getDistanceSensorValue(int id)
{
    assert(id >= 0 && id <= StatusBoardData::distanceSensorNumber);
 
    return m_pStatusBoardData->distanceSensor[id];
}
 
void MyModbusAdapter::setServoMotorAngle(int id, int angle)
{
    assert(id >=0 && id <= WorkBoardData::servoMotorNumber);
    assert(angle >= eServoMotorMinAngle && angle <= eServoMotorMaxAngle);
 
    m_pWorkBoardData->servoMotorAngle[id] = angle;
}
 
void MyModbusAdapter::setRelayState(int id, bool state)
{
    assert(id >=0 && id <= WorkBoardData::relayNumber);
 
    m_pWorkBoardData->relayStatus[id] = state;
}

В файле содержится реализация класса. Хотелось бы отметить важность макроса assert. Его использование помогает отлавливать передачу некорректных параметров внутрь функций и исправить все изъяны системы на стадии отладки. Функции modbus_read_registers() и modbus_write_registers() возвращают число считанных или записанных байт, делая проверку на желаемую длину чтения/записи можно отслеживать ошибки при работе с устройством. Если длины записанная и то что хотели записать не совпадают значит произошел сбой. Еще хотелось сказать, что при попытки чтения/записи мастер ожидает ответа о выполнении - это длиться 500ms после происходит сброс запроса.

Main файл тестового проекта выглядит так:

#include <iostream>
 
#include <mymodbusadapter.h>
 
int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
 
    MyModbusAdapter my_adapter("/dev/ttyUSB0");
 
    if(my_adapter.connectToPort())
        std::cout << "Connect" << std::endl;
    else
        std::cout << "Connect failed" << std::endl;
 
    // prepare data to write
    my_adapter.setRelayState(MyModbusAdapter::eRelay_1, true);
    my_adapter.setServoMotorAngle(MyModbusAdapter::eServoMotor_3, 60);
    my_adapter.writeData();
 
    // prepare data to write
    for(int i = 0; i < MyModbusAdapter::eServoMotorNumber; ++i)
    {
        my_adapter.setServoMotorAngle(i, 120);
    }
    for(int i = 0; i < MyModbusAdapter::eRelayNumber; ++i)
    {
        my_adapter.setRelayState(i, true);
    }
    my_adapter.writeData();
 
    // read sample
    my_adapter.readState();
    for(int i = 0; i < MyModbusAdapter::eDSensorNumber; ++i)
    {
        std::cout << my_adapter.getDistanceSensorValue(i) << std::endl;
    }
 
    my_adapter.disconnectFromPort();
 
    return a.exec();
}

Библиотека содержит множество других функций от полезных - функции записи и чтения флагов, которые упаковываются при передаче, функцию проверки устройства на линии, до бесполезных - функции записи чтения одного регистра0 флага, которые можно обобщить другими уже существующими функциями и выкинуть из библиотеки. Также существуют функции одновременного записи и чтения с устройства, иногда это может помочь сэкономить число системных вызовов, что позволяет существенно ускорить работу, так как большую часть времени работы протокола занимает подтверждение передачи и другие вспомогательные операции, не относящиеся к записи и чтению как таковым. Автор тестировал опрос данных с датчиков с помощью этого кода в цикле, 25000 запросов плата отработала стабильно, без сбоев. Отсюда вывод - протокол и его реализация в этой библиотеке надежная, можно пользоваться.

Скачать проект целиком.

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

В скором времени планируется выложить полноценную статью по ModBus TCP с использованием этой библиотеки. Следите за статьями.

10 Comments

  1. inval1d1:

    Спасибо за статью.
    Не могли бы вы подсказать, почему при сборке проекта возникает ошибка:
    "cannot find -lmodbus",
    хотя строчку "LIBS += -lmodbus" я добавил в make файл?

  2. Под какой ОС вы пишите? У вас установлена libmodbus?

  3. inval1d1:

    С библиотекой вроде разобрался.
    Я думаю над тем как как асинхронно реализовать чтение/запись в регистры.
    Лучше создать 2 независимых потока, один будет читать, а другой записывать данные?

  4. Если у вас два независимых устройства на разных портах, то можете читать/писать в них из разных потоков.
    Если порт у вас один, то вам придется защитить доступ к нему мьютексом. Одновременно писать и читать не сможете.
    Чтобы сделать именно асинхронные чтение/запись, вы можете, например, создать один поток, который сначала пишет текущие значения из буффера(ставит блокировку на чтение) в устройство, а потом читает из устройства в буффер(ставит блокировку на запись). Всё равно одновременно и читать и писать вы не сможете, так что зачем два потока? Для оптимизации можете создать булеву переменную, которая принимает значение true только если изменились значения регистров на стороне мастера. Тогда если они не изменились, писать из буффера в устройство не надо.

    • cudi:

      Помогите пожалуйста подключить библиотеку. Использую ubuntu 12.04.
      Создал в Qt Creator консольное приложение. В фаиле main.cpp добавил строчку modbus.h (заранее скинул в папку проекта все фаилы с libmodbus 2.9.2)
      Добавил в фаил *.pro строчку "LIBS += -lmodbus".
      Код никакой не писал, решил просто откопилировать, мне ответило "cannot find -lmodbus" и ":-1: ошибка: collect2: ld returned 1 exit status"
      Что делать ? Я просто не оч знаю сам компилятор Qt

  5. hokkk:

    Подскажите пожалуйста!
    Мне нужно сделать приложения чтения данных с электроприбора,
    с помощью modbus rtu по bluetooth.
    Этот пример подойдет для этого!
    Если нет то подскажите где можно найти информацию поэтому вопросу

  6. cudi:

    Подскажите пожалуйста как сделать устройство мастером, использую данную библиотеку

  7. В папке тестов http://github.com/stephane/libmodbus/tree/master/tests находятся примеры как клиента, так и мастера.

Leave a Reply

You must be logged in to post a comment.