返回博客
Modbus RTU 协议从零实现:帧结构、CRC 校验与功能码解析

Modbus RTU 协议从零实现:帧结构、CRC 校验与功能码解析

2025年3月10日Modbus RTU, STM32, 通讯协议

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 通讯。