Продолжаем изучение внешнего RS485/ModbusRTU блока расширения MA01-AACX2240 компании EBYTE. Сегодня мы разберём устройство Modbus RTU регистров, принципов доступа к ним и получения информации от MA01-AACX2240 и управления его работой.

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

В результате MA01-AACX2240 и все блоки линейки MA0x-xxCXxxx0 станут доступны для практического использования.

Итак...

Официальный вариант работы с MA01-AACX2240


Официальный вариант работы с MA01-AACX2240, на мой взгляд, не выдерживает никакой критики: производитель предлагает скачать проприетарную утилиту для компьютера и использовать её для управления блоком MA01-AACX2240.

По большому счёту это просто несерьёзно — весь смысл блока MA01-AACX2240 в управлении им при помощи Modbus RTU с контроллеров, вариант управления им при помощи компьютерной утилиты — это не более, чем экзотический частный случай, непонятно кому и зачем вообще нужный. (Я тут даже не упоминаю о необходимости устанавливать на свой компьютер непонятно какую программу с неизвестно каким набором функций.)



В общем, это определённо не наш путь — единственный осмысленный вариант использования блока MA01-AACX2240 и прочих блоков из этой серии — это подключение их к контроллеру, чем мы и займёмся далее.

Организация регистров MA01-AACX2240


Начнём мы с теории, поскольку без хорошего понимания того, что и как хранится в памяти MA01-AACX2240 и как это работает, невозможно программировать работу этого блока и вообще как-то его использовать.

Производитель в официальной документации приводит данные по Modbus RTU регистрам MA01-AACX2240, сгруппированные в две таблицы.



Во второй таблице собраны параметры, ассоциированные с работой аналоговых входов. При ближайшем изучении этих таблиц выяснилось, что они содержат множество неточностей и даже ошибок (я обнаружил ошибки в адресах регистров и т. д.). Я здесь привожу уже исправленные варианты таблиц — официальная документация так до сих пор и содержит эти ошибки и неточности.



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

Также можно заметить, что данные в этих таблицах сгруппированы по функциональным признакам, а не по принадлежности к тому или иному типу регистров, поэтому «разгребать» всё это нам придётся вручную, по мере изучения каждого конкретного параметра.

Организация доступа


Теперь немного об организации доступа к Modbus RTU регистрам MA01-AACX2240. Этот блок является Modbus RTU слейвом и его адрес задаётся смешанным хардверно — софтовым способом: часть адреса задаётся переключателями на плате (по умолчанию 31), а вторая часть — программно (по умолчанию 1). Полный адрес MA01-AACX2240 по умолчанию 32 (31 + 1) и его можно изменять в диапазоне от 1 до 247.



Сам принцип установки адреса переключателями прост — это обычное бинарное кодирование. Выше вы можете видеть иллюстрацию из официальной документации, поясняющую этот принцип.

Скорость связи по умолчанию выставлена 9600 и её можно потом изменить в настройках, хотя 9600 — это хороший выбор в большинстве случаев и, скорее всего, вам её не придётся изменять.

Тестовое оборудование


В качестве управляющего мы будем использовать контроллер Lavritech V7.1 Lite на ESP32 с внутренним модулем Lavritech RS485 V1. Можно было бы применить любой другой контроллер с поддержкой RS485, но под рукой был Lavritech V7.1 Lite, поэтому все эксперименты по управлению блоком MA01-AACX2240 мы будем проводить с ним.



Перечень оборудования для проведения тестов:

  • Клеммы на DIN-рейку ABB MA2,5 для подключения сетевых проводов
  • Автоматический выключатель ABB S201 C16 для подачи сетевого питания
  • Блок питания на DIN-рейку MEAN WELL 12В 2А для запитки MA01-AACX2240
  • Контроллер Lavritech V7.1 Lite с модулем Lavritech RS485 V1
  • Блок EBYTE MA01-AACX2240

