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

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电气特性
拓扑结构

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。
差分信号与共模干扰

差分传输:区别于“一根信号线+一根地线”的传统方式,差分传输在两根线上均传输信号,这两个信号振幅相等、相位相反(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芯片。

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芯片命名规则

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

程序设计核心: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区别

注: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事务处理;无需分配地址,负责向从节点发送请求、接收并处理从节点的应答。
-
从节点(从机):仅能被动响应主节点的请求,无主节点指令时不主动发送数据,也不与其他从节点通信;每个从节点必须有唯一的地址,用于被主节点识别。
-
通信模式:主节点对从节点的请求分为两种模式:
- 单播模式:主节点向特定地址的从节点发送请求,从节点处理完成后,向主节点返回应答;一个事务包含“主节点请求→从节点应答”两个报文。

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

Modbus地址规则

Modbus寻址空间有256个不同地址,地址范围0~255,划分如下:
- 地址0:保留为广播地址,所有从节点必须识别该地址,但无需回应;
- 地址1~247:从节点单独地址,每个从节点必须分配唯一的地址(不可重复),用于主节点单播寻址;
- 地址248~255:保留地址,暂不使用。
地址的作用:
- 主节点通过地址,精准定位要控制的从节点;
- 从节点通过地址判断请求是否针对自己:若地址非0且与自身地址一致,需返回应答;若为广播地址(0),执行写命令但不返回应答;避免多个从节点同时应答,导致总线冲突。
Modbus数据帧(PDU)
Modbus定义了简单的协议数据单元(PDU,Protocol Data Unit),核心是“功能码+数据”;在实际传输中,主节点会添加附加域,构造完整的串行链路PDU,结构如下:

各域详细说明:
- 地址域: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),所有字符均为十六进制。
帧格式(固定):

起始符(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通信的主流模式。
帧格式(固定):

起始标识:3.5个字符传输时间的空闲间隔(总线无数据);
地址域(1字节):从节点地址(0~247);
功能码(1字节):要执行的功能(如0x03=读保持寄存器);
数据域(0~252字节):具体请求/响应数据;
校验域(2字节):CRC16校验码(先附加低字节,再附加高字节);
结束标识:3.5个字符传输时间的空闲间隔。
关键特性:

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

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=高字节)。

TCP/IP模式(基于以太网)
基于以太网传输,无需RS-485/RS-232物理层,采用TCP协议进行数据传输;帧格式与串行链路不同,包含MBAP头(Modbus Application Protocol Header),用于标识Modbus报文,适用于以太网连接的工业设备,此处不详细展开。
Modbus功能码
功能码为1字节,用于指定从节点要执行的操作,核心功能码如下,此处以03为例展开,其余功能码可参考Modbus协议手册:

报文


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();
}
}

浙公网安备 33010602011771号