Протокол Modbus для RS-485

В устройствах ZETSENSOR реализован открытый коммуникационный протокол Modbus со стандартным набором команд:

  • 0x3 — для чтения регистров с настройками и текущего измеренного значения;
  • 0x4 – для чтения измеренных потоковых данных;
  • 0x6 – для чтения описателей структур;
  • 0x10 – для записи регистров с настройками (для изменения настроек).

Контроллеры на шине Modbus взаимодействуют, используя master-slave модель, основанную на транзакциях, состоящих из запроса и ответа. Каждый датчик имеет свой уникальный адрес в сети. Для master-устройств (преобразователей интерфейсов) этот адрес — 1. Для slave-устройств этот адрес может принимать любое значение от 2 до 63. При передаче данных используется порядок байт little-endian.
Для чтения регистров с настройками и текущего измеренного значения используется команда 0x3. Формат запроса такой командой представлен в Таблица 1.

Таблица 1. Структура запроса командой 0x3
Тип поляРазмер (в байтах)Номер байта

Адрес устройства

10

Команда запроса

11

Адрес регистра

22, 3

Количество данных для чтения (в словах)

24, 5

Контрольная сумма (CRC16)

26, 7

Формат ответа на запрос с командой 0x3 представлен в Таблица 2.

Таблица 2. Структура ответа на команду 0x3
Тип поляРазмер (в байтах)Номер байта

Адрес устройства

10

Команда запроса

11

Размер считанных данных (в байтах)

12

Данные

N3 — N + 2

Контрольная сумма (CRC16)

2N + 3, N + 4

Чтение значений регистров для устройства с адресом 0x4 по адресу регистра 0x0 размером 120 слов (240 байт) выглядит так:

  • запрос:
    0x04 0x03 0x00 0x00 0x00 0x78 0x45 0xbd
  • ответ:
    0x04 0x03 0xf0 0xc0 0x20 0x00 0x58 0x00 0x00 0xe5 0x4f 0x00 0x03 0x00 0x00 0x03
    0xdf 0x52 0x45 0x23 0x12 0x2b 0x17 0xbd 0xa8 0x55 0x66 0xd8 0x98 0x4e 0x6d 0x00
    0x04 0x00 0x00 0x00 0x4c 0x00 0x4d 0x00 0x00 0xc4 0x3b 0x44 0x64 0xc3 0xdd 0x00
    0x00 0x42 0xfa 0x00 0xf2 0x6a 0x00 0x00 0x68 0x00 0x00 0x45 0x5a 0x37 0x54 0x31
    0x30 0x00 0x30 0x54 0x00 0x30 0x37 0x30 0x31 0x00 0x00 0x00 0x00 0x00 0x00 0x00
    0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x44 0x64 0xc3 0xdd 0x44
    0x64 0x43 0xdd 0x00 0x00 0x3f 0x80 0x00 0x00 0x3f 0x80 0xc5 0xac 0x37 0x27 0xc0
    0x3c 0x00 0x59 0x00 0x00 0x2e 0xbc 0x00 0x01 0x00 0x00 0x00 0x02 0x00 0x00 0x00
    0x00 0x00 0x00 0xcc 0xcd 0x3d 0xcc 0x00 0xf2 0x6a 0x00 0x00 0x68 0x00 0x00 0x20
    0xe5 0xee 0xef 0xe5 0xeb 0x31 0x5f 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
    0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0xa0 0x14 0x00 0x74 0x00
    0x00 0x09 0x46 0x00 0x01 0x00 0x00 0x00 0x00 0x42 0x48 0xf8 0xa0 0x3f 0x61 0xa0
    0x10 0x00 0x76 0x00 0x00 0xf1 0x77 0x00 0x00 0x00 0x00 0x00 0x00 0x3f 0x80 0xa0
    0x10 0x00 0x77 0x00 0x00 0x2d 0x64 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0xa0
    0x14 0x00 0x47 0x00 0x00 0x94 0xbd 0xe1 0x00 0x00 0x00 0x00 0x01 0x00 0x00 0x0f
    0xd5 0x57 0x55 0x54 0x02

Для запроса текущего измеренного значения в качестве адреса регистра используется адрес поля value структуры CHANNEL_PAR.

Для чтения описателей структур используется команда 0x6. Формат запросов и ответов такой командой аналогичен команде 0x3. Для чтения измеренных потоковых данных используется команда 0x4. Формат запросов и ответов такой командой аналогичен команде 0x3. Для запроса потоковых данных в качестве адреса регистра используется адрес поля value структуры CHANNEL_PAR.

Для записи регистров с настройками (для изменения настроек) используется команда (0x10). Формат запроса такой командой представлен в Таблица 3.

Таблица 3. Структура запроса командой 0x10
Тип поляРазмер (в байтах)Номер байта

Адрес устройства

10

Команда запроса

11

Адрес регистра

22, 3

Количество данных для записи (в словах)

24, 5

Количество данных для записи (в байтах)

16

Данные

N7 — N + 6

Контрольная сумма (CRC16)