Далее устанавливаем всё это на стенд и соединяем соответствующим образом. Контроллер Lavritech V7.1 Lite будет запитываться от USB разъёма компьютера, а соединение по интерфейсу RS485 между контроллером и блоком MA01-AACX2240 будет производится при помощи отрезка витой пары на контакты A-A и B-B клеммных колодок обоих устройств.



Программирование


Теперь займёмся непосредственно программированием работы блока MA01-AACX2240 при помощи микроконтроллера, в нашем случае на ESP32. И начнём мы с управления реле. Тут можно ещё заметить, что MA01-AACX2240 позволяет не только увеличить количество реле, подключённых к вашему контроллеру, но и, например, вынести эти реле на значительное расстояние от контроллера (десятки и сотни метров).

Управление реле


За работу с реле отвечают две строки из вышеприведённой таблицы. Первая строка отвечает за текущее состояние реле, а вторая — за их состояние при подаче питания. В этой статье мы будем заниматься только вопросами, связанными с первой строкой, то есть текущим состоянием реле.

Всё сказанное по первой строке можно будет отнести и ко второй — вы сможете легко управлять состоянием реле при включении питания, просто изменив один адрес регистров на другой.



Тут нужно ещё напомнить, что все Modbus RTU регистры делятся на четыре поддиапазона, которые различаются своими свойствами, разрядностью, возможностью записи и т. д.

1. Discrete Inputs. 8-битные регистры, доступные только для чтения (код функции 0x02). Используются в MA01-AACX2240 для представления данных о состоянии DI входов.

2. Coils. 8-битные регистры, доступные для чтения и записи (коды функций 0x01, 0x05, 0x0F). Используются в MA01-AACX2240 для обслуживания DO (реле).

3. Input Registers. 16-битные регистры, доступные только для чтения (код функции 0x04). Используются в MA01-AACX2240 для предоставления информации о состоянии AI.

4. Holding Registers. 16-битные регистры, доступные для чтения и записи (коды функций 0x03, 0x06, 0x10). Используются в MA01-AACX2240 для всего остального (хранения настроек, текущих значений и т. п.).

Как вы уже поняли, в этой статье мы будем разбирать проблематику второго пункта «Coils», поскольку он отвечает за работу с реле.

Здесь используются четыре 8-битных регистра

0х0000 (00000)
0х0001 (00001)
0х0002 (00002)
0х0003 (00003)

для хранения данных о текущем состоянии каждого из четырёх реле на борту MA01-AACX2240. Мы можем считывать эти регистры и записывать их (тем самым меняя состояние реле).

Скетч чтения состояния реле


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

Исходные данные:

Поскольку управляющий контроллер Lavritech V7.1 Lite имеет в своей основе ESP32, то для работы с интерфейсом RS485 мы будем использовать «хардверный» Serial1.

#define RS485_SerialNum  1

Далее определяем RX и TX пины интерфейса RS485 на Lavritech V7.1 Lite (на вашем контроллере значения GPIO могут быть другими)

#define RS485_RX  16
#define RS485_TX  17

В нашем случае управление приёмом/передачей осуществляется автоматически, поэтому пин DE не используется

//#define RS485_DE  -1

и соответствующие пре- и пост- функции остаются пустыми (нам не нужно программно переключать направление передачи).

void preTransmission()  {;}
void postTransmission() {;}

Затем задаём значение скорости передачи (9600 выставлено по умолчанию в MA01-AACX2240).

#define MODBUS_BAUD 9600

Номер слейва нашего блока MA01-AACX2240 (32 по умолчанию).

#define MODBUS_ID   32

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

#define REQ_DELAY   100

Далее определяем адреса регистров всех четырёх реле MA01-AACX2240.

#define ADR_COILS_DO0 0
#define ADR_COILS_DO1 1
#define ADR_COILS_DO2 2
#define ADR_COILS_DO3 3

