解码RS485与Modbus通信及CRC16校验

RS485基础

概念

RS-485是一种串行通信的电气接口标准(全称TIA/EIA-485,EIA指美国电子工业协会),定义了设备之间通过差分信号传输数据的物理层规范,核心目标是实现长距离、多设备、抗干扰的可靠通信。

图片1

RS485是差分平衡式串行通信标准,专门解决RS232通信距离短、抗干扰弱的问题,广泛应用于工业自动化、楼宇控制、安防监控等需要长距离、多节点通信的场景。

RS-232

DB9串口有9个引脚,串行通信时核心用到3个:RXD(接收数据)、TXD(发送数据)、GND(信号地),其余引脚功能如下表:

引脚号 符号 功能描述
1 DCD 载波检测(Data Carrier Detect),用于Modem通知终端设备其处于在线状态
2 RXD 接收数据(Received Data),终端设备从外部设备接收数据的通道
3 TXD 发送数据(Transmit Data),终端设备向外部设备发送数据的通道
4 DTR 数据终端就绪(Data Terminal Ready),终端设备通知外部设备自身已准备就绪
5 GND RS232电气特性:采用负逻辑,逻辑1(MARK状态)对应-3V至-15V,逻辑0(SPACE状态)对应+3V至+15V);-3V至+3V为不确定区域,需通过硬件设计避免信号落入此范围。
6 DSR 数据设备就绪(Data Set Ready),外部设备通知终端设备自身已准备就绪
7 RTS 请求发送(Request To Send),终端设备向外部设备请求发送数据
8 CTS 清除发送(Clear To Send),外部设备通知终端设备允许发送数据
9 RI 振铃指示(Ring Indicator),Modem通知终端设备有呼叫进入(如电话振铃)

RS232电气特性:采用负逻辑,逻辑1(MARK状态)对应-3V至-15V,逻辑0(SPACE状态)对应+3V至+15V;-3V至+3V为不确定区域,需通过硬件设计避免信号落入此范围。

RS232传输限制:9600bps速率下最大传输距离15米,速率升高距离缩短(115200bps时≤5米),速率降低可延长(110bps时可达百米级,需信号放大);采用单端传输(一根线),以GND为参考,抗共模干扰能力弱,易受接地环路和电磁干扰影响。

应用场景:适用于短距离、低速率、点对点异步串行通信(如早期计算机外设连接、工业设备调试),目前逐渐被USB、以太网、RS485/RS422取代,仅在传统设备中保留应用。

RS-485电气特性

拓扑结构

图片2

RS-485有两线制和四线制两种接线方式,四线制仅支持点对点通信,目前极少采用;主流为两线制,采用总线式拓扑结构,同一总线上最多可挂接32个节点(通过中继器可扩展至256个以上)。

RS-485通信网络通常采用主从通信结构,即一个主机带多个从机;所有节点串联在差分总线上,总线两端需并联120Ω终端电阻,用于匹配总线阻抗,减少信号反射。

终端电阻说明:阻值应等于传输电缆的特性阻抗,多数RS-485专用双绞线的差分特性阻抗为100Ω~150Ω,行业标准推荐值为120Ω(阻抗匹配);仅在长距离传输(通常超过300米)或高速率通信时必须添加。

电平信号

RS-485采用差分信号传输,正逻辑,核心依赖A、B两根差分线,逻辑状态由两线间的电压差决定,而非单根线对地电压:

  • 逻辑1(MARK):A、B两线电压差为+2V ~ +6V(发送器),VA - VB ≥ +200mV(接收器);
  • 逻辑0(SPACE):A、B两线电压差为-2V ~ -6V(发送器),且VA - VB ≤ -200mV(接收器);

关键提示:切勿单独判断A线或B线的电压高低,仅需计算VA - VB的差值即可;若单独看单根线,会出现4种混乱情况(A+、B+;A-、B+;A+、B-;A-、B-),失去差分传输的优势。

RS-485无需RS232那样的握手信号(如RTS/CTS),仅需A、B两根线即可传输核心信号,大幅简化布线;同时,差分传输可有效抑制共模噪声(如电磁干扰、接地环路),抗干扰能力远优于RS232。

差分信号与共模干扰

图片3

差分传输:区别于“一根信号线+一根地线”的传统方式,差分传输在两根线上均传输信号,这两个信号振幅相等、相位相反(A线降低2V,B线则升高2V,反之同理),这种信号称为差分信号(差模信号)。

共模信号:指两根信号线对地的电压,不携带有效信息,是由干扰产生的无效信号;任何信号均可分解为共模信号和差模信号。

差分传输抗干扰原理:

  • 抗干扰能力强:外界噪声干扰会同时耦合到A、B两根线上,接收端仅关注两线的电压差,共模噪声会被完全抵消;
  • 抑制EMI(电磁干扰):两根信号极性相反,对外辐射的电磁场相互抵消,耦合越紧密,泄放的电磁能量越少;
  • 时序定位精确:差分信号的开关变化位于两信号的交点,而非单端信号的高低阈值,受工艺、温度影响小,时序误差低,适合低幅度信号传输。

共模干扰产生原因:

  • 电网串扰:电网电压波动、谐波成分引入共模干扰电压;
  • 辐射干扰:雷电、设备电弧、附近电台、大功率辐射源等,在信号线上感应出共模干扰;
  • 地电位差异:不同设备、系统之间的接地电位不一致,引入共模干扰;
  • 内部干扰:设备内部电线的电磁场对电源线产生干扰;此外,电源线阻抗不匹配、信号线屏蔽不良、设备电磁兼容性差等,也会产生共模干扰。

通信距离与速率关系

