Modbus RTU/TCP协议栈手写实现:主从站设计、功能码、CRC、异常响应

文章目录


每日一句正能量

两件会阻碍我们自由的事:活在过去和活在他人眼中。

过去无法改变,执着于它会困住现在;活在他人眼中,等于把评判自己的尺子交出去。真正的自由,是能放下过往,也能不在意别人的目光。

一、引言

在工业自动化领域,Modbus协议以其简单、开放、免版税的特点,成为事实上的工业通信标准。从PLC、变频器到传感器、智能仪表,几乎所有工业设备都支持Modbus通信。本文将从零开始,手写实现一个完整的Modbus RTU/TCP协议栈,涵盖主从站设计、功能码解析、CRC校验、异常响应处理等核心技术,为嵌入式开发者提供可直接落地的工程实现方案。

二、Modbus协议栈架构概览

Modbus协议栈采用经典的分层架构设计,在物理层之上构建完整的通信框架:

协议栈的核心层次包括:

  1. 物理层:RS-232/RS-485串口或以太网接口
  2. 传输层:Modbus RTU使用裸串口传输,Modbus TCP使用TCP/IP(端口502)
  3. 协议层:功能码路由、帧封装/解封装、CRC校验、异常响应处理
  4. 应用层:寄存器映射、业务逻辑、数据模型

Modbus协议栈的设计哲学是简单即可靠。相比CANopen等复杂协议,Modbus的帧格式简洁、状态机清晰,非常适合资源受限的嵌入式系统。

三、Modbus RTU帧结构与数据模型

3.1 RTU帧结构

Modbus RTU帧由四个部分组成:

字段 长度 说明
从站地址 1字节 1-247有效,0为广播地址
功能码 1字节 定义操作类型
数据域 0-252字节 变长,内容由功能码决定
CRC校验 2字节 低字节在前,高字节在后

帧间隔规则是RTU的关键特性:

  • 帧内字节间隔:≤1.5个字符时间(T1.5),超时则认为帧错误
  • 帧间间隔:≥3.5个字符时间(T3.5),用于帧边界识别

以9600bps、8N1配置为例:

  • 1个字符时间 = 11位 / 9600bps ≈ 1.146ms
  • T1.5 ≈ 1.72ms,T3.5 ≈ 4.01ms

3.2 数据模型

Modbus定义了四张数据表,对应不同的功能码:

数据表 地址范围 访问类型 数据宽度 读功能码 写功能码
线圈状态 (Coil) 00001-09999 读写 1位 0x01 0x05/0x0F
离散输入 (Discrete Input) 10001-19999 只读 1位 0x02 -
保持寄存器 (Holding Register) 40001-49999 读写 16位 0x03 0x06/0x10
输入寄存器 (Input Register) 30001-39999 只读 16位 0x04 -

注意:Modbus协议文档中的地址是1-based(40001开始),但实际通信时使用0-based地址(0x0000开始),开发时务必注意这个"差一错误"。

四、功能码详解与从站状态机

4.1 常用功能码

Modbus功能码采用1字节编码,最高位为0表示正常功能码,为1表示异常响应:

功能码 名称 操作 数据域内容 最大数量
0x01 读线圈 起始地址(2B) + 数量(2B) 2000
0x02 读离散输入 起始地址(2B) + 数量(2B) 2000
0x03 读保持寄存器 起始地址(2B) + 数量(2B) 125
0x04 读输入寄存器 起始地址(2B) + 数量(2B) 125
0x05 写单个线圈 地址(2B) + 值(2B) 1
0x06 写单个寄存器 地址(2B) + 值(2B) 1
0x0F 写多个线圈 地址+数量+字节数+数据 1968
0x10 写多个寄存器 地址+数量+字节数+数据 123
0x17 读/写多个寄存器 读写 读地址+数量+写地址+数量+字节数+数据 121

4.2 从站状态机实现

从站采用状态机驱动的方式处理通信:

