RTU 帧结构
Modbus RTU(Remote Terminal Unit)帧由四部分组成:从站地址、功能码、数据域、CRC 校验。帧格式紧凑,无起始和结束字节,通过 3.5 字符空闲时间判断帧边界。
| 从站地址 | 功能码 | 数据域 | CRC-16 |
| 1字节 | 1字节 | N字节 | 2字节 |
从站地址范围 1-247,0 为广播地址,248-255 保留。功能码定义请求类型,数据域长度随功能码变化。CRC-16 使用多项式 0xA001,低字节在前高字节在后。
CRC-16/Modbus 算法
CRC(Cyclic Redundancy Check,循环冗余校验)用于检测数据传输错误。Modbus RTU 采用 CRC-16-IBM 算法,多项式为 0xA001(即 x^16 + x^15 + x^2 + 1 的反转多项式)。
计算法实现
计算法逐字节处理,适用于内存受限场景:
#include <stdint.h>
uint16_t CRC16_Modbus(const uint8_t *data, uint16_t length) {
uint16_t crc = 0xFFFF;
for (uint16_t i = 0; i < length; i++) {
crc ^= data[i];
for (uint8_t j = 0; j < 8; j++) {
if (crc & 0x0001) {
crc = (crc >> 1) ^ 0xA001;
} else {
crc >>= 1;
}
}
}
return crc;
}
查表法实现
查表法预先计算 256 字节的 CRC 表,速度更快,适合高频场景:
#include <stdint.h>
static const uint16_t crc_table[256] = {
0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241,
0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440,
0xCC01, 0x0CC0, 0x0D80, 0xCD41, 0x0F00, 0xCFC1, 0xCE81, 0x0E40,
0x0A00, 0xCAC1, 0xCB81, 0x0B40, 0xC901, 0x09C0, 0x0880, 0xC841,
0xD801, 0x18C0, 0x1980, 0xD941, 0x1B00, 0xDBC1, 0xDA81, 0x1A40,
0x1E00, 0xDEC1, 0xDF81, 0x1F40, 0xDD01, 0x1DC0, 0x1C80, 0xDC41,
0x1400, 0xD4C1, 0xD581, 0x1540, 0xD701, 0x17C0, 0x1680, 0xD641,
0xD201, 0x12C0, 0x1380, 0xD341, 0x1100, 0xD1C1, 0xD081, 0x1040,
0xF001, 0x30C0, 0x3180, 0xF141, 0x3300, 0xF3C1, 0xF281, 0x3240,
0x3600, 0xF6C1, 0xF781, 0x3740, 0xF501, 0x35C0, 0x3480, 0xF441,
0x3C00, 0xFCC1, 0xFD81, 0x3D40, 0xFF01, 0x3FC0, 0x3E80, 0xFE41,
0xFA01, 0x3AC0, 0x3B80, 0xFB41, 0x3900, 0xF9C1, 0xF881, 0x3840,
0x2800, 0xE8C1, 0xE981, 0x2940, 0xEB01, 0x2BC0, 0x2A80, 0xEA41,
0xEE01, 0x2EC0, 0x2F80, 0xEF41, 0x2D00, 0xEDC1, 0xEC81, 0x2C40,
0xE401, 0x24C0, 0x2580, 0xE541, 0x2700, 0xE7C1, 0xE681, 0x2640,
0x2200, 0xE2C1, 0xE381, 0x2340, 0xE101, 0x21C0, 0x2080, 0xE041,
0xA001, 0x60C0, 0x6180, 0xA141, 0x6300, 0xA3C1, 0xA281, 0x6240,
0x6600, 0xA6C1, 0xA781, 0x6740, 0xA501, 0x65C0, 0x6480, 0xA441,
0x6C00, 0xACC1, 0xAD81, 0x6D40, 0xAF01, 0x6FC0, 0x6E80, 0xAE41,
0xAA01, 0x6AC0, 0x6B80, 0xAB41, 0x6900, 0xA9C1, 0xA881, 0x6840,
0x7800, 0xB8C1, 0xB981, 0x7940, 0xBB01, 0x7BC0, 0x7A80, 0xBA41,
0xBE01, 0x7EC0, 0x7F80, 0xBF41, 0x7D00, 0xBDC1, 0xBC81, 0x7C40,
0xB401, 0x74C0, 0x7580, 0xB541, 0x7700, 0xB7C1, 0xB681, 0x7640,
0x7200, 0xB2C1, 0xB381, 0x7340, 0xB101, 0x71C0, 0x7080, 0xB041,
0x5000, 0x90C1, 0x9181, 0x5140, 0x9301, 0x53C0, 0x5280, 0x9241,
0x9601, 0x56C0, 0x5780, 0x9741, 0x5500, 0x95C1, 0x9481, 0x5440,
0x9C01, 0x5CC0, 0x5D80, 0x9D41, 0x5F00, 0x9FC1, 0x9E81, 0x5E40,
0x5A00, 0x9AC1, 0x9B81, 0x5B40, 0x9901, 0x59C0, 0x5880, 0x9841,
0x8801, 0x48C0, 0x4980, 0x8941, 0x4B00, 0x8BC1, 0x8A81, 0x4A40,
0x4E00, 0x8EC1, 0x8F81, 0x4F40, 0x8D01, 0x4DC0, 0x4C80, 0x8C41,
0x4400, 0x84C1, 0x8581, 0x4540, 0x8701, 0x47C0, 0x4680, 0x8641,
0x8201, 0x42C0, 0x4380, 0x8341, 0x4100, 0x81C1, 0x8081, 0x4040
};
uint16_t CRC16_Modbus_Table(const uint8_t *data, uint16_t length) {
uint16_t crc = 0xFFFF;
for (uint16_t i = 0; i < length; i++) {
uint8_t index = (crc ^ data[i]) & 0xFF;
crc = (crc >> 8) ^ crc_table[index];
}
return crc;
}
查表法将 CRC 计算复杂度从 O(n×8) 降低到 O(n),适合实时性要求高的应用。
3.5 字符超时判帧
Modbus RTU 无起始和结束标志,通过总线空闲时间判断帧边界。协议规定:接收端检测到 3.5 个字符时间的总线空闲时,认为当前帧结束。发送端在发送下一帧前,需保持 3.5 字符空闲时间。
3.5 字符时间计算:
字符时间 = (起始位 + 数据位 + 校验位 + 停止位) / 波特率
3.5 字符时间 = 3.5 × 字符时间
8N1(8数据位、无校验、1停止位)配置下,19200 波特率的 3.5 字符时间约为 2.02ms。
STM32 实现超时检测:
#include "stm32f1xx_hal.h"
#define MODBUS_TIMEOUT_MS(parity) ((parity) ? 2 : 2) // 根据校验位调整
typedef struct {
uint8_t rx_buffer[256];
uint16_t rx_index;
uint32_t last_byte_time;
uint8_t frame_ready;
uint8_t parity; // 0=无校验, 1=奇校验, 2=偶校验
} Modbus_RTU_t;
Modbus_RTU_t modbus_rtu;
void Modbus_UART_RxCallback(uint8_t byte) {
modbus_rtu.rx_buffer[modbus_rtu.rx_index++] = byte;
modbus_rtu.last_byte_time = HAL_GetTick();
}
void Modbus_ProcessTimeout(void) {
uint32_t timeout = MODBUS_TIMEOUT_MS(modbus_rtu.parity);
if (modbus_rtu.rx_index > 0 &&
(HAL_GetTick() - modbus_rtu.last_byte_time >= timeout)) {
modbus_rtu.frame_ready = 1;
}
}
功能码详解
Modbus 定义了多种功能码,常用功能码包括读取线圈/寄存器、写入线圈/寄存器。
功能码 01:读取输出线圈
请求格式:
| 从站地址 | 功能码 | 起始地址高位 | 起始地址低位 | 线圈数量高位 | 线圈数量低位 | CRC高位 | CRC低位 |
| 1字节 | 0x01 | 1字节 | 1字节 | 1字节 | 1字节 | 1字节 | 1字节 |
响应格式:
| 从站地址 | 功能码 | 字节数 | 线圈状态(按位) | CRC高位 | CRC低位 |
| 1字节 | 0x01 | 1字节 | N字节 | 1字节 | 1字节 |
线圈状态按位打包,每个线圈占 1 bit,不足 8 位补 0。
功能码 02:读取离散输入
格式与功能码 01 相同,区别在于读取的是离散输入而非输出线圈。
功能码 03:读取保持寄存器
请求格式:
| 从站地址 | 功能码 | 起始地址高位 | 起始地址低位 | 寄存器数量高位 | 寄存器数量低位 | CRC高位 | CRC低位 |
| 1字节 | 0x03 | 1字节 | 1字节 | 1字节 | 1字节 | 1字节 | 1字节 |
响应格式:
| 从站地址 | 功能码 | 字节数 | 寄存器值(每寄存器2字节) | CRC高位 | CRC低位 |
| 1字节 | 0x03 | 1字节 | 2×N字节 | 1字节 | 1字节 |
寄存器地址从 0 开始,协议文档中的 40001 对应地址 0。
功能码 05:写入单个线圈
请求格式:
| 从站地址 | 功能码 | 输出地址高位 | 输出地址低位 | 输出值高位 | 输出值低位 | CRC高位 | CRC低位 |
| 1字节 | 0x05 | 1字节 | 1字节 | 0xFF00 | 0x0000 | 1字节 | 1字节 |
输出值 0xFF00 表示 ON,0x0000 表示 OFF。响应与请求相同(回显)。
功能码 06:写入单个寄存器
请求格式:
| 从站地址 | 功能码 | 寄存器地址高位 | 寄存器地址低位 | 寄存器值高位 | 寄存器值低位 | CRC高位 | CRC低位 |
| 1字节 | 0x06 | 1字节 | 1字节 | 1字节 | 1字节 | 1字节 | 1字节 |
响应与请求相同(回显)。
功能码 0F(15):写入多个线圈
请求格式:
| 从站地址 | 功能码 | 起始地址高位 | 起始地址低位 | 线圈数量高位 | 线圈数量低位 | 字节数 | 线圈值 | CRC高位 | CRC低位 |
| 1字节 | 0x0F | 1字节 | 1字节 | 1字节 | 1字节 | 1字节 | N字节 | 1字节 | 1字节 |
响应格式:
| 从站地址 | 功能码 | 起始地址高位 | 起始地址低位 | 线圈数量高位 | 线圈数量低位 | CRC高位 | CRC低位 |
| 1字节 | 0x0F | 1字节 | 1字节 | 1字节 | 1字节 | 1字节 | 1字节 |
功能码 10(16):写入多个寄存器
请求格式:
| 从站地址 | 功能码 | 起始地址高位 | 起始地址低位 | 寄存器数量高位 | 寄存器数量低位 | 字节数 | 寄存器值 | CRC高位 | CRC低位 |
| 1字节 | 0x10 | 1字节 | 1字节 | 1字节 | 1字节 | 1字节 | 2×N字节 | 1字节 | 1字节 |
响应格式:
| 从站地址 | 功能码 | 起始地址高位 | 起始地址低位 | 寄存器数量高位 | 寄存器数量低位 | CRC高位 | CRC低位 |
| 1字节 | 0x10 | 1字节 | 1字节 | 1字节 | 1字节 | 1字节 | 1字节 |
异常响应码
从站无法执行请求时返回异常响应,功能码最高位置 1,后跟异常码。
| 从站地址 | 功能码+0x80 | 异常码 | CRC高位 | CRC低位 |
| 1字节 | 1字节 | 1字节 | 1字节 | 1字节 |
常见异常码:
| 异常码 | 含义 |
|---|---|
| 0x01 | 非法功能码 |
| 0x02 | 非法数据地址 |
| 0x03 | 非法数据值 |
| 0x04 | 从站设备故障 |
| 0x05 | 确认(需长时间执行) |
| 0x06 | 从站设备忙 |
| 0x07 | 负极性确认 |
| 0x08 | 内存奇偶错误 |
从站协议栈完整实现
以下代码实现完整的 Modbus RTU 从站协议栈,支持功能码 01/02/03/05/06/0F/10。
#include "stm32f1xx_hal.h"
#include <string.h>
#define MODBUS_SLAVE_ADDR 1
#define MODBUS_MAX_COILS 128
#define MODBUS_MAX_REGISTERS 100
// 线圈状态存储(按位)
static uint8_t coils[MODBUS_MAX_COILS / 8] = {0};
// 离散输入存储(按位)
static uint8_t discrete_inputs[MODBUS_MAX_COILS / 8] = {0};
// 保持寄存器存储
static uint16_t holding_registers[MODBUS_MAX_REGISTERS] = {0};
// 输入寄存器存储
static uint16_t input_registers[MODBUS_MAX_REGISTERS] = {0};
typedef struct {
uint8_t slave_addr;
uint8_t function_code;
uint16_t data_addr;
uint16_t data_len;
uint8_t *data_ptr;
uint16_t data_size;
} Modbus_Request_t;
typedef struct {
uint8_t buffer[256];
uint16_t length;
} Modbus_Response_t;
// 读取单个线圈状态
static uint8_t Read_Coil(uint16_t addr) {
if (addr >= MODBUS_MAX_COILS) return 0;
return (coils[addr / 8] >> (addr % 8)) & 0x01;
}
// 写入单个线圈状态
static void Write_Coil(uint16_t addr, uint8_t value) {
if (addr >= MODBUS_MAX_COILS) return;
if (value) {
coils[addr / 8] |= (1 << (addr % 8));
} else {
coils[addr / 8] &= ~(1 << (addr % 8));
}
}
// 功能码 01:读取输出线圈
static uint8_t Modbus_FC01_Read_Coils(Modbus_Request_t *req, Modbus_Response_t *resp) {
uint16_t start_addr = req->data_addr;
uint16_t coil_count = req->data_len;
if (start_addr + coil_count > MODBUS_MAX_COILS) {
return 0x02; // 非法数据地址
}
uint8_t byte_count = (coil_count + 7) / 8;
resp->buffer[2] = byte_count;
for (uint16_t i = 0; i < coil_count; i++) {
if (Read_Coil(start_addr + i)) {
resp->buffer[3 + i / 8] |= (1 << (i % 8));
}
}
resp->length = 3 + byte_count;
return 0;
}
// 功能码 02:读取离散输入
static uint8_t Modbus_FC02_Read_Discrete_Inputs(Modbus_Request_t *req, Modbus_Response_t *resp) {
uint16_t start_addr = req->data_addr;
uint16_t input_count = req->data_len;
if (start_addr + input_count > MODBUS_MAX_COILS) {
return 0x02;
}
uint8_t byte_count = (input_count + 7) / 8;
resp->buffer[2] = byte_count;
for (uint16_t i = 0; i < input_count; i++) {
uint16_t addr = start_addr + i;
if (discrete_inputs[addr / 8] & (1 << (addr % 8))) {
resp->buffer[3 + i / 8] |= (1 << (i % 8));
}
}
resp->length = 3 + byte_count;
return 0;
}
// 功能码 03:读取保持寄存器
static uint8_t Modbus_FC03_Read_Holding_Registers(Modbus_Request_t *req, Modbus_Response_t *resp) {
uint16_t start_addr = req->data_addr;
uint16_t reg_count = req->data_len;
if (start_addr + reg_count > MODBUS_MAX_REGISTERS) {
return 0x02;
}
uint8_t byte_count = reg_count * 2;
resp->buffer[2] = byte_count;
for (uint16_t i = 0; i < reg_count; i++) {
resp->buffer[3 + i * 2] = holding_registers[start_addr + i] >> 8;
resp->buffer[4 + i * 2] = holding_registers[start_addr + i] & 0xFF;
}
resp->length = 3 + byte_count;
return 0;
}
// 功能码 05:写入单个线圈
static uint8_t Modbus_FC05_Write_Single_Coil(Modbus_Request_t *req, Modbus_Response_t *resp) {
uint16_t addr = req->data_addr;
uint16_t value = req->data_len;
if (addr >= MODBUS_MAX_COILS) {
return 0x02;
}
if (value == 0xFF00) {
Write_Coil(addr, 1);
} else if (value == 0x0000) {
Write_Coil(addr, 0);
} else {
return 0x03; // 非法数据值
}
// 回显请求
memcpy(resp->buffer, req->data_ptr - 4, 6);
resp->length = 6;
return 0;
}
// 功能码 06:写入单个寄存器
static uint8_t Modbus_FC06_Write_Single_Register(Modbus_Request_t *req, Modbus_Response_t *resp) {
uint16_t addr = req->data_addr;
uint16_t value = req->data_len;
if (addr >= MODBUS_MAX_REGISTERS) {
return 0x02;
}
holding_registers[addr] = value;
// 回显请求
memcpy(resp->buffer, req->data_ptr - 4, 6);
resp->length = 6;
return 0;
}
// 功能码 0F:写入多个线圈
static uint8_t Modbus_FC0F_Write_Multiple_Coils(Modbus_Request_t *req, Modbus_Response_t *resp) {
uint16_t start_addr = req->data_addr;
uint16_t coil_count = req->data_len;
uint8_t byte_count = req->data_ptr[0];
if (start_addr + coil_count > MODBUS_MAX_COILS) {
return 0x02;
}
if (byte_count != (coil_count + 7) / 8) {
return 0x03;
}
for (uint16_t i = 0; i < coil_count; i++) {
uint8_t value = (req->data_ptr[1 + i / 8] >> (i % 8)) & 0x01;
Write_Coil(start_addr + i, value);
}
// 响应:起始地址 + 线圈数量
resp->buffer[2] = start_addr >> 8;
resp->buffer[3] = start_addr & 0xFF;
resp->buffer[4] = coil_count >> 8;
resp->buffer[5] = coil_count & 0xFF;
resp->length = 6;
return 0;
}
// 功能码 10:写入多个寄存器
static uint8_t Modbus_FC10_Write_Multiple_Registers(Modbus_Request_t *req, Modbus_Response_t *resp) {
uint16_t start_addr = req->data_addr;
uint16_t reg_count = req->data_len;
uint8_t byte_count = req->data_ptr[0];
if (start_addr + reg_count > MODBUS_MAX_REGISTERS) {
return 0x02;
}
if (byte_count != reg_count * 2) {
return 0x03;
}
for (uint16_t i = 0; i < reg_count; i++) {
holding_registers[start_addr + i] =
(req->data_ptr[1 + i * 2] << 8) | req->data_ptr[2 + i * 2];
}
// 响应:起始地址 + 寄存器数量
resp->buffer[2] = start_addr >> 8;
resp->buffer[3] = start_addr & 0xFF;
resp->buffer[4] = reg_count >> 8;
resp->buffer[5] = reg_count & 0xFF;
resp->length = 6;
return 0;
}
// 解析请求并生成响应
uint8_t Modbus_Process_Request(uint8_t *rx_buf, uint16_t rx_len, uint8_t *tx_buf, uint16_t *tx_len) {
Modbus_Request_t req;
Modbus_Response_t resp;
uint8_t exception_code = 0;
// 帧长度检查
if (rx_len < 4) return 0;
// CRC 校验
uint16_t crc_calc = CRC16_Modbus_Table(rx_buf, rx_len - 2);
uint16_t crc_recv = (rx_buf[rx_len - 1] << 8) | rx_buf[rx_len - 2];
if (crc_calc != crc_recv) return 0;
// 从站地址检查
req.slave_addr = rx_buf[0];
if (req.slave_addr != MODBUS_SLAVE_ADDR && req.slave_addr != 0) {
*tx_len = 0;
return 0; // 不是发给本站,不响应
}
// 解析请求
req.function_code = rx_buf[1];
req.data_addr = (rx_buf[2] << 8) | rx_buf[3];
req.data_ptr = &rx_buf[4];
// 初始化响应
resp.buffer[0] = req.slave_addr;
resp.buffer[1] = req.function_code;
resp.length = 0;
// 根据功能码分发处理
switch (req.function_code) {
case 0x01:
req.data_len = (rx_buf[4] << 8) | rx_buf[5];
exception_code = Modbus_FC01_Read_Coils(&req, &resp);
break;
case 0x02:
req.data_len = (rx_buf[4] << 8) | rx_buf[5];
exception_code = Modbus_FC02_Read_Discrete_Inputs(&req, &resp);
break;
case 0x03:
req.data_len = (rx_buf[4] << 8) | rx_buf[5];
exception_code = Modbus_FC03_Read_Holding_Registers(&req, &resp);
break;
case 0x05:
req.data_len = (rx_buf[4] << 8) | rx_buf[5];
exception_code = Modbus_FC05_Write_Single_Coil(&req, &resp);
break;
case 0x06:
req.data_len = (rx_buf[4] << 8) | rx_buf[5];
exception_code = Modbus_FC06_Write_Single_Register(&req, &resp);
break;
case 0x0F:
req.data_len = (rx_buf[4] << 8) | rx_buf[5];
exception_code = Modbus_FC0F_Write_Multiple_Coils(&req, &resp);
break;
case 0x10:
req.data_len = (rx_buf[4] << 8) | rx_buf[5];
exception_code = Modbus_FC10_Write_Multiple_Registers(&req, &resp);
break;
default:
exception_code = 0x01; // 非法功能码
break;
}
// 构造响应
if (exception_code) {
resp.buffer[1] = req.function_code | 0x80; // 异常标志
resp.buffer[2] = exception_code;
resp.length = 3;
}
// 添加 CRC
uint16_t crc = CRC16_Modbus_Table(resp.buffer, resp.length);
resp.buffer[resp.length] = crc & 0xFF;
resp.buffer[resp.length + 1] = crc >> 8;
resp.length += 2;
// 复制响应数据
memcpy(tx_buf, resp.buffer, resp.length);
*tx_len = resp.length;
return 1;
}
以上代码实现了完整的 Modbus RTU 从站协议栈,可直接集成到 STM32 项目中。配合 DMA+空闲中断接收机制(详见后续文章),可实现高效可靠的 Modbus RTU 通讯。