RS-485的通信速率与传输距离成反比,速率越低,传输距离越长,具体对应关系如下表(满足不同场景需求):

通信速率 典型通信距离 备注
10 Mbps 50米 高速模式,适用于短距离、高带宽传输场景
1 Mbps 100米 中高速场景,如实时数据采集、高频响应控制
100 kbps 1200米 EIA/TIA-485标准推荐距离,适用于多数工业场景
50 kbps 1500米 低速率长距离场景,如远距离传感器数据传输
9600 bps 1800米 极限距离,需使用低电容专用电缆,速率极低

电平转换(SP3485/MAX485)

MCU(微控制器)采用TTL/CMOS电平,与RS-485电平不兼容,无法直接通过RS-485总线通信,需通过电平转换芯片实现转换;常用芯片为MAX485(美信公司)、SP3485,GEC-M4开发板中板载SP3485芯片。

图片4

SP3485芯片说明:3.3V供电,半双工低功耗RS-485收发器,符合TIA/EIA-485标准;内置1个驱动器和1个接收器,可独立使能/禁用;两者均禁用时,驱动器和接收器输出均为高阻抗状态;负载特性支持256个SP3485收发器连接到同一总线,最高支持12Mbps无差错数据传输;具备故障保护、过温保护、电流限制保护、过压保护等功能。

SP3485引脚描述(8引脚,SOP-8封装为主):

引脚编号 引脚名称 功能描述
1 RO 接收器输出:当/RE为低电平时,若A-B ≥ 200mV,RO输出高电平;若A-B ≤ -200mV,RO输出低电平
2 /RE 接收器输出使能控制(低电平有效):低电平时,接收器输出使能,RO输出有效;高电平时,接收器输出禁用,RO为高阻抗;/RE高电平且DE低电平时,器件进入低功耗模式
3 DE 驱动器输出使能控制(高电平有效):高电平时,驱动器输出有效;低电平时,驱动器输出为高阻抗;/RE高电平且DE低电平时,器件进入低功耗模式
4 DI 驱动器输入:当DE为高电平时,DI低电平强制驱动器同相输出A为低、反相输出B为高;DI高电平强制驱动器同相输出A为高、反相输出B为低
5 GND 地,所有信号的公共接地参考点
6 A 接收器同相输入、驱动器同相输出,连接RS-485总线的A线
7 B 接收器反相输入、驱动器反相输出,连接RS-485总线的B线
8 VCC 电源输入,供电范围3.0V~3.6V(SP3485为3.3V供电)

SP3485芯片命名规则

图片5

硬件接线与程序设计

硬件接线核心:RS-485采用半双工通信,芯片工作在发送还是接收模式,由/RE和DE两个使能端控制;实际应用中,通常将/RE和DE连接到MCU的同一引脚(如GEC-M4的PG8),通过该引脚电平控制通信方向:

图片6

程序设计核心:RS-485仅定义通信的电气属性,底层通信逻辑仍基于UART(串口),因此只需正常配置UART(波特率、数据位、奇偶校验位、停止位),唯一区别是发送数据前需通过GPIO引脚切换通信方向(发送/接收),发送完成后立即切换回接收模式(默认监听模式)。

RS-485 UART配置示例(以STM32为例,注释包含参数说明):

/**
 * @brief RS485单字节发送函数
 * @param byte 待发送的单字节数据(uint8_t类型)
 * @retval None
 * @note 1. 仅当RS485被配置为发送器模式时调用此函数;
 *       2. 需提前完成初始化:USART2的时钟使能、GPIOG的时钟使能,且GPIOG_Pin_8已配置为推挽输出模式;
 *       3. 若需确保字节完全发送后再切换状态,可启用“传输完成(TC)标志”的等待逻辑(代码中已标注)
 */
void RS485_SendByte(uint8_t byte)
{
    // 1. 设置RS485收发器为发送状态(GPIOG_Pin_8置高电平)
    GPIO_SetBits(GPIOG, GPIO_Pin_8);
    
    // 2. 向USART2发送目标字节
    USART_SendData(USART2, byte);
    
    // 3. 等待“发送数据寄存器空(TXE)”标志置位
    //    (表示数据已送入移位寄存器,可准备下一次发送)
    while (USART_GetFlagStatus(USART2, USART_FLAG_TXE) == RESET);
    
    // 【可选】若需确保字节完全发送到总线,可增加“传输完成(TC)”标志等待
    // while (USART_GetFlagStatus(USART2, USART_FLAG_TC) == RESET);
    
    // 4. 恢复RS485收发器为接收状态(GPIOG_Pin_8置低电平)
    GPIO_ResetBits(GPIOG, GPIO_Pin_8);
}

RS-232、RS-422、RS-485区别

微信图片_20260205200458_121_5

注:RS-422/485 标准在电气特性上非常相近,在传输方式上有所区别(RS422标准仅支持单发送端,一个主设备(Master),其余为从设备(Salve),从设备之间不能通信,所以 RS-422 支持点对多点的双向通信)

基于RS485的Modbus通信协议

Modbus协议概述

Modbus是一种应用层/软件层串行通信协议,由Modicon公司(现施耐德电气Schneider Electric)于1979年为可编程逻辑控制器(PLC)通信发布,目前已成为工业领域的业界标准,广泛用于工业电子设备之间的连接。

Modbus协议不依赖特定物理层,可基于RS-485、RS-232、以太网、IIC等物理层传输;其中,大多数Modbus设备通过EIA-485物理层通信(两线制总线拓扑,主从结构),适配工业长距离、多节点场景。

核心区别:硬件协议(如RS-485)负责“如何传输数据”,软件协议(Modbus)负责“如何有序传输数据”,保障数据传输的可靠性和规范性。

