Датчик в цеху был исправен, температура на его табло стояла правильная — а монитор у меня показывал ровно ничего. Плата говорила по RS-485 через MAX485, датчик — тоже, провода в A и B сидели плотно, скорость 9600 совпадала. Разгадка нашлась в листке к датчику, строчкой мелким шрифтом: чётность Even. Я держал 8N1, и восьмой бит каждого кадра уезжал не туда — шина молчала из-за одного бита из одиннадцати. Modbus такой расклад не прощает, и это не каприз, а цена шины, по которой с сотней устройств разговаривают по двум проводам.
Разбирать кадр Modbus RTU я буду руками, без библиотек: протокол короткий, и его полезно один раз прочитать байт за байтом — тогда любая библиотека перестанет быть магией. Считать и сверять контрольную сумму вы уже умеете по уроку про контрольную сумму — здесь та же идея доходит до промышленного стандарта: запрос из восьми байтов с функцией 0x03, ответ с регистром и CRC16 с полиномом 0xA001. Электрику шины я собирал в уроке про связь двух Arduino: RS-485 живёт на тех же TX и RX, только байты летят по витой паре, и слушать их могут сразу все.
Кадр RTU: восемь байтов на запрос, семь на ответ
Запрос на чтение одного holding-регистра занимает восемь байтов и выглядит так: 01 03 00 00 00 01 84 0A. Раскладка простая: 01 — адрес подчинённого (на шине их бывает до 247, и каждый отвечает только своему адресу), 03 — функция чтения holding-регистров, 00 00 — адрес первого регистра (у моего датчика температура живёт в 0x0000), 00 01 — сколько регистров просим, 84 0A — CRC, причём младший байт идёт первым. Ответ приходит в семи байтах: 01 03 02 01 01 78 14 — адрес тот же, функция эхом, 02 означает «два байта груза», 01 01 — сам регистр: 0x0101, это 257, датчик отдаёт десятые доли градуса, значит 25,7 °C.
Между кадрами протокол требует тишины длиной в 3,5 символа. При 9600 бод символ со стартовым и стоповыми битами тянется 11 битов, около 1,15 мс — пауза выходит примерно 4 мс. Услышали паузу длиннее — прежний кадр кончился. Это спасает парсер от слипшихся ответов: он просто ждёт тишину и начинает новый кадр с первого байта. Тот же приём границы сообщения по молчанию я применял в двоичном протоколе с маркером AA 55 — там границу держит маркер, здесь молчание.
Шестнадцать бит регистра — ещё одна промышленная привычка, которую стоит заметить сразу: старший байт первым, а отрицательные температуры датчики отдают либо в дополнительном коде, либо со смещением — у моего минус начинался от 0x8000. Умножение на 0,1 — тоже договорённость производителя: у соседнего прибора масштаб был 0,01. Физику единиц протокол не задаёт, задаёт паспорт прибора — таблицу регистров из листка я держу под рукой всегда. Диапазон адресов тут не мой каприз: 1-247, ноль отдан под широковещательные кадры, самые верхние значения зарезервированы.
| Поле кадра | В запросе | В ответе | Зачем нужно |
|---|---|---|---|
| Адрес устройства | 01 | 01 | до 247 приборов на одной паре проводов |
| Функция | 03 | 03 эхом | чтение holding-регистров |
| Регистр и количество | 00 00, 00 01 | счётчик байтов 02 | адрес 16 бит, два байта на регистр |
| Полезный груз | нет | 01 01 | 257 при масштабе x0,1 — это 25,7 °C |
| CRC16 | 84 0A | 78 14 | младший байт идёт первым |
CRC16: полином 0xA001 и младший байт вперёд
Считается CRC так: регистр заполняют числом 0xFFFF, по очереди ксорят в него каждый байт кадра и восемь раз сдвигают вправо; выпала единица из младшего разряда — ксорят константу 0xA001. Готовое 16-битное число кладут в конец кадра низким байтом вперёд — именно здесь я обжёгся в первый раз: посчитал верно, записал в обратном порядке, и датчик молча выбрасывал каждый кадр. Приёмник проверяет зеркально: прогоняет crc16 по байтам до CRC и сравнивает с двумя байтами из хвоста — не сошлось, кадр в мусор. Откуда 0xA001: это отражённый полином 0x8005, Modbus считает биты с младшего — переписывать ничего не надо, просто держите константу как есть.
Второй сугубо полевой нюанс — MAX485. На двухпроводной шине чип в каждый момент либо говорит, либо слушает, за это отвечают ножки DE и RE: их соединяют вместе и вешают на один пин Arduino. Подняли HIGH — передаём, уронили в LOW — отдаём шину. Забыть вернуть LOW — классика жанра: свой запрос плата видит, а ответ нет, и «датчик молчит» на ровном месте. Кадры без HEX-дампа в этой кухне не отлаживают — читать байты по столбикам я учил в уроке про системы счисления. Если хотите проверить алгоритм руками: прогон байтов 01 03 00 00 00 01 по нему даёт 0x0A84, и младший байт 0x84 уходит в кадр первым.
// MAX485: DI <- D1 (TX), RO -> D0 (RX), DE и RE вместе на пин 7.const int PIN_DE = 7; unsigned int crc16(byte buf[], int dlina) { unsigned int crc = 0xFFFF; for (int i = 0; i < dlina; i++) { crc ^= buf[i]; for (int j = 0; j < 8; j++) { if (crc & 1) crc = (crc >> 1) ^ 0xA001; else crc = crc >> 1; } } return crc;} byte zapros[8] = {0x01, 0x03, 0x00, 0x00, 0x00, 0x01, 0x84, 0x0A};byte otvet[32]; void setup() { Serial.begin(9600); // скорость и чётность — как у датчика pinMode(PIN_DE, OUTPUT); digitalWrite(PIN_DE, LOW); // сразу слушаем шину} void loop() { digitalWrite(PIN_DE, HIGH); // DE вверх — говорим delayMicroseconds(50); Serial.write(zapros, 8); Serial.flush(); // ждём, пока байты уйдут физически digitalWrite(PIN_DE, LOW); // и сразу отдаём шину датчику int prinyato = 0; unsigned long start = millis(); while (prinyato < 7 && millis() - start < 100) { if (Serial.available() > 0) otvet[prinyato++] = Serial.read(); } if (prinyato == 7 && otvet[0] == 0x01 && otvet[1] == 0x03 && otvet[2] == 0x02) { unsigned int raschet = crc16(otvet, 5); unsigned int priem = otvet[5] | (otvet[6] << 8); if (raschet == priem) { int desyatye = otvet[3] * 256 + otvet[4]; Serial.print("temperatura = "); Serial.print(desyatye / 10); Serial.print(","); Serial.print(desyatye % 10); Serial.println(" C"); } else { Serial.println("CRC ne soshla - kadra vybrasyvaem"); } } else { Serial.println("datnik molchit - prover A/B i chetnost"); } delay(2000);}Пройдём по порядку. digitalWrite(PIN_DE, HIGH) открывает передатчик, Serial.write(zapros, 8) вываливает кадр целиком, и Serial.flush() здесь не украшение, а обязательный шаг: без него DE упадёт раньше, чем последний стоповый бит выйдет с провода, и хвост запроса датчик не увидит. Ожидание ответа стоит не на delay, а на таймере millis() с потолком 100 мс: датчики отвечают за 10-20 мс, но обрыв линии не должен вешать скетч навсегда. Принцип неблокирующего ожидания разобран в уроке про millis против delay.
Проверка перед применением ответа трёхступенчатая: пришло ровно семь байтов, адрес и функция совпали с запросом, CRC сошлась — и только тогда байты груза склеиваются в регистр и умножаются на 0,1. Журнал я советую вести как в уроке про диагностику UART: печатать кадр в HEX целиком, даже валидный. Промышленная шина живёт своей жизнью — повторные передачи и шум встречаются чаще, чем на макетке, и без дампа в журнале вы их просто не увидите. Индикаторную вспышку светодиода на валидный кадр я вешаю всегда: одна вспышка за опрос — и по ней видно, жив ли обмен, даже без монитора.
Опрашивать датчик чаще, чем он меняется, — расточительство: температура в цеху живёт минутами, и моё правило — период опроса в разы медленнее скорости изменения величины. Зато и не реже, чем нужно остальным: пока мастер молчит, шина свободна, и на ней спокойно висят ещё полдюжины устройств со своими адресами. Каждый обмен — это 8 байтов в одну сторону и 7 в другую; на 9600 бод полный цикл с одним регистром занимает около 17 мс эфира. При паре опросов в секунду это копейки, при десяти — заметный трафик, а при сотнях приборов на линии расписание опросов превращается в отдельную инженерную задачу.
Тот же скелет читает и два регистра, и восемь: меняется поле количества (00 02 и далее) и длина ожидаемого ответа — счётчик байтов в третьем байте ответа честно подскажет, сколько груза ждать. Соседние регистры датчик отдаёт одним кадром: температура в 0x0000, влажность в 0x0001 — один опрос вместо двух, эфир занят вдвое меньше. Групповое чтение соседних регистров — стандартный приём промышленных опросов, берите его на вооружение сразу: он же экономит паузы по 3,5 символа, которых на длинной цепочке обменов набегает немало.
- Кадр RTU читается по байтам: адрес, функция 0x03, регистры по 16 бит, CRC16 младшим байтом вперёд; запрос — 8 байтов, ответ на один регистр — 7
- CRC16 Modbus: старт 0xFFFF, полином 0xA001, восемь сдвигов на байт; перепутанный порядок байтов CRC выглядит как живая шина без ответов
- Пауза 3,5 символа — около 4 мс при 9600 бод: в ней парсер считает прежнее сообщение законченным
- MAX485: DE и RE вместе на один пин, HIGH — передача, LOW — приём; Serial.flush() перед отдачей шины обязателен
- Масштаб и единицы задаёт паспорт прибора: регистр 0x0101 = 257 = 25,7 °C при масштабе x0,1
- 1. В каком порядке уходят два байта CRC в кадре Modbus RTU?
- 2. Стартовое значение и полином CRC16 в Modbus:
- 3. В ответе датчика один бит груза перевернулся помехой. Что делает корректный мастер?
- 4. Сколько примерно длится пауза 3,5 символа при 9600 бод?
- 5. Как подключают ножки DE и RE модуля MAX485?
Частые вопросы по теме урока
- В каком порядке уходят два байта CRC в кадре Modbus RTU?
- Modbus кладёт CRC низким байтом вперёд: для запроса 01 03 00 00 00 01 это 84 0A, хотя само значение равно 0x0A84. Перепутанный порядок — самая тихая причина молчащей шины. младший байт первым
- Стартовое значение и полином CRC16 в Modbus:
- Регистр заполняют 0xFFFF, а при единице из младшего разряда ксорят 0xA001 — отражённую форму полинома 0x8005, потому что биты считаются с младшего. 0xFFFF и 0xA001
- В ответе датчика один бит груза перевернулся помехой. Что делает корректный мастер?
- CRC существует ровно для этого: расхождение между расчётом и хвостом кадра означает искажение в пути, и такой кадр выбрасывают целиком — опрос придёт снова по расписанию. бракует кадр по несовпадению CRC и ждёт следующего опроса
- Сколько примерно длится пауза 3,5 символа при 9600 бод?
- Символ с стартовым и стоповыми битами — 11 битов, при 9600 бод это около 1,15 мс; умножаем на 3,5 и получаем примерно 4 мс тишины между кадрами. Выше 19200 бод спецификация фиксирует паузы константами: 1,75 мс и 750 мкс. около 4 мс
- Как подключают ножки DE и RE модуля MAX485?
- На двухпроводной шине чип либо говорит, либо слушает. DE и RE завязывают вместе на один пин Arduino, и после передачи обязателен Serial.flush(), чтобы уронить ножку позже последнего стопового бита. соединяют вместе на один пин: HIGH — передача, LOW — приём