2N + 7, N + 8

Формат ответа на запрос с командой 0x10 представлен в Таблица 4.

Таблица 4. Структура ответа на команду 0x10
Тип поляРазмер (в байтах)Номер байта

Адрес устройства

10

Команда запроса

11

Адрес регистра

22, 3

Количество записанных данных (в словах)

24, 5

Контрольная сумма (CRC16)

26, 7

Особенности при изменении настроек в ZETSENSOR

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

  1. в сети находится 2 датчика с одинаковыми адресами. При попытке записи данных в один датчик, те же самые данные запишутся и на другой датчик, что приведёт к порче настроек датчика;
  2. внешнее устройство начало записывать данные на датчик. Но ввиду каких-либо причин, связь случайно оборвалась (отсоединили устройство, пропало питание и т.д.). Таким образом, в память датчика могут быть записаны некорректные данные.

Возникает задача, построить алгоритм безопасной записи данных на датчик. Алгоритм должен решить следующие проблемы:

  1. Датчик должен принимать только те данные, которые предназначены для него. Для идентификации принимаемых данных можно использовать 64-битный серийный номер датчика.
  2. Использование принятых данных разрешено только после принятия всех данных и проверки корректности этих данных.

Первая задача решается путём добавления контрольной суммы CRC16 в каждую из подструктур основной структуры датчика. Благодаря этой контрольной суммы мы всегда можем проверить корректность текущих данных и принадлежность этих данных конкретно этому устройству.

Для решения второй задачи необходимо организовать целостность передаваемых данных. Для этого мы будем использовать поле write_enable структуры. Когда данные в структуре корректны это поле имеет статус «Корректные данные». Перед записью данных мы должны выставить статус «Начало передачи данных», обозначающий то, что значения данной структуры будут изменены. Далее записать данные. (После получения первого пакета, состояние структуры изменяется в состояние «Активная передача данных»). После окончания записи данных выставить поле write_enable в состояние «Передача данных завершена». Как только выставлен статус «Передача данных завершена», датчик начинает проверять корректность полученных данных и в случае успеха записывает их в flash- память. Далее выставляется статус «Корректные данные». Расшифровка статуса write_enable отражена в Таблица 5.

Таблица 5. Расшифровка статуса write_enable
Статус структуры (значение поля write_enable)Определение статуса

0 – «Корректные данные»

Структура содержит полностью корректные данные, эти данные можно использовать и обрабатывать датчиком

1 – «Начало передачи данных»

В данном состоянии запрещается использовать данные, находящиеся в структуре датчика. Только в этом состоянии можно начинать передавать данные датчику

2 – «Активная передача данных»

В данном состоянии запрещается использовать данные, находящиеся в структуре датчика

3 – «Конец передачи данных»

В данном состоянии датчик должен проверить все переданные ему данные. И сбросить своё состояние в состояние «Корректные данные»

Изменения статусов возможно только в следующем порядке: «Корректные данные» → «Начало передачи данных» → «Активная передача данных» → «Конец передачи данных» → «Корректные данные».
Примечание:

  1. Необходимость выставления статуса «Начало передачи данных» заключается в обозначении того, что данные изменяются извне, а не самим датчиком.
  2. Необходимость выставления статуса «Конец передачи данных» заключается в том, чтобы датчик обработал полученную информацию, а не сразу перешёл в состояние «Корректные данные». Статус «Корректные данные» выставляет только датчик (выставление данного статуса извне запрещено прошивкой устройства).
  3. Возможен вариант выставления статуса «Начало передачи данных» но сами данные не были переданы (в связи с обрывом соединения или каких-либо других причин). В данном случае статус не сможет сброситься в состояние «Корректные данные». Данную ситуацию должен обрабатывать компьютер. Так как датчик по определению не может определить были переданы все данные или нет, обрыв соединения это или задержки передачи данных. Поэтому для однозначности эту ситуацию будем обрабатывать компьютером.
  4. Возможен вариант что статус «Начало передачи данных» не выставился по каким-либо причинам. В данном случае все последующие данные будут игнорироваться.
  5. Компьютеру разрешается выставлять только статусы «Начало передачи данных» или «Конец передачи данных».

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

  1. Выставить статус «Начало передачи данных», в структуре, данные которой изменяются.
  2. Начать передачу данных. Для оптимальности передачи данных, передавать нужно не всю структуру, а только изменённые поля.
  3. Передать пересчитанное значение CRC16 для передаваемой структуры.
  4. Выставить статус «Конец передачи данных», в структуре, данные которой изменяются.