通信结构与设备

Modbus采用主从(master/slave)架构,核心规则如下:

  • 主节点(主机):唯一可主动发起命令的节点,同一时刻仅能发起一个Modbus事务处理;无需分配地址,负责向从节点发送请求、接收并处理从节点的应答。

  • 从节点(从机):仅能被动响应主节点的请求,无主节点指令时不主动发送数据,也不与其他从节点通信;每个从节点必须有唯一的地址,用于被主节点识别。

  • 通信模式:主节点对从节点的请求分为两种模式:

    • 单播模式:主节点向特定地址的从节点发送请求,从节点处理完成后,向主节点返回应答;一个事务包含“主节点请求→从节点应答”两个报文。

    图片7

    • 广播模式:主节点向所有从节点发送请求,从节点无需返回应答;仅适用于写命令(如批量设置从节点参数),所有从节点必须支持广播写功能。

    图片8

Modbus地址规则

图片9

Modbus寻址空间有256个不同地址,地址范围0~255,划分如下:

  • 地址0:保留为广播地址,所有从节点必须识别该地址,但无需回应;
  • 地址1~247:从节点单独地址,每个从节点必须分配唯一的地址(不可重复),用于主节点单播寻址;
  • 地址248~255:保留地址,暂不使用。

地址的作用:

  • 主节点通过地址,精准定位要控制的从节点;
  • 从节点通过地址判断请求是否针对自己:若地址非0且与自身地址一致,需返回应答;若为广播地址(0),执行写命令但不返回应答;避免多个从节点同时应答,导致总线冲突。

Modbus数据帧(PDU)

Modbus定义了简单的协议数据单元(PDU,Protocol Data Unit),核心是“功能码+数据”;在实际传输中,主节点会添加附加域,构造完整的串行链路PDU,结构如下:

图片13

各域详细说明:

  • 地址域:1字节(8位),仅包含从节点地址(0~247);主节点发送时,填写目标从节点地址;从节点应答时,填写自身地址,让主节点识别应答来源。
  • 功能码:1字节(8位),指明从节点要执行的动作(如读线圈、读寄存器、写线圈、写寄存器);若从节点无法执行该功能,会返回错误应答(功能码最高位置1)。
  • 数据域:包含请求/响应的具体参数(如寄存器起始地址、读取/写入的数据、数据长度等),格式由功能码决定。
  • 错误校验域:用于校验报文完整性,避免数据传输过程中因干扰导致失真;校验方式由传输模式决定(ASCII模式用LRC,RTU模式用CRC16)。

Modbus通信传输方式

Modbus支持3种传输模式,不可混用,需根据主机类型选择;其中,RS-485物理层常用RTU模式,串口常用ASCII模式,以太网常用TCP/IP模式。

ASCII模式(基于串口)

ASCII模式采用ASCII字符传输数据,每个字节(8位)的高4位和低4位,分别作为两个ASCII字符发送(如0x03→发送‘0’和‘3’→对应ASCII码0x30和0x33),所有字符均为十六进制。

帧格式(固定):

图片14

起始符(1字符):冒号(:),ASCII码0x3A;

地址域(2字符):从节点地址的ASCII表示(如地址1→“01”);

功能码(2字符):功能码的ASCII表示(如功能码3→“03”);

数据域(0~252字节,每字节对应2个ASCII字符):具体请求/响应数据;

校验域(2字符):LRC校验码的ASCII表示;

结束符(2字符):回车(CR,ASCII码0x0D)+ 换行(LF,ASCII码0x0A)。

关键特性:

  • 报文间隔:两个字符之间的间隔时间不可超过1秒,否则接收端判定报文不完整,直接丢弃;
  • 校验方式:LRC(纵向冗长校验),仅校验“起始符之后、结束符之前”的所有内容(不含起始符和结束符)。

LRC校验计算步骤:

  • 将报文中需校验的所有字节(地址域、功能码、数据域)连续累加;
  • 将累加和与256求模(取余数);
  • 用256减去该余数,得到LRC校验值(即sum%256后取反,再加1);

示例:报文需校验部分为01 A0 7C E4 02,累加和=0x01+0xA0+0x7C+0xE4+0x02=0x203(十进制515);515%256=3;LRC=256-3=253(0xFD),对应ASCII字符“FD”。

RTU模式(常用,基于RS-485/RS-232)

RTU(远程终端单元)模式采用二进制传输数据,每个字节直接以十六进制形式发送,无需转换为ASCII字符;数据密度高,相同波特率下,吞吐率高于ASCII模式,是工业RS-485通信的主流模式。

帧格式(固定):

图片10

起始标识:3.5个字符传输时间的空闲间隔(总线无数据);

地址域(1字节):从节点地址(0~247);

功能码(1字节):要执行的功能(如0x03=读保持寄存器);

数据域(0~252字节):具体请求/响应数据;

校验域(2字节):CRC16校验码(先附加低字节,再附加高字节);

结束标识:3.5个字符传输时间的空闲间隔。

关键特性:

图片11

  • 报文间隔:两个字符之间的空闲间隔不可超过1.5个字符传输时间,否则判定报文不完整;帧与帧之间的空闲间隔需≥3.5个字符传输时间,用于区分不同帧;
  • 帧长度:最大256字节(地址域+功能码+数据域+校验域);

CRC16

  • 手动计算

exported_image

