Ứng dụng nModbus trong hệ thống điều khiển công nghiệp

Trong quá trình phát triển một hệ thống thu thập dữ liệu cho nhà máy xử lý nước, tôi đã sử dụng giao thức Modbus để kết nối các thiết bị đo lường và điều khiển thông qua mạng RS-485. Tất cả các thiết bị như đồng hồ đo chất lượng nước, bộ đo lưu lượng và van điều khiển đều được kết nối với một cổng nhúng và phần mềm dịch vụ trên .NET sẽ đọc dữ liệu từ đây và gửi lên đám mây để phân tích.

Tại sao chọn nModbus?

Trong thế giới thực, hầu hết các thiết bị vẫn sử dụng giao thức Modbus. Đặc biệt ở các dự án vừa và nhỏ, PLC, cảm biến và biến tần chỉ hỗ trợ Modbus RTU hoặc TCP. Đây là giao thức đơn giản, mở và ổn định, tuy cũ nhưng rất đáng tin cậy. Tuy nhiên, .NET không có sẵn thư viện Modbus, do đó việc sử dụng nModbus, một thư viện mã nguồn mở bằng C#, là lựa chọn hợp lý. Nó hỗ trợ nhiều chế độ khác nhau của Modbus và hoạt động tốt trên nền tảng .NET.

Cách thức hoạt động của Modbus

Cấu trúc Master-Slave

Modbus sử dụng cấu trúc chủ-tớ (Master-Slave). Chỉ có Master mới có thể gửi yêu cầu đến Slave. Ví dụ: "Slave 1, hãy đọc giá trị tại thanh ghi số 100 của bạn". Do đó, ứng dụng của chúng ta đóng vai trò là Master, còn các thiết bị như PLC hay đồng hồ đo là Slave.

Loại Mã chức năng Đọc/ghi Ứng dụng phổ biến
Nhập rời rạc (Discrete Input) 0x02 Chỉ đọc Trạng thái công tắc bên ngoài
Dây cuộn (Coil) 0x01/0x05/0x0F Đọc/ghi Kiểm soát đầu ra (bật/tắt bơm, mở van)
Thanh ghi nhập (Input Register) 0x04 Chỉ đọc Đầu vào tương tự (nhiệt độ, áp suất)
Thanh ghi giữ (Holding Register) 0x03/0x06/0x10 Đọc/ghi Cấu hình tham số, biến trung gian

Vấn đề thứ tự byte

Khi đọc một giá trị kiểu float, cần chú ý đến thứ tự byte. Ví dụ:

private static float ChuyenDoiDangFloat(ushort[] cap)
{
    byte[] bytes = new byte[4];
    Buffer.BlockCopy(cap, 0, bytes, 0, 4);
    if (BitConverter.IsLittleEndian) Array.Reverse(bytes);
    return BitConverter.ToSingle(bytes, 0);
}

Xây dựng mô-đun thu thập dữ liệu với nModbus

Bắt đầu bằng cách cài đặt gói:

dotnet add package NModbus

Dưới đây là một ví dụ về mã thực tế:

using System;
using System.Net.Sockets;
using System.Threading.Tasks;
using Modbus.Device;

public class BoThuThapDuLieu : IDisposable
{
    private IModbusMaster _chu;
    private TcpClient _khach;

    public async Task<bool> KetNoiAsync(string ip, int cong = 502)
    {
        try
        {
            _khach = new TcpClient();
            await _khach.ConnectAsync(ip, cong);

            var congXuong = new ModbusFactory();
            _chu = congXuong.TaoChu(_khach);

            _khach.ReceiveTimeout = 3000;
            _khach.SendTimeout = 3000;

            Console.WriteLine($"✅ Đã kết nối tới {ip}:{cong}");
            return true;
        }
        catch (Exception ex)
        {
            Console.WriteLine($"❌ Kết nối thất bại: {ex.Message}");
            return false;
        }
    }

    public async Task DocNhietDoVaApSuat(byte idTos)
    {
        const ushort diaChiNhiet = 100;   // Tương ứng 40101
        const ushort diaChiAp = 102;      // Tương ứng 40103
        const ushort soLuong = 4;         // Hai float chiếm 4 thanh ghi

        for (int thuLai = 0; thuLai < 3; thuLai++)
        {
            try
            {
                var thanhGhi = await _chu.DocThanhGhiGiuaAsync(idTos, diaChiNhiet, soLuong);

                float nhiet = ChuyenDoiDangFloat(new[] { thanhGhi[0], thanhGhi[1] });
                float ap = ChuyenDoiDangFloat(new[] { thanhGhi[2], thanhGhi[3] });

                return new[] { nhiet, ap };
            }
            catch (TimeoutException)
            {
                await Task.Delay((thuLai + 1) * 100); 
                continue;
            }
            catch (IOException ioEx)
            {
                Console.WriteLine($"Lỗi IO: {ioEx.Message}");
                break;
            }
        }

        return null; // Thất bại sau nhiều lần thử
    }

    public void Dispose()
    {
        _chu?.Dispose();
        _khach?.Close();
        _khach?.Dispose();
    }
}

Khắc phục các vấn đề thường gặp

Vấn đề 1: Giao tiếp thường xuyên bị timeout

Nguyên nhân: Dây tín hiệu RS-485 không được lắp điện trở đầu cuối. Giải pháp: Lắp thêm điện trở 120Ω ở hai đầu dây.

Vấn đề 2: Giá trị đọc ra quá lớn

Nguyên nhân: Thứ tự byte chưa đúng. Cần kiểm tra tài liệu thiết bị hoặc dùng công cụ như Modbus Poll để xác định và viết hàm chuyển đổi phù hợp.

Vấn đề 3: Các thiết bị chờ đợi khi một thiết bị trước đó bị treo

Giải pháp: Điều chỉnh chu kỳ quét cho từng thiết bị và ưu tiên các thiết bị quan trọng hơn.

Thẻ: nmodbus công nghiệp Modbus RTU modbus tcp .NET

Đăng vào ngày 30 tháng 6 lúc 19:07