Урок 58. Обмен данными между платами Ардуино через UART по протоколу ModBus. Библиотека Tiny_ModBusRTU_Master.

обмен данными Ардуино

В уроке представлю библиотеку поддержки протокола ModBus для ведущего контроллера. С помощью нее реализую обмен данными между двумя платами Ардуино.

Предыдущий урок     Список уроков     Следующий урок

В предыдущем уроке мы разработали локальный контроллер для сети с протоколом ModBus. В качестве ведущего ModBus устройства использовали компьютер. В этом уроке подключим локальный контроллер к другой плате Ардуино. Т.е. реализуем систему из урока 49, только с использованием протокола ModBus.

 

В предыдущем уроке я представил библиотеку Tiny_ModBusRTU_Slave. Надеюсь, вы оценили, как просто с помощью нее создавать программное обеспечение ведомого ModBus контроллера. В этом уроке я представлю аналог этой библиотеки для ведущего устройства.

 

Библиотека Tiny_ModBusRTU_Master.

Загрузить библиотеку можно по этой ссылке.

Tiny_ModBusRTU_Master позволяет простыми средствами реализовать программное обеспечение ведущих ModBus контроллеров. Может быть использована совместно с интерфейсами RS-232, RS-422, RS-485, UART и т.п. Поддерживает управление состоянием передатчика в шинных интерфейсах, например в RS-485.

Библиотека является своеобразной ”ответной частью” библиотеки Tiny_ModBusRTU_Slave. Но может быть использована и с другими ведомыми контроллерами в ModBus сетях.

Библиотека работает в фоновом режиме, параллельным процессом. Все операции происходят в обработчике прерывания по таймеру. В основном цикле программа только инициирует обмен данными.

Библиотека поддерживает минимальный набор функций для работы с регистрами хранения (всего 3 функции). Это основные операции, которых, как правило, достаточно для управления любыми контроллерами.

Код функции Название Описание
03 READ HOLDING REGISTERS Чтение значений одного или нескольких регистров хранения
06 FORCE SINGLE REGISTER Запись в один регистр хранения
16 FORCE MULTIPLE REGISTERS Последовательная запись нескольких регистров хранения

 

Описание класса Tiny_ModBusRTU_Master  выглядит так:

class Tiny_ModBusRTU_Master {

  public:
    Tiny_ModBusRTU_Master(byte timeOutTransmit, byte timeOutRecieve); // конструктор
    Tiny_ModBusRTU_Master(byte timeOutTransmit, byte timeOutRecieve, byte directPin); // конструктор
    void update(); // загрузка данных
    void read(byte adress, unsigned int* reg, unsigned int holdingRegBegin, unsigned int holdingRegNumber); // чтение регистров хранения
    void writeSingle(byte adress, unsigned int data, unsigned int holdingRegBegin); // запись одного регистра хранения
    void writeMultiple(byte adress, unsigned int* reg, unsigned int holdingRegBegin, unsigned int holdingRegNumber); // запись нескольких регистров хранения
    byte state; // состояние обмена
};

Tiny_ModBusRTU_Master(byte timeOutTransmit, byte timeOutRecieve, byte directPin) - конструктор.

Создает объект  Tiny_ModBusRTU_Master со следующими параметрами:

  • timeOutTransmit – время паузы (тишины) между фреймами. Зависит от скорости обмена. Должно быть не менее времени, необходимого для передачи 3,5 байта. Рассчитывается, как  timeOutTransmit, умноженное на время периода вызова функции update().
  • timeOutRecieve – время тайм-аута ответа ведомого устройства. Время ожидания приема первого байта от ведомого устройства. Рассчитывается, как timeOutRecieve, умноженное на время периода вызова функции update(). При отсутствии ответа за это время формируется ошибка тайм-аута (код 2).
  • directPin – номер вывода разрешения передатчика ведущего устройства. Вывод используется в шинных интерфейсах, у которых передатчик имеет три состояния. Параметр необязательный. При отсутствии параметра вывод не используется.

Tiny_ModBusRTU_Master master(8, 30, 13);    // создаем объект, времена тайм-аутов 4 и 15 мс, управляющий вывод 13

 

Методы.

void update() – управление обменом. Метод должен регулярно вызываться в параллельном процессе, например, в прерывании по таймеру.

//--------------------------- обработчик прерывания 500 мкс
void  timerInterrupt() {
master.update(); // управление обменом
}

Метод полностью управляет обменом данными. Основная программа только вызывает функцию, инициирующую обмен с нужными параметрами. Сам обмен происходит параллельным процессом. О завершении операции основной программе сообщает свойство state - состояние обмена. Во время обмена программа не останавливается, не зависает.

void read(byte adress, unsigned int* reg, unsigned int holdingRegBegin, unsigned int holdingRegNumber) – чтение регистров хранения. Функция инициирует чтение одного или нескольких регистров хранения. Имеет следующие аргументы:

  • adress – адрес ведомого устройства. Может иметь значения от 1 до 247.
  • reg – указатель на массив для прочитанных данных.
  • holdingRegBegin – начальный адрес регистров хранения.
  • holdingRegNumber – количество регистров хранения.