unsigned short crc(unsigned char *data, unsigned char len)
{
    unsigned char temp = 0;
    unsigned short buff = 0xffff;  // 初始值
    unsigned char i = 0, j = 0;
    
    for(i = 0; i < len; i++)  // 遍历每个字节
    {
        buff ^= data[i];  // 当前字节与CRC寄存器低8位异或
        
        for(j = 0; j < 8; j++)  // 处理每个bit
        {
            temp = buff & 0x0001;  // 取最低位
            
            if(temp){  // 如果最低位为1
                buff >>= 1;      // 右移1位
                buff ^= 0xa001;  // 与多项式异或
            }else{  // 如果最低位为0
                buff >>= 1;      // 只右移1位
            }
        }
    }
    
    buff = ((buff & 0x00FF) << 8) | ((buff & 0xFF00) >> 8);  // 高低8位交换
    
    return buff;  // 返回CRC值
}
  • 查表
/* CRC 高位字节值表 */
const u8 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
} ;
/* CRC低位字节值表*/
const u8 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
} ;
u32 crc16( u8 *puchMsg, u32 usDataLen )
{
    u8 uchCRCHi = 0xFF ; // 高CRC字节初始化
    u8 uchCRCLo = 0xFF ; // 低CRC 字节初始化
    unsigned long uIndex ; 		// CRC循环中的索引

    while ( usDataLen-- ) 	// 传输消息缓冲区
    {
        uIndex = uchCRCHi ^ *puchMsg++ ; 	// 计算CRC
        uchCRCHi = uchCRCLo ^ auchCRCHi[uIndex] ;
        uchCRCLo = auchCRCLo[uIndex] ;
    }

    return ( uchCRCHi << 8 | uchCRCLo ) ;
}

RTU报文示例:从站地址01、功能码03、起始寄存器地址0001、读取数量0002,生成的RTU请求报文(十六进制)为01 03 00 01 00 02 95 CB;其中,01=从站地址,03=功能码,00 01=起始地址,00 02=读取数量,95 CB=CRC16校验码(95=低字节,CB=高字节)。

CRC在线校验工具

ScreenShot_2026-02-06_194615_512

TCP/IP模式(基于以太网)

基于以太网传输,无需RS-485/RS-232物理层,采用TCP协议进行数据传输;帧格式与串行链路不同,包含MBAP头(Modbus Application Protocol Header),用于标识Modbus报文,适用于以太网连接的工业设备,此处不详细展开。

Modbus功能码

功能码为1字节,用于指定从节点要执行的操作,核心功能码如下,此处以03为例展开,其余功能码可参考Modbus协议手册

图片15

报文

ScreenShot_2026-02-07_092316_902

ScreenShot_2026-02-07_101804_498

Modbus程序设计(基于RS485+RTU)

程序设计核心流程:实现RS485硬件层通信 → 配置定时器辅助(处理报文间隔、超时) → 按功能码编写对应功能函数 → 实现Modbus事务轮询(解析数据帧、调用功能函数、返回应答)。

关键要点:

  • 硬件层:已在RS485部分实现UART配置、GPIO控制(/RE和DE)、数据收发函数;
  • 定时器:配置一个定时器(如1ms定时器),用于判断报文间隔(≥3.5个字符时间)、接收超时,避免报文解析错误;
  • 功能函数:针对功能码03,编写对应的读取/写入函数,处理数据域的解析和构造;
  • 事务轮询:循环接收RS485数据,判断报文完整性(CRC16校验),解析地址域和功能码,调用对应功能函数,构造应答报文并发送。

Modbus RTU事务轮询示例代码(注释包含参数说明):

M4代码

time.h

#ifndef __TIME_H__
#define __TIME_H__

#include "stm32f4xx.h"

// 函数声明
void TIM2_Init(u16 arr, u16 psc);
void TIM2_IRQHandler(void);
void time7_delay_ms(u16 ms);

#endif

time.c

#include "time.h"
#include "modbus.h"

// TIM2初始化函数(用于Modbus报文超时检测)
void TIM2_Init(u16 arr, u16 psc) {
    NVIC_InitTypeDef NVIC_InitStruct;
    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;

    // 使能时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);

    // 配置定时器
    TIM_TimeBaseInitStruct.TIM_Period = arr;   // 重装载值
    TIM_TimeBaseInitStruct.TIM_Prescaler = psc - 1;  // 预分频值
    TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up;  // 向上计数
    TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1;
    TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStruct);

    // 使能更新中断
    TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);

    // 配置NVIC
    NVIC_InitStruct.NVIC_IRQChannel = TIM2_IRQn;  // 定时器2中断
    NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0;  // 抢占优先级
    NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;  // 响应优先级
    NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;   // 使能
    NVIC_Init(&NVIC_InitStruct);

    // 初始禁用定时器
    TIM_Cmd(TIM2, DISABLE);
}

// TIM2中断处理函数
void TIM2_IRQHandler(void) {
    if (TIM_GetITStatus(TIM2, TIM_IT_Update)) {
        TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
        modbus.recTime++;
        
        // 8ms超时判断(约3.5个字符时间,用于报文结束判断)
        if (modbus.recTime >= 8) {
            modbus.finishFlag = 1;  // 接收完成
            TIM_Cmd(TIM2, DISABLE);  // 禁用定时器
        }
    }
}

// 延时函数
void time7_delay_ms(u16 ms) {
    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;
    
    // 使能时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM7, ENABLE);
    
    // 配置定时器
    TIM_TimeBaseInitStruct.TIM_Period = ms * 10 - 1;
    TIM_TimeBaseInitStruct.TIM_Prescaler = 8400 - 1;
    TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1;
    TIM_TimeBaseInit(TIM7, &TIM_TimeBaseInitStruct);
    
    // 清除标志位
    TIM_ClearFlag(TIM7, TIM_FLAG_Update);
    
    // 启动定时器
    TIM_Cmd(TIM7, ENABLE);
    
    // 等待超时
    while (!TIM_GetFlagStatus(TIM7, TIM_FLAG_Update));
    
    // 清除标志位
    TIM_ClearFlag(TIM7, TIM_FLAG_Update);
    
    // 禁用定时器
    TIM_Cmd(TIM7, DISABLE);
    
    // 关闭时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM7, DISABLE);
}