c 复制代码
/* 从站状态定义 */
typedef enum {
    SLAVE_STATE_IDLE = 0,           // 空闲,等待接收
    SLAVE_STATE_RECEIVING,          // 接收中
    SLAVE_STATE_FRAME_READY,        // 帧接收完成,待解析
    SLAVE_STATE_PROCESSING,         // 处理请求
    SLAVE_STATE_RESPONDING,         // 发送响应
    SLAVE_STATE_EXCEPTION           // 发送异常响应
} SlaveState_t;

/* 从站主处理函数 */
void Modbus_SlavePoll(void) {
    switch (g_slaveState) {
        case SLAVE_STATE_IDLE:
            /* 启动接收,等待UART中断 */
            UART_StartReceive(g_rxBuffer, sizeof(g_rxBuffer));
            g_slaveState = SLAVE_STATE_RECEIVING;
            break;
            
        case SLAVE_STATE_RECEIVING:
            /* 检查T3.5超时,判断帧是否接收完成 */
            if (Timer_Elapsed(g_rxStartTime) > T35_TIMEOUT) {
                if (g_rxIndex > 0) {
                    g_slaveState = SLAVE_STATE_FRAME_READY;
                }
            }
            break;
            
        case SLAVE_STATE_FRAME_READY:
            /* 验证帧格式 */
            if (Modbus_ValidateFrame(g_rxBuffer, g_rxIndex)) {
                g_slaveState = SLAVE_STATE_PROCESSING;
            } else {
                /* 帧错误,丢弃并返回空闲 */
                g_rxIndex = 0;
                g_slaveState = SLAVE_STATE_IDLE;
            }
            break;
            
        case SLAVE_STATE_PROCESSING:
            /* 路由到对应的功能码处理函数 */
            Modbus_ProcessFunctionCode(g_rxBuffer, g_rxIndex);
            g_slaveState = SLAVE_STATE_RESPONDING;
            break;
            
        case SLAVE_STATE_RESPONDING:
            /* 发送正常响应 */
            UART_Send(g_txBuffer, g_txLength);
            g_slaveState = SLAVE_STATE_IDLE;
            break;
            
        case SLAVE_STATE_EXCEPTION:
            /* 发送异常响应 */
            UART_Send(g_exceptionBuffer, 5);  // 地址+功能码+异常码+CRC
            g_slaveState = SLAVE_STATE_IDLE;
            break;
    }
}

五、CRC16校验算法实现

CRC16是Modbus RTU数据完整性的核心保障,采用多项式 x^16 + x^15 + x^2 + 1(0xA001)。

5.1 逐位计算法

c 复制代码
/* CRC16逐位计算 - 适合ROM极小的系统 */
uint16_t Modbus_CRC16_Bitwise(uint8_t *data, uint16_t len) {
    uint16_t crc = 0xFFFF;
    
    for (uint16_t i = 0; i < len; i++) {
        crc ^= data[i];  // 与当前字节异或
        
        for (int j = 0; j < 8; j++) {
            if (crc & 0x0001) {
                crc = (crc >> 1) ^ 0xA001;
            } else {
                crc >>= 1;
            }
        }
    }
    
    return crc;
}

5.2 查表法优化

查表法以512字节ROM空间换取8倍速度提升,是嵌入式系统的首选方案:

c 复制代码
/* CRC16查表法 - 预计算256个CRC值 */
static const uint16_t crcTable[256] = {
    0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241,
    0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440,
    /* ... 共256项 ... */
    0x4400, 0x84C1, 0x8581, 0x4540, 0x8701, 0x47C0, 0x4680, 0x8641,
    0x8201, 0x42C0, 0x4380, 0x8341, 0x4100, 0x81C1, 0x8081, 0x4040
};

uint16_t Modbus_CRC16(uint8_t *data, uint16_t len) {
    uint16_t crc = 0xFFFF;
    
    for (uint16_t i = 0; i < len; i++) {
        crc = (crc >> 8) ^ crcTable[(crc ^ data[i]) & 0xFF];
    }
    
    return crc;
}