master.read(1, regTable, 0, 5); // инициация чтения 5 регистров хранения начиная с адреса 0, у контроллера с адресом 1, в массив regTable

void writeSingle(byte adress, unsigned int data, unsigned int holdingRegBegin) - запись одного регистра хранения. Функция инициирует запись одного регистра хранения. Имеет следующие параметры:

  • adress – адрес ведомого устройства. Может иметь значения от 0 до 247. В случае, если адрес равен 0, инициируется широковещательный режим. При этом данные передаются всем ведомым контроллерам одновременно и ответ не ожидается.
  • data – данное для записи в регистр хранения.
  • holdingRegBegin – адрес регистра хранения.

master.writeSingle(1, (unsigned int)button1.flagPress, 5); // запись регистра хранения с адресом 5, контроллера с адресом 1

void writeMultiple(byte adress, unsigned int* reg, unsigned int holdingRegBegin, unsigned int holdingRegNumber) - запись нескольких регистров хранения. Инициирует запись нескольких регистров хранения. Имеет параметры:

  • adress – адрес ведомого устройства. Может иметь значения от 0 до 247. В случае, если адрес равен 0, инициируется широковещательный режим. При этом данные передаются всем ведомым контроллерам одновременно и ответ не ожидается.
  • reg – указатель на массив данных записи.
  • holdingRegBegin – начальный адрес регистров хранения.
  • holdingRegNumber – количество регистров хранения.

master.writeMultiple(1, regTable, 5, 2); // запись 2 регистров хранения начиная с адреса 5, контроллер с адресом 1

byte state – public свойство класса - состояние обмена. Сообщает о результате операции. Может иметь следующие значения:

  • 0 - операция завершена успешно;
  • 1 - идет операция;
  • 2 - ошибка тайм-аута;
  • 4 - ошибка данных;
  • 8 - недопустимый адрес данных;
  • 16 - код функции не поддерживается;
  • 32 – другая ошибка.

 

Применение библиотеки Tiny_ModBusRTU_Master.

Для практической реализации программы ведущего контроллера необходимо:

Подключить библиотеку Tiny_ModBusRTU_Master;

создать объект Tiny_ModBusRTU_Master;

задать параметры UART (через класс Serial);

реализовать прерывание по таймеру и в его обработчике разместить функцию update().

Теперь в основной программе можно инициировать операции обмена функциями: read(), writeSingle() или writeMultiple.

Для проверки завершения операции в основной программе надо контролировать состояние свойства state. Как только его значение перестает быть равным 1, операция завершена. При state=0 операция завершена успешно.

 

Реализация ведущего контроллера с протоколом ModBus RTU.

Я использовал центральный контроллер из урока 49 без каких-либо изменений схемы.

схема центрального контроллера

У меня контроллер выглядит  так.

центральный контроллер

Программу для центрального будем разрабатывать новую. Для локального - программа из предыдущего урока.

Напомню, что центральный контроллер должен считывать из локального:

  • температуру;
  • напряжение;
  • состояние кнопки.

Кроме того центральный контроллер должен управлять светодиодом локального контроллера и выводить статистику обмена: количество циклов обмена и ошибок.

дисплей

Напомню формат регистров хранения локального контроллера.

Номер регистра Формат числа Параметр
0 float Температура
1
2 float Напряжение
3
4 бит Состояние кнопки (мл. бит)
5 бит Состояние светодиода (мл. бит)

С регистров 0-4 мы будем считывать данные, а в регистр 5 – записывать.

Первый вариант программы центрального контроллера я разработал в демонстрационных целях. У него понятная логика работы, но программа зависает в ожидании окончания операции обмена. Т.е. не используется главное преимущество библиотеки Tiny_ModBusRTU_Master – работа в фоновом режиме.

После инициации операции обмена программа ждет момента, когда переменная state перестанет быть равной 1. Т.е. когда операция закончится.

master.read(1, regTable, 0, 5); // чтение регистров хранения
while(master.state == 1) {} // ожидание данных

Обратите внимание, что в этом случае при объявлении объекта Tiny_ModBusRTU_Master  надо использовать квалификатор volatile. Об этом написано в уроке 10.

Полностью скетч выглядит так.

// центральный контроллер с протоколом ModBus
#include <TimerOne.h>
#include <LiquidCrystal.h>
#include <Button.h>
#include <Tiny_ModBusRTU_Master.h>

volatile Tiny_ModBusRTU_Master master(8, 30, 13);
LiquidCrystal disp(6, 7, 2, 3, 4, 5); // объект дисплей
volatile Button button1(10, 30); // кнопка 1 подключена к выводу 10

unsigned int regTable[6]; // таблица регистров
unsigned int cyclCount= 0; // счетчик циклов
unsigned int errorCount= 0; // счетчик ошибок

void setup() {
  Timer1.initialize(500); // инициализация таймера 1, период 500 мкс
  Timer1.attachInterrupt(timerInterrupt, 500); // задаем обработчик прерываний
  Serial.begin(9600);
  disp.begin(20, 4); // инициализируем дисплей 4 x 20 символов
}