485.h

#ifndef __485_H__
#define __485_H__

#include "stm32f4xx.h"

// 485控制引脚定义
#define RS485_EN_PORT    GPIOA
#define RS485_EN_PIN     GPIO_Pin_12

// 485模式切换宏
#define RS485_SEND_MOD()    GPIO_SetBits(RS485_EN_PORT, RS485_EN_PIN)
#define RS485_REC_MOD()     GPIO_ResetBits(RS485_EN_PORT, RS485_EN_PIN)

// 函数声明
void USART3_485_Init(u32 Bount, u8 mode);
void RS485_Send(u8 data);

#endif

485.c

#include "485.h"
#include "modbus.h"
#include "time.h"

// 硬件连接:
// USART3_TX --- PB10  --- 发送引脚
// USART3_RX --- PB11 --- 接收引脚
// 485_EN --- PA12 --- 控制引脚

// USART3初始化函数
void USART3_485_Init(u32 Bount, u8 mode) {
    NVIC_InitTypeDef NVIC_InitStruct;
    GPIO_InitTypeDef GPIO_InitStruct;
    USART_InitTypeDef USART_InitStruct;

    // 使能时钟
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA | RCC_AHB1Periph_GPIOB, ENABLE);
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3, ENABLE);

    // 配置PB10为发送引脚
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF;
    GPIO_InitStruct.GPIO_OType = GPIO_OType_PP;
    GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_UP;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &GPIO_InitStruct);

    // 配置PB11为接收引脚
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_11;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF;
    GPIO_InitStruct.GPIO_OType = GPIO_OType_PP;
    GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_UP;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &GPIO_InitStruct);

    // 配置PA12为控制引脚
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_12;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_OUT;
    GPIO_InitStruct.GPIO_OType = GPIO_OType_PP;
    GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_NOPULL;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStruct);

    // 复用功能映射
    GPIO_PinAFConfig(GPIOB, GPIO_PinSource10, GPIO_AF_USART3);
    GPIO_PinAFConfig(GPIOB, GPIO_PinSource11, GPIO_AF_USART3);

    // 配置USART3
    USART_InitStruct.USART_BaudRate = Bount;
    USART_InitStruct.USART_WordLength = USART_WordLength_8b;
    USART_InitStruct.USART_StopBits = USART_StopBits_1;
    USART_InitStruct.USART_Parity = USART_Parity_No;
    USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    USART_InitStruct.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
    USART_Init(USART3, &USART_InitStruct);

    // 配置中断
    NVIC_InitStruct.NVIC_IRQChannel = USART3_IRQn;
    NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0;
    NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStruct);

    // 使能接收中断
    USART_ITConfig(USART3, USART_IT_RXNE, ENABLE);
    USART_Cmd(USART3, ENABLE);

    // 初始化为接收模式
    RS485_REC_MOD();
}

// USART3中断处理函数
void USART3_IRQHandler(void) {
    u8 data = 0;
    if (USART_GetITStatus(USART3, USART_IT_RXNE)) {
        USART_ClearITPendingBit(USART3, USART_IT_RXNE);
        data = USART_ReceiveData(USART3);

        // 重启定时器
        TIM_Cmd(TIM2, DISABLE);
        TIM_SetCounter(TIM2, 0);

        // 存储数据
        modbus.recBuff[modbus.reccount++] = data;
        modbus.recTime = 0;

        // 启动定时器
        TIM_Cmd(TIM2, ENABLE);
    }
}

// 发送单个字节
void RS485_Send(u8 data) {
    while (USART_GetFlagStatus(USART3, USART_FLAG_TC) == RESET);
    USART_SendData(USART3, data);
    while (USART_GetFlagStatus(USART3, USART_FLAG_TC) == RESET);
}

modbus.h

#ifndef __MODBUS_H__
#define __MODBUS_H__

#include "stm32f4xx.h"

// Modbus错误码定义
#define MODBUS_ERR_ILLEGAL_FUNCTION    0x01
#define MODBUS_ERR_ILLEGAL_ADDRESS     0x02
#define MODBUS_ERR_ILLEGAL_DATA        0x03
#define MODBUS_ERR_EXECUTION_ERROR     0x04

// Modbus功能码定义
#define MODBUS_FUNC_READ_HOLDING_REG   0x03

// 寄存器数量定义
#define REGISTER_COUNT  128

// Modbus数据结构
typedef struct{
    u8 myAddr;          // 本机设备地址
    u8 recBuff[100];    // 接收缓冲区
    u8 reccount;        // 接收计数
    u8 finishFlag;      // 接收完成标志
    u8 sendBuff[100];   // 发送缓冲区
    u16 recTime;        // 接收超时计时器
}__MODBUS;

// 外部变量声明
extern __MODBUS modbus;
extern u16 reg[REGISTER_COUNT];

// 函数声明
void Modbus_Init(void);
u16 CRC_16(u8 *PuchMsg, u32 UsDataLen);
void Modbus_Fun3(void);
void Modbus_Fun(void);
void Modbus_SendError(u8 errorCode);

#endif

modbus.c

#include "modbus.h"
#include "string.h"
#include "stdio.h"
#include "485.h"

__MODBUS modbus;
u16 reg[REGISTER_COUNT] = {0};

// CRC高位字节值表
const u8 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
};

// CRC低位字节值表
const u8 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
};

