一、理清层级关系

入门第一步先把层级关系理清楚:

  1. 物理层(电平标准):TTL、RS232、RS485,解决的是「0 和 1 怎么用电信号表示、怎么传输」,决定了传输距离、抗干扰能力、节点数量。
  2. 数据链路层(串口 UART):定义了数据帧的基础格式(起始位 + 数据位 + 校验位 + 停止位),解决的是「一串比特流怎么组成一帧完整数据」。
  3. 应用层(Modbus 协议):基于串口 / 以太网的上层协议,定义了数据帧里的内容含义、主从机交互规则,解决的是「这帧数据要做什么、怎么判断执行成功 / 失败」。

RS485 是硬件传输的物理层标准,Modbus 是跑在 RS485 之上的应用层通信协议,二者是底层和上层的关系。

二、物理层:RS485 

2.1 串口基础前提

常用的 MCU(比如 STM32)串口是TTL 电平,逻辑 1 为 3.3V/5V,逻辑 0 为 0V;和电脑上位机通信时,必须用 CH340/CP2102 芯片做电平转换,因为二者电平协议不匹配。

而 RS232、RS485 是工业场景更常用的电平标准,核心差异如下表

特性 RS232 RS422 RS485
信号类型 单端 差分 差分
节点数 1 收 1 发 1 发 10 收 1 发 32 收
最大传输距离 50 英尺(约 15 米) 4000 英尺(约 1219 米) 4000 英尺(约 1219 米)
最大传输速率 20Kbps 10Mbps 10Mbps

2.2 RS485 核心特性

  1. 差分传输:核心优势,用 A、B 两条线的压差传输信号,抗干扰能力极强,适合工业远距离传输。
  2. 电平标准
    • A、B 之间正压差 + 2~+6V:逻辑电平 0
    • A、B 之间负压差 - 2~-6V:逻辑电平 1
    • 压差在 - 2~+2V 之间:无有效数据
  3. 配套芯片(SP3485)收发控制:单片机串口不能直接接 RS485 总线,必须通过 SP3485 这类收发芯片转换,核心控制引脚是RS485_EN(DE 和 RE 短接后接 MCU 的 GPIO):
    • EN 引脚高电平:芯片进入发送模式,MCU_TX 的数据会转为 485 差分信号从 A/B 输出
    • EN 引脚低电平:芯片进入接收模式,A/B 的 485 差分信号会转为 TTL 电平输出到 MCU_RX

      实操大坑提醒:发送数据前必须先把 EN 拉高,发送完成后立刻拉低,否则永远收不到从机的响应数据!

三、Modbus 协议核心知识点

Modbus 是工业领域最通用的主从通信协议,支持 RS232/485、以太网等物理介质,核心规则是主站发起请求,从站响应回复,从站永远不会主动发送数据。

3.1 核心数据模型(4 种对象,对应所有功能码)

Modbus 把设备数据分为 4 类,所有读写操作都围绕这 4 类对象展开:

对象类型 位宽 访问属性 典型用途
离散量输入 单个比特 只读 按键、热释电、开关量传感器
线圈 单个比特 读写 LED、蜂鸣器、继电器等开关控制
输入寄存器 16 位字 只读 光照、噪声、空气质量等传感器数值
保持寄存器 16 位字 读写 设备参数、控制参数、温湿度数值等(最常用)

3.2 常用公共功能码

功能码(十六进制) 功能说明 对应数据对象
01 读线圈 线圈
02 读离散量输入 离散量输入
03 读多个保持寄存器 保持寄存器
04 读多个输入寄存器 输入寄存器
05 写单个线圈 线圈
06 写单个保持寄存器 保持寄存器
0F 写多个线圈 线圈
10 写多个保持寄存器 保持寄存器

03 功能码是日常使用频率最高的,几乎所有设备的参数读取都用它,必须吃透它的帧格式。

3.3 Modbus RTU 帧格式

Modbus 串行链路有 RTU 和 ASCII 两种模式,RTU 模式传输效率更高,是绝对主流,核心帧结构基础模板:从机地址(1字节) + 功能码(1字节) + 数据域(N字节) + CRC校验(2字节)