void loop() {

  master.read(1, regTable, 0, 5); // чтение регистров хранения
  while(master.state == 1) {} // ожидание данных

  if(master.state == 0) {
    // данные получены
    cyclCount++; // счетчик циклов
    disp.clear(); // очистка экрана
    disp.print("C=");
    disp.print(cyclCount);
    disp.print(" E=");
    disp.print(errorCount);
    disp.setCursor(0, 1);
    disp.print("T=");
    disp.print(* ((float *)regTable),1);
    disp.print(" C U=");
    disp.print(* ( ((float *)regTable) +1 ),1);
    if ( (regTable[4] & 1) == 0) disp.print(" V B=F");
    else disp.print(" V B=P");
  }
  else {
    // ошибка обмена
    errorCount++; // счетчик ошибок
    disp.clear(); // очистка экрана
    disp.print("C=");
    disp.print(cyclCount);
    disp.print(" E=");
    disp.print(errorCount);
    disp.setCursor(0, 1);
    disp.print("ERROR= ");
    disp.print(master.state);
  }

  master.writeSingle(1, (unsigned int)button1.flagPress, 5); // запись регистра хранения (светодиод)
  while(master.state == 1) {} // ожидание данных
  if(master.state != 0) errorCount++;
  delay(500);
}

//--------------------------- обработчик прерывания 500 мкс
void timerInterrupt() {
  master.update(); // проверка данных обмена
  button1.scanState(); // обработка сигнала кнопки 1
}

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

 

Проверка работы программы.

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

дисплей

Нажал кнопку сброса локального контроллера. Посыпались ошибки.

дисплей

Отпустил кнопку – обмен возобновился, ошибки прекратились.

дисплей

Другой вариант программы.

Во втором варианте программа не зависает, ожидая окончания операции обмена. Каждый проход цикла loop() происходит без задержек. В квалификаторе volatile при объявлении объекта Tiny_ModBusRTU_Master нет необходимости. Обмен происходит в фоновом режиме, программу можно дополнить другими задачами в цикле loop().

// центральный контроллер с протоколом ModBus
#include <TimerOne.h>
#include <LiquidCrystal.h>
#include <Button.h>
#include <Tiny_ModBusRTU_Master.h>

Tiny_ModBusRTU_Master master(8, 30, 13);
LiquidCrystal disp(6, 7, 2, 3, 4, 5); // объект дисплей
Button button1(10, 30); // кнопка 1 подключена к выводу 10

unsigned int regTable[6]; // таблица регистров
unsigned int cyclCount= 0; // счетчик циклов
unsigned int errorCount= 0; // счетчик ошибок
byte mode=0; // режим: 0 - чтение, 1 - запись

void setup() {
  Timer1.initialize(500); // инициализация таймера 1, период 500 мкс
  Timer1.attachInterrupt(timerInterrupt, 500); // задаем обработчик прерываний
  Serial.begin(9600);
  disp.begin(20, 4); // инициализируем дисплей 4 x 20 символов
  master.read(1, regTable, 0, 5); // чтение регистров хранения
}

void loop() {

  if( (mode & 1) == 0 ) {
    // чтение регистров хранения
    if(master.state != 1) {
      // операция завершена
      if(master.state == 0) {
        // данные получены
        disp.clear(); // очистка экрана
        disp.print("C=");
        disp.print(cyclCount);
        disp.print(" E="); 
        disp.print(errorCount);
        disp.setCursor(0, 1);
        disp.print("T=");
        disp.print(* ((float *)regTable),1);
        disp.print(" C U=");
        disp.print(* ( ((float *)regTable) +1 ),1);
        if ( (regTable[4] & 1) == 0) disp.print(" V B=F");
        else disp.print(" V B=P");
      }
      else {
        // ошибка обмена
        errorCount++; // счетчик ошибок
        disp.clear(); // очистка экрана
        disp.print("C=");
        disp.print(cyclCount);
        disp.print(" E=");
        disp.print(errorCount);
        disp.setCursor(0, 1);
        disp.print("ERROR= ");
        disp.print(master.state);
      }
    master.writeSingle(1, (unsigned int)button1.flagPress, 5); // запись регистра хранения (светодиод)
    mode++;
    }
  }

  else {
    // запись регистра хранения
    if(master.state != 1) {
      // операция завершена
      if(master.state != 0) errorCount++;
      master.read(1, regTable, 0, 5); // чтение регистров хранения
      mode++;
      cyclCount++; // счетчик циклов
    }
  }
  delay(500);
}

//--------------------------- обработчик прерывания 500 мкс
void timerInterrupt() {
  master.update(); // проверка данных обмена
  button1.scanState(); // обработка сигнала кнопки 1
}

Функционально программа полностью аналогична предыдущему варианту.

 

В предыдущих уроках мы использовали радиальные интерфейсы, а значит, могли объединить в сеть только 2 устройства. В следующем уроке простыми средствами преобразуем радиальный интерфейс UART в магистральный. С помощью него по двух проводной линии связи соединим в локальную сеть 3 платы Ардуино.

Предыдущий урок     Список уроков     Следующий урок

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *