В мене дома розведена мережа RS485, яка об’єднує лічильник електроенергії SDM230, годинник, метеостанцію, водяні лічильники. Протоколом обміну вибрано Modbus RTU, яка вимагає мастера, який ініціалізує звертання до пристроїв. Канальна швидкість 9600 байт.
Можна викорстати апаратний міст, наприклад ComPoint II AS від AK-NORD на 2 порти, розглянемо його: конектор DB-9. Може бути або 2 RS-232 або RS232+RS485.
З іншої сторони LAN 10/100 mbit, індикатори статусів, конектор живлення.
Живлення на LDO, запустилася і працювала від 5 вольт. Є версії на імпульскнику. Контролер 16 бітний від NXP MC9S12NE64VTUE.
Інтерфейс RS-232 ADM560. Драйвер RS-485 на Sipex 3072E. Перемичками виставляється режим роботи і підятгуючі резистори.
Конфігурація текстова навіть через web, пароль XT. DHCP імя XT-MICRO-AABBCC.
А можна заставити роутер бути і шлюзом і керувати логікою.
Оскільки є в наявності роутер MR3020 із LEDE 17.01.7 (OpenWRT) із збільшеним flash з 8 мб (на 4 мб не поміститься) і оперативною пам’ятю 32 мб замість 16, який має USB порт, підключаємо китайський USB-RS485 адаптер ціною 1$.
Оскільки ресурси роутера обмежені, використаємо інтерпретатор, який вже інстальований, в даному випадку це LUA. Синтаксис специфічний (не дорівнює ~=, є тип даних nil). Довстановлюємо потрібні модулі:
opkg update opkg install kmod-usb-serial opkg install kmod-usb-serial-ch341 opkg install lua-rs232
Тепер можемо писати наш файл програми modbus.lua. І почнемо з функцій.
Відкриємо порт із швидкістю 9600, перед тим перевіривши чи адаптер розпізнався, переважно /dev/ttyUSB0:
local function openport()
local out = nil
e, p = rs232.open("/dev/ttyUSB0")
if e ~= rs232.RS232_ERR_NOERROR then
print(string.format("Can't open serial port %s, %s", "ttyUSB0", rs232.error_tostring(e)))
else
onport = true
p:set_baud_rate(rs232.RS232_BAUD_9600)
p:set_data_bits(rs232.RS232_DATA_8)
p:set_parity(rs232.RS232_PARITY_NONE)
p:set_stop_bits(rs232.RS232_STOP_1)
p:set_flow_control(rs232.RS232_FLOW_OFF)
out = p
end
return out
end
Прокол Modbus RTU для контролю цілісності пакетів використовує 2 байтну CRC-16. Перевірити коректність формування контрольну суму можна на сайті crccalc.com. Тому пишемо калькулятор суми, який на вході отримує масив байт:
local function crc16(s)
local crc = 0xffff
for i,val in ipairs(s) do
local c = val
crc = bit.bxor(crc, c)
for j = 1, 8 do
local k = bit.band(crc, 1)
crc = bit.rshift(crc, 1)
if k ~= 0 then
crc = bit.bxor(crc, 0xA001)
end
end
end
return crc
end
Алгоритм використовує бітову операцію сумування по модулю 2 XOR, використаємо бібліотеку бітових операцій numberlua.lua і покладемо у
/usr/lib/lua/bit/numberlua.lua
І фукнцію яка дописує контрольну суму до пакета даних:
local function crc16_add(req) crc = crc16(req) Hi = bit.rshift(crc, 8) Lo = crc - Hi*256 table.insert(req, Lo) table.insert(req, Hi) return req end
Напишемо функцію, яка запитує один регістр із пристрою функцією 04 Read Input Registers і вертатиме результат.
function modbus_req(addr, reg, bytes, IEEE754)
local valid=1
local func = 4
reg_hi = bit.rshift(reg, 8)
reg_lo = reg - reg_hi*256
req = {addr, func, reg_hi, reg_lo, 0, bytes}
req = crc16_add(req)
port_w(req)
res = port_r()
if (res ~= nil) then
if res[1] ~= addr then valid=0 end
if res[2] ~= func then valid=0 end
local datsize = res[3]
local crc_now = res[datsize+5]*256+res[datsize+4]
table.remove(res,datsize+5)
table.remove(res,datsize+4)
if (crc_now ~= crc16(res)) then
valid=0
print('Bad CRC for packet')
end
table.remove(res,3)
table.remove(res,2)
table.remove(res,1)
if valid==1 then
if (IEEE754 == true) then
return UnpackIEEE754(res)
else
return res
end
else
return nil
end
else
return res
end
end
Функція записує масив байт у фізичний порт:
function port_w(req)
local data = ""
for i,val in ipairs(req) do
data = data .. string.char(val)
end
local err, len_written = p:write(data)
p:flush()
return len_written
end
І читання із порту і виходом з функції при відсутності відповіді:
function port_r()
local out = nil
local receive = 0
local takt = 1
local a=0
while (a == 0) do
local read_len, timeout = 1 , 100
local err, data_read, size = p:read(read_len, timeout)
if size > 0 then
if (receive == 0) then
out = {string.byte(data_read)}
else
table.insert(out, string.byte(data_read))
end
receive = receive + size
end
takt = takt + 1
if takt - receive > 2 then a=1 end
end
return out
end
Оскільки лічильник віддає дані у форматі IEEE754 перетворювач даних:
function UnpackIEEE754(x)
local b1, b2, b3, b4 = x[1], x[2], x[3], x[4]
local exponent = (b1 % 0x80) * 0x02 + math.floor(b2 / 0x80)
local mantissa = math.ldexp(((b2 % 0x80) * 0x100 + b3) * 0x100 + b4, -23)
if exponent == 0xFF then
if mantissa > 0 then
return 0 / 0
else
mantissa = math.huge
exponent = 0x7F
end
elseif exponent > 0 then
mantissa = mantissa + 1
else
exponent = exponent + 1
end
if b1 >= 0x80 then
mantissa = -mantissa
end
return math.floor(math.ldexp(mantissa, exponent - 0x7F)*1000)/1000
end
Для запису у регістр функцією 06 Preset Single Register:
function modbus_w(addr, reg, val)
local func = 6
reg_hi = bit.rshift(reg, 8)
reg_lo = reg - reg_hi*256
val_hi = bit.rshift(val, 8)
val_lo = val - val_hi*256
req = {addr, func, reg_hi, reg_lo, val_hi, val_lo}
req = crc16_add(req)
port_w(req)
end
Тепер можна написати саму програму:
p = openport()
-- отримаємо показник лічильника і напругу
meter = modbus_req(10,0x0156,2,true)
volts = modbus_req(10,0x0000,2,true)
-- отримані дані відправляємо через wget на сервер
local handle = io.popen("/bin/wget -q -O - 'http://****.net.ua/****.php?meter=" .. meter .. "&volts=" .. volts)
local result = handle:read("*a")
handle:close()
-- отримані дані записуємо на індикатор
modbus_w(45, 0x0000, result)
p:close()
Для періодичного запуску в певний час (наприклад щохвилини) пропишемо у crontab час запуску (для виконання має бути пустий останній рядок):
* * * * * /usr/bin/lua /root/modbus.lua
Тепер на сервері робимо потрібну логіку збереження, обробки і віддачі потрібних команд.
Це швидка реалізація поставленного завдання з того шо було у наявності, зараз йде тестування шлюзу із логікою на базі ESP32 із GPRS модулем.
Цікаве відкриття: у Siemens SIMATIC S7 позначення ліній A і B не відповідає загально прийнятим. Просто навпаки.
Юрій Р. ◯ 0009-0005-3702-9223. (2019). Modbus RTU на роутері (LEDE OpenWRT & LUA). Блог UA ID. Взято з: https://blog.uaid.net.ua/modbus-rtu-router-openwrt-lede-lua