Полный код скетча, который реализует чтения регистров состояния реле на блоке MA01-AACX2240:
/*
  Lavritech V7.1 Lite (RS485 V1)
  EBYTE MA01-AACX2240
  Modbus RTU Read DO example
*/

#include <Wire.h>
#include <ModbusMaster.h>

#define RS485_SerialNum  1

#define RS485_RX  16
#define RS485_TX  17
//#define RS485_DE  -1

#define MODBUS_BAUD 9600
#define MODBUS_ID   32
#define REQ_DELAY   100

#define ADR_COILS_DO0 0
#define ADR_COILS_DO1 1
#define ADR_COILS_DO2 2
#define ADR_COILS_DO3 3

int cntOk = 0;
int cntEr = 0;
uint8_t result;

HardwareSerial RsSerial(RS485_SerialNum);

ModbusMaster RS;

byte do4[4] = {0, 0, 0, 0,};

void setup() {
  Serial.begin(115200);
  Serial.println();
  Serial.println(F("Start MA01-AACX2240 Read DO example..."));
  
  RsSerial.begin(MODBUS_BAUD, SERIAL_8N1, RS485_RX, RS485_TX);

  RS.begin(MODBUS_ID, RsSerial);
  RS.preTransmission(preTransmission);
  RS.postTransmission(postTransmission);
} // setup

void preTransmission()  {;}
void postTransmission() {;}

word readCoils1(word shift) {
  word res = 0;
  
  result = RS.readCoils(shift, 1);
  if (result == RS.ku8MBSuccess) {
    res = RS.getResponseBuffer(0);
    cntOk++;
  } else {
    cntEr++;
  }
  delay(REQ_DELAY);

  return res;
}

void displayInfo() {
  Serial.print(F(" ok:")); Serial.print(cntOk);
  Serial.print(F("/")); Serial.print(cntEr);
  
  Serial.print(F(" DO:"));
  Serial.print(do4[0]);
  Serial.print(do4[1]);
  Serial.print(do4[2]);
  Serial.print(do4[3]);
  
  Serial.println();
}

void loop() {
  do4[0] = readCoils1(ADR_COILS_DO0);
  do4[1] = readCoils1(ADR_COILS_DO1);
  do4[2] = readCoils1(ADR_COILS_DO2);
  do4[3] = readCoils1(ADR_COILS_DO3);

  displayInfo();
  delay(1000);
} // loop

Здесь чтением значений регистров занимается функция readCoils(shift, 1)

  result = RS.readCoils(shift, 1);

где ей в качестве параметров передаётся адрес регистра (смещение в поддиапазоне Coils) и «1» в качестве инструкции прочитать значение одного регистра.

Таким образом производится четыре запроса, по одному на каждый регистр.

  do4[0] = readCoils1(ADR_COILS_DO0);
  do4[1] = readCoils1(ADR_COILS_DO1);
  do4[2] = readCoils1(ADR_COILS_DO2);
  do4[3] = readCoils1(ADR_COILS_DO3);

Затем результат работы нашего скетча выводится в Serial:



Здесь видно, что реле #0 и реле #3 включены, а реле #1 и #2 — выключены.

Также видно, что все запросы проходят успешно и нет ни одной ошибки проведения операций чтения регистров DO с блока MA01-AACX2240. Также обратите внимание, что количество запросов возрастает на 4 за один цикл. Это непроизводительно и неэффективно — мы делаем 4 запроса для получения состояния всех реле.

Функция readCoils() позволяет читать сразу несколько последовательно расположенных регистров и можно получить все 4 значения за один запрос. Этот вариант скетча мы разберём далее.

Скетч чтения состояния 4-х реле за 1 запрос


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

Вызываем функцию чтения состояния 4-х реле за 1 запрос, передавая ей в качестве параметра адрес реле #0 (остальные три идут последовательно за ним).

readCoils4(ADR_COILS_DO0);