// CRC16校验函数
u16 CRC_16(u8 *PuchMsg, u32 UsDataLen) {
    u8 CRCH = 0xFF; // 高CRC字节初始化
    u8 CRCL = 0xFF; // 低CRC字节初始化
    unsigned long uIndex; // CRC循环中的索引

    while (UsDataLen--) { // 循环处理消息中的每个字节
        uIndex = CRCH ^ *PuchMsg++; // 计算CRC
        CRCH = CRCL ^ auchCRCHi[uIndex];
        CRCL = auchCRCLo[uIndex];
    }

    return (CRCH << 8 | CRCL);
}

// 发送错误响应
void Modbus_SendError(u8 errorCode) {
    u8 i = 0;
    u16 crc;

    // 构造错误响应报文
    modbus.sendBuff[i++] = modbus.myAddr;    // 设备地址
    modbus.sendBuff[i++] = 0x80 | MODBUS_FUNC_READ_HOLDING_REG;    // 错误功能码
    modbus.sendBuff[i++] = errorCode;    // 错误码

    // 计算CRC校验
    crc = CRC_16(modbus.sendBuff, i);
    modbus.sendBuff[i++] = crc >> 8;
    modbus.sendBuff[i++] = crc & 0xFF;

    // 发送响应
    RS485_SEND_MOD();  // 切换到发送模式
    for (u8 j = 0; j < i; j++) {
        RS485_Send(modbus.sendBuff[j]);
    }
    RS485_REC_MOD();  // 切换回接收模式
}

// 功能码03处理函数
void Modbus_Fun3(void) {
    u8 i = 0;
    u16 crc;
    u16 start_add, len;

    if (modbus.finishFlag == 0) {
        return;
    }

    // 解析起始地址和寄存器数量
    start_add = modbus.recBuff[2] << 8 | modbus.recBuff[3];
    len = modbus.recBuff[4] << 8 | modbus.recBuff[5];

    // 参数验证
    if (len < 1 || len > 125) {
        Modbus_SendError(MODBUS_ERR_ILLEGAL_DATA);
        return;
    }

    if (start_add + len > REGISTER_COUNT) {
        Modbus_SendError(MODBUS_ERR_ILLEGAL_ADDRESS);
        return;
    }

    // 构造正常响应报文
    modbus.sendBuff[i++] = modbus.myAddr;    // 设备地址
    modbus.sendBuff[i++] = MODBUS_FUNC_READ_HOLDING_REG;    // 功能码
    modbus.sendBuff[i++] = len * 2;    // 数据字节数

    // 填充寄存器数据
    for (u8 j = 0; j < len; j++) {
        modbus.sendBuff[i++] = reg[start_add + j] >> 8;   // 高8位
        modbus.sendBuff[i++] = reg[start_add + j] & 0xFF;   // 低8位
    }

    // 计算CRC校验
    crc = CRC_16(modbus.sendBuff, i);
    modbus.sendBuff[i++] = crc >> 8;
    modbus.sendBuff[i++] = crc & 0xFF;

    // 发送响应
    RS485_SEND_MOD();  // 切换到发送模式
    for (u8 j = 0; j < i; j++) {
        RS485_Send(modbus.sendBuff[j]);
    }
    RS485_REC_MOD();  // 切换回接收模式
}

// Modbus事务处理函数
void Modbus_Fun(void) {
    u16 crc = 0;
    u16 crc_val = 0;

    if (modbus.finishFlag == 0) {
        return;
    }

    // 计算CRC校验
    crc = CRC_16(modbus.recBuff, modbus.reccount - 2);
    crc_val = modbus.recBuff[modbus.reccount - 2] << 8 | modbus.recBuff[modbus.reccount - 1];

    // CRC校验
    if (crc_val == crc) {
        // 地址校验
        if (modbus.recBuff[0] == modbus.myAddr) {
            // 功能码处理
            if (modbus.recBuff[1] == MODBUS_FUNC_READ_HOLDING_REG) {
                Modbus_Fun3();
            } else {
                Modbus_SendError(MODBUS_ERR_ILLEGAL_FUNCTION);
            }
        }
    }

    // 清空缓冲区
    memset(modbus.recBuff, 0, sizeof(modbus.recBuff));
    memset(modbus.sendBuff, 0, sizeof(modbus.sendBuff));
    modbus.finishFlag = 0;
    modbus.reccount = 0;
}

// Modbus初始化
void Modbus_Init(void) {
    modbus.myAddr = 0x01;  // 设置本机地址
    USART3_485_Init(115200, 0);  // 初始化485通信

    // 初始化寄存器
    for (u8 i = 0; i < REGISTER_COUNT; i++) {
        reg[i] = i * 10;  // 初始化值
    }
}

main.c

#include "stm32f4xx.h"
#include "modbus.h"
#include "time.h"
#include "485.h"

int main() {
    // 初始化系统时钟
    SystemInit();

    // 初始化定时器2(1ms中断,用于Modbus报文超时检测)
    TIM2_Init(10, 8400);  // 1ms中断

    // 初始化Modbus
    Modbus_Init();

    printf("M4 Modbus Slave Ready\r\n");

    // 主循环
    while (1) {
        // 处理Modbus事务
        Modbus_Fun();
    }
}

M3代码

time.h

#ifndef __TIME_H__
#define __TIME_H__

#include "stm32f10x.h"

// 函数声明
void TIM2_Init(u16 arr, u16 psc);
void TIM2_IRQHandler(void);

#endif

time.c

#include "time.h"
#include "modbus.h"

