Học AI & Lập trình

Giao tiếp PLC bằng Modbus TCP từ C#: tự viết client tối giản

Hiểu tận gốc gói tin Modbus TCP, đọc/ghi thanh ghi, gửi số thực 32 bit và những bẫy về địa chỉ, thứ tự byte khi làm việc với PLC.

Giao tiếp PLC bằng Modbus TCP từ C#: tự viết client tối giản
Nội dung bài viết
  1. Cấu trúc một gói Modbus TCP
  2. Client tối giản
  3. Dùng trong trạm kiểm tra
  4. Gửi số thực 32 bit
  5. Khi đưa vào sản xuất

Kết quả kiểm tra phải đến được PLC để dây chuyền hành động. Modbus TCP là giao thức đơn giản, mở và gần như PLC nào cũng hỗ trợ. Bài này tự viết một Modbus TCP client nhỏ bằng C# để hiểu tận gốc giao thức, không phụ thuộc thư viện ngoài.

Cấu trúc một gói Modbus TCP

TrườngSố byteÝ nghĩa
Transaction ID2Mã giao dịch, phản hồi trả về đúng mã này
Protocol ID2Luôn là 0
Length2Số byte phía sau (Unit ID + PDU)
Unit ID1Địa chỉ thiết bị (thường 1)
Function code10x03 đọc holding register, 0x06 ghi 1 register, 0x10 ghi nhiều register
Dữ liệunĐịa chỉ, số lượng, giá trị (big-endian)

Client tối giản

using System.Net.Sockets;

public sealed class ModbusTcpClient : IDisposable
{
    private readonly TcpClient _tcp = new();
    private NetworkStream _stream = null!;
    private ushort _tid;
    private readonly byte _unit;

    public ModbusTcpClient(string host, int port = 502, byte unit = 1)
    {
        _unit = unit;
        _tcp.Connect(host, port);
        _stream = _tcp.GetStream();
        _stream.ReadTimeout = _stream.WriteTimeout = 1000;
    }

    private byte[] Send(byte[] pdu)
    {
        _tid++;
        var frame = new byte[7 + pdu.Length];
        frame[0] = (byte)(_tid >> 8); frame[1] = (byte)_tid;
        frame[4] = (byte)((pdu.Length + 1) >> 8); frame[5] = (byte)(pdu.Length + 1);
        frame[6] = _unit;
        pdu.CopyTo(frame, 7);
        _stream.Write(frame);

        var header = ReadExact(7);
        int len = (header[4] << 8) | header[5];
        var resp = ReadExact(len - 1);                       // PDU phản hồi
        if ((resp[0] & 0x80) != 0)
            throw new IOException($"Modbus exception code {resp[1]}");
        return resp;
    }

    private byte[] ReadExact(int n)
    {
        var buf = new byte[n];
        int read = 0;
        while (read < n)
        {
            int r = _stream.Read(buf, read, n - read);
            if (r == 0) throw new IOException("Mất kết nối");
            read += r;
        }
        return buf;
    }

    public ushort[] ReadHoldingRegisters(ushort start, ushort count)
    {
        var resp = Send(new byte[] { 0x03, (byte)(start >> 8), (byte)start, (byte)(count >> 8), (byte)count });
        var values = new ushort[count];
        for (int i = 0; i < count; i++)
            values[i] = (ushort)((resp[2 + 2 * i] << 8) | resp[3 + 2 * i]);
        return values;
    }

    public void WriteSingleRegister(ushort address, ushort value) =>
        Send(new byte[] { 0x06, (byte)(address >> 8), (byte)address, (byte)(value >> 8), (byte)value });

    public void Dispose() => _tcp.Dispose();
}

Dùng trong trạm kiểm tra

using var plc = new ModbusTcpClient("192.168.0.10");

ushort lenh = plc.ReadHoldingRegisters(0, 1)[0];   // thanh ghi 40001: lệnh từ PLC
if (lenh == 1)                                    // 1 = trigger
{
    bool ok = KiemTra();                          // xử lý ảnh
    plc.WriteSingleRegister(11, (ushort)(ok ? 1 : 2));   // 40012: kết quả
    plc.WriteSingleRegister(10, 0b0100);                  // 40011: bit DONE
}

Ở đây địa chỉ trong gói tin bắt đầu từ 0, nên "40001" trong tài liệu PLC tương ứng địa chỉ 0. Đây là chỗ nhầm lẫn phổ biến nhất, hãy thống nhất với kỹ sư PLC.

Gửi số thực 32 bit

static (ushort hi, ushort lo) FloatToRegisters(float v)
{
    var b = BitConverter.GetBytes(v);              // little-endian trên PC
    if (BitConverter.IsLittleEndian) Array.Reverse(b);
    return ((ushort)((b[0] << 8) | b[1]), (ushort)((b[2] << 8) | b[3]));
}

Một số PLC đảo thứ tự hai word ("word swap"). Kiểm tra bằng cách gửi giá trị dễ nhận biết như 1.0 và xem PLC đọc được gì.

Khi đưa vào sản xuất

  • Tự kết nối lại khi mất mạng, ghi log mọi lỗi giao tiếp.
  • Dùng phần mềm giả lập Modbus server để thử trước khi ra hiện trường.
  • Với dự án lớn, dùng thư viện Modbus đã được kiểm chứng (ví dụ NModbus) thay vì tự viết.

Xem thêm quy trình bắt tay READY/TRIGGER/DONE trong bài giao tiếp camera vision với PLC.

Ban biên tập HọcFree

Chúng tôi viết những bài hướng dẫn dễ hiểu, thực tế để ai cũng có thể tự học lập trình và ngoại ngữ, hoàn toàn miễn phí.