Tổng quan về ModbusTCP trong hệ thống nhúng
ModbusTCP là một biến thể của giao thức Modbus truyền thống, được thiết kế để chạy trên nền tảng TCP/IP qua môi trường Ethernet. Khác với Modbus RTU dựa trên RS-485 gặp hạn chế về khoảng cách và tốc độ, ModbusTCP tận dụng hạ tầng mạng công nghiệp để truyền tải dữ liệu với băng thông lớn và độ tin cậy cao. Đối với các dòng vi điều khiển như STM32, việc tích hợp ModbusTCP thường đi kèm với bộ thư viện LwIP (Lightweight IP) để quản lý tầng giao vận.
Cấu trúc chi tiết của một gói tin ModbusTCP
Một khung dữ liệu ModbusTCP bao gồm hai thành phần chính: MBAP Header và PDU. Điểm khác biệt lớn nhất so với RTU là việc loại bỏ mã kiểm tra lỗi CRC, vì tầng TCP đã đảm nhiệm việc kiểm soát toàn vẹn dữ liệu.
1. MBAP Header (7 Bytes)
Đây là phần định danh ứng dụng Modbus, giúp phân biệt các luồng dữ liệu trong môi trường mạng.
| Trường dữ liệu | Kích thước | Mô tả |
|---|---|---|
| Transaction Identifier (TID) | 2 Bytes | Mã định danh giao dịch, dùng để khớp phản hồi với yêu cầu. |
| Protocol Identifier (PID) | 2 Bytes | Luôn bằng 0 cho giao thức Modbus. |
| Length | 2 Bytes | Số byte còn lại tính từ sau trường này (bao gồm Unit ID và PDU). |
| Unit Identifier (UID) | 1 Byte | Dùng để định danh thiết bị cuối hoặc Bridge. |
2. PDU (Protocol Data Unit)
Phần này chứa mã chức năng (Function Code) và dữ liệu thực tế. Ví dụ, mã 0x03 dùng để đọc thanh ghi giữ (Holding Registers), mã 0x06 để ghi thanh ghi đơn.
Cơ chế tiếp nhận dữ liệu qua LwIP trên STM32
Trong kiến trúc STM32 sử dụng LwIP, dữ liệu từ Master truyền đến sẽ kích hoạt hàm callback nhận dữ liệu. Do đặc tính của TCP là hướng dòng (stream-oriented), dữ liệu có thể bị chia nhỏ thành nhiều gói pbuf hoặc dính liền nhau. Lập trình viên cần thiết kế một bộ đệm trung gian để thu thập đủ khung dữ liệu trước khi xử lý.
#define MB_TCP_BUF_MAX 512
static uint8_t mbus_rx_data[MB_TCP_BUF_MAX];
static uint16_t current_rx_idx = 0;
err_t mbus_tcp_recv_handler(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err) {
if (p != NULL) {
// Cập nhật thời gian phản hồi cuối cùng
update_last_activity();
// Sao chép dữ liệu từ pbuf vào bộ đệm tĩnh
if ((current_rx_idx + p->tot_len) < MB_TCP_BUF_MAX) {
pbuf_copy_partial(p, &mbus_rx_data[current_rx_idx], p->tot_len, 0);
current_rx_idx += p->tot_len;
}
tcp_recved(tpcb, p->tot_len);
pbuf_free(p);
// Gọi hàm phân tích gói tin
process_modbus_command(tpcb);
} else if (err == ERR_OK) {
// Master ngắt kết nối
return tcp_close(tpcb);
}
return ERR_OK;
}
Logic phân tích và bóc tách gói tin
Sau khi dữ liệu được nạp vào bộ đệm, bước quan trọng nhất là kiểm tra tính hợp lệ của Header và độ dài yêu cầu. Quy trình thực hiện như sau:
- Kiểm tra nếu bộ đệm có ít nhất 7 byte (độ dài tối thiểu của MBAP).
- Đọc giá trị
Lengthtại byte thứ 4 và 5 để xác định tổng kích thước gói tin dự kiến. - Nếu số byte thực tế nhận được bằng hoặc lớn hơn
6 + Length, tiến hành xử lý PDU. - Trích xuất Function Code và thực thi các logic tương ứng trên bộ nhớ thanh ghi của STM32.
void process_modbus_command(struct tcp_pcb *tpcb) {
while (current_rx_idx >= 7) {
// Xác định độ dài PDU từ MBAP Header (Big Endian)
uint16_t expected_payload = (mbus_rx_data[4] << 8) | mbus_rx_data[5];
uint16_t full_packet_size = 6 + expected_payload;
if (current_rx_idx < full_packet_size) break; // Chưa nhận đủ gói
// Kiểm tra Protocol ID (phải là 0)
if (mbus_rx_data[2] != 0 || mbus_rx_data[3] != 0) {
flush_buffer();
return;
}
uint8_t func_code = mbus_rx_data[7];
execute_function(func_code, tpcb, full_packet_size);
// Xóa gói tin đã xử lý khỏi bộ đệm (Shift buffer)
uint16_t remaining = current_rx_idx - full_packet_size;
if (remaining > 0) {
memmove(mbus_rx_data, &mbus_rx_data[full_packet_size], remaining);
}
current_rx_idx = remaining;
}
}
Xây dựng phản hồi ModbusTCP
Khi gửi phản hồi, STM32 phải giữ nguyên Transaction ID từ yêu cầu để Master có thể định danh. Dưới đây là ví dụ cấu trúc phản hồi cho mã chức năng 0x03 (Đọc thanh ghi):
void send_read_response(struct tcp_pcb *tpcb, uint16_t tid, uint8_t unit_id, uint16_t *regs, uint8_t count) {
uint8_t tx_pkt[128];
uint16_t byte_count = count * 2;
uint16_t msg_len = 3 + byte_count; // UnitID(1) + FC(1) + ByteCount(1) + Data(N)
// Thiết lập MBAP Header
tx_pkt[0] = (tid >> 8) & 0xFF;
tx_pkt[1] = tid & 0xFF;
tx_pkt[2] = 0; // PID High
tx_pkt[3] = 0; // PID Low
tx_pkt[4] = (msg_len >> 8) & 0xFF;
tx_pkt[5] = msg_len & 0xFF;
tx_pkt[6] = unit_id;
// Thiết lập PDU
tx_pkt[7] = 0x03; // Function Code
tx_pkt[8] = byte_count;
for (uint8_t i = 0; i < count; i++) {
tx_pkt[9 + i * 2] = (regs[i] >> 8) & 0xFF;
tx_pkt[10 + i * 2] = regs[i] & 0xFF;
}
tcp_write(tpcb, tx_pkt, 9 + byte_count, TCP_WRITE_FLAG_COPY);
tcp_output(tpcb);
}
Các lưu ý kỹ thuật khi triển khai
- Endianness: Modbus sử dụng định dạng Big-Endian. Trong khi đó, vi điều khiển STM32 (Cortex-M) thường hoạt động ở chế độ Little-Endian. Do đó, việc đảo ngược thứ tự byte khi đọc/ghi thanh ghi là bắt buộc.
- Quản lý kết nối: TCP là giao thức kết nối. Cần thiết lập cơ chế Keep-alive hoặc Timeout để đóng các kết nối bị treo, giải phóng tài nguyên cho các phiên làm việc mới.
- Định địa chỉ: Địa chỉ thanh ghi trong gói tin Modbus thường bắt đầu từ 0, nhưng trong tài liệu kỹ thuật có thể được ký hiệu từ 40001. Cần tính toán offset chính xác để tránh lỗi "Illegal Data Address".
- Xử lý ngoại lệ: Nếu Master yêu cầu một địa chỉ không tồn tại hoặc mã chức năng không được hỗ trợ, Slave phải phản hồi bằng một mã lỗi (Exception Code) tương ứng (ví dụ: mã chức năng gốc + 0x80).