5.3 CRC校验集成

c 复制代码
/* 帧验证 */
bool Modbus_ValidateFrame(uint8_t *frame, uint16_t len) {
    if (len < 4) return false;  // 最小帧: 地址+功能码+CRC(2)
    
    uint16_t rxCrc = (frame[len-1] << 8) | frame[len-2];
    uint16_t calcCrc = Modbus_CRC16(frame, len - 2);
    
    return (rxCrc == calcCrc);
}

/* 帧构建时附加CRC */
void Modbus_AppendCRC(uint8_t *frame, uint16_t len) {
    uint16_t crc = Modbus_CRC16(frame, len);
    frame[len] = crc & 0xFF;      // CRC低字节
    frame[len + 1] = crc >> 8;    // CRC高字节
}

六、异常响应机制

异常响应是Modbus协议健壮性的重要体现,当从站无法处理请求时,必须返回明确的错误信息。

6.1 异常响应格式

异常响应帧结构:

  • 从站地址:与请求相同
  • 功能码:请求功能码 | 0x80(最高位置1)
  • 异常码:1字节,描述错误原因
  • CRC:2字节

6.2 标准异常码

异常码 名称 触发条件 常见原因
0x01 非法功能码 不支持的功能码 功能码未实现或已禁用
0x02 非法数据地址 请求地址不存在 地址越界或寄存器未配置
0x03 非法数据值 请求数据不合法 数量超限或字节数不匹配
0x04 从站设备故障 处理时发生错误 硬件故障或内部异常
0x05 确认 请求已接收但需时间处理 仅用于0x06/0x08功能码
0x06 从站忙 正在处理其他命令 设备忙,稍后重试
0x08 存储奇偶性错误 存储器访问错误 EEPROM/Flash损坏
0x0A 网关路径不可用 网关无法路由 网络路径故障
0x0B 网关目标无响应 目标设备未响应 目标设备离线

6.3 异常处理实现

c 复制代码
/* 发送异常响应 */
void Modbus_SendException(uint8_t funcCode, uint8_t exceptionCode) {
    uint8_t resp[5];
    
    resp[0] = g_slaveAddr;                  // 从站地址
    resp[1] = funcCode | 0x80;              // 功能码最高位置1
    resp[2] = exceptionCode;                // 异常码
    
    uint16_t crc = Modbus_CRC16(resp, 3);
    resp[3] = crc & 0xFF;
    resp[4] = crc >> 8;
    
    UART_Send(resp, 5);
}

/* 功能码处理中的异常检查 */
void Modbus_ReadHoldingRegs(uint8_t *req) {
    uint16_t startAddr = (req[2] << 8) | req[3];
    uint16_t quantity  = (req[4] << 8) | req[5];
    
    /* 检查数量范围 */
    if (quantity < 1 || quantity > 125) {
        Modbus_SendException(0x03, 0x03);  // 非法数据值
        return;
    }
    
    /* 检查地址范围 */
    if (startAddr + quantity > HOLDING_REG_MAX) {
        Modbus_SendException(0x03, 0x02);  // 非法数据地址
        return;
    }
    
    /* 正常处理... */
}

七、Modbus TCP帧结构与MBAP头

Modbus TCP在以太网上运行,使用TCP端口502,其帧结构比RTU更简洁:

7.1 MBAP头结构

MBAP(Modbus Application Protocol Header)占7字节:

字段 长度 说明
事务标识符 2字节 请求-响应匹配,主站递增
协议标识符 2字节 固定为0x0000
长度字段 2字节 后续字节数(单元ID + PDU)
单元标识符 1字节 从站地址(类似RTU的地址)

7.2 RTU vs TCP关键差异

特性 Modbus RTU Modbus TCP
物理层 RS-232/RS-485 以太网
传输层 TCP/IP(端口502)
帧头 无(依赖T3.5) MBAP头(7字节)
地址识别 从站地址字节 单元标识符
错误校验 CRC16 TCP校验和(无CRC)
帧间隔 T3.5字符时间 TCP流边界
广播支持 支持(地址0) 不支持
多主站 不支持 支持

