[范例教程] MODBUS RTU通信

[复制链接]
98|34
tifmill 发表于 2026-7-15 21:18 | 显示全部楼层
Modbus RTU 采用严格的“主从架构”。总线上只能有一个主机主动发起请求,从机仅在收到包含自身地址的指令后才进行响应。物理层通常基于 RS-485 半双工总线或 RS-232 串口,通过 MAX485 等电平转换芯片实现 TTL 与差分信号的双向转换。
earlmax 发表于 2026-7-16 10:29 | 显示全部楼层
如何生成MODBUS RTU通信代码?
mollylawrence 发表于 2026-7-16 11:49 | 显示全部楼层
基于51单片机实现的Modbus RTU从站通信代码,支持功能码03和06,包含CRC16校验及串口中断处理。
zerorobert 发表于 2026-7-16 17:45 | 显示全部楼层
对于 RS-485 通信,还需通过 GPIO 精确控制收发方向引脚,并在发送完成后及时切回接收状态,避免总线冲突。
cemaj 发表于 2026-7-17 16:40 | 显示全部楼层
RS-485是半双工通信,必须通过一个GPIO引脚控制MAX485的发送/接收使能。
primojones 发表于 2026-7-17 19:58 | 显示全部楼层
从站地址范围为1~247,主站无地址。
jkl21 发表于 2026-7-18 08:18 | 显示全部楼层
51单片机作为Modbus RTU从站的核心逻辑
mickit 发表于 2026-7-18 13:55 | 显示全部楼层
总线终端匹配电阻以抑制信号反射。
updownq 发表于 2026-7-18 16:33 | 显示全部楼层
RS485收发切换的致命时序              
abotomson 发表于 2026-7-18 16:57 | 显示全部楼层

#include <reg52.h>
#include <string.h>

// 定义数据类型
typedef unsigned char u8;
typedef unsigned int u16;

// 串口配置
#define BAUD 9600       // 波特率
#define FREQ 11059200L  // 晶振频率

// Modbus RTU 地址
#define SLAVE_ADDR 0x01 // 从站地址

// 寄存器配置
#define REG_START 0x0000 // 起始寄存器地址
#define REG_NUM 10       // 保持寄存器数量

// 缓冲区大小
#define RX_BUF_SIZE 20
#define TX_BUF_SIZE 20

// 全局变量
u8 rxBuffer[RX_BUF_SIZE]; // 接收缓冲区
u8 txBuffer[TX_BUF_SIZE]; // 发送缓冲区
u8 rxCount = 0;           // 接收字节计数
u8 rxComplete = 0;        // 接收完成标志
u16 holdReg[REG_NUM];     // 保持寄存器数组

// 函数声明
void UartInit(void);
u16 CRC16(u8 *buffer, u8 length);
void SendResponse(u8 *data, u8 len);
void ProcessModbus(void);

/**
* @brief 串口初始化
* 配置定时器1产生波特率,开启串口中断
*/
void UartInit(void) {
    TMOD &= 0x0F;
    TMOD |= 0x20;          // 定时器1模式2 (8位自动重装)
    TH1 = 256 - (FREQ / 12 / 32 / BAUD);
    TL1 = TH1;
    TR1 = 1;               // 启动定时器1
    SM0 = 0;
    SM1 = 1;               // 串口模式1 (8位UART)
    REN = 1;               // 允许接收
    EA = 1;                // 开启总中断
    ES = 1;                // 开启串口中断
}

/**
* @brief 计算CRC16校验码
* @param buffer 数据指针
* @param length 数据长度
* @return CRC16值
*/
u16 CRC16(u8 *buffer, u8 length) {
    u16 crc = 0xFFFF;
    u8 i, j;
    for (i = 0; i < length; i++) {
        crc ^= buffer;
        for (j = 0; j < 8; j++) {
            if (crc & 0x0001) {
                crc >>= 1;
                crc ^= 0xA001;
            } else {
                crc >>= 1;
            }
        }
    }
    return crc;
}

/**
* @brief 发送Modbus响应数据
* @param data 数据指针
* @param len 数据长度 (不含CRC)
*/
void SendResponse(u8 *data, u8 len) {
    u16 crc = CRC16(data, len);
    u8 i;
   
    ES = 0; // 关闭串口中断,防止干扰发送
   
    // 发送数据主体
    for (i = 0; i < len; i++) {
        SBUF = data;
        while (!TI);
        TI = 0;
    }
   
    // 发送CRC低字节
    SBUF = crc & 0xFF;
    while (!TI);
    TI = 0;
   
    // 发送CRC高字节
    SBUF = (crc >> 8) & 0xFF;
    while (!TI);
    TI = 0;
   
    ES = 1; // 重新开启串口中断
}