3.3.1 关键基础规则
  1. 从机地址:范围 1~247(0 为广播地址,仅用于写命令,所有从机接收,无响应)
  2. CRC 校验:16 位循环冗余校验,低位在前、高位在后,用于校验数据传输是否出错
  3. 帧间隔规则:RTU 模式下,两帧数据之间必须有至少 3.5 个字符时间的空闲间隔,MCU 用这个规则判断一帧数据是否传输完成。
3.3.2 03 功能码(读保持寄存器)完整帧拆解
① 主机→从机:请求帧
字段 字节数 取值范围 / 说明
从机地址 1 0x01~0x7F,目标从机的地址
功能码 1 固定 0x03,代表读保持寄存器操作
起始寄存器地址高字节 1 待读取的寄存器起始地址高 8 位
起始寄存器地址低字节 1 待读取的寄存器起始地址低 8 位
寄存器数量高字节 1 待读取的寄存器总数高 8 位
寄存器数量低字节 1 待读取的寄存器总数低 8 位,范围 0x0001~0x007D(最多 125 个)
CRC 校验高字节 1 前面所有字节的 CRC16 校验结果高 8 位
CRC 校验低字节 1 前面所有字节的 CRC16 校验结果低 8 位

示例:读取地址 01 的从机,起始地址 0x0000 的 3 个保持寄存器请求帧:01 03 00 00 00 03 XX XX(XX XX 为 CRC 校验值)

② 从机→主机:正常响应帧
字段 字节数 取值范围 / 说明
从机地址 1 和请求帧的从机地址一致
功能码 1 和请求帧的功能码一致(固定 0x03)
字节计数 1 后续数据域的总字节数 = 寄存器数量 ×2(每个寄存器 16 位,占 2 字节)
寄存器数据 N×2 按顺序排列的寄存器值,每个寄存器分高、低字节
CRC 校验高字节 1 前面所有字节的 CRC16 校验结果高 8 位
CRC 校验低字节 1 前面所有字节的 CRC16 校验结果低 8 位

示例:上面请求的响应,3 个寄存器值分别为 0x0037、0x0050、0x0010响应帧:01 03 06 00 37 00 50 00 10 XX XX

③ 从机→主机:异常响应帧

如果从机执行请求出错,会返回异常响应,核心是功能码最高位置 1,同时返回异常码:

字段 字节数 说明
从机地址 1 和请求帧的从机地址一致
异常功能码 1 原功能码 + 0x80,03 功能码对应的异常功能码为 0x83
异常码 1 指明具体错误原因,排错核心
CRC 校验码 2 前面 3 个字节的 CRC16 校验结果

常见异常码(排错速查):

异常码(十六进制) 错误名称 典型触发条件
01 非法功能 从机不支持请求的功能码
02 非法数据地址 请求的寄存器起始地址 / 范围超出从机定义的范围
03 非法数据值 请求的寄存器数量超出允许上限(比如超过 125 个)

3.4 通信模式

  1. 单播模式:主站向指定地址的单个从机发请求,从机接收后执行并返回响应,是最常用的模式。
  2. 广播模式:主站向地址 0 发请求,总线上所有从机都会接收,仅用于写命令,所有从机都不会返回响应

四、STM32 实操示例

基于 RS485 物理层的 Modbus RTU 协议,采用 主从架构 :
 主机(Master) - STM32F407
- 负责发起请求
- 通过 USART1 + RS485 收发器
- 使用 TIM7 进行帧超时检测(3.6ms)

 从机(Slave) - STM32F10x
- 响应主机请求
- 通过 USART3 + RS485 收发器
- 使用 TIM3 进行帧超时检测

主机发送流程
void ModbusMaster_Transmit(void)
{
    u8 i=0;
    u16 crc=0;
    
    // 构建 Modbus 请求帧
    modbus_master.txbuff[i++] = MODBUS_ID;    // 从机地址:0x01
    modbus_master.txbuff[i++] = 0x03;         // 功能码:读保持寄存器
    modbus_master.txbuff[i++] = 0x00;         // 起始地址高字节
    modbus_master.txbuff[i++] = 0x01;         // 起始地址低字节
    modbus_master.txbuff[i++] = 0x00;         // 寄存器数量高字节
    modbus_master.txbuff[i++] = 0x05;         // 寄存器数量低字节(读 5 个)
    
    // 计算 CRC16 校验
    crc = CRC_16_Tab(modbus_master.txbuff, i);
    modbus_master.txbuff[i++] = (crc>>8)&0xff;  // CRC 高字节
    modbus_master.txbuff[i++] = crc&0xff;       // CRC 低字节
    
    modbus_master.txcount = i;
    
    // 通过 RS485 发送
    RS485_SendData(modbus_master.txbuff, modbus_master.txcount);
}
主机接收流程
//UART 中断接收 ( USART1_IRQHandler ):
void USART1_IRQHandler(void)
{
    if(USART_GetITStatus(USART1,USART_IT_RXNE)==SET)
    {	
        // 接收字节
        modbus_master.rxbuff[modbus_master.rxcount++] = USART_ReceiveData(USART1);
        
        // 第一个字节到达,启动定时器做帧超时检测
        if(modbus_master.rxcount == 1)
        {
            TIM_Cmd(TIM7, ENABLE);  // 启动 TIM7
        }
        TIM_SetCounter(TIM7, 0);  // 重置计数器
    }
}


//定时器超时判断帧结束 ( TIM7_IRQHandler ):
void TIM7_IRQHandler(void)
{
    if(TIM_GetITStatus(TIM7,TIM_IT_Update))
    {
        TIM_ClearFlag(TIM7,TIM_FLAG_Update);
        TIM_Cmd(TIM7, DISABLE);  // 关闭定时器
        
        // 超过 3.6ms 未收到新数据,判定帧接收完成
        if(modbus_master.rxcount > 1)
            modbus_master.rxover = 1;  // 标记帧接收完成
    }
}



//数据处理 ( ModbusMaster_Recive ):
void ModbusMaster_Recive(void)
{
    if(modbus_master.rxover == 0) return;
    
    // 1. CRC 校验
    current_crc = CRC_16_Tab(rxbuff, rxcount-2);
    rx_crc = (rxbuff[rxcount-2]<<8) | rxbuff[rxcount-1];
    
    if(current_crc != rx_crc) goto MODBUS_ERROR;
    
    // 2. 解析功能码 0x03 响应
    switch(rxbuff[1])
    {
        case 0x03:
            if(MODBUS_ID == rxbuff[0])  // 验证从机地址
            {
                // 提取传感器数据(每个寄存器 2 字节)
                val_buff.Hum    = rxbuff[3]<<8 | rxbuff[4];   // 湿度
                val_buff.Temp   = rxbuff[5]<<8 | rxbuff[6];   // 温度
                val_buff.Light  = rxbuff[7]<<8 | rxbuff[8];   // 光照
                val_buff.Smoke  = rxbuff[9]<<8 | rxbuff[10];  // 烟雾
                val_buff.Air    = rxbuff[11]<<8 | rxbuff[12]; // 空气质量
            }
            break;
    }
    
MODBUS_ERROR:
    // 清空缓冲区
    modbus_master.rxcount = 0;
    modbus_master.rxover = 0;
}







从机响应流程
//UART 中断接收 :
void USART3_IRQHandler(void)
{
    if(USART_GetITStatus(USART3,USART_IT_RXNE)==SET)
    {
        modbus_slve.rx_buff[modbus_slve.rx_count++] = USART3->DR;
        TIM_SetCounter(TIM3, 0);  // 重置定时器
    }
    
    // 第一个字节到达,启动定时器
    if(modbus_slve.rx_count == 1)
    {
        TIM_Cmd(TIM3, ENABLE);
    }
}

//事件轮询处理 ( modbus_EventPoll ):
void modbus_EventPoll(void)
{
    if(modbus_slve.rx_over == 1)
    {
        modbus_slve.rx_over = 0;
        
        // 1. CRC 校验
        crc = CRC16(rx_buff, rx_count-2);
        rx_crc = rx_buff[rx_count-2]<<8 | rx_buff[rx_count-1];
        
        if(crc != rx_crc) goto p1;
        
        // 2. 地址匹配
        if(rx_buff[0] != modbus_slveID) goto p1;
        
        // 3. 功能码分发
        switch(rx_buff[1])
        {
            case 0x03: 
                modbus_Func03();  // 处理读寄存器请求
                break;
            default: break;
        }
        
        // 4. 发送响应(切换 RS485 为发送模式)
        for(i=0; i<modbus_slve.tx_count; i++)
        {
            RS485_DI();  // 发送使能
            USART3->DR = tx1_buff[i];
            RS485_RE();  // 接收使能
        }
    }
}