7.3 Modbus TCP实现

c 复制代码
/* TCP帧结构 */
typedef struct {
    uint16_t transactionId;   // 事务标识符
    uint16_t protocolId;      // 协议标识符 = 0x0000
    uint16_t length;          // 长度
    uint8_t  unitId;          // 单元标识符
    uint8_t  pdu[252];        // PDU(功能码+数据)
} ModbusTCP_Frame;

/* TCP帧解析 */
bool ModbusTCP_ParseFrame(uint8_t *rawData, uint16_t len, ModbusTCP_Frame *frame) {
    if (len < 7) return false;  // MBAP头至少7字节
    
    frame->transactionId = (rawData[0] << 8) | rawData[1];
    frame->protocolId    = (rawData[2] << 8) | rawData[3];
    frame->length        = (rawData[4] << 8) | rawData[5];
    frame->unitId        = rawData[6];
    
    if (frame->protocolId != 0x0000) return false;
    if (frame->length != len - 6) return false;
    
    memcpy(frame->pdu, &rawData[7], frame->length - 1);
    
    return true;
}

/* TCP响应构建 */
uint16_t ModbusTCP_BuildResponse(ModbusTCP_Frame *req, uint8_t *respPdu, uint16_t pduLen, uint8_t *outBuf) {
    outBuf[0] = req->transactionId >> 8;
    outBuf[1] = req->transactionId & 0xFF;
    outBuf[2] = 0x00;  // protocolId高
    outBuf[3] = 0x00;  // protocolId低
    outBuf[4] = (pduLen + 1) >> 8;
    outBuf[5] = (pduLen + 1) & 0xFF;
    outBuf[6] = req->unitId;
    
    memcpy(&outBuf[7], respPdu, pduLen);
    
    return 7 + pduLen;
}

八、主站设计架构与请求-响应流程

8.1 主站核心模块

主站相比从站更复杂,需要管理请求队列、超时重试、事务匹配等:

c 复制代码
/* 主站请求结构 */
typedef struct {
    uint8_t  slaveAddr;           // 目标从站地址
    uint8_t  funcCode;            // 功能码
    uint8_t  reqData[252];        // 请求数据
    uint16_t reqLen;              // 请求长度
    uint8_t  respData[252];       // 响应数据缓冲区
    uint16_t respLen;             // 响应长度
    uint16_t transactionId;       // 事务ID(TCP)
    uint32_t timeout;             // 超时时间(ms)
    uint8_t  retryCount;          // 重试次数
    uint8_t  maxRetry;            // 最大重试次数
    void     (*callback)(uint8_t status, uint8_t *data, uint16_t len);  // 完成回调
} ModbusRequest_t;

/* 主站状态机 */
typedef enum {
    MASTER_STATE_IDLE = 0,
    MASTER_STATE_SENDING,
    MASTER_STATE_WAITING,
    MASTER_STATE_PROCESSING,
    MASTER_STATE_RETRY,
    MASTER_STATE_ERROR
} MasterState_t;

8.2 主站调度器实现