Код самой функции чтения состояния 4-х реле за 1 запрос. Здесь также последовательно читаются 4 байта из приёмного буфера.

void readCoils4(word shift) {
  result = RS.readCoils(shift, 4);
  if (result == RS.ku8MBSuccess) {
    do4[0] = bitRead(RS.getResponseBuffer(0), 0);
    do4[1] = bitRead(RS.getResponseBuffer(0), 1);
    do4[2] = bitRead(RS.getResponseBuffer(0), 2);
    do4[3] = bitRead(RS.getResponseBuffer(0), 3);
    cntOk++;
  } else {
    cntEr++;
  }
  delay(REQ_DELAY);
}

Результат работы модернизированного скетча — как вы видите, результат тот же, только для его получения используется всего один запрос, вместо четырёх, что гораздо более эффективно и правильно.



Скетч управления состоянием реле


Получение информации о состоянии реле — это хорошо, но не менее интересно научиться изменять состояние реле — это позволит нам управлять подключённым к MA01-AACX2240 оборудованием.

Для этого модернизируем скетч и добавляем в него функцию writeCoils1().

void writeCoils1(word shift, byte val) {
  result = RS.writeSingleCoil(shift, val);
  if (result == RS.ku8MBSuccess) {
    cntOk++;
  } else {
    cntEr++;
  }
  delay(REQ_DELAY);
}

А также её вызов для изменения состояния реле (в данном случае мы включаем все четыре реле).

  writeCoils1(ADR_COILS_DO0, 1);
  writeCoils1(ADR_COILS_DO1, 1);
  writeCoils1(ADR_COILS_DO2, 1);
  writeCoils1(ADR_COILS_DO3, 1);

Полный код скетча управления реле.

/*
  Lavritech V7.1 Lite (RS485 V1)
  EBYTE MA01-AACX2240
  Modbus RTU Write DO example
*/

#include <Wire.h>
#include <ModbusMaster.h>

#define RS485_SerialNum  1

#define RS485_RX  16
#define RS485_TX  17
//#define RS485_DE  -1

#define MODBUS_BAUD 9600
#define MODBUS_ID   32
#define REQ_DELAY   100

#define ADR_COILS_DO0 0
#define ADR_COILS_DO1 1
#define ADR_COILS_DO2 2
#define ADR_COILS_DO3 3

int cntOk = 0;
int cntEr = 0;
uint8_t result;

HardwareSerial RsSerial(RS485_SerialNum);

ModbusMaster RS;

byte do4[4] = {0, 0, 0, 0,};

void setup() {
  Serial.begin(115200);
  Serial.println();
  Serial.println(F("Start MA01-AACX2240 Write DO example..."));
  
  RsSerial.begin(MODBUS_BAUD, SERIAL_8N1, RS485_RX, RS485_TX);

  RS.begin(MODBUS_ID, RsSerial);
  RS.preTransmission(preTransmission);
  RS.postTransmission(postTransmission);
} // setup

void preTransmission()  {;}
void postTransmission() {;}

void writeCoils1(word shift, byte val) {
  result = RS.writeSingleCoil(shift, val);
  if (result == RS.ku8MBSuccess) {
    cntOk++;
  } else {
    cntEr++;
  }
  delay(REQ_DELAY);
}

void readCoils4(word shift) {
  result = RS.readCoils(shift, 4);
  if (result == RS.ku8MBSuccess) {
    do4[0] = bitRead(RS.getResponseBuffer(0), 0);
    do4[1] = bitRead(RS.getResponseBuffer(0), 1);
    do4[2] = bitRead(RS.getResponseBuffer(0), 2);
    do4[3] = bitRead(RS.getResponseBuffer(0), 3);
    cntOk++;
  } else {
    cntEr++;
  }
  delay(REQ_DELAY);
}