// TIM2初始化函数(用于Modbus报文超时检测)
void TIM2_Init(u16 arr, u16 psc) {
    NVIC_InitTypeDef NVIC_InitStruct;
    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;

    // 使能时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);

    // 配置定时器
    TIM_TimeBaseInitStruct.TIM_Period = arr;   // 重装载值
    TIM_TimeBaseInitStruct.TIM_Prescaler = psc - 1;  // 预分频值
    TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up;  // 向上计数
    TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStruct);

    // 使能更新中断
    TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);

    // 配置NVIC
    NVIC_InitStruct.NVIC_IRQChannel = TIM2_IRQn;  // 定时器2中断
    NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0;  // 抢占优先级
    NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;  // 响应优先级
    NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;   // 使能
    NVIC_Init(&NVIC_InitStruct);

    // 初始禁用定时器
    TIM_Cmd(TIM2, DISABLE);
}

// TIM2中断处理函数
void TIM2_IRQHandler(void) {
    if (TIM_GetITStatus(TIM2, TIM_IT_Update)) {
        TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
        modbus.recTime++;
        
        // 8ms超时判断(约3.5个字符时间,用于报文结束判断)
        if (modbus.recTime >= 8) {
            modbus.finishFlag = 1;  // 接收完成
            TIM_Cmd(TIM2, DISABLE);  // 禁用定时器
        }
    }
}

485.h

#ifndef __485_H__
#define __485_H__

#include "stm32f10x.h"

// 485控制引脚定义
#define RS485_EN_PORT    GPIOA
#define RS485_EN_PIN     GPIO_Pin_12

// 485模式切换宏
#define RS485_SEND_MOD()    GPIO_SetBits(RS485_EN_PORT, RS485_EN_PIN)
#define RS485_REC_MOD()     GPIO_ResetBits(RS485_EN_PORT, RS485_EN_PIN)

// 函数声明
void USART3_485_Init(u32 Bount);
void RS485_Send_Byte(u8 data);
void uart3_String(u8 *data, u32 len);

#endif

485.c

#include "485.h"
#include "modbus.h"
#include "time.h"

// 硬件连接:
// USART3_TX --- PB10  --- 发送引脚
// USART3_RX --- PB11 --- 接收引脚
// 485_EN --- PA12 --- 控制引脚

// USART3初始化函数
void USART3_485_Init(u32 Bount) {
    NVIC_InitTypeDef NVIC_InitStruct;
    GPIO_InitTypeDef GPIO_InitStruct;
    USART_InitTypeDef USART_InitStruct;

    // 使能时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE);
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3, ENABLE);

    // 配置PB10为发送引脚
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &GPIO_InitStruct);

    // 配置PB11为接收引脚
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_11;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_Init(GPIOB, &GPIO_InitStruct);

    // 配置PA12为控制引脚
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_12;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStruct);

    // 配置USART3
    USART_InitStruct.USART_BaudRate = Bount;
    USART_InitStruct.USART_WordLength = USART_WordLength_8b;
    USART_InitStruct.USART_StopBits = USART_StopBits_1;
    USART_InitStruct.USART_Parity = USART_Parity_No;
    USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    USART_InitStruct.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
    USART_Init(USART3, &USART_InitStruct);

    // 配置中断
    NVIC_InitStruct.NVIC_IRQChannel = USART3_IRQn;
    NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0;
    NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStruct);

    // 使能接收中断
    USART_ITConfig(USART3, USART_IT_RXNE, ENABLE);
    USART_Cmd(USART3, ENABLE);

    // 初始化为接收模式
    RS485_REC_MOD();
}

// USART3中断处理函数
void USART3_IRQHandler(void) {
    u8 data = 0;
    if (USART3->SR & 1 << 5) {   // 检查是否是接收中断
        USART3->SR &= ~(1 << 5);   // 清除标志位
        data = USART3->DR;

        // 重启定时器
        TIM_Cmd(TIM2, DISABLE);
        TIM2->CNT = 0;

        // 存储数据
        modbus.recBuff[modbus.reccount++] = data;
        modbus.recTime = 0;

        // 启动定时器
        TIM_Cmd(TIM2, ENABLE);
    }
}

// 发送字符串
void uart3_String(u8 *data, u32 len) {
    RS485_SEND_MOD();   // 发送模式
    while (len--) {
        USART3->DR = *data;
        while (!(USART3->SR & 1 << 6)) {
            // 等待发送完成
        }
        data++;
    }
    RS485_REC_MOD();   // 接收模式
}

// 发送单个字节
void RS485_Send_Byte(u8 data) {
    while (!(USART3->SR & 1 << 6));
    USART3->DR = data;
    while (!(USART3->SR & 1 << 6));
}

modbus.h

#ifndef __MODBUS_H__
#define __MODBUS_H__

#include "stm32f10x.h"

// Modbus错误码定义
#define MODBUS_ERR_ILLEGAL_FUNCTION    0x01
#define MODBUS_ERR_ILLEGAL_ADDRESS     0x02
#define MODBUS_ERR_ILLEGAL_DATA        0x03
#define MODBUS_ERR_EXECUTION_ERROR     0x04

// Modbus功能码定义
#define MODBUS_FUNC_READ_HOLDING_REG   0x03

// 寄存器数量定义
#define REGISTER_COUNT  128

// Modbus数据结构
typedef struct{
    u8 myAddr;          // 本机设备地址
    u8 recBuff[100];    // 接收缓冲区
    u8 reccount;        // 接收计数
    u8 finishFlag;      // 接收完成标志
    u8 sendBuff[100];   // 发送缓冲区
    u16 recTime;        // 接收超时计时器
}__MODBUS;

// 外部变量声明
extern __MODBUS modbus;
extern u16 reg[REGISTER_COUNT];

// 函数声明
void Modbus_Init(void);
u16 CRC_16(u8 *PuchMsg, u32 UsDataLen);
void Modbus_Fun3(void);
void Modbus_Fun(void);
void Modbus_SendError(u8 errorCode);