c 复制代码
/* 主站轮询函数 */
void Modbus_MasterPoll(void) {
    static MasterState_t state = MASTER_STATE_IDLE;
    static ModbusRequest_t *currentReq = NULL;
    static uint32_t sendTime = 0;
    
    switch (state) {
        case MASTER_STATE_IDLE:
            /* 从队列取出请求 */
            currentReq = ModbusQueue_Dequeue();
            if (currentReq != NULL) {
                state = MASTER_STATE_SENDING;
            }
            break;
            
        case MASTER_STATE_SENDING:
            /* 构建并发送请求帧 */
            if (g_protocolType == PROTOCOL_RTU) {
                ModbusRTU_SendRequest(currentReq);
            } else {
                ModbusTCP_SendRequest(currentReq);
            }
            sendTime = GetTickCount();
            state = MASTER_STATE_WAITING;
            break;
            
        case MASTER_STATE_WAITING:
            /* 检查响应或超时 */
            if (Modbus_IsResponseReady()) {
                state = MASTER_STATE_PROCESSING;
            } else if (GetTickCount() - sendTime > currentReq->timeout) {
                /* 超时处理 */
                currentReq->retryCount++;
                if (currentReq->retryCount <= currentReq->maxRetry) {
                    state = MASTER_STATE_RETRY;
                } else {
                    state = MASTER_STATE_ERROR;
                }
            }
            break;
            
        case MASTER_STATE_PROCESSING:
            /* 解析响应 */
            if (Modbus_ParseResponse(currentReq)) {
                /* 成功,调用回调 */
                if (currentReq->callback != NULL) {
                    currentReq->callback(0, currentReq->respData, currentReq->respLen);
                }
            } else {
                /* 异常响应 */
                if (currentReq->callback != NULL) {
                    currentReq->callback(currentReq->respData[2], NULL, 0);
                }
            }
            state = MASTER_STATE_IDLE;
            break;
            
        case MASTER_STATE_RETRY:
            /* 重试 */
            state = MASTER_STATE_SENDING;
            break;
            
        case MASTER_STATE_ERROR:
            /* 最终失败 */
            if (currentReq->callback != NULL) {
                currentReq->callback(0xFF, NULL, 0);  // 超时错误
            }
            state = MASTER_STATE_IDLE;
            break;
    }
}

九、RS-485总线拓扑与通信时序

9.1 RS-485总线设计要点

  1. 终端电阻:总线两端各接120Ω电阻,消除信号反射
  2. 偏置电阻:空闲时提供偏置电压,避免误触发
  3. 最大节点数:32个(标准收发器),使用高阻抗收发器可达256个
  4. 最大距离:1200m(9600bps),速率越高距离越短
  5. 半双工控制:主站需要控制DE/RE引脚切换收发状态
c 复制代码
/* RS-485发送控制 */
void RS485_SendData(uint8_t *data, uint16_t len) {
    /* 切换为发送模式 */
    RS485_DE_HIGH();    // 使能发送
    RS485_RE_HIGH();    // 禁用接收
    
    UART_Send(data, len);
    
    /* 等待发送完成 */
    while (!UART_IsTxComplete());
    
    /* 切换回接收模式 */
    RS485_DE_LOW();
    RS485_RE_LOW();
    
    /* 延时确保总线释放 */
    DelayUs(20);  // 根据波特率调整
}

9.2 T3.5帧间隔实现

c 复制代码
/* 使用定时器实现T3.5检测 */
#define BAUDRATE 9600
#define T35_US ((3500000UL + BAUDRATE - 1) / BAUDRATE)  // 3.5字符时间(微秒)

volatile uint32_t g_lastRxTime = 0;
volatile uint16_t g_rxIndex = 0;
volatile uint8_t g_rxBuffer[256];

/* UART接收中断 */
void UART_IRQHandler(void) {
    uint8_t data = UART_ReadByte();
    uint32_t currentTime = GetMicros();
    
    /* 检查帧间隔 */
    if (g_rxIndex > 0 && (currentTime - g_lastRxTime) > T35_US) {
        /* T3.5超时,帧接收完成 */
        g_frameReady = true;
        g_rxIndex = 0;
    }
    
    g_rxBuffer[g_rxIndex++] = data;
    g_lastRxTime = currentTime;
}

/* 定时器轮询检查T3.5 */
void Modbus_PollFrameTimeout(void) {
    if (g_rxIndex > 0 && !g_frameReady) {
        if ((GetMicros() - g_lastRxTime) > T35_US) {
            g_frameReady = true;
        }
    }
}

十、嵌入式代码实现架构

10.1 核心数据结构

c 复制代码
/* 寄存器映射表 */
#define COIL_MAX            2000
#define DISCRETE_INPUT_MAX  2000
#define HOLDING_REG_MAX     125
#define INPUT_REG_MAX       125

