EST. 2026 — บันทึกงานเซิร์ฟเวอร์จริง SYSADMIN · SECURITY · AI · DIRECTADMIN เล่มที่ 01
THE SYSADMIN ALMANAC
หน้าหลัก / Linux / อ่าน Modbus RTU ผ่าน gateway RS485 ให้ไม่ได้ค่าผี — บทเรียนจากงานจริง
Linux

อ่าน Modbus RTU ผ่าน gateway RS485 ให้ไม่ได้ค่าผี — บทเรียนจากงานจริง

โดย Ton-Or ·15 ก.ค. 2026·อ่าน 9 นาที
อ่าน 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 คนละโปรโตคอลกัน

Modbus-TCP

มี MBAP header

มี header 7 ไบต์นำหน้า ไม่มี CRC (TCP จัดการ integrity ให้) — library ทั่วไปสร้างเฟรมให้อัตโนมัติ

RTU-over-TCP (transparent)

เฟรม RTU ดิบ ๆ ยัดใน TCP

ไม่มี MBAP ต้องประกอบเฟรม RTU เอง รวม CRC16 ต่อท้าย ถ้าใช้ client Modbus-TCP มายิงจะคุยกันไม่รู้เรื่อง

CRC ของ Modbus RTU เป็น CRC-16 poly 0xA001 (little-endian ต่อท้ายเฟรม) ต้องคำนวณเองแล้วแปะท้าย

Modbus-TCPMBAP header7 ไบต์นำหน้าPDUfunction + dataไม่มี CRCRTU-over-TCP (transparent mode)slave1 ไบต์func1 ไบต์reg2 ไบต์count2 ไบต์CRC162 ไบต์ little-endianต้องคำนวณเองไม่มี MBAP header ต้องประกอบเฟรมเอง แล้วต่อ CRC ท้ายเฟรม
client ของ Modbus-TCP สร้างเฟรมแบบบนให้เอง แต่ transparent gateway ต้องการเฟรมแบบล่างที่เราประกอบและคำนวณ CRC เอง
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 ที่ได้ทันทีโดยไม่ตรวจ = ได้ค่าของอุปกรณ์ผิดตัวมาเป็นของตัวที่ถาม

ตรวจ reply ทุกเฟรมก่อนเชื่อreplyจาก gatewayslave ตรงตรงตัวที่ถามfunc ตรงตรงที่ส่งไปbyte-countสมเหตุผลCRC ถูกคำนวณซ้ำเทียบใช้ payloadresp[3:3+n]ไม่ผ่านข้อใดข้อหนึ่งทิ้งเฟรม แล้ว resyncอย่าเอาไปใช้เด็ดขาดบัสร่วม: เฟรมของ slave อื่นที่ค้างอยู่อาจถูกอ่านกลับมาเป็นของตัวที่ถาม
ผ่านครบสี่ข้อจึงใช้ payload ได้ ขาดข้อใดข้อหนึ่งต้องทิ้งเฟรม ค่าที่ไม่ผ่านการตรวจคือที่มาของอุปกรณ์ผี
ตรวจ 4 อย่างก่อนเชื่อ reply

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 ของข้อมูล — อย่าเดา อย่าลากค่าเก่า

อ่านไม่ได้ = เว้นว่าง อย่า carry-forward

ถ้ารอบไหนอ่านไม่สำเร็จ ให้บันทึกเป็นช่องว่าง (gap) ไปเลย อย่าเอาค่าล่าสุดมาเติมแทน เพราะถ้าเอาไปคำนวณพลังงานสะสมหรือกราฟ จะเพี้ยนแบบเนียน ๆ หาสาเหตุยาก

และอย่าคิดว่า "1 sample = 1 วินาที" ตายตัว — ถ้าบางรอบหลุด ช่วงเวลาจริงระหว่างสองค่าไม่เท่ากัน ต้องเก็บ timestamp จริงของทุกค่า ถ้าจะอินทิเกรตหาพลังงานให้ใช้ผลต่างเวลาจริง ไม่ใช่นับจำนวน sample

ลำดับที่ผมไล่เวลาต่อของใหม่

  1. ตั้ง gateway เป็น transparent + จดว่าเป็น RTU-over-TCP (ไม่ใช่ Modbus-TCP)
  2. ยิงอ่าน slave ทีละตัว ทดสอบ CRC + address ให้ผ่านก่อน
  3. ใส่ validate 4 ชั้นทุก reply
  4. stagger การ poll ตัวที่ตอบช้า
  5. อ่านไม่ได้ = gap, เก็บ timestamp จริงทุกค่า

สรุปคือ Modbus บนบัสร่วมมันไม่ยากที่ตัวโปรโตคอล แต่ยากที่ "ความเชื่อใจ" — บัส RS485 เงียบ ๆ ก็มีเฟรมของตัวอื่นวิ่งปนได้ ถ้าไม่ตรวจให้ครบทุกครั้งจะได้ข้อมูลที่ดูเหมือนจริงแต่ผิดตัว ซึ่งอันตรายกว่าอ่านไม่ได้เลยด้วยซ้ำ ใครทำงาน IoT/industrial แนวนี้ ยึด validate-ทุก-reply ไว้เป็นกฎเหล็กครับ ตรงไหนผมอธิบายตกหล่นหรือพิมพ์ผิด เดี๋ยวกลับมาแก้เพิ่มให้ภายหลังครับ

เคยเจองานแบบนี้ไหม? ติดตรงไหน?

มาแลกวิธีกันต่อในบอร์ด LinuxThai

→ คุยต่อในบอร์ด

ความคิดเห็น 0

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