อ่าน Modbus RTU ผ่าน gateway RS485 ให้ไม่ได้ค่าผี — บทเรียนจากงานจริง
บัส RS485 เส้นเดียวมีอุปกรณ์หลายตัวคุยกัน ถ้า validate reply ไม่ครบ เฟรมที่สะท้อนมาจากอุปกรณ์อีกตัวจะถูกอ่านเป็นค่าของตัวที่เราถาม กลายเป็นข้อมูลผีที่ดูเหมือนจริง — นี่คือกับดักหลักของงาน Modbus บนบัสร่วม
พอดีผมต้องเก็บค่าจากมิเตอร์กับเซนเซอร์หลายตัวที่ต่อพ่วงกันบนบัส RS485 เส้นเดียว แล้วดึงขึ้นมาเก็บ/แจ้งเตือน อุปกรณ์พวกนี้พูดภาษา Modbus RTU ต่อออกเน็ตเวิร์กผ่าน serial-to-Ethernet gateway (พวก Waveshare/ZLAN) พอเริ่มอ่านจริงเจอค่าผี ค่าเพี้ยน อุปกรณ์ที่ไม่มีอยู่จริงโผล่มา งง อยู่พักใหญ่ กว่าจะเข้าใจว่ากลไกมันเป็นยังไง เลยเอาบทเรียนมาเล่าไว้
เรื่องแรกที่ต้องเข้าใจ: transparent mode ≠ Modbus-TCP
gateway พวกนี้ถ้าตั้งเป็น transparent mode (ปล่อยข้อมูลดิบผ่าน) มันคือ RTU-over-TCP ไม่ใช่ Modbus-TCP คนละโปรโตคอลกัน
มี MBAP header
มี header 7 ไบต์นำหน้า ไม่มี CRC (TCP จัดการ integrity ให้) — library ทั่วไปสร้างเฟรมให้อัตโนมัติ
เฟรม RTU ดิบ ๆ ยัดใน TCP
ไม่มี MBAP ต้องประกอบเฟรม RTU เอง รวม CRC16 ต่อท้าย ถ้าใช้ client Modbus-TCP มายิงจะคุยกันไม่รู้เรื่อง
CRC ของ Modbus RTU เป็น CRC-16 poly 0xA001 (little-endian ต่อท้ายเฟรม) ต้องคำนวณเองแล้วแปะท้าย
def crc16_modbus(data: bytes) -> bytes:
crc = 0xFFFF
for b in data:
crc ^= b
for _ in range(8):
crc = (crc >> 1) ^ 0xA001 if (crc & 1) else crc >> 1
return bytes([crc & 0xFF, (crc >> 8) & 0xFF]) # little-endian
def read_frame(slave, func, reg, count):
body = bytes([slave, func]) + reg.to_bytes(2, 'big') + count.to_bytes(2, 'big')
return body + crc16_modbus(body)
กับดักใหญ่: ต้อง validate ทุก reply ไม่งั้นได้อุปกรณ์ผี
บัส RS485 เป็น multi-drop — อุปกรณ์หลายตัวแชร์สายคู่เดียวกัน เวลาเราถาม slave 1 แต่ถ้าจังหวะไม่ดี เฟรมที่ค้างในบัสจาก slave อื่นอาจถูกอ่านกลับมา ถ้าเราเชื่อ byte ที่ได้ทันทีโดยไม่ตรวจ = ได้ค่าของอุปกรณ์ผิดตัวมาเป็นของตัวที่ถาม
1) ไบต์แรก (slave address) ต้องตรงกับตัวที่เราถาม 2) function code ตรง 3) byte-count สมเหตุผล 4) CRC ถูก — ขาดข้อใดข้อหนึ่ง = ทิ้งเฟรม แล้ว resync อย่าเอาไปใช้เด็ดขาด
def parse_reply(resp, want_slave, want_func):
if len(resp) < 5: return None
if resp[0] != want_slave: return None # ผิดตัว -> ทิ้ง
if resp[1] != want_func: return None # ผิด function -> ทิ้ง
if crc16_modbus(resp[:-2]) != resp[-2:]: return None # CRC ไม่ผ่าน -> ทิ้ง
n = resp[2]
return resp[3:3+n] # payload จริง
อย่ายิง slave ช้าสองตัวติดกัน
อีกอาการที่เจอ: อุปกรณ์บางตัวตอบช้า ถ้าเรา poll สองตัวช้าติดกันในรอบเดียว มันจะแย่งจังหวะบัสจนสลับกันหน่วง (ตัวหนึ่งได้ อีกตัวหลุด สลับไปมา) วิธีที่ผมใช้คือกระจายภาระด้วยการ stagger — ให้แต่ละ slave ยิงคนละรอบตาม index
# ยิง slave ตัวที่ i เฉพาะรอบที่ (cycle - 1 - offset) หาร every ลงตัว
def should_poll(cycle, offset, every):
return (cycle - 1 - offset) % every == 0
เรื่อง integrity ของข้อมูล — อย่าเดา อย่าลากค่าเก่า
ถ้ารอบไหนอ่านไม่สำเร็จ ให้บันทึกเป็นช่องว่าง (gap) ไปเลย อย่าเอาค่าล่าสุดมาเติมแทน เพราะถ้าเอาไปคำนวณพลังงานสะสมหรือกราฟ จะเพี้ยนแบบเนียน ๆ หาสาเหตุยาก
และอย่าคิดว่า "1 sample = 1 วินาที" ตายตัว — ถ้าบางรอบหลุด ช่วงเวลาจริงระหว่างสองค่าไม่เท่ากัน ต้องเก็บ timestamp จริงของทุกค่า ถ้าจะอินทิเกรตหาพลังงานให้ใช้ผลต่างเวลาจริง ไม่ใช่นับจำนวน sample
ลำดับที่ผมไล่เวลาต่อของใหม่
- ตั้ง gateway เป็น transparent + จดว่าเป็น RTU-over-TCP (ไม่ใช่ Modbus-TCP)
- ยิงอ่าน slave ทีละตัว ทดสอบ CRC + address ให้ผ่านก่อน
- ใส่ validate 4 ชั้นทุก reply
- stagger การ poll ตัวที่ตอบช้า
- อ่านไม่ได้ = gap, เก็บ timestamp จริงทุกค่า
สรุปคือ Modbus บนบัสร่วมมันไม่ยากที่ตัวโปรโตคอล แต่ยากที่ "ความเชื่อใจ" — บัส RS485 เงียบ ๆ ก็มีเฟรมของตัวอื่นวิ่งปนได้ ถ้าไม่ตรวจให้ครบทุกครั้งจะได้ข้อมูลที่ดูเหมือนจริงแต่ผิดตัว ซึ่งอันตรายกว่าอ่านไม่ได้เลยด้วยซ้ำ ใครทำงาน IoT/industrial แนวนี้ ยึด validate-ทุก-reply ไว้เป็นกฎเหล็กครับ ตรงไหนผมอธิบายตกหล่นหรือพิมพ์ผิด เดี๋ยวกลับมาแก้เพิ่มให้ภายหลังครับ
เคยเจองานแบบนี้ไหม? ติดตรงไหน?
มาแลกวิธีกันต่อในบอร์ด LinuxThai



ความคิดเห็น 0
ยังไม่มีความคิดเห็น — เป็นคนแรกเลย