#endif

modbus.c

#include "modbus.h"
#include "string.h"
#include "stdio.h"
#include "485.h"

__MODBUS modbus;
u16 reg[REGISTER_COUNT] = {0};

// CRC高位字节值表
const u8 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
};

// CRC低位字节值表
const u8 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
};

// CRC16校验函数
u16 CRC_16(u8 *PuchMsg, u32 UsDataLen) {
    u8 CRCH = 0xFF; // 高CRC字节初始化
    u8 CRCL = 0xFF; // 低CRC字节初始化
    unsigned long uIndex; // CRC循环中的索引

    while (UsDataLen--) { // 循环处理消息中的每个字节
        uIndex = CRCH ^ *PuchMsg++; // 计算CRC
        CRCH = CRCL ^ auchCRCHi[uIndex];
        CRCL = auchCRCLo[uIndex];
    }

    return (CRCH << 8 | CRCL);
}

// 发送错误响应
void Modbus_SendError(u8 errorCode) {
    u8 i = 0;
    u16 crc;

    // 构造错误响应报文
    modbus.sendBuff[i++] = modbus.myAddr;    // 设备地址
    modbus.sendBuff[i++] = 0x80 | MODBUS_FUNC_READ_HOLDING_REG;    // 错误功能码
    modbus.sendBuff[i++] = errorCode;    // 错误码

    // 计算CRC校验
    crc = CRC_16(modbus.sendBuff, i);
    modbus.sendBuff[i++] = crc >> 8;
    modbus.sendBuff[i++] = crc & 0xFF;

    // 发送响应
    RS485_SEND_MOD();  // 切换到发送模式
    for (u8 j = 0; j < i; j++) {
        RS485_Send_Byte(modbus.sendBuff[j]);
    }
    RS485_REC_MOD();  // 切换回接收模式
}

// 功能码03处理函数
void Modbus_Fun3(void) {
    u8 i = 0;
    u16 crc;
    u16 start_add, len;

    if (modbus.finishFlag == 0) {
        return;
    }

    // 解析起始地址和寄存器数量
    start_add = modbus.recBuff[2] << 8 | modbus.recBuff[3];
    len = modbus.recBuff[4] << 8 | modbus.recBuff[5];

    // 参数验证
    if (len < 1 || len > 125) {
        Modbus_SendError(MODBUS_ERR_ILLEGAL_DATA);
        return;
    }

    if (start_add + len > REGISTER_COUNT) {
        Modbus_SendError(MODBUS_ERR_ILLEGAL_ADDRESS);
        return;
    }

    // 构造正常响应报文
    modbus.sendBuff[i++] = modbus.myAddr;    // 设备地址
    modbus.sendBuff[i++] = MODBUS_FUNC_READ_HOLDING_REG;    // 功能码
    modbus.sendBuff[i++] = len * 2;    // 数据字节数

    // 填充寄存器数据
    for (u8 j = 0; j < len; j++) {
        modbus.sendBuff[i++] = reg[start_add + j] >> 8;   // 高8位
        modbus.sendBuff[i++] = reg[start_add + j] & 0xFF;   // 低8位
    }

    // 计算CRC校验
    crc = CRC_16(modbus.sendBuff, i);
    modbus.sendBuff[i++] = crc >> 8;
    modbus.sendBuff[i++] = crc & 0xFF;

    // 发送响应
    RS485_SEND_MOD();  // 切换到发送模式
    for (u8 j = 0; j < i; j++) {
        RS485_Send_Byte(modbus.sendBuff[j]);
    }
    RS485_REC_MOD();  // 切换回接收模式
}

// Modbus事务处理函数
void Modbus_Fun(void) {
    u16 crc = 0;
    u16 crc_val = 0;

    if (modbus.finishFlag == 0) {
        return;
    }

    // 计算CRC校验
    crc = CRC_16(modbus.recBuff, modbus.reccount - 2);
    crc_val = modbus.recBuff[modbus.reccount - 2] << 8 | modbus.recBuff[modbus.reccount - 1];

    // CRC校验
    if (crc_val == crc) {
        // 地址校验
        if (modbus.recBuff[0] == modbus.myAddr) {
            // 功能码处理
            if (modbus.recBuff[1] == MODBUS_FUNC_READ_HOLDING_REG) {
                Modbus_Fun3();
            } else {
                Modbus_SendError(MODBUS_ERR_ILLEGAL_FUNCTION);
            }
        }
    }

    // 清空缓冲区
    memset(modbus.recBuff, 0, sizeof(modbus.recBuff));
    memset(modbus.sendBuff, 0, sizeof(modbus.sendBuff));
    modbus.finishFlag = 0;
    modbus.reccount = 0;
}

// Modbus初始化
void Modbus_Init(void) {
    modbus.myAddr = 0x01;  // 设置本机地址
    USART3_485_Init(115200);  // 初始化485通信

    // 初始化寄存器
    for (u8 i = 0; i < REGISTER_COUNT; i++) {
        reg[i] = i * 10;  // 初始化值
    }
}

main.c

#include "stm32f10x.h"
#include "modbus.h"
#include "time.h"
#include "485.h"

int main() {
    // 初始化定时器2(1ms中断,用于Modbus报文超时检测)
    TIM2_Init(10, 7200);  // 1ms中断

    // 初始化Modbus
    Modbus_Init();

    printf("M3 Modbus Slave Ready\r\n");

    // 主循环
    while (1) {
        // 处理Modbus事务
        Modbus_Fun();
    }
}
posted @ 2026-02-09 14:29  YouEmbedded  阅读(3)  评论(0)    收藏  举报