void displayInfo() {
  Serial.print(F(" ok:")); Serial.print(cntOk);
  Serial.print(F("/")); Serial.print(cntEr);
  
  Serial.print(F(" DO:"));
  Serial.print(do4[0]);
  Serial.print(do4[1]);
  Serial.print(do4[2]);
  Serial.print(do4[3]);
  
  Serial.println();
} // setup

void loop() {
  writeCoils1(ADR_COILS_DO0, 1);
  writeCoils1(ADR_COILS_DO1, 1);
  writeCoils1(ADR_COILS_DO2, 1);
  writeCoils1(ADR_COILS_DO3, 1);

  readCoils4(ADR_COILS_DO0);

  displayInfo();
  delay(1000);
} // loop

Вот результат работы нашего скетча по изменению состояния реле:



Здесь видно, что в этом варианте скетча нам приходится делать 5 запросов за один цикл (4 на запись и 1 на чтение), что тоже не очень эффективно.

Скетч изменения состояния 4-х реле за 1 запрос


Изменим немного предыдущий скетч, чтобы операция управления 4-я реле требовала не четыре, а всего один запрос. (Я снова не буду дублировать предыдущий скетч, приведу только его дополнения.)

Для этого добавляем функцию writeCoils4(), которая оперирует изменением состояния сразу 4-х реле.

void writeCoils4(word shift, word val) {
  RS.setTransmitBuffer(0, val);
  result = RS.writeMultipleCoils(shift, 4);
  if (result == RS.ku8MBSuccess) {
    cntOk++;
  } else {
    cntEr++;
  }
  delay(REQ_DELAY);
}

И её вызов.

writeCoils4(ADR_COILS_DO0, DO_1001);

Здесь требуются некоторые пояснения по механике работы функции writeCoils4(). Она использует библиотечную функцию writeMultipleCoils(), которая позволяет записывать сразу несколько последовательно расположенных регистров. Причём в качестве параметров ей передаётся начальный адрес и количество записываемых регистров.

А вот сами значения регистров берутся из внутреннего буфера (библиотеки) и устанавливаются функцией setTransmitBuffer().

  RS.setTransmitBuffer(0, val);

Другими словами, прежде чем использовать функцию writeMultipleCoils(), нам нужно вызвать функцию setTransmitBuffer() и передать ей значения регистров в виде числа (используется двоичное кодирование).

Для упрощения группового манипулирования состоянием реле в скетче можно определить специальные константы, которые потом просто подставлять в функцию writeCoils4().

//         4321
#define DO_0000 0
#define DO_0001 1
#define DO_0010 2
#define DO_0100 4
#define DO_1000 8
#define DO_0011 3
#define DO_0110 6
#define DO_1100 12
#define DO_1001 9
#define DO_1111 15

В нашем примере это

writeCoils4(ADR_COILS_DO0, DO_1001);

константа DO_1001, то есть число 9.

Для особо пытливых читателей и лучшего понимания, приведу здесь ещё определения двух вышерассмотренных функций из библиотеки ModbusMaster.

//setTransmitBuffer(uint8_t u8Index, uint16_t u16Value)
//writeMultipleCoils(uint16_t u16WriteAddress, uint16_t u16BitQty)

В итоге мы получаем тот же самый результат, только для его достижения нам уже требуется сделать всего 2 запроса (на запись и чтение), а не 5 как в предыдущем случае.



Что, как вы сами понимаете, благоприятно сказывается на загрузке Modbus интерфейса и работе всей системы.

Заключение


В этой статье мы познакомились с организацией регистров Modbus RTU интерфейса блока EBYTE MA01-AACX2240 и научились программировать работу его реле. В процентном отношении это примерно 5-10 процентов от общего количества информации о программировании MA01-AACX2240. Незатронутыми оказались вопросы работы с ещё тремя группами регистров, цифровыми и аналоговыми входами, настройками MA01-AACX2240 и т. д. и т. п., так что нам будет чем заняться в следующих статьях.

Комментарии (0)