198в. Мини-проект: модуль ввода-вывода на Modbus RTU
Этот прибор — рабочая лошадка промышленной автоматики: модуль ввода-вывода. Четыре входа под «сухой контакт», четыре выхода на ключи, и всем этим по двухпроводной линии командует ведущий — контроллер или компьютер. Протокол — Modbus RTU: ему не один десяток лет, а он всё ещё повсюду, потому что для него достаточно UART и дешёвого приёмопередатчика, обмен «запрос — ответ» предсказуем до микросекунды, и его понимает практически любое промышленное оборудование.
Раздел 16.8 уже собрал ведомого с регистрами хранения. Здесь из того же кирпича строится законченный прибор: карта регистров с битовыми областями, битовые функции с упаковкой в байты, ответ-эхо, и главное — поведение прибора, а не примера: фильтр дребезга на входах и сторож связи, который переводит выходы в безопасное состояние, когда ведущий пропал.
Как и в разделах 196–198, логика (mbio.c) не знает про регистры
микроконтроллера и прогнана на компьютере: по ней работал ведущий —
Python-скрипт, кадры шли байт в байт, как по линии. Файлы —
_verify/mbus198v/: mbio.h, mbio.c (логика), slave_pipe.c
(модель-ведомый для прогона), slave_host.c (опыты), master.py
(ведущий). Сверяет _verify/mbus198v_check.py.
Как это называется в документации
Оригинал Перевод coil катушка: битовый выход — имя осталось от катушки реле discrete input дискретный вход: битовый вход, только чтение input register регистр ввода: 16-битное слово, только чтение function code код функции: что сделать с данными slave address, unit ID адрес ведомого на шине byte count счётчик байт: длина поля данных в кадре dry contact «сухой контакт»: свободная от напряжения пара клемм communication watchdog, fail-safe сторож связи и безопасное состояние выходов
198в.1 Что делает прибор
| Функция | Как |
|---|---|
| читать 4 входа | «сухой контакт» на клеммах, подтяжка вверх, фильтр 20 мс |
| управлять 4 выходами | транзисторные ключи (раздел 52), катушки Modbus |
| отвечать ведущему | Modbus RTU, 9600 бод, символ 8E1, адрес 1 |
| настраиваться по шине | фильтр, сторож, безопасная маска — регистры хранения |
| переживать обрыв линии | сторож связи: 5,0 с без опроса — выходы в безопасную маску |
| признаваться в помехах | счётчик кадров с битой CRC — отдельный регистр |
Входные клеммы рассчитаны на «сухой контакт» — кнопку, концевик, реле другого прибора: замкнуто на общий провод — ноль, разомкнуто — подтяжка держит единицу. Последовательные резисторы защищают выводы от того, что приходит с клемм из шкафа (раздел 62). Линия — RS-485: дифференциальная пара, десятки приборов, до километра; терминаторы, смещение и вывод DE — в разделе 89.4, здесь физика линии не главное — модель обменивается кадрами напрямую.
198в.2 Кадр RTU и CRC16
Кадр Modbus RTU устроен из четырёх полей: адрес ведомого (1…247, 0 — широковещание), код функции, данные и CRC. Вот эталонный запрос «прочитать два регистра хранения с нулевого» — все дальнейшие кадры собраны точно так же:
01 |
03 |
00 00 |
00 02 |
C4 0B |
|---|---|---|---|---|
| адрес 1 | функция 03 | первый регистр | сколько | CRC |
Числа в полях данных идут старшим байтом вперёд (раздел 126), а вот CRC — наоборот, младшим: это два разных правила в одном кадре, и путаница между ними — самая частая причина «CRC не сходится».
CRC-16/MODBUS из раздела 43: полином 0xA001 (отражённый 0x8005),
начальное значение 0xFFFF:
uint16_t mbio_crc(const uint8_t *p, size_t n)
{
uint16_t c = 0xFFFF; /* начальное значение */
while (n--) {
int i;
c ^= *p++; /* байт — в младшую половину */
for (i = 0; i < 8; i++)
c = (c & 1) ? (uint16_t)((c >> 1) ^ 0xA001) /* отражённый 0x8005 */
: (uint16_t)(c >> 1);
}
return c;
}
Проверка реализации — прогоном: строка 123456789 даёт 0x4B37 —
контрольное значение CRC-16/MODBUS; CRC эталонного кадра выше — C4 0B;
кадр записи в регистры 17–18 из опыта раздела 16.8 даёт те же байты
A3 74, что и там. Ведущий в этом проекте считает CRC своей реализацией
на Python — и все кадры прогона сошлись с C-реализацией байт в байт.
198в.3 Тишина 3,5 символа
В кадре RTU нет ни стартового байта, ни поля длины — кадры разделяет тишина не короче 3,5 символа. Символ здесь из 11 бит: старт, 8 бит данных, чётность, стоп (раздел 16.8). Отсюда микросекунды:
| Скорость | Символ | Конец кадра, 3,5 симв. | Разрыв внутри, 1,5 симв. |
|---|---|---|---|
| 9600 бод | 1146 мкс | 4011 мкс | 1719 мкс |
| 115 200 бод | 96 мкс | 335 мкс | 144 мкс |
| выше 19 200 бод | — | принято 1750 мкс | принято 750 мкс |
На 9600 конец кадра — это чуть больше 4 мс молчания; на 115 200 —
всего 335 мкс, и любая заминка компьютера-ведущего длиннее, поэтому выше
19 200 бод паузы берут постоянными, а не считают по символам. Ловят
тишину так, как разобрано в разделе 16.8: таймер в одноимпульсном режиме,
каждый принятый байт обнуляет счёт; дотикал — кадр закончен. На 9600 при
тике 1 мкс это ARR = 4010.
⚠️ Флаг
IDLE— не то же самое. Он взводится уже после тишины в один символ (раздел 16.5; в HAL —ReceiveToIdle, раздел 5а.5). Для конца кадра этого мало: разрыв внутри кадра до 1,5 символа протокол разрешает, иIDLEразрежет такой кадр надвое. С таймером на 3,5 символа заминка до 1719 мкс кадр не рвёт.
198в.4 Карта регистров модуля
Данные Modbus живут в четырёх областях: дискретные входы, катушки, регистры ввода и регистры хранения. Модулю хватает трёх — и по каждой области своя таблица адресов. Это и есть паспорт прибора: ведущему, чтобы работать с модулем, нужна только она.
| Область | Адрес | Что там | Функции |
|---|---|---|---|
| дискретные входы | 0…3 | IN1…IN4 после фильтра | 02 — чтение |
| катушки | 0…3 | OUT1…OUT4 | 01 — чтение, 05 — запись |
| регистры хранения | 0 | фильтр входов, мс (заводское 20) | 03 — чтение, 16 — запись |
| 1 | сторож связи, 0,1 с (заводское 50 — это 5,0 с; 0 — выключен) | ||
| 2 | безопасная маска выходов, биты 0…3 (заводское 0) | ||
| 3 | счётчик кадров с неверной CRC |
Адреса в каждой области свои и начинаются с нуля: «катушка 1» и «регистр хранения 1» — разные вещи, различает их код функции.
⚠️ В паспортах приборов нумерация часто с единицы. В кадре адрес отсчитывается с нуля, а таблицы в документации нередко нумеруют точки с первой. Пишешь по паспорту в «регистр 1», а меняется нулевой — минус один где-то потерялся. Сверять надо и это, и область: одни производители пишут адрес в кадре, другие — номер точки.
198в.5 Запрос → ответ
Разбор кадра — одна переносимая функция mbio_frame: проверить CRC и
адрес, разобрать поля, собрать ответ прямо в буфер передачи (24 байта
стека). Каркас — как mb_process в разделе 16.8; новое здесь — битовые
функции. Ответ на «прочитать N бит» пакует биты в байты, младший бит —
первая точка, а перед ними идёт счётчик байт:
case 1: /* чтение катушек */
case 2: /* чтение дискретных входов */
if (broadcast)
return 0; /* чтение всем сразу — бессмыслица */
if (n != 8 || count < 1 || count > 2000) {
k = mb_ex(resp, m->addr, fn, MBIO_EX_VALUE);
} else if ((uint32_t)first + count > MBIO_NIO) {
k = mb_ex(resp, m->addr, fn, MBIO_EX_ADDR);
} else {
uint8_t bits = (uint8_t)((fn == 1 ? m->out : m->in_state) >> first);
resp[0] = m->addr;
resp[1] = fn;
resp[2] = (uint8_t)((count + 7) / 8); /* счётчик байт */
resp[3] = (uint8_t)(bits & ((1u << count) - 1u));
k = mb_done(resp, 4);
}
break;
Предел 2000 бит — из кадра в 256 байт: 2000 бит — это 250 байт данных, и ответ занимает ровно 255 байт, как и чтение 125 регистров функцией 03 или запись 123 регистров функцией 16.
Запись одной катушки — функция 05 — с двумя особенностями. Значений
всего два, и кодируются они не 0 и 1, а 0x0000 и 0xFF00 — всё
остальное получает исключение 03. А ответ — эхо: те же восемь байт,
что пришли в запросе:
if (count == 0xFF00)
m->out |= (uint8_t)(1u << first);
else
m->out &= (uint8_t)~(1u << first);
for (k = 0; k < 6; k++)
resp[k] = req[k]; /* ответ — эхо запроса */
k = mb_done(resp, 6);
Настройки пишутся функцией 16 в регистры хранения — разбор дословно как в 16.8, только регистров четыре. На ошибки модуль отвечает по протоколу: молчать или возражать — зависит от того, дошёл ли кадр целым.
| Что пришло | Что делает модуль |
|---|---|
| кадр с неверной CRC или обрезок | молчит и увеличивает счётчик в регистре 3 |
| чужой адрес | молчит |
| адрес 0 — широковещание | запись выполняет, не отвечает никогда |
| функция не из таблицы | исключение 01 |
| точка за пределами области | исключение 02 |
| негодное значение или длина | исключение 03 |
Ответ-исключение — три байта до CRC: адрес, функция с поднятым старшим битом, код причины.
198в.6 Фильтр входов и сторож связи
Фильтр входов. Концевики и реле дребезжат, а опрос ведущего редкий — ловить дребезг ему нельзя. Вход признаётся новым уровнем, только если продержался «время фильтра» (регистр 0, заводские 20 мс) без единого скачка:
if (((m->in_raw ^ m->in_state) & bit) == 0) {
m->in_cnt[i] = 0; /* согласие — счёт заново */
} else {
uint32_t c = m->in_cnt[i] + ms;
if (c >= m->hold[MBIO_H_FILTER]) { /* продержался — признан */
m->in_state ^= bit;
m->in_cnt[i] = 0;
} else {
m->in_cnt[i] = (uint16_t)c;
}
}
Опыт на модели: вход дребезжит скачком каждые 3 мс, последний скачок — на 12-й миллисекунде; единица признана на 32-й — ровно через 20 мс после последнего скачка, ни один промежуточный всплеск наружу не вышел.
Сторож связи. Выход, который ведущий включил, а потом пропал — с оборванной линией или зависшим компьютером — так и останется включённым. Поэтому у настоящих модулей есть сторож: нет правильного кадра в наш адрес дольше срока из регистра 1 — выходы принимают безопасную маску из регистра 2. Чужие кадры и кадры с битой CRC сторож не заряжают: шум на линии — не признак живого ведущего.
Опыт: ведущий включил катушку 1 и замолчал. 4999 мс выход держится, на
5000-й миллисекунде выходы из 0x02 становятся 0x00. Связь при
этом жива: на следующий запрос модуль отвечает как ни в чём не бывало —
01 01 01 00 51 88, катушки нули, — и ведущий видит по ответу, что
выходы пора записывать заново.
198в.7 Ведущий на компьютере и прогон в связке
Ведущий — master.py, без готовых библиотек Modbus: кадр собирается из
адреса, функции и данных, CRC — своя реализация на Python:
def frame(addr, fn, payload):
"""Кадр RTU: адрес, функция, данные, CRC младшим байтом вперёд."""
body = bytes([addr, fn]) + payload
c = crc16(body)
return body + bytes([c & 0xFF, c >> 8])
С настоящим портом конец ответа ловится той же тишиной, что делит кадры, — чтение с тайм-аутом между байтами:
def xfer(self, req):
self.s.reset_input_buffer()
self.s.write(req)
resp = b""
while True:
b = self.s.read(1) # нет байта дольше 50 мс — кадр кончился
if not b:
return resp
resp += b
Для прогона вместо порта — модель ведомого на компьютере: те же
mbio.c-байты, обёрнутые в slave_pipe.c. Ведущий отработал сценарий из
14 обменов: штатные чтения и записи, все три исключения, битая CRC,
чужой адрес, широковещание и сторож. Журнал — master_host.txt; вот его
характерные строки:
# чтение дискретных входов 0…3 — функция 02
-> 01 02 00 00 00 04 79 C9
<- 01 02 01 05 61 8B
# включить катушку 1 — функция 05, значение FF00
-> 01 05 00 01 FF 00 DD FA
<- 01 05 00 01 FF 00 DD FA
# чтение регистров 100…101 — таких нет
-> 01 03 00 64 00 02 85 D4
<- 01 83 02 C0 F1
В первом ответе счётчик байт 01 и байт 05 — замкнуты IN1 и IN3;
во втором — эхо; в третьем — функция 83 и код 02. Дальше по журналу:
| Что прислал ведущий | Ответ модуля |
|---|---|
| чтение катушек после включения первой | байт 02 |
| чтение регистров хранения 0…3 | 20, 50, 0, 0 — заводские |
| настройка: фильтр 10 мс, сторож 3,0 с (функция 16) | подтверждение |
| кадр с испорченной CRC | тишина; регистр 3 стал 1 |
| запрос к адресу 5 | тишина |
| функция 04 — регистров ввода у модуля нет | исключение 01 |
катушка 2, значение 0x1234 |
исключение 03 |
| широковещательная запись катушки 3 | тишина; чтение даёт 0A |
| 3,0 с без опросов | выходы 0A → 00, сторож сработал |
Итог прогона: 14 обменов, 3 законных молчания, 3 исключения, ни одного
расхождения с ожиданием — done=1 в журнале.
198в.8 Перенос на плату и что улучшить
Платформа для BluePill — в точности рецепт раздела 16.8: USART2 на PA2 и
PA3, 9600 бод 8E1 (BRR = 3750 от 36 МГц — без погрешности), TIM2 с
тиком 1 мкс и ARR = 4010 на паузу, DE приёмопередатчика — по флагу
TC. Логике остаётся отдать кадр в mbio_frame, раз в миллисекунду
дёргать mbio_tick и переносить out на выводы ключей.
Логика собрана arm-none-eabi-gcc -Os для Cortex-M3: 770 байт кода,
ни байта статических данных — всё состояние модуля лежит в одной
структуре из 28 байт, самая глубокая функция берёт 24 байта стека.
Модулей на таком камне может жить хоть десяток — по структуре на адрес.
Что улучшить:
- Функции 06 и 15 — запись одного регистра и нескольких катушек: разбор тот же, ведущие с ними встречаются часто.
- Зеркала битов в регистрах хранения: входы и выходы одним словом — для ведущих, которые умеют только функцию 03.
- Регистры ввода со счётчиками фронтов входов: концевик успел сработать между опросами — счётчик это покажет, а мгновенное чтение нет.
- Адрес и скорость — с перемычек или DIP-переключателя при старте: прошивать модуль ради адреса на шине — тоска.
- CRC аппаратным блоком (раздел 43) — на длинных кадрах заметно.
198в.9 Грабли
CRC «не сходится» ни на одном кадре. Порядок байтов: поля данных в
кадре идут старшим байтом вперёд, а CRC — младшим. Эталон: 01 03 00 00
00 02 даёт на конце C4 0B, не 0B C4.
Ведомый молчит на правильные кадры. Чётность: символ RTU — 11 бит с битом чётности, а порт ведущего открыт как 8N1. Каждый символ приходит с ошибкой чётности — и кадр честно летит в корзину.
Кадры рвутся или склеиваются. Конец кадра ловится по IDLE — тишине
в один символ: разрыв в 1,5 символа внутри кадра, который протокол
разрешает, режет кадр надвое. И наоборот: на 115 200 бод считать паузы
по символам — значит принять заминку ведущего за конец кадра; выше
19 200 бод паузы берут постоянными.
Записал в катушку 1 — исключение 03. Значение для функции 05 — только
0xFF00 и 0x0000. Единица, как и любое другое число, — негодное
значение.
Пишешь в точку из паспорта — меняется соседняя. Нумерация: в кадре адреса с нуля, в паспортах приборов часто с единицы. Минус один — и запись уходит не туда или ловит исключение 02.
Широковещательная запись «не работает». Работает — молча: на адрес 0 не отвечает никто, иначе на линии столкнулись бы все ведомые разом. Проверять — последующим чтением.
Выходы сами выключились посреди работы. Сторож связи: ведущий стал опрашивать реже, чем настроен сторож, — и модуль честно ушёл в безопасную маску. Либо чаще опрашивать, либо увеличить срок в регистре 1.
198в.10 Итоги
- Модуль ввода-вывода = карта регистров + разбор кадра + сторож связи; логика — 770 байт кода, состояние — структура в 28 байт, стек — 24 байта.
- Кадр RTU: адрес, функция, данные старшим байтом вперёд, CRC-16/MODBUS
младшим вперёд; полином
0xA001от0xFFFF, контроль123456789→ 0x4B37. - Кадры делит тишина 3,5 символа: 4011 мкс на 9600 бод, 335 мкс на
115 200; выше 19 200 бод — постоянные 1750 мкс. Ловить таймером, не
IDLE. - Битовые функции пакуют точки в байты со счётчиком байт; предел 2000 бит и 125/123 регистров — из кадра в 256 байт.
- Фильтр входов 20 мс: уровень признан через 20 мс после последнего скачка дребезга; сторож связи 5,0 с перевёл выходы в безопасную маску на 5000-й миллисекунде тишины.
- Прогон в связке с ведущим на Python: 14 обменов, 3 молчания, 3 исключения; CRC двух независимых реализаций сошлись байт в байт.
Полные исходники
mbio.c — логика модуля ввода-вывода Modbus RTU.
/* mbio.c — логика модуля ввода-вывода Modbus RTU.
* Функции: 01 чтение катушек, 02 чтение дискретных входов, 03 чтение
* регистров хранения, 05 запись одной катушки, 16 запись регистров. */
#include "mbio.h"
void mbio_init(struct mbio *m, uint8_t addr)
{
unsigned i;
m->addr = addr;
m->in_raw = m->in_state = m->out = 0;
m->wd_tripped = 0;
m->idle_ms = 0;
for (i = 0; i < MBIO_NIO; i++)
m->in_cnt[i] = 0;
m->hold[MBIO_H_FILTER] = 20; /* фильтр входов 20 мс */
m->hold[MBIO_H_WDOG] = 50; /* сторож связи 5,0 с */
m->hold[MBIO_H_SAFE] = 0; /* безопасное состояние: всё выключено */
m->hold[MBIO_H_CRCERR] = 0;
}
void mbio_input(struct mbio *m, uint8_t raw)
{
m->in_raw = raw & 0x0F;
}
void mbio_tick(struct mbio *m, uint32_t ms)
{
unsigned i;
for (i = 0; i < MBIO_NIO; i++) {
uint8_t bit = 1u << i;
if (((m->in_raw ^ m->in_state) & bit) == 0) {
m->in_cnt[i] = 0; /* согласие — счёт заново */
} else {
uint32_t c = m->in_cnt[i] + ms;
if (c >= m->hold[MBIO_H_FILTER]) { /* продержался — признан */
m->in_state ^= bit;
m->in_cnt[i] = 0;
} else {
m->in_cnt[i] = (uint16_t)c;
}
}
}
if (m->idle_ms < 0xFFFFFFFFu - ms)
m->idle_ms += ms;
if (m->hold[MBIO_H_WDOG] != 0 && !m->wd_tripped
&& m->idle_ms >= 100u * m->hold[MBIO_H_WDOG]) {
m->out = (uint8_t)(m->hold[MBIO_H_SAFE] & 0x0F);
m->wd_tripped = 1;
}
}
uint16_t mbio_crc(const uint8_t *p, size_t n)
{
uint16_t c = 0xFFFF; /* начальное значение */
while (n--) {
int i;
c ^= *p++; /* байт — в младшую половину */
for (i = 0; i < 8; i++)
c = (c & 1) ? (uint16_t)((c >> 1) ^ 0xA001) /* отражённый 0x8005 */
: (uint16_t)(c >> 1);
}
return c;
}
static uint16_t be16(const uint8_t *p) /* поля кадра — старшим байтом вперёд */
{
return (uint16_t)(p[0] << 8 | p[1]);
}
static size_t mb_done(uint8_t *r, size_t n) /* CRC — младшим байтом вперёд */
{
uint16_t c = mbio_crc(r, n);
r[n] = (uint8_t)c;
r[n + 1] = (uint8_t)(c >> 8);
return n + 2;
}
static size_t mb_ex(uint8_t *r, uint8_t addr, uint8_t fn, uint8_t code)
{
r[0] = addr;
r[1] = (uint8_t)(fn | 0x80); /* функция с поднятым старшим битом */
r[2] = code;
return mb_done(r, 3);
}
size_t mbio_frame(struct mbio *m, const uint8_t *req, size_t n, uint8_t *resp)
{
uint8_t fn;
uint16_t first, count;
int broadcast;
size_t k;
if (n < 4 || n > MBIO_ADU)
return 0; /* обрезок — молчать */
if (mbio_crc(req, n - 2) != (uint16_t)(req[n - 2] | (req[n - 1] << 8))) {
m->hold[MBIO_H_CRCERR]++; /* битая CRC — молчать */
return 0;
}
broadcast = req[0] == 0;
if (!broadcast && req[0] != m->addr)
return 0; /* чужой адрес — молчать */
m->idle_ms = 0; /* кадр нам — сторож заново */
m->wd_tripped = 0;
fn = req[1];
first = n >= 8 ? be16(req + 2) : 0;
count = n >= 8 ? be16(req + 4) : 0;
switch (fn) {
case 1: /* чтение катушек */
case 2: /* чтение дискретных входов */
if (broadcast)
return 0; /* чтение всем сразу — бессмыслица */
if (n != 8 || count < 1 || count > 2000) {
k = mb_ex(resp, m->addr, fn, MBIO_EX_VALUE);
} else if ((uint32_t)first + count > MBIO_NIO) {
k = mb_ex(resp, m->addr, fn, MBIO_EX_ADDR);
} else {
uint8_t bits = (uint8_t)((fn == 1 ? m->out : m->in_state) >> first);
resp[0] = m->addr;
resp[1] = fn;
resp[2] = (uint8_t)((count + 7) / 8); /* счётчик байт */
resp[3] = (uint8_t)(bits & ((1u << count) - 1u));
k = mb_done(resp, 4);
}
break;
case 3: /* чтение регистров хранения */
if (broadcast)
return 0;
if (n != 8 || count < 1 || count > 125) {
k = mb_ex(resp, m->addr, fn, MBIO_EX_VALUE);
} else if ((uint32_t)first + count > MBIO_NHOLD) {
k = mb_ex(resp, m->addr, fn, MBIO_EX_ADDR);
} else {
uint16_t i;
resp[0] = m->addr;
resp[1] = fn;
resp[2] = (uint8_t)(2 * count);
for (i = 0; i < count; i++) {
resp[3 + 2 * i] = (uint8_t)(m->hold[first + i] >> 8);
resp[4 + 2 * i] = (uint8_t)m->hold[first + i];
}
k = mb_done(resp, 3u + 2u * count);
}
break;
case 5: /* запись одной катушки */
if (n != 8 || (count != 0x0000 && count != 0xFF00)) {
k = mb_ex(resp, m->addr, fn, MBIO_EX_VALUE); /* только FF00 и 0000 */
} else if (first >= MBIO_NIO) {
k = mb_ex(resp, m->addr, fn, MBIO_EX_ADDR);
} else {
if (count == 0xFF00)
m->out |= (uint8_t)(1u << first);
else
m->out &= (uint8_t)~(1u << first);
for (k = 0; k < 6; k++)
resp[k] = req[k]; /* ответ — эхо запроса */
k = mb_done(resp, 6);
}
break;
case 16: /* запись регистров хранения */
if (n < 9 || count < 1 || count > 123
|| req[6] != 2 * count || n != 9u + req[6]) {
k = mb_ex(resp, m->addr, fn, MBIO_EX_VALUE);
} else if ((uint32_t)first + count > MBIO_NHOLD) {
k = mb_ex(resp, m->addr, fn, MBIO_EX_ADDR);
} else {
uint16_t i;
for (i = 0; i < count; i++)
m->hold[first + i] = be16(req + 7 + 2 * i);
for (i = 0; i < 6; i++) /* адрес, функция, первый, число */
resp[i] = req[i];
k = mb_done(resp, 6);
}
break;
default:
k = mb_ex(resp, m->addr, fn, MBIO_EX_FUNC);
break;
}
return broadcast ? 0 : k; /* на широковещание не отвечают */
}
master.py — ведущий Modbus RTU на компьютере.
# -*- coding: utf-8 -*-
"""master.py — ведущий Modbus RTU: опрос модуля ввода-вывода.
python3 master.py --pipe ./mbslave прогон против модели модуля на компьютере
python3 master.py --port COM7 то же по настоящему порту (нужен pyserial)
"""
import subprocess
import sys
def crc16(data):
"""CRC-16/MODBUS: полином 0xA001 (отражённый 0x8005), начальное 0xFFFF."""
c = 0xFFFF
for b in data:
c ^= b
for _ in range(8):
c = (c >> 1) ^ 0xA001 if c & 1 else c >> 1
return c
def frame(addr, fn, payload):
"""Кадр RTU: адрес, функция, данные, CRC младшим байтом вперёд."""
body = bytes([addr, fn]) + payload
c = crc16(body)
return body + bytes([c & 0xFF, c >> 8])
def be(x):
return bytes([x >> 8, x & 0xFF])
def hx(b):
return " ".join("%02X" % x for x in b)
class PipeLink:
"""Модель модуля на компьютере — обмен строками через канал."""
def __init__(self, path):
self.p = subprocess.Popen([path], stdin=subprocess.PIPE,
stdout=subprocess.PIPE, text=True)
def cmd(self, line):
self.p.stdin.write(line + "\n")
self.p.stdin.flush()
return self.p.stdout.readline().strip() if line[0] in "RO" else ""
def xfer(self, req):
r = self.cmd("R " + req.hex(" "))
r = r[2:].strip()
return b"" if r == "-" else bytes.fromhex(r)
class SerialLink:
"""Настоящий порт: конец ответа — та же тишина, что делит кадры."""
def __init__(self, port, baud=9600):
import serial # pyserial
self.s = serial.Serial(port, baud, bytesize=8,
parity="E", stopbits=1, timeout=0.05)
def xfer(self, req):
self.s.reset_input_buffer()
self.s.write(req)
resp = b""
while True:
b = self.s.read(1) # нет байта дольше 50 мс — кадр кончился
if not b:
return resp
resp += b
fails = 0
def poll(link, note, addr, fn, payload, expect):
"""Один обмен: запрос, ответ (или тишина), сверка с ожиданием."""
global fails
req = frame(addr, fn, payload)
print("# " + note)
print("-> " + hx(req))
resp = link.xfer(req)
print("<- " + (hx(resp) if resp else "тишина"))
if resp != expect:
print("ОЖИДАЛОСЬ: " + (hx(expect) if expect else "тишина"))
fails += 1
return resp
def main():
if len(sys.argv) == 3 and sys.argv[1] == "--pipe":
link = PipeLink(sys.argv[2])
elif len(sys.argv) == 3 and sys.argv[1] == "--port":
link = SerialLink(sys.argv[2])
else:
sys.exit("нужно: --pipe <модель> или --port <порт>")
silent = exceptions = 0
if isinstance(link, PipeLink):
link.cmd("I 5") # на клеммах входы 1 и 3
link.cmd("T 100") # фильтр 20 мс успел отработать
print("(входы модуля: IN1 и IN3 замкнуты, прошло 100 мс)")
poll(link, "чтение дискретных входов 0…3 — функция 02",
1, 2, be(0) + be(4), frame(1, 2, bytes([1, 0x05])))
poll(link, "включить катушку 1 — функция 05, значение FF00",
1, 5, be(1) + bytes([0xFF, 0]), frame(1, 5, be(1) + bytes([0xFF, 0])))
poll(link, "чтение катушек 0…3 — функция 01",
1, 1, be(0) + be(4), frame(1, 1, bytes([1, 0x02])))
poll(link, "чтение регистров хранения 0…3 — функция 03",
1, 3, be(0) + be(4),
frame(1, 3, bytes([8]) + be(20) + be(50) + be(0) + be(0)))
poll(link, "настройка: фильтр 10 мс, сторож 3,0 с — функция 16",
1, 16, be(0) + be(2) + bytes([4]) + be(10) + be(30),
frame(1, 16, be(0) + be(2)))
print("# кадр с испорченной CRC — модуль обязан молчать")
bad = bytearray(frame(1, 3, be(0) + be(1)))
bad[-1] ^= 0xFF
print("-> " + hx(bad))
resp = link.xfer(bytes(bad))
print("<- " + (hx(resp) if resp else "тишина"))
silent += resp == b""
resp = poll(link, "запрос к адресу 5 — на шине такого нет",
5, 3, be(0) + be(1), b"")
silent += resp == b""
poll(link, "чтение регистра 3 — счётчик кадров с битой CRC",
1, 3, be(3) + be(1), frame(1, 3, bytes([2]) + be(1)))
r = poll(link, "чтение регистров 100…101 — таких нет",
1, 3, be(100) + be(2), frame(1, 0x83, bytes([2])))
exceptions += len(r) == 5
r = poll(link, "функция 04: регистров ввода у модуля нет",
1, 4, be(0) + be(1), frame(1, 0x84, bytes([1])))
exceptions += len(r) == 5
r = poll(link, "катушка 2, значение 1234 — не FF00 и не 0000",
1, 5, be(2) + be(0x1234), frame(1, 0x85, bytes([3])))
exceptions += len(r) == 5
r = poll(link, "широковещательная запись: адрес 0, катушка 3 — FF00",
0, 5, be(3) + bytes([0xFF, 0]), b"")
silent += r == b""
poll(link, "чтение катушек: широковещательная запись выполнена молча",
1, 1, be(0) + be(4), frame(1, 1, bytes([1, 0x0A])))
if isinstance(link, PipeLink):
link.cmd("T 3000") # опросы прекратились на 3,0 с
print("(3,0 с без опросов: " + link.cmd("O") + ")")
poll(link, "после сторожа: выходы в безопасном состоянии, связь живёт",
1, 1, be(0) + be(4), frame(1, 1, bytes([1, 0x00])))
if isinstance(link, PipeLink):
link.cmd("Q")
print("exchanges=14 silent=%d exceptions=%d fails=%d" %
(silent, exceptions, fails))
if fails == 0:
print("done=1")
sys.exit(1 if fails else 0)
if __name__ == "__main__":
main()
Дальше: раздел 198г «Мини-проект: жесты по акселерометру».