typedef struct {
    uint8_t  coils[COIL_MAX / 8 + 1];           // 线圈位图
    uint8_t  discreteInputs[DISCRETE_INPUT_MAX / 8 + 1];  // 离散输入位图
    uint16_t holdingRegs[HOLDING_REG_MAX];      // 保持寄存器
    uint16_t inputRegs[INPUT_REG_MAX];          // 输入寄存器
    
    /* 访问回调 */
    void (*onCoilWrite)(uint16_t addr, bool value);
    void (*onHoldingRegWrite)(uint16_t addr, uint16_t value);
} ModbusRegMap_t;

/* 协议栈配置 */
typedef struct {
    uint8_t          slaveAddr;         // 从站地址
    uint32_t         baudrate;          // 波特率
    ProtocolType_t   protocol;          // RTU/TCP
    ModbusRegMap_t   *regMap;           // 寄存器映射
    uint16_t         responseTimeout;   // 响应超时(ms)
    uint8_t          maxRetry;          // 最大重试次数
} ModbusConfig_t;

10.2 功能码路由表

c 复制代码
/* 功能码处理函数指针 */
typedef void (*FuncHandler_t)(uint8_t *req, uint16_t reqLen);

typedef struct {
    uint8_t        funcCode;
    FuncHandler_t  handler;
    const char     *description;
} FuncCodeRoute_t;

/* 功能码路由表 */
static const FuncCodeRoute_t g_funcRoutes[] = {
    {0x01, Modbus_ReadCoils,           "Read Coils"},
    {0x02, Modbus_ReadDiscreteInputs,  "Read Discrete Inputs"},
    {0x03, Modbus_ReadHoldingRegs,     "Read Holding Registers"},
    {0x04, Modbus_ReadInputRegs,       "Read Input Registers"},
    {0x05, Modbus_WriteSingleCoil,     "Write Single Coil"},
    {0x06, Modbus_WriteSingleReg,      "Write Single Register"},
    {0x0F, Modbus_WriteMultipleCoils,  "Write Multiple Coils"},
    {0x10, Modbus_WriteMultipleRegs,   "Write Multiple Registers"},
    {0x17, Modbus_ReadWriteMultipleRegs, "Read/Write Multiple Registers"},
    {0x00, NULL, NULL}  // 结束标记
};

/* 功能码路由 */
void Modbus_RouteFunctionCode(uint8_t *req, uint16_t reqLen) {
    uint8_t funcCode = req[1];
    
    for (int i = 0; g_funcRoutes[i].handler != NULL; i++) {
        if (g_funcRoutes[i].funcCode == funcCode) {
            g_funcRoutes[i].handler(req, reqLen);
            return;
        }
    }
    
    /* 未找到对应功能码 */
    Modbus_SendException(funcCode, 0x01);  // 非法功能码
}

十一、完整代码实现示例

以下是完整的从站帧处理核心代码:

c 复制代码
/* Modbus协议栈核心实现 */
#include "modbus.h"

/* CRC16查表法 */
static const uint16_t crcTable[256] = {
    0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241,
    /* ... 完整256项 ... */
    0x8201, 0x42C0, 0x4380, 0x8341, 0x4100, 0x81C1, 0x8081, 0x4040
};

uint16_t Modbus_CRC16(uint8_t *data, uint16_t len) {
    uint16_t crc = 0xFFFF;
    for (uint16_t i = 0; i < len; i++) {
        crc = (crc >> 8) ^ crcTable[(crc ^ data[i]) & 0xFF];
    }
    return crc;
}

