Giao thức truyền thông trên STM32 (Phần 7): Modbus RTU và TCP – khung dữ liệu, CRC-16 và khoảng lặng 3,5 ký tự
Modbus RTU và Modbus TCP trên STM32, từ mô hình dữ liệu, cấu trúc khung, tính CRC-16 (ví dụ đã kiểm tra), quy tắc khoảng lặng 3,5 ký tự đến mã slave dùng DMA và ngắt IDLE. Phần 7 của series giao thức truyền thông trên STM32.

Ghi chú: sơ đồ trong bài do Capitlab vẽ để minh hoạ. Số liệu trong các bảng là giá trị tính theo công thức, cột "Đo thực tế" sẽ được cập nhật khi đo trên phần cứng. Các khung ví dụ và CRC đã được kiểm tra bằng chương trình. Mã nguồn soạn theo tài liệu HAL và datasheet, chưa chạy thử trên phần cứng, nên hãy kiểm tra lại trước khi dùng cho sản phẩm.
Modbus ra đời năm 1979 và đến giờ vẫn là ngôn ngữ chung của thiết bị công nghiệp: PLC, biến tần, đồng hồ điện, cảm biến. Nó đơn giản, đặc tả mở, và gần như thiết bị nào cũng hỗ trợ. Hai biến thể hay gặp là RTU, dạng nhị phân chạy trên cổng nối tiếp như RS-485, và TCP, chạy trên Ethernet.
Khi tự viết slave Modbus, hai lỗi kinh điển là gửi CRC sai thứ tự byte và đánh địa chỉ thanh ghi lệch một đơn vị. Bài này có ví dụ khung đã kiểm tra CRC để bạn đối chiếu, cùng cách bắt biên khung RTU bằng DMA và ngắt IDLE trên STM32.
1. Mô hình dữ liệu
Modbus hoạt động theo kiểu hỏi đáp: master (client) hỏi, slave (server) trả lời, slave không bao giờ tự phát. Dữ liệu trong thiết bị chia thành bốn vùng:
| Vùng | Kiểu | Quyền | Mã chức năng thường dùng |
|---|---|---|---|
| Coils | 1 bit | Đọc/ghi | 0x01 đọc, 0x05 ghi một, 0x0F ghi nhiều |
| Discrete Inputs | 1 bit | Chỉ đọc | 0x02 |
| Holding Registers | 16 bit | Đọc/ghi | 0x03 đọc, 0x06 ghi một, 0x10 ghi nhiều |
| Input Registers | 16 bit | Chỉ đọc | 0x04 |
Với RTU, mỗi slave có địa chỉ từ 1 đến 247. Địa chỉ 0 là quảng bá (broadcast): mọi slave thực hiện lệnh nhưng không ai trả lời.
Giá trị lớn hơn 16 bit (số nguyên 32 bit, số thực float) được ghép từ hai hay nhiều thanh ghi liên tiếp. Chuẩn không quy định thứ tự giữa các thanh ghi (word order), nên mỗi hãng làm một kiểu. Để giải mã nhanh, dán các byte vào Hex Converter, công cụ này hiện cùng lúc bốn thứ tự ABCD, DCBA, BADC, CDAB.
2. Modbus RTU
2.1. Khung dữ liệu
Hình 1. Khung RTU. Khung dài tối đa 256 byte. Hai bên là khoảng lặng ít nhất 3,5 ký tự.
| Địa chỉ slave | Mã chức năng | Dữ liệu | CRC16 (thấp) | CRC16 (cao) |
|---|---|---|---|---|
| 1 byte | 1 byte | N byte | 1 byte | 1 byte |
Trên dây, mỗi ký tự thường là 11 bit: 1 start, 8 dữ liệu, 1 parity chẵn, 1 stop. Không dùng parity thì dùng 8N2 để giữ đủ 11 bit. Dữ liệu 16 bit gửi byte cao trước (big-endian), riêng CRC thì byte thấp trước.
2.2. Ví dụ đã kiểm tra: đọc 2 thanh ghi holding
Đọc 2 thanh ghi holding của slave 1, bắt đầu từ địa chỉ 0. Yêu cầu là 01 03 00 00 00 02 C4 0B:
01là địa chỉ slave,03là đọc holding registers.00 00là địa chỉ bắt đầu,00 02là số thanh ghi.C4 0Blà CRC, byte thấpC4đi trước, byte cao0Bsau.
Nếu hai thanh ghi đang chứa 0x0012 và 0x0034, phản hồi là 01 03 04 00 12 00 34 5B E1. Ở đây 04 là số byte dữ liệu theo sau, 5B E1 là CRC. CRC của cả hai khung đã được tính bằng chương trình để đối chiếu.
Ví dụ ghi: đặt giá trị 3 vào thanh ghi địa chỉ 1 của slave 1 bằng mã 0x06 là 01 06 00 01 00 03 98 0B. Ghi thành công thì slave trả lại nguyên khung yêu cầu.
Hình 2. Một giao dịch hỏi đáp. Giữa hai khung luôn có khoảng lặng, và slave cần thời gian xử lý trước khi trả lời.
2.3. Phản hồi lỗi (exception)
Khi không thực hiện được yêu cầu, slave trả về mã chức năng có bit cao bật lên (0x03 thành 0x83) và một byte mã lỗi. Ví dụ slave 1 báo địa chỉ dữ liệu không hợp lệ cho lệnh đọc holding: 01 83 02 C0 F1.
| Mã | Ý nghĩa |
|---|---|
| 01 | Mã chức năng không hợp lệ |
| 02 | Địa chỉ dữ liệu không hợp lệ |
| 03 | Giá trị dữ liệu không hợp lệ |
| 04 | Lỗi thiết bị slave |
2.4. CRC-16 Modbus
Đa thức 0x8005, viết dạng đảo bit là 0xA001, giá trị khởi tạo 0xFFFF, không đảo kết quả:
uint16_t modbus_crc16(const uint8_t *p, uint16_t n)
{
uint16_t crc = 0xFFFF;
while (n--) {
crc ^= *p++;
for (int i = 0; i < 8; i++)
crc = (crc & 1) ? (crc >> 1) ^ 0xA001 : crc >> 1;
}
return crc; // gửi byte thấp (crc & 0xFF) trước, rồi byte cao (crc >> 8)
}
Kiểm tra: chuỗi 31 32 33 34 35 36 37 38 39 ("123456789") phải cho 0x4B37. Có thể đối chiếu bằng máy tính CRC của Capitlab, chọn CRC-16/MODBUS. Firmware cần nhanh hơn thì thay vòng lặp từng bit bằng bảng tra 256 phần tử.
2.5. Biên khung xác định bằng thời gian
RTU không có ký tự đầu hay cuối khung. Ranh giới giữa các khung là khoảng im lặng trên dây:
- Giữa hai khung phải im lặng ít nhất 3,5 thời gian ký tự.
- Trong một khung, hai ký tự liền nhau không được cách quá 1,5 thời gian ký tự, nếu không khung bị coi là hỏng.
- Thời gian một ký tự bằng số bit chia baudrate.
- Trên 19200 baud, đặc tả cho dùng giá trị cố định: 750 µs cho 1,5 ký tự và 1750 µs cho 3,5 ký tự, vì giá trị tính theo công thức quá ngắn để cài đặt.
Bảng tính cho ký tự 11 bit. Cột chu kỳ là thời gian lý thuyết của một lần đọc hai thanh ghi: yêu cầu 8 byte, phản hồi 9 byte, hai khoảng lặng 3,5 ký tự, chưa tính thời gian slave xử lý:
| Baudrate | Thời gian 1 ký tự | 1,5 ký tự | 3,5 ký tự | Chu kỳ tối đa lý thuyết | Số lần đọc mỗi giây |
|---|---|---|---|---|---|
| 9 600 | 1,146 ms | 1,72 ms | 4,01 ms | 27,5 ms | 36 |
| 19 200 | 0,573 ms | 0,86 ms | 2,01 ms | 13,7 ms | 72 |
| 38 400 | 0,286 ms | 0,75 ms (cố định) | 1,75 ms (cố định) | 8,4 ms | 119 |
| 115 200 | 0,095 ms | 0,75 ms (cố định) | 1,75 ms (cố định) | 5,1 ms | 195 |
Đọc một lần nhiều thanh ghi rẻ hơn hẳn đọc từng cái. Đọc 125 thanh ghi (mức tối đa của mã 0x03) mất khoảng 301 ms ở 9600 baud và khoảng 25 ms ở 115200 baud, tức chỉ khoảng 2,4 ms và 0,2 ms cho mỗi thanh ghi.
Trên STM32, cách tự nhiên để biết hết khung là dùng ngắt IDLE của UART cùng DMA (phần 1). Ở tốc độ cao, IDLE (một ký tự) đến sớm hơn mốc 3,5 ký tự, nhưng vẫn đủ để kết luận khung đã xong, vì trong một khung hợp lệ các byte không bao giờ cách nhau quá 1,5 ký tự. Ở tốc độ thấp, hoặc khi cần kiểm tra chặt theo đặc tả, dùng một timer 3,5 ký tự được khởi động lại mỗi khi nhận một byte.
2.6. Khung xử lý của slave
#define MY_ADDR 1
// Khi UART báo hết khung (IDLE) với buffer rx dài len:
void modbus_on_frame(const uint8_t *rx, uint16_t len)
{
if (len < 4) return; // quá ngắn
uint16_t crc = modbus_crc16(rx, len - 2);
if (rx[len-2] != (crc & 0xFF) || rx[len-1] != (crc >> 8)) return; // sai CRC: bỏ qua, không trả lời
if (rx[0] != MY_ADDR && rx[0] != 0) return; // không phải gửi cho mình
switch (rx[1]) {
case 0x03: handle_read_holding(rx, len); break;
case 0x06: handle_write_single(rx, len); break;
case 0x10: handle_write_multiple(rx, len); break;
default: send_exception(rx[0], rx[1], 0x01); break; // mã chức năng không hỗ trợ
}
}
Hàm xử lý mẫu cho mã 0x03, với regs[] là bảng thanh ghi holding của thiết bị:
static uint16_t regs[100];
static void handle_read_holding(const uint8_t *rx, uint16_t len)
{
uint16_t addr = (rx[2] << 8) | rx[3];
uint16_t qty = (rx[4] << 8) | rx[5];
if (qty < 1 || qty > 125) { send_exception(rx[0], 0x03, 0x03); return; }
if (addr + qty > 100) { send_exception(rx[0], 0x03, 0x02); return; }
uint8_t tx[5 + 250];
uint16_t k = 0;
tx[k++] = rx[0]; tx[k++] = 0x03; tx[k++] = qty * 2;
for (uint16_t i = 0; i < qty; i++) {
tx[k++] = regs[addr + i] >> 8; // byte cao trước
tx[k++] = regs[addr + i] & 0xFF;
}
uint16_t crc = modbus_crc16(tx, k);
tx[k++] = crc & 0xFF; // CRC: byte thấp trước
tx[k++] = crc >> 8;
if (rx[0] != 0) rs485_send(tx, k); // broadcast thì không trả lời
}
Hàm rs485_send và cách điều khiển chân DE có ở phần 6. Slave chỉ bật phát sau khi khung yêu cầu đã kết thúc, và trả về nhận ngay khi phát xong để sẵn sàng cho yêu cầu kế tiếp.
Có sẵn nhiều thư viện Modbus mã nguồn mở cho STM32. Dùng thư viện thì nhanh hơn, nhưng tự viết một lần giúp hiểu rõ các quy tắc thời gian và CRC ở trên.
3. Modbus TCP
Modbus TCP giữ nguyên mô hình dữ liệu và mã chức năng, chỉ đổi lớp vận chuyển sang TCP/IP, cổng 502. Thay cho địa chỉ slave và CRC, khung có tiêu đề MBAP 7 byte. CRC không cần nữa vì TCP đã lo phần toàn vẹn dữ liệu.
Hình 3. RTU và TCP dùng chung phần PDU (mã chức năng và dữ liệu), chỉ khác phần bao quanh.
| Trường | Kích thước | Ý nghĩa |
|---|---|---|
| Transaction ID | 2 byte | Khớp yêu cầu với phản hồi |
| Protocol ID | 2 byte | Luôn bằng 0 |
| Length | 2 byte | Số byte còn lại (Unit ID + PDU) |
| Unit ID | 1 byte | Thay cho địa chỉ slave, dùng khi có cổng chuyển đổi sang RTU |
Phần còn lại là PDU (mã chức năng và dữ liệu), giống hệt RTU. Ví dụ đọc 2 thanh ghi từ địa chỉ 0: 00 01 00 00 00 06 01 03 00 00 00 02. Trường 00 06 là 6 byte còn lại gồm Unit ID, mã chức năng và 4 byte dữ liệu.
Trên STM32 cần một ngăn xếp TCP/IP, sẽ nói ở phần 10. Cách đơn giản là một socket server nghe cổng 502: nhận khung, bóc MBAP, xử lý PDU, rồi gửi phản hồi kèm MBAP có cùng Transaction ID. Các gateway chuyển TCP sang RTU thường dùng Unit ID để chọn slave RTU phía sau.
4. Lỗi thường gặp
| Triệu chứng | Nguyên nhân thường gặp | Cách xử lý |
|---|---|---|
| Không có phản hồi | Sai địa chỉ slave, sai baudrate hoặc parity, chiều DE chưa đúng | Kiểm tra cấu hình hai phía, dùng logic analyzer đối chiếu khung |
| Phản hồi bị từ chối do sai CRC | Nhầm thứ tự byte CRC, hoặc tính CRC cả byte không thuộc khung | CRC phải gửi byte thấp trước, kiểm tra với ví dụ C4 0B |
| Khung bị cắt ngang | Khoảng im lặng quá ngắn, IDLE chia gói sớm, các byte cách nhau quá xa | Kiểm tra DMA/IDLE hoặc timer, gửi liền một khung |
| Sai địa chỉ thanh ghi | Tài liệu thiết bị đánh số từ 1 (ví dụ 40001), khung gửi địa chỉ từ 0 | Trừ một đơn vị và bỏ tiền tố 4xxxx khi đưa vào khung |
| Giá trị float sai | Thứ tự hai thanh ghi (word order) khác nhau giữa các hãng | Thử bốn thứ tự ABCD, DCBA, BADC, CDAB bằng Hex Converter |
| Master báo timeout | Slave xử lý quá chậm, timeout của master quá ngắn | Tăng timeout, tối ưu thời gian xử lý |
| Hai slave cùng địa chỉ | Cấu hình trùng | Đặt địa chỉ khác nhau cho mỗi thiết bị |
5. Kinh nghiệm khi làm slave
Để thử slave, dùng phần mềm master trên PC như Modbus Poll hoặc QModMaster, kèm một bộ chuyển USB-RS485. Đó là cách nhanh nhất để đối chiếu từng khung.
Khi slave nhận khung sai CRC, nó không được trả lời, kể cả exception. Khung đó có thể chỉ là nhiễu, hoặc vốn gửi cho thiết bị khác.
Khung RTU dài tối đa 256 byte, nên một lần đọc được nhiều nhất 125 thanh ghi (mã 0x03) và ghi nhiều nhất 123 thanh ghi (mã 0x10). Trong thiết kế bảng thanh ghi, gom các giá trị hay đọc cùng nhau vào một vùng liền để master đọc một lần.
6. Hỏi đáp
Modbus có mã hoá hay xác thực không? Bản gốc thì không, lệnh và dữ liệu đi dạng rõ. Vì vậy không nên đưa Modbus TCP thẳng ra mạng ngoài. Có biến thể Modbus/TCP Security dùng TLS, nhưng ít thiết bị hỗ trợ.
Chọn RTU hay TCP? RTU rẻ, tận dụng cáp RS-485 sẵn có trong nhà máy, ổn định ở khoảng cách xa nhưng chậm. TCP nhanh, dùng hạ tầng Ethernet và cho nhiều master cùng lúc, đổi lại thiết bị phải có cổng mạng và ngăn xếp TCP/IP.
Chạy Modbus RTU qua USB được không? Được, qua bộ chuyển USB sang RS-485 (hiện thành cổng COM ảo), hoặc cài slave trực tiếp trên USB CDC của STM32.
Modbus ASCII khác RTU chỗ nào? ASCII dùng ký tự văn bản, mở đầu bằng dấu hai chấm và kiểm tra bằng LRC. Đọc bằng mắt thì dễ, nhưng chậm gấp đôi RTU, nên RTU phổ biến hơn.
7. Kế hoạch đo thực tế
Cột "Đo thực tế" để trống cho đến khi chạy trên phần cứng.
| Thử nghiệm | Cách đo | Giá trị tính toán | Đo thực tế |
|---|---|---|---|
| Đối chiếu khung với phần mềm master (Modbus Poll hoặc tương đương) | Gửi 01 03 00 00 00 02, so phản hồi và CRC |
Phản hồi 01 03 04 ... 5B E1 với dữ liệu mẫu |
(điền sau) |
| Khoảng lặng 3,5 ký tự thực tế ở 9600 và 115200 | Logic analyzer | 4,01 ms và 1,75 ms | (điền sau) |
| Thời gian phản hồi (từ cuối yêu cầu đến đầu phản hồi) | Logic analyzer | Phụ thuộc xử lý của slave | (điền sau) |
| Số lần đọc 2 thanh ghi mỗi giây ở các tốc độ | Phần mềm master đếm | 36 ở 9600, 195 ở 115200 (tối đa lý thuyết) | (điền sau) |
| Đọc 125 thanh ghi: thời gian một giao dịch | Logic analyzer hoặc phần mềm master | 301 ms ở 9600, 25 ms ở 115200 | (điền sau) |
| Phản ứng khi sai CRC cố ý | Gửi khung hỏng CRC, quan sát slave | Không trả lời | (điền sau) |
| Modbus TCP: độ trễ yêu cầu-phản hồi | Wireshark hoặc công cụ master TCP | Phụ thuộc mạng và ngăn xếp | (điền sau) |
Kết luận
CRC gửi byte thấp trước. Biên khung RTU xác định bằng khoảng lặng, nên trên STM32 dùng DMA cộng ngắt IDLE là hợp lý. Đọc dữ liệu theo khối để tận dụng băng thông.
Phần 8 chuyển sang 1-Wire: giao thức một dây của cảm biến nhiệt DS18B20, nơi từng khe thời gian vài micro giây quyết định đọc đúng hay sai.
Để kiểm tra CRC hoặc giải mã giá trị float trong hai thanh ghi, có thể dùng máy tính CRC và Hex Converter của Capitlab.
Tài liệu tham khảo
- MODBUS RTU Frame Detection for MCU Implementation
- Modbus RTU CRC-16 Calculation: Algorithm and Implementation Guide
- Modbus Application Protocol Specification, Modbus over Serial Line, Modbus Messaging on TCP/IP Implementation Guide (modbus.org)