Для расчета контрольной суммы CRC16 используется следующий алгоритм с начальным значением для расчета 0xffff:
static const unsigned char g_uiCRC16tableHi[] = {
  0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x8
0, 0x41, 0x00, 0xC1, 0x81,
  0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC
1, 0x81, 0x40, 0x01, 0xC0,
  0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x0
0, 0xC1, 0x81, 0x40, 0x01,
  0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x4
1, 0x01, 0xC0, 0x80, 0x41,
  0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x8
1, 0x40, 0x00, 0xC1, 0x81,
  0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC
0, 0x80, 0x41, 0x01, 0xC0,
  0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x0
1, 0xC0, 0x80, 0x41, 0x01,
  0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x4
1, 0x00, 0xC1, 0x81, 0x40,
  0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x8
0, 0x41, 0x00, 0xC1, 0x81,
  0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC
1, 0x81, 0x40, 0x01, 0xC0,
  0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x0
0, 0xC1, 0x81, 0x40, 0x01,
  0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x4
0, 0x01, 0xC0, 0x80, 0x41,
  0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x8
0, 0x41, 0x00, 0xC1, 0x81,
  0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC
1, 0x81, 0x40, 0x01, 0xC0,
  0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x0
1, 0xC0, 0x80, 0x41, 0x01,
  0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x4
0, 0x01, 0xC0, 0x80, 0x41,
  0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x8
0, 0x41, 0x00, 0xC1, 0x81,
  0x40
};
static const unsigned char g_uiCRC16tableLo[] = {
  0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06, 0x0
7, 0xC7, 0x05, 0xC5, 0xC4,
  0x04, 0xCC, 0x0C, 0x0D, 0xCD, 0x0F, 0xCF, 0xCE, 0x0E, 0x0A, 0xC
A, 0xCB, 0x0B, 0xC9, 0x09,
  0x08, 0xC8, 0xD8, 0x18, 0x19, 0xD9, 0x1B, 0xDB, 0xDA, 0x1A, 0x1
E, 0xDE, 0xDF, 0x1F, 0xDD,
  0x1D, 0x1C, 0xDC, 0x14, 0xD4, 0xD5, 0x15, 0xD7, 0x17, 0x16, 0xD
6, 0xD2, 0x12, 0x13, 0xD3,
  0x11, 0xD1, 0xD0, 0x10, 0xF0, 0x30, 0x31, 0xF1, 0x33, 0xF3, 0xF
2, 0x32, 0x36, 0xF6, 0xF7,
  0x37, 0xF5, 0x35, 0x34, 0xF4, 0x3C, 0xFC, 0xFD, 0x3D, 0xFF, 0x3
F, 0x3E, 0xFE, 0xFA, 0x3A,
  0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38, 0x28, 0xE8, 0xE9, 0x29, 0xE
B, 0x2B, 0x2A, 0xEA, 0xEE,
  0x2E, 0x2F, 0xEF, 0x2D, 0xED, 0xEC, 0x2C, 0xE4, 0x24, 0x25, 0xE
5, 0x27, 0xE7, 0xE6, 0x26,
  0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21, 0x20, 0xE0, 0xA0, 0x60, 0x6
1, 0xA1, 0x63, 0xA3, 0xA2,
  0x62, 0x66, 0xA6, 0xA7, 0x67, 0xA5, 0x65, 0x64, 0xA4, 0x6C, 0xA
C, 0xAD, 0x6D, 0xAF, 0x6F,
  0x6E, 0xAE, 0xAA, 0x6A, 0x6B, 0xAB, 0x69, 0xA9, 0xA8, 0x68, 0x7
8, 0xB8, 0xB9, 0x79, 0xBB,
  0x7B, 0x7A, 0xBA, 0xBE, 0x7E, 0x7F, 0xBF, 0x7D, 0xBD, 0xBC, 0x7
C, 0xB4, 0x74, 0x75, 0xB5,
  0x77, 0xB7, 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71, 0x7
0, 0xB0, 0x50, 0x90, 0x91,
  0x51, 0x93, 0x53, 0x52, 0x92, 0x96, 0x56, 0x57, 0x97, 0x55, 0x9
5, 0x94, 0x54, 0x9C, 0x5C,
  0x5D, 0x9D, 0x5F, 0x9F, 0x9E, 0x5E, 0x5A, 0x9A, 0x9B, 0x5B, 0x9
9, 0x59, 0x58, 0x98, 0x88,
  0x48, 0x49, 0x89, 0x4B, 0x8B, 0x8A, 0x4A, 0x4E, 0x8E, 0x8F, 0x4
F, 0x8D, 0x4D, 0x4C, 0x8C,
  0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46, 0x86, 0x82, 0x42, 0x4
3, 0x83, 0x41, 0x81, 0x80, 0x40
};

//———————————————————————
unsigned short CRC16(unsigned short usCRC, const void *pBuffer,
unsigned int uiSize)
{
  const unsigned char* pucBuffer =
    reinterpret_cast<const unsigned char*>(pBuffer);
  unsigned char ucHi = (usCRC >> 8) & 0xFF;
  unsigned char ucLo = usCRC & 0xFF;
  unsigned uIndex;
  while (uiSize—)
  {

    uIndex = ucHi ^ *pucBuffer++;
    ucHi = ucLo ^ g_uiCRC16tableHi[uIndex];
    ucLo = g_uiCRC16tableLo[uIndex];
  }

  return (ucHi << 8 | ucLo);
}

//———————————————————————