/* 从站帧处理状态机 */
void Modbus_SlaveProcessFrame(uint8_t *rxBuf, uint16_t len) {
    /* 1. 验证CRC */
    uint16_t rxCrc = (rxBuf[len-1] << 8) | rxBuf[len-2];
    if (Modbus_CRC16(rxBuf, len - 2) != rxCrc) {
        return;  // CRC错误,静默丢弃
    }
    
    /* 2. 地址过滤 */
    uint8_t slaveAddr = rxBuf[0];
    if (slaveAddr != g_slaveAddr && slaveAddr != 0) {
        return;  // 不是发给本站的
    }
    
    /* 3. 功能码路由 */
    uint8_t funcCode = rxBuf[1];
    switch (funcCode) {
        case 0x01: Modbus_ReadCoils(rxBuf); break;
        case 0x03: Modbus_ReadHoldingRegs(rxBuf); break;
        case 0x06: Modbus_WriteSingleReg(rxBuf); break;
        case 0x10: Modbus_WriteMultipleRegs(rxBuf); break;
        default:   Modbus_SendException(funcCode, 0x01); break;
    }
}

/* 读保持寄存器 (0x03) */
void Modbus_ReadHoldingRegs(uint8_t *req) {
    uint16_t startAddr = (req[2] << 8) | req[3];
    uint16_t quantity  = (req[4] << 8) | req[5];
    
    /* 参数校验 */
    if (quantity < 1 || quantity > 125) {
        Modbus_SendException(0x03, 0x03); return;  // 非法数据值
    }
    if (startAddr + quantity > HOLDING_REG_MAX) {
        Modbus_SendException(0x03, 0x02); return;  // 非法数据地址
    }
    
    /* 构建响应 */
    uint8_t resp[256];
    resp[0] = g_slaveAddr;
    resp[1] = 0x03;
    resp[2] = quantity * 2;  // 字节数
    
    for (int i = 0; i < quantity; i++) {
        resp[3 + i*2] = g_holdingRegs[startAddr + i] >> 8;   // 高字节
        resp[4 + i*2] = g_holdingRegs[startAddr + i] & 0xFF;   // 低字节
    }
    
    uint16_t crc = Modbus_CRC16(resp, 3 + quantity * 2);
    resp[3 + quantity*2] = crc & 0xFF;
    resp[4 + quantity*2] = crc >> 8;
    
    UART_Send(resp, 5 + quantity * 2);
}

十二、调试与测试方法

12.1 常用调试工具

工具 用途 特点
Modbus Poll 主站模拟 功能强大,支持脚本
Modbus Slave 从站模拟 易于配置,适合测试主站
QModMaster 开源主站 免费,跨平台
串口助手 查看原始数据 十六进制显示,便于分析
示波器 信号质量分析 检查RS-485波形

12.2 常见问题排查

现象 可能原因 解决方法
无响应 地址错误/波特率不匹配 检查地址和串口参数
CRC错误 线路干扰/终端电阻缺失 检查接线,添加终端电阻
异常码0x02 寄存器地址越界 核对地址映射表
超时 从站处理慢/线路延迟 增加超时时间,检查从站负载
数据错乱 字节序问题 确认大端/小端转换

十三、总结与展望

本文从协议栈架构、帧结构、功能码、CRC校验、异常响应、主从站设计等多个维度,完整阐述了Modbus RTU/TCP协议栈的手写实现方案。关键技术要点包括:

  1. 帧间隔机制是RTU的核心,T3.5超时检测决定了帧边界识别
  2. CRC16查表法以512字节ROM换取8倍性能提升,适合嵌入式
  3. 功能码路由表实现可扩展的代码架构,便于新增功能
  4. 异常响应机制保障协议健壮性,每个错误都有明确反馈
  5. Modbus TCP在RTU基础上增加MBAP头,实现以太网扩展

在实际工业应用中,还需要考虑:

  • 多核RTOS环境下的并发访问与线程安全
  • RS-485总线的电气隔离与浪涌保护
  • Modbus over TCP/UDP的网关设计
  • 安全增强(TLS加密、访问控制)

通过本文的技术实现,开发者可以构建完整的Modbus主从站设备,并具备向多协议网关(Modbus转MQTT/OPC UA)扩展的基础能力。


转载自:https://blog.csdn.net/u014727709/article/details/162512176

欢迎 👍点赞✍评论⭐收藏,欢迎指正