//功能码 0x03 处理 ( modbus_Func03 ):
void modbus_Func03(void)
{
    u8 count = 0;
    u16 reg_count = rx_buff[4]<<8 | rx_buff[5];     // 读取寄存器数量
    u16 start_addr = rx_buff[2]<<8 | rx_buff[3];    // 起始地址
    
    // 构建响应帧
    tx1_buff[count++] = modbus_slveID;          // 从机地址
    tx1_buff[count++] = 0x03;                   // 功能码
    tx1_buff[count++] = (reg_count * 2);        // 返回字节数
    
    // 填充寄存器数据
    for(i=0; i<reg_count; i++)
    {
        tx1_buff[count++] = (modbus_register[i]>>8) & 0xff;  // 高字节
        tx1_buff[count++] = modbus_register[i] & 0xff;       // 低字节
    }
    
    // 添加 CRC
    tx_crc = CRC16(tx1_buff, count);
    tx1_buff[count++] = (tx_crc>>8) & 0xff;
    tx1_buff[count++] = tx_crc & 0xff;
    
    modbus_slve.tx_count = count;
}


//数据模拟 ( get_Value ):
void get_Value(void)
{
    // 模拟 5 个传感器数据
    modbus_register[0] = 0x1;  // 湿度
    modbus_register[1] = 0x2;  // 温度
    modbus_register[2] = 0x3;  // 光照
    modbus_register[3] = 0x4;  // 烟雾
    modbus_register[4] = 0x5;  // 空气质量
}