/**
* @brief 处理Modbus请求
* 解析接收到的数据,执行相应功能码操作
*/
void ProcessModbus(void) {
    u16 crc, calcCrc;
    u8 addr, func;
    u16 startAddr, regNum;
    u8 i;
   
    // 最小帧长度检查: Addr(1) + Func(1) + StartAddr(2) + Quantity(2) + CRC(2) = 8
    if (rxCount < 8) return;
   
    // 提取地址和功能码
    addr = rxBuffer;
    func = rxBuffer;
   
    // 校验从站地址
    if (addr != SLAVE_ADDR) return;
   
    // 提取并校验CRC
    crc = ((u16)rxBuffer[rxCount - 1] << 8) | rxBuffer[rxCount - 2];
    calcCrc = CRC16(rxBuffer, rxCount - 2);
    if (crc != calcCrc) return;
   
    // 根据功能码处理
    switch (func) {
        case 0x03: // 读保持寄存器
        {
            u8 regAddrH = rxBuffer;
            u8 regAddrL = rxBuffer;
            u8 regNumH = rxBuffer;
            u8 regNumL = rxBuffer;
            
            startAddr = ((u16)regAddrH << 8) | regAddrL;
            regNum = ((u16)regNumH << 8) | regNumL;
            
            // 检查地址和数量合法性
            if (startAddr + regNum > REG_NUM) {
                // 异常响应: 非法数据地址 (Exception Code 0x02)
                txBuffer = addr;
                txBuffer = func | 0x80;
                txBuffer = 0x02;
                SendResponse(txBuffer, 3);
                break;
            }
            
            // 构建响应帧
            txBuffer = addr;
            txBuffer = func;
            txBuffer = regNum * 2; // 字节数
            
            for (i = 0; i < regNum; i++) {
                u16 regVal = holdReg[startAddr + i];
                txBuffer[3 + 2 * i] = (regVal >> 8) & 0xFF; // 高字节
                txBuffer[4 + 2 * i] = regVal & 0xFF;        // 低字节
            }
            
            SendResponse(txBuffer, 3 + 2 * regNum);
            break;
        }
        
        case 0x06: // 写单个寄存器
        {
            u8 regAddrH = rxBuffer;
            u8 regAddrL = rxBuffer;
            u8 regValH = rxBuffer;
            u8 regValL = rxBuffer;
            
            startAddr = ((u16)regAddrH << 8) | regAddrL;
            u16 regVal = ((u16)regValH << 8) | regValL;
            
            // 检查地址合法性
            if (startAddr >= REG_NUM) {
                // 异常响应: 非法数据地址
                txBuffer = addr;
                txBuffer = func | 0x80;
                txBuffer = 0x02;
                SendResponse(txBuffer, 3);
                break;
            }
            
            // 写入寄存器
            holdReg[startAddr] = regVal;
            
            // 原样返回请求帧作为响应
            txBuffer = addr;
            txBuffer = func;
            txBuffer = regAddrH;
            txBuffer = regAddrL;
            txBuffer = regValH;
            txBuffer = regValL;
            
            SendResponse(txBuffer, 6);
            break;
        }
        
        default:
            // 不支持的功能码
            txBuffer = addr;
            txBuffer = func | 0x80;
            txBuffer = 0x01; // 非**能码
            SendResponse(txBuffer, 3);
            break;
    }
}

/**
* @brief 串口中断服务程序
* 接收数据并判断帧结束
*/
void UartISR(void) interrupt 4 {
    if (RI) {
        RI = 0;
        if (rxCount < RX_BUF_SIZE) {
            rxBuffer[rxCount++] = SBUF;
            // 简单帧结束判断:实际项目中建议使用定时器检测3.5个字符空闲时间
            // 此处仅为示例,假设主循环快速处理或外部有帧结束信号
        } else {
            rxCount = 0; // 溢出重置
        }
    }
    if (TI) {
        TI = 0;
    }
}

/**
* @brief 主函数
*/
void main(void) {
    UartInit();
   
    // 初始化寄存器默认值
    for (int i = 0; i < REG_NUM; i++) {
        holdReg = 0;
    }
   
    while (1) {
        // 在实际应用中,应使用定时器中断来检测Modbus帧间隔(3.5字符时间)
        // 这里简化处理,假设rxComplete由外部机制置位或通过延时判断
        if (rxCount > 0) {
            // 模拟简单的帧结束检测:如果一段时间没有新数据
            // 注意:真实项目需配合定时器实现标准的Modbus RTU帧间隔检测
            ProcessModbus();
            rxCount = 0; // 处理完后清空缓冲区
        }
    }
}
yeates333 发表于 2026-7-19 08:54 | 显示全部楼层
串口接收中断负责逐字节缓存数据,主循环或定时器定期扫描并判断帧接收是否完成,随后交由协议解析引擎进行地址匹配和 CRC-16 校验。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

快速回复 在线客服 返回列表 返回顶部
0