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.