CRC校验实现
/* 高位字节表 Table of CRC values for high-order byte */
const unsigned char auchCRCHi[] = {
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81,
0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01,
0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81,
0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01,
0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81,
0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01,
0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81,
0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01,
0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81,
0x40};
/* 低位字节表 Table of CRC values for low-order byte */
const unsigned char auchCRCLo[] = {
0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06, 0x07, 0xC7, 0x05, 0xC5, 0xC4,
0x04, 0xCC, 0x0C, 0x0D, 0xCD, 0x0F, 0xCF, 0xCE, 0x0E, 0x0A, 0xCA, 0xCB, 0x0B, 0xC9, 0x09,
0x08, 0xC8, 0xD8, 0x18, 0x19, 0xD9, 0x1B, 0xDB, 0xDA, 0x1A, 0x1E, 0xDE, 0xDF, 0x1F, 0xDD,
0x1D, 0x1C, 0xDC, 0x14, 0xD4, 0xD5, 0x15, 0xD7, 0x17, 0x16, 0xD6, 0xD2, 0x12, 0x13, 0xD3,
0x11, 0xD1, 0xD0, 0x10, 0xF0, 0x30, 0x31, 0xF1, 0x33, 0xF3, 0xF2, 0x32, 0x36, 0xF6, 0xF7,
0x37, 0xF5, 0x35, 0x34, 0xF4, 0x3C, 0xFC, 0xFD, 0x3D, 0xFF, 0x3F, 0x3E, 0xFE, 0xFA, 0x3A,
0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38, 0x28, 0xE8, 0xE9, 0x29, 0xEB, 0x2B, 0x2A, 0xEA, 0xEE,
0x2E, 0x2F, 0xEF, 0x2D, 0xED, 0xEC, 0x2C, 0xE4, 0x24, 0x25, 0xE5, 0x27, 0xE7, 0xE6, 0x26,
0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21, 0x20, 0xE0, 0xA0, 0x60, 0x61, 0xA1, 0x63, 0xA3, 0xA2,
0x62, 0x66, 0xA6, 0xA7, 0x67, 0xA5, 0x65, 0x64, 0xA4, 0x6C, 0xAC, 0xAD, 0x6D, 0xAF, 0x6F,
0x6E, 0xAE, 0xAA, 0x6A, 0x6B, 0xAB, 0x69, 0xA9, 0xA8, 0x68, 0x78, 0xB8, 0xB9, 0x79, 0xBB,
0x7B, 0x7A, 0xBA, 0xBE, 0x7E, 0x7F, 0xBF, 0x7D, 0xBD, 0xBC, 0x7C, 0xB4, 0x74, 0x75, 0xB5,
0x77, 0xB7, 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71, 0x70, 0xB0, 0x50, 0x90, 0x91,
0x51, 0x93, 0x53, 0x52, 0x92, 0x96, 0x56, 0x57, 0x97, 0x55, 0x95, 0x94, 0x54, 0x9C, 0x5C,
0x5D, 0x9D, 0x5F, 0x9F, 0x9E, 0x5E, 0x5A, 0x9A, 0x9B, 0x5B, 0x99, 0x59, 0x58, 0x98, 0x88,
0x48, 0x49, 0x89, 0x4B, 0x8B, 0x8A, 0x4A, 0x4E, 0x8E, 0x8F, 0x4F, 0x8D, 0x4D, 0x4C, 0x8C,
0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46, 0x86, 0x82, 0x42, 0x43, 0x83, 0x41, 0x81, 0x80,
0x40} ;
/*-------------------------------------------------------
**函数名:
**功 能: 查表法算 CRC_16
**描 述:
**传 入: unsigned char *puchMsg 指向含有二进制的数据的缓冲器
                  unsigned int usDataLen ; 缓冲器中的字节数。 
**传 出:CRC16位校验值
--------------------------------------------------------*/
unsigned short CRC16(unsigned char *puchMsg, unsigned short usDataLen)
{
    unsigned char uchCRCHi = 0xFF;  // 初始值
    unsigned char uchCRCLo = 0xFF;
    unsigned uIndex;
    
    while(usDataLen--)
    {
        uIndex = uchCRCLo ^ *puchMsg++;  // 异或当前字节
        uchCRCLo = uchCRCHi ^ auchCRCHi[uIndex];
        uchCRCHi = auchCRCLo[uIndex];
    }
    return (uchCRCLo << 8 | uchCRCHi);  // 返回 16 位 CRC
}
RS485硬件控制
// 发送模式
void RS485_DI(void)
{
    GPIO_ResetBits(GPIOA, GPIO_Pin_12);  // DE=0, RE=1 → 发送
}

// 接收模式
void RS485_RE(void)
{
    GPIO_SetBits(GPIOA, GPIO_Pin_12);    // DE=1, RE=0 → 接收
}
测试流程
//主机测试流程
int main()
{
    Modbus_Config(9600);  // 初始化 RS485 + TIM7
    
    while(1)
    {
        // 定时轮询
        if(modbusrun1[0] > modbusrun1[1])
        {
            modbusrun1[0] = 0;
            ModbusMaster_Recive();  // 处理接收
        }
        
        if(modbusrun2[0] > modbusrun2[1])
        {
            modbusrun2[0] = 0;
            ModbusMaster_Transmit();  // 发送请求
        }
    }
}


//从机测试流程
int main(void)
{
    modbus_Config();  // 初始化 RS485 + TIM3
    
    while(1)
    {
        if(modbusRun[0] > modbusRun[1])
        {
            get_Value();           // 更新传感器数据
            modbusRun[0] = 0;
            modbus_EventPoll();    // 处理 Modbus 事件
        }
    }
}

五、常见问题快速排错

问题现象 核心排查方向
完全收不到任何数据

1. RS485_EN 引脚电平切换是否正确;

2. A/B 线是否接反;

3. 串口波特率 / 数据位 / 停止位 / 校验位是否和从机一致;

4. 从机地址是否正确

能收到数据但全是乱码

1. 串口参数不匹配;

2. 总线接地不良、干扰过大;

3. CRC 校验错误,帧数据不完整

收到异常响应帧 直接看异常码,对照上文的异常码说明排查:01 查功能码、02 查寄存器地址、03 查寄存器数量
偶尔能收到、偶尔丢包

1. 帧间隔不符合 3.5 字符时间,接收断帧;

2. 总线未接终端电阻,信号反射;

3. EN 引脚切换时机不对,发送完成后未及时拉低

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